@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/constants.js CHANGED
@@ -35,7 +35,8 @@ const MANAGED_FILES = [
35
35
  '.aioson/agents/committer.md',
36
36
  '.aioson/agents/copywriter.md',
37
37
  '.aioson/agents/briefing.md',
38
- '.aioson/docs/squad/package-contract.md',
38
+ '.aioson/agents/briefing-refiner.md',
39
+ '.aioson/docs/squad/package-contract.md',
39
40
  '.aioson/docs/squad/creation-flow.md',
40
41
  '.aioson/docs/squad/research-loop.md',
41
42
  '.aioson/docs/squad/quality-lens.md',
@@ -438,17 +439,26 @@ const AGENT_DEFINITIONS = [
438
439
  dependsOn: [],
439
440
  output: 'marketing copy + content assets'
440
441
  },
441
- {
442
- id: 'briefing',
443
- displayName: 'Briefing',
444
- description: 'Pre-production briefings and planning',
445
- command: '@briefing',
442
+ {
443
+ id: 'briefing',
444
+ displayName: 'Briefing',
445
+ description: 'Pre-production briefings and planning',
446
+ command: '@briefing',
446
447
  path: '.aioson/agents/briefing.md',
447
- dependsOn: ['.aioson/context/project.context.md'],
448
- output: '.aioson/briefings/{slug}/'
449
- },
450
- {
451
- id: 'profiler-researcher',
448
+ dependsOn: ['.aioson/context/project.context.md'],
449
+ output: '.aioson/briefings/{slug}/'
450
+ },
451
+ {
452
+ id: 'briefing-refiner',
453
+ displayName: 'Briefing Refiner',
454
+ description: 'Interactive refinement of briefing artifacts before Product PRD generation',
455
+ command: '@briefing-refiner',
456
+ path: '.aioson/agents/briefing-refiner.md',
457
+ dependsOn: ['.aioson/context/project.context.md', '.aioson/briefings/config.md'],
458
+ output: '.aioson/briefings/{slug}/review.html + refinement-feedback.json + refinement-report.md'
459
+ },
460
+ {
461
+ id: 'profiler-researcher',
452
462
  displayName: 'Profiler Researcher',
453
463
  description: 'Clone profiler: research phase',
454
464
  command: '@profiler-researcher',
@@ -18,6 +18,9 @@ function openDb(dbPath) {
18
18
  const db = new Database(dbPath);
19
19
  db.pragma('journal_mode = WAL');
20
20
  db.pragma('synchronous = NORMAL');
21
+ // Wait up to 5s for a transient lock (e.g. WAL checkpoint, AV file-lock on
22
+ // Windows) instead of throwing SQLITE_BUSY immediately.
23
+ db.pragma('busy_timeout = 5000');
21
24
  db.exec(`
22
25
  CREATE TABLE IF NOT EXISTS schema_version (
23
26
  version INTEGER NOT NULL
package/src/doctor.js CHANGED
@@ -106,7 +106,7 @@ async function fileContainsAll(filePath, patterns) {
106
106
  }
107
107
  }
108
108
 
109
- const DESIGN_GOVERNANCE_FILES = [
109
+ const DESIGN_GOVERNANCE_FILES = [
110
110
  '.aioson/design-docs/code-reuse.md',
111
111
  '.aioson/design-docs/componentization.md',
112
112
  '.aioson/design-docs/file-size.md',
@@ -114,11 +114,11 @@ const DESIGN_GOVERNANCE_FILES = [
114
114
  '.aioson/design-docs/naming.md'
115
115
  ];
116
116
 
117
- const GATEWAY_FILE_BY_CHECK_ID = {
118
- 'gateway:claude:contract': 'CLAUDE.md',
119
- 'gateway:codex:contract': 'AGENTS.md',
120
- 'gateway:opencode:contract': 'OPENCODE.md'
121
- };
117
+ const GATEWAY_FILE_BY_CHECK_ID = {
118
+ 'gateway:claude:contract': 'CLAUDE.md',
119
+ 'gateway:codex:contract': 'AGENTS.md',
120
+ 'gateway:opencode:contract': 'OPENCODE.md'
121
+ };
122
122
 
123
123
  async function restoreTemplateFiles(targetDir, relPaths, options = {}) {
124
124
  const dryRun = Boolean(options.dryRun);
@@ -175,7 +175,7 @@ async function runDoctor(targetDir) {
175
175
  hintKey: 'doctor.gateway_codex_pointer_hint',
176
176
  patterns: ['.aioson/config.md', '.aioson/agents/']
177
177
  },
178
- {
178
+ {
179
179
  id: 'gateway:opencode:contract',
180
180
  rel: 'OPENCODE.md',
181
181
  key: 'doctor.gateway_opencode_pointer',
@@ -196,7 +196,7 @@ async function runDoctor(targetDir) {
196
196
  });
197
197
  }
198
198
 
199
- const contextPath = path.join(targetDir, '.aioson/context/project.context.md');
199
+ const contextPath = path.join(targetDir, '.aioson/context/project.context.md');
200
200
  checks.push({
201
201
  id: 'context:project',
202
202
  key: 'doctor.context_generated',
@@ -227,6 +227,22 @@ async function runDoctor(targetDir) {
227
227
  }
228
228
  }
229
229
 
230
+ // Autopilot handoff: protocol doc installed but flag never declared in the
231
+ // context frontmatter — autopilot stays silently inactive (absent = manual
232
+ // handoffs). An explicit true/false is a deliberate choice and passes.
233
+ const autopilotDocExists = await exists(path.join(targetDir, '.aioson/docs/autopilot-handoff.md'));
234
+ if (autopilotDocExists && contextValidation.exists && contextValidation.data) {
235
+ const autoHandoffDeclared = Object.prototype.hasOwnProperty.call(contextValidation.data, 'auto_handoff');
236
+ checks.push({
237
+ id: 'context:auto_handoff_declared',
238
+ severity: 'warning',
239
+ key: 'doctor.auto_handoff_declared',
240
+ params: {},
241
+ ok: autoHandoffDeclared,
242
+ hintKey: autoHandoffDeclared ? undefined : 'doctor.auto_handoff_declared_hint'
243
+ });
244
+ }
245
+
230
246
  const major = parseMajor(process.version);
231
247
  checks.push({
232
248
  id: 'node:version',
@@ -5,9 +5,10 @@ const SUPPORTED_SCHEMA_VERSIONS = Object.freeze(new Set(['1.0', '1.1', '1.2']));
5
5
 
6
6
  const CANONICAL_AGENT_IDS = Object.freeze(new Set([
7
7
  'analyst',
8
- 'architect',
9
- 'briefing',
10
- 'committer',
8
+ 'architect',
9
+ 'briefing',
10
+ 'briefing-refiner',
11
+ 'committer',
11
12
  'copywriter',
12
13
  'cypher',
13
14
  'design-hybrid-forge',
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Descoberta do contrato de harness ATIVO (loop-guardrails C-01 / REQ-20).
5
+ *
6
+ * Heurística única, compartilhada entre `git:guard` (camada 2 do scope guard)
7
+ * e `self:loop` (auto-descoberta quando nem --contract nem --spec são
8
+ * passados): varre `.aioson/plans/{slug}/progress.json`, considera candidato quem
9
+ * está `in_progress` ou `human_gate` e tem `harness-contract.json` ao lado,
10
+ * e desempata pelo `last_updated` mais recente.
11
+ *
12
+ * Best-effort por contrato (progress ilegível não é candidato), mas a função
13
+ * em si lança apenas em falha de I/O inesperada — chamadores que precisam de
14
+ * "nunca quebrar" devem envolver em try/catch.
15
+ */
16
+
17
+ const path = require('node:path');
18
+ const fs = require('node:fs');
19
+
20
+ function findActiveContract(targetDir) {
21
+ const plansDir = path.join(targetDir, '.aioson', 'plans');
22
+ if (!fs.existsSync(plansDir)) return null;
23
+ const candidates = [];
24
+ for (const slug of fs.readdirSync(plansDir)) {
25
+ const planDir = path.join(plansDir, slug);
26
+ try {
27
+ const progress = JSON.parse(fs.readFileSync(path.join(planDir, 'progress.json'), 'utf8'));
28
+ if (progress.status === 'in_progress' || progress.status === 'human_gate') {
29
+ const contractPath = path.join(planDir, 'harness-contract.json');
30
+ if (fs.existsSync(contractPath)) {
31
+ candidates.push({ slug, contractPath, lastUpdated: progress.last_updated || '' });
32
+ }
33
+ }
34
+ } catch { /* sem progress legível — não é candidato */ }
35
+ }
36
+ if (!candidates.length) return null;
37
+ candidates.sort((a, b) => String(b.lastUpdated).localeCompare(String(a.lastUpdated)));
38
+ return candidates[0];
39
+ }
40
+
41
+ module.exports = { findActiveContract };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Writer único de `.aioson/plans/{slug}/attempts/{n}/` (loop-guardrails REQ-9).
5
+ *
6
+ * Scope guard e criteria-runner ENTREGAM dados a este módulo — nunca escrevem
7
+ * direto. Registrar primeiro, julgar depois (D5: artifacts é o passo 1 do hook,
8
+ * sempre executado mesmo em falha).
9
+ *
10
+ * Estrutura:
11
+ * attempts/{n}/changed-files.json — { attempt, detected_at, files[] }
12
+ * attempts/{n}/checks/{id}.log — stdout+stderr + exit code + duração
13
+ * attempts/{n}/diff.patch — git diff da tentativa (should-have)
14
+ */
15
+
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+
19
+ function attemptDir(planDir, attempt) {
20
+ return path.join(planDir, 'attempts', String(attempt));
21
+ }
22
+
23
+ /**
24
+ * Grava os artefatos da tentativa. Cada seção é opcional e best-effort
25
+ * independente — falha em uma não impede as outras.
26
+ *
27
+ * @param {string} planDir — .aioson/plans/{slug}
28
+ * @param {number} attempt — número da tentativa (1-based)
29
+ * @param {object} data
30
+ * @param {Array<{path, status}>} [data.changedFiles]
31
+ * @param {Array<{id, command, exitCode, durationMs, stdout, stderr, timedOut}>} [data.checks]
32
+ * @param {string} [data.diffPatch]
33
+ * @returns {{ ok: boolean, dir: string, written: string[] }}
34
+ */
35
+ function writeAttemptArtifacts(planDir, attempt, { changedFiles, checks, diffPatch } = {}) {
36
+ const dir = attemptDir(planDir, attempt);
37
+ const written = [];
38
+
39
+ try {
40
+ fs.mkdirSync(dir, { recursive: true });
41
+ } catch {
42
+ return { ok: false, dir, written };
43
+ }
44
+
45
+ if (Array.isArray(changedFiles)) {
46
+ try {
47
+ const payload = {
48
+ attempt,
49
+ detected_at: new Date().toISOString(),
50
+ files: changedFiles.map((f) => ({ path: f.path, status: f.status }))
51
+ };
52
+ fs.writeFileSync(path.join(dir, 'changed-files.json'), JSON.stringify(payload, null, 2), 'utf8');
53
+ written.push('changed-files.json');
54
+ } catch { /* best-effort */ }
55
+ }
56
+
57
+ if (Array.isArray(checks) && checks.length > 0) {
58
+ try {
59
+ const checksDir = path.join(dir, 'checks');
60
+ fs.mkdirSync(checksDir, { recursive: true });
61
+ for (const check of checks) {
62
+ const safeId = String(check.id || 'check').replace(/[^A-Za-z0-9._-]/g, '_');
63
+ const body = [
64
+ `# criterion: ${check.id}`,
65
+ `# command: ${check.command || ''}`,
66
+ `# exit_code: ${check.exitCode === null || check.exitCode === undefined ? 'null' : check.exitCode}`,
67
+ `# duration_ms: ${check.durationMs ?? 0}`,
68
+ `# timed_out: ${Boolean(check.timedOut)}`,
69
+ '',
70
+ '## stdout',
71
+ check.stdout || '',
72
+ '',
73
+ '## stderr',
74
+ check.stderr || ''
75
+ ].join('\n');
76
+ fs.writeFileSync(path.join(checksDir, `${safeId}.log`), body, 'utf8');
77
+ written.push(`checks/${safeId}.log`);
78
+ }
79
+ } catch { /* best-effort */ }
80
+ }
81
+
82
+ if (typeof diffPatch === 'string' && diffPatch.length > 0) {
83
+ try {
84
+ fs.writeFileSync(path.join(dir, 'diff.patch'), diffPatch, 'utf8');
85
+ written.push('diff.patch');
86
+ } catch { /* best-effort */ }
87
+ }
88
+
89
+ return { ok: true, dir, written };
90
+ }
91
+
92
+ module.exports = {
93
+ writeAttemptArtifacts,
94
+ attemptDir
95
+ };
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Orçamento do self:loop (loop-guardrails REQ-7/8 + D3).
5
+ *
6
+ * Módulo puro: opera sobre o objeto `progress` (de progress.json) e devolve
7
+ * eventos a emitir + decisão de pausa. O wiring persiste e emite.
8
+ *
9
+ * Fonte de enforcement é o acumulador `progress.budget` — nunca SQLite no hot
10
+ * path (D3). `execution_events.token_count` é só telemetria. "Run atual" =
11
+ * acumulador zerado a cada run novo (EC-10: legados null irrelevantes).
12
+ *
13
+ * Estimativa: chars/4 sobre o output do agente (erro 5-15% aceito pelo PRD;
14
+ * `tokenx` é upgrade path documentado).
15
+ */
16
+
17
+ /** Estimativa heurística chars/4 (REQ-7). */
18
+ function estimateTokens(text) {
19
+ if (!text) return 0;
20
+ return Math.ceil(String(text).length / 4);
21
+ }
22
+
23
+ /**
24
+ * Inicializa o acumulador de budget para um run NOVO (preflight, passo 4).
25
+ * Zera tokens e flags; muta `progress` (caller persiste).
26
+ */
27
+ function startRunBudget(progress, runId) {
28
+ progress.budget = {
29
+ tokens_estimated: 0,
30
+ warned_80: false,
31
+ run_started_at: new Date().toISOString(),
32
+ run_id: runId
33
+ };
34
+ return progress.budget;
35
+ }
36
+
37
+ /** Garante que progress.budget existe (retomada de run antigo sem o campo). */
38
+ function ensureBudget(progress, runId) {
39
+ if (!progress.budget || typeof progress.budget !== 'object') {
40
+ startRunBudget(progress, runId);
41
+ }
42
+ return progress.budget;
43
+ }
44
+
45
+ /** Acumula a estimativa da tentativa; muta `progress` (caller persiste). */
46
+ function recordAttemptTokens(progress, tokens) {
47
+ ensureBudget(progress, null);
48
+ progress.budget.tokens_estimated += Math.max(0, Math.round(tokens) || 0);
49
+ return progress.budget.tokens_estimated;
50
+ }
51
+
52
+ /**
53
+ * Política 80/100% (REQ-7) + max_runtime_minutes na fronteira (REQ-8).
54
+ *
55
+ * EC-11: 80% e 100% cruzados na mesma tentativa → AMBOS os eventos em ordem,
56
+ * pausa uma vez. `warned_80` garante o warning 1x por run; muta `progress`.
57
+ *
58
+ * @returns {{ ok, pause, events: [{type, message, payload}] }}
59
+ */
60
+ function checkBudget(progress, { costCeilingTokens = null, maxRuntimeMinutes = null, now = null } = {}) {
61
+ const events = [];
62
+ let pause = false;
63
+ const budget = ensureBudget(progress, null);
64
+
65
+ if (Number.isInteger(costCeilingTokens) && costCeilingTokens > 0) {
66
+ const spent = budget.tokens_estimated;
67
+ const pct = spent / costCeilingTokens;
68
+
69
+ if (pct >= 0.8 && !budget.warned_80) {
70
+ budget.warned_80 = true;
71
+ events.push({
72
+ type: 'budget_warning',
73
+ message: `token budget at ${Math.round(pct * 100)}% (${spent}/${costCeilingTokens} estimated)`,
74
+ payload: { tokens_estimated: spent, cost_ceiling_tokens: costCeilingTokens, pct: Math.round(pct * 100) }
75
+ });
76
+ }
77
+
78
+ if (pct >= 1) {
79
+ events.push({
80
+ type: 'budget_exceeded',
81
+ message: `token budget exceeded (${spent}/${costCeilingTokens} estimated) — pausing loop`,
82
+ payload: { tokens_estimated: spent, cost_ceiling_tokens: costCeilingTokens }
83
+ });
84
+ pause = true;
85
+ }
86
+ }
87
+
88
+ if (Number.isInteger(maxRuntimeMinutes) && maxRuntimeMinutes > 0 && budget.run_started_at) {
89
+ const startedAt = Date.parse(budget.run_started_at);
90
+ const current = now ? Date.parse(now) : Date.now();
91
+ if (Number.isFinite(startedAt) && current - startedAt > maxRuntimeMinutes * 60000) {
92
+ const elapsedMin = Math.round((current - startedAt) / 60000);
93
+ events.push({
94
+ type: 'runtime_exceeded',
95
+ message: `max_runtime_minutes exceeded (${elapsedMin}min > ${maxRuntimeMinutes}min) — pausing loop`,
96
+ payload: { elapsed_minutes: elapsedMin, max_runtime_minutes: maxRuntimeMinutes }
97
+ });
98
+ pause = true;
99
+ }
100
+ }
101
+
102
+ return { ok: !pause, pause, events };
103
+ }
104
+
105
+ /**
106
+ * Resumo feito/faltante para a pausa de 100% (REQ-7).
107
+ */
108
+ function buildBudgetSummary(progress, { maxIterations = null } = {}) {
109
+ const budget = progress.budget || {};
110
+ const iterations = progress.iterations || 0;
111
+ return [
112
+ `Budget pause summary:`,
113
+ ` iterations completed: ${iterations}${maxIterations ? `/${maxIterations}` : ''}`,
114
+ ` tokens estimated (chars/4): ${budget.tokens_estimated || 0}`,
115
+ ` run started at: ${budget.run_started_at || 'unknown'}`,
116
+ ` resume: review scope/budget in harness-contract.json, then re-run self:loop`
117
+ ].join('\n');
118
+ }
119
+
120
+ module.exports = {
121
+ estimateTokens,
122
+ startRunBudget,
123
+ ensureBudget,
124
+ recordAttemptTokens,
125
+ checkBudget,
126
+ buildBudgetSummary
127
+ };
@@ -47,6 +47,13 @@ class CircuitBreaker {
47
47
  const { circuit_state, iterations, consecutive_errors } = this.progress;
48
48
  const { max_steps, error_streak_limit } = this.contract.governor;
49
49
 
50
+ // HUMAN_GATE (loop-guardrails D4): gate humano pendente nega execução até
51
+ // decisão via harness:approve / harness:reject (REQ-12/15).
52
+ const pendingGates = Array.isArray(this.progress.pending_gates) ? this.progress.pending_gates : [];
53
+ if (this.progress.status === 'human_gate' || pendingGates.length > 0) {
54
+ return { allowed: false, reason: 'human_gate_pending' };
55
+ }
56
+
50
57
  if (circuit_state === 'OPEN') {
51
58
  return { allowed: false, reason: 'circuit_open' };
52
59
  }