valent-pipeline 0.19.57 → 0.19.58
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
|
@@ -107,6 +107,7 @@ the config — surface it explicitly:
|
|
|
107
107
|
| `git.parallelism.max_stories` | `2` | Concurrent story cap |
|
|
108
108
|
| `git.parallelism.worktree_dir` | `.valent-worktrees` | Worktree container (ignored via `.git/info/exclude`) |
|
|
109
109
|
| `git.parallelism.setup_commands` | `[]` | Commands run in each fresh worktree (env files, per-story ports, non-node installs) — MUST be idempotent (setup re-runs on resume) |
|
|
110
|
+
| `git.parallelism.teardown_commands` | `[]` | Commands run IN the worktree just before removal (on ship or rollover) — symmetric cleanup (e.g. `docker compose down` on a per-story stack) so a stale stack can't leak into the next story. Best-effort: a failure is logged but never blocks the ship/rollover |
|
|
110
111
|
|
|
111
112
|
### Step 4a: Evidence Gate (machine-evidence layer)
|
|
112
113
|
|
|
@@ -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/'>, 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.
|
|
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, teardown_commands }) additionally enables concurrent stories in worktrees (off by default); `setup_commands`/`teardown_commands` run in each worktree on create/remove (the git CLI reads them from config on disk). 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
|
|
package/src/commands/git-flow.js
CHANGED
|
@@ -36,6 +36,7 @@ function resolveOpts(options) {
|
|
|
36
36
|
worktree: options.worktree === true,
|
|
37
37
|
worktreeDir: options.worktreeDir || par.worktree_dir || DEFAULT_WORKTREE_DIR,
|
|
38
38
|
setupCommands: Array.isArray(par.setup_commands) ? par.setup_commands : [],
|
|
39
|
+
teardownCommands: Array.isArray(par.teardown_commands) ? par.teardown_commands : [],
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -78,7 +79,7 @@ export async function gitCommitPhaseCmd(options) {
|
|
|
78
79
|
|
|
79
80
|
export async function gitShipStoryCmd(options) {
|
|
80
81
|
try {
|
|
81
|
-
const { root, targetBranch, prefix, worktree } = resolveOpts(options);
|
|
82
|
+
const { root, targetBranch, prefix, worktree, teardownCommands } = resolveOpts(options);
|
|
82
83
|
// Ship-time evidence binding (review pass-3 #7): the clamp config rides pipeline-config.yaml
|
|
83
84
|
// (evidence_gate.ship_clamp) — no CLI bypass flag exists by design.
|
|
84
85
|
const { evidenceShipConfig } = await import('./evidence.js');
|
|
@@ -87,6 +88,7 @@ export async function gitShipStoryCmd(options) {
|
|
|
87
88
|
root, story: options.story, verdict: options.verdict || 'ship', targetBranch, prefix, worktree,
|
|
88
89
|
into: options.into || null,
|
|
89
90
|
shipClamp: shipCfg.mode === 'off' ? null : { mode: shipCfg.mode, label: shipCfg.label },
|
|
91
|
+
teardownCommands,
|
|
90
92
|
});
|
|
91
93
|
if (result.shipClampWarning) {
|
|
92
94
|
console.error(`git-flow WARNING: ship-time evidence binding failed (${result.shipClampWarning}) — proceeding (evidence_gate.ship_clamp: warn)`);
|
|
@@ -135,8 +137,8 @@ export async function gitStoryStatusCmd(options) {
|
|
|
135
137
|
|
|
136
138
|
export async function gitLeaveStoryCmd(options) {
|
|
137
139
|
try {
|
|
138
|
-
const { root, targetBranch, prefix, worktree } = resolveOpts(options);
|
|
139
|
-
finish(leaveStory({ root, story: options.story, reason: options.reason || 'rejected', targetBranch, prefix, worktree }));
|
|
140
|
+
const { root, targetBranch, prefix, worktree, teardownCommands } = resolveOpts(options);
|
|
141
|
+
finish(leaveStory({ root, story: options.story, reason: options.reason || 'rejected', targetBranch, prefix, worktree, teardownCommands }));
|
|
140
142
|
} catch (e) {
|
|
141
143
|
fail(e);
|
|
142
144
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -554,12 +554,16 @@ git:
|
|
|
554
554
|
# worktree under worktree_dir; the main checkout stays on the target, merges are serialized, and
|
|
555
555
|
# each ship gets a post-merge acceptance verify (ATDD armed). OFF by default — sequential sprint.
|
|
556
556
|
# setup_commands run in each fresh worktree (node_modules is linked + pipeline.db copied for you;
|
|
557
|
-
# add project-specific needs here, e.g. env files or per-story ports).
|
|
557
|
+
# add project-specific needs here, e.g. env files or per-story ports). teardown_commands run in
|
|
558
|
+
# the worktree just before it is removed (on ship or rollover) — the symmetric cleanup (e.g.
|
|
559
|
+
# "docker compose down" on a per-story stack) so a stale stack can't leak into the next story.
|
|
560
|
+
# Best-effort: a teardown failure is logged but never blocks the ship/rollover.
|
|
558
561
|
parallelism:
|
|
559
562
|
enabled: ${config.git.parallelism?.enabled ?? false}
|
|
560
563
|
max_stories: ${config.git.parallelism?.max_stories ?? 2}
|
|
561
564
|
worktree_dir: "${config.git.parallelism?.worktree_dir ?? '.valent-worktrees'}"
|
|
562
565
|
setup_commands: [${(config.git.parallelism?.setup_commands ?? []).map((c) => JSON.stringify(c)).join(', ')}]
|
|
566
|
+
teardown_commands: [${(config.git.parallelism?.teardown_commands ?? []).map((c) => JSON.stringify(c)).join(', ')}]
|
|
563
567
|
|
|
564
568
|
communication:
|
|
565
569
|
handoff_format: "${config.communication.handoff_format}"
|
package/src/lib/config-schema.js
CHANGED
|
@@ -545,6 +545,12 @@ export const defaults = {
|
|
|
545
545
|
// Commands run in each fresh worktree after node_modules linking + pipeline.db copy —
|
|
546
546
|
// project-specific setup (env files, per-story ports, non-node package installs).
|
|
547
547
|
setup_commands: [],
|
|
548
|
+
// Commands run IN the worktree just before it is removed (on ship or rollover) — the
|
|
549
|
+
// symmetric complement to setup_commands. Use to tear down per-worktree side effects a setup
|
|
550
|
+
// command created (e.g. `docker compose down` on a per-story stack) so a stale stack can't
|
|
551
|
+
// leak into the next story and pin a shared host port. Best-effort: a failure is logged but
|
|
552
|
+
// never blocks the ship/rollover.
|
|
553
|
+
teardown_commands: [],
|
|
548
554
|
},
|
|
549
555
|
},
|
|
550
556
|
communication: {
|
package/src/lib/git-flow.js
CHANGED
|
@@ -276,12 +276,41 @@ function worktreeForBranch(root, branch) {
|
|
|
276
276
|
return null;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Best-effort teardown (config git.parallelism.teardown_commands), run IN the worktree just before
|
|
281
|
+
* it is removed — the symmetric complement to setupWorktree's setup_commands. Use it to tear down
|
|
282
|
+
* per-worktree side effects a setup command created (e.g. `docker compose down` on a per-story
|
|
283
|
+
* stack) so a stale stack can't leak into the next story and pin a shared host port. UNLIKE setup,
|
|
284
|
+
* a teardown failure NEVER throws: the worktree is being discarded regardless, and a failed cleanup
|
|
285
|
+
* must not strand a shipped/rolled-over story — it is reported loudly on stderr and removal still
|
|
286
|
+
* proceeds. Runs in the worktree's cwd so a command can read its compose project name / .env.test.
|
|
287
|
+
*/
|
|
288
|
+
function runWorktreeTeardown(wtPath, teardownCommands = []) {
|
|
289
|
+
if (!Array.isArray(teardownCommands) || !existsSync(wtPath)) return { ran: [], failed: [] };
|
|
290
|
+
const ran = [], failed = [];
|
|
291
|
+
for (const cmd of teardownCommands) {
|
|
292
|
+
if (typeof cmd !== 'string' || !cmd.trim()) continue;
|
|
293
|
+
try {
|
|
294
|
+
execSync(cmd, { cwd: wtPath, stdio: ['ignore', 'pipe', 'pipe'], timeout: 300000 });
|
|
295
|
+
ran.push(cmd);
|
|
296
|
+
} catch (e) {
|
|
297
|
+
const stderr = e.stderr ? e.stderr.toString().slice(-2000) : '';
|
|
298
|
+
const why = e.killed ? ' (timed out after 300s)' : '';
|
|
299
|
+
failed.push(cmd);
|
|
300
|
+
console.error(`git-flow WARNING: worktree teardown command failed${why} (best-effort, removal proceeds): ${cmd}${stderr ? `\n${stderr}` : ''}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return { ran, failed };
|
|
304
|
+
}
|
|
305
|
+
|
|
279
306
|
/**
|
|
280
307
|
* Remove a worktree AFTER its sweep commit. Plain remove refuses while untracked/ignored files
|
|
281
308
|
* remain — post-sweep the only leftovers are ignored setup artifacts (the node_modules link, the
|
|
282
|
-
* pipeline.db copy), so the --force fallback discards nothing of value.
|
|
309
|
+
* pipeline.db copy), so the --force fallback discards nothing of value. Teardown commands run FIRST
|
|
310
|
+
* (in the worktree, while its files still exist), best-effort.
|
|
283
311
|
*/
|
|
284
|
-
function removeWorktreeDir(root, wtPath) {
|
|
312
|
+
function removeWorktreeDir(root, wtPath, teardownCommands = []) {
|
|
313
|
+
runWorktreeTeardown(wtPath, teardownCommands);
|
|
285
314
|
try {
|
|
286
315
|
git(root, ['worktree', 'remove', wtPath]);
|
|
287
316
|
return true;
|
|
@@ -691,7 +720,7 @@ export function commitPhase({ root, story, phase, summary = null, gate = null })
|
|
|
691
720
|
* shipped; the branch retains every commit. Idempotent: an already-merged branch reports
|
|
692
721
|
* { merged:true, already:true }.
|
|
693
722
|
*/
|
|
694
|
-
export function shipStory({ root, story, verdict = 'ship', targetBranch = '', prefix = 'story/', worktree = false, into = null, shipClamp = null }) {
|
|
723
|
+
export function shipStory({ root, story, verdict = 'ship', targetBranch = '', prefix = 'story/', worktree = false, into = null, shipClamp = null, teardownCommands = [] }) {
|
|
695
724
|
if (!isRepoWithCommits(root)) return NOT_REPO;
|
|
696
725
|
const branch = storyBranchName(prefix, story);
|
|
697
726
|
if (!branchExists(root, branch)) throw new Error(`story branch "${branch}" does not exist — was start-story run?`);
|
|
@@ -735,7 +764,7 @@ export function shipStory({ root, story, verdict = 'ship', targetBranch = '', pr
|
|
|
735
764
|
sweep = commitAll(root, buildCommitMessage({ storyId: story, phase: 'ship', trailers: { Verdict: verdict } }));
|
|
736
765
|
}
|
|
737
766
|
|
|
738
|
-
const removeWorktree = () => (wt ? removeWorktreeDir(root, wt) : false);
|
|
767
|
+
const removeWorktree = () => (wt ? removeWorktreeDir(root, wt, teardownCommands) : false);
|
|
739
768
|
|
|
740
769
|
if (isMergedInto(root, branch, target)) {
|
|
741
770
|
if (currentBranch(root) !== target) git(root, ['checkout', target]);
|
|
@@ -898,7 +927,7 @@ export function syncStory({ root, story, targetBranch = '', prefix = 'story/', c
|
|
|
898
927
|
* must survive for the inline fix or next-sprint retry), checkout the target, branch retained.
|
|
899
928
|
* Safe to call from any branch state; idempotent.
|
|
900
929
|
*/
|
|
901
|
-
export function leaveStory({ root, story, reason = 'rejected', targetBranch = '', prefix = 'story/', worktree = false }) {
|
|
930
|
+
export function leaveStory({ root, story, reason = 'rejected', targetBranch = '', prefix = 'story/', worktree = false, teardownCommands = [] }) {
|
|
902
931
|
if (!isRepoWithCommits(root)) return NOT_REPO;
|
|
903
932
|
const branch = storyBranchName(prefix, story);
|
|
904
933
|
|
|
@@ -910,7 +939,7 @@ export function leaveStory({ root, story, reason = 'rejected', targetBranch = ''
|
|
|
910
939
|
let removed = false;
|
|
911
940
|
if (wt) {
|
|
912
941
|
sweep = commitAll(wt, buildCommitMessage({ storyId: story, phase: 'rollover', summary: `roll over (${reason})` }));
|
|
913
|
-
removed = removeWorktreeDir(root, wt);
|
|
942
|
+
removed = removeWorktreeDir(root, wt, teardownCommands);
|
|
914
943
|
}
|
|
915
944
|
return { left: true, branch, target: targetBranch || currentBranch(root), sweepSha: sweep.sha || null, reason, worktreeRemoved: removed };
|
|
916
945
|
}
|