@deftai/directive-core 0.99.0 → 0.101.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 (166) hide show
  1. package/dist/authz/actions.d.ts +6 -0
  2. package/dist/authz/actions.js +3 -0
  3. package/dist/authz/classify.js +241 -30
  4. package/dist/authz/decompose-apply.d.ts +81 -0
  5. package/dist/authz/decompose-apply.js +377 -0
  6. package/dist/authz/index.d.ts +3 -1
  7. package/dist/authz/index.js +3 -1
  8. package/dist/authz/store.d.ts +53 -0
  9. package/dist/authz/store.js +219 -1
  10. package/dist/authz/types.d.ts +19 -2
  11. package/dist/authz/types.js +5 -1
  12. package/dist/check/cached-orchestrator.d.ts +26 -5
  13. package/dist/check/cached-orchestrator.js +228 -6
  14. package/dist/check/gate-lists.d.ts +10 -4
  15. package/dist/check/gate-lists.js +19 -5
  16. package/dist/check/index.d.ts +3 -2
  17. package/dist/check/index.js +2 -1
  18. package/dist/check/named-cause.d.ts +45 -0
  19. package/dist/check/named-cause.js +121 -0
  20. package/dist/check/orchestrator.js +6 -1
  21. package/dist/doctor/checks.d.ts +44 -0
  22. package/dist/doctor/checks.js +428 -2
  23. package/dist/doctor/help.d.ts +5 -0
  24. package/dist/doctor/help.js +31 -0
  25. package/dist/doctor/index.d.ts +2 -0
  26. package/dist/doctor/index.js +2 -0
  27. package/dist/doctor/main.d.ts +6 -7
  28. package/dist/doctor/main.js +130 -79
  29. package/dist/doctor/session-coda.d.ts +82 -0
  30. package/dist/doctor/session-coda.js +151 -0
  31. package/dist/doctor/types.d.ts +16 -0
  32. package/dist/doctor/which.d.ts +20 -1
  33. package/dist/doctor/which.js +67 -1
  34. package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
  35. package/dist/finish-loop/pr-finish-loop.js +116 -5
  36. package/dist/finish-loop/types.d.ts +1 -1
  37. package/dist/hooks/dispatcher.d.ts +9 -5
  38. package/dist/hooks/dispatcher.js +23 -15
  39. package/dist/hooks/readonly.d.ts +6 -3
  40. package/dist/hooks/readonly.js +44 -25
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.js +3 -0
  43. package/dist/init-deposit/agent-hooks.d.ts +20 -0
  44. package/dist/init-deposit/agent-hooks.js +51 -31
  45. package/dist/init-deposit/gitignore.js +2 -0
  46. package/dist/intake/issue-ingest.js +36 -0
  47. package/dist/lifecycle/completed-consistency.d.ts +60 -0
  48. package/dist/lifecycle/completed-consistency.js +443 -0
  49. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
  50. package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
  51. package/dist/lifecycle/index.d.ts +2 -0
  52. package/dist/lifecycle/index.js +2 -0
  53. package/dist/literal-acceptance/capture.d.ts +52 -0
  54. package/dist/literal-acceptance/capture.js +658 -0
  55. package/dist/literal-acceptance/evaluate.d.ts +47 -0
  56. package/dist/literal-acceptance/evaluate.js +193 -0
  57. package/dist/literal-acceptance/index.d.ts +12 -0
  58. package/dist/literal-acceptance/index.js +12 -0
  59. package/dist/literal-acceptance/run.d.ts +38 -0
  60. package/dist/literal-acceptance/run.js +216 -0
  61. package/dist/literal-acceptance/safety.d.ts +24 -0
  62. package/dist/literal-acceptance/safety.js +251 -0
  63. package/dist/literal-acceptance/types.d.ts +82 -0
  64. package/dist/literal-acceptance/types.js +26 -0
  65. package/dist/policy/ac-pass-banking.d.ts +58 -0
  66. package/dist/policy/ac-pass-banking.js +150 -0
  67. package/dist/policy/ceremony-dial.d.ts +96 -11
  68. package/dist/policy/ceremony-dial.js +250 -23
  69. package/dist/policy/index.d.ts +2 -0
  70. package/dist/policy/index.js +16 -1
  71. package/dist/policy/merge-approval-head.d.ts +129 -0
  72. package/dist/policy/merge-approval-head.js +461 -0
  73. package/dist/policy/require-human-merge.d.ts +5 -0
  74. package/dist/policy/require-human-merge.js +5 -0
  75. package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
  76. package/dist/pr-merge-readiness/ci-gate.js +65 -5
  77. package/dist/pr-merge-readiness/compute.d.ts +13 -0
  78. package/dist/pr-merge-readiness/compute.js +99 -14
  79. package/dist/pr-merge-readiness/gh.d.ts +49 -0
  80. package/dist/pr-merge-readiness/gh.js +268 -0
  81. package/dist/pr-merge-readiness/index.d.ts +2 -2
  82. package/dist/pr-merge-readiness/index.js +2 -2
  83. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
  84. package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
  85. package/dist/pr-wait-mergeable/cascade.js +115 -1
  86. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  87. package/dist/pr-wait-mergeable/main.js +4 -0
  88. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  89. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
  90. package/dist/pr-wait-mergeable/wrappers.js +7 -1
  91. package/dist/preflight/evaluate.d.ts +15 -1
  92. package/dist/preflight/evaluate.js +33 -2
  93. package/dist/preflight-cache/evaluate.d.ts +3 -0
  94. package/dist/preflight-cache/evaluate.js +3 -0
  95. package/dist/product-first-done-gate/acceptance.d.ts +25 -0
  96. package/dist/product-first-done-gate/acceptance.js +247 -0
  97. package/dist/product-first-done-gate/check-mode.d.ts +45 -0
  98. package/dist/product-first-done-gate/check-mode.js +148 -0
  99. package/dist/product-first-done-gate/evaluate.d.ts +50 -0
  100. package/dist/product-first-done-gate/evaluate.js +255 -0
  101. package/dist/product-first-done-gate/index.d.ts +11 -0
  102. package/dist/product-first-done-gate/index.js +11 -0
  103. package/dist/product-first-done-gate/types.d.ts +64 -0
  104. package/dist/product-first-done-gate/types.js +61 -0
  105. package/dist/render/framework-commands.js +4 -0
  106. package/dist/run-summary/emit.d.ts +56 -0
  107. package/dist/run-summary/emit.js +171 -0
  108. package/dist/run-summary/index.d.ts +4 -0
  109. package/dist/run-summary/index.js +4 -0
  110. package/dist/run-summary/path.d.ts +25 -0
  111. package/dist/run-summary/path.js +78 -0
  112. package/dist/run-summary/types.d.ts +76 -0
  113. package/dist/run-summary/types.js +21 -0
  114. package/dist/scope/acceptance-evidence.d.ts +110 -0
  115. package/dist/scope/acceptance-evidence.js +422 -0
  116. package/dist/scope/coverage-map.d.ts +98 -0
  117. package/dist/scope/coverage-map.js +558 -0
  118. package/dist/scope/decompose.d.ts +5 -0
  119. package/dist/scope/decompose.js +134 -11
  120. package/dist/scope/index.d.ts +3 -0
  121. package/dist/scope/index.js +3 -0
  122. package/dist/scope/parent-lineage.d.ts +125 -0
  123. package/dist/scope/parent-lineage.js +626 -0
  124. package/dist/scope/transition.d.ts +8 -0
  125. package/dist/scope/transition.js +61 -2
  126. package/dist/session/ac-pass-banking.d.ts +221 -0
  127. package/dist/session/ac-pass-banking.js +761 -0
  128. package/dist/session/active-cli.d.ts +79 -0
  129. package/dist/session/active-cli.js +382 -0
  130. package/dist/session/deposit-sha.d.ts +49 -0
  131. package/dist/session/deposit-sha.js +121 -0
  132. package/dist/session/effort-budget.d.ts +136 -0
  133. package/dist/session/effort-budget.js +352 -0
  134. package/dist/session/index.d.ts +7 -0
  135. package/dist/session/index.js +7 -0
  136. package/dist/session/orientation-compression.d.ts +127 -0
  137. package/dist/session/orientation-compression.js +425 -0
  138. package/dist/session/orientation-state.d.ts +29 -0
  139. package/dist/session/orientation-state.js +85 -0
  140. package/dist/session/session-start.d.ts +31 -0
  141. package/dist/session/session-start.js +228 -2
  142. package/dist/session/toolchain-preflight.d.ts +64 -0
  143. package/dist/session/toolchain-preflight.js +151 -0
  144. package/dist/session/verify-session-ritual.d.ts +14 -0
  145. package/dist/session/verify-session-ritual.js +33 -0
  146. package/dist/story-ready/evaluate.d.ts +11 -0
  147. package/dist/story-ready/evaluate.js +41 -3
  148. package/dist/triage/actions/candidates-log.d.ts +1 -2
  149. package/dist/triage/actions/candidates-log.js +37 -6
  150. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  151. package/dist/triage/bootstrap/gitignore.js +8 -2
  152. package/dist/triage/bootstrap/index.js +15 -3
  153. package/dist/triage/classify/index.d.ts +1 -0
  154. package/dist/triage/classify/index.js +2 -0
  155. package/dist/triage/classify/label-mirror.js +16 -0
  156. package/dist/triage/classify/mirror-discovery-tip.d.ts +95 -0
  157. package/dist/triage/classify/mirror-discovery-tip.js +234 -0
  158. package/dist/triage/reconcile/reconcile.js +28 -6
  159. package/dist/triage/summary/index.js +12 -4
  160. package/dist/triage/welcome/default-mode.js +16 -0
  161. package/dist/ts-check-lane/run-lane.js +21 -4
  162. package/dist/vbrief-validate/conformance.js +7 -0
  163. package/dist/vitest-runner/coverage-debt-teardown.js +16 -4
  164. package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
  165. package/dist/xbrief-migrate/migrate-project.js +92 -3
  166. package/package.json +15 -3
@@ -7,6 +7,7 @@
7
7
  * - CLEAN + bot merge allowed + --merge → optional wait-mergeable-and-merge
8
8
  * - Missing grants → fail closed BLOCKED
9
9
  */
10
+ import { type EnforceMergeApprovalHeadInput, type MergeApprovalHeadResult } from "../policy/merge-approval-head.js";
10
11
  import { evaluateAgentMerge } from "../policy/require-human-merge.js";
11
12
  import type { WatchOptions, WatchResult } from "../pr-watch/types.js";
12
13
  import { type PrFinishLoopResult } from "./types.js";
@@ -20,13 +21,23 @@ export interface PrFinishLoopOptions {
20
21
  /** Attempt merge when CLEAN and policy allows (default false). */
21
22
  readonly merge?: boolean;
22
23
  readonly skipGrantGate?: boolean;
24
+ /** Skip #3235 head-bound plan:approved gate (tests). */
25
+ readonly skipMergeApprovalHeadGate?: boolean;
23
26
  readonly env?: Readonly<Record<string, string | undefined>>;
24
27
  readonly now?: Date;
25
28
  /** Inject watch for tests. */
26
29
  readonly watchFn?: (prNumber: number, repo: string | null, options?: WatchOptions) => WatchResult;
27
- /** Inject merge cascade for tests; returns exit code. */
28
- readonly mergeFn?: (prNumber: number, repo: string | null) => number;
30
+ /**
31
+ * Inject merge cascade for tests; returns exit code.
32
+ * Optional `matchHeadCommit` pins the merge to the gated head (#3235).
33
+ */
34
+ readonly mergeFn?: (prNumber: number, repo: string | null, options?: {
35
+ readonly matchHeadCommit?: string | null;
36
+ }) => number;
29
37
  readonly agentMergeFn?: typeof evaluateAgentMerge;
38
+ readonly mergeApprovalHeadFn?: (input: EnforceMergeApprovalHeadInput) => MergeApprovalHeadResult;
39
+ /** Inject live HEAD fetch (tests); defaults to REST pulls head.sha. */
40
+ readonly fetchPrHeadShaFn?: (prNumber: number, repo: string | null) => string | null;
30
41
  readonly writeProgress?: boolean;
31
42
  readonly iteration?: number;
32
43
  }
@@ -7,6 +7,7 @@
7
7
  * - CLEAN + bot merge allowed + --merge → optional wait-mergeable-and-merge
8
8
  * - Missing grants → fail closed BLOCKED
9
9
  */
10
+ import { disablePullRequestAutoMerge, enforceMergeApprovalHead, fetchPrHeadShaRest, } from "../policy/merge-approval-head.js";
10
11
  import { evaluateAgentMerge } from "../policy/require-human-merge.js";
11
12
  import { EXIT_CLEAN, EXIT_NEW_P0_P1, EXIT_TERMINAL_ERROR, VERDICT_CLEAN, VERDICT_NEW_P0_P1, } from "../pr-watch/constants.js";
12
13
  import { watch } from "../pr-watch/watch.js";
@@ -70,7 +71,9 @@ export function runPrFinishLoop(options) {
70
71
  }
71
72
  // --- pr:watch ---
72
73
  const watchFn = options.watchFn ?? watch;
73
- const repo = options.repo ?? process.env.GH_REPO ?? null;
74
+ // Prefer explicit repo; GH_REPO next. Repo is required for head-bound approval
75
+ // scoping when plan:approved records exist (#3235 cross-repo collision).
76
+ const repo = options.repo ?? process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? null;
74
77
  let watchResult;
75
78
  try {
76
79
  watchResult = watchFn(prNumber, repo, {
@@ -129,6 +132,61 @@ export function runPrFinishLoop(options) {
129
132
  grantId: null,
130
133
  };
131
134
  }
135
+ // #3235: head-bound plan:approved vs LIVE PR HEAD (before merge path).
136
+ // Always re-fetch HEAD. Never fall back to the watch snapshot: a failed live
137
+ // read + snapshot A would retain auto-merge while GitHub head is B.
138
+ if (options.skipMergeApprovalHeadGate !== true) {
139
+ const fetchHead = options.fetchPrHeadShaFn ?? fetchPrHeadShaRest;
140
+ const liveHead = fetchHead(prNumber, repo);
141
+ if (liveHead === null || liveHead.trim() === "") {
142
+ disablePullRequestAutoMerge(prNumber, repo);
143
+ const message = `pr:finish-loop ACTION_REQUIRED on PR #${prNumber}: cannot read live HEAD ` +
144
+ "after CLEAN; auto-merge disabled (fail closed, #3235).";
145
+ log("merge", "stale-merge-approval", message);
146
+ return {
147
+ exitCode: EXIT_ACTION_REQUIRED,
148
+ haltReason: "stale-merge-approval",
149
+ message,
150
+ prNumber,
151
+ watchVerdict: VERDICT_CLEAN,
152
+ mergeAttempted: false,
153
+ mergeSkippedReason: "stale-merge-approval",
154
+ grantId: null,
155
+ };
156
+ }
157
+ const headGateFn = options.mergeApprovalHeadFn ?? enforceMergeApprovalHead;
158
+ const headGate = headGateFn({
159
+ prNumber,
160
+ repo,
161
+ projectRoot,
162
+ currentHeadSha: liveHead,
163
+ disableAutoMergeOnDeny: true,
164
+ });
165
+ if (!headGate.allowed) {
166
+ const message = [
167
+ `pr:finish-loop ACTION_REQUIRED on PR #${prNumber}: stale or unbound merge approval.`,
168
+ headGate.message,
169
+ headGate.recovery ?? "",
170
+ ]
171
+ .filter((line) => line.length > 0)
172
+ .join("\n");
173
+ log("merge", "stale-merge-approval", message, {
174
+ approved_head_sha: headGate.approved_head_sha,
175
+ current_head_sha: headGate.current_head_sha,
176
+ auto_merge_disabled: headGate.auto_merge_disabled,
177
+ });
178
+ return {
179
+ exitCode: EXIT_ACTION_REQUIRED,
180
+ haltReason: "stale-merge-approval",
181
+ message,
182
+ prNumber,
183
+ watchVerdict: VERDICT_CLEAN,
184
+ mergeAttempted: false,
185
+ mergeSkippedReason: "stale-merge-approval",
186
+ grantId: null,
187
+ };
188
+ }
189
+ }
132
190
  // CLEAN path
133
191
  if (options.merge !== true) {
134
192
  const message = `pr:finish-loop CLEAN on PR #${prNumber}. Merge not requested ` +
@@ -165,12 +223,62 @@ export function runPrFinishLoop(options) {
165
223
  grantId: null,
166
224
  };
167
225
  }
168
- // Optional merge when policy allows
226
+ // Optional merge when policy allows — re-fetch live HEAD (not watch snapshot)
227
+ // and pin matchHeadCommit (#3235 TOCTOU).
169
228
  if (options.mergeFn !== undefined) {
170
- const rc = options.mergeFn(prNumber, repo);
229
+ const fetchHead = options.fetchPrHeadShaFn ?? fetchPrHeadShaRest;
230
+ const liveHead = fetchHead(prNumber, repo);
231
+ if (options.skipMergeApprovalHeadGate !== true) {
232
+ if (liveHead === null || liveHead.trim() === "") {
233
+ disablePullRequestAutoMerge(prNumber, repo);
234
+ const message = `pr:finish-loop ACTION_REQUIRED on PR #${prNumber}: cannot read live HEAD ` +
235
+ "before merge; auto-merge disabled (fail closed, #3235).";
236
+ log("merge", "stale-merge-approval", message);
237
+ return {
238
+ exitCode: EXIT_ACTION_REQUIRED,
239
+ haltReason: "stale-merge-approval",
240
+ message,
241
+ prNumber,
242
+ watchVerdict: VERDICT_CLEAN,
243
+ mergeAttempted: false,
244
+ mergeSkippedReason: "stale-merge-approval",
245
+ grantId: null,
246
+ };
247
+ }
248
+ const headGateFn = options.mergeApprovalHeadFn ?? enforceMergeApprovalHead;
249
+ const recheck = headGateFn({
250
+ prNumber,
251
+ repo,
252
+ projectRoot,
253
+ currentHeadSha: liveHead,
254
+ disableAutoMergeOnDeny: true,
255
+ });
256
+ if (!recheck.allowed) {
257
+ const message = [
258
+ `pr:finish-loop ACTION_REQUIRED on PR #${prNumber}: stale merge approval at merge time.`,
259
+ recheck.message,
260
+ recheck.recovery ?? "",
261
+ ]
262
+ .filter((line) => line.length > 0)
263
+ .join("\n");
264
+ log("merge", "stale-merge-approval", message);
265
+ return {
266
+ exitCode: EXIT_ACTION_REQUIRED,
267
+ haltReason: "stale-merge-approval",
268
+ message,
269
+ prNumber,
270
+ watchVerdict: VERDICT_CLEAN,
271
+ mergeAttempted: false,
272
+ mergeSkippedReason: "stale-merge-approval",
273
+ grantId: null,
274
+ };
275
+ }
276
+ }
277
+ const pinnedHead = liveHead;
278
+ const rc = options.mergeFn(prNumber, repo, { matchHeadCommit: pinnedHead });
171
279
  if (rc === 0) {
172
280
  const message = `pr:finish-loop MERGED PR #${prNumber}`;
173
- log("merge", "merged", message);
281
+ log("merge", "merged", message, { matchHeadCommit: pinnedHead });
174
282
  return {
175
283
  exitCode: EXIT_OK,
176
284
  haltReason: "merged",
@@ -182,7 +290,10 @@ export function runPrFinishLoop(options) {
182
290
  grantId: null,
183
291
  };
184
292
  }
185
- const message = `pr:finish-loop merge attempt failed exit=${rc} for PR #${prNumber}`;
293
+ // Pin mismatch / merge fail: revoke auto-merge so unapproved head cannot land.
294
+ disablePullRequestAutoMerge(prNumber, repo);
295
+ const message = `pr:finish-loop merge attempt failed exit=${rc} for PR #${prNumber}; ` +
296
+ "disabled auto-merge after pinned-merge failure (#3235).";
186
297
  log("merge", "error", message);
187
298
  return {
188
299
  exitCode: EXIT_BLOCKED,
@@ -8,7 +8,7 @@ export declare const EXIT_OK = 0;
8
8
  export declare const EXIT_ACTION_REQUIRED = 1;
9
9
  /** BLOCKED (grant/gate), ERRORED, TIMEOUT, config. */
10
10
  export declare const EXIT_BLOCKED = 2;
11
- export type FinishLoopHaltReason = "empty-queue" | "grant-missing" | "grant-expired" | "grant-deny" | "gate-deny" | "max-iterations" | "require-human-merge" | "address-findings" | "agent-implement" | "clean" | "merged" | "error";
11
+ export type FinishLoopHaltReason = "empty-queue" | "grant-missing" | "grant-expired" | "grant-deny" | "gate-deny" | "max-iterations" | "require-human-merge" | "stale-merge-approval" | "address-findings" | "agent-implement" | "clean" | "merged" | "error";
12
12
  export type FinishLoopPhase = "gate" | "queue-scan" | "implement" | "pr-open" | "pr-watch" | "address" | "merge" | "halt";
13
13
  export interface FinishLoopProgressLine {
14
14
  readonly schemaVersion: 1;
@@ -142,11 +142,15 @@ export declare function decideHook(input: HookDispatchInput, seams?: HookPolicyS
142
142
  /**
143
143
  * Render host-facing hook output.
144
144
  *
145
- * Cursor deposits use `failClosed: true`. Cursor treats empty/null stdout as a
146
- * hook failure and blocks the tool — so Cursor allows must emit explicit
147
- * `{"permission":"allow"}`. Other hosts keep empty allow so the host permission
148
- * flow is unchanged except session.start / session.compact soft re-bind
149
- * injection (#3171), which surfaces checklist text without a write tool.
145
+ * Cursor deposits use `failClosed: true` with a tool.before timeout above the
146
+ * gated-ritual / live agent-hook readiness budget
147
+ * (`CURSOR_TOOL_BEFORE_TIMEOUT_SECONDS` in init-deposit/agent-hooks; #3246).
148
+ * Cursor treats empty/null stdout (or a host timeout kill) as a hook failure
149
+ * and blocks the tool so Cursor allows must emit explicit
150
+ * `{"permission":"allow"}` within the deposit timeout. Other hosts keep empty
151
+ * allow so the host permission flow is unchanged — except session.start /
152
+ * session.compact soft re-bind injection (#3171), which surfaces checklist
153
+ * text without a write tool.
150
154
  *
151
155
  * Cursor stdout always includes `code` (stable machine-readable decision code)
152
156
  * so agents can distinguish policy denials from host-integration failures
@@ -184,10 +184,10 @@ export function isAllowlistedAssistScratchPath(projectRoot, targetPath) {
184
184
  export function isAssistScratchWrite(projectRoot, targetPath, payload, environ = process.env) {
185
185
  if (!isAllowlistedAssistScratchPath(projectRoot, targetPath))
186
186
  return false;
187
- // Structural classification only — compose with #3080 ephemeral markers.
187
+ // Structural classification only — compose with #3080 / #3259 ephemeral markers.
188
188
  if (isAssistPosture(payload, environ))
189
189
  return true;
190
- if (isEphemeralSpawn(payload))
190
+ if (isEphemeralSpawn(payload, environ))
191
191
  return true;
192
192
  return false;
193
193
  }
@@ -656,14 +656,18 @@ function inspectMutationGates(input, toolName, seams, options) {
656
656
  if (!outsideRoot || isSpawnTool(toolName)) {
657
657
  let proposedPathHint;
658
658
  if (isSpawnTool(toolName)) {
659
- // Multi-path recovery for implement-class spawns (#3080 AC4).
659
+ // Multi-path recovery for implement-class spawns (#3080 AC4 / #3259 honesty).
660
+ // Structural markers only — free-text prompt brackets are not sufficient.
660
661
  proposedPathHint =
661
662
  " Recovery: (1) Product implementation — run `deft scope:activate -- <path>` " +
662
663
  "for the approved xBRIEF, then re-run the pre-start_agent gate stack. " +
663
- "(2) Read-only research — spawn with `subagent_type`/`worker_role` explore. " +
664
- "(3) Ephemeral docs/analysisspawn with `worker_role: ephemeral` " +
665
- "(aliases: docs, assist; see commands.md), or continue in the parent without " +
666
- "a lifecycle story. Do not invent a fake scope only to satisfy this gate.";
664
+ "(2) Read-only research — spawn with structural `subagent_type`/`worker_role` explore. " +
665
+ "(3) Ephemeral docs/local-devset structural tool fields " +
666
+ "`worker_role`/`subagent_type` ∈ {ephemeral, docs, assist} (hosts that support them), " +
667
+ "or set session assist (`DEFT_SESSION_POSTURE=assist` or `DEFT_HOOK_ASSIST=1`), " +
668
+ "or run local-dev Shell (`docker compose` / `pnpm dev`) in the parent without a " +
669
+ "lifecycle story. Free-text markers such as `[worker_role: ephemeral]` in the " +
670
+ "prompt are NOT sufficient. Do not invent a fake scope only to satisfy this gate.";
667
671
  }
668
672
  else if (options.proposedLifecycleExempt &&
669
673
  relTarget !== null &&
@@ -895,9 +899,9 @@ export function decideHook(input, seams = {}) {
895
899
  scopePath: null,
896
900
  };
897
901
  }
898
- // Ephemeral/docs/assist: write-capable non-lifecycle spawn; no active xBRIEF (#3080).
899
- // Does not authorize push/merge/deploy — those remain on shell/MCP matchers.
900
- if (isEphemeralSpawn(input.payload)) {
902
+ // Ephemeral/docs/assist (+ session assist env #3259): non-lifecycle spawn;
903
+ // no active xBRIEF. Does not authorize push/merge/deploy — shell/MCP matchers.
904
+ if (isEphemeralSpawn(input.payload, environ)) {
901
905
  return {
902
906
  verdict: "allow",
903
907
  code: "spawn-ephemeral-ready",
@@ -951,11 +955,15 @@ function softAgentsRebindWireText(decision) {
951
955
  /**
952
956
  * Render host-facing hook output.
953
957
  *
954
- * Cursor deposits use `failClosed: true`. Cursor treats empty/null stdout as a
955
- * hook failure and blocks the tool — so Cursor allows must emit explicit
956
- * `{"permission":"allow"}`. Other hosts keep empty allow so the host permission
957
- * flow is unchanged except session.start / session.compact soft re-bind
958
- * injection (#3171), which surfaces checklist text without a write tool.
958
+ * Cursor deposits use `failClosed: true` with a tool.before timeout above the
959
+ * gated-ritual / live agent-hook readiness budget
960
+ * (`CURSOR_TOOL_BEFORE_TIMEOUT_SECONDS` in init-deposit/agent-hooks; #3246).
961
+ * Cursor treats empty/null stdout (or a host timeout kill) as a hook failure
962
+ * and blocks the tool so Cursor allows must emit explicit
963
+ * `{"permission":"allow"}` within the deposit timeout. Other hosts keep empty
964
+ * allow so the host permission flow is unchanged — except session.start /
965
+ * session.compact soft re-bind injection (#3171), which surfaces checklist
966
+ * text without a write tool.
959
967
  *
960
968
  * Cursor stdout always includes `code` (stable machine-readable decision code)
961
969
  * so agents can distinguish policy denials from host-integration failures
@@ -13,9 +13,12 @@ export declare function isExploreSpawn(payload: unknown): boolean;
13
13
  */
14
14
  export declare function isAssistPosture(payload: unknown, environ?: NodeJS.ProcessEnv): boolean;
15
15
  /**
16
- * Ephemeral / assist / docs spawns skip active-xBRIEF implementation gates (#3080).
16
+ * Ephemeral / assist / docs spawns skip active-xBRIEF implementation gates (#3080 / #3259).
17
17
  * True only with an explicit allowlisted marker. Absent marker → false (fail closed).
18
- * When an ephemeral marker conflicts with implement envelope signals, implement wins.
18
+ * Markers: structural `worker_role`/`subagent_type` {ephemeral,docs,assist}, OR
19
+ * session assist env (`DEFT_SESSION_POSTURE` assist-set / `DEFT_HOOK_ASSIST=1`) for spawn.
20
+ * Free-text prompt strings are never classified. When a marker conflicts with implement
21
+ * envelope signals, implement wins.
19
22
  */
20
- export declare function isEphemeralSpawn(payload: unknown): boolean;
23
+ export declare function isEphemeralSpawn(payload: unknown, environ?: NodeJS.ProcessEnv): boolean;
21
24
  //# sourceMappingURL=readonly.d.ts.map
@@ -123,8 +123,8 @@ export function isAssistPosture(payload, environ = process.env) {
123
123
  if (posture !== null && ASSIST_POSTURE_MARKERS.has(normalizePostureToken(posture))) {
124
124
  return true;
125
125
  }
126
- // Shared taxonomy with #3080 ephemeral spawn markers.
127
- if (isEphemeralSpawn(payload))
126
+ // Shared taxonomy with #3080 / #3259 ephemeral spawn markers.
127
+ if (isEphemeralSpawn(payload, environ))
128
128
  return true;
129
129
  return false;
130
130
  }
@@ -176,34 +176,53 @@ function hasImplementConflictSignal(toolInput, input) {
176
176
  return false;
177
177
  }
178
178
  /**
179
- * Ephemeral / assist / docs spawns skip active-xBRIEF implementation gates (#3080).
179
+ * Session assist env tokens that count as structural ephemeral markers for spawn (#3259).
180
+ * Reuses #1802 ASSIST_POSTURE_MARKERS / DEFT_HOOK_ASSIST — not free-text NLP.
181
+ */
182
+ function hasSessionAssistEphemeralEnv(environ) {
183
+ const envPosture = (environ[ASSIST_SESSION_POSTURE_ENV] ?? "").trim();
184
+ if (envPosture.length > 0 && ASSIST_POSTURE_MARKERS.has(normalizePostureToken(envPosture))) {
185
+ return true;
186
+ }
187
+ return envTruthy(environ, "DEFT_HOOK_ASSIST");
188
+ }
189
+ /**
190
+ * Ephemeral / assist / docs spawns skip active-xBRIEF implementation gates (#3080 / #3259).
180
191
  * True only with an explicit allowlisted marker. Absent marker → false (fail closed).
181
- * When an ephemeral marker conflicts with implement envelope signals, implement wins.
192
+ * Markers: structural `worker_role`/`subagent_type` {ephemeral,docs,assist}, OR
193
+ * session assist env (`DEFT_SESSION_POSTURE` assist-set / `DEFT_HOOK_ASSIST=1`) for spawn.
194
+ * Free-text prompt strings are never classified. When a marker conflicts with implement
195
+ * envelope signals, implement wins.
182
196
  */
183
- export function isEphemeralSpawn(payload) {
197
+ export function isEphemeralSpawn(payload, environ = process.env) {
184
198
  const input = record(payload);
185
- if (input === null)
186
- return false;
187
- const toolInput = toolInputRecord(input) ?? input;
188
- const subagentType = fieldString(toolInput, "subagent_type") ??
189
- fieldString(toolInput, "subagentType") ??
190
- fieldString(input, "subagent_type") ??
191
- fieldString(input, "subagentType");
192
- const workerRole = fieldString(toolInput, "worker_role") ??
193
- fieldString(toolInput, "workerRole") ??
194
- fieldString(input, "worker_role") ??
195
- fieldString(input, "workerRole");
196
- const marker = (workerRole !== null && EPHEMERAL_ROLE_MARKERS.has(workerRole.toLowerCase())
197
- ? workerRole.toLowerCase()
198
- : null) ??
199
- (subagentType !== null && EPHEMERAL_ROLE_MARKERS.has(subagentType.toLowerCase())
200
- ? subagentType.toLowerCase()
201
- : null);
202
- if (marker === null)
199
+ const toolInput = input !== null ? (toolInputRecord(input) ?? input) : null;
200
+ let hasMarker = false;
201
+ if (input !== null && toolInput !== null) {
202
+ const subagentType = fieldString(toolInput, "subagent_type") ??
203
+ fieldString(toolInput, "subagentType") ??
204
+ fieldString(input, "subagent_type") ??
205
+ fieldString(input, "subagentType");
206
+ const workerRole = fieldString(toolInput, "worker_role") ??
207
+ fieldString(toolInput, "workerRole") ??
208
+ fieldString(input, "worker_role") ??
209
+ fieldString(input, "workerRole");
210
+ if ((workerRole !== null && EPHEMERAL_ROLE_MARKERS.has(workerRole.toLowerCase())) ||
211
+ (subagentType !== null && EPHEMERAL_ROLE_MARKERS.has(subagentType.toLowerCase()))) {
212
+ hasMarker = true;
213
+ }
214
+ }
215
+ // #3259 D1-B: session assist env is a structural ephemeral marker for spawn tools.
216
+ // Call sites gate "spawn only"; this helper stays pure classification.
217
+ if (!hasMarker && hasSessionAssistEphemeralEnv(environ)) {
218
+ hasMarker = true;
219
+ }
220
+ if (!hasMarker)
203
221
  return false;
204
- // Implement signals win over ephemeral markers (fail closed).
205
- if (hasImplementConflictSignal(toolInput, input))
222
+ // Implement signals win over ephemeral markers including session assist env (fail closed).
223
+ if (input !== null && toolInput !== null && hasImplementConflictSignal(toolInput, input)) {
206
224
  return false;
225
+ }
207
226
  return true;
208
227
  }
209
228
  //# sourceMappingURL=readonly.js.map
package/dist/index.d.ts CHANGED
@@ -35,6 +35,7 @@ export * as intake from "./intake/index.js";
35
35
  export * as layout from "./layout/index.js";
36
36
  export * as legacyBridge from "./legacy-bridge/index.js";
37
37
  export * as lifecycle from "./lifecycle/index.js";
38
+ export * as literalAcceptance from "./literal-acceptance/index.js";
38
39
  export * as metrics from "./metrics/index.js";
39
40
  export * as orchestration from "./orchestration/index.js";
40
41
  export * as packs from "./packs/index.js";
@@ -47,12 +48,14 @@ export * as prMonitor from "./pr-monitor/index.js";
47
48
  export * as prProtectedIssues from "./pr-protected-issues/index.js";
48
49
  export * as prWaitMergeable from "./pr-wait-mergeable/index.js";
49
50
  export * as preflight from "./preflight/index.js";
51
+ export * as productFirstDoneGate from "./product-first-done-gate/index.js";
50
52
  export * as release from "./release/index.js";
51
53
  export * as releaseE2e from "./release-e2e/index.js";
52
54
  export * as releasePublish from "./release-publish/index.js";
53
55
  export * as releaseRollback from "./release-rollback/index.js";
54
56
  export * as render from "./render/index.js";
55
57
  export * as resolution from "./resolution/index.js";
58
+ export * as runSummary from "./run-summary/index.js";
56
59
  export * as scm from "./scm/index.js";
57
60
  export * as scope from "./scope/index.js";
58
61
  export * as scopeProvenance from "./scope-provenance/index.js";
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ export * as intake from "./intake/index.js";
36
36
  export * as layout from "./layout/index.js";
37
37
  export * as legacyBridge from "./legacy-bridge/index.js";
38
38
  export * as lifecycle from "./lifecycle/index.js";
39
+ export * as literalAcceptance from "./literal-acceptance/index.js";
39
40
  export * as metrics from "./metrics/index.js";
40
41
  export * as orchestration from "./orchestration/index.js";
41
42
  export * as packs from "./packs/index.js";
@@ -48,12 +49,14 @@ export * as prMonitor from "./pr-monitor/index.js";
48
49
  export * as prProtectedIssues from "./pr-protected-issues/index.js";
49
50
  export * as prWaitMergeable from "./pr-wait-mergeable/index.js";
50
51
  export * as preflight from "./preflight/index.js";
52
+ export * as productFirstDoneGate from "./product-first-done-gate/index.js";
51
53
  export * as release from "./release/index.js";
52
54
  export * as releaseE2e from "./release-e2e/index.js";
53
55
  export * as releasePublish from "./release-publish/index.js";
54
56
  export * as releaseRollback from "./release-rollback/index.js";
55
57
  export * as render from "./render/index.js";
56
58
  export * as resolution from "./resolution/index.js";
59
+ export * as runSummary from "./run-summary/index.js";
57
60
  export * as scm from "./scm/index.js";
58
61
  export * as scope from "./scope/index.js";
59
62
  export * as scopeProvenance from "./scope-provenance/index.js";
@@ -5,6 +5,26 @@ export { DIRECT_WRITE_HOOK_MATCHER, MCP_HOOK_MATCHER, SHELL_HOOK_MATCHER, SPAWN_
5
5
  export declare const DEFT_HOOK_COMMAND_MARKER = "deft-hook";
6
6
  export declare const LEGACY_DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
7
7
  export declare const AGENT_HOOK_PATHS: readonly [".claude/settings.json", ".grok/hooks/deft.json", ".cursor/hooks.json", ".codex/hooks.json"];
8
+ /**
9
+ * Cursor session.start / session.compact deposit timeout (seconds).
10
+ * Lightweight ceremony paths — keep tight so stalled hooks fail fast.
11
+ */
12
+ export declare const CURSOR_SESSION_HOOK_TIMEOUT_SECONDS = 5;
13
+ /**
14
+ * Cursor preToolUse (tool.before) deposit timeout (seconds).
15
+ *
16
+ * Mutation tool.before runs `inspectMutationGates` → gated
17
+ * `verifySessionRitual`, which re-runs non-cacheable agent-hook readiness on
18
+ * every boundary. Live readiness alone has a multi-host fixture ceiling of
19
+ * ~12s (`content/contracts/agent-hook-readiness.md`); the historical deposit
20
+ * default of 5s was below that budget. Under Cursor `failClosed: true`, a
21
+ * host timeout kill surfaces as opaque exit-1 with no Directive decision
22
+ * code (#3246 / related #2864). Keep this above the readiness ceiling plus
23
+ * dispatch overhead so allow/deny can render within the host budget.
24
+ */
25
+ export declare const CURSOR_TOOL_BEFORE_TIMEOUT_SECONDS = 30;
26
+ /** Nested Claude/Grok/Codex command-hook default timeout (seconds). */
27
+ export declare const NESTED_HOOK_TIMEOUT_SECONDS = 5;
8
28
  export type AgentHookPath = (typeof AGENT_HOOK_PATHS)[number];
9
29
  export type AgentHookRegistrationStatus = "healthy" | "disabled" | "missing" | "drifted";
10
30
  /** Whether the host receives a compact/resume hook deposit (#2113). */
@@ -13,6 +13,26 @@ export const AGENT_HOOK_PATHS = [
13
13
  ".cursor/hooks.json",
14
14
  ".codex/hooks.json",
15
15
  ];
16
+ /**
17
+ * Cursor session.start / session.compact deposit timeout (seconds).
18
+ * Lightweight ceremony paths — keep tight so stalled hooks fail fast.
19
+ */
20
+ export const CURSOR_SESSION_HOOK_TIMEOUT_SECONDS = 5;
21
+ /**
22
+ * Cursor preToolUse (tool.before) deposit timeout (seconds).
23
+ *
24
+ * Mutation tool.before runs `inspectMutationGates` → gated
25
+ * `verifySessionRitual`, which re-runs non-cacheable agent-hook readiness on
26
+ * every boundary. Live readiness alone has a multi-host fixture ceiling of
27
+ * ~12s (`content/contracts/agent-hook-readiness.md`); the historical deposit
28
+ * default of 5s was below that budget. Under Cursor `failClosed: true`, a
29
+ * host timeout kill surfaces as opaque exit-1 with no Directive decision
30
+ * code (#3246 / related #2864). Keep this above the readiness ceiling plus
31
+ * dispatch overhead so allow/deny can render within the host budget.
32
+ */
33
+ export const CURSOR_TOOL_BEFORE_TIMEOUT_SECONDS = 30;
34
+ /** Nested Claude/Grok/Codex command-hook default timeout (seconds). */
35
+ export const NESTED_HOOK_TIMEOUT_SECONDS = 5;
16
36
  function object(value) {
17
37
  return value !== null && typeof value === "object" && !Array.isArray(value)
18
38
  ? value
@@ -90,7 +110,7 @@ function nestedGroup(host, event, matcher) {
90
110
  {
91
111
  type: "command",
92
112
  command: command(host, event),
93
- timeout: 5,
113
+ timeout: NESTED_HOOK_TIMEOUT_SECONDS,
94
114
  },
95
115
  ],
96
116
  };
@@ -154,35 +174,47 @@ function mergeCursorConfig(config, path) {
154
174
  const session = eventArray(hooks, "sessionStart", path).filter((entry) => !isManagedCursorEntry(entry));
155
175
  const preTool = eventArray(hooks, "preToolUse", path).filter((entry) => !isManagedCursorEntry(entry));
156
176
  const preCompact = eventArray(hooks, "preCompact", path).filter((entry) => !isManagedCursorEntry(entry));
157
- hooks.sessionStart = [...session, { command: command("cursor", "session.start"), timeout: 5 }];
177
+ hooks.sessionStart = [
178
+ ...session,
179
+ {
180
+ command: command("cursor", "session.start"),
181
+ timeout: CURSOR_SESSION_HOOK_TIMEOUT_SECONDS,
182
+ },
183
+ ];
158
184
  hooks.preToolUse = [
159
185
  ...preTool,
160
186
  {
161
187
  command: command("cursor", "tool.before"),
162
188
  matcher: DIRECT_WRITE_HOOK_MATCHER,
163
189
  failClosed: true,
164
- timeout: 5,
190
+ timeout: CURSOR_TOOL_BEFORE_TIMEOUT_SECONDS,
165
191
  },
166
192
  {
167
193
  command: command("cursor", "tool.before"),
168
194
  matcher: SPAWN_HOOK_MATCHER,
169
195
  failClosed: true,
170
- timeout: 5,
196
+ timeout: CURSOR_TOOL_BEFORE_TIMEOUT_SECONDS,
171
197
  },
172
198
  {
173
199
  command: command("cursor", "tool.before"),
174
200
  matcher: SHELL_HOOK_MATCHER,
175
201
  failClosed: true,
176
- timeout: 5,
202
+ timeout: CURSOR_TOOL_BEFORE_TIMEOUT_SECONDS,
177
203
  },
178
204
  {
179
205
  command: command("cursor", "tool.before"),
180
206
  matcher: MCP_HOOK_MATCHER,
181
207
  failClosed: true,
182
- timeout: 5,
208
+ timeout: CURSOR_TOOL_BEFORE_TIMEOUT_SECONDS,
209
+ },
210
+ ];
211
+ hooks.preCompact = [
212
+ ...preCompact,
213
+ {
214
+ command: command("cursor", "session.compact"),
215
+ timeout: CURSOR_SESSION_HOOK_TIMEOUT_SECONDS,
183
216
  },
184
217
  ];
185
- hooks.preCompact = [...preCompact, { command: command("cursor", "session.compact"), timeout: 5 }];
186
218
  return { ...config, version: 1, hooks };
187
219
  }
188
220
  function writeJsonIfChanged(projectRoot, path, payload) {
@@ -341,6 +373,14 @@ function hasNestedRegistration(config, host, options = {}) {
341
373
  return (preCompact.some((entry) => nestedCommands(entry).includes(compactCommand)) &&
342
374
  postCompact.some((entry) => nestedCommands(entry).includes(compactCommand)));
343
375
  }
376
+ function isCursorToolBeforeEntry(value, matcher) {
377
+ const hook = object(value);
378
+ return (hook?.command === command("cursor", "tool.before") &&
379
+ hook.matcher === matcher &&
380
+ hook.failClosed === true &&
381
+ // #3246: budget must cover gated ritual + live agent-hook readiness.
382
+ hook.timeout === CURSOR_TOOL_BEFORE_TIMEOUT_SECONDS);
383
+ }
344
384
  function hasCursorRegistration(config) {
345
385
  const hooks = object(config.hooks);
346
386
  if (hooks === null || config.version !== 1)
@@ -349,30 +389,10 @@ function hasCursorRegistration(config) {
349
389
  const preTool = Array.isArray(hooks.preToolUse) ? hooks.preToolUse : [];
350
390
  const preCompact = Array.isArray(hooks.preCompact) ? hooks.preCompact : [];
351
391
  return (session.some((entry) => object(entry)?.command === command("cursor", "session.start")) &&
352
- preTool.some((entry) => {
353
- const hook = object(entry);
354
- return (hook?.command === command("cursor", "tool.before") &&
355
- hook.matcher === DIRECT_WRITE_HOOK_MATCHER &&
356
- hook.failClosed === true);
357
- }) &&
358
- preTool.some((entry) => {
359
- const hook = object(entry);
360
- return (hook?.command === command("cursor", "tool.before") &&
361
- hook.matcher === SPAWN_HOOK_MATCHER &&
362
- hook.failClosed === true);
363
- }) &&
364
- preTool.some((entry) => {
365
- const hook = object(entry);
366
- return (hook?.command === command("cursor", "tool.before") &&
367
- hook.matcher === SHELL_HOOK_MATCHER &&
368
- hook.failClosed === true);
369
- }) &&
370
- preTool.some((entry) => {
371
- const hook = object(entry);
372
- return (hook?.command === command("cursor", "tool.before") &&
373
- hook.matcher === MCP_HOOK_MATCHER &&
374
- hook.failClosed === true);
375
- }) &&
392
+ preTool.some((entry) => isCursorToolBeforeEntry(entry, DIRECT_WRITE_HOOK_MATCHER)) &&
393
+ preTool.some((entry) => isCursorToolBeforeEntry(entry, SPAWN_HOOK_MATCHER)) &&
394
+ preTool.some((entry) => isCursorToolBeforeEntry(entry, SHELL_HOOK_MATCHER)) &&
395
+ preTool.some((entry) => isCursorToolBeforeEntry(entry, MCP_HOOK_MATCHER)) &&
376
396
  preCompact.some((entry) => object(entry)?.command === command("cursor", "session.compact")));
377
397
  }
378
398
  /** Read-only registration probe shared by verify and doctor. */
@@ -30,6 +30,8 @@ export const CANONICAL_GITIGNORE_BASELINE = [
30
30
  ".deft/ritual-state.json",
31
31
  ".deft/last-session.json",
32
32
  ".deft/routing.local.json",
33
+ // #3282: opt-in default run-summary JSONL at repo root (collectible; must not dirty trees).
34
+ ".deft-run-summary.json",
33
35
  // Temporary test/local kill-switch — must stay untracked (#3039).
34
36
  DEFT_DIRECTIVE_DISABLE_GITIGNORE_LINE,
35
37
  "vbrief/.triage-cache/candidates.jsonl",