@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.
- package/dist/authz/classify.js +485 -0
- package/dist/cache/fetch.d.ts +29 -0
- package/dist/cache/fetch.js +131 -4
- package/dist/cache/operations.d.ts +8 -0
- package/dist/cache/operations.js +67 -4
- package/dist/check/cached-orchestrator.js +11 -0
- package/dist/check/consumer-gate-integrity.d.ts +68 -0
- package/dist/check/consumer-gate-integrity.js +265 -0
- package/dist/check/index.d.ts +1 -0
- package/dist/check/index.js +1 -0
- package/dist/check/orchestrator.js +10 -0
- package/dist/content-contracts/skills/greptile-detector.d.ts +9 -0
- package/dist/content-contracts/skills/greptile-detector.js +9 -2
- package/dist/doctor/main.js +98 -9
- package/dist/doctor/types.d.ts +2 -2
- package/dist/finish-loop/pr-finish-loop.js +1 -0
- package/dist/freshness/bind.d.ts +67 -0
- package/dist/freshness/bind.js +201 -0
- package/dist/freshness/cli.d.ts +30 -0
- package/dist/freshness/cli.js +180 -0
- package/dist/freshness/compare.d.ts +14 -0
- package/dist/freshness/compare.js +134 -0
- package/dist/freshness/generation.d.ts +44 -0
- package/dist/freshness/generation.js +172 -0
- package/dist/freshness/index.d.ts +13 -0
- package/dist/freshness/index.js +13 -0
- package/dist/freshness/report.d.ts +43 -0
- package/dist/freshness/report.js +139 -0
- package/dist/freshness/types.d.ts +70 -0
- package/dist/freshness/types.js +30 -0
- package/dist/handoff-evidence/index.d.ts +8 -0
- package/dist/handoff-evidence/index.js +7 -0
- package/dist/handoff-evidence/validate.d.ts +105 -0
- package/dist/handoff-evidence/validate.js +423 -0
- package/dist/hooks/dispatcher.d.ts +27 -2
- package/dist/hooks/dispatcher.js +134 -14
- package/dist/hooks/readonly.d.ts +14 -0
- package/dist/hooks/readonly.js +126 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +5 -2
- package/dist/init-deposit/init-deposit.d.ts +7 -1
- package/dist/init-deposit/init-deposit.js +24 -4
- package/dist/init-deposit/refresh.d.ts +9 -1
- package/dist/init-deposit/refresh.js +48 -5
- package/dist/intake/issue-ingest.d.ts +20 -0
- package/dist/intake/issue-ingest.js +58 -0
- package/dist/policy/index.d.ts +4 -0
- package/dist/policy/index.js +50 -1
- package/dist/policy/min-greptile-confidence.d.ts +57 -0
- package/dist/policy/min-greptile-confidence.js +123 -0
- package/dist/pr-merge-readiness/compute.d.ts +11 -1
- package/dist/pr-merge-readiness/compute.js +17 -4
- package/dist/pr-merge-readiness/evaluate.d.ts +8 -1
- package/dist/pr-merge-readiness/evaluate.js +7 -4
- package/dist/pr-merge-readiness/mergeability.d.ts +7 -1
- package/dist/pr-merge-readiness/mergeability.js +14 -2
- package/dist/pr-monitor/main.d.ts +2 -0
- package/dist/pr-monitor/main.js +31 -1
- package/dist/pr-monitor/monitor.js +5 -1
- package/dist/pr-monitor/readiness.js +3 -1
- package/dist/pr-monitor/types.d.ts +8 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
- package/dist/pr-wait-mergeable/cascade.js +28 -1
- package/dist/pr-wait-mergeable/main.d.ts +2 -0
- package/dist/pr-wait-mergeable/main.js +16 -0
- package/dist/pr-wait-mergeable/types.d.ts +3 -1
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
- package/dist/pr-wait-mergeable/wrappers.js +3 -0
- package/dist/pr-watch/constants.d.ts +4 -1
- package/dist/pr-watch/constants.js +5 -2
- package/dist/pr-watch/main.js +1 -0
- package/dist/pr-watch/probe.d.ts +1 -1
- package/dist/pr-watch/probe.js +4 -1
- package/dist/pr-watch/types.d.ts +7 -1
- package/dist/pr-watch/watch.js +3 -2
- package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
- package/dist/review-monitor/index.d.ts +1 -0
- package/dist/review-monitor/index.js +1 -0
- package/dist/review-monitor/l4-owner.d.ts +46 -0
- package/dist/review-monitor/l4-owner.js +262 -0
- package/dist/session/ritual-entrypoint.d.ts +6 -2
- package/dist/session/ritual-entrypoint.js +15 -2
- package/dist/session/session-ready.js +31 -14
- package/dist/session/session-start.d.ts +2 -1
- package/dist/session/session-start.js +66 -3
- package/dist/session/verify-session-ritual.d.ts +4 -2
- package/dist/session/verify-session-ritual.js +11 -11
- package/dist/slash/product-set.js +4 -1
- package/dist/swarm/verify-review-clean.d.ts +4 -3
- package/dist/swarm/verify-review-clean.js +28 -73
- package/dist/tool-events/classify.js +1 -0
- package/dist/triage/classify/index.d.ts +9 -0
- package/dist/triage/classify/index.js +34 -2
- package/dist/triage/classify/label-mirror.d.ts +143 -0
- package/dist/triage/classify/label-mirror.js +684 -0
- package/dist/triage/help/registry-data.d.ts +7 -7
- package/dist/triage/help/registry-data.js +27 -7
- package/dist/triage/queue/show.d.ts +1 -1
- package/dist/triage/queue/show.js +4 -2
- package/dist/triage/welcome/writers.js +16 -7
- package/dist/umbrella-current-shape/index.d.ts +45 -0
- package/dist/umbrella-current-shape/index.js +162 -8
- package/dist/vbrief-reconcile/index.d.ts +2 -2
- package/dist/vbrief-reconcile/index.js +1 -1
- package/dist/vbrief-reconcile/types.d.ts +14 -0
- package/dist/vbrief-reconcile/umbrellas.d.ts +31 -2
- package/dist/vbrief-reconcile/umbrellas.js +231 -19
- package/dist/vbrief-validate/project-definition.js +1 -1
- package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
- package/dist/verify-env/agent-hook-readiness.js +150 -0
- package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
- package/dist/verify-env/agent-hooks-live-probe.js +120 -107
- package/dist/verify-env/agent-hooks.js +11 -3
- package/dist/verify-env/index.d.ts +1 -0
- package/dist/verify-env/index.js +1 -0
- 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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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) =>
|
|
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) => !
|
|
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
|
-
|
|
274
|
-
const
|
|
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
|
|
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
|
|
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
|
|
2
|
-
export
|
|
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
|
|
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
|
-
/**
|
|
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
|