@deftai/directive-core 0.106.0 → 0.107.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/check/gate-lists.js +2 -0
  2. package/dist/design-critique/exclusive-chip.d.ts +33 -0
  3. package/dist/design-critique/exclusive-chip.js +68 -0
  4. package/dist/design-critique/parent-audit.d.ts +59 -0
  5. package/dist/design-critique/parent-audit.js +121 -0
  6. package/dist/doctor/checks.d.ts +6 -0
  7. package/dist/doctor/checks.js +35 -0
  8. package/dist/hooks/dest-form.d.ts +46 -0
  9. package/dist/hooks/dest-form.js +575 -0
  10. package/dist/hooks/dispatcher.d.ts +17 -1
  11. package/dist/hooks/dispatcher.js +82 -11
  12. package/dist/hooks/index.d.ts +1 -0
  13. package/dist/hooks/index.js +1 -0
  14. package/dist/intake/github-auth-modes-cli.js +14 -2
  15. package/dist/intake/github-auth-modes.d.ts +54 -9
  16. package/dist/intake/github-auth-modes.js +326 -123
  17. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
  18. package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
  19. package/dist/lifecycle/completed-write-guard.d.ts +48 -0
  20. package/dist/lifecycle/completed-write-guard.js +373 -0
  21. package/dist/lifecycle/index.d.ts +1 -0
  22. package/dist/lifecycle/index.js +1 -0
  23. package/dist/literal-acceptance/capture.d.ts +16 -0
  24. package/dist/literal-acceptance/capture.js +75 -7
  25. package/dist/literal-acceptance/evaluate.js +25 -3
  26. package/dist/literal-acceptance/index.d.ts +2 -2
  27. package/dist/literal-acceptance/index.js +2 -2
  28. package/dist/literal-acceptance/run.js +9 -3
  29. package/dist/literal-acceptance/types.d.ts +6 -0
  30. package/dist/literal-acceptance/types.js +6 -0
  31. package/dist/policy/runtime-authority.d.ts +18 -0
  32. package/dist/policy/runtime-authority.js +11 -0
  33. package/dist/policy/write-fence.js +3 -0
  34. package/dist/preflight-cache/evaluate.js +9 -3
  35. package/dist/product-first-done-gate/evaluate.js +36 -5
  36. package/dist/scm/design-critique-chip.d.ts +37 -0
  37. package/dist/scm/design-critique-chip.js +171 -0
  38. package/dist/scm/index.d.ts +1 -0
  39. package/dist/scm/index.js +1 -0
  40. package/dist/scm/main.d.ts +3 -0
  41. package/dist/scm/main.js +15 -1
  42. package/dist/scm/readiness-cli.d.ts +2 -0
  43. package/dist/scm/readiness-cli.js +42 -1
  44. package/dist/scm/readiness.d.ts +2 -1
  45. package/dist/scm/readiness.js +7 -1
  46. package/dist/scope/decompose.js +2 -1
  47. package/dist/scope/delivery-evidence.d.ts +27 -1
  48. package/dist/scope/index.d.ts +1 -0
  49. package/dist/scope/index.js +1 -0
  50. package/dist/scope/lifecycle-write.d.ts +24 -0
  51. package/dist/scope/lifecycle-write.js +64 -0
  52. package/dist/scope/main.js +3 -1
  53. package/dist/scope/transition.js +90 -11
  54. package/dist/scope-provenance/evaluate.js +5 -1
  55. package/dist/session/git.d.ts +18 -0
  56. package/dist/session/git.js +81 -1
  57. package/dist/session/occupancy.d.ts +2 -1
  58. package/dist/session/occupancy.js +11 -4
  59. package/dist/session/session-start.d.ts +12 -0
  60. package/dist/session/session-start.js +17 -0
  61. package/dist/session/verify-session-ritual.d.ts +22 -3
  62. package/dist/session/verify-session-ritual.js +29 -4
  63. package/dist/swarm/launch.d.ts +69 -0
  64. package/dist/swarm/launch.js +191 -1
  65. package/dist/triage/actions/index.d.ts +4 -0
  66. package/dist/triage/actions/index.js +6 -0
  67. package/dist/triage/evaluate/evaluate.d.ts +11 -0
  68. package/dist/triage/evaluate/evaluate.js +169 -0
  69. package/dist/triage/evaluate/github.d.ts +16 -0
  70. package/dist/triage/evaluate/github.js +153 -0
  71. package/dist/triage/evaluate/index.d.ts +9 -0
  72. package/dist/triage/evaluate/index.js +8 -0
  73. package/dist/triage/evaluate/paths.d.ts +7 -0
  74. package/dist/triage/evaluate/paths.js +25 -0
  75. package/dist/triage/evaluate/sink.d.ts +5 -0
  76. package/dist/triage/evaluate/sink.js +52 -0
  77. package/dist/triage/evaluate/types.d.ts +93 -0
  78. package/dist/triage/evaluate/types.js +13 -0
  79. package/dist/triage/evaluate/validity.d.ts +8 -0
  80. package/dist/triage/evaluate/validity.js +87 -0
  81. package/dist/triage/evaluate/value.d.ts +12 -0
  82. package/dist/triage/evaluate/value.js +33 -0
  83. package/dist/triage/evaluate/wip-census.d.ts +5 -0
  84. package/dist/triage/evaluate/wip-census.js +39 -0
  85. package/dist/triage/evaluate/worktrees.d.ts +8 -0
  86. package/dist/triage/evaluate/worktrees.js +68 -0
  87. package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
  88. package/dist/triage/evaluate/xbrief-refs.js +60 -0
  89. package/dist/triage/help/registry-data.d.ts +21 -7
  90. package/dist/triage/help/registry-data.js +53 -5
  91. package/dist/triage/index.d.ts +1 -0
  92. package/dist/triage/index.js +1 -0
  93. package/dist/value/feedback-file.d.ts +17 -2
  94. package/dist/value/feedback-file.js +80 -6
  95. package/dist/vbrief-reconcile/labels.d.ts +1 -0
  96. package/dist/vbrief-reconcile/labels.js +30 -0
  97. package/dist/vbrief-validation/story-quality.d.ts +7 -0
  98. package/dist/vbrief-validation/story-quality.js +8 -1
  99. package/dist/xbrief-migrate/agents-header.js +69 -7
  100. package/package.json +3 -3
@@ -17,6 +17,7 @@ import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, resolveL
17
17
  import { resolveRepo } from "../triage/queue/repo.js";
18
18
  import { referenceWithDefaultTrust, slugify } from "../vbrief-build/build.js";
19
19
  import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
20
+ import { READY_REQUIRES_PARALLEL_SAFE } from "../vbrief-validation/story-quality.js";
20
21
  import { formatCoverageReportLine, validateCoverageMap } from "./coverage-map.js";
21
22
  import { buildParentLineageArtifact } from "./parent-lineage.js";
22
23
  import { formatBriefJson } from "./vbrief-json.js";
@@ -407,7 +408,7 @@ export function storyQualityIssues(opts) {
407
408
  issues.push(...fileScopeIssues(swarm));
408
409
  issues.push(...verifyCommandIssues(swarm));
409
410
  if (swarm.parallel_safe === false) {
410
- issues.push("readiness=ready requires parallel_safe=true; use readiness=sequential or needs_refinement for non-concurrent work");
411
+ issues.push(READY_REQUIRES_PARALLEL_SAFE);
411
412
  }
412
413
  if (swarm.file_scope_confidence === "low") {
413
414
  issues.push("readiness=ready requires file_scope_confidence above low");
@@ -16,6 +16,29 @@ export declare const NON_DELIVERY_DISPOSITIONS: readonly ["cancelled", "supersed
16
16
  export type NonDeliveryDisposition = (typeof NON_DELIVERY_DISPOSITIONS)[number];
17
17
  /** Delivery verification outcome for completion provenance. */
18
18
  export type DeliveryDisposition = "delivered" | "merged_to_integration" | "not_delivered" | NonDeliveryDisposition | "unknown" | "unverified";
19
+ /**
20
+ * This is a historical audit snapshot stamped onto
21
+ * `plan.metadata.completionProvenance` at `scope:complete` (#3041 / #3690).
22
+ *
23
+ * Completed xBRIEFs have full standing as a record of *what is* and zero
24
+ * authority over *what to build next* (#3383). Fields stay because they
25
+ * answer a later reconstruction question after branches are deleted and
26
+ * refs move. A field does not need a current production-code reader.
27
+ *
28
+ * Reconstruction groups:
29
+ * - Identity and location: `repository`, `implementationCommit`, `prNumber`,
30
+ * `prBase`, `deliveryBranch`
31
+ * - Delivery evidence: `mergeCommit`, `deliveryCommit`, `disposition`,
32
+ * `handoffState`
33
+ * - Verification attribution: `verifiedAt`, `verifier`
34
+ * - Explicit operator-supplied facts: `deployed`, `uatVerified` (never
35
+ * inferred from Git)
36
+ *
37
+ * `completedSessionId` is duplicated. This nested copy is stamped so the
38
+ * snapshot is self-contained. Consumers (`verify:ac` /
39
+ * `session-completed-ac`) read the sibling
40
+ * `plan.metadata.completedSessionId`, not this field.
41
+ */
19
42
  export interface CompletionProvenance {
20
43
  readonly repository: string | null;
21
44
  readonly implementationCommit: string | null;
@@ -32,7 +55,10 @@ export interface CompletionProvenance {
32
55
  readonly deployed: boolean | null;
33
56
  /** Always null unless explicitly supplied — never inferred from Git (#3041). */
34
57
  readonly uatVerified: boolean | null;
35
- /** Session that completed the brief; used by check to target xbrief/completed (#3357). */
58
+ /**
59
+ * Nested session correlation for a self-contained snapshot.
60
+ * Consumers read sibling `plan.metadata.completedSessionId`, not this field.
61
+ */
36
62
  readonly completedSessionId?: string | null;
37
63
  }
38
64
  export interface DeliveryEvidenceInput {
@@ -8,6 +8,7 @@ export * from "./decomposed-refs.js";
8
8
  export * from "./delivery-evidence.js";
9
9
  export * from "./demote.js";
10
10
  export * from "./effort-activate-gate.js";
11
+ export * from "./lifecycle-write.js";
11
12
  export * from "./main.js";
12
13
  export * from "./open-umbrella-warning.js";
13
14
  export * from "./parent-lineage.js";
@@ -8,6 +8,7 @@ export * from "./decomposed-refs.js";
8
8
  export * from "./delivery-evidence.js";
9
9
  export * from "./demote.js";
10
10
  export * from "./effort-activate-gate.js";
11
+ export * from "./lifecycle-write.js";
11
12
  export * from "./main.js";
12
13
  export * from "./open-umbrella-warning.js";
13
14
  export * from "./parent-lineage.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Transition-write fingerprint for completed/ artifacts (#3679).
3
+ *
4
+ * Distinct from completionProvenance (#3686): this only records that
5
+ * runTransition wrote the blob. Non-code-bearing completes and scope:fail
6
+ * stamp this and still omit delivery provenance.
7
+ */
8
+ export declare const LIFECYCLE_WRITE_KEY: "lifecycleWrite";
9
+ export type LifecycleWriteAction = "complete" | "fail";
10
+ export interface LifecycleWriteStamp {
11
+ readonly action: LifecycleWriteAction;
12
+ readonly writtenAt: string;
13
+ }
14
+ /** Worker-facing leftover-land path after a scope-provenance strip (#3476 / #3679). */
15
+ export declare const LEFTOVER_LAND_PR_REMEDIATION: string;
16
+ /** Stamp that runTransition wrote this plan into completed/. */
17
+ export declare function stampLifecycleWrite(plan: Record<string, unknown>, action: LifecycleWriteAction, writtenAt: string): void;
18
+ /**
19
+ * True when a completed/ blob shows verb evidence.
20
+ * Accepts the new stamp, legacy completedAt (pre-#3679 complete), or failed status
21
+ * (scope:fail never stamped provenance and must keep passing).
22
+ */
23
+ export declare function hasTransitionWrite(plan: Record<string, unknown>): boolean;
24
+ //# sourceMappingURL=lifecycle-write.d.ts.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Transition-write fingerprint for completed/ artifacts (#3679).
3
+ *
4
+ * Distinct from completionProvenance (#3686): this only records that
5
+ * runTransition wrote the blob. Non-code-bearing completes and scope:fail
6
+ * stamp this and still omit delivery provenance.
7
+ */
8
+ export const LIFECYCLE_WRITE_KEY = "lifecycleWrite";
9
+ /** Worker-facing leftover-land path after a scope-provenance strip (#3476 / #3679). */
10
+ export const LEFTOVER_LAND_PR_REMEDIATION = "After a provenance-gated product PR strips active/ from the change set, " +
11
+ "the designed remainder is a leftover land PR (#3476): run `task scope:complete` " +
12
+ "after merge (it can stamp a brief already in completed/), then land that artifact. " +
13
+ "Do not git-add a completed/ husk to skip the verb.";
14
+ function asRecord(value) {
15
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
16
+ return null;
17
+ }
18
+ return value;
19
+ }
20
+ function metadataRecord(plan) {
21
+ const existing = asRecord(plan.metadata);
22
+ if (existing !== null) {
23
+ return existing;
24
+ }
25
+ const created = {};
26
+ plan.metadata = created;
27
+ return created;
28
+ }
29
+ /** Stamp that runTransition wrote this plan into completed/. */
30
+ export function stampLifecycleWrite(plan, action, writtenAt) {
31
+ const meta = metadataRecord(plan);
32
+ meta[LIFECYCLE_WRITE_KEY] = { action, writtenAt };
33
+ }
34
+ function hasLifecycleWriteStamp(plan) {
35
+ const meta = asRecord(plan.metadata);
36
+ if (meta === null) {
37
+ return false;
38
+ }
39
+ const stamp = asRecord(meta[LIFECYCLE_WRITE_KEY]);
40
+ if (stamp === null) {
41
+ return false;
42
+ }
43
+ const action = stamp.action;
44
+ const writtenAt = stamp.writtenAt;
45
+ return ((action === "complete" || action === "fail") &&
46
+ typeof writtenAt === "string" &&
47
+ writtenAt.trim().length > 0);
48
+ }
49
+ /**
50
+ * True when a completed/ blob shows verb evidence.
51
+ * Accepts the new stamp, legacy completedAt (pre-#3679 complete), or failed status
52
+ * (scope:fail never stamped provenance and must keep passing).
53
+ */
54
+ export function hasTransitionWrite(plan) {
55
+ if (hasLifecycleWriteStamp(plan)) {
56
+ return true;
57
+ }
58
+ const meta = asRecord(plan.metadata);
59
+ if (meta !== null && typeof meta.completedAt === "string" && meta.completedAt.trim().length > 0) {
60
+ return true;
61
+ }
62
+ return String(plan.status ?? "") === "failed";
63
+ }
64
+ //# sourceMappingURL=lifecycle-write.js.map
@@ -17,10 +17,12 @@ import { findByDecisionId, isAlreadyUndone, REVERSIBLE_ACTIONS, undoBatch, undoO
17
17
  const LIFECYCLE_USAGE_STDERR = "usage: deft scope:<action> [file] [--batch] [--from-issue N] [--repo OWNER/NAME]\n" +
18
18
  " [--force] [--project-root PATH] [--strict] [--force-no-cache]\n" +
19
19
  " [--path PATH]\n" +
20
+ " complete also accepts: --merge-commit SHA --pr N [--pr-base BRANCH] [--delivery-branch BRANCH]\n" +
20
21
  " actions: activate, block, cancel, complete, fail, promote, restore, unblock\n" +
21
22
  "The verb already encodes the action (e.g. deft scope:promote <file>). Do not pass the action again.\n" +
22
23
  "(promote --batch may omit file and promotes all proposed/ scopes; #3011)\n" +
23
- "(promote --from-issue=N may omit file; #1136)\n";
24
+ "(promote --from-issue=N may omit file; #1136)\n" +
25
+ "(complete --merge-commit / --pr are required for code-bearing delivery evidence; #3041 / #3721)\n";
24
26
  function parseLifecycleArgv(argv) {
25
27
  if (argv.length < 1) {
26
28
  return { args: null, error: "usage" };
@@ -16,6 +16,7 @@ import { LIFECYCLE_FOLDERS, MOVE_LABELS, STATUS_PRECONDITIONS, STAY_LABELS, TRAN
16
16
  import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecomposedParentBackReferences, } from "./decomposed-refs.js";
17
17
  import { classifyStoredDeliveryDisposition, evaluateDeliveryGate, resolveCompletionSessionId, stampDeliveryProvenance, } from "./delivery-evidence.js";
18
18
  import { evaluateEffortActivateGate } from "./effort-activate-gate.js";
19
+ import { stampLifecycleWrite } from "./lifecycle-write.js";
19
20
  import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
20
21
  import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
21
22
  import { utcNowIso } from "./vbrief-json.js";
@@ -81,7 +82,9 @@ export function runTransition(action, filePath, now = new Date(), options = {})
81
82
  };
82
83
  }
83
84
  const { allowedSources, targetFolder, targetStatus } = TRANSITIONS[act];
84
- if (!allowedSources.includes(currentFolder)) {
85
+ const restampCompleted = act === "complete" && currentFolder === "completed";
86
+ if (!restampCompleted &&
87
+ !allowedSources.includes(currentFolder)) {
85
88
  const allowedStr = allowedSources.map((s) => `${s}/`).join(", ");
86
89
  return {
87
90
  ok: false,
@@ -103,15 +106,9 @@ export function runTransition(action, filePath, now = new Date(), options = {})
103
106
  const requiredStatus = STATUS_PRECONDITIONS[act];
104
107
  if (requiredStatus !== undefined) {
105
108
  if (currentStatus === targetStatus) {
106
- // Surface legacy delivery disposition on already-completed briefs (#3041).
107
- let dispositionSuffix = "";
108
- if (act === "complete" && currentFolder === "completed") {
109
- const disposition = classifyStoredDeliveryDisposition(planObj);
110
- dispositionSuffix = ` (deliveryDisposition=${disposition})`;
111
- }
112
109
  return {
113
110
  ok: true,
114
- message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}/${dispositionSuffix}`,
111
+ message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}/`,
115
112
  };
116
113
  }
117
114
  if (currentStatus !== requiredStatus) {
@@ -122,14 +119,30 @@ export function runTransition(action, filePath, now = new Date(), options = {})
122
119
  };
123
120
  }
124
121
  }
122
+ const nowIso = utcNowIso(now);
123
+ const vbriefRoot = dirname(dirname(resolvedPath));
124
+ const projectRoot = dirname(vbriefRoot);
125
+ // #3679: lift #3041 already-completed handling out of STATUS_PRECONDITIONS
126
+ // so complete can stamp a brief already in completed/ without a folder trip.
127
+ if (restampCompleted) {
128
+ return restampCompletedBrief({
129
+ resolvedPath,
130
+ basename,
131
+ data,
132
+ planObj,
133
+ projectRoot,
134
+ vbriefRoot,
135
+ now,
136
+ nowIso,
137
+ options,
138
+ });
139
+ }
125
140
  if (targetFolder !== null && targetFolder === currentFolder) {
126
141
  return {
127
142
  ok: true,
128
143
  message: `No-op: ${basename} is already in ${currentFolder}/ (status: ${currentStatus})`,
129
144
  };
130
145
  }
131
- const vbriefRoot = dirname(dirname(resolvedPath));
132
- const projectRoot = dirname(vbriefRoot);
133
146
  if (targetFolder !== null) {
134
147
  const destDir = join(vbriefRoot, targetFolder);
135
148
  try {
@@ -144,7 +157,6 @@ export function runTransition(action, filePath, now = new Date(), options = {})
144
157
  throw err;
145
158
  }
146
159
  }
147
- const nowIso = utcNowIso(now);
148
160
  // #3360: hand-authored briefs run #3323 clause derivation on activate/promote.
149
161
  // #3355: stamp is state-observed (before vs after), not derivation-notice-bound.
150
162
  const previousAcceptance = planObj.acceptance;
@@ -190,6 +202,7 @@ export function runTransition(action, filePath, now = new Date(), options = {})
190
202
  ? { ...gate.provenance, completedSessionId: sessionId }
191
203
  : gate.provenance);
192
204
  }
205
+ stampLifecycleWrite(planObj, "complete", nowIso);
193
206
  }
194
207
  // #3240: per-criterion typed evidence or human-origin disposition before auto-advance.
195
208
  let acceptanceReports;
@@ -266,6 +279,9 @@ export function runTransition(action, filePath, now = new Date(), options = {})
266
279
  };
267
280
  }
268
281
  }
282
+ if (act === "fail") {
283
+ stampLifecycleWrite(planObj, "fail", nowIso);
284
+ }
269
285
  const formatted = formatBriefJson(data);
270
286
  const crud = new InstrumentedVbriefCrud({ now: () => now });
271
287
  if (targetFolder !== null) {
@@ -370,6 +386,69 @@ export function runTransition(action, filePath, now = new Date(), options = {})
370
386
  acceptanceReports,
371
387
  };
372
388
  }
389
+ /**
390
+ * Stamp a brief already in completed/ (#3679). Runs the #3041 delivery gate
391
+ * and writes in place so recovery does not need a folder round-trip.
392
+ * The #3041 deliveryDisposition suffix now actually executes for complete.
393
+ */
394
+ function restampCompletedBrief(args) {
395
+ const { resolvedPath, basename, data, planObj, projectRoot, vbriefRoot, now, nowIso, options } = args;
396
+ const gate = evaluateDeliveryGate({
397
+ projectRoot,
398
+ plan: planObj,
399
+ nowIso,
400
+ evidence: options.deliveryEvidence,
401
+ nonDeliveryDisposition: options.nonDeliveryDisposition,
402
+ runGit: options.runGit,
403
+ verifier: options.verifier ?? "scope:complete",
404
+ assumeEvidenceValidated: options.assumeEvidenceValidated,
405
+ });
406
+ if (!gate.ok) {
407
+ return { ok: false, message: gate.message };
408
+ }
409
+ if (gate.provenance !== null) {
410
+ const sessionId = resolveCompletionSessionId(projectRoot);
411
+ stampDeliveryProvenance(planObj, sessionId !== null ? { ...gate.provenance, completedSessionId: sessionId } : gate.provenance);
412
+ }
413
+ planObj.status = "completed";
414
+ planObj.updated = nowIso;
415
+ stampEnvelopeUpdated(data, nowIso);
416
+ stampCompletionMetadata(planObj, projectRoot, nowIso, {
417
+ completedSessionId: resolveCompletionSessionId(projectRoot),
418
+ });
419
+ stampLifecycleWrite(planObj, "complete", nowIso);
420
+ const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot, { projectRoot });
421
+ if (!writeResult.ok) {
422
+ return { ok: false, message: writeResult.message };
423
+ }
424
+ const crud = new InstrumentedVbriefCrud({ now: () => now });
425
+ crud.recordTrustedUpdate(resolvedPath, formatBriefJson(data));
426
+ try {
427
+ persistCrudMetrics(projectRoot, crud.getMetrics());
428
+ }
429
+ catch {
430
+ /* best-effort telemetry persistence */
431
+ }
432
+ const sessionId = resolveCompletionSessionId(projectRoot);
433
+ if (sessionId !== null) {
434
+ try {
435
+ writeSessionCompletedMarker(projectRoot, {
436
+ path: resolvedPath,
437
+ sessionId,
438
+ completedAt: nowIso,
439
+ });
440
+ }
441
+ catch {
442
+ return { ok: false, message: SESSION_COMPLETED_AC_REMEDIATION };
443
+ }
444
+ }
445
+ const disposition = classifyStoredDeliveryDisposition(planObj);
446
+ return {
447
+ ok: true,
448
+ message: `Restamped ${basename} in completed/ (status: completed, ` +
449
+ `deliveryDisposition=${disposition})`,
450
+ };
451
+ }
373
452
  export function recordWipCapOverride(filePath, projectRoot, check, now = new Date()) {
374
453
  try {
375
454
  const rel = resolve(filePath).startsWith(resolve(projectRoot))
@@ -209,7 +209,11 @@ function remediationForExpansion() {
209
209
  "`.deft/approved-scope/<plan-id>.json` with a humanApproval stamp). Commit that " +
210
210
  "approval on the merge base (or a prior PR) before expanding or activating the " +
211
211
  "scoped xBRIEF in the implementation change set. Editing the active xBRIEF alone " +
212
- "does not authorize new paths (#3145 / #3205). See content/docs/scope-provenance.md.");
212
+ "does not authorize new paths (#3145 / #3205). See content/docs/scope-provenance.md. " +
213
+ "If you cannot mint a digest (human TTY required), drop active/ from this product " +
214
+ "change set and land the completed artifact in a leftover land PR after merge (#3476). " +
215
+ "Run `task scope:complete` on the brief (it can stamp a file already in completed/). " +
216
+ "Do not git-add a completed/ husk to skip that PR (#3679).");
213
217
  }
214
218
  /**
215
219
  * Parse + lightly validate an approved-scope JSON blob (base-ref `git show` or disk).
@@ -12,5 +12,23 @@ export declare function gitHead(projectRoot: string, runGit?: GitRunner): {
12
12
  export declare function worktreePath(projectRoot: string, runGit?: GitRunner): string;
13
13
  /** True when `ancestor` is reachable from `descendant` (same commit counts). */
14
14
  export declare function gitIsAncestor(projectRoot: string, ancestor: string, descendant: string, runGit?: GitRunner): boolean | null;
15
+ /**
16
+ * Ceiling for one `git cat-file --batch` payload. The directive terminal
17
+ * corpus is ~8 MiB today; this leaves headroom without reintroducing
18
+ * per-blob `git show` on a truncated read.
19
+ */
20
+ export declare const GIT_CAT_FILE_BATCH_MAX_BUFFER: number;
21
+ /**
22
+ * Parse `git cat-file --batch` stdout for `paths.length` objects, in the
23
+ * same order the names were written to stdin. Returns null if the stream
24
+ * is truncated or a header is malformed so callers can fall back.
25
+ */
26
+ export declare function parseGitCatFileBatch(stdout: Buffer, paths: readonly string[]): Map<string, string | null> | null;
27
+ /**
28
+ * Read many `tip:path` blobs in one `git cat-file --batch` process.
29
+ * Falls back to per-path `git show` only when the batch stream cannot be
30
+ * parsed, so verdicts stay content-authoritative.
31
+ */
32
+ export declare function showBlobsBatch(projectRoot: string, tip: string, paths: readonly string[], runGit?: GitRunner): Map<string, string | null>;
15
33
  export declare function detectBranch(projectRoot: string, runGit?: GitRunner): string | null;
16
34
  //# sourceMappingURL=git.d.ts.map
@@ -1,4 +1,4 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
2
  import { resolve } from "node:path";
3
3
  function coerceGitBytes(value) {
4
4
  if (Buffer.isBuffer(value))
@@ -64,6 +64,86 @@ export function gitIsAncestor(projectRoot, ancestor, descendant, runGit = defaul
64
64
  }
65
65
  return null;
66
66
  }
67
+ /**
68
+ * Ceiling for one `git cat-file --batch` payload. The directive terminal
69
+ * corpus is ~8 MiB today; this leaves headroom without reintroducing
70
+ * per-blob `git show` on a truncated read.
71
+ */
72
+ export const GIT_CAT_FILE_BATCH_MAX_BUFFER = 64 * 1024 * 1024;
73
+ /**
74
+ * Parse `git cat-file --batch` stdout for `paths.length` objects, in the
75
+ * same order the names were written to stdin. Returns null if the stream
76
+ * is truncated or a header is malformed so callers can fall back.
77
+ */
78
+ export function parseGitCatFileBatch(stdout, paths) {
79
+ const out = new Map();
80
+ let offset = 0;
81
+ for (const path of paths) {
82
+ const nl = stdout.indexOf(0x0a, offset);
83
+ if (nl < 0) {
84
+ return null;
85
+ }
86
+ const header = stdout.subarray(offset, nl).toString("utf8");
87
+ offset = nl + 1;
88
+ if (header.endsWith(" missing") || header.endsWith(" ambiguous")) {
89
+ out.set(path, null);
90
+ continue;
91
+ }
92
+ const match = /^[0-9a-fA-F]+ \S+ (\d+)$/.exec(header);
93
+ if (match === null) {
94
+ return null;
95
+ }
96
+ const size = Number(match[1]);
97
+ if (!Number.isInteger(size) || size < 0 || offset + size > stdout.length) {
98
+ return null;
99
+ }
100
+ const content = stdout.subarray(offset, offset + size);
101
+ offset += size;
102
+ if (offset < stdout.length && stdout[offset] === 0x0a) {
103
+ offset += 1;
104
+ }
105
+ else if (offset !== stdout.length) {
106
+ return null;
107
+ }
108
+ out.set(path, content.toString("utf8"));
109
+ }
110
+ return out;
111
+ }
112
+ function showBlobViaRunner(projectRoot, tip, path, runGit) {
113
+ const result = runGit(projectRoot, ["show", `${tip}:${path}`]);
114
+ if (result.code !== 0) {
115
+ return null;
116
+ }
117
+ return result.stdout;
118
+ }
119
+ /**
120
+ * Read many `tip:path` blobs in one `git cat-file --batch` process.
121
+ * Falls back to per-path `git show` only when the batch stream cannot be
122
+ * parsed, so verdicts stay content-authoritative.
123
+ */
124
+ export function showBlobsBatch(projectRoot, tip, paths, runGit = defaultGitRunner) {
125
+ const out = new Map();
126
+ if (paths.length === 0) {
127
+ return out;
128
+ }
129
+ const input = Buffer.from(`${paths.map((path) => `${tip}:${path}`).join("\n")}\n`, "utf8");
130
+ const result = spawnSync("git", ["cat-file", "--batch"], {
131
+ cwd: projectRoot,
132
+ input,
133
+ maxBuffer: GIT_CAT_FILE_BATCH_MAX_BUFFER,
134
+ windowsHide: true,
135
+ });
136
+ if (result.error === undefined && result.status === 0 && result.stdout !== undefined) {
137
+ const parsed = parseGitCatFileBatch(coerceGitBytes(result.stdout), paths);
138
+ if (parsed !== null) {
139
+ return parsed;
140
+ }
141
+ }
142
+ for (const path of paths) {
143
+ out.set(path, showBlobViaRunner(projectRoot, tip, path, runGit));
144
+ }
145
+ return out;
146
+ }
67
147
  export function detectBranch(projectRoot, runGit = defaultGitRunner) {
68
148
  const sym = runGit(projectRoot, ["symbolic-ref", "--short", "HEAD"]);
69
149
  if (sym.code === 0 && sym.stdout.trim()) {
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Ritual-state is "this session completed ceremony." Occupancy is "who may
5
5
  * mutate this tree right now." Those lifetimes differ; do not overload
6
- * ritual-state.json. Join negotiation (`occupancy:request`) is out of scope.
6
+ * ritual-state.json. Ordinary end is occupancy:release / session:end (#3604).
7
+ * Join negotiation (`occupancy:request`) is out of scope.
7
8
  *
8
9
  * Concurrency model:
9
10
  * - Assumptions: local filesystem; cooperating processes on one machine.
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Ritual-state is "this session completed ceremony." Occupancy is "who may
5
5
  * mutate this tree right now." Those lifetimes differ; do not overload
6
- * ritual-state.json. Join negotiation (`occupancy:request`) is out of scope.
6
+ * ritual-state.json. Ordinary end is occupancy:release / session:end (#3604).
7
+ * Join negotiation (`occupancy:request`) is out of scope.
7
8
  *
8
9
  * Concurrency model:
9
10
  * - Assumptions: local filesystem; cooperating processes on one machine.
@@ -35,11 +36,16 @@ export function heartbeatAgeSeconds(record, now = new Date()) {
35
36
  export function isOccupancyExpired(record, now = new Date(), ttlMs = OCCUPANCY_TTL_MS) {
36
37
  return now.getTime() - record.heartbeatAt.getTime() > ttlMs;
37
38
  }
39
+ function occupancyClockLine(record) {
40
+ return `claimed_at=${timestampIso(record.claimedAt)} heartbeat_at=${timestampIso(record.heartbeatAt)}`;
41
+ }
38
42
  export function formatOccupancyRemediation(record, now = new Date()) {
39
43
  const age = heartbeatAgeSeconds(record, now);
40
- return (`Worktree occupied by session ${record.sessionId} (intent=${record.intent}, heartbeat ${age}s ago).\n` +
44
+ return (`Worktree occupied by session ${record.sessionId} (intent=${record.intent}, heartbeat ${age}s ago, ` +
45
+ `${occupancyClockLine(record)}).\n` +
41
46
  "Stay read-only (`session:start --read-only`), use another worktree,\n" +
42
- "queue a join (`occupancy:request`), or steal (`occupancy:steal --confirm`).");
47
+ "queue a join (`occupancy:request`), or steal (`occupancy:steal --confirm`).\n" +
48
+ "The occupant may release (`occupancy:release` / `session:end`).");
43
49
  }
44
50
  export function resolveOccupancySessionId(input = {}) {
45
51
  const explicit = input.sessionId?.trim();
@@ -194,6 +200,7 @@ export function stealOccupancy(projectRoot, input = {}) {
194
200
  };
195
201
  }
196
202
  const incoming = resolveOccupancySessionId(input);
203
+ const priorClock = existingLocked !== null ? ` (${occupancyClockLine(existingLocked)})` : "";
197
204
  const record = writeOccupancyRecord(projectRoot, {
198
205
  sessionId: incoming,
199
206
  worktreePath: resolve(projectRoot),
@@ -210,7 +217,7 @@ export function stealOccupancy(projectRoot, input = {}) {
210
217
  sessionId: record.sessionId,
211
218
  record,
212
219
  path,
213
- message: `occupancy stolen from ${named}; writer is now session ${record.sessionId}`,
220
+ message: `occupancy stolen from ${named}${priorClock}; writer is now session ${record.sessionId}`,
214
221
  code: 0,
215
222
  };
216
223
  }, input.lockDeps);
@@ -24,8 +24,20 @@ export type SessionCeremonyTier = (typeof SESSION_CEREMONY_TIERS)[number];
24
24
  export declare const COLD_CEREMONY_TIER: SessionCeremonyTier;
25
25
  export declare const REARM_CEREMONY_TIER: SessionCeremonyTier;
26
26
  export declare const QUICK_STEPS: readonly ["alignment", "branch_policy", "triage_welcome", "verify_tools"];
27
+ /** Session / full gated ritual set. Write dispatch uses a per-surface subset (#3738). */
27
28
  export declare const GATED_STEPS: readonly ["agent_hooks", "doctor", "cache_fresh"];
28
29
  export type GatedStepName = (typeof GATED_STEPS)[number];
30
+ /**
31
+ * Gated steps a write/spawn mutation must prove from recorded ritual state (#3738).
32
+ * `cache_fresh` is a work-selection precondition, not a write-authorization one.
33
+ */
34
+ export declare const WRITE_GATED_REQUIRED_STEPS: readonly ["agent_hooks", "doctor"];
35
+ /**
36
+ * Gated steps the write path may execute (#3738).
37
+ * Hook readiness is deliberately non-cacheable; doctor and cache_fresh stay
38
+ * session-surface only.
39
+ */
40
+ export declare const WRITE_GATED_EXECUTE_STEPS: readonly ["agent_hooks"];
29
41
  /** Env opt-in for optional session:start network (release probe + triage cache hydrate) (#2991). */
30
42
  export declare const ENV_SESSION_START_NETWORK = "DEFT_SESSION_START_NETWORK";
31
43
  /** Human-readable skip notice when optional network is off the hot path (#2991). */
@@ -50,7 +50,24 @@ export const QUICK_STEPS = [
50
50
  "triage_welcome",
51
51
  "verify_tools",
52
52
  ];
53
+ /** Session / full gated ritual set. Write dispatch uses a per-surface subset (#3738). */
53
54
  export const GATED_STEPS = ["agent_hooks", "doctor", "cache_fresh"];
55
+ /**
56
+ * Gated steps a write/spawn mutation must prove from recorded ritual state (#3738).
57
+ * `cache_fresh` is a work-selection precondition, not a write-authorization one.
58
+ */
59
+ export const WRITE_GATED_REQUIRED_STEPS = [
60
+ "agent_hooks",
61
+ "doctor",
62
+ ];
63
+ /**
64
+ * Gated steps the write path may execute (#3738).
65
+ * Hook readiness is deliberately non-cacheable; doctor and cache_fresh stay
66
+ * session-surface only.
67
+ */
68
+ export const WRITE_GATED_EXECUTE_STEPS = [
69
+ "agent_hooks",
70
+ ];
54
71
  /** Per-clone ignore/index hide of lifecycle roots — warn-only (#3505). */
55
72
  function pushLifecycleVisibleAdvisory(lines, projectRoot, options, runGit) {
56
73
  try {
@@ -1,8 +1,9 @@
1
1
  import { type ActiveCliCheckResult, type ActiveCliCheckSeams } from "./active-cli.js";
2
2
  import { type GitRunner } from "./git.js";
3
3
  import { type DirectivePosture } from "./posture.js";
4
- import { type GatedStepName, type SessionCeremonyTier } from "./session-start.js";
4
+ import { type GatedStepName, type SessionCeremonyTier, WRITE_GATED_EXECUTE_STEPS, WRITE_GATED_REQUIRED_STEPS } from "./session-start.js";
5
5
  export { formatCacheFetchAllRecoveryCommand, recoveryHintForStaleFailure, } from "./cache-recovery.js";
6
+ export { WRITE_GATED_EXECUTE_STEPS, WRITE_GATED_REQUIRED_STEPS };
6
7
  export declare const ENV_SKIP = "DEFT_SESSION_RITUAL_SKIP";
7
8
  export { ENTRYPOINT_TIMEOUT_EXIT_CODE, ENTRYPOINT_TIMEOUT_SECONDS, } from "./ritual-entrypoint.js";
8
9
  export declare const GATED_ENTRYPOINT_COMMANDS: Readonly<Record<GatedStepName, readonly string[]>>;
@@ -81,7 +82,24 @@ export interface VerifySessionRitualOptions {
81
82
  readonly checkActiveCli?: (targetVersion: string | null, seams?: ActiveCliCheckSeams) => ActiveCliCheckResult;
82
83
  /** Injectable work-selection detector for gated `cache_fresh` (#3507). */
83
84
  readonly detectWorkSelection?: DetectWorkSelection;
85
+ /**
86
+ * Required gated steps for this surface. Defaults to all {@link GATED_STEPS}
87
+ * (session / full gated verify). Write dispatch uses
88
+ * {@link WRITE_GATED_REQUIRED_STEPS} (#3738).
89
+ */
90
+ readonly requiredGatedSteps?: readonly GatedStepName[];
91
+ /**
92
+ * Gated steps this surface may execute. Defaults to all {@link GATED_STEPS}.
93
+ * Write dispatch uses {@link WRITE_GATED_EXECUTE_STEPS} so `cache_fresh` and
94
+ * `doctor` cannot run as write-path repair (#3738).
95
+ */
96
+ readonly executeGatedSteps?: readonly GatedStepName[];
84
97
  }
98
+ /**
99
+ * Options for the write/spawn mutation verifier (#3738).
100
+ * Required set omits `cache_fresh`; execution authority is `agent_hooks` only.
101
+ */
102
+ export declare function writeGateRitualOptions(extras?: Omit<VerifySessionRitualOptions, "requiredGatedSteps" | "executeGatedSteps" | "tier">): VerifySessionRitualOptions;
85
103
  export interface InspectSessionRitualOptions {
86
104
  readonly tier?: "quick" | "gated";
87
105
  readonly now?: Date;
@@ -94,8 +112,9 @@ export interface InspectSessionRitualOptions {
94
112
  * Read-only ritual-state inspection for host hooks.
95
113
  *
96
114
  * Unlike {@link verifySessionRitual}, this never runs missing gated entrypoints
97
- * and never rewrites `.deft/ritual-state.json`. A PreToolUse decision must be a
98
- * probe, not a hidden `doctor` / cache-refresh mutation boundary.
115
+ * and never rewrites `.deft/ritual-state.json`. Write dispatch does **not** use
116
+ * this function: it must still execute `agent_hooks` (#3738). A PreToolUse
117
+ * decision must not become a hidden `doctor` / cache-refresh mutation boundary.
99
118
  */
100
119
  export declare function inspectSessionRitual(projectRoot: string, options?: InspectSessionRitualOptions): VerifyResult;
101
120
  export declare function verifySessionRitual(projectRoot: string, options?: VerifySessionRitualOptions): VerifyResult;