@deftai/directive-core 0.55.1 → 0.56.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 (74) hide show
  1. package/dist/cache/errors.d.ts +4 -1
  2. package/dist/cache/errors.js +8 -2
  3. package/dist/cache/fetch.d.ts +50 -0
  4. package/dist/cache/fetch.js +224 -3
  5. package/dist/cache/main.js +10 -5
  6. package/dist/cache/operations.js +6 -4
  7. package/dist/codebase/map-fresh.js +7 -1
  8. package/dist/deposit/copy-tree.d.ts +16 -0
  9. package/dist/deposit/copy-tree.js +58 -0
  10. package/dist/deposit/resolve-content.d.ts +28 -0
  11. package/dist/deposit/resolve-content.js +70 -0
  12. package/dist/doctor/checks.d.ts +6 -0
  13. package/dist/doctor/checks.js +37 -0
  14. package/dist/doctor/constants.d.ts +2 -0
  15. package/dist/doctor/constants.js +8 -0
  16. package/dist/engine-version.d.ts +3 -0
  17. package/dist/engine-version.js +23 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +3 -1
  20. package/dist/init-deposit/gitignore.d.ts +51 -0
  21. package/dist/init-deposit/gitignore.js +200 -0
  22. package/dist/init-deposit/index.d.ts +6 -0
  23. package/dist/init-deposit/index.js +6 -0
  24. package/dist/init-deposit/init-deposit.d.ts +44 -0
  25. package/dist/init-deposit/init-deposit.js +197 -0
  26. package/dist/init-deposit/legacy-detect.d.ts +61 -0
  27. package/dist/init-deposit/legacy-detect.js +223 -0
  28. package/dist/init-deposit/migrate.d.ts +88 -0
  29. package/dist/init-deposit/migrate.js +196 -0
  30. package/dist/init-deposit/refresh.d.ts +50 -0
  31. package/dist/init-deposit/refresh.js +292 -0
  32. package/dist/init-deposit/scaffold.d.ts +41 -0
  33. package/dist/init-deposit/scaffold.js +753 -0
  34. package/dist/legacy-bridge/bridge-drift.d.ts +70 -0
  35. package/dist/legacy-bridge/bridge-drift.js +171 -0
  36. package/dist/legacy-bridge/freeze-gate.d.ts +79 -0
  37. package/dist/legacy-bridge/freeze-gate.js +168 -0
  38. package/dist/legacy-bridge/index.d.ts +4 -0
  39. package/dist/legacy-bridge/index.js +4 -0
  40. package/dist/legacy-bridge/sot.d.ts +45 -0
  41. package/dist/legacy-bridge/sot.js +49 -0
  42. package/dist/preflight-cache/evaluate.d.ts +11 -0
  43. package/dist/preflight-cache/evaluate.js +63 -13
  44. package/dist/release-e2e/constants.d.ts +2 -0
  45. package/dist/release-e2e/constants.js +2 -0
  46. package/dist/release-e2e/flags.js +5 -1
  47. package/dist/release-e2e/legacy-bridge-leg.d.ts +134 -0
  48. package/dist/release-e2e/legacy-bridge-leg.js +460 -0
  49. package/dist/release-e2e/main.d.ts +2 -1
  50. package/dist/release-e2e/main.js +17 -1
  51. package/dist/release-e2e/npm-ops.d.ts +3 -1
  52. package/dist/release-e2e/npm-ops.js +5 -3
  53. package/dist/release-e2e/types.d.ts +9 -0
  54. package/dist/render/index.d.ts +1 -1
  55. package/dist/render/index.js +1 -1
  56. package/dist/render/project-render.js +4 -3
  57. package/dist/render/roadmap-render.d.ts +4 -2
  58. package/dist/render/roadmap-render.js +14 -7
  59. package/dist/swarm/routing-verify.js +2 -2
  60. package/dist/swarm/subagent-backend.d.ts +16 -0
  61. package/dist/swarm/subagent-backend.js +14 -0
  62. package/dist/triage/actions/candidates-log.d.ts +12 -1
  63. package/dist/triage/actions/candidates-log.js +44 -7
  64. package/dist/triage/actions/index.d.ts +25 -1
  65. package/dist/triage/actions/index.js +94 -3
  66. package/dist/triage/summary/index.d.ts +1 -3
  67. package/dist/triage/summary/index.js +6 -57
  68. package/dist/triage/summary/reconcilable.js +4 -33
  69. package/dist/triage/welcome/default-mode.d.ts +1 -0
  70. package/dist/triage/welcome/default-mode.js +6 -0
  71. package/dist/vbrief-validate/project-definition.js +4 -69
  72. package/dist/vbrief-validate/registry-status.d.ts +20 -0
  73. package/dist/vbrief-validate/registry-status.js +85 -0
  74. package/package.json +11 -2
@@ -0,0 +1,753 @@
1
+ /**
2
+ * Greenfield deposit scaffold helpers — TS port of cmd/deft-install/setup.go +
3
+ * deposit.go + githooks.go surfaces consumed by directive init (#1942 S2).
4
+ *
5
+ * Refs #1942, #1430, #1463, #1179.
6
+ */
7
+ import { execFileSync } from "node:child_process";
8
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
9
+ import { mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
10
+ import { platform } from "node:os";
11
+ import { dirname, join, relative } from "node:path";
12
+ import { copyTree } from "../deposit/copy-tree.js";
13
+ import { agentsRefreshPlan } from "../platform/agents-md.js";
14
+ export const CANONICAL_INSTALL_ROOT = ".deft/core";
15
+ export const CORE_GLOB = ".deft/core/**";
16
+ const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
17
+ const CORE_GUARD_WORKFLOW_REL = ".github/workflows/deft-core-guard.yml";
18
+ const FRAMEWORK_SELF_TEST_REL = ".deft/core/tests";
19
+ const VENDORED_TS_PACKAGES_REL = ".deft/core/packages";
20
+ const VENDORED_TS_TEST_RE = /\.(test|spec)\.(c|m)?[jt]sx?$/i;
21
+ const CORE_GITATTRIBUTES_LINES = [
22
+ `${CORE_GLOB} linguist-generated=true`,
23
+ `${CORE_GLOB} linguist-vendored=true`,
24
+ ];
25
+ const VBRIEF_LIFECYCLE_DIRS = ["proposed", "pending", "active", "completed", "cancelled"];
26
+ const VBRIEF_LIFECYCLE_GITKEEP = `# This file keeps the lifecycle directory present in version control and
27
+ # survives installer packaging so the deft-directive-setup pre-cutover guard
28
+ # (condition 3, see skills/deft-directive-setup/SKILL.md:32 and main.md:159)
29
+ # does not fire on a fresh install. See #1179.
30
+ `;
31
+ const VBRIEF_README_BODY = `# vbrief/ -- scope vBRIEF lifecycle workspace
32
+
33
+ This directory is your project's scope vBRIEF lifecycle workspace.
34
+
35
+ - vbrief/proposed/ -- newly proposed scope vBRIEFs
36
+ - vbrief/pending/ -- accepted, awaiting activation
37
+ - vbrief/active/ -- in-flight implementation work
38
+ - vbrief/completed/ -- merged / shipped
39
+ - vbrief/cancelled/ -- closed without merge
40
+
41
+ Schemas: vbrief/schemas/ (mirrored from the framework copy at install time).
42
+ Reference template: .deft/core/vbrief/vbrief.md
43
+
44
+ Do not commit vbrief/.eval/ -- it is the local audit-log private state and
45
+ is covered by the canonical .gitignore baseline deposited by deft-install.
46
+ `;
47
+ export const MINIMAL_TASKFILE = `version: '3'
48
+
49
+ # Taskfile for this project.
50
+ # Installed by deft-install --yes (Epic-4). Add your own tasks below or in
51
+ # additional included files. The deft include makes all framework tasks
52
+ # (task check, task vbrief:*, task doctor, etc.) available from the project root.
53
+
54
+ includes:
55
+ deft:
56
+ taskfile: ./.deft/core/Taskfile.yml
57
+ optional: true
58
+ `;
59
+ export const CANONICAL_TASKFILE_INCLUDE = "taskfile: ./.deft/core/Taskfile.yml";
60
+ const DEFT_INCLUDE_CHILD_BLOCK = " # Added by deft-install --yes (Epic-4)\n" +
61
+ " deft:\n" +
62
+ " taskfile: ./.deft/core/Taskfile.yml\n" +
63
+ " optional: true\n";
64
+ const AGENTS_SKILLS = [
65
+ {
66
+ dir: "deft",
67
+ content: `---
68
+ name: deft
69
+ description: Apply deft framework standards for AI-assisted development. Use when starting projects, writing code, running tests, making commits, or when the user references deft, project standards, or coding guidelines.
70
+ ---
71
+
72
+ Read and follow: .deft/core/SKILL.md
73
+ `,
74
+ },
75
+ {
76
+ dir: "deft-directive-setup",
77
+ content: `---
78
+ name: deft-directive-setup
79
+ description: >-
80
+ Set up a new project with Deft framework standards. Use when the user wants
81
+ to bootstrap user preferences, configure a project, or generate a project
82
+ specification. Walks through setup conversationally — no separate CLI needed.
83
+ ---
84
+
85
+ Read and follow: .deft/core/skills/deft-directive-setup/SKILL.md
86
+ `,
87
+ },
88
+ {
89
+ dir: "deft-directive-build",
90
+ content: `---
91
+ name: deft-directive-build
92
+ description: >-
93
+ Build a project from scope vBRIEFs following Deft framework standards.
94
+ Use after deft-directive-setup has generated the project definition, or when
95
+ the user has scope vBRIEFs ready to implement. Handles scaffolding,
96
+ implementation, testing, and quality checks phase by phase.
97
+ ---
98
+
99
+ Read and follow: .deft/core/skills/deft-directive-build/SKILL.md
100
+ `,
101
+ },
102
+ {
103
+ dir: "deft-directive-review-cycle",
104
+ content: `---
105
+ name: deft-directive-review-cycle
106
+ description: >-
107
+ Greptile bot reviewer response workflow. Use when running a review cycle
108
+ on a PR — to audit process prerequisites, fetch bot findings, fix all
109
+ issues in a single batch commit, and exit cleanly when no P0/P1 issues
110
+ remain. Enables cloud agents to run autonomous PR review cycles.
111
+ ---
112
+
113
+ Read and follow: .deft/core/skills/deft-directive-review-cycle/SKILL.md
114
+ `,
115
+ },
116
+ {
117
+ dir: "deft-directive-refinement",
118
+ content: `---
119
+ name: deft-directive-refinement
120
+ description: >-
121
+ Structured refinement workflow. Compares open GitHub issues against
122
+ the roadmap, triages new issues one-at-a-time with human review, and updates
123
+ the roadmap with phase placement, analysis comments, and index entries.
124
+ ---
125
+
126
+ Read and follow: .deft/core/skills/deft-directive-refinement/SKILL.md
127
+ `,
128
+ },
129
+ {
130
+ dir: "deft-directive-swarm",
131
+ content: `---
132
+ name: deft-directive-swarm
133
+ description: >-
134
+ Parallel local agent orchestration. Use when running multiple agents
135
+ on roadmap items simultaneously — to select non-overlapping tasks, set up
136
+ isolated worktrees, launch agents with proven prompts, monitor progress,
137
+ handle stalled review cycles, and close out PRs cleanly.
138
+ ---
139
+
140
+ Read and follow: .deft/core/skills/deft-directive-swarm/SKILL.md
141
+ `,
142
+ },
143
+ {
144
+ dir: "deft-directive-interview",
145
+ content: `---
146
+ name: deft-directive-interview
147
+ description: >-
148
+ Deterministic structured Q&A interview skill. Use when a skill or workflow
149
+ needs to collect structured answers from the user — one question per turn,
150
+ numbered options, default acceptance, and a confirmation gate.
151
+ ---
152
+
153
+ Read and follow: .deft/core/skills/deft-directive-interview/SKILL.md
154
+ `,
155
+ },
156
+ {
157
+ dir: "deft-directive-pre-pr",
158
+ content: `---
159
+ name: deft-directive-pre-pr
160
+ description: >-
161
+ Iterative pre-PR quality loop (Read-Write-Lint-Diff-Loop). Use before
162
+ pushing a branch for PR creation — structured self-review that agents run
163
+ to catch issues before they reach the bot reviewer.
164
+ ---
165
+
166
+ Read and follow: .deft/core/skills/deft-directive-pre-pr/SKILL.md
167
+ `,
168
+ },
169
+ {
170
+ dir: "deft-directive-sync",
171
+ content: `---
172
+ name: deft-directive-sync
173
+ description: >-
174
+ Session-start framework sync skill. Use at the beginning of a session to
175
+ pull latest framework updates, validate project files, and confirm alignment
176
+ before starting work.
177
+ ---
178
+
179
+ Read and follow: .deft/core/skills/deft-directive-sync/SKILL.md
180
+ `,
181
+ },
182
+ ];
183
+ const BARE_SEMVER = /^\d+\.\d+\.\d+([-+][0-9A-Za-z.-]+)?$/;
184
+ export function buildInstallManifestText(fields) {
185
+ let effectiveTag = fields.tag;
186
+ if (effectiveTag && !effectiveTag.startsWith("v") && BARE_SEMVER.test(effectiveTag)) {
187
+ effectiveTag = `v${effectiveTag}`;
188
+ }
189
+ const effectiveRef = fields.ref || effectiveTag;
190
+ return (`ref: '${effectiveRef}'\n` +
191
+ `sha: '${fields.sha}'\n` +
192
+ `tag: '${effectiveTag}'\n` +
193
+ `install_root: '${fields.installRoot}'\n` +
194
+ `fetched_at: '${fields.fetchedAt}'\n` +
195
+ `fetched_by: '${fields.fetchedBy}'\n`);
196
+ }
197
+ export function writeInstallManifest(projectDir, deftDir, fields) {
198
+ const installRoot = fields.installRoot ||
199
+ relative(projectDir, deftDir).split("\\").join("/") ||
200
+ CANONICAL_INSTALL_ROOT;
201
+ const body = buildInstallManifestText({ ...fields, installRoot });
202
+ mkdirSync(deftDir, { recursive: true });
203
+ const path = join(deftDir, "VERSION");
204
+ writeFileSync(path, body, "utf8");
205
+ return path;
206
+ }
207
+ export function writeAgentsMd(projectDir, deftDir, io) {
208
+ const plan = agentsRefreshPlan(projectDir, { frameworkRoot: deftDir });
209
+ const state = plan.state;
210
+ if (state === "current") {
211
+ io.printf(`AGENTS.md already advertises install root ${CANONICAL_INSTALL_ROOT} — skipping.\n`);
212
+ return false;
213
+ }
214
+ if (state === "template-missing" || state === "template-malformed" || state === "unreadable") {
215
+ throw new Error(`AGENTS.md render failed: ${String(state)}`);
216
+ }
217
+ const newContent = plan.new_content;
218
+ if (typeof newContent !== "string") {
219
+ throw new Error("AGENTS.md render produced no content");
220
+ }
221
+ const path = join(projectDir, "AGENTS.md");
222
+ writeFileSync(path, newContent, "utf8");
223
+ if (state === "absent") {
224
+ io.printf("AGENTS.md created.\n");
225
+ }
226
+ else {
227
+ io.printf("AGENTS.md updated with deft entries.\n");
228
+ }
229
+ return true;
230
+ }
231
+ async function ensureVbriefLifecycleDirs(consumerVbrief) {
232
+ for (const sub of VBRIEF_LIFECYCLE_DIRS) {
233
+ const dir = join(consumerVbrief, sub);
234
+ await mkdir(dir, { recursive: true, mode: 0o755 });
235
+ const gitkeep = join(dir, ".gitkeep");
236
+ try {
237
+ await stat(gitkeep);
238
+ continue;
239
+ }
240
+ catch {
241
+ // absent — may write below
242
+ }
243
+ const entries = await readdir(dir);
244
+ if (entries.length > 0)
245
+ continue;
246
+ await writeFile(gitkeep, VBRIEF_LIFECYCLE_GITKEEP, "utf8");
247
+ }
248
+ }
249
+ function vbriefLifecycleDirsPresent(consumerVbrief) {
250
+ return VBRIEF_LIFECYCLE_DIRS.every((sub) => {
251
+ try {
252
+ return statSync(join(consumerVbrief, sub)).isDirectory();
253
+ }
254
+ catch {
255
+ return false;
256
+ }
257
+ });
258
+ }
259
+ export async function writeConsumerVbrief(projectDir, deftDir, io) {
260
+ const consumerVbrief = join(projectDir, "vbrief");
261
+ const schemasDst = join(consumerVbrief, "schemas");
262
+ const vbriefMdDst = join(consumerVbrief, "vbrief.md");
263
+ const schemasPresent = existsSync(schemasDst) && statSync(schemasDst).isDirectory();
264
+ const vbriefMdPresent = existsSync(vbriefMdDst) && statSync(vbriefMdDst).isFile();
265
+ const lifecyclePresent = vbriefLifecycleDirsPresent(consumerVbrief);
266
+ if (schemasPresent && vbriefMdPresent && lifecyclePresent) {
267
+ io.printf("vbrief/ already present at project root — skipping.\n");
268
+ return false;
269
+ }
270
+ mkdirSync(consumerVbrief, { recursive: true });
271
+ if (!schemasPresent) {
272
+ const fwSchemas = join(deftDir, "vbrief", "schemas");
273
+ if (existsSync(fwSchemas) && statSync(fwSchemas).isDirectory()) {
274
+ await copyTree(fwSchemas, schemasDst);
275
+ }
276
+ else {
277
+ mkdirSync(schemasDst, { recursive: true });
278
+ }
279
+ }
280
+ if (!vbriefMdPresent) {
281
+ const fwVbriefMd = join(deftDir, "vbrief", "vbrief.md");
282
+ if (existsSync(fwVbriefMd)) {
283
+ copyFileSync(fwVbriefMd, vbriefMdDst);
284
+ }
285
+ else {
286
+ writeFileSync(vbriefMdDst, VBRIEF_README_BODY, "utf8");
287
+ }
288
+ }
289
+ await ensureVbriefLifecycleDirs(consumerVbrief);
290
+ io.printf("vbrief/ deposited at project root (schemas + vbrief.md + lifecycle dirs).\n");
291
+ return true;
292
+ }
293
+ export function writeAgentsSkills(projectDir, io) {
294
+ const allExist = AGENTS_SKILLS.every((skill) => existsSync(join(projectDir, ".agents", "skills", skill.dir, "SKILL.md")));
295
+ if (allExist) {
296
+ io.printf(".agents/skills/ already present — skipping.\n");
297
+ return false;
298
+ }
299
+ for (const skill of AGENTS_SKILLS) {
300
+ const dir = join(projectDir, ".agents", "skills", skill.dir);
301
+ mkdirSync(dir, { recursive: true });
302
+ const path = join(dir, "SKILL.md");
303
+ if (existsSync(path))
304
+ continue;
305
+ writeFileSync(path, skill.content, "utf8");
306
+ }
307
+ io.printf(".agents/skills/ created — deft skills will be auto-discovered.\n");
308
+ return true;
309
+ }
310
+ function hasTopLevelIncludes(content) {
311
+ if (!content)
312
+ return false;
313
+ const norm = `\n${content.replace(/\r\n/g, "\n").replace(/\r/g, "\n")}`;
314
+ if (norm.includes("\nincludes:"))
315
+ return true;
316
+ return content.trimStart().startsWith("includes:");
317
+ }
318
+ function insertDeftIncludeAfterIncludesLine(content) {
319
+ const norm = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
320
+ const lines = norm.split("\n");
321
+ for (let i = 0; i < lines.length; i += 1) {
322
+ const line = lines[i] ?? "";
323
+ if (line.length === 0 || line[0] === " " || line[0] === "\t")
324
+ continue;
325
+ const trimmed = line.trimEnd();
326
+ if (trimmed === "includes:") {
327
+ const out = [
328
+ ...lines.slice(0, i + 1),
329
+ ...DEFT_INCLUDE_CHILD_BLOCK.trimEnd().split("\n"),
330
+ ...lines.slice(i + 1),
331
+ ];
332
+ return { content: out.join("\n"), ok: true };
333
+ }
334
+ if (trimmed.startsWith("includes:") && trimmed.length > "includes:".length) {
335
+ const rest = trimmed.slice("includes:".length).trimStart();
336
+ if (rest.startsWith("#")) {
337
+ const out = [
338
+ ...lines.slice(0, i + 1),
339
+ ...DEFT_INCLUDE_CHILD_BLOCK.trimEnd().split("\n"),
340
+ ...lines.slice(i + 1),
341
+ ];
342
+ return { content: out.join("\n"), ok: true };
343
+ }
344
+ }
345
+ }
346
+ return { content, ok: false };
347
+ }
348
+ export function ensureTaskfile(projectDir, io) {
349
+ const path = join(projectDir, "Taskfile.yml");
350
+ let existing = "";
351
+ if (existsSync(path)) {
352
+ existing = readFileSync(path, "utf8");
353
+ }
354
+ if (existing.includes(CANONICAL_TASKFILE_INCLUDE)) {
355
+ io.printf("Taskfile.yml already includes deft — skipping wiring.\n");
356
+ return false;
357
+ }
358
+ let resultText = "";
359
+ if (existing === "") {
360
+ resultText = MINIMAL_TASKFILE;
361
+ io.printf("Created minimal Taskfile.yml with deft include (Epic-4).\n");
362
+ }
363
+ else if (hasTopLevelIncludes(existing)) {
364
+ const inserted = insertDeftIncludeAfterIncludesLine(existing);
365
+ if (inserted.ok) {
366
+ resultText = inserted.content;
367
+ io.printf("Inserted deft entry inside existing `includes:` block in Taskfile.yml (Epic-4).\n");
368
+ }
369
+ else {
370
+ resultText =
371
+ `${existing}${existing.endsWith("\n") ? "" : "\n"}\n` +
372
+ "# deft-install --yes (Epic-4): could not locate the existing top-level `includes:` line for structural insertion; appended a fresh block. Manual merge recommended.\n" +
373
+ "includes:\n" +
374
+ " deft:\n" +
375
+ " taskfile: ./.deft/core/Taskfile.yml\n" +
376
+ " optional: true\n";
377
+ io.printf("Appended fresh `includes:` block to Taskfile.yml -- top-level includes: detected but structural insertion fell through; manual merge recommended.\n");
378
+ }
379
+ }
380
+ else {
381
+ resultText =
382
+ `${existing}${existing.endsWith("\n") ? "" : "\n"}\n` +
383
+ "# Added by deft-install --yes (Epic-4)\n" +
384
+ "includes:\n" +
385
+ " deft:\n" +
386
+ " taskfile: ./.deft/core/Taskfile.yml\n" +
387
+ " optional: true\n";
388
+ io.printf("Appended new `includes:` block with deft entry to Taskfile.yml (Epic-4).\n");
389
+ }
390
+ writeFileSync(path, resultText, "utf8");
391
+ return true;
392
+ }
393
+ const HOOK_FILENAMES = ["pre-commit", "pre-push"];
394
+ const HOOK_FILE_MODE = 0o755;
395
+ export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
396
+ const srcDir = join(deftDir, ".githooks");
397
+ if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {
398
+ io.printf(`git hooks source ${srcDir} absent — skipping hook wiring.\n`);
399
+ return false;
400
+ }
401
+ const dstDir = join(projectDir, ".githooks");
402
+ mkdirSync(dstDir, { recursive: true });
403
+ let filesDeposited = false;
404
+ for (const name of HOOK_FILENAMES) {
405
+ const src = join(srcDir, name);
406
+ if (!existsSync(src))
407
+ continue;
408
+ const data = readFileSync(src);
409
+ const dst = join(dstDir, name);
410
+ const existing = existsSync(dst) ? readFileSync(dst) : null;
411
+ if (!existing?.equals(data)) {
412
+ writeFileSync(dst, data, { mode: HOOK_FILE_MODE });
413
+ filesDeposited = true;
414
+ }
415
+ if (platform() !== "win32") {
416
+ try {
417
+ const mode = statSync(dst).mode & 0o777;
418
+ if ((mode & 0o111) === 0) {
419
+ chmodSync(dst, HOOK_FILE_MODE);
420
+ filesDeposited = true;
421
+ }
422
+ }
423
+ catch {
424
+ // non-fatal
425
+ }
426
+ }
427
+ }
428
+ const getHooksPath = seams.getHooksPath ??
429
+ ((dir) => {
430
+ try {
431
+ return execFileSync("git", ["-C", dir, "config", "--get", "core.hooksPath"], {
432
+ encoding: "utf8",
433
+ }).trim();
434
+ }
435
+ catch {
436
+ return "";
437
+ }
438
+ });
439
+ const setHooksPath = seams.setHooksPath ??
440
+ ((dir, value) => {
441
+ try {
442
+ execFileSync("git", ["-C", dir, "config", "core.hooksPath", value], { encoding: "utf8" });
443
+ return true;
444
+ }
445
+ catch {
446
+ return false;
447
+ }
448
+ });
449
+ const target = ".githooks";
450
+ const current = getHooksPath(projectDir) ?? "";
451
+ let configWired = false;
452
+ if (current !== target) {
453
+ if (setHooksPath(projectDir, target)) {
454
+ configWired = true;
455
+ io.printf("git hooks wired: core.hooksPath=.githooks (#1463).\n");
456
+ }
457
+ else {
458
+ io.printf("Warning: could not set core.hooksPath=.githooks — run `git config core.hooksPath .githooks` manually.\n");
459
+ }
460
+ }
461
+ else {
462
+ io.printf("git hooks already wired — skipping core.hooksPath write.\n");
463
+ }
464
+ if (filesDeposited) {
465
+ io.printf(".githooks/ deposited at project root (#1463).\n");
466
+ }
467
+ else if (configWired) {
468
+ io.printf(".githooks/ already present; git config updated (#1463).\n");
469
+ }
470
+ return filesDeposited || configWired;
471
+ }
472
+ function escapeEre(value) {
473
+ return value.replace(/[.^$*+?()[\]{}|\\]/g, "\\$&");
474
+ }
475
+ function installerManagedGuardEre() {
476
+ const matchers = [
477
+ { exact: "AGENTS.md" },
478
+ { prefix: ".agents/" },
479
+ { prefix: ".githooks/" },
480
+ { exact: ".gitattributes" },
481
+ { exact: ".gitignore" },
482
+ { exact: "greptile.json" },
483
+ { exact: CODEQL_CONFIG_REL },
484
+ { exact: CORE_GUARD_WORKFLOW_REL },
485
+ { exact: "vbrief/.deft-version" },
486
+ { exact: "vbrief/vbrief.md" },
487
+ { prefix: "vbrief/schemas/" },
488
+ { prefix: "vbrief/migration/" },
489
+ ...VBRIEF_LIFECYCLE_DIRS.map((sub) => ({ exact: `vbrief/${sub}/.gitkeep` })),
490
+ ];
491
+ return matchers
492
+ .map((m) => (m.exact ? `^${escapeEre(m.exact)}$` : `^${escapeEre(m.prefix ?? "")}`))
493
+ .join("|");
494
+ }
495
+ function githubActionsExpr(expression) {
496
+ return ["$", "{{ ", expression, " }}"].join("");
497
+ }
498
+ function coreGuardWorkflowContent() {
499
+ const baseSha = githubActionsExpr("github.event.pull_request.base.sha");
500
+ const headSha = githubActionsExpr("github.event.pull_request.head.sha");
501
+ return ("name: deft-core-guard\n\n" +
502
+ "# Deft framework guard (#1430): a single PR should not mix changes to the\n" +
503
+ "# vendored framework payload (.deft/core/**) with changes to your own project\n" +
504
+ "# files. Framework updates come from `deft-install` / upgrade and should\n" +
505
+ "# land in their own PR so reviewers (and bot reviewers) can treat them as\n" +
506
+ "# packaged, machine-managed assets. Delete this file if you do not want the guard.\n" +
507
+ "on:\n" +
508
+ " pull_request:\n\n" +
509
+ "permissions:\n" +
510
+ " contents: read\n\n" +
511
+ "jobs:\n" +
512
+ " no-mixed-core-and-app:\n" +
513
+ " runs-on: ubuntu-latest\n" +
514
+ " steps:\n" +
515
+ " - uses: actions/checkout@v4\n" +
516
+ " with:\n" +
517
+ " fetch-depth: 0\n" +
518
+ " - name: Refuse PRs that mix .deft/core/** with non-framework paths\n" +
519
+ " env:\n" +
520
+ ` BASE_SHA: ${baseSha}\n` +
521
+ ` HEAD_SHA: ${headSha}\n` +
522
+ " run: |\n" +
523
+ " set -eu\n" +
524
+ ' changed=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")\n' +
525
+ ' echo "Changed files:"\n' +
526
+ ' echo "$changed"\n' +
527
+ " core=$(printf '%s\\n' \"$changed\" | grep -E '^\\.deft/core/' || true)\n" +
528
+ " app=$(printf '%s\\n' \"$changed\" | grep -vE '^\\.deft/core/' | grep -vE '" +
529
+ installerManagedGuardEre() +
530
+ "' | grep -v '^$' || true)\n" +
531
+ ' if [ -n "$core" ] && [ -n "$app" ]; then\n' +
532
+ ' echo "::error title=deft-core guard (#1430)::This PR changes the vendored framework payload (.deft/core/**) AND non-framework files. Split the framework update into its own PR."\n' +
533
+ ' echo "--- framework (.deft/core/**) changes ---"; printf \'%s\\n\' "$core"\n' +
534
+ ' echo "--- non-framework changes ---"; printf \'%s\\n\' "$app"\n' +
535
+ " exit 1\n" +
536
+ " fi\n" +
537
+ ' echo "OK: no mixed framework + app changes."\n');
538
+ }
539
+ export function ensureGitattributes(projectDir, io) {
540
+ const path = join(projectDir, ".gitattributes");
541
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
542
+ const present = new Set(existing.split("\n").map((line) => line.trim()));
543
+ const additions = CORE_GITATTRIBUTES_LINES.filter((line) => !present.has(line));
544
+ if (additions.length === 0) {
545
+ io.printf(`.gitattributes already marks ${CORE_GLOB} as generated/vendored — skipping.\n`);
546
+ return false;
547
+ }
548
+ let body = existing;
549
+ if (body && !body.endsWith("\n"))
550
+ body += "\n";
551
+ if (body && !body.endsWith("\n\n"))
552
+ body += "\n";
553
+ body +=
554
+ "# Deft framework: the vendored payload is packaged framework code, not\n" +
555
+ "# consumer source. Mark it generated + vendored so language stats and\n" +
556
+ "# diffs treat .deft/core/** as machine-managed (#1430).\n";
557
+ for (const add of additions) {
558
+ body += `${add}\n`;
559
+ }
560
+ writeFileSync(path, body, "utf8");
561
+ io.printf(`.gitattributes updated with linguist markers: ${additions.join(", ")}\n`);
562
+ return true;
563
+ }
564
+ function greptilePatternPresent(patterns, glob) {
565
+ return patterns.split("\n").some((line) => line.trim() === glob);
566
+ }
567
+ function appendGreptilePattern(patterns, glob) {
568
+ if (patterns.trim() === "")
569
+ return glob;
570
+ if (patterns.endsWith("\n"))
571
+ return `${patterns}${glob}`;
572
+ return `${patterns}\n${glob}`;
573
+ }
574
+ export function ensureGreptileIgnore(projectDir, io) {
575
+ const path = join(projectDir, "greptile.json");
576
+ const fileExisted = existsSync(path);
577
+ let raw = fileExisted ? readFileSync(path, "utf8") : "";
578
+ if (!raw.trim())
579
+ raw = "{}";
580
+ let obj;
581
+ try {
582
+ const parsed = JSON.parse(raw);
583
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
584
+ throw new Error("greptile.json root must be a JSON object");
585
+ }
586
+ obj = parsed;
587
+ }
588
+ catch (cause) {
589
+ throw new Error(`could not parse greptile.json (leaving it unchanged): ${String(cause)}`);
590
+ }
591
+ let patterns = "";
592
+ if ("ignorePatterns" in obj) {
593
+ if (typeof obj.ignorePatterns !== "string") {
594
+ throw new Error("greptile.json ignorePatterns is not a newline-separated string");
595
+ }
596
+ patterns = obj.ignorePatterns;
597
+ }
598
+ if (fileExisted && greptilePatternPresent(patterns, CORE_GLOB)) {
599
+ io.printf(`greptile.json already ignores ${CORE_GLOB} — skipping.\n`);
600
+ return false;
601
+ }
602
+ obj.ignorePatterns = appendGreptilePattern(patterns, CORE_GLOB);
603
+ writeFileSync(path, `${JSON.stringify(obj, null, 2)}\n`, "utf8");
604
+ io.printf(fileExisted
605
+ ? `greptile.json updated: bot review now ignores ${CORE_GLOB}.\n`
606
+ : `greptile.json created: bot review ignores ${CORE_GLOB}.\n`);
607
+ return true;
608
+ }
609
+ function codeqlConfigDefault() {
610
+ return ("# Deft framework: exclude the vendored payload from CodeQL analysis (#1430).\n" +
611
+ "# .deft/core/** is packaged framework code, not consumer source.\n" +
612
+ 'name: "CodeQL config (deft)"\n' +
613
+ "paths-ignore:\n" +
614
+ ` - '${CORE_GLOB}'\n`);
615
+ }
616
+ function codeqlPathsIgnorePresent(content, glob) {
617
+ const candidates = [`- '${glob}'`, `- "${glob}"`, `- ${glob}`];
618
+ let inBlock = false;
619
+ for (const line of content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n")) {
620
+ if (line.length > 0 && line[0] !== " " && line[0] !== "\t") {
621
+ const trimmed = line.trimEnd();
622
+ if (trimmed === "paths-ignore:") {
623
+ inBlock = true;
624
+ continue;
625
+ }
626
+ inBlock = false;
627
+ continue;
628
+ }
629
+ if (inBlock && candidates.includes(line.trim()))
630
+ return true;
631
+ }
632
+ return false;
633
+ }
634
+ function insertCodeqlPathsIgnore(content, glob) {
635
+ const norm = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
636
+ const lines = norm.split("\n");
637
+ const entry = ` - '${glob}'`;
638
+ for (let i = 0; i < lines.length; i += 1) {
639
+ const line = lines[i] ?? "";
640
+ if (line.length === 0 || line[0] === " " || line[0] === "\t")
641
+ continue;
642
+ if ((line.trimEnd() ?? "") === "paths-ignore:") {
643
+ const out = [...lines.slice(0, i + 1), entry, ...lines.slice(i + 1)];
644
+ return { content: out.join("\n"), ok: true };
645
+ }
646
+ }
647
+ return { content, ok: false };
648
+ }
649
+ export function ensureCodeqlPathsIgnore(projectDir, io) {
650
+ const path = join(projectDir, CODEQL_CONFIG_REL);
651
+ if (!existsSync(path)) {
652
+ mkdirSync(dirname(path), { recursive: true });
653
+ writeFileSync(path, codeqlConfigDefault(), "utf8");
654
+ io.printf(`${CODEQL_CONFIG_REL} created: CodeQL ignores ${CORE_GLOB}.\n`);
655
+ return true;
656
+ }
657
+ const existing = readFileSync(path, "utf8");
658
+ if (codeqlPathsIgnorePresent(existing, CORE_GLOB)) {
659
+ io.printf(`${CODEQL_CONFIG_REL} already ignores ${CORE_GLOB} — skipping.\n`);
660
+ return false;
661
+ }
662
+ const inserted = insertCodeqlPathsIgnore(existing, CORE_GLOB);
663
+ const updated = inserted.ok
664
+ ? inserted.content
665
+ : `${existing}${existing.endsWith("\n") ? "" : "\n"}paths-ignore:\n - '${CORE_GLOB}'\n`;
666
+ writeFileSync(path, updated, "utf8");
667
+ io.printf(`${CODEQL_CONFIG_REL} updated: CodeQL now ignores ${CORE_GLOB}.\n`);
668
+ return true;
669
+ }
670
+ export function ensureCoreGuardWorkflow(projectDir, io) {
671
+ const path = join(projectDir, CORE_GUARD_WORKFLOW_REL);
672
+ const desired = coreGuardWorkflowContent();
673
+ if (existsSync(path)) {
674
+ const existing = readFileSync(path, "utf8");
675
+ if (existing === desired) {
676
+ io.printf(`${CORE_GUARD_WORKFLOW_REL} already current — skipping.\n`);
677
+ return false;
678
+ }
679
+ if (!existing.includes("name: deft-core-guard")) {
680
+ io.printf(`${CORE_GUARD_WORKFLOW_REL} present but not deft-managed — leaving unchanged.\n`);
681
+ return false;
682
+ }
683
+ writeFileSync(path, desired, "utf8");
684
+ io.printf(`${CORE_GUARD_WORKFLOW_REL} refreshed: deft-core-guard allowlist updated (#1478).\n`);
685
+ return true;
686
+ }
687
+ mkdirSync(dirname(path), { recursive: true });
688
+ writeFileSync(path, desired, "utf8");
689
+ io.printf(`${CORE_GUARD_WORKFLOW_REL} created: CI refuses PRs mixing ${CORE_GLOB} with app files.\n`);
690
+ return true;
691
+ }
692
+ export async function pruneFrameworkSelfTests(projectDir, io) {
693
+ const path = join(projectDir, FRAMEWORK_SELF_TEST_REL);
694
+ try {
695
+ const info = await stat(path);
696
+ if (!info.isDirectory())
697
+ return false;
698
+ }
699
+ catch {
700
+ return false;
701
+ }
702
+ await rm(path, { recursive: true, force: true });
703
+ io.printf(`Removed vendored framework self-tests (${FRAMEWORK_SELF_TEST_REL}) from the consumer deposit (#1474).\n`);
704
+ return true;
705
+ }
706
+ export async function pruneVendoredTsTests(projectDir, io) {
707
+ const root = join(projectDir, VENDORED_TS_PACKAGES_REL);
708
+ try {
709
+ if (!(await stat(root)).isDirectory())
710
+ return 0;
711
+ }
712
+ catch {
713
+ return 0;
714
+ }
715
+ let removed = 0;
716
+ async function walk(dir) {
717
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
718
+ const full = join(dir, entry.name);
719
+ if (entry.isDirectory()) {
720
+ await walk(full);
721
+ }
722
+ else if (entry.isFile() && VENDORED_TS_TEST_RE.test(entry.name)) {
723
+ await rm(full, { force: true });
724
+ removed += 1;
725
+ }
726
+ }
727
+ }
728
+ await walk(root);
729
+ if (removed > 0) {
730
+ io.printf(`Removed ${removed} vendored TypeScript test file(s) under ${VENDORED_TS_PACKAGES_REL} from the consumer deposit (#1878).\n`);
731
+ }
732
+ return removed;
733
+ }
734
+ /** Best-effort #1430 neutralization deposit (mirrors depositNeutralization). */
735
+ export async function depositNeutralization(projectDir, io) {
736
+ const steps = [
737
+ () => ensureGitattributes(projectDir, io),
738
+ () => ensureGreptileIgnore(projectDir, io),
739
+ () => ensureCodeqlPathsIgnore(projectDir, io),
740
+ () => ensureCoreGuardWorkflow(projectDir, io),
741
+ () => pruneFrameworkSelfTests(projectDir, io),
742
+ async () => (await pruneVendoredTsTests(projectDir, io)) > 0,
743
+ ];
744
+ for (const step of steps) {
745
+ try {
746
+ await step();
747
+ }
748
+ catch (cause) {
749
+ io.printf(`Warning: neutralization step failed: ${String(cause)}\n`);
750
+ }
751
+ }
752
+ }
753
+ //# sourceMappingURL=scaffold.js.map