@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.
- package/CHANGELOG.md +39 -2
- package/docs/en/1-understand/ecosystem-map.md +1 -1
- package/docs/en/2-start/initial-decisions.md +1 -1
- package/docs/en/4-agents/README.md +8 -7
- package/docs/en/4-agents/discovery-design-doc.md +150 -0
- package/docs/en/5-reference/cli-reference.md +42 -16
- package/docs/en/README.md +2 -2
- package/docs/pt/4-agentes/README.md +8 -6
- package/docs/pt/4-agentes/briefing-refiner.md +122 -0
- package/docs/pt/4-agentes/discovery-design-doc.md +133 -74
- package/docs/pt/4-agentes/scope-check.md +65 -0
- package/docs/pt/5-referencia/README.md +1 -0
- package/docs/pt/5-referencia/comandos-cli.md +5 -4
- package/docs/pt/5-referencia/feature-archive.md +1 -0
- package/docs/pt/5-referencia/feature-export.md +155 -0
- package/docs/pt/README.md +2 -2
- package/docs/pt/agentes.md +3 -1
- package/package.json +1 -1
- package/src/agent-manifests.js +14 -3
- package/src/agents.js +21 -20
- package/src/cli.js +72 -52
- package/src/commands/briefing.js +28 -150
- package/src/commands/commit-prepare.js +5 -2
- package/src/commands/feature-archive.js +48 -12
- package/src/commands/feature-close.js +40 -0
- package/src/commands/feature-export.js +242 -0
- package/src/commands/gate-check.js +8 -3
- package/src/commands/git-guard.js +58 -0
- package/src/commands/harness-gate.js +120 -0
- package/src/commands/harness-status.js +157 -0
- package/src/commands/harness.js +18 -1
- package/src/commands/live.js +120 -115
- package/src/commands/parallel-doctor.js +2 -1
- package/src/commands/pulse-update.js +2 -2
- package/src/commands/scan-project.js +12 -2
- package/src/commands/self-implement-loop.js +305 -5
- package/src/commands/workflow-next.js +477 -425
- package/src/constants.js +21 -11
- package/src/context-search.js +3 -0
- package/src/doctor.js +24 -8
- package/src/dossier/schema.js +4 -3
- package/src/harness/active-contract.js +41 -0
- package/src/harness/attempt-artifacts.js +95 -0
- package/src/harness/budget-guard.js +127 -0
- package/src/harness/circuit-breaker.js +7 -0
- package/src/harness/contract-schema.js +324 -0
- package/src/harness/criteria-runner.js +136 -0
- package/src/harness/git-baseline.js +204 -0
- package/src/harness/glob-match.js +126 -0
- package/src/harness/guard-events.js +71 -0
- package/src/harness/human-gate.js +182 -0
- package/src/harness/scope-guard.js +115 -0
- package/src/i18n/messages/en.js +24 -21
- package/src/i18n/messages/es.js +11 -9
- package/src/i18n/messages/fr.js +11 -9
- package/src/i18n/messages/pt-BR.js +24 -21
- package/src/lib/briefing-refiner/apply-feedback.js +134 -0
- package/src/lib/briefing-refiner/briefing-paths.js +41 -0
- package/src/lib/briefing-refiner/briefing-registry.js +204 -0
- package/src/lib/briefing-refiner/briefing-sections.js +110 -0
- package/src/lib/briefing-refiner/feedback-schema.js +122 -0
- package/src/lib/briefing-refiner/refinement-report.js +39 -0
- package/src/lib/briefing-refiner/review-html.js +230 -0
- package/src/lib/dev-resume.js +94 -45
- package/src/parser.js +8 -5
- package/src/preflight-engine.js +88 -84
- package/src/runtime-store.js +2 -0
- package/src/sandbox.js +17 -3
- package/template/.aioson/agents/analyst.md +27 -23
- package/template/.aioson/agents/architect.md +7 -3
- package/template/.aioson/agents/briefing-refiner.md +121 -0
- package/template/.aioson/agents/briefing.md +83 -74
- package/template/.aioson/agents/committer.md +8 -0
- package/template/.aioson/agents/copywriter.md +19 -7
- package/template/.aioson/agents/design-hybrid-forge.md +16 -5
- package/template/.aioson/agents/dev.md +68 -66
- package/template/.aioson/agents/deyvin.md +97 -90
- package/template/.aioson/agents/discover.md +2 -2
- package/template/.aioson/agents/discovery-design-doc.md +34 -30
- package/template/.aioson/agents/genome.md +82 -71
- package/template/.aioson/agents/neo.md +11 -3
- package/template/.aioson/agents/orache.md +10 -0
- package/template/.aioson/agents/orchestrator.md +68 -68
- package/template/.aioson/agents/pentester.md +15 -6
- package/template/.aioson/agents/pm.md +30 -25
- package/template/.aioson/agents/product.md +108 -108
- package/template/.aioson/agents/profiler-enricher.md +10 -0
- package/template/.aioson/agents/profiler-forge.md +10 -0
- package/template/.aioson/agents/profiler-researcher.md +11 -0
- package/template/.aioson/agents/qa.md +28 -20
- package/template/.aioson/agents/scope-check.md +176 -164
- package/template/.aioson/agents/setup.md +11 -1
- package/template/.aioson/agents/sheldon.md +38 -38
- package/template/.aioson/agents/site-forge.md +15 -6
- package/template/.aioson/agents/squad.md +12 -0
- package/template/.aioson/agents/tester.md +209 -209
- package/template/.aioson/agents/ux-ui.md +2 -2
- package/template/.aioson/agents/validator.md +10 -2
- package/template/.aioson/config.md +31 -28
- package/template/.aioson/docs/autopilot-handoff.md +46 -0
- package/template/.aioson/docs/dossier/agent-templates.md +191 -0
- package/template/.aioson/docs/dossier/schema.md +218 -0
- package/template/.claude/commands/aioson/agent/briefing-refiner.md +17 -0
- package/template/AGENTS.md +50 -47
- 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/
|
|
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: '
|
|
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',
|
package/src/context-search.js
CHANGED
|
@@ -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',
|
package/src/dossier/schema.js
CHANGED
|
@@ -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
|
-
'
|
|
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
|
}
|