@deftai/directive-core 0.91.0 → 0.92.0

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.
Files changed (46) hide show
  1. package/dist/cache/fetch.js +3 -2
  2. package/dist/cache/io.d.ts +13 -2
  3. package/dist/cache/io.js +36 -7
  4. package/dist/cache/operations.js +5 -3
  5. package/dist/doctor/main.js +37 -0
  6. package/dist/hooks/dispatcher.d.ts +6 -1
  7. package/dist/hooks/dispatcher.js +30 -0
  8. package/dist/init-deposit/gitignore.js +3 -0
  9. package/dist/init-deposit/hygiene.d.ts +43 -1
  10. package/dist/init-deposit/hygiene.js +112 -10
  11. package/dist/init-deposit/scaffold.js +3 -1
  12. package/dist/orchestration/probe-session.d.ts +5 -1
  13. package/dist/orchestration/probe-session.js +24 -13
  14. package/dist/platform/agents-md.js +1 -1
  15. package/dist/policy/deft-directive-disable.d.ts +86 -0
  16. package/dist/policy/deft-directive-disable.js +167 -0
  17. package/dist/policy/delivery-branch.d.ts +33 -0
  18. package/dist/policy/delivery-branch.js +124 -0
  19. package/dist/policy/index.d.ts +2 -0
  20. package/dist/policy/index.js +17 -1
  21. package/dist/scope/brief-io.d.ts +3 -1
  22. package/dist/scope/brief-io.js +5 -3
  23. package/dist/scope/delivery-evidence.d.ts +112 -0
  24. package/dist/scope/delivery-evidence.js +419 -0
  25. package/dist/scope/index.d.ts +1 -0
  26. package/dist/scope/index.js +1 -0
  27. package/dist/scope/main.d.ts +5 -0
  28. package/dist/scope/main.js +98 -3
  29. package/dist/scope/registry-artifact-sync.js +4 -1
  30. package/dist/scope/transition.d.ts +11 -1
  31. package/dist/scope/transition.js +30 -4
  32. package/dist/session/ritual-sentinel.js +21 -12
  33. package/dist/session/session-start-hook.d.ts +3 -0
  34. package/dist/session/session-start-hook.js +21 -0
  35. package/dist/session/session-start.js +31 -0
  36. package/dist/swarm/complete-cohort.d.ts +20 -1
  37. package/dist/swarm/complete-cohort.js +56 -9
  38. package/dist/swarm/finalize-cohort-cli.js +9 -0
  39. package/dist/swarm/finalize-cohort.d.ts +8 -0
  40. package/dist/swarm/finalize-cohort.js +217 -21
  41. package/dist/user-config/experimental-rules.d.ts +43 -0
  42. package/dist/user-config/experimental-rules.js +162 -0
  43. package/dist/user-config/index.d.ts +1 -0
  44. package/dist/user-config/index.js +1 -0
  45. package/dist/verify-source/contained-writes.js +1 -1
  46. package/package.json +3 -3
@@ -8,6 +8,7 @@ import { atomicWriteBrief, formatBriefJson, readBriefForMutation } from "./brief
8
8
  import { stampCompletionMetadata } from "./capacity-stamp.js";
9
9
  import { LIFECYCLE_FOLDERS, MOVE_LABELS, STATUS_PRECONDITIONS, STAY_LABELS, TRANSITIONS, } from "./constants.js";
10
10
  import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecomposedParentBackReferences, } from "./decomposed-refs.js";
11
+ import { classifyStoredDeliveryDisposition, evaluateDeliveryGate, stampDeliveryProvenance, } from "./delivery-evidence.js";
11
12
  import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
12
13
  import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
13
14
  import { utcNowIso } from "./vbrief-json.js";
@@ -48,7 +49,7 @@ function stampEnvelopeUpdated(data, nowIso) {
48
49
  }
49
50
  }
50
51
  }
51
- export function runTransition(action, filePath, now = new Date()) {
52
+ export function runTransition(action, filePath, now = new Date(), options = {}) {
52
53
  if (!(action in TRANSITIONS)) {
53
54
  const valid = Object.keys(TRANSITIONS).sort().join(", ");
54
55
  return { ok: false, message: `Unknown action '${action}'. Valid actions: ${valid}` };
@@ -95,9 +96,15 @@ export function runTransition(action, filePath, now = new Date()) {
95
96
  const requiredStatus = STATUS_PRECONDITIONS[act];
96
97
  if (requiredStatus !== undefined) {
97
98
  if (currentStatus === targetStatus) {
99
+ // Surface legacy delivery disposition on already-completed briefs (#3041).
100
+ let dispositionSuffix = "";
101
+ if (act === "complete" && currentFolder === "completed") {
102
+ const disposition = classifyStoredDeliveryDisposition(planObj);
103
+ dispositionSuffix = ` (deliveryDisposition=${disposition})`;
104
+ }
98
105
  return {
99
106
  ok: true,
100
- message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}/`,
107
+ message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}/${dispositionSuffix}`,
101
108
  };
102
109
  }
103
110
  if (currentStatus !== requiredStatus) {
@@ -131,6 +138,25 @@ export function runTransition(action, filePath, now = new Date()) {
131
138
  }
132
139
  }
133
140
  const nowIso = utcNowIso(now);
141
+ // #3041: fail closed before mutating a code-bearing complete without delivery evidence.
142
+ if (act === "complete") {
143
+ const gate = evaluateDeliveryGate({
144
+ projectRoot,
145
+ plan: planObj,
146
+ nowIso,
147
+ evidence: options.deliveryEvidence,
148
+ nonDeliveryDisposition: options.nonDeliveryDisposition,
149
+ runGit: options.runGit,
150
+ verifier: options.verifier ?? "scope:complete",
151
+ assumeEvidenceValidated: options.assumeEvidenceValidated,
152
+ });
153
+ if (!gate.ok) {
154
+ return { ok: false, message: gate.message };
155
+ }
156
+ if (gate.provenance !== null) {
157
+ stampDeliveryProvenance(planObj, gate.provenance);
158
+ }
159
+ }
134
160
  planObj.status = targetStatus;
135
161
  planObj.updated = nowIso;
136
162
  // Keep the envelope clock aligned with plan.updated on every mutating transition (#2862).
@@ -153,7 +179,7 @@ export function runTransition(action, filePath, now = new Date()) {
153
179
  }
154
180
  // #2578: stamp terminal status at the destination path in the same write as
155
181
  // folder placement — never leave a non-terminal status under completed/.
156
- const writeResult = atomicWriteBrief(destPath, data, vbriefRoot);
182
+ const writeResult = atomicWriteBrief(destPath, data, vbriefRoot, { projectRoot });
157
183
  if (!writeResult.ok) {
158
184
  return { ok: false, message: writeResult.message };
159
185
  }
@@ -193,7 +219,7 @@ export function runTransition(action, filePath, now = new Date()) {
193
219
  message: `${actionLabel} ${basename}: ${currentFolder}/ -> ${targetFolder}/ (status: ${targetStatus})`,
194
220
  };
195
221
  }
196
- const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot);
222
+ const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot, { projectRoot });
197
223
  if (!writeResult.ok) {
198
224
  return { ok: false, message: writeResult.message };
199
225
  }
@@ -1,6 +1,7 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, } from "node:fs";
2
- import { join, relative, resolve } from "node:path";
1
+ import { existsSync, readdirSync, readFileSync, renameSync, rmSync, statSync } from "node:fs";
2
+ import { basename, dirname, join, relative, resolve } from "node:path";
3
3
  import { containedWrite } from "../fs/contained-write.js";
4
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
4
5
  import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, } from "../layout/resolve.js";
5
6
  import { stableJson } from "./json.js";
6
7
  import { RITUAL_STATE_CONTRACT } from "./posture.js";
@@ -95,21 +96,29 @@ function validateSteps(raw, key) {
95
96
  }
96
97
  return [steps, null];
97
98
  }
98
- function atomicWriteJson(targetPath, payload, prefix) {
99
- const dir = join(targetPath, "..");
100
- mkdirSync(dir, { recursive: true });
101
- const tmpBase = `${prefix}${process.pid}.json.tmp`;
99
+ /**
100
+ * Contained atomic JSON write for ritual/sentinel state (#3042).
101
+ * Containment root is projectRoot (not dirname(target)) so a force-added `.deft`
102
+ * directory symlink fails closed before temp+rename.
103
+ */
104
+ function atomicWriteJson(projectRoot, targetPath, payload, prefix) {
105
+ const root = resolve(projectRoot);
106
+ const abs = resolve(targetPath);
107
+ // Refuse leaf/parent symlinks on the final path before temp+rename publish.
108
+ assertWriteTargetSafe(root, abs);
109
+ const dir = dirname(abs);
110
+ const tmpBase = `${prefix}${process.pid}.${basename(abs)}.tmp`;
102
111
  const tmpName = join(dir, tmpBase);
103
112
  const text = `${stableJson(payload, 2)}\n`;
104
113
  try {
105
- // #2980 wave D: ritual-state product write routes through containedWrite.
114
+ // #2980 wave D / #3042: product write routes through containedWrite under projectRoot.
106
115
  containedWrite({
107
- root: resolve(dir),
108
- target: tmpBase,
116
+ root,
117
+ target: tmpName,
109
118
  data: text,
110
119
  mode: "create",
111
120
  });
112
- renameSync(tmpName, targetPath);
121
+ renameSync(tmpName, abs);
113
122
  }
114
123
  catch (err) {
115
124
  try {
@@ -191,7 +200,7 @@ export function readRitualState(projectRoot) {
191
200
  }
192
201
  export function writeRitualState(projectRoot, payload) {
193
202
  const stateFile = ritualStatePath(projectRoot);
194
- atomicWriteJson(stateFile, payload, ".ritual-state.");
203
+ atomicWriteJson(projectRoot, stateFile, payload, ".ritual-state.");
195
204
  return stateFile;
196
205
  }
197
206
  /** Instant guaranteed to fail `evaluateLoadedState` age checks on any policy horizon. */
@@ -303,7 +312,7 @@ export function writeSentinel(projectRoot, input) {
303
312
  .replace(`${LEGACY_ARTIFACT_DIR}/active/`, `${MIGRATED_ARTIFACT_DIR}/active/`),
304
313
  lastBranch: input.lastBranch,
305
314
  };
306
- atomicWriteJson(sentinelFile, payload, ".last-session.");
315
+ atomicWriteJson(projectRoot, sentinelFile, payload, ".last-session.");
307
316
  return sentinelFile;
308
317
  }
309
318
  function formatElapsed(deltaMs) {
@@ -1,3 +1,4 @@
1
+ import { detectDeftDirectiveDisable } from "../policy/deft-directive-disable.js";
1
2
  import { detectNoDeftDirective } from "../policy/no-deft-directive.js";
2
3
  import { writeSentinel } from "./ritual-sentinel.js";
3
4
  export interface SessionStartHookOptions {
@@ -7,6 +8,8 @@ export interface SessionStartHookOptions {
7
8
  readonly writeSentinelFn?: typeof writeSentinel;
8
9
  /** Test seam for #2926 opt-out detection. */
9
10
  readonly detectNoDeftDirectiveFn?: typeof detectNoDeftDirective;
11
+ /** Test seam for #3039 test kill-switch detection. */
12
+ readonly detectDeftDirectiveDisableFn?: typeof detectDeftDirectiveDisable;
10
13
  }
11
14
  /** Write ``.deft/last-session.json`` from current git state (#1269). */
12
15
  export declare function runSessionStartHookWrite(projectRoot: string, options?: SessionStartHookOptions): {
@@ -1,9 +1,30 @@
1
1
  import { resolveVersion } from "../doctor/paths.js";
2
+ import { detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
2
3
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, } from "../policy/no-deft-directive.js";
3
4
  import { detectBranch } from "./git.js";
4
5
  import { detectLatestActiveVbrief, writeSentinel } from "./ritual-sentinel.js";
5
6
  /** Write ``.deft/last-session.json`` from current git state (#1269). */
6
7
  export function runSessionStartHookWrite(projectRoot, options = {}) {
8
+ const detectKill = options.detectDeftDirectiveDisableFn ?? detectDeftDirectiveDisable;
9
+ // #3039: local (untracked) kill-switch — skip ritual bookkeeping (deposit OK).
10
+ // Tracked flags do not short-circuit (enforcement stays on).
11
+ const kill = detectKill(projectRoot);
12
+ const killActive = options.detectDeftDirectiveDisableFn !== undefined
13
+ ? kill.active
14
+ : isDeftDirectiveDisableActive(projectRoot);
15
+ if (killActive) {
16
+ const detectOptOut = options.detectNoDeftDirectiveFn ?? detectNoDeftDirective;
17
+ const optOut = detectOptOut(projectRoot);
18
+ const message = formatDeftDirectiveDisableMessage({
19
+ permanentOptOutAlsoPresent: optOut.present,
20
+ trackedByGit: false,
21
+ });
22
+ return {
23
+ code: 0,
24
+ stdout: `${message}\n`,
25
+ stderr: "",
26
+ };
27
+ }
7
28
  const detectOptOut = options.detectNoDeftDirectiveFn ?? detectNoDeftDirective;
8
29
  // #2926: root opt-out wins — host SessionStart must not write ritual bookkeeping.
9
30
  const optOut = detectOptOut(projectRoot);
@@ -3,6 +3,7 @@ import { runningInsideDeftRepo } from "../doctor/paths.js";
3
3
  import { emitSessionEvalReadback } from "../eval/readback.js";
4
4
  import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposit/migrate.js";
5
5
  import { detectEnvironmentContext, environmentContextToDict, formatEnvironmentContext, } from "../platform/shell-context.js";
6
+ import { DEFT_DIRECTIVE_DISABLE_FLAG_NAME, DEFT_DIRECTIVE_DISABLE_STATUS, detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
6
7
  import { disclosureLine } from "../policy/disclosure.js";
7
8
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
8
9
  import { humanMergeDisclosureLine, resolveHumanMergePolicy, } from "../policy/require-human-merge.js";
@@ -543,6 +544,36 @@ export function runSessionStart(projectRoot, options = {}) {
543
544
  const runGit = options.runGit ?? defaultGitRunner;
544
545
  const environment = (options.probeEnvironment ?? detectEnvironmentContext)();
545
546
  const ceremonyTier = options.ceremonyTier ?? COLD_CEREMONY_TIER;
547
+ // #3039: local (untracked) test kill-switch — skip ritual write; deposit may remain.
548
+ // Recovery requires delete + new session. Tracked flags do not short-circuit.
549
+ if (isDeftDirectiveDisableActive(projectRoot)) {
550
+ const killSwitch = detectDeftDirectiveDisable(projectRoot);
551
+ const optOutAlso = detectNoDeftDirective(projectRoot);
552
+ const message = formatDeftDirectiveDisableMessage({
553
+ permanentOptOutAlsoPresent: optOutAlso.present,
554
+ trackedByGit: false,
555
+ });
556
+ const lines = message.split("\n");
557
+ return {
558
+ code: 0,
559
+ payload: {
560
+ ready: false,
561
+ exit_code: 0,
562
+ disabled: true,
563
+ disabled_via: DEFT_DIRECTIVE_DISABLE_FLAG_NAME,
564
+ status: DEFT_DIRECTIVE_DISABLE_STATUS,
565
+ kill_switch: true,
566
+ inconsistent: false,
567
+ deposit_present: killSwitch.depositPresent,
568
+ tracked_by_git: false,
569
+ permanent_opt_out_also_present: optOutAlso.present,
570
+ posture,
571
+ environment: environmentContextToDict(environment),
572
+ message,
573
+ },
574
+ lines,
575
+ };
576
+ }
546
577
  // #2926: official root opt-out wins locally — skip Directive session ritual.
547
578
  // disabled = skip ritual (exit 0 clean / 1 inconsistent). ready stays false so
548
579
  // automation does not treat opt-out as "session fully initialized for work".
@@ -1,3 +1,14 @@
1
+ import { type DeliveryEvidenceInput, type NonDeliveryDisposition } from "../scope/delivery-evidence.js";
2
+ /** Per-story or default delivery evidence for cohort completion (#3041). */
3
+ export interface CohortDeliveryContext {
4
+ /** Evidence keyed by absolute story path (preferred). */
5
+ readonly evidenceByPath?: ReadonlyMap<string, DeliveryEvidenceInput> | null;
6
+ /** Shared evidence applied to every story when per-path map misses. */
7
+ readonly defaultEvidence?: DeliveryEvidenceInput | null;
8
+ readonly nonDeliveryDisposition?: NonDeliveryDisposition | null;
9
+ readonly assumeEvidenceValidated?: boolean;
10
+ readonly verifier?: string;
11
+ }
1
12
  export interface TransitionRecord {
2
13
  kind: "story" | "epic";
3
14
  path: string;
@@ -17,7 +28,13 @@ export declare function resolveCohortPaths(positional: readonly string[], cohort
17
28
  paths: string[];
18
29
  errors: string[];
19
30
  };
20
- export declare function sweepCohort(storyPaths: readonly string[], projectRoot: string, dryRun: boolean): SweepResult;
31
+ export interface SweepCohortArgs {
32
+ readonly storyPaths: readonly string[];
33
+ readonly projectRoot: string;
34
+ readonly dryRun: boolean;
35
+ readonly delivery?: CohortDeliveryContext | null;
36
+ }
37
+ export declare function sweepCohort(storyPaths: readonly string[], projectRoot: string, dryRun: boolean, delivery?: CohortDeliveryContext | null): SweepResult;
21
38
  export declare function sweepResultToDict(result: SweepResult): Record<string, unknown>;
22
39
  export declare function renderSweepText(result: SweepResult): string;
23
40
  export declare function completeCohort(args: {
@@ -26,6 +43,8 @@ export declare function completeCohort(args: {
26
43
  projectRoot: string;
27
44
  dryRun?: boolean;
28
45
  emitJson?: boolean;
46
+ /** Per-story delivery evidence; without it code-bearing stories fail closed (#3041). */
47
+ delivery?: CohortDeliveryContext | null;
29
48
  }): {
30
49
  exitCode: number;
31
50
  stdout: string;
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
3
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
4
4
  import { detectLifecycleFolder } from "../scope/decomposed-refs.js";
5
+ import { classifyStoredDeliveryDisposition, } from "../scope/delivery-evidence.js";
5
6
  import { runTransition } from "../scope/transition.js";
6
7
  import { collectChildUris, collectPlanRefs, resolveVbriefRef } from "../scope/vbrief-ref.js";
7
8
  import { MAX_FIXPOINT_PASSES, TERMINAL_FOLDERS } from "./constants.js";
@@ -115,17 +116,38 @@ function parentCandidatesFrom(plan, vbriefDir) {
115
116
  }
116
117
  return out;
117
118
  }
118
- function completeStory(storyPath, vbriefDir, projectRoot, settled, dryRun) {
119
+ function transitionOptionsFor(storyPath, delivery) {
120
+ if (delivery === null || delivery === undefined) {
121
+ return { verifier: "swarm:complete-cohort" };
122
+ }
123
+ const pathKey = resolve(storyPath);
124
+ const evidence = delivery.evidenceByPath?.get(pathKey) ??
125
+ delivery.evidenceByPath?.get(storyPath) ??
126
+ delivery.defaultEvidence ??
127
+ null;
128
+ return {
129
+ deliveryEvidence: evidence,
130
+ nonDeliveryDisposition: delivery.nonDeliveryDisposition,
131
+ assumeEvidenceValidated: delivery.assumeEvidenceValidated,
132
+ verifier: delivery.verifier ?? "swarm:complete-cohort",
133
+ };
134
+ }
135
+ function completeStory(args) {
136
+ const { storyPath, vbriefDir, projectRoot, settled, dryRun, delivery } = args;
119
137
  const folder = detectLifecycleFolder(storyPath);
120
138
  const relpath = rel(storyPath, projectRoot);
121
139
  if (folder !== null && TERMINAL_FOLDERS.includes(folder)) {
122
140
  settled.add(resolve(storyPath));
141
+ const plan = loadPlan(storyPath);
142
+ const disposition = folder === "completed" && plan !== null ? classifyStoredDeliveryDisposition(plan) : "unknown";
123
143
  return {
124
144
  kind: "story",
125
145
  path: relpath,
126
146
  action: "noop",
127
147
  ok: true,
128
- detail: `already in ${folder}/`,
148
+ detail: folder === "completed"
149
+ ? `already in ${folder}/ (deliveryDisposition=${disposition})`
150
+ : `already in ${folder}/`,
129
151
  };
130
152
  }
131
153
  if (folder !== "active") {
@@ -147,7 +169,7 @@ function completeStory(storyPath, vbriefDir, projectRoot, settled, dryRun) {
147
169
  detail: "would complete active/ -> completed/",
148
170
  };
149
171
  }
150
- const result = runTransition("complete", storyPath);
172
+ const result = runTransition("complete", storyPath, new Date(), transitionOptionsFor(storyPath, delivery));
151
173
  if (result.ok) {
152
174
  settled.add(resolve(join(vbriefDir, "completed", storyPath.split(/[/\\]/).pop() ?? "")));
153
175
  }
@@ -159,7 +181,8 @@ function completeStory(storyPath, vbriefDir, projectRoot, settled, dryRun) {
159
181
  detail: result.message,
160
182
  };
161
183
  }
162
- function completeParent(parentPath, vbriefDir, projectRoot, settled, dryRun) {
184
+ function completeParent(args) {
185
+ const { parentPath, vbriefDir, projectRoot, settled, dryRun, delivery } = args;
163
186
  const folder = detectLifecycleFolder(parentPath);
164
187
  const relpath = rel(parentPath, projectRoot);
165
188
  if (folder !== null && TERMINAL_FOLDERS.includes(folder)) {
@@ -217,7 +240,7 @@ function completeParent(parentPath, vbriefDir, projectRoot, settled, dryRun) {
217
240
  }
218
241
  current = join(vbriefDir, "active", parentPath.split(/[/\\]/).pop() ?? "");
219
242
  }
220
- const completeResult = runTransition("complete", current);
243
+ const completeResult = runTransition("complete", current, new Date(), transitionOptionsFor(current, delivery));
221
244
  if (completeResult.ok) {
222
245
  settled.add(resolve(join(vbriefDir, "completed", parentPath.split(/[/\\]/).pop() ?? "")));
223
246
  }
@@ -229,7 +252,12 @@ function completeParent(parentPath, vbriefDir, projectRoot, settled, dryRun) {
229
252
  detail: completeResult.message,
230
253
  };
231
254
  }
232
- export function sweepCohort(storyPaths, projectRoot, dryRun) {
255
+ export function sweepCohort(storyPaths, projectRoot, dryRun, delivery) {
256
+ return sweepCohortWithArgs({ storyPaths, projectRoot, dryRun, delivery: delivery ?? null });
257
+ }
258
+ function sweepCohortWithArgs(args) {
259
+ const { storyPaths, projectRoot, dryRun } = args;
260
+ const delivery = args.delivery ?? null;
233
261
  let vbriefDir;
234
262
  try {
235
263
  vbriefDir = resolveLifecycleRoot(projectRoot);
@@ -275,7 +303,14 @@ export function sweepCohort(storyPaths, projectRoot, dryRun) {
275
303
  }
276
304
  }
277
305
  }
278
- result.stories.push(completeStory(storyPath, vbriefDir, projectRoot, settled, dryRun));
306
+ result.stories.push(completeStory({
307
+ storyPath,
308
+ vbriefDir,
309
+ projectRoot,
310
+ settled,
311
+ dryRun,
312
+ delivery,
313
+ }));
279
314
  }
280
315
  const finalized = new Set();
281
316
  let passes = 0;
@@ -294,7 +329,14 @@ export function sweepCohort(storyPaths, projectRoot, dryRun) {
294
329
  if (!allChildrenSettled(parentPlan, vbriefDir, settled, dryRun)) {
295
330
  continue;
296
331
  }
297
- const record = completeParent(candidate, vbriefDir, projectRoot, settled, dryRun);
332
+ const record = completeParent({
333
+ parentPath: candidate,
334
+ vbriefDir,
335
+ projectRoot,
336
+ settled,
337
+ dryRun,
338
+ delivery,
339
+ });
298
340
  result.parents.push(record);
299
341
  finalized.add(candidate);
300
342
  progressed = true;
@@ -403,7 +445,12 @@ export function completeCohort(args) {
403
445
  }
404
446
  return { exitCode: 2, stdout: "", stderr };
405
447
  }
406
- const result = sweepCohort(paths, projectRoot, args.dryRun ?? false);
448
+ const result = sweepCohortWithArgs({
449
+ storyPaths: paths,
450
+ projectRoot,
451
+ dryRun: args.dryRun ?? false,
452
+ delivery: args.delivery ?? null,
453
+ });
407
454
  result.errors.push(...errors);
408
455
  if (args.emitJson) {
409
456
  return {
@@ -7,6 +7,7 @@ export function parseFinalizeCohortArgv(argv) {
7
7
  let repo = null;
8
8
  let projectRoot = ".";
9
9
  let baseBranch = "master";
10
+ let deliveryBranch = null;
10
11
  let label = null;
11
12
  let dryRun = false;
12
13
  let noCommit = false;
@@ -51,6 +52,13 @@ export function parseFinalizeCohortArgv(argv) {
51
52
  baseBranch = next;
52
53
  i += 1;
53
54
  }
55
+ else if (arg === "--delivery-branch" && next !== undefined) {
56
+ deliveryBranch = next;
57
+ i += 1;
58
+ }
59
+ else if (arg?.startsWith("--delivery-branch=")) {
60
+ deliveryBranch = arg.slice("--delivery-branch=".length);
61
+ }
54
62
  else if (arg === "--label" && next !== undefined) {
55
63
  label = next;
56
64
  i += 1;
@@ -77,6 +85,7 @@ export function parseFinalizeCohortArgv(argv) {
77
85
  repo,
78
86
  projectRoot,
79
87
  baseBranch,
88
+ deliveryBranch,
80
89
  label,
81
90
  dryRun,
82
91
  noCommit,
@@ -12,6 +12,8 @@ export interface FinalizeCohortResult {
12
12
  readonly commit_sha: string | null;
13
13
  readonly branch: string | null;
14
14
  readonly pr_url: string | null;
15
+ readonly delivery_branch: string | null;
16
+ readonly delivery_errors: readonly string[];
15
17
  readonly errors: readonly string[];
16
18
  readonly warnings: readonly string[];
17
19
  readonly ok: boolean;
@@ -21,7 +23,13 @@ export interface FinalizeCohortArgs {
21
23
  readonly storyTokens?: readonly string[];
22
24
  readonly repo?: string | null;
23
25
  readonly projectRoot?: string;
26
+ /**
27
+ * Swarm/PR base for the post-merge lifecycle sweep PR only.
28
+ * Distinct from plan.policy.deliveryBranch (#3041).
29
+ */
24
30
  readonly baseBranch?: string;
31
+ /** Override delivery branch for this run (defaults to policy/git default). */
32
+ readonly deliveryBranch?: string | null;
25
33
  readonly label?: string | null;
26
34
  readonly dryRun?: boolean;
27
35
  readonly noCommit?: boolean;