@deftai/directive-core 0.100.0 → 0.102.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 (120) hide show
  1. package/dist/authz/classify.js +193 -27
  2. package/dist/authz/decompose-apply.d.ts +14 -0
  3. package/dist/authz/decompose-apply.js +93 -18
  4. package/dist/check/cached-orchestrator.d.ts +30 -5
  5. package/dist/check/cached-orchestrator.js +297 -10
  6. package/dist/check/cli-native-gates.d.ts +43 -0
  7. package/dist/check/cli-native-gates.js +84 -0
  8. package/dist/check/gate-lists.d.ts +10 -4
  9. package/dist/check/gate-lists.js +19 -5
  10. package/dist/check/index.d.ts +4 -2
  11. package/dist/check/index.js +3 -1
  12. package/dist/check/named-cause.d.ts +45 -0
  13. package/dist/check/named-cause.js +131 -0
  14. package/dist/check/orchestrator.js +6 -1
  15. package/dist/doctor/checks.d.ts +5 -6
  16. package/dist/doctor/checks.js +13 -56
  17. package/dist/doctor/help.d.ts +5 -0
  18. package/dist/doctor/help.js +31 -0
  19. package/dist/doctor/index.d.ts +2 -0
  20. package/dist/doctor/index.js +2 -0
  21. package/dist/doctor/main.js +58 -0
  22. package/dist/doctor/session-coda.d.ts +82 -0
  23. package/dist/doctor/session-coda.js +151 -0
  24. package/dist/doctor/types.d.ts +16 -0
  25. package/dist/eval/later-graduation.d.ts +39 -0
  26. package/dist/eval/later-graduation.js +91 -0
  27. package/dist/eval/report.d.ts +6 -0
  28. package/dist/eval/report.js +27 -8
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.js +3 -0
  31. package/dist/init-deposit/gitignore.js +2 -0
  32. package/dist/intake/issue-ingest.js +49 -0
  33. package/dist/policy/ac-pass-banking.d.ts +58 -0
  34. package/dist/policy/ac-pass-banking.js +150 -0
  35. package/dist/policy/ceremony-dial-escalation.d.ts +52 -0
  36. package/dist/policy/ceremony-dial-escalation.js +92 -0
  37. package/dist/policy/ceremony-dial.d.ts +32 -1
  38. package/dist/policy/ceremony-dial.js +128 -1
  39. package/dist/policy/check-resume.d.ts +9 -38
  40. package/dist/policy/check-resume.js +18 -156
  41. package/dist/policy/coverage-debt.d.ts +18 -37
  42. package/dist/policy/coverage-debt.js +38 -147
  43. package/dist/policy/index.d.ts +2 -1
  44. package/dist/policy/index.js +16 -2
  45. package/dist/preflight-cache/evaluate.d.ts +3 -0
  46. package/dist/preflight-cache/evaluate.js +3 -0
  47. package/dist/product-first-done-gate/acceptance.d.ts +25 -0
  48. package/dist/product-first-done-gate/acceptance.js +269 -0
  49. package/dist/product-first-done-gate/check-mode.d.ts +45 -0
  50. package/dist/product-first-done-gate/check-mode.js +148 -0
  51. package/dist/product-first-done-gate/empty-resolution.d.ts +26 -0
  52. package/dist/product-first-done-gate/empty-resolution.js +38 -0
  53. package/dist/product-first-done-gate/evaluate.d.ts +76 -0
  54. package/dist/product-first-done-gate/evaluate.js +411 -0
  55. package/dist/product-first-done-gate/index.d.ts +12 -0
  56. package/dist/product-first-done-gate/index.js +12 -0
  57. package/dist/product-first-done-gate/types.d.ts +67 -0
  58. package/dist/product-first-done-gate/types.js +54 -0
  59. package/dist/run-summary/emit.d.ts +66 -0
  60. package/dist/run-summary/emit.js +217 -0
  61. package/dist/run-summary/index.d.ts +5 -0
  62. package/dist/run-summary/index.js +5 -0
  63. package/dist/run-summary/path.d.ts +25 -0
  64. package/dist/run-summary/path.js +78 -0
  65. package/dist/run-summary/share.d.ts +25 -0
  66. package/dist/run-summary/share.js +106 -0
  67. package/dist/run-summary/types.d.ts +134 -0
  68. package/dist/run-summary/types.js +28 -0
  69. package/dist/scope/acceptance-activate-gate.d.ts +23 -0
  70. package/dist/scope/acceptance-activate-gate.js +70 -0
  71. package/dist/scope/acceptance-evidence.d.ts +34 -0
  72. package/dist/scope/acceptance-evidence.js +84 -10
  73. package/dist/scope/decompose.d.ts +5 -0
  74. package/dist/scope/decompose.js +11 -2
  75. package/dist/scope/index.d.ts +1 -0
  76. package/dist/scope/index.js +1 -0
  77. package/dist/scope/transition.js +5 -0
  78. package/dist/session/ac-pass-banking.d.ts +221 -0
  79. package/dist/session/ac-pass-banking.js +761 -0
  80. package/dist/session/deposit-sha.d.ts +49 -0
  81. package/dist/session/deposit-sha.js +121 -0
  82. package/dist/session/effort-budget.d.ts +6 -0
  83. package/dist/session/effort-budget.js +21 -3
  84. package/dist/session/index.d.ts +5 -1
  85. package/dist/session/index.js +5 -1
  86. package/dist/session/orientation-compression.d.ts +127 -0
  87. package/dist/session/orientation-compression.js +435 -0
  88. package/dist/session/orientation-state.d.ts +29 -0
  89. package/dist/session/orientation-state.js +85 -0
  90. package/dist/session/session-start.d.ts +31 -0
  91. package/dist/session/session-start.js +219 -22
  92. package/dist/session/toolchain-preflight.d.ts +79 -0
  93. package/dist/session/toolchain-preflight.js +211 -0
  94. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  95. package/dist/triage/bootstrap/gitignore.js +8 -2
  96. package/dist/triage/bootstrap/index.js +15 -3
  97. package/dist/triage/classify/index.d.ts +1 -0
  98. package/dist/triage/classify/index.js +2 -0
  99. package/dist/triage/classify/label-mirror.js +16 -0
  100. package/dist/triage/classify/mirror-discovery-tip.d.ts +95 -0
  101. package/dist/triage/classify/mirror-discovery-tip.js +234 -0
  102. package/dist/triage/reconcile/reconcile.js +28 -6
  103. package/dist/triage/summary/index.js +12 -4
  104. package/dist/triage/welcome/default-mode.js +16 -0
  105. package/dist/ts-check-lane/run-lane.js +21 -4
  106. package/dist/vbrief-validate/conformance.js +7 -0
  107. package/dist/verify-ac/clauses.d.ts +51 -0
  108. package/dist/verify-ac/clauses.js +490 -0
  109. package/dist/verify-ac/evaluate.d.ts +55 -0
  110. package/dist/verify-ac/evaluate.js +145 -0
  111. package/dist/verify-ac/flag.d.ts +35 -0
  112. package/dist/verify-ac/flag.js +104 -0
  113. package/dist/verify-ac/index.d.ts +10 -0
  114. package/dist/verify-ac/index.js +10 -0
  115. package/dist/vitest-runner/coverage-debt-teardown.js +16 -4
  116. package/package.json +15 -3
  117. package/dist/policy/coverage-check-resume-presets.d.ts +0 -46
  118. package/dist/policy/coverage-check-resume-presets.js +0 -228
  119. package/dist/session/coverage-check-resume-nudge.d.ts +0 -34
  120. package/dist/session/coverage-check-resume-nudge.js +0 -66
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Run-summary telemetry types (#3282).
3
+ *
4
+ * Event-driven JSONL lines for harness collection. Silent by default unless
5
+ * `DEFT_RUN_SUMMARY_PATH` is set (or the gitignored default path is writable).
6
+ */
7
+ export const RUN_SUMMARY_SCHEMA_VERSION = 1;
8
+ /** Env var that opts into / redirects run-summary JSONL (#3282). */
9
+ export const ENV_RUN_SUMMARY_PATH = "DEFT_RUN_SUMMARY_PATH";
10
+ /** Harness-supplied session tool/turn total stamped onto run-summary lines (#3320). */
11
+ export const ENV_TOTAL_TOOL_TURNS = "DEFT_TOTAL_TOOL_TURNS";
12
+ /** Repo-root default collectible path when gitignore coverage is present. */
13
+ export const DEFAULT_RUN_SUMMARY_BASENAME = ".deft-run-summary.json";
14
+ /** Prefix for stdout lines when `DEFT_RUN_SUMMARY_PATH=-`. */
15
+ export const RUN_SUMMARY_STDOUT_PREFIX = "DEFT-TLM:";
16
+ /** One warning when an explicitly requested write fails. */
17
+ export const RUN_SUMMARY_WRITE_WARNING = "[deft run-summary] failed to write DEFT_RUN_SUMMARY_PATH telemetry (fail-open; exit codes unchanged)";
18
+ export const RUN_SUMMARY_EVENT_KINDS = [
19
+ "session_start",
20
+ "dial_transition",
21
+ "dial_escalation_evaluation",
22
+ "check_invocation",
23
+ "tool_turn_denominator",
24
+ "verification",
25
+ "acceptance",
26
+ "acceptance_stamp",
27
+ ];
28
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * scope:activate refuses acceptance-shaped narratives without plan.acceptance (#3334).
3
+ *
4
+ * Agents author Test / AcceptanceCriteria / Verification prose in narratives
5
+ * and then skip the executable plan.acceptance block. Activation fails closed
6
+ * with one remediation: stamp the acceptance block.
7
+ */
8
+ export interface AcceptanceActivateHit {
9
+ readonly key: string;
10
+ }
11
+ export interface AcceptanceActivateGateResult {
12
+ readonly ok: boolean;
13
+ readonly message: string;
14
+ readonly hits: readonly AcceptanceActivateHit[];
15
+ }
16
+ /** Narrative keys that look like authored acceptance (non-empty string values). */
17
+ export declare function collectAcceptanceShapedNarrativeKeys(narratives: Record<string, unknown>): AcceptanceActivateHit[];
18
+ /**
19
+ * Fail closed when narratives carry acceptance-shaped keys and plan.acceptance
20
+ * is absent. A stamped (even empty/none_stated) block is enough.
21
+ */
22
+ export declare function evaluateAcceptanceActivateGate(plan: Record<string, unknown>): AcceptanceActivateGateResult;
23
+ //# sourceMappingURL=acceptance-activate-gate.d.ts.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * scope:activate refuses acceptance-shaped narratives without plan.acceptance (#3334).
3
+ *
4
+ * Agents author Test / AcceptanceCriteria / Verification prose in narratives
5
+ * and then skip the executable plan.acceptance block. Activation fails closed
6
+ * with one remediation: stamp the acceptance block.
7
+ */
8
+ import { validatePlanAcceptance } from "../product-first-done-gate/acceptance.js";
9
+ function asRecord(value) {
10
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
11
+ return value;
12
+ }
13
+ return null;
14
+ }
15
+ const ACCEPTANCE_SHAPED_KEYS = new Set(["test", "acceptancecriteria", "verification"]);
16
+ function normalizeNarrativeKey(key) {
17
+ return key.replace(/[\s_-]+/g, "").toLowerCase();
18
+ }
19
+ /** Narrative keys that look like authored acceptance (non-empty string values). */
20
+ export function collectAcceptanceShapedNarrativeKeys(narratives) {
21
+ const hits = [];
22
+ for (const key of Object.keys(narratives)) {
23
+ if (!ACCEPTANCE_SHAPED_KEYS.has(normalizeNarrativeKey(key))) {
24
+ continue;
25
+ }
26
+ const value = narratives[key];
27
+ if (typeof value !== "string" || value.trim().length === 0) {
28
+ continue;
29
+ }
30
+ hits.push({ key });
31
+ }
32
+ return hits;
33
+ }
34
+ /**
35
+ * Fail closed when narratives carry acceptance-shaped keys and plan.acceptance
36
+ * is absent. A stamped (even empty/none_stated) block is enough.
37
+ */
38
+ export function evaluateAcceptanceActivateGate(plan) {
39
+ const narratives = asRecord(plan.narratives);
40
+ if (narratives === null) {
41
+ return { ok: true, message: "", hits: [] };
42
+ }
43
+ const hits = collectAcceptanceShapedNarrativeKeys(narratives);
44
+ if (hits.length === 0) {
45
+ return { ok: true, message: "", hits: [] };
46
+ }
47
+ if (plan.acceptance !== undefined && plan.acceptance !== null) {
48
+ const schemaErrors = validatePlanAcceptance(plan.acceptance);
49
+ if (schemaErrors.length > 0) {
50
+ return {
51
+ ok: false,
52
+ message: `Refusing activate: plan.acceptance is present but invalid (#3334): ` +
53
+ `${schemaErrors.join("; ")}. Stamp a valid plan.acceptance block ` +
54
+ `(author commands or none_stated via intake capture / task issue:ingest).`,
55
+ hits,
56
+ };
57
+ }
58
+ return { ok: true, message: "", hits };
59
+ }
60
+ const keys = hits.map((h) => h.key).join(", ");
61
+ return {
62
+ ok: false,
63
+ message: `Refusing activate: narratives contain acceptance-shaped keys (${keys}) ` +
64
+ `but plan.acceptance is absent (#3334). Stamp plan.acceptance ` +
65
+ `(author commands or none_stated via intake capture / task issue:ingest) — ` +
66
+ `the criteria are in the wrong, non-executable field.`,
67
+ hits,
68
+ };
69
+ }
70
+ //# sourceMappingURL=acceptance-activate-gate.js.map
@@ -5,8 +5,18 @@
5
5
  * plan item needs either typed evidence or a human-origin disposition before complete
6
6
  * may advance it. Kind suitability: merge/review alone cannot satisfy smoke, UAT,
7
7
  * deploy, or observed_behavior requirements.
8
+ *
9
+ * Canonical item keys are namespaced under #1620 / #3305 (Option B):
10
+ * - plan.items[].x-directive/evidence
11
+ * - plan.items[].x-directive/disposition
12
+ * Bare `evidence` / `disposition` are not valid typed evidence (no dual-read).
13
+ * ITEM_CORE is not expanded with bare keys; verify:vbrief-conformance rejects them.
8
14
  */
9
15
  import type { GrantOrigin } from "../authz/types.js";
16
+ /** Canonical namespaced key for typed acceptance evidence (#3305 / #1620). */
17
+ export declare const ACCEPTANCE_EVIDENCE_KEY: "x-directive/evidence";
18
+ /** Canonical namespaced key for human-origin disposition (#3305 / #1620). */
19
+ export declare const ACCEPTANCE_DISPOSITION_KEY: "x-directive/disposition";
10
20
  /** Closed evidence kinds (locked Q3). */
11
21
  export declare const ACCEPTANCE_EVIDENCE_KINDS: readonly ["test", "review", "merge", "deploy", "smoke", "uat", "observed_behavior"];
12
22
  export type AcceptanceEvidenceKind = (typeof ACCEPTANCE_EVIDENCE_KINDS)[number];
@@ -64,6 +74,30 @@ export declare function inferRequiredStrictAxes(item: Record<string, unknown>):
64
74
  * or record a human-origin disposition (#3240 Greptile P1).
65
75
  */
66
76
  export declare function isEvidenceKindSuitable(kind: AcceptanceEvidenceKind, requiredAxes: readonly StrictAcceptanceAxis[]): boolean;
77
+ /**
78
+ * Read only namespaced acceptance fields (#3305). Bare `evidence` / `disposition`
79
+ * are never treated as success (no permanent dual-read).
80
+ */
81
+ export declare function readNamespacedAcceptanceFields(item: Record<string, unknown>): {
82
+ readonly evidence: unknown;
83
+ readonly disposition: unknown;
84
+ readonly hasEvidence: boolean;
85
+ readonly hasDisposition: boolean;
86
+ readonly hasBareEvidence: boolean;
87
+ readonly hasBareDisposition: boolean;
88
+ };
89
+ /**
90
+ * Stamp typed evidence under the canonical namespaced key only.
91
+ * Clears bare evidence/disposition and any prior namespaced disposition so the item
92
+ * cannot carry both acceptance fields after a replace (#3305 Greptile P2).
93
+ */
94
+ export declare function stampNamespacedEvidence(item: Record<string, unknown>, record: AcceptanceEvidenceRecord): void;
95
+ /**
96
+ * Stamp human-origin disposition under the canonical namespaced key only.
97
+ * Clears bare evidence/disposition and any prior namespaced evidence so the item
98
+ * cannot carry both acceptance fields after a replace (#3305 Greptile P2).
99
+ */
100
+ export declare function stampNamespacedDisposition(item: Record<string, unknown>, record: AcceptanceDispositionRecord): void;
67
101
  /**
68
102
  * Fail closed when any non-terminal plan item lacks suitable evidence or a human-origin disposition.
69
103
  */
@@ -5,8 +5,18 @@
5
5
  * plan item needs either typed evidence or a human-origin disposition before complete
6
6
  * may advance it. Kind suitability: merge/review alone cannot satisfy smoke, UAT,
7
7
  * deploy, or observed_behavior requirements.
8
+ *
9
+ * Canonical item keys are namespaced under #1620 / #3305 (Option B):
10
+ * - plan.items[].x-directive/evidence
11
+ * - plan.items[].x-directive/disposition
12
+ * Bare `evidence` / `disposition` are not valid typed evidence (no dual-read).
13
+ * ITEM_CORE is not expanded with bare keys; verify:vbrief-conformance rejects them.
8
14
  */
9
15
  import { isHumanOrigin } from "../authz/origin.js";
16
+ /** Canonical namespaced key for typed acceptance evidence (#3305 / #1620). */
17
+ export const ACCEPTANCE_EVIDENCE_KEY = "x-directive/evidence";
18
+ /** Canonical namespaced key for human-origin disposition (#3305 / #1620). */
19
+ export const ACCEPTANCE_DISPOSITION_KEY = "x-directive/disposition";
10
20
  /** Closed evidence kinds (locked Q3). */
11
21
  export const ACCEPTANCE_EVIDENCE_KINDS = [
12
22
  "test",
@@ -215,37 +225,100 @@ function itemLabel(item, path) {
215
225
  }
216
226
  return path;
217
227
  }
228
+ /**
229
+ * Read only namespaced acceptance fields (#3305). Bare `evidence` / `disposition`
230
+ * are never treated as success (no permanent dual-read).
231
+ */
232
+ export function readNamespacedAcceptanceFields(item) {
233
+ const evidence = item[ACCEPTANCE_EVIDENCE_KEY];
234
+ const disposition = item[ACCEPTANCE_DISPOSITION_KEY];
235
+ return {
236
+ evidence,
237
+ disposition,
238
+ hasEvidence: evidence !== undefined && evidence !== null,
239
+ hasDisposition: disposition !== undefined && disposition !== null,
240
+ hasBareEvidence: item.evidence !== undefined && item.evidence !== null,
241
+ hasBareDisposition: item.disposition !== undefined && item.disposition !== null,
242
+ };
243
+ }
244
+ /** Clear bare and namespaced acceptance fields so stamps never leave dual/conflicting keys. */
245
+ function clearAcceptanceStampFields(item) {
246
+ delete item.evidence;
247
+ delete item.disposition;
248
+ delete item[ACCEPTANCE_EVIDENCE_KEY];
249
+ delete item[ACCEPTANCE_DISPOSITION_KEY];
250
+ }
251
+ /**
252
+ * Stamp typed evidence under the canonical namespaced key only.
253
+ * Clears bare evidence/disposition and any prior namespaced disposition so the item
254
+ * cannot carry both acceptance fields after a replace (#3305 Greptile P2).
255
+ */
256
+ export function stampNamespacedEvidence(item, record) {
257
+ clearAcceptanceStampFields(item);
258
+ item[ACCEPTANCE_EVIDENCE_KEY] = {
259
+ kind: record.kind,
260
+ pointer: record.pointer,
261
+ recorded_at: record.recorded_at,
262
+ recorded_by: record.recorded_by,
263
+ };
264
+ }
265
+ /**
266
+ * Stamp human-origin disposition under the canonical namespaced key only.
267
+ * Clears bare evidence/disposition and any prior namespaced evidence so the item
268
+ * cannot carry both acceptance fields after a replace (#3305 Greptile P2).
269
+ */
270
+ export function stampNamespacedDisposition(item, record) {
271
+ clearAcceptanceStampFields(item);
272
+ const body = {
273
+ disposition: record.disposition,
274
+ reason: record.reason,
275
+ provenance: { ...record.provenance },
276
+ recorded_at: record.recorded_at,
277
+ };
278
+ if (record.resume_when !== undefined) {
279
+ body.resume_when = record.resume_when;
280
+ }
281
+ item[ACCEPTANCE_DISPOSITION_KEY] = body;
282
+ }
218
283
  function evaluateOneItem(item, path) {
219
284
  const title = itemLabel(item, path);
220
285
  const status = String(item.status ?? "");
221
286
  if (!NON_TERMINAL_ITEM_STATUSES.has(status)) {
287
+ // Already-terminal: complete does not re-validate typed evidence (#3240 / #3305).
288
+ // Suitability/provenance apply only when advancing non-terminal items. Pre-marking
289
+ // items completed with narrative-only fields still skips the typed gate — that is
290
+ // intentional for fail/cancel and historical terminals, not a silent dual success path.
222
291
  return {
223
292
  path,
224
293
  title,
225
294
  outcome: "already_terminal",
226
- detail: `status=${status || "(empty)"} (not advanced)`,
295
+ detail: `status=${status || "(empty)"} (not advanced; typed evidence not re-checked)`,
227
296
  };
228
297
  }
229
- const hasEvidence = item.evidence !== undefined && item.evidence !== null;
230
- const hasDisposition = item.disposition !== undefined && item.disposition !== null;
298
+ const fields = readNamespacedAcceptanceFields(item);
299
+ const { hasEvidence, hasDisposition, hasBareEvidence, hasBareDisposition } = fields;
231
300
  if (hasEvidence && hasDisposition) {
232
301
  return {
233
302
  path,
234
303
  title,
235
304
  outcome: "invalid",
236
- detail: "exactly one of evidence or disposition required; both present",
305
+ detail: `exactly one of ${ACCEPTANCE_EVIDENCE_KEY} or ${ACCEPTANCE_DISPOSITION_KEY} required; both present`,
237
306
  };
238
307
  }
239
308
  if (!hasEvidence && !hasDisposition) {
309
+ const bareHint = hasBareEvidence || hasBareDisposition
310
+ ? ` bare evidence/disposition ignored — use ${ACCEPTANCE_EVIDENCE_KEY} or ${ACCEPTANCE_DISPOSITION_KEY} (#3305);`
311
+ : "";
240
312
  return {
241
313
  path,
242
314
  title,
243
315
  outcome: "missing",
244
- detail: "no evidence or disposition; scope:complete refuses to auto-complete (#3240)",
316
+ detail: `no ${ACCEPTANCE_EVIDENCE_KEY} or ${ACCEPTANCE_DISPOSITION_KEY};` +
317
+ `${bareHint} scope:complete refuses to auto-complete (#3240)`,
245
318
  };
246
319
  }
247
320
  if (hasDisposition) {
248
- const parsed = parseDisposition(item.disposition);
321
+ const parsed = parseDisposition(fields.disposition);
249
322
  if (!parsed.ok) {
250
323
  return { path, title, outcome: "invalid", detail: parsed.message };
251
324
  }
@@ -257,7 +330,7 @@ function evaluateOneItem(item, path) {
257
330
  disposition: parsed.record,
258
331
  };
259
332
  }
260
- const parsed = parseEvidence(item.evidence);
333
+ const parsed = parseEvidence(fields.evidence);
261
334
  if (!parsed.ok) {
262
335
  return { path, title, outcome: "invalid", detail: parsed.message };
263
336
  }
@@ -315,12 +388,13 @@ export function evaluateAcceptanceEvidenceGate(plan) {
315
388
  const lines = blockers.map((r) => ` - ${r.path} "${r.title}": ${r.detail}`);
316
389
  return {
317
390
  ok: false,
318
- message: `Acceptance evidence required for scope:complete (#3240). ` +
391
+ message: `Acceptance evidence required for scope:complete (#3240 / #3305). ` +
319
392
  `${blockers.length} criterion/criteria missing suitable evidence or disposition:\n` +
320
393
  `${lines.join("\n")}\n` +
321
- `Each non-terminal plan item needs evidence ` +
394
+ `Each non-terminal plan item needs ${ACCEPTANCE_EVIDENCE_KEY} ` +
322
395
  `{kind: test|review|merge|deploy|smoke|uat|observed_behavior, pointer, recorded_at, recorded_by} ` +
323
- `or disposition {disposition: waived|deferred|not_applicable, reason, provenance (human-origin), recorded_at}. ` +
396
+ `or ${ACCEPTANCE_DISPOSITION_KEY} {disposition: waived|deferred|not_applicable, reason, provenance (human-origin), recorded_at}. ` +
397
+ `Bare evidence/disposition keys are not valid (#1620 / #3305). ` +
324
398
  `merge/review alone cannot satisfy smoke|uat|deploy|observed_behavior criteria.`,
325
399
  reports,
326
400
  };
@@ -34,6 +34,11 @@ export interface ApplyDecompositionOptions {
34
34
  draftPath: string;
35
35
  checkOnly: boolean;
36
36
  date: string;
37
+ /**
38
+ * Optional GitHub-style repo (owner/name). When omitted, resolved via
39
+ * DEFT_TRIAGE_REPO / git remote origin so repo-bound structural grants match (#3291).
40
+ */
41
+ repo?: string | null;
37
42
  }
38
43
  export declare function applyDecomposition(opts: ApplyDecompositionOptions): string[];
39
44
  /** CLI entry for scope-decompose (mirrors scope_decompose.py main()). */
@@ -14,6 +14,7 @@ import { claimSingleUseGrantForApply } from "../authz/store.js";
14
14
  import { containedWrite } from "../fs/contained-write.js";
15
15
  import { ProjectionContainmentError } from "../fs/projection-containment.js";
16
16
  import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, resolveLifecycleRoot, } from "../layout/resolve.js";
17
+ import { resolveRepo } from "../triage/queue/repo.js";
17
18
  import { referenceWithDefaultTrust, slugify } from "../vbrief-build/build.js";
18
19
  import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
19
20
  import { formatCoverageReportLine, validateCoverageMap } from "./coverage-map.js";
@@ -828,6 +829,9 @@ function buildChildVbrief(opts) {
828
829
  }
829
830
  export function applyDecomposition(opts) {
830
831
  const { projectRoot, parentPath, draftPath, checkOnly, date } = opts;
832
+ // Repo identity for structural apply grants that pin scope.repo (#3291).
833
+ const explicitRepo = (opts.repo ?? "").trim();
834
+ const repo = explicitRepo.length > 0 ? explicitRepo : resolveRepo(null, projectRoot);
831
835
  const vbriefDirPath = vbriefDir(projectRoot);
832
836
  const parent = loadJson(parentPath);
833
837
  // Single draft read: digest must bind the exact bytes used to build child docs (#3239 P1).
@@ -929,13 +933,15 @@ export function applyDecomposition(opts) {
929
933
  }
930
934
  if (checkOnly)
931
935
  return actions;
932
- // #3239: structural apply requires human-origin grant bound to exact draft digest
933
- // from the single load above. --check is ungated (returned earlier).
936
+ // #3239 / #3291: structural apply requires human-origin grant bound to exact draft digest
937
+ // from the single load above. Thread repo into both evaluator calls so repo-bound grants
938
+ // match. --check is ungated (returned earlier).
934
939
  const authz = evaluateDecomposeStructuralApply({
935
940
  projectRoot,
936
941
  parentPath,
937
942
  draftPath,
938
943
  draftDigest,
944
+ repo,
939
945
  });
940
946
  if (!authz.allowed) {
941
947
  throw new DecompositionError(authz.reason);
@@ -1015,12 +1021,15 @@ export function applyDecomposition(opts) {
1015
1021
  if (authz.humanApprovalRef !== null) {
1016
1022
  const claim = claimSingleUseGrantForApply(projectRoot, authz.humanApprovalRef, {
1017
1023
  revalidate: (grant) => {
1024
+ // Same repo binding as the initial check (#3291) — under-lock revalidation must not
1025
+ // silently drop project identity or a repo-bound grant would fail closed incorrectly.
1018
1026
  const again = evaluateDecomposeStructuralApply({
1019
1027
  projectRoot,
1020
1028
  parentPath,
1021
1029
  draftPath,
1022
1030
  draftDigest,
1023
1031
  grants: [grant],
1032
+ repo,
1024
1033
  });
1025
1034
  return again.allowed ? { ok: true } : { ok: false, reason: again.reason };
1026
1035
  },
@@ -1,3 +1,4 @@
1
+ export * from "./acceptance-activate-gate.js";
1
2
  export * from "./acceptance-evidence.js";
2
3
  export * from "./audit-log.js";
3
4
  export * from "./batch-promote.js";
@@ -1,3 +1,4 @@
1
+ export * from "./acceptance-activate-gate.js";
1
2
  export * from "./acceptance-evidence.js";
2
3
  export * from "./audit-log.js";
3
4
  export * from "./batch-promote.js";
@@ -5,6 +5,7 @@ import { assertProjectionContained, ProjectionContainmentError, } from "../fs/pr
5
5
  import { hasArtifactSuffix } from "../layout/resolve.js";
6
6
  import { evaluateCompletedPlanConsistency } from "../lifecycle/completed-consistency.js";
7
7
  import { evaluateLiteralAcceptanceFromPlan } from "../literal-acceptance/index.js";
8
+ import { evaluateAcceptanceActivateGate } from "./acceptance-activate-gate.js";
8
9
  import { evaluateAcceptanceEvidenceGate, formatAcceptanceCompletionListing, } from "./acceptance-evidence.js";
9
10
  import { append, canonicalLogPath, newDecisionId } from "./audit-log.js";
10
11
  import { atomicWriteBrief, formatBriefJson, readBriefForMutation } from "./brief-io.js";
@@ -148,6 +149,10 @@ export function runTransition(action, filePath, now = new Date(), options = {})
148
149
  if (!effortGate.ok) {
149
150
  return { ok: false, message: effortGate.message };
150
151
  }
152
+ const acceptanceGate = evaluateAcceptanceActivateGate(planObj);
153
+ if (!acceptanceGate.ok) {
154
+ return { ok: false, message: acceptanceGate.message };
155
+ }
151
156
  }
152
157
  // #3041: fail closed before mutating a code-bearing complete without delivery evidence.
153
158
  if (act === "complete") {
@@ -0,0 +1,221 @@
1
+ /**
2
+ * AC-pass banking checkpoint (#3285).
3
+ *
4
+ * When stated acceptance criteria first pass, FINALIZE: record a durable bank
5
+ * checkpoint, optionally append a bank-event to the run summary, and refuse
6
+ * self-imposed deepening unless remaining budget meets the surplus threshold
7
+ * (default 20% of max turns/cost). Post-bank out-of-scope findings are
8
+ * reported, not chased, when surplus is insufficient.
9
+ *
10
+ * Sharpens #3266 bank-the-pass. Composes product-first done-gate (#3284) and
11
+ * optional DEFT_RUN_SUMMARY_PATH telemetry (#3282 hook, fail-open).
12
+ */
13
+ import { type AcPassBankingConfig } from "../policy/ac-pass-banking.js";
14
+ import type { HardEffortBudget, VerificationDepthPolicy } from "./effort-budget.js";
15
+ /** Durable bank ledger directory (gitignored under `.deft/`). */
16
+ export declare const AC_PASS_BANK_DIR = ".deft/ac-pass-banks";
17
+ export declare const AC_PASS_BANK_SCHEMA_VERSION: 1;
18
+ /** Env path for optional JSONL run summary (#3282). Unset = silent. */
19
+ export declare const ENV_RUN_SUMMARY_PATH = "DEFT_RUN_SUMMARY_PATH";
20
+ export type PostBankFindingAction = "report" | "chase" | "fix-regression";
21
+ export type AcPassNextAction = "still_open" | "finalize_and_ship" | "finalize_and_deepen";
22
+ export interface SurplusEvaluation {
23
+ readonly hasSurplus: boolean;
24
+ /** Remaining / max when both known; null when unknown. */
25
+ readonly remainingFraction: number | null;
26
+ readonly surplusThreshold: number;
27
+ readonly axis: "turns" | "cost" | "both" | "unknown";
28
+ readonly reason: string;
29
+ }
30
+ export interface PostBankFinding {
31
+ readonly summary: string;
32
+ readonly action: PostBankFindingAction;
33
+ readonly recordedAt: string;
34
+ readonly regressesStatedAc: boolean;
35
+ }
36
+ export interface AcPassBankRecord {
37
+ readonly schemaVersion: typeof AC_PASS_BANK_SCHEMA_VERSION;
38
+ readonly scopeId: string;
39
+ readonly bankedAt: string;
40
+ readonly headSha: string | null;
41
+ readonly remainingTurns: number | null;
42
+ readonly remainingBudget: number | null;
43
+ readonly maxTurns: number | null;
44
+ readonly maxBudget: number | null;
45
+ readonly surplusThreshold: number;
46
+ readonly hadSurplus: boolean;
47
+ readonly nextAction: Exclude<AcPassNextAction, "still_open">;
48
+ readonly postBankFindings: readonly PostBankFinding[];
49
+ }
50
+ export interface EvaluateSurplusInput {
51
+ readonly budget: HardEffortBudget;
52
+ /** Fraction 0–1; default from policy / {@link DEFAULT_SURPLUS_THRESHOLD}. */
53
+ readonly surplusThreshold?: number;
54
+ }
55
+ export interface EvaluateAcPassBankingInput {
56
+ readonly budget: HardEffortBudget;
57
+ /** Whether stated AC (verify:ac / official checker) is met. */
58
+ readonly statedAcceptanceMet: boolean;
59
+ readonly config?: Partial<AcPassBankingConfig>;
60
+ /** Absolute turn reserve floor (still applied; #3266). Default 3. */
61
+ readonly deepenReserveTurns?: number;
62
+ readonly deepenReserveBudget?: number | null;
63
+ }
64
+ export interface AcPassBankingDecision {
65
+ readonly nextAction: AcPassNextAction;
66
+ readonly surplus: SurplusEvaluation;
67
+ readonly depthPolicy: VerificationDepthPolicy;
68
+ readonly deepeningAllowed: boolean;
69
+ readonly notes: readonly string[];
70
+ }
71
+ export interface BankAcPassInput {
72
+ readonly projectRoot: string;
73
+ readonly scopeId: string;
74
+ readonly budget: HardEffortBudget;
75
+ readonly surplus: SurplusEvaluation;
76
+ readonly nextAction: Exclude<AcPassNextAction, "still_open">;
77
+ readonly headSha?: string | null;
78
+ readonly now?: string;
79
+ readonly environ?: Readonly<Record<string, string | undefined>>;
80
+ }
81
+ export interface DecidePostBankFindingInput {
82
+ readonly findingSummary: string;
83
+ readonly regressesStatedAc: boolean;
84
+ readonly hasSurplus: boolean;
85
+ readonly now?: string;
86
+ }
87
+ /**
88
+ * Evaluate whether remaining budget meets the surplus threshold (#3285).
89
+ *
90
+ * When max is known: remaining/max >= threshold on every known axis.
91
+ * When only remaining is known (no max): surplus is false (fail-safe) —
92
+ * absolute #3266 reserve still applies separately via recommendVerificationDepth.
93
+ */
94
+ export declare function evaluateSurplus(input: EvaluateSurplusInput): SurplusEvaluation;
95
+ /**
96
+ * Full AC-pass banking decision: still open / finalize+ship / finalize+deepen.
97
+ */
98
+ export declare function evaluateAcPassBanking(input: EvaluateAcPassBankingInput): AcPassBankingDecision;
99
+ /**
100
+ * Post-bank finding disposition: report (default) vs chase (only with surplus)
101
+ * vs fix-regression (always when stated AC regressed).
102
+ */
103
+ export declare function decidePostBankFinding(input: DecidePostBankFindingInput): PostBankFinding;
104
+ export declare function acPassBanksDir(projectRoot: string): string;
105
+ /**
106
+ * Sanitize scopeId for a ledger filename without unbounded quantifiers (CodeQL).
107
+ * Walks characters once — O(n), no ReDoS risk on pathological "-" runs.
108
+ */
109
+ export declare function sanitizeScopeIdForFilename(scopeId: string): string;
110
+ /** Stable filename for a scope bank record. */
111
+ export declare function acPassBankFilename(scopeId: string): string;
112
+ export declare function acPassBankPath(projectRoot: string, scopeId: string): string;
113
+ /**
114
+ * Persist a bank checkpoint that survives session death (#3285).
115
+ * Also appends a bank-event to DEFT_RUN_SUMMARY_PATH when set (fail-open).
116
+ */
117
+ /** Append-only findings journal (never truncated by re-bank). */
118
+ export declare function acPassFindingsJournalPath(projectRoot: string, scopeId: string): string;
119
+ export declare function bankAcPass(input: BankAcPassInput): AcPassBankRecord;
120
+ /** Recover complete JSON objects from a (possibly truncated) array body. */
121
+ export declare function recoverCompleteJsonObjects(arrayBody: string): readonly unknown[];
122
+ export declare function recoverFindingsFromLedgerText(raw: string): readonly PostBankFinding[];
123
+ /**
124
+ * Load bank record. When both primary and .tmp exist, prefer a fully-valid
125
+ * JSON primary over a partial tmp, and union recovered findings so partial
126
+ * recovery cannot erase durable history (#3285 Greptile residual).
127
+ */
128
+ export declare function readAcPassBank(projectRoot: string, scopeId: string): AcPassBankRecord | null;
129
+ /**
130
+ * Append a post-bank finding to an existing bank record (report/chase/fix).
131
+ */
132
+ export declare function recordPostBankFinding(projectRoot: string, scopeId: string, finding: PostBankFinding): AcPassBankRecord | null;
133
+ export interface BankEventPayload {
134
+ readonly type: "ac_pass_bank" | "ac_pass_post_bank_finding" | "ac_pass_deepening_skipped";
135
+ readonly schemaVersion: number;
136
+ readonly scopeId?: string;
137
+ readonly bankedAt?: string;
138
+ readonly nextAction?: string;
139
+ readonly hadSurplus?: boolean;
140
+ readonly surplusThreshold?: number;
141
+ readonly remainingFraction?: number | null;
142
+ readonly remainingTurns?: number | null;
143
+ readonly remainingBudget?: number | null;
144
+ readonly maxTurns?: number | null;
145
+ readonly maxBudget?: number | null;
146
+ readonly headSha?: string | null;
147
+ readonly path?: string;
148
+ readonly finding?: PostBankFinding;
149
+ readonly note?: string;
150
+ readonly [key: string]: unknown;
151
+ }
152
+ /**
153
+ * Format one JSONL bank-event line for run-summary telemetry (#3285 / #3282).
154
+ */
155
+ export declare function formatBankEventLine(event: BankEventPayload): string;
156
+ /**
157
+ * Append bank event to DEFT_RUN_SUMMARY_PATH when set.
158
+ * Unset = silent. `-` = stdout. Failures warn once to stderr (fail-open).
159
+ */
160
+ export declare function appendBankEventToRunSummary(input: {
161
+ readonly environ?: Readonly<Record<string, string | undefined>>;
162
+ readonly event: BankEventPayload;
163
+ readonly writeLine?: (path: string, line: string) => void;
164
+ readonly warn?: (message: string) => void;
165
+ }): {
166
+ written: boolean;
167
+ path: string | null;
168
+ };
169
+ /**
170
+ * Simulated run used by the surplus-insufficient acceptance test (#3285):
171
+ * AC passes → bank → extra checks would exceed budget → ship + report finding.
172
+ */
173
+ export declare function simulateSurplusInsufficientRun(input: {
174
+ readonly projectRoot: string;
175
+ readonly scopeId: string;
176
+ readonly budget: HardEffortBudget;
177
+ readonly findingSummary: string;
178
+ readonly surplusThreshold?: number;
179
+ readonly headSha?: string | null;
180
+ readonly now?: string;
181
+ readonly environ?: Readonly<Record<string, string | undefined>>;
182
+ }): {
183
+ readonly decision: AcPassBankingDecision;
184
+ readonly bank: AcPassBankRecord;
185
+ readonly finding: PostBankFinding;
186
+ readonly shipped: boolean;
187
+ readonly findingAction: PostBankFindingAction;
188
+ };
189
+ /**
190
+ * Resolve policy config for a project root (typed → env → default).
191
+ */
192
+ export declare function resolveBankingConfigForRoot(projectRoot?: string | null, environ?: Readonly<Record<string, string | undefined>>): AcPassBankingConfig;
193
+ export interface MaybeBankOnAcPassInput {
194
+ readonly projectRoot: string;
195
+ /** plan.id or path-derived scope key. */
196
+ readonly scopeId: string;
197
+ readonly budget?: HardEffortBudget;
198
+ readonly config?: Partial<AcPassBankingConfig>;
199
+ readonly headSha?: string | null;
200
+ readonly now?: string;
201
+ readonly environ?: Readonly<Record<string, string | undefined>>;
202
+ /**
203
+ * Only bank when real acceptance commands executed and passed.
204
+ * Soft/advisory passes with zero runs do not bank.
205
+ */
206
+ readonly executableRuns: number;
207
+ readonly quiet?: boolean;
208
+ }
209
+ export interface MaybeBankOnAcPassResult {
210
+ readonly banked: boolean;
211
+ readonly decision: AcPassBankingDecision | null;
212
+ readonly bank: AcPassBankRecord | null;
213
+ readonly notes: readonly string[];
214
+ }
215
+ /**
216
+ * Production bridge (#3285 Greptile P1): after verify:ac reports executable
217
+ * success, FINALIZE the bank checkpoint + surplus decision.
218
+ * Throws on I/O failure so verify:ac can fail closed (checkpoint mandatory).
219
+ */
220
+ export declare function maybeBankOnAcPass(input: MaybeBankOnAcPassInput): MaybeBankOnAcPassResult;
221
+ //# sourceMappingURL=ac-pass-banking.d.ts.map