@deftai/directive-core 0.105.0 → 0.106.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 (85) hide show
  1. package/dist/authz/classify.js +591 -34
  2. package/dist/check/named-cause.js +1 -1
  3. package/dist/forward-coverage/diff-coverage.d.ts +39 -0
  4. package/dist/forward-coverage/diff-coverage.js +112 -0
  5. package/dist/forward-coverage/diff-lines.d.ts +19 -0
  6. package/dist/forward-coverage/diff-lines.js +173 -0
  7. package/dist/forward-coverage/evaluate.d.ts +24 -9
  8. package/dist/forward-coverage/evaluate.js +125 -19
  9. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  10. package/dist/init-deposit/agent-hooks.js +1 -1
  11. package/dist/intake/clause-derivation.d.ts +9 -0
  12. package/dist/intake/clause-derivation.js +30 -3
  13. package/dist/lifecycle-visible/evaluate.d.ts +98 -0
  14. package/dist/lifecycle-visible/evaluate.js +710 -0
  15. package/dist/lifecycle-visible/index.d.ts +2 -0
  16. package/dist/lifecycle-visible/index.js +2 -0
  17. package/dist/literal-acceptance/capture.js +3 -3
  18. package/dist/orchestration/judgment-policy.d.ts +13 -0
  19. package/dist/orchestration/judgment-policy.js +15 -0
  20. package/dist/orchestration/probe-session.js +13 -8
  21. package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
  22. package/dist/orchestration/verify-judgment-gates.js +43 -11
  23. package/dist/policy/ceremony-dial.js +7 -7
  24. package/dist/policy/host-hooks.d.ts +20 -0
  25. package/dist/policy/host-hooks.js +121 -2
  26. package/dist/policy/org-force-on-migration.js +2 -1
  27. package/dist/policy/plan-extensions.d.ts +1 -1
  28. package/dist/policy/plan-extensions.js +2 -0
  29. package/dist/policy/product-signal.js +6 -7
  30. package/dist/policy/require-human-merge.js +8 -6
  31. package/dist/policy/resolve.d.ts +7 -2
  32. package/dist/policy/resolve.js +25 -8
  33. package/dist/policy/value-feedback.js +7 -8
  34. package/dist/preflight-cache/evaluate.d.ts +12 -0
  35. package/dist/preflight-cache/evaluate.js +15 -1
  36. package/dist/preflight-cache/index.d.ts +1 -1
  37. package/dist/preflight-cache/index.js +1 -1
  38. package/dist/product-first-done-gate/evaluate.d.ts +4 -0
  39. package/dist/product-first-done-gate/evaluate.js +46 -5
  40. package/dist/release/closed-verb-gate.d.ts +12 -0
  41. package/dist/release/closed-verb-gate.js +51 -0
  42. package/dist/release/index.d.ts +1 -0
  43. package/dist/release/index.js +1 -0
  44. package/dist/release/pipeline.js +7 -0
  45. package/dist/release/types.d.ts +12 -0
  46. package/dist/render/constants.d.ts +2 -1
  47. package/dist/render/constants.js +2 -1
  48. package/dist/render/framework-commands.js +4 -0
  49. package/dist/render/spec-validate.js +23 -12
  50. package/dist/run-summary/types.d.ts +4 -0
  51. package/dist/scope/main.js +15 -5
  52. package/dist/scope/promote-from-issue.js +5 -5
  53. package/dist/scope-provenance/evaluate.js +3 -3
  54. package/dist/session/ac-pass-banking.d.ts +5 -0
  55. package/dist/session/ac-pass-banking.js +3 -2
  56. package/dist/session/process-cost.d.ts +32 -10
  57. package/dist/session/process-cost.js +189 -1
  58. package/dist/session/product-state-hash.js +41 -24
  59. package/dist/session/ritual-entrypoint.js +16 -1
  60. package/dist/session/session-start.d.ts +6 -0
  61. package/dist/session/session-start.js +32 -2
  62. package/dist/session/verify-session-ritual.d.ts +24 -1
  63. package/dist/session/verify-session-ritual.js +89 -6
  64. package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
  65. package/dist/swarm/finalize-cohort-cli.js +165 -35
  66. package/dist/swarm/finalize-cohort.d.ts +1 -0
  67. package/dist/swarm/finalize-cohort.js +14 -2
  68. package/dist/swarm/routing-set-cli.js +2 -0
  69. package/dist/swarm/routing-verify.d.ts +1 -1
  70. package/dist/swarm/routing-verify.js +2 -2
  71. package/dist/triage/bootstrap/index.js +6 -6
  72. package/dist/triage/help/registry-data.d.ts +1 -1
  73. package/dist/triage/help/registry-data.js +1 -1
  74. package/dist/triage/welcome/writers.d.ts +1 -1
  75. package/dist/triage/welcome/writers.js +10 -5
  76. package/dist/value/readback.d.ts +4 -1
  77. package/dist/value/readback.js +51 -12
  78. package/dist/vbrief-validate/schema.d.ts +4 -0
  79. package/dist/vbrief-validate/schema.js +1 -1
  80. package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
  81. package/dist/verify-env/agent-hook-readiness.js +12 -8
  82. package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
  83. package/dist/verify-env/agent-hooks-live-probe.js +26 -5
  84. package/dist/verify-env/agent-hooks.js +3 -4
  85. package/package.json +7 -3
@@ -205,7 +205,7 @@ function listActiveXbriefPaths(projectRoot) {
205
205
  }
206
206
  function remediationForExpansion() {
207
207
  return ("Renew human approval: re-record the approved-scope digest after operator review " +
208
- "(`task scope:record-approved-scope -- <xbrief-path> --actor <you>` writes " +
208
+ "(`task scope:record-approved-scope -- <xbrief-path> --actor <you> --confirm` writes " +
209
209
  "`.deft/approved-scope/<plan-id>.json` with a humanApproval stamp). Commit that " +
210
210
  "approval on the merge base (or a prior PR) before expanding or activating the " +
211
211
  "scoped xBRIEF in the implementation change set. Editing the active xBRIEF alone " +
@@ -344,7 +344,7 @@ export function evaluateOneScopeProvenance(input) {
344
344
  detail: "active xBRIEF modified with a non-human (agent/missing) approved-scope stamp; " +
345
345
  "only humanApproval stamps authorize non-empty file_scope",
346
346
  remediation: "Record a human-origin approval via `task scope:record-approved-scope -- " +
347
- "<xbrief-path> --actor <you>` (#3145 / #3205).",
347
+ "<xbrief-path> --actor <you> --confirm` (#3145 / #3205).",
348
348
  };
349
349
  }
350
350
  }
@@ -361,7 +361,7 @@ export function evaluateOneScopeProvenance(input) {
361
361
  detail: "active xBRIEF modified with a non-human approved-scope stamp (scope shrink/noise path); " +
362
362
  "only humanApproval stamps authorize non-empty file_scope",
363
363
  remediation: "Record a human-origin approval via `task scope:record-approved-scope -- " +
364
- "<xbrief-path> --actor <you>` (#3145 / #3205).",
364
+ "<xbrief-path> --actor <you> --confirm` (#3145 / #3205).",
365
365
  };
366
366
  }
367
367
  return null;
@@ -173,6 +173,11 @@ export interface MaybeBankOnAcPassInput {
173
173
  readonly executableRuns: number;
174
174
  readonly quiet?: boolean;
175
175
  readonly productStateHash?: string | null;
176
+ /**
177
+ * Bank a verified-pass walk even when no shell runs were recorded (#3558).
178
+ * Soft/advisory empty passes still skip via executableRuns === 0.
179
+ */
180
+ readonly verifiedPass?: boolean;
176
181
  }
177
182
  export interface MaybeBankOnAcPassResult {
178
183
  readonly banked: boolean;
@@ -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,
@@ -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("⚠")) {
@@ -1,3 +1,4 @@
1
+ import { type LifecycleVisibleResult } from "../lifecycle-visible/evaluate.js";
1
2
  import { type HostContentSurfaceSeams } from "../platform/host-content-surface.js";
2
3
  import { type EnvironmentContext } from "../platform/shell-context.js";
3
4
  import { type CeremonyDialInputs, type CeremonyDialSelection, type ProvisionalCeremonyEstimateHints } from "../policy/ceremony-dial.js";
@@ -117,6 +118,11 @@ export interface SessionStartOptions {
117
118
  * network is enabled. Inject in tests.
118
119
  */
119
120
  readonly probeScm?: (options: ProbeScmReadinessOptions) => ScmReadinessReport;
121
+ /**
122
+ * #3505: per-clone lifecycle-root visibility (ignore / skip-worktree).
123
+ * Warn-only; never blocks session:start. Inject in tests.
124
+ */
125
+ readonly probeLifecycleVisible?: (projectRoot: string, runGit: GitRunner) => LifecycleVisibleResult;
120
126
  /**
121
127
  * #3214: ceremony dial inputs (task size × model tier × project shape).
122
128
  * Missing fields are filled by the headless provisional classifier
@@ -3,6 +3,7 @@ import { emitSessionEvalReadback } from "../eval/readback.js";
3
3
  import { bindSessionGeneration } from "../freshness/bind.js";
4
4
  import { readLiveGeneration } from "../freshness/generation.js";
5
5
  import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposit/migrate.js";
6
+ import { evaluateLifecycleVisible, formatLifecycleVisibleSessionLines, } from "../lifecycle-visible/evaluate.js";
6
7
  import { hostContentSurfaceToDict, maybeFormatHostContentSurfaceLines, } from "../platform/host-content-surface.js";
7
8
  import { detectEnvironmentContext, environmentContextToDict, formatEnvironmentContext, } from "../platform/shell-context.js";
8
9
  import { ceremonyDialToDict, formatCeremonyDialStatusLine, mergeCeremonyDialDeferrals, resolveCeremonyDial, resolveSessionCeremonyDialInputs, } from "../policy/ceremony-dial.js";
@@ -26,8 +27,8 @@ import { ceremonyDialEvidenceToDict, collectCeremonyDialConsumerEvidence, format
26
27
  import { effortBudgetToDict, maybeFormatEffortBudgetLines, resolveProductionHostEffortDescriptor, } from "./effort-budget.js";
27
28
  import { defaultGitRunner, gitHead, gitIsAncestor, worktreePath } from "./git.js";
28
29
  import { applyWorktreeOccupancy, } from "./occupancy.js";
29
- import { runOrientationCompression, } from "./orientation-compression.js";
30
- import { emitSessionStartProcessCost } from "./process-cost.js";
30
+ import { resolveSessionCompact, runOrientationCompression, } from "./orientation-compression.js";
31
+ import { emitSessionStartProcessCost, formatSessionStartCeremonyCostLine } from "./process-cost.js";
31
32
  import { probeSessionReleaseAvailability, } from "./release-availability.js";
32
33
  import { newRitualStatePayload, readRitualState, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
33
34
  import { timestampIso } from "./time.js";
@@ -50,6 +51,18 @@ export const QUICK_STEPS = [
50
51
  "verify_tools",
51
52
  ];
52
53
  export const GATED_STEPS = ["agent_hooks", "doctor", "cache_fresh"];
54
+ /** Per-clone ignore/index hide of lifecycle roots — warn-only (#3505). */
55
+ function pushLifecycleVisibleAdvisory(lines, projectRoot, options, runGit) {
56
+ try {
57
+ const probe = options.probeLifecycleVisible ??
58
+ ((root, git) => evaluateLifecycleVisible({ projectRoot: root, runGit: git }));
59
+ const result = probe(projectRoot, runGit);
60
+ lines.push(...formatLifecycleVisibleSessionLines(result));
61
+ }
62
+ catch {
63
+ // best-effort — session:start must not abort on a clone-local advisory
64
+ }
65
+ }
53
66
  /** Standing one-liner when coverageDebt/checkResume is non-default (#3314). */
54
67
  function pushCoverageCheckResumeDisclosure(lines, projectRoot) {
55
68
  try {
@@ -494,6 +507,8 @@ function runReadOnlySessionStart(projectRoot, options, instant, environment) {
494
507
  lines.push(...formatScmReadinessLines(scm));
495
508
  lines.push(...hostSurface.lines);
496
509
  lines.push(...effortBudget.lines);
510
+ const runGit = options.runGit ?? defaultGitRunner;
511
+ pushLifecycleVisibleAdvisory(lines, projectRoot, options, runGit);
497
512
  const resultPayload = {
498
513
  ready: true,
499
514
  exit_code: 0,
@@ -576,6 +591,7 @@ function runSessionRearm(projectRoot, options, instant, environment) {
576
591
  ...effortBudget.lines,
577
592
  REARM_SKIPPED_FAT_PATH_MESSAGE,
578
593
  ];
594
+ pushLifecycleVisibleAdvisory(lines, projectRoot, options, runGit);
579
595
  // Light branch-policy disclosure (local only) so re-arm still surfaces policy state.
580
596
  const policyResult = resolvePolicy(projectRoot);
581
597
  const policyMessage = disclosureLine(policyResult);
@@ -739,6 +755,10 @@ function runSessionRearm(projectRoot, options, instant, environment) {
739
755
  optionalNetwork: false,
740
756
  steps: stepTimings,
741
757
  }, { projectRoot });
758
+ // #3508: operator-visible CLI process time. ⊗ not #3286 Later graduation input.
759
+ if (!resolveSessionCompact({ compact: options.compact, env: options.env })) {
760
+ lines.push(formatSessionStartCeremonyCostLine(REARM_CEREMONY_TIER, totalMs));
761
+ }
742
762
  return {
743
763
  code,
744
764
  payload: {
@@ -976,6 +996,12 @@ export function runSessionStart(projectRoot, options = {}) {
976
996
  name: "effort_budget",
977
997
  duration_ms: elapsedMs(effortBudgetStepStarted),
978
998
  });
999
+ const lifecycleVisibleStarted = performance.now();
1000
+ pushLifecycleVisibleAdvisory(lines, projectRoot, options, runGit);
1001
+ stepTimings.push({
1002
+ name: "lifecycle_visible",
1003
+ duration_ms: elapsedMs(lifecycleVisibleStarted),
1004
+ });
979
1005
  if (!quickSteps.branch_policy) {
980
1006
  const stepStarted = performance.now();
981
1007
  const result = resolvePolicy(projectRoot);
@@ -1461,6 +1487,10 @@ export function runSessionStart(projectRoot, options = {}) {
1461
1487
  optionalNetwork: allowOptionalNetwork,
1462
1488
  steps: stepTimings,
1463
1489
  }, { projectRoot });
1490
+ // #3508: operator-visible CLI process time. ⊗ not #3286 Later graduation input.
1491
+ if (!resolveSessionCompact({ compact: options.compact, env: options.env })) {
1492
+ lines.push(formatSessionStartCeremonyCostLine(COLD_CEREMONY_TIER, totalMs));
1493
+ }
1464
1494
  return { code, payload: resultPayload, lines };
1465
1495
  }
1466
1496
  export { ritualStatePath };
@@ -6,6 +6,26 @@ export { formatCacheFetchAllRecoveryCommand, recoveryHintForStaleFailure, } from
6
6
  export declare const ENV_SKIP = "DEFT_SESSION_RITUAL_SKIP";
7
7
  export { ENTRYPOINT_TIMEOUT_EXIT_CODE, ENTRYPOINT_TIMEOUT_SECONDS, } from "./ritual-entrypoint.js";
8
8
  export declare const GATED_ENTRYPOINT_COMMANDS: Readonly<Record<GatedStepName, readonly string[]>>;
9
+ /** Explicit skip flag threaded through the gated `cache_fresh` argv (#3507). */
10
+ export declare const SKIP_DRIFT_PROBE_FLAG = "--skip-drift-probe";
11
+ /** Explicit work-selection flag threaded through the gated `cache_fresh` argv (#3507). */
12
+ export declare const WORK_SELECTION_FLAG = "--work-selection";
13
+ /** Distinct ritual-state field — never `deferred_reason` (#3507). */
14
+ export declare const DRIFT_PROBE_SKIPPED_NO_WORK_SELECTION = "skipped-no-work-selection";
15
+ export type WorkSelectionKind = "none" | "active-story" | "triage-queue" | "ordered-plan" | "for-issue" | "explicit-flag";
16
+ export interface WorkSelectionSignal {
17
+ readonly inPlay: boolean;
18
+ readonly kind: WorkSelectionKind;
19
+ }
20
+ export type DetectWorkSelection = (projectRoot: string) => WorkSelectionSignal;
21
+ /**
22
+ * Default work-selection detector for the gated `cache_fresh` seam (#3507).
23
+ * Active story xBRIEF (scope:activate residue), unexhausted ordered-plan /
24
+ * triage sequence. `--for-issue` is a positive evaluate override, not this
25
+ * detector — the gated argv never carries it.
26
+ */
27
+ export declare function detectWorkSelection(projectRoot: string): WorkSelectionSignal;
28
+ export declare function resolveGatedEntrypointCommand(stepName: GatedStepName, projectRoot: string, detect?: DetectWorkSelection): string[];
9
29
  export interface VerifyResult {
10
30
  readonly code: number;
11
31
  readonly message: string;
@@ -30,7 +50,8 @@ export type RitualRunner = (command: readonly string[], projectRoot: string) =>
30
50
  export declare function formatRitualRecoveryInstruction(tier?: SessionCeremonyTier): string;
31
51
  /**
32
52
  * Sanctioned audited defer copy for gated `cache_fresh` failure (#3506 / #3507).
33
- * The gated argv is always `["verify:cache-fresh"]`, so this is unconditional.
53
+ * Independent of the #3507 work-selection argv seam — defer is still the
54
+ * operator-audited escape, not the drift-probe skip.
34
55
  */
35
56
  export declare function formatCacheFreshDeferSoftPath(): string;
36
57
  export interface VerifySessionRitualOptions {
@@ -58,6 +79,8 @@ export interface VerifySessionRitualOptions {
58
79
  * {@link checkActiveCliAgainstTarget}.
59
80
  */
60
81
  readonly checkActiveCli?: (targetVersion: string | null, seams?: ActiveCliCheckSeams) => ActiveCliCheckResult;
82
+ /** Injectable work-selection detector for gated `cache_fresh` (#3507). */
83
+ readonly detectWorkSelection?: DetectWorkSelection;
61
84
  }
62
85
  export interface InspectSessionRitualOptions {
63
86
  readonly tier?: "quick" | "gated";