@deftai/directive-core 0.93.0 → 0.95.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 (118) hide show
  1. package/dist/authz/classify.js +485 -0
  2. package/dist/cache/fetch.d.ts +29 -0
  3. package/dist/cache/fetch.js +131 -4
  4. package/dist/cache/operations.d.ts +8 -0
  5. package/dist/cache/operations.js +67 -4
  6. package/dist/check/cached-orchestrator.js +11 -0
  7. package/dist/check/consumer-gate-integrity.d.ts +68 -0
  8. package/dist/check/consumer-gate-integrity.js +265 -0
  9. package/dist/check/index.d.ts +1 -0
  10. package/dist/check/index.js +1 -0
  11. package/dist/check/orchestrator.js +10 -0
  12. package/dist/content-contracts/skills/greptile-detector.d.ts +9 -0
  13. package/dist/content-contracts/skills/greptile-detector.js +9 -2
  14. package/dist/doctor/main.js +98 -9
  15. package/dist/doctor/types.d.ts +2 -2
  16. package/dist/finish-loop/pr-finish-loop.js +1 -0
  17. package/dist/freshness/bind.d.ts +67 -0
  18. package/dist/freshness/bind.js +201 -0
  19. package/dist/freshness/cli.d.ts +30 -0
  20. package/dist/freshness/cli.js +180 -0
  21. package/dist/freshness/compare.d.ts +14 -0
  22. package/dist/freshness/compare.js +134 -0
  23. package/dist/freshness/generation.d.ts +44 -0
  24. package/dist/freshness/generation.js +172 -0
  25. package/dist/freshness/index.d.ts +13 -0
  26. package/dist/freshness/index.js +13 -0
  27. package/dist/freshness/report.d.ts +43 -0
  28. package/dist/freshness/report.js +139 -0
  29. package/dist/freshness/types.d.ts +70 -0
  30. package/dist/freshness/types.js +30 -0
  31. package/dist/handoff-evidence/index.d.ts +8 -0
  32. package/dist/handoff-evidence/index.js +7 -0
  33. package/dist/handoff-evidence/validate.d.ts +105 -0
  34. package/dist/handoff-evidence/validate.js +423 -0
  35. package/dist/hooks/dispatcher.d.ts +27 -2
  36. package/dist/hooks/dispatcher.js +134 -14
  37. package/dist/hooks/readonly.d.ts +14 -0
  38. package/dist/hooks/readonly.js +126 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +2 -0
  41. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  42. package/dist/init-deposit/agent-hooks.js +5 -2
  43. package/dist/init-deposit/init-deposit.d.ts +7 -1
  44. package/dist/init-deposit/init-deposit.js +24 -4
  45. package/dist/init-deposit/refresh.d.ts +9 -1
  46. package/dist/init-deposit/refresh.js +48 -5
  47. package/dist/intake/issue-ingest.d.ts +20 -0
  48. package/dist/intake/issue-ingest.js +58 -0
  49. package/dist/policy/index.d.ts +4 -0
  50. package/dist/policy/index.js +50 -1
  51. package/dist/policy/min-greptile-confidence.d.ts +57 -0
  52. package/dist/policy/min-greptile-confidence.js +123 -0
  53. package/dist/pr-merge-readiness/compute.d.ts +11 -1
  54. package/dist/pr-merge-readiness/compute.js +17 -4
  55. package/dist/pr-merge-readiness/evaluate.d.ts +8 -1
  56. package/dist/pr-merge-readiness/evaluate.js +7 -4
  57. package/dist/pr-merge-readiness/mergeability.d.ts +7 -1
  58. package/dist/pr-merge-readiness/mergeability.js +14 -2
  59. package/dist/pr-monitor/main.d.ts +2 -0
  60. package/dist/pr-monitor/main.js +31 -1
  61. package/dist/pr-monitor/monitor.js +5 -1
  62. package/dist/pr-monitor/readiness.js +3 -1
  63. package/dist/pr-monitor/types.d.ts +8 -0
  64. package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
  65. package/dist/pr-wait-mergeable/cascade.js +28 -1
  66. package/dist/pr-wait-mergeable/main.d.ts +2 -0
  67. package/dist/pr-wait-mergeable/main.js +16 -0
  68. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  69. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  70. package/dist/pr-wait-mergeable/wrappers.js +3 -0
  71. package/dist/pr-watch/constants.d.ts +4 -1
  72. package/dist/pr-watch/constants.js +5 -2
  73. package/dist/pr-watch/main.js +1 -0
  74. package/dist/pr-watch/probe.d.ts +1 -1
  75. package/dist/pr-watch/probe.js +4 -1
  76. package/dist/pr-watch/types.d.ts +7 -1
  77. package/dist/pr-watch/watch.js +3 -2
  78. package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
  79. package/dist/review-monitor/index.d.ts +1 -0
  80. package/dist/review-monitor/index.js +1 -0
  81. package/dist/review-monitor/l4-owner.d.ts +46 -0
  82. package/dist/review-monitor/l4-owner.js +262 -0
  83. package/dist/session/ritual-entrypoint.d.ts +6 -2
  84. package/dist/session/ritual-entrypoint.js +15 -2
  85. package/dist/session/session-ready.js +31 -14
  86. package/dist/session/session-start.d.ts +2 -1
  87. package/dist/session/session-start.js +66 -3
  88. package/dist/session/verify-session-ritual.d.ts +4 -2
  89. package/dist/session/verify-session-ritual.js +11 -11
  90. package/dist/slash/product-set.js +4 -1
  91. package/dist/swarm/verify-review-clean.d.ts +4 -3
  92. package/dist/swarm/verify-review-clean.js +28 -73
  93. package/dist/tool-events/classify.js +1 -0
  94. package/dist/triage/classify/index.d.ts +9 -0
  95. package/dist/triage/classify/index.js +34 -2
  96. package/dist/triage/classify/label-mirror.d.ts +143 -0
  97. package/dist/triage/classify/label-mirror.js +684 -0
  98. package/dist/triage/help/registry-data.d.ts +7 -7
  99. package/dist/triage/help/registry-data.js +27 -7
  100. package/dist/triage/queue/show.d.ts +1 -1
  101. package/dist/triage/queue/show.js +4 -2
  102. package/dist/triage/welcome/writers.js +16 -7
  103. package/dist/umbrella-current-shape/index.d.ts +45 -0
  104. package/dist/umbrella-current-shape/index.js +162 -8
  105. package/dist/vbrief-reconcile/index.d.ts +2 -2
  106. package/dist/vbrief-reconcile/index.js +1 -1
  107. package/dist/vbrief-reconcile/types.d.ts +14 -0
  108. package/dist/vbrief-reconcile/umbrellas.d.ts +31 -2
  109. package/dist/vbrief-reconcile/umbrellas.js +231 -19
  110. package/dist/vbrief-validate/project-definition.js +1 -1
  111. package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
  112. package/dist/verify-env/agent-hook-readiness.js +150 -0
  113. package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
  114. package/dist/verify-env/agent-hooks-live-probe.js +120 -107
  115. package/dist/verify-env/agent-hooks.js +11 -3
  116. package/dist/verify-env/index.d.ts +1 -0
  117. package/dist/verify-env/index.js +1 -0
  118. package/package.json +7 -3
@@ -1,15 +1,18 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { basename, join, resolve } from "node:path";
3
3
  import { referenceTypeMatches } from "@deftai/directive-types";
4
- import { createIssueComment, editIssueCommentBody, GitHubBodyError, } from "../intake/github-body.js";
4
+ import { createIssueComment, editIssueBody, editIssueCommentBody, fetchIssueBody, GitHubBodyError, } from "../intake/github-body.js";
5
+ import { fetchIssueStates, IssueState } from "../intake/reconcile-issues.js";
5
6
  import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "../layout/resolve.js";
6
7
  import { call } from "../scm/call.js";
7
- import { extractIssueRef } from "../triage/reconcile/parse-uri.js";
8
+ import { extractIssueRef, parseGithubIssueUri } from "../triage/reconcile/parse-uri.js";
8
9
  import { isRepoMutationAllowed } from "./repo-guard.js";
9
10
  export const OPEN_FOLDERS = ["proposed", "pending", "active"];
10
11
  export const CLOSED_FOLDERS = ["completed", "cancelled"];
11
12
  export const LIFECYCLE_FOLDERS = [...OPEN_FOLDERS, ...CLOSED_FOLDERS];
12
13
  export const CHILD_REF_TYPE = "x-xbrief/plan";
14
+ /** Synthetic lifecycle folder for issue-only children with no local xBRIEF (#1649). */
15
+ export const UNKNOWN_FOLDER = "unknown";
13
16
  const SCM_SOURCE = "github-issue";
14
17
  const HEADER_RE = /^## Current shape \(as of pass-(\d+)\)/m;
15
18
  // ReDoS-hardened (#1782 s4 / CodeQL js/polynomial-redos): the original
@@ -23,6 +26,12 @@ const HISTORY_RE = /^Child-count history:\s*(\S.*|)$/m;
23
26
  const LAST_UPDATED_RE = /^Last updated:\s*(\S.*|)$/m;
24
27
  const LAST_PASS_TYPE_RE = /^Last pass type:\s*(\S.*|)$/m;
25
28
  const HISTORY_TOKEN_RE = /^\s*pass-(\d+):\s*(\d+)\s*$/;
29
+ /**
30
+ * Issue-body checklist line with a `#N` child ref (#1649).
31
+ * Captures leading marker, checkbox state, and the remainder after `]`.
32
+ * Linear-time: no nested quantifiers on overlapping classes.
33
+ */
34
+ const CHECKLIST_ISSUE_LINE_RE = /^([ \t]*[-*+][ \t]+)\[([ xX])\]([ \t]*#[0-9]+\b.*)$/;
26
35
  const READING_ORDER = "1. Read the umbrella issue body.\n" +
27
36
  "2. Read this current-shape comment.\n" +
28
37
  "3. Read the amendment comments in chronological order for the full audit trail.";
@@ -52,12 +61,14 @@ export function childFromData(data, folder, fallbackId) {
52
61
  : {};
53
62
  const rawDeps = swarm.depends_on;
54
63
  const dependsOn = Array.isArray(rawDeps) ? rawDeps.map((d) => String(d)) : [];
64
+ const [, issueNumber] = extractIssueRef(data);
55
65
  return {
56
66
  story_id: String(plan.id ?? fallbackId),
57
67
  title: String(plan.title ?? plan.id ?? fallbackId),
58
68
  kind: String(metadata.kind ?? "story"),
59
69
  folder,
60
70
  depends_on: dependsOn,
71
+ issue_number: issueNumber,
61
72
  };
62
73
  }
63
74
  export function buildChildIndex(vbriefDir) {
@@ -80,7 +91,22 @@ export function buildChildIndex(vbriefDir) {
80
91
  }
81
92
  return index;
82
93
  }
83
- export function computeChildren(epicData, index) {
94
+ /** Index children by forge issue number for github-issue ref resolution (#1649). */
95
+ export function indexChildrenByIssueNumber(index) {
96
+ const byIssue = new Map();
97
+ for (const child of Object.values(index)) {
98
+ if (typeof child.issue_number === "number" && !byIssue.has(child.issue_number)) {
99
+ byIssue.set(child.issue_number, child);
100
+ }
101
+ }
102
+ return byIssue;
103
+ }
104
+ /**
105
+ * Resolve epic children from plan refs AND github-issue refs (#1649 defect 2).
106
+ * The epic's own github-issue ref is skipped when `epicIssueNumber` is provided.
107
+ * Issue-only children (no local xBRIEF) are synthesized with folder `unknown`.
108
+ */
109
+ export function computeChildren(epicData, index, options = {}) {
84
110
  const plan = typeof epicData.plan === "object" && epicData.plan !== null && !Array.isArray(epicData.plan)
85
111
  ? epicData.plan
86
112
  : {};
@@ -89,21 +115,102 @@ export function computeChildren(epicData, index) {
89
115
  const seen = new Set();
90
116
  if (!Array.isArray(refs))
91
117
  return children;
118
+ const byIssue = indexChildrenByIssueNumber(index);
119
+ const epicIssue = options.epicIssueNumber ?? null;
92
120
  for (const ref of refs) {
93
121
  if (typeof ref !== "object" || ref === null || Array.isArray(ref))
94
122
  continue;
95
123
  const rec = ref;
96
- if (!referenceTypeMatches(String(rec.type ?? ""), "plan"))
97
- continue;
98
- const name = basename(String(rec.uri ?? ""));
99
- const child = index[name];
100
- if (!child || seen.has(child.story_id))
124
+ const refType = String(rec.type ?? "");
125
+ if (referenceTypeMatches(refType, "plan")) {
126
+ const name = basename(String(rec.uri ?? ""));
127
+ const child = index[name];
128
+ if (!child || seen.has(child.story_id))
129
+ continue;
130
+ seen.add(child.story_id);
131
+ children.push(child);
101
132
  continue;
102
- seen.add(child.story_id);
103
- children.push(child);
133
+ }
134
+ if (referenceTypeMatches(refType, "github-issue")) {
135
+ const [, number] = parseGithubIssueUri(rec.uri);
136
+ if (number === null)
137
+ continue;
138
+ if (epicIssue !== null && number === epicIssue)
139
+ continue;
140
+ const fromIndex = byIssue.get(number);
141
+ if (fromIndex) {
142
+ if (seen.has(fromIndex.story_id))
143
+ continue;
144
+ seen.add(fromIndex.story_id);
145
+ children.push(fromIndex);
146
+ continue;
147
+ }
148
+ // Synthetic child: hand-filed / issue:emit epic with issue-only child links.
149
+ const syntheticId = `#${number}`;
150
+ if (seen.has(syntheticId))
151
+ continue;
152
+ seen.add(syntheticId);
153
+ const title = typeof rec.title === "string" && rec.title.trim().length > 0
154
+ ? rec.title.trim()
155
+ : syntheticId;
156
+ children.push({
157
+ story_id: syntheticId,
158
+ title,
159
+ kind: "story",
160
+ folder: UNKNOWN_FOLDER,
161
+ depends_on: [],
162
+ issue_number: number,
163
+ });
164
+ }
104
165
  }
105
166
  return children;
106
167
  }
168
+ /**
169
+ * Whether a child counts as open for current-shape + checklist (#1649 defect 3).
170
+ * Prefer forge issue state when present; fall back to lifecycle folder.
171
+ * `unknown` (issue-only, no local xBRIEF) counts open without forge confirmation.
172
+ */
173
+ export function isChildOpen(child, forgeStates) {
174
+ if (typeof child.issue_number === "number" &&
175
+ forgeStates != null &&
176
+ forgeStates.has(child.issue_number)) {
177
+ return forgeStates.get(child.issue_number) === "open";
178
+ }
179
+ if (CLOSED_FOLDERS.includes(child.folder))
180
+ return false;
181
+ // proposed/pending/active/unknown (and any unexpected folder) → open
182
+ return true;
183
+ }
184
+ /**
185
+ * Reconcile umbrella issue-body checkboxes `- [ ] #N` / `- [x] #N` to child state (#1649).
186
+ * Only lines that mention a known child issue number are rewritten; other checkboxes untouched.
187
+ * Idempotent: returns `changed: false` when already correct.
188
+ */
189
+ export function reconcileBodyChecklist(body, closedIssueNumbers, knownIssueNumbers) {
190
+ if (knownIssueNumbers.size === 0)
191
+ return { body, changed: false };
192
+ const lines = body.split("\n");
193
+ let changed = false;
194
+ const out = lines.map((line) => {
195
+ const match = CHECKLIST_ISSUE_LINE_RE.exec(line);
196
+ if (!match)
197
+ return line;
198
+ const issueMatch = /#(\d+)\b/.exec(match[3] ?? "");
199
+ if (!issueMatch?.[1])
200
+ return line;
201
+ const issueNum = Number(issueMatch[1]);
202
+ if (!knownIssueNumbers.has(issueNum))
203
+ return line;
204
+ const wantChecked = closedIssueNumbers.has(issueNum);
205
+ const isChecked = (match[2] ?? " ").toLowerCase() === "x";
206
+ if (isChecked === wantChecked)
207
+ return line;
208
+ changed = true;
209
+ const marker = wantChecked ? "x" : " ";
210
+ return `${match[1]}[${marker}]${match[3]}`;
211
+ });
212
+ return { body: out.join("\n"), changed };
213
+ }
107
214
  export function computeWaves(children) {
108
215
  const ids = new Set(children.map((c) => c.story_id));
109
216
  const deps = {};
@@ -249,18 +356,92 @@ export class ScmUmbrellaClient {
249
356
  throw new UmbrellaScmError(`create comment #${issueNumber} (${repo}) failed: ${message}`);
250
357
  }
251
358
  }
359
+ fetchIssueStates(repo, issueNumbers) {
360
+ const result = new Map();
361
+ if (issueNumbers.length === 0)
362
+ return result;
363
+ const states = fetchIssueStates(repo, new Set(issueNumbers));
364
+ if (states === null) {
365
+ throw new UmbrellaScmError(`fetch issue states (${repo}) failed`);
366
+ }
367
+ for (const [num, state] of states) {
368
+ if (!(state instanceof IssueState))
369
+ continue;
370
+ const value = state.value.toUpperCase();
371
+ if (value === "OPEN")
372
+ result.set(num, "open");
373
+ else if (value === "CLOSED")
374
+ result.set(num, "closed");
375
+ // NOT_FOUND / other → omit so folder fallback applies
376
+ }
377
+ return result;
378
+ }
379
+ fetchIssueBody(repo, issueNumber) {
380
+ try {
381
+ return fetchIssueBody(repo, issueNumber);
382
+ }
383
+ catch (exc) {
384
+ const message = exc instanceof GitHubBodyError ? exc.message : String(exc);
385
+ throw new UmbrellaScmError(`fetch issue body #${issueNumber} (${repo}) failed: ${message}`);
386
+ }
387
+ }
388
+ editIssueBody(repo, issueNumber, body) {
389
+ try {
390
+ editIssueBody(repo, issueNumber, { body });
391
+ }
392
+ catch (exc) {
393
+ const message = exc instanceof GitHubBodyError ? exc.message : String(exc);
394
+ throw new UmbrellaScmError(`edit issue body #${issueNumber} (${repo}) failed: ${message}`);
395
+ }
396
+ }
252
397
  }
253
- function planShape(epicData, index) {
254
- const children = computeChildren(epicData, index);
398
+ function planShape(epicData, index, options = {}) {
399
+ const children = computeChildren(epicData, index, {
400
+ epicIssueNumber: options.epicIssueNumber,
401
+ });
255
402
  const openChildren = children
256
- .filter((c) => OPEN_FOLDERS.includes(c.folder))
403
+ .filter((c) => isChildOpen(c, options.forgeStates))
257
404
  .sort((a, b) => a.story_id.localeCompare(b.story_id));
258
405
  const closedChildren = children
259
- .filter((c) => !OPEN_FOLDERS.includes(c.folder))
406
+ .filter((c) => !isChildOpen(c, options.forgeStates))
260
407
  .sort((a, b) => a.story_id.localeCompare(b.story_id));
261
408
  const waves = computeWaves(children);
262
409
  return [openChildren, closedChildren, waves];
263
410
  }
411
+ function resolveForgeStates(client, repo, children) {
412
+ if (typeof client.fetchIssueStates !== "function")
413
+ return null;
414
+ const numbers = children
415
+ .map((c) => c.issue_number)
416
+ .filter((n) => typeof n === "number");
417
+ if (numbers.length === 0)
418
+ return null;
419
+ return client.fetchIssueStates(repo, numbers);
420
+ }
421
+ function reconcileChecklistForEpic(client, repo, issueNumber, closedChildren, allChildren, dryRun) {
422
+ if (typeof client.fetchIssueBody !== "function" || typeof client.editIssueBody !== "function") {
423
+ return "skipped";
424
+ }
425
+ const known = new Set();
426
+ for (const c of allChildren) {
427
+ if (typeof c.issue_number === "number")
428
+ known.add(c.issue_number);
429
+ }
430
+ if (known.size === 0)
431
+ return "skipped";
432
+ const closed = new Set();
433
+ for (const c of closedChildren) {
434
+ if (typeof c.issue_number === "number")
435
+ closed.add(c.issue_number);
436
+ }
437
+ const currentBody = client.fetchIssueBody(repo, issueNumber);
438
+ const { body: nextBody, changed } = reconcileBodyChecklist(currentBody, closed, known);
439
+ if (!changed)
440
+ return "unchanged";
441
+ if (!dryRun)
442
+ client.editIssueBody(repo, issueNumber, nextBody);
443
+ return "edited";
444
+ }
264
445
  function reconcileOneEpic(epicData, index, options) {
265
446
  const mutateGate = isRepoMutationAllowed(options.repo, options.projectRoot, {
266
447
  allowCrossRepo: options.allowCrossRepo,
@@ -270,10 +451,28 @@ function reconcileOneEpic(epicData, index, options) {
270
451
  if (!mutateGate.allowed) {
271
452
  throw new Error(mutateGate.reason ?? `refusing cross-repo mutation on ${options.repo}`);
272
453
  }
273
- const [openChildren, closedChildren, waves] = planShape(epicData, index);
274
- const total = openChildren.length + closedChildren.length;
454
+ // Resolve children once without forge state to know which issue numbers to fetch.
455
+ const childrenProbe = computeChildren(epicData, index, {
456
+ epicIssueNumber: options.number,
457
+ });
458
+ const forgeStates = resolveForgeStates(options.client, options.repo, childrenProbe);
459
+ const [openChildren, closedChildren, waves] = planShape(epicData, index, {
460
+ epicIssueNumber: options.number,
461
+ forgeStates,
462
+ });
463
+ const allChildren = [...openChildren, ...closedChildren];
464
+ const total = allChildren.length;
275
465
  const comments = options.client.fetchComments(options.repo, options.number);
276
466
  const existing = comments.find((c) => hasCurrentShape(c.body));
467
+ let checklistAction = "skipped";
468
+ try {
469
+ checklistAction = reconcileChecklistForEpic(options.client, options.repo, options.number, closedChildren, allChildren, options.dryRun);
470
+ }
471
+ catch (exc) {
472
+ // Checklist is best-effort relative to comment path: surface as throw so
473
+ // callers see the failure (body drift is the #1649 user-visible bug).
474
+ throw new UmbrellaScmError(exc instanceof Error ? exc.message : `checklist reconcile failed: ${String(exc)}`);
475
+ }
277
476
  if (!existing) {
278
477
  const body = renderBody({
279
478
  passN: 1,
@@ -293,6 +492,7 @@ function reconcileOneEpic(epicData, index, options) {
293
492
  action: "created",
294
493
  pass_n: 1,
295
494
  body,
495
+ checklist_action: checklistAction,
296
496
  };
297
497
  }
298
498
  const parsed = parseCurrentShape(existing.body);
@@ -308,13 +508,16 @@ function reconcileOneEpic(epicData, index, options) {
308
508
  history: parsed.history.length > 0 ? parsed.history : [[prevPass, total]],
309
509
  });
310
510
  if (candidate === existing.body) {
511
+ // Comment unchanged; still report edited when checklist flipped (#1649).
512
+ const action = checklistAction === "edited" ? "edited" : "unchanged";
311
513
  return {
312
514
  story_id: options.storyId,
313
515
  repo: options.repo,
314
516
  issue_number: options.number,
315
- action: "unchanged",
517
+ action,
316
518
  pass_n: prevPass,
317
519
  body: candidate,
520
+ checklist_action: checklistAction,
318
521
  };
319
522
  }
320
523
  const passN = prevPass + 1;
@@ -336,6 +539,7 @@ function reconcileOneEpic(epicData, index, options) {
336
539
  action: "edited",
337
540
  pass_n: passN,
338
541
  body,
542
+ checklist_action: checklistAction,
339
543
  };
340
544
  }
341
545
  export function nowIso(date = new Date()) {
@@ -441,13 +645,19 @@ export function reconcileUmbrellas(projectRoot, options = {}) {
441
645
  }
442
646
  return [outcome.errors.length > 0 ? 1 : 0, outcome];
443
647
  }
648
+ function formatChecklistSuffix(action) {
649
+ if (action === undefined || action === "skipped")
650
+ return "";
651
+ return `; checklist=${action}`;
652
+ }
444
653
  export function renderUmbrellasReport(outcome) {
445
654
  const lines = ["vBRIEF reconcile umbrellas", ""];
446
655
  const suffix = outcome.dry_run ? " (dry-run)" : "";
447
656
  lines.push(`Changed${suffix}:`);
448
657
  if (outcome.changed.length > 0) {
449
658
  for (const c of outcome.changed) {
450
- lines.push(`- #${c.issue_number} (${c.repo}) [${c.story_id}]: ${c.action} -> pass-${c.pass_n}`);
659
+ lines.push(`- #${c.issue_number} (${c.repo}) [${c.story_id}]: ${c.action} -> pass-${c.pass_n}` +
660
+ formatChecklistSuffix(c.checklist_action));
451
661
  }
452
662
  }
453
663
  else {
@@ -457,7 +667,8 @@ export function renderUmbrellasReport(outcome) {
457
667
  lines.push("Unchanged:");
458
668
  if (outcome.unchanged.length > 0) {
459
669
  for (const c of outcome.unchanged) {
460
- lines.push(`- #${c.issue_number} (${c.repo}) [${c.story_id}]: pass-${c.pass_n}`);
670
+ lines.push(`- #${c.issue_number} (${c.repo}) [${c.story_id}]: pass-${c.pass_n}` +
671
+ formatChecklistSuffix(c.checklist_action));
461
672
  }
462
673
  }
463
674
  else {
@@ -484,6 +695,7 @@ export function umbrellasOutcomeToJson(outcome) {
484
695
  issue_number: c.issue_number,
485
696
  action: c.action,
486
697
  pass_n: c.pass_n,
698
+ checklist_action: c.checklist_action ?? "skipped",
487
699
  });
488
700
  return {
489
701
  changed: outcome.changed.map(toChange),
@@ -78,7 +78,7 @@ export function validateProjectDefinition(filepath, data, vbriefDir) {
78
78
  }
79
79
  const uriRaw = ref.uri;
80
80
  const uri = typeof uriRaw === "string" ? uriRaw : "";
81
- if (uri && uri.startsWith("file://")) {
81
+ if (uri.startsWith("file://")) {
82
82
  const refPath = uri.replace("file://", "");
83
83
  const fullPath = resolve(vbriefDir, refPath);
84
84
  if (!isRelativeTo(fullPath, resolvedRoot)) {
@@ -0,0 +1,42 @@
1
+ import type { HookHost } from "../hooks/dispatcher.js";
2
+ import type { AgentHookInspection } from "../init-deposit/agent-hooks.js";
3
+ import type { HostHooksPolicy } from "../policy/host-hooks.js";
4
+ import { type AgentHookHealthResult } from "./agent-hooks.js";
5
+ import { type AgentHookLiveProbeResult, type AgentHookLiveProbeSeams } from "./agent-hooks-live-probe.js";
6
+ import type { OutputStream } from "./verify-hooks-installed.js";
7
+ export type AgentHookReadinessRegistration = "registered" | "disabled" | "missing" | "drifted" | "not-applicable";
8
+ export type AgentHookReadinessFunctionality = "functional" | "non-functional" | "unavailable" | "not-run" | "disabled" | "not-applicable";
9
+ export type AgentHookReadinessTrust = "manual-review-required" | "not-applicable" | "disabled";
10
+ export type AgentHookReadinessInterception = "not-directly-verified" | "not-applicable" | "disabled";
11
+ export type AgentHookReadinessLiveStatus = "functional" | "non-functional" | "unavailable" | "not-run" | "disabled" | "skipped";
12
+ export interface AgentHookReadinessHostResult {
13
+ readonly host: HookHost;
14
+ readonly registration: AgentHookReadinessRegistration;
15
+ readonly functionality: AgentHookReadinessFunctionality;
16
+ readonly trust: AgentHookReadinessTrust;
17
+ readonly interception: AgentHookReadinessInterception;
18
+ }
19
+ export interface AgentHookReadinessResult {
20
+ readonly code: 0 | 1 | 2;
21
+ readonly message: string;
22
+ readonly stream: OutputStream;
23
+ readonly skipped: boolean;
24
+ readonly liveStatus: AgentHookReadinessLiveStatus;
25
+ readonly hosts: readonly AgentHookReadinessHostResult[];
26
+ readonly registrations: readonly AgentHookInspection[];
27
+ readonly liveProbe: AgentHookLiveProbeResult | null;
28
+ }
29
+ export interface AgentHookReadinessOptions {
30
+ readonly hostHooksPolicy?: HostHooksPolicy;
31
+ readonly consumerContext?: (projectRoot: string) => boolean;
32
+ readonly evaluateStructural?: (projectRoot: string, policy: HostHooksPolicy) => AgentHookHealthResult;
33
+ readonly probeLive?: (projectRoot: string, seams?: AgentHookLiveProbeSeams) => AgentHookLiveProbeResult;
34
+ readonly liveProbeSeams?: Omit<AgentHookLiveProbeSeams, "hosts">;
35
+ }
36
+ /** Stable CLI JSON projection; keep the four readiness dimensions orthogonal. */
37
+ export declare function agentHookReadinessJson(result: AgentHookReadinessResult): Record<string, unknown>;
38
+ /** Preserve fail-closed readiness semantics when an unexpected evaluator dependency throws. */
39
+ export declare function evaluateAgentHookReadinessSafely(projectRoot: string, evaluate?: (projectRoot: string) => AgentHookReadinessResult): AgentHookReadinessResult;
40
+ /** Fail-closed consumer readiness: structural registration first, installed-shim live probe second. */
41
+ export declare function evaluateAgentHookReadiness(projectRoot: string, options?: AgentHookReadinessOptions): AgentHookReadinessResult;
42
+ //# sourceMappingURL=agent-hook-readiness.d.ts.map
@@ -0,0 +1,150 @@
1
+ import { resolve } from "node:path";
2
+ import { resolveFrameworkRootForProject } from "../doctor/paths.js";
3
+ import { loadHostHooksPolicyFromProject } from "../policy/host-hooks.js";
4
+ import { evaluateAgentHooks } from "./agent-hooks.js";
5
+ import { probeAgentHooksLive, } from "./agent-hooks-live-probe.js";
6
+ /** Stable CLI JSON projection; keep the four readiness dimensions orthogonal. */
7
+ export function agentHookReadinessJson(result) {
8
+ return {
9
+ ready: result.code === 0,
10
+ exit_code: result.code,
11
+ skipped: result.skipped,
12
+ live_status: result.liveStatus,
13
+ hosts: result.hosts.map((entry) => ({ ...entry })),
14
+ message: result.message,
15
+ };
16
+ }
17
+ /** Preserve fail-closed readiness semantics when an unexpected evaluator dependency throws. */
18
+ export function evaluateAgentHookReadinessSafely(projectRoot, evaluate = evaluateAgentHookReadiness) {
19
+ try {
20
+ return evaluate(projectRoot);
21
+ }
22
+ catch (cause) {
23
+ const detail = cause instanceof Error ? cause.message : String(cause);
24
+ return {
25
+ code: 2,
26
+ message: `❌ deft agent hook readiness unavailable: ${detail}`,
27
+ stream: "stderr",
28
+ skipped: false,
29
+ liveStatus: "unavailable",
30
+ hosts: [],
31
+ registrations: [],
32
+ liveProbe: null,
33
+ };
34
+ }
35
+ }
36
+ function isConsumerContext(projectRoot) {
37
+ return resolve(projectRoot) !== resolve(resolveFrameworkRootForProject(projectRoot));
38
+ }
39
+ function registrationState(entry) {
40
+ if (entry.status === "healthy")
41
+ return "registered";
42
+ return entry.status;
43
+ }
44
+ function renderHosts(hosts) {
45
+ return hosts
46
+ .map((entry) => ` - ${entry.host}: registration=${entry.registration}; functionality=${entry.functionality}; ` +
47
+ `trust=${entry.trust}; interception=${entry.interception}`)
48
+ .join("\n");
49
+ }
50
+ function baseHostResult(entry, policy) {
51
+ if (!policy[entry.host]) {
52
+ return {
53
+ host: entry.host,
54
+ registration: "disabled",
55
+ functionality: "disabled",
56
+ trust: "disabled",
57
+ interception: "disabled",
58
+ };
59
+ }
60
+ return {
61
+ host: entry.host,
62
+ registration: registrationState(entry),
63
+ functionality: "not-run",
64
+ trust: entry.host === "codex" ? "manual-review-required" : "not-applicable",
65
+ interception: "not-directly-verified",
66
+ };
67
+ }
68
+ /** Fail-closed consumer readiness: structural registration first, installed-shim live probe second. */
69
+ export function evaluateAgentHookReadiness(projectRoot, options = {}) {
70
+ const root = resolve(projectRoot);
71
+ const consumerContext = options.consumerContext ?? isConsumerContext;
72
+ if (!consumerContext(root)) {
73
+ return {
74
+ code: 0,
75
+ message: "✓ deft agent hook readiness skipped: maintainer source checkout; consumer hook deposit is not applicable.",
76
+ stream: "stdout",
77
+ skipped: true,
78
+ liveStatus: "skipped",
79
+ hosts: [],
80
+ registrations: [],
81
+ liveProbe: null,
82
+ };
83
+ }
84
+ const policy = options.hostHooksPolicy ?? loadHostHooksPolicyFromProject(root);
85
+ const structural = (options.evaluateStructural ?? evaluateAgentHooks)(root, policy);
86
+ const hosts = structural.registrations.map((entry) => baseHostResult(entry, policy));
87
+ if (structural.code !== 0) {
88
+ return {
89
+ code: structural.code,
90
+ message: `${structural.message}\n${renderHosts(hosts)}`,
91
+ stream: "stderr",
92
+ skipped: false,
93
+ liveStatus: "not-run",
94
+ hosts,
95
+ registrations: structural.registrations,
96
+ liveProbe: null,
97
+ };
98
+ }
99
+ const enabledHosts = structural.registrations
100
+ .filter((entry) => policy[entry.host])
101
+ .map((entry) => entry.host);
102
+ const liveProbe = (options.probeLive ?? probeAgentHooksLive)(root, {
103
+ ...options.liveProbeSeams,
104
+ hosts: enabledHosts,
105
+ });
106
+ const liveByHost = new Map(liveProbe.hosts.map((entry) => [entry.host, entry.status]));
107
+ const evaluatedHosts = hosts.map((entry) => {
108
+ if (entry.functionality === "disabled")
109
+ return entry;
110
+ const liveStatus = liveByHost.get(entry.host);
111
+ return {
112
+ ...entry,
113
+ functionality: liveStatus === "functional"
114
+ ? "functional"
115
+ : liveStatus === "unavailable"
116
+ ? "unavailable"
117
+ : liveStatus === "non-functional"
118
+ ? "non-functional"
119
+ : "not-run",
120
+ };
121
+ });
122
+ const allDisabled = enabledHosts.length === 0;
123
+ const liveStatus = allDisabled
124
+ ? "disabled"
125
+ : liveProbe.code === 0
126
+ ? "functional"
127
+ : liveProbe.code === 2
128
+ ? "unavailable"
129
+ : "non-functional";
130
+ const trustReview = policy.codex
131
+ ? "\n Codex trust: manual-review-required. Open `/hooks` and approve the exact project hook commands. " +
132
+ "The live probe validates the shim and codec, not host interception."
133
+ : "";
134
+ return {
135
+ code: liveProbe.code,
136
+ message: `${liveProbe.code === 0 ? "✓" : "❌"} deft agent hook readiness: ${liveProbe.message}\n` +
137
+ renderHosts(evaluatedHosts) +
138
+ trustReview +
139
+ (liveProbe.code === 0
140
+ ? ""
141
+ : "\n If an affected host is unused, inspect `deft policy:show --field=hostHooks`, set `plan.policy.hostHooks.<host> = false`, and run `deft update`."),
142
+ stream: liveProbe.code === 0 ? "stdout" : "stderr",
143
+ skipped: false,
144
+ liveStatus,
145
+ hosts: evaluatedHosts,
146
+ registrations: structural.registrations,
147
+ liveProbe,
148
+ };
149
+ }
150
+ //# sourceMappingURL=agent-hook-readiness.js.map
@@ -1,5 +1,6 @@
1
- import type { HookEvent, HookHost } from "../hooks/dispatcher.js";
2
- export type AgentHookLiveProbeIssue = "hook-command-missing" | "spawn-failed" | "empty-stdout" | "unparseable-json" | "missing-allow" | "missing-deny";
1
+ import { type HookEvent, type HookHost } from "../hooks/dispatcher.js";
2
+ export declare const LIVE_PROBE_CASE_TIMEOUT_MS = 1500;
3
+ export type AgentHookLiveProbeIssue = "hook-command-missing" | "spawn-failed" | "timed-out" | "empty-stdout" | "unparseable-json" | "missing-allow" | "missing-deny";
3
4
  export interface AgentHookLiveProbeCase {
4
5
  readonly host: HookHost;
5
6
  readonly event: HookEvent;
@@ -7,26 +8,37 @@ export interface AgentHookLiveProbeCase {
7
8
  readonly issue: AgentHookLiveProbeIssue;
8
9
  readonly detail: string;
9
10
  }
11
+ export type AgentHookLiveProbeHostStatus = "functional" | "non-functional" | "unavailable";
12
+ export interface AgentHookLiveProbeHostResult {
13
+ readonly host: HookHost;
14
+ readonly status: AgentHookLiveProbeHostStatus;
15
+ }
10
16
  export interface AgentHookLiveProbeResult {
11
17
  readonly code: 0 | 1 | 2;
12
18
  readonly message: string;
13
19
  readonly cases: readonly AgentHookLiveProbeCase[];
20
+ readonly hosts: readonly AgentHookLiveProbeHostResult[];
21
+ readonly durationMs: number;
14
22
  }
15
23
  export interface AgentHookLiveProbeSeams {
24
+ /** Enabled hosts to probe. Defaults to every deposited host. */
25
+ readonly hosts?: readonly HookHost[];
16
26
  readonly resolveCommand?: (name: string) => string | null;
17
27
  readonly spawnHook?: (input: {
18
28
  readonly command: string;
19
29
  readonly args: readonly string[];
20
30
  readonly stdin: string;
21
31
  readonly cwd: string;
22
- readonly env?: NodeJS.ProcessEnv;
32
+ readonly env: NodeJS.ProcessEnv;
23
33
  }) => {
24
34
  readonly status: number;
25
35
  readonly stdout: string;
26
36
  readonly stderr: string;
37
+ readonly timedOut?: boolean;
27
38
  };
28
39
  }
40
+ /** Quote one Windows cmd.exe argument used by the installed hook shim. */
29
41
  export declare function quoteWindowsCmdArg(value: string): string;
30
- /** Spawn the configured hook command and assert Cursor tool.before allow/deny behavior. */
42
+ /** Invoke the installed deft-hook shim and validate enabled host allow/deny codecs. */
31
43
  export declare function probeAgentHooksLive(projectRoot: string, seams?: AgentHookLiveProbeSeams): AgentHookLiveProbeResult;
32
44
  //# sourceMappingURL=agent-hooks-live-probe.d.ts.map