@deftai/directive-core 0.69.0 → 0.70.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.
Files changed (38) hide show
  1. package/dist/deposit/contain.d.ts +41 -0
  2. package/dist/deposit/contain.js +113 -0
  3. package/dist/deposit/copy-tree.js +8 -2
  4. package/dist/doctor/checks.d.ts +11 -0
  5. package/dist/doctor/checks.js +75 -2
  6. package/dist/doctor/main.js +54 -1
  7. package/dist/doctor/manifest.d.ts +20 -0
  8. package/dist/doctor/manifest.js +22 -0
  9. package/dist/doctor/types.d.ts +8 -0
  10. package/dist/init-deposit/init-deposit.js +6 -0
  11. package/dist/init-deposit/refresh.js +5 -0
  12. package/dist/intake/issue-ingest.d.ts +11 -0
  13. package/dist/intake/issue-ingest.js +86 -11
  14. package/dist/policy/plan-extensions.d.ts +31 -0
  15. package/dist/policy/plan-extensions.js +45 -0
  16. package/dist/policy/wip.d.ts +2 -2
  17. package/dist/policy/wip.js +2 -2
  18. package/dist/swarm/routing.js +10 -3
  19. package/dist/triage/bulk/index.d.ts +11 -2
  20. package/dist/triage/bulk/index.js +41 -4
  21. package/dist/triage/help/registry-data.d.ts +5 -5
  22. package/dist/triage/help/registry-data.js +18 -5
  23. package/dist/triage/scope/cli.d.ts +2 -1
  24. package/dist/triage/scope/cli.js +30 -6
  25. package/dist/triage/scope/mutations.d.ts +10 -0
  26. package/dist/triage/scope/mutations.js +22 -0
  27. package/dist/triage/welcome/constants.d.ts +1 -2
  28. package/dist/triage/welcome/constants.js +1 -2
  29. package/dist/triage/welcome/index.d.ts +1 -0
  30. package/dist/triage/welcome/index.js +1 -0
  31. package/dist/triage/welcome/onboard.d.ts +35 -0
  32. package/dist/triage/welcome/onboard.js +94 -0
  33. package/dist/umbrella-current-shape/index.d.ts +25 -1
  34. package/dist/umbrella-current-shape/index.js +84 -4
  35. package/dist/vbrief-build/project-definition-io.d.ts +6 -0
  36. package/dist/vbrief-build/project-definition-io.js +7 -2
  37. package/dist/vbrief-validate/precutover.js +7 -3
  38. package/package.json +3 -3
@@ -0,0 +1,35 @@
1
+ /** Default triage-scope preset applied when `--onboard` is run without `--preset`. */
2
+ export declare const DEFAULT_ONBOARD_PRESET = "small";
3
+ export interface OnboardOptions {
4
+ /** Triage-scope preset key; defaults to {@link DEFAULT_ONBOARD_PRESET}. */
5
+ readonly preset?: string | null;
6
+ /** Explicit WIP cap to persist; omitted keeps the current/default cap. */
7
+ readonly wipCap?: number | null;
8
+ readonly output?: (line: string) => void;
9
+ readonly writeHistory?: boolean;
10
+ readonly taskPrefix?: string | null;
11
+ readonly selfHealFn?: (projectRoot: string) => void;
12
+ readonly now?: Date;
13
+ }
14
+ export interface OnboardOutcome {
15
+ readonly exitCode: number;
16
+ readonly presetApplied: string | null;
17
+ readonly triageScopeChanged: boolean;
18
+ readonly wipCapApplied: number | null;
19
+ readonly wipCapChanged: boolean;
20
+ readonly reliefOffered: boolean;
21
+ }
22
+ /**
23
+ * Non-interactive onboarding for `deft triage:welcome --onboard` (#2295).
24
+ *
25
+ * The welcome nudges point every fresh consumer at `--onboard`; this wires that
26
+ * command to the already-tested core writers instead of the old "not
27
+ * implemented" stub. It writes the chosen triage-scope preset and (optionally)
28
+ * a WIP cap via {@link writeTriageScope} / {@link writeWipCap} -- which already
29
+ * target the canonical `x-directive/policy` key and migrate any legacy bare
30
+ * block -- previews WIP relief when at/over cap, then prints a completion
31
+ * summary and the next-step guidance. Flag-driven by design: agents/CI pass
32
+ * `--preset` / `--wip-cap`; sensible defaults apply otherwise.
33
+ */
34
+ export declare function runOnboardMode(projectRoot: string, options?: OnboardOptions): OnboardOutcome;
35
+ //# sourceMappingURL=onboard.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { maybeSelfHealCache } from "../../cache/fetch.js";
4
+ import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
5
+ import { DEFAULT_WIP_CAP, SUBSCRIPTION_PRESETS, TRIAGE_SKILL_PATH } from "./constants.js";
6
+ import { formatWelcomeCommand } from "./default-mode.js";
7
+ import { detectPriorState } from "./prior-state.js";
8
+ import { emitOneliner } from "./summary.js";
9
+ import { previewWipRelief, subscriptionPreset, writeTriageScope, writeWipCap } from "./writers.js";
10
+ /** Default triage-scope preset applied when `--onboard` is run without `--preset`. */
11
+ export const DEFAULT_ONBOARD_PRESET = "small";
12
+ function failure(exitCode) {
13
+ return {
14
+ exitCode,
15
+ presetApplied: null,
16
+ triageScopeChanged: false,
17
+ wipCapApplied: null,
18
+ wipCapChanged: false,
19
+ reliefOffered: false,
20
+ };
21
+ }
22
+ /**
23
+ * Non-interactive onboarding for `deft triage:welcome --onboard` (#2295).
24
+ *
25
+ * The welcome nudges point every fresh consumer at `--onboard`; this wires that
26
+ * command to the already-tested core writers instead of the old "not
27
+ * implemented" stub. It writes the chosen triage-scope preset and (optionally)
28
+ * a WIP cap via {@link writeTriageScope} / {@link writeWipCap} -- which already
29
+ * target the canonical `x-directive/policy` key and migrate any legacy bare
30
+ * block -- previews WIP relief when at/over cap, then prints a completion
31
+ * summary and the next-step guidance. Flag-driven by design: agents/CI pass
32
+ * `--preset` / `--wip-cap`; sensible defaults apply otherwise.
33
+ */
34
+ export function runOnboardMode(projectRoot, options = {}) {
35
+ const out = options.output ?? ((line) => process.stdout.write(`${line}\n`));
36
+ const preset = (options.preset ?? DEFAULT_ONBOARD_PRESET).trim() || DEFAULT_ONBOARD_PRESET;
37
+ if (!Object.hasOwn(SUBSCRIPTION_PRESETS, preset)) {
38
+ out(`[welcome] Unknown --preset '${preset}'. Choose one of: ${Object.keys(SUBSCRIPTION_PRESETS).join(", ")}.`);
39
+ return failure(2);
40
+ }
41
+ const wipCap = options.wipCap ?? null;
42
+ if (wipCap !== null && (!Number.isInteger(wipCap) || wipCap < 1)) {
43
+ out(`[welcome] --wip-cap must be a positive integer, got ${JSON.stringify(options.wipCap)}.`);
44
+ return failure(2);
45
+ }
46
+ const pdPath = resolveProjectDefinitionPath(projectRoot);
47
+ if (!existsSync(pdPath)) {
48
+ out(`[welcome] No project definition found at ${pdPath}. Run project setup first (deft-directive-setup) before onboarding triage.`);
49
+ return failure(2);
50
+ }
51
+ const heal = options.selfHealFn ??
52
+ ((root) => {
53
+ maybeSelfHealCache(resolve(root));
54
+ });
55
+ heal(projectRoot);
56
+ const rules = subscriptionPreset(preset);
57
+ const [triageScopeChanged] = writeTriageScope(projectRoot, rules, { presetLabel: preset });
58
+ let wipCapChanged = false;
59
+ if (wipCap !== null) {
60
+ [wipCapChanged] = writeWipCap(projectRoot, wipCap);
61
+ }
62
+ emitOneliner(projectRoot, {
63
+ writeHistory: options.writeHistory !== false,
64
+ now: options.now,
65
+ output: out,
66
+ applyD2Suppression: false,
67
+ });
68
+ const state = detectPriorState(projectRoot);
69
+ let reliefOffered = false;
70
+ if (state.wipCount >= state.wipCap) {
71
+ const relief = previewWipRelief(projectRoot);
72
+ if (relief.eligibleCount > 0) {
73
+ reliefOffered = true;
74
+ const demote = formatWelcomeCommand(["scope:demote", "--batch", "--older-than-days", String(relief.olderThanDays)], options.taskPrefix).replace(/\r?\n/g, " ");
75
+ const eligibleCount = String(relief.eligibleCount).replace(/\r?\n/g, " ");
76
+ out(`[welcome] WIP ${state.wipCount}/${state.wipCap} at/over cap -- ${eligibleCount} pending scope(s) ` +
77
+ `older than ${relief.olderThanDays}d are demote-eligible. Relieve with \`${demote}\`.`);
78
+ }
79
+ }
80
+ const capNote = wipCap !== null ? `, wipCap ${wipCap}` : ` (wipCap default ${DEFAULT_WIP_CAP})`;
81
+ out(`[welcome] Onboarding applied: triage scope preset '${preset}'${capNote}.`);
82
+ const bootstrap = formatWelcomeCommand(["triage:bootstrap"], options.taskPrefix);
83
+ const queue = formatWelcomeCommand(["triage:queue", "--limit=10"], options.taskPrefix);
84
+ out(`[welcome] Next: run \`${bootstrap}\` to populate the triage cache, then \`${queue}\` to pick work. See ${TRIAGE_SKILL_PATH}.`);
85
+ return {
86
+ exitCode: 0,
87
+ presetApplied: preset,
88
+ triageScopeChanged,
89
+ wipCapApplied: wipCap,
90
+ wipCapChanged,
91
+ reliefOffered,
92
+ };
93
+ }
94
+ //# sourceMappingURL=onboard.js.map
@@ -1,3 +1,13 @@
1
+ /**
2
+ * #2307: only comments authored by a repo maintainer may be treated as the
3
+ * authoritative current-shape state. GitHub's `author_association` on an issue
4
+ * comment is the trust signal; anything outside this set (CONTRIBUTOR, NONE,
5
+ * FIRST_TIME_CONTRIBUTOR, ...) is an untrusted third party and cannot forge a
6
+ * higher-pass current-shape comment.
7
+ */
8
+ export declare const MAINTAINER_ASSOCIATIONS: ReadonlySet<string>;
9
+ /** True when a comment's author_association marks it as maintainer-authored (#2307). */
10
+ export declare function isMaintainerAuthored(association: string): boolean;
1
11
  /** Matches `## Current shape (as of pass-N)` — same pattern as vbrief-reconcile/umbrellas.ts. */
2
12
  export declare const CURRENT_SHAPE_HEADER_RE: RegExp;
3
13
  export declare const SECTION_MARKERS: {
@@ -20,6 +30,8 @@ export interface IssueComment {
20
30
  readonly body: string;
21
31
  readonly htmlUrl: string;
22
32
  readonly updatedAt: string;
33
+ readonly authorLogin: string;
34
+ readonly authorAssociation: string;
23
35
  }
24
36
  export interface CurrentShapeComment extends IssueComment {
25
37
  readonly pass: number;
@@ -37,6 +49,8 @@ export interface CurrentShapeResult {
37
49
  readonly htmlUrl: string;
38
50
  readonly pass: number;
39
51
  readonly body: string;
52
+ readonly authorLogin: string;
53
+ readonly authorAssociation: string;
40
54
  readonly sections: SectionPresence;
41
55
  }
42
56
  export type ScmFetcher = (repo: string, issueNumber: number) => IssueComment[] | {
@@ -55,11 +69,20 @@ export interface RunCurrentShapeOptions {
55
69
  /** Merge `gh api --paginate` concatenated JSON array pages into comment rows. */
56
70
  export declare function parseCommentsFromGhStdout(stdout: string): IssueComment[];
57
71
  export declare function extractPassFromBody(body: string): number | null;
58
- /** Pick the canonical comment — highest pass-N; tie-break by comment id (latest). */
72
+ /**
73
+ * Pick the canonical comment — highest pass-N; tie-break by comment id (latest).
74
+ *
75
+ * #2307: only MAINTAINER-authored comments (author_association in
76
+ * {OWNER, MEMBER, COLLABORATOR}) are eligible. A non-maintainer higher-pass
77
+ * comment is ignored, which defeats the forged-higher-pass primitive: an
78
+ * attacker cannot inject authoritative state by simply commenting with a bigger
79
+ * pass number.
80
+ */
59
81
  export declare function selectCurrentShapeComment(comments: readonly IssueComment[]): CurrentShapeComment | null;
60
82
  export declare function detectSections(body: string): SectionPresence;
61
83
  export declare function sectionsRecord(presence: SectionPresence): Record<string, boolean>;
62
84
  export declare const NO_CURRENT_SHAPE_MESSAGE: string;
85
+ export declare const NON_MAINTAINER_CURRENT_SHAPE_MESSAGE: string;
63
86
  export declare function fetchCurrentShape(options: {
64
87
  repo: string;
65
88
  issueNumber: number;
@@ -75,6 +98,7 @@ export declare function fetchCurrentShape(options: {
75
98
  export declare function emitCurrentShape(result: CurrentShapeResult, options: {
76
99
  jsonMode: boolean;
77
100
  writeOut: (text: string) => void;
101
+ writeErr?: (text: string) => void;
78
102
  }): number;
79
103
  export declare function runCurrentShape(options: RunCurrentShapeOptions): number;
80
104
  export declare function parseCurrentShapeArgv(argv: readonly string[]): {
@@ -1,7 +1,24 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { scan } from "../cache/scanner.js";
2
3
  import { resolveBinary } from "../scm/binary.js";
3
4
  import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
4
5
  import { resolveRepo } from "../triage/queue/repo.js";
6
+ /**
7
+ * #2307: only comments authored by a repo maintainer may be treated as the
8
+ * authoritative current-shape state. GitHub's `author_association` on an issue
9
+ * comment is the trust signal; anything outside this set (CONTRIBUTOR, NONE,
10
+ * FIRST_TIME_CONTRIBUTOR, ...) is an untrusted third party and cannot forge a
11
+ * higher-pass current-shape comment.
12
+ */
13
+ export const MAINTAINER_ASSOCIATIONS = new Set([
14
+ "OWNER",
15
+ "MEMBER",
16
+ "COLLABORATOR",
17
+ ]);
18
+ /** True when a comment's author_association marks it as maintainer-authored (#2307). */
19
+ export function isMaintainerAuthored(association) {
20
+ return MAINTAINER_ASSOCIATIONS.has(association.toUpperCase());
21
+ }
5
22
  /** Matches `## Current shape (as of pass-N)` — same pattern as vbrief-reconcile/umbrellas.ts. */
6
23
  export const CURRENT_SHAPE_HEADER_RE = /^## Current shape \(as of pass-(\d+)\)/m;
7
24
  export const SECTION_MARKERS = {
@@ -35,11 +52,16 @@ function mapCommentEntry(entry) {
35
52
  if (typeof rec.id !== "number" || typeof rec.body !== "string") {
36
53
  return null;
37
54
  }
55
+ const user = typeof rec.user === "object" && rec.user !== null
56
+ ? rec.user
57
+ : null;
38
58
  return {
39
59
  id: rec.id,
40
60
  body: rec.body,
41
61
  htmlUrl: typeof rec.html_url === "string" ? rec.html_url : "",
42
62
  updatedAt: typeof rec.updated_at === "string" ? rec.updated_at : "",
63
+ authorLogin: user !== null && typeof user.login === "string" ? user.login : "",
64
+ authorAssociation: typeof rec.author_association === "string" ? rec.author_association : "",
43
65
  };
44
66
  }
45
67
  /** Merge `gh api --paginate` concatenated JSON array pages into comment rows. */
@@ -135,10 +157,21 @@ export function extractPassFromBody(body) {
135
157
  const pass = Number(match[1]);
136
158
  return Number.isFinite(pass) ? pass : null;
137
159
  }
138
- /** Pick the canonical comment — highest pass-N; tie-break by comment id (latest). */
160
+ /**
161
+ * Pick the canonical comment — highest pass-N; tie-break by comment id (latest).
162
+ *
163
+ * #2307: only MAINTAINER-authored comments (author_association in
164
+ * {OWNER, MEMBER, COLLABORATOR}) are eligible. A non-maintainer higher-pass
165
+ * comment is ignored, which defeats the forged-higher-pass primitive: an
166
+ * attacker cannot inject authoritative state by simply commenting with a bigger
167
+ * pass number.
168
+ */
139
169
  export function selectCurrentShapeComment(comments) {
140
170
  let best = null;
141
171
  for (const comment of comments) {
172
+ if (!isMaintainerAuthored(comment.authorAssociation)) {
173
+ continue;
174
+ }
142
175
  const pass = extractPassFromBody(comment.body);
143
176
  if (pass === null) {
144
177
  continue;
@@ -188,6 +221,16 @@ export function sectionsRecord(presence) {
188
221
  export const NO_CURRENT_SHAPE_MESSAGE = "No ## Current shape (as of pass-N) comment found on this issue. " +
189
222
  "Create one per AGENTS.md ## Umbrella current-shape convention (#1152) — " +
190
223
  "do not fall back to the issue body (stale by design).";
224
+ // #2307 (Greptile review): a structurally valid current-shape comment authored
225
+ // by a non-maintainer is filtered out for provenance and would otherwise be
226
+ // reported with the generic "not found" message above -- indistinguishable from
227
+ // genuine absence, which makes --strict especially confusing to debug. This
228
+ // message names the provenance filter explicitly so the caller knows the
229
+ // comment exists but was ignored, not that it is missing.
230
+ export const NON_MAINTAINER_CURRENT_SHAPE_MESSAGE = "A ## Current shape (as of pass-N) comment exists but was authored by a " +
231
+ "non-maintainer (author_association not in OWNER/MEMBER/COLLABORATOR) and is " +
232
+ "ignored per AGENTS.md ## Umbrella current-shape convention (#1152 / #2307). " +
233
+ "A maintainer must (re-)post the current-shape comment for it to be authoritative.";
191
234
  export function fetchCurrentShape(options) {
192
235
  const fetcher = options.fetchComments ?? defaultFetchComments;
193
236
  const fetched = fetcher(options.repo, options.issueNumber);
@@ -196,7 +239,15 @@ export function fetchCurrentShape(options) {
196
239
  }
197
240
  const selected = selectCurrentShapeComment(fetched);
198
241
  if (selected === null) {
199
- return { ok: false, error: NO_CURRENT_SHAPE_MESSAGE, kind: "not-found" };
242
+ // Distinguish provenance-filtered absence from genuine absence (#2307).
243
+ const hadNonMaintainerShape = fetched.some((c) => extractPassFromBody(c.body) !== null && !isMaintainerAuthored(c.authorAssociation));
244
+ return {
245
+ ok: false,
246
+ error: hadNonMaintainerShape
247
+ ? NON_MAINTAINER_CURRENT_SHAPE_MESSAGE
248
+ : NO_CURRENT_SHAPE_MESSAGE,
249
+ kind: "not-found",
250
+ };
200
251
  }
201
252
  const sections = detectSections(selected.body);
202
253
  return {
@@ -208,11 +259,36 @@ export function fetchCurrentShape(options) {
208
259
  htmlUrl: selected.htmlUrl,
209
260
  pass: selected.pass,
210
261
  body: selected.body,
262
+ authorLogin: selected.authorLogin,
263
+ authorAssociation: selected.authorAssociation,
211
264
  sections,
212
265
  },
213
266
  };
214
267
  }
215
268
  export function emitCurrentShape(result, options) {
269
+ // #2307: the selected comment body is still attacker-influencable text (a
270
+ // maintainer can quote/paste untrusted content). Run it through the same
271
+ // quarantine scanner used for cache content and emit the fenced transform so
272
+ // injection-shaped sections are quarantined, never rendered as authoritative
273
+ // instructions.
274
+ const scanned = scan(result.body);
275
+ // #2307 fail-closed (Greptile review): a scanner hard-fail (e.g. a credential
276
+ // pattern) is only FLAGGED by scan() -- detectCredentials sets passed=false
277
+ // but does NOT redact the secret from transformed_content. Emitting the
278
+ // transform regardless would forward the raw credential to stdout / CI logs /
279
+ // JSON consumers. Mirror buildIssueVbrief (#2306, issue:ingest), which throws
280
+ // ScannerHardFailError and writes nothing: refuse to emit and return non-zero.
281
+ if (!scanned.passed) {
282
+ const details = scanned.flags
283
+ .filter((f) => f.severity === "hard-fail")
284
+ .map((f) => f.detail)
285
+ .join("; ");
286
+ const writeErr = options.writeErr ?? ((text) => process.stderr.write(text));
287
+ writeErr(`umbrella:current-shape: refused issue #${result.issueNumber}: quarantine scanner hard-fail` +
288
+ (details.length > 0 ? ` (${details})` : "") +
289
+ " -- nothing written.\n");
290
+ return 1;
291
+ }
216
292
  if (options.jsonMode) {
217
293
  const payload = {
218
294
  issueNumber: result.issueNumber,
@@ -220,7 +296,10 @@ export function emitCurrentShape(result, options) {
220
296
  commentId: result.commentId,
221
297
  htmlUrl: result.htmlUrl,
222
298
  pass: result.pass,
223
- body: result.body,
299
+ body: scanned.transformed_content,
300
+ authorLogin: result.authorLogin,
301
+ authorAssociation: result.authorAssociation,
302
+ scannerPassed: scanned.passed,
224
303
  sections: sectionsRecord(result.sections),
225
304
  missingSections: result.sections.missing,
226
305
  missingOptionalSections: result.sections.optionalMissing,
@@ -228,7 +307,7 @@ export function emitCurrentShape(result, options) {
228
307
  options.writeOut(`${JSON.stringify(payload)}\n`);
229
308
  }
230
309
  else {
231
- options.writeOut(`${result.body}\n`);
310
+ options.writeOut(`${scanned.transformed_content}\n`);
232
311
  }
233
312
  return 0;
234
313
  }
@@ -260,6 +339,7 @@ export function runCurrentShape(options) {
260
339
  return emitCurrentShape(fetched.result, {
261
340
  jsonMode: options.jsonMode ?? false,
262
341
  writeOut,
342
+ writeErr,
263
343
  });
264
344
  }
265
345
  export function parseCurrentShapeArgv(argv) {
@@ -1,4 +1,10 @@
1
1
  import type { JsonObject } from "./types.js";
2
+ /**
3
+ * Absolute path to the PROJECT-DEFINITION artifact. Layout-aware (#2302):
4
+ * resolves `xbrief/PROJECT-DEFINITION.xbrief.json` on a migrated tree, else the
5
+ * legacy `vbrief/PROJECT-DEFINITION.vbrief.json`, so loader not-found messages
6
+ * name the path that actually applies to the project's layout.
7
+ */
2
8
  export declare function projectDefinitionPath(projectRoot: string): string;
3
9
  export interface MutationLockDeps {
4
10
  readonly sleepMs?: (ms: number) => void;
@@ -2,12 +2,17 @@ import { randomBytes } from "node:crypto";
2
2
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
5
- import { PROJECT_DEFINITION_REL_PATH } from "./constants.js";
6
5
  import { pythonJsonPretty } from "./json.js";
7
6
  import { ProjectDefinitionIOError } from "./types.js";
8
7
  const mutationThreadLock = { held: false };
8
+ /**
9
+ * Absolute path to the PROJECT-DEFINITION artifact. Layout-aware (#2302):
10
+ * resolves `xbrief/PROJECT-DEFINITION.xbrief.json` on a migrated tree, else the
11
+ * legacy `vbrief/PROJECT-DEFINITION.vbrief.json`, so loader not-found messages
12
+ * name the path that actually applies to the project's layout.
13
+ */
9
14
  export function projectDefinitionPath(projectRoot) {
10
- return join(projectRoot, PROJECT_DEFINITION_REL_PATH);
15
+ return resolveProjectDefinitionPath(resolve(projectRoot));
11
16
  }
12
17
  function defaultSleep(ms) {
13
18
  const end = Date.now() + ms;
@@ -103,9 +103,13 @@ export function detectPreCutover(projectRoot) {
103
103
  /** Last release that ships `scripts/migrate_vbrief.py` on the consumer deposit path (#2068). */
104
104
  export const FROZEN_PRECUTOVER_MIGRATION_TAG = "v0.59.0";
105
105
  export function frozenPreCutoverMigrationGuidance() {
106
- return (`Current npm releases no longer ship in-product \`task migrate:vbrief\`. Pin framework ${FROZEN_PRECUTOVER_MIGRATION_TAG} ` +
107
- `(frozen Go installer or git tag), install Python 3.11+ and uv, run \`task migrate:vbrief\` once from that payload, ` +
108
- `then upgrade to current npm. See UPGRADING.md § Frozen pre-v0.20 document-model migration (#2068).`);
106
+ return (`Current npm releases no longer ship in-product \`task migrate:vbrief\`. Best-effort path (anchored on the ` +
107
+ `${FROZEN_PRECUTOVER_MIGRATION_TAG} git tag, from which GitHub serves a source tarball on demand): pin framework ` +
108
+ `${FROZEN_PRECUTOVER_MIGRATION_TAG}, install Python 3.11+ and uv, run \`task migrate:vbrief\` once from that payload, ` +
109
+ `then upgrade to current npm. This is a two-hop chain: pre-v0.20 flat model -> vBRIEF v0.6 (on ${FROZEN_PRECUTOVER_MIGRATION_TAG}) ` +
110
+ `-> xBRIEF via \`deft migrate:xbrief\` on current npm. If the ${FROZEN_PRECUTOVER_MIGRATION_TAG} payload is unavailable, ` +
111
+ `fall back to a manual fresh start: \`directive init\` a new project on current npm and hand-port your spec content. ` +
112
+ `See UPGRADING.md § Frozen pre-v0.20 document-model migration (#2068).`);
109
113
  }
110
114
  export function renderPrecutoverLine(projectRoot) {
111
115
  const { preCutover, reasons } = detectPreCutover(projectRoot);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.69.0",
3
+ "version": "0.70.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -249,8 +249,8 @@
249
249
  "provenance": true
250
250
  },
251
251
  "dependencies": {
252
- "@deftai/directive-content": "^0.69.0",
253
- "@deftai/directive-types": "^0.69.0",
252
+ "@deftai/directive-content": "^0.70.0",
253
+ "@deftai/directive-types": "^0.70.0",
254
254
  "archiver": "^8.0.0"
255
255
  },
256
256
  "scripts": {