@jaimevalasek/aioson 1.22.0 → 1.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +932 -919
  2. package/docs/en/5-reference/cli-reference.md +85 -0
  3. package/docs/pt/4-agentes/pm.md +31 -4
  4. package/docs/pt/5-referencia/README.md +3 -0
  5. package/docs/pt/5-referencia/autopilot-handoff.md +131 -0
  6. package/docs/pt/5-referencia/comandos-cli.md +72 -6
  7. package/docs/pt/5-referencia/harness-retro.md +133 -0
  8. package/docs/pt/5-referencia/loop-guardrails.md +225 -0
  9. package/docs/pt/5-referencia/sdd-automation-scripts.md +25 -13
  10. package/package.json +1 -1
  11. package/src/agents.js +1 -1
  12. package/src/cli.js +70 -29
  13. package/src/commands/agent-epilogue.js +186 -0
  14. package/src/commands/context-select.js +33 -0
  15. package/src/commands/harness-preview.js +74 -0
  16. package/src/commands/harness-retro.js +221 -0
  17. package/src/commands/preflight-context.js +13 -9
  18. package/src/commands/review-cycle.js +328 -0
  19. package/src/commands/runtime.js +4 -4
  20. package/src/commands/self-implement-loop.js +12 -2
  21. package/src/commands/state-save.js +2 -0
  22. package/src/commands/workflow-execute.js +138 -28
  23. package/src/commands/workflow-next.js +11 -2
  24. package/src/commands/workflow-status.js +30 -10
  25. package/src/constants.js +15 -13
  26. package/src/context-memory.js +50 -25
  27. package/src/context-selector.js +394 -0
  28. package/src/harness/preview-artifact.js +85 -0
  29. package/src/i18n/messages/en.js +34 -7
  30. package/src/i18n/messages/es.js +34 -7
  31. package/src/i18n/messages/fr.js +34 -7
  32. package/src/i18n/messages/pt-BR.js +34 -7
  33. package/src/lib/retro/retro-aggregate.js +192 -0
  34. package/src/lib/retro/retro-render.js +185 -0
  35. package/src/lib/retro/retro-sources.js +624 -0
  36. package/src/parser.js +1 -1
  37. package/src/squad/preflight-context.js +26 -27
  38. package/template/.aioson/agents/analyst.md +41 -46
  39. package/template/.aioson/agents/architect.md +33 -46
  40. package/template/.aioson/agents/briefing.md +76 -67
  41. package/template/.aioson/agents/dev.md +73 -55
  42. package/template/.aioson/agents/deyvin.md +55 -50
  43. package/template/.aioson/agents/discovery-design-doc.md +35 -22
  44. package/template/.aioson/agents/manifests/architect.manifest.json +11 -1
  45. package/template/.aioson/agents/manifests/dev.manifest.json +15 -0
  46. package/template/.aioson/agents/manifests/pm.manifest.json +20 -0
  47. package/template/.aioson/agents/orchestrator.md +31 -18
  48. package/template/.aioson/agents/pentester.md +12 -4
  49. package/template/.aioson/agents/pm.md +41 -35
  50. package/template/.aioson/agents/product.md +116 -165
  51. package/template/.aioson/agents/qa.md +44 -13
  52. package/template/.aioson/agents/scope-check.md +46 -24
  53. package/template/.aioson/agents/sheldon.md +13 -0
  54. package/template/.aioson/agents/tester.md +28 -5
  55. package/template/.aioson/agents/ux-ui.md +36 -31
  56. package/template/.aioson/agents/validator.md +10 -2
  57. package/template/.aioson/config/autonomy-protocol.json +7 -0
  58. package/template/.aioson/design-docs/code-reuse.md +10 -5
  59. package/template/.aioson/design-docs/componentization.md +10 -5
  60. package/template/.aioson/design-docs/file-size.md +10 -5
  61. package/template/.aioson/design-docs/folder-structure.md +10 -5
  62. package/template/.aioson/design-docs/naming.md +10 -5
  63. package/template/.aioson/docs/autonomy-protocol.md +2 -2
  64. package/template/.aioson/docs/autopilot-handoff.md +82 -34
  65. package/template/.aioson/docs/briefing/briefing-craft.md +9 -3
  66. package/template/.aioson/docs/deyvin/continuity-recovery.md +18 -22
  67. package/template/.aioson/docs/product/conversation-playbook.md +8 -3
  68. package/template/.aioson/docs/product/prd-contract.md +8 -3
  69. package/template/.aioson/docs/product/quality-lens.md +8 -3
  70. package/template/.aioson/docs/product/research-loop.md +8 -3
  71. package/template/.aioson/docs/ux-ui/accessibility-audit.md +7 -2
  72. package/template/.aioson/docs/ux-ui/audit-mode.md +7 -2
  73. package/template/.aioson/docs/ux-ui/component-map.md +7 -2
  74. package/template/.aioson/docs/ux-ui/design-execution.md +7 -2
  75. package/template/.aioson/docs/ux-ui/design-gate.md +7 -2
  76. package/template/.aioson/docs/ux-ui/research-mode.md +7 -2
  77. package/template/.aioson/docs/ux-ui/site-delivery.md +7 -2
  78. package/template/.aioson/docs/ux-ui/token-contract.md +7 -2
  79. package/template/.aioson/rules/aioson-context-boundary.md +11 -9
  80. package/template/.aioson/rules/disk-first-artifacts.md +1 -1
  81. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +1 -1
  82. package/template/.aioson/skills/process/aioson-spec-driven/references/architect.md +3 -2
  83. package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +21 -9
  84. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +2 -1
  85. package/template/.aioson/skills/process/aioson-spec-driven/references/pm.md +2 -1
  86. package/template/.aioson/skills/static/web-research-cache.md +29 -8
  87. package/template/AGENTS.md +1 -1
  88. package/template/CLAUDE.md +1 -1
@@ -0,0 +1,328 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const { ensureDir, exists } = require('../utils');
6
+
7
+ const DEFAULT_MAX_CYCLES = 3;
8
+ const EXECUTION_STATE_RELATIVE_PATH = '.aioson/context/workflow-execute.json';
9
+
10
+ function resolveTargetDir(args) {
11
+ return path.resolve(process.cwd(), args[0] || '.');
12
+ }
13
+
14
+ function normalizeAgent(value, fallback) {
15
+ const normalized = String(value || fallback || '')
16
+ .trim()
17
+ .toLowerCase()
18
+ .replace(/^@/, '')
19
+ .replace(/[^a-z0-9-]+/g, '-')
20
+ .replace(/^-+|-+$/g, '');
21
+ return normalized || fallback;
22
+ }
23
+
24
+ function stateFileName(source, target) {
25
+ if (source === 'qa' && target === 'dev') return 'qa-dev-cycle.json';
26
+ return `review-cycle-${source}-${target}.json`;
27
+ }
28
+
29
+ function resolveStatePath(targetDir, source, target) {
30
+ return path.join(targetDir, '.aioson', 'runtime', stateFileName(source, target));
31
+ }
32
+
33
+ async function readJsonIfExists(filePath) {
34
+ try {
35
+ return JSON.parse(await fs.readFile(filePath, 'utf8'));
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ async function writeJson(filePath, payload) {
42
+ await ensureDir(path.dirname(filePath));
43
+ await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
44
+ }
45
+
46
+ function parseMax(value, fallback = DEFAULT_MAX_CYCLES) {
47
+ const parsed = Number.parseInt(String(value || ''), 10);
48
+ if (!Number.isInteger(parsed) || parsed <= 0) return fallback;
49
+ return Math.max(1, Math.min(parsed, 10));
50
+ }
51
+
52
+ async function readAgenticPolicy(targetDir) {
53
+ const payload = await readJsonIfExists(path.join(targetDir, EXECUTION_STATE_RELATIVE_PATH));
54
+ return payload && payload.agentic_policy && payload.agentic_policy.enabled
55
+ ? payload.agentic_policy
56
+ : null;
57
+ }
58
+
59
+ async function resolveMaxCycles(targetDir, source, options = {}) {
60
+ const explicit = options['max-cycles'] || options.maxCycles;
61
+ if (explicit) return parseMax(explicit);
62
+
63
+ const policy = await readAgenticPolicy(targetDir);
64
+ const review = policy && policy.review_cycle ? policy.review_cycle : null;
65
+ if (!review) return DEFAULT_MAX_CYCLES;
66
+
67
+ if (source === 'qa') return parseMax(review.max_dev_qa_cycles, DEFAULT_MAX_CYCLES);
68
+ if (source === 'tester') return parseMax(review.max_tester_correction_cycles, DEFAULT_MAX_CYCLES);
69
+ if (source === 'pentester') return parseMax(review.max_pentester_correction_cycles, DEFAULT_MAX_CYCLES);
70
+ return DEFAULT_MAX_CYCLES;
71
+ }
72
+
73
+ function resolveSafeProjectPath(targetDir, filePath) {
74
+ if (!filePath) return null;
75
+ const absolute = path.isAbsolute(filePath) ? filePath : path.resolve(targetDir, filePath);
76
+ const relative = path.relative(targetDir, absolute);
77
+ if (relative.startsWith('..') || path.isAbsolute(relative)) return null;
78
+ return absolute;
79
+ }
80
+
81
+ async function updatePlanStatus(targetDir, planPath, status) {
82
+ const absolute = resolveSafeProjectPath(targetDir, planPath);
83
+ if (!absolute || !(await exists(absolute))) {
84
+ return { ok: false, reason: 'plan_not_found' };
85
+ }
86
+
87
+ const relativePath = path.relative(targetDir, absolute).replace(/\\/g, '/');
88
+ const extension = path.extname(absolute).toLowerCase();
89
+ if (extension && extension !== '.md' && extension !== '.markdown') {
90
+ return {
91
+ ok: true,
92
+ skipped: true,
93
+ reason: 'non_markdown_plan',
94
+ path: relativePath,
95
+ status
96
+ };
97
+ }
98
+
99
+ const raw = await fs.readFile(absolute, 'utf8');
100
+ let next;
101
+ if (/^---\r?\n/.test(raw)) {
102
+ const endMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
103
+ if (endMatch) {
104
+ const frontmatter = endMatch[1];
105
+ const rest = raw.slice(endMatch[0].length);
106
+ const lines = frontmatter.split(/\r?\n/);
107
+ let found = false;
108
+ const updated = lines.map((line) => {
109
+ if (/^status\s*:/i.test(line)) {
110
+ found = true;
111
+ return `status: ${status}`;
112
+ }
113
+ return line;
114
+ });
115
+ if (!found) updated.push(`status: ${status}`);
116
+ next = `---\n${updated.join('\n')}\n---${rest}`;
117
+ }
118
+ }
119
+
120
+ if (!next) {
121
+ next = `---\nstatus: ${status}\n---\n\n${raw}`;
122
+ }
123
+
124
+ await fs.writeFile(absolute, next, 'utf8');
125
+ return {
126
+ ok: true,
127
+ path: relativePath,
128
+ status
129
+ };
130
+ }
131
+
132
+ function buildNextTask({ source, planPath }) {
133
+ if (source === 'qa') return `apply mandatory corrections from ${planPath}`;
134
+ if (source === 'tester') return `apply test-engineering corrections from ${planPath}`;
135
+ if (source === 'pentester') return `fix security findings from ${planPath}`;
136
+ return `apply review corrections from ${planPath}`;
137
+ }
138
+
139
+ async function readStatus(targetDir, source, target, options = {}) {
140
+ const statePath = resolveStatePath(targetDir, source, target);
141
+ const state = await readJsonIfExists(statePath);
142
+ const maxCycles = await resolveMaxCycles(targetDir, source, options);
143
+ return {
144
+ ok: true,
145
+ source,
146
+ target,
147
+ path: path.relative(targetDir, statePath).replace(/\\/g, '/'),
148
+ exists: Boolean(state),
149
+ max_cycles: maxCycles,
150
+ remaining_cycles: Math.max(0, maxCycles - Number(state?.cycle || 0)),
151
+ state
152
+ };
153
+ }
154
+
155
+ async function runAdvance(targetDir, source, target, options = {}) {
156
+ const feature = options.feature ? String(options.feature).trim() : null;
157
+ const planPath = options.plan ? String(options.plan).trim() : null;
158
+ const criticalSecurity = Boolean(options['critical-security'] || options.criticalSecurity);
159
+ const maxCycles = await resolveMaxCycles(targetDir, source, options);
160
+ const statePath = resolveStatePath(targetDir, source, target);
161
+
162
+ if (!feature) return { ok: false, reason: 'missing_feature' };
163
+ if (!planPath) return { ok: false, reason: 'missing_plan' };
164
+
165
+ if (criticalSecurity) {
166
+ return {
167
+ ok: true,
168
+ action: 'human_gate',
169
+ reason: 'critical_security',
170
+ feature,
171
+ source,
172
+ target,
173
+ max_cycles: maxCycles,
174
+ next_agent: null,
175
+ plan: planPath
176
+ };
177
+ }
178
+
179
+ const existing = await readJsonIfExists(statePath);
180
+ const sameFeature = existing && existing.slug === feature;
181
+ const currentCycle = sameFeature ? Number(existing.cycle || 0) : 0;
182
+
183
+ if (currentCycle >= maxCycles) {
184
+ try { await fs.unlink(statePath); } catch { /* absent is fine */ }
185
+ return {
186
+ ok: true,
187
+ action: 'stop_cycle_limit',
188
+ reason: 'cycle_limit_reached',
189
+ feature,
190
+ source,
191
+ target,
192
+ cycle: currentCycle,
193
+ max_cycles: maxCycles,
194
+ next_agent: null,
195
+ plan: planPath
196
+ };
197
+ }
198
+
199
+ const now = new Date().toISOString();
200
+ const nextCycle = currentCycle + 1;
201
+ const state = {
202
+ slug: feature,
203
+ source,
204
+ target,
205
+ cycle: nextCycle,
206
+ max_cycles: maxCycles,
207
+ status: 'open',
208
+ started_at: sameFeature && existing.started_at ? existing.started_at : now,
209
+ updated_at: now,
210
+ last_plan: planPath,
211
+ last_summary: options.summary ? String(options.summary).trim() : null
212
+ };
213
+
214
+ await writeJson(statePath, state);
215
+
216
+ return {
217
+ ok: true,
218
+ action: `invoke_${target}`,
219
+ feature,
220
+ source,
221
+ target,
222
+ next_agent: target,
223
+ cycle: nextCycle,
224
+ max_cycles: maxCycles,
225
+ remaining_cycles: Math.max(0, maxCycles - nextCycle),
226
+ plan: planPath,
227
+ task: buildNextTask({ source, planPath }),
228
+ state_path: path.relative(targetDir, statePath).replace(/\\/g, '/'),
229
+ state
230
+ };
231
+ }
232
+
233
+ async function runResolve(targetDir, source, target, options = {}) {
234
+ const feature = options.feature ? String(options.feature).trim() : null;
235
+ const planPath = options.plan ? String(options.plan).trim() : null;
236
+ const statePath = resolveStatePath(targetDir, source, target);
237
+ const existing = await readJsonIfExists(statePath);
238
+
239
+ if (!feature) return { ok: false, reason: 'missing_feature' };
240
+ if (!existing || existing.slug !== feature) {
241
+ return { ok: true, action: 'no_active_cycle', feature, source, target, next_agent: 'qa' };
242
+ }
243
+
244
+ let planUpdate = null;
245
+ if (planPath) {
246
+ planUpdate = await updatePlanStatus(targetDir, planPath, 'resolved');
247
+ }
248
+
249
+ const now = new Date().toISOString();
250
+ const state = {
251
+ ...existing,
252
+ status: 'resolved',
253
+ resolved_at: now,
254
+ updated_at: now,
255
+ resolved_plan: planPath || existing.last_plan || null
256
+ };
257
+ await writeJson(statePath, state);
258
+
259
+ return {
260
+ ok: true,
261
+ action: 'invoke_qa',
262
+ feature,
263
+ source,
264
+ target,
265
+ next_agent: 'qa',
266
+ state_path: path.relative(targetDir, statePath).replace(/\\/g, '/'),
267
+ plan_update: planUpdate,
268
+ state
269
+ };
270
+ }
271
+
272
+ async function runReset(targetDir, source, target, options = {}) {
273
+ const statePath = resolveStatePath(targetDir, source, target);
274
+ const existed = await exists(statePath);
275
+ if (existed) {
276
+ await fs.unlink(statePath);
277
+ }
278
+ return {
279
+ ok: true,
280
+ action: 'reset',
281
+ source,
282
+ target,
283
+ feature: options.feature ? String(options.feature).trim() : null,
284
+ removed: existed,
285
+ path: path.relative(targetDir, statePath).replace(/\\/g, '/')
286
+ };
287
+ }
288
+
289
+ async function runReviewCycle({ args, options = {}, logger }) {
290
+ const targetDir = resolveTargetDir(args);
291
+ const action = normalizeAgent(options.sub || options.action || 'status', 'status');
292
+ const source = normalizeAgent(options.source || options.agent, 'qa');
293
+ const target = normalizeAgent(options.to || options.target, 'dev');
294
+ let result;
295
+
296
+ if (action === 'status') {
297
+ result = await readStatus(targetDir, source, target, options);
298
+ } else if (action === 'advance' || action === 'start') {
299
+ result = await runAdvance(targetDir, source, target, options);
300
+ } else if (action === 'resolve') {
301
+ result = await runResolve(targetDir, source, target, options);
302
+ } else if (action === 'reset') {
303
+ result = await runReset(targetDir, source, target, options);
304
+ } else {
305
+ result = { ok: false, reason: 'unknown_review_cycle_action', action };
306
+ }
307
+
308
+ if (options.json) return result;
309
+
310
+ if (!result.ok) {
311
+ logger.error(`review-cycle:${action} failed: ${result.reason || 'unknown'}`);
312
+ return result;
313
+ }
314
+
315
+ logger.log(`review-cycle:${action} — ${result.action || 'status'}`);
316
+ if (result.feature) logger.log(` feature: ${result.feature}`);
317
+ logger.log(` route: @${source} -> @${target}`);
318
+ if (result.cycle !== undefined) logger.log(` cycle: ${result.cycle}/${result.max_cycles}`);
319
+ if (result.next_agent) logger.log(` next: @${result.next_agent}`);
320
+ if (result.task) logger.log(` task: ${result.task}`);
321
+ return result;
322
+ }
323
+
324
+ module.exports = {
325
+ DEFAULT_MAX_CYCLES,
326
+ resolveStatePath,
327
+ runReviewCycle
328
+ };
@@ -1224,7 +1224,7 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1224
1224
  }
1225
1225
 
1226
1226
  // F2 (workflow-handoff-integrity v1.9.5) — best-effort auto-advance workflow pointer
1227
- await maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options, logger, t });
1227
+ const autoAdvance = await maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options, logger, t });
1228
1228
 
1229
1229
  if (isDocCreatingAgent(normalizedAgent)) {
1230
1230
  backupAiosonDocs(targetDir).catch(() => {});
@@ -1252,7 +1252,7 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1252
1252
  });
1253
1253
  } catch { /* ignore — never blocks agent_done */ }
1254
1254
 
1255
- return { ok: true, targetDir, dbPath, agent: normalizedAgent, mode: 'live_event', runKey: session.runKey };
1255
+ return { ok: true, targetDir, dbPath, agent: normalizedAgent, mode: 'live_event', runKey: session.runKey, auto_advance: autoAdvance };
1256
1256
  }
1257
1257
 
1258
1258
  // No active session — create a standalone task+run and immediately complete it.
@@ -1297,7 +1297,7 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1297
1297
  }
1298
1298
 
1299
1299
  // F2 (workflow-handoff-integrity v1.9.5) — best-effort auto-advance workflow pointer
1300
- await maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options, logger, t });
1300
+ const autoAdvance = await maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options, logger, t });
1301
1301
 
1302
1302
  if (isDocCreatingAgent(normalizedAgent)) {
1303
1303
  backupAiosonDocs(targetDir).catch(() => {});
@@ -1325,7 +1325,7 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1325
1325
  });
1326
1326
  } catch { /* ignore — never blocks agent_done */ }
1327
1327
 
1328
- return { ok: true, targetDir, dbPath, agent: normalizedAgent, mode: 'standalone', runKey, taskKey };
1328
+ return { ok: true, targetDir, dbPath, agent: normalizedAgent, mode: 'standalone', runKey, taskKey, auto_advance: autoAdvance };
1329
1329
  } finally {
1330
1330
  db.close();
1331
1331
  }
@@ -31,6 +31,7 @@ const { captureBaseline, computeChangedSet, captureDiffPatch } = require('../har
31
31
  const { checkScope, checkDiffLimits, buildRollbackFeedback } = require('../harness/scope-guard');
32
32
  const { estimateTokens, startRunBudget, recordAttemptTokens, checkBudget, buildBudgetSummary } = require('../harness/budget-guard');
33
33
  const { writeAttemptArtifacts } = require('../harness/attempt-artifacts');
34
+ const { previewArtifact } = require('../harness/preview-artifact');
34
35
  const { emitGuardEvent } = require('../harness/guard-events');
35
36
  const { detectGates, createGate, enterHumanGate, resolveGateState, pendingGates, loadGates } = require('../harness/human-gate');
36
37
  const { runCriteria, registerFailureSignatures, startRunSignatures } = require('../harness/criteria-runner');
@@ -293,9 +294,18 @@ async function runPostAttemptGuards({ targetDir, guards, cb, logger, attempt, ag
293
294
  } else if (failed.length > 0) {
294
295
  logger.log(` ✗ Criteria checks falharam: ${failed.map((c) => c.id).join(', ')}`);
295
296
  outcome.reason = 'criteria_check_failed';
297
+ // AC-13: feedback = preview + ponteiro para attempts/{n}/checks/{id}.log
298
+ // (já persistido integralmente por writeAttemptArtifacts acima — persist-first
299
+ // satisfeito pelo fluxo existente). Evita dump integral no contexto do agente.
296
300
  outcome.feedback = failed
297
- .map((c) => `Criterion ${c.id} failed (exit ${c.exitCode}${c.timedOut ? ', timeout' : ''}): ${(c.stderr || c.stdout || '').split('\n').find((l) => l.trim()) || 'no output'}`)
298
- .join('\n');
301
+ .map((c) => {
302
+ const safeId = String(c.id || 'check').replace(/[^A-Za-z0-9._-]/g, '_');
303
+ const logPath = path.join(planDir, 'attempts', String(attempt), 'checks', `${safeId}.log`);
304
+ const raw = `${c.stdout || ''}${c.stderr ? `\n${c.stderr}` : ''}`.trim() || 'no output';
305
+ const { preview } = previewArtifact(raw, { maxBytes: 1024, artifactPath: logPath, persist: false });
306
+ return `Criterion ${c.id} failed (exit ${c.exitCode}${c.timedOut ? ', timeout' : ''}):\n${preview}`;
307
+ })
308
+ .join('\n\n');
299
309
  outcome.issues = [{ message: outcome.feedback }];
300
310
  }
301
311
  }
@@ -28,6 +28,7 @@
28
28
  * sheldon → sheldon-enrichment-{slug}.md
29
29
  * design-doc → design-doc-{slug}.md (falls back to design-doc.md)
30
30
  * readiness → readiness-{slug}.md (falls back to readiness.md)
31
+ * ui-spec → ui-spec.md
31
32
  * dossier → features/{slug}/dossier.md
32
33
  * simple-plan → simple-plans/{slug}.md
33
34
  *
@@ -50,6 +51,7 @@ const CONTEXT_TYPE_MAP = {
50
51
  sheldon: { rel: (slug) => `sheldon-enrichment-${slug}.md` },
51
52
  'design-doc': { rel: (slug) => `design-doc-${slug}.md`, fallback: () => 'design-doc.md' },
52
53
  readiness: { rel: (slug) => `readiness-${slug}.md`, fallback: () => 'readiness.md' },
54
+ 'ui-spec': { rel: () => 'ui-spec.md' },
53
55
  dossier: { rel: (slug) => `features/${slug}/dossier.md` },
54
56
  'simple-plan': { rel: (slug) => `simple-plans/${slug}.md` }
55
57
  };
@@ -37,8 +37,9 @@ const {
37
37
  extractStatusWritePathItems
38
38
  } = require('../parallel-workspace');
39
39
 
40
- const BAR = '━'.repeat(45);
41
- const EXECUTION_STATE_RELATIVE_PATH = '.aioson/context/workflow-execute.json';
40
+ const BAR = '━'.repeat(45);
41
+ const EXECUTION_STATE_RELATIVE_PATH = '.aioson/context/workflow-execute.json';
42
+ const DEFAULT_AGENTIC_MAX_CYCLES = 3;
42
43
 
43
44
  const STEP_META = {
44
45
  setup: { description: 'Initialize project context', gate_before: null, gate_after: null },
@@ -96,6 +97,96 @@ function quoteCliArg(value) {
96
97
  return `'${String(value || '').replace(/'/g, "'\\''")}'`;
97
98
  }
98
99
 
100
+ function parsePositiveIntegerOption(value, fallback, min = 1, max = 10) {
101
+ const parsed = Number.parseInt(String(value || ''), 10);
102
+ if (!Number.isInteger(parsed)) return fallback;
103
+ if (parsed < min) return min;
104
+ if (parsed > max) return max;
105
+ return parsed;
106
+ }
107
+
108
+ function isAgenticRequested(options = {}) {
109
+ return Boolean(
110
+ options.agentic ||
111
+ options['agentic-run'] ||
112
+ options.autopilot === 'agentic' ||
113
+ options.autopilot === 'runtime'
114
+ );
115
+ }
116
+
117
+ function buildAgenticPolicy(options = {}, classification = 'SMALL') {
118
+ const enabled = isAgenticRequested(options);
119
+ if (!enabled) return null;
120
+
121
+ const maxDevQaCycles = parsePositiveIntegerOption(
122
+ options['max-dev-qa-cycles'] || options.maxDevQaCycles || options['max-cycles'],
123
+ DEFAULT_AGENTIC_MAX_CYCLES
124
+ );
125
+ const maxTesterCycles = parsePositiveIntegerOption(
126
+ options['max-tester-cycles'] || options.maxTesterCycles || options['max-specialist-cycles'],
127
+ DEFAULT_AGENTIC_MAX_CYCLES
128
+ );
129
+ const maxPentesterCycles = parsePositiveIntegerOption(
130
+ options['max-pentester-cycles'] || options.maxPentesterCycles || options['max-specialist-cycles'],
131
+ DEFAULT_AGENTIC_MAX_CYCLES
132
+ );
133
+
134
+ return {
135
+ enabled: true,
136
+ mode: 'runtime_policy',
137
+ source: 'workflow:execute',
138
+ stop_conditions: [
139
+ 'feature_status_done',
140
+ 'human_decision_required',
141
+ 'gate_blocked',
142
+ 'context_budget_exceeded',
143
+ 'cycle_limit_reached',
144
+ 'critical_security_human_gate',
145
+ 'feature_close_human_gate'
146
+ ],
147
+ review_cycle: {
148
+ hub: 'qa',
149
+ max_dev_qa_cycles: maxDevQaCycles,
150
+ max_tester_correction_cycles: maxTesterCycles,
151
+ max_pentester_correction_cycles: maxPentesterCycles,
152
+ qa_fail_route: 'dev',
153
+ tester_route: 'tester after qa coverage trigger',
154
+ pentester_route: 'pentester after sensitive-surface trigger or MEDIUM sequence stage',
155
+ validator_route: 'validator when harness contract is present',
156
+ feature_close: 'human_gate'
157
+ },
158
+ lanes: {
159
+ enabled: classification === 'MEDIUM',
160
+ strategy: 'parallelize_only_independent_write_scopes',
161
+ guard_command: 'aioson parallel:guard . --lane=<n>',
162
+ conflict_action: 'block_lane'
163
+ },
164
+ sidecars: {
165
+ scouts: {
166
+ enabled: true,
167
+ read_only: true,
168
+ max_per_session: 3,
169
+ max_files_in_scope: 20,
170
+ allowed_parent_agents: ['deyvin', 'dev', 'product', 'briefing', 'orache']
171
+ },
172
+ research: {
173
+ enabled: true,
174
+ cache_dir: 'researchs/',
175
+ cache_ttl_days: 7
176
+ }
177
+ }
178
+ };
179
+ }
180
+
181
+ function formatAgenticPolicyLines(policy) {
182
+ if (!policy || !policy.enabled) return [];
183
+ return [
184
+ `Agentic policy: enabled (dev<->qa max ${policy.review_cycle.max_dev_qa_cycles} cycles)`,
185
+ `Review loop: qa fail -> dev; tester max ${policy.review_cycle.max_tester_correction_cycles}; pentester max ${policy.review_cycle.max_pentester_correction_cycles}; close=${policy.review_cycle.feature_close}`,
186
+ `Parallel lanes: ${policy.lanes.enabled ? 'enabled for independent write scopes' : 'disabled for this classification'}`
187
+ ];
188
+ }
189
+
99
190
  function findNextFromSequence(sequence, completed, skipped = []) {
100
191
  const done = new Set([...(completed || []), ...(skipped || [])].map(normalizeAgentName));
101
192
  return sequence.find((stage) => !done.has(normalizeAgentName(stage))) || null;
@@ -474,9 +565,10 @@ async function writeExecutionCheckpoint(targetDir, payload) {
474
565
  checkpoint: payload.checkpoint || null,
475
566
  status_snapshot: payload.statusSnapshot || null,
476
567
  suggestion: payload.suggestion || null,
477
- resume_command: payload.resumeCommand || null,
478
- history
479
- };
568
+ resume_command: payload.resumeCommand || null,
569
+ agentic_policy: payload.agenticPolicy || null,
570
+ history
571
+ };
480
572
  await writeJson(execPath, nextPayload);
481
573
  return nextPayload;
482
574
  }
@@ -630,6 +722,7 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
630
722
  if (!classification) classification = await detectClassification(targetDir, slug);
631
723
  if (!classification) classification = 'SMALL';
632
724
  classification = normalizeClassification(classification, 'SMALL');
725
+ const agenticPolicy = buildAgenticPolicy(options, classification);
633
726
 
634
727
  const autonomyProtocol = await readAutonomyProtocol(targetDir);
635
728
  const toolPolicy = getToolPolicy(autonomyProtocol, tool);
@@ -698,7 +791,17 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
698
791
  `--feature=${quoteCliArg(slug)}`,
699
792
  `--tool=${quoteCliArg(tool)}`,
700
793
  ...(requestedMode ? [`--mode=${quoteCliArg(requestedMode)}`] : []),
701
- ...(maxCheckpoints !== 1 ? [`--max-checkpoints=${quoteCliArg(maxCheckpoints)}`] : [])
794
+ ...(maxCheckpoints !== 1 ? [`--max-checkpoints=${quoteCliArg(maxCheckpoints)}`] : []),
795
+ ...(agenticPolicy ? ['--agentic'] : []),
796
+ ...(agenticPolicy && agenticPolicy.review_cycle.max_dev_qa_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
797
+ ? [`--max-dev-qa-cycles=${quoteCliArg(agenticPolicy.review_cycle.max_dev_qa_cycles)}`]
798
+ : []),
799
+ ...(agenticPolicy && agenticPolicy.review_cycle.max_tester_correction_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
800
+ ? [`--max-tester-cycles=${quoteCliArg(agenticPolicy.review_cycle.max_tester_correction_cycles)}`]
801
+ : []),
802
+ ...(agenticPolicy && agenticPolicy.review_cycle.max_pentester_correction_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
803
+ ? [`--max-pentester-cycles=${quoteCliArg(agenticPolicy.review_cycle.max_pentester_correction_cycles)}`]
804
+ : [])
702
805
  ].join(' ');
703
806
 
704
807
  if (dryRun) {
@@ -718,10 +821,11 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
718
821
  blocked_steps: blockedSteps.length,
719
822
  gates: planData.gates,
720
823
  status_snapshot: statusSnapshot,
721
- suggestion: statusSnapshot && statusSnapshot.suggestion ? statusSnapshot.suggestion : null,
722
- resume_command: resumeCommand,
723
- parallel_guard: parallelGuard
724
- };
824
+ suggestion: statusSnapshot && statusSnapshot.suggestion ? statusSnapshot.suggestion : null,
825
+ resume_command: resumeCommand,
826
+ agentic_policy: agenticPolicy,
827
+ parallel_guard: parallelGuard
828
+ };
725
829
 
726
830
  if (options.json) return result;
727
831
 
@@ -740,11 +844,14 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
740
844
  logger.log('');
741
845
  logger.log(`Blocked steps: ${blockedSteps.length} | Remaining: ${activePlan.length}`);
742
846
  logger.log(`Resume state: ${seeded.resumed ? 'existing workflow state reused' : 'new workflow state seeded'}`);
743
- if (statusSnapshot && statusSnapshot.suggestion && statusSnapshot.suggestion.command) {
744
- logger.log(`Suggested command: ${statusSnapshot.suggestion.command}`);
745
- }
746
- logger.log('');
747
- return result;
847
+ if (statusSnapshot && statusSnapshot.suggestion && statusSnapshot.suggestion.command) {
848
+ logger.log(`Suggested command: ${statusSnapshot.suggestion.command}`);
849
+ }
850
+ for (const line of formatAgenticPolicyLines(agenticPolicy)) {
851
+ logger.log(line);
852
+ }
853
+ logger.log('');
854
+ return result;
748
855
  }
749
856
 
750
857
  const executionTransitions = [];
@@ -824,10 +931,11 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
824
931
  resumed: seeded.resumed,
825
932
  status: activation && activation.agent ? 'active' : 'completed',
826
933
  checkpoint: buildCheckpointPayload(activation, handoff, handoffProtocol),
827
- statusSnapshot: refreshedStatus,
828
- suggestion: refreshedStatus && refreshedStatus.suggestion ? refreshedStatus.suggestion : null,
829
- resumeCommand
830
- });
934
+ statusSnapshot: refreshedStatus,
935
+ suggestion: refreshedStatus && refreshedStatus.suggestion ? refreshedStatus.suggestion : null,
936
+ resumeCommand,
937
+ agenticPolicy
938
+ });
831
939
 
832
940
  const result = {
833
941
  ok: true,
@@ -842,9 +950,10 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
842
950
  checkpoint: executionState.checkpoint,
843
951
  execution_state: executionState,
844
952
  status_snapshot: refreshedStatus,
845
- suggestion: refreshedStatus && refreshedStatus.suggestion ? refreshedStatus.suggestion : null,
846
- resume_command: resumeCommand,
847
- transitions: executionTransitions,
953
+ suggestion: refreshedStatus && refreshedStatus.suggestion ? refreshedStatus.suggestion : null,
954
+ resume_command: resumeCommand,
955
+ agentic_policy: agenticPolicy,
956
+ transitions: executionTransitions,
848
957
  active_stage: activation && activation.agent ? activation.agent : null,
849
958
  next_stage: activation && activation.next ? activation.next : null,
850
959
  handoff,
@@ -862,9 +971,10 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
862
971
  return result;
863
972
  }
864
973
 
865
- module.exports = {
866
- EXECUTION_STATE_RELATIVE_PATH,
867
- buildExecutionPlan,
868
- seedFeatureWorkflowState,
869
- runWorkflowExecute
870
- };
974
+ module.exports = {
975
+ EXECUTION_STATE_RELATIVE_PATH,
976
+ buildAgenticPolicy,
977
+ buildExecutionPlan,
978
+ seedFeatureWorkflowState,
979
+ runWorkflowExecute
980
+ };
@@ -40,8 +40,16 @@ const DEFAULT_FEATURE_WORKFLOW_BY_CLASSIFICATION = {
40
40
  };
41
41
 
42
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']);
43
+ // Two segments — see .aioson/docs/autopilot-handoff.md:
44
+ // 1. analyst -> dev: deterministic pre-dev chain. Prompt-only clients stop
45
+ // before the first @dev entry; workflow:execute --agentic may resume it
46
+ // through a fresh checkpointed activation.
47
+ // 2. post-dev review cycle: @dev → @qa → @tester/@pentester (when their @qa triggers
48
+ // fire) → @validator → STOPS before feature:close (human approves the close).
49
+ const AUTOPILOT_HANDOFF_STAGES = new Set([
50
+ 'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'pm',
51
+ 'dev', 'qa', 'tester', 'pentester', 'validator'
52
+ ]);
45
53
 
46
54
  function normalizeAgentName(input) {
47
55
  return String(input || '')
@@ -1671,6 +1679,7 @@ async function runWorkflowNext({ args, options, logger, t }) {
1671
1679
  }
1672
1680
 
1673
1681
  module.exports = {
1682
+ AUTOPILOT_HANDOFF_STAGES,
1674
1683
  STATE_RELATIVE_PATH,
1675
1684
  CONFIG_RELATIVE_PATH,
1676
1685
  EVENTS_RELATIVE_PATH,