@deftai/directive-core 0.105.0 → 0.107.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/dist/authz/classify.js +591 -34
  2. package/dist/check/gate-lists.js +2 -0
  3. package/dist/check/named-cause.js +1 -1
  4. package/dist/design-critique/exclusive-chip.d.ts +33 -0
  5. package/dist/design-critique/exclusive-chip.js +68 -0
  6. package/dist/design-critique/parent-audit.d.ts +59 -0
  7. package/dist/design-critique/parent-audit.js +121 -0
  8. package/dist/doctor/checks.d.ts +6 -0
  9. package/dist/doctor/checks.js +35 -0
  10. package/dist/forward-coverage/diff-coverage.d.ts +39 -0
  11. package/dist/forward-coverage/diff-coverage.js +112 -0
  12. package/dist/forward-coverage/diff-lines.d.ts +19 -0
  13. package/dist/forward-coverage/diff-lines.js +173 -0
  14. package/dist/forward-coverage/evaluate.d.ts +24 -9
  15. package/dist/forward-coverage/evaluate.js +125 -19
  16. package/dist/hooks/dest-form.d.ts +46 -0
  17. package/dist/hooks/dest-form.js +575 -0
  18. package/dist/hooks/dispatcher.d.ts +17 -1
  19. package/dist/hooks/dispatcher.js +82 -11
  20. package/dist/hooks/index.d.ts +1 -0
  21. package/dist/hooks/index.js +1 -0
  22. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  23. package/dist/init-deposit/agent-hooks.js +1 -1
  24. package/dist/intake/clause-derivation.d.ts +9 -0
  25. package/dist/intake/clause-derivation.js +30 -3
  26. package/dist/intake/github-auth-modes-cli.js +14 -2
  27. package/dist/intake/github-auth-modes.d.ts +54 -9
  28. package/dist/intake/github-auth-modes.js +326 -123
  29. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
  30. package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
  31. package/dist/lifecycle/completed-write-guard.d.ts +48 -0
  32. package/dist/lifecycle/completed-write-guard.js +373 -0
  33. package/dist/lifecycle/index.d.ts +1 -0
  34. package/dist/lifecycle/index.js +1 -0
  35. package/dist/lifecycle-visible/evaluate.d.ts +98 -0
  36. package/dist/lifecycle-visible/evaluate.js +710 -0
  37. package/dist/lifecycle-visible/index.d.ts +2 -0
  38. package/dist/lifecycle-visible/index.js +2 -0
  39. package/dist/literal-acceptance/capture.d.ts +16 -0
  40. package/dist/literal-acceptance/capture.js +78 -10
  41. package/dist/literal-acceptance/evaluate.js +25 -3
  42. package/dist/literal-acceptance/index.d.ts +2 -2
  43. package/dist/literal-acceptance/index.js +2 -2
  44. package/dist/literal-acceptance/run.js +9 -3
  45. package/dist/literal-acceptance/types.d.ts +6 -0
  46. package/dist/literal-acceptance/types.js +6 -0
  47. package/dist/orchestration/judgment-policy.d.ts +13 -0
  48. package/dist/orchestration/judgment-policy.js +15 -0
  49. package/dist/orchestration/probe-session.js +13 -8
  50. package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
  51. package/dist/orchestration/verify-judgment-gates.js +43 -11
  52. package/dist/policy/ceremony-dial.js +7 -7
  53. package/dist/policy/host-hooks.d.ts +20 -0
  54. package/dist/policy/host-hooks.js +121 -2
  55. package/dist/policy/org-force-on-migration.js +2 -1
  56. package/dist/policy/plan-extensions.d.ts +1 -1
  57. package/dist/policy/plan-extensions.js +2 -0
  58. package/dist/policy/product-signal.js +6 -7
  59. package/dist/policy/require-human-merge.js +8 -6
  60. package/dist/policy/resolve.d.ts +7 -2
  61. package/dist/policy/resolve.js +25 -8
  62. package/dist/policy/runtime-authority.d.ts +18 -0
  63. package/dist/policy/runtime-authority.js +11 -0
  64. package/dist/policy/value-feedback.js +7 -8
  65. package/dist/policy/write-fence.js +3 -0
  66. package/dist/preflight-cache/evaluate.d.ts +12 -0
  67. package/dist/preflight-cache/evaluate.js +24 -4
  68. package/dist/preflight-cache/index.d.ts +1 -1
  69. package/dist/preflight-cache/index.js +1 -1
  70. package/dist/product-first-done-gate/evaluate.d.ts +4 -0
  71. package/dist/product-first-done-gate/evaluate.js +82 -10
  72. package/dist/release/closed-verb-gate.d.ts +12 -0
  73. package/dist/release/closed-verb-gate.js +51 -0
  74. package/dist/release/index.d.ts +1 -0
  75. package/dist/release/index.js +1 -0
  76. package/dist/release/pipeline.js +7 -0
  77. package/dist/release/types.d.ts +12 -0
  78. package/dist/render/constants.d.ts +2 -1
  79. package/dist/render/constants.js +2 -1
  80. package/dist/render/framework-commands.js +4 -0
  81. package/dist/render/spec-validate.js +23 -12
  82. package/dist/run-summary/types.d.ts +4 -0
  83. package/dist/scm/design-critique-chip.d.ts +37 -0
  84. package/dist/scm/design-critique-chip.js +171 -0
  85. package/dist/scm/index.d.ts +1 -0
  86. package/dist/scm/index.js +1 -0
  87. package/dist/scm/main.d.ts +3 -0
  88. package/dist/scm/main.js +15 -1
  89. package/dist/scm/readiness-cli.d.ts +2 -0
  90. package/dist/scm/readiness-cli.js +42 -1
  91. package/dist/scm/readiness.d.ts +2 -1
  92. package/dist/scm/readiness.js +7 -1
  93. package/dist/scope/decompose.js +2 -1
  94. package/dist/scope/delivery-evidence.d.ts +27 -1
  95. package/dist/scope/index.d.ts +1 -0
  96. package/dist/scope/index.js +1 -0
  97. package/dist/scope/lifecycle-write.d.ts +24 -0
  98. package/dist/scope/lifecycle-write.js +64 -0
  99. package/dist/scope/main.js +18 -6
  100. package/dist/scope/promote-from-issue.js +5 -5
  101. package/dist/scope/transition.js +90 -11
  102. package/dist/scope-provenance/evaluate.js +8 -4
  103. package/dist/session/ac-pass-banking.d.ts +5 -0
  104. package/dist/session/ac-pass-banking.js +3 -2
  105. package/dist/session/git.d.ts +18 -0
  106. package/dist/session/git.js +81 -1
  107. package/dist/session/occupancy.d.ts +2 -1
  108. package/dist/session/occupancy.js +11 -4
  109. package/dist/session/process-cost.d.ts +32 -10
  110. package/dist/session/process-cost.js +189 -1
  111. package/dist/session/product-state-hash.js +41 -24
  112. package/dist/session/ritual-entrypoint.js +16 -1
  113. package/dist/session/session-start.d.ts +18 -0
  114. package/dist/session/session-start.js +49 -2
  115. package/dist/session/verify-session-ritual.d.ts +46 -4
  116. package/dist/session/verify-session-ritual.js +118 -10
  117. package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
  118. package/dist/swarm/finalize-cohort-cli.js +165 -35
  119. package/dist/swarm/finalize-cohort.d.ts +1 -0
  120. package/dist/swarm/finalize-cohort.js +14 -2
  121. package/dist/swarm/launch.d.ts +69 -0
  122. package/dist/swarm/launch.js +191 -1
  123. package/dist/swarm/routing-set-cli.js +2 -0
  124. package/dist/swarm/routing-verify.d.ts +1 -1
  125. package/dist/swarm/routing-verify.js +2 -2
  126. package/dist/triage/actions/index.d.ts +4 -0
  127. package/dist/triage/actions/index.js +6 -0
  128. package/dist/triage/bootstrap/index.js +6 -6
  129. package/dist/triage/evaluate/evaluate.d.ts +11 -0
  130. package/dist/triage/evaluate/evaluate.js +169 -0
  131. package/dist/triage/evaluate/github.d.ts +16 -0
  132. package/dist/triage/evaluate/github.js +153 -0
  133. package/dist/triage/evaluate/index.d.ts +9 -0
  134. package/dist/triage/evaluate/index.js +8 -0
  135. package/dist/triage/evaluate/paths.d.ts +7 -0
  136. package/dist/triage/evaluate/paths.js +25 -0
  137. package/dist/triage/evaluate/sink.d.ts +5 -0
  138. package/dist/triage/evaluate/sink.js +52 -0
  139. package/dist/triage/evaluate/types.d.ts +93 -0
  140. package/dist/triage/evaluate/types.js +13 -0
  141. package/dist/triage/evaluate/validity.d.ts +8 -0
  142. package/dist/triage/evaluate/validity.js +87 -0
  143. package/dist/triage/evaluate/value.d.ts +12 -0
  144. package/dist/triage/evaluate/value.js +33 -0
  145. package/dist/triage/evaluate/wip-census.d.ts +5 -0
  146. package/dist/triage/evaluate/wip-census.js +39 -0
  147. package/dist/triage/evaluate/worktrees.d.ts +8 -0
  148. package/dist/triage/evaluate/worktrees.js +68 -0
  149. package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
  150. package/dist/triage/evaluate/xbrief-refs.js +60 -0
  151. package/dist/triage/help/registry-data.d.ts +22 -8
  152. package/dist/triage/help/registry-data.js +54 -6
  153. package/dist/triage/index.d.ts +1 -0
  154. package/dist/triage/index.js +1 -0
  155. package/dist/triage/welcome/writers.d.ts +1 -1
  156. package/dist/triage/welcome/writers.js +10 -5
  157. package/dist/value/feedback-file.d.ts +17 -2
  158. package/dist/value/feedback-file.js +80 -6
  159. package/dist/value/readback.d.ts +4 -1
  160. package/dist/value/readback.js +51 -12
  161. package/dist/vbrief-reconcile/labels.d.ts +1 -0
  162. package/dist/vbrief-reconcile/labels.js +30 -0
  163. package/dist/vbrief-validate/schema.d.ts +4 -0
  164. package/dist/vbrief-validate/schema.js +1 -1
  165. package/dist/vbrief-validation/story-quality.d.ts +7 -0
  166. package/dist/vbrief-validation/story-quality.js +8 -1
  167. package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
  168. package/dist/verify-env/agent-hook-readiness.js +12 -8
  169. package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
  170. package/dist/verify-env/agent-hooks-live-probe.js +26 -5
  171. package/dist/verify-env/agent-hooks.js +3 -4
  172. package/dist/xbrief-migrate/agents-header.js +69 -7
  173. package/package.json +7 -3
@@ -17,6 +17,7 @@ import { containedWrite } from "../fs/contained-write.js";
17
17
  import { DEFAULT_SURPLUS_THRESHOLD, resolveAcPassBanking, } from "../policy/ac-pass-banking.js";
18
18
  import { RunSummaryEmitter } from "../run-summary/emit.js";
19
19
  import { detectHardEffortBudget, formatDeepeningSkippedNote, recommendVerificationDepth, } from "./effort-budget.js";
20
+ import { gitHead } from "./git.js";
20
21
  /** Durable bank ledger directory (gitignored under `.deft/`). */
21
22
  export const AC_PASS_BANK_DIR = ".deft/ac-pass-banks";
22
23
  export const AC_PASS_BANK_SCHEMA_VERSION = 1;
@@ -682,7 +683,7 @@ export function resolveBankingConfigForRoot(projectRoot, environ) {
682
683
  * Throws on I/O failure so verify:ac can fail closed (checkpoint mandatory).
683
684
  */
684
685
  export function maybeBankOnAcPass(input) {
685
- if (input.executableRuns <= 0) {
686
+ if (input.executableRuns <= 0 && input.verifiedPass !== true) {
686
687
  return { banked: false, decision: null, bank: null, notes: [] };
687
688
  }
688
689
  const budget = input.budget ?? detectHardEffortBudget({ environ: input.environ ?? process.env });
@@ -699,7 +700,7 @@ export function maybeBankOnAcPass(input) {
699
700
  budget,
700
701
  surplus: decision.surplus,
701
702
  nextAction,
702
- headSha: input.headSha ?? null,
703
+ headSha: input.headSha !== undefined ? input.headSha : gitHead(input.projectRoot).head,
703
704
  productStateHash: input.productStateHash ?? null,
704
705
  now: input.now,
705
706
  environ: input.environ,
@@ -12,5 +12,23 @@ export declare function gitHead(projectRoot: string, runGit?: GitRunner): {
12
12
  export declare function worktreePath(projectRoot: string, runGit?: GitRunner): string;
13
13
  /** True when `ancestor` is reachable from `descendant` (same commit counts). */
14
14
  export declare function gitIsAncestor(projectRoot: string, ancestor: string, descendant: string, runGit?: GitRunner): boolean | null;
15
+ /**
16
+ * Ceiling for one `git cat-file --batch` payload. The directive terminal
17
+ * corpus is ~8 MiB today; this leaves headroom without reintroducing
18
+ * per-blob `git show` on a truncated read.
19
+ */
20
+ export declare const GIT_CAT_FILE_BATCH_MAX_BUFFER: number;
21
+ /**
22
+ * Parse `git cat-file --batch` stdout for `paths.length` objects, in the
23
+ * same order the names were written to stdin. Returns null if the stream
24
+ * is truncated or a header is malformed so callers can fall back.
25
+ */
26
+ export declare function parseGitCatFileBatch(stdout: Buffer, paths: readonly string[]): Map<string, string | null> | null;
27
+ /**
28
+ * Read many `tip:path` blobs in one `git cat-file --batch` process.
29
+ * Falls back to per-path `git show` only when the batch stream cannot be
30
+ * parsed, so verdicts stay content-authoritative.
31
+ */
32
+ export declare function showBlobsBatch(projectRoot: string, tip: string, paths: readonly string[], runGit?: GitRunner): Map<string, string | null>;
15
33
  export declare function detectBranch(projectRoot: string, runGit?: GitRunner): string | null;
16
34
  //# sourceMappingURL=git.d.ts.map
@@ -1,4 +1,4 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
2
  import { resolve } from "node:path";
3
3
  function coerceGitBytes(value) {
4
4
  if (Buffer.isBuffer(value))
@@ -64,6 +64,86 @@ export function gitIsAncestor(projectRoot, ancestor, descendant, runGit = defaul
64
64
  }
65
65
  return null;
66
66
  }
67
+ /**
68
+ * Ceiling for one `git cat-file --batch` payload. The directive terminal
69
+ * corpus is ~8 MiB today; this leaves headroom without reintroducing
70
+ * per-blob `git show` on a truncated read.
71
+ */
72
+ export const GIT_CAT_FILE_BATCH_MAX_BUFFER = 64 * 1024 * 1024;
73
+ /**
74
+ * Parse `git cat-file --batch` stdout for `paths.length` objects, in the
75
+ * same order the names were written to stdin. Returns null if the stream
76
+ * is truncated or a header is malformed so callers can fall back.
77
+ */
78
+ export function parseGitCatFileBatch(stdout, paths) {
79
+ const out = new Map();
80
+ let offset = 0;
81
+ for (const path of paths) {
82
+ const nl = stdout.indexOf(0x0a, offset);
83
+ if (nl < 0) {
84
+ return null;
85
+ }
86
+ const header = stdout.subarray(offset, nl).toString("utf8");
87
+ offset = nl + 1;
88
+ if (header.endsWith(" missing") || header.endsWith(" ambiguous")) {
89
+ out.set(path, null);
90
+ continue;
91
+ }
92
+ const match = /^[0-9a-fA-F]+ \S+ (\d+)$/.exec(header);
93
+ if (match === null) {
94
+ return null;
95
+ }
96
+ const size = Number(match[1]);
97
+ if (!Number.isInteger(size) || size < 0 || offset + size > stdout.length) {
98
+ return null;
99
+ }
100
+ const content = stdout.subarray(offset, offset + size);
101
+ offset += size;
102
+ if (offset < stdout.length && stdout[offset] === 0x0a) {
103
+ offset += 1;
104
+ }
105
+ else if (offset !== stdout.length) {
106
+ return null;
107
+ }
108
+ out.set(path, content.toString("utf8"));
109
+ }
110
+ return out;
111
+ }
112
+ function showBlobViaRunner(projectRoot, tip, path, runGit) {
113
+ const result = runGit(projectRoot, ["show", `${tip}:${path}`]);
114
+ if (result.code !== 0) {
115
+ return null;
116
+ }
117
+ return result.stdout;
118
+ }
119
+ /**
120
+ * Read many `tip:path` blobs in one `git cat-file --batch` process.
121
+ * Falls back to per-path `git show` only when the batch stream cannot be
122
+ * parsed, so verdicts stay content-authoritative.
123
+ */
124
+ export function showBlobsBatch(projectRoot, tip, paths, runGit = defaultGitRunner) {
125
+ const out = new Map();
126
+ if (paths.length === 0) {
127
+ return out;
128
+ }
129
+ const input = Buffer.from(`${paths.map((path) => `${tip}:${path}`).join("\n")}\n`, "utf8");
130
+ const result = spawnSync("git", ["cat-file", "--batch"], {
131
+ cwd: projectRoot,
132
+ input,
133
+ maxBuffer: GIT_CAT_FILE_BATCH_MAX_BUFFER,
134
+ windowsHide: true,
135
+ });
136
+ if (result.error === undefined && result.status === 0 && result.stdout !== undefined) {
137
+ const parsed = parseGitCatFileBatch(coerceGitBytes(result.stdout), paths);
138
+ if (parsed !== null) {
139
+ return parsed;
140
+ }
141
+ }
142
+ for (const path of paths) {
143
+ out.set(path, showBlobViaRunner(projectRoot, tip, path, runGit));
144
+ }
145
+ return out;
146
+ }
67
147
  export function detectBranch(projectRoot, runGit = defaultGitRunner) {
68
148
  const sym = runGit(projectRoot, ["symbolic-ref", "--short", "HEAD"]);
69
149
  if (sym.code === 0 && sym.stdout.trim()) {
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Ritual-state is "this session completed ceremony." Occupancy is "who may
5
5
  * mutate this tree right now." Those lifetimes differ; do not overload
6
- * ritual-state.json. Join negotiation (`occupancy:request`) is out of scope.
6
+ * ritual-state.json. Ordinary end is occupancy:release / session:end (#3604).
7
+ * Join negotiation (`occupancy:request`) is out of scope.
7
8
  *
8
9
  * Concurrency model:
9
10
  * - Assumptions: local filesystem; cooperating processes on one machine.
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Ritual-state is "this session completed ceremony." Occupancy is "who may
5
5
  * mutate this tree right now." Those lifetimes differ; do not overload
6
- * ritual-state.json. Join negotiation (`occupancy:request`) is out of scope.
6
+ * ritual-state.json. Ordinary end is occupancy:release / session:end (#3604).
7
+ * Join negotiation (`occupancy:request`) is out of scope.
7
8
  *
8
9
  * Concurrency model:
9
10
  * - Assumptions: local filesystem; cooperating processes on one machine.
@@ -35,11 +36,16 @@ export function heartbeatAgeSeconds(record, now = new Date()) {
35
36
  export function isOccupancyExpired(record, now = new Date(), ttlMs = OCCUPANCY_TTL_MS) {
36
37
  return now.getTime() - record.heartbeatAt.getTime() > ttlMs;
37
38
  }
39
+ function occupancyClockLine(record) {
40
+ return `claimed_at=${timestampIso(record.claimedAt)} heartbeat_at=${timestampIso(record.heartbeatAt)}`;
41
+ }
38
42
  export function formatOccupancyRemediation(record, now = new Date()) {
39
43
  const age = heartbeatAgeSeconds(record, now);
40
- return (`Worktree occupied by session ${record.sessionId} (intent=${record.intent}, heartbeat ${age}s ago).\n` +
44
+ return (`Worktree occupied by session ${record.sessionId} (intent=${record.intent}, heartbeat ${age}s ago, ` +
45
+ `${occupancyClockLine(record)}).\n` +
41
46
  "Stay read-only (`session:start --read-only`), use another worktree,\n" +
42
- "queue a join (`occupancy:request`), or steal (`occupancy:steal --confirm`).");
47
+ "queue a join (`occupancy:request`), or steal (`occupancy:steal --confirm`).\n" +
48
+ "The occupant may release (`occupancy:release` / `session:end`).");
43
49
  }
44
50
  export function resolveOccupancySessionId(input = {}) {
45
51
  const explicit = input.sessionId?.trim();
@@ -194,6 +200,7 @@ export function stealOccupancy(projectRoot, input = {}) {
194
200
  };
195
201
  }
196
202
  const incoming = resolveOccupancySessionId(input);
203
+ const priorClock = existingLocked !== null ? ` (${occupancyClockLine(existingLocked)})` : "";
197
204
  const record = writeOccupancyRecord(projectRoot, {
198
205
  sessionId: incoming,
199
206
  worktreePath: resolve(projectRoot),
@@ -210,7 +217,7 @@ export function stealOccupancy(projectRoot, input = {}) {
210
217
  sessionId: record.sessionId,
211
218
  record,
212
219
  path,
213
- message: `occupancy stolen from ${named}; writer is now session ${record.sessionId}`,
220
+ message: `occupancy stolen from ${named}${priorClock}; writer is now session ${record.sessionId}`,
214
221
  code: 0,
215
222
  };
216
223
  }, input.lockDeps);
@@ -1,13 +1,3 @@
1
- /**
2
- * Local process-cost ceremony events (#2994).
3
- *
4
- * Always-on, best-effort appends to `.deft-cache/events.jsonl`.
5
- * Not gated on valueFeedback (distinct from #1709 attribution).
6
- * No remote / Product Insights upload (#2603 not required).
7
- *
8
- * Types are declared locally (not imported from session-start) so this module
9
- * does not form an import cycle with session-start call sites.
10
- */
11
1
  import { type BehavioralEventRecord } from "../lifecycle/events.js";
12
2
  export { PROCESS_COST_EVENT_NAMES, PROCESS_COST_REQUIRED_PAYLOAD, type ProcessCostEventName, } from "./process-cost-constants.js";
13
3
  /** Ceremony tier labels mirror session-start cold|rearm (#2992 / #2994). */
@@ -50,4 +40,36 @@ export declare function emitSessionStartProcessCost(input: SessionStartProcessCo
50
40
  * Returns null on any failure (telemetry must not change deny verdict).
51
41
  */
52
42
  export declare function emitSessionRitualBlockedProcessCost(input: SessionRitualBlockedProcessCostInput, options: EmitProcessCostOptions): BehavioralEventRecord | null;
43
+ export interface CeremonyCostRollup {
44
+ /** Discriminator: CLI process time, not agent-turn wall clock (#3500 / #3508). */
45
+ readonly kind: "cli_process_time";
46
+ readonly windowLabel: string;
47
+ readonly lastColdDurationMs: number | null;
48
+ readonly lastRearmDurationMs: number | null;
49
+ readonly lastColdSteps: readonly ProcessCostStepTiming[];
50
+ readonly lastRearmSteps: readonly ProcessCostStepTiming[];
51
+ readonly blockedRitualCount: number;
52
+ readonly recoveryTierDistribution: Readonly<Record<string, number>>;
53
+ }
54
+ /**
55
+ * Roll up ceremony cost from `.deft-cache/events.jsonl` (#3508).
56
+ * Measures CLI process time, not agent-turn wall clock (#3500).
57
+ */
58
+ export declare function computeCeremonyCostRollup(options: {
59
+ projectRoot: string;
60
+ logPath?: string | null;
61
+ windowMs?: number;
62
+ now?: Date;
63
+ windowLabel?: string;
64
+ }): CeremonyCostRollup;
65
+ /**
66
+ * Human rollup for the composed `value:show` reader (#3508).
67
+ * CLI process time, not agent-turn wall clock (#3500).
68
+ */
69
+ export declare function formatCeremonyCostReport(rollup: CeremonyCostRollup): string;
70
+ /**
71
+ * Operator-visible mutation `session:start` line (#3508).
72
+ * CLI process time only. ⊗ not #3286 Later graduation input.
73
+ */
74
+ export declare function formatSessionStartCeremonyCostLine(ceremonyTier: ProcessCostCeremonyTier, durationMs: number): string;
53
75
  //# sourceMappingURL=process-cost.d.ts.map
@@ -7,8 +7,13 @@
7
7
  *
8
8
  * Types are declared locally (not imported from session-start) so this module
9
9
  * does not form an import cycle with session-start call sites.
10
+ *
11
+ * #3508: composed readers live on `task value:show` (`value/readback.ts`).
12
+ * This module measures CLI process time, not agent-turn wall clock (#3500).
13
+ * ⊗ Do not use a printed CLI duration as #3286 Later graduation input.
10
14
  */
11
- import { emit } from "../lifecycle/events.js";
15
+ import { resolve } from "node:path";
16
+ import { DEFAULT_EVENT_LOG, emit, readEvents, } from "../lifecycle/events.js";
12
17
  import { PROCESS_COST_EVENT_NAMES } from "./process-cost-constants.js";
13
18
  export { PROCESS_COST_EVENT_NAMES, PROCESS_COST_REQUIRED_PAYLOAD, } from "./process-cost-constants.js";
14
19
  /**
@@ -79,4 +84,187 @@ export function emitSessionRitualBlockedProcessCost(input, options) {
79
84
  return null;
80
85
  }
81
86
  }
87
+ function isRecordPayload(payload) {
88
+ return payload !== null && typeof payload === "object" && !Array.isArray(payload);
89
+ }
90
+ function parseDetectedAt(record) {
91
+ const raw = record.detected_at;
92
+ if (typeof raw !== "string" || raw.trim().length === 0) {
93
+ return null;
94
+ }
95
+ let text = raw.trim();
96
+ if (text.endsWith("Z")) {
97
+ text = `${text.slice(0, -1)}+00:00`;
98
+ }
99
+ const parsed = new Date(text);
100
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
101
+ }
102
+ function payloadNumber(payload, key) {
103
+ const raw = payload[key];
104
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : null;
105
+ }
106
+ function resolveProcessCostLogPath(projectRoot, logPath) {
107
+ if (logPath !== undefined && logPath !== null) {
108
+ return resolve(logPath);
109
+ }
110
+ return resolve(projectRoot, DEFAULT_EVENT_LOG);
111
+ }
112
+ function parseSteps(payload) {
113
+ const raw = payload?.steps;
114
+ if (!Array.isArray(raw)) {
115
+ return [];
116
+ }
117
+ const out = [];
118
+ for (const item of raw) {
119
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
120
+ continue;
121
+ }
122
+ const rec = item;
123
+ if (typeof rec.name !== "string" || typeof rec.duration_ms !== "number") {
124
+ continue;
125
+ }
126
+ if (!Number.isFinite(rec.duration_ms)) {
127
+ continue;
128
+ }
129
+ const step = {
130
+ name: rec.name,
131
+ duration_ms: rec.duration_ms,
132
+ };
133
+ if (rec.skipped === true) {
134
+ out.push({ ...step, skipped: true });
135
+ }
136
+ else {
137
+ out.push(step);
138
+ }
139
+ }
140
+ return out;
141
+ }
142
+ function lastSessionStartForTier(events, tier) {
143
+ for (let i = events.length - 1; i >= 0; i -= 1) {
144
+ const record = events[i];
145
+ if (record === undefined || record.event !== PROCESS_COST_EVENT_NAMES.sessionStart) {
146
+ continue;
147
+ }
148
+ const payload = isRecordPayload(record.payload) ? record.payload : null;
149
+ if (payload?.ceremony_tier === tier) {
150
+ return record;
151
+ }
152
+ }
153
+ return null;
154
+ }
155
+ function durationFrom(record) {
156
+ if (record === null || !isRecordPayload(record.payload)) {
157
+ return null;
158
+ }
159
+ return payloadNumber(record.payload, "duration_ms");
160
+ }
161
+ function formatWindowLabel(windowMs) {
162
+ const days = Math.round(windowMs / 86_400_000);
163
+ if (days >= 1 && days * 86_400_000 === windowMs) {
164
+ return `${days}d`;
165
+ }
166
+ const hours = Math.round(windowMs / 3_600_000);
167
+ if (hours >= 1 && hours * 3_600_000 === windowMs) {
168
+ return `${hours}h`;
169
+ }
170
+ return `${windowMs}ms`;
171
+ }
172
+ /**
173
+ * Roll up ceremony cost from `.deft-cache/events.jsonl` (#3508).
174
+ * Measures CLI process time, not agent-turn wall clock (#3500).
175
+ */
176
+ export function computeCeremonyCostRollup(options) {
177
+ const windowMs = options.windowMs ?? 7 * 86_400_000;
178
+ const now = options.now ?? new Date();
179
+ const sinceMs = now.getTime() - windowMs;
180
+ const logPath = resolveProcessCostLogPath(options.projectRoot, options.logPath);
181
+ let records = [];
182
+ try {
183
+ records = readEvents(logPath).filter((record) => {
184
+ if (record.event !== PROCESS_COST_EVENT_NAMES.sessionStart &&
185
+ record.event !== PROCESS_COST_EVENT_NAMES.sessionRitualBlocked) {
186
+ return false;
187
+ }
188
+ const at = parseDetectedAt(record);
189
+ if (at === null) {
190
+ return false;
191
+ }
192
+ return at.getTime() >= sinceMs;
193
+ });
194
+ }
195
+ catch {
196
+ records = [];
197
+ }
198
+ const lastCold = lastSessionStartForTier(records, "cold");
199
+ const lastRearm = lastSessionStartForTier(records, "rearm");
200
+ const recoveryTierDistribution = {};
201
+ let blockedRitualCount = 0;
202
+ for (const record of records) {
203
+ if (record.event !== PROCESS_COST_EVENT_NAMES.sessionRitualBlocked) {
204
+ continue;
205
+ }
206
+ blockedRitualCount += 1;
207
+ const payload = isRecordPayload(record.payload) ? record.payload : null;
208
+ const rawTier = payload?.recovery_tier;
209
+ const tier = typeof rawTier === "string" && rawTier.trim().length > 0 ? rawTier.trim() : "unspecified";
210
+ recoveryTierDistribution[tier] = (recoveryTierDistribution[tier] ?? 0) + 1;
211
+ }
212
+ return {
213
+ kind: "cli_process_time",
214
+ windowLabel: options.windowLabel ?? formatWindowLabel(windowMs),
215
+ lastColdDurationMs: durationFrom(lastCold),
216
+ lastRearmDurationMs: durationFrom(lastRearm),
217
+ lastColdSteps: parseSteps(lastCold !== null && isRecordPayload(lastCold.payload) ? lastCold.payload : null),
218
+ lastRearmSteps: parseSteps(lastRearm !== null && isRecordPayload(lastRearm.payload) ? lastRearm.payload : null),
219
+ blockedRitualCount,
220
+ recoveryTierDistribution,
221
+ };
222
+ }
223
+ function formatMs(value) {
224
+ return value === null ? "none" : `${value}ms`;
225
+ }
226
+ function formatSteps(steps) {
227
+ if (steps.length === 0) {
228
+ return "none";
229
+ }
230
+ return steps
231
+ .map((step) => {
232
+ const skip = step.skipped === true ? " skipped" : "";
233
+ return `${step.name}=${step.duration_ms}ms${skip}`;
234
+ })
235
+ .join(", ");
236
+ }
237
+ function formatRecovery(distribution) {
238
+ const parts = Object.entries(distribution)
239
+ .sort(([a], [b]) => a.localeCompare(b))
240
+ .map(([tier, count]) => `${tier}=${count}`);
241
+ return parts.length === 0 ? "none" : parts.join(", ");
242
+ }
243
+ /**
244
+ * Human rollup for the composed `value:show` reader (#3508).
245
+ * CLI process time, not agent-turn wall clock (#3500).
246
+ */
247
+ export function formatCeremonyCostReport(rollup) {
248
+ const lines = [
249
+ `[ceremony] CLI process time (not agent-turn wall clock; #3508 / #2994) ` +
250
+ `(${rollup.windowLabel}):`,
251
+ ` last cold: ${formatMs(rollup.lastColdDurationMs)}`,
252
+ ` last re-arm: ${formatMs(rollup.lastRearmDurationMs)}`,
253
+ ` steps (last cold ${PROCESS_COST_EVENT_NAMES.sessionStart}): ` +
254
+ formatSteps(rollup.lastColdSteps),
255
+ ` steps (last re-arm ${PROCESS_COST_EVENT_NAMES.sessionStart}): ` +
256
+ formatSteps(rollup.lastRearmSteps),
257
+ ` blocked-ritual (${PROCESS_COST_EVENT_NAMES.sessionRitualBlocked}): ` +
258
+ String(rollup.blockedRitualCount),
259
+ ` recovery-tier: ${formatRecovery(rollup.recoveryTierDistribution)}`,
260
+ ];
261
+ return `${lines.join("\n")}\n`;
262
+ }
263
+ /**
264
+ * Operator-visible mutation `session:start` line (#3508).
265
+ * CLI process time only. ⊗ not #3286 Later graduation input.
266
+ */
267
+ export function formatSessionStartCeremonyCostLine(ceremonyTier, durationMs) {
268
+ return `[deft session] ceremony ${ceremonyTier} ${durationMs}ms`;
269
+ }
82
270
  //# sourceMappingURL=process-cost.js.map
@@ -19,6 +19,8 @@ const EXCLUDED_DIR_NAMES = new Set([
19
19
  ".planning",
20
20
  ]);
21
21
  const EXCLUDED_PATH_PREFIXES = ["xbrief/", "vbrief/", ".deft/", ".git/"];
22
+ /** Repo-root run-summary telemetry must not invalidate a green bank (#3558). */
23
+ const ROOT_RUN_SUMMARY_REL = ".deft-run-summary.json";
22
24
  function asRecord(value) {
23
25
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
24
26
  return value;
@@ -39,13 +41,15 @@ function stableJson(value) {
39
41
  function toPosix(rel) {
40
42
  return rel.replace(/\\/g, "/");
41
43
  }
42
- function isExcludedRel(rel) {
44
+ function isExcludedRel(rel, options) {
43
45
  const posix = toPosix(rel);
44
46
  if (posix === "." || posix.length === 0)
45
47
  return false;
46
48
  const first = posix.split("/")[0] ?? "";
47
49
  if (EXCLUDED_DIR_NAMES.has(first))
48
50
  return true;
51
+ if (!options?.keepTelemetryFiles && posix === ROOT_RUN_SUMMARY_REL)
52
+ return true;
49
53
  return EXCLUDED_PATH_PREFIXES.some((prefix) => posix === prefix.slice(0, -1) || posix.startsWith(prefix));
50
54
  }
51
55
  function hashBytes(buf) {
@@ -130,7 +134,7 @@ function relFromRootBytes(root, abs) {
130
134
  }
131
135
  return toPosix(relative(root, abs.toString("utf8")));
132
136
  }
133
- function walkFiles(root, dir, out, seen = new Set()) {
137
+ function walkFiles(root, dir, out, seen = new Set(), keepTelemetryFiles = false) {
134
138
  const dirBuf = asPathBuf(dir);
135
139
  let real;
136
140
  try {
@@ -153,7 +157,7 @@ function walkFiles(root, dir, out, seen = new Set()) {
153
157
  const nameLatin1 = nameBuf.toString("latin1");
154
158
  const abs = joinNameBytes(dirBuf, nameBuf);
155
159
  const rel = relFromRootBytes(root, abs);
156
- if (isExcludedRel(rel))
160
+ if (isExcludedRel(rel, { keepTelemetryFiles }))
157
161
  continue;
158
162
  let st;
159
163
  try {
@@ -165,7 +169,7 @@ function walkFiles(root, dir, out, seen = new Set()) {
165
169
  if (st.isDirectory()) {
166
170
  if (EXCLUDED_DIR_NAMES.has(nameLatin1))
167
171
  continue;
168
- walkFiles(root, abs, out, seen);
172
+ walkFiles(root, abs, out, seen, keepTelemetryFiles);
169
173
  continue;
170
174
  }
171
175
  if (st.isFile())
@@ -409,7 +413,7 @@ function literalDirPrefix(rel) {
409
413
  }
410
414
  return literal.join("/");
411
415
  }
412
- function expandPath(root, relOrGlob) {
416
+ function expandPath(root, relOrGlob, keepTelemetryFiles = false) {
413
417
  const rel = toPosix(relOrGlob).replace(/^\.\//, "");
414
418
  if (looksLikeGlob(rel)) {
415
419
  try {
@@ -418,8 +422,11 @@ function expandPath(root, relOrGlob) {
418
422
  for (const pattern of globPatternsIncludingDotfiles(rel)) {
419
423
  for (const match of globSync(pattern, { cwd: root })) {
420
424
  const posix = toPosix(relative(root, resolve(root, match)));
421
- if (posix.length === 0 || isExcludedRel(posix) || seen.has(posix))
425
+ if (posix.length === 0 ||
426
+ isExcludedRel(posix, { keepTelemetryFiles }) ||
427
+ seen.has(posix)) {
422
428
  continue;
429
+ }
423
430
  seen.add(posix);
424
431
  const absMatch = resolve(root, posix);
425
432
  let st;
@@ -432,7 +439,7 @@ function expandPath(root, relOrGlob) {
432
439
  if (st.isFile())
433
440
  out.push(posix);
434
441
  else if (st.isDirectory())
435
- walkFiles(root, absMatch, out);
442
+ walkFiles(root, absMatch, out, new Set(), keepTelemetryFiles);
436
443
  }
437
444
  }
438
445
  {
@@ -448,7 +455,7 @@ function expandPath(root, relOrGlob) {
448
455
  }
449
456
  if (st?.isDirectory()) {
450
457
  const walked = [];
451
- walkFiles(root, startDir, walked);
458
+ walkFiles(root, startDir, walked, new Set(), keepTelemetryFiles);
452
459
  const re = globToRegExp(rel);
453
460
  for (const file of walked) {
454
461
  if (seen.has(file) || !re.test(file))
@@ -472,7 +479,7 @@ function expandPath(root, relOrGlob) {
472
479
  const st = statSync(abs);
473
480
  if (st.isDirectory()) {
474
481
  const out = [];
475
- walkFiles(root, abs, out);
482
+ walkFiles(root, abs, out, new Set(), keepTelemetryFiles);
476
483
  return out;
477
484
  }
478
485
  if (st.isFile())
@@ -629,45 +636,55 @@ export function hashProductState(input) {
629
636
  const runGit = input.runGit ?? defaultGitRunner;
630
637
  const files = new Set();
631
638
  let statusOk = true;
639
+ const scope = fileScopePaths(input.plan);
640
+ const specifiedSurface = input.productPaths !== undefined || scope.length > 0;
641
+ const keepTelemetryFiles = specifiedSurface;
632
642
  if (input.productPaths !== undefined) {
633
643
  for (const rel of input.productPaths) {
634
- for (const expanded of expandPath(root, rel))
644
+ for (const expanded of expandPath(root, rel, true))
635
645
  files.add(expanded);
636
646
  }
637
647
  }
638
- else {
639
- const scope = fileScopePaths(input.plan);
640
- if (scope.length > 0) {
641
- for (const entry of scope) {
642
- for (const expanded of expandPath(root, entry))
643
- files.add(expanded);
644
- }
648
+ else if (scope.length > 0) {
649
+ for (const entry of scope) {
650
+ for (const expanded of expandPath(root, entry, keepTelemetryFiles))
651
+ files.add(expanded);
645
652
  }
646
- else {
647
- const git = existsSync(join(root, ".git"));
648
- if (git) {
649
- const dirty = dirtyProductFiles(root, runGit);
650
- statusOk = dirty.ok;
653
+ }
654
+ else {
655
+ const git = existsSync(join(root, ".git"));
656
+ if (git) {
657
+ const dirty = dirtyProductFiles(root, runGit);
658
+ if (dirty.ok) {
659
+ statusOk = true;
651
660
  for (const rel of dirty.files) {
652
661
  for (const expanded of expandIfDirectory(root, rel))
653
662
  files.add(expanded);
654
663
  }
655
664
  }
656
665
  else {
666
+ // git status failed (containers, missing git, safe.directory).
667
+ // Walk product files instead of miss-forever (#3558 / #3549).
657
668
  const walked = [];
658
669
  walkFiles(root, root, walked);
659
670
  for (const rel of walked)
660
671
  files.add(rel);
672
+ statusOk = true;
661
673
  }
662
674
  }
675
+ else {
676
+ const walked = [];
677
+ walkFiles(root, root, walked);
678
+ for (const rel of walked)
679
+ files.add(rel);
680
+ }
663
681
  }
664
- const sorted = [...files].sort();
682
+ const sorted = [...files].filter((rel) => !isExcludedRel(rel, { keepTelemetryFiles })).sort();
665
683
  const fileHashes = {};
666
684
  for (const rel of sorted) {
667
685
  fileHashes[rel] = hashProductRel(root, rel, runGit);
668
686
  }
669
687
  const head = existsSync(join(root, ".git")) ? gitHead(root, runGit).head : null;
670
- const specifiedSurface = input.productPaths !== undefined || fileScopePaths(input.plan).length > 0;
671
688
  const hasSurface = specifiedSurface
672
689
  ? sorted.length > 0
673
690
  : statusOk && (sorted.length > 0 || head !== null);
@@ -46,12 +46,27 @@ function parseProjectRoot(argv) {
46
46
  }
47
47
  return ".";
48
48
  }
49
+ function parseOptionalForIssue(argv) {
50
+ for (let i = 0; i < argv.length; i += 1) {
51
+ if (argv[i] !== "--for-issue")
52
+ continue;
53
+ const next = argv[i + 1];
54
+ if (next === undefined || next.startsWith("-"))
55
+ return null;
56
+ const parsed = Number.parseInt(next, 10);
57
+ return Number.isNaN(parsed) ? null : parsed;
58
+ }
59
+ return null;
60
+ }
49
61
  /** CLI-shaped cache-fresh entrypoint (mirrors scripts/preflight_cache.py main). */
50
62
  export function runCacheFreshMain(argv) {
51
63
  const projectRoot = parseProjectRoot(argv);
52
64
  const allowMissingBootstrap = argv.includes("--allow-missing-bootstrap");
53
65
  const quiet = argv.includes("--quiet");
54
- const result = evaluate(projectRoot, { allowMissingBootstrap });
66
+ const workSelection = argv.includes("--work-selection");
67
+ const skipDriftProbe = argv.includes("--skip-drift-probe") && !workSelection;
68
+ const forIssue = parseOptionalForIssue(argv);
69
+ const result = evaluate(projectRoot, { allowMissingBootstrap, skipDriftProbe, forIssue });
55
70
  if (result.code === 0) {
56
71
  if (!quiet) {
57
72
  if (result.message.startsWith("⚠")) {