@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
@@ -1569,58 +1569,63 @@ async function runLiveStart({ args, options = {}, logger, t }) {
1569
1569
  }
1570
1570
  }
1571
1571
 
1572
- async function runRuntimeEmit({ args, options = {}, logger, t }) {
1573
- const targetDir = resolveTargetDir(args);
1574
- const agentName = normalizeAgentHandle(requireOption(options, 'agent', t));
1575
- const eventType = String(options.type || 'note').trim() || 'note';
1576
- const now = new Date().toISOString();
1577
- const refs = parseRefs(options.refs);
1578
- const planStep = options['plan-step'] ? String(options['plan-step']).trim() : null;
1579
- const summary = truncateMessage(
1580
- options.summary || options.message || options.title || `${eventType} emitted by ${agentName}`
1581
- );
1582
- const meta = parseJsonOption(options.meta);
1583
- const payload = meta && typeof meta === 'object' ? { ...meta } : {};
1584
- if (refs.length > 0) payload.refs = refs;
1585
- if (planStep) payload.plan_step = planStep;
1586
-
1587
- let liveHandle;
1588
- if (await runtimeStoreExists(targetDir)) {
1589
- try {
1590
- liveHandle = await requireActiveLiveContext(targetDir, agentName, t, {
1591
- limit: options.limit
1592
- });
1593
- } catch (err) {
1594
- const noActive = t('live.no_active_session', { agent: agentName });
1595
- const notActive = t('live.session_not_active', { agent: agentName });
1596
- if (err && !(err.message === noActive || err.message === notActive)) {
1597
- throw err;
1598
- }
1599
- }
1600
- }
1601
-
1602
- if (!liveHandle) {
1603
- const standalone = await emitStandaloneRuntimeEvent({
1604
- targetDir,
1605
- agentName,
1606
- eventType,
1607
- summary,
1608
- payload,
1609
- options,
1610
- now
1611
- });
1612
- if (!options.json) {
1613
- logger.log(`runtime:emit — ${agentName} | standalone event logged | run: ${standalone.runKey} (${standalone.dbPath})`);
1614
- }
1615
- return standalone;
1616
- }
1617
-
1618
- const { db, dbPath, runtimeDir, context } = liveHandle;
1619
-
1620
- try {
1621
- const state = context.state || createLiveState(targetDir, context.run, context.task, {
1622
- sessionKey: context.sessionKey,
1623
- activeAgent: context.agentName,
1572
+ async function runRuntimeEmit({ args, options = {}, logger, t }) {
1573
+ const targetDir = resolveTargetDir(args);
1574
+ const agentName = normalizeAgentHandle(requireOption(options, 'agent', t));
1575
+ const eventType = String(options.type || 'note').trim() || 'note';
1576
+ const now = new Date().toISOString();
1577
+ const refs = parseRefs(options.refs);
1578
+ const planStep = options['plan-step'] ? String(options['plan-step']).trim() : null;
1579
+ const summary = truncateMessage(
1580
+ options.summary || options.message || options.title || `${eventType} emitted by ${agentName}`
1581
+ );
1582
+ const meta = parseJsonOption(options.meta);
1583
+ const payload = meta && typeof meta === 'object' ? { ...meta } : {};
1584
+ if (refs.length > 0) payload.refs = refs;
1585
+ if (planStep) payload.plan_step = planStep;
1586
+
1587
+ let liveHandle;
1588
+ if (await runtimeStoreExists(targetDir)) {
1589
+ try {
1590
+ liveHandle = await requireActiveLiveContext(targetDir, agentName, t, {
1591
+ limit: options.limit
1592
+ });
1593
+ } catch (err) {
1594
+ const noActive = t('live.no_active_session', { agent: agentName });
1595
+ const notActive = t('live.session_not_active', { agent: agentName });
1596
+ if (err && !(err.message === noActive || err.message === notActive)) {
1597
+ throw err;
1598
+ }
1599
+ }
1600
+ }
1601
+
1602
+ if (!liveHandle) {
1603
+ const standalone = await emitStandaloneRuntimeEvent({
1604
+ targetDir,
1605
+ agentName,
1606
+ eventType,
1607
+ summary,
1608
+ payload,
1609
+ options,
1610
+ now
1611
+ });
1612
+ if (!options.json) {
1613
+ logger.log(t('live.standalone_event_recorded', {
1614
+ agent: agentName,
1615
+ eventType,
1616
+ runKey: standalone.runKey,
1617
+ dbPath: standalone.dbPath
1618
+ }));
1619
+ }
1620
+ return standalone;
1621
+ }
1622
+
1623
+ const { db, dbPath, runtimeDir, context } = liveHandle;
1624
+
1625
+ try {
1626
+ const state = context.state || createLiveState(targetDir, context.run, context.task, {
1627
+ sessionKey: context.sessionKey,
1628
+ activeAgent: context.agentName,
1624
1629
  projectPath: targetDir
1625
1630
  });
1626
1631
 
@@ -1758,69 +1763,69 @@ async function runRuntimeEmit({ args, options = {}, logger, t }) {
1758
1763
  };
1759
1764
  } finally {
1760
1765
  db.close();
1761
- }
1762
- }
1763
-
1764
- async function emitStandaloneRuntimeEvent({
1765
- targetDir,
1766
- agentName,
1767
- eventType,
1768
- summary,
1769
- payload,
1770
- options = {},
1771
- now
1772
- }) {
1773
- const { db, dbPath } = await openRuntimeDb(targetDir);
1774
- try {
1775
- const taskKey = startTask(db, {
1776
- title: options.title ? String(options.title).trim() : `runtime:emit ${agentName}`,
1777
- goal: summary,
1778
- status: 'completed',
1779
- createdBy: agentName,
1780
- taskKind: 'runtime_event',
1781
- metaJson: {
1782
- mode: 'standalone',
1783
- reason: 'no_active_live_session'
1784
- }
1785
- });
1786
- const runKey = startRun(db, {
1787
- taskKey,
1788
- agentName,
1789
- agentKind: 'official',
1790
- source: 'direct',
1791
- title: options.title ? String(options.title).trim() : `runtime:emit ${agentName}`,
1792
- status: 'completed',
1793
- summary,
1794
- eventType,
1795
- phase: 'direct',
1796
- message: summary,
1797
- payload: Object.keys(payload).length > 0 ? {
1798
- ...payload,
1799
- standalone: true,
1800
- reason: 'no_active_live_session'
1801
- } : {
1802
- standalone: true,
1803
- reason: 'no_active_live_session'
1804
- }
1805
- });
1806
-
1807
- return {
1808
- ok: true,
1809
- targetDir,
1810
- dbPath,
1811
- agent: agentName,
1812
- eventType,
1813
- sessionKey: null,
1814
- runKey,
1815
- taskKey,
1816
- currentTask: null,
1817
- open: false,
1818
- standalone: true
1819
- };
1820
- } finally {
1821
- db.close();
1822
- }
1823
- }
1766
+ }
1767
+ }
1768
+
1769
+ async function emitStandaloneRuntimeEvent({
1770
+ targetDir,
1771
+ agentName,
1772
+ eventType,
1773
+ summary,
1774
+ payload,
1775
+ options = {},
1776
+ now
1777
+ }) {
1778
+ const { db, dbPath } = await openRuntimeDb(targetDir);
1779
+ try {
1780
+ const taskKey = startTask(db, {
1781
+ title: options.title ? String(options.title).trim() : `runtime:emit ${agentName}`,
1782
+ goal: summary,
1783
+ status: 'completed',
1784
+ createdBy: agentName,
1785
+ taskKind: 'runtime_event',
1786
+ metaJson: {
1787
+ mode: 'standalone',
1788
+ reason: 'no_active_live_session'
1789
+ }
1790
+ });
1791
+ const runKey = startRun(db, {
1792
+ taskKey,
1793
+ agentName,
1794
+ agentKind: 'official',
1795
+ source: 'direct',
1796
+ title: options.title ? String(options.title).trim() : `runtime:emit ${agentName}`,
1797
+ status: 'completed',
1798
+ summary,
1799
+ eventType,
1800
+ phase: 'direct',
1801
+ message: summary,
1802
+ payload: Object.keys(payload).length > 0 ? {
1803
+ ...payload,
1804
+ standalone: true,
1805
+ reason: 'no_active_live_session'
1806
+ } : {
1807
+ standalone: true,
1808
+ reason: 'no_active_live_session'
1809
+ }
1810
+ });
1811
+
1812
+ return {
1813
+ ok: true,
1814
+ targetDir,
1815
+ dbPath,
1816
+ agent: agentName,
1817
+ eventType,
1818
+ sessionKey: null,
1819
+ runKey,
1820
+ taskKey,
1821
+ currentTask: null,
1822
+ open: false,
1823
+ standalone: true
1824
+ };
1825
+ } finally {
1826
+ db.close();
1827
+ }
1828
+ }
1824
1829
 
1825
1830
 
1826
1831
  async function runLiveHandoff({ args, options = {}, logger, t }) {
@@ -528,7 +528,8 @@ function buildChecks(context, state, prerequisites, workersOption, force, analys
528
528
  }
529
529
 
530
530
  async function applyParallelFixes(targetDir, context, state, options) {
531
- const dryRun = Boolean(options.dryRun);
531
+ // accept both --dry-run (kebab, as the parser stores it) and --dryRun (camel)
532
+ const dryRun = Boolean(options.dryRun || options['dry-run']);
532
533
  const generatedAt = new Date().toISOString();
533
534
  const projectName =
534
535
  String((context.data && context.data.project_name) || '').trim() || path.basename(targetDir) || 'project';
@@ -45,9 +45,9 @@ async function runPulseUpdate({ args, options = {}, logger }) {
45
45
  // Extract existing recent_activity lines (keep last 2 to add 1 new = 3 total)
46
46
  const existingActivities = [];
47
47
  if (existing) {
48
- const activityMatch = existing.match(/## Recent Activity\n([\s\S]*?)(?=\n##|\s*$)/);
48
+ const activityMatch = existing.match(/## Recent Activity\r?\n([\s\S]*?)(?=\r?\n##|\s*$)/);
49
49
  if (activityMatch) {
50
- const lines = activityMatch[1].split('\n').filter((l) => l.trim().startsWith('-'));
50
+ const lines = activityMatch[1].split(/\r?\n/).filter((l) => l.trim().startsWith('-'));
51
51
  existingActivities.push(...lines.slice(-2));
52
52
  }
53
53
  }
@@ -355,7 +355,13 @@ async function callOpenAICompatible(baseUrl, apiKey, model, prompt) {
355
355
  }
356
356
 
357
357
  const data = JSON.parse(text);
358
- return data.choices[0].message.content;
358
+ const content = data && data.choices && data.choices[0] && data.choices[0].message
359
+ ? data.choices[0].message.content
360
+ : undefined;
361
+ if (typeof content !== 'string') {
362
+ throw new Error(`Unexpected LLM response shape: ${String(text).slice(0, 300)}`);
363
+ }
364
+ return content;
359
365
  }
360
366
 
361
367
  async function callAnthropic(apiKey, model, prompt) {
@@ -365,7 +371,11 @@ async function callAnthropic(apiKey, model, prompt) {
365
371
  { model, max_tokens: 4096, messages: [{ role: 'user', content: prompt }] }
366
372
  );
367
373
  const data = JSON.parse(text);
368
- return data.content[0].text;
374
+ const content = data && data.content && data.content[0] ? data.content[0].text : undefined;
375
+ if (typeof content !== 'string') {
376
+ throw new Error(`Unexpected Anthropic response shape: ${String(text).slice(0, 300)}`);
377
+ }
378
+ return content;
369
379
  }
370
380
 
371
381
  async function callLLM(providerName, providerCfg, prompt) {
@@ -26,6 +26,15 @@ const fsp = require('node:fs/promises');
26
26
  const bus = require('../squad/intra-bus');
27
27
  const stateManager = require('../squad/state-manager');
28
28
  const { createCircuitBreaker } = require('../harness/circuit-breaker');
29
+ const { validateContract, resolveContract } = require('../harness/contract-schema');
30
+ const { captureBaseline, computeChangedSet, captureDiffPatch } = require('../harness/git-baseline');
31
+ const { checkScope, checkDiffLimits, buildRollbackFeedback } = require('../harness/scope-guard');
32
+ const { estimateTokens, startRunBudget, recordAttemptTokens, checkBudget, buildBudgetSummary } = require('../harness/budget-guard');
33
+ const { writeAttemptArtifacts } = require('../harness/attempt-artifacts');
34
+ const { emitGuardEvent } = require('../harness/guard-events');
35
+ const { detectGates, createGate, enterHumanGate, resolveGateState, pendingGates, loadGates } = require('../harness/human-gate');
36
+ const { runCriteria, registerFailureSignatures, startRunSignatures } = require('../harness/criteria-runner');
37
+ const { findActiveContract } = require('../harness/active-contract');
29
38
 
30
39
  // ─── Agent execution ─────────────────────────────────────────────────────────
31
40
 
@@ -127,6 +136,195 @@ async function criteriaOnlyVerify(projectDir, artifactPath, criteria) {
127
136
  };
128
137
  }
129
138
 
139
+ // ─── Loop Guardrails (loop-guardrails) ───────────────────────────────────────
140
+
141
+ /**
142
+ * Hook pós-attempt na ordem D5: (1) artifacts → (2) scope guard + re-hash D2 →
143
+ * (3) diff limits → (4) human gates (Fase 2) → (5) criteria (Fase 2) →
144
+ * (6) budget/runtime. Registrar primeiro, julgar depois.
145
+ *
146
+ * Retorna { blocked, reason, feedback, issues } — `blocked` encerra o run;
147
+ * `feedback` injeta instrução de rollback e segue para a próxima iteração.
148
+ */
149
+ async function runPostAttemptGuards({ targetDir, guards, cb, logger, attempt, agentOutput }) {
150
+ const { resolved, planDir } = guards;
151
+ const slug = resolved.feature;
152
+
153
+ // (1) registrar sempre, mesmo em falha
154
+ let changed = { files: [], rehashViolations: [] };
155
+ let diffPatch = '';
156
+ if (guards.baseline) {
157
+ try {
158
+ changed = computeChangedSet(targetDir, guards.baseline);
159
+ diffPatch = captureDiffPatch(targetDir);
160
+ } catch { /* git indisponível neste attempt — artifacts parciais */ }
161
+ }
162
+ writeAttemptArtifacts(planDir, attempt, { changedFiles: changed.files, diffPatch });
163
+
164
+ // tokens da tentativa acumulam sempre — o gasto já ocorreu (D3)
165
+ recordAttemptTokens(cb.progress, estimateTokens(agentOutput));
166
+
167
+ const outcome = { blocked: false, reason: null, feedback: null, issues: [] };
168
+
169
+ // (2) scope guard + re-hash D2 (REQ-4/5/6)
170
+ if (guards.baseline) {
171
+ const scope = checkScope({
172
+ changedFiles: changed.files,
173
+ rehashViolations: changed.rehashViolations,
174
+ allowedGlobs: resolved.allowed_files,
175
+ forbiddenGlobs: resolved.forbidden_files
176
+ });
177
+ if (!scope.ok) {
178
+ guards.scopeViolationCount += 1;
179
+ const fileList = scope.violations.map((v) => v.path);
180
+ logger.log(` ✗ Scope violation (${fileList.length} file(s)): ${fileList.slice(0, 5).join(', ')}`);
181
+ await emitGuardEvent(targetDir, {
182
+ eventType: 'scope_violation',
183
+ message: `scope violation on attempt ${attempt}`,
184
+ payload: { slug, attempt, violations: scope.violations }
185
+ });
186
+ if (guards.scopeViolationCount >= 2) {
187
+ // reincidência abre o circuito e escala para humano (REQ-6)
188
+ cb.progress.circuit_state = 'OPEN';
189
+ cb.progress.status = 'circuit_open';
190
+ cb.progress.last_error = `scope_violation_repeat: ${fileList.slice(0, 3).join(', ')}`;
191
+ await cb._save();
192
+ outcome.blocked = true;
193
+ outcome.reason = 'scope_violation_repeat';
194
+ return outcome;
195
+ }
196
+ outcome.reason = 'scope_violation';
197
+ outcome.feedback = buildRollbackFeedback(scope.violations);
198
+ outcome.issues = [{ message: outcome.feedback }];
199
+ }
200
+ }
201
+
202
+ // (3) diff limits (REQ-10) — não julga se a tentativa já vai para rollback
203
+ if (!outcome.feedback) {
204
+ const limits = checkDiffLimits({
205
+ changedFiles: changed.files,
206
+ diffPatch,
207
+ maxChangedFiles: resolved.governor.max_changed_files ?? null,
208
+ maxDiffLines: resolved.governor.max_diff_lines ?? null
209
+ });
210
+ if (!limits.ok) {
211
+ const detail = limits.exceeded.map((e) => `${e.limit}: ${e.actual} > ${e.max}`).join('; ');
212
+ logger.log(` ✗ Diff limit exceeded — ${detail}`);
213
+ await emitGuardEvent(targetDir, {
214
+ eventType: 'diff_limit_exceeded',
215
+ message: `diff limits exceeded on attempt ${attempt} (${detail})`,
216
+ payload: { slug, attempt, exceeded: limits.exceeded }
217
+ });
218
+ outcome.blocked = true;
219
+ outcome.reason = 'diff_limit_exceeded';
220
+ }
221
+ }
222
+
223
+ // (4) human gates (REQ-12, D4) — violação de escopo precede gate: arquivo
224
+ // fora do escopo merece rollback, não aprovação humana
225
+ if (!outcome.feedback && !outcome.blocked && guards.baseline) {
226
+ const detections = detectGates({
227
+ changedFiles: changed.files,
228
+ requiredFor: resolved.human_gate.required_for,
229
+ themePaths: resolved.human_gate.theme_paths,
230
+ existingGates: loadGates(planDir),
231
+ runId: cb.progress.budget ? cb.progress.budget.run_id : null
232
+ });
233
+ if (detections.length > 0) {
234
+ const created = [];
235
+ for (const detection of detections) {
236
+ const gate = createGate(planDir, {
237
+ theme: detection.theme,
238
+ attempt,
239
+ triggeredBy: detection.triggeredBy,
240
+ diffSummary: `${detection.triggeredBy.length} file(s): ${detection.triggeredBy.slice(0, 3).join(', ')}`,
241
+ runId: cb.progress.budget ? cb.progress.budget.run_id : null
242
+ });
243
+ created.push(gate);
244
+ await emitGuardEvent(targetDir, {
245
+ eventType: 'human_gate_requested',
246
+ message: `human gate ${gate.id} requested on attempt ${attempt}`,
247
+ payload: { slug, attempt, gate_id: gate.id, theme: gate.theme, triggered_by: gate.triggered_by }
248
+ });
249
+ }
250
+ enterHumanGate(cb.progress, created.map((g) => g.id));
251
+ await cb._save();
252
+ logger.log(` ✗ Human gate requerido (${created.length}):`);
253
+ for (const gate of created) {
254
+ logger.log(` - ${gate.id} [${gate.theme}] — ${gate.diff_summary}`);
255
+ logger.log(` aioson harness:approve . --slug=${slug} --gate=${gate.id}`);
256
+ }
257
+ outcome.blocked = true;
258
+ outcome.reason = 'human_gate_pending';
259
+ }
260
+ }
261
+
262
+ // (5) criteria checks (REQ-16/17, D7) — pulado se a tentativa já vai para
263
+ // rollback ou gate (processo encerra)
264
+ if (!outcome.feedback && !outcome.blocked) {
265
+ const checks = await runCriteria({ criteria: resolved.criteria, cwd: targetDir });
266
+ if (checks.length > 0) {
267
+ writeAttemptArtifacts(planDir, attempt, { checks });
268
+ const failed = checks.filter((c) => !c.ok);
269
+ for (const check of failed) {
270
+ await emitGuardEvent(targetDir, {
271
+ eventType: 'criteria_check_failed',
272
+ message: `criterion ${check.id} failed on attempt ${attempt} (exit ${check.exitCode}${check.timedOut ? ', timeout' : ''})`,
273
+ payload: { slug, attempt, criterion_id: check.id, exit_code: check.exitCode, timed_out: check.timedOut, signature: check.signature }
274
+ });
275
+ }
276
+ const repeats = registerFailureSignatures(cb.progress, failed);
277
+ if (repeats.length > 0) {
278
+ // mesma assinatura 2x no run (EC-13) → para e escala para humano
279
+ cb.progress.circuit_state = 'OPEN';
280
+ cb.progress.status = 'circuit_open';
281
+ cb.progress.last_error = `failure_signature_repeat: ${repeats.map((r) => r.criterion_id).join(', ')}`;
282
+ await cb._save();
283
+ for (const repeat of repeats) {
284
+ await emitGuardEvent(targetDir, {
285
+ eventType: 'failure_signature_repeat',
286
+ message: `criterion ${repeat.criterion_id} failed twice with the same signature in this run`,
287
+ payload: { slug, attempt, criterion_id: repeat.criterion_id, signature: repeat.signature }
288
+ });
289
+ }
290
+ logger.log(` ✗ Mesma falha 2x no run (${repeats.map((r) => r.criterion_id).join(', ')}) — escalando para humano`);
291
+ outcome.blocked = true;
292
+ outcome.reason = 'failure_signature_repeat';
293
+ } else if (failed.length > 0) {
294
+ logger.log(` ✗ Criteria checks falharam: ${failed.map((c) => c.id).join(', ')}`);
295
+ outcome.reason = 'criteria_check_failed';
296
+ outcome.feedback = failed
297
+ .map((c) => `Criterion ${c.id} failed (exit ${c.exitCode}${c.timedOut ? ', timeout' : ''}): ${(c.stderr || c.stdout || '').split('\n').find((l) => l.trim()) || 'no output'}`)
298
+ .join('\n');
299
+ outcome.issues = [{ message: outcome.feedback }];
300
+ }
301
+ }
302
+ }
303
+
304
+ // (6) budget/runtime (REQ-7/8, EC-11) — sempre avaliado e persistido
305
+ const budget = checkBudget(cb.progress, {
306
+ costCeilingTokens: resolved.governor.cost_ceiling_tokens ?? null,
307
+ maxRuntimeMinutes: resolved.governor.max_runtime_minutes ?? null
308
+ });
309
+ for (const event of budget.events) {
310
+ logger.log(` ${event.type === 'budget_warning' ? '⚠' : '✗'} ${event.message}`);
311
+ await emitGuardEvent(targetDir, {
312
+ eventType: event.type,
313
+ message: event.message,
314
+ payload: { slug, attempt, ...event.payload },
315
+ tokenCount: cb.progress.budget.tokens_estimated
316
+ });
317
+ }
318
+ await cb._save();
319
+ if (budget.pause && !outcome.blocked) {
320
+ logger.log(buildBudgetSummary(cb.progress, { maxIterations: resolved.governor.max_steps }));
321
+ outcome.blocked = true;
322
+ outcome.reason = budget.events.find((e) => e.type !== 'budget_warning')?.type || 'budget_exceeded';
323
+ }
324
+
325
+ return outcome;
326
+ }
327
+
130
328
  // ─── Public API ──────────────────────────────────────────────────────────────
131
329
 
132
330
  /**
@@ -156,19 +354,29 @@ async function runSelfLoop({ args, options = {}, logger }) {
156
354
  if (fs.existsSync(autoPath)) contractPath = autoPath;
157
355
  }
158
356
 
357
+ // C-01 (QA 2026-06-09): sem --contract e sem --spec, descobre o contrato
358
+ // ATIVO em disco (mesma heurística do git:guard). O happy path do PRD e a
359
+ // retomada instruída por harness:approve/budget-guard re-entram sem flags —
360
+ // um contrato ativo nunca pode ficar silenciosamente fora do loop (REQ-1).
361
+ if (!contractPath) {
362
+ try {
363
+ const active = findActiveContract(targetDir);
364
+ if (active) contractPath = active.contractPath;
365
+ } catch { /* best-effort: descoberta nunca derruba o loop */ }
366
+ }
367
+
159
368
  if (contractPath && fs.existsSync(contractPath)) {
160
369
  const progressPath = path.join(path.dirname(contractPath), 'progress.json');
161
370
  cb = createCircuitBreaker(contractPath, progressPath);
162
371
  await cb.load();
163
372
  logger.log(`[Harness] Contract loaded: ${path.relative(targetDir, contractPath)}`);
373
+ } else {
374
+ logger.log('[Harness] guardrails inactive — no harness contract loaded');
164
375
  }
165
376
 
166
- // Set max iterations: Contract policy takes precedence over flag
377
+ // Teto de iterações pela flag; o contrato (governor EFETIVO) sobrescreve no
378
+ // preflight dos guards, após validação de schema (C-02 / REQ-19).
167
379
  let maxIterations = Math.min(Math.max(Number(options['max-iterations'] || 3), 1), 5);
168
- if (cb && cb.contract && cb.contract.governor && cb.contract.governor.max_steps > 0) {
169
- maxIterations = cb.contract.governor.max_steps;
170
- logger.log(`[Harness] Max iterations set by contract: ${maxIterations}`);
171
- }
172
380
 
173
381
  if (!task) {
174
382
  logger.error('Error: --task is required');
@@ -178,6 +386,74 @@ async function runSelfLoop({ args, options = {}, logger }) {
178
386
  const sessionId = randomUUID();
179
387
  const feedbackHistory = [];
180
388
 
389
+ // Loop Guardrails — preflight (REQ-1/2 + D3): valida o schema do contrato,
390
+ // captura o baseline git e zera o orçamento do run. Sem contrato = sem
391
+ // guards (retrocompat REQ-11).
392
+ let guards = null;
393
+ if (cb) {
394
+ const schemaResult = validateContract(cb.contract);
395
+ if (!schemaResult.ok) {
396
+ logger.log(`── Harness Block ──────────────────────────────────────────`);
397
+ for (const err of schemaResult.errors) {
398
+ logger.log(` ✗ contract schema invalid: ${err.field} — ${err.reason}`);
399
+ }
400
+ await emitGuardEvent(targetDir, {
401
+ eventType: 'contract_invalid',
402
+ message: 'harness-contract.json failed schema validation',
403
+ payload: { slug: (cb.contract && cb.contract.feature) || null, errors: schemaResult.errors }
404
+ });
405
+ return { ok: false, iterations: 0, verdict: 'BLOCKED', reason: 'contract_schema_invalid', errors: schemaResult.errors };
406
+ }
407
+ for (const warning of schemaResult.warnings) {
408
+ logger.log(` ⚠ contract: ${warning.field} — ${warning.reason}`);
409
+ }
410
+
411
+ const resolved = resolveContract(cb.contract);
412
+ const planDir = path.dirname(contractPath);
413
+
414
+ // C-02 (REQ-19): o breaker (check/recordError) e o teto de iterações leem
415
+ // `contract.governor` — injeta o governor EFETIVO (presets do contract_mode
416
+ // aplicados) para que `builder`/`autopilot` valham fora de budget/diff.
417
+ cb.contract.governor = resolved.governor;
418
+ if (resolved.governor && resolved.governor.max_steps > 0) {
419
+ maxIterations = resolved.governor.max_steps;
420
+ logger.log(`[Harness] Max iterations set by contract: ${maxIterations}`);
421
+ }
422
+
423
+ // (EC-9) gates pendentes de run anterior são REAPRESENTADOS antes de
424
+ // qualquer detecção nova; aprovação prévia restaura a retomada (REQ-15)
425
+ resolveGateState(cb.progress, planDir);
426
+ const pendingFromBefore = pendingGates(planDir);
427
+ if (pendingFromBefore.length > 0) {
428
+ enterHumanGate(cb.progress, pendingFromBefore.map((g) => g.id));
429
+ await cb._save();
430
+ logger.log(`── Harness Block ──────────────────────────────────────────`);
431
+ logger.log(` ✗ Human gate pendente (${pendingFromBefore.length}):`);
432
+ for (const gate of pendingFromBefore) {
433
+ logger.log(` - ${gate.id} [${gate.theme}] attempt ${gate.attempt} — ${gate.diff_summary || (gate.triggered_by || []).join(', ')}`);
434
+ logger.log(` aioson harness:approve . --slug=${resolved.feature} --gate=${gate.id}`);
435
+ logger.log(` aioson harness:reject . --slug=${resolved.feature} --gate=${gate.id} --reason="..."`);
436
+ }
437
+ return { ok: false, iterations: 0, verdict: 'BLOCKED', reason: 'human_gate_pending', gates: pendingFromBefore.map((g) => g.id) };
438
+ }
439
+
440
+ let baseline = null;
441
+ try {
442
+ const captured = captureBaseline(targetDir, planDir, { forbiddenGlobs: resolved.forbidden_files });
443
+ baseline = captured.baseline;
444
+ for (const warning of captured.warnings) {
445
+ logger.log(` ⚠ baseline: ${warning.path} — ${warning.reason}`);
446
+ }
447
+ } catch (err) {
448
+ logger.log(` ⚠ scope guard inactive for this run: git baseline unavailable (${String(err.message || err).slice(0, 80)})`);
449
+ }
450
+
451
+ startRunBudget(cb.progress, sessionId);
452
+ startRunSignatures(cb.progress); // D7: assinaturas de falha são por run
453
+ await cb._save();
454
+ guards = { resolved, planDir, baseline, scopeViolationCount: 0 };
455
+ }
456
+
181
457
  logger.log(`Self-implement loop: @${agent} — "${task.slice(0, 60)}${task.length > 60 ? '...' : ''}"`);
182
458
  logger.log(`Max iterations: ${maxIterations}`);
183
459
  if (spec) logger.log(`Spec: ${spec}`);
@@ -223,6 +499,30 @@ async function runSelfLoop({ args, options = {}, logger }) {
223
499
  logger.log(' Verifying...');
224
500
  const verifyResult = await runVerification(targetDir, spec, artifact, criteria);
225
501
 
502
+ // Loop Guardrails — hook pós-attempt (ordem D5). Roda ANTES de aceitar o
503
+ // sucesso: violação de escopo em tentativa "verde" ainda bloqueia.
504
+ if (guards) {
505
+ const guardOutcome = await runPostAttemptGuards({
506
+ targetDir,
507
+ guards,
508
+ cb,
509
+ logger,
510
+ attempt: iteration,
511
+ agentOutput: agentResult.output
512
+ });
513
+ if (guardOutcome.blocked) {
514
+ logger.log(`── Harness Block ──────────────────────────────────────────`);
515
+ logger.log(` ✗ Loop paused by guardrail: ${guardOutcome.reason}`);
516
+ return { ok: false, iterations: iteration, verdict: 'BLOCKED', reason: guardOutcome.reason, feedback: feedbackHistory };
517
+ }
518
+ if (guardOutcome.feedback) {
519
+ const guardVerdict = guardOutcome.reason === 'scope_violation' ? 'SCOPE_VIOLATION' : 'CRITERIA_FAILED';
520
+ feedbackHistory.push({ iteration, verdict: guardVerdict, issues: guardOutcome.issues });
521
+ await cb.recordError(guardOutcome.reason);
522
+ continue;
523
+ }
524
+ }
525
+
226
526
  // Record on bus
227
527
  if (squad) {
228
528
  await bus.post(targetDir, squad, sessionId, {