@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.
- package/CHANGELOG.md +237 -0
- package/README.md +44 -1
- package/package.json +1 -1
- package/src/cli.js +50 -1
- package/src/commands/chain-audit.js +156 -0
- package/src/commands/op-capture.js +146 -0
- package/src/commands/op-forget.js +54 -0
- package/src/commands/op-identity.js +145 -0
- package/src/commands/op-list.js +105 -0
- package/src/commands/op-migrate.js +158 -0
- package/src/commands/op-promote.js +66 -0
- package/src/commands/op-reinforce.js +73 -0
- package/src/commands/op-show.js +71 -0
- package/src/commands/op-stubs.js +67 -0
- package/src/commands/preflight.js +6 -2
- package/src/commands/runtime.js +178 -0
- package/src/commands/state-save.js +61 -0
- package/src/commands/sync-agents-preflight.js +117 -3
- package/src/commands/workflow-next.js +64 -0
- package/src/handoff-contract.js +25 -0
- package/src/i18n/messages/en.js +9 -0
- package/src/i18n/messages/es.js +9 -0
- package/src/i18n/messages/fr.js +9 -0
- package/src/i18n/messages/pt-BR.js +9 -0
- package/src/lib/agent-semantic-diff.js +199 -0
- package/src/neural-chain-agent-ingest.js +400 -0
- package/src/neural-chain-config.js +95 -0
- package/src/neural-chain-git-ingest.js +280 -0
- package/src/neural-chain-migration.js +61 -0
- package/src/neural-chain-noise-file.js +332 -0
- package/src/neural-chain-sanitize.js +0 -0
- package/src/neural-chain-telemetry.js +90 -0
- package/src/operator-memory/conflict.js +202 -0
- package/src/operator-memory/decay.js +157 -0
- package/src/operator-memory/decision.js +274 -0
- package/src/operator-memory/identity.js +109 -0
- package/src/operator-memory/index-md.js +170 -0
- package/src/operator-memory/loader.js +106 -0
- package/src/operator-memory/proposal.js +179 -0
- package/src/operator-memory/prune.js +81 -0
- package/src/operator-memory/slug.js +90 -0
- package/src/operator-memory/storage.js +121 -0
- package/src/preflight-engine.js +91 -1
- package/src/runtime-store.js +2 -0
- package/template/.aioson/agents/dev.md +1 -1
- package/template/.aioson/agents/deyvin.md +3 -3
- package/template/.aioson/agents/neo.md +23 -1
- package/template/.aioson/agents/product.md +1 -1
- package/template/.aioson/agents/setup.md +1 -1
- package/template/.aioson/docs/deyvin/pair-execution.md +1 -1
- package/template/.aioson/skills/process/decision-presentation/SKILL.md +9 -0
- package/template/AGENTS.md +23 -0
- package/template/CLAUDE.md +23 -0
- 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
|
-
|
|
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
|
package/src/commands/runtime.js
CHANGED
|
@@ -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
|
-
|
|
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 };
|