@deftai/directive-core 0.99.0 → 0.100.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 (102) 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 +157 -23
  4. package/dist/authz/decompose-apply.d.ts +67 -0
  5. package/dist/authz/decompose-apply.js +302 -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/doctor/checks.d.ts +44 -0
  13. package/dist/doctor/checks.js +428 -2
  14. package/dist/doctor/main.d.ts +6 -7
  15. package/dist/doctor/main.js +72 -79
  16. package/dist/doctor/which.d.ts +20 -1
  17. package/dist/doctor/which.js +67 -1
  18. package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
  19. package/dist/finish-loop/pr-finish-loop.js +116 -5
  20. package/dist/finish-loop/types.d.ts +1 -1
  21. package/dist/hooks/dispatcher.d.ts +9 -5
  22. package/dist/hooks/dispatcher.js +23 -15
  23. package/dist/hooks/readonly.d.ts +6 -3
  24. package/dist/hooks/readonly.js +44 -25
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +1 -0
  27. package/dist/init-deposit/agent-hooks.d.ts +20 -0
  28. package/dist/init-deposit/agent-hooks.js +51 -31
  29. package/dist/intake/issue-ingest.js +29 -0
  30. package/dist/lifecycle/completed-consistency.d.ts +60 -0
  31. package/dist/lifecycle/completed-consistency.js +443 -0
  32. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
  33. package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
  34. package/dist/lifecycle/index.d.ts +2 -0
  35. package/dist/lifecycle/index.js +2 -0
  36. package/dist/literal-acceptance/capture.d.ts +52 -0
  37. package/dist/literal-acceptance/capture.js +658 -0
  38. package/dist/literal-acceptance/evaluate.d.ts +47 -0
  39. package/dist/literal-acceptance/evaluate.js +193 -0
  40. package/dist/literal-acceptance/index.d.ts +12 -0
  41. package/dist/literal-acceptance/index.js +12 -0
  42. package/dist/literal-acceptance/run.d.ts +38 -0
  43. package/dist/literal-acceptance/run.js +216 -0
  44. package/dist/literal-acceptance/safety.d.ts +24 -0
  45. package/dist/literal-acceptance/safety.js +251 -0
  46. package/dist/literal-acceptance/types.d.ts +82 -0
  47. package/dist/literal-acceptance/types.js +26 -0
  48. package/dist/policy/ceremony-dial.d.ts +67 -11
  49. package/dist/policy/ceremony-dial.js +174 -23
  50. package/dist/policy/index.d.ts +1 -0
  51. package/dist/policy/index.js +1 -0
  52. package/dist/policy/merge-approval-head.d.ts +129 -0
  53. package/dist/policy/merge-approval-head.js +461 -0
  54. package/dist/policy/require-human-merge.d.ts +5 -0
  55. package/dist/policy/require-human-merge.js +5 -0
  56. package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
  57. package/dist/pr-merge-readiness/ci-gate.js +65 -5
  58. package/dist/pr-merge-readiness/compute.d.ts +13 -0
  59. package/dist/pr-merge-readiness/compute.js +99 -14
  60. package/dist/pr-merge-readiness/gh.d.ts +49 -0
  61. package/dist/pr-merge-readiness/gh.js +268 -0
  62. package/dist/pr-merge-readiness/index.d.ts +2 -2
  63. package/dist/pr-merge-readiness/index.js +2 -2
  64. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
  65. package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
  66. package/dist/pr-wait-mergeable/cascade.js +115 -1
  67. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  68. package/dist/pr-wait-mergeable/main.js +4 -0
  69. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  70. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
  71. package/dist/pr-wait-mergeable/wrappers.js +7 -1
  72. package/dist/preflight/evaluate.d.ts +15 -1
  73. package/dist/preflight/evaluate.js +33 -2
  74. package/dist/render/framework-commands.js +4 -0
  75. package/dist/scope/acceptance-evidence.d.ts +76 -0
  76. package/dist/scope/acceptance-evidence.js +348 -0
  77. package/dist/scope/coverage-map.d.ts +98 -0
  78. package/dist/scope/coverage-map.js +558 -0
  79. package/dist/scope/decompose.js +125 -11
  80. package/dist/scope/index.d.ts +3 -0
  81. package/dist/scope/index.js +3 -0
  82. package/dist/scope/parent-lineage.d.ts +125 -0
  83. package/dist/scope/parent-lineage.js +626 -0
  84. package/dist/scope/transition.d.ts +8 -0
  85. package/dist/scope/transition.js +61 -2
  86. package/dist/session/active-cli.d.ts +79 -0
  87. package/dist/session/active-cli.js +382 -0
  88. package/dist/session/effort-budget.d.ts +130 -0
  89. package/dist/session/effort-budget.js +334 -0
  90. package/dist/session/index.d.ts +2 -0
  91. package/dist/session/index.js +2 -0
  92. package/dist/session/session-start.d.ts +6 -0
  93. package/dist/session/session-start.js +53 -2
  94. package/dist/session/verify-session-ritual.d.ts +14 -0
  95. package/dist/session/verify-session-ritual.js +33 -0
  96. package/dist/story-ready/evaluate.d.ts +11 -0
  97. package/dist/story-ready/evaluate.js +41 -3
  98. package/dist/triage/actions/candidates-log.d.ts +1 -2
  99. package/dist/triage/actions/candidates-log.js +37 -6
  100. package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
  101. package/dist/xbrief-migrate/migrate-project.js +92 -3
  102. package/package.json +7 -3
@@ -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";
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";
@@ -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. */
@@ -4,6 +4,7 @@ import { cacheGet } from "../cache/operations.js";
4
4
  import { scan } from "../cache/scanner.js";
5
5
  import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
6
6
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
7
+ import { captureAndAttachLiteralAcceptance } from "../literal-acceptance/index.js";
7
8
  import { call } from "../scm/call.js";
8
9
  import { resolveProjectRoot } from "../scope/project-context.js";
9
10
  import { resolveProjectRepo } from "../slice/project-context.js";
@@ -444,6 +445,34 @@ export function buildIssueVbrief(issue, status, repoUrl, options = {}) {
444
445
  }
445
446
  plan.references = references;
446
447
  }
448
+ // #3267: capture exact stated acceptance commands from the issue body at intake
449
+ // into plan.metadata.literal_acceptance_commands (source=task_statement, capture-only).
450
+ // Agents MUST promote exact strings into swarm.verify_commands before shell execution
451
+ // (Greptile P1: raw issue text must not auto-spawn). Not paraphrased.
452
+ if (bodyStr.length > 0) {
453
+ const intakeText = [title, bodyStr].filter((s) => s.length > 0).join("\n\n");
454
+ const attached = captureAndAttachLiteralAcceptance(plan, intakeText);
455
+ // Re-tag stored capture as task_statement so run refuses until promote.
456
+ const meta = attached.plan.metadata;
457
+ if (meta !== undefined && Array.isArray(meta.literal_acceptance_commands)) {
458
+ meta.literal_acceptance_commands = meta.literal_acceptance_commands.map((row) => ({ ...row, source: "task_statement" }));
459
+ // Do not copy untrusted issue text into swarm.verify_commands (executable).
460
+ const swarm = meta.swarm;
461
+ if (swarm !== undefined && Array.isArray(swarm.verify_commands)) {
462
+ // Keep only pre-existing verify_commands that were not just attached from capture.
463
+ // captureAndAttach merges captured into verify_commands — strip those that match
464
+ // task_statement captures so issue text cannot auto-execute.
465
+ const capturedSet = new Set(meta.literal_acceptance_commands
466
+ .map((r) => (typeof r.command === "string" ? r.command : ""))
467
+ .filter((s) => s.length > 0));
468
+ swarm.verify_commands = swarm.verify_commands.filter((c) => typeof c === "string" && !capturedSet.has(c));
469
+ if (swarm.verify_commands.length === 0) {
470
+ delete swarm.verify_commands;
471
+ }
472
+ }
473
+ }
474
+ Object.assign(plan, attached.plan);
475
+ }
447
476
  const infoRootKey = options.infoRootKey ?? LEGACY_INFO_ROOT_KEY;
448
477
  const infoVersion = options.infoVersion ?? EMITTED_VBRIEF_VERSION;
449
478
  const briefLabel = infoRootKey === MIGRATED_INFO_ROOT_KEY ? "xBRIEF" : "vBRIEF";
@@ -0,0 +1,60 @@
1
+ /** Item statuses that still represent unfinished checklist work (#2862 / #3240 / #3242). */
2
+ export declare const NON_TERMINAL_ITEM_STATUSES: Set<string>;
3
+ /**
4
+ * Terminal checklist statuses for completed-folder fail-closed (#3242).
5
+ * Align with #3240 non-terminal set: any valid item status that is not
6
+ * pending|proposed|running is terminal; unknown/invalid statuses fail closed.
7
+ */
8
+ export declare const TERMINAL_ITEM_STATUSES: Set<string>;
9
+ /** Statuses allowed under completed/ (D2/D13). */
10
+ export declare const COMPLETED_FOLDER_PLAN_STATUSES: ReadonlySet<string>;
11
+ export type CompletedConsistencyKind = "status_mismatch" | "open_items" | "unreadable";
12
+ export interface OpenItemHit {
13
+ readonly path: string;
14
+ readonly status: string;
15
+ readonly title: string;
16
+ }
17
+ export interface CompletedConsistencyFinding {
18
+ /** Lifecycle-relative path, e.g. `completed/foo.xbrief.json`. */
19
+ readonly relPath: string;
20
+ readonly planStatus: string;
21
+ readonly folder: "completed";
22
+ readonly kind: CompletedConsistencyKind;
23
+ /** Human detail including exact path + statuses. */
24
+ readonly detail: string;
25
+ readonly openItems?: readonly OpenItemHit[];
26
+ }
27
+ export interface CompletedConsistencyResult {
28
+ readonly ok: boolean;
29
+ readonly findings: readonly CompletedConsistencyFinding[];
30
+ readonly message: string;
31
+ }
32
+ /**
33
+ * Collect non-terminal plan.items / subItems / nested items.
34
+ * Fail-closed on malformed slots: non-object entries and present non-array
35
+ * nested collections are reported so completed corpus cannot green-skip them.
36
+ */
37
+ export declare function collectOpenPlanItems(items: unknown, pathPrefix?: string): OpenItemHit[];
38
+ /**
39
+ * Evaluate Q4 consistency for a single plan that claims completed lifecycle.
40
+ *
41
+ * @param plan - plan object
42
+ * @param options.relPath - artifact path for messages (default `plan`)
43
+ * @param options.requireStatus - when set, plan.status must equal this exactly
44
+ * (scope:complete uses `completed`). When omitted, any completed-folder
45
+ * terminal status (completed|failed) is accepted.
46
+ */
47
+ export declare function evaluateCompletedPlanConsistency(plan: Record<string, unknown>, options?: {
48
+ relPath?: string;
49
+ requireStatus?: string;
50
+ }): CompletedConsistencyResult;
51
+ /** Format fail-closed doctor/complete message with exact paths and statuses. */
52
+ export declare function formatCompletedConsistencyFailure(findings: readonly CompletedConsistencyFinding[]): string;
53
+ /**
54
+ * Scan `xbrief/completed/` and/or legacy `vbrief/completed/` for Q4 mismatches.
55
+ * Offline / filesystem only. Missing completed/ is green (nothing to check).
56
+ * Malformed completed artifacts and unreadable roots fail closed (no silent skip).
57
+ * Mixed roots scan both.
58
+ */
59
+ export declare function scanCompletedLifecycleConsistency(projectRoot: string): CompletedConsistencyResult;
60
+ //# sourceMappingURL=completed-consistency.d.ts.map