@deftai/directive-core 0.77.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 (126) 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.d.ts +1 -0
  8. package/dist/doctor/main.js +48 -0
  9. package/dist/doctor/payload-staleness.js +7 -1
  10. package/dist/doctor/types.d.ts +3 -0
  11. package/dist/eval/triggers.d.ts +76 -0
  12. package/dist/eval/triggers.js +259 -0
  13. package/dist/eval-triggers-relocation/evaluate.d.ts +36 -0
  14. package/dist/eval-triggers-relocation/evaluate.js +115 -0
  15. package/dist/eval-triggers-relocation/index.d.ts +2 -0
  16. package/dist/eval-triggers-relocation/index.js +2 -0
  17. package/dist/hooks/dispatcher.d.ts +43 -0
  18. package/dist/hooks/dispatcher.js +173 -0
  19. package/dist/hooks/index.d.ts +3 -0
  20. package/dist/hooks/index.js +3 -0
  21. package/dist/hooks/scope.d.ts +12 -0
  22. package/dist/hooks/scope.js +46 -0
  23. package/dist/hooks/tools.d.ts +4 -0
  24. package/dist/hooks/tools.js +23 -0
  25. package/dist/init-deposit/agent-hooks.d.ts +22 -0
  26. package/dist/init-deposit/agent-hooks.js +238 -0
  27. package/dist/init-deposit/hygiene.js +4 -0
  28. package/dist/init-deposit/index.d.ts +1 -0
  29. package/dist/init-deposit/index.js +1 -0
  30. package/dist/init-deposit/init-deposit.js +4 -0
  31. package/dist/init-deposit/prettierignore.d.ts +21 -0
  32. package/dist/init-deposit/prettierignore.js +83 -0
  33. package/dist/init-deposit/refresh.d.ts +3 -1
  34. package/dist/init-deposit/refresh.js +31 -40
  35. package/dist/init-deposit/scaffold.js +6 -12
  36. package/dist/init-deposit/xbrief-projections.d.ts +18 -0
  37. package/dist/init-deposit/xbrief-projections.js +117 -0
  38. package/dist/intake/reconcile-issues.d.ts +11 -0
  39. package/dist/intake/reconcile-issues.js +166 -51
  40. package/dist/issue-sync/index.d.ts +2 -0
  41. package/dist/issue-sync/index.js +2 -0
  42. package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
  43. package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
  44. package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
  45. package/dist/issue-sync/sync-from-xbrief.js +208 -0
  46. package/dist/platform/index.d.ts +1 -0
  47. package/dist/platform/index.js +1 -0
  48. package/dist/platform/platform-capabilities.d.ts +5 -0
  49. package/dist/platform/platform-capabilities.js +9 -0
  50. package/dist/platform/shell-context.d.ts +30 -0
  51. package/dist/platform/shell-context.js +111 -0
  52. package/dist/pr-monitor/cadence.d.ts +6 -0
  53. package/dist/pr-monitor/cadence.js +13 -0
  54. package/dist/pr-monitor/index.d.ts +1 -1
  55. package/dist/pr-monitor/index.js +1 -1
  56. package/dist/pr-monitor/monitor.d.ts +7 -0
  57. package/dist/pr-monitor/monitor.js +59 -13
  58. package/dist/preflight-cache/evaluate.d.ts +3 -8
  59. package/dist/preflight-cache/evaluate.js +19 -19
  60. package/dist/release/build-dist.js +1 -0
  61. package/dist/release/constants.d.ts +2 -0
  62. package/dist/release/constants.js +8 -1
  63. package/dist/release/flags.js +26 -0
  64. package/dist/release/issue-state-fetch.d.ts +27 -0
  65. package/dist/release/issue-state-fetch.js +146 -0
  66. package/dist/release/main.js +1 -0
  67. package/dist/release/native-steps.js +6 -4
  68. package/dist/release/pipeline.js +15 -5
  69. package/dist/release/preflight.d.ts +6 -2
  70. package/dist/release/preflight.js +19 -5
  71. package/dist/release/types.d.ts +3 -1
  72. package/dist/release-e2e/constants.js +6 -6
  73. package/dist/release-e2e/flags.d.ts +2 -0
  74. package/dist/release-e2e/flags.js +8 -4
  75. package/dist/release-e2e/greenfield-python-free-smoke-cli.js +40 -1
  76. package/dist/release-e2e/greenfield-python-free-smoke.d.ts +2 -0
  77. package/dist/release-e2e/greenfield-python-free-smoke.js +28 -8
  78. package/dist/release-e2e/main.js +15 -9
  79. package/dist/release-e2e/types.d.ts +4 -2
  80. package/dist/resolution/classify.d.ts +1 -1
  81. package/dist/resolution/classify.js +36 -11
  82. package/dist/scope/project-definition-sync.js +2 -150
  83. package/dist/scope/registry-artifact-sync.d.ts +3 -0
  84. package/dist/scope/registry-artifact-sync.js +159 -0
  85. package/dist/scope/specification-sync.d.ts +3 -0
  86. package/dist/scope/specification-sync.js +16 -0
  87. package/dist/scope/transition.js +40 -12
  88. package/dist/session/cache-recovery.d.ts +19 -0
  89. package/dist/session/cache-recovery.js +26 -0
  90. package/dist/session/session-start.d.ts +2 -0
  91. package/dist/session/session-start.js +14 -3
  92. package/dist/session/verify-session-ritual.d.ts +17 -0
  93. package/dist/session/verify-session-ritual.js +64 -0
  94. package/dist/triage/help/registry-data.d.ts +12 -1
  95. package/dist/triage/help/registry-data.js +22 -1
  96. package/dist/triage/welcome/default-mode.js +2 -0
  97. package/dist/triage/welcome/onboard.js +2 -0
  98. package/dist/ts-check-lane/run-lane.d.ts +5 -0
  99. package/dist/ts-check-lane/run-lane.js +9 -0
  100. package/dist/vbrief-reconcile/labels.d.ts +3 -0
  101. package/dist/vbrief-reconcile/labels.js +14 -0
  102. package/dist/vbrief-reconcile/main.d.ts +1 -0
  103. package/dist/vbrief-reconcile/main.js +13 -3
  104. package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
  105. package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
  106. package/dist/vbrief-reconcile/repo-guard.js +33 -0
  107. package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
  108. package/dist/vbrief-reconcile/umbrellas.js +17 -1
  109. package/dist/verify-env/agent-hooks.d.ts +11 -0
  110. package/dist/verify-env/agent-hooks.js +46 -0
  111. package/dist/verify-env/command-spawn.d.ts +5 -0
  112. package/dist/verify-env/command-spawn.js +28 -9
  113. package/dist/verify-env/index.d.ts +1 -0
  114. package/dist/verify-env/index.js +1 -0
  115. package/dist/verify-env/verify-hooks-installed.js +5 -5
  116. package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
  117. package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
  118. package/dist/vitest-runner/coverage-debt.d.ts +50 -0
  119. package/dist/vitest-runner/coverage-debt.js +155 -0
  120. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
  121. package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
  122. package/dist/xbrief-migrate/detect.js +46 -1
  123. package/dist/xbrief-migrate/migrate-project.js +19 -4
  124. package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
  125. package/dist/xbrief-migrate/migration-containment.js +37 -0
  126. package/package.json +19 -3
@@ -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
@@ -26,13 +26,19 @@ export interface FetchIssuesOptions {
26
26
  export declare function fetchOpenIssues(repo: string, options?: FetchIssuesOptions): Record<string, unknown>[] | null;
27
27
  export declare function fetchAllOpenIssues(repo: string, options?: FetchIssuesOptions): Record<string, unknown>[] | null;
28
28
  export interface FetchIssueStatesOptions extends FetchIssuesOptions {
29
+ /** @deprecated REST fetch is per-issue; batch size is ignored (#2557 / #954). */
29
30
  readonly batchSize?: number;
30
31
  }
31
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
+ }
32
37
  export interface ReconcileReport {
33
38
  linked: Record<string, unknown>[];
34
39
  no_open_issue: Record<string, unknown>[];
35
40
  unlinked?: Record<string, unknown>[];
41
+ completed_status_drift?: CompletedStatusDriftEntry[];
36
42
  summary: Record<string, number>;
37
43
  }
38
44
  export declare function reconcile(issueToVbriefs: Map<number, string[]>, issueStateMap: Map<number, IssueState | string>): ReconcileReport;
@@ -42,10 +48,15 @@ export declare function parseParentIssue(data: Record<string, unknown>): number
42
48
  export declare function parseDecompositionOrigin(data: Record<string, unknown>): number | null;
43
49
  export declare function resolveLifecycleAnchor(data: Record<string, unknown>): [number | null, string];
44
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;
45
54
  export declare function buildLifecycleReport(anchors: Record<string, unknown>[], issueStateMap: Map<number, IssueState | string>, log?: boolean): ReconcileReport;
46
55
  export declare function formatJson(report: ReconcileReport): string;
47
56
  export declare function formatMarkdown(report: ReconcileReport): string;
48
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[]];
49
60
  export declare function detectRepo(): string | null;
50
61
  export interface ReconcileCliArgs {
51
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",
@@ -188,6 +189,52 @@ function splitRepoSlug(repo) {
188
189
  }
189
190
  return [parts[0], parts[1]];
190
191
  }
192
+ function normalizeRestIssueState(raw) {
193
+ if (typeof raw !== "string" || raw.length === 0) {
194
+ return "NOT_FOUND";
195
+ }
196
+ return raw.toUpperCase();
197
+ }
198
+ function normalizeRestStateReason(raw) {
199
+ if (typeof raw !== "string" || raw.length === 0) {
200
+ return null;
201
+ }
202
+ return raw.toUpperCase();
203
+ }
204
+ function isRestNotFoundResult(result) {
205
+ const stderr = result.stderr.trim();
206
+ return (stderr.includes("404") ||
207
+ stderr.includes("Not Found") ||
208
+ stderr.includes("Could not resolve to an Issue"));
209
+ }
210
+ function issueStateFromRestPayload(payload) {
211
+ return new IssueState(normalizeRestIssueState(payload.state), normalizeRestStateReason(payload.state_reason));
212
+ }
213
+ function fetchOneIssueStateRest(owner, name, issueNumber, scmCall, cwd) {
214
+ let result;
215
+ try {
216
+ result = scmCall("github-issue", "api", [`repos/${owner}/${name}/issues/${issueNumber}`], {
217
+ timeout: 30,
218
+ cwd,
219
+ });
220
+ }
221
+ catch {
222
+ return "CLI_MISSING";
223
+ }
224
+ if (result.returncode !== 0) {
225
+ if (isRestNotFoundResult(result)) {
226
+ return new IssueState("NOT_FOUND", null);
227
+ }
228
+ process.stderr.write(`Error: gh REST failed fetching issue #${issueNumber}: ${result.stderr.trim()}\n`);
229
+ return null;
230
+ }
231
+ try {
232
+ return issueStateFromRestPayload(JSON.parse(result.stdout));
233
+ }
234
+ catch {
235
+ return null;
236
+ }
237
+ }
191
238
  export function fetchIssueStates(repo, issueNumbers, options = {}) {
192
239
  if (issueNumbers.size === 0) {
193
240
  return new Map();
@@ -198,63 +245,19 @@ export function fetchIssueStates(repo, issueNumbers, options = {}) {
198
245
  return null;
199
246
  }
200
247
  const [owner, name] = parsed;
201
- const batchSize = options.batchSize ?? GRAPHQL_BATCH_SIZE;
202
248
  const sortedNumbers = [...issueNumbers].sort((a, b) => a - b);
203
249
  const states = new Map();
204
250
  const scmCall = options.scmCall ?? call;
205
- for (let start = 0; start < sortedNumbers.length; start += batchSize) {
206
- const batch = sortedNumbers.slice(start, start + batchSize);
207
- const aliases = batch
208
- .map((n) => `i${n}: issue(number: ${n}) { state stateReason }`)
209
- .join("\n ");
210
- const query = `query {\n repository(owner: "${owner}", name: "${name}") {\n ${aliases}\n }\n}\n`;
211
- let result;
212
- try {
213
- result = scmCall("github-issue", "api", ["graphql", "-f", `query=${query}`], {
214
- timeout: 60,
215
- cwd: options.cwd ?? undefined,
216
- });
217
- }
218
- catch {
251
+ for (const n of sortedNumbers) {
252
+ const fetched = fetchOneIssueStateRest(owner, name, n, scmCall, options.cwd ?? undefined);
253
+ if (fetched === "CLI_MISSING") {
219
254
  process.stderr.write("Error: gh CLI not found. Install GitHub CLI.\n");
220
255
  return null;
221
256
  }
222
- let payload = null;
223
- try {
224
- payload = result.stdout ? JSON.parse(result.stdout) : null;
225
- }
226
- catch {
227
- payload = null;
228
- }
229
- if (result.returncode !== 0) {
230
- if (payload === null || typeof payload.data !== "object" || payload.data === null) {
231
- process.stderr.write(`Error: gh CLI failed: ${result.stderr.trim()}\n`);
232
- return null;
233
- }
234
- const firstLine = result.stderr.trim().split("\n")[0] ?? "";
235
- process.stderr.write(`Warning: gh GraphQL returned partial errors (likely PR numbers referenced as issues): ${firstLine}\n`);
236
- }
237
- if (payload === null) {
238
- process.stderr.write("Error: failed to parse gh CLI graphql output.\n");
239
- return null;
240
- }
241
- const repoData = payload.data?.repository;
242
- if (repoData === null || typeof repoData !== "object" || Array.isArray(repoData)) {
243
- process.stderr.write("Error: gh CLI graphql response missing repository payload.\n");
257
+ if (fetched === null) {
244
258
  return null;
245
259
  }
246
- for (const n of batch) {
247
- const node = repoData[`i${n}`];
248
- if (node !== null && typeof node === "object" && !Array.isArray(node)) {
249
- const nodeObj = node;
250
- if (typeof nodeObj.state === "string") {
251
- const reason = typeof nodeObj.stateReason === "string" ? nodeObj.stateReason : null;
252
- states.set(n, new IssueState(nodeObj.state, reason));
253
- continue;
254
- }
255
- }
256
- states.set(n, new IssueState("NOT_FOUND", null));
257
- }
260
+ states.set(n, fetched);
258
261
  }
259
262
  return states;
260
263
  }
@@ -433,6 +436,52 @@ export function scanLifecycleAnchors(vbriefDir) {
433
436
  }
434
437
  return anchors;
435
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
+ }
436
485
  export function buildLifecycleReport(anchors, issueStateMap, log = true) {
437
486
  const linked = [];
438
487
  const noOpenIssue = [];
@@ -525,6 +574,18 @@ export function formatMarkdown(report) {
525
574
  lines.push("None.");
526
575
  }
527
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("");
528
589
  return lines.join("\n");
529
590
  }
530
591
  function utcNowIso() {
@@ -661,6 +722,54 @@ export function applyLifecycleFixes(vbriefDir, report, projectRoot = null) {
661
722
  }
662
723
  return [moved, skipped, failures];
663
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
+ }
664
773
  export function detectRepo() {
665
774
  try {
666
775
  const stdout = execFileSync("git", ["remote", "get-url", "origin"], {
@@ -742,6 +851,8 @@ export function reconcileMain(args) {
742
851
  report = reconcile(issueToVbriefsObj, issueStateMap);
743
852
  }
744
853
  const fmt = args.format ?? "markdown";
854
+ const completedDrift = scanCompletedStatusDrift(vbriefDir);
855
+ report = attachCompletedStatusDrift(report, completedDrift);
745
856
  if (fmt === "json") {
746
857
  process.stdout.write(formatJson(report));
747
858
  }
@@ -759,11 +870,15 @@ export function reconcileMain(args) {
759
870
  return acc;
760
871
  }, 0);
761
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];
762
876
  process.stderr.write(`[${moved}/${candidates}] vBRIEFs reconciled (moved=${moved}, already-terminal=${skipped}, failures=${failures.length})\n`);
763
- 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) {
764
879
  process.stderr.write(` -- FAIL: ${f}\n`);
765
880
  }
766
- if (failures.length > 0) {
881
+ if (allFailures.length > 0) {
767
882
  return 1;
768
883
  }
769
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