@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.
- package/dist/cache/fetch.js +3 -2
- package/dist/cache/io.d.ts +13 -2
- package/dist/cache/io.js +36 -7
- package/dist/cache/operations.js +5 -3
- package/dist/doctor/main.js +37 -0
- package/dist/hooks/dispatcher.d.ts +6 -1
- package/dist/hooks/dispatcher.js +30 -0
- package/dist/init-deposit/gitignore.js +3 -0
- package/dist/init-deposit/hygiene.d.ts +43 -1
- package/dist/init-deposit/hygiene.js +112 -10
- package/dist/init-deposit/scaffold.js +3 -1
- package/dist/orchestration/probe-session.d.ts +5 -1
- package/dist/orchestration/probe-session.js +24 -13
- package/dist/platform/agents-md.js +1 -1
- package/dist/policy/deft-directive-disable.d.ts +86 -0
- package/dist/policy/deft-directive-disable.js +167 -0
- package/dist/policy/delivery-branch.d.ts +33 -0
- package/dist/policy/delivery-branch.js +124 -0
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +17 -1
- package/dist/scope/brief-io.d.ts +3 -1
- package/dist/scope/brief-io.js +5 -3
- package/dist/scope/delivery-evidence.d.ts +112 -0
- package/dist/scope/delivery-evidence.js +419 -0
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/main.d.ts +5 -0
- package/dist/scope/main.js +98 -3
- package/dist/scope/registry-artifact-sync.js +4 -1
- package/dist/scope/transition.d.ts +11 -1
- package/dist/scope/transition.js +30 -4
- package/dist/session/ritual-sentinel.js +21 -12
- package/dist/session/session-start-hook.d.ts +3 -0
- package/dist/session/session-start-hook.js +21 -0
- package/dist/session/session-start.js +31 -0
- package/dist/swarm/complete-cohort.d.ts +20 -1
- package/dist/swarm/complete-cohort.js +56 -9
- package/dist/swarm/finalize-cohort-cli.js +9 -0
- package/dist/swarm/finalize-cohort.d.ts +8 -0
- package/dist/swarm/finalize-cohort.js +217 -21
- package/dist/user-config/experimental-rules.d.ts +43 -0
- package/dist/user-config/experimental-rules.js +162 -0
- package/dist/user-config/index.d.ts +1 -0
- package/dist/user-config/index.js +1 -0
- package/dist/verify-source/contained-writes.js +1 -1
- package/package.json +3 -3
package/dist/scope/transition.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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:
|
|
114
|
+
// #2980 wave D / #3042: product write routes through containedWrite under projectRoot.
|
|
106
115
|
containedWrite({
|
|
107
|
-
root
|
|
108
|
-
target:
|
|
116
|
+
root,
|
|
117
|
+
target: tmpName,
|
|
109
118
|
data: text,
|
|
110
119
|
mode: "create",
|
|
111
120
|
});
|
|
112
|
-
renameSync(tmpName,
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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;
|