@stackbilt/cli 0.9.2 → 0.11.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 (103) hide show
  1. package/LICENSE +17 -1
  2. package/README.md +57 -0
  3. package/dist/__tests__/auth-wiring.test.d.ts +2 -0
  4. package/dist/__tests__/auth-wiring.test.d.ts.map +1 -0
  5. package/dist/__tests__/auth-wiring.test.js +158 -0
  6. package/dist/__tests__/auth-wiring.test.js.map +1 -0
  7. package/dist/__tests__/bootstrap.test.d.ts +2 -0
  8. package/dist/__tests__/bootstrap.test.d.ts.map +1 -0
  9. package/dist/__tests__/bootstrap.test.js +134 -0
  10. package/dist/__tests__/bootstrap.test.js.map +1 -0
  11. package/dist/__tests__/credentials.test.d.ts +2 -0
  12. package/dist/__tests__/credentials.test.d.ts.map +1 -0
  13. package/dist/__tests__/credentials.test.js +145 -0
  14. package/dist/__tests__/credentials.test.js.map +1 -0
  15. package/dist/__tests__/integration/precommit-hook.test.js +4 -4
  16. package/dist/__tests__/login.test.d.ts +2 -0
  17. package/dist/__tests__/login.test.d.ts.map +1 -0
  18. package/dist/__tests__/login.test.js +43 -0
  19. package/dist/__tests__/login.test.js.map +1 -0
  20. package/dist/__tests__/named-scaffolds.test.d.ts +2 -0
  21. package/dist/__tests__/named-scaffolds.test.d.ts.map +1 -0
  22. package/dist/__tests__/named-scaffolds.test.js +58 -0
  23. package/dist/__tests__/named-scaffolds.test.js.map +1 -0
  24. package/dist/__tests__/score.test.d.ts +2 -0
  25. package/dist/__tests__/score.test.d.ts.map +1 -0
  26. package/dist/__tests__/score.test.js +234 -0
  27. package/dist/__tests__/score.test.js.map +1 -0
  28. package/dist/commands/adf-named-scaffolds.d.ts +45 -0
  29. package/dist/commands/adf-named-scaffolds.d.ts.map +1 -0
  30. package/dist/commands/adf-named-scaffolds.js +114 -0
  31. package/dist/commands/adf-named-scaffolds.js.map +1 -0
  32. package/dist/commands/adf-tidy.d.ts.map +1 -1
  33. package/dist/commands/adf-tidy.js +6 -3
  34. package/dist/commands/adf-tidy.js.map +1 -1
  35. package/dist/commands/adf.d.ts +1 -0
  36. package/dist/commands/adf.d.ts.map +1 -1
  37. package/dist/commands/adf.js +39 -15
  38. package/dist/commands/adf.js.map +1 -1
  39. package/dist/commands/architect.d.ts.map +1 -1
  40. package/dist/commands/architect.js +7 -3
  41. package/dist/commands/architect.js.map +1 -1
  42. package/dist/commands/audit.js +24 -7
  43. package/dist/commands/audit.js.map +1 -1
  44. package/dist/commands/blast.d.ts +13 -0
  45. package/dist/commands/blast.d.ts.map +1 -0
  46. package/dist/commands/blast.js +226 -0
  47. package/dist/commands/blast.js.map +1 -0
  48. package/dist/commands/bootstrap.d.ts.map +1 -1
  49. package/dist/commands/bootstrap.js +300 -101
  50. package/dist/commands/bootstrap.js.map +1 -1
  51. package/dist/commands/doctor.d.ts.map +1 -1
  52. package/dist/commands/doctor.js +9 -2
  53. package/dist/commands/doctor.js.map +1 -1
  54. package/dist/commands/init.d.ts.map +1 -1
  55. package/dist/commands/init.js +127 -8
  56. package/dist/commands/init.js.map +1 -1
  57. package/dist/commands/login.d.ts +4 -1
  58. package/dist/commands/login.d.ts.map +1 -1
  59. package/dist/commands/login.js +27 -6
  60. package/dist/commands/login.js.map +1 -1
  61. package/dist/commands/run.d.ts.map +1 -1
  62. package/dist/commands/run.js +8 -5
  63. package/dist/commands/run.js.map +1 -1
  64. package/dist/commands/score.d.ts +9 -0
  65. package/dist/commands/score.d.ts.map +1 -0
  66. package/dist/commands/score.js +1273 -0
  67. package/dist/commands/score.js.map +1 -0
  68. package/dist/commands/serve.d.ts.map +1 -1
  69. package/dist/commands/serve.js +44 -0
  70. package/dist/commands/serve.js.map +1 -1
  71. package/dist/commands/setup.js +2 -2
  72. package/dist/commands/setup.js.map +1 -1
  73. package/dist/commands/surface.d.ts +15 -0
  74. package/dist/commands/surface.d.ts.map +1 -0
  75. package/dist/commands/surface.js +112 -0
  76. package/dist/commands/surface.js.map +1 -0
  77. package/dist/commands/validate-ontology.d.ts +14 -0
  78. package/dist/commands/validate-ontology.d.ts.map +1 -0
  79. package/dist/commands/validate-ontology.js +328 -0
  80. package/dist/commands/validate-ontology.js.map +1 -0
  81. package/dist/commands/validate.d.ts.map +1 -1
  82. package/dist/commands/validate.js +44 -0
  83. package/dist/commands/validate.js.map +1 -1
  84. package/dist/config.d.ts +25 -0
  85. package/dist/config.d.ts.map +1 -1
  86. package/dist/config.js +1 -0
  87. package/dist/config.js.map +1 -1
  88. package/dist/credentials.d.ts +22 -1
  89. package/dist/credentials.d.ts.map +1 -1
  90. package/dist/credentials.js +35 -1
  91. package/dist/credentials.js.map +1 -1
  92. package/dist/http-client.d.ts +13 -4
  93. package/dist/http-client.d.ts.map +1 -1
  94. package/dist/http-client.js +3 -2
  95. package/dist/http-client.js.map +1 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +22 -3
  98. package/dist/index.js.map +1 -1
  99. package/dist/types/scaffold-contract-types.d.ts +90 -0
  100. package/dist/types/scaffold-contract-types.d.ts.map +1 -0
  101. package/dist/types/scaffold-contract-types.js +22 -0
  102. package/dist/types/scaffold-contract-types.js.map +1 -0
  103. package/package.json +12 -9
@@ -42,6 +42,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.bootstrapCommand = bootstrapCommand;
43
43
  const fs = __importStar(require("node:fs"));
44
44
  const path = __importStar(require("node:path"));
45
+ const readline = __importStar(require("node:readline"));
45
46
  const crypto = __importStar(require("node:crypto"));
46
47
  const node_child_process_1 = require("node:child_process");
47
48
  const index_1 = require("../index");
@@ -62,7 +63,9 @@ async function bootstrapCommand(options, args) {
62
63
  const presetFlag = (0, flags_1.getFlag)(args, '--preset');
63
64
  const skipInstall = args.includes('--skip-install');
64
65
  const skipDoctor = args.includes('--skip-doctor');
65
- const force = options.yes;
66
+ const force = args.includes('--force');
67
+ const nonInteractive = options.yes;
68
+ const setupOverwrite = options.yes || force;
66
69
  if (ciTarget && ciTarget !== 'github') {
67
70
  throw new index_1.CLIError(`Unsupported CI target: ${ciTarget}. Supported: github`);
68
71
  }
@@ -80,14 +83,13 @@ async function bootstrapCommand(options, args) {
80
83
  // ========================================================================
81
84
  const detectResult = runDetectPhase(options, presetFlag);
82
85
  result.steps.push(detectResult.step);
83
- if (detectResult.step.status === 'fail')
84
- warnings++;
86
+ warnings += detectResult.step.warnings.length;
85
87
  const selectedPreset = detectResult.selectedPreset;
86
88
  const detection = detectResult.detection;
87
89
  const contexts = detectResult.contexts;
88
90
  const packageManager = detectResult.packageManager;
89
91
  if (options.format === 'text') {
90
- console.log('[1/6] Detecting stack...');
92
+ console.log('[1/7] Detecting stack...');
91
93
  console.log(` Stack: ${selectedPreset} (${detection.confidence} confidence)`);
92
94
  console.log(` Monorepo: ${detection.monorepo ? 'yes' : 'no'}${detection.monorepo && detection.signals.hasPnpm ? ' (pnpm workspace)' : ''}`);
93
95
  if (detection.warnings.length > 0) {
@@ -100,12 +102,11 @@ async function bootstrapCommand(options, args) {
100
102
  // ========================================================================
101
103
  // Phase 2: Setup
102
104
  // ========================================================================
103
- const setupResult = runSetupPhase(options, selectedPreset, detection, contexts, ciTarget, packageManager, force);
105
+ const setupResult = runSetupPhase(options, selectedPreset, detection, contexts, ciTarget, packageManager, setupOverwrite);
104
106
  result.steps.push(setupResult.step);
105
- if (setupResult.step.status === 'fail')
106
- warnings++;
107
+ warnings += setupResult.step.warnings.length;
107
108
  if (options.format === 'text') {
108
- console.log('[2/6] Setting up governance...');
109
+ console.log('[2/7] Setting up governance...');
109
110
  for (const f of (setupResult.step.details.created || [])) {
110
111
  console.log(` Created ${f}`);
111
112
  }
@@ -119,27 +120,51 @@ async function bootstrapCommand(options, args) {
119
120
  // ========================================================================
120
121
  const adfResult = runAdfInitPhase(options, force, selectedPreset);
121
122
  result.steps.push(adfResult.step);
122
- if (adfResult.step.status === 'fail')
123
- warnings++;
123
+ warnings += adfResult.step.warnings.length;
124
124
  if (options.format === 'text') {
125
- console.log('[3/6] Initializing ADF context...');
125
+ console.log('[3/7] Initializing ADF context...');
126
126
  for (const f of (adfResult.step.details.files || [])) {
127
127
  console.log(` Created ${f}`);
128
128
  }
129
129
  for (const f of (adfResult.step.details.pointers || [])) {
130
130
  console.log(` Generated ${f}`);
131
131
  }
132
+ const backedUp = adfResult.step.details.backedUp;
133
+ if (backedUp && backedUp > 0) {
134
+ console.log(` Backed up ${backedUp} files to .ai/.backup/`);
135
+ }
136
+ for (const warning of adfResult.step.warnings) {
137
+ console.log(` Warning: ${warning}`);
138
+ }
132
139
  console.log('');
133
140
  }
141
+ // Orphan registration: auto-register in --yes mode, prompt interactively otherwise
142
+ const orphans = adfResult.step.details.orphans || [];
143
+ if (orphans.length > 0) {
144
+ let shouldRegister = false;
145
+ if (nonInteractive) {
146
+ shouldRegister = true;
147
+ }
148
+ else if (options.format === 'text') {
149
+ shouldRegister = await promptYesNo(' Register these modules now? (y/N) ');
150
+ }
151
+ if (shouldRegister) {
152
+ registerOrphansInManifest(path.join('.ai', 'manifest.adf'), orphans);
153
+ (0, adf_migrate_1.updateModuleIndex)('CLAUDE.md', '.ai');
154
+ if (options.format === 'text') {
155
+ console.log(` Registered ${orphans.length} module(s) as ON_DEMAND in manifest.adf`);
156
+ console.log('');
157
+ }
158
+ }
159
+ }
134
160
  // ========================================================================
135
161
  // Phase 4: Migrate Agent Configs
136
162
  // ========================================================================
137
- const migrateResult = runMigratePhase(options, force);
163
+ const migrateResult = runMigratePhase(options, nonInteractive);
138
164
  result.steps.push(migrateResult.step);
139
- if (migrateResult.step.status === 'fail')
140
- warnings++;
165
+ warnings += migrateResult.step.warnings.length;
141
166
  if (options.format === 'text') {
142
- console.log('[4/6] Migrating agent configs...');
167
+ console.log('[4/7] Migrating agent configs...');
143
168
  if (migrateResult.step.status === 'skip') {
144
169
  console.log(' Skipped (no migratable files)');
145
170
  }
@@ -159,10 +184,9 @@ async function bootstrapCommand(options, args) {
159
184
  // ========================================================================
160
185
  const installResult = runInstallPhase(options, skipInstall);
161
186
  result.steps.push(installResult.step);
162
- if (installResult.step.status === 'fail')
163
- warnings++;
187
+ warnings += installResult.step.warnings.length;
164
188
  if (options.format === 'text') {
165
- console.log('[5/6] Installing dependencies...');
189
+ console.log('[5/7] Installing dependencies...');
166
190
  if (skipInstall) {
167
191
  console.log(' Skipped (--skip-install)');
168
192
  }
@@ -185,14 +209,31 @@ async function bootstrapCommand(options, args) {
185
209
  console.log('');
186
210
  }
187
211
  // ========================================================================
188
- // Phase 6: Doctor
212
+ // Phase 6: Populate (#89)
213
+ // ========================================================================
214
+ const populateResult = await runPopulatePhase(options);
215
+ result.steps.push(populateResult.step);
216
+ warnings += populateResult.step.warnings.length;
217
+ if (options.format === 'text') {
218
+ console.log('[6/7] Auto-populating ADF modules...');
219
+ const populated = populateResult.step.details.populated;
220
+ const skipped = populateResult.step.details.skipped;
221
+ if (populated > 0) {
222
+ console.log(` Populated ${populated} module(s), skipped ${skipped} (already customized)`);
223
+ }
224
+ else {
225
+ console.log(' No scaffold content to replace');
226
+ }
227
+ console.log('');
228
+ }
229
+ // ========================================================================
230
+ // Phase 7: Doctor
189
231
  // ========================================================================
190
232
  const doctorResult = runDoctorPhase(options, skipDoctor);
191
233
  result.steps.push(doctorResult.step);
192
- if (doctorResult.step.status === 'fail')
193
- warnings++;
234
+ warnings += doctorResult.step.warnings.length;
194
235
  if (options.format === 'text') {
195
- console.log('[6/6] Running health check...');
236
+ console.log('[7/7] Running health check...');
196
237
  if (skipDoctor) {
197
238
  console.log(' Skipped (--skip-doctor)');
198
239
  }
@@ -212,25 +253,80 @@ async function bootstrapCommand(options, args) {
212
253
  result.status = failCount === 0 ? 'success' : failCount < result.steps.length ? 'partial' : 'failure';
213
254
  // Build next steps
214
255
  result.nextSteps.push({
215
- cmd: 'Review .charter/patterns/ and customize for your stack',
256
+ cmd: 'charter serve # start MCP server for Claude Code / Cursor integration',
216
257
  required: false,
217
- reason: 'Customize blessed stack patterns',
258
+ reason: 'Enable real-time governance via MCP (add to .claude/settings.json)',
218
259
  });
219
260
  result.nextSteps.push({
220
- cmd: 'charter adf populate # auto-fill ADF files from codebase signals',
261
+ cmd: 'Review .charter/patterns/ and customize for your stack',
221
262
  required: false,
222
- reason: 'Populate ADF context from package.json, README, and stack detection',
263
+ reason: 'Customize blessed stack patterns',
223
264
  });
224
265
  result.nextSteps.push({
225
266
  cmd: 'git add .charter .ai CLAUDE.md .cursorrules agents.md && git commit -m "chore: bootstrap charter governance"',
226
267
  required: false,
227
268
  reason: 'Commit governance baseline',
228
269
  });
270
+ // ========================================================================
271
+ // Governance Gaps — surface what's configured but not enforced
272
+ // ========================================================================
273
+ const gaps = [];
274
+ // Check: trailers enabled but no commit-msg hook
275
+ if (fs.existsSync('.charter/config.json')) {
276
+ try {
277
+ const cfg = JSON.parse(fs.readFileSync('.charter/config.json', 'utf-8'));
278
+ if (cfg.git?.requireTrailers) {
279
+ const hookPath = path.resolve('.githooks/commit-msg');
280
+ const gitHookPath = path.resolve('.git/hooks/commit-msg');
281
+ if (!fs.existsSync(hookPath) && !fs.existsSync(gitHookPath)) {
282
+ gaps.push({
283
+ gap: 'requireTrailers enabled but no commit-msg hook installed',
284
+ fix: 'charter hook install --commit-msg',
285
+ });
286
+ }
287
+ }
288
+ if (cfg.drift?.enabled && !ciTarget) {
289
+ const workflowPath = path.resolve('.github/workflows/charter.yml');
290
+ if (!fs.existsSync(workflowPath)) {
291
+ gaps.push({
292
+ gap: 'drift detection enabled but no CI workflow',
293
+ fix: 'charter bootstrap --ci github (or add manually)',
294
+ });
295
+ }
296
+ }
297
+ }
298
+ catch { /* config not parseable — doctor already caught it */ }
299
+ }
300
+ // Check: no SECURITY.md
301
+ if (!fs.existsSync('SECURITY.md')) {
302
+ gaps.push({
303
+ gap: 'no SECURITY.md for responsible disclosure',
304
+ fix: 'add SECURITY.md with reporting contact and supported versions',
305
+ });
306
+ }
307
+ // Check: no pre-commit hook for ADF evidence
308
+ const preCommitHook = path.resolve('.githooks/pre-commit');
309
+ const gitPreCommit = path.resolve('.git/hooks/pre-commit');
310
+ if (!fs.existsSync(preCommitHook) && !fs.existsSync(gitPreCommit)) {
311
+ gaps.push({
312
+ gap: 'no pre-commit hook for ADF evidence gate',
313
+ fix: 'charter hook install --pre-commit',
314
+ });
315
+ }
229
316
  if (options.format === 'json') {
317
+ result.governanceGaps = gaps;
230
318
  console.log(JSON.stringify(result, null, 2));
231
319
  }
232
320
  else {
233
321
  console.log(`Bootstrap complete. ${warnings} warning${warnings === 1 ? '' : 's'}.`);
322
+ if (gaps.length > 0) {
323
+ console.log('');
324
+ console.log('Governance gaps (configured but not enforced):');
325
+ for (const { gap, fix } of gaps) {
326
+ console.log(` ⚠ ${gap}`);
327
+ console.log(` → ${fix}`);
328
+ }
329
+ }
234
330
  console.log('');
235
331
  console.log('Next steps:');
236
332
  result.nextSteps.forEach((step, i) => {
@@ -384,74 +480,109 @@ function runSetupPhase(options, selectedPreset, detection, contexts, ciTarget, p
384
480
  // ============================================================================
385
481
  // Phase 3: ADF Init
386
482
  // ============================================================================
387
- function writeAdfScaffolds(aiDir, preset) {
388
- fs.mkdirSync(aiDir, { recursive: true });
389
- fs.writeFileSync(path.join(aiDir, 'manifest.adf'), (0, adf_2.manifestForPreset)(preset));
390
- fs.writeFileSync(path.join(aiDir, 'core.adf'), adf_2.CORE_SCAFFOLD);
391
- fs.writeFileSync(path.join(aiDir, 'state.adf'), adf_2.STATE_SCAFFOLD);
392
- const files = ['.ai/manifest.adf', '.ai/core.adf', '.ai/state.adf'];
483
+ function getAdfScaffolds(preset) {
484
+ const scaffolds = [
485
+ { name: 'manifest.adf', content: (0, adf_2.manifestForPreset)(preset) },
486
+ { name: 'core.adf', content: adf_2.CORE_SCAFFOLD },
487
+ { name: 'state.adf', content: adf_2.STATE_SCAFFOLD },
488
+ ];
393
489
  if (preset === 'docs') {
394
- fs.writeFileSync(path.join(aiDir, 'content.adf'), adf_2.CONTENT_SCAFFOLD);
395
- fs.writeFileSync(path.join(aiDir, 'decisions.adf'), adf_2.DECISIONS_SCAFFOLD);
396
- fs.writeFileSync(path.join(aiDir, 'planning.adf'), adf_2.PLANNING_SCAFFOLD);
397
- files.push('.ai/content.adf', '.ai/decisions.adf', '.ai/planning.adf');
490
+ scaffolds.push({ name: 'content.adf', content: adf_2.CONTENT_SCAFFOLD }, { name: 'decisions.adf', content: adf_2.DECISIONS_SCAFFOLD }, { name: 'planning.adf', content: adf_2.PLANNING_SCAFFOLD });
398
491
  }
399
492
  else if (preset === 'frontend') {
400
- fs.writeFileSync(path.join(aiDir, 'frontend.adf'), adf_2.FRONTEND_SCAFFOLD);
401
- files.push('.ai/frontend.adf');
493
+ scaffolds.push({ name: 'frontend.adf', content: adf_2.FRONTEND_SCAFFOLD });
402
494
  }
403
495
  else if (preset === 'backend' || preset === 'worker') {
404
- fs.writeFileSync(path.join(aiDir, 'backend.adf'), adf_2.BACKEND_SCAFFOLD);
405
- files.push('.ai/backend.adf');
496
+ scaffolds.push({ name: 'backend.adf', content: adf_2.BACKEND_SCAFFOLD });
406
497
  }
407
498
  else {
408
- // fullstack or undefined both frontend + backend
409
- fs.writeFileSync(path.join(aiDir, 'frontend.adf'), adf_2.FRONTEND_SCAFFOLD);
410
- fs.writeFileSync(path.join(aiDir, 'backend.adf'), adf_2.BACKEND_SCAFFOLD);
411
- files.push('.ai/frontend.adf', '.ai/backend.adf');
499
+ scaffolds.push({ name: 'frontend.adf', content: adf_2.FRONTEND_SCAFFOLD }, { name: 'backend.adf', content: adf_2.BACKEND_SCAFFOLD });
412
500
  }
413
- return files;
501
+ return scaffolds;
502
+ }
503
+ function buildAdfLockContent(aiDir) {
504
+ const lockData = {};
505
+ for (const mod of ['core.adf', 'state.adf']) {
506
+ const modPath = path.join(aiDir, mod);
507
+ if (!fs.existsSync(modPath))
508
+ continue;
509
+ lockData[mod] = hashContent(fs.readFileSync(modPath, 'utf-8'));
510
+ }
511
+ return JSON.stringify(lockData, null, 2) + '\n';
512
+ }
513
+ function writeAdfScaffolds(aiDir, force, preset) {
514
+ fs.mkdirSync(aiDir, { recursive: true });
515
+ const files = [];
516
+ const warnings = [];
517
+ let backedUp = 0;
518
+ let backupDir;
519
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
520
+ for (const scaffold of getAdfScaffolds(preset)) {
521
+ const targetPath = path.join(aiDir, scaffold.name);
522
+ const label = `.ai/${scaffold.name}`;
523
+ if (!fs.existsSync(targetPath)) {
524
+ fs.writeFileSync(targetPath, scaffold.content);
525
+ files.push(label);
526
+ continue;
527
+ }
528
+ const existing = fs.readFileSync(targetPath, 'utf-8');
529
+ if (existing.trim() === scaffold.content.trim()) {
530
+ continue;
531
+ }
532
+ const byteCount = Buffer.byteLength(existing, 'utf-8');
533
+ if (!force) {
534
+ warnings.push(`${label} has custom content (${byteCount} bytes); skipping scaffold overwrite`);
535
+ continue;
536
+ }
537
+ backupDir ||= path.join(aiDir, '.backup');
538
+ fs.mkdirSync(backupDir, { recursive: true });
539
+ const backupName = `${scaffold.name}.${timestamp}`;
540
+ fs.copyFileSync(targetPath, path.join(backupDir, backupName));
541
+ warnings.push(`Backed up ${label} (${byteCount} bytes) → .ai/.backup/${backupName}`);
542
+ backedUp++;
543
+ fs.writeFileSync(targetPath, scaffold.content);
544
+ files.push(label);
545
+ }
546
+ const lockPath = path.join(aiDir, '.adf.lock');
547
+ const lockContent = buildAdfLockContent(aiDir);
548
+ if (!fs.existsSync(lockPath) || fs.readFileSync(lockPath, 'utf-8') !== lockContent) {
549
+ fs.writeFileSync(lockPath, lockContent);
550
+ files.push('.ai/.adf.lock');
551
+ }
552
+ return { files, warnings, backedUp };
414
553
  }
415
554
  function runAdfInitPhase(options, force, preset) {
416
555
  const warnings = [];
417
556
  const files = [];
418
557
  const pointers = [];
558
+ const detectedOrphans = [];
419
559
  try {
420
560
  const aiDir = '.ai';
421
- const manifestPath = path.join(aiDir, 'manifest.adf');
422
- // Create .ai/ scaffolds
423
- const alreadyExists = fs.existsSync(manifestPath);
424
- const hasCustomContent = alreadyExists && hasCustomAdfContent(aiDir);
425
- if (!alreadyExists) {
426
- // Greenfield: write scaffolds (preset-aware on-demand modules)
427
- files.push(...writeAdfScaffolds(aiDir, preset));
428
- // Write .adf.lock
429
- const lockData = {};
430
- for (const mod of ['core.adf', 'state.adf']) {
431
- const content = fs.readFileSync(path.join(aiDir, mod), 'utf-8');
432
- lockData[mod] = hashContent(content);
561
+ const scaffoldResult = writeAdfScaffolds(aiDir, force, preset);
562
+ files.push(...scaffoldResult.files);
563
+ warnings.push(...scaffoldResult.warnings);
564
+ // Detect orphaned ADF modules not registered in manifest (#65)
565
+ const manifestPath2 = path.join(aiDir, 'manifest.adf');
566
+ if (fs.existsSync(manifestPath2)) {
567
+ try {
568
+ const manifestContent = fs.readFileSync(manifestPath2, 'utf-8');
569
+ const allAdfFiles = fs.readdirSync(aiDir).filter(f => f.endsWith('.adf') && f !== 'manifest.adf');
570
+ const doc = (0, adf_3.parseAdf)(manifestContent);
571
+ const manifest = (0, adf_3.parseManifest)(doc);
572
+ const registeredModules = new Set([
573
+ ...manifest.defaultLoad,
574
+ ...manifest.onDemand.map(m => m.path),
575
+ ]);
576
+ const orphans = allAdfFiles.filter(f => !registeredModules.has(f));
577
+ if (orphans.length > 0) {
578
+ detectedOrphans.push(...orphans);
579
+ warnings.push(`Found ${orphans.length} unregistered .adf module(s): ${orphans.join(', ')}`);
580
+ warnings.push('Run `charter adf register` to add them to the manifest.');
581
+ }
433
582
  }
434
- fs.writeFileSync(path.join(aiDir, '.adf.lock'), JSON.stringify(lockData, null, 2) + '\n');
435
- files.push('.ai/.adf.lock');
436
- }
437
- else if (hasCustomContent && !force) {
438
- // Custom ADF content exists — don't overwrite, suggest migrate
439
- warnings.push('.ai/ contains custom ADF content; skipping scaffold overwrite');
440
- warnings.push("Run 'charter adf migrate' to consolidate agent configs into ADF");
441
- }
442
- else if (force) {
443
- // Force overwrite (preset-aware on-demand modules)
444
- files.push(...writeAdfScaffolds(aiDir, preset));
445
- const lockData = {};
446
- for (const mod of ['core.adf', 'state.adf']) {
447
- const content = fs.readFileSync(path.join(aiDir, mod), 'utf-8');
448
- lockData[mod] = hashContent(content);
583
+ catch {
584
+ // Non-critical — manifest parse failure shouldn't block bootstrap
449
585
  }
450
- fs.writeFileSync(path.join(aiDir, '.adf.lock'), JSON.stringify(lockData, null, 2) + '\n');
451
- files.push('.ai/.adf.lock');
452
- }
453
- else {
454
- warnings.push('.ai/ already exists; skipping scaffold (use --yes to overwrite)');
455
586
  }
456
587
  // Generate pointer files (CLAUDE.md uses hybrid template with module index)
457
588
  const pointerFiles = [
@@ -468,16 +599,16 @@ function runAdfInitPhase(options, force, preset) {
468
599
  fs.writeFileSync(pointerPath, pf.content);
469
600
  pointers.push(pf.label);
470
601
  }
471
- else if (exists && !isAlreadyThinPointer(pointerPath)) {
472
- // File has custom content — don't overwrite, suggest migrate
473
- warnings.push(`${pf.name} has custom content; skipping pointer (use 'charter adf migrate' first)`);
474
- }
475
602
  else if (force) {
476
603
  fs.writeFileSync(pointerPath, pf.content);
477
604
  pointers.push(pf.label);
478
605
  }
606
+ else if (!isAlreadyThinPointer(pointerPath)) {
607
+ // File has custom content — don't overwrite, suggest migrate
608
+ warnings.push(`${pf.name} has custom content; skipping pointer (run 'charter adf migrate' first or use --force to overwrite)`);
609
+ }
479
610
  else {
480
- warnings.push(`${pf.name} already exists; skipping (use --yes to overwrite)`);
611
+ warnings.push(`${pf.name} already exists; skipping (use --force to overwrite)`);
481
612
  }
482
613
  }
483
614
  // Populate module index in CLAUDE.md from manifest
@@ -486,7 +617,7 @@ function runAdfInitPhase(options, force, preset) {
486
617
  step: {
487
618
  name: 'adf-init',
488
619
  status: 'pass',
489
- details: { files, pointers },
620
+ details: { files, pointers, backedUp: scaffoldResult.backedUp, orphans: detectedOrphans },
490
621
  warnings,
491
622
  },
492
623
  };
@@ -498,7 +629,7 @@ function runAdfInitPhase(options, force, preset) {
498
629
  step: {
499
630
  name: 'adf-init',
500
631
  status: 'fail',
501
- details: { files, pointers, error: msg },
632
+ details: { files, pointers, orphans: detectedOrphans, error: msg },
502
633
  warnings,
503
634
  },
504
635
  };
@@ -643,7 +774,48 @@ function detectPackageManagerFromLockfiles() {
643
774
  return 'npm';
644
775
  }
645
776
  // ============================================================================
646
- // Phase 6: Doctor
777
+ // Phase 6: Populate (#89)
778
+ // ============================================================================
779
+ async function runPopulatePhase(options) {
780
+ const warnings = [];
781
+ const aiDir = '.ai';
782
+ if (!fs.existsSync(path.join(aiDir, 'manifest.adf'))) {
783
+ return {
784
+ step: {
785
+ name: 'populate',
786
+ status: 'skip',
787
+ details: { populated: 0, skipped: 0, reason: 'no manifest.adf' },
788
+ warnings,
789
+ },
790
+ };
791
+ }
792
+ try {
793
+ const { adfPopulateCommand } = require('./adf-populate');
794
+ const code = await adfPopulateCommand(options, ['--force']);
795
+ return {
796
+ step: {
797
+ name: 'populate',
798
+ status: code === 0 ? 'pass' : 'fail',
799
+ details: { populated: code === 0 ? 1 : 0, skipped: 0 },
800
+ warnings,
801
+ },
802
+ };
803
+ }
804
+ catch (err) {
805
+ const msg = err instanceof Error ? err.message : String(err);
806
+ warnings.push(`Populate failed (non-fatal): ${msg}`);
807
+ return {
808
+ step: {
809
+ name: 'populate',
810
+ status: 'pass', // non-fatal — bootstrap shouldn't fail on populate
811
+ details: { populated: 0, skipped: 0, error: msg },
812
+ warnings,
813
+ },
814
+ };
815
+ }
816
+ }
817
+ // ============================================================================
818
+ // Phase 7: Doctor
647
819
  // ============================================================================
648
820
  function runDoctorPhase(options, skipDoctor) {
649
821
  const warnings = [];
@@ -754,32 +926,59 @@ function hashContent(content) {
754
926
  return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
755
927
  }
756
928
  /**
757
- * Check if .ai/core.adf has content beyond the scaffold template.
929
+ * Check if a file is already a thin ADF pointer.
758
930
  */
759
- function hasCustomAdfContent(aiDir) {
760
- const coreAdfPath = path.join(aiDir, 'core.adf');
761
- if (!fs.existsSync(coreAdfPath))
762
- return false;
931
+ function isAlreadyThinPointer(filePath) {
763
932
  try {
764
- const content = fs.readFileSync(coreAdfPath, 'utf-8');
765
- // Check if the file has been modified from default scaffold
766
- // A custom file will have different content than the CORE_SCAFFOLD
767
- return content.trim() !== adf_2.CORE_SCAFFOLD.trim();
933
+ const content = fs.readFileSync(filePath, 'utf-8');
934
+ return adf_1.POINTER_MARKERS.some(marker => content.includes(marker));
768
935
  }
769
936
  catch {
770
937
  return false;
771
938
  }
772
939
  }
773
940
  /**
774
- * Check if a file is already a thin ADF pointer.
941
+ * Prompt user for a yes/no answer via readline.
775
942
  */
776
- function isAlreadyThinPointer(filePath) {
777
- try {
778
- const content = fs.readFileSync(filePath, 'utf-8');
779
- return adf_1.POINTER_MARKERS.some(marker => content.includes(marker));
943
+ function promptYesNo(question) {
944
+ const rl = readline.createInterface({
945
+ input: process.stdin,
946
+ output: process.stdout,
947
+ });
948
+ return new Promise((resolve) => {
949
+ rl.question(question, (answer) => {
950
+ rl.close();
951
+ resolve(answer.trim().toLowerCase() === 'y');
952
+ });
953
+ });
954
+ }
955
+ /**
956
+ * Append orphaned modules to the ON_DEMAND section of manifest.adf.
957
+ */
958
+ function registerOrphansInManifest(manifestPath, orphans) {
959
+ const content = fs.readFileSync(manifestPath, 'utf-8');
960
+ const lines = content.split('\n');
961
+ // Find ON_DEMAND section
962
+ const onDemandIdx = lines.findIndex(l => l.includes('ON_DEMAND:'));
963
+ if (onDemandIdx === -1) {
964
+ // No ON_DEMAND section — append one
965
+ const newEntries = orphans.map(name => {
966
+ const stem = name.replace('.adf', '');
967
+ return ` - ${name} (Triggers on: ${stem})`;
968
+ });
969
+ fs.writeFileSync(manifestPath, content.trimEnd() + '\n\n📂 ON_DEMAND:\n' + newEntries.join('\n') + '\n');
970
+ return;
780
971
  }
781
- catch {
782
- return false;
972
+ // Find end of ON_DEMAND entries
973
+ let insertIdx = onDemandIdx + 1;
974
+ while (insertIdx < lines.length && lines[insertIdx].match(/^\s+-\s/)) {
975
+ insertIdx++;
783
976
  }
977
+ const newEntries = orphans.map(name => {
978
+ const stem = name.replace('.adf', '');
979
+ return ` - ${name} (Triggers on: ${stem})`;
980
+ });
981
+ lines.splice(insertIdx, 0, ...newEntries);
982
+ fs.writeFileSync(manifestPath, lines.join('\n'));
784
983
  }
785
984
  //# sourceMappingURL=bootstrap.js.map