@deftai/directive-core 0.94.0 → 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authz/classify.js +485 -0
- package/dist/cache/archive.d.ts +134 -0
- package/dist/cache/archive.js +630 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +1 -0
- package/dist/cache/main.js +298 -1
- package/dist/content-contracts/skills/helpers.d.ts +1 -1
- package/dist/content-contracts/skills/helpers.js +1 -0
- package/dist/doctor/main.js +41 -9
- package/dist/doctor/types.d.ts +2 -2
- 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 +3 -0
- package/dist/hooks/dispatcher.js +12 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +5 -2
- package/dist/init-deposit/hygiene.d.ts +1 -1
- package/dist/init-deposit/hygiene.js +16 -4
- 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/init-deposit/scaffold.js +6 -5
- package/dist/parent-turn-shape/evaluate.d.ts +84 -0
- package/dist/parent-turn-shape/evaluate.js +353 -0
- package/dist/parent-turn-shape/index.d.ts +8 -0
- package/dist/parent-turn-shape/index.js +8 -0
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +35 -0
- package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
- package/dist/review-monitor/constants.js +3 -2
- package/dist/review-monitor/tier-detection.d.ts +7 -3
- package/dist/review-monitor/tier-detection.js +18 -1
- package/dist/review-monitor/verify.js +18 -0
- package/dist/scope/index.d.ts +2 -0
- package/dist/scope/index.js +2 -0
- package/dist/scope/main.d.ts +10 -0
- package/dist/scope/main.js +109 -24
- package/dist/scope/promote-from-issue.d.ts +49 -0
- package/dist/scope/promote-from-issue.js +367 -0
- package/dist/scope/promote-path.d.ts +39 -0
- package/dist/scope/promote-path.js +105 -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/routing.d.ts +4 -2
- package/dist/swarm/routing.js +26 -4
- package/dist/swarm/verify-review-clean.d.ts +4 -3
- package/dist/swarm/verify-review-clean.js +28 -73
- package/dist/triage/actions/index.js +62 -2
- package/dist/triage/actions/types.d.ts +8 -1
- package/dist/triage/author-filter.d.ts +51 -0
- package/dist/triage/author-filter.js +152 -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 +206 -0
- package/dist/triage/classify/label-mirror.js +914 -0
- package/dist/triage/help/registry-data.d.ts +49 -38
- package/dist/triage/help/registry-data.js +134 -39
- package/dist/triage/index.d.ts +1 -0
- package/dist/triage/index.js +1 -0
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/render.d.ts +2 -0
- package/dist/triage/queue/render.js +6 -0
- package/dist/triage/queue/show.d.ts +1 -1
- package/dist/triage/queue/show.js +4 -2
- 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 +11 -3
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scope:promote --from-issue reciprocity gate (#1136 / D18).
|
|
3
|
+
*
|
|
4
|
+
* Gates promote of a proposed scope on the latest triage-cache decision for
|
|
5
|
+
* the issue, locates the proposed artifact via provenance scan, and runs the
|
|
6
|
+
* shared promotePath path (WIP rules unchanged).
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
9
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
10
|
+
import { provenanceIssueNumber, scanProvenanceRefs } from "../intake/issue-ingest.js";
|
|
11
|
+
import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
|
|
12
|
+
import { createCandidatesLog, resolveAuditLogPath } from "../triage/actions/candidates-log.js";
|
|
13
|
+
import { resolveRepo } from "../triage/queue/repo.js";
|
|
14
|
+
import { resolveProjectRoot } from "./project-context.js";
|
|
15
|
+
import { promotePath } from "./promote-path.js";
|
|
16
|
+
/** Collapse CR/LF in operator-facing messages (SLizard CWE-116). */
|
|
17
|
+
function sanitizeMsg(value) {
|
|
18
|
+
return value.replace(/\r?\n/g, " ");
|
|
19
|
+
}
|
|
20
|
+
/** True when brief references or Origin point at owner/name issue N. */
|
|
21
|
+
function briefMatchesIssueAndRepo(data, issueNumber, repo) {
|
|
22
|
+
if (provenanceIssueNumber(data) !== issueNumber) {
|
|
23
|
+
// Also accept plan.references github-issue URIs without Origin text.
|
|
24
|
+
const plan = data.plan;
|
|
25
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const refs = plan.references;
|
|
29
|
+
if (!Array.isArray(refs)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
let hit = false;
|
|
33
|
+
for (const ref of refs) {
|
|
34
|
+
if (typeof ref !== "object" || ref === null || Array.isArray(ref))
|
|
35
|
+
continue;
|
|
36
|
+
const uri = String(ref.uri ?? "");
|
|
37
|
+
if (uri.includes(`/issues/${issueNumber}`)) {
|
|
38
|
+
hit = true;
|
|
39
|
+
if (repo === null ||
|
|
40
|
+
uri.includes(`github.com/${repo}/`) ||
|
|
41
|
+
uri.includes(`${repo}/issues/`)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return hit && repo === null;
|
|
47
|
+
}
|
|
48
|
+
if (repo === null) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const plan = data.plan;
|
|
52
|
+
if (typeof plan === "object" && plan !== null && !Array.isArray(plan)) {
|
|
53
|
+
const refs = plan.references;
|
|
54
|
+
if (Array.isArray(refs)) {
|
|
55
|
+
for (const ref of refs) {
|
|
56
|
+
if (typeof ref !== "object" || ref === null || Array.isArray(ref))
|
|
57
|
+
continue;
|
|
58
|
+
const uri = String(ref.uri ?? "");
|
|
59
|
+
if (uri.includes(`/issues/${issueNumber}`) &&
|
|
60
|
+
(uri.includes(`github.com/${repo}/`) || uri.includes(`${repo}/issues/`))) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const narratives = plan.narratives;
|
|
66
|
+
if (typeof narratives === "object" && narratives !== null && !Array.isArray(narratives)) {
|
|
67
|
+
const origin = String(narratives.Origin ?? "");
|
|
68
|
+
if (origin.includes(`github.com/${repo}/issues/${issueNumber}`)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Provenance matched issue number but no repo-scoped URI — refuse when repo known.
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Locate lifecycle artifacts whose provenance owns ``issueNumber`` (optionally repo-scoped).
|
|
78
|
+
*/
|
|
79
|
+
export function findLifecycleArtifactsForIssue(projectRoot, issueNumber, options = {}) {
|
|
80
|
+
const folder = options.folder ?? "proposed";
|
|
81
|
+
const repo = options.repo ?? null;
|
|
82
|
+
const root = resolve(projectRoot);
|
|
83
|
+
let lifecycleRoot;
|
|
84
|
+
try {
|
|
85
|
+
lifecycleRoot = resolveLifecycleRoot(root);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const byIssue = scanProvenanceRefs(lifecycleRoot);
|
|
91
|
+
const rels = (byIssue.get(issueNumber) ?? []).filter((rel) => {
|
|
92
|
+
const f = rel.split(/[/\\]/)[0];
|
|
93
|
+
return f === folder;
|
|
94
|
+
});
|
|
95
|
+
const candidates = rels.map((rel) => join(lifecycleRoot, rel));
|
|
96
|
+
// Always also scan the folder for Origin-only scaffolds.
|
|
97
|
+
const folderDir = join(lifecycleRoot, folder);
|
|
98
|
+
if (existsSync(folderDir)) {
|
|
99
|
+
for (const name of readdirSync(folderDir).filter((f) => hasArtifactSuffix(f))) {
|
|
100
|
+
candidates.push(join(folderDir, name));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const abs of uniqueExisting(candidates)) {
|
|
105
|
+
try {
|
|
106
|
+
const data = JSON.parse(readFileSync(abs, "utf8"));
|
|
107
|
+
if (briefMatchesIssueAndRepo(data, issueNumber, repo)) {
|
|
108
|
+
out.push(abs);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* skip */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return uniqueExisting(out);
|
|
116
|
+
}
|
|
117
|
+
/** Locate proposed/ artifacts for issue N (repo-scoped when provided). */
|
|
118
|
+
export function findProposedArtifactsForIssue(projectRoot, issueNumber, repo = null) {
|
|
119
|
+
return findLifecycleArtifactsForIssue(projectRoot, issueNumber, { folder: "proposed", repo });
|
|
120
|
+
}
|
|
121
|
+
function uniqueExisting(paths) {
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const p of paths) {
|
|
125
|
+
const key = resolve(p);
|
|
126
|
+
if (seen.has(key))
|
|
127
|
+
continue;
|
|
128
|
+
seen.add(key);
|
|
129
|
+
if (existsSync(key)) {
|
|
130
|
+
out.push(key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out.sort();
|
|
134
|
+
}
|
|
135
|
+
function resolveLatestDecision(options, projectRoot, repo) {
|
|
136
|
+
if (options.latestDecision !== undefined) {
|
|
137
|
+
return options.latestDecision;
|
|
138
|
+
}
|
|
139
|
+
const log = createCandidatesLog(projectRoot);
|
|
140
|
+
const path = options.candidatesLogPath ?? resolveAuditLogPath(projectRoot);
|
|
141
|
+
return log.latestDecision(options.issueNumber, repo, { path });
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Promote the proposed scope for an issue, gated on triage-cache latestDecision.
|
|
145
|
+
*/
|
|
146
|
+
export function promoteFromIssue(options) {
|
|
147
|
+
const warnings = [];
|
|
148
|
+
const projectRoot = resolveProjectRoot(options.projectRoot);
|
|
149
|
+
if (projectRoot === null) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
message: "Cannot determine project root. Pass --project-root PATH, set $DEFT_PROJECT_ROOT, or run from inside a directory tree that contains vbrief/ or .git/ (#535).",
|
|
153
|
+
exitCode: 2,
|
|
154
|
+
warnings,
|
|
155
|
+
repo: null,
|
|
156
|
+
matchedPaths: [],
|
|
157
|
+
cacheStateAtPromote: null,
|
|
158
|
+
cacheDecisionId: null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const repo = resolveRepo(options.repo, projectRoot);
|
|
162
|
+
if (repo === null) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
message: "scope:promote --from-issue requires --repo OWNER/NAME (or $DEFT_TRIAGE_REPO / git remote origin).",
|
|
166
|
+
exitCode: 2,
|
|
167
|
+
warnings,
|
|
168
|
+
repo: null,
|
|
169
|
+
matchedPaths: [],
|
|
170
|
+
cacheStateAtPromote: null,
|
|
171
|
+
cacheDecisionId: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const repoSafe = sanitizeMsg(repo);
|
|
175
|
+
const n = options.issueNumber;
|
|
176
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
message: `Invalid --from-issue value: ${String(options.issueNumber)} (expected positive integer).`,
|
|
180
|
+
exitCode: 2,
|
|
181
|
+
warnings,
|
|
182
|
+
repo,
|
|
183
|
+
matchedPaths: [],
|
|
184
|
+
cacheStateAtPromote: null,
|
|
185
|
+
cacheDecisionId: null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const latest = resolveLatestDecision(options, projectRoot, repo);
|
|
189
|
+
const cacheState = latest?.decision ?? null;
|
|
190
|
+
const cacheDecisionId = latest?.decision_id ?? null;
|
|
191
|
+
const forceNoCache = options.forceNoCache === true;
|
|
192
|
+
if (!forceNoCache) {
|
|
193
|
+
if (latest === null) {
|
|
194
|
+
if (options.strict === true) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
message: `No triage-cache decision for #${n} (${repoSafe}). ` +
|
|
198
|
+
`Accept first: task triage:accept -- --issue ${n} --repo ${repoSafe} ` +
|
|
199
|
+
`(or omit --strict to soft-warn and proceed; --force-no-cache skips the gate).`,
|
|
200
|
+
exitCode: 1,
|
|
201
|
+
warnings,
|
|
202
|
+
repo,
|
|
203
|
+
matchedPaths: [],
|
|
204
|
+
cacheStateAtPromote: null,
|
|
205
|
+
cacheDecisionId: null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
warnings.push(`[scope:promote --from-issue] no triage-cache decision for #${n} (${repoSafe}); proceeding (soft warn). Use --strict to fail.`);
|
|
209
|
+
}
|
|
210
|
+
else if (latest.decision !== "accept") {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
message: `Refusing promote for #${n} (${repoSafe}): latest triage decision is '${sanitizeMsg(latest.decision)}' ` +
|
|
214
|
+
`(decision_id=${latest.decision_id}). ` +
|
|
215
|
+
`Accept first: task triage:accept -- --issue ${n} --repo ${repoSafe} ` +
|
|
216
|
+
`or override with --force-no-cache.`,
|
|
217
|
+
exitCode: 1,
|
|
218
|
+
warnings,
|
|
219
|
+
repo,
|
|
220
|
+
matchedPaths: [],
|
|
221
|
+
cacheStateAtPromote: cacheState,
|
|
222
|
+
cacheDecisionId,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
warnings.push(`[scope:promote --from-issue] --force-no-cache: skipped reciprocity gate for #${n} ` +
|
|
228
|
+
`(cache decision was ${cacheState === null ? "absent" : `'${sanitizeMsg(cacheState)}'`}).`);
|
|
229
|
+
}
|
|
230
|
+
// Already in pending/ for this issue → idempotent success (auto-promote re-entry).
|
|
231
|
+
const alreadyPending = findLifecycleArtifactsForIssue(projectRoot, n, {
|
|
232
|
+
folder: "pending",
|
|
233
|
+
repo,
|
|
234
|
+
});
|
|
235
|
+
if (alreadyPending.length === 1 && options.explicitPath === undefined) {
|
|
236
|
+
warnings.push(`[scope:promote --from-issue] #${n} already pending (${sanitizeMsg(alreadyPending[0] ?? "")}); no-op.`);
|
|
237
|
+
return {
|
|
238
|
+
ok: true,
|
|
239
|
+
message: `No-op: issue #${n} already has pending scope ${alreadyPending[0]}`,
|
|
240
|
+
exitCode: 0,
|
|
241
|
+
warnings,
|
|
242
|
+
repo,
|
|
243
|
+
matchedPaths: alreadyPending,
|
|
244
|
+
cacheStateAtPromote: cacheState,
|
|
245
|
+
cacheDecisionId: forceNoCache && cacheState !== "accept" ? null : cacheDecisionId,
|
|
246
|
+
destPath: alreadyPending[0],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
let matched = findProposedArtifactsForIssue(projectRoot, n, repo);
|
|
250
|
+
if (options.explicitPath !== undefined && options.explicitPath.trim().length > 0) {
|
|
251
|
+
const raw = options.explicitPath.trim();
|
|
252
|
+
const explicit = isAbsolute(raw) ? resolve(raw) : resolve(projectRoot, raw);
|
|
253
|
+
if (!existsSync(explicit)) {
|
|
254
|
+
return {
|
|
255
|
+
ok: false,
|
|
256
|
+
message: `Explicit path not found: ${sanitizeMsg(explicit)}`,
|
|
257
|
+
exitCode: 2,
|
|
258
|
+
warnings,
|
|
259
|
+
repo,
|
|
260
|
+
matchedPaths: matched,
|
|
261
|
+
cacheStateAtPromote: cacheState,
|
|
262
|
+
cacheDecisionId,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
// Never accept an unrelated path — must match issue provenance (and repo).
|
|
266
|
+
try {
|
|
267
|
+
const data = JSON.parse(readFileSync(explicit, "utf8"));
|
|
268
|
+
if (!briefMatchesIssueAndRepo(data, n, repo)) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
message: `Explicit path is not a provenance match for #${n} (${repoSafe}): ${sanitizeMsg(explicit)}.`,
|
|
272
|
+
exitCode: 2,
|
|
273
|
+
warnings,
|
|
274
|
+
repo,
|
|
275
|
+
matchedPaths: matched,
|
|
276
|
+
cacheStateAtPromote: cacheState,
|
|
277
|
+
cacheDecisionId,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
message: `Explicit path is not readable JSON: ${sanitizeMsg(explicit)} (${String(err)})`,
|
|
285
|
+
exitCode: 2,
|
|
286
|
+
warnings,
|
|
287
|
+
repo,
|
|
288
|
+
matchedPaths: matched,
|
|
289
|
+
cacheStateAtPromote: cacheState,
|
|
290
|
+
cacheDecisionId,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (matched.length > 0) {
|
|
294
|
+
const hit = matched.find((p) => resolve(p) === explicit);
|
|
295
|
+
if (hit === undefined) {
|
|
296
|
+
return {
|
|
297
|
+
ok: false,
|
|
298
|
+
message: `Explicit path is not among proposed matches for #${n}: ${sanitizeMsg(explicit)}. ` +
|
|
299
|
+
`Matched: ${matched.map(sanitizeMsg).join(", ")}`,
|
|
300
|
+
exitCode: 2,
|
|
301
|
+
warnings,
|
|
302
|
+
repo,
|
|
303
|
+
matchedPaths: matched,
|
|
304
|
+
cacheStateAtPromote: cacheState,
|
|
305
|
+
cacheDecisionId,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
matched = [hit];
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
matched = [explicit];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (matched.length === 0) {
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
message: `No proposed/ scope artifact found for issue #${n} (${repoSafe}). ` +
|
|
318
|
+
`Ingest first (task triage:accept -- --issue ${n} --repo ${repoSafe}) ` +
|
|
319
|
+
`or pass an explicit path: task scope:promote -- <path>.`,
|
|
320
|
+
exitCode: 1,
|
|
321
|
+
warnings,
|
|
322
|
+
repo,
|
|
323
|
+
matchedPaths: [],
|
|
324
|
+
cacheStateAtPromote: cacheState,
|
|
325
|
+
cacheDecisionId,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (matched.length > 1) {
|
|
329
|
+
const list = matched.map((p) => ` - ${sanitizeMsg(p)}`).join("\n");
|
|
330
|
+
return {
|
|
331
|
+
ok: false,
|
|
332
|
+
message: `Multiple proposed/ artifacts match issue #${n}; refuse to guess.\n${list}\n` +
|
|
333
|
+
`Promote one path explicitly: task scope:promote -- <path> ` +
|
|
334
|
+
`or re-run with --path <one-of-the-above>.`,
|
|
335
|
+
exitCode: 1,
|
|
336
|
+
warnings,
|
|
337
|
+
repo,
|
|
338
|
+
matchedPaths: matched,
|
|
339
|
+
cacheStateAtPromote: cacheState,
|
|
340
|
+
cacheDecisionId,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const filePath = matched[0];
|
|
344
|
+
const promoteResult = promotePath(filePath, {
|
|
345
|
+
projectRoot,
|
|
346
|
+
force: options.force,
|
|
347
|
+
actor: options.actor,
|
|
348
|
+
now: options.now,
|
|
349
|
+
fromIssue: n,
|
|
350
|
+
cacheDecisionId: forceNoCache && cacheState !== "accept" ? null : cacheDecisionId,
|
|
351
|
+
cacheStateAtPromote: cacheState,
|
|
352
|
+
forceNoCache,
|
|
353
|
+
requireAudit: true,
|
|
354
|
+
});
|
|
355
|
+
warnings.push(`[scope:promote --from-issue] cache decision for #${n} was ` +
|
|
356
|
+
`${cacheState === null ? "absent" : `'${sanitizeMsg(cacheState)}'`}` +
|
|
357
|
+
(promoteResult.ok ? " (proceeded)" : " (promote failed)"));
|
|
358
|
+
return {
|
|
359
|
+
...promoteResult,
|
|
360
|
+
warnings,
|
|
361
|
+
repo,
|
|
362
|
+
matchedPaths: matched,
|
|
363
|
+
cacheStateAtPromote: cacheState,
|
|
364
|
+
cacheDecisionId: forceNoCache && cacheState !== "accept" ? null : cacheDecisionId,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
//# sourceMappingURL=promote-from-issue.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared single-path promote (proposed/ → pending/) with optional triage audit linkage (#1136).
|
|
3
|
+
*/
|
|
4
|
+
export interface PromotePathOptions {
|
|
5
|
+
readonly projectRoot?: string;
|
|
6
|
+
/** WIP-cap override (--force). */
|
|
7
|
+
readonly force?: boolean;
|
|
8
|
+
readonly actor?: string;
|
|
9
|
+
readonly now?: Date;
|
|
10
|
+
/** Issue number that triggered this promote (from-issue / auto-promote). */
|
|
11
|
+
readonly fromIssue?: number;
|
|
12
|
+
/** Accept (or other) decision_id from candidates.jsonl, when known. */
|
|
13
|
+
readonly cacheDecisionId?: string | null;
|
|
14
|
+
/** Latest cache decision string at promote time (accept / defer / null). */
|
|
15
|
+
readonly cacheStateAtPromote?: string | null;
|
|
16
|
+
/** True when reciprocity gate was skipped via --force-no-cache. */
|
|
17
|
+
readonly forceNoCache?: boolean;
|
|
18
|
+
/** Write promote audit entry even without from-issue linkage (default: only when linkage present). */
|
|
19
|
+
readonly alwaysAudit?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* When true, audit append failure is a hard error (from-issue / auto-promote
|
|
22
|
+
* require from_issue + cache fields). Default false for plain path promote.
|
|
23
|
+
*/
|
|
24
|
+
readonly requireAudit?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface PromotePathResult {
|
|
27
|
+
readonly ok: boolean;
|
|
28
|
+
readonly message: string;
|
|
29
|
+
readonly exitCode: number;
|
|
30
|
+
readonly destPath?: string;
|
|
31
|
+
readonly auditEntry?: Record<string, unknown> | null;
|
|
32
|
+
readonly wipCapOverride?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Promote a single proposed-scope path to pending/, enforcing WIP and
|
|
36
|
+
* optionally recording from_issue / cache_decision_id on the scope audit log.
|
|
37
|
+
*/
|
|
38
|
+
export declare function promotePath(filePath: string, options?: PromotePathOptions): PromotePathResult;
|
|
39
|
+
//# sourceMappingURL=promote-path.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared single-path promote (proposed/ → pending/) with optional triage audit linkage (#1136).
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { append, canonicalLogPath, newDecisionId } from "./audit-log.js";
|
|
7
|
+
import { resolveProjectRoot } from "./project-context.js";
|
|
8
|
+
import { recordWipCapOverride, runTransition } from "./transition.js";
|
|
9
|
+
import { utcNowIso } from "./vbrief-json.js";
|
|
10
|
+
import { canonicalRelpath } from "./vbrief-ref.js";
|
|
11
|
+
import { checkWipCap, formatWipCapRefusal } from "./wip-cap-check.js";
|
|
12
|
+
/**
|
|
13
|
+
* Promote a single proposed-scope path to pending/, enforcing WIP and
|
|
14
|
+
* optionally recording from_issue / cache_decision_id on the scope audit log.
|
|
15
|
+
*/
|
|
16
|
+
export function promotePath(filePath, options = {}) {
|
|
17
|
+
const root = resolveProjectRoot(options.projectRoot);
|
|
18
|
+
if (root === null) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
message: "Cannot determine project root. Pass --project-root PATH, set $DEFT_PROJECT_ROOT, or run from inside a directory tree that contains vbrief/ or .git/ (#535).",
|
|
22
|
+
exitCode: 2,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const resolved = resolve(filePath);
|
|
26
|
+
if (!existsSync(resolved)) {
|
|
27
|
+
return { ok: false, message: `File not found: ${resolved}`, exitCode: 2 };
|
|
28
|
+
}
|
|
29
|
+
const capCheck = checkWipCap(root, options.force === true);
|
|
30
|
+
if (!capCheck.allowed) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
message: formatWipCapRefusal(capCheck),
|
|
34
|
+
exitCode: 1,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const now = options.now ?? new Date();
|
|
38
|
+
const result = runTransition("promote", resolved, now);
|
|
39
|
+
if (!result.ok) {
|
|
40
|
+
return { ok: false, message: result.message, exitCode: 1 };
|
|
41
|
+
}
|
|
42
|
+
const basename = resolved.split(/[/\\]/).pop() ?? "";
|
|
43
|
+
// Destination after promote is sibling pending/ under the same lifecycle root.
|
|
44
|
+
const lifecycleRoot = dirname(dirname(resolved));
|
|
45
|
+
const destPath = join(lifecycleRoot, "pending", basename);
|
|
46
|
+
let auditEntry = null;
|
|
47
|
+
const shouldAudit = options.alwaysAudit === true ||
|
|
48
|
+
options.fromIssue !== undefined ||
|
|
49
|
+
options.forceNoCache === true ||
|
|
50
|
+
options.cacheDecisionId !== undefined ||
|
|
51
|
+
options.cacheStateAtPromote !== undefined;
|
|
52
|
+
if (shouldAudit) {
|
|
53
|
+
try {
|
|
54
|
+
const entry = {
|
|
55
|
+
decision_id: newDecisionId(),
|
|
56
|
+
timestamp: utcNowIso(now),
|
|
57
|
+
action: "promote",
|
|
58
|
+
vbrief_path: canonicalRelpath(destPath, root),
|
|
59
|
+
from_status: "proposed",
|
|
60
|
+
to_status: "pending",
|
|
61
|
+
actor: options.actor ?? "operator",
|
|
62
|
+
};
|
|
63
|
+
if (options.fromIssue !== undefined) {
|
|
64
|
+
entry.from_issue = options.fromIssue;
|
|
65
|
+
}
|
|
66
|
+
if (options.cacheDecisionId !== undefined) {
|
|
67
|
+
entry.cache_decision_id = options.cacheDecisionId;
|
|
68
|
+
}
|
|
69
|
+
if (options.cacheStateAtPromote !== undefined) {
|
|
70
|
+
entry.cache_state_at_promote = options.cacheStateAtPromote;
|
|
71
|
+
}
|
|
72
|
+
if (options.forceNoCache === true) {
|
|
73
|
+
entry.force_no_cache = true;
|
|
74
|
+
}
|
|
75
|
+
append(entry, canonicalLogPath(root));
|
|
76
|
+
auditEntry = entry;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (options.requireAudit === true) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
message: `Promoted ${basename} to pending/ but required scope audit failed: ${String(err)}. ` +
|
|
83
|
+
`Artifact is in pending/; re-run promote will no-op once audit is writable.`,
|
|
84
|
+
exitCode: 1,
|
|
85
|
+
destPath,
|
|
86
|
+
auditEntry: null,
|
|
87
|
+
wipCapOverride: capCheck.forceOverride,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/* best-effort audit for plain path promote */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (capCheck.forceOverride) {
|
|
94
|
+
recordWipCapOverride(destPath, root, capCheck, now);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
message: result.message,
|
|
99
|
+
exitCode: 0,
|
|
100
|
+
destPath,
|
|
101
|
+
auditEntry,
|
|
102
|
+
wipCapOverride: capCheck.forceOverride,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=promote-path.js.map
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { type AgentHookReadinessResult } from "../verify-env/agent-hook-readiness.js";
|
|
1
2
|
export declare const ENTRYPOINT_TIMEOUT_SECONDS = 300;
|
|
2
3
|
export declare const ENTRYPOINT_TIMEOUT_EXIT_CODE = 124;
|
|
3
4
|
export type RitualEntrypointFn = (argv: readonly string[]) => number;
|
|
5
|
+
export interface DefaultRitualRunnerSeams {
|
|
6
|
+
readonly evaluateAgentHookReadiness?: (projectRoot: string) => AgentHookReadinessResult;
|
|
7
|
+
}
|
|
4
8
|
/** Run an in-process ritual entrypoint with stdout/stderr capture. */
|
|
5
9
|
export declare function callMain(mainFn: RitualEntrypointFn, argv: readonly string[], _options?: {
|
|
6
10
|
label?: string;
|
|
@@ -11,8 +15,8 @@ export declare function callMain(mainFn: RitualEntrypointFn, argv: readonly stri
|
|
|
11
15
|
};
|
|
12
16
|
/** CLI-shaped cache-fresh entrypoint (mirrors scripts/preflight_cache.py main). */
|
|
13
17
|
export declare function runCacheFreshMain(argv: readonly string[]): number;
|
|
14
|
-
/** Default gated-tier runner: in-process doctor
|
|
15
|
-
export declare function defaultRitualRunner(command: readonly string[], projectRoot: string): {
|
|
18
|
+
/** Default gated-tier runner: in-process hook readiness, doctor, and cache freshness. */
|
|
19
|
+
export declare function defaultRitualRunner(command: readonly string[], projectRoot: string, seams?: DefaultRitualRunnerSeams): {
|
|
16
20
|
code: number;
|
|
17
21
|
stdout: string;
|
|
18
22
|
stderr: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cmdDoctor } from "../doctor/main.js";
|
|
2
2
|
import { evaluate } from "../preflight-cache/evaluate.js";
|
|
3
|
+
import { evaluateAgentHookReadinessSafely, } from "../verify-env/agent-hook-readiness.js";
|
|
3
4
|
export const ENTRYPOINT_TIMEOUT_SECONDS = 300;
|
|
4
5
|
export const ENTRYPOINT_TIMEOUT_EXIT_CODE = 124;
|
|
5
6
|
/** Run an in-process ritual entrypoint with stdout/stderr capture. */
|
|
@@ -66,9 +67,21 @@ export function runCacheFreshMain(argv) {
|
|
|
66
67
|
}
|
|
67
68
|
return result.code;
|
|
68
69
|
}
|
|
69
|
-
/** Default gated-tier runner: in-process doctor
|
|
70
|
-
export function defaultRitualRunner(command, projectRoot) {
|
|
70
|
+
/** Default gated-tier runner: in-process hook readiness, doctor, and cache freshness. */
|
|
71
|
+
export function defaultRitualRunner(command, projectRoot, seams = {}) {
|
|
71
72
|
const [verb, ...rest] = command;
|
|
73
|
+
if (verb === "verify:hooks-installed") {
|
|
74
|
+
const evaluateReadiness = seams.evaluateAgentHookReadiness
|
|
75
|
+
? (root) => evaluateAgentHookReadinessSafely(root, seams.evaluateAgentHookReadiness)
|
|
76
|
+
: evaluateAgentHookReadinessSafely;
|
|
77
|
+
const result = evaluateReadiness(projectRoot);
|
|
78
|
+
const output = `${result.message}\n`;
|
|
79
|
+
return {
|
|
80
|
+
code: result.code,
|
|
81
|
+
stdout: result.stream === "stdout" ? output : "",
|
|
82
|
+
stderr: result.stream === "stderr" ? output : "",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
72
85
|
if (verb === "doctor") {
|
|
73
86
|
return callMain((argv) => cmdDoctor(argv), ["--project-root", projectRoot, ...rest]);
|
|
74
87
|
}
|
|
@@ -90,12 +90,20 @@ export function runSessionReady(projectRoot, options = {}) {
|
|
|
90
90
|
const env = options.env ?? process.env;
|
|
91
91
|
const lines = [];
|
|
92
92
|
const steps = [];
|
|
93
|
-
const inspect = options.inspectRitual ??
|
|
94
|
-
const verify = options.verifyRitual ??
|
|
95
|
-
const start = options.runStart ??
|
|
93
|
+
const inspect = options.inspectRitual ?? inspectSessionRitual;
|
|
94
|
+
const verify = options.verifyRitual ?? verifySessionRitual;
|
|
95
|
+
const start = options.runStart ?? runSessionStart;
|
|
96
96
|
const fetchAll = options.fetchAll ?? cacheFetchAll;
|
|
97
97
|
const inferRepo = options.inferRepo ?? ((root) => inferSessionReadyRepo(root, env));
|
|
98
|
-
|
|
98
|
+
const verifyOpts = {
|
|
99
|
+
tier: "gated",
|
|
100
|
+
posture: "mutation",
|
|
101
|
+
now,
|
|
102
|
+
runGit: options.runGit,
|
|
103
|
+
runner: options.runner,
|
|
104
|
+
forceGatedSteps: ["agent_hooks"],
|
|
105
|
+
};
|
|
106
|
+
// --- Fast path: inspect is green, but refresh functional hook readiness before returning. ---
|
|
99
107
|
const gatedInspect = inspect(projectRoot, {
|
|
100
108
|
tier: "gated",
|
|
101
109
|
posture: "mutation",
|
|
@@ -103,6 +111,22 @@ export function runSessionReady(projectRoot, options = {}) {
|
|
|
103
111
|
runGit: options.runGit,
|
|
104
112
|
});
|
|
105
113
|
if (gatedInspect.code === 0) {
|
|
114
|
+
steps.push("verify:session-ritual:gated");
|
|
115
|
+
const refreshed = verify(projectRoot, verifyOpts);
|
|
116
|
+
if (!isGatedVerifyActuallyReady(refreshed)) {
|
|
117
|
+
const message = refreshed.code === 0 && refreshed.bypassed
|
|
118
|
+
? bypassedReadyFailure(refreshed)
|
|
119
|
+
: `${refreshed.message}\n Remaining blocker after session:ready. Fix the step above, then re-run \`${readyCommand()}\`.`;
|
|
120
|
+
lines.push(message);
|
|
121
|
+
return {
|
|
122
|
+
code: refreshed.code === 0 ? 1 : refreshed.code,
|
|
123
|
+
message,
|
|
124
|
+
path: SESSION_READY_FAILED,
|
|
125
|
+
lines,
|
|
126
|
+
steps,
|
|
127
|
+
duration_ms: elapsedMs(started),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
106
130
|
const message = "OK session ready (gated ritual already fresh).";
|
|
107
131
|
lines.push(message);
|
|
108
132
|
return {
|
|
@@ -138,7 +162,7 @@ export function runSessionReady(projectRoot, options = {}) {
|
|
|
138
162
|
`session:start failed (exit ${startResult.code}). Recovery: run \`${readyCommand()}\` again after fixing the blocker.`;
|
|
139
163
|
lines.push(message);
|
|
140
164
|
return {
|
|
141
|
-
code: startResult.code
|
|
165
|
+
code: startResult.code,
|
|
142
166
|
message,
|
|
143
167
|
path: SESSION_READY_FAILED,
|
|
144
168
|
lines,
|
|
@@ -149,13 +173,6 @@ export function runSessionReady(projectRoot, options = {}) {
|
|
|
149
173
|
}
|
|
150
174
|
// --- Gated verify (lazy doctor + cache_fresh) ---
|
|
151
175
|
steps.push("verify:session-ritual:gated");
|
|
152
|
-
const verifyOpts = {
|
|
153
|
-
tier: "gated",
|
|
154
|
-
posture: "mutation",
|
|
155
|
-
now,
|
|
156
|
-
runGit: options.runGit,
|
|
157
|
-
runner: options.runner,
|
|
158
|
-
};
|
|
159
176
|
let verifyResult = verify(projectRoot, verifyOpts);
|
|
160
177
|
if (isGatedVerifyActuallyReady(verifyResult)) {
|
|
161
178
|
const message = "OK session ready (gated ritual verified).";
|
|
@@ -222,7 +239,7 @@ export function runSessionReady(projectRoot, options = {}) {
|
|
|
222
239
|
};
|
|
223
240
|
}
|
|
224
241
|
steps.push("verify:session-ritual:gated:retry");
|
|
225
|
-
verifyResult = verify(projectRoot, verifyOpts);
|
|
242
|
+
verifyResult = verify(projectRoot, { ...verifyOpts, forceGatedSteps: [] });
|
|
226
243
|
if (isGatedVerifyActuallyReady(verifyResult)) {
|
|
227
244
|
const message = "OK session ready (recovered via cache refresh).";
|
|
228
245
|
lines.push(message);
|
|
@@ -253,7 +270,7 @@ export function runSessionReady(projectRoot, options = {}) {
|
|
|
253
270
|
` Remaining blocker after session:ready. Fix the step above, then re-run \`${readyCommand()}\`.`;
|
|
254
271
|
lines.push(message);
|
|
255
272
|
return {
|
|
256
|
-
code: verifyResult.code
|
|
273
|
+
code: verifyResult.code,
|
|
257
274
|
message,
|
|
258
275
|
path: SESSION_READY_FAILED,
|
|
259
276
|
lines,
|
|
@@ -16,7 +16,8 @@ export type SessionCeremonyTier = (typeof SESSION_CEREMONY_TIERS)[number];
|
|
|
16
16
|
export declare const COLD_CEREMONY_TIER: SessionCeremonyTier;
|
|
17
17
|
export declare const REARM_CEREMONY_TIER: SessionCeremonyTier;
|
|
18
18
|
export declare const QUICK_STEPS: readonly ["alignment", "branch_policy", "triage_welcome"];
|
|
19
|
-
export declare const GATED_STEPS: readonly ["doctor", "cache_fresh"];
|
|
19
|
+
export declare const GATED_STEPS: readonly ["agent_hooks", "doctor", "cache_fresh"];
|
|
20
|
+
export type GatedStepName = (typeof GATED_STEPS)[number];
|
|
20
21
|
/** Env opt-in for optional session:start network (release probe + triage cache hydrate) (#2991). */
|
|
21
22
|
export declare const ENV_SESSION_START_NETWORK = "DEFT_SESSION_START_NETWORK";
|
|
22
23
|
/** Human-readable skip notice when optional network is off the hot path (#2991). */
|