@jaimevalasek/aioson 1.36.0 → 1.37.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 +28 -0
- package/docs/en/1-understand/ecosystem-map.md +1 -1
- package/docs/en/1-understand/glossary.md +1 -1
- package/docs/en/2-start/first-project.md +1 -1
- package/docs/en/2-start/initial-decisions.md +4 -2
- package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +8 -1
- package/docs/en/3-recipes/full-feature-with-sheldon.md +3 -1
- package/docs/en/4-agents/README.md +6 -3
- package/docs/en/4-agents/briefing-refiner.md +146 -0
- package/docs/en/5-reference/README.md +1 -0
- package/docs/en/5-reference/autopilot-handoff.md +286 -0
- package/docs/en/5-reference/cli-reference.md +6 -0
- package/docs/pt/1-entender/glossario.md +1 -1
- package/docs/pt/1-entender/mapa-do-ecossistema.md +1 -1
- package/docs/pt/2-comecar/decisoes-iniciais.md +4 -2
- package/docs/pt/2-comecar/primeiro-projeto.md +1 -1
- package/docs/pt/3-receitas/da-ideia-ao-prd-via-briefing.md +8 -1
- package/docs/pt/3-receitas/feature-completa-com-sheldon.md +3 -1
- package/docs/pt/4-agentes/README.md +13 -11
- package/docs/pt/4-agentes/briefing-refiner.md +64 -40
- package/docs/pt/4-agentes/briefing.md +6 -1
- package/docs/pt/4-agentes/dev.md +19 -1
- package/docs/pt/4-agentes/deyvin.md +4 -0
- package/docs/pt/4-agentes/discover.md +4 -0
- package/docs/pt/4-agentes/neo.md +4 -0
- package/docs/pt/4-agentes/orache.md +6 -0
- package/docs/pt/4-agentes/orchestrator.md +12 -0
- package/docs/pt/4-agentes/pentester.md +6 -0
- package/docs/pt/4-agentes/product.md +19 -1
- package/docs/pt/4-agentes/qa.md +10 -2
- package/docs/pt/4-agentes/setup.md +3 -1
- package/docs/pt/4-agentes/sheldon.md +12 -0
- package/docs/pt/4-agentes/tester.md +6 -0
- package/docs/pt/4-agentes/ux-ui.md +2 -1
- package/docs/pt/5-referencia/README.md +1 -1
- package/docs/pt/5-referencia/agent-chain-continuity.md +1 -1
- package/docs/pt/5-referencia/autopilot-handoff.md +191 -74
- package/docs/pt/5-referencia/comandos-cli.md +16 -7
- package/docs/pt/5-referencia/skills.md +2 -0
- package/docs/pt/agentes.md +3 -1
- package/package.json +1 -1
- package/src/agents.js +1 -1
- package/src/artifact-kinds.js +2 -1
- package/src/autopilot-signal.js +71 -0
- package/src/cli.js +9 -1
- package/src/commands/agents.js +18 -2
- package/src/commands/briefing.js +337 -1
- package/src/commands/feature-close.js +136 -43
- package/src/commands/live.js +47 -11
- package/src/commands/update.js +5 -1
- package/src/commands/verification-plan.js +28 -1
- package/src/commands/verify-artifact.js +64 -1
- package/src/commands/workflow-execute.js +149 -31
- package/src/commands/workflow-next.js +60 -16
- package/src/doctor.js +4 -2
- package/src/i18n/messages/en.js +2 -1
- package/src/i18n/messages/es.js +2 -1
- package/src/i18n/messages/fr.js +2 -1
- package/src/i18n/messages/pt-BR.js +2 -1
- package/src/lib/briefing-refiner/apply-feedback.js +18 -4
- package/src/lib/briefing-refiner/feedback-schema.js +73 -4
- package/src/lib/briefing-refiner/refinement-report.js +11 -0
- package/src/lib/briefing-refiner/review-html.js +388 -68
- package/src/parser.js +6 -0
- package/template/.aioson/agents/briefing-refiner.md +87 -47
- package/template/.aioson/agents/briefing.md +4 -0
- package/template/.aioson/agents/dev.md +9 -2
- package/template/.aioson/agents/deyvin.md +4 -0
- package/template/.aioson/agents/discover.md +4 -0
- package/template/.aioson/agents/neo.md +4 -0
- package/template/.aioson/agents/orache.md +4 -0
- package/template/.aioson/agents/orchestrator.md +16 -0
- package/template/.aioson/agents/pentester.md +4 -0
- package/template/.aioson/agents/product.md +25 -1
- package/template/.aioson/agents/qa.md +5 -1
- package/template/.aioson/agents/sheldon.md +9 -1
- package/template/.aioson/agents/tester.md +4 -0
- package/template/.aioson/agents/ux-ui.md +1 -1
- package/template/.aioson/docs/agent-help.md +126 -0
- package/template/.aioson/docs/autopilot-handoff.md +26 -16
- package/template/.aioson/docs/dev/phase-loop.md +8 -5
- package/template/.aioson/docs/play/llm-data-and-bindings.md +70 -7
- package/template/.aioson/skills/design/interface-design/SKILL.md +17 -0
- package/template/AGENTS.md +36 -36
- package/template/CLAUDE.md +1 -1
package/src/commands/live.js
CHANGED
|
@@ -235,14 +235,30 @@ async function resolveExecutablePath(command) {
|
|
|
235
235
|
.map((entry) => entry.trim())
|
|
236
236
|
.filter(Boolean);
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
// No Windows NUNCA testamos o arquivo sem extensão: o npm instala um shim
|
|
239
|
+
// extensionless (script sh) ao lado do .cmd, e ele não é executável via
|
|
240
|
+
// CreateProcess — spawná-lo dá ENOENT mesmo com o arquivo existindo.
|
|
241
|
+
// filter(Boolean) descarta segmentos vazios de um PATHEXT malformado
|
|
242
|
+
// (";.COM;..."), que viravam candidato sem extensão.
|
|
243
|
+
let extensions;
|
|
244
|
+
if (process.platform === 'win32') {
|
|
245
|
+
extensions = Array.from(new Set(
|
|
246
|
+
String(process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
247
|
+
.split(';')
|
|
248
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
249
|
+
.filter(Boolean)
|
|
250
|
+
));
|
|
251
|
+
if (extensions.length === 0) {
|
|
252
|
+
extensions = ['.exe', '.cmd', '.bat', '.com'];
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
extensions = [''];
|
|
256
|
+
}
|
|
241
257
|
|
|
242
258
|
for (const dir of pathEntries) {
|
|
243
259
|
for (const ext of extensions) {
|
|
244
|
-
const candidate = process.platform === 'win32'
|
|
245
|
-
? path.join(dir, `${binary}${ext}`)
|
|
260
|
+
const candidate = process.platform === 'win32'
|
|
261
|
+
? (binary.toLowerCase().endsWith(ext) ? path.join(dir, binary) : path.join(dir, `${binary}${ext}`))
|
|
246
262
|
: path.join(dir, binary);
|
|
247
263
|
if (await exists(candidate)) {
|
|
248
264
|
return candidate;
|
|
@@ -673,10 +689,30 @@ function createLiveEventRecord(context, options = {}) {
|
|
|
673
689
|
};
|
|
674
690
|
}
|
|
675
691
|
|
|
692
|
+
// Latch de spawn: entre o spawn() e o await de waitForChild existem awaits
|
|
693
|
+
// (writeLiveState etc.). Se o spawn falhar nessa janela, o 'error' emitia sem
|
|
694
|
+
// listener e DERRUBAVA o node ("Unhandled 'error' event"). Guardamos o
|
|
695
|
+
// desfecho no próprio child pra waitForChild resolver/rejeitar mesmo quando o
|
|
696
|
+
// evento já passou.
|
|
697
|
+
function trackChild(child) {
|
|
698
|
+
child.aiosonSpawnError = null;
|
|
699
|
+
child.aiosonExit = null;
|
|
700
|
+
child.on('error', (err) => {
|
|
701
|
+
if (!child.aiosonSpawnError) child.aiosonSpawnError = err;
|
|
702
|
+
});
|
|
703
|
+
child.on('close', (code, signal) => {
|
|
704
|
+
child.aiosonExit = { code: Number(code || 0), signal: signal || null };
|
|
705
|
+
});
|
|
706
|
+
return child;
|
|
707
|
+
}
|
|
708
|
+
|
|
676
709
|
function waitForChild(child) {
|
|
677
710
|
return new Promise((resolve, reject) => {
|
|
711
|
+
if (child.aiosonSpawnError) return reject(child.aiosonSpawnError);
|
|
712
|
+
if (child.aiosonExit) return resolve(child.aiosonExit);
|
|
678
713
|
child.once('error', reject);
|
|
679
714
|
child.once('close', (code, signal) => {
|
|
715
|
+
if (child.aiosonSpawnError) return reject(child.aiosonSpawnError);
|
|
680
716
|
resolve({ code: Number(code || 0), signal: signal || null });
|
|
681
717
|
});
|
|
682
718
|
});
|
|
@@ -1319,12 +1355,12 @@ async function runLiveStart({ args, options = {}, logger, t }) {
|
|
|
1319
1355
|
let attachResult = null;
|
|
1320
1356
|
|
|
1321
1357
|
if (attach && !noLaunch) {
|
|
1322
|
-
attachChild = spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
|
|
1358
|
+
attachChild = trackChild(spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
|
|
1323
1359
|
cwd: targetDir,
|
|
1324
1360
|
env: process.env,
|
|
1325
1361
|
stdio: 'inherit',
|
|
1326
1362
|
shell: process.platform === 'win32'
|
|
1327
|
-
});
|
|
1363
|
+
}));
|
|
1328
1364
|
state.child_pid = attachChild.pid || null;
|
|
1329
1365
|
if (existing.task?.task_key) {
|
|
1330
1366
|
const taskMeta = parseTaskMeta(existing.task);
|
|
@@ -1428,12 +1464,12 @@ async function runLiveStart({ args, options = {}, logger, t }) {
|
|
|
1428
1464
|
});
|
|
1429
1465
|
} else {
|
|
1430
1466
|
// Fallback to normal spawn if tmux not available
|
|
1431
|
-
child = spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
|
|
1467
|
+
child = trackChild(spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
|
|
1432
1468
|
cwd: targetDir,
|
|
1433
1469
|
env: process.env,
|
|
1434
1470
|
stdio: 'inherit',
|
|
1435
1471
|
shell: process.platform === 'win32'
|
|
1436
|
-
});
|
|
1472
|
+
}));
|
|
1437
1473
|
taskMeta.child_pid = child.pid || null;
|
|
1438
1474
|
updateTask(db, {
|
|
1439
1475
|
taskKey,
|
|
@@ -1441,12 +1477,12 @@ async function runLiveStart({ args, options = {}, logger, t }) {
|
|
|
1441
1477
|
});
|
|
1442
1478
|
}
|
|
1443
1479
|
} else {
|
|
1444
|
-
child = spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
|
|
1480
|
+
child = trackChild(spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
|
|
1445
1481
|
cwd: targetDir,
|
|
1446
1482
|
env: process.env,
|
|
1447
1483
|
stdio: 'inherit',
|
|
1448
1484
|
shell: process.platform === 'win32'
|
|
1449
|
-
});
|
|
1485
|
+
}));
|
|
1450
1486
|
taskMeta.child_pid = child.pid || null;
|
|
1451
1487
|
updateTask(db, {
|
|
1452
1488
|
taskKey,
|
package/src/commands/update.js
CHANGED
|
@@ -5,6 +5,7 @@ const { detectFramework } = require('../detector');
|
|
|
5
5
|
const { updateInstallation } = require('../updater');
|
|
6
6
|
const { validateProjectContextFile, getInteractionLanguage } = require('../context');
|
|
7
7
|
const { applyAgentLocale } = require('../locales');
|
|
8
|
+
const { getCliVersionLabelSync } = require('../version');
|
|
8
9
|
|
|
9
10
|
async function runUpdate({ args, options, logger, t }) {
|
|
10
11
|
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
@@ -36,10 +37,13 @@ async function runUpdate({ args, options, logger, t }) {
|
|
|
36
37
|
(result.savedProfile && result.savedProfile.locale
|
|
37
38
|
? result.savedProfile.locale
|
|
38
39
|
: 'en');
|
|
39
|
-
localeSync = await applyAgentLocale(targetDir, language, { dryRun, selectiveUpdate: selective && !all });
|
|
40
|
+
localeSync = await applyAgentLocale(targetDir, language, { dryRun, selectiveUpdate: selective && !all });
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
logger.log(t('update.done_at', { targetDir }));
|
|
44
|
+
// Surface WHICH template landed — stale-template updates used to be silent
|
|
45
|
+
// (the copy comes from the installed CLI's own bundle, not from npm latest).
|
|
46
|
+
logger.log(t('update.template_version', { version: getCliVersionLabelSync() }));
|
|
43
47
|
logger.log(t('update.files_updated', { count: result.copied.length }));
|
|
44
48
|
logger.log(t('update.backups_created', { count: result.backedUp.length }));
|
|
45
49
|
if (result.migrations && result.migrations.profileRename && result.migrations.profileRename.changed) {
|
|
@@ -24,6 +24,22 @@ const {
|
|
|
24
24
|
const BAR = '─'.repeat(60);
|
|
25
25
|
const CLASSIFICATIONS = ['MICRO', 'SMALL', 'MEDIUM'];
|
|
26
26
|
|
|
27
|
+
// A loud, deterministic instruction the phase loop echoes back to @dev at every
|
|
28
|
+
// phase boundary. On a per-phase trigger with auto_continue it is the fresh
|
|
29
|
+
// tool-result that stops the agent from ending its turn to "report" after a
|
|
30
|
+
// phase — the exact failure the phase loop exists to prevent. It never tells the
|
|
31
|
+
// agent to /compact: context is shed transparently by the host's auto-compact,
|
|
32
|
+
// without ending the turn.
|
|
33
|
+
function buildContinuationDirective(trigger, phaseLoop) {
|
|
34
|
+
if (trigger === 'end-of-feature') {
|
|
35
|
+
return 'END-OF-FEATURE — last phase: run the full runtime smoke (build + migrate + boot + Core happy-path), then hand off through the post-dev review cycle. Do not close/publish — that is the human gate.';
|
|
36
|
+
}
|
|
37
|
+
if (phaseLoop && phaseLoop.auto_continue) {
|
|
38
|
+
return 'CONTINUE-NOW — a clean report is a checkpoint, not a stop. Go DIRECTLY into the next phase in this same turn: do not stop, do not ask "continue?", do not summarize-and-end, and never self-issue /compact (the host auto-compacts transparently). Halt only on a failing gate/verification after retries or a genuine hard stop.';
|
|
39
|
+
}
|
|
40
|
+
return 'PAUSE — auto_continue is off: after this phase report, stop and wait for confirmation before the next phase.';
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
async function readFileSafe(filePath) {
|
|
28
44
|
try {
|
|
29
45
|
return await fs.readFile(filePath, 'utf8');
|
|
@@ -52,7 +68,16 @@ async function runVerificationPlan({ args, options = {}, logger }) {
|
|
|
52
68
|
const slug = (options.feature || options.slug || '').toString().trim() || null;
|
|
53
69
|
|
|
54
70
|
const triggerRaw = (options.trigger || 'per-phase').toString().trim().toLowerCase();
|
|
55
|
-
|
|
71
|
+
// An unknown trigger must fail loudly. Silently coercing it to per-phase used
|
|
72
|
+
// to emit an imperative CONTINUE-NOW directive on what was actually a typo'd
|
|
73
|
+
// end-of-feature call — steering @dev into a phase that does not exist.
|
|
74
|
+
if (!TRIGGERS.includes(triggerRaw)) {
|
|
75
|
+
const failure = { ok: false, reason: 'invalid_trigger', trigger: triggerRaw, valid_triggers: [...TRIGGERS] };
|
|
76
|
+
if (options.json) return failure;
|
|
77
|
+
logger.error(`Invalid --trigger=${triggerRaw}. Valid triggers: ${TRIGGERS.join(', ')}.`);
|
|
78
|
+
return failure;
|
|
79
|
+
}
|
|
80
|
+
const trigger = triggerRaw;
|
|
56
81
|
|
|
57
82
|
const config = await readVerificationConfig(targetDir);
|
|
58
83
|
const host = resolveHost(config, options.host);
|
|
@@ -92,6 +117,7 @@ async function runVerificationPlan({ args, options = {}, logger }) {
|
|
|
92
117
|
phase_loop: getPhaseLoop(config),
|
|
93
118
|
budget: getBudget(config)
|
|
94
119
|
};
|
|
120
|
+
result.continuation_directive = buildContinuationDirective(trigger, result.phase_loop);
|
|
95
121
|
|
|
96
122
|
if (options.json) return result;
|
|
97
123
|
|
|
@@ -108,6 +134,7 @@ async function runVerificationPlan({ args, options = {}, logger }) {
|
|
|
108
134
|
logger.log(BAR);
|
|
109
135
|
const pl = result.phase_loop;
|
|
110
136
|
logger.log(`Phase loop: auto_continue=${pl.auto_continue} compact_between_phases=${pl.compact_between_phases} max_fix_retries=${pl.max_fix_retries_per_phase}`);
|
|
137
|
+
logger.log(`▶ ${result.continuation_directive}`);
|
|
111
138
|
logger.log('');
|
|
112
139
|
|
|
113
140
|
return result;
|
|
@@ -179,7 +179,7 @@ const RULESETS = {
|
|
|
179
179
|
|
|
180
180
|
// Kinds whose target file path is keyed by --slug; without it we cannot resolve
|
|
181
181
|
// the artifact, so fail with a clear usage error instead of a `null/` path.
|
|
182
|
-
const REQUIRES_SLUG = new Set(['genome', 'research-report', 'enriched-profile', 'hybrid-skill', 'copy']);
|
|
182
|
+
const REQUIRES_SLUG = new Set(['genome', 'research-report', 'enriched-profile', 'hybrid-skill', 'copy', 'review']);
|
|
183
183
|
|
|
184
184
|
// Kinds whose artifact has a date-stamped / caller-known path — resolved via
|
|
185
185
|
// --file=<path> rather than derived from a slug.
|
|
@@ -374,6 +374,69 @@ const ADAPTERS = {
|
|
|
374
374
|
return { ok: issues.length === 0, issues, warnings, checks: [{ id: 'site', ok: issues.length === 0, detail: issues.join('; ') || null }] };
|
|
375
375
|
},
|
|
376
376
|
|
|
377
|
+
// briefing-refiner — the review surface must be the deterministic, fully
|
|
378
|
+
// self-contained artifact `aioson briefing:review` renders: export fallbacks
|
|
379
|
+
// present, no external resources, an embedded source_hash, and a feedback
|
|
380
|
+
// JSON that passes the canonical schema. Staleness (briefings.md changed
|
|
381
|
+
// after generation, e.g. right after an apply) is a warning, not a failure.
|
|
382
|
+
review: async (ctx) => {
|
|
383
|
+
if (!ctx.slug) {
|
|
384
|
+
return { ok: false, issues: ['kind=review requires --slug=<briefing-slug>'], warnings: [], checks: [] };
|
|
385
|
+
}
|
|
386
|
+
const { hashText } = require('../lib/briefing-refiner/briefing-sections');
|
|
387
|
+
const { validateFeedback } = require('../lib/briefing-refiner/feedback-schema');
|
|
388
|
+
const briefingDir = path.resolve(ctx.targetDir, '.aioson', 'briefings', ctx.slug);
|
|
389
|
+
const issues = [];
|
|
390
|
+
const warnings = [];
|
|
391
|
+
|
|
392
|
+
let html = null;
|
|
393
|
+
try {
|
|
394
|
+
html = fs.readFileSync(path.join(briefingDir, 'review.html'), 'utf8');
|
|
395
|
+
} catch {
|
|
396
|
+
issues.push(`review.html not found for briefing "${ctx.slug}" — run: aioson briefing:review . --slug=${ctx.slug}`);
|
|
397
|
+
}
|
|
398
|
+
if (html) {
|
|
399
|
+
if (!html.includes('aioson:review')) {
|
|
400
|
+
issues.push('review.html lacks the aioson:review marker (hand-rolled surface) — regenerate with `aioson briefing:review`');
|
|
401
|
+
}
|
|
402
|
+
if (!/id="download"/.test(html)) issues.push('review.html has no download fallback button (id="download")');
|
|
403
|
+
if (!/id="copy"/.test(html)) issues.push('review.html has no copy-JSON fallback button (id="copy")');
|
|
404
|
+
// User content is HTML-escaped by the generator (quotes become "),
|
|
405
|
+
// so a real external reference can only be generator-emitted markup —
|
|
406
|
+
// match those forms only, or briefing text quoting HTML/CSS would
|
|
407
|
+
// false-positive the gate.
|
|
408
|
+
if (/<script[^>]+\bsrc=|<link[^>]+href=["']https?:|\bsrc=["']https?:/i.test(html)) {
|
|
409
|
+
issues.push('review.html references external resources — it must be fully self-contained');
|
|
410
|
+
}
|
|
411
|
+
const hashMatch = html.match(/source_hash=([0-9a-f]{64})/);
|
|
412
|
+
if (!hashMatch) {
|
|
413
|
+
issues.push('review.html has no embedded source_hash');
|
|
414
|
+
} else {
|
|
415
|
+
try {
|
|
416
|
+
const currentHash = hashText(fs.readFileSync(path.join(briefingDir, 'briefings.md'), 'utf8'));
|
|
417
|
+
if (hashMatch[1] !== currentHash) {
|
|
418
|
+
warnings.push('review.html is stale: source_hash differs from the current briefings.md — regenerate the review for the next round');
|
|
419
|
+
}
|
|
420
|
+
} catch {
|
|
421
|
+
issues.push('briefings.md not found next to review.html');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const feedback = JSON.parse(fs.readFileSync(path.join(briefingDir, 'refinement-feedback.json'), 'utf8'));
|
|
428
|
+
const res = validateFeedback(feedback, { slug: ctx.slug, allowStale: true });
|
|
429
|
+
if (!res.ok) {
|
|
430
|
+
for (const err of res.errors) issues.push(`refinement-feedback.json: ${err}`);
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
// Absent right after an apply (it gets archived) — only a warning.
|
|
434
|
+
warnings.push('refinement-feedback.json missing or unreadable — expected right after review generation');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return { ok: issues.length === 0, issues, warnings, checks: [{ id: `review:${ctx.slug}`, ok: issues.length === 0, detail: issues.join('; ') || null }] };
|
|
438
|
+
},
|
|
439
|
+
|
|
377
440
|
// committer — advisory subject-quality audit. Reads --file if given, else the
|
|
378
441
|
// HEAD commit message (post-commit, so the agent can amend before push).
|
|
379
442
|
'commit-message': async (ctx) => {
|
|
@@ -27,6 +27,7 @@ const { readHandoff, readHandoffProtocol } = require('../session-handoff');
|
|
|
27
27
|
const {
|
|
28
28
|
STATE_RELATIVE_PATH,
|
|
29
29
|
buildDefaultWorkflowConfig,
|
|
30
|
+
detectWorkflowMode,
|
|
30
31
|
readWorkflowConfig,
|
|
31
32
|
runWorkflowNext
|
|
32
33
|
} = require('./workflow-next');
|
|
@@ -110,11 +111,26 @@ function isAgenticRequested(options = {}) {
|
|
|
110
111
|
options.agentic ||
|
|
111
112
|
options['agentic-run'] ||
|
|
112
113
|
options.autopilot === 'agentic' ||
|
|
113
|
-
options.autopilot === 'runtime'
|
|
114
|
+
options.autopilot === 'runtime' ||
|
|
115
|
+
// Seeding the scheme IS turning on autopilot — the whole point of --seed is to
|
|
116
|
+
// persist an enabled agentic_policy the interactive agents then follow.
|
|
117
|
+
options.seed ||
|
|
118
|
+
options['seed-only']
|
|
114
119
|
);
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
function buildAgenticPolicy(options = {}, classification = 'SMALL') {
|
|
123
|
+
// --seed --step: the per-feature "step by step" choice. Writes an explicitly
|
|
124
|
+
// DISARMED scheme — resolveAutopilotSignal and the agent triggers treat it as
|
|
125
|
+
// the feature-level override that wins over a project-wide auto_handoff: true.
|
|
126
|
+
if (options.step) {
|
|
127
|
+
return {
|
|
128
|
+
enabled: false,
|
|
129
|
+
mode: 'step_by_step',
|
|
130
|
+
source: 'workflow:execute --step'
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
118
134
|
const enabled = isAgenticRequested(options);
|
|
119
135
|
if (!enabled) return null;
|
|
120
136
|
|
|
@@ -229,6 +245,18 @@ function inferCompletedStagesFromArtifacts(sequence, artifacts, gates) {
|
|
|
229
245
|
inferred = Boolean(artifacts.requirements && artifacts.requirements.exists && gates.requirements === 'approved');
|
|
230
246
|
} else if (normalized === 'architect') {
|
|
231
247
|
inferred = Boolean(artifacts.architecture && artifacts.architecture.exists && gates.design === 'approved');
|
|
248
|
+
} else if (normalized === 'sheldon' || normalized === 'orchestrator') {
|
|
249
|
+
// Single spec authorities (lean @sheldon / maestro @orchestrator) collapse
|
|
250
|
+
// Gates A/B/C into one hop — done once the spec package exists with the
|
|
251
|
+
// collapsed gates approved. Without this, a seed run AFTER the spec stage
|
|
252
|
+
// finished reported `next: sheldon|orchestrator` and pointed the whole
|
|
253
|
+
// agentic scheme backwards.
|
|
254
|
+
inferred = Boolean(
|
|
255
|
+
artifacts.spec && artifacts.spec.exists &&
|
|
256
|
+
gates.requirements === 'approved' &&
|
|
257
|
+
gates.design === 'approved' &&
|
|
258
|
+
gates.plan === 'approved'
|
|
259
|
+
);
|
|
232
260
|
} else if (normalized === 'pm') {
|
|
233
261
|
inferred = Boolean(artifacts.implementation_plan && artifacts.implementation_plan.exists && gates.plan === 'approved');
|
|
234
262
|
} else if (normalized === 'qa') {
|
|
@@ -244,9 +272,17 @@ function inferCompletedStagesFromArtifacts(sequence, artifacts, gates) {
|
|
|
244
272
|
return completed;
|
|
245
273
|
}
|
|
246
274
|
|
|
247
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Reads the persisted workflow state for a seed/preview of `slug`, applying the
|
|
277
|
+
* SAME staleness guard as loadOrCreateState: a state whose feature is no longer
|
|
278
|
+
* the features.md-active feature is stale and gets discarded (the loader would
|
|
279
|
+
* throw it away one command later anyway — hard-failing the seed on it just
|
|
280
|
+
* silently disarmed autopilot). The refusal survives only for a genuinely
|
|
281
|
+
* active different feature.
|
|
282
|
+
*/
|
|
283
|
+
async function resolveExistingFeatureState(targetDir, slug) {
|
|
248
284
|
const statePath = path.join(targetDir, STATE_RELATIVE_PATH);
|
|
249
|
-
|
|
285
|
+
let existing = await readJsonIfExists(statePath);
|
|
250
286
|
const focusStage = getFocusStage(existing);
|
|
251
287
|
|
|
252
288
|
if (
|
|
@@ -256,14 +292,41 @@ async function seedFeatureWorkflowState(targetDir, slug, classification, startFr
|
|
|
256
292
|
existing.featureSlug !== slug &&
|
|
257
293
|
focusStage
|
|
258
294
|
) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
295
|
+
let modeInfo = null;
|
|
296
|
+
try {
|
|
297
|
+
modeInfo = await detectWorkflowMode(targetDir);
|
|
298
|
+
} catch {
|
|
299
|
+
modeInfo = null;
|
|
300
|
+
}
|
|
301
|
+
const existingIsActive = Boolean(
|
|
302
|
+
modeInfo && modeInfo.mode === 'feature' && modeInfo.featureSlug === existing.featureSlug
|
|
303
|
+
);
|
|
304
|
+
const requestedIsActive = Boolean(
|
|
305
|
+
modeInfo && modeInfo.mode === 'feature' && modeInfo.featureSlug === slug
|
|
306
|
+
);
|
|
307
|
+
if (!existingIsActive || requestedIsActive) {
|
|
308
|
+
existing = null; // stale pointer — reseed for the requested feature
|
|
309
|
+
} else {
|
|
310
|
+
return {
|
|
311
|
+
existing: null,
|
|
312
|
+
refusal: {
|
|
313
|
+
ok: false,
|
|
314
|
+
reason: 'different_active_feature',
|
|
315
|
+
active_feature: existing.featureSlug,
|
|
316
|
+
active_stage: focusStage
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
265
320
|
}
|
|
266
321
|
|
|
322
|
+
return { existing, refusal: null };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function seedFeatureWorkflowState(targetDir, slug, classification, startFrom) {
|
|
326
|
+
const statePath = path.join(targetDir, STATE_RELATIVE_PATH);
|
|
327
|
+
const { existing, refusal } = await resolveExistingFeatureState(targetDir, slug);
|
|
328
|
+
if (refusal) return refusal;
|
|
329
|
+
|
|
267
330
|
if (existing && existing.mode === 'feature' && existing.featureSlug === slug) {
|
|
268
331
|
return {
|
|
269
332
|
ok: true,
|
|
@@ -312,23 +375,8 @@ async function seedFeatureWorkflowState(targetDir, slug, classification, startFr
|
|
|
312
375
|
}
|
|
313
376
|
|
|
314
377
|
async function previewFeatureWorkflowState(targetDir, slug, classification, startFrom) {
|
|
315
|
-
const existing = await
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (
|
|
319
|
-
existing &&
|
|
320
|
-
existing.mode === 'feature' &&
|
|
321
|
-
existing.featureSlug &&
|
|
322
|
-
existing.featureSlug !== slug &&
|
|
323
|
-
focusStage
|
|
324
|
-
) {
|
|
325
|
-
return {
|
|
326
|
-
ok: false,
|
|
327
|
-
reason: 'different_active_feature',
|
|
328
|
-
active_feature: existing.featureSlug,
|
|
329
|
-
active_stage: focusStage
|
|
330
|
-
};
|
|
331
|
-
}
|
|
378
|
+
const { existing, refusal } = await resolveExistingFeatureState(targetDir, slug);
|
|
379
|
+
if (refusal) return refusal;
|
|
332
380
|
|
|
333
381
|
if (existing && existing.mode === 'feature' && existing.featureSlug === slug) {
|
|
334
382
|
return {
|
|
@@ -539,7 +587,11 @@ function buildCheckpointPayload(activation, handoff, handoffProtocol) {
|
|
|
539
587
|
async function writeExecutionCheckpoint(targetDir, payload) {
|
|
540
588
|
const execPath = path.join(targetDir, EXECUTION_STATE_RELATIVE_PATH);
|
|
541
589
|
const existing = await readJsonIfExists(execPath);
|
|
542
|
-
|
|
590
|
+
// History is per-feature: never carry a previous feature's checkpoints into
|
|
591
|
+
// a new feature's scheme.
|
|
592
|
+
const history = existing && existing.feature === payload.feature && Array.isArray(existing.history)
|
|
593
|
+
? [...existing.history]
|
|
594
|
+
: [];
|
|
543
595
|
if (payload.checkpoint) {
|
|
544
596
|
history.push({
|
|
545
597
|
at: new Date().toISOString(),
|
|
@@ -702,6 +754,9 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
|
|
|
702
754
|
const tool = options.tool ? String(options.tool).trim() : 'claude';
|
|
703
755
|
const requestedMode = options.mode ? String(options.mode).trim() : null;
|
|
704
756
|
const dryRun = Boolean(options['dry-run'] || options.dry);
|
|
757
|
+
// --step (disarm) is by definition record-only: it writes the disarmed scheme
|
|
758
|
+
// and never drives stage transitions, with or without an explicit --seed.
|
|
759
|
+
const seedOnly = Boolean(options.seed || options['seed-only'] || options.step);
|
|
705
760
|
const startFrom = options['start-from'] ? String(options['start-from']).trim() : null;
|
|
706
761
|
const skipOptional = Boolean(options['skip-optional']);
|
|
707
762
|
const parsedMaxCheckpoints = Number.parseInt(String(options['max-checkpoints'] || '1'), 10);
|
|
@@ -792,14 +847,18 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
|
|
|
792
847
|
`--tool=${quoteCliArg(tool)}`,
|
|
793
848
|
...(requestedMode ? [`--mode=${quoteCliArg(requestedMode)}`] : []),
|
|
794
849
|
...(maxCheckpoints !== 1 ? [`--max-checkpoints=${quoteCliArg(maxCheckpoints)}`] : []),
|
|
795
|
-
|
|
796
|
-
|
|
850
|
+
// A seed run must resume as a seed run — `--agentic` (the CLI-advancing
|
|
851
|
+
// runner) is the opposite of the seed-only contract. A disarm run keeps
|
|
852
|
+
// its --step so replaying it never re-arms the scheme.
|
|
853
|
+
...(seedOnly ? ['--seed'] : agenticPolicy && agenticPolicy.enabled ? ['--agentic'] : []),
|
|
854
|
+
...(options.step ? ['--step'] : []),
|
|
855
|
+
...(agenticPolicy && agenticPolicy.review_cycle && agenticPolicy.review_cycle.max_dev_qa_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
|
|
797
856
|
? [`--max-dev-qa-cycles=${quoteCliArg(agenticPolicy.review_cycle.max_dev_qa_cycles)}`]
|
|
798
857
|
: []),
|
|
799
|
-
...(agenticPolicy && agenticPolicy.review_cycle.max_tester_correction_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
|
|
858
|
+
...(agenticPolicy && agenticPolicy.review_cycle && agenticPolicy.review_cycle.max_tester_correction_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
|
|
800
859
|
? [`--max-tester-cycles=${quoteCliArg(agenticPolicy.review_cycle.max_tester_correction_cycles)}`]
|
|
801
860
|
: []),
|
|
802
|
-
...(agenticPolicy && agenticPolicy.review_cycle.max_pentester_correction_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
|
|
861
|
+
...(agenticPolicy && agenticPolicy.review_cycle && agenticPolicy.review_cycle.max_pentester_correction_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
|
|
803
862
|
? [`--max-pentester-cycles=${quoteCliArg(agenticPolicy.review_cycle.max_pentester_correction_cycles)}`]
|
|
804
863
|
: [])
|
|
805
864
|
].join(' ');
|
|
@@ -854,6 +913,65 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
|
|
|
854
913
|
return result;
|
|
855
914
|
}
|
|
856
915
|
|
|
916
|
+
// --seed: persist the workflow.state.json + workflow-execute.json (with an
|
|
917
|
+
// enabled agentic_policy) and STOP. Unlike a full run, it does not drive stage
|
|
918
|
+
// transitions from the CLI — in an interactive Claude Code session the agents
|
|
919
|
+
// themselves follow the scheme. This is what the spec->dev handoff calls so a
|
|
920
|
+
// feature built the normal way carries the autopilot contract without the user
|
|
921
|
+
// running anything. Idempotent: re-seeding the same slug reuses existing state.
|
|
922
|
+
if (seedOnly) {
|
|
923
|
+
const handoff = await readHandoff(targetDir);
|
|
924
|
+
const handoffProtocol = await readHandoffProtocol(targetDir);
|
|
925
|
+
const nextStage = seeded.state ? (seeded.state.current || seeded.state.next || null) : null;
|
|
926
|
+
const executionState = await writeExecutionCheckpoint(targetDir, {
|
|
927
|
+
feature: slug,
|
|
928
|
+
classification,
|
|
929
|
+
tool,
|
|
930
|
+
requestedMode,
|
|
931
|
+
resumed: seeded.resumed,
|
|
932
|
+
status: nextStage ? 'active' : 'completed',
|
|
933
|
+
checkpoint: buildCheckpointPayload(null, handoff, handoffProtocol),
|
|
934
|
+
statusSnapshot,
|
|
935
|
+
suggestion: statusSnapshot && statusSnapshot.suggestion ? statusSnapshot.suggestion : null,
|
|
936
|
+
resumeCommand,
|
|
937
|
+
agenticPolicy
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
const result = {
|
|
941
|
+
ok: true,
|
|
942
|
+
feature: slug,
|
|
943
|
+
classification,
|
|
944
|
+
tool,
|
|
945
|
+
requested_mode: requestedMode,
|
|
946
|
+
seeded: true,
|
|
947
|
+
resumed: seeded.resumed,
|
|
948
|
+
state_path: seeded.statePath,
|
|
949
|
+
execution_state_path: EXECUTION_STATE_RELATIVE_PATH,
|
|
950
|
+
next_stage: nextStage,
|
|
951
|
+
checkpoint: executionState.checkpoint,
|
|
952
|
+
execution_state: executionState,
|
|
953
|
+
status_snapshot: statusSnapshot,
|
|
954
|
+
suggestion: statusSnapshot && statusSnapshot.suggestion ? statusSnapshot.suggestion : null,
|
|
955
|
+
resume_command: resumeCommand,
|
|
956
|
+
agentic_policy: agenticPolicy,
|
|
957
|
+
parallel_guard: parallelGuard
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
if (options.json) return result;
|
|
961
|
+
|
|
962
|
+
logger.log('');
|
|
963
|
+
logger.log(`Agentic workflow scheme seeded → ${EXECUTION_STATE_RELATIVE_PATH}`);
|
|
964
|
+
logger.log(`Feature: ${slug} (${classification}) Next stage: @${nextStage || 'none'}`);
|
|
965
|
+
logger.log(
|
|
966
|
+
`Autopilot: ${agenticPolicy && agenticPolicy.enabled
|
|
967
|
+
? 'enabled — interactive agents run the chain to feature:close (human gate)'
|
|
968
|
+
: 'disabled'}`
|
|
969
|
+
);
|
|
970
|
+
for (const line of formatAgenticPolicyLines(agenticPolicy)) logger.log(line);
|
|
971
|
+
logger.log('');
|
|
972
|
+
return result;
|
|
973
|
+
}
|
|
974
|
+
|
|
857
975
|
const executionTransitions = [];
|
|
858
976
|
let activation = null;
|
|
859
977
|
let currentStatus = statusSnapshot;
|
|
@@ -16,6 +16,7 @@ const { logError, buildHealingPrompt } = require('../self-healing');
|
|
|
16
16
|
const { validateHandoffProtocol } = require('../handoff-validator');
|
|
17
17
|
const { readAutonomyProtocol, resolveEffectiveMode } = require('../autonomy-policy');
|
|
18
18
|
const { readAgentManifest, buildAgentCapabilitySummary } = require('../agent-manifests');
|
|
19
|
+
const { resolveAutopilotSignal } = require('../autopilot-signal');
|
|
19
20
|
const { runMemoryReflectPrepare } = require('./memory-reflect-prepare');
|
|
20
21
|
const { inspectStagedChanges } = require('../lib/git-commit-guard');
|
|
21
22
|
const { emitSecurityRuntimeEvent } = require('../lib/security/runtime-events');
|
|
@@ -61,19 +62,18 @@ const DEFAULT_FEATURE_WORKFLOW_BY_CLASSIFICATION = {
|
|
|
61
62
|
MEDIUM: ['product', 'orchestrator', 'dev', 'pentester', 'qa']
|
|
62
63
|
};
|
|
63
64
|
|
|
64
|
-
// Stages eligible for autopilot handoff
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// pre-dev — @sheldon is the only pre-dev agent and hands off by hand — and is
|
|
75
|
-
// active only on the post-dev cycle (dev → qa). This is by design, not an omission.
|
|
65
|
+
// Stages eligible for autopilot handoff — the FULL feature chain (see
|
|
66
|
+
// .aioson/docs/autopilot-handoff.md). Activation = auto_handoff: true in
|
|
67
|
+
// project.context.md OR the seeded scheme (resolveAutopilotSignal). Two segments:
|
|
68
|
+
// 1. spec → dev chain: @product seeds the agentic scheme and invokes the spec
|
|
69
|
+
// authority (@sheldon lean / @orchestrator maestro), which crosses into
|
|
70
|
+
// @dev via the dev-state.md cold-start packet once its own gates/decisions
|
|
71
|
+
// are settled. Detour agents (analyst/architect/pm/...) chain only when an
|
|
72
|
+
// opt-in detour adds them to the active sequence.
|
|
73
|
+
// 2. post-dev review cycle: @dev → @qa → @tester/@pentester (when their @qa
|
|
74
|
+
// triggers fire) → @validator → STOPS before feature:close (human gate).
|
|
76
75
|
const AUTOPILOT_HANDOFF_STAGES = new Set([
|
|
76
|
+
'product', 'sheldon', 'orchestrator',
|
|
77
77
|
'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'pm',
|
|
78
78
|
'dev', 'qa', 'tester', 'pentester', 'validator'
|
|
79
79
|
]);
|
|
@@ -99,6 +99,14 @@ function isMaestroOrchestratorState(state) {
|
|
|
99
99
|
return idx !== -1 && sequence[idx + 1] === 'dev';
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// Lean lane (SMALL, sheldon → dev): @sheldon is the single spec authority.
|
|
103
|
+
// Mirrors isLeanSheldonState in handoff-contract.js.
|
|
104
|
+
function isLeanSheldonState(state) {
|
|
105
|
+
const sequence = Array.isArray(state && state.sequence) ? state.sequence.map(normalizeAgentName) : [];
|
|
106
|
+
const idx = sequence.indexOf('sheldon');
|
|
107
|
+
return idx !== -1 && sequence[idx + 1] === 'dev';
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
function buildDefaultWorkflowConfig() {
|
|
103
111
|
return {
|
|
104
112
|
version: 1,
|
|
@@ -376,6 +384,29 @@ async function validateStageArtifacts(targetDir, state, stage) {
|
|
|
376
384
|
return true;
|
|
377
385
|
}
|
|
378
386
|
|
|
387
|
+
if (stage === 'sheldon') {
|
|
388
|
+
// Lean lane (SMALL, sheldon → dev): @sheldon is the single spec authority —
|
|
389
|
+
// "done" once the collapsed spec package exists (mirrors the orchestrator
|
|
390
|
+
// maestro branch below; the handoff contract re-checks Gates A/B/C). Without
|
|
391
|
+
// this branch nothing ever marks the sheldon stage resolved, and a later
|
|
392
|
+
// `--complete=dev` computes `next: sheldon` — the state machine walking
|
|
393
|
+
// BACKWARDS into the spec agent after implementation.
|
|
394
|
+
if (state.mode === 'feature' && slug && isLeanSheldonState(state)) {
|
|
395
|
+
const designDoc = [path.join(base, `design-doc-${slug}.md`), path.join(base, 'design-doc.md')];
|
|
396
|
+
const readiness = [path.join(base, `readiness-${slug}.md`), path.join(base, 'readiness.md')];
|
|
397
|
+
return (await exists(path.join(base, `sheldon-enrichment-${slug}.md`)))
|
|
398
|
+
&& (await exists(path.join(base, `requirements-${slug}.md`)))
|
|
399
|
+
&& (await exists(path.join(base, `spec-${slug}.md`)))
|
|
400
|
+
&& (await exists(path.join(base, `implementation-plan-${slug}.md`)))
|
|
401
|
+
&& (await anyExists(designDoc))
|
|
402
|
+
&& (await anyExists(readiness));
|
|
403
|
+
}
|
|
404
|
+
if (state.mode === 'feature' && slug) {
|
|
405
|
+
return await exists(path.join(base, `sheldon-enrichment-${slug}.md`));
|
|
406
|
+
}
|
|
407
|
+
return await exists(path.join(base, 'sheldon-enrichment.md'));
|
|
408
|
+
}
|
|
409
|
+
|
|
379
410
|
if (stage === 'orchestrator') {
|
|
380
411
|
// Maestro lane (MEDIUM, orchestrator → dev): the orchestrator is the single
|
|
381
412
|
// spec authority — "done" once the gated spec package exists (mirrors how the
|
|
@@ -519,7 +550,10 @@ function isInferableStage(stage) {
|
|
|
519
550
|
// could never infer scope-check as completed during stale-state recovery.
|
|
520
551
|
// pm is inferable from implementation-plan-{slug}.md for the same reason:
|
|
521
552
|
// it sits before scope-check in the MEDIUM feature sequence.
|
|
522
|
-
|
|
553
|
+
// sheldon (lean lane) and orchestrator (maestro lane) are the single spec
|
|
554
|
+
// authorities — both inferable from their collapsed spec package so stale-state
|
|
555
|
+
// recovery and mid-lane seeding never re-activate a finished spec stage.
|
|
556
|
+
return ['setup', 'product', 'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'ux-ui', 'pm', 'sheldon', 'orchestrator'].includes(
|
|
523
557
|
normalizeAgentName(stage)
|
|
524
558
|
);
|
|
525
559
|
}
|
|
@@ -993,8 +1027,15 @@ async function finalizeCurrentStage(targetDir, config, state, stageName) {
|
|
|
993
1027
|
detour: null
|
|
994
1028
|
});
|
|
995
1029
|
|
|
1030
|
+
// Reconcile eagerly: completing a later stage must never leave `next` pointing
|
|
1031
|
+
// at an earlier unresolved stage (e.g. lean-lane sheldon that only chained via
|
|
1032
|
+
// prompt). Without this, `--complete=dev` re-activated the spec agent and the
|
|
1033
|
+
// reconcile only healed on the NEXT load — after the backwards activation had
|
|
1034
|
+
// already been printed and persisted.
|
|
1035
|
+
const reconciled = reconcileWorkflowState(nextState);
|
|
1036
|
+
|
|
996
1037
|
return {
|
|
997
|
-
state: nextState,
|
|
1038
|
+
state: reconciled.changed ? reconciled.state : nextState,
|
|
998
1039
|
completedStage: normalizedStage,
|
|
999
1040
|
...(auditCodeSummary ? { auditCode: auditCodeSummary } : {})
|
|
1000
1041
|
};
|
|
@@ -1560,9 +1601,12 @@ async function activateStage(
|
|
|
1560
1601
|
state.mode === 'feature' &&
|
|
1561
1602
|
(state.classification === 'SMALL' || state.classification === 'MEDIUM')
|
|
1562
1603
|
) {
|
|
1604
|
+
// Frontmatter flag OR the seeded scheme (slug-scoped) — the per-feature
|
|
1605
|
+
// "Autopilot" choice only seeds workflow-execute.json and never writes
|
|
1606
|
+
// auto_handoff, so reading the frontmatter alone silently disabled it.
|
|
1563
1607
|
try {
|
|
1564
|
-
const
|
|
1565
|
-
autoHandoff =
|
|
1608
|
+
const signal = await resolveAutopilotSignal(targetDir, { slug: state.featureSlug });
|
|
1609
|
+
autoHandoff = signal.enabled;
|
|
1566
1610
|
} catch {
|
|
1567
1611
|
autoHandoff = false;
|
|
1568
1612
|
}
|