@deftai/directive-core 0.68.0 → 0.69.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 (59) hide show
  1. package/dist/doctor/constants.d.ts +9 -0
  2. package/dist/doctor/constants.js +11 -0
  3. package/dist/doctor/main.d.ts +47 -1
  4. package/dist/doctor/main.js +352 -2
  5. package/dist/doctor/payload-staleness.d.ts +8 -0
  6. package/dist/doctor/payload-staleness.js +11 -8
  7. package/dist/doctor/types.d.ts +48 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +2 -0
  10. package/dist/init-deposit/gitignore.d.ts +18 -0
  11. package/dist/init-deposit/gitignore.js +94 -15
  12. package/dist/init-deposit/headless-manifest.d.ts +87 -0
  13. package/dist/init-deposit/headless-manifest.js +261 -0
  14. package/dist/init-deposit/hygiene.js +11 -0
  15. package/dist/init-deposit/index.d.ts +3 -0
  16. package/dist/init-deposit/index.js +3 -0
  17. package/dist/init-deposit/init-dispatch.d.ts +90 -0
  18. package/dist/init-deposit/init-dispatch.js +170 -0
  19. package/dist/init-deposit/refresh.d.ts +79 -2
  20. package/dist/init-deposit/refresh.js +177 -5
  21. package/dist/init-deposit/scaffold.d.ts +25 -0
  22. package/dist/init-deposit/scaffold.js +57 -0
  23. package/dist/init-deposit/untrack-core.d.ts +84 -0
  24. package/dist/init-deposit/untrack-core.js +150 -0
  25. package/dist/resolution/classify.d.ts +41 -0
  26. package/dist/resolution/classify.js +138 -0
  27. package/dist/resolution/engine-ladder.d.ts +98 -0
  28. package/dist/resolution/engine-ladder.js +185 -0
  29. package/dist/resolution/index.d.ts +19 -0
  30. package/dist/resolution/index.js +19 -0
  31. package/dist/resolution/integrity.d.ts +48 -0
  32. package/dist/resolution/integrity.js +80 -0
  33. package/dist/resolution/package-manager.d.ts +77 -0
  34. package/dist/resolution/package-manager.js +103 -0
  35. package/dist/resolution/pin.d.ts +73 -0
  36. package/dist/resolution/pin.js +169 -0
  37. package/dist/resolution/plan.d.ts +66 -0
  38. package/dist/resolution/plan.js +219 -0
  39. package/dist/resolution/skew-policy.d.ts +46 -0
  40. package/dist/resolution/skew-policy.js +120 -0
  41. package/dist/session/session-start.d.ts +2 -0
  42. package/dist/session/session-start.js +28 -1
  43. package/dist/user-config/index.d.ts +2 -0
  44. package/dist/user-config/index.js +2 -0
  45. package/dist/user-config/resolve-user-md.d.ts +76 -0
  46. package/dist/user-config/resolve-user-md.js +120 -0
  47. package/dist/xbrief-migrate/constants.d.ts +10 -0
  48. package/dist/xbrief-migrate/constants.js +19 -0
  49. package/dist/xbrief-migrate/detect.d.ts +34 -0
  50. package/dist/xbrief-migrate/detect.js +33 -0
  51. package/dist/xbrief-migrate/fs-helpers.d.ts +13 -0
  52. package/dist/xbrief-migrate/fs-helpers.js +46 -1
  53. package/dist/xbrief-migrate/index.d.ts +4 -2
  54. package/dist/xbrief-migrate/index.js +4 -2
  55. package/dist/xbrief-migrate/migrate-project.d.ts +23 -1
  56. package/dist/xbrief-migrate/migrate-project.js +97 -8
  57. package/dist/xbrief-migrate/signpost.d.ts +7 -1
  58. package/dist/xbrief-migrate/signpost.js +18 -3
  59. package/package.json +7 -3
@@ -43,6 +43,24 @@ export declare function resolveInitGitignoreLines(projectDir: string, gitLsFiles
43
43
  export declare function ensureInitGitignoreLines(projectDir: string, io: InitDepositIo, options?: {
44
44
  gitLsFiles?: GitLsFiles;
45
45
  }): EnsureInitGitignoreResult;
46
+ /**
47
+ * The reconciled ignore set for the vendored→hybrid un-commit (#2269): the
48
+ * canonical baseline (which already covers `.deft/.cli/`, `.deft/ritual-state.json`,
49
+ * and the `.deft-cache/` path) plus the `.deft/core/` deposit entry that
50
+ * greenfield init born-ignores. `package.json` is deliberately absent — the
51
+ * committed pin MUST stay tracked so content can be reconstituted after the
52
+ * deposit is un-committed.
53
+ */
54
+ export declare const UNTRACK_CORE_GITIGNORE_LINES: readonly string[];
55
+ /**
56
+ * Reconcile `.gitignore` for the `migrate --untrack-core` path: force the
57
+ * `.deft/core/` deposit entry (init leaves a tracked deposit alone, but
58
+ * un-track has just removed it from the index, so it MUST now be ignored) plus
59
+ * the canonical baseline, and never ignore the committed `package.json` pin.
60
+ * Idempotent: a second run over an already-reconciled `.gitignore` makes no
61
+ * change.
62
+ */
63
+ export declare function ensureUntrackCoreGitignoreLines(projectDir: string, io: InitDepositIo): EnsureInitGitignoreResult;
46
64
  /**
47
65
  * Copy the content package into `.deft/core`, reporting whether the deposit was
48
66
  * absent before copy (reconstitution). Always refreshes when present.
@@ -22,6 +22,7 @@ const DEFT_CORE_COVERING_LINES = new Set([".deft/core/", ".deft/core"]);
22
22
  */
23
23
  export const CANONICAL_GITIGNORE_BASELINE = [
24
24
  ".deft-cache/",
25
+ ".deft/.cli/",
25
26
  ".deft/ritual-state.json",
26
27
  ".deft/last-session.json",
27
28
  ".deft/routing.local.json",
@@ -95,14 +96,20 @@ export function resolveInitGitignoreLines(projectDir, gitLsFiles = defaultGitLsF
95
96
  };
96
97
  }
97
98
  /**
98
- * Ensure the consumer `.gitignore` carries the canonical baseline plus, for
99
- * greenfield installs, the `.deft/core/` ignore entry. Heals forbidden blanket
100
- * `vbrief/.eval/` lines (#1464). Never un-commits a tracked deposit (#1941).
99
+ * Ignore entries the deft framework MUST NEVER add to `.gitignore`. The
100
+ * committed `package.json` pin (#2264) is the reconstitution anchor for the
101
+ * un-committed deposit; ignoring it would silently drop the pin from version
102
+ * control and break `migrate --untrack-core`'s reconstitution guarantee (#2269).
101
103
  */
102
- export function ensureInitGitignoreLines(projectDir, io, options = {}) {
103
- const gitLsFiles = options.gitLsFiles ?? defaultGitLsFiles;
104
- const { lines: targetLines, includeDeftCore } = resolveInitGitignoreLines(projectDir, gitLsFiles);
105
- const tracked = isDepositTrackedInGit(projectDir, gitLsFiles);
104
+ const NEVER_IGNORE_LINES = new Set(["package.json", "/package.json"]);
105
+ /**
106
+ * Read, heal, and reconcile `.gitignore` against `targetLines`. Shared by the
107
+ * greenfield-init and the vendored→hybrid un-commit reconcilers so the
108
+ * read/heal/append/write path lives in exactly one place. Pure of any caller
109
+ * messaging — the callers decide what to print from the returned record. The
110
+ * `package.json` pin is filtered out unconditionally (#2269 invariant).
111
+ */
112
+ function reconcileGitignoreFile(projectDir, targetLines, includeDeftCoreRationale) {
106
113
  const path = join(projectDir, ".gitignore");
107
114
  let existing = "";
108
115
  if (existsSync(path)) {
@@ -134,16 +141,20 @@ export function ensureInitGitignoreLines(projectDir, io, options = {}) {
134
141
  }
135
142
  const additions = [];
136
143
  for (const line of targetLines) {
144
+ // Invariant (#2269): never ignore the committed package.json pin.
145
+ if (NEVER_IGNORE_LINES.has(line.trim()))
146
+ continue;
137
147
  if (!gitignoreCoversLine(present, line)) {
138
148
  additions.push(line);
139
149
  }
140
150
  }
141
151
  if (!blanketRemoved && additions.length === 0) {
142
- io.printf(".gitignore already covers the canonical deft entries — skipping.\n");
143
152
  return {
144
153
  changed: false,
154
+ additions,
155
+ blanketRemoved,
145
156
  deftCoreIgnored: gitignoreCoversLine(present, GITIGNORE_DEFT_CORE_LINE),
146
- skippedDeftCoreBecauseTracked: tracked === true,
157
+ alreadyCovered: true,
147
158
  };
148
159
  }
149
160
  let healed = kept.join("\n");
@@ -159,7 +170,7 @@ export function ensureInitGitignoreLines(projectDir, io, options = {}) {
159
170
  body += "\n";
160
171
  }
161
172
  body += DEFT_FRAMEWORK_GITIGNORE_HEADER;
162
- if (includeDeftCore && additions.includes(GITIGNORE_DEFT_CORE_LINE)) {
173
+ if (includeDeftCoreRationale && additions.includes(GITIGNORE_DEFT_CORE_LINE)) {
163
174
  body += DEFT_CORE_GITIGNORE_RATIONALE;
164
175
  }
165
176
  for (const add of additions) {
@@ -172,22 +183,90 @@ export function ensureInitGitignoreLines(projectDir, io, options = {}) {
172
183
  catch (cause) {
173
184
  throw new Error(`could not write .gitignore: ${String(cause)}`);
174
185
  }
175
- if (additions.length > 0) {
176
- io.printf(`.gitignore updated with canonical entries: ${additions.join(", ")}\n`);
186
+ const finalPresent = collectPresentGitignoreLines(body);
187
+ return {
188
+ changed: true,
189
+ additions,
190
+ blanketRemoved,
191
+ deftCoreIgnored: gitignoreCoversLine(finalPresent, GITIGNORE_DEFT_CORE_LINE),
192
+ alreadyCovered: false,
193
+ };
194
+ }
195
+ /**
196
+ * Ensure the consumer `.gitignore` carries the canonical baseline plus, for
197
+ * greenfield installs, the `.deft/core/` ignore entry. Heals forbidden blanket
198
+ * `vbrief/.eval/` lines (#1464). Never un-commits a tracked deposit (#1941).
199
+ */
200
+ export function ensureInitGitignoreLines(projectDir, io, options = {}) {
201
+ const gitLsFiles = options.gitLsFiles ?? defaultGitLsFiles;
202
+ const { lines: targetLines, includeDeftCore } = resolveInitGitignoreLines(projectDir, gitLsFiles);
203
+ const tracked = isDepositTrackedInGit(projectDir, gitLsFiles);
204
+ const res = reconcileGitignoreFile(projectDir, targetLines, includeDeftCore);
205
+ if (res.alreadyCovered) {
206
+ io.printf(".gitignore already covers the canonical deft entries — skipping.\n");
207
+ return {
208
+ changed: false,
209
+ deftCoreIgnored: res.deftCoreIgnored,
210
+ skippedDeftCoreBecauseTracked: tracked === true,
211
+ };
177
212
  }
178
- if (blanketRemoved) {
213
+ if (res.additions.length > 0) {
214
+ io.printf(`.gitignore updated with canonical entries: ${res.additions.join(", ")}\n`);
215
+ }
216
+ if (res.blanketRemoved) {
179
217
  io.printf(".gitignore healed: removed forbidden blanket vbrief/.eval/ line (#1464).\n");
180
218
  }
181
219
  if (tracked === true) {
182
220
  io.printf(".deft/core is tracked in git — leaving it tracked; vendored→hybrid un-commit is #1941.\n");
183
221
  }
184
- const finalPresent = collectPresentGitignoreLines(body);
185
222
  return {
186
223
  changed: true,
187
- deftCoreIgnored: gitignoreCoversLine(finalPresent, GITIGNORE_DEFT_CORE_LINE),
224
+ deftCoreIgnored: res.deftCoreIgnored,
188
225
  skippedDeftCoreBecauseTracked: tracked === true,
189
226
  };
190
227
  }
228
+ /**
229
+ * The reconciled ignore set for the vendored→hybrid un-commit (#2269): the
230
+ * canonical baseline (which already covers `.deft/.cli/`, `.deft/ritual-state.json`,
231
+ * and the `.deft-cache/` path) plus the `.deft/core/` deposit entry that
232
+ * greenfield init born-ignores. `package.json` is deliberately absent — the
233
+ * committed pin MUST stay tracked so content can be reconstituted after the
234
+ * deposit is un-committed.
235
+ */
236
+ export const UNTRACK_CORE_GITIGNORE_LINES = [
237
+ ...CANONICAL_GITIGNORE_BASELINE,
238
+ GITIGNORE_DEFT_CORE_LINE,
239
+ ];
240
+ /**
241
+ * Reconcile `.gitignore` for the `migrate --untrack-core` path: force the
242
+ * `.deft/core/` deposit entry (init leaves a tracked deposit alone, but
243
+ * un-track has just removed it from the index, so it MUST now be ignored) plus
244
+ * the canonical baseline, and never ignore the committed `package.json` pin.
245
+ * Idempotent: a second run over an already-reconciled `.gitignore` makes no
246
+ * change.
247
+ */
248
+ export function ensureUntrackCoreGitignoreLines(projectDir, io) {
249
+ const res = reconcileGitignoreFile(projectDir, UNTRACK_CORE_GITIGNORE_LINES, false);
250
+ if (res.alreadyCovered) {
251
+ io.printf(".gitignore already ignores .deft/core/ and the canonical deft entries — skipping.\n");
252
+ return {
253
+ changed: false,
254
+ deftCoreIgnored: res.deftCoreIgnored,
255
+ skippedDeftCoreBecauseTracked: false,
256
+ };
257
+ }
258
+ if (res.additions.length > 0) {
259
+ io.printf(`.gitignore updated with canonical entries: ${res.additions.join(", ")}\n`);
260
+ }
261
+ if (res.blanketRemoved) {
262
+ io.printf(".gitignore healed: removed forbidden blanket vbrief/.eval/ line (#1464).\n");
263
+ }
264
+ return {
265
+ changed: true,
266
+ deftCoreIgnored: res.deftCoreIgnored,
267
+ skippedDeftCoreBecauseTracked: false,
268
+ };
269
+ }
191
270
  /**
192
271
  * Copy the content package into `.deft/core`, reporting whether the deposit was
193
272
  * absent before copy (reconstitution). Always refreshes when present.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `directive init --headless` — manifest emitter for no-shell consumer products
3
+ * (#2268 / epic #2203).
4
+ *
5
+ * Hosted consumer products (e.g. deftai/deftvisage#1036) have no shell or npm
6
+ * access to the end user's machine. This module serialises the merged keystone
7
+ * `plan()` schema into a `{ version, files: [{ path, content, encoding }] }`
8
+ * manifest with ALL execution side effects suppressed: no interactive prompts,
9
+ * no git operations, no git-hook installation, and no filesystem writes outside
10
+ * the explicit `--output` target. It makes no git-repo assumption and runs
11
+ * against an empty / non-existent directory.
12
+ *
13
+ * Single-sourcing (the #2268 acceptance): the manifest is DERIVED from `plan()`
14
+ * — the collected file set is threaded through `plan(..., { files })` and read
15
+ * back out of {@link ResolutionPlan.files}. There is no separate "what would
16
+ * init produce" reimplementation; the collectors here are the manifest-only,
17
+ * read-only counterparts of the executing deposit path in `scaffold.ts` /
18
+ * `init-deposit.ts` (they reuse the same render helpers WITHOUT filesystem or
19
+ * git writes).
20
+ *
21
+ * On registry / content-resolution failure the CLI wrapper exits non-zero and
22
+ * emits a JSON error object so the calling backend can treat it as non-fatal
23
+ * (never a crash or a partial write).
24
+ */
25
+ import type { ResolutionFile } from "@deftai/directive-types";
26
+ /** The five xBRIEF lifecycle folders scaffolded into a fresh project. */
27
+ export declare const HEADLESS_XBRIEF_LIFECYCLE_DIRS: readonly ["proposed", "pending", "active", "completed", "cancelled"];
28
+ /** A single file entry in the headless manifest. */
29
+ export type HeadlessManifestFile = ResolutionFile;
30
+ /** The serialised headless manifest shape. */
31
+ export interface HeadlessManifest {
32
+ /** Resolved framework version the manifest was built against. */
33
+ readonly version: string;
34
+ /** All files a consumer backend should materialise for a fresh install. */
35
+ readonly files: readonly HeadlessManifestFile[];
36
+ }
37
+ /**
38
+ * JSON error object emitted on a registry / content-resolution failure. The
39
+ * calling backend treats a non-zero exit + this object as non-fatal.
40
+ */
41
+ export interface HeadlessManifestErrorObject {
42
+ readonly success: false;
43
+ readonly error: string;
44
+ readonly error_code: string;
45
+ }
46
+ /** Injectable seams; defaults are the real content-package resolution path. */
47
+ export interface HeadlessManifestSeams {
48
+ /** Resolve the installed content-package root. Default: real npm resolution. */
49
+ readonly resolveContentRoot?: () => Promise<string>;
50
+ /** Override the resolved version (default: read the content package.json). */
51
+ readonly readVersion?: (contentRoot: string) => string;
52
+ /** Deterministic UTC ISO timestamp for the VERSION marker (default: now). */
53
+ readonly nowIso?: () => string;
54
+ /** Deterministic managed-section session id (default: fixed headless token). */
55
+ readonly newSession?: () => string;
56
+ }
57
+ /**
58
+ * Build the headless manifest. Reads the content package read-only, collects
59
+ * the fresh-install file set (payload + version-consistent AGENTS.md + xBRIEF
60
+ * scaffold + pinned package.json), threads it through `plan()` so the manifest
61
+ * is single-sourced from the resolution schema, and returns the serialisable
62
+ * shape. Performs NO filesystem writes and NO git operations.
63
+ *
64
+ * Throws (never partially writes) on content-resolution failure; the CLI
65
+ * wrapper converts the throw into a JSON error object + non-zero exit.
66
+ */
67
+ export declare function buildHeadlessManifest(seams?: HeadlessManifestSeams): Promise<HeadlessManifest>;
68
+ export interface RunInitHeadlessCliOptions {
69
+ /** Explicit `--output` target, or null to emit the manifest to stdout. */
70
+ readonly outputPath: string | null;
71
+ readonly writeOut: (text: string) => void;
72
+ readonly writeErr: (text: string) => void;
73
+ readonly seams?: HeadlessManifestSeams;
74
+ /**
75
+ * Filesystem write seam (test-only). The default creates the parent directory
76
+ * and writes the file; an injected seam OWNS all filesystem contact (including
77
+ * directory creation), so the manifest write stays fully isolatable in tests.
78
+ */
79
+ readonly writeFile?: (path: string, data: string) => void;
80
+ }
81
+ /**
82
+ * CLI-facing entrypoint for `directive init --headless`. Emits the manifest to
83
+ * `--output` (the ONLY permitted filesystem write) or stdout, and on failure
84
+ * writes a JSON error object to stdout and returns exit code 1.
85
+ */
86
+ export declare function runInitHeadlessCli(options: RunInitHeadlessCliOptions): Promise<number>;
87
+ //# sourceMappingURL=headless-manifest.d.ts.map
@@ -0,0 +1,261 @@
1
+ /**
2
+ * `directive init --headless` — manifest emitter for no-shell consumer products
3
+ * (#2268 / epic #2203).
4
+ *
5
+ * Hosted consumer products (e.g. deftai/deftvisage#1036) have no shell or npm
6
+ * access to the end user's machine. This module serialises the merged keystone
7
+ * `plan()` schema into a `{ version, files: [{ path, content, encoding }] }`
8
+ * manifest with ALL execution side effects suppressed: no interactive prompts,
9
+ * no git operations, no git-hook installation, and no filesystem writes outside
10
+ * the explicit `--output` target. It makes no git-repo assumption and runs
11
+ * against an empty / non-existent directory.
12
+ *
13
+ * Single-sourcing (the #2268 acceptance): the manifest is DERIVED from `plan()`
14
+ * — the collected file set is threaded through `plan(..., { files })` and read
15
+ * back out of {@link ResolutionPlan.files}. There is no separate "what would
16
+ * init produce" reimplementation; the collectors here are the manifest-only,
17
+ * read-only counterparts of the executing deposit path in `scaffold.ts` /
18
+ * `init-deposit.ts` (they reuse the same render helpers WITHOUT filesystem or
19
+ * git writes).
20
+ *
21
+ * On registry / content-resolution failure the CLI wrapper exits non-zero and
22
+ * emits a JSON error object so the calling backend can treat it as non-fatal
23
+ * (never a crash or a partial write).
24
+ */
25
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
26
+ import { dirname, join, relative, resolve, sep } from "node:path";
27
+ import { ContentPackageNotFoundError, resolveInstalledContentRoot, } from "../deposit/resolve-content.js";
28
+ import { readCorePackageVersion } from "../engine-version.js";
29
+ import { agentsRefreshPlan } from "../platform/agents-md.js";
30
+ import { plan } from "../resolution/plan.js";
31
+ import { CANONICAL_INSTALL_ROOT } from "./constants.js";
32
+ import { buildInstallManifestText, PIN_DEPENDENCY_NAME } from "./scaffold.js";
33
+ /** POSIX-relative path of the payload VERSION marker inside the manifest. */
34
+ const PAYLOAD_VERSION_PATH = `${CANONICAL_INSTALL_ROOT}/VERSION`;
35
+ /** Provenance stamp written into the emitted `.deft/core/VERSION` marker. */
36
+ const HEADLESS_FETCHED_BY = "directive-init-headless";
37
+ /** The five xBRIEF lifecycle folders scaffolded into a fresh project. */
38
+ export const HEADLESS_XBRIEF_LIFECYCLE_DIRS = [
39
+ "proposed",
40
+ "pending",
41
+ "active",
42
+ "completed",
43
+ "cancelled",
44
+ ];
45
+ const XBRIEF_LIFECYCLE_GITKEEP = `# Keeps this xbrief/ lifecycle directory present in version control so the
46
+ # deft-directive-setup pre-cutover guard does not fire on a fresh headless
47
+ # install. Emitted by \`directive init --headless\` (#2268).
48
+ `;
49
+ function defaultNowIso() {
50
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
51
+ }
52
+ /** Read the resolved version from the content package.json (engine fallback). */
53
+ function resolveVersion(contentRoot, seams) {
54
+ if (seams.readVersion)
55
+ return seams.readVersion(contentRoot);
56
+ try {
57
+ const parsed = JSON.parse(readFileSync(join(contentRoot, "package.json"), "utf8"));
58
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
59
+ const version = parsed.version;
60
+ if (version?.trim())
61
+ return version.trim();
62
+ }
63
+ }
64
+ catch {
65
+ // Fall through to the engine version.
66
+ }
67
+ return readCorePackageVersion();
68
+ }
69
+ /**
70
+ * Decide the manifest encoding for a file's raw bytes. UTF-8 text round-trips
71
+ * losslessly; anything else (or bytes carrying a NUL) is base64 so binary
72
+ * payload assets survive JSON serialisation without corruption.
73
+ */
74
+ function encodeBytes(buf) {
75
+ if (!buf.includes(0)) {
76
+ const text = buf.toString("utf8");
77
+ if (Buffer.from(text, "utf8").equals(buf)) {
78
+ return { content: text, encoding: "utf-8" };
79
+ }
80
+ }
81
+ return { content: buf.toString("base64"), encoding: "base64" };
82
+ }
83
+ /** POSIX-normalise a path (manifest paths are always forward-slash). */
84
+ function toPosix(p) {
85
+ return sep === "/" ? p : p.split(sep).join("/");
86
+ }
87
+ /**
88
+ * Walk the content-package tree read-only and collect one manifest entry per
89
+ * file, prefixed with the canonical install root. The manifest-only counterpart
90
+ * of `reconstituteDepositFromContent` — it materialises bytes into memory
91
+ * instead of copying them onto disk.
92
+ */
93
+ function collectPayloadFiles(contentRoot) {
94
+ const out = [];
95
+ const stack = [contentRoot];
96
+ while (stack.length > 0) {
97
+ const dir = stack.pop();
98
+ if (dir === undefined)
99
+ continue;
100
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
101
+ const full = join(dir, entry.name);
102
+ if (entry.isDirectory()) {
103
+ stack.push(full);
104
+ }
105
+ else if (entry.isFile()) {
106
+ const rel = toPosix(relative(contentRoot, full));
107
+ const { content, encoding } = encodeBytes(readFileSync(full));
108
+ out.push({ path: `${CANONICAL_INSTALL_ROOT}/${rel}`, content, encoding });
109
+ }
110
+ }
111
+ }
112
+ return out;
113
+ }
114
+ /**
115
+ * Render the version-consistent `.deft/core/VERSION` marker. The `sha=` slot is
116
+ * stamped with the resolved version (headless mode has no git checkout), which
117
+ * is what keeps the emitted AGENTS.md managed section and the payload provably
118
+ * consistent against a single resolved version.
119
+ */
120
+ function collectVersionFile(version, nowIso) {
121
+ const tag = version.startsWith("v") ? version : `v${version}`;
122
+ const body = buildInstallManifestText({
123
+ ref: tag,
124
+ sha: version,
125
+ tag,
126
+ installRoot: CANONICAL_INSTALL_ROOT,
127
+ fetchedAt: nowIso,
128
+ fetchedBy: HEADLESS_FETCHED_BY,
129
+ });
130
+ return { path: PAYLOAD_VERSION_PATH, content: body, encoding: "utf-8" };
131
+ }
132
+ /**
133
+ * Render the greenfield AGENTS.md (unmanaged header + attributed managed
134
+ * section) from the SAME content root as the payload, so the managed section
135
+ * can never drift from the deposited framework version. The managed-section
136
+ * `sha=` is stamped with the resolved version for version-consistency.
137
+ */
138
+ function collectAgentsMdFile(contentRoot, version, nowIso, sessionId) {
139
+ const agentsResult = agentsRefreshPlan("headless-project", {
140
+ frameworkRoot: contentRoot,
141
+ resolveSha: () => version,
142
+ nowIso: () => nowIso,
143
+ newSession: () => sessionId,
144
+ readAgents: () => null,
145
+ });
146
+ const newContent = agentsResult.new_content;
147
+ if (typeof newContent !== "string") {
148
+ throw new Error(`AGENTS.md render produced no content (state: ${String(agentsResult.state)})`);
149
+ }
150
+ return { path: "AGENTS.md", content: newContent, encoding: "utf-8" };
151
+ }
152
+ /** The five xBRIEF lifecycle `.gitkeep` files (empty dirs can't ride a manifest). */
153
+ function collectXbriefScaffoldFiles() {
154
+ return HEADLESS_XBRIEF_LIFECYCLE_DIRS.map((sub) => ({
155
+ path: `xbrief/${sub}/.gitkeep`,
156
+ content: XBRIEF_LIFECYCLE_GITKEEP,
157
+ encoding: "utf-8",
158
+ }));
159
+ }
160
+ /** The pinned `package.json` devDependency snippet (exact, private workspace). */
161
+ function collectPackageJsonFile(version) {
162
+ const pinVersion = version.trim().replace(/^v/i, "");
163
+ const pkg = {
164
+ private: true,
165
+ devDependencies: { [PIN_DEPENDENCY_NAME]: pinVersion },
166
+ };
167
+ return {
168
+ path: "package.json",
169
+ content: `${JSON.stringify(pkg, null, 2)}\n`,
170
+ encoding: "utf-8",
171
+ };
172
+ }
173
+ /**
174
+ * Greenfield fact-set for the headless manifest. `hasDeftCore: false` routes
175
+ * `plan()` to `init` mode (Row 2), before any engine/pin reconciliation — no
176
+ * git-repo assumption, no reachable-engine requirement.
177
+ */
178
+ function headlessGreenfieldFacts() {
179
+ return {
180
+ hasGit: false,
181
+ hasAppCode: false,
182
+ hasDeftCore: false,
183
+ deftCorePayloadVersion: null,
184
+ hasManagedSection: false,
185
+ managedSectionSha: null,
186
+ hasVbrief: false,
187
+ hasXbrief: false,
188
+ preCutoverArtifacts: false,
189
+ engineReachable: false,
190
+ engineVersion: null,
191
+ pinVersion: null,
192
+ };
193
+ }
194
+ /**
195
+ * Build the headless manifest. Reads the content package read-only, collects
196
+ * the fresh-install file set (payload + version-consistent AGENTS.md + xBRIEF
197
+ * scaffold + pinned package.json), threads it through `plan()` so the manifest
198
+ * is single-sourced from the resolution schema, and returns the serialisable
199
+ * shape. Performs NO filesystem writes and NO git operations.
200
+ *
201
+ * Throws (never partially writes) on content-resolution failure; the CLI
202
+ * wrapper converts the throw into a JSON error object + non-zero exit.
203
+ */
204
+ export async function buildHeadlessManifest(seams = {}) {
205
+ const resolveContentRoot = seams.resolveContentRoot ?? resolveInstalledContentRoot;
206
+ const contentRoot = await resolveContentRoot();
207
+ const version = resolveVersion(contentRoot, seams);
208
+ const nowIso = (seams.nowIso ?? defaultNowIso)();
209
+ const sessionId = (seams.newSession ?? (() => "headlessmanif"))();
210
+ const payload = collectPayloadFiles(contentRoot).filter((file) => file.path !== PAYLOAD_VERSION_PATH);
211
+ const files = [
212
+ ...payload,
213
+ collectVersionFile(version, nowIso),
214
+ collectAgentsMdFile(contentRoot, version, nowIso, sessionId),
215
+ ...collectXbriefScaffoldFiles(),
216
+ collectPackageJsonFile(version),
217
+ ].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
218
+ // Single-source the manifest from plan(): the collected set rides the
219
+ // ResolutionPlan.files channel, and we read it back out — no reimplementation.
220
+ const resolved = plan(headlessGreenfieldFacts(), {}, { files });
221
+ return { version, files: resolved.files };
222
+ }
223
+ /**
224
+ * CLI-facing entrypoint for `directive init --headless`. Emits the manifest to
225
+ * `--output` (the ONLY permitted filesystem write) or stdout, and on failure
226
+ * writes a JSON error object to stdout and returns exit code 1.
227
+ */
228
+ export async function runInitHeadlessCli(options) {
229
+ try {
230
+ const manifest = await buildHeadlessManifest(options.seams);
231
+ const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
232
+ if (options.outputPath) {
233
+ const abs = resolve(options.outputPath);
234
+ const writeFile = options.writeFile ??
235
+ ((path, data) => {
236
+ mkdirSync(dirname(path), { recursive: true });
237
+ writeFileSync(path, data);
238
+ });
239
+ writeFile(abs, serialized);
240
+ options.writeErr(`directive init --headless: wrote ${manifest.files.length}-file manifest (v${manifest.version}) to ${abs}\n`);
241
+ }
242
+ else {
243
+ options.writeOut(serialized);
244
+ }
245
+ return 0;
246
+ }
247
+ catch (cause) {
248
+ const message = cause instanceof Error ? cause.message : String(cause);
249
+ const error_code = cause instanceof ContentPackageNotFoundError
250
+ ? "content_resolution_failed"
251
+ : "headless_manifest_failed";
252
+ const errorObject = {
253
+ success: false,
254
+ error: message,
255
+ error_code,
256
+ };
257
+ options.writeOut(`${JSON.stringify(errorObject, null, 2)}\n`);
258
+ return 1;
259
+ }
260
+ }
261
+ //# sourceMappingURL=headless-manifest.js.map
@@ -11,6 +11,8 @@ import { gitPorcelain } from "../story-ready/git.js";
11
11
  import { CANONICAL_INSTALL_ROOT } from "./constants.js";
12
12
  export const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
13
13
  export const CORE_GUARD_WORKFLOW_REL = ".github/workflows/deft-core-guard.yml";
14
+ // The lifecycle dir names are identical across the legacy `vbrief/` tree and the
15
+ // post-#2034 / #2110 `xbrief/` tree, so both allowlist families reuse this list.
14
16
  const VBRIEF_LIFECYCLE_DIRS = ["proposed", "pending", "active", "completed", "cancelled"];
15
17
  /** Single source of truth for installer-managed paths (#1440 / #1576). */
16
18
  export function installerManagedMatchers() {
@@ -24,11 +26,20 @@ export function installerManagedMatchers() {
24
26
  { exact: CODEQL_CONFIG_REL },
25
27
  { exact: CORE_GUARD_WORKFLOW_REL },
26
28
  { exact: "Taskfile.yml" },
29
+ // Legacy vbrief/ tree -- retained for not-yet-migrated consumers.
27
30
  { exact: "vbrief/.deft-version" },
28
31
  { exact: "vbrief/vbrief.md" },
29
32
  { prefix: "vbrief/schemas/" },
30
33
  { prefix: "vbrief/migration/" },
31
34
  ...VBRIEF_LIFECYCLE_DIRS.map((sub) => ({ exact: `vbrief/${sub}/.gitkeep` })),
35
+ // Migrated xbrief/ tree (#2034 / #2110). The framework-managed version marker
36
+ // now lives at xbrief/.deft-version, so it MUST be allowlisted or every routine
37
+ // `deft update` framework-deposit PR trips no-mixed-core-and-app (#2277).
38
+ { exact: "xbrief/.deft-version" },
39
+ { exact: "xbrief/xbrief.md" },
40
+ { prefix: "xbrief/schemas/" },
41
+ { prefix: "xbrief/migration/" },
42
+ ...VBRIEF_LIFECYCLE_DIRS.map((sub) => ({ exact: `xbrief/${sub}/.gitkeep` })),
32
43
  ];
33
44
  }
34
45
  function escapeEre(value) {
@@ -1,6 +1,9 @@
1
+ export * from "./headless-manifest.js";
1
2
  export * from "./init-deposit.js";
3
+ export * from "./init-dispatch.js";
2
4
  export * from "./legacy-detect.js";
3
5
  export * from "./migrate.js";
4
6
  export * from "./refresh.js";
5
7
  export * from "./scaffold.js";
8
+ export * from "./untrack-core.js";
6
9
  //# sourceMappingURL=index.d.ts.map
@@ -1,6 +1,9 @@
1
+ export * from "./headless-manifest.js";
1
2
  export * from "./init-deposit.js";
3
+ export * from "./init-dispatch.js";
2
4
  export * from "./legacy-detect.js";
3
5
  export * from "./migrate.js";
4
6
  export * from "./refresh.js";
5
7
  export * from "./scaffold.js";
8
+ export * from "./untrack-core.js";
6
9
  //# sourceMappingURL=index.js.map