arcane-cli 0.17.0 → 0.19.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.
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { fileURLToPath } from "url";
9
9
  import { dirname as dirname3, join as join11 } from "path";
10
10
 
11
11
  // src/commands/init.ts
12
- import { select as select2, confirm } from "@inquirer/prompts";
12
+ import { select as select2, confirm, input as input2 } from "@inquirer/prompts";
13
13
  import { join as join4 } from "path";
14
14
  import chalk2 from "chalk";
15
15
 
@@ -98,6 +98,19 @@ var ManifestInvalidFieldError = class extends Error {
98
98
  var MANIFEST_FILE = ".arcane.json";
99
99
  var VALID_TRACKING_MODES = ["internal", "external"];
100
100
  var VALID_EXTERNAL_PROVIDERS = ["ado", "jira", "other"];
101
+ var VALID_CONTENT_SENSITIVITY = ["standard", "sensitive"];
102
+ function isValidSubjectRoot(value) {
103
+ const invalid = value.trim() === "" || path2.isAbsolute(value) || // Windows drive-relative ("C:docs") and UNC-ish values also escape.
104
+ /^[a-zA-Z]:/.test(value) || value.startsWith("\\\\") || value.startsWith("\\") || value.split(/[\\/]/).includes("..") || // A stored subject_root is interpolated into agent prompts downstream, so
105
+ // keep it a single path-shaped token rather than a multi-line payload.
106
+ /[\r\n\0]/.test(value);
107
+ return !invalid;
108
+ }
109
+ function validateSubjectRoot(value, filePath) {
110
+ if (!isValidSubjectRoot(value)) {
111
+ throw new ManifestInvalidFieldError(filePath, "subject_root", value);
112
+ }
113
+ }
101
114
  function manifestPath(targetDir) {
102
115
  return path2.join(targetDir, MANIFEST_FILE);
103
116
  }
@@ -108,6 +121,19 @@ function validateTrackingFields(manifest, filePath) {
108
121
  if (manifest.external_provider !== void 0 && manifest.external_provider !== null && !VALID_EXTERNAL_PROVIDERS.includes(manifest.external_provider)) {
109
122
  throw new ManifestInvalidFieldError(filePath, "external_provider", manifest.external_provider);
110
123
  }
124
+ if (manifest.content_sensitivity !== void 0 && !VALID_CONTENT_SENSITIVITY.includes(manifest.content_sensitivity)) {
125
+ throw new ManifestInvalidFieldError(
126
+ filePath,
127
+ "content_sensitivity",
128
+ manifest.content_sensitivity
129
+ );
130
+ }
131
+ if (manifest.subject_root !== void 0 && manifest.subject_root !== null) {
132
+ if (typeof manifest.subject_root !== "string") {
133
+ throw new ManifestInvalidFieldError(filePath, "subject_root", manifest.subject_root);
134
+ }
135
+ validateSubjectRoot(manifest.subject_root, filePath);
136
+ }
111
137
  }
112
138
  async function readManifest(targetDir) {
113
139
  const filePath = manifestPath(targetDir);
@@ -155,15 +181,35 @@ var PROFILE_CONFIGS = [
155
181
  id: "lite",
156
182
  displayName: "Essentials",
157
183
  description: "Spells + core governance \u2014 fast start",
158
- components: ["spell-prompts", "claude-commands", "agent-output-instructions", "git-conventions", "testing-standards", "framework-decisions", "session-continuity"]
184
+ components: [
185
+ "spells-session",
186
+ "spells-capture",
187
+ "spells-delivery",
188
+ "spells-review",
189
+ "spells-planning",
190
+ "spells-build",
191
+ "spells-venture",
192
+ "spells-meta",
193
+ "agent-output-instructions",
194
+ "git-conventions",
195
+ "testing-standards",
196
+ "framework-decisions",
197
+ "session-continuity"
198
+ ]
159
199
  },
160
200
  {
161
201
  id: "methodology",
162
202
  displayName: "Methodology Suite",
163
203
  description: "Spells + governance \u2014 full methodology without security/infra docs",
164
204
  components: [
165
- "spell-prompts",
166
- "claude-commands",
205
+ "spells-session",
206
+ "spells-capture",
207
+ "spells-delivery",
208
+ "spells-review",
209
+ "spells-planning",
210
+ "spells-build",
211
+ "spells-venture",
212
+ "spells-meta",
167
213
  "agent-output-instructions",
168
214
  "git-conventions",
169
215
  "testing-standards",
@@ -175,6 +221,44 @@ var PROFILE_CONFIGS = [
175
221
  "spell-authoring-standards"
176
222
  ]
177
223
  },
224
+ {
225
+ id: "docs",
226
+ displayName: "Docs / Records",
227
+ description: "Documentation and records repositories \u2014 no code, test, or deployment workflows",
228
+ components: [
229
+ // Deliberate exclusions, per docs-mode PRD MH-01: "spells-build"
230
+ // (implementation, tests, stack experts, release, deployment, PRD
231
+ // enchantment, asset tooling), "spells-review" (adversarial CODE review
232
+ // -- its own workflow validates test coverage against source, which a
233
+ // docs repo has neither of), and "spells-venture" (hub-only).
234
+ //
235
+ // MH-01 requires every retained spell to complete its core workflow
236
+ // without source code, tests, CI, or an external tracker. Asserted --
237
+ // exclusions included, which MH-01 requires explicitly -- in
238
+ // test/docs-profile-registry-split.test.ts.
239
+ "spells-session",
240
+ "spells-capture",
241
+ "spells-delivery",
242
+ "spells-planning",
243
+ "spells-meta",
244
+ "spells-docs",
245
+ "docs-baseline",
246
+ "records-conventions",
247
+ "agent-output-instructions",
248
+ "git-conventions",
249
+ // Four retained docs spells link to agent-policies; it carries no code
250
+ // implication, so shipping it closes a dangling reference cheaply.
251
+ "agent-policies",
252
+ "framework-decisions",
253
+ "session-continuity",
254
+ "decision-documentation-standard",
255
+ "agent-work-queue-model",
256
+ "development-methodology",
257
+ "naming-conventions",
258
+ "universal-agent-rules",
259
+ "spell-authoring-standards"
260
+ ]
261
+ },
178
262
  {
179
263
  id: "governance-only",
180
264
  displayName: "Governance Only",
@@ -323,46 +407,159 @@ var COMPONENTS = [
323
407
  description: "Copilot enforcement rules: PR link format, merge strategy, and branch naming",
324
408
  files: [".github/instructions/agent-output.instructions.md"]
325
409
  },
326
- // Spell prompts — installed to .github/prompts/ (VS Code Copilot convention)
410
+ // Repository baseline files (EF-10 + EF-17). skipExisting: these are
411
+ // user-owned once written -- a repo with its own .gitattributes has an
412
+ // intentional policy Arcane must not overwrite. Sources are stored under a
413
+ // non-dotfile path and mapped via sourceOverrides; see RegistryComponent.
414
+ {
415
+ name: "docs-baseline",
416
+ description: "Line-ending and binary-format baseline for document repositories (.gitattributes/.gitignore)",
417
+ files: [".gitattributes", ".gitignore"],
418
+ sourceOverrides: {
419
+ ".gitattributes": "docs-baseline/gitattributes",
420
+ ".gitignore": "docs-baseline/gitignore"
421
+ },
422
+ skipExisting: true,
423
+ initOnly: true
424
+ },
425
+ {
426
+ name: "records-conventions",
427
+ description: "Supersession, tombstone, and retention conventions for records repositories",
428
+ files: [".arcane/governance/records-conventions.md"]
429
+ },
430
+ // ─── Spells ────────────────────────────────────────────────────────────────
431
+ // Split from the former monolithic `spell-prompts` + `claude-commands` pair
432
+ // so a profile can select spells by capability (see ARC-0NN). Grouping lives
433
+ // here and only here -- every spell file stays flat and unrenamed on disk.
434
+ //
435
+ // Each component carries BOTH client formats of the same spell: the Copilot
436
+ // prompt (.github/prompts/*.prompt.md) and the Claude Code wrapper
437
+ // (.claude/commands/*.md). They are never independently selectable -- every
438
+ // profile that wanted one always wanted the other -- and pairing them here
439
+ // makes it structurally impossible for the two formats of one spell to drift
440
+ // apart across profiles.
327
441
  {
328
- name: "spell-prompts",
329
- description: "Full Arcane spell prompt library (34 spells)",
442
+ name: "spells-session",
443
+ description: "Session lifecycle \u2014 open, close, commit, status, version",
330
444
  files: [
445
+ ".github/prompts/spell-open-session.prompt.md",
446
+ ".claude/commands/spell-open-session.md",
447
+ ".github/prompts/spell-close-session.prompt.md",
448
+ ".claude/commands/spell-close-session.md",
449
+ ".github/prompts/spell-commit-work.prompt.md",
450
+ ".claude/commands/spell-commit-work.md",
451
+ ".github/prompts/spell-status.prompt.md",
452
+ ".claude/commands/spell-status.md",
331
453
  ".github/prompts/spell-arcane-version.prompt.md",
332
- ".github/prompts/spell-architect.prompt.md",
454
+ ".claude/commands/spell-arcane-version.md"
455
+ ]
456
+ },
457
+ {
458
+ name: "spells-capture",
459
+ description: "Idea, todo, feedback and explanation capture",
460
+ files: [
461
+ ".github/prompts/spell-save-idea.prompt.md",
462
+ ".claude/commands/spell-save-idea.md",
463
+ ".github/prompts/spell-todo.prompt.md",
464
+ ".claude/commands/spell-todo.md",
465
+ ".github/prompts/spell-feedback.prompt.md",
466
+ ".claude/commands/spell-feedback.md",
467
+ ".github/prompts/spell-suggest-feature.prompt.md",
468
+ ".claude/commands/spell-suggest-feature.md",
469
+ ".github/prompts/spell-document.prompt.md",
470
+ ".claude/commands/spell-document.md",
333
471
  ".github/prompts/spell-brainstorm.prompt.md",
472
+ ".claude/commands/spell-brainstorm.md",
473
+ ".github/prompts/spell-explain-concept.prompt.md",
474
+ ".claude/commands/spell-explain-concept.md"
475
+ ]
476
+ },
477
+ {
478
+ name: "spells-delivery",
479
+ description: "Pull-request lifecycle \u2014 open a PR, respond to review feedback",
480
+ files: [
481
+ ".github/prompts/spell-create-pull-request.prompt.md",
482
+ ".claude/commands/spell-create-pull-request.md",
483
+ ".github/prompts/spell-address-review.prompt.md",
484
+ ".claude/commands/spell-address-review.md"
485
+ ]
486
+ },
487
+ {
488
+ name: "spells-review",
489
+ description: "Adversarial code review \u2014 requires source and tests, so excluded from docs-only profiles",
490
+ files: [
491
+ ".github/prompts/spell-review.prompt.md",
492
+ ".claude/commands/spell-review.md",
493
+ ".github/prompts/spell-review-batch.prompt.md",
494
+ ".claude/commands/spell-review-batch.md"
495
+ ]
496
+ },
497
+ {
498
+ name: "spells-planning",
499
+ description: "PRD, architecture, scoping and product review",
500
+ files: [
501
+ ".github/prompts/spell-plan.prompt.md",
502
+ ".claude/commands/spell-plan.md",
503
+ ".github/prompts/spell-architect.prompt.md",
504
+ ".claude/commands/spell-architect.md",
334
505
  ".github/prompts/spell-scope.prompt.md",
335
- ".github/prompts/spell-summon-venture.prompt.md",
506
+ ".claude/commands/spell-scope.md",
507
+ ".github/prompts/spell-product-review.prompt.md",
508
+ ".claude/commands/spell-product-review.md"
509
+ ]
510
+ },
511
+ {
512
+ name: "spells-build",
513
+ description: "Code and product delivery \u2014 implementation, tests, stack experts, release, deployment, asset tooling",
514
+ files: [
515
+ ".github/prompts/spell-implement.prompt.md",
516
+ ".claude/commands/spell-implement.md",
517
+ ".github/prompts/spell-test.prompt.md",
518
+ ".claude/commands/spell-test.md",
519
+ ".github/prompts/spell-full-cycle.prompt.md",
520
+ ".claude/commands/spell-full-cycle.md",
336
521
  ".github/prompts/spell-bug.prompt.md",
522
+ ".claude/commands/spell-bug.md",
337
523
  ".github/prompts/spell-bump.prompt.md",
338
- ".github/prompts/spell-check-drift.prompt.md",
339
- ".github/prompts/spell-close-session.prompt.md",
340
- ".github/prompts/spell-commit-work.prompt.md",
524
+ ".claude/commands/spell-bump.md",
341
525
  ".github/prompts/spell-dotnet-expert.prompt.md",
342
- ".github/prompts/spell-enchant.prompt.md",
343
- ".github/prompts/spell-explain-concept.prompt.md",
344
- ".github/prompts/spell-full-cycle.prompt.md",
345
- ".github/prompts/spell-generate-bot-icons.prompt.md",
346
- ".github/prompts/spell-implement.prompt.md",
347
- ".github/prompts/spell-open-session.prompt.md",
348
- ".github/prompts/spell-plan.prompt.md",
349
- ".github/prompts/spell-present-arcane.prompt.md",
350
- ".github/prompts/spell-product-review.prompt.md",
351
- ".github/prompts/spell-review.prompt.md",
526
+ ".claude/commands/spell-dotnet-expert.md",
352
527
  ".github/prompts/spell-security-review.prompt.md",
528
+ ".claude/commands/spell-security-review.md",
353
529
  ".github/prompts/spell-ship.prompt.md",
354
- ".github/prompts/spell-suggest-feature.prompt.md",
355
- ".github/prompts/spell-test.prompt.md",
356
- ".github/prompts/spell-todo.prompt.md",
357
- // Workflow spell additions — PR lifecycle, status, capture, feedback
358
- ".github/prompts/spell-address-review.prompt.md",
359
- ".github/prompts/spell-create-pull-request.prompt.md",
360
- ".github/prompts/spell-document.prompt.md",
361
- ".github/prompts/spell-feedback.prompt.md",
530
+ ".claude/commands/spell-ship.md",
531
+ ".github/prompts/spell-enchant.prompt.md",
532
+ ".claude/commands/spell-enchant.md",
533
+ ".github/prompts/spell-generate-bot-icons.prompt.md",
534
+ ".claude/commands/spell-generate-bot-icons.md"
535
+ ]
536
+ },
537
+ {
538
+ name: "spells-docs",
539
+ description: "Documentation and records workflows \u2014 adopt an existing document tree",
540
+ files: [
541
+ ".github/prompts/spell-adopt-docs.prompt.md",
542
+ ".claude/commands/spell-adopt-docs.md"
543
+ ]
544
+ },
545
+ {
546
+ name: "spells-venture",
547
+ description: "Hub-only venture management",
548
+ files: [
549
+ ".github/prompts/spell-summon-venture.prompt.md",
550
+ ".claude/commands/spell-summon-venture.md",
362
551
  ".github/prompts/spell-manifest.prompt.md",
363
- ".github/prompts/spell-save-idea.prompt.md",
364
- ".github/prompts/spell-status.prompt.md",
365
- ".github/prompts/spell-review-batch.prompt.md"
552
+ ".claude/commands/spell-manifest.md"
553
+ ]
554
+ },
555
+ {
556
+ name: "spells-meta",
557
+ description: "Arcane-about-Arcane \u2014 presentation and documentation drift detection",
558
+ files: [
559
+ ".github/prompts/spell-present-arcane.prompt.md",
560
+ ".claude/commands/spell-present-arcane.md",
561
+ ".github/prompts/spell-check-drift.prompt.md",
562
+ ".claude/commands/spell-check-drift.md"
366
563
  ]
367
564
  },
368
565
  // Templates
@@ -396,53 +593,11 @@ var COMPONENTS = [
396
593
  name: "agent-definitions",
397
594
  description: "Agent policy reference template (for governance-only setups \u2014 use `spell agents init` for the full agent system)",
398
595
  files: [".arcane/agents/agent-policies-template.md"]
399
- },
596
+ }
400
597
  // NOTE: the static `agent-files` component was retired — `.github/agents/*.agent.md`
401
598
  // files are generated per-roster by `spell agents init` / `spell agents sync`, so
402
599
  // installing a fixed-name set via `spell init` shipped stale names and collided
403
600
  // with the generated output.
404
- // Claude commands
405
- {
406
- name: "claude-commands",
407
- description: "Claude Code slash command wrappers for all 34 Arcane spells",
408
- files: [
409
- ".claude/commands/spell-arcane-version.md",
410
- ".claude/commands/spell-architect.md",
411
- ".claude/commands/spell-brainstorm.md",
412
- ".claude/commands/spell-scope.md",
413
- ".claude/commands/spell-summon-venture.md",
414
- ".claude/commands/spell-bug.md",
415
- ".claude/commands/spell-bump.md",
416
- ".claude/commands/spell-check-drift.md",
417
- ".claude/commands/spell-close-session.md",
418
- ".claude/commands/spell-commit-work.md",
419
- ".claude/commands/spell-dotnet-expert.md",
420
- ".claude/commands/spell-enchant.md",
421
- ".claude/commands/spell-explain-concept.md",
422
- ".claude/commands/spell-full-cycle.md",
423
- ".claude/commands/spell-generate-bot-icons.md",
424
- ".claude/commands/spell-implement.md",
425
- ".claude/commands/spell-open-session.md",
426
- ".claude/commands/spell-plan.md",
427
- ".claude/commands/spell-present-arcane.md",
428
- ".claude/commands/spell-product-review.md",
429
- ".claude/commands/spell-review.md",
430
- ".claude/commands/spell-security-review.md",
431
- ".claude/commands/spell-ship.md",
432
- ".claude/commands/spell-suggest-feature.md",
433
- ".claude/commands/spell-test.md",
434
- ".claude/commands/spell-todo.md",
435
- // Workflow spell additions — PR lifecycle, status, capture, feedback
436
- ".claude/commands/spell-address-review.md",
437
- ".claude/commands/spell-create-pull-request.md",
438
- ".claude/commands/spell-document.md",
439
- ".claude/commands/spell-feedback.md",
440
- ".claude/commands/spell-manifest.md",
441
- ".claude/commands/spell-save-idea.md",
442
- ".claude/commands/spell-status.md",
443
- ".claude/commands/spell-review-batch.md"
444
- ]
445
- }
446
601
  ];
447
602
  function buildProfileMap() {
448
603
  const allNames = COMPONENTS.map((c) => c.name);
@@ -454,6 +609,23 @@ function buildProfileMap() {
454
609
  }
455
610
  var PROFILE_MAP = buildProfileMap();
456
611
  var componentIndex = new Map(COMPONENTS.map((c) => [c.name, c]));
612
+ var SPELL_COMPONENT_NAMES = COMPONENTS.map((c) => c.name).filter(
613
+ (n) => n.startsWith("spells-")
614
+ );
615
+ var LEGACY_MONOLITH_REPLACEMENTS = [
616
+ "spells-session",
617
+ "spells-capture",
618
+ "spells-delivery",
619
+ "spells-review",
620
+ "spells-planning",
621
+ "spells-build",
622
+ "spells-venture",
623
+ "spells-meta"
624
+ ];
625
+ var LEGACY_COMPONENT_MIGRATIONS = Object.assign(/* @__PURE__ */ Object.create(null), {
626
+ "spell-prompts": LEGACY_MONOLITH_REPLACEMENTS,
627
+ "claude-commands": LEGACY_MONOLITH_REPLACEMENTS
628
+ });
457
629
  function getComponent(name) {
458
630
  const component = componentIndex.get(name);
459
631
  if (!component) {
@@ -1662,6 +1834,8 @@ var READ_SUBCOMMANDS = /* @__PURE__ */ new Set([
1662
1834
  "ls-files",
1663
1835
  "ls-tree",
1664
1836
  "symbolic-ref",
1837
+ "show-ref",
1838
+ "for-each-ref",
1665
1839
  "merge-base",
1666
1840
  "blame",
1667
1841
  "shortlog",
@@ -1788,10 +1962,25 @@ async function correctUnbornMasterDefault(cwd) {
1788
1962
  return { corrected: false, to: "main" };
1789
1963
  } catch {
1790
1964
  }
1965
+ let mainRef;
1791
1966
  try {
1792
- await runGit(cwd, ["rev-parse", "--verify", "refs/heads/main"]);
1793
- return { corrected: false, to: "main" };
1967
+ mainRef = await runGit(cwd, ["for-each-ref", "--format=%(objectname)", "refs/heads/main"]);
1794
1968
  } catch {
1969
+ return { corrected: false, to: "main", blockedReason: "target-unreadable" };
1970
+ }
1971
+ if (mainRef.stdout.trim() !== "") {
1972
+ return { corrected: false, to: "main" };
1973
+ }
1974
+ if (mainRef.stderr.trim() !== "") {
1975
+ return { corrected: false, to: "main", blockedReason: "target-unreadable" };
1976
+ }
1977
+ try {
1978
+ await runGit(cwd, ["symbolic-ref", "-q", "refs/heads/main"]);
1979
+ return { corrected: false, to: "main", blockedReason: "target-unreadable" };
1980
+ } catch (error) {
1981
+ if (error.code !== 1) {
1982
+ return { corrected: false, to: "main", blockedReason: "target-unreadable" };
1983
+ }
1795
1984
  }
1796
1985
  await runGit(cwd, ["symbolic-ref", "HEAD", "refs/heads/main"]);
1797
1986
  return { corrected: true, from: current, to: "main" };
@@ -1820,27 +2009,28 @@ async function ensureLocalPullRebase(cwd) {
1820
2009
  }
1821
2010
 
1822
2011
  // src/commands/init.ts
1823
- var VALID_PROFILES = ["full", "lite", "governance-only", "methodology"];
2012
+ var VALID_PROFILES = ["full", "lite", "governance-only", "methodology", "docs"];
2013
+ var PROFILE_ICONS = {
2014
+ full: "\u{1F9D9}",
2015
+ lite: "\u26A1",
2016
+ docs: "\u{1F4D6}"
2017
+ };
2018
+ function isSpellComponent(name) {
2019
+ return name.startsWith("spells-");
2020
+ }
1824
2021
  function categorizeComponents(components) {
1825
2022
  const groups = [];
1826
- const prompts = components.find((c) => c.name === "spell-prompts");
1827
- if (prompts) {
1828
- groups.push({
1829
- icon: "\u2728",
1830
- label: "Copilot Spells",
1831
- count: prompts.files.length
1832
- });
2023
+ const spellFiles = components.filter((c) => isSpellComponent(c.name)).flatMap((c) => c.files);
2024
+ const promptCount = spellFiles.filter((f) => f.startsWith(".github/prompts/")).length;
2025
+ if (promptCount > 0) {
2026
+ groups.push({ icon: "\u2728", label: "Copilot Spells", count: promptCount });
1833
2027
  }
1834
- const claude = components.find((c) => c.name === "claude-commands");
1835
- if (claude) {
1836
- groups.push({
1837
- icon: "\u26A1",
1838
- label: "Claude Spells",
1839
- count: claude.files.length
1840
- });
2028
+ const claudeCount = spellFiles.filter((f) => f.startsWith(".claude/commands/")).length;
2029
+ if (claudeCount > 0) {
2030
+ groups.push({ icon: "\u26A1", label: "Claude Spells", count: claudeCount });
1841
2031
  }
1842
2032
  const governance = components.filter(
1843
- (c) => c.name !== "spell-prompts" && c.name !== "claude-commands" && c.name !== "venture-template" && c.name !== "agent-definitions" && c.files.some((f) => f.startsWith(".arcane/governance/"))
2033
+ (c) => !isSpellComponent(c.name) && c.name !== "venture-template" && c.name !== "agent-definitions" && c.files.some((f) => f.startsWith(".arcane/governance/"))
1844
2034
  );
1845
2035
  if (governance.length > 0) {
1846
2036
  groups.push({
@@ -1907,6 +2097,10 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
1907
2097
  printInfo(
1908
2098
  `Repointed the unborn branch from "${correction.from}" to "${correction.to}" (avoids the Git for Windows default-branch trap).`
1909
2099
  );
2100
+ } else if (correction.blockedReason === "target-unreadable") {
2101
+ printWarning(
2102
+ 'This repo\'s "main" branch ref exists but could not be read (broken ref). Left the current branch alone \u2014 repointing onto an unreadable ref risks attaching your staged work to history Arcane cannot see. Check `git for-each-ref refs/heads/main` before committing.'
2103
+ );
1910
2104
  }
1911
2105
  }
1912
2106
  if (gitState.status === "ready" && gitState.uncommittedChanges > 0) {
@@ -1942,7 +2136,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
1942
2136
  const profileComponents = getProfile(p.id);
1943
2137
  return {
1944
2138
  value: p.id,
1945
- name: `${p.id === "full" ? "\u{1F9D9}" : p.id === "lite" ? "\u26A1" : "\u{1F4CB}"} ${p.displayName} \u2014 ${p.description}`,
2139
+ name: `${PROFILE_ICONS[p.id] ?? "\u{1F4CB}"} ${p.displayName} \u2014 ${p.description}`,
1946
2140
  description: buildProfileDescription(profileComponents, p.id)
1947
2141
  };
1948
2142
  })
@@ -1976,7 +2170,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
1976
2170
  for (const component of components) {
1977
2171
  const installedFiles = [];
1978
2172
  for (const file of component.files) {
1979
- const srcPath = join4(assetsDir, file);
2173
+ const srcPath = join4(assetsDir, component.sourceOverrides?.[file] ?? file);
1980
2174
  if (options.dryRun) {
1981
2175
  printDryRun(`Would copy: ${file}`);
1982
2176
  } else {
@@ -2041,7 +2235,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
2041
2235
  }
2042
2236
  let tracking_mode;
2043
2237
  let external_provider;
2044
- if (profile === "governance-only" || profile === "methodology") {
2238
+ if (profile === "governance-only" || profile === "methodology" || profile === "docs") {
2045
2239
  tracking_mode = "internal";
2046
2240
  external_provider = null;
2047
2241
  } else if (!options.profile) {
@@ -2066,13 +2260,57 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
2066
2260
  external_provider = null;
2067
2261
  }
2068
2262
  }
2263
+ let subject_root;
2264
+ if (profile === "docs" && !options.profile) {
2265
+ console.log();
2266
+ const shape = await select2({
2267
+ message: "What does this repository hold?",
2268
+ choices: [
2269
+ {
2270
+ value: "root",
2271
+ name: "One subject, at the repository root (documents sit alongside Arcane's files)"
2272
+ },
2273
+ { value: "subdir", name: "One subject, in its own directory" },
2274
+ { value: "portfolio", name: "Several subjects or ventures (decide per-document later)" }
2275
+ ]
2276
+ });
2277
+ if (shape === "root") {
2278
+ subject_root = ".";
2279
+ } else if (shape === "subdir") {
2280
+ const answer = await input2({
2281
+ message: "Directory holding the subject's documents:",
2282
+ default: "docs",
2283
+ validate: (v) => isValidSubjectRoot(v.trim() || "docs") || "Must be a relative path inside the repository (no leading /, drive letter, or ..)."
2284
+ });
2285
+ subject_root = answer.trim() || "docs";
2286
+ } else if (shape === "portfolio") {
2287
+ subject_root = null;
2288
+ }
2289
+ }
2290
+ let content_sensitivity;
2291
+ if (!options.profile) {
2292
+ console.log();
2293
+ content_sensitivity = await select2({
2294
+ message: "How should agents treat this repository's contents?",
2295
+ choices: [
2296
+ { value: "standard", name: "Standard \u2014 agents may quote contents in journals and decisions" },
2297
+ {
2298
+ value: "sensitive",
2299
+ name: "Sensitive \u2014 agents reference documents by path, never transcribe them"
2300
+ }
2301
+ ],
2302
+ default: "standard"
2303
+ });
2304
+ }
2069
2305
  const manifest = {
2070
2306
  version: packageVersion,
2071
2307
  profile,
2072
2308
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
2073
2309
  components: installedComponents,
2074
2310
  ...role ? { role } : {},
2075
- ...tracking_mode ? { tracking_mode, external_provider } : {}
2311
+ ...tracking_mode ? { tracking_mode, external_provider } : {},
2312
+ ...subject_root !== void 0 ? { subject_root } : {},
2313
+ ...content_sensitivity ? { content_sensitivity } : {}
2076
2314
  };
2077
2315
  await writeManifest(targetDir, manifest);
2078
2316
  printSuccess(`Initialized with profile "${profile}" \u2014 ${fileCount} files installed`);
@@ -2156,7 +2394,7 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
2156
2394
  let fileCount = 0;
2157
2395
  const installedFiles = [];
2158
2396
  for (const file of component.files) {
2159
- const srcPath = join5(assetsDir, file);
2397
+ const srcPath = join5(assetsDir, component.sourceOverrides?.[file] ?? file);
2160
2398
  if (options.dryRun) {
2161
2399
  console.log(` [dry-run] Would copy: ${file}`);
2162
2400
  } else {
@@ -2199,7 +2437,7 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
2199
2437
  import { join as join7 } from "path";
2200
2438
 
2201
2439
  // src/modules/hub.ts
2202
- import { confirm as confirm2, select as select3 } from "@inquirer/prompts";
2440
+ import { confirm as confirm2, select as select3, input as input3 } from "@inquirer/prompts";
2203
2441
  import { readdir as readdir3, writeFile as writeFile5 } from "fs/promises";
2204
2442
  import { join as join6 } from "path";
2205
2443
  var MANIFEST_RETROFITS = [
@@ -2220,7 +2458,7 @@ var MANIFEST_RETROFITS = [
2220
2458
  // Mirrors init.ts's Step 5b branching exactly (EF-14 D5): docs-only
2221
2459
  // profiles get a silent default, full/lite get asked.
2222
2460
  ask: async (manifest) => {
2223
- if (manifest.profile === "governance-only" || manifest.profile === "methodology") {
2461
+ if (manifest.profile === "governance-only" || manifest.profile === "methodology" || manifest.profile === "docs") {
2224
2462
  return { tracking_mode: "internal", external_provider: null };
2225
2463
  }
2226
2464
  const tracking_mode = await select3({
@@ -2243,6 +2481,64 @@ var MANIFEST_RETROFITS = [
2243
2481
  }
2244
2482
  return { tracking_mode, external_provider: null };
2245
2483
  }
2484
+ },
2485
+ {
2486
+ field: "content_sensitivity",
2487
+ needsRetrofit: (m) => m.content_sensitivity === void 0,
2488
+ // EF-12. Asked for every profile -- a code repo can hold sensitive records
2489
+ // too -- and defaults to "standard", so an operator who just presses enter
2490
+ // keeps today's behaviour exactly.
2491
+ ask: async () => {
2492
+ const content_sensitivity = await select3({
2493
+ message: "How should agents treat this repository's contents?",
2494
+ choices: [
2495
+ {
2496
+ value: "standard",
2497
+ name: "Standard \u2014 agents may quote contents in journals and decisions"
2498
+ },
2499
+ {
2500
+ value: "sensitive",
2501
+ name: "Sensitive \u2014 agents reference documents by path, never transcribe them"
2502
+ }
2503
+ ],
2504
+ default: "standard"
2505
+ });
2506
+ return { content_sensitivity };
2507
+ }
2508
+ },
2509
+ {
2510
+ field: "subject_root",
2511
+ // EF-07. Only the docs profile is asked: other profiles describe code or a
2512
+ // venture portfolio, where "what is this repo about" is already answered.
2513
+ // A docs install that legitimately holds several subjects answers
2514
+ // "portfolio" and stays unset -- so this retrofit deliberately does NOT
2515
+ // re-ask on every update. It is gated on the field being absent AND the
2516
+ // profile being docs, and once answered (either way) it never fires again,
2517
+ // because "portfolio" writes an explicit empty marker.
2518
+ needsRetrofit: (m) => m.profile === "docs" && m.subject_root === void 0,
2519
+ ask: async () => {
2520
+ const shape = await select3({
2521
+ message: "What does this repository hold?",
2522
+ choices: [
2523
+ {
2524
+ value: "root",
2525
+ name: "One subject, at the repository root (documents sit alongside Arcane's files)"
2526
+ },
2527
+ { value: "subdir", name: "One subject, in its own directory" },
2528
+ { value: "portfolio", name: "Several subjects or ventures" }
2529
+ ]
2530
+ });
2531
+ if (shape === "root") return { subject_root: "." };
2532
+ if (shape === "subdir") {
2533
+ const answer = await input3({
2534
+ message: "Directory holding the subject's documents:",
2535
+ default: "docs",
2536
+ validate: (v) => isValidSubjectRoot(v.trim() || "docs") || "Must be a relative path inside the repository (no leading /, drive letter, or ..)."
2537
+ });
2538
+ return { subject_root: answer.trim() || "docs" };
2539
+ }
2540
+ return { subject_root: null };
2541
+ }
2246
2542
  }
2247
2543
  ];
2248
2544
  async function runManifestRetrofits(manifest) {
@@ -2307,6 +2603,25 @@ async function offerRegistryScaffold(targetDir, businessRoot) {
2307
2603
  }
2308
2604
 
2309
2605
  // src/commands/update.ts
2606
+ function migrateLegacyComponents(components) {
2607
+ const out = [];
2608
+ const seen = /* @__PURE__ */ new Set();
2609
+ for (const entry of components) {
2610
+ const replacements = LEGACY_COMPONENT_MIGRATIONS[entry.name];
2611
+ if (!replacements) {
2612
+ if (seen.has(entry.name)) continue;
2613
+ seen.add(entry.name);
2614
+ out.push(entry);
2615
+ continue;
2616
+ }
2617
+ for (const name of replacements) {
2618
+ if (seen.has(name)) continue;
2619
+ seen.add(name);
2620
+ out.push({ name, files: [], installedVersion: entry.installedVersion });
2621
+ }
2622
+ }
2623
+ return out;
2624
+ }
2310
2625
  async function runUpdate(options, targetDir, assetsDir, packageVersion) {
2311
2626
  let manifest;
2312
2627
  try {
@@ -2359,7 +2674,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
2359
2674
  }
2360
2675
  let fileCount = 0;
2361
2676
  const updatedComponents = [];
2362
- for (const installed of manifest.components) {
2677
+ const componentsToUpdate = migrateLegacyComponents(manifest.components);
2678
+ for (const installed of componentsToUpdate) {
2363
2679
  let component;
2364
2680
  try {
2365
2681
  component = getComponent(installed.name);
@@ -2373,8 +2689,15 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
2373
2689
  }
2374
2690
  const updatedFiles = [];
2375
2691
  for (const file of component.files) {
2376
- const srcPath = join7(assetsDir, file);
2377
- const preserveExisting = component.skipExisting && await fileExists(join7(targetDir, file));
2692
+ const srcPath = join7(assetsDir, component.sourceOverrides?.[file] ?? file);
2693
+ const targetExists = await fileExists(join7(targetDir, file));
2694
+ const preserveExisting = Boolean(component.skipExisting) && targetExists;
2695
+ if (component.initOnly && !targetExists) {
2696
+ console.log(
2697
+ ` ! Missing: ${file} \u2014 not added automatically, because doing so would change how Git treats existing files. Run "spell add ${component.name}" if you want it.`
2698
+ );
2699
+ continue;
2700
+ }
2378
2701
  if (preserveExisting) {
2379
2702
  console.log(` ${options.dryRun ? "[dry-run] Would preserve" : "Preserved"}: ${file}`);
2380
2703
  } else if (options.dryRun) {
@@ -2384,7 +2707,9 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
2384
2707
  await copyFile(srcPath, targetDir, file, { force: true });
2385
2708
  fileCount++;
2386
2709
  }
2387
- updatedFiles.push(file);
2710
+ if (!preserveExisting || installed.files.includes(file)) {
2711
+ updatedFiles.push(file);
2712
+ }
2388
2713
  }
2389
2714
  for (const dir of component.directories ?? []) {
2390
2715
  const srcDirPath = join7(assetsDir, dir);
@@ -2415,7 +2740,16 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
2415
2740
  }
2416
2741
  return;
2417
2742
  }
2418
- const retrofitPatch = await runManifestRetrofits(manifest);
2743
+ const interactive = Boolean(process.stdin.isTTY);
2744
+ const retrofitPatch = interactive ? await runManifestRetrofits(manifest) : {};
2745
+ if (!interactive) {
2746
+ const pending = MANIFEST_RETROFITS.filter((r) => r.needsRetrofit(manifest));
2747
+ if (pending.length > 0) {
2748
+ console.log(
2749
+ ` ! Skipped ${pending.length} manifest question${pending.length === 1 ? "" : "s"} (${pending.map((r) => r.field).join(", ")}) \u2014 no interactive terminal. Run \`spell update\` from a terminal to answer.`
2750
+ );
2751
+ }
2752
+ }
2419
2753
  const updated = {
2420
2754
  ...manifest,
2421
2755
  version: packageVersion,
@@ -2963,7 +3297,7 @@ program.name("spell").description("Arcane framework CLI \u2014 scaffold and mana
2963
3297
  await printWelcome(pkg.version, process.cwd());
2964
3298
  program.outputHelp();
2965
3299
  });
2966
- program.command("init").description("Scaffold Arcane framework files into the current repository").option("--profile <profile>", "Profile: full | lite | methodology | governance-only").option("--force", "Overwrite existing files without error").option("--dry-run", "Preview what would be installed without making changes").action(
3300
+ program.command("init").description("Scaffold Arcane framework files into the current repository").option("--profile <profile>", "Profile: full | lite | methodology | docs | governance-only").option("--force", "Overwrite existing files without error").option("--dry-run", "Preview what would be installed without making changes").action(
2967
3301
  async (opts) => {
2968
3302
  await runInit(
2969
3303
  {