@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.
@@ -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(", ")}). Run \`task migrate:vbrief\` first -- it migrates legacy artifacts and creates the lifecycle folder structure. This command only records the framework version.\n`);
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("If legacy SPECIFICATION.md or PROJECT.md content remains, run `task migrate:vbrief` to complete the upgrade.\n");
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, which?: (cmd: string) => string | null): {
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 { defaultWhich } from "../scm/binary.js";
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} -- migrator will create it on first run; this is expected for greenfield projects.`,
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 migrator + schemas present; project vbrief/ at ${projectVbrief}.`,
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. The migrator will refuse to run without --force; preview with `task migrate:vbrief -- --dry-run` first.",
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. Migrator's dirty-tree guard will still fire if applicable.",
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: "PASS",
95
- message: `Legacy root artifact(s) detected: ${legacy.join(", ")}.`,
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: "FAIL",
121
- message: "Current generated SPECIFICATION.md detected (source: vbrief/specification.vbrief.json); `task migrate:vbrief` is not needed.",
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: "PASS",
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: "WARN",
139
- message: "No legacy root SPECIFICATION.md/PROJECT.md artifacts detected. Migration may have nothing to do.",
119
+ status: "PASS",
120
+ message: "No pre-v0.20 document-model artifacts detected.",
140
121
  };
141
122
  }
142
- export function evaluate(deftRoot, projectRoot, which = defaultWhich) {
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 -- resolve the FAIL line(s) above before running `task migrate:vbrief`.\n");
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 -- environment ready for `task migrate:vbrief`.\n");
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}. Run \`deft migrate:vbrief\` to migrate.`;
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
- return LEGACY_HOOK_PATTERNS.some((pattern) => pattern.test(content));
83
+ const body = executableHookBody(content);
84
+ return LEGACY_HOOK_PATTERNS.some((pattern) => pattern.test(body));
74
85
  }
75
86
  function hookInvokesDeftCli(content, requiredCommands) {
76
- if (!/\bdeft\b/.test(content))
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) => content.includes(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?.includes("verify:branch")) {
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.61.1",
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.61.1",
225
- "@deftai/directive-types": "^0.61.1"
232
+ "@deftai/directive-content": "^0.62.0",
233
+ "@deftai/directive-types": "^0.62.0"
226
234
  },
227
235
  "scripts": {
228
236
  "build": "tsc -b",