@deftai/directive-core 0.72.0 → 0.73.1

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 (103) hide show
  1. package/dist/branch/evaluate.js +2 -1
  2. package/dist/category-b-namespace/index.js +16 -2
  3. package/dist/codebase/default-extractor.js +9 -1
  4. package/dist/codebase/map.js +1 -1
  5. package/dist/deposit/copy-tree.js +31 -7
  6. package/dist/doctor/checks.d.ts +9 -0
  7. package/dist/doctor/checks.js +85 -5
  8. package/dist/doctor/main.js +2 -1
  9. package/dist/eval/health.js +7 -1
  10. package/dist/eval/readback.js +7 -1
  11. package/dist/init-deposit/gitignore.js +6 -0
  12. package/dist/init-deposit/hygiene.d.ts +30 -0
  13. package/dist/init-deposit/hygiene.js +60 -1
  14. package/dist/init-deposit/refresh.d.ts +16 -2
  15. package/dist/init-deposit/refresh.js +162 -52
  16. package/dist/init-deposit/scaffold.d.ts +11 -1
  17. package/dist/init-deposit/scaffold.js +9 -8
  18. package/dist/integration-e2e/helpers.js +7 -7
  19. package/dist/layout/index.d.ts +1 -1
  20. package/dist/layout/index.js +1 -1
  21. package/dist/layout/resolve.d.ts +47 -31
  22. package/dist/layout/resolve.js +93 -36
  23. package/dist/lifecycle/lifecycle-hygiene.d.ts +1 -1
  24. package/dist/lifecycle/lifecycle-hygiene.js +1 -1
  25. package/dist/migrate-preflight/index.js +10 -10
  26. package/dist/policy/disclosure.d.ts +1 -0
  27. package/dist/policy/disclosure.js +1 -0
  28. package/dist/policy/index.d.ts +1 -0
  29. package/dist/policy/index.js +1 -0
  30. package/dist/policy/policy-invocation.d.ts +5 -0
  31. package/dist/policy/policy-invocation.js +9 -0
  32. package/dist/policy/resolve.d.ts +2 -2
  33. package/dist/policy/resolve.js +15 -5
  34. package/dist/policy/value-feedback.js +6 -3
  35. package/dist/policy/wip.d.ts +1 -1
  36. package/dist/policy/wip.js +15 -3
  37. package/dist/preflight-cache/evaluate.js +1 -1
  38. package/dist/release/native-steps.js +14 -2
  39. package/dist/render/export-spec.js +2 -2
  40. package/dist/render/project-render.js +40 -9
  41. package/dist/scope/decompose.js +12 -2
  42. package/dist/session/resume-conditions.js +2 -2
  43. package/dist/session/ritual-sentinel.d.ts +1 -1
  44. package/dist/session/ritual-sentinel.js +6 -4
  45. package/dist/slice/constants.d.ts +1 -1
  46. package/dist/slice/constants.js +1 -1
  47. package/dist/slice/project-context.js +1 -1
  48. package/dist/spec-authority/resolver.js +9 -2
  49. package/dist/swarm/complete-cohort.js +14 -1
  50. package/dist/swarm/constants.d.ts +1 -1
  51. package/dist/swarm/constants.js +2 -1
  52. package/dist/swarm/readiness.js +33 -3
  53. package/dist/swarm/subagent-backend.js +2 -1
  54. package/dist/task-surface/index.js +3 -3
  55. package/dist/triage/actions/candidates-log.d.ts +1 -1
  56. package/dist/triage/bootstrap/gitignore.js +22 -10
  57. package/dist/triage/bootstrap/index.d.ts +1 -1
  58. package/dist/triage/bootstrap/index.js +9 -3
  59. package/dist/triage/cache-path.d.ts +1 -1
  60. package/dist/triage/cache-path.js +32 -6
  61. package/dist/triage/classify/index.d.ts +1 -1
  62. package/dist/triage/classify/index.js +11 -4
  63. package/dist/triage/help/registry-data.d.ts +2 -2
  64. package/dist/triage/help/registry-data.js +2 -2
  65. package/dist/triage/queue/constants.d.ts +4 -4
  66. package/dist/triage/queue/constants.js +4 -4
  67. package/dist/triage/queue/scope-walk.js +7 -1
  68. package/dist/triage/reconcile/reconcile.js +23 -3
  69. package/dist/triage/reconcile/types.d.ts +1 -1
  70. package/dist/triage/reconcile/types.js +1 -1
  71. package/dist/triage/refresh/refresh.js +9 -2
  72. package/dist/triage/scope/constants.d.ts +2 -2
  73. package/dist/triage/scope/constants.js +2 -2
  74. package/dist/triage/scope/renderers.js +7 -1
  75. package/dist/triage/smoketest/index.js +9 -6
  76. package/dist/triage/subscribe/index.d.ts +2 -2
  77. package/dist/triage/subscribe/index.js +2 -2
  78. package/dist/triage/summary/index.d.ts +2 -2
  79. package/dist/triage/summary/index.js +1 -1
  80. package/dist/triage/summary/reconcilable.d.ts +1 -1
  81. package/dist/triage/welcome/constants.d.ts +2 -2
  82. package/dist/triage/welcome/constants.js +2 -2
  83. package/dist/triage/welcome/onboard.js +8 -1
  84. package/dist/triage/welcome/prior-state.js +14 -2
  85. package/dist/triage/welcome/summary.js +14 -2
  86. package/dist/triage/welcome/writers.js +7 -1
  87. package/dist/ts-check-lane/run-lane.d.ts +4 -0
  88. package/dist/ts-check-lane/run-lane.js +20 -6
  89. package/dist/validate-content/validate-strategy-output.js +11 -1
  90. package/dist/value/feedback-file.js +3 -2
  91. package/dist/value/readback.js +3 -2
  92. package/dist/vbrief-build/parity-scenarios.js +3 -3
  93. package/dist/vbrief-reconcile/labels.js +16 -1
  94. package/dist/vbrief-reconcile/parity-scenarios.js +10 -10
  95. package/dist/vbrief-reconcile/umbrellas.js +16 -1
  96. package/dist/vbrief-validate/main.js +13 -1
  97. package/dist/vbrief-validate/precutover.js +33 -5
  98. package/dist/vbrief-validate/staleness.d.ts +1 -1
  99. package/dist/vbrief-validate/staleness.js +5 -2
  100. package/dist/verify-source/code-structure-validate.js +7 -1
  101. package/dist/xbrief-migrate/signpost.d.ts +4 -4
  102. package/dist/xbrief-migrate/signpost.js +11 -15
  103. package/package.json +3 -3
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * Refs #1942, #1430, #1671.
9
9
  */
10
+ import { execFileSync } from "node:child_process";
10
11
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
11
12
  import { platform as osPlatform } from "node:os";
12
13
  import { join, resolve } from "node:path";
@@ -20,8 +21,8 @@ import { resolveLifecycleRoot } from "../layout/resolve.js";
20
21
  import { DEV_FALLBACK } from "../platform/constants.js";
21
22
  import { checkLocalEngineIntegrity, classify, ENGINE_PACKAGE, plan, renderGlobalInstall, resolveEngine, } from "../resolution/index.js";
22
23
  import { gitPorcelain } from "../story-ready/git.js";
23
- import { ensureInitGitignoreLines } from "./gitignore.js";
24
- import { depositStagePaths, isInstallerManagedPath, printCommitGuidance } from "./hygiene.js";
24
+ import { ensureInitGitignoreLines, isDepositTrackedInGit } from "./gitignore.js";
25
+ import { depositStagePaths, isInstallerManagedPath, printCommitGuidance, pruneStrayDepositPaths, } from "./hygiene.js";
25
26
  import { parseInitArgv } from "./init-deposit.js";
26
27
  import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
27
28
  import { printMigrateNudgeIfNeeded } from "./migrate.js";
@@ -155,14 +156,20 @@ function syncBareVersionMarker(projectDir, version) {
155
156
  const normalized = normalizeVersion(version);
156
157
  if (!normalized || normalized === DEV_FALLBACK)
157
158
  return;
158
- const vbriefDir = resolveLifecycleRoot(projectDir);
159
+ let vbriefDir = null;
160
+ try {
161
+ vbriefDir = resolveLifecycleRoot(projectDir);
162
+ }
163
+ catch {
164
+ // No xbrief/ layout present — write the root-level derivative instead.
165
+ }
159
166
  let targetDir = projectDir;
160
167
  try {
161
- if (statSync(vbriefDir).isDirectory())
168
+ if (vbriefDir !== null && statSync(vbriefDir).isDirectory())
162
169
  targetDir = vbriefDir;
163
170
  }
164
171
  catch {
165
- // vbrief/ absent — write the root-level derivative instead
172
+ // xbrief/ absent — write the root-level derivative instead
166
173
  }
167
174
  try {
168
175
  mkdirSync(targetDir, { recursive: true });
@@ -212,11 +219,12 @@ function unquoteGitPath(path) {
212
219
  }
213
220
  return path;
214
221
  }
215
- function porcelainStatusPaths(porcelain) {
216
- const paths = [];
222
+ function porcelainStatusEntries(porcelain) {
223
+ const entries = [];
217
224
  for (const line of porcelain.split("\n")) {
218
225
  if (line.length < 4)
219
226
  continue;
227
+ const status = line.slice(0, 2);
220
228
  let rest = line.slice(3);
221
229
  const arrow = rest.indexOf(" -> ");
222
230
  if (arrow >= 0)
@@ -224,43 +232,102 @@ function porcelainStatusPaths(porcelain) {
224
232
  const trimmed = unquoteGitPath(rest.trim());
225
233
  if (!trimmed)
226
234
  continue;
227
- paths.push(trimmed.replace(/\\/g, "/"));
235
+ entries.push({ status, path: trimmed.replace(/\\/g, "/") });
228
236
  }
229
- return paths;
237
+ return entries;
230
238
  }
231
- function classifyChangedPaths(changed) {
239
+ function classifyChangedEntries(changed) {
232
240
  const core = [];
233
241
  const installerManaged = [];
234
- for (const path of changed) {
242
+ for (const entry of changed) {
243
+ const { path } = entry;
235
244
  if (!path)
236
245
  continue;
237
246
  if (path.startsWith(".deft/core/") || path === ".deft/core") {
238
- core.push(path);
247
+ core.push(entry);
239
248
  }
240
249
  else if (isInstallerManagedPath(path)) {
241
- installerManaged.push(path);
250
+ installerManaged.push(entry);
242
251
  }
243
252
  }
244
253
  return { core, installerManaged };
245
254
  }
255
+ function isCrlfNoiseCandidate(status) {
256
+ return status.includes("M") && /^[ M]+$/.test(status);
257
+ }
258
+ function normalizeGitNameList(output) {
259
+ return output
260
+ .split("\n")
261
+ .map((line) => line.trim().replace(/\\/g, "/"))
262
+ .filter(Boolean);
263
+ }
264
+ function gitSemanticDiffNames(projectRoot, paths) {
265
+ if (paths.length === 0)
266
+ return [];
267
+ try {
268
+ const args = ["diff", "--ignore-cr-at-eol", "--name-only", "--", ...paths];
269
+ const worktree = execFileSync("git", args, {
270
+ cwd: projectRoot,
271
+ encoding: "utf8",
272
+ stdio: ["ignore", "pipe", "pipe"],
273
+ });
274
+ const cached = execFileSync("git", ["diff", "--cached", "--ignore-cr-at-eol", "--name-only", "--", ...paths], {
275
+ cwd: projectRoot,
276
+ encoding: "utf8",
277
+ stdio: ["ignore", "pipe", "pipe"],
278
+ });
279
+ return [...new Set([...normalizeGitNameList(worktree), ...normalizeGitNameList(cached)])];
280
+ }
281
+ catch {
282
+ return null;
283
+ }
284
+ }
246
285
  /** Framework-managed uncommitted paths after refresh (#1671). */
247
- export function frameworkRefreshSideEffects(projectDir, readPorcelain = gitPorcelain) {
286
+ export function frameworkRefreshSideEffects(projectDir, options) {
287
+ const readPorcelain = options?.readPorcelain ?? gitPorcelain;
288
+ const readSemanticDiffNames = options?.readSemanticDiffNames ?? gitSemanticDiffNames;
248
289
  const porcelain = readPorcelain(projectDir);
249
290
  if (porcelain === null)
250
- return [];
251
- const changed = porcelainStatusPaths(porcelain);
252
- const { core, installerManaged } = classifyChangedPaths(changed);
253
- const files = [...core, ...installerManaged].sort();
254
- return files.length > 0 ? files : [];
291
+ return { files: [], crlfOnlyCoreFiles: [] };
292
+ const changed = porcelainStatusEntries(porcelain);
293
+ const { core, installerManaged } = classifyChangedEntries(changed);
294
+ const semanticCoreNames = core.length > 0
295
+ ? readSemanticDiffNames(projectDir, core.map((entry) => entry.path))
296
+ : [];
297
+ const semanticCoreSet = semanticCoreNames === null ? null : new Set(semanticCoreNames);
298
+ const coreFiles = [];
299
+ const crlfOnlyCoreFiles = [];
300
+ for (const entry of core) {
301
+ if (semanticCoreSet !== null &&
302
+ isCrlfNoiseCandidate(entry.status) &&
303
+ !semanticCoreSet.has(entry.path)) {
304
+ crlfOnlyCoreFiles.push(entry.path);
305
+ continue;
306
+ }
307
+ coreFiles.push(entry.path);
308
+ }
309
+ const files = [...coreFiles, ...installerManaged.map((entry) => entry.path)].sort();
310
+ return { files, crlfOnlyCoreFiles: crlfOnlyCoreFiles.sort() };
255
311
  }
256
- export function printRefreshSideEffects(io, files) {
257
- if (files.length === 0)
312
+ export function printRefreshSideEffects(io, effects) {
313
+ if (effects.crlfOnlyCoreFiles.length > 0) {
314
+ io.printf("\nWindows line-ending note (#2118): suppressed .deft/core CRLF/LF-only noise; " +
315
+ "ensure .gitattributes contains `.deft/core/** text eol=lf`.\n");
316
+ }
317
+ if (effects.files.length === 0)
258
318
  return;
319
+ if (effects.payloadSwapped === false) {
320
+ io.printf("\nFramework-managed files still have semantic uncommitted changes:\n");
321
+ for (const file of effects.files) {
322
+ io.printf(` ${file}\n`);
323
+ }
324
+ return;
325
+ }
259
326
  io.printf("\nAGENTS.md refresh side effects (#1671): the refresh and framework payload swap\n");
260
327
  io.printf("left these framework files with uncommitted changes -- they belong in the\n");
261
328
  io.printf("framework deposit commit (the installer stages them before printing the\n");
262
329
  io.printf("`git add` list below, so there are no post-stage stragglers):\n");
263
- for (const file of files) {
330
+ for (const file of effects.files) {
264
331
  io.printf(` ${file}\n`);
265
332
  }
266
333
  }
@@ -301,7 +368,8 @@ export function buildUpdateSummaryJson(result, options, updateState) {
301
368
  user_config_dir: "",
302
369
  skills_created: false,
303
370
  payload_layout: "vendored",
304
- strategy: "file-swap",
371
+ strategy: result.strategy,
372
+ already_current: result.alreadyCurrent,
305
373
  dirty_tree: false,
306
374
  dirty_files: [],
307
375
  staged_paths: result.stagedPaths,
@@ -313,9 +381,12 @@ export function buildUpdateSummaryJson(result, options, updateState) {
313
381
  };
314
382
  }
315
383
  export function printUpdateComplete(result, io, updateState) {
316
- io.printf("\n✓ Deft framework payload refreshed.\n\n");
384
+ io.printf(result.alreadyCurrent
385
+ ? "\nOK Deft framework payload already current.\n\n"
386
+ : "\nOK Deft framework payload refreshed.\n\n");
317
387
  io.printf(` Location : ${result.deftDir}\n`);
318
388
  io.printf(` Content : v${normalizeVersion(result.contentVersion)}\n`);
389
+ io.printf(` Strategy : ${result.strategy}\n`);
319
390
  if (updateState) {
320
391
  io.printf(` State : ${updateState}\n`);
321
392
  }
@@ -350,29 +421,49 @@ export async function runRefreshDeposit(args, io, seams = {}) {
350
421
  const engineVersion = readEngine();
351
422
  const contentVersion = readContentPackageVersion(contentRoot, readPackageVersion);
352
423
  const versionSkewNotice = buildVersionSkewNotice(engineVersion, contentVersion, previousDepositVersion);
353
- await copyContent(contentRoot, deftDir);
354
- await prunePythonArtifactsFromDeposit(deftDir, projectDir, io);
355
- const nowIso = seams.nowIso ?? (() => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"));
356
- const manifestFields = {
357
- ref: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
358
- sha: "content-package",
359
- tag: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
360
- installRoot: CANONICAL_INSTALL_ROOT,
361
- fetchedAt: nowIso(),
362
- fetchedBy: "directive-update",
363
- ...(previousManagedBy ? { managedBy: previousManagedBy } : {}),
364
- };
365
- const writtenManifestPath = writeInstallManifest(projectDir, deftDir, manifestFields);
366
- // #2064: retire a stale legacy .deft/VERSION now that the canonical
367
- // .deft/core/VERSION has been rewritten (folded in from install-upgrade so no
368
- // manifest behavior is lost by the redirect). Best-effort; never fatal.
369
- migrateLegacyInstallManifest(projectDir, writtenManifestPath);
370
- // #2055: regenerate the bare .deft-version derivative so it agrees with the
371
- // freshly written manifest tag (otherwise doctor's manifest-agreement check
372
- // fails and the operator must hand-edit the marker).
373
- syncBareVersionMarker(projectDir, contentVersion);
424
+ const alreadyCurrent = previousDepositVersion !== null &&
425
+ normalizeVersion(previousDepositVersion) === normalizeVersion(contentVersion);
426
+ const strategy = alreadyCurrent ? "no-op" : "file-swap";
427
+ if (alreadyCurrent) {
428
+ io.printf("[deft update] Framework payload already current; skipping payload copy.\n");
429
+ migrateLegacyInstallManifest(projectDir, join(deftDir, "VERSION"));
430
+ }
431
+ else {
432
+ await copyContent(contentRoot, deftDir);
433
+ await prunePythonArtifactsFromDeposit(deftDir, projectDir, io);
434
+ // #2347: prune framework-source paths that the content package does not ship.
435
+ // The additive file-swap never removes them, causing the deposit-hygiene
436
+ // advisory to persist across every upgrade until manually cleaned.
437
+ await pruneStrayDepositPaths(deftDir, contentRoot, io);
438
+ const nowIso = seams.nowIso ?? (() => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"));
439
+ const manifestFields = {
440
+ ref: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
441
+ sha: "content-package",
442
+ tag: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
443
+ installRoot: CANONICAL_INSTALL_ROOT,
444
+ fetchedAt: nowIso(),
445
+ fetchedBy: "directive-update",
446
+ ...(previousManagedBy ? { managedBy: previousManagedBy } : {}),
447
+ };
448
+ const writtenManifestPath = writeInstallManifest(projectDir, deftDir, manifestFields);
449
+ // #2064: retire a stale legacy .deft/VERSION now that the canonical
450
+ // .deft/core/VERSION has been rewritten (folded in from install-upgrade so no
451
+ // manifest behavior is lost by the redirect). Best-effort; never fatal.
452
+ migrateLegacyInstallManifest(projectDir, writtenManifestPath);
453
+ // #2055: regenerate the bare .deft-version derivative so it agrees with the
454
+ // freshly written manifest tag (otherwise doctor's manifest-agreement check
455
+ // fails and the operator must hand-edit the marker). No-op refreshes skip
456
+ // this projection because the manifest tag was not rewritten (#2118).
457
+ syncBareVersionMarker(projectDir, contentVersion);
458
+ }
374
459
  const agentsMdUpdated = writeAgentsMd(projectDir, deftDir, io);
375
- await depositNeutralization(projectDir, io);
460
+ // #2148: the deft-core-guard CI workflow is only meaningful when the deposit
461
+ // is git-tracked (committed vendor layout). On an npm-managed (gitignored)
462
+ // deposit it creates untracked noise after every `directive update`. Probe
463
+ // git-tracked status once and share the result with the gitignore upkeep below.
464
+ const depositTracked = isDepositTrackedInGit(projectDir, seams.gitLsFiles);
465
+ const skipGuardWorkflow = depositTracked !== true;
466
+ await depositNeutralization(projectDir, io, { skipGuardWorkflow });
376
467
  // #2266: non-destructive `.gitignore` upkeep for framework-owned paths. This
377
468
  // NEVER un-tracks a committed deposit -- `ensureInitGitignoreLines` only writes
378
469
  // `.gitignore` and leaves an already-tracked `.deft/core` tracked. The
@@ -383,12 +474,27 @@ export async function runRefreshDeposit(args, io, seams = {}) {
383
474
  if (args.nonInteractive) {
384
475
  taskfileWired = ensureTaskfile(projectDir, io);
385
476
  }
386
- const { stagePaths, staged, stagedPaths } = depositStagePaths(projectDir, {
387
- includeTaskfile: taskfileWired,
388
- });
389
- printCommitGuidance(io, stagePaths, staged);
390
477
  const readPorcelain = seams.gitPorcelain ?? gitPorcelain;
391
- printRefreshSideEffects(io, frameworkRefreshSideEffects(projectDir, readPorcelain));
478
+ const effects = frameworkRefreshSideEffects(projectDir, {
479
+ readPorcelain,
480
+ readSemanticDiffNames: seams.gitSemanticDiffNames ?? gitSemanticDiffNames,
481
+ });
482
+ let stagedPaths = [];
483
+ if (!alreadyCurrent || effects.files.length > 0) {
484
+ const stagedResult = depositStagePaths(projectDir, {
485
+ includeTaskfile: taskfileWired,
486
+ includeCore: !alreadyCurrent,
487
+ });
488
+ stagedPaths = stagedResult.stagedPaths;
489
+ if (!alreadyCurrent) {
490
+ printCommitGuidance(io, stagedResult.stagePaths, stagedResult.staged);
491
+ }
492
+ else if (stagedResult.stagedPaths.length > 0) {
493
+ io.printf("\nUpdated installer-managed projections for the current framework deposit:\n");
494
+ io.printf(` git add ${stagedResult.stagedPaths.join(" ")}\n`);
495
+ }
496
+ }
497
+ printRefreshSideEffects(io, { ...effects, payloadSwapped: !alreadyCurrent });
392
498
  if (versionSkewNotice) {
393
499
  io.printf(`${versionSkewNotice}\n`);
394
500
  }
@@ -398,6 +504,8 @@ export async function runRefreshDeposit(args, io, seams = {}) {
398
504
  contentVersion,
399
505
  engineVersion,
400
506
  previousDepositVersion,
507
+ alreadyCurrent,
508
+ strategy,
401
509
  agentsMdUpdated,
402
510
  versionSkewNotice,
403
511
  legacyLayout: false,
@@ -499,7 +607,9 @@ export async function runRefreshDepositCli(options) {
499
607
  }
500
608
  try {
501
609
  const result = await runRefreshDeposit(options, io, options.seams);
502
- const state = classification?.state;
610
+ const state = result.alreadyCurrent
611
+ ? "current"
612
+ : classification?.state;
503
613
  if (options.jsonOut) {
504
614
  options.writeOut(`${JSON.stringify(buildUpdateSummaryJson(result, options, state), null, 2)}\n`);
505
615
  printUpdateComplete(result, { printf: options.writeErr }, state);
@@ -67,6 +67,16 @@ export declare function ensureCodeqlPathsIgnore(projectDir: string, io: InitDepo
67
67
  export declare function ensureCoreGuardWorkflow(projectDir: string, io: InitDepositIo): boolean;
68
68
  export declare function pruneFrameworkSelfTests(projectDir: string, io: InitDepositIo): Promise<boolean>;
69
69
  export declare function pruneVendoredTsTests(projectDir: string, io: InitDepositIo): Promise<number>;
70
+ export interface DepositNeutralizationOptions {
71
+ /**
72
+ * #2148: skip depositing `.github/workflows/deft-core-guard.yml` when the
73
+ * framework deposit is not git-tracked (npm-managed / gitignored layout). The
74
+ * guard CI check is meaningless when there is no committed `.deft/core/**` to
75
+ * guard; skipping it prevents the file from re-appearing as untracked noise
76
+ * after every `directive update`.
77
+ */
78
+ readonly skipGuardWorkflow?: boolean;
79
+ }
70
80
  /** Best-effort #1430 neutralization deposit (mirrors depositNeutralization). */
71
- export declare function depositNeutralization(projectDir: string, io: InitDepositIo): Promise<void>;
81
+ export declare function depositNeutralization(projectDir: string, io: InitDepositIo, options?: DepositNeutralizationOptions): Promise<void>;
72
82
  //# sourceMappingURL=scaffold.d.ts.map
@@ -10,8 +10,8 @@ import { mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
10
10
  import { platform } from "node:os";
11
11
  import { dirname, join, relative } from "node:path";
12
12
  import { copyTree } from "../deposit/copy-tree.js";
13
- import { resolveLifecycleRoot } from "../layout/resolve.js";
14
13
  import { agentsRefreshPlan } from "../platform/agents-md.js";
14
+ import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
15
15
  import { CANONICAL_INSTALL_ROOT } from "./constants.js";
16
16
  import { installerManagedGuardEre } from "./hygiene.js";
17
17
  export { CANONICAL_INSTALL_ROOT };
@@ -22,6 +22,7 @@ const FRAMEWORK_SELF_TEST_REL = ".deft/core/tests";
22
22
  const VENDORED_TS_PACKAGES_REL = ".deft/core/packages";
23
23
  const VENDORED_TS_TEST_RE = /\.(test|spec)\.(c|m)?[jt]sx?$/i;
24
24
  const CORE_GITATTRIBUTES_LINES = [
25
+ `${CORE_GLOB} text eol=lf`,
25
26
  `${CORE_GLOB} linguist-generated=true`,
26
27
  `${CORE_GLOB} linguist-vendored=true`,
27
28
  ];
@@ -322,7 +323,7 @@ function vbriefLifecycleDirsPresent(consumerVbrief) {
322
323
  });
323
324
  }
324
325
  export async function writeConsumerVbrief(projectDir, deftDir, io) {
325
- const consumerVbrief = resolveLifecycleRoot(projectDir);
326
+ const consumerVbrief = join(projectDir, MIGRATED_ARTIFACT_DIR);
326
327
  const schemasDst = join(consumerVbrief, "schemas");
327
328
  const vbriefMdDst = join(consumerVbrief, "vbrief.md");
328
329
  const schemasPresent = existsSync(schemasDst) && statSync(schemasDst).isDirectory();
@@ -584,7 +585,7 @@ export function ensureGitattributes(projectDir, io) {
584
585
  const present = new Set(existing.split("\n").map((line) => line.trim()));
585
586
  const additions = CORE_GITATTRIBUTES_LINES.filter((line) => !present.has(line));
586
587
  if (additions.length === 0) {
587
- io.printf(`.gitattributes already marks ${CORE_GLOB} as generated/vendored — skipping.\n`);
588
+ io.printf(`.gitattributes already marks ${CORE_GLOB} as LF-pinned/generated/vendored — skipping.\n`);
588
589
  return false;
589
590
  }
590
591
  let body = existing;
@@ -594,13 +595,13 @@ export function ensureGitattributes(projectDir, io) {
594
595
  body += "\n";
595
596
  body +=
596
597
  "# Deft framework: the vendored payload is packaged framework code, not\n" +
597
- "# consumer source. Mark it generated + vendored so language stats and\n" +
598
- "# diffs treat .deft/core/** as machine-managed (#1430).\n";
598
+ "# consumer source. Pin LF endings and mark it generated + vendored so\n" +
599
+ "# Git does not rewrite it and diffs treat .deft/core/** as machine-managed (#1430, #2118).\n";
599
600
  for (const add of additions) {
600
601
  body += `${add}\n`;
601
602
  }
602
603
  writeFileSync(path, body, "utf8");
603
- io.printf(`.gitattributes updated with linguist markers: ${additions.join(", ")}\n`);
604
+ io.printf(`.gitattributes updated with Deft core markers: ${additions.join(", ")}\n`);
604
605
  return true;
605
606
  }
606
607
  function greptilePatternPresent(patterns, glob) {
@@ -774,12 +775,12 @@ export async function pruneVendoredTsTests(projectDir, io) {
774
775
  return removed;
775
776
  }
776
777
  /** Best-effort #1430 neutralization deposit (mirrors depositNeutralization). */
777
- export async function depositNeutralization(projectDir, io) {
778
+ export async function depositNeutralization(projectDir, io, options = {}) {
778
779
  const steps = [
779
780
  () => ensureGitattributes(projectDir, io),
780
781
  () => ensureGreptileIgnore(projectDir, io),
781
782
  () => ensureCodeqlPathsIgnore(projectDir, io),
782
- () => ensureCoreGuardWorkflow(projectDir, io),
783
+ ...(options.skipGuardWorkflow ? [] : [() => ensureCoreGuardWorkflow(projectDir, io)]),
783
784
  () => pruneFrameworkSelfTests(projectDir, io),
784
785
  async () => (await pruneVendoredTsTests(projectDir, io)) > 0,
785
786
  ];
@@ -77,12 +77,12 @@ export function populateCacheLayout(cacheRoot, repo, issueNumbers) {
77
77
  }
78
78
  export function createConsumerProject(parent) {
79
79
  const root = join(parent, "consumer");
80
- mkdirSync(join(root, "vbrief", "proposed"), { recursive: true });
81
- mkdirSync(join(root, "vbrief", "pending"), { recursive: true });
82
- mkdirSync(join(root, "vbrief", "active"), { recursive: true });
83
- mkdirSync(join(root, "vbrief", "completed"), { recursive: true });
84
- mkdirSync(join(root, "vbrief", "cancelled"), { recursive: true });
85
- writeFileSync(join(root, "vbrief", "specification.vbrief.json"), `${JSON.stringify({
80
+ mkdirSync(join(root, "xbrief", "proposed"), { recursive: true });
81
+ mkdirSync(join(root, "xbrief", "pending"), { recursive: true });
82
+ mkdirSync(join(root, "xbrief", "active"), { recursive: true });
83
+ mkdirSync(join(root, "xbrief", "completed"), { recursive: true });
84
+ mkdirSync(join(root, "xbrief", "cancelled"), { recursive: true });
85
+ writeFileSync(join(root, "xbrief", "specification.xbrief.json"), `${JSON.stringify({
86
86
  vBRIEFInfo: { version: "0.5" },
87
87
  plan: {
88
88
  title: "Consumer Project",
@@ -96,7 +96,7 @@ export function createConsumerProject(parent) {
96
96
  return root;
97
97
  }
98
98
  export function writeScopeVbrief(projectRoot, folder, filename, status = "proposed") {
99
- const target = join(projectRoot, "vbrief", folder, filename);
99
+ const target = join(projectRoot, "xbrief", folder, filename);
100
100
  writeFileSync(target, `${JSON.stringify({
101
101
  vBRIEFInfo: { version: "0.5" },
102
102
  plan: {
@@ -1,2 +1,2 @@
1
- export { ARTIFACT_SUFFIXES, hasArtifactSuffix, isLifecycleArtifactPath, LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, LIFECYCLE_DIR_NAMES, type LifecycleLayout, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, projectDefinitionFilename, projectDefinitionRelPath, resolveAuditDir, resolveAuditPath, resolveEvalDir, resolveEvalPath, resolveLifecycleFolder, resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, resolveSpecArtifactPath, specArtifactRelPath, stripArtifactSuffix, } from "./resolve.js";
1
+ export { ARTIFACT_SUFFIXES, hasArtifactSuffix, isLifecycleArtifactPath, LIFECYCLE_DIR_NAMES, type LifecycleLayout, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, projectDefinitionFilename, projectDefinitionRelPath, resolveAuditDir, resolveAuditPath, resolveEvalDir, resolveEvalPath, resolveLifecycleFolder, resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, resolveSpecArtifactPath, specArtifactRelPath, stripArtifactSuffix, } from "./resolve.js";
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1,2 @@
1
- export { ARTIFACT_SUFFIXES, hasArtifactSuffix, isLifecycleArtifactPath, LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, LIFECYCLE_DIR_NAMES, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, projectDefinitionFilename, projectDefinitionRelPath, resolveAuditDir, resolveAuditPath, resolveEvalDir, resolveEvalPath, resolveLifecycleFolder, resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, resolveSpecArtifactPath, specArtifactRelPath, stripArtifactSuffix, } from "./resolve.js";
1
+ export { ARTIFACT_SUFFIXES, hasArtifactSuffix, isLifecycleArtifactPath, LIFECYCLE_DIR_NAMES, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, projectDefinitionFilename, projectDefinitionRelPath, resolveAuditDir, resolveAuditPath, resolveEvalDir, resolveEvalPath, resolveLifecycleFolder, resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, resolveSpecArtifactPath, specArtifactRelPath, stripArtifactSuffix, } from "./resolve.js";
2
2
  //# sourceMappingURL=index.js.map
@@ -1,28 +1,39 @@
1
- import { LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
2
- export { LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, };
1
+ import { LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
2
+ export { LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, };
3
3
  /**
4
- * Lifecycle directory names recognized by the layout-aware resolver, in
5
- * preference order (migrated `xbrief` first, legacy `vbrief` second).
4
+ * Lifecycle directory names recognized by the layout-aware resolver and path
5
+ * validators. Includes both `xbrief` (canonical) and `vbrief` (legacy) so that
6
+ * path-validation helpers remain accurate during and after migration.
6
7
  */
7
8
  export declare const LIFECYCLE_DIR_NAMES: readonly ["xbrief", "vbrief"];
8
9
  /**
9
- * Artifact filename suffixes recognized by the layout-aware resolver, in
10
- * preference order (`.xbrief.json` first, `.vbrief.json` second).
10
+ * Artifact filename suffixes recognized by the layout-aware resolver and path
11
+ * validators. Includes both `.xbrief.json` (canonical) and `.vbrief.json`
12
+ * (legacy) so that path-validation helpers remain accurate during and after
13
+ * migration.
11
14
  */
12
15
  export declare const ARTIFACT_SUFFIXES: readonly [".xbrief.json", ".vbrief.json"];
13
- /** The on-disk lifecycle layout an engine call site should resolve against (#2109). */
16
+ /** The on-disk lifecycle layout an engine call site should resolve against (#2109 / #2112). */
14
17
  export interface LifecycleLayout {
15
- /** The lifecycle directory name -- `xbrief` when migrated, else `vbrief`. */
16
- readonly artifactDir: typeof MIGRATED_ARTIFACT_DIR | typeof LEGACY_ARTIFACT_DIR;
17
- /** The artifact filename suffix matching `artifactDir`. */
18
- readonly artifactSuffix: typeof MIGRATED_ARTIFACT_SUFFIX | typeof LEGACY_ARTIFACT_SUFFIX;
19
- /** The `*BRIEFInfo` root key matching `artifactDir`. */
20
- readonly infoRootKey: typeof MIGRATED_INFO_ROOT_KEY | typeof LEGACY_INFO_ROOT_KEY;
21
- /** Absolute path to the resolved lifecycle root (`<projectRoot>/<artifactDir>`). */
18
+ /** The lifecycle directory name -- always `xbrief` after the vbrief read path removal (#2112). */
19
+ readonly artifactDir: typeof MIGRATED_ARTIFACT_DIR;
20
+ /** The artifact filename suffix -- always `.xbrief.json` after #2112. */
21
+ readonly artifactSuffix: typeof MIGRATED_ARTIFACT_SUFFIX;
22
+ /** The `xBRIEFInfo` root key. */
23
+ readonly infoRootKey: typeof MIGRATED_INFO_ROOT_KEY;
24
+ /** Absolute path to the resolved lifecycle root (`<projectRoot>/xbrief`). */
22
25
  readonly root: string;
23
- /** True when the resolved layout is the migrated `xbrief` layout. */
24
- readonly migrated: boolean;
26
+ /** Always `true` -- the legacy vbrief read path was removed in #2112. */
27
+ readonly migrated: true;
25
28
  }
29
+ /**
30
+ * Returns the layout root for non-layout-sensitive helpers (#2112).
31
+ *
32
+ * - If a valid `xbrief/` layout exists, returns its root.
33
+ * - If only `vbrief/` exists (legacy-only), throws so the caller surfaces the migrate hint.
34
+ * - If neither exists (empty/new project), falls back to `<projectRoot>/xbrief/` canonical path.
35
+ */
36
+ export declare function resolveLayoutRootOrCanonical(projectRoot: string): string;
26
37
  /** True when `name` ends with any recognized artifact suffix (.xbrief.json or .vbrief.json). */
27
38
  export declare function hasArtifactSuffix(name: string): boolean;
28
39
  /**
@@ -31,24 +42,30 @@ export declare function hasArtifactSuffix(name: string): boolean;
31
42
  */
32
43
  export declare function stripArtifactSuffix(name: string): string;
33
44
  /**
34
- * True when a POSIX-style path is a lifecycle artifact under either layout
35
- * root (`xbrief/` or `vbrief/`) carrying either artifact suffix.
45
+ * True when a POSIX-style path is a lifecycle artifact path carrying a
46
+ * recognized lifecycle directory prefix (`xbrief/` or `vbrief/`) and a
47
+ * recognized artifact suffix. The `vbrief/` prefix and `.vbrief.json` suffix
48
+ * remain recognized for path-validation purposes during and after migration.
36
49
  */
37
50
  export declare function isLifecycleArtifactPath(posix: string): boolean;
38
51
  /**
39
- * Resolve the active lifecycle layout for `projectRoot` (#2109 part 1).
52
+ * Resolve the active lifecycle layout for `projectRoot` (#2109 part 1 / #2112).
53
+ *
54
+ * Requires the migrated `xbrief/` layout: the `xbrief/` directory must exist
55
+ * AND contain at least one `.xbrief.json` artifact. If no such layout is found,
56
+ * throws with a clear error directing the operator to run `deft migrate:xbrief`.
57
+ *
58
+ * The legacy `vbrief/` read-path fallback was removed in #2112 (0.73.0 MINOR).
59
+ * Projects that have not migrated must run `deft migrate:xbrief` first.
40
60
  *
41
- * Prefers the migrated `xbrief/` layout only when BOTH the `xbrief/` directory
42
- * exists AND it contains at least one `.xbrief.json` artifact; otherwise falls
43
- * back to the legacy `vbrief/` layout. With only `vbrief/` present (today's
44
- * repo) the result is the legacy layout, so existing behavior is unchanged.
61
+ * @throws {Error} When no `xbrief/` layout with `.xbrief.json` artifacts exists.
45
62
  */
46
63
  export declare function resolveLifecycleLayout(projectRoot: string): LifecycleLayout;
47
64
  /** Convenience accessor for the absolute resolved lifecycle root directory. */
48
65
  export declare function resolveLifecycleRoot(projectRoot: string): string;
49
66
  /**
50
- * Absolute path to a lifecycle folder under the resolved layout root
51
- * (e.g. `<root>/<xbrief|vbrief>/active`). Layout-aware (#2109 part 2a).
67
+ * Absolute path to a lifecycle folder under the resolved `xbrief/` layout root
68
+ * (e.g. `<root>/xbrief/active`). Layout-aware (#2109 part 2a / #2112).
52
69
  */
53
70
  export declare function resolveLifecycleFolder(projectRoot: string, folder: string): string;
54
71
  /** Absolute path to the layout-aware `.eval/` directory for version-eval results (#1703). */
@@ -68,20 +85,19 @@ export declare function projectDefinitionFilename(projectRoot: string): string;
68
85
  export declare function resolveProjectDefinitionPath(projectRoot: string): string;
69
86
  /**
70
87
  * POSIX-style display path to the resolved PROJECT-DEFINITION artifact relative
71
- * to the project root (e.g. `vbrief/PROJECT-DEFINITION.vbrief.json`).
88
+ * to the project root (e.g. `xbrief/PROJECT-DEFINITION.xbrief.json`).
72
89
  */
73
90
  export declare function projectDefinitionRelPath(projectRoot: string): string;
74
91
  /**
75
- * Absolute path to the resolved specification artifact (#2132).
92
+ * Absolute path to the resolved specification artifact (#2132 / #2112).
76
93
  *
77
- * Prefers `xbrief/specification.xbrief.json` on a migrated tree; falls back to
78
- * `vbrief/specification.vbrief.json` on an unmigrated tree. Consistent with the
79
- * layout resolver used by the #2109 Part-2a call-site sweep.
94
+ * Returns `xbrief/specification.xbrief.json`; throws when no migrated xbrief/
95
+ * layout is present (see `resolveLifecycleLayout`).
80
96
  */
81
97
  export declare function resolveSpecArtifactPath(projectRoot: string): string;
82
98
  /**
83
99
  * POSIX-style display path to the resolved specification artifact relative to
84
- * the project root (e.g. `xbrief/specification.xbrief.json`).
100
+ * the project root (`xbrief/specification.xbrief.json`).
85
101
  */
86
102
  export declare function specArtifactRelPath(projectRoot: string): string;
87
103
  //# sourceMappingURL=resolve.d.ts.map