@deftai/directive-core 0.93.0 → 0.94.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/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 +57 -0
- package/dist/finish-loop/pr-finish-loop.js +1 -0
- package/dist/hooks/dispatcher.d.ts +24 -2
- package/dist/hooks/dispatcher.js +122 -11
- package/dist/hooks/readonly.d.ts +14 -0
- package/dist/hooks/readonly.js +126 -0
- package/dist/intake/issue-ingest.d.ts +20 -0
- package/dist/intake/issue-ingest.js +58 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +15 -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/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/tool-events/classify.js +1 -0
- 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/package.json +3 -3
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner Continuity / L4 owner gate (#3090).
|
|
3
|
+
*
|
|
4
|
+
* Exit 0 only when a fresh sticky review-owner lease exists on the PR,
|
|
5
|
+
* or the caller asserts `review_cycle: done` after Step 6.
|
|
6
|
+
* Freeform started/pending/initiated values are rejected.
|
|
7
|
+
* skipped / n/a / parent-retained are process evidence only — they do NOT
|
|
8
|
+
* satisfy this machine gate when --pr is required (lease-or-done).
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, statSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { resolveRepo } from "../triage/queue/repo.js";
|
|
13
|
+
import { EXIT_CONFIG_ERROR, EXIT_NOT_READY, EXIT_READY } from "./constants.js";
|
|
14
|
+
import { fetchActiveMonitorFromGithub } from "./record.js";
|
|
15
|
+
export const L4_OWNER_HELP = "usage: task verify:l4-owner -- --pr <N> [options]\n" +
|
|
16
|
+
"\n" +
|
|
17
|
+
"Owner Continuity / L4 owner gate (#3090): after drive-to:merge-ready /\n" +
|
|
18
|
+
"babysit / shepherd claims, exit 0 only when a sticky GitHub review-owner\n" +
|
|
19
|
+
"lease is fresh on the PR, or the caller asserts --review-cycle done\n" +
|
|
20
|
+
"(Step 6 fail-closed all-of on HEAD). Freeform started/pending/initiated\n" +
|
|
21
|
+
"are rejected. skipped / n/a / parent-retained do NOT satisfy this machine\n" +
|
|
22
|
+
"gate (process A/B/C still applies). Silent hold exits 1.\n" +
|
|
23
|
+
"\n" +
|
|
24
|
+
"options:\n" +
|
|
25
|
+
" -h, --help Show this help and exit 0\n" +
|
|
26
|
+
" --pr N Pull request number (required unless --help)\n" +
|
|
27
|
+
" --repo OWNER/REPO Repository (optional; inferred from origin)\n" +
|
|
28
|
+
" --head-sha SHA Expected HEAD SHA (optional freshness check)\n" +
|
|
29
|
+
" --project-root PATH Project root (default: cwd)\n" +
|
|
30
|
+
" --review-cycle VALUE done | in_progress:<pr>#<ref> | skipped:<reason> | n/a\n" +
|
|
31
|
+
" --json Emit structured JSON on stdout\n" +
|
|
32
|
+
"\n" +
|
|
33
|
+
"exit codes:\n" +
|
|
34
|
+
" 0 READY Fresh sticky lease, or --review-cycle done\n" +
|
|
35
|
+
" 1 NOT READY Silent hold / illegal evidence / lease missing\n" +
|
|
36
|
+
" 2 CONFIG Usage / path / GitHub fetch error\n";
|
|
37
|
+
const FORBIDDEN_FREEFORM = new Set(["started", "pending", "initiated", "start", "in_progress"]);
|
|
38
|
+
export function parseReviewCycleEvidence(raw) {
|
|
39
|
+
if (raw === null || raw === undefined || raw.trim() === "") {
|
|
40
|
+
return { ok: true, value: null };
|
|
41
|
+
}
|
|
42
|
+
const value = raw.trim();
|
|
43
|
+
const lower = value.toLowerCase();
|
|
44
|
+
if (FORBIDDEN_FREEFORM.has(lower)) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
reason: `illegal freeform review_cycle value '${value}' (#3090); ` +
|
|
48
|
+
"use done | in_progress:<pr>#<ref> | skipped:<reason> | n/a",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (value === "done" || value === "n/a") {
|
|
52
|
+
return { ok: true, value };
|
|
53
|
+
}
|
|
54
|
+
if (value.startsWith("in_progress:")) {
|
|
55
|
+
const rest = value.slice("in_progress:".length);
|
|
56
|
+
if (!rest.includes("#") || rest.endsWith("#") || rest.startsWith("#")) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
reason: "in_progress requires form in_progress:<pr>#<monitor_or_lease_ref> (#3090)",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const prPart = rest.slice(0, rest.indexOf("#"));
|
|
63
|
+
if (!/^\d+$/.test(prPart) || Number(prPart) <= 0) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
reason: "in_progress:<pr> must be a positive integer PR number (#3090)",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, value: value };
|
|
70
|
+
}
|
|
71
|
+
if (value.startsWith("skipped:")) {
|
|
72
|
+
const rest = value.slice("skipped:".length).trim();
|
|
73
|
+
if (rest.length === 0) {
|
|
74
|
+
return { ok: false, reason: "skipped requires a non-empty reason (#3090)" };
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, value: value };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
reason: `unknown review_cycle value '${value}' (#3090); ` +
|
|
81
|
+
"use done | in_progress:<pr>#<ref> | skipped:<reason> | n/a",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Parse in_progress:<pr>#<ref> into parts; null if not that form. */
|
|
85
|
+
export function parseInProgressEvidence(value) {
|
|
86
|
+
if (!value.startsWith("in_progress:")) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const rest = value.slice("in_progress:".length);
|
|
90
|
+
const hash = rest.indexOf("#");
|
|
91
|
+
if (hash <= 0) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const prPart = rest.slice(0, hash);
|
|
95
|
+
const ref = rest.slice(hash + 1);
|
|
96
|
+
if (!/^\d+$/.test(prPart) || ref.length === 0) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return { pr: Number(prPart), ref };
|
|
100
|
+
}
|
|
101
|
+
function oneLine(value) {
|
|
102
|
+
return value.replace(/\r?\n/g, " ");
|
|
103
|
+
}
|
|
104
|
+
export function evaluateL4OwnerGate(args) {
|
|
105
|
+
const projectRoot = resolve(args.projectRoot);
|
|
106
|
+
let isDir = false;
|
|
107
|
+
try {
|
|
108
|
+
isDir = existsSync(projectRoot) && statSync(projectRoot).isDirectory();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
isDir = false;
|
|
112
|
+
}
|
|
113
|
+
if (!isDir) {
|
|
114
|
+
return {
|
|
115
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
116
|
+
message: `verify_l4_owner: --project-root is not a directory: ${oneLine(projectRoot)}`,
|
|
117
|
+
monitorRecord: null,
|
|
118
|
+
reviewCycle: null,
|
|
119
|
+
path: "config",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const parsed = parseReviewCycleEvidence(args.reviewCycle);
|
|
123
|
+
if (!parsed.ok) {
|
|
124
|
+
return {
|
|
125
|
+
exitCode: EXIT_NOT_READY,
|
|
126
|
+
message: `verify_l4_owner: ${oneLine(parsed.reason)}`,
|
|
127
|
+
monitorRecord: null,
|
|
128
|
+
reviewCycle: null,
|
|
129
|
+
path: "illegal",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Machine gate is lease-or-done only (#3090). skipped/n/a are handoff enum values
|
|
133
|
+
// but must not green-light ownership of an open PR without lease or Step 6 done.
|
|
134
|
+
if (parsed.value === "n/a" || parsed.value?.startsWith("skipped:")) {
|
|
135
|
+
return {
|
|
136
|
+
exitCode: EXIT_NOT_READY,
|
|
137
|
+
message: `verify_l4_owner: review_cycle=${oneLine(parsed.value)} does not satisfy the machine ` +
|
|
138
|
+
`gate for PR #${args.pr} (#3090). Exit 0 requires a fresh sticky lease or --review-cycle done.`,
|
|
139
|
+
monitorRecord: null,
|
|
140
|
+
reviewCycle: parsed.value,
|
|
141
|
+
path: "none",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (parsed.value === "done") {
|
|
145
|
+
return {
|
|
146
|
+
exitCode: EXIT_READY,
|
|
147
|
+
message: `verify_l4_owner: review_cycle=done asserted for PR #${args.pr} (#3090). ` +
|
|
148
|
+
"Caller MUST have satisfied Step 6 fail-closed all-of on HEAD before this claim.",
|
|
149
|
+
monitorRecord: null,
|
|
150
|
+
reviewCycle: "done",
|
|
151
|
+
path: "done",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// Validate in_progress PR binding when evidence is present (before lease fetch).
|
|
155
|
+
if (parsed.value?.startsWith("in_progress:")) {
|
|
156
|
+
const parts = parseInProgressEvidence(parsed.value);
|
|
157
|
+
if (parts === null) {
|
|
158
|
+
return {
|
|
159
|
+
exitCode: EXIT_NOT_READY,
|
|
160
|
+
message: `verify_l4_owner: malformed in_progress evidence ${oneLine(parsed.value)} (#3090).`,
|
|
161
|
+
monitorRecord: null,
|
|
162
|
+
reviewCycle: parsed.value,
|
|
163
|
+
path: "illegal",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (parts.pr !== args.pr) {
|
|
167
|
+
return {
|
|
168
|
+
exitCode: EXIT_NOT_READY,
|
|
169
|
+
message: `verify_l4_owner: review_cycle PR #${parts.pr} does not match --pr ${args.pr} (#3090). ` +
|
|
170
|
+
"Ownership evidence must bind to the target PR.",
|
|
171
|
+
monitorRecord: null,
|
|
172
|
+
reviewCycle: parsed.value,
|
|
173
|
+
path: "none",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const repo = resolveRepo(args.repo ?? null, projectRoot);
|
|
178
|
+
if (repo === null) {
|
|
179
|
+
return {
|
|
180
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
181
|
+
message: "verify_l4_owner: could not resolve owner/repo — pass --repo OWNER/REPO or run inside a git repo with origin",
|
|
182
|
+
monitorRecord: null,
|
|
183
|
+
reviewCycle: parsed.value,
|
|
184
|
+
path: "config",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const now = args.now ?? new Date();
|
|
188
|
+
const githubMonitor = fetchActiveMonitorFromGithub(repo, args.pr, {
|
|
189
|
+
now,
|
|
190
|
+
headSha: args.headSha ?? null,
|
|
191
|
+
seams: args.seams,
|
|
192
|
+
});
|
|
193
|
+
if (githubMonitor !== null && typeof githubMonitor === "object" && "error" in githubMonitor) {
|
|
194
|
+
return {
|
|
195
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
196
|
+
message: `verify_l4_owner: ${oneLine(githubMonitor.error)}`,
|
|
197
|
+
monitorRecord: null,
|
|
198
|
+
reviewCycle: parsed.value,
|
|
199
|
+
path: "config",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (githubMonitor !== null) {
|
|
203
|
+
const ref = parsed.value ??
|
|
204
|
+
`in_progress:${args.pr}#${githubMonitor.monitor_agent_id}`;
|
|
205
|
+
return {
|
|
206
|
+
exitCode: EXIT_READY,
|
|
207
|
+
message: `verify_l4_owner: active GitHub review-owner lease for PR #${args.pr} ` +
|
|
208
|
+
`(monitor_agent_id=${oneLine(githubMonitor.monitor_agent_id)}, owner=${oneLine(githubMonitor.owner)}) ` +
|
|
209
|
+
"— review_cycle path=lease (#3090).",
|
|
210
|
+
monitorRecord: githubMonitor,
|
|
211
|
+
reviewCycle: ref,
|
|
212
|
+
path: "lease",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// parent-retained is process path B — machine gate cannot verify it (lease-or-done only).
|
|
216
|
+
if (parsed.value?.startsWith("in_progress:")) {
|
|
217
|
+
const parts = parseInProgressEvidence(parsed.value);
|
|
218
|
+
if (parts?.ref === "parent-retained") {
|
|
219
|
+
return {
|
|
220
|
+
exitCode: EXIT_NOT_READY,
|
|
221
|
+
message: `verify_l4_owner: parent-retained for PR #${args.pr} is process path B only (#3090). ` +
|
|
222
|
+
"Machine gate requires a sticky <!-- deft:review-owner --> lease or --review-cycle done. " +
|
|
223
|
+
"Keep parent dual-source poll/fix; do not emit L4 status:pass as terminal.",
|
|
224
|
+
monitorRecord: null,
|
|
225
|
+
reviewCycle: parsed.value,
|
|
226
|
+
path: "none",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
exitCode: EXIT_NOT_READY,
|
|
231
|
+
message: `verify_l4_owner: review_cycle=${oneLine(parsed.value)} claims in_progress but no sticky ` +
|
|
232
|
+
`<!-- deft:review-owner --> lease found for PR #${args.pr} (#3090 / #2797).\n` +
|
|
233
|
+
" Register: deft review-monitor:register --pr <N> --monitor-agent-id <id> --platform-primitive …",
|
|
234
|
+
monitorRecord: null,
|
|
235
|
+
reviewCycle: parsed.value,
|
|
236
|
+
path: "none",
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
exitCode: EXIT_NOT_READY,
|
|
241
|
+
message: `verify_l4_owner: silent hold on PR #${args.pr} — no fresh sticky lease and no ` +
|
|
242
|
+
"review_cycle=done (#3090 Owner Continuity Gate).\n" +
|
|
243
|
+
" Same turn MUST end in A (monitor+lease), B (parent-retained + next dual-source action),\n" +
|
|
244
|
+
" or C (explicit BLOCKED/FAILED finish). Check-run SUCCESS alone is not CLEAN.",
|
|
245
|
+
monitorRecord: null,
|
|
246
|
+
reviewCycle: null,
|
|
247
|
+
path: "none",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
export function l4OwnerResultToJson(result) {
|
|
251
|
+
return {
|
|
252
|
+
exit_code: result.exitCode,
|
|
253
|
+
message: result.message,
|
|
254
|
+
monitor_agent_id: result.monitorRecord?.monitor_agent_id ?? null,
|
|
255
|
+
monitor_owner: result.monitorRecord?.owner ?? null,
|
|
256
|
+
monitor_record: result.monitorRecord,
|
|
257
|
+
path: result.path,
|
|
258
|
+
ready: result.exitCode === EXIT_READY,
|
|
259
|
+
review_cycle: result.reviewCycle,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=l4-owner.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { containedWrite } from "../../fs/contained-write.js";
|
|
4
|
+
import { assertWriteTargetSafe } from "../../fs/projection-containment.js";
|
|
4
5
|
import { hasArtifactSuffix, resolveLifecycleFolder, resolveProjectDefinitionPath, } from "../../layout/resolve.js";
|
|
5
6
|
import { migrateLegacyPolicyKey, PLAN_ONBOARDING_KEY, PLAN_POLICY_KEY, } from "../../policy/plan-extensions.js";
|
|
6
7
|
import { projectDefinitionMutationLock } from "../../vbrief-build/project-definition-io.js";
|
|
@@ -32,21 +33,29 @@ export function appendAuditEntry(projectRoot, entry) {
|
|
|
32
33
|
});
|
|
33
34
|
return logPath;
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Atomically write PROJECT-DEFINITION JSON under projectRoot containment (#3077).
|
|
38
|
+
* Containment root is projectRoot (not dirname(path)) so a force-added `xbrief/`
|
|
39
|
+
* directory symlink fails closed before temp+rename — same class as #3042.
|
|
40
|
+
*/
|
|
41
|
+
function atomicWrite(projectRoot, path, data) {
|
|
42
|
+
const root = resolve(projectRoot);
|
|
43
|
+
const targetAbs = resolve(path);
|
|
44
|
+
// Refuse leaf/parent symlinks and out-of-root targets before temp+rename.
|
|
45
|
+
assertWriteTargetSafe(root, targetAbs);
|
|
46
|
+
const dir = dirname(targetAbs);
|
|
38
47
|
const payload = `${JSON.stringify(data, null, 2)}\n`;
|
|
39
48
|
const tmpName = `.${Date.now()}.tmp`;
|
|
40
49
|
const tmp = join(dir, tmpName);
|
|
41
50
|
try {
|
|
42
|
-
// #2980 wave D:
|
|
51
|
+
// #2980 wave D / #3077: product write routes through containedWrite under projectRoot.
|
|
43
52
|
containedWrite({
|
|
44
|
-
root
|
|
45
|
-
target:
|
|
53
|
+
root,
|
|
54
|
+
target: tmp,
|
|
46
55
|
data: payload,
|
|
47
56
|
mode: "create",
|
|
48
57
|
});
|
|
49
|
-
renameSync(tmp,
|
|
58
|
+
renameSync(tmp, targetAbs);
|
|
50
59
|
}
|
|
51
60
|
catch (err) {
|
|
52
61
|
try {
|
|
@@ -66,6 +66,51 @@ export interface RunCurrentShapeOptions {
|
|
|
66
66
|
readonly writeOut?: (text: string) => void;
|
|
67
67
|
readonly writeErr?: (text: string) => void;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Map a GitHub REST issue-comment object (or an already-normalized row) onto
|
|
71
|
+
* the internal `IssueComment` shape. Used by `umbrella:current-shape`, the
|
|
72
|
+
* triage cache put path, and `issue:ingest` so all three share one selector.
|
|
73
|
+
*/
|
|
74
|
+
export declare function mapIssueCommentEntry(entry: unknown): IssueComment | null;
|
|
75
|
+
/**
|
|
76
|
+
* Labels that mark an issue as umbrella/tracker-like for cache comment enrichment
|
|
77
|
+
* (#1870). Mirrors `TRACKER_LABELS` in scope/open-umbrella-warning.ts.
|
|
78
|
+
*/
|
|
79
|
+
export declare const UMBRELLA_TRACKER_LABELS: ReadonlySet<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Key under which `cache:fetch-all` / `cache:put` stash the issue comment thread
|
|
82
|
+
* on the raw.json payload so `content.md` and `issue:ingest` can surface the
|
|
83
|
+
* canonical current-shape comment without a second live fetch (#1870).
|
|
84
|
+
*/
|
|
85
|
+
export declare const RAW_ISSUE_COMMENTS_KEY: "comments";
|
|
86
|
+
/** Sidecar filename next to content.md carrying the selected current-shape payload. */
|
|
87
|
+
export declare const CURRENT_SHAPE_SIDECAR: "current-shape.json";
|
|
88
|
+
export interface CurrentShapeSidecar {
|
|
89
|
+
readonly commentId: number;
|
|
90
|
+
readonly htmlUrl: string;
|
|
91
|
+
readonly pass: number;
|
|
92
|
+
readonly authorLogin: string;
|
|
93
|
+
readonly authorAssociation: string;
|
|
94
|
+
readonly body: string;
|
|
95
|
+
}
|
|
96
|
+
/** True when labels or sub-issue summary mark the issue as umbrella/tracker-like. */
|
|
97
|
+
export declare function isUmbrellaLikeIssue(raw: Record<string, unknown>): boolean;
|
|
98
|
+
/** Normalize a raw issue payload's comment array (REST or pre-mapped). */
|
|
99
|
+
export declare function commentsFromRawPayload(raw: Record<string, unknown>): IssueComment[];
|
|
100
|
+
/**
|
|
101
|
+
* Count maintainer-authored `## Current shape` comments (for the !=1 lint).
|
|
102
|
+
* Non-maintainer forgeries are excluded (#2307).
|
|
103
|
+
*/
|
|
104
|
+
export declare function countMaintainerCurrentShapeComments(comments: readonly IssueComment[]): number;
|
|
105
|
+
/** Markdown section appended to cache content.md when a canonical shape exists. */
|
|
106
|
+
export declare function formatCurrentShapeSection(selected: CurrentShapeComment): string;
|
|
107
|
+
/**
|
|
108
|
+
* Append the canonical current-shape comment (if any) to a rendered cache body.
|
|
109
|
+
* Returns the input unchanged when no maintainer-authored current-shape exists.
|
|
110
|
+
*/
|
|
111
|
+
export declare function appendCurrentShapeSection(baseContent: string, raw: Record<string, unknown>): string;
|
|
112
|
+
/** Build the current-shape.json sidecar payload, or null when none is selectable. */
|
|
113
|
+
export declare function buildCurrentShapeSidecar(raw: Record<string, unknown>): CurrentShapeSidecar | null;
|
|
69
114
|
/** Merge `gh api --paginate` concatenated JSON array pages into comment rows. */
|
|
70
115
|
export declare function parseCommentsFromGhStdout(stdout: string): IssueComment[];
|
|
71
116
|
export declare function extractPassFromBody(body: string): number | null;
|
|
@@ -44,24 +44,178 @@ export const REQUIRED_SECTIONS = [
|
|
|
44
44
|
"readingOrder",
|
|
45
45
|
];
|
|
46
46
|
export const OPTIONAL_SECTIONS = ["openQuestions"];
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Map a GitHub REST issue-comment object (or an already-normalized row) onto
|
|
49
|
+
* the internal `IssueComment` shape. Used by `umbrella:current-shape`, the
|
|
50
|
+
* triage cache put path, and `issue:ingest` so all three share one selector.
|
|
51
|
+
*/
|
|
52
|
+
export function mapIssueCommentEntry(entry) {
|
|
48
53
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
49
54
|
return null;
|
|
50
55
|
}
|
|
51
56
|
const rec = entry;
|
|
52
|
-
|
|
57
|
+
// Accept either GitHub REST field names or the already-mapped camelCase shape.
|
|
58
|
+
const id = typeof rec.id === "number" ? rec.id : null;
|
|
59
|
+
const body = typeof rec.body === "string" ? rec.body : null;
|
|
60
|
+
if (id === null || body === null) {
|
|
53
61
|
return null;
|
|
54
62
|
}
|
|
55
63
|
const user = typeof rec.user === "object" && rec.user !== null
|
|
56
64
|
? rec.user
|
|
57
65
|
: null;
|
|
66
|
+
const htmlUrl = typeof rec.html_url === "string"
|
|
67
|
+
? rec.html_url
|
|
68
|
+
: typeof rec.htmlUrl === "string"
|
|
69
|
+
? rec.htmlUrl
|
|
70
|
+
: "";
|
|
71
|
+
const updatedAt = typeof rec.updated_at === "string"
|
|
72
|
+
? rec.updated_at
|
|
73
|
+
: typeof rec.updatedAt === "string"
|
|
74
|
+
? rec.updatedAt
|
|
75
|
+
: "";
|
|
76
|
+
const authorLogin = user !== null && typeof user.login === "string"
|
|
77
|
+
? user.login
|
|
78
|
+
: typeof rec.authorLogin === "string"
|
|
79
|
+
? rec.authorLogin
|
|
80
|
+
: "";
|
|
81
|
+
const authorAssociation = typeof rec.author_association === "string"
|
|
82
|
+
? rec.author_association
|
|
83
|
+
: typeof rec.authorAssociation === "string"
|
|
84
|
+
? rec.authorAssociation
|
|
85
|
+
: "";
|
|
86
|
+
return {
|
|
87
|
+
id,
|
|
88
|
+
body,
|
|
89
|
+
htmlUrl,
|
|
90
|
+
updatedAt,
|
|
91
|
+
authorLogin,
|
|
92
|
+
authorAssociation,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** @deprecated Prefer `mapIssueCommentEntry` — kept as a private alias for call sites. */
|
|
96
|
+
function mapCommentEntry(entry) {
|
|
97
|
+
return mapIssueCommentEntry(entry);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Labels that mark an issue as umbrella/tracker-like for cache comment enrichment
|
|
101
|
+
* (#1870). Mirrors `TRACKER_LABELS` in scope/open-umbrella-warning.ts.
|
|
102
|
+
*/
|
|
103
|
+
export const UMBRELLA_TRACKER_LABELS = new Set([
|
|
104
|
+
"epic",
|
|
105
|
+
"meta",
|
|
106
|
+
"tracker",
|
|
107
|
+
"type:tracker",
|
|
108
|
+
"status:tracker",
|
|
109
|
+
]);
|
|
110
|
+
/**
|
|
111
|
+
* Key under which `cache:fetch-all` / `cache:put` stash the issue comment thread
|
|
112
|
+
* on the raw.json payload so `content.md` and `issue:ingest` can surface the
|
|
113
|
+
* canonical current-shape comment without a second live fetch (#1870).
|
|
114
|
+
*/
|
|
115
|
+
export const RAW_ISSUE_COMMENTS_KEY = "comments";
|
|
116
|
+
/** Sidecar filename next to content.md carrying the selected current-shape payload. */
|
|
117
|
+
export const CURRENT_SHAPE_SIDECAR = "current-shape.json";
|
|
118
|
+
/** True when labels or sub-issue summary mark the issue as umbrella/tracker-like. */
|
|
119
|
+
export function isUmbrellaLikeIssue(raw) {
|
|
120
|
+
const labelsRaw = raw.labels;
|
|
121
|
+
if (Array.isArray(labelsRaw)) {
|
|
122
|
+
for (const label of labelsRaw) {
|
|
123
|
+
let name = "";
|
|
124
|
+
if (typeof label === "string") {
|
|
125
|
+
name = label;
|
|
126
|
+
}
|
|
127
|
+
else if (label !== null && typeof label === "object" && !Array.isArray(label)) {
|
|
128
|
+
const n = label.name;
|
|
129
|
+
if (typeof n === "string")
|
|
130
|
+
name = n;
|
|
131
|
+
}
|
|
132
|
+
if (name.length > 0 && UMBRELLA_TRACKER_LABELS.has(name.toLowerCase())) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const summary = raw.sub_issues_summary;
|
|
138
|
+
if (summary !== null && typeof summary === "object" && !Array.isArray(summary)) {
|
|
139
|
+
const total = summary.total;
|
|
140
|
+
if (typeof total === "number" && Number.isFinite(total) && total > 0) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
/** Normalize a raw issue payload's comment array (REST or pre-mapped). */
|
|
147
|
+
export function commentsFromRawPayload(raw) {
|
|
148
|
+
const rawComments = raw[RAW_ISSUE_COMMENTS_KEY];
|
|
149
|
+
if (!Array.isArray(rawComments)) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const entry of rawComments) {
|
|
154
|
+
const mapped = mapIssueCommentEntry(entry);
|
|
155
|
+
if (mapped !== null) {
|
|
156
|
+
out.push(mapped);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Count maintainer-authored `## Current shape` comments (for the !=1 lint).
|
|
163
|
+
* Non-maintainer forgeries are excluded (#2307).
|
|
164
|
+
*/
|
|
165
|
+
export function countMaintainerCurrentShapeComments(comments) {
|
|
166
|
+
let count = 0;
|
|
167
|
+
for (const comment of comments) {
|
|
168
|
+
if (!isMaintainerAuthored(comment.authorAssociation)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (extractPassFromBody(comment.body) !== null) {
|
|
172
|
+
count += 1;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return count;
|
|
176
|
+
}
|
|
177
|
+
/** Markdown section appended to cache content.md when a canonical shape exists. */
|
|
178
|
+
export function formatCurrentShapeSection(selected) {
|
|
179
|
+
const permalink = selected.htmlUrl.length > 0 ? selected.htmlUrl : `comment id ${selected.id}`;
|
|
180
|
+
const author = selected.authorLogin.length > 0 ? `@${selected.authorLogin}` : "maintainer";
|
|
181
|
+
return [
|
|
182
|
+
"",
|
|
183
|
+
"---",
|
|
184
|
+
"",
|
|
185
|
+
"## Canonical current shape (#1152 / #1870)",
|
|
186
|
+
"",
|
|
187
|
+
`_Authoritative planning surface — prefer this over the issue body (stale by design). ` +
|
|
188
|
+
`Source: [${permalink}](${permalink}) · pass-${selected.pass} · ${author}. ` +
|
|
189
|
+
`Deterministic read path: \`task umbrella:current-shape <N>\`._`,
|
|
190
|
+
"",
|
|
191
|
+
selected.body.trimEnd(),
|
|
192
|
+
"",
|
|
193
|
+
].join("\n");
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Append the canonical current-shape comment (if any) to a rendered cache body.
|
|
197
|
+
* Returns the input unchanged when no maintainer-authored current-shape exists.
|
|
198
|
+
*/
|
|
199
|
+
export function appendCurrentShapeSection(baseContent, raw) {
|
|
200
|
+
const selected = selectCurrentShapeComment(commentsFromRawPayload(raw));
|
|
201
|
+
if (selected === null) {
|
|
202
|
+
return baseContent;
|
|
203
|
+
}
|
|
204
|
+
return `${baseContent.trimEnd()}\n${formatCurrentShapeSection(selected)}`;
|
|
205
|
+
}
|
|
206
|
+
/** Build the current-shape.json sidecar payload, or null when none is selectable. */
|
|
207
|
+
export function buildCurrentShapeSidecar(raw) {
|
|
208
|
+
const selected = selectCurrentShapeComment(commentsFromRawPayload(raw));
|
|
209
|
+
if (selected === null) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
58
212
|
return {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
213
|
+
commentId: selected.id,
|
|
214
|
+
htmlUrl: selected.htmlUrl,
|
|
215
|
+
pass: selected.pass,
|
|
216
|
+
authorLogin: selected.authorLogin,
|
|
217
|
+
authorAssociation: selected.authorAssociation,
|
|
218
|
+
body: selected.body,
|
|
65
219
|
};
|
|
66
220
|
}
|
|
67
221
|
/** Merge `gh api --paginate` concatenated JSON array pages into comment rows. */
|
|
@@ -5,6 +5,6 @@ export { PARITY_SCENARIO_NAMES, renderScenarioOutput, runParityScenario, } from
|
|
|
5
5
|
export { pyRepr } from "./py-repr.js";
|
|
6
6
|
export { buildSpecTaskIndex, detectStatusMarker, folderFromStatus, formatReconciliationMarkdown, hasDisagreement, loadOverrides, normalizeTaskId, OVERRIDES_FILENAME, parseOverridesYaml, reconcileScopeItems, writeReconciliationReport, } from "./reconciliation.js";
|
|
7
7
|
export { allScopeIds, asStrList, candidateDepGraph, candidateFromPath, markCycles, } from "./swarm-deps.js";
|
|
8
|
-
export type { Candidate, Child, ConflictEntry, LabelChange, LabelClient, ReconciledItem, ReconcileGraphOutcome, ReconcileLabelsOutcome, ReconcileUmbrellasOutcome, ReconciliationReport, SpecTaskEntry, UmbrellaChange, UmbrellaClient, } from "./types.js";
|
|
9
|
-
export { buildChildIndex, CHILD_REF_TYPE, CLOSED_FOLDERS, childFromData, classifyPassType, computeChildren, computeWaves, LIFECYCLE_FOLDERS, nowIso, OPEN_FOLDERS, parseCurrentShape, reconcileUmbrellas, renderBody, renderUmbrellasReport, ScmUmbrellaClient, UmbrellaScmError, umbrellasOutcomeToJson, } from "./umbrellas.js";
|
|
8
|
+
export type { Candidate, Child, ConflictEntry, ForgeIssueState, LabelChange, LabelClient, ReconciledItem, ReconcileGraphOutcome, ReconcileLabelsOutcome, ReconcileUmbrellasOutcome, ReconciliationReport, SpecTaskEntry, UmbrellaChange, UmbrellaClient, } from "./types.js";
|
|
9
|
+
export { buildChildIndex, CHILD_REF_TYPE, CLOSED_FOLDERS, childFromData, classifyPassType, computeChildren, computeWaves, indexChildrenByIssueNumber, isChildOpen, LIFECYCLE_FOLDERS, nowIso, OPEN_FOLDERS, parseCurrentShape, reconcileBodyChecklist, reconcileUmbrellas, renderBody, renderUmbrellasReport, ScmUmbrellaClient, UmbrellaScmError, UNKNOWN_FOLDER, umbrellasOutcomeToJson, } from "./umbrellas.js";
|
|
10
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -5,5 +5,5 @@ export { PARITY_SCENARIO_NAMES, renderScenarioOutput, runParityScenario, } from
|
|
|
5
5
|
export { pyRepr } from "./py-repr.js";
|
|
6
6
|
export { buildSpecTaskIndex, detectStatusMarker, folderFromStatus, formatReconciliationMarkdown, hasDisagreement, loadOverrides, normalizeTaskId, OVERRIDES_FILENAME, parseOverridesYaml, reconcileScopeItems, writeReconciliationReport, } from "./reconciliation.js";
|
|
7
7
|
export { allScopeIds, asStrList, candidateDepGraph, candidateFromPath, markCycles, } from "./swarm-deps.js";
|
|
8
|
-
export { buildChildIndex, CHILD_REF_TYPE, CLOSED_FOLDERS, childFromData, classifyPassType, computeChildren, computeWaves, LIFECYCLE_FOLDERS, nowIso, OPEN_FOLDERS, parseCurrentShape, reconcileUmbrellas, renderBody, renderUmbrellasReport, ScmUmbrellaClient, UmbrellaScmError, umbrellasOutcomeToJson, } from "./umbrellas.js";
|
|
8
|
+
export { buildChildIndex, CHILD_REF_TYPE, CLOSED_FOLDERS, childFromData, classifyPassType, computeChildren, computeWaves, indexChildrenByIssueNumber, isChildOpen, LIFECYCLE_FOLDERS, nowIso, OPEN_FOLDERS, parseCurrentShape, reconcileBodyChecklist, reconcileUmbrellas, renderBody, renderUmbrellasReport, ScmUmbrellaClient, UmbrellaScmError, UNKNOWN_FOLDER, umbrellasOutcomeToJson, } from "./umbrellas.js";
|
|
9
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -94,7 +94,10 @@ export interface Child {
|
|
|
94
94
|
readonly kind: string;
|
|
95
95
|
readonly folder: string;
|
|
96
96
|
readonly depends_on: string[];
|
|
97
|
+
/** Linked forge issue number when known (#1649). */
|
|
98
|
+
readonly issue_number?: number | null;
|
|
97
99
|
}
|
|
100
|
+
export type ForgeIssueState = "open" | "closed";
|
|
98
101
|
export interface UmbrellaChange {
|
|
99
102
|
readonly story_id: string;
|
|
100
103
|
readonly repo: string;
|
|
@@ -102,6 +105,11 @@ export interface UmbrellaChange {
|
|
|
102
105
|
readonly action: "created" | "edited" | "unchanged";
|
|
103
106
|
readonly pass_n: number;
|
|
104
107
|
readonly body: string;
|
|
108
|
+
/**
|
|
109
|
+
* Issue-body checkbox reconcile (#1649): `edited` when checkboxes flipped,
|
|
110
|
+
* `unchanged` when already correct, `skipped` when body APIs unavailable.
|
|
111
|
+
*/
|
|
112
|
+
readonly checklist_action?: "edited" | "unchanged" | "skipped";
|
|
105
113
|
}
|
|
106
114
|
export interface ReconcileUmbrellasOutcome {
|
|
107
115
|
changed: UmbrellaChange[];
|
|
@@ -124,5 +132,11 @@ export interface UmbrellaClient {
|
|
|
124
132
|
}>;
|
|
125
133
|
editComment(repo: string, commentId: number, body: string): void;
|
|
126
134
|
createComment(repo: string, issueNumber: number, body: string): number | null;
|
|
135
|
+
/** Optional: forge open/closed for child issues (#1649). Folder fallback when absent. */
|
|
136
|
+
fetchIssueStates?(repo: string, issueNumbers: readonly number[]): ReadonlyMap<number, ForgeIssueState>;
|
|
137
|
+
/** Optional: umbrella issue body for checklist reconcile (#1649). */
|
|
138
|
+
fetchIssueBody?(repo: string, issueNumber: number): string;
|
|
139
|
+
/** Optional: write reconciled checklist body (#1649). */
|
|
140
|
+
editIssueBody?(repo: string, issueNumber: number, body: string): void;
|
|
127
141
|
}
|
|
128
142
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1,14 +1,40 @@
|
|
|
1
|
-
import type { Child, ReconcileUmbrellasOutcome, UmbrellaClient } from "./types.js";
|
|
1
|
+
import type { Child, ForgeIssueState, ReconcileUmbrellasOutcome, UmbrellaClient } from "./types.js";
|
|
2
2
|
export declare const OPEN_FOLDERS: readonly ["proposed", "pending", "active"];
|
|
3
3
|
export declare const CLOSED_FOLDERS: readonly ["completed", "cancelled"];
|
|
4
4
|
export declare const LIFECYCLE_FOLDERS: readonly ["proposed", "pending", "active", "completed", "cancelled"];
|
|
5
5
|
export declare const CHILD_REF_TYPE = "x-xbrief/plan";
|
|
6
|
+
/** Synthetic lifecycle folder for issue-only children with no local xBRIEF (#1649). */
|
|
7
|
+
export declare const UNKNOWN_FOLDER: "unknown";
|
|
6
8
|
export declare class UmbrellaScmError extends Error {
|
|
7
9
|
name: string;
|
|
8
10
|
}
|
|
9
11
|
export declare function childFromData(data: Record<string, unknown>, folder: string, fallbackId: string): Child;
|
|
10
12
|
export declare function buildChildIndex(vbriefDir: string): Record<string, Child>;
|
|
11
|
-
|
|
13
|
+
/** Index children by forge issue number for github-issue ref resolution (#1649). */
|
|
14
|
+
export declare function indexChildrenByIssueNumber(index: Record<string, Child>): Map<number, Child>;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve epic children from plan refs AND github-issue refs (#1649 defect 2).
|
|
17
|
+
* The epic's own github-issue ref is skipped when `epicIssueNumber` is provided.
|
|
18
|
+
* Issue-only children (no local xBRIEF) are synthesized with folder `unknown`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function computeChildren(epicData: Record<string, unknown>, index: Record<string, Child>, options?: {
|
|
21
|
+
epicIssueNumber?: number | null;
|
|
22
|
+
}): Child[];
|
|
23
|
+
/**
|
|
24
|
+
* Whether a child counts as open for current-shape + checklist (#1649 defect 3).
|
|
25
|
+
* Prefer forge issue state when present; fall back to lifecycle folder.
|
|
26
|
+
* `unknown` (issue-only, no local xBRIEF) counts open without forge confirmation.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isChildOpen(child: Child, forgeStates?: ReadonlyMap<number, ForgeIssueState> | null): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Reconcile umbrella issue-body checkboxes `- [ ] #N` / `- [x] #N` to child state (#1649).
|
|
31
|
+
* Only lines that mention a known child issue number are rewritten; other checkboxes untouched.
|
|
32
|
+
* Idempotent: returns `changed: false` when already correct.
|
|
33
|
+
*/
|
|
34
|
+
export declare function reconcileBodyChecklist(body: string, closedIssueNumbers: ReadonlySet<number>, knownIssueNumbers: ReadonlySet<number>): {
|
|
35
|
+
body: string;
|
|
36
|
+
changed: boolean;
|
|
37
|
+
};
|
|
12
38
|
export declare function computeWaves(children: readonly Child[]): string[][];
|
|
13
39
|
export declare function renderBody(options: {
|
|
14
40
|
passN: number;
|
|
@@ -34,6 +60,9 @@ export declare class ScmUmbrellaClient implements UmbrellaClient {
|
|
|
34
60
|
}>;
|
|
35
61
|
editComment(repo: string, commentId: number, body: string): void;
|
|
36
62
|
createComment(repo: string, issueNumber: number, body: string): number | null;
|
|
63
|
+
fetchIssueStates(repo: string, issueNumbers: readonly number[]): ReadonlyMap<number, ForgeIssueState>;
|
|
64
|
+
fetchIssueBody(repo: string, issueNumber: number): string;
|
|
65
|
+
editIssueBody(repo: string, issueNumber: number, body: string): void;
|
|
37
66
|
}
|
|
38
67
|
export declare function nowIso(date?: Date): string;
|
|
39
68
|
export interface ReconcileUmbrellasOptions {
|