@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
@@ -3,7 +3,8 @@ export interface E2EConfig {
3
3
  owner: string;
4
4
  projectRoot: string;
5
5
  dryRun: boolean;
6
- keepRepo: boolean;
6
+ /** When true, attempt gh repo delete after rehearsal (#2572 opt-in privileged mode). */
7
+ destroyRepo: boolean;
7
8
  /** When true, skip the npm publish dry-run rehearsal step (#1910). */
8
9
  skipNpm: boolean;
9
10
  /**
@@ -20,7 +21,8 @@ export interface ParsedE2EFlags {
20
21
  help: boolean;
21
22
  owner: string;
22
23
  dryRun: boolean;
23
- keepRepo: boolean;
24
+ /** When true, attempt gh repo delete after rehearsal (#2572 opt-in privileged mode). */
25
+ destroyRepo: boolean;
24
26
  projectRoot: string | null;
25
27
  skipNpm: boolean;
26
28
  /** Opt-in: run the pinned legacy->bridge->npm-hybrid migration leg (#1912). */
@@ -30,7 +30,7 @@ export interface ClassifySeams {
30
30
  /** Pre-cutover probe; defaults to the shared `detectPreCutover` detector. */
31
31
  readonly preCutoverProbe?: (cwd: string) => boolean;
32
32
  }
33
- /** Default engine probe: try `directive --version`, then `deft --version`. */
33
+ /** Default engine probe: in-process identity, then `directive` / `deft` on PATH. */
34
34
  export declare function defaultEngineProbe(): EngineProbeResult;
35
35
  /**
36
36
  * Classify a project directory into the orthogonal resolution fact-set. All I/O
@@ -11,13 +11,16 @@
11
11
  * - deposited payload version-> `../doctor/manifest.ts` + `../init-deposit/constants.ts`
12
12
  * - committed pin -> `./pin.ts`
13
13
  */
14
- import { execFileSync } from "node:child_process";
15
14
  import { existsSync, readFileSync, statSync } from "node:fs";
16
- import { join } from "node:path";
15
+ import { dirname, join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
17
  import { locateManifest, manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
18
+ import { readCorePackageVersion } from "../engine-version.js";
18
19
  import { CANONICAL_INSTALL_ROOT } from "../init-deposit/constants.js";
19
20
  import { extractManagedSection, parseManagedSectionAttrs } from "../platform/agents-md.js";
21
+ import { ENV_VAR } from "../platform/constants.js";
20
22
  import { detectPreCutover } from "../vbrief-validate/precutover.js";
23
+ import { resolveCommandOnPath, spawnCommandText } from "../verify-env/command-spawn.js";
21
24
  import { readPin } from "./pin.js";
22
25
  /** App-source markers used for the `hasAppCode` heuristic. */
23
26
  const APP_CODE_MARKERS = [
@@ -57,22 +60,44 @@ function defaultReadText(p) {
57
60
  return null;
58
61
  }
59
62
  }
60
- function probeEngineVersion(binary) {
63
+ function parseVersionFromOutput(out) {
64
+ const match = SEMVER_IN_TEXT_RE.exec(out);
65
+ return match?.[1] ?? null;
66
+ }
67
+ /**
68
+ * Prefer the engine version from the already-loaded core package when doctor /
69
+ * update are executing inside `@deftai/directive` (#2606).
70
+ */
71
+ function readInProcessEngineVersion() {
72
+ const envVersion = process.env[ENV_VAR]?.trim();
73
+ if (envVersion) {
74
+ return parseVersionFromOutput(envVersion) ?? envVersion;
75
+ }
61
76
  try {
62
- const out = execFileSync(binary, ["--version"], {
63
- encoding: "utf8",
64
- timeout: 5000,
65
- stdio: ["ignore", "pipe", "ignore"],
66
- });
67
- const match = SEMVER_IN_TEXT_RE.exec(out);
68
- return match?.[1] ?? null;
77
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
78
+ if (!existsSync(pkgPath)) {
79
+ return null;
80
+ }
81
+ return readCorePackageVersion();
69
82
  }
70
83
  catch {
71
84
  return null;
72
85
  }
73
86
  }
74
- /** Default engine probe: try `directive --version`, then `deft --version`. */
87
+ function probeEngineVersion(binary) {
88
+ const resolved = resolveCommandOnPath(binary);
89
+ const result = spawnCommandText(resolved ?? binary, ["--version"], { timeoutMs: 5000 });
90
+ if (result.status !== 0) {
91
+ return null;
92
+ }
93
+ return parseVersionFromOutput(result.stdout);
94
+ }
95
+ /** Default engine probe: in-process identity, then `directive` / `deft` on PATH. */
75
96
  export function defaultEngineProbe() {
97
+ const inProcess = readInProcessEngineVersion();
98
+ if (inProcess !== null) {
99
+ return { reachable: true, version: inProcess };
100
+ }
76
101
  for (const binary of ["directive", "deft"]) {
77
102
  const version = probeEngineVersion(binary);
78
103
  if (version !== null) {
@@ -1,158 +1,10 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
1
  import { dirname, resolve } from "node:path";
3
- import { referenceTypeMatches } from "@deftai/directive-types";
4
2
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
5
- import { formatVbriefJson } from "./vbrief-json.js";
6
- import { relativeToVbrief, resolveVbriefRef, scopeIdsForFilename } from "./vbrief-ref.js";
7
- function rewriteProjectDefinitionPlanReference(ref, oldResolved, newRel, vbriefRoot) {
8
- if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
9
- return false;
10
- }
11
- const r = ref;
12
- if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
13
- return false;
14
- }
15
- const resolved = resolveVbriefRef(r.uri, vbriefRoot);
16
- if (resolved === null || resolve(resolved) !== resolve(oldResolved)) {
17
- return false;
18
- }
19
- const uri = r.uri;
20
- const newUri = typeof uri === "string" && uri.startsWith("file://") ? `file://${newRel}` : newRel;
21
- if (newUri === uri) {
22
- return false;
23
- }
24
- r.uri = newUri;
25
- return true;
26
- }
27
- function projectItemReferencesScope(item, oldResolved, newResolved, vbriefRoot) {
28
- const metadata = item.metadata;
29
- if (typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)) {
30
- const meta = metadata;
31
- const sourcePath = meta.source_path;
32
- if (typeof sourcePath === "string") {
33
- const resolved = resolveVbriefRef(sourcePath, vbriefRoot);
34
- if (resolved !== null && [oldResolved, newResolved].includes(resolve(resolved))) {
35
- return true;
36
- }
37
- }
38
- const metadataRefs = meta.references;
39
- if (Array.isArray(metadataRefs)) {
40
- for (const ref of metadataRefs) {
41
- if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
42
- continue;
43
- }
44
- const r = ref;
45
- if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
46
- continue;
47
- }
48
- const resolved = resolveVbriefRef(r.uri, vbriefRoot);
49
- if (resolved !== null && [oldResolved, newResolved].includes(resolve(resolved))) {
50
- return true;
51
- }
52
- }
53
- }
54
- }
55
- const refs = item.references;
56
- if (Array.isArray(refs)) {
57
- for (const ref of refs) {
58
- if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
59
- continue;
60
- }
61
- const r = ref;
62
- if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
63
- continue;
64
- }
65
- const resolved = resolveVbriefRef(r.uri, vbriefRoot);
66
- if (resolved !== null && [oldResolved, newResolved].includes(resolve(resolved))) {
67
- return true;
68
- }
69
- }
70
- }
71
- return false;
72
- }
73
- function projectItemMatchesScope(item, scopeData, oldPath, newPath, vbriefRoot) {
74
- const oldResolved = resolve(oldPath);
75
- const newResolved = resolve(newPath);
76
- if (projectItemReferencesScope(item, oldResolved, newResolved, vbriefRoot)) {
77
- return true;
78
- }
79
- const itemId = item.id;
80
- if (typeof itemId === "string" &&
81
- scopeIdsForFilename(resolve(newPath).split(/[/\\]/).pop() ?? "").has(itemId)) {
82
- return true;
83
- }
84
- const scopePlan = scopeData.plan;
85
- const scopeTitle = typeof scopePlan === "object" && scopePlan !== null && !Array.isArray(scopePlan)
86
- ? scopePlan.title
87
- : undefined;
88
- const itemTitle = item.title;
89
- return (typeof scopeTitle === "string" && typeof itemTitle === "string" && itemTitle === scopeTitle);
90
- }
3
+ import { syncRegistryArtifactAfterScopeMove } from "./registry-artifact-sync.js";
91
4
  /** Best-effort sync of PROJECT-DEFINITION after a lifecycle move (#1527). */
92
5
  export function syncProjectDefinitionAfterScopeMove(scopeData, oldPath, newPath, vbriefRoot, targetStatus) {
93
- const newRel = relativeToVbrief(newPath, vbriefRoot);
94
- if (newRel === null) {
95
- return;
96
- }
97
6
  const projectRoot = dirname(resolve(vbriefRoot));
98
7
  const projectDefPath = resolveProjectDefinitionPath(projectRoot);
99
- if (!existsSync(projectDefPath)) {
100
- return;
101
- }
102
- try {
103
- const projectDef = JSON.parse(readFileSync(projectDefPath, "utf8"));
104
- const plan = projectDef.plan;
105
- if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
106
- return;
107
- }
108
- const p = plan;
109
- let changed = false;
110
- const oldResolved = resolve(oldPath);
111
- const refs = p.references;
112
- if (Array.isArray(refs)) {
113
- for (const ref of refs) {
114
- if (rewriteProjectDefinitionPlanReference(ref, oldResolved, newRel, vbriefRoot)) {
115
- changed = true;
116
- }
117
- }
118
- }
119
- const items = p.items;
120
- if (Array.isArray(items)) {
121
- for (const item of items) {
122
- if (typeof item !== "object" || item === null || Array.isArray(item)) {
123
- continue;
124
- }
125
- const i = item;
126
- if (!projectItemMatchesScope(i, scopeData, oldPath, newPath, vbriefRoot)) {
127
- continue;
128
- }
129
- if (i.status !== targetStatus) {
130
- i.status = targetStatus;
131
- changed = true;
132
- }
133
- let metadata = i.metadata;
134
- if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
135
- metadata = {};
136
- i.metadata = metadata;
137
- }
138
- const meta = metadata;
139
- const targetFolder = dirname(resolve(newPath)).split(/[/\\]/).pop() ?? "";
140
- if (meta.source_path !== newRel) {
141
- meta.source_path = newRel;
142
- changed = true;
143
- }
144
- if (meta.lifecycle_folder !== targetFolder) {
145
- meta.lifecycle_folder = targetFolder;
146
- changed = true;
147
- }
148
- }
149
- }
150
- if (changed) {
151
- writeFileSync(projectDefPath, formatVbriefJson(projectDef), "utf8");
152
- }
153
- }
154
- catch {
155
- /* best-effort */
156
- }
8
+ syncRegistryArtifactAfterScopeMove(projectDefPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus);
157
9
  }
158
10
  //# sourceMappingURL=project-definition-sync.js.map
@@ -0,0 +1,3 @@
1
+ /** Best-effort sync of a registry artifact (PROJECT-DEFINITION, specification, etc.) after a lifecycle move. */
2
+ export declare function syncRegistryArtifactAfterScopeMove(registryPath: string, scopeData: Record<string, unknown>, oldPath: string, newPath: string, vbriefRoot: string, targetStatus: string): void;
3
+ //# sourceMappingURL=registry-artifact-sync.d.ts.map
@@ -0,0 +1,159 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { referenceTypeMatches } from "@deftai/directive-types";
4
+ import { formatVbriefJson } from "./vbrief-json.js";
5
+ import { relativeToVbrief, resolveVbriefRef, scopeIdsForFilename } from "./vbrief-ref.js";
6
+ function rewriteRegistryPlanReference(ref, oldResolved, newRel, vbriefRoot) {
7
+ if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
8
+ return false;
9
+ }
10
+ const r = ref;
11
+ if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
12
+ return false;
13
+ }
14
+ const resolved = resolveVbriefRef(r.uri, vbriefRoot);
15
+ if (resolved === null || resolve(resolved) !== resolve(oldResolved)) {
16
+ return false;
17
+ }
18
+ const uri = r.uri;
19
+ const newUri = typeof uri === "string" && uri.startsWith("file://") ? `file://${newRel}` : newRel;
20
+ if (newUri === uri) {
21
+ return false;
22
+ }
23
+ r.uri = newUri;
24
+ return true;
25
+ }
26
+ function registryItemReferencesScope(item, oldResolved, newResolved, vbriefRoot) {
27
+ const metadata = item.metadata;
28
+ if (typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)) {
29
+ const meta = metadata;
30
+ const sourcePath = meta.source_path;
31
+ if (typeof sourcePath === "string") {
32
+ const resolved = resolveVbriefRef(sourcePath, vbriefRoot);
33
+ if (resolved !== null && [oldResolved, newResolved].includes(resolve(resolved))) {
34
+ return true;
35
+ }
36
+ }
37
+ const metadataRefs = meta.references;
38
+ if (Array.isArray(metadataRefs)) {
39
+ for (const ref of metadataRefs) {
40
+ if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
41
+ continue;
42
+ }
43
+ const r = ref;
44
+ if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
45
+ continue;
46
+ }
47
+ const resolved = resolveVbriefRef(r.uri, vbriefRoot);
48
+ if (resolved !== null && [oldResolved, newResolved].includes(resolve(resolved))) {
49
+ return true;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ const refs = item.references;
55
+ if (Array.isArray(refs)) {
56
+ for (const ref of refs) {
57
+ if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
58
+ continue;
59
+ }
60
+ const r = ref;
61
+ if (!referenceTypeMatches(String(r.type ?? ""), "plan")) {
62
+ continue;
63
+ }
64
+ const resolved = resolveVbriefRef(r.uri, vbriefRoot);
65
+ if (resolved !== null && [oldResolved, newResolved].includes(resolve(resolved))) {
66
+ return true;
67
+ }
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+ function registryItemMatchesScope(item, scopeData, oldPath, newPath, vbriefRoot) {
73
+ const oldResolved = resolve(oldPath);
74
+ const newResolved = resolve(newPath);
75
+ if (registryItemReferencesScope(item, oldResolved, newResolved, vbriefRoot)) {
76
+ return true;
77
+ }
78
+ const itemId = item.id;
79
+ if (typeof itemId === "string" &&
80
+ scopeIdsForFilename(resolve(newPath).split(/[/\\]/).pop() ?? "").has(itemId)) {
81
+ return true;
82
+ }
83
+ const scopePlan = scopeData.plan;
84
+ const scopeTitle = typeof scopePlan === "object" && scopePlan !== null && !Array.isArray(scopePlan)
85
+ ? scopePlan.title
86
+ : undefined;
87
+ const itemTitle = item.title;
88
+ return (typeof scopeTitle === "string" && typeof itemTitle === "string" && itemTitle === scopeTitle);
89
+ }
90
+ /** Best-effort sync of a registry artifact (PROJECT-DEFINITION, specification, etc.) after a lifecycle move. */
91
+ export function syncRegistryArtifactAfterScopeMove(registryPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus) {
92
+ const newRel = relativeToVbrief(newPath, vbriefRoot);
93
+ if (newRel === null) {
94
+ return;
95
+ }
96
+ if (!existsSync(registryPath)) {
97
+ return;
98
+ }
99
+ try {
100
+ const parsed = JSON.parse(readFileSync(registryPath, "utf8"));
101
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
102
+ return;
103
+ }
104
+ const registry = parsed;
105
+ const plan = registry.plan;
106
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
107
+ return;
108
+ }
109
+ const p = plan;
110
+ let changed = false;
111
+ const oldResolved = resolve(oldPath);
112
+ const refs = p.references;
113
+ if (Array.isArray(refs)) {
114
+ for (const ref of refs) {
115
+ if (rewriteRegistryPlanReference(ref, oldResolved, newRel, vbriefRoot)) {
116
+ changed = true;
117
+ }
118
+ }
119
+ }
120
+ const items = p.items;
121
+ if (Array.isArray(items)) {
122
+ for (const item of items) {
123
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
124
+ continue;
125
+ }
126
+ const i = item;
127
+ if (!registryItemMatchesScope(i, scopeData, oldPath, newPath, vbriefRoot)) {
128
+ continue;
129
+ }
130
+ if (i.status !== targetStatus) {
131
+ i.status = targetStatus;
132
+ changed = true;
133
+ }
134
+ let metadata = i.metadata;
135
+ if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
136
+ metadata = {};
137
+ i.metadata = metadata;
138
+ }
139
+ const meta = metadata;
140
+ const targetFolder = dirname(resolve(newPath)).split(/[/\\]/).pop() ?? "";
141
+ if (meta.source_path !== newRel) {
142
+ meta.source_path = newRel;
143
+ changed = true;
144
+ }
145
+ if (meta.lifecycle_folder !== targetFolder) {
146
+ meta.lifecycle_folder = targetFolder;
147
+ changed = true;
148
+ }
149
+ }
150
+ }
151
+ if (changed) {
152
+ writeFileSync(registryPath, formatVbriefJson(registry), "utf8");
153
+ }
154
+ }
155
+ catch {
156
+ /* best-effort */
157
+ }
158
+ }
159
+ //# sourceMappingURL=registry-artifact-sync.js.map
@@ -0,0 +1,3 @@
1
+ /** Best-effort sync of specification.xbrief.json after a lifecycle move (#2566). */
2
+ export declare function syncSpecificationAfterScopeMove(scopeData: Record<string, unknown>, oldPath: string, newPath: string, vbriefRoot: string, targetStatus: string): void;
3
+ //# sourceMappingURL=specification-sync.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { resolveSpecArtifactPath } from "../layout/resolve.js";
3
+ import { syncRegistryArtifactAfterScopeMove } from "./registry-artifact-sync.js";
4
+ /** Best-effort sync of specification.xbrief.json after a lifecycle move (#2566). */
5
+ export function syncSpecificationAfterScopeMove(scopeData, oldPath, newPath, vbriefRoot, targetStatus) {
6
+ const projectRoot = dirname(resolve(vbriefRoot));
7
+ let specPath;
8
+ try {
9
+ specPath = resolveSpecArtifactPath(projectRoot);
10
+ }
11
+ catch {
12
+ return;
13
+ }
14
+ syncRegistryArtifactAfterScopeMove(specPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus);
15
+ }
16
+ //# sourceMappingURL=specification-sync.js.map
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { InstrumentedVbriefCrud, persistCrudMetrics } from "../eval/crud-telemetry.js";
4
4
  import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
@@ -8,6 +8,7 @@ import { stampCompletionMetadata } from "./capacity-stamp.js";
8
8
  import { LIFECYCLE_FOLDERS, MOVE_LABELS, STATUS_PRECONDITIONS, STAY_LABELS, TRANSITIONS, } from "./constants.js";
9
9
  import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecomposedParentBackReferences, } from "./decomposed-refs.js";
10
10
  import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
11
+ import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
11
12
  import { formatVbriefJson, utcNowIso } from "./vbrief-json.js";
12
13
  export function runTransition(action, filePath, now = new Date()) {
13
14
  if (!(action in TRANSITIONS)) {
@@ -101,30 +102,57 @@ export function runTransition(action, filePath, now = new Date()) {
101
102
  }
102
103
  const formatted = formatVbriefJson(data);
103
104
  const crud = new InstrumentedVbriefCrud({ now: () => now });
104
- const writeResult = crud.update(resolvedPath, formatted, { trustedWrite: true });
105
- if (!writeResult.ok) {
106
- return { ok: false, message: writeResult.error ?? `CRUD update failed for ${resolvedPath}` };
107
- }
108
- try {
109
- persistCrudMetrics(projectRoot, crud.getMetrics());
110
- }
111
- catch {
112
- /* best-effort telemetry persistence */
113
- }
114
105
  if (targetFolder !== null) {
115
106
  const destDir = join(vbriefRoot, targetFolder);
116
107
  mkdirSync(destDir, { recursive: true });
117
108
  const destPath = join(destDir, basename);
118
- renameSync(resolvedPath, destPath);
109
+ if (existsSync(destPath)) {
110
+ return { ok: false, message: `Target already exists: ${destPath}` };
111
+ }
112
+ // #2578: stamp terminal status at the destination path in the same write as
113
+ // folder placement — never leave a non-terminal status under completed/.
114
+ const writeResult = crud.update(destPath, formatted, { trustedWrite: true });
115
+ if (!writeResult.ok) {
116
+ return { ok: false, message: writeResult.error ?? `CRUD update failed for ${destPath}` };
117
+ }
118
+ try {
119
+ unlinkSync(resolvedPath);
120
+ }
121
+ catch (err) {
122
+ try {
123
+ unlinkSync(destPath);
124
+ }
125
+ catch {
126
+ /* best-effort rollback */
127
+ }
128
+ return { ok: false, message: `Failed to remove source after move: ${String(err)}` };
129
+ }
130
+ try {
131
+ persistCrudMetrics(projectRoot, crud.getMetrics());
132
+ }
133
+ catch {
134
+ /* best-effort telemetry persistence */
135
+ }
119
136
  updateDecomposedParentBackReferences(data, resolvedPath, destPath, vbriefRoot);
120
137
  updateDecomposedChildBackReferences(data, resolvedPath, destPath, vbriefRoot);
121
138
  syncProjectDefinitionAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
139
+ syncSpecificationAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
122
140
  const actionLabel = MOVE_LABELS[act] ?? act.charAt(0).toUpperCase() + act.slice(1);
123
141
  return {
124
142
  ok: true,
125
143
  message: `${actionLabel} ${basename}: ${currentFolder}/ -> ${targetFolder}/ (status: ${targetStatus})`,
126
144
  };
127
145
  }
146
+ const writeResult = crud.update(resolvedPath, formatted, { trustedWrite: true });
147
+ if (!writeResult.ok) {
148
+ return { ok: false, message: writeResult.error ?? `CRUD update failed for ${resolvedPath}` };
149
+ }
150
+ try {
151
+ persistCrudMetrics(projectRoot, crud.getMetrics());
152
+ }
153
+ catch {
154
+ /* best-effort telemetry persistence */
155
+ }
128
156
  const actionLabel = STAY_LABELS[act] ?? act.charAt(0).toUpperCase() + act.slice(1);
129
157
  return {
130
158
  ok: true,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Format a consumer-runnable `deft cache fetch-all` recovery command (#2574).
3
+ *
4
+ * Uses the space-separated CLI surface (not the colon task alias) and always
5
+ * includes required `--source` / `--repo` flags so following the hint literally
6
+ * reaches the handler instead of `unknown verb 'cache:fetch-all'`.
7
+ */
8
+ export declare function formatCacheFetchAllRecoveryCommand(repo: string | null, options?: {
9
+ force?: boolean;
10
+ }): string;
11
+ /**
12
+ * Branch-aware recovery hint (#1953 Option 3, #2574 argv fix).
13
+ * Age-stale (or age+drift mixed) → --force bypasses TTL; drift-only → plain refetch.
14
+ */
15
+ export declare function recoveryHintForStaleFailure(causes: Readonly<{
16
+ ageStale: boolean;
17
+ driftDetected: boolean;
18
+ }>, repo?: string | null): string;
19
+ //# sourceMappingURL=cache-recovery.d.ts.map
@@ -0,0 +1,26 @@
1
+ import { formatFrameworkCommand } from "../render/framework-commands.js";
2
+ const CACHE_FETCH_SOURCE = "github-issue";
3
+ /**
4
+ * Format a consumer-runnable `deft cache fetch-all` recovery command (#2574).
5
+ *
6
+ * Uses the space-separated CLI surface (not the colon task alias) and always
7
+ * includes required `--source` / `--repo` flags so following the hint literally
8
+ * reaches the handler instead of `unknown verb 'cache:fetch-all'`.
9
+ */
10
+ export function formatCacheFetchAllRecoveryCommand(repo, options = {}) {
11
+ const repoSlug = repo ?? "OWNER/NAME";
12
+ const args = ["cache", "fetch-all", "--source", CACHE_FETCH_SOURCE, "--repo", repoSlug];
13
+ if (options.force) {
14
+ args.push("--force");
15
+ }
16
+ return formatFrameworkCommand(args);
17
+ }
18
+ /**
19
+ * Branch-aware recovery hint (#1953 Option 3, #2574 argv fix).
20
+ * Age-stale (or age+drift mixed) → --force bypasses TTL; drift-only → plain refetch.
21
+ */
22
+ export function recoveryHintForStaleFailure(causes, repo = null) {
23
+ const command = formatCacheFetchAllRecoveryCommand(repo, { force: causes.ageStale });
24
+ return ` Recovery: run \`${command}\` to refresh and reconcile upstream state.`;
25
+ }
26
+ //# sourceMappingURL=cache-recovery.js.map
@@ -1,3 +1,4 @@
1
+ import { type EnvironmentContext } from "../platform/shell-context.js";
1
2
  import { type ResolveUserMdResult } from "../user-config/resolve-user-md.js";
2
3
  import type { GitRunner } from "./git.js";
3
4
  import { ritualStatePath } from "./ritual-sentinel.js";
@@ -40,6 +41,7 @@ export interface SessionStartOptions {
40
41
  exitCode: number;
41
42
  };
42
43
  readonly resolveUserMd?: (projectRoot: string) => ResolveUserMdResult;
44
+ readonly probeEnvironment?: () => EnvironmentContext;
43
45
  }
44
46
  export declare function parseDeferrals(rawValues: readonly string[]): {
45
47
  deferrals: Record<string, string>;
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { runningInsideDeftRepo } from "../doctor/paths.js";
3
3
  import { emitSessionEvalReadback } from "../eval/readback.js";
4
4
  import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposit/migrate.js";
5
+ import { detectEnvironmentContext, environmentContextToDict, formatEnvironmentContext, } from "../platform/shell-context.js";
5
6
  import { disclosureLine } from "../policy/disclosure.js";
6
7
  import { resolvePolicy } from "../policy/resolve.js";
7
8
  import { runDefaultMode } from "../triage/welcome/default-mode.js";
@@ -187,7 +188,7 @@ export function defaultBranchSync(projectRoot, runGit = defaultGitRunner) {
187
188
  }
188
189
  return { branch, upstream, ahead, behind, warning };
189
190
  }
190
- function runReadOnlySessionStart(projectRoot, options, instant) {
191
+ function runReadOnlySessionStart(projectRoot, options, instant, environment) {
191
192
  const lines = [];
192
193
  const resolveUserMd = options.resolveUserMd ?? ((root) => resolveUserMdPath({ projectRoot: root }));
193
194
  const userMd = resolveUserMd(projectRoot);
@@ -198,6 +199,7 @@ function runReadOnlySessionStart(projectRoot, options, instant) {
198
199
  : safeDiagnostic;
199
200
  lines.push(READ_ONLY_ALIGNMENT_MESSAGE);
200
201
  lines.push(userMdLine);
202
+ lines.push(formatEnvironmentContext(environment));
201
203
  const resultPayload = {
202
204
  ready: true,
203
205
  exit_code: 0,
@@ -217,6 +219,7 @@ function runReadOnlySessionStart(projectRoot, options, instant) {
217
219
  found: userMd.found,
218
220
  diagnostic: userMd.diagnostic,
219
221
  },
222
+ environment: environmentContextToDict(environment),
220
223
  message: READ_ONLY_RESULT_MESSAGE,
221
224
  };
222
225
  return { code: 0, payload: resultPayload, lines };
@@ -226,16 +229,22 @@ export function runSessionStart(projectRoot, options = {}) {
226
229
  const instant = options.now ?? new Date();
227
230
  const deferrals = options.deferrals ?? {};
228
231
  const runGit = options.runGit ?? defaultGitRunner;
232
+ const environment = (options.probeEnvironment ?? detectEnvironmentContext)();
229
233
  if (posture === READ_ONLY_POSTURE) {
230
- return runReadOnlySessionStart(projectRoot, options, instant);
234
+ return runReadOnlySessionStart(projectRoot, options, instant, environment);
231
235
  }
232
236
  const { head: gitHeadValue, error: gitError } = gitHead(projectRoot, runGit);
233
237
  if (gitHeadValue === null) {
234
238
  const payload = {
235
239
  ready: false,
240
+ environment: environmentContextToDict(environment),
236
241
  message: gitError ?? "could not resolve git HEAD",
237
242
  };
238
- return { code: 2, payload, lines: [payload.message] };
243
+ return {
244
+ code: 2,
245
+ payload,
246
+ lines: [formatEnvironmentContext(environment), payload.message],
247
+ };
239
248
  }
240
249
  const quickSteps = recordDeferredSteps(QUICK_STEPS, deferrals, instant);
241
250
  const gatedSteps = recordDeferredSteps(GATED_STEPS, deferrals, instant);
@@ -265,6 +274,7 @@ export function runSessionStart(projectRoot, options = {}) {
265
274
  lines.push(message);
266
275
  lines.push(userMdLine);
267
276
  }
277
+ lines.push(formatEnvironmentContext(environment));
268
278
  if (!quickSteps.branch_policy) {
269
279
  const result = resolvePolicy(projectRoot);
270
280
  const message = disclosureLine(result);
@@ -377,6 +387,7 @@ export function runSessionStart(projectRoot, options = {}) {
377
387
  found: userMd.found,
378
388
  diagnostic: userMd.diagnostic,
379
389
  },
390
+ environment: environmentContextToDict(environment),
380
391
  message: code === 0 ? "session ritual recorded" : "session ritual failed",
381
392
  };
382
393
  return { code, payload: resultPayload, lines };