@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,151 @@
1
+ /**
2
+ * Optional deterministic session coda for interactive doctor success (#2712).
3
+ *
4
+ * Human TTY affordance only: never JSON, never AGENTS/skills, never exit-code
5
+ * changes. Env three-state DEFT_SESSION_CODA (unset / 1 / 0). Copy lives in
6
+ * content/doctor/session-coda.json — not embedded as an engine literal.
7
+ */
8
+ import { readFileSync, realpathSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { contentRoot } from "../content-root.js";
11
+ /** Env var name (env-only v1; no USER.md key). */
12
+ export const SESSION_CODA_ENV = "DEFT_SESSION_CODA";
13
+ /**
14
+ * Fixed discoverability line when env is unset and gates pass (D3).
15
+ * Snapshot-stable; not selected from the pack; no ✦ prefix.
16
+ */
17
+ export const SESSION_CODA_OFF_HINT = "Session coda: off (set DEFT_SESSION_CODA=1 to enable)";
18
+ /** U+2726 FOUR RAYED STAR + space — stable; do not localize. */
19
+ export const SESSION_CODA_STAR_PREFIX = "\u2726 ";
20
+ /** Max body length; longer pack entries are skipped (fail-open). */
21
+ export const SESSION_CODA_MAX_CHARS = 100;
22
+ /** Relative path under contentRoot (source: content/doctor/…; deposit: doctor/…). */
23
+ export const SESSION_CODA_REL_PATH = join("doctor", "session-coda.json");
24
+ /**
25
+ * Map DEFT_SESSION_CODA to the three-state mode (D3).
26
+ * - undefined / "" → unset (discoverability off-hint)
27
+ * - "1" → on (✦ coda)
28
+ * - "0" → off (silent)
29
+ * - any other non-empty value → unset (show correct enable form)
30
+ */
31
+ export function sessionCodaMode(envValue) {
32
+ if (envValue === undefined || envValue === "")
33
+ return "unset";
34
+ if (envValue === "1")
35
+ return "on";
36
+ if (envValue === "0")
37
+ return "off";
38
+ return "unset";
39
+ }
40
+ /**
41
+ * Whether any extra line (off-hint or coda) is allowed (D1 + surface gates).
42
+ * TTY = stdout.isTTY; CI truthy blocks; json blocks; exitOk = errorCount === 0.
43
+ */
44
+ export function shouldEmitSessionCodaLine(input) {
45
+ return input.exitOk === true && input.tty === true && input.ci !== true && input.json !== true;
46
+ }
47
+ /**
48
+ * FNV-1a 32-bit (#2712). Documented choice for hash_u32(date + "\\0" + root).
49
+ * No Math.random; same inputs ⇒ same u32 on every platform.
50
+ */
51
+ export function hashU32(input) {
52
+ let hash = 0x811c9dc5;
53
+ for (let i = 0; i < input.length; i += 1) {
54
+ hash ^= input.charCodeAt(i);
55
+ hash = Math.imul(hash, 0x01000193);
56
+ }
57
+ return hash >>> 0;
58
+ }
59
+ /** UTC calendar date yyyy-mm-dd from a Date (stable for 24h across a team day). */
60
+ export function utcDateYmd(date) {
61
+ return date.toISOString().slice(0, 10);
62
+ }
63
+ /** Best-effort realpath; falls back to the given path when realpath fails. */
64
+ export function projectRootRealpath(projectRoot) {
65
+ try {
66
+ return realpathSync(projectRoot);
67
+ }
68
+ catch {
69
+ return projectRoot;
70
+ }
71
+ }
72
+ /**
73
+ * Select one coda body from the pack (enabled path only).
74
+ * index = hash_u32(utc_date + "\\0" + project_root_realpath) % len(codas)
75
+ * Empty list → null (omit ✦; doctor still 0).
76
+ */
77
+ export function selectSessionCoda(input) {
78
+ const { date, projectRoot, codas } = input;
79
+ if (codas.length === 0)
80
+ return null;
81
+ const key = `${date}\0${projectRoot}`;
82
+ const index = hashU32(key) % codas.length;
83
+ return codas[index] ?? null;
84
+ }
85
+ /** Skip non-strings and bodies over SESSION_CODA_MAX_CHARS. */
86
+ export function filterCodaEntries(raw) {
87
+ if (!Array.isArray(raw))
88
+ return [];
89
+ const out = [];
90
+ for (const entry of raw) {
91
+ if (typeof entry !== "string")
92
+ continue;
93
+ if (entry.length === 0 || entry.length > SESSION_CODA_MAX_CHARS)
94
+ continue;
95
+ out.push(entry);
96
+ }
97
+ return out;
98
+ }
99
+ /**
100
+ * Load + filter coda lines from content. Fail-open: missing/unreadable/invalid
101
+ * / empty after filter → [] (omit ✦; no error noise).
102
+ */
103
+ export function loadSessionCodas(frameworkRoot, readText) {
104
+ const packPath = join(contentRoot(frameworkRoot), SESSION_CODA_REL_PATH);
105
+ try {
106
+ const text = readText !== undefined ? readText(packPath) : readFileSync(packPath, { encoding: "utf8" });
107
+ if (text === null || text === "")
108
+ return [];
109
+ return filterCodaEntries(JSON.parse(text));
110
+ }
111
+ catch {
112
+ return [];
113
+ }
114
+ }
115
+ /** Format enabled coda line: ✦ + space + body. */
116
+ export function formatSessionCodaEnabledLine(body) {
117
+ return `${SESSION_CODA_STAR_PREFIX}${body}`;
118
+ }
119
+ /**
120
+ * Resolve the single extra line (or null) after final success footer.
121
+ * Does not print; caller places after blank line post footer (D2).
122
+ */
123
+ export function resolveSessionCodaLine(input) {
124
+ if (!input.shouldEmit)
125
+ return null;
126
+ if (input.mode === "off")
127
+ return null;
128
+ if (input.mode === "unset")
129
+ return SESSION_CODA_OFF_HINT;
130
+ const body = selectSessionCoda({
131
+ date: input.date,
132
+ projectRoot: input.projectRoot,
133
+ codas: input.codas,
134
+ });
135
+ if (body === null)
136
+ return null;
137
+ return formatSessionCodaEnabledLine(body);
138
+ }
139
+ /** Help / env-docs fragment for DEFT_SESSION_CODA three-state (#2712). */
140
+ export function sessionCodaHelpText() {
141
+ return [
142
+ "Environment:",
143
+ ` ${SESSION_CODA_ENV} Session coda after interactive success (exit 0, TTY, not CI, not --json):`,
144
+ " unset Print discoverability line:",
145
+ ` ${SESSION_CODA_OFF_HINT}`,
146
+ " =1 Print one curated line, e.g.:",
147
+ ` ${formatSessionCodaEnabledLine("Doctor first. Then continue.")}`,
148
+ " =0 Silent (no hint, no coda)",
149
+ ].join("\n");
150
+ }
151
+ //# sourceMappingURL=session-coda.js.map
@@ -123,6 +123,22 @@ export interface DoctorSeams {
123
123
  now?: Date;
124
124
  }) => string | null;
125
125
  readonly isTty?: () => boolean;
126
+ /**
127
+ * stdout TTY probe for session coda (#2712). Defaults to
128
+ * `process.stdout.isTTY === true` (not stdin). Separate from `isTty` (stdin
129
+ * used by --fix prompts).
130
+ */
131
+ readonly stdoutIsTty?: () => boolean;
132
+ /**
133
+ * Override DEFT_SESSION_CODA for tests (#2712). `undefined` means read
134
+ * process.env; pass `""` or omit via explicit nullish env simulation with
135
+ * `undefined` from a seam that returns undefined.
136
+ */
137
+ readonly sessionCodaEnv?: string | undefined;
138
+ /** Override CI env string for session coda gates (#2712). */
139
+ readonly ciEnv?: string | undefined;
140
+ /** Override session-coda pack loader for fail-open tests (#2712). */
141
+ readonly loadSessionCodas?: (frameworkRoot: string) => string[];
126
142
  readonly readYn?: (prompt: string, defaultYes: boolean) => boolean;
127
143
  readonly writeText?: (path: string, content: string) => void;
128
144
  readonly now?: () => Date;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * #3286 Later graduation trigger from a #3282 run summary (#3320).
3
+ *
4
+ * Named surface: eval:report prints graduate / do-not-graduate / unevaluable.
5
+ * A graduation or decline MUST cite an emitted share. Counts without a
6
+ * denominator are unevaluable — never invent a ratio.
7
+ */
8
+ import { type RitualGateShare } from "../run-summary/share.js";
9
+ /** #3286 Later trigger: graduate when ritual+gate share is at least this fraction. */
10
+ export declare const LATER_GRADUATION_SHARE_THRESHOLD = 0.25;
11
+ export type LaterGraduationVerdict = "graduate" | "do-not-graduate" | "unevaluable";
12
+ export interface LaterGraduationTrigger {
13
+ readonly evaluable: boolean;
14
+ readonly verdict: LaterGraduationVerdict;
15
+ readonly summary: string;
16
+ readonly ritualGateCount: number;
17
+ readonly totalToolTurns: number | null;
18
+ readonly share: number | null;
19
+ readonly threshold: number;
20
+ }
21
+ export interface EvaluateLaterGraduationOptions {
22
+ readonly projectRoot: string;
23
+ readonly runSummaryPath?: string;
24
+ readonly env?: NodeJS.ProcessEnv;
25
+ readonly threshold?: number;
26
+ }
27
+ export declare function laterGraduationFromShare(share: RitualGateShare, threshold?: number): LaterGraduationTrigger;
28
+ /**
29
+ * Resolve a readable run-summary path. Unset env + missing default file → null
30
+ * (fail-open / unevaluable). stdout dest (`-`) is not a file.
31
+ */
32
+ export declare function resolveRunSummaryReadPath(projectRoot: string, options?: {
33
+ readonly runSummaryPath?: string;
34
+ readonly env?: NodeJS.ProcessEnv;
35
+ }): string | null;
36
+ /** Evaluate #3286 Later from the run-summary file (or unevaluable). */
37
+ export declare function evaluateLaterGraduationTrigger(options: EvaluateLaterGraduationOptions): LaterGraduationTrigger;
38
+ export declare function formatLaterGraduationLine(trigger: LaterGraduationTrigger): string;
39
+ //# sourceMappingURL=later-graduation.d.ts.map
@@ -0,0 +1,91 @@
1
+ /**
2
+ * #3286 Later graduation trigger from a #3282 run summary (#3320).
3
+ *
4
+ * Named surface: eval:report prints graduate / do-not-graduate / unevaluable.
5
+ * A graduation or decline MUST cite an emitted share. Counts without a
6
+ * denominator are unevaluable — never invent a ratio.
7
+ */
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { isAbsolute, resolve } from "node:path";
10
+ import { computeRitualGateShare, parseRunSummaryJsonl, } from "../run-summary/share.js";
11
+ import { DEFAULT_RUN_SUMMARY_BASENAME, ENV_RUN_SUMMARY_PATH } from "../run-summary/types.js";
12
+ /** #3286 Later trigger: graduate when ritual+gate share is at least this fraction. */
13
+ export const LATER_GRADUATION_SHARE_THRESHOLD = 0.25;
14
+ function formatPercent(value) {
15
+ return `${(value * 100).toFixed(1)}%`;
16
+ }
17
+ export function laterGraduationFromShare(share, threshold = LATER_GRADUATION_SHARE_THRESHOLD) {
18
+ if (!share.evaluable || share.share === null || share.totalToolTurns === null) {
19
+ return {
20
+ evaluable: false,
21
+ verdict: "unevaluable",
22
+ summary: "trigger unevaluable (no tool/turn denominator on run summary)",
23
+ ritualGateCount: share.ritualGateCount,
24
+ totalToolTurns: share.totalToolTurns,
25
+ share: null,
26
+ threshold,
27
+ };
28
+ }
29
+ const graduate = share.share >= threshold;
30
+ const verdict = graduate ? "graduate" : "do-not-graduate";
31
+ return {
32
+ evaluable: true,
33
+ verdict,
34
+ summary: `ritual+gate share ${formatPercent(share.share)} (${share.ritualGateCount}/${share.totalToolTurns}) — ${verdict} (threshold ${formatPercent(threshold)})`,
35
+ ritualGateCount: share.ritualGateCount,
36
+ totalToolTurns: share.totalToolTurns,
37
+ share: share.share,
38
+ threshold,
39
+ };
40
+ }
41
+ /**
42
+ * Resolve a readable run-summary path. Unset env + missing default file → null
43
+ * (fail-open / unevaluable). stdout dest (`-`) is not a file.
44
+ */
45
+ export function resolveRunSummaryReadPath(projectRoot, options = {}) {
46
+ if (options.runSummaryPath !== undefined && options.runSummaryPath.trim().length > 0) {
47
+ return resolve(options.runSummaryPath.trim());
48
+ }
49
+ const env = options.env ?? process.env;
50
+ const raw = env[ENV_RUN_SUMMARY_PATH];
51
+ if (raw !== undefined && raw.trim() === "-") {
52
+ return null;
53
+ }
54
+ if (raw !== undefined && raw.trim().length > 0) {
55
+ const trimmed = raw.trim();
56
+ return isAbsolute(trimmed) ? resolve(trimmed) : resolve(projectRoot, trimmed);
57
+ }
58
+ const fallback = resolve(projectRoot, DEFAULT_RUN_SUMMARY_BASENAME);
59
+ return existsSync(fallback) ? fallback : null;
60
+ }
61
+ function readSummaryText(path) {
62
+ try {
63
+ if (!existsSync(path)) {
64
+ return null;
65
+ }
66
+ return readFileSync(path, "utf8");
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ /** Evaluate #3286 Later from the run-summary file (or unevaluable). */
73
+ export function evaluateLaterGraduationTrigger(options) {
74
+ const threshold = options.threshold ?? LATER_GRADUATION_SHARE_THRESHOLD;
75
+ const path = resolveRunSummaryReadPath(options.projectRoot, {
76
+ runSummaryPath: options.runSummaryPath,
77
+ env: options.env,
78
+ });
79
+ if (path === null) {
80
+ return laterGraduationFromShare({ evaluable: false, ritualGateCount: 0, totalToolTurns: null, share: null }, threshold);
81
+ }
82
+ const text = readSummaryText(path);
83
+ if (text === null) {
84
+ return laterGraduationFromShare({ evaluable: false, ritualGateCount: 0, totalToolTurns: null, share: null }, threshold);
85
+ }
86
+ return laterGraduationFromShare(computeRitualGateShare(parseRunSummaryJsonl(text)), threshold);
87
+ }
88
+ export function formatLaterGraduationLine(trigger) {
89
+ return `#3286 Later: ${trigger.summary}`;
90
+ }
91
+ //# sourceMappingURL=later-graduation.js.map
@@ -1,3 +1,4 @@
1
+ import { type LaterGraduationTrigger } from "./later-graduation.js";
1
2
  import { type GoldenRunRecord } from "./run.js";
2
3
  import { type CellVersionPurity, type MixedVersionPolicy } from "./version-pin.js";
3
4
  export declare const REPORT_SCHEMA_VERSION: 1;
@@ -39,6 +40,8 @@ export interface GoldenEvalReport {
39
40
  readonly holdoutTripwire: HoldoutTripwire;
40
41
  /** Cell-level framework version purity for the reported model (#3215). */
41
42
  readonly versionPurity: VersionPurityEvidence;
43
+ /** #3286 Later graduation trigger from the run-summary denominator (#3320). */
44
+ readonly laterGraduation: LaterGraduationTrigger;
42
45
  }
43
46
  export interface ReportGoldenEvalOptions {
44
47
  readonly projectRoot?: string;
@@ -50,6 +53,9 @@ export interface ReportGoldenEvalOptions {
50
53
  * ledger runs for this model disagree on framework version within a treatment.
51
54
  */
52
55
  readonly mixedVersionPolicy?: MixedVersionPolicy;
56
+ /** Override run-summary JSONL path (#3320). Default: DEFT_RUN_SUMMARY_PATH or repo default. */
57
+ readonly runSummaryPath?: string;
58
+ readonly env?: NodeJS.ProcessEnv;
53
59
  }
54
60
  export interface ReportGoldenEvalResult {
55
61
  readonly code: 0 | 1 | 2;
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { resolveEvalPath } from "../layout/resolve.js";
3
+ import { evaluateLaterGraduationTrigger, formatLaterGraduationLine, } from "./later-graduation.js";
3
4
  import { GOLDEN_RUNS_HISTORY_REL } from "./run.js";
4
5
  import { aggregateCellWithVersionPurity, evaluateLedgerVersionPurity, } from "./version-pin.js";
5
6
  export const REPORT_SCHEMA_VERSION = 1;
@@ -107,17 +108,33 @@ export function goldenRunToVersionedRun(record) {
107
108
  }
108
109
  /** Diff two directive versions with metric deltas and significance (#1703 Tier 2). */
109
110
  export function reportGoldenEval(options) {
110
- if (!options.championVersion.trim() || !options.challengerVersion.trim()) {
111
+ const projectRoot = options.projectRoot ?? process.cwd();
112
+ const laterGraduation = evaluateLaterGraduationTrigger({
113
+ projectRoot,
114
+ runSummaryPath: options.runSummaryPath,
115
+ env: options.env,
116
+ });
117
+ const laterLine = formatLaterGraduationLine(laterGraduation);
118
+ const championMissing = !options.championVersion.trim();
119
+ const challengerMissing = !options.challengerVersion.trim();
120
+ const modelMissing = !options.model.trim();
121
+ if (championMissing && challengerMissing && modelMissing) {
122
+ return {
123
+ code: 0,
124
+ report: null,
125
+ message: `eval:report ${laterLine}`,
126
+ };
127
+ }
128
+ if (championMissing || challengerMissing) {
111
129
  return {
112
130
  code: 2,
113
131
  report: null,
114
- message: "eval:report: --champion and --challenger are required",
132
+ message: `eval:report: --champion and --challenger are required\n ${laterLine}`,
115
133
  };
116
134
  }
117
- if (!options.model.trim()) {
118
- return { code: 2, report: null, message: "eval:report: --model is required" };
135
+ if (modelMissing) {
136
+ return { code: 2, report: null, message: `eval:report: --model is required\n ${laterLine}` };
119
137
  }
120
- const projectRoot = options.projectRoot ?? process.cwd();
121
138
  const policy = options.mixedVersionPolicy ?? "refuse";
122
139
  const records = readGoldenRuns(projectRoot);
123
140
  const modelRecords = records.filter((r) => r.model === options.model);
@@ -157,7 +174,7 @@ export function reportGoldenEval(options) {
157
174
  return {
158
175
  code: 1,
159
176
  report: null,
160
- message: `eval:report: mixed framework versions in treatment cell(s) — aggregation refused (#3215)\n ${versionPurity.summary}`,
177
+ message: `eval:report: mixed framework versions in treatment cell(s) — aggregation refused (#3215)\n ${versionPurity.summary}\n ${laterLine}`,
161
178
  };
162
179
  }
163
180
  const champion = findLatestGoldenRun(records, options.championVersion, options.model);
@@ -166,14 +183,14 @@ export function reportGoldenEval(options) {
166
183
  return {
167
184
  code: 1,
168
185
  report: null,
169
- message: `eval:report: no golden run found for champion v${options.championVersion} model=${options.model}`,
186
+ message: `eval:report: no golden run found for champion v${options.championVersion} model=${options.model}\n ${laterLine}`,
170
187
  };
171
188
  }
172
189
  if (challenger === null) {
173
190
  return {
174
191
  code: 1,
175
192
  report: null,
176
- message: `eval:report: no golden run found for challenger v${options.challengerVersion} model=${options.model}`,
193
+ message: `eval:report: no golden run found for challenger v${options.challengerVersion} model=${options.model}\n ${laterLine}`,
177
194
  };
178
195
  }
179
196
  const championPrimary = champion.results.filter((r) => !r.holdout);
@@ -208,6 +225,7 @@ export function reportGoldenEval(options) {
208
225
  deltas,
209
226
  holdoutTripwire,
210
227
  versionPurity,
228
+ laterGraduation,
211
229
  };
212
230
  const lines = [
213
231
  `eval:report champion=v${options.championVersion} challenger=v${options.challengerVersion} model=${options.model}`,
@@ -223,6 +241,7 @@ export function reportGoldenEval(options) {
223
241
  ` version purity: ${versionPurity.pure ? "ok" : "mixed"} — ${versionPurity.summary}`,
224
242
  ` champion cell pin: v${champion.frameworkVersionPin?.frameworkVersion ?? champion.directiveVersion}`,
225
243
  ` challenger cell pin: v${challenger.frameworkVersionPin?.frameworkVersion ?? challenger.directiveVersion}`,
244
+ ` ${laterLine}`,
226
245
  ];
227
246
  const code = holdoutTripwire.triggered ? 1 : 0;
228
247
  return { code, report, message: lines.join("\n") };
package/dist/index.d.ts CHANGED
@@ -48,12 +48,14 @@ export * as prMonitor from "./pr-monitor/index.js";
48
48
  export * as prProtectedIssues from "./pr-protected-issues/index.js";
49
49
  export * as prWaitMergeable from "./pr-wait-mergeable/index.js";
50
50
  export * as preflight from "./preflight/index.js";
51
+ export * as productFirstDoneGate from "./product-first-done-gate/index.js";
51
52
  export * as release from "./release/index.js";
52
53
  export * as releaseE2e from "./release-e2e/index.js";
53
54
  export * as releasePublish from "./release-publish/index.js";
54
55
  export * as releaseRollback from "./release-rollback/index.js";
55
56
  export * as render from "./render/index.js";
56
57
  export * as resolution from "./resolution/index.js";
58
+ export * as runSummary from "./run-summary/index.js";
57
59
  export * as scm from "./scm/index.js";
58
60
  export * as scope from "./scope/index.js";
59
61
  export * as scopeProvenance from "./scope-provenance/index.js";
@@ -72,6 +74,7 @@ export * as vbriefBuild from "./vbrief-build/index.js";
72
74
  export * as vbriefReconcile from "./vbrief-reconcile/index.js";
73
75
  export * as vbriefValidate from "./vbrief-validate/index.js";
74
76
  export * as vbriefValidation from "./vbrief-validation/index.js";
77
+ export * as verifyAc from "./verify-ac/index.js";
75
78
  export * as verifyEnv from "./verify-env/index.js";
76
79
  export * as verifySource from "./verify-source/index.js";
77
80
  export * as wipCap from "./wip-cap/index.js";
package/dist/index.js CHANGED
@@ -49,12 +49,14 @@ export * as prMonitor from "./pr-monitor/index.js";
49
49
  export * as prProtectedIssues from "./pr-protected-issues/index.js";
50
50
  export * as prWaitMergeable from "./pr-wait-mergeable/index.js";
51
51
  export * as preflight from "./preflight/index.js";
52
+ export * as productFirstDoneGate from "./product-first-done-gate/index.js";
52
53
  export * as release from "./release/index.js";
53
54
  export * as releaseE2e from "./release-e2e/index.js";
54
55
  export * as releasePublish from "./release-publish/index.js";
55
56
  export * as releaseRollback from "./release-rollback/index.js";
56
57
  export * as render from "./render/index.js";
57
58
  export * as resolution from "./resolution/index.js";
59
+ export * as runSummary from "./run-summary/index.js";
58
60
  export * as scm from "./scm/index.js";
59
61
  export * as scope from "./scope/index.js";
60
62
  export * as scopeProvenance from "./scope-provenance/index.js";
@@ -73,6 +75,7 @@ export * as vbriefBuild from "./vbrief-build/index.js";
73
75
  export * as vbriefReconcile from "./vbrief-reconcile/index.js";
74
76
  export * as vbriefValidate from "./vbrief-validate/index.js";
75
77
  export * as vbriefValidation from "./vbrief-validation/index.js";
78
+ export * as verifyAc from "./verify-ac/index.js";
76
79
  export * as verifyEnv from "./verify-env/index.js";
77
80
  export * as verifySource from "./verify-source/index.js";
78
81
  export * as wipCap from "./wip-cap/index.js";
@@ -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",
@@ -5,12 +5,15 @@ 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
7
  import { captureAndAttachLiteralAcceptance } from "../literal-acceptance/index.js";
8
+ import { stampAcceptanceFromLiteralCapture } from "../product-first-done-gate/index.js";
9
+ import { ENV_RUN_SUMMARY_PATH, RunSummaryEmitter } from "../run-summary/index.js";
8
10
  import { call } from "../scm/call.js";
9
11
  import { resolveProjectRoot } from "../scope/project-context.js";
10
12
  import { resolveProjectRepo } from "../slice/project-context.js";
11
13
  import { countMaintainerCurrentShapeComments, mapIssueCommentEntry, RAW_ISSUE_COMMENTS_KEY, selectCurrentShapeComment, } from "../umbrella-current-shape/index.js";
12
14
  import { slugify, TODAY } from "../vbrief-build/build.js";
13
15
  import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
16
+ import { stampDerivedClausesOnAcceptance } from "../verify-ac/clauses.js";
14
17
  import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, VBRIEF_VERSION, } from "../xbrief-migrate/constants.js";
15
18
  import { findAcHeading, parseCheckboxItems, parseListItems, sliceAcSection, stripCodeBlocks, stripFencedCodeBlocks, } from "./markdown-scanners.js";
16
19
  import { detectRepo, extractReferencesFromVbrief, fetchOpenIssues, GITHUB_ISSUE_REF_TYPES, LIFECYCLE_FOLDERS, parseIssueNumber, } from "./reconcile-issues.js";
@@ -472,6 +475,15 @@ export function buildIssueVbrief(issue, status, repoUrl, options = {}) {
472
475
  }
473
476
  }
474
477
  Object.assign(plan, attached.plan);
478
+ // #3284: stamp plan.acceptance from captured literals (stated or none_stated floor).
479
+ Object.assign(plan, stampAcceptanceFromLiteralCapture(plan));
480
+ // #3323: when no commands were stated, derive numbered clauses before product edit.
481
+ const derived = stampDerivedClausesOnAcceptance(plan, [title, overviewSource].filter((s) => s.length > 0).join("\n\n"));
482
+ Object.assign(plan, derived.plan);
483
+ }
484
+ else {
485
+ // No body: still record none_stated acceptance so absence is a decision.
486
+ Object.assign(plan, stampAcceptanceFromLiteralCapture(plan));
475
487
  }
476
488
  const infoRootKey = options.infoRootKey ?? LEGACY_INFO_ROOT_KEY;
477
489
  const infoVersion = options.infoVersion ?? EMITTED_VBRIEF_VERSION;
@@ -711,8 +723,45 @@ export function ingestOne(issue, options) {
711
723
  assertWriteTargetSafe(projectRoot, target);
712
724
  mkdirSync(folderPath, { recursive: true });
713
725
  writeFileSync(target, `${JSON.stringify(vbrief, null, 2)}\n`, "utf8");
726
+ emitIntakeAcceptanceStamp(projectRoot, vbrief.plan);
714
727
  return ["created", target, `CREATED ${folder}/${filename}`];
715
728
  }
729
+ function emitIntakeAcceptanceStamp(projectRoot, plan) {
730
+ const rec = plan !== null && typeof plan === "object" && !Array.isArray(plan)
731
+ ? plan
732
+ : null;
733
+ if (rec === null) {
734
+ return;
735
+ }
736
+ const acceptance = rec.acceptance !== null && typeof rec.acceptance === "object" && !Array.isArray(rec.acceptance)
737
+ ? rec.acceptance
738
+ : null;
739
+ if (acceptance === null) {
740
+ return;
741
+ }
742
+ const env = process.env;
743
+ const dest = env[ENV_RUN_SUMMARY_PATH];
744
+ if (dest === undefined || dest.trim().length === 0) {
745
+ return;
746
+ }
747
+ try {
748
+ const sessionId = typeof env.DEFT_SESSION_ID === "string" && env.DEFT_SESSION_ID.trim().length > 0
749
+ ? env.DEFT_SESSION_ID.trim()
750
+ : "issue-ingest";
751
+ const emitter = new RunSummaryEmitter({ projectRoot, sessionId, env });
752
+ const commands = Array.isArray(acceptance.commands) ? acceptance.commands.length : 0;
753
+ const clauses = Array.isArray(acceptance.clauses) ? acceptance.clauses.length : 0;
754
+ emitter.emitAcceptanceStamp({
755
+ rung: typeof acceptance.source_rung === "string" ? acceptance.source_rung : "project_floor",
756
+ none_stated: acceptance.none_stated === true,
757
+ command_count: commands,
758
+ clause_count: clauses,
759
+ });
760
+ }
761
+ catch {
762
+ // fail-open
763
+ }
764
+ }
716
765
  export function ingestBulk(issues, options) {
717
766
  let filtered = issues;
718
767
  if (options.label !== undefined && options.label !== null) {
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Typed plan.policy.acPassBanking (#3285).
3
+ *
4
+ * Surplus-threshold policy for post-AC-pass deepening: after the first
5
+ * stated-AC bank, agents may deepen only when remaining budget is at least
6
+ * this fraction of the hard max (default 0.2 = 20%).
7
+ *
8
+ * Composes #3266 bank-the-pass and product-first done-gate (#3284).
9
+ */
10
+ /** Canonical dotted path for policy:show / PROJECT-DEFINITION. */
11
+ export declare const FIELD_AC_PASS_BANKING = "plan.policy.acPassBanking";
12
+ /** CLI alias for `task policy:show --field=acPassBanking`. */
13
+ export declare const FIELD_AC_PASS_BANKING_CLI_ALIAS = "acPassBanking";
14
+ /**
15
+ * Default surplus fraction of max turns/cost that must remain after bank
16
+ * before self-imposed deepening is allowed (#3285).
17
+ */
18
+ export declare const DEFAULT_SURPLUS_THRESHOLD = 0.2;
19
+ /** Floor / ceiling for surplusThreshold (inclusive). */
20
+ export declare const SURPLUS_THRESHOLD_MIN = 0;
21
+ export declare const SURPLUS_THRESHOLD_MAX = 1;
22
+ /** Optional env override (fraction 0–1 or percent 0–100). */
23
+ export declare const ENV_SURPLUS_THRESHOLD = "DEFT_AC_PASS_SURPLUS_THRESHOLD";
24
+ export interface AcPassBankingConfig {
25
+ /**
26
+ * When false, surplus gate is inactive (unbounded / optional discipline only).
27
+ * Default true — the gate still only binds under a hard effort budget.
28
+ */
29
+ readonly enabled: boolean;
30
+ /**
31
+ * Minimum remaining fraction of max turns/cost required to deepen after bank.
32
+ * Default {@link DEFAULT_SURPLUS_THRESHOLD} (0.2).
33
+ */
34
+ readonly surplusThreshold: number;
35
+ }
36
+ export type AcPassBankingSource = "typed" | "env" | "default" | "default-on-error";
37
+ export interface AcPassBankingResolved extends AcPassBankingConfig {
38
+ readonly source: AcPassBankingSource;
39
+ readonly error: string | null;
40
+ }
41
+ /** Parse a 0–1 fraction or a 0–100 percent string/number into a fraction. */
42
+ export declare function parseSurplusThreshold(raw: unknown): number | null;
43
+ /** Validate a `plan.policy.acPassBanking` payload. */
44
+ export declare function validateAcPassBanking(value: unknown): string[];
45
+ /**
46
+ * Resolve surplus policy from typed plan.policy.acPassBanking, then env,
47
+ * then defaults (#3285).
48
+ */
49
+ export declare function resolveAcPassBanking(projectRoot?: string | null, environ?: Readonly<Record<string, string | undefined>>): AcPassBankingResolved;
50
+ export interface AcPassBankingPolicyField {
51
+ readonly name: typeof FIELD_AC_PASS_BANKING;
52
+ readonly current: AcPassBankingConfig;
53
+ readonly default: AcPassBankingConfig;
54
+ readonly source: string;
55
+ }
56
+ /** Inspector row for `task policy:show --field=acPassBanking` (#3285). */
57
+ export declare function inspectAcPassBanking(_data: Record<string, unknown> | null, projectRoot?: string): AcPassBankingPolicyField;
58
+ //# sourceMappingURL=ac-pass-banking.d.ts.map