@jaimevalasek/aioson 1.6.0 → 1.7.2
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 +74 -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 +22 -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/copywriter.md +463 -0
- package/template/.aioson/agents/design-hybrid-forge.md +14 -0
- package/template/.aioson/agents/dev.md +271 -25
- package/template/.aioson/agents/deyvin.md +67 -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 +83 -2
- 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 +273 -21
- package/template/.aioson/agents/setup.md +96 -10
- package/template/.aioson/agents/sheldon.md +131 -6
- package/template/.aioson/agents/site-forge.md +1753 -0
- package/template/.aioson/agents/squad.md +352 -0
- package/template/.aioson/agents/tester.md +53 -0
- package/template/.aioson/agents/ux-ui.md +203 -4
- 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/genomes/copywriting.md +204 -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/cognitive-core-ui/references/motion.md +2 -0
- 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/marketing/references/anti-patterns.md +254 -0
- package/template/.aioson/skills/marketing/references/fascinations.md +192 -0
- package/template/.aioson/skills/marketing/references/five-acts.md +248 -0
- package/template/.aioson/skills/marketing/references/market-intelligence.md +198 -0
- package/template/.aioson/skills/marketing/references/offer-structure.md +203 -0
- package/template/.aioson/skills/marketing/references/one-belief.md +149 -0
- package/template/.aioson/skills/marketing/references/patterns.md +218 -0
- package/template/.aioson/skills/marketing/references/pms-research.md +193 -0
- package/template/.aioson/skills/marketing/vsl-craft.md +385 -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/landing-page-deploy.md +192 -0
- package/template/.aioson/skills/static/landing-page-forge.md +730 -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/ui-ux-modern.md +1 -0
- package/template/.aioson/skills/static/web-research-cache.md +112 -0
- package/template/.aioson/tasks/implementation-plan.md +21 -1
- package/template/.aioson/tasks/squad-create.md +22 -0
- package/template/.aioson/tasks/squad-design.md +30 -0
- package/template/.aioson/templates/squads/digital-marketing-agency/template.json +96 -0
- 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 +31 -0
- package/template/OPENCODE.md +4 -0
- package/template/researchs/.gitkeep +0 -0
- package/template/.aioson/skills/design-system/components/SKILL.md:Zone.Identifier +0 -0
- package/template/.aioson/skills/design-system/dashboards/SKILL.md:Zone.Identifier +0 -0
- package/template/.aioson/skills/design-system/foundations/SKILL.md:Zone.Identifier +0 -0
- package/template/.aioson/skills/design-system/motion/SKILL.md:Zone.Identifier +0 -0
- package/template/.aioson/skills/design-system/patterns/SKILL.md:Zone.Identifier +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson feature:close — close a feature after QA sign-off.
|
|
5
|
+
*
|
|
6
|
+
* Updates spec-{slug}.md (adds QA sign-off block), features.md (sets status to done),
|
|
7
|
+
* and project-pulse.md (removes from active work).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* aioson feature:close . --feature=checkout --verdict=PASS
|
|
11
|
+
* aioson feature:close . --feature=checkout --verdict=PASS --residual="Email delivery not tested E2E"
|
|
12
|
+
* aioson feature:close . --feature=checkout --verdict=FAIL --notes="Auth edge case missing"
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('node:fs/promises');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const { contextDir, readFileSafe, parseFrontmatter } = require('../preflight-engine');
|
|
18
|
+
|
|
19
|
+
function nowDate() {
|
|
20
|
+
return new Date().toISOString().slice(0, 10);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function updateSpecFile(specPath, verdict, residual, date) {
|
|
24
|
+
const content = await readFileSafe(specPath);
|
|
25
|
+
if (!content) return false;
|
|
26
|
+
|
|
27
|
+
const signOff = [
|
|
28
|
+
'',
|
|
29
|
+
'## QA Sign-off',
|
|
30
|
+
'',
|
|
31
|
+
`- **Date:** ${date}`,
|
|
32
|
+
`- **Verdict:** ${verdict}`,
|
|
33
|
+
residual ? `- **Residual:** ${residual}` : null,
|
|
34
|
+
`- **Gate D (execution):** ${verdict === 'PASS' ? 'approved' : 'rejected'}`,
|
|
35
|
+
''
|
|
36
|
+
].filter((l) => l !== null).join('\n');
|
|
37
|
+
|
|
38
|
+
// Update gate_execution in frontmatter first (on original content)
|
|
39
|
+
const newStatus = verdict === 'PASS' ? 'approved' : 'rejected';
|
|
40
|
+
const fm = parseFrontmatter(content);
|
|
41
|
+
let baseContent = content;
|
|
42
|
+
if (Object.keys(fm).length > 0) {
|
|
43
|
+
baseContent = content.replace(
|
|
44
|
+
/^---\r?\n[\s\S]*?\r?\n---/,
|
|
45
|
+
(block) => {
|
|
46
|
+
if (block.includes('gate_execution')) {
|
|
47
|
+
return block.replace(/gate_execution:\s*.+/, `gate_execution: ${newStatus}`);
|
|
48
|
+
}
|
|
49
|
+
return block.replace(/^---\r?\n/, `---\ngate_execution: ${newStatus}\n`);
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Now apply QA sign-off on top of the frontmatter-updated content
|
|
55
|
+
if (baseContent.includes('## QA Sign-off')) {
|
|
56
|
+
const updated = baseContent.replace(
|
|
57
|
+
/## QA Sign-off[\s\S]*?(?=\n##|\s*$)/,
|
|
58
|
+
signOff.trimStart()
|
|
59
|
+
);
|
|
60
|
+
await fs.writeFile(specPath, updated, 'utf8');
|
|
61
|
+
} else {
|
|
62
|
+
await fs.writeFile(specPath, baseContent + signOff, 'utf8');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function updateFeaturesFile(featuresPath, slug, verdict, date) {
|
|
69
|
+
const content = await readFileSafe(featuresPath);
|
|
70
|
+
if (!content) return false;
|
|
71
|
+
|
|
72
|
+
const status = verdict === 'PASS' ? 'done' : 'qa_failed';
|
|
73
|
+
|
|
74
|
+
// Try to find and update the feature row
|
|
75
|
+
const updated = content.replace(
|
|
76
|
+
new RegExp(`(\\|[^|]*${slug}[^|]*\\|[^|]*\\|)[^|]*(\\|)`, 'g'),
|
|
77
|
+
(match, before, after) => `${before} ${status} (${date}) ${after}`
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (updated !== content) {
|
|
81
|
+
await fs.writeFile(featuresPath, updated, 'utf8');
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Append if not found
|
|
86
|
+
const line = `| ${slug} | ${verdict === 'PASS' ? 'done' : 'qa_failed'} | ${date} | QA ${verdict} |`;
|
|
87
|
+
await fs.appendFile(featuresPath, `\n${line}\n`, 'utf8');
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function runFeatureClose({ args, options = {}, logger }) {
|
|
92
|
+
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
93
|
+
const slug = options.feature ? String(options.feature) : null;
|
|
94
|
+
const verdict = options.verdict ? String(options.verdict).toUpperCase() : null;
|
|
95
|
+
const residual = options.residual ? String(options.residual) : null;
|
|
96
|
+
const notes = options.notes ? String(options.notes) : null;
|
|
97
|
+
|
|
98
|
+
if (!slug) {
|
|
99
|
+
if (options.json) return { ok: false, reason: 'missing_feature' };
|
|
100
|
+
logger.log('--feature=<slug> is required.');
|
|
101
|
+
return { ok: false };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!verdict || !['PASS', 'FAIL'].includes(verdict)) {
|
|
105
|
+
if (options.json) return { ok: false, reason: 'invalid_verdict' };
|
|
106
|
+
logger.log('--verdict=PASS or --verdict=FAIL is required.');
|
|
107
|
+
return { ok: false };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const today = nowDate();
|
|
111
|
+
const dir = contextDir(targetDir);
|
|
112
|
+
const updates = [];
|
|
113
|
+
|
|
114
|
+
// 1. Update spec file
|
|
115
|
+
const specPath = path.join(dir, `spec-${slug}.md`);
|
|
116
|
+
const specUpdated = await updateSpecFile(specPath, verdict, residual || notes, today);
|
|
117
|
+
if (specUpdated) {
|
|
118
|
+
updates.push(`spec-${slug}.md: added QA sign-off (${today}, ${verdict})`);
|
|
119
|
+
} else {
|
|
120
|
+
updates.push(`spec-${slug}.md: not found (skipped)`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 2. Update features.md
|
|
124
|
+
const featuresPath = path.join(dir, 'features.md');
|
|
125
|
+
const featuresContent = await readFileSafe(featuresPath);
|
|
126
|
+
if (featuresContent) {
|
|
127
|
+
await updateFeaturesFile(featuresPath, slug, verdict, today);
|
|
128
|
+
updates.push(`features.md: ${slug} → ${verdict === 'PASS' ? 'done' : 'qa_failed'} (${today})`);
|
|
129
|
+
} else {
|
|
130
|
+
updates.push('features.md: not found (skipped)');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 3. Update project-pulse.md
|
|
134
|
+
const pulsePath = path.join(dir, 'project-pulse.md');
|
|
135
|
+
const pulseContent = await readFileSafe(pulsePath);
|
|
136
|
+
if (pulseContent) {
|
|
137
|
+
const fm = parseFrontmatter(pulseContent);
|
|
138
|
+
const status = verdict === 'PASS' ? 'closed' : 'qa_failed';
|
|
139
|
+
const updatedPulse = pulseContent
|
|
140
|
+
.replace(/active_feature:\s*.+/, `active_feature: (none)`)
|
|
141
|
+
.replace(/active_work:\s*".+"/, `active_work: ""`)
|
|
142
|
+
.replace(/last_agent:\s*.+/, `last_agent: qa`)
|
|
143
|
+
.replace(/last_gate:\s*.+/, `last_gate: Gate D: ${verdict === 'PASS' ? 'approved' : 'rejected'}`);
|
|
144
|
+
await fs.writeFile(pulsePath, updatedPulse, 'utf8');
|
|
145
|
+
updates.push('project-pulse.md: updated active work');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const result = {
|
|
149
|
+
ok: true,
|
|
150
|
+
feature: slug,
|
|
151
|
+
verdict,
|
|
152
|
+
date: today,
|
|
153
|
+
residual: residual || notes || null,
|
|
154
|
+
updates
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (options.json) return result;
|
|
158
|
+
|
|
159
|
+
logger.log(`Feature closure — ${slug}:`);
|
|
160
|
+
for (const u of updates) logger.log(` ${u}`);
|
|
161
|
+
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { runFeatureClose };
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson gate:check — check if a phase gate is approved for a feature.
|
|
5
|
+
*
|
|
6
|
+
* Reads spec-{slug}.md frontmatter and artifact chain to verify gate status.
|
|
7
|
+
* Returns PASS or BLOCKED with evidence. No LLM calls.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* aioson gate:check . --feature=checkout --gate=C
|
|
11
|
+
* aioson gate:check . --feature=checkout --gate=D
|
|
12
|
+
* aioson gate:check . --feature=checkout --gate=C --json
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const {
|
|
17
|
+
contextDir,
|
|
18
|
+
readFileSafe,
|
|
19
|
+
fileExists,
|
|
20
|
+
parseGatesFromSpec,
|
|
21
|
+
parseFrontmatter,
|
|
22
|
+
GATE_NAMES,
|
|
23
|
+
GATE_ALIASES
|
|
24
|
+
} = require('../preflight-engine');
|
|
25
|
+
|
|
26
|
+
const BAR = '━'.repeat(35);
|
|
27
|
+
|
|
28
|
+
const GATE_PREREQUISITES = {
|
|
29
|
+
A: [],
|
|
30
|
+
B: ['A'],
|
|
31
|
+
C: ['A', 'B'],
|
|
32
|
+
D: ['A', 'B', 'C']
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const GATE_REQUIRED_ARTIFACTS = {
|
|
36
|
+
A: (slug) => [`requirements-${slug}.md`],
|
|
37
|
+
B: (slug) => ['architecture.md'],
|
|
38
|
+
C: (slug) => [`implementation-plan-${slug}.md`],
|
|
39
|
+
D: (slug) => [] // Gate D validated by QA sign-off in spec
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const GATE_DESCRIPTIONS = {
|
|
43
|
+
A: 'requirements',
|
|
44
|
+
B: 'design',
|
|
45
|
+
C: 'plan',
|
|
46
|
+
D: 'execution'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
async function checkGate(targetDir, slug, gateLetter) {
|
|
50
|
+
const dir = contextDir(targetDir);
|
|
51
|
+
const specFile = path.join(dir, `spec-${slug}.md`);
|
|
52
|
+
const specContent = await readFileSafe(specFile);
|
|
53
|
+
const gates = specContent ? parseGatesFromSpec(specContent) : {};
|
|
54
|
+
const fm = specContent ? parseFrontmatter(specContent) : {};
|
|
55
|
+
|
|
56
|
+
const gateName = GATE_NAMES[gateLetter];
|
|
57
|
+
const gateStatus = gates[gateName] || 'pending';
|
|
58
|
+
const prerequisites = GATE_PREREQUISITES[gateLetter] || [];
|
|
59
|
+
|
|
60
|
+
const evidence = [];
|
|
61
|
+
const missing = [];
|
|
62
|
+
|
|
63
|
+
// Check prerequisites
|
|
64
|
+
for (const prereq of prerequisites) {
|
|
65
|
+
const prereqName = GATE_NAMES[prereq];
|
|
66
|
+
const prereqStatus = gates[prereqName] || 'pending';
|
|
67
|
+
if (prereqStatus === 'approved') {
|
|
68
|
+
evidence.push({ type: 'prereq', gate: prereq, name: prereqName, status: 'approved', ok: true });
|
|
69
|
+
} else {
|
|
70
|
+
evidence.push({ type: 'prereq', gate: prereq, name: prereqName, status: prereqStatus, ok: false });
|
|
71
|
+
missing.push(`Gate ${prereq} (${prereqName}) not approved: ${prereqStatus}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check required artifacts
|
|
76
|
+
const requiredFiles = GATE_REQUIRED_ARTIFACTS[gateLetter](slug);
|
|
77
|
+
for (const fileName of requiredFiles) {
|
|
78
|
+
const filePath = path.join(dir, fileName);
|
|
79
|
+
const exists = await fileExists(filePath);
|
|
80
|
+
if (exists) {
|
|
81
|
+
let detail = null;
|
|
82
|
+
const content = await readFileSafe(filePath);
|
|
83
|
+
if (content) {
|
|
84
|
+
const fileFm = parseFrontmatter(content);
|
|
85
|
+
if (fileFm.status) detail = `status: ${fileFm.status}`;
|
|
86
|
+
}
|
|
87
|
+
evidence.push({ type: 'artifact', file: fileName, exists: true, detail, ok: true });
|
|
88
|
+
} else {
|
|
89
|
+
evidence.push({ type: 'artifact', file: fileName, exists: false, ok: false });
|
|
90
|
+
missing.push(`${fileName} not found`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Gate D: check for QA sign-off in spec
|
|
95
|
+
if (gateLetter === 'D') {
|
|
96
|
+
if (specContent && specContent.includes('## QA Sign-off')) {
|
|
97
|
+
// Check verdict
|
|
98
|
+
const passMatch = specContent.match(/\*\*Verdict:\*\*\s*(PASS|FAIL)/i);
|
|
99
|
+
const passVerdict = passMatch ? passMatch[1].toUpperCase() : null;
|
|
100
|
+
if (passVerdict === 'PASS') {
|
|
101
|
+
evidence.push({ type: 'qa_signoff', verdict: 'PASS', ok: true });
|
|
102
|
+
} else if (passVerdict === 'FAIL') {
|
|
103
|
+
evidence.push({ type: 'qa_signoff', verdict: 'FAIL', ok: false });
|
|
104
|
+
missing.push('QA sign-off verdict: FAIL');
|
|
105
|
+
} else {
|
|
106
|
+
evidence.push({ type: 'qa_signoff', verdict: null, ok: false });
|
|
107
|
+
missing.push('QA sign-off found but verdict unclear');
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// Check spec last_checkpoint for completion indicators
|
|
111
|
+
const checkpoint = fm.last_checkpoint || '';
|
|
112
|
+
if (checkpoint.toLowerCase().includes('complet') || checkpoint.toLowerCase().includes('done')) {
|
|
113
|
+
evidence.push({ type: 'checkpoint', value: checkpoint, ok: true });
|
|
114
|
+
} else {
|
|
115
|
+
evidence.push({ type: 'qa_signoff', exists: false, ok: false });
|
|
116
|
+
missing.push('No QA sign-off in spec file');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Also check spec version for explicit gate_execution
|
|
121
|
+
if (gates.execution && gates.execution !== 'pending') {
|
|
122
|
+
const gateD = gates.execution;
|
|
123
|
+
evidence.push({ type: 'gate_field', field: 'gate_execution', value: gateD, ok: gateD === 'approved' });
|
|
124
|
+
if (gateD !== 'approved') missing.push(`gate_execution: ${gateD}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const allOk = missing.length === 0;
|
|
129
|
+
const result = allOk ? 'PASS' : 'BLOCKED';
|
|
130
|
+
|
|
131
|
+
let recommendation = '';
|
|
132
|
+
if (result === 'PASS') {
|
|
133
|
+
const nextAgents = { A: '@architect', B: '@dev or @pm', C: '@dev', D: 'feature complete' };
|
|
134
|
+
recommendation = `${nextAgents[gateLetter] || 'proceed'} can proceed`;
|
|
135
|
+
} else {
|
|
136
|
+
const fixAgents = { A: 'complete requirements (@analyst)', B: 'complete design (@architect)', C: 'approve implementation plan', D: 'complete implementation and run @qa' };
|
|
137
|
+
recommendation = `BLOCKED — ${fixAgents[gateLetter] || 'resolve missing items'} first`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
gate: gateLetter,
|
|
142
|
+
gate_name: gateName,
|
|
143
|
+
feature: slug,
|
|
144
|
+
status: gateStatus,
|
|
145
|
+
result,
|
|
146
|
+
evidence,
|
|
147
|
+
missing,
|
|
148
|
+
recommendation
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function runGateCheck({ args, options = {}, logger }) {
|
|
153
|
+
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
154
|
+
const slug = options.feature ? String(options.feature) : null;
|
|
155
|
+
let gateLetter = options.gate ? String(options.gate).toUpperCase() : null;
|
|
156
|
+
|
|
157
|
+
if (!slug) {
|
|
158
|
+
if (options.json) return { ok: false, reason: 'missing_feature' };
|
|
159
|
+
logger.log('--feature=<slug> is required.');
|
|
160
|
+
return { ok: false };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!gateLetter) {
|
|
164
|
+
if (options.json) return { ok: false, reason: 'missing_gate' };
|
|
165
|
+
logger.log('--gate=<A|B|C|D> is required.');
|
|
166
|
+
return { ok: false };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Allow gate name aliases (requirements → A, etc.)
|
|
170
|
+
if (GATE_ALIASES[gateLetter.toLowerCase()]) {
|
|
171
|
+
gateLetter = GATE_ALIASES[gateLetter.toLowerCase()];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!GATE_NAMES[gateLetter]) {
|
|
175
|
+
if (options.json) return { ok: false, reason: 'invalid_gate', gate: gateLetter };
|
|
176
|
+
logger.log(`Invalid gate: ${gateLetter}. Use A, B, C, or D.`);
|
|
177
|
+
return { ok: false };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const check = await checkGate(targetDir, slug, gateLetter);
|
|
181
|
+
|
|
182
|
+
const result = { ok: check.result === 'PASS', ...check };
|
|
183
|
+
|
|
184
|
+
if (options.json) return result;
|
|
185
|
+
|
|
186
|
+
logger.log('');
|
|
187
|
+
logger.log(`Gate ${gateLetter} (${check.gate_name}) — ${slug}`);
|
|
188
|
+
logger.log(BAR);
|
|
189
|
+
logger.log(`Status: ${check.status}`);
|
|
190
|
+
|
|
191
|
+
const prereqs = check.evidence.filter((e) => e.type === 'prereq');
|
|
192
|
+
if (prereqs.length > 0) {
|
|
193
|
+
logger.log('Prerequisites met:');
|
|
194
|
+
for (const p of prereqs) {
|
|
195
|
+
const icon = p.ok ? ' ✓' : ' ✗';
|
|
196
|
+
logger.log(`${icon} Gate ${p.gate} (${p.name}): ${p.status}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const artifacts = check.evidence.filter((e) => e.type === 'artifact');
|
|
201
|
+
if (artifacts.length > 0) {
|
|
202
|
+
logger.log('Artifacts:');
|
|
203
|
+
for (const a of artifacts) {
|
|
204
|
+
const icon = a.ok ? ' ✓' : ' ✗';
|
|
205
|
+
const detail = a.detail ? ` (${a.detail})` : '';
|
|
206
|
+
logger.log(`${icon} ${a.file}${a.ok ? ' exists' : ' missing'}${detail}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const qaEvidence = check.evidence.filter((e) => e.type === 'qa_signoff' || e.type === 'checkpoint' || e.type === 'gate_field');
|
|
211
|
+
if (qaEvidence.length > 0) {
|
|
212
|
+
for (const q of qaEvidence) {
|
|
213
|
+
const icon = q.ok ? ' ✓' : ' ✗';
|
|
214
|
+
if (q.type === 'qa_signoff') logger.log(`${icon} QA sign-off: ${q.exists === false ? 'missing' : `verdict ${q.verdict || 'unclear'}`}`);
|
|
215
|
+
if (q.type === 'checkpoint') logger.log(` ✓ last_checkpoint: "${q.value}"`);
|
|
216
|
+
if (q.type === 'gate_field') logger.log(`${icon} gate_execution: ${q.value}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
logger.log('');
|
|
221
|
+
const resultIcon = check.result === 'PASS' ? '✓' : '✗';
|
|
222
|
+
logger.log(`Result: ${resultIcon} ${check.result} — ${check.recommendation}`);
|
|
223
|
+
logger.log('');
|
|
224
|
+
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = { runGateCheck };
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson hooks:emit [projectDir] --agent=<name> --source=<tool>
|
|
5
|
+
*
|
|
6
|
+
* Called by Claude Code / Antigravity / Codex hooks on every tool use.
|
|
7
|
+
* Reads the hook payload from stdin (JSON), maps it to an AIOSON runtime event,
|
|
8
|
+
* and writes it to the active live session in SQLite + events.ndjson.
|
|
9
|
+
*
|
|
10
|
+
* If no live session exists, auto-starts one (--no-launch mode) before emitting.
|
|
11
|
+
*
|
|
12
|
+
* Designed to be fast (< 50ms hot path) and never block the agent.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const fs = require('node:fs/promises');
|
|
17
|
+
const { execFileSync } = require('node:child_process');
|
|
18
|
+
const {
|
|
19
|
+
openRuntimeDb,
|
|
20
|
+
resolveRuntimePaths,
|
|
21
|
+
readAgentSession,
|
|
22
|
+
appendRunEvent,
|
|
23
|
+
startTask,
|
|
24
|
+
startRun,
|
|
25
|
+
writeAgentSession
|
|
26
|
+
} = require('../runtime-store');
|
|
27
|
+
|
|
28
|
+
const HOOKS_EMIT_VERSION = '1';
|
|
29
|
+
|
|
30
|
+
// Tool name → event_type mapping
|
|
31
|
+
const TOOL_EVENT_MAP = {
|
|
32
|
+
Write: 'artifact',
|
|
33
|
+
Edit: 'artifact',
|
|
34
|
+
MultiEdit: 'artifact',
|
|
35
|
+
Bash: 'step_done',
|
|
36
|
+
Task: 'note',
|
|
37
|
+
TodoWrite: 'note',
|
|
38
|
+
WebSearch: 'note',
|
|
39
|
+
WebFetch: 'note'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Tools to skip — too noisy, no meaningful event
|
|
43
|
+
const SKIP_TOOLS = new Set(['Read', 'Glob', 'Grep', 'LS', 'NotebookRead', 'mcp__']);
|
|
44
|
+
|
|
45
|
+
function nowIso() {
|
|
46
|
+
return new Date().toISOString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readStdin() {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
let data = '';
|
|
52
|
+
if (process.stdin.isTTY) { resolve(null); return; }
|
|
53
|
+
process.stdin.setEncoding('utf8');
|
|
54
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
55
|
+
process.stdin.on('end', () => {
|
|
56
|
+
try { resolve(JSON.parse(data)); }
|
|
57
|
+
catch { resolve(null); }
|
|
58
|
+
});
|
|
59
|
+
// Timeout: don't block if stdin is empty
|
|
60
|
+
setTimeout(() => resolve(null), 500);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildEventFromPayload(payload, source) {
|
|
65
|
+
if (!payload) return null;
|
|
66
|
+
|
|
67
|
+
const toolName = payload.tool_name || payload.toolName || null;
|
|
68
|
+
if (!toolName) return null;
|
|
69
|
+
|
|
70
|
+
// Skip noisy read-only tools
|
|
71
|
+
if (SKIP_TOOLS.has(toolName) || [...SKIP_TOOLS].some((p) => toolName.startsWith(p))) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const eventType = TOOL_EVENT_MAP[toolName] || 'note';
|
|
76
|
+
const input = payload.tool_input || payload.toolInput || {};
|
|
77
|
+
|
|
78
|
+
let message = `[${source}] ${toolName}`;
|
|
79
|
+
let filePath = null;
|
|
80
|
+
|
|
81
|
+
if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
|
|
82
|
+
filePath = input.file_path || input.path || null;
|
|
83
|
+
message = filePath ? `${toolName}: ${path.basename(filePath)}` : `${toolName}`;
|
|
84
|
+
} else if (toolName === 'Bash') {
|
|
85
|
+
const cmd = String(input.command || input.cmd || '').trim().slice(0, 80);
|
|
86
|
+
message = cmd ? `$ ${cmd}` : 'Bash';
|
|
87
|
+
} else if (toolName === 'Task') {
|
|
88
|
+
const desc = String(input.description || input.prompt || '').slice(0, 80);
|
|
89
|
+
message = desc || 'Task launched';
|
|
90
|
+
} else if (toolName === 'TodoWrite') {
|
|
91
|
+
message = 'Task list updated';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
eventType,
|
|
96
|
+
message,
|
|
97
|
+
filePath,
|
|
98
|
+
toolName,
|
|
99
|
+
sessionId: payload.session_id || payload.sessionId || null
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function ensureOrCreateLiveSession(targetDir, agentName, source, runtimeDir) {
|
|
104
|
+
// Fast path: check existing session file
|
|
105
|
+
const session = await readAgentSession(runtimeDir, agentName);
|
|
106
|
+
if (session && !session.finished && session.source === 'live' && session.runKey) {
|
|
107
|
+
return session.runKey;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// No session — auto-start one inline (no-launch mode)
|
|
111
|
+
const now = nowIso();
|
|
112
|
+
const sessionKey = `hooks-${agentName}-${Date.now()}`;
|
|
113
|
+
const title = `[hooks] ${agentName} via ${source}`;
|
|
114
|
+
|
|
115
|
+
const { db } = await openRuntimeDb(targetDir);
|
|
116
|
+
try {
|
|
117
|
+
const taskKey = startTask(db, {
|
|
118
|
+
sessionKey,
|
|
119
|
+
title,
|
|
120
|
+
status: 'running',
|
|
121
|
+
createdBy: agentName,
|
|
122
|
+
taskKind: 'live_session',
|
|
123
|
+
metaJson: { tool_session: source, path: targetDir, auto_started: true }
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const runKey = startRun(db, {
|
|
127
|
+
taskKey,
|
|
128
|
+
agentName,
|
|
129
|
+
agentKind: 'official',
|
|
130
|
+
sessionKey,
|
|
131
|
+
source: 'live',
|
|
132
|
+
title,
|
|
133
|
+
eventType: 'session_started',
|
|
134
|
+
phase: 'live',
|
|
135
|
+
message: `Auto-started by hooks:emit (${source})`,
|
|
136
|
+
payload: { tool_session: source, path: targetDir, hooks_version: HOOKS_EMIT_VERSION }
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await writeAgentSession(runtimeDir, agentName, {
|
|
140
|
+
runKey,
|
|
141
|
+
taskKey,
|
|
142
|
+
sessionKey,
|
|
143
|
+
startedAt: now,
|
|
144
|
+
finished: false,
|
|
145
|
+
source: 'live'
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Write state.json for dashboard live view
|
|
149
|
+
const stateDir = path.join(runtimeDir, 'live', sessionKey);
|
|
150
|
+
await fs.mkdir(stateDir, { recursive: true });
|
|
151
|
+
await fs.writeFile(path.join(stateDir, 'state.json'), JSON.stringify({
|
|
152
|
+
session_key: sessionKey,
|
|
153
|
+
run_key: runKey,
|
|
154
|
+
task_key: taskKey,
|
|
155
|
+
agent_name: agentName,
|
|
156
|
+
tool_session: source,
|
|
157
|
+
status: 'running',
|
|
158
|
+
started_at: now,
|
|
159
|
+
updated_at: now,
|
|
160
|
+
auto_started: true,
|
|
161
|
+
last_events: [{ ts: now, type: 'session_started', summary: `Auto-started by hooks:emit (${source})` }]
|
|
162
|
+
}, null, 2), 'utf8');
|
|
163
|
+
|
|
164
|
+
return runKey;
|
|
165
|
+
} finally {
|
|
166
|
+
db.close();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function appendLiveEventFile(runtimeDir, runKey, event) {
|
|
171
|
+
// Find the session dir for this runKey
|
|
172
|
+
try {
|
|
173
|
+
const liveRoot = path.join(runtimeDir, 'live');
|
|
174
|
+
const entries = await fs.readdir(liveRoot).catch(() => []);
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const statePath = path.join(liveRoot, entry, 'state.json');
|
|
177
|
+
try {
|
|
178
|
+
const state = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
179
|
+
if (state.run_key === runKey) {
|
|
180
|
+
const eventsPath = path.join(liveRoot, entry, 'events.ndjson');
|
|
181
|
+
await fs.appendFile(eventsPath, JSON.stringify(event) + '\n', 'utf8');
|
|
182
|
+
|
|
183
|
+
// Update state.json updated_at + last_events
|
|
184
|
+
state.updated_at = event.ts;
|
|
185
|
+
const lastEvents = state.last_events || [];
|
|
186
|
+
lastEvents.push({ ts: event.ts, type: event.type, summary: event.summary || event.message });
|
|
187
|
+
state.last_events = lastEvents.slice(-10); // keep last 10
|
|
188
|
+
await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf8');
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
} catch { /* skip */ }
|
|
192
|
+
}
|
|
193
|
+
} catch { /* non-fatal */ }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function runHooksEmit({ args, options = {} }) {
|
|
197
|
+
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
198
|
+
const agentName = options.agent ? String(options.agent).replace(/^@/, '') : 'dev';
|
|
199
|
+
const source = options.source ? String(options.source).trim() : 'claude';
|
|
200
|
+
|
|
201
|
+
// Silence all output — hooks must be silent
|
|
202
|
+
const logger = { log: () => {}, error: () => {} };
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const { runtimeDir } = resolveRuntimePaths(targetDir);
|
|
206
|
+
|
|
207
|
+
// Read hook payload from stdin
|
|
208
|
+
const payload = await readStdin();
|
|
209
|
+
const event = buildEventFromPayload(payload, source);
|
|
210
|
+
|
|
211
|
+
// Skip if no meaningful event (read-only tools, etc.)
|
|
212
|
+
if (!event) return { ok: true, skipped: true };
|
|
213
|
+
|
|
214
|
+
const now = nowIso();
|
|
215
|
+
|
|
216
|
+
// Ensure live session exists (fast path: session file read)
|
|
217
|
+
const runKey = await ensureOrCreateLiveSession(targetDir, agentName, source, runtimeDir);
|
|
218
|
+
|
|
219
|
+
// Write to SQLite
|
|
220
|
+
const { db } = await openRuntimeDb(targetDir, { mustExist: true });
|
|
221
|
+
try {
|
|
222
|
+
appendRunEvent(db, {
|
|
223
|
+
runKey,
|
|
224
|
+
eventType: event.eventType,
|
|
225
|
+
phase: 'live',
|
|
226
|
+
status: 'running',
|
|
227
|
+
message: event.message,
|
|
228
|
+
payload: event.filePath ? { file: event.filePath, tool: event.toolName } : { tool: event.toolName },
|
|
229
|
+
createdAt: now
|
|
230
|
+
});
|
|
231
|
+
} finally {
|
|
232
|
+
db.close();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Append to events.ndjson for real-time dashboard view
|
|
236
|
+
await appendLiveEventFile(runtimeDir, runKey, {
|
|
237
|
+
ts: now,
|
|
238
|
+
type: event.eventType,
|
|
239
|
+
message: event.message,
|
|
240
|
+
tool: event.toolName,
|
|
241
|
+
file: event.filePath || undefined,
|
|
242
|
+
source,
|
|
243
|
+
agent: agentName
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return { ok: true, runKey, event: event.eventType, message: event.message };
|
|
247
|
+
} catch {
|
|
248
|
+
// Never fail — hooks must not block the agent
|
|
249
|
+
return { ok: false };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = { runHooksEmit };
|