valent-pipeline 0.19.53 → 0.19.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valent-pipeline",
3
- "version": "0.19.53",
3
+ "version": "0.19.55",
4
4
  "description": "v3 multi-agent AI pipeline for software development lifecycle",
5
5
  "type": "module",
6
6
  "bin": {
@@ -885,16 +885,27 @@ const parCfg = (() => {
885
885
  const parallelEnabled = gitFlowEnabled && parCfg.enabled === true
886
886
  const maxParallelStories = Math.max(1, Number.isInteger(parCfg.maxStories) ? parCfg.maxStories
887
887
  : Number.isInteger(parCfg.max_stories) ? parCfg.max_stories : 2)
888
+ // Worktree ISOLATION (review 2026-06-24, M-152): each story builds in its OWN worktree under
889
+ // worktreeDir/<id> instead of the shared main checkout. Parallel mode REQUIRES it (concurrent
890
+ // stories cannot share one checkout); serial mode can opt in via `git.worktree_isolation: true` to
891
+ // stop cross-story working-tree residue from bleeding into a story's review-pin `git add -A` sweep
892
+ // (the contamination that swept 3.10 files into story/3.1). Isolation is DECOUPLED from CONCURRENCY:
893
+ // the ship-lock, work queue, and post-merge re-verify stay keyed on `parallelEnabled` (concurrency),
894
+ // while the worktree create/cd/workdir paths + --require-evidence-on-assert (evidence must be
895
+ // self-contained in the worktree) key on THIS. Parallel implies isolation. Default off => no
896
+ // behavior change until a project opts in at a clean boundary.
897
+ const worktreeIsolation = gitFlowEnabled
898
+ && (gitCfg.worktree_isolation === true || gitCfg.worktreeIsolation === true || parallelEnabled)
888
899
  const worktreeDir = (typeof (parCfg.worktreeDir ?? parCfg.worktree_dir) === 'string'
889
900
  && (parCfg.worktreeDir ?? parCfg.worktree_dir).trim()) || '.valent-worktrees'
890
- const wtFlag = parallelEnabled
901
+ const wtFlag = worktreeIsolation
891
902
  ? ` --worktree${worktreeDir !== '.valent-worktrees' ? ` --worktree-dir ${worktreeDir}` : ''}`
892
903
  : ''
893
904
  const storyWorkDir = (sid) => `${worktreeDir}/${sid}`
894
- // Where a story's commands run: its worktree in parallel mode, the project root otherwise.
905
+ // Where a story's commands run: its own worktree when isolation is on, the project root otherwise.
895
906
  // `inWorktree` is false for sprint-level runners (backlog/integration) and the revalidation path.
896
907
  const runDirPhrase = (sid, inWorktree = true) =>
897
- parallelEnabled && inWorktree ? `the story worktree at \`${storyWorkDir(sid)}\` (cd into it FIRST)` : 'the project root'
908
+ worktreeIsolation && inWorktree ? `the story worktree at \`${storyWorkDir(sid)}\` (cd into it FIRST)` : 'the project root'
898
909
 
899
910
  // Ship/leave mutex + ORDERED ship queue (review 2026-06-10 #9): merges mutate the shared target
900
911
  // ref + main working tree — strictly one at a time, and in QUEUE ORDER, not completion order, so
@@ -1762,10 +1773,10 @@ async function runStory(story) {
1762
1773
  }
1763
1774
  }
1764
1775
 
1765
- // In parallel mode every per-story agent works inside the story's worktree (the revalidation
1776
+ // When isolation is on, every per-story agent works inside the story's worktree (the revalidation
1766
1777
  // and re-ship paths are the exceptions — they work the MAIN tree: the fix already merged there /
1767
1778
  // sync-story checks the retained branch out there).
1768
- const storyWorkCopy = parallelEnabled && !revalidate && !reshipOnly ? storyWorkDir(storyId) : null
1779
+ const storyWorkCopy = worktreeIsolation && !revalidate && !reshipOnly ? storyWorkDir(storyId) : null
1769
1780
  const spawn = (role, promptFile, taskSubject, opts = {}) =>
1770
1781
  agent(buildPrompt({ role, promptFile, storyId, taskSubject, workDir: storyWorkCopy, ...opts }), {
1771
1782
  label: opts.label || `${role.toLowerCase()}:${storyId}`,
@@ -2076,6 +2087,25 @@ async function runStory(story) {
2076
2087
  if (has('uxa')) {
2077
2088
  await spawn('UXA', 'uxa.md', 'Translate the brief into uxa-spec.md.', { phase: 'Spec' })
2078
2089
  }
2090
+ // Fix D (rework-reduction, 2026-06-24): QA-A proves its acceptance suite is honestly RED BEFORE
2091
+ // handoff, instead of the RED gate discovering a premature-pass a cycle later — after the dev
2092
+ // fan-out is already staged (the 2.29 class: 10 cycles, an escalation, a build break, zero
2093
+ // implementation burned). The lever mirrors the dev pre-handoff self-gate (Fix #3): thread the
2094
+ // SAME acceptance command the RED gate will run into QA-A's authoring turn so it self-checks in
2095
+ // context. Deliberately the RAW command (not the `evidence run` wrapper) — QA-A only needs to
2096
+ // OBSERVE pass/fail, and minting a labeled `acceptance` assert here would shadow the baseline the
2097
+ // RED/green/evidence gates freeze against. The RED gate stays the mechanical backstop. atddEnabled
2098
+ // + args only => journal-replay safe.
2099
+ const qaaRedSelfCheck = atddEnabled
2100
+ ? ` Then prove the suite is honestly RED before handoff (Fix D — shift-left the RED gate): run ` +
2101
+ `\`${subCmd(atddCommandTpl, storyId, effectiveWorkspaces)}\` against the current (unimplemented) tree. ` +
2102
+ `Every \`required: true\` case MUST run and FAIL for a behavioral reason (404 / empty / missing element) — ` +
2103
+ `exactly what the RED gate will demand. A required case that PASSES now is a premature-pass spec bug: ` +
2104
+ `re-target it to assert a NEW endpoint/element this story has not built yet, or — if it only guards an ` +
2105
+ `already-established infrastructure invariant — mark it \`required: false\` ` +
2106
+ `(steps/qa-a/write-acceptance-tests.md Step C), never weakening or deleting the guard. Fix every ` +
2107
+ `premature-pass before handoff so the RED gate passes first-try.`
2108
+ : ''
2079
2109
  await spawn('QA-A', 'qa-a.md',
2080
2110
  'Produce qa-test-spec.md before any code is written.' +
2081
2111
  (atddEnabled
@@ -2083,7 +2113,7 @@ async function runStory(story) {
2083
2113
  `under ${atddDir}/${storyId}/ — one+ per AC, TC id in the test title, black-box against the running system, ` +
2084
2114
  `no mocks — plus the manifest kind:"acceptance"/testFile/files[] fields. The prose spec is companion notes. ` +
2085
2115
  `Then self-attack your tests (steps/qa-a/self-attack.md) and fix what you find before handoff.`
2086
- : ''),
2116
+ : '') + qaaRedSelfCheck,
2087
2117
  { phase: 'Spec' })
2088
2118
 
2089
2119
  phase('SpecCheck')
@@ -2721,10 +2751,10 @@ async function runStory(story) {
2721
2751
  const commands = [
2722
2752
  `node .valent-pipeline/bin/cli.js evidence pin --story ${sid} --gate evidence`,
2723
2753
  `node .valent-pipeline/bin/cli.js trace check --story ${sid} --label ${evidenceLabel}`,
2724
- // --require-evidence in parallel mode (review #37): evidence lives in the WORKTREE — a gate
2725
- // accidentally run at the main root would otherwise find nothing and no-op PASS. Serial mode
2726
- // keeps the graceful no-op (the legacy/pre-evidence story path).
2727
- `node .valent-pipeline/bin/cli.js evidence assert --story ${sid} --label ${evidenceLabel}${pinsFlag}${parallelEnabled ? ' --require-evidence' : ''}`,
2754
+ // --require-evidence when isolation is on (review #37): evidence lives in the WORKTREE — a gate
2755
+ // accidentally run at the main root would otherwise find nothing and no-op PASS. Serial WITHOUT
2756
+ // isolation keeps the graceful no-op (the legacy/pre-evidence story path).
2757
+ `node .valent-pipeline/bin/cli.js evidence assert --story ${sid} --label ${evidenceLabel}${pinsFlag}${worktreeIsolation ? ' --require-evidence' : ''}`,
2728
2758
  ]
2729
2759
  let rejections = 0
2730
2760
  let reverify = null // last rework re-review (pin coherence v2) — open Highs ride to JUDGE
@@ -12,3 +12,15 @@ Probe each acceptance case as the laziest implementer who wants green with minim
12
12
  - **Implementation sniffing (ATDD):** the dev can read your tests. Avoid magic fixture values an implementation could special-case; randomize or parameterize identifying data where cheap. (CRITIC's acceptance pass audits the implementation side of this.)
13
13
 
14
14
  Rate residual gameability **low | medium | high** per AC. HIGH means you have not finished — strengthen the test and re-rate. Record the final ratings + what you hardened in `qa-test-spec.md#self-attack` so CRITIC and JUDGE see what was already attacked.
15
+
16
+ ## Prove the suite is RED before handoff (ATDD)
17
+
18
+ This is the mechanical close-out, run AFTER the gameability pass above. The orchestrator hands you the exact acceptance command to run — execute it against the current, **unimplemented** tree and read the result:
19
+
20
+ - **Every `required: true` case MUST run and FAIL for a behavioral reason** — a 404, an empty/absent response, a missing element. That is precisely what the RED gate checks a cycle later; proving it here means the gate passes first-try instead of bouncing your spec back after the dev fan-out is already staged.
21
+ - **A `required: true` case that PASSES now is a premature-pass spec bug.** It does not assert anything this story builds — it asserts behavior that already exists. Two fixes, both before handoff:
22
+ - **Re-target it** so it asserts a NEW endpoint/element/field this story has not built yet (e.g. route the assertion through a new path that 404s until implemented). This is the right fix when the case is meant to prove new behavior.
23
+ - **Or mark it `required: false`** (write-acceptance-tests.md Step C) when it only guards an already-established infrastructure invariant — still executed and asserted at the GREEN gate, but exempt from the red-required set. **Never weaken or delete the guard** — just make red honest.
24
+ - A required case that **errors for a non-behavioral reason** (import failure, harness misconfig, syntax) is not honest red either — fix the test so it fails *because the feature is absent*, not because it cannot run.
25
+
26
+ Do not hand off while any `required: true` case passes against the unimplemented tree. The RED gate will dead-end the story if you do; catching it here costs one self-run instead of a full round-trip.
@@ -82,7 +82,19 @@ Present defaults and let the user adjust:
82
82
  | `retrospective_every_n_stories` | `5` | How often the Retrospective Agent runs (range: 3-10) |
83
83
  | `stall_threshold_minutes` | `15` | Minutes before a stalled agent gets a check-in (range: 5-30) |
84
84
 
85
- ### Step 4b: Parallel story execution (`git.parallelism`)
85
+ ### Step 4b: Worktree isolation (`git.worktree_isolation`)
86
+
87
+ Off by default. When on, each story builds in its OWN git worktree (under
88
+ `git.parallelism.worktree_dir/<id>`) even in serial mode — so cross-story working-tree residue
89
+ can't bleed into a story's review-pin `git add -A` sweep and ship under the wrong story. It is the
90
+ precondition parallelism builds on (parallel mode forces it on); enabling it serially is the safe
91
+ first step toward parallel execution.
92
+
93
+ | Setting | Default | Description |
94
+ |---------|---------|-------------|
95
+ | `git.worktree_isolation` | `false` | Build each story in its own worktree even when serial (parallel forces on) |
96
+
97
+ ### Step 4c: Parallel story execution (`git.parallelism`)
86
98
 
87
99
  Off by default (`enabled: false` = strictly sequential). Turning it on runs up to `max_stories`
88
100
  stories concurrently, each in its own git worktree under `worktree_dir` (default
@@ -170,7 +170,7 @@ Feed the planned batch straight into `sprint.workflow.js`:
170
170
  ```js
171
171
  Workflow({
172
172
  scriptPath: '.valent-pipeline/orchestrators/claude-code/sprint.workflow.js',
173
- args: { stories: <plan output .stories>, backlogPath: '{backlog_path}', maxRejectionCycles: {quality.max_rejection_cycles or 3}, maxJudgeRejects: {quality.max_judge_rejects or 2}, targetedReverify: {quality.targeted_reverify or false}, models: <config.models or omit>, reasoning: <config.reasoning or omit>, preCriticGate: <config.pre_critic_gate or omit>, packGate: <config.pack_gate or omit>, evidenceGate: <config.evidence_gate or omit>, atdd: <config.atdd or omit>, monorepo: <config.monorepo or omit>, git: { enabled: <config.git.enabled ?? true>, targetBranch: <config.git.target_branch or ''>, storyBranchPrefix: <config.git.story_branch_prefix or 'story/'>, parallelism: <config.git.parallelism or omit> }, ref: <config.ref or omit>, knowledge: { mode: <config.knowledge.mode or 'sqlite'>, curatedPath: <config.knowledge.curated_files_path or 'knowledge/curated'> }, visual: { onMissingMcp: <config.visual.on_missing_mcp or 'block'>, browserMcp: <tech_stack.browser_automation_mcp or 'playwright-mcp'> } }
173
+ args: { stories: <plan output .stories>, backlogPath: '{backlog_path}', maxRejectionCycles: {quality.max_rejection_cycles or 3}, maxJudgeRejects: {quality.max_judge_rejects or 2}, targetedReverify: {quality.targeted_reverify or false}, models: <config.models or omit>, reasoning: <config.reasoning or omit>, preCriticGate: <config.pre_critic_gate or omit>, packGate: <config.pack_gate or omit>, evidenceGate: <config.evidence_gate or omit>, atdd: <config.atdd or omit>, monorepo: <config.monorepo or omit>, git: { enabled: <config.git.enabled ?? true>, targetBranch: <config.git.target_branch or ''>, storyBranchPrefix: <config.git.story_branch_prefix or 'story/'>, worktreeIsolation: <config.git.worktree_isolation ?? false>, parallelism: <config.git.parallelism or omit> }, ref: <config.ref or omit>, knowledge: { mode: <config.knowledge.mode or 'sqlite'>, curatedPath: <config.knowledge.curated_files_path or 'knowledge/curated'> }, visual: { onMissingMcp: <config.visual.on_missing_mcp or 'block'>, browserMcp: <tech_stack.browser_automation_mcp or 'playwright-mcp'> } }
174
174
  })
175
175
  ```
176
176
 
@@ -178,7 +178,7 @@ Feed the planned batch straight into `sprint.workflow.js`:
178
178
  ```js
179
179
  Workflow({
180
180
  scriptPath: '.valent-pipeline/orchestrators/claude-code/sprint.workflow.js',
181
- args: { stories: <plan output .stories>, backlogPath: '{backlog_path}', maxRejectionCycles: {quality.max_rejection_cycles or 3}, maxJudgeRejects: {quality.max_judge_rejects or 2}, targetedReverify: {quality.targeted_reverify or false}, models: <config.models or omit>, reasoning: <config.reasoning or omit>, preCriticGate: <config.pre_critic_gate or omit>, packGate: <config.pack_gate or omit>, evidenceGate: <config.evidence_gate or omit>, atdd: <config.atdd or omit>, monorepo: <config.monorepo or omit>, git: { enabled: <config.git.enabled ?? true>, targetBranch: <config.git.target_branch or ''>, storyBranchPrefix: <config.git.story_branch_prefix or 'story/'>, parallelism: <config.git.parallelism or omit> }, ref: <config.ref or omit>, knowledge: { mode: <config.knowledge.mode or 'sqlite'>, curatedPath: <config.knowledge.curated_files_path or 'knowledge/curated'> }, visual: { onMissingMcp: <config.visual.on_missing_mcp or 'block'>, browserMcp: <tech_stack.browser_automation_mcp or 'playwright-mcp'> } }
181
+ args: { stories: <plan output .stories>, backlogPath: '{backlog_path}', maxRejectionCycles: {quality.max_rejection_cycles or 3}, maxJudgeRejects: {quality.max_judge_rejects or 2}, targetedReverify: {quality.targeted_reverify or false}, models: <config.models or omit>, reasoning: <config.reasoning or omit>, preCriticGate: <config.pre_critic_gate or omit>, packGate: <config.pack_gate or omit>, evidenceGate: <config.evidence_gate or omit>, atdd: <config.atdd or omit>, monorepo: <config.monorepo or omit>, git: { enabled: <config.git.enabled ?? true>, targetBranch: <config.git.target_branch or ''>, storyBranchPrefix: <config.git.story_branch_prefix or 'story/'>, worktreeIsolation: <config.git.worktree_isolation ?? false>, parallelism: <config.git.parallelism or omit> }, ref: <config.ref or omit>, knowledge: { mode: <config.knowledge.mode or 'sqlite'>, curatedPath: <config.knowledge.curated_files_path or 'knowledge/curated'> }, visual: { onMissingMcp: <config.visual.on_missing_mcp or 'block'>, browserMcp: <tech_stack.browser_automation_mcp or 'playwright-mcp'> } }
182
182
  })
183
183
  ```
184
184
 
@@ -29,7 +29,7 @@ If no argument is provided, resolve the next work item from the backlog (see Ste
29
29
 
30
30
  ### Step 1: Load Pipeline Config
31
31
 
32
- Read `.valent-pipeline/pipeline-config.yaml` and resolve the variables you need below (project type, paths, quality caps, `tech_stack`, `knowledge`). Capture `project.type` — it feeds `--project-type` in Step 1b, and the resolved candidate object then carries `projectType`, `profiles`, and the execution-mode flags for Step 4 (when the user gave an explicit story argument instead, run the Step 1b resolver command anyway and select that story's candidate object from its buckets). Also capture the entire `models` section (the `{ opus:[...], sonnet:[...], haiku:[...] }` tier→roles map) — pass it as the `models` arg so per-agent model tiers stay config-driven (editable via `/valent-configure`). If the config has no `models` section, omit the arg and the workflow uses its baked-in default assignment. Likewise capture the `reasoning` section (the `{ ultrathink:[...], 'think-harder':[...], 'think-hard':[...], think:[...] }` level→roles map) and pass it as the `reasoning` arg; it is blank by default (injects nothing). Omit the arg if the section is absent or all levels are empty. Also capture the `pre_critic_gate` section (deterministic lint/type/static checks) and pass it as the `preCriticGate` arg; omit it if absent or its `commands` list is empty (the gate is then a no-op). Likewise capture the `pack_gate` section (run-level pack-time freshness checks — they run ONCE at sprint intake, before the story spends tokens) and pass it as the `packGate` arg; omit it if absent or its `commands` list is empty. Capture the `atdd` section and pass it as the `atdd` arg — it drives the RED gate (pre-implementation acceptance-suite run that must fail) and the GREEN gate (post-CRITIC re-run that must pass with the acceptance sources unchanged); omit it if absent or its `command` is blank (both gates are then a loud no-op). Capture the `git` section and pass it as the `git` arg `{ enabled: <git.enabled ?? true>, targetBranch: <git.target_branch or ''>, storyBranchPrefix: <git.story_branch_prefix or 'story/'>, parallelism: <git.parallelism or omit> }` — it drives the story git flow (branch off the target, full commit trail, merge --no-ff on SHIP); `parallelism` ({ enabled, max_stories, worktree_dir, setup_commands }) additionally enables concurrent stories in worktrees (off by default). Omit the whole arg only to accept the defaults (flow ON, parallelism OFF, graceful no-op outside a git repo). Capture the `visual` section together with `tech_stack.browser_automation_mcp` and pass them as the `visual` arg `{ onMissingMcp: <visual.on_missing_mcp or 'block'>, browserMcp: <tech_stack.browser_automation_mcp or 'playwright-mcp'> }` — this drives the Visual (PMCP) phase for `ui`-profile stories.
32
+ Read `.valent-pipeline/pipeline-config.yaml` and resolve the variables you need below (project type, paths, quality caps, `tech_stack`, `knowledge`). Capture `project.type` — it feeds `--project-type` in Step 1b, and the resolved candidate object then carries `projectType`, `profiles`, and the execution-mode flags for Step 4 (when the user gave an explicit story argument instead, run the Step 1b resolver command anyway and select that story's candidate object from its buckets). Also capture the entire `models` section (the `{ opus:[...], sonnet:[...], haiku:[...] }` tier→roles map) — pass it as the `models` arg so per-agent model tiers stay config-driven (editable via `/valent-configure`). If the config has no `models` section, omit the arg and the workflow uses its baked-in default assignment. Likewise capture the `reasoning` section (the `{ ultrathink:[...], 'think-harder':[...], 'think-hard':[...], think:[...] }` level→roles map) and pass it as the `reasoning` arg; it is blank by default (injects nothing). Omit the arg if the section is absent or all levels are empty. Also capture the `pre_critic_gate` section (deterministic lint/type/static checks) and pass it as the `preCriticGate` arg; omit it if absent or its `commands` list is empty (the gate is then a no-op). Likewise capture the `pack_gate` section (run-level pack-time freshness checks — they run ONCE at sprint intake, before the story spends tokens) and pass it as the `packGate` arg; omit it if absent or its `commands` list is empty. Capture the `atdd` section and pass it as the `atdd` arg — it drives the RED gate (pre-implementation acceptance-suite run that must fail) and the GREEN gate (post-CRITIC re-run that must pass with the acceptance sources unchanged); omit it if absent or its `command` is blank (both gates are then a loud no-op). Capture the `git` section and pass it as the `git` arg `{ enabled: <git.enabled ?? true>, targetBranch: <git.target_branch or ''>, storyBranchPrefix: <git.story_branch_prefix or 'story/'>, worktreeIsolation: <git.worktree_isolation ?? false>, parallelism: <git.parallelism or omit> }` — it drives the story git flow (branch off the target, full commit trail, merge --no-ff on SHIP); `worktreeIsolation` builds each story in its own worktree even when serial (parallel mode forces it on); `parallelism` ({ enabled, max_stories, worktree_dir, setup_commands }) additionally enables concurrent stories in worktrees (off by default). Omit the whole arg only to accept the defaults (flow ON, parallelism OFF, graceful no-op outside a git repo). Capture the `visual` section together with `tech_stack.browser_automation_mcp` and pass them as the `visual` arg `{ onMissingMcp: <visual.on_missing_mcp or 'block'>, browserMcp: <tech_stack.browser_automation_mcp or 'playwright-mcp'> }` — this drives the Visual (PMCP) phase for `ui`-profile stories.
33
33
 
34
34
  ### Step 1b: Resolve Next Work Item (when no argument provided)
35
35
 
@@ -546,6 +546,10 @@ git:
546
546
  # Blank = whatever branch the sprint starts on (recorded per story by git start-story).
547
547
  target_branch: "${config.git.target_branch}"
548
548
  story_branch_prefix: "${config.git.story_branch_prefix}"
549
+ # Worktree isolation: build each story in its own worktree (under parallelism.worktree_dir/<id>)
550
+ # even when serial, so cross-story working-tree residue can't ship under the wrong story. Parallel
551
+ # mode forces this ON. OFF by default for serial runs — opt in at a clean boundary.
552
+ worktree_isolation: ${config.git.worktree_isolation ?? false}
549
553
  # Parallel story execution (worktrees): up to max_stories run concurrently, each in its own git
550
554
  # worktree under worktree_dir; the main checkout stays on the target, merges are serialized, and
551
555
  # each ship gets a post-merge acceptance verify (ATDD armed). OFF by default — sequential sprint.
@@ -528,6 +528,12 @@ export const defaults = {
528
528
  // (recorded per story by `git start-story`, so ship-story returns to the same place).
529
529
  target_branch: '',
530
530
  story_branch_prefix: 'story/',
531
+ // Worktree isolation: build each story in its OWN worktree (under parallelism.worktree_dir/<id>)
532
+ // even when running serially, so cross-story working-tree residue can't bleed into a story's
533
+ // review-pin `git add -A` sweep and ship under the wrong story. Parallel mode forces this ON
534
+ // (concurrent stories cannot share a checkout). OFF by default for serial runs — opt in at a
535
+ // clean boundary; it's the precondition the parallelism rollout builds on.
536
+ worktree_isolation: false,
531
537
  // Parallel story execution: up to max_stories run concurrently, each in its own git worktree
532
538
  // (main checkout stays on the target; merges are serialized; each ship gets a post-merge
533
539
  // acceptance verify when ATDD is armed). OFF by default — the sprint is strictly sequential.