@jaimevalasek/aioson 1.9.3 → 1.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +237 -0
  2. package/README.md +44 -1
  3. package/package.json +1 -1
  4. package/src/cli.js +50 -1
  5. package/src/commands/chain-audit.js +156 -0
  6. package/src/commands/op-capture.js +146 -0
  7. package/src/commands/op-forget.js +54 -0
  8. package/src/commands/op-identity.js +145 -0
  9. package/src/commands/op-list.js +105 -0
  10. package/src/commands/op-migrate.js +158 -0
  11. package/src/commands/op-promote.js +66 -0
  12. package/src/commands/op-reinforce.js +73 -0
  13. package/src/commands/op-show.js +71 -0
  14. package/src/commands/op-stubs.js +67 -0
  15. package/src/commands/preflight.js +6 -2
  16. package/src/commands/runtime.js +178 -0
  17. package/src/commands/state-save.js +61 -0
  18. package/src/commands/sync-agents-preflight.js +117 -3
  19. package/src/commands/workflow-next.js +64 -0
  20. package/src/handoff-contract.js +25 -0
  21. package/src/i18n/messages/en.js +9 -0
  22. package/src/i18n/messages/es.js +9 -0
  23. package/src/i18n/messages/fr.js +9 -0
  24. package/src/i18n/messages/pt-BR.js +9 -0
  25. package/src/lib/agent-semantic-diff.js +199 -0
  26. package/src/neural-chain-agent-ingest.js +400 -0
  27. package/src/neural-chain-config.js +95 -0
  28. package/src/neural-chain-git-ingest.js +280 -0
  29. package/src/neural-chain-migration.js +61 -0
  30. package/src/neural-chain-noise-file.js +332 -0
  31. package/src/neural-chain-sanitize.js +0 -0
  32. package/src/neural-chain-telemetry.js +90 -0
  33. package/src/operator-memory/conflict.js +202 -0
  34. package/src/operator-memory/decay.js +157 -0
  35. package/src/operator-memory/decision.js +274 -0
  36. package/src/operator-memory/identity.js +109 -0
  37. package/src/operator-memory/index-md.js +170 -0
  38. package/src/operator-memory/loader.js +106 -0
  39. package/src/operator-memory/proposal.js +179 -0
  40. package/src/operator-memory/prune.js +81 -0
  41. package/src/operator-memory/slug.js +90 -0
  42. package/src/operator-memory/storage.js +121 -0
  43. package/src/preflight-engine.js +91 -1
  44. package/src/runtime-store.js +2 -0
  45. package/template/.aioson/agents/dev.md +1 -1
  46. package/template/.aioson/agents/deyvin.md +3 -3
  47. package/template/.aioson/agents/neo.md +23 -1
  48. package/template/.aioson/agents/product.md +1 -1
  49. package/template/.aioson/agents/setup.md +1 -1
  50. package/template/.aioson/docs/deyvin/pair-execution.md +1 -1
  51. package/template/.aioson/skills/process/decision-presentation/SKILL.md +9 -0
  52. package/template/AGENTS.md +23 -0
  53. package/template/CLAUDE.md +23 -0
  54. package/template/agents/_shared/memory-capture-directive.md +115 -0
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson op:reinforce <slug> — update last_reinforced without signal capture.
5
+ * Phase 5 (v1.16.0). User-driven action when decay prompt fires.
6
+ */
7
+
8
+ const fs = require('node:fs');
9
+ const { resolveIdentity } = require('../operator-memory/identity');
10
+ const { ensureStorageTree } = require('../operator-memory/storage');
11
+ const { readDecision, decisionPath, serializeDecision } = require('../operator-memory/decision');
12
+ const { regenerateIndex } = require('../operator-memory/index-md');
13
+ const { emitDossierEvent } = require('../lib/dossier-telemetry');
14
+
15
+ async function runOpReinforce({ args = [], options = {}, logger }) {
16
+ const targetDir = process.cwd();
17
+ const positional = (args || []).filter((a) => typeof a === 'string' && !a.startsWith('-') && a !== '.');
18
+ const slug = positional[0];
19
+
20
+ if (options.help === true || args.includes('--help') || args.includes('-h')) {
21
+ if (logger) logger.log('op:reinforce <slug> — refresh a decision\'s last_reinforced timestamp without re-capturing the signal.');
22
+ return { ok: true };
23
+ }
24
+
25
+ if (!slug) {
26
+ const err = 'op:reinforce — required argument: <slug>. Usage: aioson op:reinforce <slug>';
27
+ if (options.json) return { ok: false, error: err };
28
+ if (logger && logger.error) logger.error(err);
29
+ return { ok: false, exitCode: 1, error: err };
30
+ }
31
+
32
+ const resolved = resolveIdentity();
33
+ ensureStorageTree(resolved.identity);
34
+ const decision = readDecision(resolved.identity, slug);
35
+ if (!decision) {
36
+ const err = `op:reinforce — decision '${slug}' not found for identity ${resolved.identity}.`;
37
+ if (options.json) return { ok: false, error: err };
38
+ if (logger && logger.error) logger.error(err);
39
+ return { ok: false, exitCode: 1, error: err };
40
+ }
41
+
42
+ const now = new Date().toISOString();
43
+ const previous = decision.last_reinforced;
44
+ const updated = {
45
+ ...decision,
46
+ slug,
47
+ last_reinforced: now,
48
+ reinforcement_count: Number(decision.reinforcement_count || 0) + 1
49
+ };
50
+ // serialize keeps quotes + body + frontmatter intact
51
+ const out = serializeDecision({ ...updated, body: decision.body, title: decision.body?.split('\n')[0]?.replace(/^# /, '') || slug });
52
+ const filePath = decisionPath(resolved.identity, slug);
53
+ const tmp = `${filePath}.tmp`;
54
+ fs.writeFileSync(tmp, out, 'utf8');
55
+ fs.renameSync(tmp, filePath);
56
+
57
+ try { regenerateIndex(resolved.identity); } catch { /* non-fatal */ }
58
+
59
+ await emitDossierEvent(targetDir, {
60
+ agent: 'op-reinforce',
61
+ type: 'op_reinforce',
62
+ summary: `reinforced ${slug}`,
63
+ meta: { identity_prefix: resolved.identity.slice(0, 8), slug, previous, now }
64
+ });
65
+
66
+ if (options.json) {
67
+ return { ok: true, slug, last_reinforced: now, previous, reinforcement_count: updated.reinforcement_count };
68
+ }
69
+ if (logger) logger.log(`op:reinforce — '${slug}' last_reinforced refreshed to ${now}.`);
70
+ return { ok: true, slug };
71
+ }
72
+
73
+ module.exports = { runOpReinforce };
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson op:show <slug> — print a single decision body + frontmatter (Phase 3, v1.14.0).
5
+ */
6
+
7
+ const fs = require('node:fs');
8
+ const { resolveIdentity } = require('../operator-memory/identity');
9
+ const { ensureStorageTree } = require('../operator-memory/storage');
10
+ const { readDecision, decisionPath } = require('../operator-memory/decision');
11
+ const { readProposal } = require('../operator-memory/proposal');
12
+
13
+ async function runOpShow({ args = [], options = {}, logger }) {
14
+ const positional = (args || []).filter((a) => typeof a === 'string' && !a.startsWith('-') && a !== '.');
15
+ const slug = positional[0];
16
+
17
+ if (options.help === true || args.includes('--help') || args.includes('-h')) {
18
+ if (logger) logger.log('op:show <slug> — print a single decision (frontmatter + body). --json for structured output.');
19
+ return { ok: true };
20
+ }
21
+
22
+ if (!slug) {
23
+ const err = 'op:show — required argument: <slug>. Usage: aioson op:show <slug>';
24
+ if (options.json) return { ok: false, error: err };
25
+ if (logger && logger.error) logger.error(err);
26
+ return { ok: false, exitCode: 1, error: err };
27
+ }
28
+
29
+ const resolved = resolveIdentity();
30
+ ensureStorageTree(resolved.identity);
31
+
32
+ const decision = readDecision(resolved.identity, slug);
33
+ if (decision) {
34
+ if (options.json) {
35
+ return { ok: true, kind: 'decision', identity: resolved.identity, slug, ...decision };
36
+ }
37
+ if (logger) {
38
+ const filePath = decisionPath(resolved.identity, slug);
39
+ const raw = fs.readFileSync(filePath, 'utf8');
40
+ logger.log(raw);
41
+ }
42
+ return { ok: true, kind: 'decision' };
43
+ }
44
+
45
+ const proposal = readProposal(resolved.identity, slug);
46
+ if (proposal) {
47
+ if (options.json) {
48
+ return { ok: true, kind: 'proposal', identity: resolved.identity, slug, ...proposal };
49
+ }
50
+ if (logger) {
51
+ logger.log(`# Proposal: ${slug}`);
52
+ logger.log('');
53
+ logger.log(`signal_type: ${proposal.signal_type}`);
54
+ logger.log(`detected_count: ${proposal.detected_count}`);
55
+ logger.log(`first_detected: ${proposal.first_detected}`);
56
+ logger.log(`last_detected: ${proposal.last_detected}`);
57
+ logger.log(`proposal: ${proposal.proposal}`);
58
+ logger.log('');
59
+ logger.log('## Quotes');
60
+ for (const q of (proposal.quotes || [])) logger.log(`- "${q}"`);
61
+ }
62
+ return { ok: true, kind: 'proposal' };
63
+ }
64
+
65
+ const err = `op:show — '${slug}' not found in decisions/ or proposals/ for identity ${resolved.identity}.`;
66
+ if (options.json) return { ok: false, error: err };
67
+ if (logger && logger.error) logger.error(err);
68
+ return { ok: false, exitCode: 1, error: err };
69
+ }
70
+
71
+ module.exports = { runOpShow };
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson op:* — Phase 1 stubs for commands shipped in later phases.
5
+ *
6
+ * Each stub emits a "Not yet implemented" stderr message + structured
7
+ * `op_command_stub` telemetry event, then exits non-zero. Replaced in
8
+ * Phases 2-5 with full implementations.
9
+ *
10
+ * AC-P1-07: six CLI commands respond with at least --help text; op:identity
11
+ * is fully functional in Phase 1; the other five are stubs until their
12
+ * respective phases ship.
13
+ */
14
+
15
+ const { emitDossierEvent } = require('../lib/dossier-telemetry');
16
+
17
+ const STUB_INFO = {
18
+ 'op:capture': { phase: 2, release: 'v1.13.0', summary: 'capture LLM-driven signal into proposals/ queue' },
19
+ 'op:promote': { phase: 2, release: 'v1.13.0', summary: 'manually promote a proposal to decisions/ (skip 2x threshold)' },
20
+ 'op:forget': { phase: 2, release: 'v1.13.0', summary: 'soft-delete a decision or proposal to history/' },
21
+ 'op:list': { phase: 3, release: 'v1.14.0', summary: 'list active decisions (and --include-archived)' },
22
+ 'op:show': { phase: 3, release: 'v1.14.0', summary: 'print a single decision body + frontmatter' }
23
+ };
24
+
25
+ function makeStub(commandName) {
26
+ const info = STUB_INFO[commandName];
27
+ if (!info) {
28
+ throw new Error(`makeStub: unknown command '${commandName}'`);
29
+ }
30
+
31
+ return async function runStub({ args = [], options = {}, logger } = {}) {
32
+ const targetDir = process.cwd();
33
+ const helpRequested = options.help === true || args.includes('--help') || args.includes('-h');
34
+
35
+ if (helpRequested) {
36
+ const msg = `${commandName} — ${info.summary}\n Status: shipped in Phase ${info.phase} (${info.release}).\n Phase 1 (v1.12.0) wires the command surface but defers logic to that release.`;
37
+ if (options.json) return { ok: true, stub: true, command: commandName, phase: info.phase, release: info.release, summary: info.summary };
38
+ if (logger) logger.log(msg);
39
+ return { ok: true, stub: true };
40
+ }
41
+
42
+ await emitDossierEvent(targetDir, {
43
+ agent: commandName,
44
+ type: 'op_command_stub',
45
+ summary: `${commandName} invoked before its release phase`,
46
+ meta: { command: commandName, phase: info.phase, release: info.release }
47
+ });
48
+
49
+ const errMsg = `${commandName} — Not yet implemented (ships in Phase ${info.phase} / ${info.release}). Run \`${commandName} --help\` for scope.`;
50
+ if (options.json) {
51
+ return { ok: false, stub: true, command: commandName, phase: info.phase, release: info.release, error: errMsg };
52
+ }
53
+ if (logger && logger.error) {
54
+ logger.error(errMsg);
55
+ }
56
+ return { ok: false, stub: true, exitCode: 1 };
57
+ };
58
+ }
59
+
60
+ module.exports = {
61
+ runOpCapture: makeStub('op:capture'),
62
+ runOpPromote: makeStub('op:promote'),
63
+ runOpForget: makeStub('op:forget'),
64
+ runOpList: makeStub('op:list'),
65
+ runOpShow: makeStub('op:show'),
66
+ STUB_INFO
67
+ };
@@ -28,6 +28,7 @@ const {
28
28
  buildContextPackage,
29
29
  evaluateReadiness,
30
30
  detectStaleDevState,
31
+ detectStaleDevStateRich,
31
32
  extractSpecVersion,
32
33
  extractLastCheckpoint,
33
34
  GATE_NAMES
@@ -69,8 +70,11 @@ async function runPreflight({ args, options = {}, logger }) {
69
70
  ? manifest.path
70
71
  : (artifacts.implementation_plan.exists ? artifacts.implementation_plan.path : null);
71
72
 
72
- // Stale dev-state detection (AC-SDLC-12)
73
- const staleDevStateWarning = devState.exists ? detectStaleDevState(devState, slug) : null;
73
+ // Stale dev-state detection (AC-SDLC-12 + F1 workflow-handoff-integrity v1.9.7).
74
+ // Use rich variant: cross-references features.md (orphan/done detection) and applies 30d TTL.
75
+ const staleDevStateWarning = devState.exists
76
+ ? await detectStaleDevStateRich(devState, slug, targetDir)
77
+ : null;
74
78
 
75
79
  // Determine mode
76
80
  const mode = slug
@@ -22,6 +22,7 @@ const { runAutoDelivery } = require('../delivery-runner');
22
22
  const { writeHandoff, buildRuntimeLogHandoff } = require('../session-handoff');
23
23
  const { backupAiosonDocs, isDocCreatingAgent } = require('../backup-local');
24
24
  const { runMemoryReflectPrepare } = require('./memory-reflect-prepare');
25
+ const { runChainHookOnAgentDone } = require('../neural-chain-agent-ingest');
25
26
 
26
27
  const ALLOWED_LAYOUTS = new Set(['document', 'tabs', 'accordion', 'stack', 'mixed']);
27
28
  const DEFAULT_TEXT_FIELDS = ['content', 'text', 'body', 'lyrics', 'markdown'];
@@ -1222,6 +1223,9 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1222
1223
  logger.log(`agent:done — ${normalizedAgent} | live session active, event logged | run: ${session.runKey} (${dbPath})`);
1223
1224
  }
1224
1225
 
1226
+ // F2 (workflow-handoff-integrity v1.9.5) — best-effort auto-advance workflow pointer
1227
+ await maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options, logger, t });
1228
+
1225
1229
  if (isDocCreatingAgent(normalizedAgent)) {
1226
1230
  backupAiosonDocs(targetDir).catch(() => {});
1227
1231
  }
@@ -1235,6 +1239,19 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1235
1239
  });
1236
1240
  } catch { /* ignore */ }
1237
1241
 
1242
+ // Neural Chain: best-effort agent_event ingest + per-file audit telemetry.
1243
+ // BR-NC-05 (per-session hook), BR-NC-10 (telemetry obligation), BR-NC-11
1244
+ // (failure non-blocking), EC-NC-05 (no-edits skip path still emits event).
1245
+ try {
1246
+ runChainHookOnAgentDone({
1247
+ db,
1248
+ targetDir,
1249
+ artifacts: artifactPaths,
1250
+ agentName: normalizedAgent,
1251
+ featureSlug: options.feature ? String(options.feature).trim() : null
1252
+ });
1253
+ } catch { /* ignore — never blocks agent_done */ }
1254
+
1238
1255
  return { ok: true, targetDir, dbPath, agent: normalizedAgent, mode: 'live_event', runKey: session.runKey };
1239
1256
  }
1240
1257
 
@@ -1279,6 +1296,9 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1279
1296
  logger.log(`agent:done — ${normalizedAgent} | task: ${taskKey} | run: ${runKey} (${dbPath})`);
1280
1297
  }
1281
1298
 
1299
+ // F2 (workflow-handoff-integrity v1.9.5) — best-effort auto-advance workflow pointer
1300
+ await maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options, logger, t });
1301
+
1282
1302
  if (isDocCreatingAgent(normalizedAgent)) {
1283
1303
  backupAiosonDocs(targetDir).catch(() => {});
1284
1304
  }
@@ -1292,6 +1312,19 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1292
1312
  });
1293
1313
  } catch { /* ignore */ }
1294
1314
 
1315
+ // Neural Chain: best-effort agent_event ingest + per-file audit telemetry.
1316
+ // BR-NC-05 (per-session hook), BR-NC-10 (telemetry obligation), BR-NC-11
1317
+ // (failure non-blocking), EC-NC-05 (no-edits skip path still emits event).
1318
+ try {
1319
+ runChainHookOnAgentDone({
1320
+ db,
1321
+ targetDir,
1322
+ artifacts: artifactPaths,
1323
+ agentName: normalizedAgent,
1324
+ featureSlug: options.feature ? String(options.feature).trim() : null
1325
+ });
1326
+ } catch { /* ignore — never blocks agent_done */ }
1327
+
1295
1328
  return { ok: true, targetDir, dbPath, agent: normalizedAgent, mode: 'standalone', runKey, taskKey };
1296
1329
  } finally {
1297
1330
  db.close();
@@ -1299,6 +1332,150 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1299
1332
  }
1300
1333
 
1301
1334
 
1335
+ /**
1336
+ * maybeAutoAdvanceWorkflow — F2 (workflow-handoff-integrity v1.9.5)
1337
+ *
1338
+ * Best-effort: when a workflow is active for the project AND the calling
1339
+ * agent has produced its canonical artifact on disk, internally invokes
1340
+ * `runWorkflowNext({ complete: <agent> })` so the pointer advances without
1341
+ * requiring every agent prompt to literal-call `aioson workflow:next`.
1342
+ *
1343
+ * Gating (DD-01 — workflow.state.json presence-detection):
1344
+ * - workflow.state.json absent OR `--no-auto-advance` flag → skip (backward-compat)
1345
+ * - workflow.state.json corrupt → log warning, skip (AC-F2-09 graceful degradation)
1346
+ * - agent unknown in handoff-contract CONTRACTS → log warning, skip (AC-F2-10)
1347
+ *
1348
+ * Idempotency (BR-01): `last_workflow_event_at` in workflow.state.json blocks
1349
+ * re-emission within a 1s window.
1350
+ *
1351
+ * Side effects (best-effort, every failure is non-fatal):
1352
+ * - reads `.aioson/context/workflow.state.json`
1353
+ * - writes `last_workflow_event_at` back to that file on success
1354
+ * - calls `runWorkflowNext` with quiet logger + `--json` to suppress prose
1355
+ * - emits ONE concise stdout line on success when not in --json mode
1356
+ *
1357
+ * @param {object} ctx
1358
+ * @param {string} ctx.targetDir Project root.
1359
+ * @param {string} ctx.normalizedAgent Agent name with leading `@`.
1360
+ * @param {object} ctx.options agent:done CLI options.
1361
+ * @param {object} ctx.logger Logger (logger.log + logger.error).
1362
+ * @param {Function} [ctx.t] Translation fn (passed through).
1363
+ * @returns {Promise<{advanced: boolean, skipped?: string, error?: string}>}
1364
+ */
1365
+ async function maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options = {}, logger, t }) {
1366
+ // DD-01 opt-out — explicit --no-auto-advance disables, regardless of state.
1367
+ if (options['no-auto-advance'] || options.noAutoAdvance) {
1368
+ return { advanced: false, skipped: 'opt-out' };
1369
+ }
1370
+
1371
+ const statePath = path.join(targetDir, '.aioson', 'context', 'workflow.state.json');
1372
+
1373
+ // 1. Read workflow.state.json (graceful absent OR corrupt — AC-F2-02 / AC-F2-09).
1374
+ let state;
1375
+ try {
1376
+ const raw = await fs.readFile(statePath, 'utf8');
1377
+ state = JSON.parse(raw);
1378
+ } catch (err) {
1379
+ if (err.code === 'ENOENT') {
1380
+ return { advanced: false, skipped: 'no_active_workflow' };
1381
+ }
1382
+ if (!options.json && logger?.error) {
1383
+ logger.error(`[agent:done] workflow.state.json unreadable (${err.code || err.message}); fallback to backward-compat (no auto-advance)`);
1384
+ }
1385
+ return { advanced: false, skipped: 'state_corrupt', error: err.message };
1386
+ }
1387
+
1388
+ // 2. Inactive workflow → skip.
1389
+ if (!state || (!state.featureSlug && state.mode !== 'project') || state.current === null) {
1390
+ return { advanced: false, skipped: 'inactive_workflow' };
1391
+ }
1392
+
1393
+ // 3. Idempotency guard (BR-01 — 1s window).
1394
+ const now = Date.now();
1395
+ const lastEventAt = Number(state.last_workflow_event_at) || 0;
1396
+ if (now - lastEventAt < 1000) {
1397
+ return { advanced: false, skipped: 'idempotency_window' };
1398
+ }
1399
+
1400
+ // 4. Lookup canonical artifact via handoff-contract (DPC-03 — reuse CONTRACTS map).
1401
+ let artifacts;
1402
+ try {
1403
+ const { getCanonicalArtifactsForAgent } = require('../handoff-contract');
1404
+ artifacts = await getCanonicalArtifactsForAgent(normalizedAgent, targetDir, {
1405
+ mode: state.mode || 'feature',
1406
+ featureSlug: state.featureSlug,
1407
+ classification: state.classification
1408
+ });
1409
+ } catch (err) {
1410
+ if (!options.json && logger?.error) {
1411
+ logger.error(`[agent:done] handoff-contract lookup failed (${err.message}); skip auto-advance`);
1412
+ }
1413
+ return { advanced: false, skipped: 'contract_error', error: err.message };
1414
+ }
1415
+
1416
+ // AC-F2-10 — agent not registered in CONTRACTS.
1417
+ if (artifacts === null) {
1418
+ if (!options.json && logger?.error) {
1419
+ logger.error(`[agent:done] agent '${normalizedAgent}' not in handoff-contract CONTRACTS map; skip auto-advance`);
1420
+ }
1421
+ return { advanced: false, skipped: 'unknown_agent' };
1422
+ }
1423
+
1424
+ // Empty array — agent legitimately produces no canonical artifact (e.g. @committer, @dev).
1425
+ // Don't auto-advance; the workflow advances on explicit user action when needed.
1426
+ if (artifacts.length === 0) {
1427
+ return { advanced: false, skipped: 'no_canonical_artifact' };
1428
+ }
1429
+
1430
+ // 5. At least one declared artifact must exist on disk before we trust auto-advance.
1431
+ let anyExists = false;
1432
+ for (const artifactPath of artifacts) {
1433
+ try {
1434
+ await fs.access(artifactPath);
1435
+ anyExists = true;
1436
+ break;
1437
+ } catch { /* not found — try next */ }
1438
+ }
1439
+ if (!anyExists) {
1440
+ return { advanced: false, skipped: 'artifact_missing' };
1441
+ }
1442
+
1443
+ // 6. Internal invocation of runWorkflowNext (lazy require — circular safety).
1444
+ let result;
1445
+ try {
1446
+ const { runWorkflowNext } = require('./workflow-next');
1447
+ result = await runWorkflowNext({
1448
+ args: [targetDir],
1449
+ options: { complete: normalizedAgent.replace(/^@/, ''), json: true },
1450
+ logger: { log: () => {}, error: () => {}, warn: () => {} },
1451
+ t
1452
+ });
1453
+ } catch (err) {
1454
+ if (!options.json && logger?.error) {
1455
+ logger.error(`[agent:done] workflow:next failed (${err.message}); pointer unchanged`);
1456
+ }
1457
+ return { advanced: false, skipped: 'workflow_next_failed', error: err.message };
1458
+ }
1459
+
1460
+ // 7. Persist last_workflow_event_at for idempotency (best-effort).
1461
+ try {
1462
+ const refreshedRaw = await fs.readFile(statePath, 'utf8').catch(() => null);
1463
+ const refreshed = refreshedRaw ? JSON.parse(refreshedRaw) : state;
1464
+ refreshed.last_workflow_event_at = now;
1465
+ await fs.writeFile(statePath, `${JSON.stringify(refreshed, null, 2)}\n`);
1466
+ } catch { /* non-fatal */ }
1467
+
1468
+ // 8. Surface concise outcome — single line, AFTER existing standard log (AC-F2-02 preserved).
1469
+ if (!options.json && logger?.log && result?.ok) {
1470
+ const nextStage = result.next || result.nextStage || null;
1471
+ const tag = nextStage ? `→ ${nextStage}` : '(workflow complete)';
1472
+ logger.log(`[agent:done] auto-advanced ${tag}`);
1473
+ }
1474
+
1475
+ return { advanced: true, result };
1476
+ }
1477
+
1478
+
1302
1479
  async function runRuntimeSessionStart({ args, options = {}, logger, t }) {
1303
1480
  const targetDir = resolveTargetDir(args);
1304
1481
  const { db, dbPath, runtimeDir } = await openRuntimeDb(targetDir);
@@ -2074,6 +2251,7 @@ module.exports = {
2074
2251
  runRuntimeLog,
2075
2252
  runAgentDone,
2076
2253
  runAgentRecover,
2254
+ maybeAutoAdvanceWorkflow,
2077
2255
  runRuntimeSessionStart,
2078
2256
  runRuntimeSessionLog,
2079
2257
  runRuntimeSessionFinish,
@@ -212,8 +212,69 @@ async function runStateSave({ args, options = {}, logger }) {
212
212
  return result;
213
213
  }
214
214
 
215
+ /**
216
+ * runStateReset — F1 (workflow-handoff-integrity v1.9.7)
217
+ *
218
+ * Clears `.aioson/context/dev-state.md`. Idempotent: no-op when file is absent.
219
+ *
220
+ * Behavior:
221
+ * - Default: deletes the file outright.
222
+ * - With `--archive`: moves to `.aioson/runtime/devstate-history/{ISO}.md` for audit trail.
223
+ *
224
+ * Usage:
225
+ * aioson state:reset .
226
+ * aioson state:reset . --archive
227
+ * aioson state:reset . --archive --json
228
+ */
229
+ async function runStateReset({ args, options = {}, logger }) {
230
+ const targetDir = path.resolve(process.cwd(), args[0] || '.');
231
+ const statePath = path.join(contextDir(targetDir), 'dev-state.md');
232
+ const archive = Boolean(options.archive);
233
+
234
+ let archivedTo = null;
235
+ let removed = false;
236
+
237
+ try {
238
+ await fs.access(statePath);
239
+ } catch {
240
+ // AC-F1-03 — idempotent: no-op when absent.
241
+ const result = { ok: true, removed: false, archived: null, reason: 'no_state_file' };
242
+ if (options.json) return result;
243
+ logger.log('state:reset — no dev-state.md present; nothing to do.');
244
+ return result;
245
+ }
246
+
247
+ if (archive) {
248
+ const historyDir = path.join(targetDir, '.aioson', 'runtime', 'devstate-history');
249
+ await fs.mkdir(historyDir, { recursive: true });
250
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
251
+ archivedTo = path.join(historyDir, `${stamp}.md`);
252
+ const content = await fs.readFile(statePath, 'utf8');
253
+ await fs.writeFile(archivedTo, content, 'utf8');
254
+ }
255
+
256
+ await fs.unlink(statePath);
257
+ removed = true;
258
+
259
+ const result = {
260
+ ok: true,
261
+ removed,
262
+ archived: archivedTo ? path.relative(targetDir, archivedTo) : null
263
+ };
264
+
265
+ if (options.json) return result;
266
+
267
+ if (archivedTo) {
268
+ logger.log(`state:reset — dev-state.md archived to ${result.archived} and removed.`);
269
+ } else {
270
+ logger.log('state:reset — dev-state.md removed (no archive).');
271
+ }
272
+ return result;
273
+ }
274
+
215
275
  module.exports = {
216
276
  runStateSave,
277
+ runStateReset,
217
278
  CONTEXT_TYPE_MAP,
218
279
  MAX_CONTEXT,
219
280
  parseContextFlag
@@ -22,6 +22,7 @@ const path = require('node:path');
22
22
 
23
23
  const { CHAIN_AGENTS, FEATURE_DOSSIER_HEADER, extractSection } = require('./dossier-audit');
24
24
  const dossierTelemetry = require('../lib/dossier-telemetry');
25
+ const { diffAgentFile } = require('../lib/agent-semantic-diff');
25
26
 
26
27
  // Active Learning Loop Phase 6 — workspace ↔ template parity checks for the
27
28
  // `.aioson/` artifacts this feature ships. Phase 1 dev's documented decision
@@ -105,6 +106,90 @@ function checkLearningLoopTemplateParity(projectRoot) {
105
106
  return issues;
106
107
  }
107
108
 
109
+ /**
110
+ * checkSemanticParity — T5 (workflow-handoff-integrity v1.9.8)
111
+ *
112
+ * Detects semantic drift between `.aioson/agents/{agent}.md` and
113
+ * `template/.aioson/agents/{agent}.md`: headers (presence + order), section
114
+ * content (hash), and frontmatter fields. Catches 981a8fd-style migration
115
+ * incompleteness that the original Feature-dossier-length check misses.
116
+ *
117
+ * Mode-aware severity (per PMD-04 / DD-03):
118
+ * - Default (local dev): returns issues with severity='warning' (caller is non-blocking).
119
+ * - `AIOSON_PREPUBLISH=true`: returns issues with severity='error' (caller blocks publish).
120
+ *
121
+ * Returns array of issue objects.
122
+ */
123
+ function checkSemanticParity(projectRoot) {
124
+ const issues = [];
125
+ const isPrepublish = process.env.AIOSON_PREPUBLISH === 'true';
126
+ const severity = isPrepublish ? 'error' : 'warning';
127
+
128
+ for (const agent of CHAIN_AGENTS) {
129
+ const workspacePath = path.join(projectRoot, '.aioson', 'agents', `${agent}.md`);
130
+ const templatePath = path.join(projectRoot, 'template', '.aioson', 'agents', `${agent}.md`);
131
+ const workspaceRaw = readFileOrEmpty(workspacePath);
132
+ const templateRaw = readFileOrEmpty(templatePath);
133
+
134
+ const diff = diffAgentFile(workspaceRaw, templateRaw);
135
+ if (!diff) continue;
136
+
137
+ if (diff.missingFile) {
138
+ // AC-T5-08 — file removed in one side, still present in the other.
139
+ issues.push({
140
+ agent,
141
+ kind: 'missing_file',
142
+ side: diff.missingFile,
143
+ severity,
144
+ hint: diff.missingFile === 'workspace'
145
+ ? `template/.aioson/agents/${agent}.md exists but workspace/.aioson/agents/${agent}.md does not — sync would create it (likely fine, unless workspace deletion was intentional)`
146
+ : `workspace/.aioson/agents/${agent}.md exists but template/.aioson/agents/${agent}.md does not — workspace edits are unpropagated; copy to template OR delete workspace file`
147
+ });
148
+ continue;
149
+ }
150
+
151
+ if (diff.missingInTemplate.length > 0) {
152
+ issues.push({
153
+ agent, kind: 'sections_missing_in_template', sections: diff.missingInTemplate, severity,
154
+ hint: `Workspace has sections not in template: ${diff.missingInTemplate.map((s) => `'${s}'`).join(', ')}. Likely unpropagated workspace edits (981a8fd pattern).`
155
+ });
156
+ }
157
+ if (diff.missingInWorkspace.length > 0) {
158
+ issues.push({
159
+ agent, kind: 'sections_missing_in_workspace', sections: diff.missingInWorkspace, severity,
160
+ hint: `Template has sections not in workspace: ${diff.missingInWorkspace.map((s) => `'${s}'`).join(', ')}. Workspace lost content OR template added new sections post-sync.`
161
+ });
162
+ }
163
+ if (diff.reordered) {
164
+ issues.push({
165
+ agent, kind: 'sections_reordered', severity,
166
+ hint: 'Section order differs between workspace and template — review for unintended structural drift.'
167
+ });
168
+ }
169
+ if (diff.divergedSections.length > 0) {
170
+ issues.push({
171
+ agent, kind: 'section_content_diverged', sections: diff.divergedSections.map((d) => d.header), severity,
172
+ hint: `Section content hash differs (not cosmetic): ${diff.divergedSections.map((d) => `'${d.header}'`).join(', ')}. Investigate before sync to avoid 981a8fd-style migration regression.`
173
+ });
174
+ }
175
+ if (diff.frontmatter) {
176
+ const fm = diff.frontmatter;
177
+ if (fm.missingInTemplate.length > 0) {
178
+ issues.push({ agent, kind: 'frontmatter_missing_in_template', fields: fm.missingInTemplate, severity });
179
+ }
180
+ if (fm.missingInWorkspace.length > 0) {
181
+ issues.push({ agent, kind: 'frontmatter_missing_in_workspace', fields: fm.missingInWorkspace, severity });
182
+ }
183
+ if (fm.valueChanged.length > 0) {
184
+ issues.push({ agent, kind: 'frontmatter_value_changed', changes: fm.valueChanged, severity });
185
+ }
186
+ }
187
+ }
188
+
189
+ return issues;
190
+ }
191
+
192
+
108
193
  function checkParity(projectRoot) {
109
194
  const violations = [];
110
195
  for (const agent of CHAIN_AGENTS) {
@@ -132,8 +217,11 @@ function checkParity(projectRoot) {
132
217
  async function main(projectRoot = process.cwd()) {
133
218
  const violations = checkParity(projectRoot);
134
219
  const learningLoopIssues = checkLearningLoopTemplateParity(projectRoot);
220
+ // T5 (workflow-handoff-integrity v1.9.8) — semantic parity check on top of length check.
221
+ const semanticIssues = checkSemanticParity(projectRoot);
222
+ const isPrepublish = process.env.AIOSON_PREPUBLISH === 'true';
135
223
 
136
- if (violations.length === 0 && learningLoopIssues.length === 0) {
224
+ if (violations.length === 0 && learningLoopIssues.length === 0 && semanticIssues.length === 0) {
137
225
  return 0;
138
226
  }
139
227
 
@@ -166,11 +254,37 @@ async function main(projectRoot = process.cwd()) {
166
254
  });
167
255
  }
168
256
 
169
- return 1;
257
+ // T5 semantic drift — warning-only locally, error in pre-publish mode.
258
+ if (semanticIssues.length > 0) {
259
+ const banner = isPrepublish
260
+ ? '[sync:agents BLOCKED] Semantic parity drift detected (pre-publish hard fail — AIOSON_PREPUBLISH=true):'
261
+ : '[sync:agents WARN] Semantic parity drift detected (warning only — non-blocking for local dev):';
262
+ process.stderr.write(`${banner}\n`);
263
+ for (const issue of semanticIssues) {
264
+ const detail = issue.sections ? ` [${issue.sections.join(', ')}]`
265
+ : issue.fields ? ` [${issue.fields.join(', ')}]`
266
+ : issue.changes ? ` [${issue.changes.map((c) => c.key).join(', ')}]`
267
+ : '';
268
+ process.stderr.write(` - @${issue.agent}: ${issue.kind}${detail}\n`);
269
+ if (issue.hint) process.stderr.write(` → ${issue.hint}\n`);
270
+ }
271
+ await dossierTelemetry.emitDossierEvent(projectRoot, {
272
+ agent: 'sync-agents-preflight',
273
+ type: 'semantic_parity_violation',
274
+ summary: `${semanticIssues.length} semantic drift issue(s) (mode: ${isPrepublish ? 'prepublish-fail' : 'local-warn'})`,
275
+ meta: { issues: semanticIssues, prepublish: isPrepublish }
276
+ });
277
+ }
278
+
279
+ // Block only when there are HARD failures: length violations + learning-loop issues
280
+ // always block (existing behavior). Semantic drift blocks ONLY in pre-publish mode.
281
+ const hardBlock = violations.length > 0 || learningLoopIssues.length > 0
282
+ || (isPrepublish && semanticIssues.length > 0);
283
+ return hardBlock ? 1 : 0;
170
284
  }
171
285
 
172
286
  if (require.main === module) {
173
287
  main().then((code) => process.exit(code));
174
288
  }
175
289
 
176
- module.exports = { checkParity, checkLearningLoopTemplateParity, main };
290
+ module.exports = { checkParity, checkLearningLoopTemplateParity, checkSemanticParity, main };