arcane-cli 0.17.1 → 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) {
@@ -1837,27 +2009,28 @@ async function ensureLocalPullRebase(cwd) {
1837
2009
  }
1838
2010
 
1839
2011
  // src/commands/init.ts
1840
- 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
+ }
1841
2021
  function categorizeComponents(components) {
1842
2022
  const groups = [];
1843
- const prompts = components.find((c) => c.name === "spell-prompts");
1844
- if (prompts) {
1845
- groups.push({
1846
- icon: "\u2728",
1847
- label: "Copilot Spells",
1848
- count: prompts.files.length
1849
- });
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 });
1850
2027
  }
1851
- const claude = components.find((c) => c.name === "claude-commands");
1852
- if (claude) {
1853
- groups.push({
1854
- icon: "\u26A1",
1855
- label: "Claude Spells",
1856
- count: claude.files.length
1857
- });
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 });
1858
2031
  }
1859
2032
  const governance = components.filter(
1860
- (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/"))
1861
2034
  );
1862
2035
  if (governance.length > 0) {
1863
2036
  groups.push({
@@ -1963,7 +2136,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
1963
2136
  const profileComponents = getProfile(p.id);
1964
2137
  return {
1965
2138
  value: p.id,
1966
- 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}`,
1967
2140
  description: buildProfileDescription(profileComponents, p.id)
1968
2141
  };
1969
2142
  })
@@ -1997,7 +2170,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
1997
2170
  for (const component of components) {
1998
2171
  const installedFiles = [];
1999
2172
  for (const file of component.files) {
2000
- const srcPath = join4(assetsDir, file);
2173
+ const srcPath = join4(assetsDir, component.sourceOverrides?.[file] ?? file);
2001
2174
  if (options.dryRun) {
2002
2175
  printDryRun(`Would copy: ${file}`);
2003
2176
  } else {
@@ -2062,7 +2235,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
2062
2235
  }
2063
2236
  let tracking_mode;
2064
2237
  let external_provider;
2065
- if (profile === "governance-only" || profile === "methodology") {
2238
+ if (profile === "governance-only" || profile === "methodology" || profile === "docs") {
2066
2239
  tracking_mode = "internal";
2067
2240
  external_provider = null;
2068
2241
  } else if (!options.profile) {
@@ -2087,13 +2260,57 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
2087
2260
  external_provider = null;
2088
2261
  }
2089
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
+ }
2090
2305
  const manifest = {
2091
2306
  version: packageVersion,
2092
2307
  profile,
2093
2308
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
2094
2309
  components: installedComponents,
2095
2310
  ...role ? { role } : {},
2096
- ...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 } : {}
2097
2314
  };
2098
2315
  await writeManifest(targetDir, manifest);
2099
2316
  printSuccess(`Initialized with profile "${profile}" \u2014 ${fileCount} files installed`);
@@ -2177,7 +2394,7 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
2177
2394
  let fileCount = 0;
2178
2395
  const installedFiles = [];
2179
2396
  for (const file of component.files) {
2180
- const srcPath = join5(assetsDir, file);
2397
+ const srcPath = join5(assetsDir, component.sourceOverrides?.[file] ?? file);
2181
2398
  if (options.dryRun) {
2182
2399
  console.log(` [dry-run] Would copy: ${file}`);
2183
2400
  } else {
@@ -2220,7 +2437,7 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
2220
2437
  import { join as join7 } from "path";
2221
2438
 
2222
2439
  // src/modules/hub.ts
2223
- import { confirm as confirm2, select as select3 } from "@inquirer/prompts";
2440
+ import { confirm as confirm2, select as select3, input as input3 } from "@inquirer/prompts";
2224
2441
  import { readdir as readdir3, writeFile as writeFile5 } from "fs/promises";
2225
2442
  import { join as join6 } from "path";
2226
2443
  var MANIFEST_RETROFITS = [
@@ -2241,7 +2458,7 @@ var MANIFEST_RETROFITS = [
2241
2458
  // Mirrors init.ts's Step 5b branching exactly (EF-14 D5): docs-only
2242
2459
  // profiles get a silent default, full/lite get asked.
2243
2460
  ask: async (manifest) => {
2244
- if (manifest.profile === "governance-only" || manifest.profile === "methodology") {
2461
+ if (manifest.profile === "governance-only" || manifest.profile === "methodology" || manifest.profile === "docs") {
2245
2462
  return { tracking_mode: "internal", external_provider: null };
2246
2463
  }
2247
2464
  const tracking_mode = await select3({
@@ -2264,6 +2481,64 @@ var MANIFEST_RETROFITS = [
2264
2481
  }
2265
2482
  return { tracking_mode, external_provider: null };
2266
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
+ }
2267
2542
  }
2268
2543
  ];
2269
2544
  async function runManifestRetrofits(manifest) {
@@ -2328,6 +2603,25 @@ async function offerRegistryScaffold(targetDir, businessRoot) {
2328
2603
  }
2329
2604
 
2330
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
+ }
2331
2625
  async function runUpdate(options, targetDir, assetsDir, packageVersion) {
2332
2626
  let manifest;
2333
2627
  try {
@@ -2380,7 +2674,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
2380
2674
  }
2381
2675
  let fileCount = 0;
2382
2676
  const updatedComponents = [];
2383
- for (const installed of manifest.components) {
2677
+ const componentsToUpdate = migrateLegacyComponents(manifest.components);
2678
+ for (const installed of componentsToUpdate) {
2384
2679
  let component;
2385
2680
  try {
2386
2681
  component = getComponent(installed.name);
@@ -2394,8 +2689,15 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
2394
2689
  }
2395
2690
  const updatedFiles = [];
2396
2691
  for (const file of component.files) {
2397
- const srcPath = join7(assetsDir, file);
2398
- 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
+ }
2399
2701
  if (preserveExisting) {
2400
2702
  console.log(` ${options.dryRun ? "[dry-run] Would preserve" : "Preserved"}: ${file}`);
2401
2703
  } else if (options.dryRun) {
@@ -2405,7 +2707,9 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
2405
2707
  await copyFile(srcPath, targetDir, file, { force: true });
2406
2708
  fileCount++;
2407
2709
  }
2408
- updatedFiles.push(file);
2710
+ if (!preserveExisting || installed.files.includes(file)) {
2711
+ updatedFiles.push(file);
2712
+ }
2409
2713
  }
2410
2714
  for (const dir of component.directories ?? []) {
2411
2715
  const srcDirPath = join7(assetsDir, dir);
@@ -2436,7 +2740,16 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
2436
2740
  }
2437
2741
  return;
2438
2742
  }
2439
- 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
+ }
2440
2753
  const updated = {
2441
2754
  ...manifest,
2442
2755
  version: packageVersion,
@@ -2984,7 +3297,7 @@ program.name("spell").description("Arcane framework CLI \u2014 scaffold and mana
2984
3297
  await printWelcome(pkg.version, process.cwd());
2985
3298
  program.outputHelp();
2986
3299
  });
2987
- 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(
2988
3301
  async (opts) => {
2989
3302
  await runInit(
2990
3303
  {