@deftai/directive-core 0.90.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/doctor/openclaw-skills.d.ts +42 -8
- package/dist/doctor/openclaw-skills.js +281 -38
- package/dist/eval/health.d.ts +10 -1
- package/dist/eval/health.js +16 -16
- 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 -7
- package/dist/init-deposit/init-deposit.d.ts +5 -0
- package/dist/init-deposit/init-deposit.js +37 -2
- package/dist/init-deposit/scaffold.js +3 -1
- package/dist/intake/github-auth-modes.d.ts +1 -1
- package/dist/intake/github-auth-modes.js +25 -3
- package/dist/intake/issue-ingest-cli.js +13 -0
- package/dist/intake/reconcile-issues-cli.js +13 -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/index.d.ts +2 -0
- package/dist/policy/index.js +17 -1
- package/dist/policy/plan-extensions.d.ts +10 -0
- package/dist/policy/plan-extensions.js +16 -0
- package/dist/pr-closing-keywords/detect.d.ts +13 -0
- package/dist/pr-closing-keywords/detect.js +73 -8
- package/dist/pr-closing-keywords/index.d.ts +2 -2
- package/dist/pr-closing-keywords/index.js +1 -1
- package/dist/pr-closing-keywords/main.js +119 -90
- package/dist/pr-closing-keywords/types.d.ts +10 -0
- package/dist/render/constants.d.ts +16 -0
- package/dist/render/constants.js +16 -1
- package/dist/render/export-spec.d.ts +7 -1
- package/dist/render/export-spec.js +51 -5
- package/dist/render/index.d.ts +3 -2
- package/dist/render/index.js +2 -2
- package/dist/render/roadmap-render.js +164 -60
- package/dist/render/scope-outlook.d.ts +5 -0
- package/dist/render/scope-outlook.js +3 -0
- package/dist/render/spec-render.d.ts +28 -2
- package/dist/render/spec-render.js +111 -14
- package/dist/render/text-utils.d.ts +6 -0
- package/dist/render/text-utils.js +23 -0
- package/dist/scm/binary.d.ts +3 -0
- package/dist/scm/binary.js +10 -2
- package/dist/scm/call.d.ts +5 -0
- package/dist/scm/call.js +5 -0
- package/dist/scm/index.d.ts +1 -0
- package/dist/scm/index.js +1 -0
- package/dist/scm/main.d.ts +5 -0
- package/dist/scm/main.js +27 -0
- package/dist/scm/readiness-cli.d.ts +24 -0
- package/dist/scm/readiness-cli.js +85 -0
- package/dist/scm/readiness.d.ts +104 -0
- package/dist/scm/readiness.js +421 -0
- package/dist/scope/batch-promote.d.ts +26 -0
- package/dist/scope/batch-promote.js +167 -0
- 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 +2 -0
- package/dist/scope/index.js +2 -0
- package/dist/scope/main.d.ts +7 -0
- package/dist/scope/main.js +155 -9
- 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/scope/wip-cap-check.d.ts +6 -0
- package/dist/scope/wip-cap-check.js +14 -2
- 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.d.ts +7 -0
- package/dist/session/session-start.js +85 -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/triage/welcome/index.d.ts +1 -1
- package/dist/triage/welcome/index.js +1 -1
- package/dist/triage/welcome/onboard.js +7 -1
- package/dist/triage/welcome/prior-state.d.ts +7 -0
- package/dist/triage/welcome/prior-state.js +16 -2
- package/dist/triage/welcome/writers.d.ts +10 -0
- package/dist/triage/welcome/writers.js +72 -2
- 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
|
@@ -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
|
}
|
|
@@ -3,5 +3,5 @@ export { type DefaultModeOptions, formatWelcomeCommand, normalizeTaskPrefix, run
|
|
|
3
3
|
export { DEFAULT_ONBOARD_PRESET, type OnboardOptions, type OnboardOutcome, runOnboardMode, } from "./onboard.js";
|
|
4
4
|
export { candidatesLogPath, classifyOnboarding, detectPriorState, type PriorState, pendingDecisionsNudgeLine, } from "./prior-state.js";
|
|
5
5
|
export { computeSummary, emitOneliner, formatOneLiner, formatSummary, type SummaryResult, } from "./summary.js";
|
|
6
|
-
export { appendAuditEntry, previewWipRelief, type ReliefPreview, subscriptionPreset, writeTriageScope, writeWipCap, } from "./writers.js";
|
|
6
|
+
export { appendAuditEntry, previewWipRelief, type ReliefPreview, subscriptionPreset, writeTriageScope, writeWipCap, writeWipCapDecision, } from "./writers.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -3,5 +3,5 @@ export { formatWelcomeCommand, normalizeTaskPrefix, runDefaultMode, } from "./de
|
|
|
3
3
|
export { DEFAULT_ONBOARD_PRESET, runOnboardMode, } from "./onboard.js";
|
|
4
4
|
export { candidatesLogPath, classifyOnboarding, detectPriorState, pendingDecisionsNudgeLine, } from "./prior-state.js";
|
|
5
5
|
export { computeSummary, emitOneliner, formatOneLiner, formatSummary, } from "./summary.js";
|
|
6
|
-
export { appendAuditEntry, previewWipRelief, subscriptionPreset, writeTriageScope, writeWipCap, } from "./writers.js";
|
|
6
|
+
export { appendAuditEntry, previewWipRelief, subscriptionPreset, writeTriageScope, writeWipCap, writeWipCapDecision, } from "./writers.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -7,7 +7,7 @@ import { DEFAULT_WIP_CAP, SUBSCRIPTION_PRESETS, TRIAGE_SKILL_PATH } from "./cons
|
|
|
7
7
|
import { formatWelcomeCommand } from "./default-mode.js";
|
|
8
8
|
import { detectPriorState } from "./prior-state.js";
|
|
9
9
|
import { emitOneliner } from "./summary.js";
|
|
10
|
-
import { previewWipRelief, subscriptionPreset, writeTriageScope, writeWipCap } from "./writers.js";
|
|
10
|
+
import { previewWipRelief, subscriptionPreset, writeTriageScope, writeWipCap, writeWipCapDecision, } from "./writers.js";
|
|
11
11
|
/** Default triage-scope preset applied when `--onboard` is run without `--preset`. */
|
|
12
12
|
export const DEFAULT_ONBOARD_PRESET = "small";
|
|
13
13
|
function failure(exitCode) {
|
|
@@ -66,8 +66,14 @@ export function runOnboardMode(projectRoot, options = {}) {
|
|
|
66
66
|
const [triageScopeChanged] = writeTriageScope(projectRoot, rules, { presetLabel: preset });
|
|
67
67
|
let wipCapChanged = false;
|
|
68
68
|
if (wipCap !== null) {
|
|
69
|
+
// writeWipCap also records out-of-band decision-provenance (#1694).
|
|
69
70
|
[wipCapChanged] = writeWipCap(projectRoot, wipCap);
|
|
70
71
|
}
|
|
72
|
+
else {
|
|
73
|
+
// Accepting the framework default is a real onboarding decision: record it
|
|
74
|
+
// without materializing plan.policy.wipCap (#1694 / #1186 D1 / #1250).
|
|
75
|
+
writeWipCapDecision(projectRoot, { acceptedDefault: true });
|
|
76
|
+
}
|
|
71
77
|
emitOneliner(projectRoot, {
|
|
72
78
|
writeHistory: options.writeHistory !== false,
|
|
73
79
|
now: options.now,
|
|
@@ -3,7 +3,14 @@ export interface PriorState {
|
|
|
3
3
|
readonly triageScopeSummary: string;
|
|
4
4
|
readonly cacheEmpty: boolean;
|
|
5
5
|
readonly cacheEntryCount: number;
|
|
6
|
+
/** True when plan.policy.wipCap is materialized (deliberate non-default override). */
|
|
6
7
|
readonly wipCapSet: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* True when an out-of-band decision-provenance marker records that the
|
|
10
|
+
* operator considered WIP cap (#1694). Orthogonal to {@link wipCapSet}:
|
|
11
|
+
* accepting the framework default sets this without materializing the field.
|
|
12
|
+
*/
|
|
13
|
+
readonly wipCapDecided: boolean;
|
|
7
14
|
readonly wipCap: number;
|
|
8
15
|
readonly wipCount: number;
|
|
9
16
|
readonly auditLogPresent: boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { hasArtifactSuffix, resolveAuditPath, resolveLifecycleRoot, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
|
|
4
|
-
import { readPlanPolicy } from "../../policy/plan-extensions.js";
|
|
4
|
+
import { readPlanOnboarding, readPlanPolicy } from "../../policy/plan-extensions.js";
|
|
5
5
|
import { resolveCandidatesLogPath } from "../cache-path.js";
|
|
6
6
|
import { CACHE_DIR_NAME, CACHE_SOURCE, DEFAULT_WIP_CAP, SUBSCRIPTION_PRESETS, WIP_LIFECYCLE_DIRS, } from "./constants.js";
|
|
7
7
|
function loadProjectDefinition(projectRoot) {
|
|
@@ -152,6 +152,17 @@ export function detectPriorState(projectRoot) {
|
|
|
152
152
|
wipCapSet = true;
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
+
// Decision-provenance is out-of-band from the value field (#1694 / #1695).
|
|
156
|
+
// Accepting the framework default records wipCapDecided without materializing
|
|
157
|
+
// plan.policy.wipCap. A materialized override also counts as a decision.
|
|
158
|
+
const onboarding = readPlanOnboarding(plan);
|
|
159
|
+
let wipCapDecided = false;
|
|
160
|
+
if (typeof onboarding === "object" && onboarding !== null && !Array.isArray(onboarding)) {
|
|
161
|
+
wipCapDecided = onboarding.wipCapDecided === true;
|
|
162
|
+
}
|
|
163
|
+
if (wipCapSet) {
|
|
164
|
+
wipCapDecided = true;
|
|
165
|
+
}
|
|
155
166
|
const [scopeSet, scopeLabel] = summarizeScope(scopeRules);
|
|
156
167
|
const cacheCount = countCacheEntries(projectRoot);
|
|
157
168
|
return {
|
|
@@ -160,6 +171,7 @@ export function detectPriorState(projectRoot) {
|
|
|
160
171
|
cacheEmpty: cacheCount === 0,
|
|
161
172
|
cacheEntryCount: cacheCount,
|
|
162
173
|
wipCapSet,
|
|
174
|
+
wipCapDecided,
|
|
163
175
|
wipCap,
|
|
164
176
|
wipCount: countWip(projectRoot),
|
|
165
177
|
auditLogPresent: existsSync(candidatesLogPath(projectRoot)),
|
|
@@ -167,10 +179,12 @@ export function detectPriorState(projectRoot) {
|
|
|
167
179
|
};
|
|
168
180
|
}
|
|
169
181
|
export function classifyOnboarding(state) {
|
|
182
|
+
// wipCap onboarding completeness is decision-provenance, not field presence
|
|
183
|
+
// (#1694). Omission of plan.policy.wipCap with wipCapDecided=true is fully set up.
|
|
170
184
|
const signals = {
|
|
171
185
|
"candidates.jsonl": state.auditLogPresent,
|
|
172
186
|
triageScope: state.triageScopeSet,
|
|
173
|
-
wipCap: state.
|
|
187
|
+
wipCap: state.wipCapDecided,
|
|
174
188
|
};
|
|
175
189
|
const present = Object.keys(signals).filter((k) => signals[k]);
|
|
176
190
|
const missing = Object.keys(signals).filter((k) => !signals[k]);
|
|
@@ -3,6 +3,16 @@ export declare function writeTriageScope(projectRoot: string, rules: Array<Recor
|
|
|
3
3
|
presetLabel: string;
|
|
4
4
|
actor?: string;
|
|
5
5
|
}): [boolean, string];
|
|
6
|
+
/**
|
|
7
|
+
* Record out-of-band WIP-cap decision provenance without materializing
|
|
8
|
+
* `plan.policy.wipCap` (#1694). Accepting the framework default is a real
|
|
9
|
+
* onboarding decision and MUST be representable without writing the value field.
|
|
10
|
+
*/
|
|
11
|
+
export declare function writeWipCapDecision(projectRoot: string, options?: {
|
|
12
|
+
actor?: string;
|
|
13
|
+
acceptedDefault?: boolean;
|
|
14
|
+
value?: number | null;
|
|
15
|
+
}): [boolean, string];
|
|
6
16
|
export declare function writeWipCap(projectRoot: string, wipCap: number, options?: {
|
|
7
17
|
actor?: string;
|
|
8
18
|
}): [boolean, string];
|
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, s
|
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { containedWrite } from "../../fs/contained-write.js";
|
|
4
4
|
import { hasArtifactSuffix, resolveLifecycleFolder, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
|
|
5
|
-
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY } from "../../policy/plan-extensions.js";
|
|
5
|
+
import { migrateLegacyPolicyKey, PLAN_ONBOARDING_KEY, PLAN_POLICY_KEY, } from "../../policy/plan-extensions.js";
|
|
6
6
|
import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
|
|
7
7
|
import { AUDIT_LOG_REL_PATH, DEFAULT_RELIEF_AGE_DAYS, DEFAULT_WIP_CAP, SUBSCRIPTION_PRESETS, WELCOME_AUDIT_TAG, } from "./constants.js";
|
|
8
8
|
function projectDefinitionPath(projectRoot) {
|
|
@@ -97,6 +97,57 @@ export function writeTriageScope(projectRoot, rules, options = { presetLabel: "c
|
|
|
97
97
|
return [changed, auditEntry];
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Record out-of-band WIP-cap decision provenance without materializing
|
|
102
|
+
* `plan.policy.wipCap` (#1694). Accepting the framework default is a real
|
|
103
|
+
* onboarding decision and MUST be representable without writing the value field.
|
|
104
|
+
*/
|
|
105
|
+
export function writeWipCapDecision(projectRoot, options = {}) {
|
|
106
|
+
return projectDefinitionMutationLock(projectRoot, () => {
|
|
107
|
+
const path = projectDefinitionPath(projectRoot);
|
|
108
|
+
if (!existsSync(path))
|
|
109
|
+
throw new Error(`PROJECT-DEFINITION not found at ${path}`);
|
|
110
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
111
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
112
|
+
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
113
|
+
}
|
|
114
|
+
const data = parsed;
|
|
115
|
+
const plan = data.plan;
|
|
116
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
117
|
+
throw new Error("PROJECT-DEFINITION 'plan' is not an object");
|
|
118
|
+
}
|
|
119
|
+
const planRec = plan;
|
|
120
|
+
const previous = planRec[PLAN_ONBOARDING_KEY];
|
|
121
|
+
const previousRec = typeof previous === "object" && previous !== null && !Array.isArray(previous)
|
|
122
|
+
? previous
|
|
123
|
+
: {};
|
|
124
|
+
const alreadyDecided = previousRec.wipCapDecided === true;
|
|
125
|
+
const acceptedDefault = options.acceptedDefault ?? true;
|
|
126
|
+
const actor = options.actor ?? WELCOME_AUDIT_TAG;
|
|
127
|
+
// Rebuild cleanly so acceptedDefault=true never retains a prior custom value.
|
|
128
|
+
const next = {
|
|
129
|
+
wipCapDecided: true,
|
|
130
|
+
acceptedDefault,
|
|
131
|
+
decidedAt: utcIso(),
|
|
132
|
+
actor,
|
|
133
|
+
};
|
|
134
|
+
if (!acceptedDefault && options.value !== undefined && options.value !== null) {
|
|
135
|
+
next.value = options.value;
|
|
136
|
+
}
|
|
137
|
+
planRec[PLAN_ONBOARDING_KEY] = next;
|
|
138
|
+
atomicWrite(projectRoot, path, data);
|
|
139
|
+
const changed = !alreadyDecided || previousRec.acceptedDefault !== acceptedDefault;
|
|
140
|
+
const auditEntry = [
|
|
141
|
+
`actor=${actor}`,
|
|
142
|
+
`field=${PLAN_ONBOARDING_KEY}.wipCapDecided`,
|
|
143
|
+
"value=true",
|
|
144
|
+
`acceptedDefault=${acceptedDefault ? "true" : "false"}`,
|
|
145
|
+
`changed=${changed ? "true" : "false"}`,
|
|
146
|
+
].join(" ");
|
|
147
|
+
appendAuditEntry(projectRoot, auditEntry);
|
|
148
|
+
return [changed, auditEntry];
|
|
149
|
+
});
|
|
150
|
+
}
|
|
100
151
|
export function writeWipCap(projectRoot, wipCap, options = {}) {
|
|
101
152
|
if (!Number.isInteger(wipCap) || wipCap < 1) {
|
|
102
153
|
throw new Error(`wipCap must be a positive int, got ${JSON.stringify(wipCap)}`);
|
|
@@ -125,8 +176,27 @@ export function writeWipCap(projectRoot, wipCap, options = {}) {
|
|
|
125
176
|
const policyRec = policy;
|
|
126
177
|
const previous = policyRec.wipCap;
|
|
127
178
|
const actor = options.actor ?? WELCOME_AUDIT_TAG;
|
|
179
|
+
// Always record decision-provenance out-of-band (#1694). Accepting the
|
|
180
|
+
// default omits the value field but still marks the decision complete.
|
|
181
|
+
// Rebuild the onboarding record (do not spread a prior custom `value` into
|
|
182
|
+
// an acceptedDefault=true payload -- that is contradictory provenance).
|
|
183
|
+
const acceptedDefault = wipCap === DEFAULT_WIP_CAP;
|
|
184
|
+
const onboardingRecord = {
|
|
185
|
+
wipCapDecided: true,
|
|
186
|
+
acceptedDefault,
|
|
187
|
+
decidedAt: utcIso(),
|
|
188
|
+
actor,
|
|
189
|
+
};
|
|
190
|
+
if (!acceptedDefault) {
|
|
191
|
+
onboardingRecord.value = wipCap;
|
|
192
|
+
}
|
|
193
|
+
planRec[PLAN_ONBOARDING_KEY] = onboardingRecord;
|
|
128
194
|
if (previous === undefined && wipCap === DEFAULT_WIP_CAP) {
|
|
129
|
-
|
|
195
|
+
atomicWrite(projectRoot, path, data);
|
|
196
|
+
const auditEntry = `actor=${actor} field=${PLAN_ONBOARDING_KEY}.wipCapDecided value=true ` +
|
|
197
|
+
`acceptedDefault=true changed=true note=default-no-materialize`;
|
|
198
|
+
appendAuditEntry(projectRoot, auditEntry);
|
|
199
|
+
return [true, auditEntry];
|
|
130
200
|
}
|
|
131
201
|
if (previous !== undefined && wipCap === DEFAULT_WIP_CAP) {
|
|
132
202
|
delete policyRec.wipCap;
|
|
@@ -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
|