@deftai/directive-core 0.94.0 → 0.96.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 (99) hide show
  1. package/dist/authz/classify.js +485 -0
  2. package/dist/cache/archive.d.ts +134 -0
  3. package/dist/cache/archive.js +630 -0
  4. package/dist/cache/index.d.ts +1 -0
  5. package/dist/cache/index.js +1 -0
  6. package/dist/cache/main.js +298 -1
  7. package/dist/content-contracts/skills/helpers.d.ts +1 -1
  8. package/dist/content-contracts/skills/helpers.js +1 -0
  9. package/dist/doctor/main.js +41 -9
  10. package/dist/doctor/types.d.ts +2 -2
  11. package/dist/freshness/bind.d.ts +67 -0
  12. package/dist/freshness/bind.js +201 -0
  13. package/dist/freshness/cli.d.ts +30 -0
  14. package/dist/freshness/cli.js +180 -0
  15. package/dist/freshness/compare.d.ts +14 -0
  16. package/dist/freshness/compare.js +134 -0
  17. package/dist/freshness/generation.d.ts +44 -0
  18. package/dist/freshness/generation.js +172 -0
  19. package/dist/freshness/index.d.ts +13 -0
  20. package/dist/freshness/index.js +13 -0
  21. package/dist/freshness/report.d.ts +43 -0
  22. package/dist/freshness/report.js +139 -0
  23. package/dist/freshness/types.d.ts +70 -0
  24. package/dist/freshness/types.js +30 -0
  25. package/dist/handoff-evidence/index.d.ts +8 -0
  26. package/dist/handoff-evidence/index.js +7 -0
  27. package/dist/handoff-evidence/validate.d.ts +105 -0
  28. package/dist/handoff-evidence/validate.js +423 -0
  29. package/dist/hooks/dispatcher.d.ts +3 -0
  30. package/dist/hooks/dispatcher.js +12 -3
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.js +3 -0
  33. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  34. package/dist/init-deposit/agent-hooks.js +5 -2
  35. package/dist/init-deposit/hygiene.d.ts +1 -1
  36. package/dist/init-deposit/hygiene.js +16 -4
  37. package/dist/init-deposit/init-deposit.d.ts +7 -1
  38. package/dist/init-deposit/init-deposit.js +24 -4
  39. package/dist/init-deposit/refresh.d.ts +9 -1
  40. package/dist/init-deposit/refresh.js +48 -5
  41. package/dist/init-deposit/scaffold.js +6 -5
  42. package/dist/parent-turn-shape/evaluate.d.ts +84 -0
  43. package/dist/parent-turn-shape/evaluate.js +353 -0
  44. package/dist/parent-turn-shape/index.d.ts +8 -0
  45. package/dist/parent-turn-shape/index.js +8 -0
  46. package/dist/policy/index.d.ts +3 -0
  47. package/dist/policy/index.js +35 -0
  48. package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
  49. package/dist/review-monitor/constants.js +3 -2
  50. package/dist/review-monitor/tier-detection.d.ts +7 -3
  51. package/dist/review-monitor/tier-detection.js +18 -1
  52. package/dist/review-monitor/verify.js +18 -0
  53. package/dist/scope/index.d.ts +2 -0
  54. package/dist/scope/index.js +2 -0
  55. package/dist/scope/main.d.ts +10 -0
  56. package/dist/scope/main.js +109 -24
  57. package/dist/scope/promote-from-issue.d.ts +49 -0
  58. package/dist/scope/promote-from-issue.js +367 -0
  59. package/dist/scope/promote-path.d.ts +39 -0
  60. package/dist/scope/promote-path.js +105 -0
  61. package/dist/session/ritual-entrypoint.d.ts +6 -2
  62. package/dist/session/ritual-entrypoint.js +15 -2
  63. package/dist/session/session-ready.js +31 -14
  64. package/dist/session/session-start.d.ts +2 -1
  65. package/dist/session/session-start.js +66 -3
  66. package/dist/session/verify-session-ritual.d.ts +4 -2
  67. package/dist/session/verify-session-ritual.js +11 -11
  68. package/dist/slash/product-set.js +4 -1
  69. package/dist/swarm/routing.d.ts +4 -2
  70. package/dist/swarm/routing.js +26 -4
  71. package/dist/swarm/verify-review-clean.d.ts +4 -3
  72. package/dist/swarm/verify-review-clean.js +28 -73
  73. package/dist/triage/actions/index.js +62 -2
  74. package/dist/triage/actions/types.d.ts +8 -1
  75. package/dist/triage/author-filter.d.ts +51 -0
  76. package/dist/triage/author-filter.js +152 -0
  77. package/dist/triage/classify/index.d.ts +9 -0
  78. package/dist/triage/classify/index.js +34 -2
  79. package/dist/triage/classify/label-mirror.d.ts +206 -0
  80. package/dist/triage/classify/label-mirror.js +914 -0
  81. package/dist/triage/help/registry-data.d.ts +49 -38
  82. package/dist/triage/help/registry-data.js +134 -39
  83. package/dist/triage/index.d.ts +1 -0
  84. package/dist/triage/index.js +1 -0
  85. package/dist/triage/queue/index.d.ts +1 -0
  86. package/dist/triage/queue/index.js +1 -0
  87. package/dist/triage/queue/render.d.ts +2 -0
  88. package/dist/triage/queue/render.js +6 -0
  89. package/dist/triage/queue/show.d.ts +1 -1
  90. package/dist/triage/queue/show.js +4 -2
  91. package/dist/vbrief-validate/project-definition.js +1 -1
  92. package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
  93. package/dist/verify-env/agent-hook-readiness.js +150 -0
  94. package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
  95. package/dist/verify-env/agent-hooks-live-probe.js +120 -107
  96. package/dist/verify-env/agent-hooks.js +11 -3
  97. package/dist/verify-env/index.d.ts +1 -0
  98. package/dist/verify-env/index.js +1 -0
  99. package/package.json +11 -3
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Deterministic handoff-evidence validator (#3120).
3
+ *
4
+ * status pass is forbidden when any remote artifact is claimed unless
5
+ * proof_status is bound and same-turn probe snippets cover each claim.
6
+ * Invented-done (false remote artifacts under pass) ranks stricter than
7
+ * empty-done (pass with no completion substance).
8
+ */
9
+ const PROOF_STATUSES = new Set(["bound", "unbound", "n/a-no-remote-claim"]);
10
+ const AXIS_STATES = new Set(["done", "in_progress", "not_started", "blocked", "n/a"]);
11
+ const STATUS_VALUES = new Set(["pass", "fail", "blocked", "partial"]);
12
+ function isNonEmptyString(value) {
13
+ return typeof value === "string" && value.trim().length > 0;
14
+ }
15
+ function isPresentClaim(value) {
16
+ if (value === null || value === undefined)
17
+ return false;
18
+ if (typeof value === "string")
19
+ return value.trim().length > 0;
20
+ if (typeof value === "number")
21
+ return Number.isFinite(value);
22
+ return true;
23
+ }
24
+ /** Commands that look like same-turn forge/git/gate tool invocations. */
25
+ const TOOLISH_CMD_RE = /^(?:gh\b|git\b|task\b|deft\b|directive\b|pnpm\b|npm\b|npx\b|curl\b|python\b|node\b)/i;
26
+ function probePresent(probe) {
27
+ if (!probe)
28
+ return false;
29
+ if (!isNonEmptyString(probe.command) || !isNonEmptyString(probe.snippet))
30
+ return false;
31
+ // Reject freeform narration commands ("I checked" / "x") — require tool-like invocation.
32
+ if (!TOOLISH_CMD_RE.test(probe.command.trim()))
33
+ return false;
34
+ // Trivial snippet that is only the claim itself is not a probe payload.
35
+ if (probe.snippet.trim().length < 8)
36
+ return false;
37
+ return true;
38
+ }
39
+ /** Escape untrusted claim text before interpolating into RegExp constructors. */
40
+ function escapeRe(value) {
41
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42
+ }
43
+ /**
44
+ * Token-boundary match: needle must appear not as a substring of a longer
45
+ * alphanumeric identifier (PR 12 ⊄ 3120; score 5 ⊄ elapsed=15s).
46
+ */
47
+ function hasToken(snippet, needle) {
48
+ if (!needle)
49
+ return false;
50
+ try {
51
+ return new RegExp(`(^|[^A-Za-z0-9_])${escapeRe(needle)}([^A-Za-z0-9_]|$)`, "i").test(snippet);
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ /**
58
+ * True when the probe's raw snippet actually mentions the claimed remote value
59
+ * with token-level binding (not incidental substring collisions).
60
+ * Never throws on malformed claims — returns false instead.
61
+ *
62
+ * Scope: textual shape binding only. Live probe execution/provenance is agent
63
+ * process responsibility (preamble probe-then-fill); this library does not
64
+ * re-run forge/git commands.
65
+ */
66
+ export function probeBindsClaim(claim, evidence, probe) {
67
+ try {
68
+ if (!probePresent(probe) || !probe)
69
+ return false;
70
+ const snippet = probe.snippet;
71
+ switch (claim) {
72
+ case "pr_url": {
73
+ const url = String(evidence.pr_url ?? "").trim();
74
+ if (!url)
75
+ return false;
76
+ // Exact claimed URL present in probe output.
77
+ if (snippet.toLowerCase().includes(url.toLowerCase()))
78
+ return true;
79
+ // GitHub HTML or API form must preserve owner/repo + number identity.
80
+ const html = url.match(/github\.com\/([^/]+)\/([^/]+)\/pulls?\/(\d+)/i);
81
+ if (html) {
82
+ const owner = html[1] ?? "";
83
+ const repo = html[2] ?? "";
84
+ const num = html[3] ?? "";
85
+ const o = escapeRe(owner);
86
+ const r = escapeRe(repo);
87
+ const n = escapeRe(num);
88
+ return (new RegExp(`${o}\\/${r}\\/pulls?\\/${n}(?:[^0-9]|$)`, "i").test(snippet) ||
89
+ new RegExp(`repos\\/${o}\\/${r}\\/pulls\\/${n}(?:[^0-9]|$)`, "i").test(snippet));
90
+ }
91
+ // Non-GitHub URL: require the full URL string; no number-only fallback.
92
+ return false;
93
+ }
94
+ case "pr_number": {
95
+ const num = String(evidence.pr_number ?? "").trim();
96
+ if (!num || !/^\d+$/.test(num))
97
+ return false;
98
+ const n = escapeRe(num);
99
+ return (new RegExp(`(?:number["'\\s:=]+|#|pulls?\\/)${n}(?:[^0-9]|$)`, "i").test(snippet) ||
100
+ hasToken(snippet, num));
101
+ }
102
+ case "commit_sha":
103
+ case "head_sha": {
104
+ const sha = String(claim === "commit_sha" ? (evidence.commit_sha ?? "") : (evidence.head_sha ?? ""))
105
+ .trim()
106
+ .toLowerCase();
107
+ if (sha.length < 7 || !/^[0-9a-f]+$/i.test(sha))
108
+ return false;
109
+ const prefix = sha.slice(0, 7);
110
+ return (hasToken(snippet, sha) ||
111
+ new RegExp(`(^|[^0-9a-f])${prefix}(?![0-9a-f])`, "i").test(snippet));
112
+ }
113
+ case "ci_status": {
114
+ const ci = String(evidence.ci_status ?? "")
115
+ .trim()
116
+ .toLowerCase();
117
+ if (!ci)
118
+ return false;
119
+ const greenFamily = [
120
+ "green",
121
+ "pass",
122
+ "passed",
123
+ "success",
124
+ "successful",
125
+ "ok",
126
+ "clean",
127
+ ];
128
+ const redFamily = [
129
+ "fail",
130
+ "failed",
131
+ "failure",
132
+ "failing",
133
+ "error",
134
+ "errored",
135
+ "cancelled",
136
+ "canceled",
137
+ ];
138
+ if (greenFamily.includes(ci)) {
139
+ const hasGreen = /"state"\s*:\s*"(success|successful|ok|clean|pass(?:ed)?)"/i.test(snippet) ||
140
+ greenFamily.some((tok) => hasToken(snippet, tok));
141
+ const hasRed = /"state"\s*:\s*"(failure|failed|error|cancelled|canceled)"/i.test(snippet) ||
142
+ redFamily.some((tok) => hasToken(snippet, tok));
143
+ // Mixed success+failure in one snippet is not a clean green bind.
144
+ return hasGreen && !hasRed;
145
+ }
146
+ if (redFamily.includes(ci)) {
147
+ return (/"state"\s*:\s*"(failure|failed|error|cancelled|canceled)"/i.test(snippet) ||
148
+ hasToken(snippet, ci));
149
+ }
150
+ return hasToken(snippet, ci);
151
+ }
152
+ case "review_score": {
153
+ const score = String(evidence.review_score ?? "").trim();
154
+ if (!score || !/^\d+(\.\d+)?$/.test(score))
155
+ return false;
156
+ const s = escapeRe(score);
157
+ // Labeled fields only — bare hasToken would bind p0=5 to review_score 5.
158
+ return new RegExp(`(?:confidence(?:\\s*score)?|score|rating)\\s*[:=]?\\s*${s}(?:\\s*\\/\\s*5)?(?:[^0-9]|$)`, "i").test(snippet);
159
+ }
160
+ default:
161
+ return false;
162
+ }
163
+ }
164
+ catch {
165
+ // Malformed claims must fail closed as unbound, never throw.
166
+ return false;
167
+ }
168
+ }
169
+ /** Detect which remote claim keys are present on the evidence object. */
170
+ export function detectRemoteClaims(evidence) {
171
+ const claims = [];
172
+ if (isPresentClaim(evidence.pr_url))
173
+ claims.push("pr_url");
174
+ if (isPresentClaim(evidence.pr_number))
175
+ claims.push("pr_number");
176
+ if (isPresentClaim(evidence.commit_sha))
177
+ claims.push("commit_sha");
178
+ if (isPresentClaim(evidence.head_sha))
179
+ claims.push("head_sha");
180
+ if (isPresentClaim(evidence.ci_status)) {
181
+ const ci = String(evidence.ci_status).trim();
182
+ // Neutral tokens are not remote claims; green-family and other explicit
183
+ // forge outcomes (failing, cancelled, …) are claims that need probes.
184
+ if (!/^(unknown|pending|n\/a|none|skipped)$/i.test(ci)) {
185
+ claims.push("ci_status");
186
+ }
187
+ }
188
+ if (isPresentClaim(evidence.review_score))
189
+ claims.push("review_score");
190
+ return claims;
191
+ }
192
+ /** Map a remote claim key to the probe slot that must bind it. */
193
+ function probeKeyForClaim(claim) {
194
+ if (claim === "pr_url" || claim === "pr_number")
195
+ return "pr";
196
+ if (claim === "commit_sha" || claim === "head_sha")
197
+ return "sha";
198
+ if (claim === "ci_status")
199
+ return "ci";
200
+ return "review";
201
+ }
202
+ function unboundClaimKeys(evidence, claims) {
203
+ const unbound = [];
204
+ for (const claim of claims) {
205
+ const slot = probeKeyForClaim(claim);
206
+ const probe = evidence.probes?.[slot];
207
+ if (!probeBindsClaim(claim, evidence, probe)) {
208
+ unbound.push(claim);
209
+ }
210
+ }
211
+ // Cross-claim consistency: PR + SHA must co-appear so unrelated forge artifacts
212
+ // cannot be stitched into one "bound" pass (Greptile residual on #3120).
213
+ try {
214
+ const claimSet = new Set(claims);
215
+ const hasPr = claimSet.has("pr_url") || claimSet.has("pr_number");
216
+ const shaClaim = claimSet.has("head_sha")
217
+ ? "head_sha"
218
+ : claimSet.has("commit_sha")
219
+ ? "commit_sha"
220
+ : null;
221
+ const rawSha = shaClaim
222
+ ? String(shaClaim === "head_sha" ? (evidence.head_sha ?? "") : (evidence.commit_sha ?? ""))
223
+ .trim()
224
+ .toLowerCase()
225
+ : "";
226
+ // Only hex SHAs participate in co-binding; malformed SHA fails closed as unbound.
227
+ const hexOk = /^[0-9a-f]{7,}$/i.test(rawSha);
228
+ if (shaClaim && !hexOk) {
229
+ if (!unbound.includes(shaClaim))
230
+ unbound.push(shaClaim);
231
+ }
232
+ const prefix = hexOk ? rawSha.slice(0, 7) : "";
233
+ if (hasPr && shaClaim && prefix) {
234
+ const prSnippet = evidence.probes?.pr?.snippet ?? "";
235
+ if (!new RegExp(`(^|[^0-9a-f])${prefix}`, "i").test(prSnippet)) {
236
+ if (!unbound.includes(shaClaim))
237
+ unbound.push(shaClaim);
238
+ if (!unbound.includes("pr_number") && claimSet.has("pr_number")) {
239
+ unbound.push("pr_number");
240
+ }
241
+ if (!unbound.includes("pr_url") && claimSet.has("pr_url")) {
242
+ unbound.push("pr_url");
243
+ }
244
+ }
245
+ }
246
+ if (claimSet.has("ci_status") && shaClaim && prefix) {
247
+ const ciCmd = evidence.probes?.ci?.command ?? "";
248
+ const ciSnippet = evidence.probes?.ci?.snippet ?? "";
249
+ const joined = `${ciCmd}\n${ciSnippet}`;
250
+ if (!new RegExp(`(^|[^0-9a-f])${prefix}`, "i").test(joined)) {
251
+ if (!unbound.includes("ci_status"))
252
+ unbound.push("ci_status");
253
+ }
254
+ }
255
+ }
256
+ catch {
257
+ // Fail closed on any unexpected error during consistency checks.
258
+ for (const claim of claims) {
259
+ if (!unbound.includes(claim))
260
+ unbound.push(claim);
261
+ }
262
+ }
263
+ return unbound;
264
+ }
265
+ function isPassStatus(status) {
266
+ return status.trim().toLowerCase() === "pass";
267
+ }
268
+ /** Collect shape errors for unsupported status / proof_status / axis enums. */
269
+ function enumShapeReasons(evidence) {
270
+ const reasons = [];
271
+ const status = evidence.status.toString().trim().toLowerCase();
272
+ if (!STATUS_VALUES.has(status)) {
273
+ reasons.push(`status must be one of pass|fail|blocked|partial (got ${evidence.status})`);
274
+ }
275
+ if (evidence.proof_status !== undefined && evidence.proof_status !== null) {
276
+ const proof = evidence.proof_status.toString().trim().toLowerCase();
277
+ if (proof.length > 0 && !PROOF_STATUSES.has(proof)) {
278
+ reasons.push(`proof_status must be bound|unbound|n/a-no-remote-claim (got ${evidence.proof_status})`);
279
+ }
280
+ }
281
+ for (const [name, axis] of [
282
+ ["work", evidence.work],
283
+ ["ship", evidence.ship],
284
+ ["gate", evidence.gate],
285
+ ]) {
286
+ const state = axis?.state;
287
+ if (state === undefined || state === null)
288
+ continue;
289
+ const s = state.toString().trim().toLowerCase();
290
+ if (s.length > 0 && !AXIS_STATES.has(s)) {
291
+ reasons.push(`${name}.state must be done|in_progress|not_started|blocked|n/a (got ${state})`);
292
+ }
293
+ }
294
+ return reasons;
295
+ }
296
+ function isEmptyDone(evidence) {
297
+ // Pass claimed but no work/ship/gate substance and no remote claims —
298
+ // the empty completion shape (stricter handling still prefers invented-done
299
+ // when remote claims are present without binding).
300
+ const work = evidence.work?.state?.toString().trim().toLowerCase();
301
+ const ship = evidence.ship?.state?.toString().trim().toLowerCase();
302
+ const gate = evidence.gate?.state?.toString().trim().toLowerCase();
303
+ const axesEmpty = (!work || work === "n/a" || work === "not_started") &&
304
+ (!ship || ship === "n/a" || ship === "not_started") &&
305
+ (!gate || gate === "n/a" || gate === "not_started");
306
+ return axesEmpty;
307
+ }
308
+ /**
309
+ * Validate handoff evidence against the #3120 bound-proof contract.
310
+ *
311
+ * Ranking when invalid under status pass:
312
+ * 1. invented-done — remote claims present without bound proof
313
+ * 2. empty-done — pass with no completion substance and no remote claims
314
+ * 3. unbound-remote-claim — non-pass status but still has unbound remote claims
315
+ * 4. shape-error — malformed proof_status / inconsistent n/a
316
+ */
317
+ export function validateHandoffEvidence(evidence) {
318
+ if (!evidence || typeof evidence !== "object") {
319
+ return {
320
+ ok: false,
321
+ failClass: "shape-error",
322
+ reasons: ["evidence must be a non-null object"],
323
+ hasRemoteClaims: false,
324
+ unboundClaims: [],
325
+ };
326
+ }
327
+ if (!isNonEmptyString(evidence.status)) {
328
+ return {
329
+ ok: false,
330
+ failClass: "shape-error",
331
+ reasons: ["status is required"],
332
+ hasRemoteClaims: false,
333
+ unboundClaims: [],
334
+ };
335
+ }
336
+ const claims = detectRemoteClaims(evidence);
337
+ const hasRemoteClaims = claims.length > 0;
338
+ const unbound = unboundClaimKeys(evidence, claims);
339
+ const proof = (evidence.proof_status ?? "").toString().trim().toLowerCase();
340
+ const reasons = [...enumShapeReasons(evidence)];
341
+ const pass = isPassStatus(evidence.status);
342
+ // proof_status consistency
343
+ if (hasRemoteClaims && proof === "n/a-no-remote-claim") {
344
+ reasons.push("proof_status is n/a-no-remote-claim but remote claims are present (" +
345
+ claims.join(", ") +
346
+ ")");
347
+ }
348
+ if (!hasRemoteClaims && proof === "bound") {
349
+ reasons.push("proof_status is bound but no remote claims are present");
350
+ }
351
+ // Core rule: pass + remote claims requires bound + probes
352
+ if (hasRemoteClaims) {
353
+ if (proof !== "bound") {
354
+ reasons.push(`remote claims present (${claims.join(", ")}) require proof_status=bound` +
355
+ (proof ? ` (got ${proof})` : " (missing)"));
356
+ }
357
+ if (unbound.length > 0) {
358
+ reasons.push(`missing same-turn probes for: ${unbound.join(", ")} ` +
359
+ "(probe-then-fill: copy IDs from probe JSON/text, never recollection)");
360
+ }
361
+ }
362
+ else if (pass && (!proof || proof === "unbound")) {
363
+ // No remote claims under pass: n/a-no-remote-claim is the legal proof_status.
364
+ // unbound without remote claims is a soft shape issue under pass.
365
+ if (proof === "unbound") {
366
+ reasons.push("proof_status=unbound with no remote claims; use n/a-no-remote-claim under pass");
367
+ }
368
+ }
369
+ // Rank fail class
370
+ let failClass = "none";
371
+ let ok = reasons.length === 0;
372
+ if (pass && hasRemoteClaims && (proof !== "bound" || unbound.length > 0)) {
373
+ // Invented-done: complete-looking remote artifacts under pass without binding.
374
+ failClass = "invented-done";
375
+ ok = false;
376
+ if (!reasons.some((r) => r.includes("invented-done"))) {
377
+ reasons.unshift("invented-done: status pass with remote PR/SHA/CI/review claims " +
378
+ "without bound same-turn probes (stricter than empty-done)");
379
+ }
380
+ }
381
+ else if (pass && !hasRemoteClaims && isEmptyDone(evidence)) {
382
+ failClass = "empty-done";
383
+ ok = false;
384
+ reasons.unshift("empty-done: status pass with no work/ship/gate substance and no remote claims");
385
+ }
386
+ else if (!pass && hasRemoteClaims && (proof !== "bound" || unbound.length > 0)) {
387
+ failClass = "unbound-remote-claim";
388
+ ok = false;
389
+ }
390
+ else if (reasons.length > 0) {
391
+ failClass = "shape-error";
392
+ ok = false;
393
+ }
394
+ // Legal partial: work done + ship not_started without PR fields → ok when not pass-with-remote
395
+ if (ok &&
396
+ evidence.status === "partial" &&
397
+ !hasRemoteClaims &&
398
+ (evidence.ship?.state === "not_started" || !evidence.ship?.state)) {
399
+ return {
400
+ ok: true,
401
+ failClass: "none",
402
+ reasons: [],
403
+ hasRemoteClaims: false,
404
+ unboundClaims: [],
405
+ };
406
+ }
407
+ return {
408
+ ok,
409
+ failClass,
410
+ reasons,
411
+ hasRemoteClaims,
412
+ unboundClaims: unbound,
413
+ };
414
+ }
415
+ /**
416
+ * Convenience: true when status may legally be pass for this evidence.
417
+ * Equivalent to validateHandoffEvidence(...).ok under a pass status intent.
418
+ */
419
+ export function canClaimPass(evidence) {
420
+ const candidate = { ...evidence, status: "pass" };
421
+ return validateHandoffEvidence(candidate).ok;
422
+ }
423
+ //# sourceMappingURL=validate.js.map
@@ -54,6 +54,9 @@ export interface HookDispatchInput {
54
54
  readonly environ?: NodeJS.ProcessEnv;
55
55
  }
56
56
  export interface HookPolicySeams {
57
+ /** Active mutation-boundary verifier. Defaults to gated verification. */
58
+ readonly verifyRitual?: (projectRoot: string) => VerifyResult;
59
+ /** @deprecated Compatibility seam for existing tests and callers. */
57
60
  readonly inspectRitual?: (projectRoot: string) => VerifyResult;
58
61
  readonly inspectScope?: (projectRoot: string) => ActiveScopeInspection;
59
62
  readonly sessionStart?: (projectRoot: string) => {
@@ -11,7 +11,7 @@ import { detectBranch } from "../session/git.js";
11
11
  import { emitSessionRitualBlockedProcessCost } from "../session/process-cost.js";
12
12
  import { markRitualStaleAfterCompact } from "../session/ritual-sentinel.js";
13
13
  import { runSessionStartHookWrite } from "../session/session-start-hook.js";
14
- import { formatRitualRecoveryInstruction, inspectSessionRitual, } from "../session/verify-session-ritual.js";
14
+ import { formatRitualRecoveryInstruction, verifySessionRitual, } from "../session/verify-session-ritual.js";
15
15
  import { hookMcpArgsText, hookShellCommand, hookToolName, hookWriteTargetPath, missingToolNameMessage, record, } from "./classify/index.js";
16
16
  import { isAssistPosture, isEphemeralSpawn, isExploreSpawn, isReadOnlyHookContext, } from "./readonly.js";
17
17
  import { inspectActiveScope } from "./scope.js";
@@ -524,8 +524,17 @@ function inspectMutationGates(input, toolName, seams, options) {
524
524
  }
525
525
  let ritual;
526
526
  try {
527
- ritual = (seams.inspectRitual ??
528
- ((root) => inspectSessionRitual(root, { tier: "gated", posture: "mutation" })))(projectRoot);
527
+ // Mutation dispatch is a live gated boundary: active verification reruns
528
+ // non-cacheable agent-hook readiness instead of trusting persisted success.
529
+ // Its allow fixture is non-write and its deny fixture is read-only, so the
530
+ // installed-shim probe cannot recurse into this mutation path.
531
+ ritual = (seams.verifyRitual ??
532
+ seams.inspectRitual ??
533
+ ((root) => verifySessionRitual(root, {
534
+ tier: "gated",
535
+ posture: "mutation",
536
+ bypass: false,
537
+ })))(projectRoot);
529
538
  }
530
539
  catch (cause) {
531
540
  // #2994: best-effort local process-cost; never changes deny verdict.
package/dist/index.d.ts CHANGED
@@ -24,8 +24,10 @@ export * as evalReport from "./eval/report.js";
24
24
  export * as evalRun from "./eval/run.js";
25
25
  export * as events from "./events/attribution-ledger.js";
26
26
  export * from "./forward-coverage/evaluate.js";
27
+ export * as freshness from "./freshness/index.js";
27
28
  export { ContainedWriteError, ContainedWriteErrorCode, type ContainedWriteInput, type ContainedWriteMode, type ContainedWriteResult, containedWrite, resolveContainedTarget, } from "./fs/contained-write.js";
28
29
  export { assertDestinationNotSymlink, assertDirectoryNotSymlink, assertProjectionContained, assertWriteTargetSafe, PROJECTION_CONTAINMENT_REFUSED_EXIT_CODE, ProjectionContainmentError, walkDirectoryRejectSymlinks, } from "./fs/projection-containment.js";
30
+ export * as handoffEvidence from "./handoff-evidence/index.js";
29
31
  export * as intake from "./intake/index.js";
30
32
  export * as layout from "./layout/index.js";
31
33
  export * as legacyBridge from "./legacy-bridge/index.js";
@@ -33,6 +35,7 @@ export * as lifecycle from "./lifecycle/index.js";
33
35
  export * as metrics from "./metrics/index.js";
34
36
  export * as orchestration from "./orchestration/index.js";
35
37
  export * as packs from "./packs/index.js";
38
+ export * as parentTurnShape from "./parent-turn-shape/index.js";
36
39
  export * as platform from "./platform/index.js";
37
40
  export * as policy from "./policy/index.js";
38
41
  export * as prClosingKeywords from "./pr-closing-keywords/index.js";
package/dist/index.js CHANGED
@@ -24,9 +24,11 @@ export * as evalReport from "./eval/report.js";
24
24
  export * as evalRun from "./eval/run.js";
25
25
  export * as events from "./events/attribution-ledger.js";
26
26
  export * from "./forward-coverage/evaluate.js";
27
+ export * as freshness from "./freshness/index.js";
27
28
  // #2951 Phase 1: contained-write API for product sinks.
28
29
  export { ContainedWriteError, ContainedWriteErrorCode, containedWrite, resolveContainedTarget, } from "./fs/contained-write.js";
29
30
  export { assertDestinationNotSymlink, assertDirectoryNotSymlink, assertProjectionContained, assertWriteTargetSafe, PROJECTION_CONTAINMENT_REFUSED_EXIT_CODE, ProjectionContainmentError, walkDirectoryRejectSymlinks, } from "./fs/projection-containment.js";
31
+ export * as handoffEvidence from "./handoff-evidence/index.js";
30
32
  export * as intake from "./intake/index.js";
31
33
  export * as layout from "./layout/index.js";
32
34
  export * as legacyBridge from "./legacy-bridge/index.js";
@@ -34,6 +36,7 @@ export * as lifecycle from "./lifecycle/index.js";
34
36
  export * as metrics from "./metrics/index.js";
35
37
  export * as orchestration from "./orchestration/index.js";
36
38
  export * as packs from "./packs/index.js";
39
+ export * as parentTurnShape from "./parent-turn-shape/index.js";
37
40
  export * as platform from "./platform/index.js";
38
41
  export * as policy from "./policy/index.js";
39
42
  export * as prClosingKeywords from "./pr-closing-keywords/index.js";
@@ -6,7 +6,7 @@ export declare const DEFT_HOOK_COMMAND_MARKER = "deft-hook";
6
6
  export declare const LEGACY_DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
7
7
  export declare const AGENT_HOOK_PATHS: readonly [".claude/settings.json", ".grok/hooks/deft.json", ".cursor/hooks.json", ".codex/hooks.json"];
8
8
  export type AgentHookPath = (typeof AGENT_HOOK_PATHS)[number];
9
- export type AgentHookRegistrationStatus = "healthy" | "missing" | "drifted";
9
+ export type AgentHookRegistrationStatus = "healthy" | "disabled" | "missing" | "drifted";
10
10
  /** Whether the host receives a compact/resume hook deposit (#2113). */
11
11
  export type AgentHookCompactSupport = "deposited" | "unsupported";
12
12
  export interface AgentHookInspection {
@@ -312,7 +312,10 @@ function hasNestedRegistration(config, host, options = {}) {
312
312
  const sessionCommand = command(host, "session.start");
313
313
  const toolCommand = command(host, "tool.before");
314
314
  const compactCommand = command(host, "session.compact");
315
- const base = session.some((entry) => nestedCommands(entry).includes(sessionCommand)) &&
315
+ const base = session.some((entry) => {
316
+ const group = object(entry);
317
+ return group?.matcher === undefined && nestedCommands(entry).includes(sessionCommand);
318
+ }) &&
316
319
  preTool.some((entry) => {
317
320
  const group = object(entry);
318
321
  return (group?.matcher === DIRECT_WRITE_HOOK_MATCHER && nestedCommands(entry).includes(toolCommand));
@@ -409,7 +412,7 @@ export function inspectAgentHookDeposit(projectRoot, hostHooksPolicy = loadHostH
409
412
  return {
410
413
  host: definition.host,
411
414
  path: definition.path,
412
- status: "healthy",
415
+ status: "disabled",
413
416
  compactSupport: definition.compactSupport,
414
417
  detail: `plan.policy.hostHooks.${definition.host} is false — Directive hook deposit is skipped for this host.`,
415
418
  };
@@ -8,7 +8,7 @@
8
8
  * and scope briefs are never installer-managed; if they reappear in
9
9
  * `installerManagedMatchers()`, unit tests and deposit-time assert fail closed.
10
10
  *
11
- * Refs #1576, #1453, #1430, #3029, #3030.
11
+ * Refs #1576, #1453, #1430, #3029, #3030, #3127, #3117.
12
12
  */
13
13
  import { type InitDepositIo } from "./constants.js";
14
14
  export declare const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
@@ -8,7 +8,7 @@
8
8
  * and scope briefs are never installer-managed; if they reappear in
9
9
  * `installerManagedMatchers()`, unit tests and deposit-time assert fail closed.
10
10
  *
11
- * Refs #1576, #1453, #1430, #3029, #3030.
11
+ * Refs #1576, #1453, #1430, #3029, #3030, #3127, #3117.
12
12
  */
13
13
  import { execFileSync } from "node:child_process";
14
14
  import { existsSync, readdirSync, rmSync } from "node:fs";
@@ -82,6 +82,16 @@ export function installerManagedMatchers() {
82
82
  { exact: CODEQL_CONFIG_REL },
83
83
  { exact: CORE_GUARD_WORKFLOW_REL },
84
84
  { exact: "Taskfile.yml" },
85
+ // Upgrade co-travel unit (#3127): npm pin + lock follow-through and the
86
+ // freshness live stamp (#3117) are part of a normal framework upgrade, not
87
+ // product feature work. Path-level allow for v1 (stricter “only
88
+ // @deftai/directive version changed” is a follow-on if needed).
89
+ // Mixing .deft/core/** with true app/product paths still fails (#1430).
90
+ { exact: "package.json" },
91
+ { exact: "package-lock.json" },
92
+ { exact: "pnpm-lock.yaml" },
93
+ { exact: "yarn.lock" },
94
+ { exact: ".deft/GENERATION.json" },
85
95
  // Legacy vbrief/ tree -- retained for not-yet-migrated consumers.
86
96
  { exact: "vbrief/.deft-version" },
87
97
  { exact: "vbrief/vbrief.md" },
@@ -476,9 +486,11 @@ export function printCommitGuidance(io, paths, staged) {
476
486
  if (paths.length === 0)
477
487
  return;
478
488
  const addCmd = `git add ${paths.join(" ")}`;
479
- io.printf("\nCommit hygiene (#1453, #1671): keep the framework deposit in its OWN branch/PR.\n");
480
- io.printf("Do NOT use `git add -A` -- mixing the payload with your own files trips the\n");
489
+ io.printf("\nCommit hygiene (#1453, #1671, #3127): keep the framework upgrade in its OWN branch/PR.\n");
490
+ io.printf("Do NOT use `git add -A` -- mixing the payload with product/app files trips the\n");
481
491
  io.printf("deft-core-guard CI check.\n");
492
+ io.printf("One upgrade PR MAY co-travel: .deft/core/** + installer-managed deposits + package.json\n");
493
+ io.printf("pin/lock + .deft/GENERATION.json. True app/product paths still require a separate PR.\n");
482
494
  if (staged) {
483
495
  io.printf("The installer already staged ONLY these framework + installer-managed paths:\n");
484
496
  io.printf(` ${addCmd}\n`);
@@ -488,7 +500,7 @@ export function printCommitGuidance(io, paths, staged) {
488
500
  io.printf(` ${addCmd}\n`);
489
501
  }
490
502
  io.printf("Then take the framework deposit through the full PR lifecycle so deft-core-guard\n");
491
- io.printf("evaluates a clean, standalone PR:\n");
503
+ io.printf("evaluates a clean, standalone upgrade PR:\n");
492
504
  io.printf(` 1. Branch: git switch -c ${COMMIT_HYGIENE_BRANCH_NAME}\n`);
493
505
  io.printf(' 2. Commit: git commit -m "chore(deft): update framework payload"\n');
494
506
  io.printf(` 3. Push: git push -u origin ${COMMIT_HYGIENE_BRANCH_NAME}\n`);
@@ -5,6 +5,7 @@
5
5
  * scaffold, skills/githooks/#1430 neutralization, and Taskfile wiring.
6
6
  * Refs #1942, #11, #1430.
7
7
  */
8
+ import { type AgentHookReadinessResult } from "../verify-env/agent-hook-readiness.js";
8
9
  import { type LegacyLayoutDetection } from "./legacy-detect.js";
9
10
  import { type InitDepositIo, writeConsumerGitHooks } from "./scaffold.js";
10
11
  export interface InitDepositArgs {
@@ -28,11 +29,16 @@ export interface InitDepositSeams {
28
29
  nowIso?: () => string;
29
30
  gitHooks?: Parameters<typeof writeConsumerGitHooks>[3];
30
31
  detectLegacy?: (projectDir: string) => LegacyLayoutDetection;
32
+ evaluateAgentHookReadiness?: (projectRoot: string) => AgentHookReadinessResult;
31
33
  }
32
34
  export declare function parseInitArgv(canonicalArgv: readonly string[], userArgv?: readonly string[]): InitDepositArgs;
33
35
  export declare function userConfigDir(): string;
34
36
  export declare function createUserConfigDir(io: InitDepositIo): string;
35
- export declare function buildInstallSummaryJson(result: InitDepositResult, options: InitDepositArgs): Record<string, unknown>;
37
+ export declare function buildInstallSummaryJson(input: {
38
+ result: InitDepositResult;
39
+ options: InitDepositArgs;
40
+ readiness: AgentHookReadinessResult | undefined;
41
+ }): Record<string, unknown>;
36
42
  export declare function printNextSteps(result: InitDepositResult, io: InitDepositIo): void;
37
43
  /**
38
44
  * Ensure a minimal render-ready PROJECT-DEFINITION exists after lifecycle dirs