diff --git a/crates/plan-issue-cli/docs/README.md b/crates/plan-issue-cli/docs/README.md index fdc4655..96b6af0 100644 --- a/crates/plan-issue-cli/docs/README.md +++ b/crates/plan-issue-cli/docs/README.md @@ -3,7 +3,7 @@ ## Purpose Crate-local documentation for `nils-plan-issue-cli`. -`Task Decomposition` is the crate's documented runtime-truth execution table for plan/sprint orchestration. Specs define `Owner` as a dispatch alias, document `group + auto` single-lane normalization to `per-sprint`, and treat task-spec/subagent prompts as derived artifacts (not a second issue-body dispatch table). +`Task Decomposition` is the crate's documented runtime-truth execution table for plan/sprint orchestration. Specs define `Owner` as a dispatch alias, document `group + auto` single-lane normalization to `per-sprint`, and treat task-spec/subagent prompts as derived artifacts (not a second issue-body dispatch table). `start-sprint` validates drift against plan-derived lanes and does not rewrite issue rows in runtime-truth mode. ## Specs - [plan-issue CLI contract v1](specs/plan-issue-cli-contract-v1.md) diff --git a/crates/plan-issue-cli/docs/specs/plan-issue-cli-contract-v1.md b/crates/plan-issue-cli/docs/specs/plan-issue-cli-contract-v1.md index 84d253d..6314b5a 100644 --- a/crates/plan-issue-cli/docs/specs/plan-issue-cli-contract-v1.md +++ b/crates/plan-issue-cli/docs/specs/plan-issue-cli-contract-v1.md @@ -46,7 +46,7 @@ v1 subcommands: | `ready-plan` | Request final plan review | yes (unless `--body-file` only mode) | `--issue` or `--body-file` | review-ready signal (+ label/comment controls) | | `close-plan` | Final gate + issue close | yes (except dry-run with `--body-file`) | `--approved-comment-url` and issue context | issue close + required worktree cleanup | | `cleanup-worktrees` | Remove all issue-assigned worktrees | yes (to read issue body) | `--issue` | deleted worktree set (or dry-run listing) | -| `start-sprint` | Begin sprint execution on existing plan issue | yes (unless dry-run) | `--plan`, `--issue`, `--sprint`, `--pr-grouping` | sprint TSV, rendered subagent prompts, issue row sync, kickoff comment | +| `start-sprint` | Begin sprint execution on existing plan issue | yes (unless dry-run) | `--plan`, `--issue`, `--sprint`, `--pr-grouping` | sprint TSV, rendered subagent prompts, issue-row runtime-truth validation, kickoff comment | | `ready-sprint` | Request sprint acceptance review | yes | `--plan`, `--issue`, `--sprint` | sprint review-ready comment | | `accept-sprint` | Enforce merged-PR gate and mark sprint done | yes | `--plan`, `--issue`, `--sprint`, `--approved-comment-url` | task status sync to `done` + acceptance comment | | `multi-sprint-guide` | Print repeatable command flow | no (with `--dry-run`) | `--plan` | execution guide text | @@ -58,7 +58,7 @@ v1 subcommands: - `--pr-grouping `: - required for `build-task-spec`, `build-plan-task-spec`, `start-plan`, `start-sprint`. - `per-spring` must be accepted as compatibility alias for `per-sprint`. - - with `--pr-grouping group --strategy auto`, when a sprint resolves to exactly one shared PR group, issue-sync/render paths normalize `Execution Mode` to `per-sprint` (single-lane semantics). + - with `--pr-grouping group --strategy auto`, when a sprint resolves to exactly one shared PR group, runtime-truth/render paths normalize `Execution Mode` to `per-sprint` (single-lane semantics). - `--pr-group `: - repeatable. - valid only when `--pr-grouping group`. diff --git a/crates/plan-issue-cli/docs/specs/plan-issue-gate-matrix-v1.md b/crates/plan-issue-cli/docs/specs/plan-issue-gate-matrix-v1.md index 3efed06..c4d79a1 100644 --- a/crates/plan-issue-cli/docs/specs/plan-issue-gate-matrix-v1.md +++ b/crates/plan-issue-cli/docs/specs/plan-issue-gate-matrix-v1.md @@ -29,27 +29,28 @@ execution. | `G6` | Worktree cleanup gate | `cleanup-worktrees`, successful `close-plan` | targeted linked worktrees removed, prune succeeds, no targeted residues | `1` | | `G7` | Dry-run non-mutation gate | all commands with `--dry-run` | command prints intended actions and performs no GitHub mutation | `1` | | `G8` | Close-plan dry-run body-file gate | `close-plan --dry-run` | `--body-file` is provided for local gate evaluation | `2` | +| `G9` | Runtime-truth drift gate | `start-sprint` | sprint issue rows match plan-derived runtime lane metadata before artifact render | `1` | ## Command-to-Gate Matrix -| Command | G0 | G1 | G2 | G3 | G4 | G5 | G6 | G7 | G8 | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `build-task-spec` | required | - | - | - | - | - | - | optional | - | -| `build-plan-task-spec` | required | - | - | - | - | - | - | optional | - | -| `start-plan` | required | - | - | - | - | - | - | optional | - | -| `status-plan` | required | required (issue/body mode) | - | - | - | - | - | optional | - | -| `ready-plan` | required | required (issue/body mode) | - | - | - | - | - | optional | - | -| `close-plan` | required | required | required | - | - | required | required (on success path) | optional | required when dry-run | -| `cleanup-worktrees` | required | required | - | - | - | - | required | optional | - | -| `start-sprint` | required | required | required for `N > 1` | required for `N > 1` | - | - | - | optional | - | -| `ready-sprint` | required | required | - | - | - | - | - | optional | - | -| `accept-sprint` | required | required | required | - | required | - | - | optional | - | -| `multi-sprint-guide` | required | - | - | - | - | - | - | optional | - | +| Command | G0 | G1 | G2 | G3 | G4 | G5 | G6 | G7 | G8 | G9 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `build-task-spec` | required | - | - | - | - | - | - | optional | - | - | +| `build-plan-task-spec` | required | - | - | - | - | - | - | optional | - | - | +| `start-plan` | required | - | - | - | - | - | - | optional | - | - | +| `status-plan` | required | required (issue/body mode) | - | - | - | - | - | optional | - | - | +| `ready-plan` | required | required (issue/body mode) | - | - | - | - | - | optional | - | - | +| `close-plan` | required | required | required | - | - | required | required (on success path) | optional | required when dry-run | - | +| `cleanup-worktrees` | required | required | - | - | - | - | required | optional | - | - | +| `start-sprint` | required | required | required for `N > 1` | required for `N > 1` | - | - | - | optional | - | required | +| `ready-sprint` | required | required | - | - | - | - | - | optional | - | - | +| `accept-sprint` | required | required | required | - | required | - | - | optional | - | - | +| `multi-sprint-guide` | required | - | - | - | - | - | - | optional | - | - | ## Gate Evaluation Order (Normative) 1. `G0` usage/argument validation. 2. Command-specific structural checks (`G1`) before remote mutations. -3. Progression/merge gates (`G2`, `G3`, `G4`, `G5`) in command-specific order. +3. Progression/merge/drift gates (`G2`, `G3`, `G4`, `G5`, `G9`) in command-specific order. 4. Cleanup gate (`G6`) only after command gate success where cleanup is required. 5. Dry-run behavior (`G7`, `G8`) wraps command execution and must preserve non-mutation semantics. @@ -62,5 +63,5 @@ execution. ## Failure Contract - `exit 2`: usage errors (`G0`, `G8`) and invalid required inputs. -- `exit 1`: gate failures (`G1` through `G7`) and runtime dependency failures. +- `exit 1`: gate failures (`G1` through `G7`, `G9`) and runtime dependency failures. - `exit 0`: all applicable gates pass. diff --git a/crates/plan-issue-cli/docs/specs/plan-issue-state-machine-v1.md b/crates/plan-issue-cli/docs/specs/plan-issue-state-machine-v1.md index 40fcb8d..aa35aef 100644 --- a/crates/plan-issue-cli/docs/specs/plan-issue-state-machine-v1.md +++ b/crates/plan-issue-cli/docs/specs/plan-issue-state-machine-v1.md @@ -61,7 +61,8 @@ States per sprint `N`: Transitions: - `start-sprint N`: `SPRINT_NOT_STARTED -> SPRINT_IN_PROGRESS` - Renders sprint task-spec and subagent prompts. - - Syncs Task Decomposition execution metadata from task-spec. + - Validates Task Decomposition runtime-truth rows against plan-derived sprint lanes. + - Derives sprint task-spec and prompt artifacts from runtime-truth issue rows (no row rewrite). - For `N > 1`, requires previous sprint merge gate pass (see gate invariants). - `ready-sprint N`: `SPRINT_IN_PROGRESS -> SPRINT_REVIEW_READY` - Posts or prints sprint-ready review artifact. diff --git a/crates/plan-issue-cli/src/commands/mod.rs b/crates/plan-issue-cli/src/commands/mod.rs index 1dee7f9..4ab74bc 100644 --- a/crates/plan-issue-cli/src/commands/mod.rs +++ b/crates/plan-issue-cli/src/commands/mod.rs @@ -154,7 +154,7 @@ pub enum Command { /// Enforce cleanup of all issue-assigned task worktrees. CleanupWorktrees(CleanupWorktreesArgs), - /// Start sprint only after previous sprint merge+done gate passes. + /// Start sprint from Task Decomposition runtime truth after previous sprint merge+done gate passes. StartSprint(StartSprintArgs), /// Post sprint-ready comment for main-agent review before merge. diff --git a/crates/plan-issue-cli/tests/fixtures/shell_parity/help.txt b/crates/plan-issue-cli/tests/fixtures/shell_parity/help.txt index 51902d1..c712e8b 100644 --- a/crates/plan-issue-cli/tests/fixtures/shell_parity/help.txt +++ b/crates/plan-issue-cli/tests/fixtures/shell_parity/help.txt @@ -9,7 +9,7 @@ Subcommands: ready-plan Wrapper of issue-delivery-loop ready-for-review for final plan review close-plan Close the single plan issue after final approval + merged PR gates, then enforce worktree cleanup cleanup-worktrees Enforce cleanup of all issue-assigned task worktrees - start-sprint Start sprint only after previous sprint merge+done gate passes + start-sprint Start sprint from Task Decomposition runtime truth after previous sprint merge+done gate passes ready-sprint Post sprint-ready comment for main-agent review before merge accept-sprint Enforce merged-PR gate, sync sprint status=done, then post accepted comment multi-sprint-guide Print the full repeated command flow for a plan (1 plan = 1 issue) diff --git a/crates/plan-issue-cli/tests/live_issue_ops.rs b/crates/plan-issue-cli/tests/live_issue_ops.rs index 50f1cb2..57765d6 100644 --- a/crates/plan-issue-cli/tests/live_issue_ops.rs +++ b/crates/plan-issue-cli/tests/live_issue_ops.rs @@ -110,12 +110,12 @@ fn issue_body_with_preface(task_rows: &str) -> String { ## Overview - This plan delivers a shell-free Rust implementation for the current plan-issue orchestration workflow. -- The issue body keeps pre-sprint context so sprint commands only sync task table rows. +- The issue body keeps pre-sprint context and uses Task Decomposition as runtime truth. ## Scope - Maintain one plan issue for the full multi-sprint workflow. -- Keep pre-sprint sections stable when sprint commands update Task Decomposition. +- Keep pre-sprint sections stable while sprint commands read/validate runtime-truth rows. ## Task Decomposition @@ -131,9 +131,9 @@ fn issue_body_sprint4_planned() -> String { r#"| S3T1 | Implement task-spec generation core using `plan-tooling` | subagent-s3-t1 | issue/s3-t1-implement-task-spec-generation-core-using-plan-t | issue-s3-t1 | per-sprint | #221 | done | sprint=S3; plan-task:Task 3.1 | | S3T2 | Implement issue-body and sprint-comment rendering engine | subagent-s3-t1 | issue/s3-t1-implement-task-spec-generation-core-using-plan-t | issue-s3-t1 | per-sprint | #221 | done | sprint=S3; plan-task:Task 3.2 | | S3T3 | Implement independent local dry-run workflow | subagent-s3-t1 | issue/s3-t1-implement-task-spec-generation-core-using-plan-t | issue-s3-t1 | per-sprint | #221 | done | sprint=S3; plan-task:Task 3.3 | -| S4T1 | Implement GitHub adapter abstraction and `gh` backend | TBD | TBD | TBD | TBD | TBD | planned | sprint=S4; plan-task:Task 4.1 | -| S4T2 | Implement live plan-level commands | TBD | TBD | TBD | TBD | TBD | planned | sprint=S4; plan-task:Task 4.2 | -| S4T3 | Implement live sprint-level commands and guide output | TBD | TBD | TBD | TBD | TBD | planned | sprint=S4; plan-task:Task 4.3 | +| S4T1 | Implement GitHub adapter abstraction and `gh` backend | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | TBD | planned | sprint=S4; plan-task:Task 4.1; deps=Task 3.3; validate=cargo test -p nils-plan-issue-cli github_adapter; pr-grouping=per-sprint; pr-group=s4; shared-pr-anchor=S4T1 | +| S4T2 | Implement live plan-level commands | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | TBD | planned | sprint=S4; plan-task:Task 4.1; deps=Task 3.3; validate=cargo test -p nils-plan-issue-cli github_adapter; pr-grouping=per-sprint; pr-group=s4; shared-pr-anchor=S4T1 | +| S4T3 | Implement live sprint-level commands and guide output | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | TBD | planned | sprint=S4; plan-task:Task 4.1; deps=Task 3.3; validate=cargo test -p nils-plan-issue-cli github_adapter; pr-grouping=per-sprint; pr-group=s4; shared-pr-anchor=S4T1 | "#, ) } @@ -143,18 +143,18 @@ fn issue_body_sprint4_in_progress() -> String { r#"| S3T1 | Implement task-spec generation core using `plan-tooling` | subagent-s3-t1 | issue/s3-t1-implement-task-spec-generation-core-using-plan-t | issue-s3-t1 | per-sprint | #221 | done | sprint=S3; plan-task:Task 3.1 | | S3T2 | Implement issue-body and sprint-comment rendering engine | subagent-s3-t1 | issue/s3-t1-implement-task-spec-generation-core-using-plan-t | issue-s3-t1 | per-sprint | #221 | done | sprint=S3; plan-task:Task 3.2 | | S3T3 | Implement independent local dry-run workflow | subagent-s3-t1 | issue/s3-t1-implement-task-spec-generation-core-using-plan-t | issue-s3-t1 | per-sprint | #221 | done | sprint=S3; plan-task:Task 3.3 | -| S4T1 | Implement GitHub adapter abstraction and `gh` backend | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | #222 | in-progress | sprint=S4; plan-task:Task 4.1 | -| S4T2 | Implement live plan-level commands | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | #223 | in-progress | sprint=S4; plan-task:Task 4.2 | -| S4T3 | Implement live sprint-level commands and guide output | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | #224 | in-progress | sprint=S4; plan-task:Task 4.3 | +| S4T1 | Implement GitHub adapter abstraction and `gh` backend | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | #222 | in-progress | sprint=S4; plan-task:Task 4.1; deps=Task 3.3; validate=cargo test -p nils-plan-issue-cli github_adapter; pr-grouping=per-sprint; pr-group=s4; shared-pr-anchor=S4T1 | +| S4T2 | Implement live plan-level commands | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | #223 | in-progress | sprint=S4; plan-task:Task 4.1; deps=Task 3.3; validate=cargo test -p nils-plan-issue-cli github_adapter; pr-grouping=per-sprint; pr-group=s4; shared-pr-anchor=S4T1 | +| S4T3 | Implement live sprint-level commands and guide output | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | #224 | in-progress | sprint=S4; plan-task:Task 4.1; deps=Task 3.3; validate=cargo test -p nils-plan-issue-cli github_adapter; pr-grouping=per-sprint; pr-group=s4; shared-pr-anchor=S4T1 | "#, ) } fn issue_body_plan_done() -> String { issue_body_with_preface( - r#"| S4T1 | Implement GitHub adapter abstraction and `gh` backend | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | #222 | done | sprint=S4; plan-task:Task 4.1 | -| S4T2 | Implement live plan-level commands | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | #223 | done | sprint=S4; plan-task:Task 4.2 | -| S4T3 | Implement live sprint-level commands and guide output | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | #224 | done | sprint=S4; plan-task:Task 4.3 | + r#"| S4T1 | Implement GitHub adapter abstraction and `gh` backend | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | #222 | done | sprint=S4; plan-task:Task 4.1; deps=Task 3.3; validate=cargo test -p nils-plan-issue-cli github_adapter; pr-grouping=per-sprint; pr-group=s4; shared-pr-anchor=S4T1 | +| S4T2 | Implement live plan-level commands | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | #223 | done | sprint=S4; plan-task:Task 4.1; deps=Task 3.3; validate=cargo test -p nils-plan-issue-cli github_adapter; pr-grouping=per-sprint; pr-group=s4; shared-pr-anchor=S4T1 | +| S4T3 | Implement live sprint-level commands and guide output | subagent-s4-t1 | issue/s4-t1-implement-github-adapter-abstraction-and-gh-back | issue-s4-t1 | per-sprint | #224 | done | sprint=S4; plan-task:Task 4.1; deps=Task 3.3; validate=cargo test -p nils-plan-issue-cli github_adapter; pr-grouping=per-sprint; pr-group=s4; shared-pr-anchor=S4T1 | "#, ) } @@ -325,8 +325,6 @@ fn live_sprint_commands_start_ready_accept_and_guide_are_deterministic() { fs::create_dir_all(&agent_home).expect("agent home"); let agent_home_s = agent_home.to_string_lossy().to_string(); - let start_capture = tmp.path().join("start-sprint-body.md"); - let start_capture_s = start_capture.to_string_lossy().to_string(); let start_body_json = json!({"body": issue_body_sprint4_planned()}).to_string(); let start_out = common::run_plan_issue_with_options( @@ -351,7 +349,6 @@ fn live_sprint_commands_start_ready_accept_and_guide_are_deterministic() { &[ ("PLAN_ISSUE_GH_LOG", &log_s), ("PLAN_ISSUE_GH_BODY_JSON", &start_body_json), - ("PLAN_ISSUE_GH_CAPTURE_BODY_FILE", &start_capture_s), ("AGENT_HOME", &agent_home_s), ], ), @@ -361,20 +358,22 @@ fn live_sprint_commands_start_ready_accept_and_guide_are_deterministic() { let start_payload = parse_json(&start_out.stdout); assert_eq!(start_payload["command"], "start-sprint"); assert_eq!(start_payload["payload"]["result"]["synced_issue_rows"], 3); - - let start_body = fs::read_to_string(&start_capture).expect("captured start body"); - assert!( - start_body.contains("## Overview"), - "preface should be preserved\n{start_body}" + assert_eq!( + start_payload["payload"]["result"]["live_mutations_performed"], + false ); + let start_spec_path = start_payload["payload"]["result"]["task_spec_path"] + .as_str() + .expect("start task-spec path"); + let start_spec = fs::read_to_string(start_spec_path).expect("read start task-spec"); + assert!(start_spec.contains("subagent-s4-t1"), "{start_spec}"); assert!( - start_body.contains("shell-free Rust implementation"), - "preface should be preserved\n{start_body}" + start_spec.contains("issue/s4-t1-implement-github-adapter-abstraction-and-gh-back"), + "{start_spec}" ); - assert!(start_body.contains("subagent-s4-t1"), "{start_body}"); assert!( - start_body.contains("pr-grouping=per-sprint"), - "{start_body}" + start_spec.contains("pr-grouping=per-sprint"), + "{start_spec}" ); let ready_out = common::run_plan_issue_with_options( diff --git a/crates/plan-issue-cli/tests/sprint4_delivery.rs b/crates/plan-issue-cli/tests/sprint4_delivery.rs index 44a883a..23396be 100644 --- a/crates/plan-issue-cli/tests/sprint4_delivery.rs +++ b/crates/plan-issue-cli/tests/sprint4_delivery.rs @@ -137,13 +137,6 @@ fn parse_task_spec_rows(tsv: &str) -> HashMap { rows } -fn note_value(notes: &str, key: &str) -> Option { - notes - .split(';') - .map(str::trim) - .find_map(|part| part.strip_prefix(&format!("{key}=")).map(str::to_string)) -} - fn gh_stub_script() -> &'static str { r#"#!/usr/bin/env bash set -euo pipefail @@ -196,26 +189,8 @@ fn gh_cmd_options(stub_dir: &Path, envs: &[(&str, &str)]) -> CmdOptions { .with_envs(envs) } -fn issue_body_with_preface(task_rows: &str) -> String { - format!( - r#"# Plan: Sprint 1 overwrite characterization - -## Overview - -- This issue body starts with canonical shared-lane metadata for the sprint rows. -- The test characterizes current start-sprint behavior that rewrites those rows from task-spec. - -## Task Decomposition - -| Task | Summary | Owner | Branch | Worktree | Execution Mode | PR | Status | Notes | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | -{task_rows} -"# - ) -} - #[test] -fn live_start_sprint_overwrites_issue_rows_from_recomputed_task_spec_in_auto_single_lane() { +fn live_start_sprint_uses_issue_table_runtime_truth_without_rewrite() { let tmp = TempDir::new().expect("temp dir"); let stub = StubBinDir::new(); stub.write_exe("gh", gh_stub_script()); @@ -229,11 +204,11 @@ fn live_start_sprint_overwrites_issue_rows_from_recomputed_task_spec_in_auto_sin let capture_body = tmp.path().join("captured-start-sprint-body.md"); let capture_body_s = capture_body.to_string_lossy().to_string(); - let plan_file = tmp.path().join("sprint1-auto-single-lane.md"); + let plan_file = tmp.path().join("sprint1-runtime-truth.md"); let plan_file_s = plan_file.to_string_lossy().to_string(); fs::write( &plan_file, - r#"# Plan: Sprint 1 overwrite characterization + r#"# Plan: Sprint 1 runtime truth ## Sprint 1: Shared lane - **PR grouping intent**: `group`. @@ -254,12 +229,34 @@ fn live_start_sprint_overwrites_issue_rows_from_recomputed_task_spec_in_auto_sin ) .expect("write plan"); - let canonical_issue_body = issue_body_with_preface( - r#"| S1T1 | First lane task | subagent-s1-lane | issue/s1-shared-lane | issue-s1-lane | per-sprint | TBD | planned | sprint=S1; plan-task:Task 1.1; pr-group=s1-auto-g1; shared-pr-anchor=S1T2; source=issue-table-canonical | -| S1T2 | Follow-up lane task | subagent-s1-lane | issue/s1-shared-lane | issue-s1-lane | per-sprint | TBD | planned | sprint=S1; plan-task:Task 1.2; pr-group=s1-auto-g1; shared-pr-anchor=S1T2; source=issue-table-canonical | -"#, + let plan_task_spec = tmp.path().join("plan-task-spec.tsv"); + let plan_issue_body = tmp.path().join("plan-issue-body.md"); + let plan_task_spec_s = plan_task_spec.to_string_lossy().to_string(); + let plan_issue_body_s = plan_issue_body.to_string_lossy().to_string(); + + let start_plan_out = common::run_plan_issue_local_with_env( + &[ + "--format", + "json", + "--dry-run", + "start-plan", + "--plan", + &plan_file_s, + "--pr-grouping", + "group", + "--strategy", + "auto", + "--task-spec-out", + &plan_task_spec_s, + "--issue-body-out", + &plan_issue_body_s, + ], + &[("AGENT_HOME", &agent_home_s)], ); - let body_json = json!({ "body": canonical_issue_body }).to_string(); + assert_eq!(start_plan_out.code, 0, "stderr: {}", start_plan_out.stderr); + + let issue_body = fs::read_to_string(&plan_issue_body).expect("read issue body"); + let body_json = json!({ "body": issue_body.clone() }).to_string(); let task_spec_out = tmp.path().join("sprint1-task-spec.tsv"); let task_spec_out_s = task_spec_out.to_string_lossy().to_string(); @@ -310,51 +307,50 @@ fn live_start_sprint_overwrites_issue_rows_from_recomputed_task_spec_in_auto_sin assert_eq!(payload["payload"]["result"]["synced_issue_rows"], 2); assert_eq!( payload["payload"]["result"]["live_mutations_performed"], - true + false + ); + + assert!( + !capture_body.exists(), + "start-sprint should not rewrite issue body in runtime-truth mode" ); - let captured_body = fs::read_to_string(&capture_body).expect("read captured issue body"); - let issue_rows = parse_task_decomposition_rows(&captured_body); + let issue_rows = parse_task_decomposition_rows(&issue_body); let issue_s1t1 = issue_rows.get("S1T1").expect("S1T1 issue row"); let issue_s1t2 = issue_rows.get("S1T2").expect("S1T2 issue row"); assert_eq!(issue_s1t1.execution_mode, "per-sprint"); assert_eq!(issue_s1t2.execution_mode, "per-sprint"); - assert!( - !captured_body.contains("source=issue-table-canonical"), - "{captured_body}" - ); - assert!( - !captured_body.contains("issue/s1-shared-lane"), - "{captured_body}" - ); - assert!(!captured_body.contains("issue-s1-lane"), "{captured_body}"); - assert!( - !captured_body.contains("| subagent-s1-lane |"), - "{captured_body}" - ); let spec_path = result_path(&payload, "task_spec_path"); let spec_text = fs::read_to_string(&spec_path).expect("read task-spec"); let spec_rows = parse_task_spec_rows(&spec_text); - let anchor_task = note_value(&issue_s1t1.notes, "shared-pr-anchor").expect("shared anchor"); - let spec_anchor = spec_rows - .get(&anchor_task) - .unwrap_or_else(|| panic!("missing spec row for anchor task {anchor_task}")); - - for issue_row in [issue_s1t1, issue_s1t2] { - assert_eq!(issue_row.owner, spec_anchor.owner); - assert_eq!(issue_row.branch, spec_anchor.branch); - assert_eq!(issue_row.worktree, spec_anchor.worktree); - assert_eq!(issue_row.notes, spec_anchor.notes); + + for (task_id, issue_row) in [("S1T1", issue_s1t1), ("S1T2", issue_s1t2)] { + let spec_row = spec_rows + .get(task_id) + .unwrap_or_else(|| panic!("missing spec row {task_id}")); + assert_eq!(issue_row.owner, spec_row.owner); + assert_eq!(issue_row.branch, spec_row.branch); + assert_eq!(issue_row.worktree, spec_row.worktree); + assert_eq!(issue_row.notes, spec_row.notes); } + let prompt_files = payload["payload"]["result"]["subagent_prompt_files"] + .as_array() + .expect("subagent prompt files"); + assert_eq!(prompt_files.len(), 1, "{}", out.stdout); + let prompt_path = prompt_files[0].as_str().expect("prompt path"); + let prompt = fs::read_to_string(prompt_path).expect("read prompt"); + assert!(prompt.contains("Tasks: S1T1, S1T2"), "{prompt}"); + assert!(prompt.contains("Execution Mode: per-sprint"), "{prompt}"); + let log = fs::read_to_string(&log_path).expect("read gh log"); assert!( log.contains("issue view 217 --repo graysurf/nils-cli --json body"), "{log}" ); assert!( - log.contains("issue edit 217 --repo graysurf/nils-cli --body-file"), + !log.contains("issue edit 217 --repo graysurf/nils-cli --body-file"), "{log}" ); assert!(