@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/commands/live.js
CHANGED
|
@@ -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(
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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, {
|