@jaimevalasek/aioson 1.6.0 → 1.7.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 +49 -0
- package/README.md +729 -232
- package/docs/design-previews/pt.squarespace.com-homepage.html +889 -0
- package/docs/integrations/sdlc-genius-boundary.md +76 -0
- package/docs/integrations/sdlc-genius-eval-matrix.md +75 -0
- package/docs/integrations/sdlc-genius-install-checklist.md +93 -0
- package/docs/integrations/sdlc-genius-review-samples.md +86 -0
- package/docs/pt/README.md +3 -0
- package/docs/pt/agentes.md +1 -0
- package/docs/pt/comandos-cli.md +888 -2
- package/docs/pt/design-hybrid-forge.md +255 -6
- package/docs/pt/devlog-pipeline.md +270 -0
- package/docs/pt/fluxo-artefatos.md +178 -0
- package/docs/pt/hooks-session-guard.md +454 -0
- package/docs/pt/monitor-de-contexto.md +59 -5
- package/docs/pt/sdd-automation-scripts.md +557 -0
- package/docs/pt/site-forge.md +309 -0
- package/docs/pt/spec-learnings-pipeline.md +265 -0
- package/package.json +1 -1
- package/src/a2a/client.js +165 -0
- package/src/a2a/server.js +223 -0
- package/src/cli.js +235 -1
- package/src/commands/agent-audit.js +397 -0
- package/src/commands/agent-export-skill.js +229 -0
- package/src/commands/artifact-validate.js +189 -0
- package/src/commands/brief-gen.js +405 -0
- package/src/commands/brief-validate.js +65 -0
- package/src/commands/classify.js +256 -0
- package/src/commands/context-compact.js +49 -0
- package/src/commands/context-health.js +175 -0
- package/src/commands/context-monitor.js +71 -0
- package/src/commands/context-trim.js +177 -0
- package/src/commands/detect-test-runner.js +55 -0
- package/src/commands/devlog-export-brains.js +27 -0
- package/src/commands/devlog-process.js +292 -0
- package/src/commands/devlog-watch.js +131 -0
- package/src/commands/feature-close.js +165 -0
- package/src/commands/gate-check.js +228 -0
- package/src/commands/hooks-emit.js +253 -0
- package/src/commands/hooks-install.js +347 -0
- package/src/commands/learning-auto-promote.js +195 -0
- package/src/commands/learning-evolve.js +18 -9
- package/src/commands/learning-export.js +103 -0
- package/src/commands/learning-rollback.js +164 -0
- package/src/commands/live.js +25 -1
- package/src/commands/pattern-detect.js +33 -0
- package/src/commands/preflight-context.js +30 -0
- package/src/commands/preflight.js +208 -0
- package/src/commands/pulse-update.js +130 -0
- package/src/commands/runner-daemon.js +274 -0
- package/src/commands/runner-plan.js +70 -0
- package/src/commands/runner-queue-from-plan.js +166 -0
- package/src/commands/runner-queue.js +189 -0
- package/src/commands/runner-run.js +129 -0
- package/src/commands/runtime.js +47 -1
- package/src/commands/self-implement-loop.js +256 -0
- package/src/commands/session-guard.js +218 -0
- package/src/commands/sizing.js +165 -0
- package/src/commands/skill.js +65 -0
- package/src/commands/spec-checkpoint.js +177 -0
- package/src/commands/spec-status.js +79 -0
- package/src/commands/spec-sync.js +190 -0
- package/src/commands/spec-tasks.js +288 -0
- package/src/commands/squad-autorun.js +1220 -0
- package/src/commands/squad-bus.js +217 -0
- package/src/commands/squad-card.js +149 -0
- package/src/commands/squad-daemon.js +134 -0
- package/src/commands/squad-dependency-graph.js +164 -0
- package/src/commands/squad-review.js +106 -0
- package/src/commands/squad-scaffold.js +55 -0
- package/src/commands/squad-tool-register.js +157 -0
- package/src/commands/state-save.js +122 -0
- package/src/commands/update.js +2 -0
- package/src/commands/verify-gate.js +572 -0
- package/src/commands/workflow-execute.js +241 -0
- package/src/constants.js +9 -0
- package/src/install-profile.js +2 -2
- package/src/install-wizard.js +3 -2
- package/src/installer.js +6 -0
- package/src/lib/health-check.js +158 -0
- package/src/lib/hook-protocol.js +76 -0
- package/src/mcp/apps/squad-dashboard/app.js +163 -0
- package/src/mcp/apps/squad-dashboard/index.html +261 -0
- package/src/mcp/apps/squad-dashboard/mcp-manifest.json +23 -0
- package/src/mcp/resources/squad-state.js +130 -0
- package/src/preflight-engine.js +443 -0
- package/src/runner/cascade.js +97 -0
- package/src/runner/cli-launcher.js +109 -0
- package/src/runner/plan-importer.js +63 -0
- package/src/runner/queue-store.js +159 -0
- package/src/runtime-store.js +61 -3
- package/src/squad/agent-teams-adapter.js +264 -0
- package/src/squad/brief-validator.js +350 -0
- package/src/squad/bus-bridge.js +140 -0
- package/src/squad/context-compactor.js +265 -0
- package/src/squad/cross-ai-synthesizer.js +250 -0
- package/src/squad/hooks-generator.js +196 -0
- package/src/squad/inter-squad-events.js +175 -0
- package/src/squad/intra-bus.js +345 -0
- package/src/squad/learning-extractor.js +213 -0
- package/src/squad/pattern-detector.js +365 -0
- package/src/squad/preflight-context.js +296 -0
- package/src/squad/recovery-context.js +242 -71
- package/src/squad/reflection.js +365 -0
- package/src/squad/squad-scaffold.js +177 -0
- package/src/squad/state-manager.js +310 -0
- package/src/squad/task-decomposer.js +652 -0
- package/src/squad/verify-gate.js +303 -0
- package/src/updater.js +4 -5
- package/src/worker-runner.js +186 -1
- package/template/.aioson/agents/analyst.md +62 -1
- package/template/.aioson/agents/architect.md +61 -1
- package/template/.aioson/agents/design-hybrid-forge.md +14 -0
- package/template/.aioson/agents/dev.md +242 -24
- package/template/.aioson/agents/deyvin.md +66 -8
- package/template/.aioson/agents/discovery-design-doc.md +44 -0
- package/template/.aioson/agents/genome.md +14 -0
- package/template/.aioson/agents/neo.md +78 -1
- package/template/.aioson/agents/orache.md +50 -4
- package/template/.aioson/agents/orchestrator.md +197 -1
- package/template/.aioson/agents/pm.md +35 -0
- package/template/.aioson/agents/product.md +50 -5
- package/template/.aioson/agents/profiler-enricher.md +14 -0
- package/template/.aioson/agents/profiler-forge.md +14 -0
- package/template/.aioson/agents/profiler-researcher.md +14 -0
- package/template/.aioson/agents/qa.md +172 -21
- package/template/.aioson/agents/setup.md +79 -9
- package/template/.aioson/agents/sheldon.md +131 -6
- package/template/.aioson/agents/site-forge.md +1753 -0
- package/template/.aioson/agents/squad.md +162 -0
- package/template/.aioson/agents/tester.md +53 -0
- package/template/.aioson/agents/ux-ui.md +34 -1
- package/template/.aioson/brains/README.md +128 -0
- package/template/.aioson/brains/_index.json +16 -0
- package/template/.aioson/brains/scripts/query.js +103 -0
- package/template/.aioson/brains/site-forge/visual-patterns.brain.json +205 -0
- package/template/.aioson/config.md +143 -13
- package/template/.aioson/constitution.md +33 -0
- package/template/.aioson/context/project-pulse.md +34 -0
- package/template/.aioson/docs/LAYERS.md +79 -0
- package/template/.aioson/docs/README.md +76 -0
- package/template/.aioson/docs/example-external-api-context.md +72 -0
- package/template/.aioson/locales/en/agents/architect.md +17 -0
- package/template/.aioson/locales/en/agents/dev.md +79 -13
- package/template/.aioson/locales/en/agents/orache.md +6 -0
- package/template/.aioson/locales/en/agents/orchestrator.md +24 -0
- package/template/.aioson/locales/en/agents/product.md +50 -0
- package/template/.aioson/locales/en/agents/sheldon.md +115 -0
- package/template/.aioson/locales/en/agents/squad.md +14 -0
- package/template/.aioson/locales/en/agents/tester.md +6 -0
- package/template/.aioson/locales/es/agents/analyst.md +2 -0
- package/template/.aioson/locales/es/agents/architect.md +19 -0
- package/template/.aioson/locales/es/agents/dev.md +64 -4
- package/template/.aioson/locales/es/agents/deyvin.md +2 -0
- package/template/.aioson/locales/es/agents/discovery-design-doc.md +2 -0
- package/template/.aioson/locales/es/agents/genome.md +2 -0
- package/template/.aioson/locales/es/agents/neo.md +2 -0
- package/template/.aioson/locales/es/agents/orache.md +2 -0
- package/template/.aioson/locales/es/agents/orchestrator.md +26 -0
- package/template/.aioson/locales/es/agents/pair.md +2 -0
- package/template/.aioson/locales/es/agents/pm.md +2 -0
- package/template/.aioson/locales/es/agents/product.md +52 -0
- package/template/.aioson/locales/es/agents/profiler-enricher.md +2 -0
- package/template/.aioson/locales/es/agents/profiler-forge.md +2 -0
- package/template/.aioson/locales/es/agents/profiler-researcher.md +2 -0
- package/template/.aioson/locales/es/agents/qa.md +2 -0
- package/template/.aioson/locales/es/agents/setup.md +2 -0
- package/template/.aioson/locales/es/agents/sheldon.md +117 -0
- package/template/.aioson/locales/es/agents/squad.md +16 -0
- package/template/.aioson/locales/es/agents/tester.md +9 -0
- package/template/.aioson/locales/es/agents/ux-ui.md +2 -0
- package/template/.aioson/locales/fr/agents/analyst.md +2 -0
- package/template/.aioson/locales/fr/agents/architect.md +19 -0
- package/template/.aioson/locales/fr/agents/dev.md +64 -4
- package/template/.aioson/locales/fr/agents/deyvin.md +2 -0
- package/template/.aioson/locales/fr/agents/discovery-design-doc.md +2 -0
- package/template/.aioson/locales/fr/agents/genome.md +2 -0
- package/template/.aioson/locales/fr/agents/neo.md +2 -0
- package/template/.aioson/locales/fr/agents/orache.md +2 -0
- package/template/.aioson/locales/fr/agents/orchestrator.md +26 -0
- package/template/.aioson/locales/fr/agents/pair.md +2 -0
- package/template/.aioson/locales/fr/agents/pm.md +2 -0
- package/template/.aioson/locales/fr/agents/product.md +52 -0
- package/template/.aioson/locales/fr/agents/profiler-enricher.md +2 -0
- package/template/.aioson/locales/fr/agents/profiler-forge.md +2 -0
- package/template/.aioson/locales/fr/agents/profiler-researcher.md +2 -0
- package/template/.aioson/locales/fr/agents/qa.md +2 -0
- package/template/.aioson/locales/fr/agents/setup.md +2 -0
- package/template/.aioson/locales/fr/agents/sheldon.md +117 -0
- package/template/.aioson/locales/fr/agents/squad.md +16 -0
- package/template/.aioson/locales/fr/agents/tester.md +9 -0
- package/template/.aioson/locales/fr/agents/ux-ui.md +2 -0
- package/template/.aioson/locales/pt-BR/agents/analyst.md +64 -3
- package/template/.aioson/locales/pt-BR/agents/architect.md +42 -0
- package/template/.aioson/locales/pt-BR/agents/dev.md +147 -14
- package/template/.aioson/locales/pt-BR/agents/deyvin.md +47 -0
- package/template/.aioson/locales/pt-BR/agents/neo.md +62 -1
- package/template/.aioson/locales/pt-BR/agents/orchestrator.md +158 -2
- package/template/.aioson/locales/pt-BR/agents/pm.md +95 -1
- package/template/.aioson/locales/pt-BR/agents/product.md +145 -18
- package/template/.aioson/locales/pt-BR/agents/qa.md +16 -0
- package/template/.aioson/locales/pt-BR/agents/setup.md +101 -18
- package/template/.aioson/locales/pt-BR/agents/sheldon.md +132 -1
- package/template/.aioson/locales/pt-BR/agents/squad.md +14 -0
- package/template/.aioson/locales/pt-BR/agents/tester.md +449 -0
- package/template/.aioson/rules/README.md +69 -0
- package/template/.aioson/rules/data-format-convention.md +136 -0
- package/template/.aioson/rules/example-monetary-values.md +30 -0
- package/template/.aioson/schemas/squad-manifest.schema.json +124 -3
- package/template/.aioson/skills/design/pt.squarespace.com/.skill-meta.json +31 -0
- package/template/.aioson/skills/design/pt.squarespace.com/SKILL.md +66 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/components.md +368 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/design-tokens.md +150 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/motion.md +270 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/patterns.md +189 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/websites.md +165 -0
- package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +1 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/analyst.md +30 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/architect.md +23 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +47 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/deyvin.md +27 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/maintenance-and-state.md +35 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/product.md +25 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/qa.md +30 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/sheldon.md +25 -0
- package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +4 -1
- package/template/.aioson/skills/process/design-hybrid-forge/references/output-contract.md +15 -0
- package/template/.aioson/skills/process/design-hybrid-forge/references/pair-compatibility.md +32 -0
- package/template/.aioson/skills/process/design-hybrid-forge/references/quality-gates.md +20 -0
- package/template/.aioson/skills/process/simplify/SKILL.md +173 -0
- package/template/.aioson/skills/static/context-budget-guide.md +46 -0
- package/template/.aioson/skills/static/harness-sensors.md +74 -0
- package/template/.aioson/skills/static/multi-agent-patterns.md +43 -0
- package/template/.aioson/skills/static/react-motion-patterns.md +22 -0
- package/template/.aioson/skills/static/static-html-patterns/checklists.md +43 -0
- package/template/.aioson/skills/static/static-html-patterns/css-tokens.md +609 -0
- package/template/.aioson/skills/static/static-html-patterns/motion.md +193 -0
- package/template/.aioson/skills/static/static-html-patterns/premium.md +711 -0
- package/template/.aioson/skills/static/static-html-patterns/structure.md +209 -0
- package/template/.aioson/skills/static/static-html-patterns/utilities.md +190 -0
- package/template/.aioson/skills/static/static-html-patterns.md +58 -1913
- package/template/.aioson/skills/static/threejs-patterns.md +929 -0
- package/template/.aioson/skills/static/web-research-cache.md +112 -0
- package/template/.aioson/tasks/implementation-plan.md +21 -1
- package/template/.claude/commands/aioson/agent/design-hybrid-forge.md +5 -0
- package/template/.claude/commands/aioson/agent/orache.md +5 -0
- package/template/.claude/commands/aioson/agent/sheldon.md +5 -0
- package/template/.claude/commands/aioson/agent/site-forge.md +5 -0
- package/template/AGENTS.md +55 -3
- package/template/CLAUDE.md +30 -0
- package/template/OPENCODE.md +4 -0
- package/template/researchs/.gitkeep +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { launchCLI, detectCLI } = require('../runner/cli-launcher');
|
|
5
|
+
const { runWithCascade, parseCascadeChain } = require('../runner/cascade');
|
|
6
|
+
const { openRuntimeDb, startRun, updateRun, createRunKey } = require('../runtime-store');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* aioson runner:run — executa uma única task headless usando o CLI de AI ativo.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* aioson runner:run . --task="Fix the auth modal" --agent=dev
|
|
13
|
+
* aioson runner:run . --task="..." --agent=qa --timeout=300
|
|
14
|
+
* aioson runner:run . --task="..." --agent=dev --cascade=haiku,sonnet
|
|
15
|
+
* aioson runner:run . --task="..." --dry-run
|
|
16
|
+
*/
|
|
17
|
+
async function runRunnerRun({ args, options = {}, logger }) {
|
|
18
|
+
const projectDir = path.resolve(process.cwd(), args[0] || '.');
|
|
19
|
+
const { task, agent = 'dev', dryRun, cascade: cascadeStr } = options;
|
|
20
|
+
const timeout = options.timeout ? Number(options.timeout) * 1000 : 120000;
|
|
21
|
+
|
|
22
|
+
if (!task) {
|
|
23
|
+
logger.error('--task is required. Example: aioson runner:run . --task="Fix the login modal"');
|
|
24
|
+
return { ok: false };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const agentFile = path.join(projectDir, '.aioson', 'agents', `${agent}.md`);
|
|
28
|
+
const prompt = buildRunnerPrompt(task, agentFile);
|
|
29
|
+
|
|
30
|
+
if (dryRun) {
|
|
31
|
+
let cli;
|
|
32
|
+
try { cli = await detectCLI(); } catch { cli = 'claude'; }
|
|
33
|
+
logger.log(`[dry-run] Would run: ${cli} -p "${prompt.slice(0, 120)}..."`);
|
|
34
|
+
logger.log(`[dry-run] Agent: @${agent} | Timeout: ${timeout / 1000}s`);
|
|
35
|
+
if (cascadeStr) logger.log(`[dry-run] Cascade: ${cascadeStr}`);
|
|
36
|
+
return { ok: true, dryRun: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
logger.log(`[runner] Task: ${task}`);
|
|
40
|
+
logger.log(`[runner] Agent: @${agent} | Timeout: ${timeout / 1000}s`);
|
|
41
|
+
if (cascadeStr) logger.log(`[runner] Cascade: ${cascadeStr}`);
|
|
42
|
+
|
|
43
|
+
const start = Date.now();
|
|
44
|
+
|
|
45
|
+
// Abre DB para registrar evento (melhor esforço — não bloqueia se não existir)
|
|
46
|
+
let db = null;
|
|
47
|
+
let runKey = null;
|
|
48
|
+
try {
|
|
49
|
+
const handle = await openRuntimeDb(projectDir, {});
|
|
50
|
+
if (handle) {
|
|
51
|
+
db = handle.db;
|
|
52
|
+
runKey = createRunKey(agent);
|
|
53
|
+
startRun(db, {
|
|
54
|
+
runKey,
|
|
55
|
+
agentName: agent,
|
|
56
|
+
agentKind: 'runner',
|
|
57
|
+
source: 'runner',
|
|
58
|
+
title: task.slice(0, 80),
|
|
59
|
+
status: 'running',
|
|
60
|
+
message: `Runner task started: ${task.slice(0, 80)}`
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
} catch { /* dashboard logging is best-effort */ }
|
|
64
|
+
|
|
65
|
+
let result;
|
|
66
|
+
const cascadeChain = parseCascadeChain(cascadeStr);
|
|
67
|
+
|
|
68
|
+
if (cascadeChain.length > 0) {
|
|
69
|
+
const cascadeResult = await runWithCascade(projectDir, prompt, cascadeChain, {
|
|
70
|
+
timeout,
|
|
71
|
+
onProgress: ({ model, attempt, maxAttempts, status, reason }) => {
|
|
72
|
+
if (status === 'running') {
|
|
73
|
+
logger.log(`[cascade] ${model} attempt ${attempt}/${maxAttempts}...`);
|
|
74
|
+
} else if (status === 'gate_failed') {
|
|
75
|
+
logger.log(`[cascade] ${model} attempt ${attempt} — gate failed${reason ? ': ' + reason : ''}, escalating...`);
|
|
76
|
+
} else if (status === 'cli_failed') {
|
|
77
|
+
logger.log(`[cascade] ${model} attempt ${attempt} — CLI failed, retrying...`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
if (cascadeResult.ok) {
|
|
82
|
+
result = cascadeResult.result;
|
|
83
|
+
logger.log(`[runner] Completed via ${cascadeResult.modelUsed} (attempt ${cascadeResult.attempts})`);
|
|
84
|
+
} else {
|
|
85
|
+
result = { ok: false, output: '', completionMarker: false };
|
|
86
|
+
logger.error(`[runner] ${cascadeResult.error}`);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
result = await launchCLI(projectDir, prompt, {
|
|
90
|
+
timeout,
|
|
91
|
+
onData: (chunk) => process.stdout.write(chunk)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const elapsed = Math.round((Date.now() - start) / 1000);
|
|
96
|
+
const status = result.ok ? 'completed' : 'failed';
|
|
97
|
+
|
|
98
|
+
logger.log(`\n[runner] Task ${status} in ${elapsed}s`);
|
|
99
|
+
if (result.completionMarker) logger.log('[runner] TASK_COMPLETE marker detected');
|
|
100
|
+
|
|
101
|
+
// Atualiza run no dashboard
|
|
102
|
+
if (db && runKey) {
|
|
103
|
+
try {
|
|
104
|
+
updateRun(db, {
|
|
105
|
+
runKey,
|
|
106
|
+
status,
|
|
107
|
+
message: `Runner task ${status}: ${task.slice(0, 80)}`,
|
|
108
|
+
payload: { task, agent, elapsed, ok: result.ok, completionMarker: result.completionMarker }
|
|
109
|
+
});
|
|
110
|
+
} catch { /* best-effort */ }
|
|
111
|
+
try { db.close(); } catch { /* noop */ }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { ok: result.ok, output: result.output, elapsed, completionMarker: result.completionMarker };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildRunnerPrompt(task, agentFile) {
|
|
118
|
+
return [
|
|
119
|
+
'You are operating in autonomous headless mode. Complete the following task independently.',
|
|
120
|
+
`Agent role: read ${agentFile} for your operating instructions.`,
|
|
121
|
+
'',
|
|
122
|
+
`Task: ${task}`,
|
|
123
|
+
'',
|
|
124
|
+
'When the task is complete, write TASK_COMPLETE on a new line as the final output.',
|
|
125
|
+
'If you cannot complete the task, write TASK_FAILED: [reason].'
|
|
126
|
+
].join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { runRunnerRun };
|
package/src/commands/runtime.js
CHANGED
|
@@ -1179,6 +1179,11 @@ async function runAgentDone({ args, options = {}, logger, t }) {
|
|
|
1179
1179
|
const summary = String(options.summary || options.message || `${normalizedAgent} session completed`).trim();
|
|
1180
1180
|
const title = options.title ? String(options.title).trim() : null;
|
|
1181
1181
|
const status = options.status || 'completed';
|
|
1182
|
+
const verdict = options.verdict ? String(options.verdict).trim().toUpperCase() : null;
|
|
1183
|
+
const planStepId = options['plan-step'] ? String(options['plan-step']).trim() : null;
|
|
1184
|
+
const artifactPaths = options.artifacts
|
|
1185
|
+
? String(options.artifacts).split(',').map((p) => p.trim()).filter(Boolean)
|
|
1186
|
+
: [];
|
|
1182
1187
|
|
|
1183
1188
|
const { db, dbPath, runtimeDir } = await openRuntimeDb(targetDir);
|
|
1184
1189
|
|
|
@@ -1194,9 +1199,24 @@ async function runAgentDone({ args, options = {}, logger, t }) {
|
|
|
1194
1199
|
eventType: 'agent_done',
|
|
1195
1200
|
phase: 'live',
|
|
1196
1201
|
status: 'running',
|
|
1197
|
-
message: summary
|
|
1202
|
+
message: summary,
|
|
1203
|
+
verdict,
|
|
1204
|
+
planStepId
|
|
1198
1205
|
});
|
|
1199
1206
|
|
|
1207
|
+
if (artifactPaths.length > 0) {
|
|
1208
|
+
for (const filePath of artifactPaths) {
|
|
1209
|
+
try {
|
|
1210
|
+
attachArtifact(db, {
|
|
1211
|
+
runKey: session.runKey,
|
|
1212
|
+
agentName: normalizedAgent,
|
|
1213
|
+
kind: 'output',
|
|
1214
|
+
filePath
|
|
1215
|
+
});
|
|
1216
|
+
} catch { /* non-fatal */ }
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1200
1220
|
if (!options.json) {
|
|
1201
1221
|
logger.log(`agent:done — ${normalizedAgent} | live session active, event logged | run: ${session.runKey} (${dbPath})`);
|
|
1202
1222
|
}
|
|
@@ -1219,6 +1239,32 @@ async function runAgentDone({ args, options = {}, logger, t }) {
|
|
|
1219
1239
|
summary
|
|
1220
1240
|
});
|
|
1221
1241
|
|
|
1242
|
+
if (verdict || planStepId) {
|
|
1243
|
+
appendRunEvent(db, {
|
|
1244
|
+
runKey,
|
|
1245
|
+
eventType: 'agent_done',
|
|
1246
|
+
phase: 'direct',
|
|
1247
|
+
status: 'completed',
|
|
1248
|
+
message: summary,
|
|
1249
|
+
verdict,
|
|
1250
|
+
planStepId
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
if (artifactPaths.length > 0) {
|
|
1255
|
+
for (const filePath of artifactPaths) {
|
|
1256
|
+
try {
|
|
1257
|
+
attachArtifact(db, {
|
|
1258
|
+
runKey,
|
|
1259
|
+
taskKey,
|
|
1260
|
+
agentName: normalizedAgent,
|
|
1261
|
+
kind: 'output',
|
|
1262
|
+
filePath
|
|
1263
|
+
});
|
|
1264
|
+
} catch { /* non-fatal */ }
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1222
1268
|
if (!options.json) {
|
|
1223
1269
|
logger.log(`agent:done — ${normalizedAgent} | task: ${taskKey} | run: ${runKey} (${dbPath})`);
|
|
1224
1270
|
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson self:loop — Autonomous implement + verify loop
|
|
5
|
+
*
|
|
6
|
+
* Runs an agent task, verifies with verify-gate in a fresh context,
|
|
7
|
+
* and retries with feedback injection on failure.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* aioson self:loop . --agent=dev --task="implement stripe webhook handler" --max-iterations=3
|
|
11
|
+
* aioson self:loop . --agent=dev --task="..." --verification-criteria="all tests pass"
|
|
12
|
+
* aioson self:loop . --agent=dev --task="..." --spec=briefs/phase-1.md --artifact=src/
|
|
13
|
+
*
|
|
14
|
+
* Integrates with:
|
|
15
|
+
* - verify-gate.js for tiered verification (tiers 1–4)
|
|
16
|
+
* - intra-bus.js for recording attempts
|
|
17
|
+
* - state-manager.js for recording final result
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
const { execSync, execFileSync } = require('node:child_process');
|
|
22
|
+
const { randomUUID } = require('node:crypto');
|
|
23
|
+
const fs = require('node:fs/promises');
|
|
24
|
+
|
|
25
|
+
const bus = require('../squad/intra-bus');
|
|
26
|
+
const stateManager = require('../squad/state-manager');
|
|
27
|
+
|
|
28
|
+
// ─── Agent execution ─────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Execute an agent task via the aioson CLI.
|
|
32
|
+
* Returns { ok, output, error }.
|
|
33
|
+
*/
|
|
34
|
+
function executeAgent(projectDir, agent, task, feedbackContext, timeoutMs) {
|
|
35
|
+
const prompt = feedbackContext
|
|
36
|
+
? `${task}\n\n---\nPrevious attempt feedback:\n${feedbackContext}`
|
|
37
|
+
: task;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const output = execSync(
|
|
41
|
+
`aioson agent:prompt ${agent} . --tool=claude`,
|
|
42
|
+
{
|
|
43
|
+
cwd: projectDir,
|
|
44
|
+
input: prompt,
|
|
45
|
+
timeout: timeoutMs,
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
maxBuffer: 1024 * 1024 * 5,
|
|
48
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
return { ok: true, output: output.trim() };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return { ok: false, output: '', error: err.message.slice(0, 500) };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run verify-gate on the result.
|
|
59
|
+
* Returns { ok, verdict, issues }.
|
|
60
|
+
*/
|
|
61
|
+
async function runVerification(projectDir, spec, artifact, criteria) {
|
|
62
|
+
if (!spec || !artifact) {
|
|
63
|
+
// Fallback: criteria-only verification
|
|
64
|
+
return criteriaOnlyVerify(projectDir, artifact, criteria);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const output = execSync(
|
|
69
|
+
`aioson verify:gate . --spec="${spec}" --artifact="${artifact}" --json`,
|
|
70
|
+
{
|
|
71
|
+
cwd: projectDir,
|
|
72
|
+
timeout: 30_000,
|
|
73
|
+
encoding: 'utf8',
|
|
74
|
+
maxBuffer: 1024 * 1024
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
const result = JSON.parse(output);
|
|
78
|
+
return {
|
|
79
|
+
ok: result.verdict === 'PASS' || result.verdict === 'PASS_WITH_NOTES',
|
|
80
|
+
verdict: result.verdict,
|
|
81
|
+
issues: result.issues || []
|
|
82
|
+
};
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return { ok: false, verdict: 'BLOCKED', issues: [{ message: err.message.slice(0, 200) }] };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Simple criteria-based verification when no spec/artifact is given.
|
|
90
|
+
* Checks if verification criteria strings are present in the output directory.
|
|
91
|
+
*/
|
|
92
|
+
async function criteriaOnlyVerify(projectDir, artifactPath, criteria) {
|
|
93
|
+
if (!criteria) {
|
|
94
|
+
return { ok: true, verdict: 'PASS', issues: [] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const issues = [];
|
|
98
|
+
const criteriaList = criteria.split(',').map((c) => c.trim());
|
|
99
|
+
|
|
100
|
+
for (const criterion of criteriaList) {
|
|
101
|
+
if (/test/i.test(criterion)) {
|
|
102
|
+
// Check if tests pass
|
|
103
|
+
try {
|
|
104
|
+
execSync('npm test --if-present 2>&1', {
|
|
105
|
+
cwd: projectDir,
|
|
106
|
+
timeout: 60_000,
|
|
107
|
+
encoding: 'utf8'
|
|
108
|
+
});
|
|
109
|
+
} catch {
|
|
110
|
+
issues.push({ message: `Test criterion failed: "${criterion}"` });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
ok: issues.length === 0,
|
|
117
|
+
verdict: issues.length === 0 ? 'PASS' : 'FAIL_WITH_ISSUES',
|
|
118
|
+
issues
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Run the self-implement loop.
|
|
126
|
+
*
|
|
127
|
+
* @param {object} params
|
|
128
|
+
* @returns {Promise<object>} — { ok, iterations, verdict, feedback[] }
|
|
129
|
+
*/
|
|
130
|
+
async function runSelfLoop({ args, options = {}, logger }) {
|
|
131
|
+
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
132
|
+
const agent = String(options.agent || options.a || 'dev').trim();
|
|
133
|
+
const task = String(options.task || options.t || '').trim();
|
|
134
|
+
const maxIterations = Math.min(Math.max(Number(options['max-iterations'] || 3), 1), 5);
|
|
135
|
+
const spec = options.spec ? String(options.spec).trim() : null;
|
|
136
|
+
const artifact = options.artifact ? String(options.artifact).trim() : null;
|
|
137
|
+
const criteria = options['verification-criteria'] || options.criteria || '';
|
|
138
|
+
const squad = options.squad ? String(options.squad).trim() : null;
|
|
139
|
+
const timeoutMs = (Number(options.timeout) || 300) * 1000;
|
|
140
|
+
|
|
141
|
+
if (!task) {
|
|
142
|
+
logger.error('Error: --task is required');
|
|
143
|
+
return { ok: false, error: 'missing_task' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const sessionId = randomUUID();
|
|
147
|
+
const feedbackHistory = [];
|
|
148
|
+
|
|
149
|
+
logger.log(`Self-implement loop: @${agent} — "${task.slice(0, 60)}${task.length > 60 ? '...' : ''}"`);
|
|
150
|
+
logger.log(`Max iterations: ${maxIterations}`);
|
|
151
|
+
if (spec) logger.log(`Spec: ${spec}`);
|
|
152
|
+
if (artifact) logger.log(`Artifact: ${artifact}`);
|
|
153
|
+
logger.log('');
|
|
154
|
+
|
|
155
|
+
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
156
|
+
logger.log(`── Iteration ${iteration}/${maxIterations} ──────────────────────────`);
|
|
157
|
+
|
|
158
|
+
// Step 1: Execute agent
|
|
159
|
+
const feedbackContext = feedbackHistory.length > 0
|
|
160
|
+
? feedbackHistory.map((f) => `[Iteration ${f.iteration}] ${f.verdict}: ${f.issues.map((i) => i.message).join('; ')}`).join('\n')
|
|
161
|
+
: null;
|
|
162
|
+
|
|
163
|
+
logger.log(` Running @${agent}...`);
|
|
164
|
+
const agentResult = executeAgent(targetDir, agent, task, feedbackContext, timeoutMs);
|
|
165
|
+
|
|
166
|
+
if (!agentResult.ok) {
|
|
167
|
+
logger.log(` ✗ Agent execution failed: ${agentResult.error?.slice(0, 100)}`);
|
|
168
|
+
// Record on bus if squad context
|
|
169
|
+
if (squad) {
|
|
170
|
+
await bus.post(targetDir, squad, sessionId, {
|
|
171
|
+
from: 'self-loop',
|
|
172
|
+
type: 'status',
|
|
173
|
+
content: `Iteration ${iteration} — agent failed: ${agentResult.error?.slice(0, 200)}`
|
|
174
|
+
}).catch(() => {});
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Step 2: Verify (fresh context)
|
|
180
|
+
logger.log(' Verifying...');
|
|
181
|
+
const verifyResult = await runVerification(targetDir, spec, artifact, criteria);
|
|
182
|
+
|
|
183
|
+
// Record on bus
|
|
184
|
+
if (squad) {
|
|
185
|
+
await bus.post(targetDir, squad, sessionId, {
|
|
186
|
+
from: 'self-loop',
|
|
187
|
+
type: verifyResult.ok ? 'result' : 'gap_closure_attempt',
|
|
188
|
+
content: `Iteration ${iteration}: ${verifyResult.verdict}`,
|
|
189
|
+
metadata: {
|
|
190
|
+
iteration,
|
|
191
|
+
verdict: verifyResult.verdict,
|
|
192
|
+
issues_count: verifyResult.issues.length
|
|
193
|
+
}
|
|
194
|
+
}).catch(() => {});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Step 3: Check result
|
|
198
|
+
if (verifyResult.ok) {
|
|
199
|
+
logger.log(` ✓ PASS${iteration > 1 ? ` (after ${iteration} iteration${iteration > 1 ? 's' : ''})` : ''}`);
|
|
200
|
+
|
|
201
|
+
// Record success in state
|
|
202
|
+
if (squad) {
|
|
203
|
+
await stateManager.updateState(targetDir, squad, {
|
|
204
|
+
addDecision: [`self-loop: "${task.slice(0, 50)}" completed in ${iteration} iteration(s)`]
|
|
205
|
+
}).catch(() => {});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (options.json) {
|
|
209
|
+
return { ok: true, iterations: iteration, verdict: verifyResult.verdict, feedback: feedbackHistory };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { ok: true, iterations: iteration, verdict: verifyResult.verdict };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Step 4: Collect feedback for next iteration
|
|
216
|
+
logger.log(` ✗ ${verifyResult.verdict} — ${verifyResult.issues.length} issue(s)`);
|
|
217
|
+
for (const issue of verifyResult.issues.slice(0, 5)) {
|
|
218
|
+
logger.log(` - ${issue.message}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
feedbackHistory.push({
|
|
222
|
+
iteration,
|
|
223
|
+
verdict: verifyResult.verdict,
|
|
224
|
+
issues: verifyResult.issues
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Step 5: Exhausted — escalate
|
|
229
|
+
logger.log('');
|
|
230
|
+
logger.log(`✗ Max iterations (${maxIterations}) exhausted — escalating to user`);
|
|
231
|
+
|
|
232
|
+
if (squad) {
|
|
233
|
+
await bus.post(targetDir, squad, sessionId, {
|
|
234
|
+
from: 'self-loop',
|
|
235
|
+
type: 'block',
|
|
236
|
+
content: `Self-implement loop exhausted ${maxIterations} iterations for: "${task.slice(0, 100)}"`,
|
|
237
|
+
metadata: { exhausted: true, iterations: maxIterations }
|
|
238
|
+
}).catch(() => {});
|
|
239
|
+
|
|
240
|
+
await stateManager.updateState(targetDir, squad, {
|
|
241
|
+
addBlocker: [`self-loop exhausted: "${task.slice(0, 50)}" (${maxIterations} iterations)`]
|
|
242
|
+
}).catch(() => {});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const result = {
|
|
246
|
+
ok: false,
|
|
247
|
+
iterations: maxIterations,
|
|
248
|
+
verdict: 'EXHAUSTED',
|
|
249
|
+
feedback: feedbackHistory
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (options.json) return result;
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = { runSelfLoop };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson session:guard [projectDir] --agent=<name> --tool=<tool>
|
|
5
|
+
*
|
|
6
|
+
* Background supervisor that keeps a live session alive.
|
|
7
|
+
* - If no live session exists: auto-starts one (no-launch mode)
|
|
8
|
+
* - Polls every 30s to verify the session is still open
|
|
9
|
+
* - Detects inactivity (no events for --idle-minutes, default: 60) and closes gracefully
|
|
10
|
+
* - Works alongside hooks:emit — guard handles session lifecycle, hooks handle events
|
|
11
|
+
*
|
|
12
|
+
* Run in background:
|
|
13
|
+
* aioson session:guard . --agent=dev --tool=claude &
|
|
14
|
+
*
|
|
15
|
+
* Or as a foreground check (--once):
|
|
16
|
+
* aioson session:guard . --agent=dev --tool=claude --once
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const fs = require('node:fs/promises');
|
|
21
|
+
const {
|
|
22
|
+
openRuntimeDb,
|
|
23
|
+
resolveRuntimePaths,
|
|
24
|
+
readAgentSession,
|
|
25
|
+
writeAgentSession,
|
|
26
|
+
startTask,
|
|
27
|
+
startRun,
|
|
28
|
+
updateRun,
|
|
29
|
+
updateTask,
|
|
30
|
+
appendRunEvent
|
|
31
|
+
} = require('../runtime-store');
|
|
32
|
+
|
|
33
|
+
const POLL_INTERVAL_MS = 30_000;
|
|
34
|
+
const DEFAULT_IDLE_MINUTES = 60;
|
|
35
|
+
|
|
36
|
+
function nowIso() { return new Date().toISOString(); }
|
|
37
|
+
function log(msg) { process.stderr.write(`[session:guard] ${msg}\n`); }
|
|
38
|
+
|
|
39
|
+
async function getLastEventTime(runtimeDir, sessionKey) {
|
|
40
|
+
const eventsPath = path.join(runtimeDir, 'live', sessionKey, 'events.ndjson');
|
|
41
|
+
try {
|
|
42
|
+
const content = await fs.readFile(eventsPath, 'utf8');
|
|
43
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
44
|
+
if (lines.length === 0) return null;
|
|
45
|
+
const last = JSON.parse(lines[lines.length - 1]);
|
|
46
|
+
return last.ts ? new Date(last.ts) : null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function startLiveSession(targetDir, runtimeDir, agentName, tool) {
|
|
53
|
+
const now = nowIso();
|
|
54
|
+
const sessionKey = `guard-${agentName}-${Date.now()}`;
|
|
55
|
+
const title = `[guard] ${agentName} via ${tool}`;
|
|
56
|
+
|
|
57
|
+
const { db } = await openRuntimeDb(targetDir);
|
|
58
|
+
try {
|
|
59
|
+
const taskKey = startTask(db, {
|
|
60
|
+
sessionKey,
|
|
61
|
+
title,
|
|
62
|
+
status: 'running',
|
|
63
|
+
createdBy: agentName,
|
|
64
|
+
taskKind: 'live_session',
|
|
65
|
+
metaJson: { tool_session: tool, path: targetDir, guarded: true }
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const runKey = startRun(db, {
|
|
69
|
+
taskKey,
|
|
70
|
+
agentName,
|
|
71
|
+
agentKind: 'official',
|
|
72
|
+
sessionKey,
|
|
73
|
+
source: 'live',
|
|
74
|
+
title,
|
|
75
|
+
eventType: 'session_started',
|
|
76
|
+
phase: 'live',
|
|
77
|
+
message: `Session auto-started by session:guard (${tool})`,
|
|
78
|
+
payload: { tool_session: tool, path: targetDir, guarded: true }
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await writeAgentSession(runtimeDir, agentName, {
|
|
82
|
+
runKey, taskKey, sessionKey,
|
|
83
|
+
startedAt: now, finished: false, source: 'live'
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Create state.json for dashboard
|
|
87
|
+
const stateDir = path.join(runtimeDir, 'live', sessionKey);
|
|
88
|
+
await fs.mkdir(stateDir, { recursive: true });
|
|
89
|
+
await fs.writeFile(path.join(stateDir, 'state.json'), JSON.stringify({
|
|
90
|
+
session_key: sessionKey, run_key: runKey, task_key: taskKey,
|
|
91
|
+
agent_name: agentName, tool_session: tool,
|
|
92
|
+
status: 'running', started_at: now, updated_at: now, guarded: true,
|
|
93
|
+
last_events: [{ ts: now, type: 'session_started', summary: `Auto-started by session:guard (${tool})` }]
|
|
94
|
+
}, null, 2), 'utf8');
|
|
95
|
+
|
|
96
|
+
log(`Session started: ${sessionKey} (run: ${runKey})`);
|
|
97
|
+
return { runKey, taskKey, sessionKey };
|
|
98
|
+
} finally {
|
|
99
|
+
db.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function closeSession(targetDir, runtimeDir, agentName, runKey, taskKey, reason) {
|
|
104
|
+
const now = nowIso();
|
|
105
|
+
const { db } = await openRuntimeDb(targetDir, { mustExist: true });
|
|
106
|
+
try {
|
|
107
|
+
appendRunEvent(db, {
|
|
108
|
+
runKey, eventType: 'session_ended', phase: 'live',
|
|
109
|
+
status: 'completed', message: `Session closed by session:guard: ${reason}`,
|
|
110
|
+
createdAt: now
|
|
111
|
+
});
|
|
112
|
+
updateRun(db, runKey, { status: 'completed', summary: reason, finishedAt: now });
|
|
113
|
+
if (taskKey) updateTask(db, taskKey, { status: 'completed', finishedAt: now });
|
|
114
|
+
|
|
115
|
+
// Update state.json
|
|
116
|
+
const { db: _, ...rest } = await readAgentSession(runtimeDir, agentName).catch(() => ({}));
|
|
117
|
+
const sessionKey = rest?.sessionKey;
|
|
118
|
+
if (sessionKey) {
|
|
119
|
+
const statePath = path.join(runtimeDir, 'live', sessionKey, 'state.json');
|
|
120
|
+
try {
|
|
121
|
+
const state = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
122
|
+
state.status = 'closed';
|
|
123
|
+
state.updated_at = now;
|
|
124
|
+
await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf8');
|
|
125
|
+
} catch { /* non-fatal */ }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Clear session file
|
|
129
|
+
const sessionFile = path.join(runtimeDir, '.sessions', `${agentName}.json`);
|
|
130
|
+
try { await fs.unlink(sessionFile); } catch { /* already gone */ }
|
|
131
|
+
|
|
132
|
+
log(`Session closed: ${runKey} (${reason})`);
|
|
133
|
+
} finally {
|
|
134
|
+
db.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function tick(targetDir, runtimeDir, agentName, tool, idleMs, state) {
|
|
139
|
+
const session = await readAgentSession(runtimeDir, agentName);
|
|
140
|
+
|
|
141
|
+
if (!session || session.finished) {
|
|
142
|
+
// No session — start one
|
|
143
|
+
const created = await startLiveSession(targetDir, runtimeDir, agentName, tool);
|
|
144
|
+
state.runKey = created.runKey;
|
|
145
|
+
state.taskKey = created.taskKey;
|
|
146
|
+
state.sessionKey = created.sessionKey;
|
|
147
|
+
state.startedAt = Date.now();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Session exists — check for idle timeout
|
|
152
|
+
const sessionKey = session.sessionKey;
|
|
153
|
+
const lastEvent = await getLastEventTime(runtimeDir, sessionKey);
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
const lastActivity = lastEvent ? lastEvent.getTime() : state.startedAt;
|
|
156
|
+
const idleFor = now - lastActivity;
|
|
157
|
+
|
|
158
|
+
if (idleFor > idleMs) {
|
|
159
|
+
const idleMin = Math.round(idleFor / 60000);
|
|
160
|
+
log(`Idle for ${idleMin}m — closing session`);
|
|
161
|
+
await closeSession(targetDir, runtimeDir, agentName, session.runKey, session.taskKey,
|
|
162
|
+
`Idle for ${idleMin} minutes`);
|
|
163
|
+
state.runKey = null;
|
|
164
|
+
state.taskKey = null;
|
|
165
|
+
state.sessionKey = null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function runSessionGuard({ args, options = {}, logger }) {
|
|
170
|
+
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
171
|
+
const agentName = options.agent ? String(options.agent).replace(/^@/, '') : 'dev';
|
|
172
|
+
const tool = options.tool ? String(options.tool).trim() : 'claude';
|
|
173
|
+
const once = options.once || options['once'] || false;
|
|
174
|
+
const idleMinutes = Number(options['idle-minutes'] || options.idleMinutes || DEFAULT_IDLE_MINUTES);
|
|
175
|
+
const idleMs = idleMinutes * 60 * 1000;
|
|
176
|
+
const intervalMs = Number(options.interval || POLL_INTERVAL_MS);
|
|
177
|
+
|
|
178
|
+
const { runtimeDir } = resolveRuntimePaths(targetDir);
|
|
179
|
+
const state = { runKey: null, taskKey: null, sessionKey: null, startedAt: Date.now() };
|
|
180
|
+
|
|
181
|
+
if (!options.json) {
|
|
182
|
+
logger.log(`[session:guard] Watching: ${targetDir}`);
|
|
183
|
+
logger.log(`[session:guard] Agent: @${agentName} | Tool: ${tool} | Idle timeout: ${idleMinutes}m`);
|
|
184
|
+
logger.log(`[session:guard] Press Ctrl+C to stop.`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await tick(targetDir, runtimeDir, agentName, tool, idleMs, state);
|
|
188
|
+
|
|
189
|
+
if (once) {
|
|
190
|
+
return { ok: true, runKey: state.runKey, sessionKey: state.sessionKey };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return new Promise((resolve) => {
|
|
194
|
+
const timer = setInterval(async () => {
|
|
195
|
+
try {
|
|
196
|
+
await tick(targetDir, runtimeDir, agentName, tool, idleMs, state);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
log(`Error: ${err.message}`);
|
|
199
|
+
}
|
|
200
|
+
}, intervalMs);
|
|
201
|
+
|
|
202
|
+
const shutdown = async () => {
|
|
203
|
+
clearInterval(timer);
|
|
204
|
+
if (state.runKey) {
|
|
205
|
+
try {
|
|
206
|
+
await closeSession(targetDir, runtimeDir, agentName, state.runKey, state.taskKey, 'session:guard stopped');
|
|
207
|
+
} catch { /* best-effort */ }
|
|
208
|
+
}
|
|
209
|
+
if (!options.json) logger.log('[session:guard] Stopped.');
|
|
210
|
+
resolve({ ok: true });
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
process.on('SIGINT', shutdown);
|
|
214
|
+
process.on('SIGTERM', shutdown);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = { runSessionGuard };
|