@deftai/directive-core 0.78.0 → 0.79.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 (101) hide show
  1. package/dist/cache/empty-populate.d.ts +44 -0
  2. package/dist/cache/empty-populate.js +117 -0
  3. package/dist/cache/index.d.ts +1 -0
  4. package/dist/cache/index.js +1 -0
  5. package/dist/check-updates/index.d.ts +10 -0
  6. package/dist/check-updates/index.js +81 -1
  7. package/dist/doctor/main.js +13 -2
  8. package/dist/doctor/payload-staleness.js +7 -1
  9. package/dist/hooks/dispatcher.d.ts +14 -2
  10. package/dist/hooks/dispatcher.js +77 -20
  11. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  12. package/dist/init-deposit/agent-hooks.js +10 -0
  13. package/dist/init-deposit/hygiene.js +1 -0
  14. package/dist/init-deposit/init-deposit.js +2 -0
  15. package/dist/init-deposit/prettierignore.d.ts +21 -0
  16. package/dist/init-deposit/prettierignore.js +83 -0
  17. package/dist/init-deposit/refresh.d.ts +3 -1
  18. package/dist/init-deposit/refresh.js +29 -40
  19. package/dist/init-deposit/scaffold.js +6 -12
  20. package/dist/init-deposit/xbrief-projections.d.ts +18 -0
  21. package/dist/init-deposit/xbrief-projections.js +117 -0
  22. package/dist/intake/reconcile-issues.d.ts +10 -0
  23. package/dist/intake/reconcile-issues.js +121 -3
  24. package/dist/issue-sync/index.d.ts +2 -0
  25. package/dist/issue-sync/index.js +2 -0
  26. package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
  27. package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
  28. package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
  29. package/dist/issue-sync/sync-from-xbrief.js +208 -0
  30. package/dist/platform/index.d.ts +1 -0
  31. package/dist/platform/index.js +1 -0
  32. package/dist/platform/platform-capabilities.d.ts +5 -0
  33. package/dist/platform/platform-capabilities.js +9 -0
  34. package/dist/platform/shell-context.d.ts +30 -0
  35. package/dist/platform/shell-context.js +111 -0
  36. package/dist/pr-monitor/cadence.d.ts +6 -0
  37. package/dist/pr-monitor/cadence.js +13 -0
  38. package/dist/pr-monitor/index.d.ts +1 -1
  39. package/dist/pr-monitor/index.js +1 -1
  40. package/dist/pr-monitor/monitor.d.ts +7 -0
  41. package/dist/pr-monitor/monitor.js +59 -13
  42. package/dist/pr-wait-mergeable/wrappers.d.ts +9 -0
  43. package/dist/pr-wait-mergeable/wrappers.js +31 -2
  44. package/dist/preflight-cache/evaluate.d.ts +3 -8
  45. package/dist/preflight-cache/evaluate.js +19 -19
  46. package/dist/release/constants.d.ts +2 -0
  47. package/dist/release/constants.js +10 -1
  48. package/dist/release/flags.js +26 -0
  49. package/dist/release/issue-state-fetch.d.ts +27 -0
  50. package/dist/release/issue-state-fetch.js +146 -0
  51. package/dist/release/main.js +1 -0
  52. package/dist/release/native-steps.js +6 -4
  53. package/dist/release/pipeline.js +15 -5
  54. package/dist/release/preflight.d.ts +6 -2
  55. package/dist/release/preflight.js +19 -5
  56. package/dist/release/types.d.ts +3 -1
  57. package/dist/release-e2e/constants.js +6 -6
  58. package/dist/release-e2e/flags.d.ts +2 -0
  59. package/dist/release-e2e/flags.js +8 -4
  60. package/dist/release-e2e/main.js +15 -9
  61. package/dist/release-e2e/types.d.ts +4 -2
  62. package/dist/resolution/classify.d.ts +1 -1
  63. package/dist/resolution/classify.js +36 -11
  64. package/dist/scope/project-definition-sync.js +2 -150
  65. package/dist/scope/registry-artifact-sync.d.ts +3 -0
  66. package/dist/scope/registry-artifact-sync.js +159 -0
  67. package/dist/scope/specification-sync.d.ts +3 -0
  68. package/dist/scope/specification-sync.js +16 -0
  69. package/dist/scope/transition.js +40 -12
  70. package/dist/session/cache-recovery.d.ts +19 -0
  71. package/dist/session/cache-recovery.js +26 -0
  72. package/dist/session/session-start.d.ts +2 -0
  73. package/dist/session/session-start.js +14 -3
  74. package/dist/session/verify-session-ritual.d.ts +1 -0
  75. package/dist/session/verify-session-ritual.js +1 -0
  76. package/dist/triage/welcome/default-mode.js +2 -0
  77. package/dist/triage/welcome/onboard.js +2 -0
  78. package/dist/ts-check-lane/run-lane.d.ts +5 -0
  79. package/dist/ts-check-lane/run-lane.js +9 -0
  80. package/dist/vbrief-reconcile/labels.d.ts +3 -0
  81. package/dist/vbrief-reconcile/labels.js +14 -0
  82. package/dist/vbrief-reconcile/main.d.ts +1 -0
  83. package/dist/vbrief-reconcile/main.js +13 -3
  84. package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
  85. package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
  86. package/dist/vbrief-reconcile/repo-guard.js +33 -0
  87. package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
  88. package/dist/vbrief-reconcile/umbrellas.js +17 -1
  89. package/dist/verify-env/agent-hooks.js +4 -3
  90. package/dist/verify-env/verify-hooks-installed.js +5 -5
  91. package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
  92. package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
  93. package/dist/vitest-runner/coverage-debt.d.ts +50 -0
  94. package/dist/vitest-runner/coverage-debt.js +155 -0
  95. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
  96. package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
  97. package/dist/xbrief-migrate/detect.js +46 -1
  98. package/dist/xbrief-migrate/migrate-project.js +19 -4
  99. package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
  100. package/dist/xbrief-migrate/migration-containment.js +37 -0
  101. package/package.json +7 -3
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Consumer `.prettierignore` upkeep for install and npm-managed update (#2534).
3
+ *
4
+ * Managed `.deft/core/` is outside the consumer Prettier format gate. Official
5
+ * install and `directive update` heal the ignore entry idempotently without
6
+ * reformatting the vendored deposit.
7
+ *
8
+ * Refs #2534, #670.
9
+ */
10
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { assertProjectionContained } from "../fs/projection-containment.js";
13
+ import { stripGitignoreInlineComment } from "../triage/bootstrap/gitignore.js";
14
+ /** Directory ignore entry for the hybrid deposit. */
15
+ export const PRETTIERIGNORE_DEFT_CORE_LINE = ".deft/core/";
16
+ /** Alternate spellings that already cover the deposit ignore entry. */
17
+ const DEFT_CORE_COVERING_LINES = new Set([".deft/core/", ".deft/core", ".deft/core/**"]);
18
+ const DEFT_FRAMEWORK_PRETTIERIGNORE_HEADER = "# Deft framework: the vendored payload is outside the consumer Prettier gate (#2534).\n";
19
+ function projectionTarget(projectDir) {
20
+ const target = join(projectDir, ".prettierignore");
21
+ assertProjectionContained(projectDir, target);
22
+ return target;
23
+ }
24
+ function prettierIgnoreCoversLine(present, line) {
25
+ if (present.has(line))
26
+ return true;
27
+ if (line === PRETTIERIGNORE_DEFT_CORE_LINE) {
28
+ return [...DEFT_CORE_COVERING_LINES].some((candidate) => present.has(candidate));
29
+ }
30
+ return false;
31
+ }
32
+ function collectPresentPrettierIgnoreLines(existing) {
33
+ const present = new Set();
34
+ for (const raw of existing.split("\n")) {
35
+ const stripped = stripGitignoreInlineComment(raw);
36
+ if (stripped)
37
+ present.add(stripped);
38
+ }
39
+ return present;
40
+ }
41
+ /**
42
+ * Ensure the consumer `.prettierignore` excludes managed `.deft/core/` from
43
+ * Prettier. Idempotent; preserves consumer preamble.
44
+ */
45
+ export function ensurePrettierIgnoreLines(projectDir, io) {
46
+ const path = projectionTarget(projectDir);
47
+ let existing = "";
48
+ if (existsSync(path)) {
49
+ try {
50
+ existing = readFileSync(path, { encoding: "utf8" });
51
+ }
52
+ catch (cause) {
53
+ throw new Error(`could not read .prettierignore: ${String(cause)}`);
54
+ }
55
+ }
56
+ const present = collectPresentPrettierIgnoreLines(existing);
57
+ const targetLines = [PRETTIERIGNORE_DEFT_CORE_LINE];
58
+ const additions = targetLines.filter((line) => !prettierIgnoreCoversLine(present, line));
59
+ if (additions.length === 0) {
60
+ io.printf(".prettierignore already excludes the managed deft framework deposit — skipping.\n");
61
+ return { changed: false };
62
+ }
63
+ let body = existing;
64
+ if (body !== "" && !body.endsWith("\n")) {
65
+ body += "\n";
66
+ }
67
+ if (body !== "" && !body.endsWith("\n\n")) {
68
+ body += "\n";
69
+ }
70
+ body += DEFT_FRAMEWORK_PRETTIERIGNORE_HEADER;
71
+ for (const add of additions) {
72
+ body += `${add}\n`;
73
+ }
74
+ try {
75
+ writeFileSync(path, body, { encoding: "utf8", mode: 0o644 });
76
+ }
77
+ catch (cause) {
78
+ throw new Error(`could not write .prettierignore: ${String(cause)}`);
79
+ }
80
+ io.printf(`.prettierignore updated with Deft framework exclusions: ${additions.join(", ")}\n`);
81
+ return { changed: true };
82
+ }
83
+ //# sourceMappingURL=prettierignore.js.map
@@ -12,7 +12,7 @@ import { type ClassifySeams, type EngineInstallRunner, type EngineResolution, ty
12
12
  import { type GitLsFiles } from "./gitignore.js";
13
13
  import { type InitDepositArgs } from "./init-deposit.js";
14
14
  import { type LegacyLayoutDetection } from "./legacy-detect.js";
15
- import { type InitDepositIo } from "./scaffold.js";
15
+ import { type GitHooksSeams, type InitDepositIo } from "./scaffold.js";
16
16
  export interface RefreshDepositArgs extends InitDepositArgs {
17
17
  readonly upgrade: boolean;
18
18
  }
@@ -45,6 +45,8 @@ export interface RefreshDepositSeams {
45
45
  * upkeep so the refresh never invokes a destructive `git rm --cached` path.
46
46
  */
47
47
  gitLsFiles?: GitLsFiles;
48
+ /** #2530: injected git config seams for {@link writeConsumerGitHooks}. */
49
+ gitHooks?: GitHooksSeams;
48
50
  }
49
51
  /**
50
52
  * The four states `directive update` classifies an EXISTING install into BEFORE
@@ -8,7 +8,7 @@
8
8
  * Refs #1942, #1430, #1671.
9
9
  */
10
10
  import { execFileSync } from "node:child_process";
11
- import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
11
+ import { existsSync, readFileSync, renameSync, statSync } from "node:fs";
12
12
  import { platform as osPlatform } from "node:os";
13
13
  import { join, resolve } from "node:path";
14
14
  import { assertDepositContained } from "../deposit/contain.js";
@@ -18,7 +18,6 @@ import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
18
18
  import { manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
19
19
  import { readCorePackageVersion } from "../engine-version.js";
20
20
  import { resolveLifecycleRoot } from "../layout/resolve.js";
21
- import { DEV_FALLBACK } from "../platform/constants.js";
22
21
  import { checkLocalEngineIntegrity, classify, ENGINE_PACKAGE, plan, renderGlobalInstall, resolveEngine, } from "../resolution/index.js";
23
22
  import { gitPorcelain } from "../story-ready/git.js";
24
23
  import { writeAgentHookDeposit } from "./agent-hooks.js";
@@ -27,7 +26,18 @@ import { depositStagePaths, isInstallerManagedPath, printCommitGuidance, pruneSt
27
26
  import { parseInitArgv } from "./init-deposit.js";
28
27
  import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
29
28
  import { printMigrateNudgeIfNeeded } from "./migrate.js";
30
- import { CANONICAL_INSTALL_ROOT, depositNeutralization, ensureTaskfile, writeAgentsMd, writeInstallManifest, } from "./scaffold.js";
29
+ import { ensurePrettierIgnoreLines } from "./prettierignore.js";
30
+ import { CANONICAL_INSTALL_ROOT, depositNeutralization, ensureTaskfile, writeAgentsMd, writeConsumerGitHooks, writeInstallManifest, } from "./scaffold.js";
31
+ import { syncBareVersionMarker, syncConsumerXbriefSchemas, syncExistingBareVersionMarker, } from "./xbrief-projections.js";
32
+ function hasCanonicalXbriefLifecycle(projectDir) {
33
+ try {
34
+ resolveLifecycleRoot(projectDir);
35
+ return true;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
31
41
  /** The exact refusal shown when `update` runs against an un-initialized project. */
32
42
  export const NOT_INITIALIZED_MESSAGE = "This project is not initialized. Run directive init.";
33
43
  /** `update` refused because the project has no install / needs a different verb. */
@@ -147,39 +157,6 @@ function readRecordedManagedBy(deftDir) {
147
157
  return null;
148
158
  }
149
159
  }
150
- /**
151
- * Regenerate the bare `.deft-version` derivative from the deposited content
152
- * version in the same transaction as the payload swap (#2055). The canonical
153
- * marker lives at `vbrief/.deft-version`; fall back to the project root only
154
- * when `vbrief/` is absent. Never persist the dev fallback.
155
- */
156
- function syncBareVersionMarker(projectDir, version) {
157
- const normalized = normalizeVersion(version);
158
- if (!normalized || normalized === DEV_FALLBACK)
159
- return;
160
- let vbriefDir = null;
161
- try {
162
- vbriefDir = resolveLifecycleRoot(projectDir);
163
- }
164
- catch {
165
- // No xbrief/ layout present — write the root-level derivative instead.
166
- }
167
- let targetDir = projectDir;
168
- try {
169
- if (vbriefDir !== null && statSync(vbriefDir).isDirectory())
170
- targetDir = vbriefDir;
171
- }
172
- catch {
173
- // xbrief/ absent — write the root-level derivative instead
174
- }
175
- try {
176
- mkdirSync(targetDir, { recursive: true });
177
- writeFileSync(join(targetDir, ".deft-version"), `${normalized}\n`, "utf8");
178
- }
179
- catch {
180
- // best-effort, mirrors install-upgrade marker write
181
- }
182
- }
183
160
  /**
184
161
  * Retire a stale legacy `.deft/VERSION` after the canonical `.deft/core/VERSION`
185
162
  * has been (re)written (#2064). Folded in from the former `install-upgrade`
@@ -451,14 +428,25 @@ export async function runRefreshDeposit(args, io, seams = {}) {
451
428
  // .deft/core/VERSION has been rewritten (folded in from install-upgrade so no
452
429
  // manifest behavior is lost by the redirect). Best-effort; never fatal.
453
430
  migrateLegacyInstallManifest(projectDir, writtenManifestPath);
454
- // #2055: regenerate the bare .deft-version derivative so it agrees with the
455
- // freshly written manifest tag (otherwise doctor's manifest-agreement check
456
- // fails and the operator must hand-edit the marker). No-op refreshes skip
457
- // this projection because the manifest tag was not rewritten (#2118).
431
+ }
432
+ // #2595: payload freshness and consumer derivative freshness are independent.
433
+ // Always repair these cheap projections, including on the #2118 no-op path.
434
+ if (alreadyCurrent) {
435
+ syncExistingBareVersionMarker(projectDir, contentVersion);
436
+ }
437
+ else {
458
438
  syncBareVersionMarker(projectDir, contentVersion);
459
439
  }
440
+ // Do not turn a legacy-only or cache-only support tree into canonical
441
+ // lifecycle content before migrate:xbrief can transactionally converge it.
442
+ if (hasCanonicalXbriefLifecycle(projectDir)) {
443
+ syncConsumerXbriefSchemas(projectDir, deftDir);
444
+ }
460
445
  const agentsMdUpdated = writeAgentsMd(projectDir, deftDir, io);
461
446
  writeAgentHookDeposit(projectDir, io);
447
+ // #2530: root `.githooks/` is a consumer derivative like #2595 marker/schemas —
448
+ // repair on every refresh, including the already-current no-op path.
449
+ writeConsumerGitHooks(projectDir, deftDir, io, seams.gitHooks);
462
450
  // #2148: the deft-core-guard CI workflow is only meaningful when the deposit
463
451
  // is git-tracked (committed vendor layout). On an npm-managed (gitignored)
464
452
  // deposit it creates untracked noise after every `directive update`. Probe
@@ -472,6 +460,7 @@ export async function runRefreshDeposit(args, io, seams = {}) {
472
460
  // destructive `git rm --cached .deft/core` un-track is the deliberate
473
461
  // `migrate --untrack-core` step (#2269), not `update`.
474
462
  ensureInitGitignoreLines(projectDir, io, { gitLsFiles: seams.gitLsFiles });
463
+ ensurePrettierIgnoreLines(projectDir, io);
475
464
  let taskfileWired = false;
476
465
  if (args.nonInteractive) {
477
466
  taskfileWired = ensureTaskfile(projectDir, io);
@@ -9,12 +9,12 @@ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync,
9
9
  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
- import { copyTree } from "../deposit/copy-tree.js";
13
12
  import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
14
13
  import { agentsRefreshPlan } from "../platform/agents-md.js";
15
14
  import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
16
15
  import { CANONICAL_INSTALL_ROOT } from "./constants.js";
17
16
  import { installerManagedGuardEre } from "./hygiene.js";
17
+ import { syncConsumerXbriefSchemas } from "./xbrief-projections.js";
18
18
  export { CANONICAL_INSTALL_ROOT };
19
19
  export const CORE_GLOB = ".deft/core/**";
20
20
  /** Refuse init/update projection writes that escape via repo-controlled symlinks (#2446). */
@@ -336,20 +336,14 @@ export async function writeConsumerVbrief(projectDir, deftDir, io) {
336
336
  const schemasPresent = existsSync(schemasDst) && statSync(schemasDst).isDirectory();
337
337
  const vbriefMdPresent = existsSync(vbriefMdDst) && statSync(vbriefMdDst).isFile();
338
338
  const lifecyclePresent = vbriefLifecycleDirsPresent(consumerVbrief);
339
+ const schemasChanged = syncConsumerXbriefSchemas(projectDir, deftDir);
339
340
  if (schemasPresent && vbriefMdPresent && lifecyclePresent) {
340
- io.printf("vbrief/ already present at project root — skipping.\n");
341
- return false;
341
+ io.printf(schemasChanged
342
+ ? "vbrief/ schemas refreshed at project root.\n"
343
+ : "vbrief/ already present at project root — skipping.\n");
344
+ return schemasChanged;
342
345
  }
343
346
  mkdirSync(consumerVbrief, { recursive: true });
344
- if (!schemasPresent) {
345
- const fwSchemas = join(deftDir, "vbrief", "schemas");
346
- if (existsSync(fwSchemas) && statSync(fwSchemas).isDirectory()) {
347
- await copyTree(fwSchemas, schemasDst);
348
- }
349
- else {
350
- mkdirSync(schemasDst, { recursive: true });
351
- }
352
- }
353
347
  if (!vbriefMdPresent) {
354
348
  const fwVbriefMd = join(deftDir, "vbrief", "vbrief.md");
355
349
  if (existsSync(fwVbriefMd)) {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Idempotent consumer xBRIEF derivatives maintained by init/update (#2595).
3
+ *
4
+ * The framework payload manifest and consumer projections have independent
5
+ * freshness. A current `.deft/core/VERSION` therefore cannot short-circuit
6
+ * these repairs.
7
+ */
8
+ /**
9
+ * Synchronize framework-owned xBRIEF schemas while preserving unknown consumer
10
+ * files. The obsolete v0.6 root schema is the only destination-only file this
11
+ * repair removes.
12
+ */
13
+ export declare function syncConsumerXbriefSchemas(projectDir: string, deftDir: string): boolean;
14
+ /** Regenerate the bare consumer version derivative, retaining the historical root fallback. */
15
+ export declare function syncBareVersionMarker(projectDir: string, version: string): boolean;
16
+ /** Repair an existing marker without creating root state when no lifecycle artifact exists. */
17
+ export declare function syncExistingBareVersionMarker(projectDir: string, version: string): boolean;
18
+ //# sourceMappingURL=xbrief-projections.d.ts.map
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Idempotent consumer xBRIEF derivatives maintained by init/update (#2595).
3
+ *
4
+ * The framework payload manifest and consumer projections have independent
5
+ * freshness. A current `.deft/core/VERSION` therefore cannot short-circuit
6
+ * these repairs.
7
+ */
8
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
9
+ import { join, relative } from "node:path";
10
+ import { assertProjectionContained } from "../fs/projection-containment.js";
11
+ import { resolveLifecycleRoot } from "../layout/resolve.js";
12
+ import { DEV_FALLBACK } from "../platform/constants.js";
13
+ import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
14
+ const OBSOLETE_CORE_SCHEMA = "vbrief-core.schema.json";
15
+ const CURRENT_CORE_SCHEMA = "xbrief-core-0.8.schema.json";
16
+ function normalizeVersion(version) {
17
+ return version.trim().replace(/^v/, "");
18
+ }
19
+ function isDirectory(path) {
20
+ try {
21
+ return statSync(path).isDirectory();
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ function collectSchemaFiles(root, dir = root, files = []) {
28
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
29
+ const full = join(dir, entry.name);
30
+ if (entry.isSymbolicLink()) {
31
+ throw new Error(`refusing xbrief schema projection from symlink: ${full}`);
32
+ }
33
+ if (entry.isDirectory()) {
34
+ collectSchemaFiles(root, full, files);
35
+ }
36
+ else if (entry.isFile()) {
37
+ files.push(relative(root, full));
38
+ }
39
+ }
40
+ return files;
41
+ }
42
+ function writeFileIfChanged(projectDir, target, content) {
43
+ assertProjectionContained(projectDir, target);
44
+ const desired = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
45
+ try {
46
+ if (readFileSync(target).equals(desired))
47
+ return false;
48
+ }
49
+ catch {
50
+ // Missing or unreadable target is replaced below.
51
+ }
52
+ mkdirSync(join(target, ".."), { recursive: true });
53
+ writeFileSync(target, desired);
54
+ return true;
55
+ }
56
+ /**
57
+ * Synchronize framework-owned xBRIEF schemas while preserving unknown consumer
58
+ * files. The obsolete v0.6 root schema is the only destination-only file this
59
+ * repair removes.
60
+ */
61
+ export function syncConsumerXbriefSchemas(projectDir, deftDir) {
62
+ const sourceDir = join(deftDir, "vbrief", "schemas");
63
+ const currentSource = join(sourceDir, CURRENT_CORE_SCHEMA);
64
+ if (!isDirectory(sourceDir) || !existsSync(currentSource) || !statSync(currentSource).isFile()) {
65
+ throw new Error(`cannot project xbrief schemas: framework payload is missing ${CURRENT_CORE_SCHEMA}`);
66
+ }
67
+ const destinationDir = join(projectDir, MIGRATED_ARTIFACT_DIR, "schemas");
68
+ assertProjectionContained(projectDir, destinationDir);
69
+ mkdirSync(destinationDir, { recursive: true });
70
+ let changed = false;
71
+ for (const rel of collectSchemaFiles(sourceDir)) {
72
+ if (rel === OBSOLETE_CORE_SCHEMA)
73
+ continue;
74
+ const source = join(sourceDir, rel);
75
+ const destination = join(destinationDir, rel);
76
+ changed = writeFileIfChanged(projectDir, destination, readFileSync(source)) || changed;
77
+ }
78
+ const obsoleteDestination = join(destinationDir, OBSOLETE_CORE_SCHEMA);
79
+ assertProjectionContained(projectDir, obsoleteDestination);
80
+ if (existsSync(obsoleteDestination)) {
81
+ rmSync(obsoleteDestination, { force: true });
82
+ changed = true;
83
+ }
84
+ return changed;
85
+ }
86
+ function syncBareVersionMarkerWithPolicy(projectDir, version, allowRootFallback) {
87
+ const normalized = normalizeVersion(version);
88
+ if (!normalized || normalized === DEV_FALLBACK)
89
+ return false;
90
+ const canonicalRoot = join(projectDir, MIGRATED_ARTIFACT_DIR);
91
+ if (existsSync(canonicalRoot)) {
92
+ assertProjectionContained(projectDir, join(canonicalRoot, ".deft-version"));
93
+ }
94
+ let targetDir = projectDir;
95
+ try {
96
+ targetDir = resolveLifecycleRoot(projectDir);
97
+ }
98
+ catch {
99
+ // Preserve the historical root fallback for payload-changing refreshes,
100
+ // and repair an existing fallback on no-op refreshes without creating new
101
+ // untracked state (#2118 / #2595).
102
+ const rootMarker = join(projectDir, ".deft-version");
103
+ if (!allowRootFallback && !existsSync(rootMarker))
104
+ return false;
105
+ }
106
+ const target = join(targetDir, ".deft-version");
107
+ return writeFileIfChanged(projectDir, target, `${normalized}\n`);
108
+ }
109
+ /** Regenerate the bare consumer version derivative, retaining the historical root fallback. */
110
+ export function syncBareVersionMarker(projectDir, version) {
111
+ return syncBareVersionMarkerWithPolicy(projectDir, version, true);
112
+ }
113
+ /** Repair an existing marker without creating root state when no lifecycle artifact exists. */
114
+ export function syncExistingBareVersionMarker(projectDir, version) {
115
+ return syncBareVersionMarkerWithPolicy(projectDir, version, false);
116
+ }
117
+ //# sourceMappingURL=xbrief-projections.js.map
@@ -30,10 +30,15 @@ export interface FetchIssueStatesOptions extends FetchIssuesOptions {
30
30
  readonly batchSize?: number;
31
31
  }
32
32
  export declare function fetchIssueStates(repo: string, issueNumbers: ReadonlySet<number>, options?: FetchIssueStatesOptions): Map<number, IssueState> | null;
33
+ export interface CompletedStatusDriftEntry {
34
+ readonly rel_path: string;
35
+ readonly status: string;
36
+ }
33
37
  export interface ReconcileReport {
34
38
  linked: Record<string, unknown>[];
35
39
  no_open_issue: Record<string, unknown>[];
36
40
  unlinked?: Record<string, unknown>[];
41
+ completed_status_drift?: CompletedStatusDriftEntry[];
37
42
  summary: Record<string, number>;
38
43
  }
39
44
  export declare function reconcile(issueToVbriefs: Map<number, string[]>, issueStateMap: Map<number, IssueState | string>): ReconcileReport;
@@ -43,10 +48,15 @@ export declare function parseParentIssue(data: Record<string, unknown>): number
43
48
  export declare function parseDecompositionOrigin(data: Record<string, unknown>): number | null;
44
49
  export declare function resolveLifecycleAnchor(data: Record<string, unknown>): [number | null, string];
45
50
  export declare function scanLifecycleAnchors(vbriefDir: string): Record<string, unknown>[];
51
+ /** Scan completed/ for xBRIEFs whose plan.status is not terminal (D2 drift, #2578). */
52
+ export declare function scanCompletedStatusDrift(vbriefDir: string): CompletedStatusDriftEntry[];
53
+ export declare function attachCompletedStatusDrift(report: ReconcileReport, drift: CompletedStatusDriftEntry[]): ReconcileReport;
46
54
  export declare function buildLifecycleReport(anchors: Record<string, unknown>[], issueStateMap: Map<number, IssueState | string>, log?: boolean): ReconcileReport;
47
55
  export declare function formatJson(report: ReconcileReport): string;
48
56
  export declare function formatMarkdown(report: ReconcileReport): string;
49
57
  export declare function applyLifecycleFixes(vbriefDir: string, report: ReconcileReport, projectRoot?: string | null): [number, number, string[]];
58
+ /** Repair completed/ files whose plan.status is not terminal (#2578). */
59
+ export declare function repairCompletedStatusDrift(vbriefDir: string, drift: readonly CompletedStatusDriftEntry[]): [number, number, string[]];
50
60
  export declare function detectRepo(): string | null;
51
61
  export interface ReconcileCliArgs {
52
62
  vbriefDir?: string;
@@ -6,6 +6,7 @@ import { call } from "../scm/call.js";
6
6
  import { updateDecomposedChildBackReferences } from "../scope/decomposed-refs.js";
7
7
  import { resolveProjectRoot } from "../scope/project-context.js";
8
8
  import { resolveProjectRepo } from "../slice/project-context.js";
9
+ import { FOLDER_ALLOWED_STATUSES } from "../vbrief-validate/constants.js";
9
10
  import { LEGACY_INFO_ROOT_KEY, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
10
11
  export const LIFECYCLE_FOLDERS = [
11
12
  "proposed",
@@ -435,6 +436,52 @@ export function scanLifecycleAnchors(vbriefDir) {
435
436
  }
436
437
  return anchors;
437
438
  }
439
+ const COMPLETED_TERMINAL_STATUSES = FOLDER_ALLOWED_STATUSES.completed ?? new Set(["completed", "failed"]);
440
+ /** Scan completed/ for xBRIEFs whose plan.status is not terminal (D2 drift, #2578). */
441
+ export function scanCompletedStatusDrift(vbriefDir) {
442
+ const completedDir = join(vbriefDir, "completed");
443
+ const drift = [];
444
+ try {
445
+ if (!statSync(completedDir).isDirectory()) {
446
+ return drift;
447
+ }
448
+ }
449
+ catch {
450
+ return drift;
451
+ }
452
+ for (const filename of readdirSync(completedDir)
453
+ .filter((f) => hasArtifactSuffix(f))
454
+ .sort()) {
455
+ const relPath = `completed/${filename}`;
456
+ let data;
457
+ try {
458
+ data = JSON.parse(readFileSync(join(completedDir, filename), "utf8"));
459
+ }
460
+ catch {
461
+ continue;
462
+ }
463
+ const plan = data.plan;
464
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
465
+ continue;
466
+ }
467
+ const status = String(plan.status ?? "");
468
+ if (status.length === 0 || COMPLETED_TERMINAL_STATUSES.has(status)) {
469
+ continue;
470
+ }
471
+ drift.push({ rel_path: relPath, status });
472
+ }
473
+ return drift;
474
+ }
475
+ export function attachCompletedStatusDrift(report, drift) {
476
+ return {
477
+ ...report,
478
+ completed_status_drift: drift,
479
+ summary: {
480
+ ...report.summary,
481
+ completed_status_drift_count: drift.length,
482
+ },
483
+ };
484
+ }
438
485
  export function buildLifecycleReport(anchors, issueStateMap, log = true) {
439
486
  const linked = [];
440
487
  const noOpenIssue = [];
@@ -527,6 +574,18 @@ export function formatMarkdown(report) {
527
574
  lines.push("None.");
528
575
  }
529
576
  lines.push("");
577
+ lines.push("## (d) completed/ xBRIEFs with non-terminal plan.status (D2 drift)", "");
578
+ const drift = report.completed_status_drift ?? [];
579
+ if (drift.length > 0) {
580
+ for (const entry of drift) {
581
+ const safeStatus = entry.status.replace(/\r?\n/g, " ");
582
+ lines.push(`- \`${entry.rel_path}\` -- plan.status='${safeStatus}'`);
583
+ }
584
+ }
585
+ else {
586
+ lines.push("None.");
587
+ }
588
+ lines.push("");
530
589
  return lines.join("\n");
531
590
  }
532
591
  function utcNowIso() {
@@ -625,7 +684,12 @@ export function applyLifecycleFixes(vbriefDir, report, projectRoot = null) {
625
684
  mkdirSync(join(vbriefDir, destFolder), { recursive: true });
626
685
  try {
627
686
  statSync(dst);
628
- failures.push(`target already exists in ${destFolder}/: ${filename}`);
687
+ // Destination already holds this basename (#2622). Treat as already-terminal
688
+ // and continue the sweep. Do not unlink the source — a same-basename collision
689
+ // can be a non-identical artifact; leave removal to the operator / git.
690
+ process.stderr.write(`reconcile: skipped ${relPath} — already present in ${destFolder}/ (#2622); ` +
691
+ "left source in place for manual cleanup\n");
692
+ skipped += 1;
629
693
  continue;
630
694
  }
631
695
  catch {
@@ -663,6 +727,54 @@ export function applyLifecycleFixes(vbriefDir, report, projectRoot = null) {
663
727
  }
664
728
  return [moved, skipped, failures];
665
729
  }
730
+ /** Repair completed/ files whose plan.status is not terminal (#2578). */
731
+ export function repairCompletedStatusDrift(vbriefDir, drift) {
732
+ let repaired = 0;
733
+ let skipped = 0;
734
+ const failures = [];
735
+ const completedDir = join(vbriefDir, "completed");
736
+ for (const entry of drift) {
737
+ const slash = entry.rel_path.indexOf("/");
738
+ if (slash < 0) {
739
+ failures.push(`unexpected vBRIEF path shape (no folder): ${JSON.stringify(entry.rel_path)}`);
740
+ continue;
741
+ }
742
+ const filename = entry.rel_path.slice(slash + 1);
743
+ const src = join(completedDir, filename);
744
+ let data;
745
+ try {
746
+ data = JSON.parse(readFileSync(src, "utf8"));
747
+ }
748
+ catch (exc) {
749
+ failures.push(`failed to parse ${entry.rel_path}: ${String(exc)}`);
750
+ continue;
751
+ }
752
+ const plan = (data.plan ?? {});
753
+ data.plan = plan;
754
+ const currentStatus = String(plan.status ?? "");
755
+ if (COMPLETED_TERMINAL_STATUSES.has(currentStatus)) {
756
+ skipped += 1;
757
+ continue;
758
+ }
759
+ const stamp = utcNowIso();
760
+ plan.status = "completed";
761
+ plan.updated = stamp;
762
+ const infoKey = MIGRATED_INFO_ROOT_KEY in data ? MIGRATED_INFO_ROOT_KEY : LEGACY_INFO_ROOT_KEY;
763
+ const info = (data[infoKey] ?? {});
764
+ data[infoKey] = info;
765
+ info.updated = stamp;
766
+ propagateItemStatus(plan.items, "completed", stamp);
767
+ try {
768
+ writeFileSync(src, `${JSON.stringify(data, null, 2)}\n`, "utf8");
769
+ }
770
+ catch (exc) {
771
+ failures.push(`failed to write ${entry.rel_path}: ${String(exc)}`);
772
+ continue;
773
+ }
774
+ repaired += 1;
775
+ }
776
+ return [repaired, skipped, failures];
777
+ }
666
778
  export function detectRepo() {
667
779
  try {
668
780
  const stdout = execFileSync("git", ["remote", "get-url", "origin"], {
@@ -744,6 +856,8 @@ export function reconcileMain(args) {
744
856
  report = reconcile(issueToVbriefsObj, issueStateMap);
745
857
  }
746
858
  const fmt = args.format ?? "markdown";
859
+ const completedDrift = scanCompletedStatusDrift(vbriefDir);
860
+ report = attachCompletedStatusDrift(report, completedDrift);
747
861
  if (fmt === "json") {
748
862
  process.stdout.write(formatJson(report));
749
863
  }
@@ -761,11 +875,15 @@ export function reconcileMain(args) {
761
875
  return acc;
762
876
  }, 0);
763
877
  const [moved, skipped, failures] = applyLifecycleFixes(vbriefDir, applyReport, projectRoot);
878
+ const driftAfterMove = scanCompletedStatusDrift(vbriefDir);
879
+ const [driftRepaired, driftSkipped, driftFailures] = repairCompletedStatusDrift(vbriefDir, driftAfterMove);
880
+ const allFailures = [...failures, ...driftFailures];
764
881
  process.stderr.write(`[${moved}/${candidates}] vBRIEFs reconciled (moved=${moved}, already-terminal=${skipped}, failures=${failures.length})\n`);
765
- for (const f of failures) {
882
+ process.stderr.write(`[${driftRepaired}/${completedDrift.length}] completed/ status drift repaired (repaired=${driftRepaired}, already-terminal=${driftSkipped}, failures=${driftFailures.length})\n`);
883
+ for (const f of allFailures) {
766
884
  process.stderr.write(` -- FAIL: ${f}\n`);
767
885
  }
768
- if (failures.length > 0) {
886
+ if (allFailures.length > 0) {
769
887
  return 1;
770
888
  }
771
889
  }
@@ -0,0 +1,2 @@
1
+ export * from "./sync-from-xbrief.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export * from "./sync-from-xbrief.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ export interface ParsedSyncFromXbriefCliArgs {
3
+ path?: string;
4
+ dryRun?: boolean;
5
+ projectRoot?: string;
6
+ repo?: string;
7
+ error?: string;
8
+ }
9
+ export declare function parseArgs(argv: readonly string[]): ParsedSyncFromXbriefCliArgs;
10
+ export declare function mainEntry(argv?: string[]): number;
11
+ //# sourceMappingURL=sync-from-xbrief-cli.d.ts.map
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from "node:url";
3
+ import { syncFromXbriefMain } from "./sync-from-xbrief.js";
4
+ export function parseArgs(argv) {
5
+ const out = {};
6
+ for (let i = 0; i < argv.length; i += 1) {
7
+ const arg = argv[i];
8
+ if (arg === "--dry-run") {
9
+ out.dryRun = true;
10
+ }
11
+ else if (arg === "--project-root") {
12
+ const value = argv[i + 1];
13
+ if (value === undefined) {
14
+ return { ...out, error: "argument --project-root: expected one argument" };
15
+ }
16
+ out.projectRoot = value;
17
+ i += 1;
18
+ }
19
+ else if (arg === "--repo") {
20
+ const value = argv[i + 1];
21
+ if (value === undefined) {
22
+ return { ...out, error: "argument --repo: expected one argument" };
23
+ }
24
+ out.repo = value;
25
+ i += 1;
26
+ }
27
+ else if (!arg.startsWith("-")) {
28
+ out.path = arg;
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+ export function mainEntry(argv = process.argv.slice(2)) {
34
+ const args = parseArgs(argv);
35
+ if (args.error !== undefined) {
36
+ process.stderr.write(`issue:sync-from-xbrief: ${args.error}\n`);
37
+ return 2;
38
+ }
39
+ return syncFromXbriefMain(args);
40
+ }
41
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
42
+ process.exit(mainEntry());
43
+ }
44
+ //# sourceMappingURL=sync-from-xbrief-cli.js.map