@jaimevalasek/aioson 1.18.0 → 1.20.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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +121 -9
  3. package/src/commands/feature-archive.js +165 -48
  4. package/src/constants.js +73 -0
  5. package/src/i18n/messages/en.js +24 -7
  6. package/src/i18n/messages/es.js +18 -1
  7. package/src/i18n/messages/fr.js +18 -1
  8. package/src/i18n/messages/pt-BR.js +24 -7
  9. package/template/.aioson/agents/dev.md +2 -0
  10. package/template/.aioson/agents/deyvin.md +1 -0
  11. package/template/.claude/commands/aioson/agent/analyst.md +16 -5
  12. package/template/.claude/commands/aioson/agent/architect.md +17 -5
  13. package/template/.claude/commands/aioson/agent/briefing.md +16 -5
  14. package/template/.claude/commands/aioson/agent/committer.md +16 -5
  15. package/template/.claude/commands/aioson/agent/copywriter.md +16 -5
  16. package/template/.claude/commands/aioson/agent/design-hybrid-forge.md +16 -5
  17. package/template/.claude/commands/aioson/agent/dev.md +18 -5
  18. package/template/.claude/commands/aioson/agent/deyvin.md +16 -5
  19. package/template/.claude/commands/aioson/agent/discover.md +16 -5
  20. package/template/.claude/commands/aioson/agent/discovery-design-doc.md +16 -5
  21. package/template/.claude/commands/aioson/agent/genome.md +16 -5
  22. package/template/.claude/commands/aioson/agent/neo.md +16 -5
  23. package/template/.claude/commands/aioson/agent/orache.md +16 -5
  24. package/template/.claude/commands/aioson/agent/orchestrator.md +21 -5
  25. package/template/.claude/commands/aioson/agent/pair.md +16 -5
  26. package/template/.claude/commands/aioson/agent/pentester.md +22 -5
  27. package/template/.claude/commands/aioson/agent/pm.md +20 -5
  28. package/template/.claude/commands/aioson/agent/product.md +16 -5
  29. package/template/.claude/commands/aioson/agent/profiler-enricher.md +16 -5
  30. package/template/.claude/commands/aioson/agent/profiler-forge.md +16 -5
  31. package/template/.claude/commands/aioson/agent/profiler-researcher.md +16 -5
  32. package/template/.claude/commands/aioson/agent/qa.md +16 -5
  33. package/template/.claude/commands/aioson/agent/setup.md +16 -5
  34. package/template/.claude/commands/aioson/agent/sheldon.md +16 -5
  35. package/template/.claude/commands/aioson/agent/site-forge.md +16 -5
  36. package/template/.claude/commands/aioson/agent/squad.md +16 -5
  37. package/template/.claude/commands/aioson/agent/tester.md +16 -5
  38. package/template/.claude/commands/aioson/agent/ux-ui.md +19 -5
  39. package/template/.claude/commands/aioson/agent/validator.md +17 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaimevalasek/aioson",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "description": "AI operating framework for hyper-personalized software.",
5
5
  "keywords": [
6
6
  "ai",
package/src/cli.js CHANGED
@@ -188,7 +188,7 @@ const { runOpShow } = require('./commands/op-show');
188
188
  const { runOpReinforce } = require('./commands/op-reinforce');
189
189
  const { runOpMigrate } = require('./commands/op-migrate');
190
190
  const { runFeatureClose } = require('./commands/feature-close');
191
- const { runFeatureArchive } = require('./commands/feature-archive');
191
+ const { runFeatureArchive, runFeatureSweep } = require('./commands/feature-archive');
192
192
  const { runDossierInit, runDossierShow, runDossierAddFinding, runDossierAddCodemap, runDossierLinkRule, runDossierCompact } = require('./commands/dossier');
193
193
  const { runDossierAddResearch } = require('./commands/dossier-add-research');
194
194
  const { runDossierAudit } = require('./commands/dossier-audit');
@@ -229,6 +229,8 @@ const JSON_SUPPORTED_COMMANDS = new Set([
229
229
  'agents',
230
230
  'agent:prompt',
231
231
  'agent-prompt',
232
+ 'agent:help',
233
+ 'agent-help',
232
234
  'agent:invoke',
233
235
  'agent-invoke',
234
236
  'setup:context',
@@ -618,6 +620,8 @@ const JSON_SUPPORTED_COMMANDS = new Set([
618
620
  'feature-close',
619
621
  'feature:archive',
620
622
  'feature-archive',
623
+ 'feature:sweep',
624
+ 'feature-sweep',
621
625
  'dossier:init',
622
626
  'dossier-init',
623
627
  'dossier:show',
@@ -700,6 +704,10 @@ const JSON_SUPPORTED_COMMANDS = new Set([
700
704
  'system-install'
701
705
  ]);
702
706
 
707
+ const AGENT_HELP_COMMANDS = new Set([
708
+ 'agent:prompt', 'agent-prompt', 'agent:invoke', 'agent-invoke', 'agent:help', 'agent-help'
709
+ ]);
710
+
703
711
  const LEGACY_DASHBOARD_COMMANDS = new Set([
704
712
  'dashboard:init',
705
713
  'dashboard-init',
@@ -748,6 +756,7 @@ function printHelp(t, logger) {
748
756
  logHelpLine(t, logger, 'cli.help_i18n_add');
749
757
  logHelpLine(t, logger, 'cli.help_agents');
750
758
  logHelpLine(t, logger, 'cli.help_agent_prompt');
759
+ logHelpLine(t, logger, 'cli.help_agent_help');
751
760
  logHelpLine(t, logger, 'cli.help_agent_invoke');
752
761
  logHelpLine(t, logger, 'cli.help_context_validate');
753
762
  logHelpLine(t, logger, 'cli.help_context_pack');
@@ -851,6 +860,91 @@ function printHelp(t, logger) {
851
860
  logHelpLine(t, logger, 'cli.help_squad_grant');
852
861
  }
853
862
 
863
+ function printAgentHelp(agentName, jsonMode, logger, t) {
864
+ const { AGENT_DEFINITIONS } = require('./constants');
865
+ const { normalizeAgentName } = require('./agents');
866
+
867
+ if (!agentName) {
868
+ const list = AGENT_DEFINITIONS.map((a) => ({
869
+ id: a.id,
870
+ displayName: a.displayName,
871
+ description: a.description || ''
872
+ }));
873
+ if (jsonMode) {
874
+ writeJson({ ok: true, agents: list });
875
+ return;
876
+ }
877
+ logger.log(`${t('agents.help_available')}\n`);
878
+ for (const a of list) {
879
+ logger.log(` @${a.id.padEnd(24)} ${a.description}`);
880
+ }
881
+ logger.log(`\n${t('agents.help_run_detail')}`);
882
+ return;
883
+ }
884
+
885
+ const normalized = normalizeAgentName(agentName);
886
+ const agent = AGENT_DEFINITIONS.find((a) => {
887
+ if (a.id === normalized) return true;
888
+ return Array.isArray(a.aliases) && a.aliases.includes(normalized);
889
+ });
890
+
891
+ if (!agent) {
892
+ if (jsonMode) {
893
+ writeJson({ ok: false, error: t('agents.help_unknown_agent', { agent: agentName }) });
894
+ } else {
895
+ logger.error(t('agents.help_unknown_agent', { agent: agentName }));
896
+ }
897
+ process.exitCode = 1;
898
+ return;
899
+ }
900
+
901
+ if (jsonMode) {
902
+ writeJson({
903
+ ok: true,
904
+ agent: {
905
+ id: agent.id,
906
+ displayName: agent.displayName,
907
+ description: agent.description || '',
908
+ command: agent.command,
909
+ path: agent.path,
910
+ aliases: agent.aliases || [],
911
+ flags: agent.flags || [],
912
+ dependsOn: agent.dependsOn,
913
+ output: agent.output
914
+ }
915
+ });
916
+ return;
917
+ }
918
+
919
+ const aliasLine = agent.aliases && agent.aliases.length > 0
920
+ ? ` (aliases: ${agent.aliases.join(', ')})`
921
+ : '';
922
+ logger.log(`${agent.command} — ${agent.description || agent.displayName}${aliasLine}\n`);
923
+ logger.log(`${t('agents.help_usage')}`);
924
+ logger.log(` aioson agent:prompt ${agent.id} [path] [options]`);
925
+ logger.log(` /${agent.id} [task description] ${t('agents.help_claude_code')}\n`);
926
+ logger.log(t('agents.help_common_options'));
927
+ logger.log(` --tool=<tool> ${t('agents.help_opt_tool')}`);
928
+ logger.log(` --language=<lang> ${t('agents.help_opt_language')}`);
929
+ logger.log(` --headless ${t('agents.help_opt_headless')}`);
930
+ logger.log(` --output=<path> ${t('agents.help_opt_output')}`);
931
+ logger.log(` --json ${t('agents.help_opt_json')}`);
932
+ if (agent.flags && agent.flags.length > 0) {
933
+ logger.log(`\n${t('agents.help_agent_options', { command: agent.command })}`);
934
+ for (const flag of agent.flags) {
935
+ logger.log(` --${flag.name}=${flag.value.padEnd(Math.max(12 - flag.name.length, 1))} ${flag.description}`);
936
+ }
937
+ }
938
+ if (agent.dependsOn.length > 0) {
939
+ logger.log(`\n${t('agents.help_requires')}`);
940
+ for (const dep of agent.dependsOn) {
941
+ logger.log(` ${dep}`);
942
+ }
943
+ }
944
+ logger.log(`\n${t('agents.help_produces')}\n ${agent.output}`);
945
+ logger.log(`\n${t('agents.help_instruction_file')}\n ${agent.path}`);
946
+ }
947
+
854
948
  function commandSupportsJson(command) {
855
949
  return JSON_SUPPORTED_COMMANDS.has(command);
856
950
  }
@@ -868,19 +962,31 @@ async function main() {
868
962
  const logger = createLogger();
869
963
  const silentLogger = createSilentLogger();
870
964
 
871
- if (command === 'help' || options.help || command === '--help' || command === '-h') {
872
- printHelp(t, logger);
873
- return;
874
- }
875
-
876
965
  if (command === '--version' || command === '-v' || command === 'version' || options.version) {
877
- const result = await runInfo({ args: ['.'], options, logger, t });
966
+ const { getCliVersionSync } = require('./version');
967
+ const version = getCliVersionSync();
878
968
  if (jsonMode) {
879
- writeJson(result);
969
+ writeJson({ ok: true, version });
970
+ } else {
971
+ logger.log(`aioson ${version}`);
880
972
  }
881
973
  return;
882
974
  }
883
975
 
976
+ if (options.help && AGENT_HELP_COMMANDS.has(command)) {
977
+ printAgentHelp(args[0], jsonMode, logger, t);
978
+ return;
979
+ }
980
+ if (command === 'agent:help' || command === 'agent-help') {
981
+ printAgentHelp(args[0], jsonMode, logger, t);
982
+ return;
983
+ }
984
+
985
+ if (command === 'help' || options.help || command === '--help' || command === '-h') {
986
+ printHelp(t, logger);
987
+ return;
988
+ }
989
+
884
990
  if (LEGACY_DASHBOARD_COMMANDS.has(command)) {
885
991
  const message = t('cli.dashboard_moved', { command });
886
992
  if (jsonMode) {
@@ -1376,7 +1482,13 @@ async function main() {
1376
1482
  } else if (command === 'feature:close' || command === 'feature-close') {
1377
1483
  result = await runFeatureClose({ args, options, logger: commandLogger });
1378
1484
  } else if (command === 'feature:archive' || command === 'feature-archive') {
1379
- result = await runFeatureArchive({ args, options, logger: commandLogger });
1485
+ if (options.sweep) {
1486
+ result = await runFeatureSweep({ args, options, logger: commandLogger });
1487
+ } else {
1488
+ result = await runFeatureArchive({ args, options, logger: commandLogger });
1489
+ }
1490
+ } else if (command === 'feature:sweep' || command === 'feature-sweep') {
1491
+ result = await runFeatureSweep({ args, options, logger: commandLogger });
1380
1492
  } else if (command === 'dossier:init' || command === 'dossier-init') {
1381
1493
  result = await runDossierInit({ args, options, logger: commandLogger });
1382
1494
  } else if (command === 'dossier:show' || command === 'dossier-show') {
@@ -270,19 +270,42 @@ async function runFeatureArchive({ args = [], options = {}, logger }) {
270
270
  const otherSlugs = await readOtherSlugs(featuresPath, slug);
271
271
  const rootFiles = await findSlugFiles(ctxDir, slug, otherSlugs);
272
272
  const alreadyArchived = (await dirExists(archiveDir)) ? await findArchivedFiles(archiveDir) : [];
273
- const dossierSourceDir = path.join(ctxDir, 'features', slug);
274
- const dossierTargetDir = path.join(archiveDir, 'dossier');
275
- const hasDossierToMove = await dirExists(dossierSourceDir);
276
- const dossierAlreadyArchived = await dirExists(dossierTargetDir);
273
+
274
+ const SLUG_DIRS = [
275
+ { label: 'dossier', sourceBase: path.join(ctxDir, 'features'), archiveLabel: 'dossier' },
276
+ { label: 'plans', sourceBase: path.join(targetDir, '.aioson', 'plans'), archiveLabel: 'plans' },
277
+ { label: 'briefings', sourceBase: path.join(targetDir, '.aioson', 'briefings'), archiveLabel: 'briefings' }
278
+ ];
279
+
280
+ const dirPlans = [];
281
+ for (const dir of SLUG_DIRS) {
282
+ const sourceDir = path.join(dir.sourceBase, slug);
283
+ const targetDirPath = path.join(archiveDir, dir.archiveLabel);
284
+ const hasSource = await dirExists(sourceDir);
285
+ const alreadyDone = await dirExists(targetDirPath);
286
+ if (hasSource || alreadyDone) {
287
+ dirPlans.push({
288
+ label: dir.label,
289
+ sourceDir,
290
+ targetDir: targetDirPath,
291
+ sourceBase: dir.sourceBase,
292
+ action: hasSource
293
+ ? (alreadyDone ? 'skip' : 'move')
294
+ : (alreadyDone ? 'noop' : null),
295
+ reason: alreadyDone ? 'already_archived' : null
296
+ });
297
+ }
298
+ }
299
+
300
+ const hasAnyDir = dirPlans.some((d) => d.action === 'move' || d.action === 'skip' || d.action === 'noop');
277
301
 
278
302
  if (
279
303
  rootFiles.length === 0 &&
280
304
  alreadyArchived.length === 0 &&
281
- !hasDossierToMove &&
282
- !dossierAlreadyArchived
305
+ !hasAnyDir
283
306
  ) {
284
307
  if (jsonOut) return { ok: true, slug, moved: [], skipped: [], alreadyArchived: [], noop: true };
285
- log(`No files matched "*-${slug}.{${ARCHIVED_EXTENSIONS.join(',')}}" in .aioson/context/ root and no features/${slug}/ dossier dir — nothing to archive.`);
308
+ log(`No files matched "*-${slug}.{${ARCHIVED_EXTENSIONS.join(',')}}" in .aioson/context/ root and no slug directories found — nothing to archive.`);
286
309
  return { ok: true, noop: true };
287
310
  }
288
311
 
@@ -305,11 +328,16 @@ async function runFeatureArchive({ args = [], options = {}, logger }) {
305
328
  : null;
306
329
  const summary = summarySource ? await extractSummary(summarySource) : null;
307
330
 
308
- const dossierPlan = hasDossierToMove
309
- ? (dossierAlreadyArchived ? { action: 'skip', reason: 'already_archived' } : { action: 'move' })
310
- : (dossierAlreadyArchived ? { action: 'noop', reason: 'already_archived' } : null);
311
-
312
331
  if (dryRun) {
332
+ const dirs = dirPlans
333
+ .filter((d) => d.action)
334
+ .map((d) => ({
335
+ label: d.label,
336
+ source: path.relative(targetDir, d.sourceDir),
337
+ target: path.relative(targetDir, d.targetDir),
338
+ action: d.action,
339
+ reason: d.reason
340
+ }));
313
341
  const result = {
314
342
  ok: true,
315
343
  dryRun: true,
@@ -317,13 +345,7 @@ async function runFeatureArchive({ args = [], options = {}, logger }) {
317
345
  targetDir: path.relative(targetDir, archiveDir),
318
346
  move: toMove,
319
347
  skip: toSkip,
320
- dossier: dossierPlan
321
- ? {
322
- source: path.relative(targetDir, dossierSourceDir),
323
- target: path.relative(targetDir, dossierTargetDir),
324
- ...dossierPlan
325
- }
326
- : null,
348
+ dirs,
327
349
  manifestEntry: {
328
350
  slug,
329
351
  completed,
@@ -340,10 +362,12 @@ async function runFeatureArchive({ args = [], options = {}, logger }) {
340
362
  log(` would skip: ${toSkip.length} file(s)`);
341
363
  for (const s of toSkip) log(` • ${s.name} (${s.reason})`);
342
364
  }
343
- if (dossierPlan && dossierPlan.action === 'move') {
344
- log(` would move dossier dir: features/${slug}/ → ${path.relative(targetDir, dossierTargetDir)}/`);
345
- } else if (dossierPlan && dossierPlan.action === 'skip') {
346
- log(` would skip dossier dir: already archived at ${path.relative(targetDir, dossierTargetDir)}/`);
365
+ for (const d of dirPlans) {
366
+ if (d.action === 'move') {
367
+ log(` would move ${d.label} dir: ${path.relative(targetDir, d.sourceDir)}/ → ${path.relative(targetDir, d.targetDir)}/`);
368
+ } else if (d.action === 'skip') {
369
+ log(` would skip ${d.label} dir: already archived at ${path.relative(targetDir, d.targetDir)}/`);
370
+ }
347
371
  }
348
372
  log(` manifest entry: | ${slug} | ${completed} | ${toMove.length + alreadyArchived.length} | ${summary || '—'} |`);
349
373
  return result;
@@ -359,27 +383,29 @@ async function runFeatureArchive({ args = [], options = {}, logger }) {
359
383
  moved.push(name);
360
384
  }
361
385
 
362
- let dossierResult = null;
363
- if (dossierPlan && dossierPlan.action === 'move') {
364
- await fs.rename(dossierSourceDir, dossierTargetDir);
365
- dossierResult = {
366
- action: 'moved',
367
- source: path.relative(targetDir, dossierSourceDir),
368
- target: path.relative(targetDir, dossierTargetDir)
369
- };
370
- try {
371
- const parent = path.join(ctxDir, 'features');
372
- const remaining = await fs.readdir(parent);
373
- if (remaining.length === 0) await fs.rmdir(parent);
374
- } catch {
375
- // parent missing or non-empty — leave it
386
+ const dirResults = [];
387
+ for (const d of dirPlans) {
388
+ if (d.action === 'move') {
389
+ await fs.mkdir(path.dirname(d.targetDir), { recursive: true });
390
+ await fs.rename(d.sourceDir, d.targetDir);
391
+ dirResults.push({
392
+ label: d.label,
393
+ action: 'moved',
394
+ source: path.relative(targetDir, d.sourceDir),
395
+ target: path.relative(targetDir, d.targetDir)
396
+ });
397
+ try {
398
+ const remaining = await fs.readdir(d.sourceBase);
399
+ if (remaining.length === 0) await fs.rmdir(d.sourceBase);
400
+ } catch { /* parent missing or non-empty */ }
401
+ } else if (d.action === 'skip') {
402
+ dirResults.push({
403
+ label: d.label,
404
+ action: 'skipped',
405
+ reason: d.reason,
406
+ target: path.relative(targetDir, d.targetDir)
407
+ });
376
408
  }
377
- } else if (dossierPlan && dossierPlan.action === 'skip') {
378
- dossierResult = {
379
- action: 'skipped',
380
- reason: dossierPlan.reason,
381
- target: path.relative(targetDir, dossierTargetDir)
382
- };
383
409
  }
384
410
 
385
411
  const totalArchived = (await findArchivedFiles(archiveDir)).length;
@@ -399,7 +425,8 @@ async function runFeatureArchive({ args = [], options = {}, logger }) {
399
425
  moved,
400
426
  skipped: toSkip,
401
427
  totalArchived,
402
- dossier: dossierResult,
428
+ dirs: dirResults.length > 0 ? dirResults : undefined,
429
+ dossier: dirResults.find((d) => d.label === 'dossier') || null,
403
430
  manifestEntry: entry
404
431
  };
405
432
 
@@ -412,10 +439,12 @@ async function runFeatureArchive({ args = [], options = {}, logger }) {
412
439
  log(` skipped: ${toSkip.length} file(s) already in archive`);
413
440
  for (const s of toSkip) log(` • ${s.name}`);
414
441
  }
415
- if (dossierResult && dossierResult.action === 'moved') {
416
- log(` moved dossier dir: ${dossierResult.source}/ → ${dossierResult.target}/`);
417
- } else if (dossierResult && dossierResult.action === 'skipped') {
418
- log(` skipped dossier dir: already archived at ${dossierResult.target}/`);
442
+ for (const d of dirResults) {
443
+ if (d.action === 'moved') {
444
+ log(` moved ${d.label} dir: ${d.source}/ ${d.target}/`);
445
+ } else if (d.action === 'skipped') {
446
+ log(` skipped ${d.label} dir: already archived at ${d.target}/`);
447
+ }
419
448
  }
420
449
  log(` manifest updated: .aioson/context/done/MANIFEST.md`);
421
450
  return result;
@@ -510,4 +539,92 @@ async function runRestore({ slug, ctxDir, archiveDir, manifestPath, dryRun, json
510
539
  return result;
511
540
  }
512
541
 
513
- module.exports = { runFeatureArchive };
542
+ async function listDoneFeatures(featuresPath) {
543
+ const content = await readFileSafe(featuresPath);
544
+ if (!content) return [];
545
+ const results = [];
546
+ const lines = content.split(/\r?\n/);
547
+ for (const line of lines) {
548
+ const m = line.match(/^\|\s*([a-z][a-z0-9-]*)\s*\|\s*done\s*\|/i);
549
+ if (m) results.push(m[1].toLowerCase());
550
+ }
551
+ return results;
552
+ }
553
+
554
+ async function listArchivedSlugs(manifestPath) {
555
+ const content = await readFileSafe(manifestPath);
556
+ if (!content) return new Set();
557
+ const slugs = new Set();
558
+ const lines = content.split(/\r?\n/);
559
+ for (const line of lines) {
560
+ const m = line.match(/^\|\s*([a-z][a-z0-9-]+)\s*\|/i);
561
+ if (m && m[1] !== 'slug') slugs.add(m[1].toLowerCase());
562
+ }
563
+ return slugs;
564
+ }
565
+
566
+ async function runFeatureSweep({ args = [], options = {}, logger }) {
567
+ const targetDir = path.resolve(process.cwd(), args[0] || '.');
568
+ const dryRun = Boolean(options['dry-run'] || options.dryRun);
569
+ const jsonOut = Boolean(options.json);
570
+ const log = (msg) => { if (logger && !jsonOut) logger.log(msg); };
571
+
572
+ const ctxDir = contextDir(targetDir);
573
+ if (!(await dirExists(ctxDir))) {
574
+ if (jsonOut) return { ok: false, reason: 'no_context_dir' };
575
+ log('.aioson/context/ not found. Run aioson setup first.');
576
+ return { ok: false };
577
+ }
578
+
579
+ const featuresPath = path.join(ctxDir, 'features.md');
580
+ const manifestPath = path.join(ctxDir, 'done', 'MANIFEST.md');
581
+
582
+ const doneSlugs = await listDoneFeatures(featuresPath);
583
+ const archivedSlugs = await listArchivedSlugs(manifestPath);
584
+ const pending = doneSlugs.filter((s) => !archivedSlugs.has(s));
585
+
586
+ if (pending.length === 0) {
587
+ const result = { ok: true, pending: [], archived: [] };
588
+ if (jsonOut) return result;
589
+ log('All done features are already archived.');
590
+ return result;
591
+ }
592
+
593
+ if (dryRun) {
594
+ const result = { ok: true, dryRun: true, pending, archived: [] };
595
+ if (jsonOut) return result;
596
+ log(`[dry-run] ${pending.length} done feature(s) not yet archived:`);
597
+ for (const s of pending) log(` • ${s}`);
598
+ return result;
599
+ }
600
+
601
+ const archived = [];
602
+ const failed = [];
603
+ for (const slug of pending) {
604
+ try {
605
+ const archiveResult = await runFeatureArchive({
606
+ args: [targetDir],
607
+ options: { feature: slug, json: true },
608
+ logger: null
609
+ });
610
+ if (archiveResult && archiveResult.ok) {
611
+ const movedCount = archiveResult.moved ? archiveResult.moved.length : 0;
612
+ archived.push({ slug, moved: movedCount });
613
+ log(` ✓ ${slug} — ${movedCount} file(s) archived`);
614
+ } else {
615
+ failed.push({ slug, reason: archiveResult.reason || 'unknown' });
616
+ log(` ✗ ${slug} — ${archiveResult.reason || 'unknown'}`);
617
+ }
618
+ } catch (err) {
619
+ failed.push({ slug, reason: err.message || String(err) });
620
+ log(` ✗ ${slug} — ${err.message || err}`);
621
+ }
622
+ }
623
+
624
+ const result = { ok: true, pending, archived, failed: failed.length > 0 ? failed : undefined };
625
+ if (jsonOut) return result;
626
+ log(`\nSweep complete: ${archived.length} archived, ${failed.length} failed.`);
627
+ return result;
628
+ }
629
+
630
+ module.exports = { runFeatureArchive, runFeatureSweep };