@deftai/directive-core 0.78.0 → 0.79.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 (99) 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 +1 -1
  10. package/dist/hooks/dispatcher.js +19 -17
  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 +115 -2
  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/preflight-cache/evaluate.d.ts +3 -8
  43. package/dist/preflight-cache/evaluate.js +19 -19
  44. package/dist/release/constants.d.ts +2 -0
  45. package/dist/release/constants.js +8 -1
  46. package/dist/release/flags.js +26 -0
  47. package/dist/release/issue-state-fetch.d.ts +27 -0
  48. package/dist/release/issue-state-fetch.js +146 -0
  49. package/dist/release/main.js +1 -0
  50. package/dist/release/native-steps.js +6 -4
  51. package/dist/release/pipeline.js +15 -5
  52. package/dist/release/preflight.d.ts +6 -2
  53. package/dist/release/preflight.js +19 -5
  54. package/dist/release/types.d.ts +3 -1
  55. package/dist/release-e2e/constants.js +6 -6
  56. package/dist/release-e2e/flags.d.ts +2 -0
  57. package/dist/release-e2e/flags.js +8 -4
  58. package/dist/release-e2e/main.js +15 -9
  59. package/dist/release-e2e/types.d.ts +4 -2
  60. package/dist/resolution/classify.d.ts +1 -1
  61. package/dist/resolution/classify.js +36 -11
  62. package/dist/scope/project-definition-sync.js +2 -150
  63. package/dist/scope/registry-artifact-sync.d.ts +3 -0
  64. package/dist/scope/registry-artifact-sync.js +159 -0
  65. package/dist/scope/specification-sync.d.ts +3 -0
  66. package/dist/scope/specification-sync.js +16 -0
  67. package/dist/scope/transition.js +40 -12
  68. package/dist/session/cache-recovery.d.ts +19 -0
  69. package/dist/session/cache-recovery.js +26 -0
  70. package/dist/session/session-start.d.ts +2 -0
  71. package/dist/session/session-start.js +14 -3
  72. package/dist/session/verify-session-ritual.d.ts +1 -0
  73. package/dist/session/verify-session-ritual.js +1 -0
  74. package/dist/triage/welcome/default-mode.js +2 -0
  75. package/dist/triage/welcome/onboard.js +2 -0
  76. package/dist/ts-check-lane/run-lane.d.ts +5 -0
  77. package/dist/ts-check-lane/run-lane.js +9 -0
  78. package/dist/vbrief-reconcile/labels.d.ts +3 -0
  79. package/dist/vbrief-reconcile/labels.js +14 -0
  80. package/dist/vbrief-reconcile/main.d.ts +1 -0
  81. package/dist/vbrief-reconcile/main.js +13 -3
  82. package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
  83. package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
  84. package/dist/vbrief-reconcile/repo-guard.js +33 -0
  85. package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
  86. package/dist/vbrief-reconcile/umbrellas.js +17 -1
  87. package/dist/verify-env/agent-hooks.js +4 -3
  88. package/dist/verify-env/verify-hooks-installed.js +5 -5
  89. package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
  90. package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
  91. package/dist/vitest-runner/coverage-debt.d.ts +50 -0
  92. package/dist/vitest-runner/coverage-debt.js +155 -0
  93. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
  94. package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
  95. package/dist/xbrief-migrate/detect.js +46 -1
  96. package/dist/xbrief-migrate/migrate-project.js +19 -4
  97. package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
  98. package/dist/xbrief-migrate/migration-containment.js +37 -0
  99. package/package.json +7 -3
@@ -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() {
@@ -663,6 +722,54 @@ export function applyLifecycleFixes(vbriefDir, report, projectRoot = null) {
663
722
  }
664
723
  return [moved, skipped, failures];
665
724
  }
725
+ /** Repair completed/ files whose plan.status is not terminal (#2578). */
726
+ export function repairCompletedStatusDrift(vbriefDir, drift) {
727
+ let repaired = 0;
728
+ let skipped = 0;
729
+ const failures = [];
730
+ const completedDir = join(vbriefDir, "completed");
731
+ for (const entry of drift) {
732
+ const slash = entry.rel_path.indexOf("/");
733
+ if (slash < 0) {
734
+ failures.push(`unexpected vBRIEF path shape (no folder): ${JSON.stringify(entry.rel_path)}`);
735
+ continue;
736
+ }
737
+ const filename = entry.rel_path.slice(slash + 1);
738
+ const src = join(completedDir, filename);
739
+ let data;
740
+ try {
741
+ data = JSON.parse(readFileSync(src, "utf8"));
742
+ }
743
+ catch (exc) {
744
+ failures.push(`failed to parse ${entry.rel_path}: ${String(exc)}`);
745
+ continue;
746
+ }
747
+ const plan = (data.plan ?? {});
748
+ data.plan = plan;
749
+ const currentStatus = String(plan.status ?? "");
750
+ if (COMPLETED_TERMINAL_STATUSES.has(currentStatus)) {
751
+ skipped += 1;
752
+ continue;
753
+ }
754
+ const stamp = utcNowIso();
755
+ plan.status = "completed";
756
+ plan.updated = stamp;
757
+ const infoKey = MIGRATED_INFO_ROOT_KEY in data ? MIGRATED_INFO_ROOT_KEY : LEGACY_INFO_ROOT_KEY;
758
+ const info = (data[infoKey] ?? {});
759
+ data[infoKey] = info;
760
+ info.updated = stamp;
761
+ propagateItemStatus(plan.items, "completed", stamp);
762
+ try {
763
+ writeFileSync(src, `${JSON.stringify(data, null, 2)}\n`, "utf8");
764
+ }
765
+ catch (exc) {
766
+ failures.push(`failed to write ${entry.rel_path}: ${String(exc)}`);
767
+ continue;
768
+ }
769
+ repaired += 1;
770
+ }
771
+ return [repaired, skipped, failures];
772
+ }
666
773
  export function detectRepo() {
667
774
  try {
668
775
  const stdout = execFileSync("git", ["remote", "get-url", "origin"], {
@@ -744,6 +851,8 @@ export function reconcileMain(args) {
744
851
  report = reconcile(issueToVbriefsObj, issueStateMap);
745
852
  }
746
853
  const fmt = args.format ?? "markdown";
854
+ const completedDrift = scanCompletedStatusDrift(vbriefDir);
855
+ report = attachCompletedStatusDrift(report, completedDrift);
747
856
  if (fmt === "json") {
748
857
  process.stdout.write(formatJson(report));
749
858
  }
@@ -761,11 +870,15 @@ export function reconcileMain(args) {
761
870
  return acc;
762
871
  }, 0);
763
872
  const [moved, skipped, failures] = applyLifecycleFixes(vbriefDir, applyReport, projectRoot);
873
+ const driftAfterMove = scanCompletedStatusDrift(vbriefDir);
874
+ const [driftRepaired, driftSkipped, driftFailures] = repairCompletedStatusDrift(vbriefDir, driftAfterMove);
875
+ const allFailures = [...failures, ...driftFailures];
764
876
  process.stderr.write(`[${moved}/${candidates}] vBRIEFs reconciled (moved=${moved}, already-terminal=${skipped}, failures=${failures.length})\n`);
765
- for (const f of failures) {
877
+ process.stderr.write(`[${driftRepaired}/${completedDrift.length}] completed/ status drift repaired (repaired=${driftRepaired}, already-terminal=${driftSkipped}, failures=${driftFailures.length})\n`);
878
+ for (const f of allFailures) {
766
879
  process.stderr.write(` -- FAIL: ${f}\n`);
767
880
  }
768
- if (failures.length > 0) {
881
+ if (allFailures.length > 0) {
769
882
  return 1;
770
883
  }
771
884
  }
@@ -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
@@ -0,0 +1,46 @@
1
+ import { type RunGhApiFn } from "../intake/github-body.js";
2
+ export declare const SYNC_COMMENT_HEADER = "## xBRIEF sync (deft issue:sync-from-xbrief)";
3
+ export interface OriginIssueTarget {
4
+ readonly repo: string;
5
+ readonly number: number;
6
+ readonly uri: string;
7
+ }
8
+ export interface SyncSnapshot {
9
+ readonly status: string;
10
+ readonly title: string;
11
+ readonly acceptance: string;
12
+ readonly items: readonly {
13
+ title: string;
14
+ status: string;
15
+ }[];
16
+ }
17
+ export interface ResolveOriginOptions {
18
+ readonly fallbackRepo?: string | null;
19
+ }
20
+ export declare function resolveOriginIssue(data: Record<string, unknown>, options?: ResolveOriginOptions): OriginIssueTarget | null;
21
+ export declare function extractSyncSnapshot(data: Record<string, unknown>): SyncSnapshot;
22
+ export declare function fingerprintSyncSnapshot(snapshot: SyncSnapshot): string;
23
+ export declare function readStoredFingerprint(data: Record<string, unknown>): string | null;
24
+ export declare function hasMaterialChanges(data: Record<string, unknown>): boolean;
25
+ export declare function sanitizeMarkdownInline(text: string): string;
26
+ export declare function buildSyncComment(data: Record<string, unknown>, xbriefPath: string): string;
27
+ export interface SyncFromXbriefOptions {
28
+ readonly xbriefPath: string;
29
+ readonly dryRun?: boolean;
30
+ readonly projectRoot?: string;
31
+ readonly repo?: string;
32
+ readonly runFn?: RunGhApiFn;
33
+ readonly writeErr?: (message: string) => void;
34
+ readonly writeOut?: (message: string) => void;
35
+ readonly writeFingerprint?: (absPath: string, data: Record<string, unknown>) => void;
36
+ }
37
+ export declare function stampIssueSyncFingerprint(data: Record<string, unknown>, origin: OriginIssueTarget): Record<string, unknown>;
38
+ export declare function syncFromXbrief(options: SyncFromXbriefOptions): number;
39
+ export interface SyncFromXbriefCliArgs {
40
+ readonly path?: string;
41
+ readonly dryRun?: boolean;
42
+ readonly projectRoot?: string;
43
+ readonly repo?: string;
44
+ }
45
+ export declare function syncFromXbriefMain(args: SyncFromXbriefCliArgs): number;
46
+ //# sourceMappingURL=sync-from-xbrief.d.ts.map