@jaimevalasek/aioson 1.21.7 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -2
- package/docs/en/1-understand/ecosystem-map.md +1 -1
- package/docs/en/2-start/initial-decisions.md +1 -1
- package/docs/en/4-agents/README.md +8 -7
- package/docs/en/4-agents/discovery-design-doc.md +150 -0
- package/docs/en/5-reference/cli-reference.md +42 -16
- package/docs/en/README.md +2 -2
- package/docs/pt/4-agentes/README.md +8 -6
- package/docs/pt/4-agentes/briefing-refiner.md +122 -0
- package/docs/pt/4-agentes/discovery-design-doc.md +133 -74
- package/docs/pt/4-agentes/scope-check.md +65 -0
- package/docs/pt/5-referencia/README.md +1 -0
- package/docs/pt/5-referencia/comandos-cli.md +5 -4
- package/docs/pt/5-referencia/feature-archive.md +1 -0
- package/docs/pt/5-referencia/feature-export.md +155 -0
- package/docs/pt/README.md +2 -2
- package/docs/pt/agentes.md +3 -1
- package/package.json +1 -1
- package/src/agent-manifests.js +14 -3
- package/src/agents.js +21 -20
- package/src/cli.js +72 -52
- package/src/commands/briefing.js +28 -150
- package/src/commands/commit-prepare.js +5 -2
- package/src/commands/feature-archive.js +48 -12
- package/src/commands/feature-close.js +40 -0
- package/src/commands/feature-export.js +242 -0
- package/src/commands/gate-check.js +8 -3
- package/src/commands/git-guard.js +58 -0
- package/src/commands/harness-gate.js +120 -0
- package/src/commands/harness-status.js +157 -0
- package/src/commands/harness.js +18 -1
- package/src/commands/live.js +120 -115
- package/src/commands/parallel-doctor.js +2 -1
- package/src/commands/pulse-update.js +2 -2
- package/src/commands/scan-project.js +12 -2
- package/src/commands/self-implement-loop.js +305 -5
- package/src/commands/workflow-next.js +477 -425
- package/src/constants.js +21 -11
- package/src/context-search.js +3 -0
- package/src/doctor.js +24 -8
- package/src/dossier/schema.js +4 -3
- package/src/harness/active-contract.js +41 -0
- package/src/harness/attempt-artifacts.js +95 -0
- package/src/harness/budget-guard.js +127 -0
- package/src/harness/circuit-breaker.js +7 -0
- package/src/harness/contract-schema.js +324 -0
- package/src/harness/criteria-runner.js +136 -0
- package/src/harness/git-baseline.js +204 -0
- package/src/harness/glob-match.js +126 -0
- package/src/harness/guard-events.js +71 -0
- package/src/harness/human-gate.js +182 -0
- package/src/harness/scope-guard.js +115 -0
- package/src/i18n/messages/en.js +24 -21
- package/src/i18n/messages/es.js +11 -9
- package/src/i18n/messages/fr.js +11 -9
- package/src/i18n/messages/pt-BR.js +24 -21
- package/src/lib/briefing-refiner/apply-feedback.js +134 -0
- package/src/lib/briefing-refiner/briefing-paths.js +41 -0
- package/src/lib/briefing-refiner/briefing-registry.js +204 -0
- package/src/lib/briefing-refiner/briefing-sections.js +110 -0
- package/src/lib/briefing-refiner/feedback-schema.js +122 -0
- package/src/lib/briefing-refiner/refinement-report.js +39 -0
- package/src/lib/briefing-refiner/review-html.js +230 -0
- package/src/lib/dev-resume.js +94 -45
- package/src/parser.js +8 -5
- package/src/preflight-engine.js +88 -84
- package/src/runtime-store.js +2 -0
- package/src/sandbox.js +17 -3
- package/template/.aioson/agents/analyst.md +27 -23
- package/template/.aioson/agents/architect.md +7 -3
- package/template/.aioson/agents/briefing-refiner.md +121 -0
- package/template/.aioson/agents/briefing.md +83 -74
- package/template/.aioson/agents/committer.md +8 -0
- package/template/.aioson/agents/copywriter.md +19 -7
- package/template/.aioson/agents/design-hybrid-forge.md +16 -5
- package/template/.aioson/agents/dev.md +68 -66
- package/template/.aioson/agents/deyvin.md +97 -90
- package/template/.aioson/agents/discover.md +2 -2
- package/template/.aioson/agents/discovery-design-doc.md +34 -30
- package/template/.aioson/agents/genome.md +82 -71
- package/template/.aioson/agents/neo.md +11 -3
- package/template/.aioson/agents/orache.md +10 -0
- package/template/.aioson/agents/orchestrator.md +68 -68
- package/template/.aioson/agents/pentester.md +15 -6
- package/template/.aioson/agents/pm.md +30 -25
- package/template/.aioson/agents/product.md +108 -108
- package/template/.aioson/agents/profiler-enricher.md +10 -0
- package/template/.aioson/agents/profiler-forge.md +10 -0
- package/template/.aioson/agents/profiler-researcher.md +11 -0
- package/template/.aioson/agents/qa.md +28 -20
- package/template/.aioson/agents/scope-check.md +176 -164
- package/template/.aioson/agents/setup.md +11 -1
- package/template/.aioson/agents/sheldon.md +38 -38
- package/template/.aioson/agents/site-forge.md +15 -6
- package/template/.aioson/agents/squad.md +12 -0
- package/template/.aioson/agents/tester.md +209 -209
- package/template/.aioson/agents/ux-ui.md +2 -2
- package/template/.aioson/agents/validator.md +10 -2
- package/template/.aioson/config.md +31 -28
- package/template/.aioson/docs/autopilot-handoff.md +46 -0
- package/template/.aioson/docs/dossier/agent-templates.md +191 -0
- package/template/.aioson/docs/dossier/schema.md +218 -0
- package/template/.claude/commands/aioson/agent/briefing-refiner.md +17 -0
- package/template/AGENTS.md +50 -47
- package/template/CLAUDE.md +29 -27
|
@@ -26,16 +26,24 @@ const { emitDossierEvent } = require('../lib/dossier-telemetry');
|
|
|
26
26
|
|
|
27
27
|
const STATE_RELATIVE_PATH = '.aioson/context/workflow.state.json';
|
|
28
28
|
const CONFIG_RELATIVE_PATH = '.aioson/context/workflow.config.json';
|
|
29
|
-
const EVENTS_RELATIVE_PATH = '.aioson/context/workflow.events.jsonl';
|
|
30
|
-
const SCOPE_CHECK_MODES = new Set(['pre-dev', 'post-dev', 'post-fix', 'final']);
|
|
29
|
+
const EVENTS_RELATIVE_PATH = '.aioson/context/workflow.events.jsonl';
|
|
30
|
+
const SCOPE_CHECK_MODES = new Set(['pre-dev', 'post-dev', 'post-fix', 'final']);
|
|
31
|
+
|
|
32
|
+
const DEFAULT_FEATURE_WORKFLOW_BY_CLASSIFICATION = {
|
|
33
|
+
MICRO: ['product', 'dev', 'qa'],
|
|
34
|
+
SMALL: ['product', 'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'dev', 'qa'],
|
|
35
|
+
// MEDIUM routes through @pm after discovery-design-doc (mirrors the
|
|
36
|
+
// project-mode position): Gate C requires implementation-plan-{slug}.md and
|
|
37
|
+
// @pm is its canonical owner (AC-SDLC-15/16) — without the stage, the
|
|
38
|
+
// sequence dead-ends at @dev preflight with no agent to produce the plan.
|
|
39
|
+
MEDIUM: ['product', 'analyst', 'architect', 'discovery-design-doc', 'pm', 'scope-check', 'dev', 'pentester', 'qa']
|
|
40
|
+
};
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
MEDIUM: ['product', 'analyst', 'architect', 'discovery-design-doc', 'scope-check', 'dev', 'pentester', 'qa']
|
|
36
|
-
};
|
|
42
|
+
// Stages eligible for autopilot handoff (auto_handoff: true in project.context.md).
|
|
43
|
+
// The chain always breaks at the @dev handoff — see .aioson/docs/autopilot-handoff.md.
|
|
44
|
+
const AUTOPILOT_HANDOFF_STAGES = new Set(['analyst', 'scope-check', 'architect', 'discovery-design-doc', 'pm']);
|
|
37
45
|
|
|
38
|
-
function normalizeAgentName(input) {
|
|
46
|
+
function normalizeAgentName(input) {
|
|
39
47
|
return String(input || '')
|
|
40
48
|
.trim()
|
|
41
49
|
.toLowerCase()
|
|
@@ -51,11 +59,11 @@ function normalizeClassification(value, fallback = 'MICRO') {
|
|
|
51
59
|
function buildDefaultWorkflowConfig() {
|
|
52
60
|
return {
|
|
53
61
|
version: 1,
|
|
54
|
-
project: {
|
|
55
|
-
MICRO: ['setup', 'dev'],
|
|
56
|
-
SMALL: ['setup', 'product', 'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'dev', 'qa'],
|
|
57
|
-
MEDIUM: ['setup', 'product', 'analyst', 'architect', 'discovery-design-doc', 'ux-ui', 'pm', 'orchestrator', 'scope-check', 'dev', 'qa']
|
|
58
|
-
},
|
|
62
|
+
project: {
|
|
63
|
+
MICRO: ['setup', 'dev'],
|
|
64
|
+
SMALL: ['setup', 'product', 'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'dev', 'qa'],
|
|
65
|
+
MEDIUM: ['setup', 'product', 'analyst', 'architect', 'discovery-design-doc', 'ux-ui', 'pm', 'orchestrator', 'scope-check', 'dev', 'qa']
|
|
66
|
+
},
|
|
59
67
|
feature: DEFAULT_FEATURE_WORKFLOW_BY_CLASSIFICATION,
|
|
60
68
|
rules: {
|
|
61
69
|
required: ['dev'],
|
|
@@ -64,9 +72,9 @@ function buildDefaultWorkflowConfig() {
|
|
|
64
72
|
};
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
function parseFeaturesMarkdown(markdown) {
|
|
68
|
-
return String(markdown || '')
|
|
69
|
-
.split(/\r?\n/)
|
|
75
|
+
function parseFeaturesMarkdown(markdown) {
|
|
76
|
+
return String(markdown || '')
|
|
77
|
+
.split(/\r?\n/)
|
|
70
78
|
.slice(3)
|
|
71
79
|
.map((line) => line.trim())
|
|
72
80
|
.filter(Boolean)
|
|
@@ -79,39 +87,39 @@ function parseFeaturesMarkdown(markdown) {
|
|
|
79
87
|
started: parts[3],
|
|
80
88
|
completed: parts[4]
|
|
81
89
|
}))
|
|
82
|
-
.filter((row) => row.slug && row.slug !== 'slug')
|
|
83
|
-
.filter((row) => !/^-+$/ .test(row.slug));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function normalizeScopeCheckMode(input) {
|
|
87
|
-
const mode = String(input || '').trim().toLowerCase();
|
|
88
|
-
return SCOPE_CHECK_MODES.has(mode) ? mode : null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function getScopeCheckModeOption(options = {}) {
|
|
92
|
-
return normalizeScopeCheckMode(
|
|
93
|
-
options.scopeMode ||
|
|
94
|
-
options['scope-mode'] ||
|
|
95
|
-
options.checkMode ||
|
|
96
|
-
options['check-mode'] ||
|
|
97
|
-
options.mode
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function chooseActiveFeature(features, preferredSlug = null) {
|
|
102
|
-
const activeFeatures = (features || []).filter((feature) => feature.status === 'in_progress');
|
|
103
|
-
if (preferredSlug) {
|
|
104
|
-
const preferred = activeFeatures.find((feature) => feature.slug === preferredSlug);
|
|
105
|
-
if (preferred) return preferred;
|
|
106
|
-
}
|
|
107
|
-
return activeFeatures.length > 0 ? activeFeatures[activeFeatures.length - 1] : null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function readJsonIfExists(filePath) {
|
|
111
|
-
if (!(await exists(filePath))) return null;
|
|
112
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
113
|
-
return JSON.parse(content);
|
|
114
|
-
}
|
|
90
|
+
.filter((row) => row.slug && row.slug !== 'slug')
|
|
91
|
+
.filter((row) => !/^-+$/ .test(row.slug));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeScopeCheckMode(input) {
|
|
95
|
+
const mode = String(input || '').trim().toLowerCase();
|
|
96
|
+
return SCOPE_CHECK_MODES.has(mode) ? mode : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getScopeCheckModeOption(options = {}) {
|
|
100
|
+
return normalizeScopeCheckMode(
|
|
101
|
+
options.scopeMode ||
|
|
102
|
+
options['scope-mode'] ||
|
|
103
|
+
options.checkMode ||
|
|
104
|
+
options['check-mode'] ||
|
|
105
|
+
options.mode
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function chooseActiveFeature(features, preferredSlug = null) {
|
|
110
|
+
const activeFeatures = (features || []).filter((feature) => feature.status === 'in_progress');
|
|
111
|
+
if (preferredSlug) {
|
|
112
|
+
const preferred = activeFeatures.find((feature) => feature.slug === preferredSlug);
|
|
113
|
+
if (preferred) return preferred;
|
|
114
|
+
}
|
|
115
|
+
return activeFeatures.length > 0 ? activeFeatures[activeFeatures.length - 1] : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function readJsonIfExists(filePath) {
|
|
119
|
+
if (!(await exists(filePath))) return null;
|
|
120
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
121
|
+
return JSON.parse(content);
|
|
122
|
+
}
|
|
115
123
|
|
|
116
124
|
async function writeJson(filePath, payload) {
|
|
117
125
|
await ensureDir(path.dirname(filePath));
|
|
@@ -213,19 +221,19 @@ async function resolveExistingInstructionPath(targetDir, agent, locale) {
|
|
|
213
221
|
return agent.path;
|
|
214
222
|
}
|
|
215
223
|
|
|
216
|
-
async function detectWorkflowMode(targetDir) {
|
|
217
|
-
const prdPath = path.join(targetDir, '.aioson/context/prd.md');
|
|
218
|
-
const featuresPath = path.join(targetDir, '.aioson/context/features.md');
|
|
219
|
-
const handoffPath = path.join(targetDir, '.aioson/context/last-handoff.json');
|
|
220
|
-
const hasProjectPrd = await exists(prdPath);
|
|
221
|
-
const featuresMarkdown = await fs.readFile(featuresPath, 'utf8').catch(() => '');
|
|
222
|
-
const features = parseFeaturesMarkdown(featuresMarkdown);
|
|
223
|
-
const lastHandoff = await readJsonIfExists(handoffPath).catch(() => null);
|
|
224
|
-
const preferredSlug = lastHandoff && lastHandoff.feature_slug ? lastHandoff.feature_slug : null;
|
|
225
|
-
const activeFeature = chooseActiveFeature(features, preferredSlug);
|
|
226
|
-
|
|
227
|
-
if (activeFeature) {
|
|
228
|
-
return {
|
|
224
|
+
async function detectWorkflowMode(targetDir) {
|
|
225
|
+
const prdPath = path.join(targetDir, '.aioson/context/prd.md');
|
|
226
|
+
const featuresPath = path.join(targetDir, '.aioson/context/features.md');
|
|
227
|
+
const handoffPath = path.join(targetDir, '.aioson/context/last-handoff.json');
|
|
228
|
+
const hasProjectPrd = await exists(prdPath);
|
|
229
|
+
const featuresMarkdown = await fs.readFile(featuresPath, 'utf8').catch(() => '');
|
|
230
|
+
const features = parseFeaturesMarkdown(featuresMarkdown);
|
|
231
|
+
const lastHandoff = await readJsonIfExists(handoffPath).catch(() => null);
|
|
232
|
+
const preferredSlug = lastHandoff && lastHandoff.feature_slug ? lastHandoff.feature_slug : null;
|
|
233
|
+
const activeFeature = chooseActiveFeature(features, preferredSlug);
|
|
234
|
+
|
|
235
|
+
if (activeFeature) {
|
|
236
|
+
return {
|
|
229
237
|
mode: 'feature',
|
|
230
238
|
featureSlug: activeFeature.slug,
|
|
231
239
|
features
|
|
@@ -245,15 +253,15 @@ function getSequenceForMode(config, mode, classification) {
|
|
|
245
253
|
return Array.isArray(sequence) && sequence.length > 0 ? [...sequence] : [];
|
|
246
254
|
}
|
|
247
255
|
|
|
248
|
-
async function validateStageArtifacts(targetDir, state, stage) {
|
|
249
|
-
const base = path.join(targetDir, '.aioson/context');
|
|
250
|
-
const slug = state.featureSlug;
|
|
251
|
-
const anyExists = async (candidates) => {
|
|
252
|
-
for (const candidate of candidates) {
|
|
253
|
-
if (await exists(candidate)) return true;
|
|
254
|
-
}
|
|
255
|
-
return false;
|
|
256
|
-
};
|
|
256
|
+
async function validateStageArtifacts(targetDir, state, stage) {
|
|
257
|
+
const base = path.join(targetDir, '.aioson/context');
|
|
258
|
+
const slug = state.featureSlug;
|
|
259
|
+
const anyExists = async (candidates) => {
|
|
260
|
+
for (const candidate of candidates) {
|
|
261
|
+
if (await exists(candidate)) return true;
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
};
|
|
257
265
|
|
|
258
266
|
if (stage === 'setup') {
|
|
259
267
|
const context = await validateProjectContextFile(targetDir);
|
|
@@ -269,41 +277,51 @@ async function validateStageArtifacts(targetDir, state, stage) {
|
|
|
269
277
|
return await exists(path.join(base, 'prd.md'));
|
|
270
278
|
}
|
|
271
279
|
|
|
272
|
-
if (stage === 'analyst') {
|
|
273
|
-
if (state.mode === 'feature' && slug) {
|
|
274
|
-
const requirements = path.join(base, `requirements-${slug}.md`);
|
|
275
|
-
const spec = path.join(base, `spec-${slug}.md`);
|
|
276
|
-
return (await exists(requirements)) && (await exists(spec));
|
|
277
|
-
}
|
|
278
|
-
return await exists(path.join(base, 'discovery.md'));
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (stage === 'scope-check') {
|
|
282
|
-
if (state.mode === 'feature' && slug) {
|
|
283
|
-
return await exists(path.join(base, `scope-check-${slug}.md`));
|
|
284
|
-
}
|
|
285
|
-
return await exists(path.join(base, 'scope-check.md'));
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (stage === 'architect') {
|
|
289
|
-
return await exists(path.join(base, 'architecture.md'));
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (stage === 'ux-ui') {
|
|
293
|
-
return await exists(path.join(base, 'ui-spec.md'));
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (stage === 'discovery-design-doc') {
|
|
297
|
-
const designDocCandidates = slug
|
|
298
|
-
? [path.join(base, `design-doc-${slug}.md`), path.join(base, 'design-doc.md')]
|
|
299
|
-
: [path.join(base, 'design-doc.md')];
|
|
300
|
-
const readinessCandidates = slug
|
|
301
|
-
? [path.join(base, `readiness-${slug}.md`), path.join(base, 'readiness.md')]
|
|
302
|
-
: [path.join(base, 'readiness.md')];
|
|
303
|
-
return (await anyExists(designDocCandidates)) && (await anyExists(readinessCandidates));
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (stage === '
|
|
280
|
+
if (stage === 'analyst') {
|
|
281
|
+
if (state.mode === 'feature' && slug) {
|
|
282
|
+
const requirements = path.join(base, `requirements-${slug}.md`);
|
|
283
|
+
const spec = path.join(base, `spec-${slug}.md`);
|
|
284
|
+
return (await exists(requirements)) && (await exists(spec));
|
|
285
|
+
}
|
|
286
|
+
return await exists(path.join(base, 'discovery.md'));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (stage === 'scope-check') {
|
|
290
|
+
if (state.mode === 'feature' && slug) {
|
|
291
|
+
return await exists(path.join(base, `scope-check-${slug}.md`));
|
|
292
|
+
}
|
|
293
|
+
return await exists(path.join(base, 'scope-check.md'));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (stage === 'architect') {
|
|
297
|
+
return await exists(path.join(base, 'architecture.md'));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (stage === 'ux-ui') {
|
|
301
|
+
return await exists(path.join(base, 'ui-spec.md'));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (stage === 'discovery-design-doc') {
|
|
305
|
+
const designDocCandidates = slug
|
|
306
|
+
? [path.join(base, `design-doc-${slug}.md`), path.join(base, 'design-doc.md')]
|
|
307
|
+
: [path.join(base, 'design-doc.md')];
|
|
308
|
+
const readinessCandidates = slug
|
|
309
|
+
? [path.join(base, `readiness-${slug}.md`), path.join(base, 'readiness.md')]
|
|
310
|
+
: [path.join(base, 'readiness.md')];
|
|
311
|
+
return (await anyExists(designDocCandidates)) && (await anyExists(readinessCandidates));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (stage === 'pm') {
|
|
315
|
+
// Feature mode: @pm's canonical artifact is the implementation plan
|
|
316
|
+
// (Gate C input). Project mode has no single canonical pm artifact —
|
|
317
|
+
// the handoff contract covers feature MEDIUM (AC-SDLC-16).
|
|
318
|
+
if (state.mode === 'feature' && slug) {
|
|
319
|
+
return await exists(path.join(base, `implementation-plan-${slug}.md`));
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (stage === 'orchestrator') {
|
|
307
325
|
return await exists(path.join(base, 'parallel'));
|
|
308
326
|
}
|
|
309
327
|
|
|
@@ -426,11 +444,17 @@ function reconcileWorkflowState(state) {
|
|
|
426
444
|
};
|
|
427
445
|
}
|
|
428
446
|
|
|
429
|
-
function isInferableStage(stage) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
447
|
+
function isInferableStage(stage) {
|
|
448
|
+
// discovery-design-doc is inferable from its design-doc + readiness artifacts
|
|
449
|
+
// (it has both a validateStageArtifacts branch and a handoff contract). Without
|
|
450
|
+
// it, MEDIUM sequences — where scope-check sits AFTER discovery-design-doc —
|
|
451
|
+
// could never infer scope-check as completed during stale-state recovery.
|
|
452
|
+
// pm is inferable from implementation-plan-{slug}.md for the same reason:
|
|
453
|
+
// it sits before scope-check in the MEDIUM feature sequence.
|
|
454
|
+
return ['setup', 'product', 'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'ux-ui', 'pm', 'orchestrator'].includes(
|
|
455
|
+
normalizeAgentName(stage)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
434
458
|
|
|
435
459
|
function isSecurityGateBlocked(contractCheck, state, stageName) {
|
|
436
460
|
if (normalizeAgentName(stageName) !== 'qa' || state.mode !== 'feature' || !state.featureSlug) {
|
|
@@ -465,58 +489,58 @@ function buildQaSecurityAuditBriefing(result, targetDir) {
|
|
|
465
489
|
].join('\n');
|
|
466
490
|
}
|
|
467
491
|
|
|
468
|
-
async function inferCompletedStages(targetDir, draftState) {
|
|
469
|
-
const completed = [];
|
|
470
|
-
for (const stage of draftState.sequence) {
|
|
471
|
-
if (!isInferableStage(stage)) break;
|
|
472
|
-
const valid = await validateStageArtifacts(targetDir, draftState, stage);
|
|
473
|
-
if (!valid) break;
|
|
474
|
-
const contractCheck = await validateHandoffContract(targetDir, draftState, normalizeAgentName(stage));
|
|
475
|
-
if (!contractCheck.ok) break;
|
|
476
|
-
completed.push(normalizeAgentName(stage));
|
|
477
|
-
}
|
|
478
|
-
return completed;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function mergeInferredCompletedStages(state, inferredCompleted) {
|
|
482
|
-
if (!state || !Array.isArray(state.sequence) || !Array.isArray(inferredCompleted)) {
|
|
483
|
-
return { state, changed: false };
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const sequence = state.sequence.map(normalizeAgentName);
|
|
487
|
-
const completedSet = new Set((state.completed || []).map(normalizeAgentName).filter(Boolean));
|
|
488
|
-
const skippedSet = new Set((state.skipped || []).map(normalizeAgentName).filter(Boolean));
|
|
489
|
-
let changed = false;
|
|
490
|
-
|
|
491
|
-
for (const stage of inferredCompleted.map(normalizeAgentName).filter(Boolean)) {
|
|
492
|
-
if (!sequence.includes(stage)) continue;
|
|
493
|
-
if (!completedSet.has(stage)) {
|
|
494
|
-
completedSet.add(stage);
|
|
495
|
-
changed = true;
|
|
496
|
-
}
|
|
497
|
-
if (skippedSet.delete(stage)) {
|
|
498
|
-
changed = true;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (!changed) return { state, changed: false };
|
|
503
|
-
|
|
504
|
-
return {
|
|
505
|
-
changed: true,
|
|
506
|
-
state: buildStatePayload({
|
|
507
|
-
...state,
|
|
508
|
-
sequence,
|
|
509
|
-
completed: sequence.filter((stage) => completedSet.has(stage)),
|
|
510
|
-
skipped: sequence.filter((stage) => skippedSet.has(stage))
|
|
511
|
-
})
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// SF-project-18: cross-check workflow.state.json#completed against runtime
|
|
516
|
-
// telemetry. Stages claimed as completed without a corresponding agent_done
|
|
517
|
-
// event in .aioson/runtime/aios.sqlite are surfaced as a warning. Detection
|
|
518
|
-
// is best-effort — if the runtime DB is unavailable, the check is silently
|
|
519
|
-
// skipped (the framework still works in environments without telemetry).
|
|
492
|
+
async function inferCompletedStages(targetDir, draftState) {
|
|
493
|
+
const completed = [];
|
|
494
|
+
for (const stage of draftState.sequence) {
|
|
495
|
+
if (!isInferableStage(stage)) break;
|
|
496
|
+
const valid = await validateStageArtifacts(targetDir, draftState, stage);
|
|
497
|
+
if (!valid) break;
|
|
498
|
+
const contractCheck = await validateHandoffContract(targetDir, draftState, normalizeAgentName(stage));
|
|
499
|
+
if (!contractCheck.ok) break;
|
|
500
|
+
completed.push(normalizeAgentName(stage));
|
|
501
|
+
}
|
|
502
|
+
return completed;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function mergeInferredCompletedStages(state, inferredCompleted) {
|
|
506
|
+
if (!state || !Array.isArray(state.sequence) || !Array.isArray(inferredCompleted)) {
|
|
507
|
+
return { state, changed: false };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const sequence = state.sequence.map(normalizeAgentName);
|
|
511
|
+
const completedSet = new Set((state.completed || []).map(normalizeAgentName).filter(Boolean));
|
|
512
|
+
const skippedSet = new Set((state.skipped || []).map(normalizeAgentName).filter(Boolean));
|
|
513
|
+
let changed = false;
|
|
514
|
+
|
|
515
|
+
for (const stage of inferredCompleted.map(normalizeAgentName).filter(Boolean)) {
|
|
516
|
+
if (!sequence.includes(stage)) continue;
|
|
517
|
+
if (!completedSet.has(stage)) {
|
|
518
|
+
completedSet.add(stage);
|
|
519
|
+
changed = true;
|
|
520
|
+
}
|
|
521
|
+
if (skippedSet.delete(stage)) {
|
|
522
|
+
changed = true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (!changed) return { state, changed: false };
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
changed: true,
|
|
530
|
+
state: buildStatePayload({
|
|
531
|
+
...state,
|
|
532
|
+
sequence,
|
|
533
|
+
completed: sequence.filter((stage) => completedSet.has(stage)),
|
|
534
|
+
skipped: sequence.filter((stage) => skippedSet.has(stage))
|
|
535
|
+
})
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// SF-project-18: cross-check workflow.state.json#completed against runtime
|
|
540
|
+
// telemetry. Stages claimed as completed without a corresponding agent_done
|
|
541
|
+
// event in .aioson/runtime/aios.sqlite are surfaced as a warning. Detection
|
|
542
|
+
// is best-effort — if the runtime DB is unavailable, the check is silently
|
|
543
|
+
// skipped (the framework still works in environments without telemetry).
|
|
520
544
|
async function detectUnsubstantiatedCompletions(targetDir, completedStages, logger = null) {
|
|
521
545
|
if (!Array.isArray(completedStages) || completedStages.length === 0) return [];
|
|
522
546
|
let runtimeStore;
|
|
@@ -529,35 +553,48 @@ async function detectUnsubstantiatedCompletions(targetDir, completedStages, logg
|
|
|
529
553
|
let dbExists;
|
|
530
554
|
try { dbExists = await runtimeStore.runtimeStoreExists(targetDir); } catch { return []; }
|
|
531
555
|
if (!dbExists) return [];
|
|
532
|
-
let
|
|
556
|
+
let handle;
|
|
533
557
|
try {
|
|
534
|
-
|
|
558
|
+
handle = await runtimeStore.openRuntimeDb(targetDir);
|
|
535
559
|
} catch {
|
|
536
560
|
return [];
|
|
537
561
|
}
|
|
562
|
+
// openRuntimeDb resolves to { db, dbPath, runtimeDir } — the raw better-sqlite3
|
|
563
|
+
// handle lives on `.db`.
|
|
564
|
+
const db = handle && handle.db;
|
|
538
565
|
if (!db || typeof db.prepare !== 'function') {
|
|
539
566
|
try { if (db && typeof db.close === 'function') db.close(); } catch { /* ignore */ }
|
|
540
567
|
return [];
|
|
541
568
|
}
|
|
542
|
-
|
|
569
|
+
let unsubstantiated = [];
|
|
543
570
|
try {
|
|
544
571
|
let stmt;
|
|
545
572
|
try {
|
|
573
|
+
// agent identity lives on execution_events.agent_name (agent_events has no
|
|
574
|
+
// agent column). agent_done/stage_completed events are written there by
|
|
575
|
+
// appendRunEvent for every tracked run.
|
|
546
576
|
stmt = db.prepare(
|
|
547
|
-
"SELECT 1 FROM
|
|
577
|
+
"SELECT 1 FROM execution_events WHERE agent_name = ? AND event_type IN ('agent_done', 'stage_completed') LIMIT 1"
|
|
548
578
|
);
|
|
549
579
|
} catch {
|
|
550
580
|
// schema differences across versions — abort the cross-check.
|
|
551
581
|
return [];
|
|
552
582
|
}
|
|
583
|
+
const missing = [];
|
|
584
|
+
let substantiated = 0;
|
|
553
585
|
for (const stage of completedStages) {
|
|
554
586
|
try {
|
|
555
|
-
|
|
556
|
-
|
|
587
|
+
if (stmt.get(stage)) substantiated += 1;
|
|
588
|
+
else missing.push(stage);
|
|
557
589
|
} catch {
|
|
558
590
|
return [];
|
|
559
591
|
}
|
|
560
592
|
}
|
|
593
|
+
// Only treat missing stages as suspicious when the workflow demonstrably
|
|
594
|
+
// emits per-stage telemetry (≥1 completed stage has an agent_done event).
|
|
595
|
+
// Projects that never emit per-stage telemetry would otherwise warn on every
|
|
596
|
+
// run — keep the cross-check best-effort and silent for them.
|
|
597
|
+
unsubstantiated = substantiated > 0 ? missing : [];
|
|
561
598
|
} finally {
|
|
562
599
|
try { db.close(); } catch { /* ignore */ }
|
|
563
600
|
}
|
|
@@ -574,39 +611,39 @@ async function loadOrCreateState(targetDir, options = {}) {
|
|
|
574
611
|
const statePath = path.join(targetDir, STATE_RELATIVE_PATH);
|
|
575
612
|
let existing = await readJsonIfExists(statePath);
|
|
576
613
|
|
|
577
|
-
// Mode/feature-transition guard: if the persisted state no longer matches
|
|
578
|
-
// the current mode from features.md, it is stale. This covers both directions:
|
|
579
|
-
// a feature was paused/closed and project mode should resume, or a new
|
|
580
|
-
// feature was opened while a project workflow state still exists.
|
|
581
|
-
if (existing) {
|
|
582
|
-
const modeInfo = await detectWorkflowMode(targetDir);
|
|
583
|
-
if (
|
|
584
|
-
existing.mode !== modeInfo.mode ||
|
|
585
|
-
(modeInfo.mode === 'feature' && existing.featureSlug !== modeInfo.featureSlug) ||
|
|
586
|
-
(modeInfo.mode !== 'feature' && existing.featureSlug)
|
|
587
|
-
) {
|
|
588
|
-
existing = null;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
if (existing && typeof existing === 'object' && Array.isArray(existing.sequence)) {
|
|
593
|
-
// SF-project-18: warn-on-mismatch only, never refuse — preserves
|
|
594
|
-
// backwards-compat with environments that lack runtime telemetry.
|
|
595
|
-
if (Array.isArray(existing.completed) && existing.completed.length > 0 && options.logger) {
|
|
596
|
-
await detectUnsubstantiatedCompletions(targetDir, existing.completed, options.logger);
|
|
597
|
-
}
|
|
598
|
-
const reconciled = reconcileWorkflowState(existing);
|
|
599
|
-
const inferredCompleted = (reconciled.state.current || (reconciled.state.detour && reconciled.state.detour.active))
|
|
600
|
-
? []
|
|
601
|
-
: await inferCompletedStages(targetDir, reconciled.state);
|
|
602
|
-
const merged = mergeInferredCompletedStages(reconciled.state, inferredCompleted);
|
|
603
|
-
const finalReconciled = merged.changed ? reconcileWorkflowState(merged.state) : reconciled;
|
|
604
|
-
const changed = reconciled.changed || merged.changed || finalReconciled.changed;
|
|
605
|
-
if (changed) {
|
|
606
|
-
await writeJson(statePath, finalReconciled.state);
|
|
607
|
-
}
|
|
608
|
-
return { statePath, state: finalReconciled.state, created: false };
|
|
609
|
-
}
|
|
614
|
+
// Mode/feature-transition guard: if the persisted state no longer matches
|
|
615
|
+
// the current mode from features.md, it is stale. This covers both directions:
|
|
616
|
+
// a feature was paused/closed and project mode should resume, or a new
|
|
617
|
+
// feature was opened while a project workflow state still exists.
|
|
618
|
+
if (existing) {
|
|
619
|
+
const modeInfo = await detectWorkflowMode(targetDir);
|
|
620
|
+
if (
|
|
621
|
+
existing.mode !== modeInfo.mode ||
|
|
622
|
+
(modeInfo.mode === 'feature' && existing.featureSlug !== modeInfo.featureSlug) ||
|
|
623
|
+
(modeInfo.mode !== 'feature' && existing.featureSlug)
|
|
624
|
+
) {
|
|
625
|
+
existing = null;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (existing && typeof existing === 'object' && Array.isArray(existing.sequence)) {
|
|
630
|
+
// SF-project-18: warn-on-mismatch only, never refuse — preserves
|
|
631
|
+
// backwards-compat with environments that lack runtime telemetry.
|
|
632
|
+
if (Array.isArray(existing.completed) && existing.completed.length > 0 && options.logger) {
|
|
633
|
+
await detectUnsubstantiatedCompletions(targetDir, existing.completed, options.logger);
|
|
634
|
+
}
|
|
635
|
+
const reconciled = reconcileWorkflowState(existing);
|
|
636
|
+
const inferredCompleted = (reconciled.state.current || (reconciled.state.detour && reconciled.state.detour.active))
|
|
637
|
+
? []
|
|
638
|
+
: await inferCompletedStages(targetDir, reconciled.state);
|
|
639
|
+
const merged = mergeInferredCompletedStages(reconciled.state, inferredCompleted);
|
|
640
|
+
const finalReconciled = merged.changed ? reconcileWorkflowState(merged.state) : reconciled;
|
|
641
|
+
const changed = reconciled.changed || merged.changed || finalReconciled.changed;
|
|
642
|
+
if (changed) {
|
|
643
|
+
await writeJson(statePath, finalReconciled.state);
|
|
644
|
+
}
|
|
645
|
+
return { statePath, state: finalReconciled.state, created: false };
|
|
646
|
+
}
|
|
610
647
|
|
|
611
648
|
const context = await validateProjectContextFile(targetDir);
|
|
612
649
|
const modeInfo = await detectWorkflowMode(targetDir);
|
|
@@ -856,7 +893,7 @@ function applySkip(config, state, target) {
|
|
|
856
893
|
});
|
|
857
894
|
}
|
|
858
895
|
|
|
859
|
-
async function ensureFeatureDossier(targetDir, state) {
|
|
896
|
+
async function ensureFeatureDossier(targetDir, state) {
|
|
860
897
|
if (state.mode !== 'feature' || !state.featureSlug) return;
|
|
861
898
|
const classification = String(state.classification || '').toUpperCase();
|
|
862
899
|
if (classification !== 'SMALL' && classification !== 'MEDIUM') return;
|
|
@@ -912,205 +949,205 @@ async function ensureFeatureDossier(targetDir, state) {
|
|
|
912
949
|
mode
|
|
913
950
|
}
|
|
914
951
|
});
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
async function readTextIfExists(filePath) {
|
|
919
|
-
try {
|
|
920
|
-
return await fs.readFile(filePath, 'utf8');
|
|
921
|
-
} catch (err) {
|
|
922
|
-
if (err && err.code === 'ENOENT') return null;
|
|
923
|
-
throw err;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
function parseDevStateContextPackage(raw) {
|
|
928
|
-
if (!raw) return [];
|
|
929
|
-
const section = raw.match(/## Context package\r?\n\r?\n([\s\S]*?)(?:\r?\n\r?\n## |\s*$)/);
|
|
930
|
-
if (!section) return [];
|
|
931
|
-
return section[1]
|
|
932
|
-
.split(/\r?\n/)
|
|
933
|
-
.map((line) => {
|
|
934
|
-
const match = line.trim().match(/^\d+\.\s+(.+)$/);
|
|
935
|
-
return match ? match[1].trim() : null;
|
|
936
|
-
})
|
|
937
|
-
.filter(Boolean);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
function parseDevStateFrontmatter(raw) {
|
|
941
|
-
if (!raw) return {};
|
|
942
|
-
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
943
|
-
if (!fmMatch) return {};
|
|
944
|
-
const fm = {};
|
|
945
|
-
for (const line of fmMatch[1].split(/\r?\n/)) {
|
|
946
|
-
const idx = line.indexOf(':');
|
|
947
|
-
if (idx === -1) continue;
|
|
948
|
-
const key = line.slice(0, idx).trim();
|
|
949
|
-
const value = line.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
|
|
950
|
-
if (key) fm[key] = value;
|
|
951
|
-
}
|
|
952
|
-
return fm;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
function shouldUseDevStateForFeature(raw, featureSlug) {
|
|
956
|
-
if (!raw) return false;
|
|
957
|
-
const fm = parseDevStateFrontmatter(raw);
|
|
958
|
-
if (!fm.active_feature) return false;
|
|
959
|
-
const status = String(fm.status || '').toLowerCase();
|
|
960
|
-
if (status === 'done' || status === 'abandoned') return false;
|
|
961
|
-
if (fm.active_feature !== featureSlug) return false;
|
|
962
|
-
return true;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
function normalizeContextDependency(relPath) {
|
|
966
|
-
const cleaned = String(relPath || '').trim().replace(/\\/g, '/');
|
|
967
|
-
if (!cleaned) return null;
|
|
968
|
-
if (cleaned.startsWith('.aioson/')) return cleaned;
|
|
969
|
-
return `.aioson/context/${cleaned}`;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
async function resolveStageDependencies(targetDir, state, stageName, agent) {
|
|
973
|
-
if (stageName === 'scope-check') {
|
|
974
|
-
const contextDir = path.join(targetDir, '.aioson', 'context');
|
|
975
|
-
const slug = state.featureSlug;
|
|
976
|
-
const candidates = [
|
|
977
|
-
'project.context.md',
|
|
978
|
-
'features.md',
|
|
979
|
-
slug ? `prd-${slug}.md` : 'prd.md',
|
|
980
|
-
slug ? `requirements-${slug}.md` : 'discovery.md',
|
|
981
|
-
slug ? `spec-${slug}.md` : 'spec.md',
|
|
982
|
-
slug ? `sheldon-enrichment-${slug}.md` : 'sheldon-enrichment.md',
|
|
983
|
-
'architecture.md',
|
|
984
|
-
slug ? `design-doc-${slug}.md` : null,
|
|
985
|
-
slug ? `readiness-${slug}.md` : null,
|
|
986
|
-
'design-doc.md',
|
|
987
|
-
'readiness.md',
|
|
988
|
-
'ui-spec.md',
|
|
989
|
-
slug ? `implementation-plan-${slug}.md` : 'implementation-plan.md',
|
|
990
|
-
'dev-state.md',
|
|
991
|
-
'last-handoff.json',
|
|
992
|
-
'project-pulse.md'
|
|
993
|
-
].filter(Boolean);
|
|
994
|
-
const existing = [];
|
|
995
|
-
for (const candidate of candidates) {
|
|
996
|
-
if (await exists(path.join(contextDir, candidate))) {
|
|
997
|
-
existing.push(normalizeContextDependency(candidate));
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
return existing.length > 0 ? existing : agent.dependsOn;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
if (stageName === 'discovery-design-doc') {
|
|
1004
|
-
const contextDir = path.join(targetDir, '.aioson', 'context');
|
|
1005
|
-
const slug = state.featureSlug;
|
|
1006
|
-
const candidates = [
|
|
1007
|
-
'project.context.md',
|
|
1008
|
-
slug ? `prd-${slug}.md` : 'prd.md',
|
|
1009
|
-
slug ? `requirements-${slug}.md` : 'discovery.md',
|
|
1010
|
-
slug ? `spec-${slug}.md` : 'spec.md',
|
|
1011
|
-
'architecture.md',
|
|
1012
|
-
slug ? `design-doc-${slug}.md` : null,
|
|
1013
|
-
slug ? `readiness-${slug}.md` : null,
|
|
1014
|
-
'design-doc.md',
|
|
1015
|
-
'readiness.md',
|
|
1016
|
-
'project-map.md'
|
|
1017
|
-
].filter(Boolean);
|
|
1018
|
-
const existing = [];
|
|
1019
|
-
for (const candidate of candidates) {
|
|
1020
|
-
if (await exists(path.join(contextDir, candidate))) {
|
|
1021
|
-
existing.push(normalizeContextDependency(candidate));
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
return existing.length > 0 ? existing : agent.dependsOn;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
if (stageName !== 'dev' || state.mode !== 'feature' || !state.featureSlug) {
|
|
1028
|
-
return agent.dependsOn;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
const contextDir = path.join(targetDir, '.aioson', 'context');
|
|
1032
|
-
const devStatePath = path.join(contextDir, 'dev-state.md');
|
|
1033
|
-
const devStateRaw = await readTextIfExists(devStatePath);
|
|
1034
|
-
const devStatePackage = shouldUseDevStateForFeature(devStateRaw, state.featureSlug)
|
|
1035
|
-
? parseDevStateContextPackage(devStateRaw)
|
|
1036
|
-
.map(normalizeContextDependency)
|
|
1037
|
-
.filter(Boolean)
|
|
1038
|
-
: [];
|
|
1039
|
-
|
|
1040
|
-
if (devStatePackage.length > 0) {
|
|
1041
|
-
return Array.from(new Set(['.aioson/context/dev-state.md', ...devStatePackage]));
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
const slug = state.featureSlug;
|
|
1045
|
-
const candidates = [
|
|
1046
|
-
'project.context.md',
|
|
1047
|
-
`prd-${slug}.md`,
|
|
1048
|
-
`requirements-${slug}.md`,
|
|
1049
|
-
`spec-${slug}.md`,
|
|
1050
|
-
`design-doc-${slug}.md`,
|
|
1051
|
-
`readiness-${slug}.md`,
|
|
1052
|
-
'design-doc.md',
|
|
1053
|
-
'readiness.md',
|
|
1054
|
-
`scope-check-${slug}.md`,
|
|
1055
|
-
'scope-check.md',
|
|
1056
|
-
`implementation-plan-${slug}.md`
|
|
1057
|
-
];
|
|
1058
|
-
const existing = [];
|
|
1059
|
-
for (const candidate of candidates) {
|
|
1060
|
-
if (await exists(path.join(contextDir, candidate))) {
|
|
1061
|
-
existing.push(normalizeContextDependency(candidate));
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
return existing.length > 0 ? existing : agent.dependsOn;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
function inferScopeCheckMode(state, requestedMode = null) {
|
|
1068
|
-
if (requestedMode) return requestedMode;
|
|
1069
|
-
const completed = Array.isArray(state.completed) ? state.completed.map(normalizeAgentName) : [];
|
|
1070
|
-
const current = normalizeAgentName(state.current || state.next);
|
|
1071
|
-
if (completed.includes('dev')) return 'post-dev';
|
|
1072
|
-
if (completed.includes('qa') || completed.includes('tester') || completed.includes('pentester')) return 'post-fix';
|
|
1073
|
-
if (current === 'scope-check') return 'pre-dev';
|
|
1074
|
-
return 'pre-dev';
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
function buildScopeCheckActivationContext(state, mode) {
|
|
1078
|
-
const resolvedMode = inferScopeCheckMode(state, mode);
|
|
1079
|
-
const lines = [
|
|
1080
|
-
`Scope-check mode: ${resolvedMode}`,
|
|
1081
|
-
`Workflow mode: ${state.mode || 'unknown'}`,
|
|
1082
|
-
`Classification: ${state.classification || 'unknown'}`
|
|
1083
|
-
];
|
|
1084
|
-
if (state.featureSlug) lines.push(`Feature slug: ${state.featureSlug}`);
|
|
1085
|
-
if (resolvedMode === 'pre-dev') {
|
|
1086
|
-
lines.push('Compare user intent against planning artifacts before implementation.');
|
|
1087
|
-
} else if (resolvedMode === 'post-dev') {
|
|
1088
|
-
lines.push('Compare the approved scope-check/design artifacts against the actual implementation diff and changed files before QA.');
|
|
1089
|
-
} else if (resolvedMode === 'post-fix') {
|
|
1090
|
-
lines.push('Compare approved scope, QA/tester/pentester findings, and the correction diff; confirm the fix did not change product intent.');
|
|
1091
|
-
} else if (resolvedMode === 'final') {
|
|
1092
|
-
lines.push('Reconcile intent, plan, delivered behavior, and remaining exclusions before close/commit/release.');
|
|
1093
|
-
}
|
|
1094
|
-
return lines.join('\n');
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
function buildStageActivationContext(state, stageName, dependencies, scopeCheckMode = null) {
|
|
1098
|
-
if (stageName === 'scope-check') {
|
|
1099
|
-
return buildScopeCheckActivationContext(state, scopeCheckMode);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
if (stageName !== 'dev' || state.mode !== 'feature' || !state.featureSlug) return '';
|
|
1103
|
-
return [
|
|
1104
|
-
`Feature slug: ${state.featureSlug}`,
|
|
1105
|
-
`Workflow mode: ${state.mode}`,
|
|
1106
|
-
`Classification: ${state.classification || 'unknown'}`,
|
|
1107
|
-
dependencies.includes('.aioson/context/dev-state.md')
|
|
1108
|
-
? 'Resume source: .aioson/context/dev-state.md'
|
|
1109
|
-
: 'Resume source: active feature artifacts'
|
|
1110
|
-
].join('\n');
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
async function activateStage(targetDir, state, locale, tool, explicitAgent = null, requestedMode = null, scopeCheckMode = null) {
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async function readTextIfExists(filePath) {
|
|
956
|
+
try {
|
|
957
|
+
return await fs.readFile(filePath, 'utf8');
|
|
958
|
+
} catch (err) {
|
|
959
|
+
if (err && err.code === 'ENOENT') return null;
|
|
960
|
+
throw err;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function parseDevStateContextPackage(raw) {
|
|
965
|
+
if (!raw) return [];
|
|
966
|
+
const section = raw.match(/## Context package\r?\n\r?\n([\s\S]*?)(?:\r?\n\r?\n## |\s*$)/);
|
|
967
|
+
if (!section) return [];
|
|
968
|
+
return section[1]
|
|
969
|
+
.split(/\r?\n/)
|
|
970
|
+
.map((line) => {
|
|
971
|
+
const match = line.trim().match(/^\d+\.\s+(.+)$/);
|
|
972
|
+
return match ? match[1].trim() : null;
|
|
973
|
+
})
|
|
974
|
+
.filter(Boolean);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function parseDevStateFrontmatter(raw) {
|
|
978
|
+
if (!raw) return {};
|
|
979
|
+
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
980
|
+
if (!fmMatch) return {};
|
|
981
|
+
const fm = {};
|
|
982
|
+
for (const line of fmMatch[1].split(/\r?\n/)) {
|
|
983
|
+
const idx = line.indexOf(':');
|
|
984
|
+
if (idx === -1) continue;
|
|
985
|
+
const key = line.slice(0, idx).trim();
|
|
986
|
+
const value = line.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
|
|
987
|
+
if (key) fm[key] = value;
|
|
988
|
+
}
|
|
989
|
+
return fm;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function shouldUseDevStateForFeature(raw, featureSlug) {
|
|
993
|
+
if (!raw) return false;
|
|
994
|
+
const fm = parseDevStateFrontmatter(raw);
|
|
995
|
+
if (!fm.active_feature) return false;
|
|
996
|
+
const status = String(fm.status || '').toLowerCase();
|
|
997
|
+
if (status === 'done' || status === 'abandoned') return false;
|
|
998
|
+
if (fm.active_feature !== featureSlug) return false;
|
|
999
|
+
return true;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function normalizeContextDependency(relPath) {
|
|
1003
|
+
const cleaned = String(relPath || '').trim().replace(/\\/g, '/');
|
|
1004
|
+
if (!cleaned) return null;
|
|
1005
|
+
if (cleaned.startsWith('.aioson/')) return cleaned;
|
|
1006
|
+
return `.aioson/context/${cleaned}`;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
async function resolveStageDependencies(targetDir, state, stageName, agent) {
|
|
1010
|
+
if (stageName === 'scope-check') {
|
|
1011
|
+
const contextDir = path.join(targetDir, '.aioson', 'context');
|
|
1012
|
+
const slug = state.featureSlug;
|
|
1013
|
+
const candidates = [
|
|
1014
|
+
'project.context.md',
|
|
1015
|
+
'features.md',
|
|
1016
|
+
slug ? `prd-${slug}.md` : 'prd.md',
|
|
1017
|
+
slug ? `requirements-${slug}.md` : 'discovery.md',
|
|
1018
|
+
slug ? `spec-${slug}.md` : 'spec.md',
|
|
1019
|
+
slug ? `sheldon-enrichment-${slug}.md` : 'sheldon-enrichment.md',
|
|
1020
|
+
'architecture.md',
|
|
1021
|
+
slug ? `design-doc-${slug}.md` : null,
|
|
1022
|
+
slug ? `readiness-${slug}.md` : null,
|
|
1023
|
+
'design-doc.md',
|
|
1024
|
+
'readiness.md',
|
|
1025
|
+
'ui-spec.md',
|
|
1026
|
+
slug ? `implementation-plan-${slug}.md` : 'implementation-plan.md',
|
|
1027
|
+
'dev-state.md',
|
|
1028
|
+
'last-handoff.json',
|
|
1029
|
+
'project-pulse.md'
|
|
1030
|
+
].filter(Boolean);
|
|
1031
|
+
const existing = [];
|
|
1032
|
+
for (const candidate of candidates) {
|
|
1033
|
+
if (await exists(path.join(contextDir, candidate))) {
|
|
1034
|
+
existing.push(normalizeContextDependency(candidate));
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return existing.length > 0 ? existing : agent.dependsOn;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (stageName === 'discovery-design-doc') {
|
|
1041
|
+
const contextDir = path.join(targetDir, '.aioson', 'context');
|
|
1042
|
+
const slug = state.featureSlug;
|
|
1043
|
+
const candidates = [
|
|
1044
|
+
'project.context.md',
|
|
1045
|
+
slug ? `prd-${slug}.md` : 'prd.md',
|
|
1046
|
+
slug ? `requirements-${slug}.md` : 'discovery.md',
|
|
1047
|
+
slug ? `spec-${slug}.md` : 'spec.md',
|
|
1048
|
+
'architecture.md',
|
|
1049
|
+
slug ? `design-doc-${slug}.md` : null,
|
|
1050
|
+
slug ? `readiness-${slug}.md` : null,
|
|
1051
|
+
'design-doc.md',
|
|
1052
|
+
'readiness.md',
|
|
1053
|
+
'project-map.md'
|
|
1054
|
+
].filter(Boolean);
|
|
1055
|
+
const existing = [];
|
|
1056
|
+
for (const candidate of candidates) {
|
|
1057
|
+
if (await exists(path.join(contextDir, candidate))) {
|
|
1058
|
+
existing.push(normalizeContextDependency(candidate));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return existing.length > 0 ? existing : agent.dependsOn;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (stageName !== 'dev' || state.mode !== 'feature' || !state.featureSlug) {
|
|
1065
|
+
return agent.dependsOn;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const contextDir = path.join(targetDir, '.aioson', 'context');
|
|
1069
|
+
const devStatePath = path.join(contextDir, 'dev-state.md');
|
|
1070
|
+
const devStateRaw = await readTextIfExists(devStatePath);
|
|
1071
|
+
const devStatePackage = shouldUseDevStateForFeature(devStateRaw, state.featureSlug)
|
|
1072
|
+
? parseDevStateContextPackage(devStateRaw)
|
|
1073
|
+
.map(normalizeContextDependency)
|
|
1074
|
+
.filter(Boolean)
|
|
1075
|
+
: [];
|
|
1076
|
+
|
|
1077
|
+
if (devStatePackage.length > 0) {
|
|
1078
|
+
return Array.from(new Set(['.aioson/context/dev-state.md', ...devStatePackage]));
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const slug = state.featureSlug;
|
|
1082
|
+
const candidates = [
|
|
1083
|
+
'project.context.md',
|
|
1084
|
+
`prd-${slug}.md`,
|
|
1085
|
+
`requirements-${slug}.md`,
|
|
1086
|
+
`spec-${slug}.md`,
|
|
1087
|
+
`design-doc-${slug}.md`,
|
|
1088
|
+
`readiness-${slug}.md`,
|
|
1089
|
+
'design-doc.md',
|
|
1090
|
+
'readiness.md',
|
|
1091
|
+
`scope-check-${slug}.md`,
|
|
1092
|
+
'scope-check.md',
|
|
1093
|
+
`implementation-plan-${slug}.md`
|
|
1094
|
+
];
|
|
1095
|
+
const existing = [];
|
|
1096
|
+
for (const candidate of candidates) {
|
|
1097
|
+
if (await exists(path.join(contextDir, candidate))) {
|
|
1098
|
+
existing.push(normalizeContextDependency(candidate));
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return existing.length > 0 ? existing : agent.dependsOn;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function inferScopeCheckMode(state, requestedMode = null) {
|
|
1105
|
+
if (requestedMode) return requestedMode;
|
|
1106
|
+
const completed = Array.isArray(state.completed) ? state.completed.map(normalizeAgentName) : [];
|
|
1107
|
+
const current = normalizeAgentName(state.current || state.next);
|
|
1108
|
+
if (completed.includes('dev')) return 'post-dev';
|
|
1109
|
+
if (completed.includes('qa') || completed.includes('tester') || completed.includes('pentester')) return 'post-fix';
|
|
1110
|
+
if (current === 'scope-check') return 'pre-dev';
|
|
1111
|
+
return 'pre-dev';
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function buildScopeCheckActivationContext(state, mode) {
|
|
1115
|
+
const resolvedMode = inferScopeCheckMode(state, mode);
|
|
1116
|
+
const lines = [
|
|
1117
|
+
`Scope-check mode: ${resolvedMode}`,
|
|
1118
|
+
`Workflow mode: ${state.mode || 'unknown'}`,
|
|
1119
|
+
`Classification: ${state.classification || 'unknown'}`
|
|
1120
|
+
];
|
|
1121
|
+
if (state.featureSlug) lines.push(`Feature slug: ${state.featureSlug}`);
|
|
1122
|
+
if (resolvedMode === 'pre-dev') {
|
|
1123
|
+
lines.push('Compare user intent against planning artifacts before implementation.');
|
|
1124
|
+
} else if (resolvedMode === 'post-dev') {
|
|
1125
|
+
lines.push('Compare the approved scope-check/design artifacts against the actual implementation diff and changed files before QA.');
|
|
1126
|
+
} else if (resolvedMode === 'post-fix') {
|
|
1127
|
+
lines.push('Compare approved scope, QA/tester/pentester findings, and the correction diff; confirm the fix did not change product intent.');
|
|
1128
|
+
} else if (resolvedMode === 'final') {
|
|
1129
|
+
lines.push('Reconcile intent, plan, delivered behavior, and remaining exclusions before close/commit/release.');
|
|
1130
|
+
}
|
|
1131
|
+
return lines.join('\n');
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function buildStageActivationContext(state, stageName, dependencies, scopeCheckMode = null) {
|
|
1135
|
+
if (stageName === 'scope-check') {
|
|
1136
|
+
return buildScopeCheckActivationContext(state, scopeCheckMode);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (stageName !== 'dev' || state.mode !== 'feature' || !state.featureSlug) return '';
|
|
1140
|
+
return [
|
|
1141
|
+
`Feature slug: ${state.featureSlug}`,
|
|
1142
|
+
`Workflow mode: ${state.mode}`,
|
|
1143
|
+
`Classification: ${state.classification || 'unknown'}`,
|
|
1144
|
+
dependencies.includes('.aioson/context/dev-state.md')
|
|
1145
|
+
? 'Resume source: .aioson/context/dev-state.md'
|
|
1146
|
+
: 'Resume source: active feature artifacts'
|
|
1147
|
+
].join('\n');
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
async function activateStage(targetDir, state, locale, tool, explicitAgent = null, requestedMode = null, scopeCheckMode = null) {
|
|
1114
1151
|
const stageName = normalizeAgentName(explicitAgent || state.current || state.next);
|
|
1115
1152
|
if (!stageName) {
|
|
1116
1153
|
return {
|
|
@@ -1209,17 +1246,32 @@ async function activateStage(targetDir, state, locale, tool, explicitAgent = nul
|
|
|
1209
1246
|
requestedMode
|
|
1210
1247
|
});
|
|
1211
1248
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1249
|
+
let autoHandoff = false;
|
|
1250
|
+
if (
|
|
1251
|
+
AUTOPILOT_HANDOFF_STAGES.has(stageName) &&
|
|
1252
|
+
state.mode === 'feature' &&
|
|
1253
|
+
(state.classification === 'SMALL' || state.classification === 'MEDIUM')
|
|
1254
|
+
) {
|
|
1255
|
+
try {
|
|
1256
|
+
const projectContext = await validateProjectContextFile(targetDir);
|
|
1257
|
+
autoHandoff = Boolean(projectContext && projectContext.data && projectContext.data.auto_handoff === true);
|
|
1258
|
+
} catch {
|
|
1259
|
+
autoHandoff = false;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const instructionPath = await resolveExistingInstructionPath(targetDir, agent, locale);
|
|
1264
|
+
const dependencies = await resolveStageDependencies(targetDir, state, stageName, agent);
|
|
1265
|
+
let prompt = buildAgentPrompt(agent, tool, {
|
|
1266
|
+
instructionPath,
|
|
1267
|
+
targetDir,
|
|
1268
|
+
interactionLanguage: locale,
|
|
1269
|
+
autonomyMode: effectiveMode,
|
|
1270
|
+
capabilitySummary: buildAgentCapabilitySummary(agentManifest, tool),
|
|
1271
|
+
dependsOn: dependencies,
|
|
1272
|
+
autoHandoff,
|
|
1273
|
+
activationContext: buildStageActivationContext(state, stageName, dependencies, scopeCheckMode)
|
|
1274
|
+
});
|
|
1223
1275
|
|
|
1224
1276
|
if (testBriefing) {
|
|
1225
1277
|
prompt += '\n\n' + testBriefing;
|
|
@@ -1489,10 +1541,10 @@ async function runWorkflowNext({ args, options, logger, t }) {
|
|
|
1489
1541
|
requestedAgent = 'validator';
|
|
1490
1542
|
}
|
|
1491
1543
|
|
|
1492
|
-
const activationAgent = normalizeAgentName(requestedAgent || state.current || state.next);
|
|
1493
|
-
const scopeCheckMode = activationAgent === 'scope-check' ? getScopeCheckModeOption(options) : null;
|
|
1494
|
-
const requestedAutonomyMode = scopeCheckMode && activationAgent === 'scope-check' ? null : options.mode || null;
|
|
1495
|
-
const activation = await activateStage(targetDir, state, locale, tool, requestedAgent, requestedAutonomyMode, scopeCheckMode);
|
|
1544
|
+
const activationAgent = normalizeAgentName(requestedAgent || state.current || state.next);
|
|
1545
|
+
const scopeCheckMode = activationAgent === 'scope-check' ? getScopeCheckModeOption(options) : null;
|
|
1546
|
+
const requestedAutonomyMode = scopeCheckMode && activationAgent === 'scope-check' ? null : options.mode || null;
|
|
1547
|
+
const activation = await activateStage(targetDir, state, locale, tool, requestedAgent, requestedAutonomyMode, scopeCheckMode);
|
|
1496
1548
|
state = activation.state;
|
|
1497
1549
|
|
|
1498
1550
|
// ── Living Memory: if a reflect manifest is pending (created above by the
|
|
@@ -1624,14 +1676,14 @@ module.exports = {
|
|
|
1624
1676
|
EVENTS_RELATIVE_PATH,
|
|
1625
1677
|
buildDefaultWorkflowConfig,
|
|
1626
1678
|
parseFeaturesMarkdown,
|
|
1627
|
-
readWorkflowConfig,
|
|
1628
|
-
detectWorkflowMode,
|
|
1629
|
-
loadOrCreateState,
|
|
1630
|
-
persistState,
|
|
1631
|
-
appendWorkflowEvent,
|
|
1632
|
-
resolveLocaleForTarget,
|
|
1633
|
-
reconcileWorkflowState,
|
|
1634
|
-
finalizeCurrentStage,
|
|
1679
|
+
readWorkflowConfig,
|
|
1680
|
+
detectWorkflowMode,
|
|
1681
|
+
loadOrCreateState,
|
|
1682
|
+
persistState,
|
|
1683
|
+
appendWorkflowEvent,
|
|
1684
|
+
resolveLocaleForTarget,
|
|
1685
|
+
reconcileWorkflowState,
|
|
1686
|
+
finalizeCurrentStage,
|
|
1635
1687
|
applySkip,
|
|
1636
1688
|
activateStage,
|
|
1637
1689
|
runWorkflowNext,
|