@deftai/directive-core 0.61.1 → 0.62.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/check-updates/index.d.ts +49 -0
- package/dist/check-updates/index.js +323 -0
- package/dist/init-deposit/init-deposit.js +2 -0
- package/dist/init-deposit/migrate.d.ts +9 -0
- package/dist/init-deposit/migrate.js +23 -0
- package/dist/init-deposit/refresh.js +2 -0
- package/dist/install-upgrade/index.js +3 -3
- package/dist/migrate-preflight/index.d.ts +1 -2
- package/dist/migrate-preflight/index.js +16 -36
- package/dist/render/framework-commands.js +0 -1
- package/dist/session/session-start.js +5 -0
- package/dist/umbrella-current-shape/index.d.ts +87 -0
- package/dist/umbrella-current-shape/index.js +329 -0
- package/dist/vbrief-validate/precutover.d.ts +3 -0
- package/dist/vbrief-validate/precutover.js +8 -1
- package/dist/verify-env/verify-hooks-installed.d.ts +2 -0
- package/dist/verify-env/verify-hooks-installed.js +20 -4
- package/package.json +11 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Default subprocess timeout for `git ls-remote` (seconds). */
|
|
2
|
+
export declare const REMOTE_PROBE_DEFAULT_TIMEOUT = 5;
|
|
3
|
+
/** Baked-in canonical upstream (#1320). Never probe consumer origin. */
|
|
4
|
+
export declare const DEFT_UPSTREAM_URL = "https://github.com/deftai/directive.git";
|
|
5
|
+
/** Reject git ls-remote targets that could be parsed as options (#CodeQL). */
|
|
6
|
+
export declare function isSafeGitLsRemoteTarget(url: string): boolean;
|
|
7
|
+
/** Return a trimmed upstream URL safe for `git ls-remote`, or null when rejected. */
|
|
8
|
+
export declare function sanitizeGitLsRemoteTarget(url: string): string | null;
|
|
9
|
+
export type RemoteProbeStatus = "ok" | "behind" | "skipped" | "no-upstream" | "no-tags" | "error";
|
|
10
|
+
export interface RemoteProbeResult {
|
|
11
|
+
readonly status: RemoteProbeStatus;
|
|
12
|
+
readonly current: string;
|
|
13
|
+
readonly remote?: string;
|
|
14
|
+
readonly upstream_url?: string;
|
|
15
|
+
readonly reason?: string;
|
|
16
|
+
readonly error?: string;
|
|
17
|
+
}
|
|
18
|
+
export type SemverKey = readonly [number, number, number, number, string];
|
|
19
|
+
export interface GitRunner {
|
|
20
|
+
lsRemoteTags(upstreamUrl: string, timeoutMs: number): string[] | "timeout" | "os-error";
|
|
21
|
+
}
|
|
22
|
+
export declare function parseSemverTag(tag: string): SemverKey | null;
|
|
23
|
+
export declare function maxSemverTag(tags: readonly string[]): string | null;
|
|
24
|
+
export declare function resolveProbeCurrentVersion(projectRoot: string, frameworkRoot?: string): string;
|
|
25
|
+
export declare function resolveUpstreamUrl(projectRoot: string): string;
|
|
26
|
+
export declare function resolveProbeTimeout(env?: NodeJS.ProcessEnv): number;
|
|
27
|
+
export declare function noNetworkActive(env?: NodeJS.ProcessEnv): boolean;
|
|
28
|
+
export declare function parseLsRemoteTags(stdout: string): string[];
|
|
29
|
+
export declare function defaultGitRunner(): GitRunner;
|
|
30
|
+
export interface RunRemoteProbeOptions {
|
|
31
|
+
readonly projectRoot: string;
|
|
32
|
+
readonly frameworkRoot?: string;
|
|
33
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
34
|
+
readonly git?: GitRunner;
|
|
35
|
+
}
|
|
36
|
+
export declare function runRemoteProbe(options: RunRemoteProbeOptions): RemoteProbeResult;
|
|
37
|
+
export interface EmitCheckUpdatesOptions {
|
|
38
|
+
readonly jsonMode: boolean;
|
|
39
|
+
readonly writeOut: (text: string) => void;
|
|
40
|
+
}
|
|
41
|
+
export declare function emitCheckUpdates(result: RemoteProbeResult, options: EmitCheckUpdatesOptions): number;
|
|
42
|
+
export declare function runCheckUpdates(argv: readonly string[], options?: {
|
|
43
|
+
projectRoot?: string;
|
|
44
|
+
frameworkRoot?: string;
|
|
45
|
+
env?: NodeJS.ProcessEnv;
|
|
46
|
+
git?: GitRunner;
|
|
47
|
+
writeOut?: (text: string) => void;
|
|
48
|
+
}): number;
|
|
49
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
|
|
5
|
+
import { resolveVersion } from "../doctor/paths.js";
|
|
6
|
+
/** Default subprocess timeout for `git ls-remote` (seconds). */
|
|
7
|
+
export const REMOTE_PROBE_DEFAULT_TIMEOUT = 5.0;
|
|
8
|
+
/** Baked-in canonical upstream (#1320). Never probe consumer origin. */
|
|
9
|
+
export const DEFT_UPSTREAM_URL = "https://github.com/deftai/directive.git";
|
|
10
|
+
const MANIFEST_UPSTREAM_URL_KEYS = [
|
|
11
|
+
"source_url",
|
|
12
|
+
"url",
|
|
13
|
+
"upstream_url",
|
|
14
|
+
"upstream",
|
|
15
|
+
"origin",
|
|
16
|
+
];
|
|
17
|
+
/** Reject git ls-remote targets that could be parsed as options (#CodeQL). */
|
|
18
|
+
export function isSafeGitLsRemoteTarget(url) {
|
|
19
|
+
return sanitizeGitLsRemoteTarget(url) !== null;
|
|
20
|
+
}
|
|
21
|
+
/** Return a trimmed upstream URL safe for `git ls-remote`, or null when rejected. */
|
|
22
|
+
export function sanitizeGitLsRemoteTarget(url) {
|
|
23
|
+
const trimmed = url.trim();
|
|
24
|
+
if (!trimmed || trimmed.startsWith("-")) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (/^https?:\/\/\S+$/i.test(trimmed)) {
|
|
28
|
+
return trimmed;
|
|
29
|
+
}
|
|
30
|
+
if (/^git@[\w.-]+:[\w./-]+\.git$/i.test(trimmed)) {
|
|
31
|
+
return trimmed;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function normalizePrereleaseForSort(pre) {
|
|
36
|
+
const dot = pre.indexOf(".");
|
|
37
|
+
if (dot <= 0) {
|
|
38
|
+
return pre;
|
|
39
|
+
}
|
|
40
|
+
const kind = pre.slice(0, dot).toLowerCase();
|
|
41
|
+
if (kind !== "alpha" && kind !== "beta" && kind !== "rc") {
|
|
42
|
+
return pre;
|
|
43
|
+
}
|
|
44
|
+
const afterKind = pre.slice(dot + 1);
|
|
45
|
+
let numEnd = 0;
|
|
46
|
+
while (numEnd < afterKind.length) {
|
|
47
|
+
const ch = afterKind[numEnd];
|
|
48
|
+
if (ch === undefined || ch < "0" || ch > "9") {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
numEnd += 1;
|
|
52
|
+
}
|
|
53
|
+
if (numEnd === 0) {
|
|
54
|
+
return pre;
|
|
55
|
+
}
|
|
56
|
+
const padded = afterKind.slice(0, numEnd).padStart(8, "0");
|
|
57
|
+
return `${kind}.${padded}${afterKind.slice(numEnd)}`;
|
|
58
|
+
}
|
|
59
|
+
const SEMVER_TAG_RE = /^v?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?<pre>-[\w][\w.-]*)?$/;
|
|
60
|
+
export function parseSemverTag(tag) {
|
|
61
|
+
if (!tag) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const match = SEMVER_TAG_RE.exec(tag.trim());
|
|
65
|
+
if (!match?.groups) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const pre = match.groups.pre ?? "";
|
|
69
|
+
const preStripped = pre.replace(/^-/, "");
|
|
70
|
+
return [
|
|
71
|
+
Number(match.groups.major),
|
|
72
|
+
Number(match.groups.minor),
|
|
73
|
+
Number(match.groups.patch),
|
|
74
|
+
pre ? 0 : 1,
|
|
75
|
+
normalizePrereleaseForSort(preStripped),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
export function maxSemverTag(tags) {
|
|
79
|
+
const parsed = [];
|
|
80
|
+
for (const tag of tags) {
|
|
81
|
+
const key = parseSemverTag(tag);
|
|
82
|
+
if (key !== null) {
|
|
83
|
+
parsed.push({ key, tag });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (parsed.length === 0) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
parsed.sort((a, b) => {
|
|
90
|
+
for (let i = 0; i < a.key.length; i += 1) {
|
|
91
|
+
const av = a.key[i];
|
|
92
|
+
const bv = b.key[i];
|
|
93
|
+
if (typeof av === "number" && typeof bv === "number" && av !== bv) {
|
|
94
|
+
return av - bv;
|
|
95
|
+
}
|
|
96
|
+
if (typeof av === "string" && typeof bv === "string" && av !== bv) {
|
|
97
|
+
return av < bv ? -1 : 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return 0;
|
|
101
|
+
});
|
|
102
|
+
return parsed[parsed.length - 1]?.tag ?? null;
|
|
103
|
+
}
|
|
104
|
+
function readVendoredManifest(projectRoot) {
|
|
105
|
+
for (const candidate of [
|
|
106
|
+
join(projectRoot, ".deft", "core", "VERSION"),
|
|
107
|
+
join(projectRoot, ".deft", "VERSION"),
|
|
108
|
+
join(projectRoot, "deft", "VERSION"),
|
|
109
|
+
]) {
|
|
110
|
+
if (!existsSync(candidate)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
return parseInstallManifest(readFileSync(candidate, "utf8"));
|
|
115
|
+
}
|
|
116
|
+
catch { }
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
export function resolveProbeCurrentVersion(projectRoot, frameworkRoot) {
|
|
121
|
+
const manifest = readVendoredManifest(projectRoot);
|
|
122
|
+
if (manifest) {
|
|
123
|
+
const derived = manifestTagToVersion(manifest);
|
|
124
|
+
if (derived) {
|
|
125
|
+
return derived;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return resolveVersion(frameworkRoot).replace(/^v/, "");
|
|
129
|
+
}
|
|
130
|
+
export function resolveUpstreamUrl(projectRoot) {
|
|
131
|
+
const manifest = readVendoredManifest(projectRoot);
|
|
132
|
+
if (manifest) {
|
|
133
|
+
for (const key of MANIFEST_UPSTREAM_URL_KEYS) {
|
|
134
|
+
const value = manifest[key];
|
|
135
|
+
if (typeof value === "string" && value.trim()) {
|
|
136
|
+
const safe = sanitizeGitLsRemoteTarget(value);
|
|
137
|
+
if (safe) {
|
|
138
|
+
return safe;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return DEFT_UPSTREAM_URL;
|
|
144
|
+
}
|
|
145
|
+
export function resolveProbeTimeout(env = process.env) {
|
|
146
|
+
const raw = (env.DEFT_REMOTE_PROBE_TIMEOUT ?? "").trim();
|
|
147
|
+
if (!raw) {
|
|
148
|
+
return REMOTE_PROBE_DEFAULT_TIMEOUT;
|
|
149
|
+
}
|
|
150
|
+
const value = Number(raw);
|
|
151
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
152
|
+
return REMOTE_PROBE_DEFAULT_TIMEOUT;
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
export function noNetworkActive(env = process.env) {
|
|
157
|
+
return (env.DEFT_NO_NETWORK ?? "").trim() === "1";
|
|
158
|
+
}
|
|
159
|
+
export function parseLsRemoteTags(stdout) {
|
|
160
|
+
const tags = [];
|
|
161
|
+
for (const line of stdout.split("\n")) {
|
|
162
|
+
const marker = "\trefs/tags/";
|
|
163
|
+
const idx = line.indexOf(marker);
|
|
164
|
+
if (idx < 0) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const tag = line.slice(idx + marker.length).trim();
|
|
168
|
+
if (tag) {
|
|
169
|
+
tags.push(tag);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return tags;
|
|
173
|
+
}
|
|
174
|
+
export function defaultGitRunner() {
|
|
175
|
+
return {
|
|
176
|
+
lsRemoteTags(upstreamUrl, timeoutMs) {
|
|
177
|
+
const safeUrl = sanitizeGitLsRemoteTarget(upstreamUrl);
|
|
178
|
+
if (!safeUrl) {
|
|
179
|
+
return "os-error";
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const stdout = execFileSync("git", ["ls-remote", "--tags", "--refs", "--", safeUrl], {
|
|
183
|
+
encoding: "utf8",
|
|
184
|
+
timeout: timeoutMs,
|
|
185
|
+
});
|
|
186
|
+
return parseLsRemoteTags(stdout);
|
|
187
|
+
}
|
|
188
|
+
catch (exc) {
|
|
189
|
+
if (exc instanceof Error &&
|
|
190
|
+
(exc.message.includes("ETIMEDOUT") || exc.message.includes("timed out"))) {
|
|
191
|
+
return "timeout";
|
|
192
|
+
}
|
|
193
|
+
return "os-error";
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
export function runRemoteProbe(options) {
|
|
199
|
+
const env = options.env ?? process.env;
|
|
200
|
+
const current = resolveProbeCurrentVersion(options.projectRoot, options.frameworkRoot);
|
|
201
|
+
if (noNetworkActive(env)) {
|
|
202
|
+
return {
|
|
203
|
+
status: "skipped",
|
|
204
|
+
reason: "DEFT_NO_NETWORK=1",
|
|
205
|
+
current,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const timeoutSec = resolveProbeTimeout(env);
|
|
209
|
+
const upstreamUrl = resolveUpstreamUrl(options.projectRoot);
|
|
210
|
+
const git = options.git ?? defaultGitRunner();
|
|
211
|
+
const tagsResult = git.lsRemoteTags(upstreamUrl, Math.ceil(timeoutSec * 1000));
|
|
212
|
+
if (tagsResult === "timeout") {
|
|
213
|
+
return {
|
|
214
|
+
status: "error",
|
|
215
|
+
current,
|
|
216
|
+
upstream_url: upstreamUrl,
|
|
217
|
+
error: "timeout",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (tagsResult === "os-error") {
|
|
221
|
+
return {
|
|
222
|
+
status: "error",
|
|
223
|
+
current,
|
|
224
|
+
upstream_url: upstreamUrl,
|
|
225
|
+
error: "Error: git unavailable",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const remoteTag = maxSemverTag(tagsResult);
|
|
229
|
+
if (remoteTag === null) {
|
|
230
|
+
return {
|
|
231
|
+
status: "no-tags",
|
|
232
|
+
current,
|
|
233
|
+
upstream_url: upstreamUrl,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const remoteKey = parseSemverTag(remoteTag);
|
|
237
|
+
const currentKey = parseSemverTag(current.startsWith("v") ? current : `v${current}`);
|
|
238
|
+
if (currentKey !== null && remoteKey !== null && compareSemverKeys(remoteKey, currentKey) > 0) {
|
|
239
|
+
return {
|
|
240
|
+
status: "behind",
|
|
241
|
+
current,
|
|
242
|
+
remote: remoteTag,
|
|
243
|
+
upstream_url: upstreamUrl,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
status: "ok",
|
|
248
|
+
current,
|
|
249
|
+
remote: remoteTag,
|
|
250
|
+
upstream_url: upstreamUrl,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function compareSemverKeys(a, b) {
|
|
254
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
255
|
+
const av = a[i];
|
|
256
|
+
const bv = b[i];
|
|
257
|
+
if (av === bv) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (typeof av === "number" && typeof bv === "number") {
|
|
261
|
+
return av - bv;
|
|
262
|
+
}
|
|
263
|
+
return String(av) < String(bv) ? -1 : 1;
|
|
264
|
+
}
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
export function emitCheckUpdates(result, options) {
|
|
268
|
+
if (options.jsonMode) {
|
|
269
|
+
const payload = {
|
|
270
|
+
status: result.status,
|
|
271
|
+
current: result.current,
|
|
272
|
+
};
|
|
273
|
+
if (result.remote !== undefined) {
|
|
274
|
+
payload.remote = result.remote;
|
|
275
|
+
}
|
|
276
|
+
if (result.upstream_url !== undefined) {
|
|
277
|
+
payload.upstream_url = result.upstream_url;
|
|
278
|
+
}
|
|
279
|
+
if (result.reason !== undefined) {
|
|
280
|
+
payload.reason = result.reason;
|
|
281
|
+
}
|
|
282
|
+
if (result.error !== undefined) {
|
|
283
|
+
payload.error = result.error;
|
|
284
|
+
}
|
|
285
|
+
options.writeOut(`${JSON.stringify(payload, Object.keys(payload).sort())}\n`);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
const current = result.current;
|
|
289
|
+
switch (result.status) {
|
|
290
|
+
case "ok":
|
|
291
|
+
options.writeOut(`OK upstream=${result.remote}\n`);
|
|
292
|
+
break;
|
|
293
|
+
case "behind":
|
|
294
|
+
options.writeOut(`BEHIND upstream=${result.remote} current=v${current} commits-behind=unknown\n`);
|
|
295
|
+
break;
|
|
296
|
+
case "skipped":
|
|
297
|
+
options.writeOut(`SKIPPED reason=DEFT_NO_NETWORK current=v${current}\n`);
|
|
298
|
+
break;
|
|
299
|
+
case "no-upstream":
|
|
300
|
+
options.writeOut(`NO-UPSTREAM current=v${current}\n`);
|
|
301
|
+
break;
|
|
302
|
+
case "no-tags":
|
|
303
|
+
options.writeOut(`NO-TAGS current=v${current} upstream=${result.upstream_url ?? ""}\n`);
|
|
304
|
+
break;
|
|
305
|
+
default:
|
|
306
|
+
options.writeOut(`ERROR current=v${current} upstream=${result.upstream_url ?? ""} error=${result.error ?? "unknown"}\n`);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return result.status === "behind" ? 1 : 0;
|
|
311
|
+
}
|
|
312
|
+
export function runCheckUpdates(argv, options = {}) {
|
|
313
|
+
const jsonMode = argv.includes("--json");
|
|
314
|
+
const result = runRemoteProbe({
|
|
315
|
+
projectRoot: options.projectRoot ?? process.cwd(),
|
|
316
|
+
frameworkRoot: options.frameworkRoot,
|
|
317
|
+
env: options.env,
|
|
318
|
+
git: options.git,
|
|
319
|
+
});
|
|
320
|
+
const writeOut = options.writeOut ?? ((text) => process.stdout.write(text));
|
|
321
|
+
return emitCheckUpdates(result, { jsonMode, writeOut });
|
|
322
|
+
}
|
|
323
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -14,6 +14,7 @@ import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
|
|
|
14
14
|
import { readCorePackageVersion } from "../engine-version.js";
|
|
15
15
|
import { ensureInitGitignoreLines, reconstituteDepositFromContent } from "./gitignore.js";
|
|
16
16
|
import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
|
|
17
|
+
import { printMigrateNudgeIfNeeded } from "./migrate.js";
|
|
17
18
|
import { CANONICAL_INSTALL_ROOT, depositNeutralization, ensureTaskfile, writeAgentsMd, writeAgentsSkills, writeConsumerGitHooks, writeConsumerVbrief, writeInstallManifest, } from "./scaffold.js";
|
|
18
19
|
export function parseInitArgv(canonicalArgv, userArgv = []) {
|
|
19
20
|
const args = [...canonicalArgv, ...userArgv];
|
|
@@ -109,6 +110,7 @@ export function printNextSteps(result, io) {
|
|
|
109
110
|
io.printf(" 2. Deft skill auto-discovery is partially implemented — if your agent doesn't\n");
|
|
110
111
|
io.printf(' start setup automatically, tell it: "Use AGENTS.md"\n');
|
|
111
112
|
io.printf(" 3. On first session, the agent will guide you through creating USER.md and PROJECT-DEFINITION.vbrief.json\n");
|
|
113
|
+
printMigrateNudgeIfNeeded(result.projectDir, io);
|
|
112
114
|
io.printf("\n");
|
|
113
115
|
}
|
|
114
116
|
export async function runInitDeposit(args, io, seams = {}) {
|
|
@@ -85,4 +85,13 @@ export interface RunMigrateCliOptions {
|
|
|
85
85
|
* stderr; config errors print to stderr; success prints to stdout.
|
|
86
86
|
*/
|
|
87
87
|
export declare function runMigrateCli(options: RunMigrateCliOptions): number;
|
|
88
|
+
/** One-line nudge for init/update/session completion when provenance is unstamped (#2059). */
|
|
89
|
+
export declare const MIGRATE_COMPLETION_NUDGE = "[deft] One-time: run `directive migrate` to stamp npm provenance (idempotent). See content/UPGRADING.md.";
|
|
90
|
+
/** True when a canonical-vendored deposit exists and lacks the npm-managed sentinel. */
|
|
91
|
+
export declare function shouldEmitMigrateNudge(projectRoot: string, seams?: Pick<MigrateSeams, "isFile" | "readText">): boolean;
|
|
92
|
+
export interface MigrateNudgeIo {
|
|
93
|
+
printf: (text: string) => void;
|
|
94
|
+
}
|
|
95
|
+
/** Prints {@link MIGRATE_COMPLETION_NUDGE} when {@link shouldEmitMigrateNudge} is true. */
|
|
96
|
+
export declare function printMigrateNudgeIfNeeded(projectRoot: string, io: MigrateNudgeIo, seams?: Pick<MigrateSeams, "isFile" | "readText">): void;
|
|
88
97
|
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -193,4 +193,27 @@ export function runMigrateCli(options) {
|
|
|
193
193
|
}
|
|
194
194
|
return result.exitCode;
|
|
195
195
|
}
|
|
196
|
+
/** One-line nudge for init/update/session completion when provenance is unstamped (#2059). */
|
|
197
|
+
export const MIGRATE_COMPLETION_NUDGE = "[deft] One-time: run `directive migrate` to stamp npm provenance (idempotent). See content/UPGRADING.md.";
|
|
198
|
+
/** True when a canonical-vendored deposit exists and lacks the npm-managed sentinel. */
|
|
199
|
+
export function shouldEmitMigrateNudge(projectRoot, seams = {}) {
|
|
200
|
+
const isFile = seams.isFile ?? existsSync;
|
|
201
|
+
const readText = seams.readText ?? defaultReadText;
|
|
202
|
+
const manifestPath = detectCanonicalVendoredManifest(projectRoot, isFile);
|
|
203
|
+
if (manifestPath === null)
|
|
204
|
+
return false;
|
|
205
|
+
const text = readText(manifestPath);
|
|
206
|
+
if (text === null)
|
|
207
|
+
return false;
|
|
208
|
+
const manifest = parseInstallManifest(text);
|
|
209
|
+
if (Object.keys(manifest).length === 0)
|
|
210
|
+
return false;
|
|
211
|
+
return !isNpmManaged(manifest);
|
|
212
|
+
}
|
|
213
|
+
/** Prints {@link MIGRATE_COMPLETION_NUDGE} when {@link shouldEmitMigrateNudge} is true. */
|
|
214
|
+
export function printMigrateNudgeIfNeeded(projectRoot, io, seams = {}) {
|
|
215
|
+
if (shouldEmitMigrateNudge(projectRoot, seams)) {
|
|
216
|
+
io.printf(`\n${MIGRATE_COMPLETION_NUDGE}\n`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
196
219
|
//# sourceMappingURL=migrate.js.map
|
|
@@ -18,6 +18,7 @@ import { DEV_FALLBACK } from "../platform/constants.js";
|
|
|
18
18
|
import { gitPorcelain } from "../story-ready/git.js";
|
|
19
19
|
import { parseInitArgv } from "./init-deposit.js";
|
|
20
20
|
import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
|
|
21
|
+
import { printMigrateNudgeIfNeeded } from "./migrate.js";
|
|
21
22
|
import { CANONICAL_INSTALL_ROOT, depositNeutralization, writeAgentsMd, writeInstallManifest, } from "./scaffold.js";
|
|
22
23
|
const INSTALLER_MANAGED_EXACT = new Set([
|
|
23
24
|
"AGENTS.md",
|
|
@@ -231,6 +232,7 @@ export function printUpdateComplete(result, io) {
|
|
|
231
232
|
if (result.versionSkewNotice) {
|
|
232
233
|
io.printf(`\n${result.versionSkewNotice}\n`);
|
|
233
234
|
}
|
|
235
|
+
printMigrateNudgeIfNeeded(result.projectDir, io);
|
|
234
236
|
io.printf("\n");
|
|
235
237
|
}
|
|
236
238
|
export async function runRefreshDeposit(args, io, seams = {}) {
|
|
@@ -6,7 +6,7 @@ import { buildInstallManifestText, CANONICAL_INSTALL_ROOT, } from "../init-depos
|
|
|
6
6
|
import { agentsRefreshPlan } from "../platform/agents-md.js";
|
|
7
7
|
import { DEV_FALLBACK } from "../platform/constants.js";
|
|
8
8
|
import { resolveVersion } from "../platform/resolve-version.js";
|
|
9
|
-
import { detectPreCutoverLegacy } from "../vbrief-validate/precutover.js";
|
|
9
|
+
import { detectPreCutoverLegacy, frozenPreCutoverMigrationGuidance, } from "../vbrief-validate/precutover.js";
|
|
10
10
|
const ENGINE_PACKAGE_FALLBACK = "0.0.0";
|
|
11
11
|
/**
|
|
12
12
|
* Resolve the framework version for an upgrade (#2053).
|
|
@@ -193,7 +193,7 @@ export function runInstallUpgrade(args, io) {
|
|
|
193
193
|
}
|
|
194
194
|
const legacy = detectPreCutoverLegacy(projectRoot);
|
|
195
195
|
if (legacy.length > 0) {
|
|
196
|
-
io.writeOut(`Pre-v0.20 document model detected (${legacy.join(", ")}).
|
|
196
|
+
io.writeOut(`Pre-v0.20 document model detected (${legacy.join(", ")}). ${frozenPreCutoverMigrationGuidance()}\n`);
|
|
197
197
|
}
|
|
198
198
|
const vbriefDir = join(projectRoot, "vbrief");
|
|
199
199
|
const targetDir = existsSync(vbriefDir) && statSync(vbriefDir).isDirectory() ? vbriefDir : projectRoot;
|
|
@@ -216,7 +216,7 @@ export function runInstallUpgrade(args, io) {
|
|
|
216
216
|
else {
|
|
217
217
|
io.writeOut(`Updated .deft-version from ${recorded} to ${normalizedVersion}.\n`);
|
|
218
218
|
}
|
|
219
|
-
io.writeOut(
|
|
219
|
+
io.writeOut(`If legacy SPECIFICATION.md or PROJECT.md content remains, see UPGRADING.md § Frozen pre-v0.20 document-model migration (#2068).\n`);
|
|
220
220
|
return runAgentsRefresh(projectRoot, agentsRoot, io);
|
|
221
221
|
}
|
|
222
222
|
//# sourceMappingURL=index.js.map
|
|
@@ -18,11 +18,10 @@ export type MigratePreflightOutcome = {
|
|
|
18
18
|
readonly exitCode: 0 | 1;
|
|
19
19
|
readonly results: readonly CheckResult[];
|
|
20
20
|
} | MigratePreflightConfigError;
|
|
21
|
-
export declare function checkUv(which?: (cmd: string) => string | null): CheckResult;
|
|
22
21
|
export declare function checkLayout(deftRoot: string, projectRoot: string): CheckResult;
|
|
23
22
|
export declare function checkGitClean(projectRoot: string): CheckResult;
|
|
24
23
|
export declare function checkDocumentModel(projectRoot: string): CheckResult;
|
|
25
|
-
export declare function evaluate(deftRoot: string, projectRoot: string
|
|
24
|
+
export declare function evaluate(deftRoot: string, projectRoot: string): {
|
|
26
25
|
exitCode: 0 | 1;
|
|
27
26
|
results: CheckResult[];
|
|
28
27
|
};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import { detectPreCutoverLegacy, isCurrentGeneratedSpecification, isGeneratedSpecificationExport, missingLifecycleFolders, } from "../vbrief-validate/precutover.js";
|
|
4
|
+
import { detectPreCutoverLegacy, frozenPreCutoverMigrationGuidance, isCurrentGeneratedSpecification, isGeneratedSpecificationExport, missingLifecycleFolders, } from "../vbrief-validate/precutover.js";
|
|
6
5
|
function resolveContentRoot(frameworkRoot) {
|
|
7
6
|
const nested = join(frameworkRoot, "content");
|
|
8
7
|
try {
|
|
@@ -14,25 +13,7 @@ function resolveContentRoot(frameworkRoot) {
|
|
|
14
13
|
}
|
|
15
14
|
return frameworkRoot;
|
|
16
15
|
}
|
|
17
|
-
export function checkUv(which = defaultWhich) {
|
|
18
|
-
if (which("uv") !== null) {
|
|
19
|
-
return { name: "uv", status: "PASS", message: "uv is on PATH." };
|
|
20
|
-
}
|
|
21
|
-
return {
|
|
22
|
-
name: "uv",
|
|
23
|
-
status: "FAIL",
|
|
24
|
-
message: "uv is not on PATH. Install from https://docs.astral.sh/uv/ and re-run.",
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
16
|
export function checkLayout(deftRoot, projectRoot) {
|
|
28
|
-
const migrator = join(deftRoot, "scripts", "migrate_vbrief.py");
|
|
29
|
-
if (!existsSync(migrator) || !statSync(migrator).isFile()) {
|
|
30
|
-
return {
|
|
31
|
-
name: "layout",
|
|
32
|
-
status: "FAIL",
|
|
33
|
-
message: `Migrator script missing at ${migrator}. The framework checkout appears incomplete or pre-v0.20; refresh per deft/QUICK-START.md.`,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
17
|
const schemasDir = join(resolveContentRoot(deftRoot), "vbrief", "schemas");
|
|
37
18
|
if (!existsSync(schemasDir) || !statSync(schemasDir).isDirectory()) {
|
|
38
19
|
return {
|
|
@@ -46,13 +27,13 @@ export function checkLayout(deftRoot, projectRoot) {
|
|
|
46
27
|
return {
|
|
47
28
|
name: "layout",
|
|
48
29
|
status: "WARN",
|
|
49
|
-
message: `Project vbrief/ not present at ${projectVbrief} --
|
|
30
|
+
message: `Project vbrief/ not present at ${projectVbrief} -- expected for greenfield projects.`,
|
|
50
31
|
};
|
|
51
32
|
}
|
|
52
33
|
return {
|
|
53
34
|
name: "layout",
|
|
54
35
|
status: "PASS",
|
|
55
|
-
message: `Framework
|
|
36
|
+
message: `Framework schemas present; project vbrief/ at ${projectVbrief}.`,
|
|
56
37
|
};
|
|
57
38
|
}
|
|
58
39
|
export function checkGitClean(projectRoot) {
|
|
@@ -65,7 +46,7 @@ export function checkGitClean(projectRoot) {
|
|
|
65
46
|
return {
|
|
66
47
|
name: "git-clean",
|
|
67
48
|
status: "WARN",
|
|
68
|
-
message: "Working tree is dirty.
|
|
49
|
+
message: "Working tree is dirty. Commit or stash before running a frozen-release migration.",
|
|
69
50
|
};
|
|
70
51
|
}
|
|
71
52
|
return { name: "git-clean", status: "PASS", message: "Working tree is clean." };
|
|
@@ -76,7 +57,7 @@ export function checkGitClean(projectRoot) {
|
|
|
76
57
|
return {
|
|
77
58
|
name: "git-clean",
|
|
78
59
|
status: "WARN",
|
|
79
|
-
message: "git executable not on PATH; skipping working-tree check.
|
|
60
|
+
message: "git executable not on PATH; skipping working-tree check.",
|
|
80
61
|
};
|
|
81
62
|
}
|
|
82
63
|
return {
|
|
@@ -91,8 +72,8 @@ export function checkDocumentModel(projectRoot) {
|
|
|
91
72
|
if (legacy.length > 0) {
|
|
92
73
|
return {
|
|
93
74
|
name: "document-model",
|
|
94
|
-
status: "
|
|
95
|
-
message: `
|
|
75
|
+
status: "FAIL",
|
|
76
|
+
message: `Pre-v0.20 document model detected (${legacy.join(", ")}). ${frozenPreCutoverMigrationGuidance()}`,
|
|
96
77
|
};
|
|
97
78
|
}
|
|
98
79
|
const specPath = join(projectRoot, "SPECIFICATION.md");
|
|
@@ -117,8 +98,8 @@ export function checkDocumentModel(projectRoot) {
|
|
|
117
98
|
if (isCurrentGeneratedSpecification(projectRoot, content)) {
|
|
118
99
|
return {
|
|
119
100
|
name: "document-model",
|
|
120
|
-
status: "
|
|
121
|
-
message: "Current generated SPECIFICATION.md detected (source: vbrief/specification.vbrief.json);
|
|
101
|
+
status: "PASS",
|
|
102
|
+
message: "Current generated SPECIFICATION.md detected (source: vbrief/specification.vbrief.json); pre-v0.20 migration is not needed.",
|
|
122
103
|
};
|
|
123
104
|
}
|
|
124
105
|
}
|
|
@@ -128,20 +109,19 @@ export function checkDocumentModel(projectRoot) {
|
|
|
128
109
|
if (missing.length > 0) {
|
|
129
110
|
return {
|
|
130
111
|
name: "document-model",
|
|
131
|
-
status: "
|
|
132
|
-
message: `Partial vBRIEF layout detected; missing lifecycle folder(s): ${missing.join(", ")}
|
|
112
|
+
status: "FAIL",
|
|
113
|
+
message: `Partial vBRIEF layout detected; missing lifecycle folder(s): ${missing.join(", ")}. Create the folders or follow ${frozenPreCutoverMigrationGuidance()}`,
|
|
133
114
|
};
|
|
134
115
|
}
|
|
135
116
|
}
|
|
136
117
|
return {
|
|
137
118
|
name: "document-model",
|
|
138
|
-
status: "
|
|
139
|
-
message: "No
|
|
119
|
+
status: "PASS",
|
|
120
|
+
message: "No pre-v0.20 document-model artifacts detected.",
|
|
140
121
|
};
|
|
141
122
|
}
|
|
142
|
-
export function evaluate(deftRoot, projectRoot
|
|
123
|
+
export function evaluate(deftRoot, projectRoot) {
|
|
143
124
|
const results = [
|
|
144
|
-
checkUv(which),
|
|
145
125
|
checkLayout(deftRoot, projectRoot),
|
|
146
126
|
checkDocumentModel(projectRoot),
|
|
147
127
|
checkGitClean(projectRoot),
|
|
@@ -183,10 +163,10 @@ export function emitMigratePreflight(outcome, io, quiet) {
|
|
|
183
163
|
io.writeOut(line);
|
|
184
164
|
}
|
|
185
165
|
if (outcome.exitCode === 1) {
|
|
186
|
-
io.writeErr("migrate:preflight FAILED --
|
|
166
|
+
io.writeErr("migrate:preflight FAILED -- pre-v0.20 document model or incomplete vBRIEF layout. Resolve using UPGRADING.md § Frozen pre-v0.20 document-model migration (#2068).\n");
|
|
187
167
|
}
|
|
188
168
|
else {
|
|
189
|
-
io.writeOut("migrate:preflight OK --
|
|
169
|
+
io.writeOut("migrate:preflight OK -- no pre-v0.20 document-model migration required.\n");
|
|
190
170
|
}
|
|
191
171
|
return outcome.exitCode;
|
|
192
172
|
}
|
|
@@ -42,7 +42,6 @@ export const COMMANDS = {
|
|
|
42
42
|
"triage:accept": spec("triage:accept", "triage_actions:main", { defaultArgs: ["accept"] }),
|
|
43
43
|
"triage:status": spec("triage:status", "triage_actions:main", { defaultArgs: ["status"] }),
|
|
44
44
|
"triage:scope": spec("triage:scope", "triage_scope:main"),
|
|
45
|
-
"migrate:vbrief": spec("migrate:vbrief", "framework_commands:_cmd_migrate_vbrief"),
|
|
46
45
|
"cache:fetch-all": spec("cache:fetch-all", "cache:main", { defaultArgs: ["fetch-all"] }),
|
|
47
46
|
"capacity:show": spec("capacity:show", "capacity_show:main", {
|
|
48
47
|
projectRootArg: "--project-root",
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { runningInsideDeftRepo } from "../doctor/paths.js";
|
|
3
|
+
import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposit/migrate.js";
|
|
2
4
|
import { disclosureLine } from "../policy/disclosure.js";
|
|
3
5
|
import { resolvePolicy } from "../policy/resolve.js";
|
|
4
6
|
import { runDefaultMode } from "../triage/welcome/default-mode.js";
|
|
@@ -263,6 +265,9 @@ export function runSessionStart(projectRoot, options = {}) {
|
|
|
263
265
|
lines.push(message);
|
|
264
266
|
}
|
|
265
267
|
}
|
|
268
|
+
if (!runningInsideDeftRepo(projectRoot) && shouldEmitMigrateNudge(projectRoot)) {
|
|
269
|
+
lines.push(MIGRATE_COMPLETION_NUDGE);
|
|
270
|
+
}
|
|
266
271
|
const payload = newRitualStatePayload({
|
|
267
272
|
sessionId: (options.newSessionId ?? randomUUID)(),
|
|
268
273
|
gitHead: gitHeadValue,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Matches `## Current shape (as of pass-N)` — same pattern as vbrief-reconcile/umbrellas.ts. */
|
|
2
|
+
export declare const CURRENT_SHAPE_HEADER_RE: RegExp;
|
|
3
|
+
export declare const SECTION_MARKERS: {
|
|
4
|
+
readonly lastUpdated: RegExp;
|
|
5
|
+
readonly lastPassType: RegExp;
|
|
6
|
+
readonly childCount: RegExp;
|
|
7
|
+
readonly childCountHistory: RegExp;
|
|
8
|
+
readonly openChildren: RegExp;
|
|
9
|
+
readonly closedChildren: RegExp;
|
|
10
|
+
readonly waveOrder: RegExp;
|
|
11
|
+
readonly openQuestions: RegExp;
|
|
12
|
+
readonly readingOrder: RegExp;
|
|
13
|
+
};
|
|
14
|
+
export type SectionKey = keyof typeof SECTION_MARKERS;
|
|
15
|
+
/** Required per AGENTS.md #1152; openQuestions is optional. */
|
|
16
|
+
export declare const REQUIRED_SECTIONS: readonly SectionKey[];
|
|
17
|
+
export declare const OPTIONAL_SECTIONS: readonly SectionKey[];
|
|
18
|
+
export interface IssueComment {
|
|
19
|
+
readonly id: number;
|
|
20
|
+
readonly body: string;
|
|
21
|
+
readonly htmlUrl: string;
|
|
22
|
+
readonly updatedAt: string;
|
|
23
|
+
}
|
|
24
|
+
export interface CurrentShapeComment extends IssueComment {
|
|
25
|
+
readonly pass: number;
|
|
26
|
+
}
|
|
27
|
+
export interface SectionPresence {
|
|
28
|
+
readonly present: SectionKey[];
|
|
29
|
+
readonly missing: SectionKey[];
|
|
30
|
+
readonly optionalPresent: SectionKey[];
|
|
31
|
+
readonly optionalMissing: SectionKey[];
|
|
32
|
+
}
|
|
33
|
+
export interface CurrentShapeResult {
|
|
34
|
+
readonly issueNumber: number;
|
|
35
|
+
readonly repo: string;
|
|
36
|
+
readonly commentId: number;
|
|
37
|
+
readonly htmlUrl: string;
|
|
38
|
+
readonly pass: number;
|
|
39
|
+
readonly body: string;
|
|
40
|
+
readonly sections: SectionPresence;
|
|
41
|
+
}
|
|
42
|
+
export type ScmFetcher = (repo: string, issueNumber: number) => IssueComment[] | {
|
|
43
|
+
error: string;
|
|
44
|
+
};
|
|
45
|
+
export interface RunCurrentShapeOptions {
|
|
46
|
+
readonly issueNumber: number;
|
|
47
|
+
readonly projectRoot: string;
|
|
48
|
+
readonly repo?: string | null;
|
|
49
|
+
readonly jsonMode?: boolean;
|
|
50
|
+
readonly strict?: boolean;
|
|
51
|
+
readonly fetchComments?: ScmFetcher;
|
|
52
|
+
readonly writeOut?: (text: string) => void;
|
|
53
|
+
readonly writeErr?: (text: string) => void;
|
|
54
|
+
}
|
|
55
|
+
/** Merge `gh api --paginate` concatenated JSON array pages into comment rows. */
|
|
56
|
+
export declare function parseCommentsFromGhStdout(stdout: string): IssueComment[];
|
|
57
|
+
export declare function extractPassFromBody(body: string): number | null;
|
|
58
|
+
/** Pick the canonical comment — highest pass-N; tie-break by comment id (latest). */
|
|
59
|
+
export declare function selectCurrentShapeComment(comments: readonly IssueComment[]): CurrentShapeComment | null;
|
|
60
|
+
export declare function detectSections(body: string): SectionPresence;
|
|
61
|
+
export declare function sectionsRecord(presence: SectionPresence): Record<string, boolean>;
|
|
62
|
+
export declare const NO_CURRENT_SHAPE_MESSAGE: string;
|
|
63
|
+
export declare function fetchCurrentShape(options: {
|
|
64
|
+
repo: string;
|
|
65
|
+
issueNumber: number;
|
|
66
|
+
fetchComments?: ScmFetcher;
|
|
67
|
+
}): {
|
|
68
|
+
ok: true;
|
|
69
|
+
result: CurrentShapeResult;
|
|
70
|
+
} | {
|
|
71
|
+
ok: false;
|
|
72
|
+
error: string;
|
|
73
|
+
kind: "not-found" | "config";
|
|
74
|
+
};
|
|
75
|
+
export declare function emitCurrentShape(result: CurrentShapeResult, options: {
|
|
76
|
+
jsonMode: boolean;
|
|
77
|
+
writeOut: (text: string) => void;
|
|
78
|
+
}): number;
|
|
79
|
+
export declare function runCurrentShape(options: RunCurrentShapeOptions): number;
|
|
80
|
+
export declare function parseCurrentShapeArgv(argv: readonly string[]): {
|
|
81
|
+
issueNumber: number | null;
|
|
82
|
+
repo: string | null;
|
|
83
|
+
jsonMode: boolean;
|
|
84
|
+
strict: boolean;
|
|
85
|
+
passthroughError?: string;
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { resolveBinary } from "../scm/binary.js";
|
|
3
|
+
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
4
|
+
import { resolveRepo } from "../triage/queue/repo.js";
|
|
5
|
+
/** Matches `## Current shape (as of pass-N)` — same pattern as vbrief-reconcile/umbrellas.ts. */
|
|
6
|
+
export const CURRENT_SHAPE_HEADER_RE = /^## Current shape \(as of pass-(\d+)\)/m;
|
|
7
|
+
export const SECTION_MARKERS = {
|
|
8
|
+
lastUpdated: /^Last updated:\s/m,
|
|
9
|
+
lastPassType: /^Last pass type:\s/m,
|
|
10
|
+
childCount: /^Child count:\s/m,
|
|
11
|
+
childCountHistory: /^Child-count history:\s/m,
|
|
12
|
+
openChildren: /^### Open children\s*$/m,
|
|
13
|
+
closedChildren: /^### Closed children\s*$/m,
|
|
14
|
+
waveOrder: /^### Wave order\s*$/m,
|
|
15
|
+
openQuestions: /^### Open questions\s*$/m,
|
|
16
|
+
readingOrder: /^### Reading order for fresh contributors\s*$/m,
|
|
17
|
+
};
|
|
18
|
+
/** Required per AGENTS.md #1152; openQuestions is optional. */
|
|
19
|
+
export const REQUIRED_SECTIONS = [
|
|
20
|
+
"lastUpdated",
|
|
21
|
+
"lastPassType",
|
|
22
|
+
"childCount",
|
|
23
|
+
"childCountHistory",
|
|
24
|
+
"openChildren",
|
|
25
|
+
"closedChildren",
|
|
26
|
+
"waveOrder",
|
|
27
|
+
"readingOrder",
|
|
28
|
+
];
|
|
29
|
+
export const OPTIONAL_SECTIONS = ["openQuestions"];
|
|
30
|
+
function mapCommentEntry(entry) {
|
|
31
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const rec = entry;
|
|
35
|
+
if (typeof rec.id !== "number" || typeof rec.body !== "string") {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
id: rec.id,
|
|
40
|
+
body: rec.body,
|
|
41
|
+
htmlUrl: typeof rec.html_url === "string" ? rec.html_url : "",
|
|
42
|
+
updatedAt: typeof rec.updated_at === "string" ? rec.updated_at : "",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Merge `gh api --paginate` concatenated JSON array pages into comment rows. */
|
|
46
|
+
export function parseCommentsFromGhStdout(stdout) {
|
|
47
|
+
const comments = [];
|
|
48
|
+
const text = stdout.trim();
|
|
49
|
+
if (!text) {
|
|
50
|
+
return comments;
|
|
51
|
+
}
|
|
52
|
+
const pages = [];
|
|
53
|
+
try {
|
|
54
|
+
pages.push(JSON.parse(text));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
let idx = 0;
|
|
58
|
+
while (idx < text.length) {
|
|
59
|
+
while (idx < text.length && /\s/.test(text[idx] ?? "")) {
|
|
60
|
+
idx += 1;
|
|
61
|
+
}
|
|
62
|
+
if (idx >= text.length) {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
const slice = text.slice(idx);
|
|
66
|
+
if (!slice.startsWith("[")) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
let depth = 0;
|
|
70
|
+
let end = -1;
|
|
71
|
+
for (let j = 0; j < slice.length; j += 1) {
|
|
72
|
+
const ch = slice[j];
|
|
73
|
+
if (ch === "[") {
|
|
74
|
+
depth += 1;
|
|
75
|
+
}
|
|
76
|
+
else if (ch === "]") {
|
|
77
|
+
depth -= 1;
|
|
78
|
+
if (depth === 0) {
|
|
79
|
+
end = j + 1;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (end < 0) {
|
|
85
|
+
throw new SyntaxError("invalid paginated JSON");
|
|
86
|
+
}
|
|
87
|
+
pages.push(JSON.parse(slice.slice(0, end)));
|
|
88
|
+
idx += end;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const page of pages) {
|
|
92
|
+
if (!Array.isArray(page)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const entry of page) {
|
|
96
|
+
const mapped = mapCommentEntry(entry);
|
|
97
|
+
if (mapped !== null) {
|
|
98
|
+
comments.push(mapped);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return comments;
|
|
103
|
+
}
|
|
104
|
+
function defaultFetchComments(repo, issueNumber) {
|
|
105
|
+
const binary = resolveBinary();
|
|
106
|
+
const path = `repos/${repo}/issues/${issueNumber}/comments?per_page=100`;
|
|
107
|
+
const proc = spawnSync(binary, ["api", "--paginate", path], {
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
110
|
+
});
|
|
111
|
+
if (proc.error !== undefined) {
|
|
112
|
+
return {
|
|
113
|
+
error: `fetch comments #${issueNumber} (${repo}) failed: ${proc.error.message}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (proc.status !== 0) {
|
|
117
|
+
return {
|
|
118
|
+
error: `fetch comments #${issueNumber} (${repo}) failed: ${(proc.stderr || proc.stdout || "").trim()}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return parseCommentsFromGhStdout(String(proc.stdout ?? ""));
|
|
123
|
+
}
|
|
124
|
+
catch (exc) {
|
|
125
|
+
return {
|
|
126
|
+
error: `fetch comments #${issueNumber} (${repo}) returned non-JSON: ${String(exc)}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function extractPassFromBody(body) {
|
|
131
|
+
const match = CURRENT_SHAPE_HEADER_RE.exec(body);
|
|
132
|
+
if (!match?.[1]) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const pass = Number(match[1]);
|
|
136
|
+
return Number.isFinite(pass) ? pass : null;
|
|
137
|
+
}
|
|
138
|
+
/** Pick the canonical comment — highest pass-N; tie-break by comment id (latest). */
|
|
139
|
+
export function selectCurrentShapeComment(comments) {
|
|
140
|
+
let best = null;
|
|
141
|
+
for (const comment of comments) {
|
|
142
|
+
const pass = extractPassFromBody(comment.body);
|
|
143
|
+
if (pass === null) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const candidate = { ...comment, pass };
|
|
147
|
+
if (best === null ||
|
|
148
|
+
candidate.pass > best.pass ||
|
|
149
|
+
(candidate.pass === best.pass && candidate.id > best.id)) {
|
|
150
|
+
best = candidate;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return best;
|
|
154
|
+
}
|
|
155
|
+
export function detectSections(body) {
|
|
156
|
+
const present = [];
|
|
157
|
+
const missing = [];
|
|
158
|
+
const optionalPresent = [];
|
|
159
|
+
const optionalMissing = [];
|
|
160
|
+
for (const key of REQUIRED_SECTIONS) {
|
|
161
|
+
if (SECTION_MARKERS[key].test(body)) {
|
|
162
|
+
present.push(key);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
missing.push(key);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const key of OPTIONAL_SECTIONS) {
|
|
169
|
+
if (SECTION_MARKERS[key].test(body)) {
|
|
170
|
+
optionalPresent.push(key);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
optionalMissing.push(key);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { present, missing, optionalPresent, optionalMissing };
|
|
177
|
+
}
|
|
178
|
+
export function sectionsRecord(presence) {
|
|
179
|
+
const out = {};
|
|
180
|
+
for (const key of REQUIRED_SECTIONS) {
|
|
181
|
+
out[key] = presence.present.includes(key);
|
|
182
|
+
}
|
|
183
|
+
for (const key of OPTIONAL_SECTIONS) {
|
|
184
|
+
out[key] = presence.optionalPresent.includes(key);
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
export const NO_CURRENT_SHAPE_MESSAGE = "No ## Current shape (as of pass-N) comment found on this issue. " +
|
|
189
|
+
"Create one per AGENTS.md ## Umbrella current-shape convention (#1152) — " +
|
|
190
|
+
"do not fall back to the issue body (stale by design).";
|
|
191
|
+
export function fetchCurrentShape(options) {
|
|
192
|
+
const fetcher = options.fetchComments ?? defaultFetchComments;
|
|
193
|
+
const fetched = fetcher(options.repo, options.issueNumber);
|
|
194
|
+
if (!Array.isArray(fetched)) {
|
|
195
|
+
return { ok: false, error: fetched.error, kind: "config" };
|
|
196
|
+
}
|
|
197
|
+
const selected = selectCurrentShapeComment(fetched);
|
|
198
|
+
if (selected === null) {
|
|
199
|
+
return { ok: false, error: NO_CURRENT_SHAPE_MESSAGE, kind: "not-found" };
|
|
200
|
+
}
|
|
201
|
+
const sections = detectSections(selected.body);
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
result: {
|
|
205
|
+
issueNumber: options.issueNumber,
|
|
206
|
+
repo: options.repo,
|
|
207
|
+
commentId: selected.id,
|
|
208
|
+
htmlUrl: selected.htmlUrl,
|
|
209
|
+
pass: selected.pass,
|
|
210
|
+
body: selected.body,
|
|
211
|
+
sections,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export function emitCurrentShape(result, options) {
|
|
216
|
+
if (options.jsonMode) {
|
|
217
|
+
const payload = {
|
|
218
|
+
issueNumber: result.issueNumber,
|
|
219
|
+
repo: result.repo,
|
|
220
|
+
commentId: result.commentId,
|
|
221
|
+
htmlUrl: result.htmlUrl,
|
|
222
|
+
pass: result.pass,
|
|
223
|
+
body: result.body,
|
|
224
|
+
sections: sectionsRecord(result.sections),
|
|
225
|
+
missingSections: result.sections.missing,
|
|
226
|
+
missingOptionalSections: result.sections.optionalMissing,
|
|
227
|
+
};
|
|
228
|
+
options.writeOut(`${JSON.stringify(payload)}\n`);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
options.writeOut(`${result.body}\n`);
|
|
232
|
+
}
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
export function runCurrentShape(options) {
|
|
236
|
+
const writeOut = options.writeOut ?? ((text) => process.stdout.write(text));
|
|
237
|
+
const writeErr = options.writeErr ?? ((text) => process.stderr.write(text));
|
|
238
|
+
if (!Number.isInteger(options.issueNumber) || options.issueNumber <= 0) {
|
|
239
|
+
writeErr("umbrella:current-shape: issue number must be a positive integer\n");
|
|
240
|
+
return 2;
|
|
241
|
+
}
|
|
242
|
+
const repo = resolveRepo(options.repo ?? null, options.projectRoot);
|
|
243
|
+
if (repo === null) {
|
|
244
|
+
writeErr("umbrella:current-shape: could not resolve owner/repo — pass --repo OWNER/REPO or run inside a git repo with origin\n");
|
|
245
|
+
return 2;
|
|
246
|
+
}
|
|
247
|
+
const fetched = fetchCurrentShape({
|
|
248
|
+
repo,
|
|
249
|
+
issueNumber: options.issueNumber,
|
|
250
|
+
fetchComments: options.fetchComments,
|
|
251
|
+
});
|
|
252
|
+
if (!fetched.ok) {
|
|
253
|
+
writeErr(`umbrella:current-shape: ${fetched.error}\n`);
|
|
254
|
+
return fetched.kind === "config" ? 2 : 1;
|
|
255
|
+
}
|
|
256
|
+
if (options.strict === true && fetched.result.sections.missing.length > 0) {
|
|
257
|
+
writeErr(`umbrella:current-shape: --strict: missing required section(s): ${fetched.result.sections.missing.join(", ")}\n`);
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
return emitCurrentShape(fetched.result, {
|
|
261
|
+
jsonMode: options.jsonMode ?? false,
|
|
262
|
+
writeOut,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
export function parseCurrentShapeArgv(argv) {
|
|
266
|
+
let issueNumber = null;
|
|
267
|
+
let repo = null;
|
|
268
|
+
let jsonMode = false;
|
|
269
|
+
let strict = false;
|
|
270
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
271
|
+
const arg = argv[i] ?? "";
|
|
272
|
+
if (arg === "--json") {
|
|
273
|
+
jsonMode = true;
|
|
274
|
+
}
|
|
275
|
+
else if (arg === "--strict") {
|
|
276
|
+
strict = true;
|
|
277
|
+
}
|
|
278
|
+
else if (arg === "--repo") {
|
|
279
|
+
const value = argv[i + 1];
|
|
280
|
+
if (value === undefined) {
|
|
281
|
+
return {
|
|
282
|
+
issueNumber,
|
|
283
|
+
repo,
|
|
284
|
+
jsonMode,
|
|
285
|
+
strict,
|
|
286
|
+
passthroughError: "argument --repo: expected one argument",
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
repo = value;
|
|
290
|
+
i += 1;
|
|
291
|
+
}
|
|
292
|
+
else if (arg.startsWith("--repo=")) {
|
|
293
|
+
repo = arg.slice("--repo=".length);
|
|
294
|
+
}
|
|
295
|
+
else if (arg.startsWith("-")) {
|
|
296
|
+
return {
|
|
297
|
+
issueNumber,
|
|
298
|
+
repo,
|
|
299
|
+
jsonMode,
|
|
300
|
+
strict,
|
|
301
|
+
passthroughError: `unknown flag: ${arg}`,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
else if (issueNumber === null) {
|
|
305
|
+
const parsed = Number(arg);
|
|
306
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
307
|
+
return {
|
|
308
|
+
issueNumber,
|
|
309
|
+
repo,
|
|
310
|
+
jsonMode,
|
|
311
|
+
strict,
|
|
312
|
+
passthroughError: `invalid issue number: ${arg}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
issueNumber = parsed;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
return {
|
|
319
|
+
issueNumber,
|
|
320
|
+
repo,
|
|
321
|
+
jsonMode,
|
|
322
|
+
strict,
|
|
323
|
+
passthroughError: `unexpected positional argument: ${arg}`,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return { issueNumber, repo, jsonMode, strict };
|
|
328
|
+
}
|
|
329
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -16,6 +16,9 @@ export interface PrecutoverDetection {
|
|
|
16
16
|
reasons: string[];
|
|
17
17
|
}
|
|
18
18
|
export declare function detectPreCutover(projectRoot: string): PrecutoverDetection;
|
|
19
|
+
/** Last release that ships `scripts/migrate_vbrief.py` on the consumer deposit path (#2068). */
|
|
20
|
+
export declare const FROZEN_PRECUTOVER_MIGRATION_TAG = "v0.59.0";
|
|
21
|
+
export declare function frozenPreCutoverMigrationGuidance(): string;
|
|
19
22
|
export declare function renderPrecutoverLine(projectRoot: string): string;
|
|
20
23
|
export { isFullSpecState, isGreenfieldSpecExport };
|
|
21
24
|
//# sourceMappingURL=precutover.d.ts.map
|
|
@@ -98,13 +98,20 @@ export function detectPreCutover(projectRoot) {
|
|
|
98
98
|
}
|
|
99
99
|
return { preCutover: reasons.length > 0, reasons };
|
|
100
100
|
}
|
|
101
|
+
/** Last release that ships `scripts/migrate_vbrief.py` on the consumer deposit path (#2068). */
|
|
102
|
+
export const FROZEN_PRECUTOVER_MIGRATION_TAG = "v0.59.0";
|
|
103
|
+
export function frozenPreCutoverMigrationGuidance() {
|
|
104
|
+
return (`Current npm releases no longer ship in-product \`task migrate:vbrief\`. Pin framework ${FROZEN_PRECUTOVER_MIGRATION_TAG} ` +
|
|
105
|
+
`(frozen Go installer or git tag), install Python 3.11+ and uv, run \`task migrate:vbrief\` once from that payload, ` +
|
|
106
|
+
`then upgrade to current npm. See UPGRADING.md § Frozen pre-v0.20 document-model migration (#2068).`);
|
|
107
|
+
}
|
|
101
108
|
export function renderPrecutoverLine(projectRoot) {
|
|
102
109
|
const { preCutover, reasons } = detectPreCutover(projectRoot);
|
|
103
110
|
if (!preCutover) {
|
|
104
111
|
return "Pre-cutover: none -- project is on the current vBRIEF document model.";
|
|
105
112
|
}
|
|
106
113
|
const summary = reasons.join("; ").replace(/\r?\n/g, " ");
|
|
107
|
-
return `Pre-cutover: migration needed -- ${summary}.
|
|
114
|
+
return `Pre-cutover: migration needed -- ${summary}. ${frozenPreCutoverMigrationGuidance()}`;
|
|
108
115
|
}
|
|
109
116
|
// Re-export for classify callers (#2013).
|
|
110
117
|
export { isFullSpecState, isGreenfieldSpecExport };
|
|
@@ -16,6 +16,8 @@ export interface EvaluateOptions {
|
|
|
16
16
|
readonly gitConfigReader?: GitConfigReader;
|
|
17
17
|
readonly platform?: NodeJS.Platform;
|
|
18
18
|
}
|
|
19
|
+
/** Drop shell ``#`` comment lines before pattern scans (#2049 shipped-hook false positives). */
|
|
20
|
+
export declare function stripShellCommentLines(content: string): string;
|
|
19
21
|
/** Pure evaluator mirroring scripts/verify_hooks_installed.py::evaluate. */
|
|
20
22
|
export declare function evaluate(projectRoot: string, options?: EvaluateOptions): EvaluateResult;
|
|
21
23
|
//# sourceMappingURL=verify-hooks-installed.d.ts.map
|
|
@@ -69,15 +69,31 @@ function readHookContent(hookPath) {
|
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
/** Drop shell ``#`` comment lines before pattern scans (#2049 shipped-hook false positives). */
|
|
73
|
+
export function stripShellCommentLines(content) {
|
|
74
|
+
return content
|
|
75
|
+
.split(/\r?\n/)
|
|
76
|
+
.filter((line) => !/^\s*#/.test(line))
|
|
77
|
+
.join("\n");
|
|
78
|
+
}
|
|
79
|
+
function executableHookBody(content) {
|
|
80
|
+
return stripShellCommentLines(content);
|
|
81
|
+
}
|
|
72
82
|
function usesLegacyPythonDispatch(content) {
|
|
73
|
-
|
|
83
|
+
const body = executableHookBody(content);
|
|
84
|
+
return LEGACY_HOOK_PATTERNS.some((pattern) => pattern.test(body));
|
|
74
85
|
}
|
|
75
86
|
function hookInvokesDeftCli(content, requiredCommands) {
|
|
76
|
-
|
|
87
|
+
const body = executableHookBody(content);
|
|
88
|
+
if (!/\bdeft\b/.test(body))
|
|
77
89
|
return false;
|
|
78
90
|
if (usesLegacyPythonDispatch(content))
|
|
79
91
|
return false;
|
|
80
|
-
return requiredCommands.every((cmd) =>
|
|
92
|
+
return requiredCommands.every((cmd) => body.includes(cmd));
|
|
93
|
+
}
|
|
94
|
+
function prePushInvokesVerifyBranch(content) {
|
|
95
|
+
const body = executableHookBody(content);
|
|
96
|
+
return /\bdeft\s+verify:branch\b/.test(body);
|
|
81
97
|
}
|
|
82
98
|
function validateHookContent(hookName, content, requiredCommands) {
|
|
83
99
|
if (content === null) {
|
|
@@ -176,7 +192,7 @@ export function evaluate(projectRoot, options = {}) {
|
|
|
176
192
|
stream: "stderr",
|
|
177
193
|
};
|
|
178
194
|
}
|
|
179
|
-
if (prePushContent
|
|
195
|
+
if (prePushContent && prePushInvokesVerifyBranch(prePushContent)) {
|
|
180
196
|
return {
|
|
181
197
|
code: 1,
|
|
182
198
|
message: "❌ deft hooks wired but NON-FUNCTIONAL: pre-push must not invoke verify:branch (#1814).\n" +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.0",
|
|
4
4
|
"description": "TypeScript engine core for the Directive framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -194,6 +194,14 @@
|
|
|
194
194
|
"types": "./dist/migrate-preflight/index.d.ts",
|
|
195
195
|
"default": "./dist/migrate-preflight/index.js"
|
|
196
196
|
},
|
|
197
|
+
"./check-updates": {
|
|
198
|
+
"types": "./dist/check-updates/index.d.ts",
|
|
199
|
+
"default": "./dist/check-updates/index.js"
|
|
200
|
+
},
|
|
201
|
+
"./umbrella-current-shape": {
|
|
202
|
+
"types": "./dist/umbrella-current-shape/index.d.ts",
|
|
203
|
+
"default": "./dist/umbrella-current-shape/index.js"
|
|
204
|
+
},
|
|
197
205
|
"./install-upgrade": {
|
|
198
206
|
"types": "./dist/install-upgrade/index.d.ts",
|
|
199
207
|
"default": "./dist/install-upgrade/index.js"
|
|
@@ -221,8 +229,8 @@
|
|
|
221
229
|
"provenance": true
|
|
222
230
|
},
|
|
223
231
|
"dependencies": {
|
|
224
|
-
"@deftai/directive-content": "^0.
|
|
225
|
-
"@deftai/directive-types": "^0.
|
|
232
|
+
"@deftai/directive-content": "^0.62.0",
|
|
233
|
+
"@deftai/directive-types": "^0.62.0"
|
|
226
234
|
},
|
|
227
235
|
"scripts": {
|
|
228
236
|
"build": "tsc -b",
|