@jaimevalasek/aioson 1.21.7 → 1.22.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 (105) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/docs/en/1-understand/ecosystem-map.md +1 -1
  3. package/docs/en/2-start/initial-decisions.md +1 -1
  4. package/docs/en/4-agents/README.md +8 -7
  5. package/docs/en/4-agents/discovery-design-doc.md +150 -0
  6. package/docs/en/5-reference/cli-reference.md +42 -16
  7. package/docs/en/README.md +2 -2
  8. package/docs/pt/4-agentes/README.md +8 -6
  9. package/docs/pt/4-agentes/briefing-refiner.md +122 -0
  10. package/docs/pt/4-agentes/discovery-design-doc.md +133 -74
  11. package/docs/pt/4-agentes/scope-check.md +65 -0
  12. package/docs/pt/5-referencia/README.md +1 -0
  13. package/docs/pt/5-referencia/comandos-cli.md +5 -4
  14. package/docs/pt/5-referencia/feature-archive.md +1 -0
  15. package/docs/pt/5-referencia/feature-export.md +155 -0
  16. package/docs/pt/README.md +2 -2
  17. package/docs/pt/agentes.md +3 -1
  18. package/package.json +1 -1
  19. package/src/agent-manifests.js +14 -3
  20. package/src/agents.js +21 -20
  21. package/src/cli.js +72 -52
  22. package/src/commands/briefing.js +28 -150
  23. package/src/commands/commit-prepare.js +5 -2
  24. package/src/commands/feature-archive.js +48 -12
  25. package/src/commands/feature-close.js +40 -0
  26. package/src/commands/feature-export.js +242 -0
  27. package/src/commands/gate-check.js +8 -3
  28. package/src/commands/git-guard.js +58 -0
  29. package/src/commands/harness-gate.js +120 -0
  30. package/src/commands/harness-status.js +157 -0
  31. package/src/commands/harness.js +18 -1
  32. package/src/commands/live.js +120 -115
  33. package/src/commands/parallel-doctor.js +2 -1
  34. package/src/commands/pulse-update.js +2 -2
  35. package/src/commands/scan-project.js +12 -2
  36. package/src/commands/self-implement-loop.js +305 -5
  37. package/src/commands/workflow-next.js +477 -425
  38. package/src/constants.js +21 -11
  39. package/src/context-search.js +3 -0
  40. package/src/doctor.js +24 -8
  41. package/src/dossier/schema.js +4 -3
  42. package/src/harness/active-contract.js +41 -0
  43. package/src/harness/attempt-artifacts.js +95 -0
  44. package/src/harness/budget-guard.js +127 -0
  45. package/src/harness/circuit-breaker.js +7 -0
  46. package/src/harness/contract-schema.js +324 -0
  47. package/src/harness/criteria-runner.js +136 -0
  48. package/src/harness/git-baseline.js +204 -0
  49. package/src/harness/glob-match.js +126 -0
  50. package/src/harness/guard-events.js +71 -0
  51. package/src/harness/human-gate.js +182 -0
  52. package/src/harness/scope-guard.js +115 -0
  53. package/src/i18n/messages/en.js +24 -21
  54. package/src/i18n/messages/es.js +11 -9
  55. package/src/i18n/messages/fr.js +11 -9
  56. package/src/i18n/messages/pt-BR.js +24 -21
  57. package/src/lib/briefing-refiner/apply-feedback.js +134 -0
  58. package/src/lib/briefing-refiner/briefing-paths.js +41 -0
  59. package/src/lib/briefing-refiner/briefing-registry.js +204 -0
  60. package/src/lib/briefing-refiner/briefing-sections.js +110 -0
  61. package/src/lib/briefing-refiner/feedback-schema.js +122 -0
  62. package/src/lib/briefing-refiner/refinement-report.js +39 -0
  63. package/src/lib/briefing-refiner/review-html.js +230 -0
  64. package/src/lib/dev-resume.js +94 -45
  65. package/src/parser.js +8 -5
  66. package/src/preflight-engine.js +88 -84
  67. package/src/runtime-store.js +2 -0
  68. package/src/sandbox.js +17 -3
  69. package/template/.aioson/agents/analyst.md +27 -23
  70. package/template/.aioson/agents/architect.md +7 -3
  71. package/template/.aioson/agents/briefing-refiner.md +121 -0
  72. package/template/.aioson/agents/briefing.md +83 -74
  73. package/template/.aioson/agents/committer.md +8 -0
  74. package/template/.aioson/agents/copywriter.md +19 -7
  75. package/template/.aioson/agents/design-hybrid-forge.md +16 -5
  76. package/template/.aioson/agents/dev.md +68 -66
  77. package/template/.aioson/agents/deyvin.md +97 -90
  78. package/template/.aioson/agents/discover.md +2 -2
  79. package/template/.aioson/agents/discovery-design-doc.md +34 -30
  80. package/template/.aioson/agents/genome.md +82 -71
  81. package/template/.aioson/agents/neo.md +11 -3
  82. package/template/.aioson/agents/orache.md +10 -0
  83. package/template/.aioson/agents/orchestrator.md +68 -68
  84. package/template/.aioson/agents/pentester.md +15 -6
  85. package/template/.aioson/agents/pm.md +30 -25
  86. package/template/.aioson/agents/product.md +108 -108
  87. package/template/.aioson/agents/profiler-enricher.md +10 -0
  88. package/template/.aioson/agents/profiler-forge.md +10 -0
  89. package/template/.aioson/agents/profiler-researcher.md +11 -0
  90. package/template/.aioson/agents/qa.md +28 -20
  91. package/template/.aioson/agents/scope-check.md +176 -164
  92. package/template/.aioson/agents/setup.md +11 -1
  93. package/template/.aioson/agents/sheldon.md +38 -38
  94. package/template/.aioson/agents/site-forge.md +15 -6
  95. package/template/.aioson/agents/squad.md +12 -0
  96. package/template/.aioson/agents/tester.md +209 -209
  97. package/template/.aioson/agents/ux-ui.md +2 -2
  98. package/template/.aioson/agents/validator.md +10 -2
  99. package/template/.aioson/config.md +31 -28
  100. package/template/.aioson/docs/autopilot-handoff.md +46 -0
  101. package/template/.aioson/docs/dossier/agent-templates.md +191 -0
  102. package/template/.aioson/docs/dossier/schema.md +218 -0
  103. package/template/.claude/commands/aioson/agent/briefing-refiner.md +17 -0
  104. package/template/AGENTS.md +50 -47
  105. package/template/CLAUDE.md +29 -27
package/src/cli.js CHANGED
@@ -82,12 +82,12 @@ const { runSquadWebhook } = require('./commands/squad-webhook');
82
82
  const { runSquadBus } = require('./commands/squad-bus');
83
83
  const { runSquadAutorun } = require('./commands/squad-autorun');
84
84
  const { runSquadDependencyGraph } = require('./commands/squad-dependency-graph');
85
- const { runSquadToolRegister } = require('./commands/squad-tool-register');
86
- const { runSquadReview } = require('./commands/squad-review');
87
- const { runAgentAudit } = require('./commands/agent-audit');
88
- const { runSkillAudit } = require('./commands/skill-audit');
89
- const { runQualityAudit } = require('./commands/quality-audit');
90
- const { runBriefGen } = require('./commands/brief-gen');
85
+ const { runSquadToolRegister } = require('./commands/squad-tool-register');
86
+ const { runSquadReview } = require('./commands/squad-review');
87
+ const { runAgentAudit } = require('./commands/agent-audit');
88
+ const { runSkillAudit } = require('./commands/skill-audit');
89
+ const { runQualityAudit } = require('./commands/quality-audit');
90
+ const { runBriefGen } = require('./commands/brief-gen');
91
91
  const { runHarnessInit, runHarnessValidate, runHarnessApplyValidation } = require('./commands/harness');
92
92
  const { runVerifyGate } = require('./commands/verify-gate');
93
93
  const {
@@ -194,6 +194,7 @@ const { runOpReinforce } = require('./commands/op-reinforce');
194
194
  const { runOpMigrate } = require('./commands/op-migrate');
195
195
  const { runFeatureClose } = require('./commands/feature-close');
196
196
  const { runFeatureArchive, runFeatureSweep } = require('./commands/feature-archive');
197
+ const { runFeatureExport } = require('./commands/feature-export');
197
198
  const { runDossierInit, runDossierShow, runDossierAddFinding, runDossierAddCodemap, runDossierLinkRule, runDossierCompact } = require('./commands/dossier');
198
199
  const { runDossierAddResearch } = require('./commands/dossier-add-research');
199
200
  const { runDossierAudit } = require('./commands/dossier-audit');
@@ -204,10 +205,10 @@ const { runGateApprove } = require('./commands/gate-approve');
204
205
  const { runArtifactValidate } = require('./commands/artifact-validate');
205
206
  const { runWorkflowExecute } = require('./commands/workflow-execute');
206
207
  const { runRunnerQueueFromPlan } = require('./commands/runner-queue-from-plan');
207
- const { runLearningAutoPromote } = require('./commands/learning-auto-promote');
208
- const { runBriefValidate } = require('./commands/brief-validate');
209
- const { runIntakeAsk } = require('./commands/intake-ask');
210
- const { runPreflightContext } = require('./commands/preflight-context');
208
+ const { runLearningAutoPromote } = require('./commands/learning-auto-promote');
209
+ const { runBriefValidate } = require('./commands/brief-validate');
210
+ const { runIntakeAsk } = require('./commands/intake-ask');
211
+ const { runPreflightContext } = require('./commands/preflight-context');
211
212
  const { runContextCompact } = require('./commands/context-compact');
212
213
  const { runSquadScaffold } = require('./commands/squad-scaffold');
213
214
  const { runPatternDetect } = require('./commands/pattern-detect');
@@ -385,27 +386,33 @@ const JSON_SUPPORTED_COMMANDS = new Set([
385
386
  'squad-tool-register',
386
387
  'squad:review',
387
388
  'squad-review',
388
- 'agent:audit',
389
- 'agent-audit',
390
- 'skill:audit',
391
- 'skill-audit',
392
- 'quality:audit',
393
- 'quality-audit',
394
- 'brief:gen',
389
+ 'agent:audit',
390
+ 'agent-audit',
391
+ 'skill:audit',
392
+ 'skill-audit',
393
+ 'quality:audit',
394
+ 'quality-audit',
395
+ 'brief:gen',
395
396
  'harness:init',
396
397
  'harness-init',
397
398
  'harness:validate',
398
399
  'harness-validate',
399
400
  'harness:apply-validation',
400
401
  'harness-apply-validation',
402
+ 'harness:approve',
403
+ 'harness-approve',
404
+ 'harness:reject',
405
+ 'harness-reject',
406
+ 'harness:status',
407
+ 'harness-status',
401
408
  'brief-gen',
402
409
  'verify:gate',
403
410
  'verify-gate',
404
- 'brief:validate',
405
- 'brief-validate',
406
- 'intake:ask',
407
- 'intake-ask',
408
- 'preflight:context',
411
+ 'brief:validate',
412
+ 'brief-validate',
413
+ 'intake:ask',
414
+ 'intake-ask',
415
+ 'preflight:context',
409
416
  'preflight-context',
410
417
  'context:compact',
411
418
  'context-compact',
@@ -640,6 +647,8 @@ const JSON_SUPPORTED_COMMANDS = new Set([
640
647
  'feature-archive',
641
648
  'feature:sweep',
642
649
  'feature-sweep',
650
+ 'feature:export',
651
+ 'feature-export',
643
652
  'dossier:init',
644
653
  'dossier-init',
645
654
  'dossier:show',
@@ -827,11 +836,11 @@ function printHelp(t, logger) {
827
836
  logHelpLine(t, logger, 'cli.help_squad_daemon');
828
837
  logHelpLine(t, logger, 'cli.help_squad_mcp');
829
838
  logHelpLine(t, logger, 'cli.help_squad_roi');
830
- logHelpLine(t, logger, 'cli.help_squad_score');
831
- logHelpLine(t, logger, 'cli.help_squad_learning');
832
- logHelpLine(t, logger, 'cli.help_agent_audit');
833
- logHelpLine(t, logger, 'cli.help_quality_audit');
834
- logHelpLine(t, logger, 'cli.help_learning');
839
+ logHelpLine(t, logger, 'cli.help_squad_score');
840
+ logHelpLine(t, logger, 'cli.help_squad_learning');
841
+ logHelpLine(t, logger, 'cli.help_agent_audit');
842
+ logHelpLine(t, logger, 'cli.help_quality_audit');
843
+ logHelpLine(t, logger, 'cli.help_learning');
835
844
  logHelpLine(t, logger, 'cli.help_runtime_init');
836
845
  logHelpLine(t, logger, 'cli.help_runtime_ingest');
837
846
  logHelpLine(t, logger, 'cli.help_runtime_task_start');
@@ -840,10 +849,10 @@ function printHelp(t, logger) {
840
849
  logHelpLine(t, logger, 'cli.help_runtime_task_finish');
841
850
  logHelpLine(t, logger, 'cli.help_runtime_finish');
842
851
  logHelpLine(t, logger, 'cli.help_runtime_task_fail');
843
- logHelpLine(t, logger, 'cli.help_runtime_fail');
844
- logHelpLine(t, logger, 'cli.help_runtime_status');
845
- logHelpLine(t, logger, 'cli.help_agent_recover');
846
- logHelpLine(t, logger, 'cli.help_runtime_session_start');
852
+ logHelpLine(t, logger, 'cli.help_runtime_fail');
853
+ logHelpLine(t, logger, 'cli.help_runtime_status');
854
+ logHelpLine(t, logger, 'cli.help_agent_recover');
855
+ logHelpLine(t, logger, 'cli.help_runtime_session_start');
847
856
  logHelpLine(t, logger, 'cli.help_runtime_session_log');
848
857
  logHelpLine(t, logger, 'cli.help_runtime_session_finish');
849
858
  logHelpLine(t, logger, 'cli.help_runtime_session_status');
@@ -855,10 +864,10 @@ function printHelp(t, logger) {
855
864
  logHelpLine(t, logger, 'cli.help_scaffold_complete');
856
865
  logHelpLine(t, logger, 'cli.help_runtime_backup');
857
866
  logHelpLine(t, logger, 'cli.help_runtime_restore');
858
- logHelpLine(t, logger, 'cli.help_skill_install');
859
- logHelpLine(t, logger, 'cli.help_skill_list');
860
- logHelpLine(t, logger, 'cli.help_skill_remove');
861
- logHelpLine(t, logger, 'cli.help_skill_audit');
867
+ logHelpLine(t, logger, 'cli.help_skill_install');
868
+ logHelpLine(t, logger, 'cli.help_skill_list');
869
+ logHelpLine(t, logger, 'cli.help_skill_remove');
870
+ logHelpLine(t, logger, 'cli.help_skill_audit');
862
871
  logHelpLine(t, logger, 'cli.help_design_hybrid_options');
863
872
  logHelpLine(t, logger, 'cli.help_cloud_import_squad');
864
873
  logHelpLine(t, logger, 'cli.help_cloud_import_genome');
@@ -1239,29 +1248,38 @@ async function main() {
1239
1248
  result = await runSquadToolRegister({ args, options, logger: commandLogger });
1240
1249
  } else if (command === 'squad:review' || command === 'squad-review') {
1241
1250
  result = await runSquadReview({ args, options, logger: commandLogger });
1242
- } else if (command === 'agent:audit' || command === 'agent-audit') {
1243
- result = await runAgentAudit({ args, options, logger: commandLogger });
1244
- } else if (command === 'skill:audit' || command === 'skill-audit') {
1245
- result = await runSkillAudit({ args, options, logger: commandLogger });
1246
- } else if (command === 'quality:audit' || command === 'quality-audit') {
1247
- result = await runQualityAudit({ args, options, logger: commandLogger });
1248
- } else if (command === 'brief:gen' || command === 'brief-gen') {
1249
- result = await runBriefGen({ args, options, logger: commandLogger, t });
1251
+ } else if (command === 'agent:audit' || command === 'agent-audit') {
1252
+ result = await runAgentAudit({ args, options, logger: commandLogger });
1253
+ } else if (command === 'skill:audit' || command === 'skill-audit') {
1254
+ result = await runSkillAudit({ args, options, logger: commandLogger });
1255
+ } else if (command === 'quality:audit' || command === 'quality-audit') {
1256
+ result = await runQualityAudit({ args, options, logger: commandLogger });
1257
+ } else if (command === 'brief:gen' || command === 'brief-gen') {
1258
+ result = await runBriefGen({ args, options, logger: commandLogger, t });
1250
1259
  } else if (command === 'harness:init' || command === 'harness-init') {
1251
1260
  result = await runHarnessInit({ args, options, logger: commandLogger, t });
1252
1261
  } else if (command === 'harness:validate' || command === 'harness-validate') {
1253
1262
  result = await runHarnessValidate({ args, options, logger: commandLogger, t });
1254
1263
  } else if (command === 'harness:apply-validation' || command === 'harness-apply-validation') {
1255
1264
  result = await runHarnessApplyValidation({ args, options, logger: commandLogger, t });
1265
+ } else if (command === 'harness:approve' || command === 'harness-approve') {
1266
+ const { runHarnessApprove } = require('./commands/harness-gate');
1267
+ result = await runHarnessApprove({ args, options, logger: commandLogger, t });
1268
+ } else if (command === 'harness:reject' || command === 'harness-reject') {
1269
+ const { runHarnessReject } = require('./commands/harness-gate');
1270
+ result = await runHarnessReject({ args, options, logger: commandLogger, t });
1271
+ } else if (command === 'harness:status' || command === 'harness-status') {
1272
+ const { runHarnessStatus } = require('./commands/harness-status');
1273
+ result = await runHarnessStatus({ args, options, logger: commandLogger, t });
1256
1274
  } else if (command === 'verify:gate' || command === 'verify-gate') {
1257
1275
  result = await runVerifyGate({ args, options, logger: commandLogger, t });
1258
1276
 
1259
- } else if (command === 'brief:validate' || command === 'brief-validate') {
1260
- result = await runBriefValidate({ args, options, logger: commandLogger });
1261
- } else if (command === 'intake:ask' || command === 'intake-ask') {
1262
- result = await runIntakeAsk({ args, options, logger: commandLogger });
1263
- } else if (command === 'preflight:context' || command === 'preflight-context') {
1264
- result = await runPreflightContext({ args, options, logger: commandLogger });
1277
+ } else if (command === 'brief:validate' || command === 'brief-validate') {
1278
+ result = await runBriefValidate({ args, options, logger: commandLogger });
1279
+ } else if (command === 'intake:ask' || command === 'intake-ask') {
1280
+ result = await runIntakeAsk({ args, options, logger: commandLogger });
1281
+ } else if (command === 'preflight:context' || command === 'preflight-context') {
1282
+ result = await runPreflightContext({ args, options, logger: commandLogger });
1265
1283
  } else if (command === 'context:compact' || command === 'context-compact') {
1266
1284
  result = await runContextCompact({ args, options, logger: commandLogger });
1267
1285
  } else if (command === 'squad:scaffold' || command === 'squad-scaffold') {
@@ -1297,9 +1315,9 @@ async function main() {
1297
1315
  result = await runSpecCheckpoint({ args, options, logger: commandLogger });
1298
1316
  } else if (command === 'spec:tasks' || command === 'spec-tasks') {
1299
1317
  result = await runSpecTasks({ args, options, logger: commandLogger });
1300
- } else if (command.startsWith('learning:') || command === 'learning') {
1301
- const sub = command === 'learning' ? (options.sub || args[1] || 'list') : command.split(':')[1];
1302
- result = await runLearning({ args, options: { ...options, sub }, logger: commandLogger, t });
1318
+ } else if (command.startsWith('learning:') || command === 'learning') {
1319
+ const sub = command === 'learning' ? (options.sub || args[1] || 'list') : command.split(':')[1];
1320
+ result = await runLearning({ args, options: { ...options, sub }, logger: commandLogger, t });
1303
1321
  } else if (command.startsWith('plan:') || command === 'plan') {
1304
1322
  const sub = command === 'plan' ? (args[1] || 'show') : command.split(':')[1];
1305
1323
  result = await runImplementationPlan({ args, options: { ...options, sub }, logger: commandLogger, t });
@@ -1523,6 +1541,8 @@ async function main() {
1523
1541
  }
1524
1542
  } else if (command === 'feature:sweep' || command === 'feature-sweep') {
1525
1543
  result = await runFeatureSweep({ args, options, logger: commandLogger });
1544
+ } else if (command === 'feature:export' || command === 'feature-export') {
1545
+ result = await runFeatureExport({ args, options, logger: commandLogger });
1526
1546
  } else if (command === 'dossier:init' || command === 'dossier-init') {
1527
1547
  result = await runDossierInit({ args, options, logger: commandLogger });
1528
1548
  } else if (command === 'dossier:show' || command === 'dossier-show') {
@@ -14,134 +14,13 @@
14
14
  * Format: YAML frontmatter (briefings: array) + Markdown table
15
15
  */
16
16
 
17
- const fs = require('node:fs/promises');
18
17
  const path = require('node:path');
19
18
  const readline = require('node:readline');
20
-
21
- // ─── Config path ──────────────────────────────────────────────────────────────
22
-
23
- function configPath(projectDir) {
24
- return path.join(projectDir, '.aioson', 'briefings', 'config.md');
25
- }
26
-
27
- // ─── YAML frontmatter parser (briefings-specific) ────────────────────────────
28
-
29
- /**
30
- * Parse .aioson/briefings/config.md frontmatter.
31
- * Returns { updated_at, briefings: [...] }
32
- *
33
- * Expected format:
34
- * ---
35
- * updated_at: 2026-04-10
36
- * briefings:
37
- * - slug: foo
38
- * status: draft
39
- * source_plans: [plans/x.md]
40
- * created_at: "2026-04-10"
41
- * approved_at: null
42
- * prd_generated: null
43
- * ---
44
- */
45
- function parseConfigFrontmatter(content) {
46
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
47
- if (!match) return null;
48
-
49
- const raw = match[1];
50
- const lines = raw.split(/\r?\n/);
51
-
52
- const result = { updated_at: null, briefings: [] };
53
- let inBriefings = false;
54
- let currentItem = null;
55
-
56
- for (const line of lines) {
57
- if (/^updated_at:\s*(.*)$/.test(line)) {
58
- result.updated_at = line.replace(/^updated_at:\s*/, '').trim().replace(/^["']|["']$/g, '');
59
- continue;
60
- }
61
-
62
- if (/^briefings:/.test(line)) {
63
- inBriefings = true;
64
- continue;
65
- }
66
-
67
- if (!inBriefings) continue;
68
-
69
- // New item in array
70
- if (/^\s{2}-\s+slug:\s*(.+)/.test(line)) {
71
- if (currentItem) result.briefings.push(currentItem);
72
- const slug = line.replace(/^\s{2}-\s+slug:\s*/, '').trim().replace(/^["']|["']$/g, '');
73
- currentItem = { slug, status: 'draft', source_plans: [], created_at: null, approved_at: null, prd_generated: null };
74
- continue;
75
- }
76
-
77
- if (currentItem) {
78
- const fieldMatch = line.match(/^\s{4}(\w+):\s*(.*)/);
79
- if (!fieldMatch) continue;
80
- const [, key, rawVal] = fieldMatch;
81
- const val = rawVal.trim().replace(/^["']|["']$/g, '');
82
-
83
- if (key === 'source_plans') {
84
- // Parse inline array: [plans/x.md, plans/y.md]
85
- const arrMatch = val.match(/^\[(.+)\]$/);
86
- currentItem.source_plans = arrMatch
87
- ? arrMatch[1].split(',').map((s) => s.trim().replace(/^["']|["']$/g, ''))
88
- : val === '' ? [] : [val];
89
- } else if (val === 'null' || val === '') {
90
- currentItem[key] = null;
91
- } else {
92
- currentItem[key] = val;
93
- }
94
- }
95
- }
96
-
97
- if (currentItem) result.briefings.push(currentItem);
98
- return result;
99
- }
100
-
101
- // ─── Config serializer ────────────────────────────────────────────────────────
102
-
103
- function serializeSourcePlans(plans) {
104
- if (!plans || plans.length === 0) return '[]';
105
- const items = plans.map((p) => `"${p}"`).join(', ');
106
- return `[${items}]`;
107
- }
108
-
109
- function serializeConfigFrontmatter(data) {
110
- const lines = ['---', `updated_at: ${data.updated_at || new Date().toISOString().slice(0, 10)}`, 'briefings:'];
111
-
112
- for (const b of data.briefings) {
113
- lines.push(` - slug: ${b.slug}`);
114
- lines.push(` status: ${b.status}`);
115
- lines.push(` source_plans: ${serializeSourcePlans(b.source_plans)}`);
116
- lines.push(` created_at: "${b.created_at || ''}"`);
117
- lines.push(` approved_at: ${b.approved_at ? `"${b.approved_at}"` : 'null'}`);
118
- lines.push(` prd_generated: ${b.prd_generated ? `"${b.prd_generated}"` : 'null'}`);
119
- }
120
-
121
- lines.push('---');
122
- return lines.join('\n');
123
- }
124
-
125
- // ─── Markdown table builder ───────────────────────────────────────────────────
126
-
127
- function buildMarkdownTable(briefings) {
128
- const header = '| slug | status | source_plans | created | approved | prd |';
129
- const sep = '|------|--------|-------------|---------|----------|-----|';
130
- const rows = briefings.map((b) => {
131
- const sources = (b.source_plans || []).join(', ') || '—';
132
- return `| ${b.slug} | ${b.status} | ${sources} | ${b.created_at || '—'} | ${b.approved_at || '—'} | ${b.prd_generated || '—'} |`;
133
- });
134
- return [header, sep, ...rows].join('\n');
135
- }
136
-
137
- // ─── Config writer ────────────────────────────────────────────────────────────
138
-
139
- async function writeConfig(configFile, data) {
140
- const frontmatter = serializeConfigFrontmatter(data);
141
- const table = buildMarkdownTable(data.briefings);
142
- const body = `\n# Briefings Registry\n\n${table}\n`;
143
- await fs.writeFile(configFile, `${frontmatter}\n${body}`, 'utf8');
144
- }
19
+ const {
20
+ configPath: registryConfigPath,
21
+ readBriefingRegistry,
22
+ writeBriefingRegistry
23
+ } = require('../lib/briefing-refiner/briefing-registry');
145
24
 
146
25
  // ─── Interactive prompt helpers ───────────────────────────────────────────────
147
26
 
@@ -205,23 +84,21 @@ function promptCheckboxDeselect(items, promptText) {
205
84
  async function runBriefingApprove({ args, options = {}, logger }) {
206
85
  const projectDir = path.resolve(process.cwd(), args[0] || '.');
207
86
  const slugOpt = String(options.slug || '').trim() || null;
208
- const configFile = configPath(projectDir);
87
+ const configFile = registryConfigPath(projectDir);
209
88
 
210
89
  // ── Read config ────────────────────────────────────────────────────────────
211
- let raw;
90
+ let data;
212
91
  try {
213
- raw = await fs.readFile(configFile, 'utf8');
214
- } catch {
92
+ data = await readBriefingRegistry(projectDir);
93
+ } catch (error) {
94
+ if (error && error.code === 'invalid_frontmatter') {
95
+ logger.error('config.md com frontmatter inválido. Verifique o arquivo manualmente.');
96
+ return { ok: false, error: 'invalid_frontmatter' };
97
+ }
215
98
  logger.error('Nenhum briefing encontrado. Ative @briefing para criar o primeiro briefing.');
216
99
  return { ok: false, error: 'no_config' };
217
100
  }
218
101
 
219
- const data = parseConfigFrontmatter(raw);
220
- if (!data) {
221
- logger.error('config.md com frontmatter inválido. Verifique o arquivo manualmente.');
222
- return { ok: false, error: 'invalid_frontmatter' };
223
- }
224
-
225
102
  const drafts = data.briefings.filter((b) => b.status === 'draft');
226
103
 
227
104
  if (drafts.length === 0) {
@@ -259,7 +136,7 @@ async function runBriefingApprove({ args, options = {}, logger }) {
259
136
  briefingEntry.approved_at = today;
260
137
  data.updated_at = today;
261
138
 
262
- await writeConfig(configFile, data);
139
+ await writeBriefingRegistry(projectDir, data);
263
140
 
264
141
  logger.log(`✓ Briefing "${target.slug}" aprovado.`);
265
142
  logger.log(' Ative @product para gerar o PRD — ele detectará o briefing aprovado automaticamente.');
@@ -272,25 +149,26 @@ async function runBriefingApprove({ args, options = {}, logger }) {
272
149
  async function runBriefingUnapprove({ args, options = {}, logger }) {
273
150
  const projectDir = path.resolve(process.cwd(), args[0] || '.');
274
151
  const slugOpt = String(options.slug || '').trim() || null;
275
- const configFile = configPath(projectDir);
152
+ const configFile = registryConfigPath(projectDir);
276
153
 
277
154
  // ── Read config ────────────────────────────────────────────────────────────
278
- let raw;
155
+ let data;
279
156
  try {
280
- raw = await fs.readFile(configFile, 'utf8');
281
- } catch {
157
+ data = await readBriefingRegistry(projectDir);
158
+ } catch (error) {
159
+ if (error && error.code === 'invalid_frontmatter') {
160
+ logger.error('config.md com frontmatter inválido. Verifique o arquivo manualmente.');
161
+ return { ok: false, error: 'invalid_frontmatter' };
162
+ }
282
163
  logger.error('Nenhum briefing encontrado. Ative @briefing para criar o primeiro briefing.');
283
164
  return { ok: false, error: 'no_config' };
284
165
  }
285
166
 
286
- const data = parseConfigFrontmatter(raw);
287
- if (!data) {
288
- logger.error('config.md com frontmatter inválido. Verifique o arquivo manualmente.');
289
- return { ok: false, error: 'invalid_frontmatter' };
290
- }
291
-
292
- // Only approved and non-implemented briefings can be unapproved
293
- const approveds = data.briefings.filter((b) => b.status === 'approved');
167
+ // Only approved briefings that have NOT yet generated a PRD can be unapproved.
168
+ // Reverting a prd_generated briefing would desync it from its downstream PRD,
169
+ // so it is excluded here (mirrors the registry-level guard in
170
+ // returnApprovedBriefingToDraft).
171
+ const approveds = data.briefings.filter((b) => b.status === 'approved' && !b.prd_generated);
294
172
 
295
173
  if (approveds.length === 0) {
296
174
  logger.log('Nenhum briefing aprovado disponível para retornar a draft.');
@@ -333,7 +211,7 @@ async function runBriefingUnapprove({ args, options = {}, logger }) {
333
211
  }
334
212
  data.updated_at = today;
335
213
 
336
- await writeConfig(configFile, data);
214
+ await writeBriefingRegistry(projectDir, data);
337
215
 
338
216
  const names = targets.map((b) => b.slug);
339
217
  logger.log(`✓ ${names.length === 1 ? `Briefing "${names[0]}" retornado` : `Briefings retornados`} para draft: ${names.join(', ')}`);
@@ -33,7 +33,10 @@ function resolveGitRoot(projectDir) {
33
33
  }
34
34
 
35
35
  function parseGitStatusShort(gitRoot) {
36
- const output = runGit(gitRoot, ['status', '--short']);
36
+ // core.quotePath=false makes git emit non-ASCII paths literally (UTF-8) instead
37
+ // of C-style double-quoted/escaped form, so the parsed path is the real path and
38
+ // `git add -- <path>` later targets the correct file.
39
+ const output = runGit(gitRoot, ['-c', 'core.quotePath=false', 'status', '--short']);
37
40
  const lines = output.split('\n').filter(Boolean);
38
41
  const staged = [];
39
42
  const unstaged = [];
@@ -223,7 +226,7 @@ async function resolveGuardFindings(gitRoot, guardResult, logger) {
223
226
  logger.log(` ✔ Adicionado a contentAllowPaths: ${finding.path}`);
224
227
  }
225
228
  } else if (action === 'block') {
226
- const pattern = `${finding.path.includes('**') ? finding.path : finding.path}`;
229
+ const pattern = finding.path;
227
230
  if (!guardConfig.blockPaths.includes(pattern)) {
228
231
  guardConfig.blockPaths.push(pattern);
229
232
  guardConfigChanged = true;
@@ -131,6 +131,42 @@ async function findArchivedFiles(archiveDir) {
131
131
  return entries.filter((e) => e.isFile()).map((e) => e.name);
132
132
  }
133
133
 
134
+ /**
135
+ * Enumerate every artefact that belongs to a feature slug — the exact surface
136
+ * `feature:archive` would move — but as a pure read-only discovery, for
137
+ * non-destructive consumers (e.g. `feature:export`). Never mutates the tree.
138
+ *
139
+ * Reuses the slug-collision guard (readOtherSlugs/findSlugFiles) so a longer
140
+ * sibling slug (`checkout-v2`) never leaks into `checkout`.
141
+ *
142
+ * @returns {{ rootFiles: string[], dirs: Array<{label:string, sourceDir:string}>, doneDir: string|null }}
143
+ * rootFiles are bare names under `.aioson/context/`; dirs/doneDir are absolute paths.
144
+ */
145
+ async function collectFeatureArtifacts({ ctxDir, targetDir, slug, includeDone = true }) {
146
+ const featuresPath = path.join(ctxDir, 'features.md');
147
+ const otherSlugs = await readOtherSlugs(featuresPath, slug);
148
+ const rootFiles = await findSlugFiles(ctxDir, slug, otherSlugs);
149
+
150
+ const slugDirCandidates = [
151
+ { label: 'dossier', sourceDir: path.join(ctxDir, 'features', slug) },
152
+ { label: 'plans', sourceDir: path.join(targetDir, '.aioson', 'plans', slug) },
153
+ { label: 'briefings', sourceDir: path.join(targetDir, '.aioson', 'briefings', slug) }
154
+ ];
155
+ const dirs = [];
156
+ for (const d of slugDirCandidates) {
157
+ // eslint-disable-next-line no-await-in-loop
158
+ if (await dirExists(d.sourceDir)) dirs.push(d);
159
+ }
160
+
161
+ let doneDir = null;
162
+ if (includeDone) {
163
+ const candidate = path.join(ctxDir, 'done', slug);
164
+ if (await dirExists(candidate)) doneDir = candidate;
165
+ }
166
+
167
+ return { rootFiles, dirs, doneDir };
168
+ }
169
+
134
170
  async function extractSummary(prdPath) {
135
171
  const content = await readFileSafe(prdPath);
136
172
  if (!content) return null;
@@ -338,18 +374,18 @@ async function runFeatureArchive({ args = [], options = {}, logger }) {
338
374
  action: d.action,
339
375
  reason: d.reason
340
376
  }));
341
- const result = {
342
- ok: true,
343
- dryRun: true,
344
- slug,
377
+ const result = {
378
+ ok: true,
379
+ dryRun: true,
380
+ slug,
345
381
  targetDir: path.relative(targetDir, archiveDir),
346
- move: toMove,
347
- skip: toSkip,
348
- dirs,
349
- dossier: dirs.find((d) => d.label === 'dossier') || null,
350
- manifestEntry: {
351
- slug,
352
- completed,
382
+ move: toMove,
383
+ skip: toSkip,
384
+ dirs,
385
+ dossier: dirs.find((d) => d.label === 'dossier') || null,
386
+ manifestEntry: {
387
+ slug,
388
+ completed,
353
389
  files: String(toMove.length + alreadyArchived.length),
354
390
  summary: summary || '—'
355
391
  }
@@ -628,4 +664,4 @@ async function runFeatureSweep({ args = [], options = {}, logger }) {
628
664
  return result;
629
665
  }
630
666
 
631
- module.exports = { runFeatureArchive, runFeatureSweep };
667
+ module.exports = { runFeatureArchive, runFeatureSweep, collectFeatureArtifacts };
@@ -333,6 +333,46 @@ async function runFeatureClose({ args, options = {}, logger }) {
333
333
  const contractContent = await readFileSafe(contractPath);
334
334
  const progressContent = await readFileSafe(progressPath);
335
335
 
336
+ // REQ-13 (loop-guardrails): tema `publish` é gate de COMANDO — intercepta
337
+ // o feature:close quando o contrato ativo o exige e não há gate publish
338
+ // aprovado. Nunca detectado por diff. `--force` NÃO bypassa: a aprovação
339
+ // humana é o propósito do gate (decisão registrada no spec da feature).
340
+ if (contractContent) {
341
+ try {
342
+ const contract = JSON.parse(contractContent);
343
+ const requiredFor = contract && contract.human_gate && Array.isArray(contract.human_gate.required_for)
344
+ ? contract.human_gate.required_for
345
+ : [];
346
+ if (requiredFor.includes('publish')) {
347
+ const { hasApprovedPublishGate, pendingGates, createGate } = require('../harness/human-gate');
348
+ const { emitGuardEvent } = require('../harness/guard-events');
349
+ if (!hasApprovedPublishGate(planDir)) {
350
+ let gate = pendingGates(planDir).find((g) => g.theme === 'publish');
351
+ if (!gate) {
352
+ gate = createGate(planDir, {
353
+ theme: 'publish',
354
+ attempt: 0,
355
+ triggeredBy: [],
356
+ diffSummary: `feature:close ${slug}`,
357
+ runId: null
358
+ });
359
+ await emitGuardEvent(targetDir, {
360
+ eventType: 'human_gate_requested',
361
+ message: `publish gate ${gate.id} requested by feature:close`,
362
+ payload: { slug, gate_id: gate.id, theme: 'publish' }
363
+ });
364
+ }
365
+ const errMsg = `[Publish Gate BLOCKED] Feature "${slug}" requires human approval before closing (human_gate.required_for includes "publish"). Approve with: aioson harness:approve . --slug=${slug} --gate=${gate.id}`;
366
+ if (options.json) {
367
+ return { ok: false, reason: 'publish_gate_pending', feature: slug, gate: gate.id, error: errMsg };
368
+ }
369
+ logger.log(errMsg);
370
+ return { ok: false };
371
+ }
372
+ }
373
+ } catch { /* contrato ilegível — o done gate abaixo lida com o estado */ }
374
+ }
375
+
336
376
  if (contractContent && progressContent) {
337
377
  const force = options.force === true;
338
378
  let progress = null;