@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
|
@@ -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
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
/** Canonical Experimental Rules entries (setup Phase 1 steps 5a–5c). */
|
|
13
|
+
export const EXPERIMENTAL_META_ENTRIES = [
|
|
14
|
+
{
|
|
15
|
+
id: "soul",
|
|
16
|
+
path: "meta/SOUL.md",
|
|
17
|
+
line: "- ! Use meta/SOUL.md for strategic context and purpose-driven guidance",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "morals",
|
|
21
|
+
path: "meta/morals.md",
|
|
22
|
+
line: "- ! Use meta/morals.md for ethical AI development principles",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "code-field",
|
|
26
|
+
path: "meta/code-field.md",
|
|
27
|
+
line: "- ~ Use meta/code-field.md for advanced architecture patterns",
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
const SECTION_HEADING = "## Experimental Rules";
|
|
31
|
+
function detectNewline(text) {
|
|
32
|
+
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
33
|
+
}
|
|
34
|
+
function lineMentionsPath(line, path) {
|
|
35
|
+
return line.includes(path);
|
|
36
|
+
}
|
|
37
|
+
function findExperimentalSection(text) {
|
|
38
|
+
const re = /^## Experimental Rules[ \t]*\r?\n?/m;
|
|
39
|
+
const match = re.exec(text);
|
|
40
|
+
if (!match || match.index === undefined) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const start = match.index;
|
|
44
|
+
const afterHeading = start + match[0].length;
|
|
45
|
+
const rest = text.slice(afterHeading);
|
|
46
|
+
// Next H2 or a horizontal rule at line start ends the section.
|
|
47
|
+
const next = /^(## |---[ \t]*$)/m.exec(rest);
|
|
48
|
+
const end = next && next.index !== undefined ? afterHeading + next.index : text.length;
|
|
49
|
+
return {
|
|
50
|
+
start,
|
|
51
|
+
end,
|
|
52
|
+
full: text.slice(start, end),
|
|
53
|
+
body: text.slice(afterHeading, end),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parse on/off state for the three experimental meta entries.
|
|
58
|
+
* A path is ON only when a line under `## Experimental Rules` mentions it —
|
|
59
|
+
* mentions in Personal/Defaults/prose do not count (writes are section-scoped).
|
|
60
|
+
*/
|
|
61
|
+
export function parseExperimentalRulesState(userMdText) {
|
|
62
|
+
const state = {
|
|
63
|
+
soul: false,
|
|
64
|
+
morals: false,
|
|
65
|
+
"code-field": false,
|
|
66
|
+
};
|
|
67
|
+
const section = findExperimentalSection(userMdText);
|
|
68
|
+
if (!section) {
|
|
69
|
+
return state;
|
|
70
|
+
}
|
|
71
|
+
for (const line of section.body.split(/\r?\n/)) {
|
|
72
|
+
for (const entry of EXPERIMENTAL_META_ENTRIES) {
|
|
73
|
+
if (lineMentionsPath(line, entry.path)) {
|
|
74
|
+
state[entry.id] = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return state;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build section body lines: preserve non-meta custom bullets; apply desired
|
|
82
|
+
* on/off for the three canonical paths; stable order soul → morals → code-field.
|
|
83
|
+
*/
|
|
84
|
+
function buildSectionBody(existingBody, desired, nl) {
|
|
85
|
+
const rawLines = existingBody.split(/\r?\n/);
|
|
86
|
+
const custom = [];
|
|
87
|
+
for (const line of rawLines) {
|
|
88
|
+
if (line.trim() === "") {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const isMeta = EXPERIMENTAL_META_ENTRIES.some((e) => lineMentionsPath(line, e.path));
|
|
92
|
+
if (!isMeta) {
|
|
93
|
+
custom.push(line);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const metaLines = EXPERIMENTAL_META_ENTRIES.filter((e) => desired[e.id]).map((e) => e.line);
|
|
97
|
+
const bullets = [...metaLines, ...custom];
|
|
98
|
+
if (bullets.length === 0) {
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
// Heading + blank line + bullets + trailing blank line for clean separation.
|
|
102
|
+
return `${SECTION_HEADING}${nl}${nl}${bullets.join(nl)}${nl}`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Insert a new Experimental Rules section before the trailing `---` / Note
|
|
106
|
+
* block when present; otherwise append after the last non-empty content.
|
|
107
|
+
*/
|
|
108
|
+
function insertSection(text, section, nl) {
|
|
109
|
+
// Prefer before the trailing horizontal rule that precedes the USER.md Note
|
|
110
|
+
// block (allow blank lines between `---` and `**Note**`).
|
|
111
|
+
const noteRe = /\r?\n---[ \t]*\r?\n(?:[ \t]*\r?\n)*[ \t]*\*\*Note\*\*/;
|
|
112
|
+
const noteMatch = noteRe.exec(text);
|
|
113
|
+
if (noteMatch && noteMatch.index !== undefined) {
|
|
114
|
+
const before = text.slice(0, noteMatch.index).replace(/[ \t]+$/u, "");
|
|
115
|
+
const after = text.slice(noteMatch.index);
|
|
116
|
+
const sep = before.endsWith(nl) ? nl : `${nl}${nl}`;
|
|
117
|
+
return `${before}${sep}${section.replace(/\s+$/u, "")}${after}`;
|
|
118
|
+
}
|
|
119
|
+
const trimmed = text.replace(/\s+$/u, "");
|
|
120
|
+
return `${trimmed}${nl}${nl}${section.replace(/\s+$/u, "")}${nl}`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Apply desired Experimental Rules on/off state.
|
|
124
|
+
*
|
|
125
|
+
* - Only mutates the `## Experimental Rules` region (or inserts/removes it).
|
|
126
|
+
* - Personal / Defaults content is left byte-identical outside that region.
|
|
127
|
+
* - Enabling uses the canonical setup-skill lines; disabling drops path matches.
|
|
128
|
+
* - Custom non-meta bullets under the section are preserved.
|
|
129
|
+
* - When all three are off and no custom bullets remain, the section is removed.
|
|
130
|
+
*/
|
|
131
|
+
export function applyExperimentalRulesState(userMdText, desired) {
|
|
132
|
+
const nl = detectNewline(userMdText);
|
|
133
|
+
const sectionText = buildSectionBody(findExperimentalSection(userMdText)?.body ?? "", desired, nl);
|
|
134
|
+
const existing = findExperimentalSection(userMdText);
|
|
135
|
+
if (existing) {
|
|
136
|
+
if (!sectionText) {
|
|
137
|
+
// Remove section; tidy surrounding blank lines.
|
|
138
|
+
const before = userMdText.slice(0, existing.start).replace(/[ \t]+$/u, "");
|
|
139
|
+
let after = userMdText.slice(existing.end);
|
|
140
|
+
// Collapse to at most two newlines at the join.
|
|
141
|
+
const beforeCore = before.replace(/(\r?\n){2,}$/u, nl);
|
|
142
|
+
after = after.replace(/^(\r?\n)+/u, nl);
|
|
143
|
+
if (after.startsWith("---") || after.startsWith("## ")) {
|
|
144
|
+
return `${beforeCore.replace(/(\r?\n)+$/u, nl)}${nl}${after}`;
|
|
145
|
+
}
|
|
146
|
+
return `${beforeCore}${after}`;
|
|
147
|
+
}
|
|
148
|
+
return userMdText.slice(0, existing.start) + sectionText + userMdText.slice(existing.end);
|
|
149
|
+
}
|
|
150
|
+
if (!sectionText) {
|
|
151
|
+
return userMdText;
|
|
152
|
+
}
|
|
153
|
+
return insertSection(userMdText, sectionText, nl);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Toggle a single experimental meta entry; leave the other two unchanged.
|
|
157
|
+
*/
|
|
158
|
+
export function setExperimentalRule(userMdText, id, enabled) {
|
|
159
|
+
const current = parseExperimentalRulesState(userMdText);
|
|
160
|
+
return applyExperimentalRulesState(userMdText, { ...current, [id]: enabled });
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=experimental-rules.js.map
|
|
@@ -59,7 +59,7 @@ export const CONTAINED_WRITES_ALLOWLIST = [
|
|
|
59
59
|
"packages/core/src/intake/issue-ingest.ts",
|
|
60
60
|
"packages/core/src/intake/reconcile-issues.ts",
|
|
61
61
|
"packages/core/src/issue-sync/sync-from-xbrief.ts",
|
|
62
|
-
|
|
62
|
+
// probe-session.ts removed from allowlist after #3042 contained writeSession.
|
|
63
63
|
"packages/core/src/orchestration/verify-judgment-gates.ts",
|
|
64
64
|
"packages/core/src/platform/changelog-cli.ts",
|
|
65
65
|
"packages/core/src/release/gh.ts",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.92.0",
|
|
4
4
|
"description": "TypeScript engine core for the Directive framework.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -334,8 +334,8 @@
|
|
|
334
334
|
"provenance": true
|
|
335
335
|
},
|
|
336
336
|
"dependencies": {
|
|
337
|
-
"@deftai/directive-content": "^0.
|
|
338
|
-
"@deftai/directive-types": "^0.
|
|
337
|
+
"@deftai/directive-content": "^0.92.0",
|
|
338
|
+
"@deftai/directive-types": "^0.92.0",
|
|
339
339
|
"archiver": "^8.0.0"
|
|
340
340
|
},
|
|
341
341
|
"scripts": {
|