@deftai/directive-core 0.91.0 → 0.93.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/index.d.ts +1 -0
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/main.js +49 -0
- package/dist/doctor/openclaw-l2-adapter.d.ts +26 -0
- package/dist/doctor/openclaw-l2-adapter.js +199 -0
- package/dist/hooks/dispatcher.d.ts +6 -1
- package/dist/hooks/dispatcher.js +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/gitignore.js +3 -0
- package/dist/init-deposit/hygiene.d.ts +43 -1
- package/dist/init-deposit/hygiene.js +134 -10
- package/dist/init-deposit/index.d.ts +3 -0
- package/dist/init-deposit/index.js +3 -0
- package/dist/init-deposit/init-deposit.js +10 -0
- package/dist/init-deposit/refresh.js +11 -0
- package/dist/init-deposit/scaffold.js +6 -134
- package/dist/init-deposit/skill-discovery-deposit.d.ts +65 -0
- package/dist/init-deposit/skill-discovery-deposit.js +287 -0
- package/dist/init-deposit/skill-discovery-hosts.d.ts +94 -0
- package/dist/init-deposit/skill-discovery-hosts.js +217 -0
- package/dist/init-deposit/slash-deposit.d.ts +46 -0
- package/dist/init-deposit/slash-deposit.js +174 -0
- 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/host-slash-commands.d.ts +28 -0
- package/dist/policy/host-slash-commands.js +103 -0
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +61 -5
- 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/slash/emitters.d.ts +102 -0
- package/dist/slash/emitters.js +148 -0
- package/dist/slash/generator.d.ts +98 -0
- package/dist/slash/generator.js +145 -0
- package/dist/slash/index.d.ts +16 -0
- package/dist/slash/index.js +16 -0
- package/dist/slash/openclaw-adapter.d.ts +64 -0
- package/dist/slash/openclaw-adapter.js +198 -0
- package/dist/slash/openclaw-deposit.d.ts +73 -0
- package/dist/slash/openclaw-deposit.js +279 -0
- package/dist/slash/openclaw-slugs.d.ts +52 -0
- package/dist/slash/openclaw-slugs.js +126 -0
- package/dist/slash/product-set.d.ts +50 -0
- package/dist/slash/product-set.js +142 -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/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +50 -0
- package/dist/verify-source/contained-writes.js +1 -1
- package/dist/xbrief/create.d.ts +36 -0
- package/dist/xbrief/create.js +285 -0
- package/dist/xbrief/index.d.ts +14 -0
- package/dist/xbrief/index.js +42 -0
- package/dist/xbrief/paths.d.ts +37 -0
- package/dist/xbrief/paths.js +123 -0
- package/dist/xbrief/styles.d.ts +36 -0
- package/dist/xbrief/styles.js +235 -0
- package/dist/xbrief/types.d.ts +50 -0
- package/dist/xbrief/types.js +17 -0
- package/dist/xbrief/verify.d.ts +30 -0
- package/dist/xbrief/verify.js +251 -0
- package/package.json +11 -3
|
@@ -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;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { evaluate as evaluateBranchPolicy } from "../branch/evaluate.js";
|
|
4
|
+
import { extractIssueRef } from "../capacity/backfill.js";
|
|
4
5
|
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
6
|
+
import { resolveDeliveryBranch } from "../policy/delivery-branch.js";
|
|
5
7
|
import { defaultRunGh, fetchClosingIssuesReferences } from "../pr-protected-issues/gh.js";
|
|
8
|
+
import { classifyStoredDeliveryDisposition, evidenceFromPrPayload, verifyDeliveryAncestry, } from "../scope/delivery-evidence.js";
|
|
6
9
|
import { completeCohort } from "./complete-cohort.js";
|
|
7
10
|
import { EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK } from "./constants.js";
|
|
8
11
|
import { completedBriefReferencesIssue, resolveStories } from "./launch.js";
|
|
@@ -55,45 +58,73 @@ function parseRepo(repo) {
|
|
|
55
58
|
}
|
|
56
59
|
return { owner: repo.slice(0, slash), name: repo.slice(slash + 1) };
|
|
57
60
|
}
|
|
58
|
-
function
|
|
61
|
+
function fetchPrDeliverySnapshot(prNumber, repo, deliveryBranch, runGh) {
|
|
59
62
|
const parsed = parseRepo(repo);
|
|
60
63
|
if (parsed === null) {
|
|
61
|
-
return {
|
|
64
|
+
return { snapshot: null, error: `invalid --repo value: ${JSON.stringify(repo)}` };
|
|
62
65
|
}
|
|
63
66
|
const path = `repos/${parsed.owner}/${parsed.name}/pulls/${prNumber}`;
|
|
64
67
|
const result = runGh(["gh", "api", path]);
|
|
65
68
|
if (result.returncode !== 0) {
|
|
66
69
|
return {
|
|
67
|
-
|
|
70
|
+
snapshot: null,
|
|
68
71
|
error: `gh api ${path} failed: ${result.stderr.trim() || result.stdout.trim()}`,
|
|
69
72
|
};
|
|
70
73
|
}
|
|
71
74
|
try {
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
75
|
+
const body = JSON.parse(result.stdout);
|
|
76
|
+
if (body === null || typeof body !== "object") {
|
|
74
77
|
return {
|
|
75
|
-
|
|
78
|
+
snapshot: null,
|
|
76
79
|
error: `unexpected gh api response for PR #${prNumber}: not a JSON object`,
|
|
77
80
|
};
|
|
78
81
|
}
|
|
79
|
-
const payload =
|
|
82
|
+
const payload = body;
|
|
80
83
|
const mergedAt = payload.merged_at;
|
|
81
|
-
if (mergedAt === null) {
|
|
82
|
-
return {
|
|
84
|
+
if (mergedAt === null || typeof mergedAt !== "string" || mergedAt.length === 0) {
|
|
85
|
+
return { snapshot: null, error: `PR #${prNumber} is not merged yet.` };
|
|
83
86
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
const base = payload.base;
|
|
88
|
+
const prBase = typeof base === "object" &&
|
|
89
|
+
base !== null &&
|
|
90
|
+
!Array.isArray(base) &&
|
|
91
|
+
typeof base.ref === "string"
|
|
92
|
+
? String(base.ref)
|
|
93
|
+
: null;
|
|
94
|
+
const mergeCommitSha = typeof payload.merge_commit_sha === "string" && payload.merge_commit_sha.length > 0
|
|
95
|
+
? payload.merge_commit_sha
|
|
96
|
+
: null;
|
|
97
|
+
const evidence = evidenceFromPrPayload(payload, prNumber, repo, deliveryBranch);
|
|
98
|
+
return {
|
|
99
|
+
snapshot: {
|
|
100
|
+
mergedAt,
|
|
101
|
+
prBase,
|
|
102
|
+
mergeCommitSha,
|
|
103
|
+
payload,
|
|
104
|
+
evidence,
|
|
105
|
+
},
|
|
106
|
+
error: null,
|
|
107
|
+
};
|
|
88
108
|
}
|
|
89
109
|
catch (exc) {
|
|
90
110
|
const message = exc instanceof Error ? exc.message : String(exc);
|
|
91
111
|
return {
|
|
92
|
-
|
|
112
|
+
snapshot: null,
|
|
93
113
|
error: `failed to parse gh api response for PR #${prNumber}: ${message}`,
|
|
94
114
|
};
|
|
95
115
|
}
|
|
96
116
|
}
|
|
117
|
+
/** Adapt swarm runText to the session GitRunner shape used by delivery-evidence. */
|
|
118
|
+
function asGitRunner(runGit) {
|
|
119
|
+
return (projectRoot, args) => {
|
|
120
|
+
const result = runGit(["git", ...args], { cwd: projectRoot });
|
|
121
|
+
return {
|
|
122
|
+
code: result.returncode,
|
|
123
|
+
stdout: result.stdout,
|
|
124
|
+
stderr: result.stderr,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
}
|
|
97
128
|
function fetchClosingIssues(prNumber, repo, runGh) {
|
|
98
129
|
const parsed = parseRepo(repo);
|
|
99
130
|
if (parsed === null) {
|
|
@@ -280,14 +311,34 @@ export function finalizeCohort(args) {
|
|
|
280
311
|
const projectRoot = resolve(args.projectRoot ?? process.cwd());
|
|
281
312
|
const runGh = args.runGh ?? defaultRunGh;
|
|
282
313
|
const runGit = args.runGit ?? runText;
|
|
314
|
+
const gitRunner = asGitRunner(runGit);
|
|
283
315
|
const prNumbers = [...(args.prNumbers ?? [])].sort((a, b) => a - b);
|
|
284
316
|
const storyTokens = splitCsv(args.storyTokens ?? []);
|
|
285
317
|
const repo = args.repo ?? process.env.GH_REPO ?? null;
|
|
318
|
+
// baseBranch is for the lifecycle sweep PR only — not delivery proof (#3041).
|
|
286
319
|
const baseBranch = args.baseBranch ?? "master";
|
|
287
320
|
const dryRun = args.dryRun ?? false;
|
|
288
321
|
const noCommit = args.noCommit ?? false;
|
|
289
322
|
const noOpenPr = args.noOpenPr ?? false;
|
|
290
323
|
const errors = [];
|
|
324
|
+
const deliveryErrors = [];
|
|
325
|
+
// plan.policy.deliveryBranch (or git default) is SoT. CLI may only fill when policy is
|
|
326
|
+
// not typed — never redefine a typed delivery branch to an integration target (#3041).
|
|
327
|
+
const policyDelivery = resolveDeliveryBranch(projectRoot, gitRunner);
|
|
328
|
+
const cliDelivery = args.deliveryBranch !== null &&
|
|
329
|
+
args.deliveryBranch !== undefined &&
|
|
330
|
+
args.deliveryBranch.trim().length > 0
|
|
331
|
+
? args.deliveryBranch.trim()
|
|
332
|
+
: null;
|
|
333
|
+
if (cliDelivery !== null && cliDelivery !== policyDelivery.branch) {
|
|
334
|
+
if (policyDelivery.source === "typed") {
|
|
335
|
+
errors.push(`--delivery-branch '${cliDelivery}' conflicts with plan.policy.deliveryBranch ` +
|
|
336
|
+
`'${policyDelivery.branch}'. Typed policy wins; do not redefine delivery via CLI (#3041).`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const deliveryBranch = policyDelivery.source === "typed"
|
|
340
|
+
? policyDelivery.branch
|
|
341
|
+
: (cliDelivery ?? policyDelivery.branch);
|
|
291
342
|
if (!existsSync(projectRoot)) {
|
|
292
343
|
return buildResponse({
|
|
293
344
|
projectRoot,
|
|
@@ -300,6 +351,8 @@ export function finalizeCohort(args) {
|
|
|
300
351
|
commitSha: null,
|
|
301
352
|
branch: null,
|
|
302
353
|
prUrl: null,
|
|
354
|
+
deliveryBranch,
|
|
355
|
+
deliveryErrors: [],
|
|
303
356
|
errors: [`project root does not exist: ${projectRoot}`],
|
|
304
357
|
warnings: [],
|
|
305
358
|
ok: false,
|
|
@@ -319,6 +372,8 @@ export function finalizeCohort(args) {
|
|
|
319
372
|
commitSha: null,
|
|
320
373
|
branch: null,
|
|
321
374
|
prUrl: null,
|
|
375
|
+
deliveryBranch,
|
|
376
|
+
deliveryErrors: [],
|
|
322
377
|
errors: [`no xbrief/ directory under project root: ${projectRoot}`],
|
|
323
378
|
warnings: [],
|
|
324
379
|
ok: false,
|
|
@@ -327,27 +382,63 @@ export function finalizeCohort(args) {
|
|
|
327
382
|
});
|
|
328
383
|
}
|
|
329
384
|
const closingIssues = new Set();
|
|
330
|
-
|
|
385
|
+
/** Per-closing-issue delivery evidence so multi-PR cohorts do not collapse provenance (#3041). */
|
|
386
|
+
const evidenceByIssue = new Map();
|
|
387
|
+
const validatedPrs = [];
|
|
388
|
+
if (prNumbers.length > 0 && errors.length === 0) {
|
|
331
389
|
if (repo === null || repo.length === 0) {
|
|
332
390
|
errors.push("--repo OWNER/REPO is required when --pr is supplied (or set $GH_REPO).");
|
|
333
391
|
}
|
|
334
392
|
else {
|
|
335
393
|
for (const prNumber of prNumbers) {
|
|
336
|
-
const
|
|
337
|
-
if (
|
|
338
|
-
errors.push(
|
|
394
|
+
const fetched = fetchPrDeliverySnapshot(prNumber, repo, deliveryBranch, runGh);
|
|
395
|
+
if (fetched.error !== null || fetched.snapshot === null) {
|
|
396
|
+
errors.push(fetched.error ?? `PR #${prNumber}: delivery snapshot unavailable`);
|
|
339
397
|
continue;
|
|
340
398
|
}
|
|
399
|
+
const snap = fetched.snapshot;
|
|
400
|
+
// base.ref must equal configured deliveryBranch — integration bases fail closed (#3041).
|
|
401
|
+
if (snap.prBase === null || snap.prBase.length === 0) {
|
|
402
|
+
deliveryErrors.push(`PR #${prNumber}: missing base.ref; cannot verify delivery branch (#3041).`);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (snap.prBase !== deliveryBranch) {
|
|
406
|
+
deliveryErrors.push(`PR #${prNumber}: base.ref '${snap.prBase}' is not the delivery branch ` +
|
|
407
|
+
`'${deliveryBranch}'. Merged-to-integration is not delivery evidence (#3041). ` +
|
|
408
|
+
`(Note: --base-branch only controls the lifecycle sweep PR target, not delivery.)`);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (snap.mergeCommitSha === null) {
|
|
412
|
+
deliveryErrors.push(`PR #${prNumber}: missing merge_commit_sha; cannot prove delivery ancestry (#3041).`);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
// Refresh remote delivery ref and require merge commit ancestry (#3041).
|
|
416
|
+
const ancestry = verifyDeliveryAncestry(projectRoot, snap.mergeCommitSha, deliveryBranch, gitRunner);
|
|
417
|
+
if (!ancestry.ok) {
|
|
418
|
+
deliveryErrors.push(`PR #${prNumber}: ${ancestry.error}`);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
validatedPrs.push(prNumber);
|
|
422
|
+
const prEvidence = {
|
|
423
|
+
...snap.evidence,
|
|
424
|
+
deliveryBranch,
|
|
425
|
+
deliveryCommit: ancestry.remoteTip,
|
|
426
|
+
verifier: "swarm:finalize-cohort",
|
|
427
|
+
};
|
|
341
428
|
const closing = fetchClosingIssues(prNumber, repo, runGh);
|
|
342
429
|
if (closing.error !== null) {
|
|
343
430
|
errors.push(closing.error);
|
|
344
431
|
}
|
|
345
432
|
for (const issue of closing.issues) {
|
|
346
433
|
closingIssues.add(issue);
|
|
434
|
+
evidenceByIssue.set(issue, prEvidence);
|
|
347
435
|
}
|
|
348
436
|
}
|
|
349
437
|
}
|
|
350
438
|
}
|
|
439
|
+
if (deliveryErrors.length > 0) {
|
|
440
|
+
errors.push(...deliveryErrors);
|
|
441
|
+
}
|
|
351
442
|
if (storyTokens.length === 0 && closingIssues.size === 0) {
|
|
352
443
|
errors.push("empty cohort: pass --pr <numbers> and/or --stories <ids|paths>.");
|
|
353
444
|
}
|
|
@@ -393,8 +484,33 @@ export function finalizeCohort(args) {
|
|
|
393
484
|
const completedBrief = completedBriefReferencesIssue(projectRoot, issue);
|
|
394
485
|
const issueClosed = completedBrief || fetchIssueClosed(issue, repo, runGh);
|
|
395
486
|
if (completedBrief || issueClosed) {
|
|
487
|
+
let dispositionNote = "";
|
|
488
|
+
if (completedBrief) {
|
|
489
|
+
try {
|
|
490
|
+
const completedDir = resolve(projectRoot, "xbrief", "completed");
|
|
491
|
+
// Best-effort surface of legacy delivery disposition for completed briefs (#3041).
|
|
492
|
+
if (existsSync(completedDir)) {
|
|
493
|
+
for (const name of readdirSync(completedDir)) {
|
|
494
|
+
if (!name.endsWith(".json"))
|
|
495
|
+
continue;
|
|
496
|
+
const raw = JSON.parse(readFileSync(resolve(completedDir, name), "utf8"));
|
|
497
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
498
|
+
continue;
|
|
499
|
+
const plan = raw.plan;
|
|
500
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan))
|
|
501
|
+
continue;
|
|
502
|
+
const disposition = classifyStoredDeliveryDisposition(plan);
|
|
503
|
+
dispositionNote = ` deliveryDisposition=${disposition}`;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
/* best-effort disposition surfacing for legacy completed records */
|
|
510
|
+
}
|
|
511
|
+
}
|
|
396
512
|
const reason = completedBrief
|
|
397
|
-
?
|
|
513
|
+
? `a completed brief already exists${dispositionNote}`
|
|
398
514
|
: "the issue is already closed";
|
|
399
515
|
warnings.push(`#${issue}: no active story references this closing issue; skipped (${reason}).`);
|
|
400
516
|
}
|
|
@@ -417,17 +533,78 @@ export function finalizeCohort(args) {
|
|
|
417
533
|
commitSha: null,
|
|
418
534
|
branch: null,
|
|
419
535
|
prUrl: null,
|
|
536
|
+
deliveryBranch,
|
|
537
|
+
deliveryErrors,
|
|
420
538
|
errors,
|
|
421
539
|
warnings,
|
|
422
540
|
ok: cleanNoop,
|
|
423
541
|
emitJson: args.emitJson ?? false,
|
|
424
542
|
exitCode: cleanNoop
|
|
425
543
|
? EXIT_OK
|
|
426
|
-
: errors.some((e) => e.includes("not merged"))
|
|
544
|
+
: errors.some((e) => e.includes("not merged") || e.includes("delivery"))
|
|
427
545
|
? EXIT_GATE_FAILED
|
|
428
546
|
: EXIT_CONFIG_ERROR,
|
|
429
547
|
});
|
|
430
548
|
}
|
|
549
|
+
// When --pr was supplied, every PR must pass delivery validation before sweep (#3041).
|
|
550
|
+
if (prNumbers.length > 0 && validatedPrs.length !== prNumbers.length && errors.length > 0) {
|
|
551
|
+
return buildResponse({
|
|
552
|
+
projectRoot,
|
|
553
|
+
dryRun,
|
|
554
|
+
noCommit,
|
|
555
|
+
prNumbers,
|
|
556
|
+
storyPaths,
|
|
557
|
+
closingIssues: [...closingIssues],
|
|
558
|
+
sweep: null,
|
|
559
|
+
commitSha: null,
|
|
560
|
+
branch: null,
|
|
561
|
+
prUrl: null,
|
|
562
|
+
deliveryBranch,
|
|
563
|
+
deliveryErrors,
|
|
564
|
+
errors,
|
|
565
|
+
warnings,
|
|
566
|
+
ok: false,
|
|
567
|
+
emitJson: args.emitJson ?? false,
|
|
568
|
+
exitCode: EXIT_GATE_FAILED,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
// Stories without PR-backed delivery evidence still hit the complete gate; if
|
|
572
|
+
// only --stories was supplied, complete-cohort fails closed for code-bearing
|
|
573
|
+
// scopes unless callers pass evidence (finalize requires --pr for delivery).
|
|
574
|
+
if (prNumbers.length === 0 && evidenceByIssue.size === 0) {
|
|
575
|
+
warnings.push("No --pr supplied: code-bearing stories require delivery evidence or " +
|
|
576
|
+
"will fail closed at scope:complete (#3041).");
|
|
577
|
+
}
|
|
578
|
+
// Bind each story path to the evidence of its closing-issue PR (no cohort-wide collapse).
|
|
579
|
+
const evidenceByPath = new Map();
|
|
580
|
+
for (const storyPath of storyPaths) {
|
|
581
|
+
try {
|
|
582
|
+
const raw = JSON.parse(readFileSync(storyPath, "utf8"));
|
|
583
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const plan = raw.plan;
|
|
587
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const [, issueNum] = extractIssueRef(plan);
|
|
591
|
+
if (issueNum !== null) {
|
|
592
|
+
const bound = evidenceByIssue.get(issueNum);
|
|
593
|
+
if (bound !== undefined) {
|
|
594
|
+
evidenceByPath.set(resolve(storyPath), bound);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
/* unreadable brief — complete gate fails closed later if code-bearing */
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Single-PR cohorts: every story inherits that PR's evidence when issue binding misses
|
|
603
|
+
// (operator --stories + one --pr is the common finalize path).
|
|
604
|
+
let defaultEvidence = null;
|
|
605
|
+
if (validatedPrs.length === 1 && evidenceByIssue.size > 0) {
|
|
606
|
+
defaultEvidence = evidenceByIssue.values().next().value ?? null;
|
|
607
|
+
}
|
|
431
608
|
let commitSha = null;
|
|
432
609
|
let branch = null;
|
|
433
610
|
let prUrl = null;
|
|
@@ -447,11 +624,21 @@ export function finalizeCohort(args) {
|
|
|
447
624
|
}
|
|
448
625
|
}
|
|
449
626
|
}
|
|
627
|
+
const hasDelivery = evidenceByPath.size > 0 || defaultEvidence !== null;
|
|
450
628
|
const sweepResult = completeCohort({
|
|
451
629
|
stories: storyPaths,
|
|
452
630
|
projectRoot,
|
|
453
631
|
dryRun,
|
|
454
632
|
emitJson: false,
|
|
633
|
+
delivery: hasDelivery
|
|
634
|
+
? {
|
|
635
|
+
evidenceByPath,
|
|
636
|
+
defaultEvidence,
|
|
637
|
+
// Ancestry already verified above; avoid double remote fetch on each story.
|
|
638
|
+
assumeEvidenceValidated: true,
|
|
639
|
+
verifier: "swarm:finalize-cohort",
|
|
640
|
+
}
|
|
641
|
+
: null,
|
|
455
642
|
});
|
|
456
643
|
if (sweepResult.exitCode !== 0) {
|
|
457
644
|
errors.push("cohort completion sweep failed.");
|
|
@@ -466,6 +653,8 @@ export function finalizeCohort(args) {
|
|
|
466
653
|
commitSha: null,
|
|
467
654
|
branch: null,
|
|
468
655
|
prUrl: null,
|
|
656
|
+
deliveryBranch,
|
|
657
|
+
deliveryErrors,
|
|
469
658
|
errors,
|
|
470
659
|
warnings,
|
|
471
660
|
ok: false,
|
|
@@ -503,6 +692,8 @@ export function finalizeCohort(args) {
|
|
|
503
692
|
commitSha,
|
|
504
693
|
branch,
|
|
505
694
|
prUrl,
|
|
695
|
+
deliveryBranch,
|
|
696
|
+
deliveryErrors,
|
|
506
697
|
errors,
|
|
507
698
|
warnings,
|
|
508
699
|
ok,
|
|
@@ -522,6 +713,8 @@ function buildResponse(input) {
|
|
|
522
713
|
commit_sha: input.commitSha,
|
|
523
714
|
branch: input.branch,
|
|
524
715
|
pr_url: input.prUrl,
|
|
716
|
+
delivery_branch: input.deliveryBranch,
|
|
717
|
+
delivery_errors: input.deliveryErrors,
|
|
525
718
|
errors: input.errors,
|
|
526
719
|
warnings: input.warnings,
|
|
527
720
|
ok: input.ok,
|
|
@@ -539,6 +732,9 @@ function buildResponse(input) {
|
|
|
539
732
|
`(${input.storyPaths.length} stor${input.storyPaths.length === 1 ? "y" : "ies"})`,
|
|
540
733
|
` Project root: ${input.projectRoot}`,
|
|
541
734
|
];
|
|
735
|
+
if (input.deliveryBranch !== null) {
|
|
736
|
+
lines.push(` Delivery branch: ${input.deliveryBranch}`);
|
|
737
|
+
}
|
|
542
738
|
if (input.prNumbers.length > 0) {
|
|
543
739
|
lines.push(` PRs: ${input.prNumbers.map((n) => `#${n}`).join(", ")}`);
|
|
544
740
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe USER.md Experimental Rules toggle (#46).
|
|
3
|
+
*
|
|
4
|
+
* Pure string helpers that add/remove only the three canonical meta-philosophy
|
|
5
|
+
* reference lines (SOUL / morals / code-field). Personal and Defaults section
|
|
6
|
+
* bodies are never rewritten — only the `## Experimental Rules` region changes.
|
|
7
|
+
*
|
|
8
|
+
* Detection matches lines under `## Experimental Rules` that contain the deposit
|
|
9
|
+
* path (`meta/SOUL.md`, …); enable writes the canonical setup-skill bullet.
|
|
10
|
+
* Custom bullets for other topics in the same section are preserved.
|
|
11
|
+
*/
|
|
12
|
+
export type ExperimentalMetaId = "soul" | "morals" | "code-field";
|
|
13
|
+
export interface ExperimentalMetaEntry {
|
|
14
|
+
readonly id: ExperimentalMetaId;
|
|
15
|
+
/** Path fragment matched in USER.md lines (e.g. `meta/SOUL.md`). */
|
|
16
|
+
readonly path: string;
|
|
17
|
+
/** Canonical bullet written when enabling. */
|
|
18
|
+
readonly line: string;
|
|
19
|
+
}
|
|
20
|
+
/** Canonical Experimental Rules entries (setup Phase 1 steps 5a–5c). */
|
|
21
|
+
export declare const EXPERIMENTAL_META_ENTRIES: readonly ExperimentalMetaEntry[];
|
|
22
|
+
export type ExperimentalRulesState = Record<ExperimentalMetaId, boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Parse on/off state for the three experimental meta entries.
|
|
25
|
+
* A path is ON only when a line under `## Experimental Rules` mentions it —
|
|
26
|
+
* mentions in Personal/Defaults/prose do not count (writes are section-scoped).
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseExperimentalRulesState(userMdText: string): ExperimentalRulesState;
|
|
29
|
+
/**
|
|
30
|
+
* Apply desired Experimental Rules on/off state.
|
|
31
|
+
*
|
|
32
|
+
* - Only mutates the `## Experimental Rules` region (or inserts/removes it).
|
|
33
|
+
* - Personal / Defaults content is left byte-identical outside that region.
|
|
34
|
+
* - Enabling uses the canonical setup-skill lines; disabling drops path matches.
|
|
35
|
+
* - Custom non-meta bullets under the section are preserved.
|
|
36
|
+
* - When all three are off and no custom bullets remain, the section is removed.
|
|
37
|
+
*/
|
|
38
|
+
export declare function applyExperimentalRulesState(userMdText: string, desired: ExperimentalRulesState): string;
|
|
39
|
+
/**
|
|
40
|
+
* Toggle a single experimental meta entry; leave the other two unchanged.
|
|
41
|
+
*/
|
|
42
|
+
export declare function setExperimentalRule(userMdText: string, id: ExperimentalMetaId, enabled: boolean): string;
|
|
43
|
+
//# sourceMappingURL=experimental-rules.d.ts.map
|