@jaimevalasek/aioson 1.37.0 → 1.37.1
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 +14 -0
- package/package.json +1 -1
- package/src/agent-execution/adapters/base.js +15 -0
- package/src/agent-execution/adapters/claude.js +3 -0
- package/src/agent-execution/adapters/codex.js +3 -0
- package/src/agent-execution/adapters/opencode.js +3 -0
- package/src/agent-execution/capabilities.js +9 -0
- package/src/agent-execution/dispatcher.js +72 -0
- package/src/agent-execution/executable-resolver.js +7 -0
- package/src/agent-execution/manifest.js +62 -0
- package/src/agent-execution/model-catalog.js +80 -0
- package/src/agent-execution/model-resolver.js +132 -0
- package/src/agent-execution/reports.js +9 -0
- package/src/agent-execution/schema.js +48 -0
- package/src/agent-execution/telemetry-bridge.js +15 -0
- package/src/cli.js +22 -4
- package/src/commands/agent-execution.js +36 -0
- package/src/commands/op-capture.js +33 -3
- package/src/commands/op-reinforce.js +10 -22
- package/src/commands/verification-plan.js +28 -11
- package/src/commands/workflow-execute.js +20 -6
- package/src/harness/criteria-runner.js +4 -1
- package/src/operator-memory/decision.js +41 -0
- package/src/runtime-store.js +167 -8
- package/template/.aioson/agents/dev.md +1 -1
- package/template/.aioson/agents/product.md +2 -1
- package/template/.aioson/docs/autopilot-handoff.md +7 -1
- package/template/.aioson/docs/dev/phase-loop.md +2 -3
- package/template/.aioson/schemas/agent-execution.schema.json +28 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const path=require('node:path');
|
|
3
|
+
const {initManifest,loadManifest,resolveAgentExecution}=require('../agent-execution/manifest');
|
|
4
|
+
const {dispatch,readState}=require('../agent-execution/dispatcher');
|
|
5
|
+
const {openRuntimeDb,getExecutionSnapshot,listExecutionEvents}=require('../runtime-store');
|
|
6
|
+
async function resolveAllAgents(manifest,catalogLoader){
|
|
7
|
+
const entries=await Promise.all(Object.keys(manifest.agents).map(async id=>[id,await resolveAgentExecution(manifest,id,{}, {catalogLoader})]));
|
|
8
|
+
return Object.fromEntries(entries);
|
|
9
|
+
}
|
|
10
|
+
function resolutionErrors(resolutions){
|
|
11
|
+
return Object.entries(resolutions).filter(([,value])=>!value.ok).map(([id,value])=>({path:`$.agents.${id}.model`,message:value.reason,candidates:value.candidates||[],supported:value.supported||[]}));
|
|
12
|
+
}
|
|
13
|
+
async function runAgentExecution({args,options={},logger,catalogLoader}){
|
|
14
|
+
const projectDir=path.resolve(process.cwd(),args[0]||'.'); const sub=options.sub||'show'; const feature=String(options.feature||options.slug||'').trim();
|
|
15
|
+
if(!feature)return {ok:false,reason:'feature_required',message:'Use --feature=<slug>'};
|
|
16
|
+
if(sub==='init'){const created=await initManifest(projectDir,feature,String(options.host||'codex')); const result={ok:true,feature,path:path.relative(projectDir,created.path),digest:created.digest,manifest:created.manifest,availability:'validated_at_dispatch'}; if(!options.json)logger.log(`Agent execution manifest: ${result.path}\nValidate: aioson agent:execution:validate . --feature=${feature}`); return result;}
|
|
17
|
+
const loaded=await loadManifest(projectDir,feature);
|
|
18
|
+
if(sub==='validate'){
|
|
19
|
+
let resolutions={};let errors=loaded.errors||[];
|
|
20
|
+
if(loaded.exists&&loaded.ok){resolutions=await resolveAllAgents(loaded.manifest,catalogLoader);errors=resolutionErrors(resolutions)}
|
|
21
|
+
const result={ok:Boolean(loaded.exists&&loaded.ok&&!errors.length),feature,path:path.relative(projectDir,loaded.path),legacy:!loaded.exists,digest:loaded.digest||null,errors,resolutions,availability:'validated_at_dispatch'};
|
|
22
|
+
if(!options.json)(result.ok?logger.log(`Valid: ${result.path} (models resolved against the current runtime catalog).`):logger.error(JSON.stringify(result.errors,null,2)));return result;
|
|
23
|
+
}
|
|
24
|
+
if(sub==='show'){
|
|
25
|
+
if(!loaded.exists)return {ok:true,legacy:true,feature,path:path.relative(projectDir,loaded.path),message:'Manifest absent; legacy configured-default remains active.'};
|
|
26
|
+
if(!loaded.ok)return {ok:false,feature,errors:loaded.errors};
|
|
27
|
+
const agents=await resolveAllAgents(loaded.manifest,catalogLoader);const errors=resolutionErrors(agents);
|
|
28
|
+
return {ok:!errors.length,feature,path:path.relative(projectDir,loaded.path),digest:loaded.digest,agents,errors,availability:'validated_at_dispatch'};
|
|
29
|
+
}
|
|
30
|
+
if(sub==='dispatch')return dispatch({projectDir,feature,agent:options.agent||'dev',runId:options['run-id'],promptPath:options.prompt,catalogLoader});
|
|
31
|
+
if(sub==='resume'){const state=await readState(projectDir,feature);if(!state)return {ok:false,reason:'state_missing'};return dispatch({projectDir,feature,agent:options.agent||state.attempts.at(-1)?.agent||'dev',runId:state.run_id,promptPath:options.prompt,catalogLoader});}
|
|
32
|
+
if(sub==='status'){const {db}=await openRuntimeDb(projectDir);try{return{ok:true,schema_version:1,feature,runs:getExecutionSnapshot(db,{feature,agent:options.agent,state:options.state,limit:options.limit})}}finally{db.close()}}
|
|
33
|
+
if(sub==='events'){const state=await readState(projectDir,feature);const run=String(options.run||state?.attempts?.at(-1)?.telemetry_run_id||'');if(!run)return{ok:false,reason:'telemetry_run_required'};const {db}=await openRuntimeDb(projectDir);try{const owned=getExecutionSnapshot(db,{feature,limit:200}).some(item=>item.telemetry_run_id===run);if(!owned)return{ok:false,reason:'telemetry_run_not_found'};return{ok:true,schema_version:1,trust:'sanitized_untrusted_output',feature,telemetry_run_id:run,...listExecutionEvents(db,run,{after:options.after,limit:options.limit})}}finally{db.close()}}
|
|
34
|
+
return {ok:false,reason:'invalid_subcommand',valid:['init','validate','show','dispatch','resume','status','events']};
|
|
35
|
+
}
|
|
36
|
+
module.exports={runAgentExecution};
|
|
@@ -23,14 +23,27 @@ const path = require('node:path');
|
|
|
23
23
|
const { resolveIdentity } = require('../operator-memory/identity');
|
|
24
24
|
const { ensureStorageTree, recordIdentityActivity, openIndexDb, getStorageRoot } = require('../operator-memory/storage');
|
|
25
25
|
const { deriveSlug, fingerprintProposal } = require('../operator-memory/slug');
|
|
26
|
-
const { captureSignal, readProposal, VALID_SIGNAL_TYPES } = require('../operator-memory/proposal');
|
|
27
|
-
const { promoteProposal } = require('../operator-memory/decision');
|
|
26
|
+
const { captureSignal, readProposal, deleteProposal, VALID_SIGNAL_TYPES } = require('../operator-memory/proposal');
|
|
27
|
+
const { promoteProposal, readDecision, reinforceDecision } = require('../operator-memory/decision');
|
|
28
28
|
const { emitDossierEvent } = require('../lib/dossier-telemetry');
|
|
29
29
|
|
|
30
30
|
const CONFIRMATIONS_JSONL = '.aioson/runtime/session-confirmations.jsonl';
|
|
31
31
|
|
|
32
32
|
const PROMOTION_THRESHOLD = 2;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Detections required before a signal promotes to a decision, by signal type.
|
|
36
|
+
*
|
|
37
|
+
* `confirmation` needs 2x — the user accepting the same non-obvious approach twice
|
|
38
|
+
* is what turns it into a signal. `authorization` / `exclusion` / `correction` are
|
|
39
|
+
* single explicit standing decisions ("pode sempre X", "nunca X", "pare de X"), so
|
|
40
|
+
* they promote on first detection. Mirrors the taxonomy in
|
|
41
|
+
* agents/_shared/memory-capture-directive.md.
|
|
42
|
+
*/
|
|
43
|
+
function promotionThresholdFor(signalType) {
|
|
44
|
+
return signalType === 'confirmation' ? PROMOTION_THRESHOLD : 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
function existsCheckFactory(identity) {
|
|
35
48
|
return (slug) => {
|
|
36
49
|
const existing = readProposal(identity, slug);
|
|
@@ -130,7 +143,23 @@ First detection writes to proposals/{slug}.md. Second detection promotes to deci
|
|
|
130
143
|
|
|
131
144
|
const count = result.proposal.detected_count;
|
|
132
145
|
|
|
133
|
-
|
|
146
|
+
// Idempotent re-detection: a signal already promoted to a decision is reinforced,
|
|
147
|
+
// not re-promoted — re-promotion would duplicate the FTS row and reset promoted_at.
|
|
148
|
+
const existingDecision = readDecision(resolved.identity, slug);
|
|
149
|
+
if (existingDecision) {
|
|
150
|
+
let reinforced = null;
|
|
151
|
+
try { reinforced = reinforceDecision(resolved.identity, slug); } catch { /* best-effort */ }
|
|
152
|
+
// captureSignal re-created a stray proposal (the decision had none) — drop it.
|
|
153
|
+
try { deleteProposal(resolved.identity, slug); } catch { /* best-effort */ }
|
|
154
|
+
const auditLine = `✔ Memory reforçada: '${proposal}'. aioson op:forget ${slug} p/ desfazer.`;
|
|
155
|
+
if (options.json) {
|
|
156
|
+
return { ok: true, promoted: false, reinforced: true, slug, identity: resolved.identity, reinforcement_count: reinforced ? reinforced.reinforcement_count : undefined };
|
|
157
|
+
}
|
|
158
|
+
if (logger) logger.log(auditLine);
|
|
159
|
+
return { ok: true, promoted: false, reinforced: true, slug };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (count >= promotionThresholdFor(signal)) {
|
|
134
163
|
// Promote to decision
|
|
135
164
|
let decision;
|
|
136
165
|
try {
|
|
@@ -166,6 +195,7 @@ First detection writes to proposals/{slug}.md. Second detection promotes to deci
|
|
|
166
195
|
|
|
167
196
|
module.exports = {
|
|
168
197
|
runOpCapture,
|
|
198
|
+
promotionThresholdFor,
|
|
169
199
|
PROMOTION_THRESHOLD,
|
|
170
200
|
CONFIRMATIONS_JSONL
|
|
171
201
|
};
|
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
* Phase 5 (v1.16.0). User-driven action when decay prompt fires.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const fs = require('node:fs');
|
|
9
8
|
const { resolveIdentity } = require('../operator-memory/identity');
|
|
10
9
|
const { ensureStorageTree } = require('../operator-memory/storage');
|
|
11
|
-
const {
|
|
12
|
-
const { regenerateIndex } = require('../operator-memory/index-md');
|
|
10
|
+
const { reinforceDecision } = require('../operator-memory/decision');
|
|
13
11
|
const { emitDossierEvent } = require('../lib/dossier-telemetry');
|
|
14
12
|
|
|
15
13
|
async function runOpReinforce({ args = [], options = {}, logger }) {
|
|
@@ -31,30 +29,20 @@ async function runOpReinforce({ args = [], options = {}, logger }) {
|
|
|
31
29
|
|
|
32
30
|
const resolved = resolveIdentity();
|
|
33
31
|
ensureStorageTree(resolved.identity);
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
|
|
33
|
+
// Surgical in-place reinforce: bumps reinforcement_count + refreshes
|
|
34
|
+
// last_reinforced and regenerates the index, preserving title/body/trigger-quotes
|
|
35
|
+
// byte-for-byte. (The old re-serialize path double-wrapped them, because the merged
|
|
36
|
+
// body from readDecision already contained the "# title" and "## Trigger quotes".)
|
|
37
|
+
const reinforced = reinforceDecision(resolved.identity, slug);
|
|
38
|
+
if (!reinforced) {
|
|
36
39
|
const err = `op:reinforce — decision '${slug}' not found for identity ${resolved.identity}.`;
|
|
37
40
|
if (options.json) return { ok: false, error: err };
|
|
38
41
|
if (logger && logger.error) logger.error(err);
|
|
39
42
|
return { ok: false, exitCode: 1, error: err };
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
const now =
|
|
43
|
-
const previous = decision.last_reinforced;
|
|
44
|
-
const updated = {
|
|
45
|
-
...decision,
|
|
46
|
-
slug,
|
|
47
|
-
last_reinforced: now,
|
|
48
|
-
reinforcement_count: Number(decision.reinforcement_count || 0) + 1
|
|
49
|
-
};
|
|
50
|
-
// serialize keeps quotes + body + frontmatter intact
|
|
51
|
-
const out = serializeDecision({ ...updated, body: decision.body, title: decision.body?.split('\n')[0]?.replace(/^# /, '') || slug });
|
|
52
|
-
const filePath = decisionPath(resolved.identity, slug);
|
|
53
|
-
const tmp = `${filePath}.tmp`;
|
|
54
|
-
fs.writeFileSync(tmp, out, 'utf8');
|
|
55
|
-
fs.renameSync(tmp, filePath);
|
|
56
|
-
|
|
57
|
-
try { regenerateIndex(resolved.identity); } catch { /* non-fatal */ }
|
|
45
|
+
const { last_reinforced: now, previous, reinforcement_count } = reinforced;
|
|
58
46
|
|
|
59
47
|
await emitDossierEvent(targetDir, {
|
|
60
48
|
agent: 'op-reinforce',
|
|
@@ -64,7 +52,7 @@ async function runOpReinforce({ args = [], options = {}, logger }) {
|
|
|
64
52
|
});
|
|
65
53
|
|
|
66
54
|
if (options.json) {
|
|
67
|
-
return { ok: true, slug, last_reinforced: now, previous, reinforcement_count
|
|
55
|
+
return { ok: true, slug, last_reinforced: now, previous, reinforcement_count };
|
|
68
56
|
}
|
|
69
57
|
if (logger) logger.log(`op:reinforce — '${slug}' last_reinforced refreshed to ${now}.`);
|
|
70
58
|
return { ok: true, slug };
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// instead of re-interpreting the config in prose. Read-only; never mutates state.
|
|
8
8
|
|
|
9
9
|
const fs = require('node:fs/promises');
|
|
10
|
-
const path = require('node:path');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const { loadManifest, resolveAgentExecution } = require('../agent-execution/manifest');
|
|
11
12
|
const {
|
|
12
13
|
readVerificationConfig,
|
|
13
14
|
resolveHost,
|
|
@@ -63,7 +64,7 @@ async function detectClassification(targetDir, slug) {
|
|
|
63
64
|
return null;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
async function runVerificationPlan({ args, options = {}, logger }) {
|
|
67
|
+
async function runVerificationPlan({ args, options = {}, logger, catalogLoader }) {
|
|
67
68
|
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
68
69
|
const slug = (options.feature || options.slug || '').toString().trim() || null;
|
|
69
70
|
|
|
@@ -79,8 +80,12 @@ async function runVerificationPlan({ args, options = {}, logger }) {
|
|
|
79
80
|
}
|
|
80
81
|
const trigger = triggerRaw;
|
|
81
82
|
|
|
82
|
-
const config = await readVerificationConfig(targetDir);
|
|
83
|
-
const host = resolveHost(config, options.host);
|
|
83
|
+
const config = await readVerificationConfig(targetDir);
|
|
84
|
+
const host = resolveHost(config, options.host);
|
|
85
|
+
const executionManifest = slug ? await loadManifest(targetDir, slug) : { exists: false, legacy: true, ok: true };
|
|
86
|
+
if (executionManifest.exists && !executionManifest.ok) {
|
|
87
|
+
return { ok: false, reason: 'agent_execution_manifest_invalid', errors: executionManifest.errors, manifest_path: executionManifest.path };
|
|
88
|
+
}
|
|
84
89
|
|
|
85
90
|
let classification = (options.classification || '').toString().trim().toUpperCase();
|
|
86
91
|
if (!CLASSIFICATIONS.includes(classification)) {
|
|
@@ -91,13 +96,24 @@ async function runVerificationPlan({ args, options = {}, logger }) {
|
|
|
91
96
|
const context = { trigger, classification, sensitiveSurface };
|
|
92
97
|
const agents = [];
|
|
93
98
|
for (const agentId of VERIFICATION_AGENTS) {
|
|
94
|
-
const dispatch = getAgentDispatch(config, agentId, host);
|
|
95
|
-
const
|
|
99
|
+
const dispatch = getAgentDispatch(config, agentId, host);
|
|
100
|
+
const execution = executionManifest.exists ? await resolveAgentExecution(executionManifest.manifest, agentId, {}, { catalogLoader }) : null;
|
|
101
|
+
if (execution && !execution.ok) {
|
|
102
|
+
return { ok: false, reason: 'model_resolution_failed', agent: agentId, resolution: execution };
|
|
103
|
+
}
|
|
104
|
+
const entry = {
|
|
96
105
|
agent: agentId,
|
|
97
106
|
run: shouldRunForTrigger(config, agentId, context),
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
host: execution ? execution.host : (dispatch ? dispatch.host : host),
|
|
108
|
+
mode: execution ? execution.mode : (dispatch ? dispatch.mode : 'native'),
|
|
109
|
+
model: execution ? execution.model : (dispatch ? dispatch.model : 'configured-default'),
|
|
110
|
+
model_requested: execution ? execution.model_requested : (dispatch ? dispatch.model : 'configured-default'),
|
|
111
|
+
model_resolved: execution ? execution.model_resolved : (dispatch ? dispatch.model : 'configured-default'),
|
|
112
|
+
model_resolution_strategy: execution ? execution.model_resolution_strategy : 'legacy',
|
|
113
|
+
reasoning_effort: execution ? (execution.reasoning_effort || null) : null,
|
|
114
|
+
report: execution ? execution.report.replace('{run_id}', '{run_id}') : resolveAgentReportPath(config, agentId, slug || '{slug}'),
|
|
115
|
+
execution_source: execution ? 'manifest' : 'legacy',
|
|
116
|
+
execution: execution || undefined
|
|
101
117
|
};
|
|
102
118
|
if (agentId === 'validator') {
|
|
103
119
|
const cc = getCrossCheck(config, agentId);
|
|
@@ -115,8 +131,9 @@ async function runVerificationPlan({ args, options = {}, logger }) {
|
|
|
115
131
|
sensitive_surface: sensitiveSurface,
|
|
116
132
|
agents,
|
|
117
133
|
phase_loop: getPhaseLoop(config),
|
|
118
|
-
budget: getBudget(config)
|
|
119
|
-
};
|
|
134
|
+
budget: getBudget(config)
|
|
135
|
+
};
|
|
136
|
+
result.agent_execution = executionManifest.exists ? { source: 'manifest', path: executionManifest.path, digest: executionManifest.digest } : { source: 'legacy' };
|
|
120
137
|
result.continuation_directive = buildContinuationDirective(trigger, result.phase_loop);
|
|
121
138
|
|
|
122
139
|
if (options.json) return result;
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const fs = require('node:fs/promises');
|
|
11
|
-
const path = require('node:path');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const { initManifest, loadManifest } = require('../agent-execution/manifest');
|
|
12
13
|
const {
|
|
13
14
|
detectClassification,
|
|
14
15
|
scanArtifacts,
|
|
@@ -751,7 +752,7 @@ async function performRunnerTransition(targetDir, suggestion, tool, requestedMod
|
|
|
751
752
|
async function runWorkflowExecute({ args, options = {}, logger }) {
|
|
752
753
|
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
753
754
|
const slug = options.feature ? String(options.feature).trim() : null;
|
|
754
|
-
const tool = options.tool ? String(options.tool).trim() : 'claude';
|
|
755
|
+
const tool = options.tool ? String(options.tool).trim() : 'claude';
|
|
755
756
|
const requestedMode = options.mode ? String(options.mode).trim() : null;
|
|
756
757
|
const dryRun = Boolean(options['dry-run'] || options.dry);
|
|
757
758
|
// --step (disarm) is by definition record-only: it writes the disarmed scheme
|
|
@@ -788,9 +789,20 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
|
|
|
788
789
|
return failure;
|
|
789
790
|
}
|
|
790
791
|
|
|
791
|
-
const parallelGuard = laneIndex !== null
|
|
792
|
+
const parallelGuard = laneIndex !== null
|
|
792
793
|
? await runLaneGuardPreflight(targetDir, laneIndex)
|
|
793
|
-
: null;
|
|
794
|
+
: null;
|
|
795
|
+
|
|
796
|
+
let agentExecution = await loadManifest(targetDir, slug);
|
|
797
|
+
if (!dryRun && !agentExecution.exists) {
|
|
798
|
+
await initManifest(targetDir, slug, tool);
|
|
799
|
+
agentExecution = await loadManifest(targetDir, slug);
|
|
800
|
+
}
|
|
801
|
+
if (agentExecution.exists && !agentExecution.ok) {
|
|
802
|
+
const failure = { ok: false, reason: 'agent_execution_manifest_invalid', errors: agentExecution.errors, manifest_path: agentExecution.path };
|
|
803
|
+
if (!options.json) logger.error(`Invalid agent execution manifest: ${agentExecution.path}`);
|
|
804
|
+
return failure;
|
|
805
|
+
}
|
|
794
806
|
|
|
795
807
|
if (parallelGuard && !parallelGuard.ok && !parallelGuard.skipped) {
|
|
796
808
|
if (parallelGuard.reason === 'lane_not_found') {
|
|
@@ -882,7 +894,8 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
|
|
|
882
894
|
status_snapshot: statusSnapshot,
|
|
883
895
|
suggestion: statusSnapshot && statusSnapshot.suggestion ? statusSnapshot.suggestion : null,
|
|
884
896
|
resume_command: resumeCommand,
|
|
885
|
-
agentic_policy: agenticPolicy,
|
|
897
|
+
agentic_policy: agenticPolicy,
|
|
898
|
+
agent_execution: agentExecution.exists ? { path: agentExecution.path, digest: agentExecution.digest } : { source: 'legacy' },
|
|
886
899
|
parallel_guard: parallelGuard
|
|
887
900
|
};
|
|
888
901
|
|
|
@@ -953,7 +966,8 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
|
|
|
953
966
|
status_snapshot: statusSnapshot,
|
|
954
967
|
suggestion: statusSnapshot && statusSnapshot.suggestion ? statusSnapshot.suggestion : null,
|
|
955
968
|
resume_command: resumeCommand,
|
|
956
|
-
agentic_policy: agenticPolicy,
|
|
969
|
+
agentic_policy: agenticPolicy,
|
|
970
|
+
agent_execution: { path: agentExecution.path, digest: agentExecution.digest },
|
|
957
971
|
parallel_guard: parallelGuard
|
|
958
972
|
};
|
|
959
973
|
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
const crypto = require('node:crypto');
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Repository-wide quality gates can legitimately exceed two minutes on
|
|
20
|
+
// Windows. Keep the CLI --timeout override, but give deterministic contract
|
|
21
|
+
// checks enough headroom to avoid classifying a healthy full suite as failed.
|
|
22
|
+
const DEFAULT_CHECK_TIMEOUT_MS = 300000;
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Normaliza uma linha de erro para assinatura estável (D7):
|
|
@@ -226,6 +226,46 @@ function promoteProposal({ identity, proposal: proposalData }) {
|
|
|
226
226
|
return decision;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Reinforce an existing decision in place: bump reinforcement_count and refresh
|
|
231
|
+
* last_reinforced (PMD-11). Used when a signal already promoted is re-detected —
|
|
232
|
+
* strengthens the memory instead of re-promoting (which would duplicate the FTS
|
|
233
|
+
* row and reset promoted_at).
|
|
234
|
+
*
|
|
235
|
+
* Surgical frontmatter edit — preserves title, body and trigger quotes byte-for-byte
|
|
236
|
+
* (unlike a full re-serialize, which cannot recover the title from the merged body).
|
|
237
|
+
* Returns { slug, last_reinforced, previous, reinforcement_count } or null if absent.
|
|
238
|
+
*/
|
|
239
|
+
function reinforceDecision(identity, slug) {
|
|
240
|
+
const filePath = decisionPath(identity, slug);
|
|
241
|
+
if (!fs.existsSync(filePath)) return null;
|
|
242
|
+
|
|
243
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
244
|
+
const parsed = parseDecisionFile(content);
|
|
245
|
+
const previous = (parsed && parsed.last_reinforced) || null;
|
|
246
|
+
const nextCount = Number((parsed && parsed.reinforcement_count) || 0) + 1;
|
|
247
|
+
const now = new Date().toISOString();
|
|
248
|
+
|
|
249
|
+
// Replace only the first (frontmatter) occurrence of each field; `.` never
|
|
250
|
+
// spans newlines, so the body is untouched.
|
|
251
|
+
const updated = content
|
|
252
|
+
.replace(/^(reinforcement_count:[ \t]*).*$/m, `$1${nextCount}`)
|
|
253
|
+
.replace(/^(last_reinforced:[ \t]*).*$/m, `$1${now}`);
|
|
254
|
+
|
|
255
|
+
const tmpPath = `${filePath}.tmp`;
|
|
256
|
+
fs.writeFileSync(tmpPath, updated, 'utf8');
|
|
257
|
+
fs.renameSync(tmpPath, filePath);
|
|
258
|
+
|
|
259
|
+
// Post-write: regenerate MEMORY.md index (best-effort — it surfaces the
|
|
260
|
+
// reinforced date). FTS body/category are unchanged, so no FTS write needed.
|
|
261
|
+
try {
|
|
262
|
+
const { regenerateIndex } = require('./index-md');
|
|
263
|
+
regenerateIndex(identity);
|
|
264
|
+
} catch { /* index regen failure is non-fatal */ }
|
|
265
|
+
|
|
266
|
+
return { slug, last_reinforced: now, previous, reinforcement_count: nextCount };
|
|
267
|
+
}
|
|
268
|
+
|
|
229
269
|
/**
|
|
230
270
|
* Soft-delete a decision or proposal to history/.
|
|
231
271
|
* Returns { mode: 'decision'|'proposal'|'noop', archivedPath: string|null }.
|
|
@@ -267,6 +307,7 @@ function forgetEntry(identity, slug) {
|
|
|
267
307
|
|
|
268
308
|
module.exports = {
|
|
269
309
|
promoteProposal,
|
|
310
|
+
reinforceDecision,
|
|
270
311
|
forgetEntry,
|
|
271
312
|
readDecision,
|
|
272
313
|
decisionPath,
|
package/src/runtime-store.js
CHANGED
|
@@ -176,7 +176,7 @@ async function openRuntimeDb(targetDir, options = {}) {
|
|
|
176
176
|
FOREIGN KEY (run_key) REFERENCES agent_runs(run_key) ON DELETE CASCADE
|
|
177
177
|
);
|
|
178
178
|
|
|
179
|
-
CREATE TABLE IF NOT EXISTS execution_events (
|
|
179
|
+
CREATE TABLE IF NOT EXISTS execution_events (
|
|
180
180
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
181
181
|
task_key TEXT,
|
|
182
182
|
run_key TEXT,
|
|
@@ -199,7 +199,51 @@ async function openRuntimeDb(targetDir, options = {}) {
|
|
|
199
199
|
created_at TEXT NOT NULL,
|
|
200
200
|
FOREIGN KEY (task_key) REFERENCES tasks(task_key) ON DELETE SET NULL,
|
|
201
201
|
FOREIGN KEY (run_key) REFERENCES agent_runs(run_key) ON DELETE CASCADE
|
|
202
|
-
);
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
CREATE TABLE IF NOT EXISTS agent_execution_runs (
|
|
205
|
+
telemetry_run_id TEXT PRIMARY KEY,
|
|
206
|
+
dispatcher_run_id TEXT NOT NULL,
|
|
207
|
+
attempt_id TEXT NOT NULL,
|
|
208
|
+
feature TEXT NOT NULL,
|
|
209
|
+
agent TEXT NOT NULL,
|
|
210
|
+
host TEXT NOT NULL,
|
|
211
|
+
model TEXT NOT NULL,
|
|
212
|
+
model_requested TEXT,
|
|
213
|
+
model_resolved TEXT,
|
|
214
|
+
reasoning_effort TEXT,
|
|
215
|
+
model_resolution_strategy TEXT,
|
|
216
|
+
catalog_source TEXT,
|
|
217
|
+
state TEXT NOT NULL DEFAULT 'queued',
|
|
218
|
+
pid INTEGER,
|
|
219
|
+
process_fingerprint TEXT,
|
|
220
|
+
report_path TEXT,
|
|
221
|
+
report_digest TEXT,
|
|
222
|
+
reason TEXT,
|
|
223
|
+
truncated_bytes INTEGER NOT NULL DEFAULT 0,
|
|
224
|
+
dropped_events INTEGER NOT NULL DEFAULT 0,
|
|
225
|
+
created_at TEXT NOT NULL,
|
|
226
|
+
updated_at TEXT NOT NULL,
|
|
227
|
+
finished_at TEXT,
|
|
228
|
+
UNIQUE(feature, agent, dispatcher_run_id, attempt_id)
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
CREATE TABLE IF NOT EXISTS agent_execution_events (
|
|
232
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
233
|
+
telemetry_run_id TEXT NOT NULL,
|
|
234
|
+
sequence_no INTEGER NOT NULL,
|
|
235
|
+
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
236
|
+
event_type TEXT NOT NULL,
|
|
237
|
+
stream TEXT,
|
|
238
|
+
safe_summary TEXT NOT NULL DEFAULT '',
|
|
239
|
+
payload_json TEXT,
|
|
240
|
+
bytes INTEGER NOT NULL DEFAULT 0,
|
|
241
|
+
created_at TEXT NOT NULL,
|
|
242
|
+
UNIQUE(telemetry_run_id, sequence_no),
|
|
243
|
+
FOREIGN KEY (telemetry_run_id) REFERENCES agent_execution_runs(telemetry_run_id) ON DELETE CASCADE
|
|
244
|
+
);
|
|
245
|
+
CREATE INDEX IF NOT EXISTS idx_agent_execution_events_cursor ON agent_execution_events(telemetry_run_id, sequence_no);
|
|
246
|
+
CREATE INDEX IF NOT EXISTS idx_agent_execution_runs_feature_updated ON agent_execution_runs(feature, updated_at DESC);
|
|
203
247
|
|
|
204
248
|
CREATE TABLE IF NOT EXISTS artifacts (
|
|
205
249
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -650,8 +694,14 @@ function normalizeTaskStatus(value, fallback) {
|
|
|
650
694
|
return VALID_TASK_STATUSES.has(candidate) ? candidate : fallback;
|
|
651
695
|
}
|
|
652
696
|
|
|
653
|
-
function ensureLegacyColumns(db) {
|
|
654
|
-
const
|
|
697
|
+
function ensureLegacyColumns(db) {
|
|
698
|
+
const executionRunColumns = db.prepare('PRAGMA table_info(agent_execution_runs)').all();
|
|
699
|
+
const executionRunColumnNames = new Set(executionRunColumns.map((column) => column.name));
|
|
700
|
+
for (const column of ['model_requested', 'model_resolved', 'reasoning_effort', 'model_resolution_strategy', 'catalog_source']) {
|
|
701
|
+
if (!executionRunColumnNames.has(column)) db.exec(`ALTER TABLE agent_execution_runs ADD COLUMN ${column} TEXT`);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const taskColumns = db.prepare('PRAGMA table_info(tasks)').all();
|
|
655
705
|
const taskColumnNames = new Set(taskColumns.map((column) => column.name));
|
|
656
706
|
|
|
657
707
|
if (!taskColumnNames.has('task_kind')) {
|
|
@@ -2619,9 +2669,115 @@ function insertEvolutionLog(db, opts) {
|
|
|
2619
2669
|
return id;
|
|
2620
2670
|
}
|
|
2621
2671
|
|
|
2622
|
-
function listEvolutionLog(db, limit = 20) {
|
|
2672
|
+
function listEvolutionLog(db, limit = 20) {
|
|
2623
2673
|
return db.prepare('SELECT * FROM evolution_log ORDER BY applied_at DESC LIMIT ?').all(limit);
|
|
2624
|
-
}
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
const EXECUTION_TERMINAL_STATES = new Set(['passed', 'failed', 'cancelled']);
|
|
2677
|
+
const EXECUTION_TRANSITIONS = {
|
|
2678
|
+
queued: new Set(['running', 'waiting_report', 'paused', 'failed', 'cancelled']),
|
|
2679
|
+
running: new Set(['waiting_report', 'paused', 'failed', 'cancelled']),
|
|
2680
|
+
waiting_report: new Set(['passed', 'failed', 'correcting', 'paused']),
|
|
2681
|
+
correcting: new Set(['running', 'paused', 'failed', 'cancelled']),
|
|
2682
|
+
paused: new Set(['running', 'cancelled'])
|
|
2683
|
+
};
|
|
2684
|
+
|
|
2685
|
+
function executionCorrelation(input) {
|
|
2686
|
+
return {
|
|
2687
|
+
feature: String(input.feature), agent: String(input.agent),
|
|
2688
|
+
dispatcherRunId: String(input.dispatcher_run_id || input.run_id),
|
|
2689
|
+
attemptId: String(input.attempt_id)
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
function findExecutionRun(db, input) {
|
|
2694
|
+
const c = executionCorrelation(input);
|
|
2695
|
+
return db.prepare(`SELECT * FROM agent_execution_runs WHERE feature=? AND agent=? AND dispatcher_run_id=? AND attempt_id=?`)
|
|
2696
|
+
.get(c.feature, c.agent, c.dispatcherRunId, c.attemptId) || null;
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
function createExecutionRun(db, input) {
|
|
2700
|
+
const c = executionCorrelation(input); const now = nowIso();
|
|
2701
|
+
const id = input.telemetry_run_id || `aex-${slugify(c.feature)}-${require('node:crypto').randomUUID()}`;
|
|
2702
|
+
const modelResolved = String(input.model_resolved || input.model);
|
|
2703
|
+
const modelRequested = String(input.model_requested || input.model || '');
|
|
2704
|
+
db.prepare(`INSERT OR IGNORE INTO agent_execution_runs
|
|
2705
|
+
(telemetry_run_id,dispatcher_run_id,attempt_id,feature,agent,host,model,model_requested,model_resolved,reasoning_effort,model_resolution_strategy,catalog_source,state,created_at,updated_at)
|
|
2706
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id,c.dispatcherRunId,c.attemptId,c.feature,c.agent,String(input.host),modelResolved,modelRequested,modelResolved,input.reasoning_effort?String(input.reasoning_effort):null,input.model_resolution_strategy?String(input.model_resolution_strategy):null,input.catalog_source?String(input.catalog_source):null,'queued',now,now);
|
|
2707
|
+
const run = findExecutionRun(db,input);
|
|
2708
|
+
appendExecutionEvent(db,run.telemetry_run_id,{type:'run_created',safe_summary:'Execution queued'});
|
|
2709
|
+
return run;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
function appendExecutionEvent(db, runOrCorrelation, event) {
|
|
2713
|
+
const run = typeof runOrCorrelation === 'string'
|
|
2714
|
+
? db.prepare('SELECT * FROM agent_execution_runs WHERE telemetry_run_id=?').get(runOrCorrelation)
|
|
2715
|
+
: findExecutionRun(db,runOrCorrelation);
|
|
2716
|
+
if (!run) throw new Error('execution_run_not_found');
|
|
2717
|
+
const allowed = new Set(['run_created','process_started','state_changed','output','retry','fallback','timeout','report_attached','output_truncated','recovered','diagnostic']);
|
|
2718
|
+
if (!allowed.has(event.type)) throw new Error('execution_event_type_invalid');
|
|
2719
|
+
const sanitize=(value,depth=0)=>{if(depth>4)return'[TRUNCATED]';if(typeof value==='string')return value.replace(/(authorization\s*[:=]\s*(?:bearer\s+)?|api[_-]?key\s*[:=]\s*|token\s*[:=]\s*|password\s*[:=]\s*)[^\s,;]+/gi,'$1[REDACTED]').slice(0,2000);if(Array.isArray(value))return value.slice(0,25).map(v=>sanitize(v,depth+1));if(value&&typeof value==='object'){const out={};for(const key of Object.keys(value).slice(0,25))out[String(key).slice(0,100)]=sanitize(value[key],depth+1);return out}return value};
|
|
2720
|
+
const safeSummary=sanitize(String(event.safe_summary||''));const safePayload=event.payload?sanitize(event.payload):null;
|
|
2721
|
+
const tx=db.transaction(()=>{const seq=db.prepare('SELECT COALESCE(MAX(sequence_no),0)+1 n FROM agent_execution_events WHERE telemetry_run_id=?').get(run.telemetry_run_id).n;
|
|
2722
|
+
db.prepare(`INSERT INTO agent_execution_events(telemetry_run_id,sequence_no,event_type,stream,safe_summary,payload_json,bytes,created_at) VALUES(?,?,?,?,?,?,?,?)`)
|
|
2723
|
+
.run(run.telemetry_run_id,seq,event.type,event.stream||null,safeSummary,safePayload?JSON.stringify(safePayload).slice(0,16384):null,Math.min(Math.max(Number(event.bytes||0),0),16384),nowIso());return seq;});
|
|
2724
|
+
return tx();
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
function attachExecutionProcess(db, correlation, process) {
|
|
2728
|
+
const run=findExecutionRun(db,correlation); if(!run)throw new Error('execution_run_not_found');
|
|
2729
|
+
if(run.pid && run.pid!==process.pid)throw new Error('execution_process_conflict');
|
|
2730
|
+
db.prepare('UPDATE agent_execution_runs SET pid=?,process_fingerprint=?,state=?,updated_at=? WHERE telemetry_run_id=?')
|
|
2731
|
+
.run(Number(process.pid),String(process.fingerprint||''),'running',nowIso(),run.telemetry_run_id);
|
|
2732
|
+
appendExecutionEvent(db,run.telemetry_run_id,{type:'process_started',safe_summary:`Process ${process.pid} started`});
|
|
2733
|
+
return db.prepare('SELECT * FROM agent_execution_runs WHERE telemetry_run_id=?').get(run.telemetry_run_id);
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
function transitionExecutionRun(db, correlation, next, reason=null) {
|
|
2737
|
+
const run=findExecutionRun(db,correlation); if(!run)throw new Error('execution_run_not_found');
|
|
2738
|
+
if(run.state===next)return run;if(EXECUTION_TERMINAL_STATES.has(run.state)||!EXECUTION_TRANSITIONS[run.state]?.has(next))throw new Error('execution_state_transition_invalid');
|
|
2739
|
+
const safeReason=String(reason||'').replace(/(token|password|api[_-]?key)\s*[:=]\s*\S+/gi,'$1=[REDACTED]').slice(0,500)||null;
|
|
2740
|
+
const now=nowIso();db.prepare('UPDATE agent_execution_runs SET state=?,reason=?,updated_at=?,finished_at=? WHERE telemetry_run_id=? AND state=?')
|
|
2741
|
+
.run(next,safeReason,now,EXECUTION_TERMINAL_STATES.has(next)?now:null,run.telemetry_run_id,run.state);
|
|
2742
|
+
appendExecutionEvent(db,run.telemetry_run_id,{type:'state_changed',safe_summary:`${run.state} -> ${next}`,payload:{state:next,reason}});
|
|
2743
|
+
return db.prepare('SELECT * FROM agent_execution_runs WHERE telemetry_run_id=?').get(run.telemetry_run_id);
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
function attachExecutionReport(db, correlation, report) {
|
|
2747
|
+
const run=findExecutionRun(db,correlation);if(!run)throw new Error('execution_run_not_found');
|
|
2748
|
+
if(report.attempt_id!==run.attempt_id||report.run_id!==run.dispatcher_run_id)throw new Error('execution_report_binding_invalid');
|
|
2749
|
+
db.prepare('UPDATE agent_execution_runs SET report_path=?,report_digest=?,updated_at=? WHERE telemetry_run_id=?')
|
|
2750
|
+
.run(String(report.path||''),String(report.digest||''),nowIso(),run.telemetry_run_id);
|
|
2751
|
+
appendExecutionEvent(db,run.telemetry_run_id,{type:'report_attached',safe_summary:'Bound report attached'});return findExecutionRun(db,correlation);
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
function getExecutionSnapshot(db, filters={}) {
|
|
2755
|
+
const where=[],args=[];for(const [column,key] of [['feature','feature'],['agent','agent'],['state','state']])if(filters[key]){where.push(`${column}=?`);args.push(String(filters[key]));}
|
|
2756
|
+
const limit=Math.min(Math.max(Number(filters.limit)||50,1),200);
|
|
2757
|
+
return db.prepare(`SELECT * FROM agent_execution_runs ${where.length?'WHERE '+where.join(' AND '):''} ORDER BY updated_at DESC LIMIT ?`).all(...args,limit);
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
function listExecutionEvents(db, runId, options={}) {
|
|
2761
|
+
const after=Math.max(Number(options.after)||0,0),limit=Math.min(Math.max(Number(options.limit)||100,1),500);
|
|
2762
|
+
const rows=db.prepare('SELECT * FROM agent_execution_events WHERE telemetry_run_id=? AND sequence_no>? ORDER BY sequence_no LIMIT ?').all(runId,after,limit+1);
|
|
2763
|
+
const hasMore=rows.length>limit,events=hasMore?rows.slice(0,limit):rows;
|
|
2764
|
+
return {events,next_cursor:events.at(-1)?.sequence_no||after,has_more:hasMore};
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
function reconcileExecutionRun(db, correlation, probe=()=>false) {
|
|
2768
|
+
const run=findExecutionRun(db,correlation);if(!run)throw new Error('execution_run_not_found');
|
|
2769
|
+
if(EXECUTION_TERMINAL_STATES.has(run.state)||run.state==='paused')return run;
|
|
2770
|
+
if(!run.pid)return transitionExecutionRun(db,correlation,'paused','spawn_identity_missing');
|
|
2771
|
+
const observed=probe({pid:run.pid,fingerprint:run.process_fingerprint,created_at:run.created_at});
|
|
2772
|
+
const alive=observed===true?false:Boolean(observed?.alive&&observed?.fingerprint&&observed.fingerprint===run.process_fingerprint);
|
|
2773
|
+
if(alive)return transitionExecutionRun(db,correlation,'paused','detached_process');
|
|
2774
|
+
return transitionExecutionRun(db,correlation,'paused','process_not_alive');
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
function pruneExecutionTelemetry(db, options={}) {
|
|
2778
|
+
const days=Math.max(Number(options.retentionDays)||14,1),cutoff=new Date(Date.now()-days*86400000).toISOString(),batch=Math.min(Math.max(Number(options.batch)||100,1),1000);
|
|
2779
|
+
return db.prepare(`DELETE FROM agent_execution_runs WHERE telemetry_run_id IN (SELECT telemetry_run_id FROM agent_execution_runs WHERE updated_at < ? AND state IN ('passed','failed','cancelled','paused') ORDER BY updated_at LIMIT ?)` ).run(cutoff,batch).changes;
|
|
2780
|
+
}
|
|
2625
2781
|
|
|
2626
2782
|
module.exports = {
|
|
2627
2783
|
resolveRuntimePaths,
|
|
@@ -2725,5 +2881,8 @@ module.exports = {
|
|
|
2725
2881
|
listDynamicTools,
|
|
2726
2882
|
// Evolution Log CRUD
|
|
2727
2883
|
insertEvolutionLog,
|
|
2728
|
-
listEvolutionLog
|
|
2729
|
-
|
|
2884
|
+
listEvolutionLog
|
|
2885
|
+
,createExecutionRun, findExecutionRun, attachExecutionProcess, transitionExecutionRun,
|
|
2886
|
+
appendExecutionEvent, attachExecutionReport, getExecutionSnapshot, listExecutionEvents,
|
|
2887
|
+
reconcileExecutionRun, pruneExecutionTelemetry
|
|
2888
|
+
};
|
|
@@ -149,7 +149,7 @@ Also check `.aioson/plans/{slug}/manifest.md` before any implementation:
|
|
|
149
149
|
|
|
150
150
|
## Phase loop (auto-continue)
|
|
151
151
|
|
|
152
|
-
@dev runs a phased plan as **one continuous drive to the end of the feature, not one phase per turn.** Auto-continue is imperative (`phase_loop.auto_continue`): when a phase's gate is clean, go straight into the next phase — never stop to ask "continue?", never summarize-and-end, and **never self-issue `/compact
|
|
152
|
+
@dev runs a phased plan as **one continuous drive to the end of the feature, not one phase per turn.** Auto-continue is imperative (`phase_loop.auto_continue`): when a phase's gate is clean, go straight into the next phase — never stop to ask "continue?", never summarize-and-end, and **never self-issue `/compact`**. Per phase: `harness:check`, then `aioson verification:plan . --feature={slug} --trigger=per-phase --json`; dispatch each `run: true` entry through `aioson agent:execution:dispatch`, never by an assumed prompt-only subagent. `unsupported_capability` pauses honestly. A clean schema-valid report advances; bugs are fixed in-phase up to `max_fix_retries_per_phase`. `aioson dev:state:write` between phases is a resumable safety net. Full runtime smoke runs once at end-of-feature. Full protocol: `.aioson/docs/dev/phase-loop.md`.
|
|
153
153
|
|
|
154
154
|
## Context size detection
|
|
155
155
|
|
|
@@ -339,7 +339,8 @@ Only `@product` asks (the kickoff). Downstream agents (`@sheldon`/`@orchestrator
|
|
|
339
339
|
|
|
340
340
|
**Autopilot actions** (per `.aioson/docs/autopilot-handoff.md`):
|
|
341
341
|
1. Finish the PRD, the `features.md` line, and — MICRO (`→ @dev`) — the `## Dev handoff producer` `dev-state.md`.
|
|
342
|
-
2. Seed the contract (idempotent): `aioson workflow:execute . --feature={slug} --seed --tool=claude`. **Check the result.** A `different_active_feature` failure means another feature still holds `workflow.state.json`: surface it (close/pause it or `aioson feature:sweep .`) and stop with the manual handoff — a failed seed never arms the chain.
|
|
342
|
+
2. Seed the contract (idempotent): `aioson workflow:execute . --feature={slug} --seed --tool=claude`. **Check the result.** A `different_active_feature` failure means another feature still holds `workflow.state.json`: surface it (close/pause it or `aioson feature:sweep .`) and stop with the manual handoff — a failed seed never arms the chain.
|
|
343
|
+
The seed creates `agent-execution-{slug}.json`; mention editable defaults. Aliases are not model IDs.
|
|
343
344
|
3. Register closing duties (`agent:epilogue`/`agent:done`), emit `Autopilot: @product done → invoking @<next> (Ctrl+C to interrupt)`.
|
|
344
345
|
4. Invoke the lane's next stage: SMALL → `Skill(aioson:agent:sheldon)`; MEDIUM → `Skill(aioson:agent:orchestrator)`; MICRO → `Skill(aioson:agent:dev)`; site → `Skill(aioson:agent:copywriter)`. Task: `"continue feature {slug} — autopilot handoff from @product"`.
|
|
345
346
|
|
|
@@ -35,7 +35,13 @@ aioson workflow:execute . --feature={slug} --seed --tool=<tool>
|
|
|
35
35
|
|
|
36
36
|
**Seed failure is a stop condition.** The seeding agent must check the command result: a `different_active_feature` failure means another feature is genuinely active in `workflow.state.json` — surface it to the user (close/pause it, or `aioson feature:sweep .`) and stop with the manual handoff. Never continue the chain as if autopilot were armed when the seed failed.
|
|
37
37
|
|
|
38
|
-
The headless/tracked runner `aioson workflow:execute . --feature={slug} --tool=<tool> --agentic` (without `--seed`) is the same contract but also advances checkpoints from the CLI — use it for non-interactive runs. Prompt-level `Skill(...)` chaining is how interactive Claude Code / codex sessions consume the scheme.
|
|
38
|
+
The headless/tracked runner `aioson workflow:execute . --feature={slug} --tool=<tool> --agentic` (without `--seed`) is the same contract but also advances checkpoints from the CLI — use it for non-interactive runs. Prompt-level `Skill(...)` chaining is how interactive Claude Code / codex sessions consume the scheme.
|
|
39
|
+
|
|
40
|
+
Execution selection lives in `.aioson/context/agent-execution-{slug}.json`. Validate it before code with `aioson agent:execution:validate`; use `agent:execution:dispatch|resume` for execution. Generated manifests default to `external`: the installed Claude/Codex/OpenCode CLI runs headlessly in a fresh process and writes a bound report. Native subagent/fresh-session modes require an explicit bridge capability; prompt-level chaining is not evidence. The core cannot force a client to open a visible interactive chat window.
|
|
41
|
+
|
|
42
|
+
Codex entries may use a canonical slug or a human form such as `"model": "GPT 5.6 Terra"`, plus an optional `"reasoning_effort": "high"`. Validation resolves the current local Codex model catalog in conservative tiers: exact slug, normalized display name, unique alias, then a uniquely safe fuzzy match. Version numbers must remain identical, and ambiguous matches pause before process spawn. The manifest remains unchanged; state, reports, CLI output, and telemetry keep `model_requested`, `model_resolved`, and `model_resolution_strategy` separately. When the catalog is unavailable, only `configured-default` and literal model IDs are accepted as unverified fallbacks. Explicit reasoning effort is never silently downgraded or moved to another provider.
|
|
43
|
+
|
|
44
|
+
Cross-repository writes are opt-in per agent through `writable_roots`. Every path must exist, be a directory, contain no traversal, and is canonicalized before dispatch and recorded in state/report. Codex maps roots to repeated `exec --add-dir <absolute>` argv; Claude maps to `--add-dir`. OpenCode currently has no verified additional-writable-root flag, so a non-empty list returns `host_capability_missing` rather than widening access silently.
|
|
39
45
|
|
|
40
46
|
## Routing — deterministic, never LLM-chosen
|
|
41
47
|
|
|
@@ -21,9 +21,8 @@ After finishing each phase:
|
|
|
21
21
|
```bash
|
|
22
22
|
aioson verification:plan . --feature={slug} --trigger=per-phase --json
|
|
23
23
|
```
|
|
24
|
-
For every agent with `run: true`, dispatch
|
|
25
|
-
- `mode:
|
|
26
|
-
- `mode: external` → only the explicitly configured cross-vendor auditor (`cross_check`); never spawn one otherwise.
|
|
24
|
+
For every agent with `run: true`, use `aioson agent:execution:dispatch . --feature={slug} --agent={agent} --json`. The resolved manifest is authoritative for host/model/mode. Validate model aliases before dispatch and preserve the distinct requested/resolved model, resolution strategy, and optional reasoning effort in state and reports; ambiguity or an unsupported effort is a real pause before spawn. A `unsupported_capability` or `manifest_invalid` result is also a real pause: never imitate a sub-agent or fresh session in prose. Only `external` execution backed by an installed CLI, or a native capability explicitly exposed by the current harness, may run.
|
|
25
|
+
- `mode: external` → the portable default: an installed host CLI runs headlessly in a fresh process, waits for exit, and must write the bound JSON report. This creates an isolated headless context, not a new interactive chat window.
|
|
27
26
|
Read the report: **PASS** → continue. **Bugs** → fix them within this phase, re-run `harness:check`, and re-dispatch — up to `phase_loop.max_fix_retries_per_phase` times, then stop and surface the failure instead of advancing.
|
|
28
27
|
4. **Checkpoint, then keep going — do NOT end the turn.** Write the cold-start packet as a crash/interrupt safety net:
|
|
29
28
|
```bash
|