@jaimevalasek/aioson 1.9.2 → 1.16.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 +206 -0
- package/README.md +44 -1
- package/package.json +1 -1
- package/src/cli.js +45 -1
- 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 +151 -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/lib/agent-semantic-diff.js +199 -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/template/.aioson/agents/dev.md +1 -1
- package/template/.aioson/agents/deyvin.md +3 -3
- package/template/.aioson/agents/manifests/pm.manifest.json +2 -1
- package/template/.aioson/agents/neo.md +1 -1
- package/template/.aioson/agents/orchestrator.md +4 -3
- package/template/.aioson/agents/pm.md +58 -6
- 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/aioson-spec-driven/references/artifact-map.md +2 -2
- 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
|
@@ -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
|
@@ -1222,6 +1222,9 @@ async function runAgentDone({ args, options = {}, logger, t }) {
|
|
|
1222
1222
|
logger.log(`agent:done — ${normalizedAgent} | live session active, event logged | run: ${session.runKey} (${dbPath})`);
|
|
1223
1223
|
}
|
|
1224
1224
|
|
|
1225
|
+
// F2 (workflow-handoff-integrity v1.9.5) — best-effort auto-advance workflow pointer
|
|
1226
|
+
await maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options, logger, t });
|
|
1227
|
+
|
|
1225
1228
|
if (isDocCreatingAgent(normalizedAgent)) {
|
|
1226
1229
|
backupAiosonDocs(targetDir).catch(() => {});
|
|
1227
1230
|
}
|
|
@@ -1279,6 +1282,9 @@ async function runAgentDone({ args, options = {}, logger, t }) {
|
|
|
1279
1282
|
logger.log(`agent:done — ${normalizedAgent} | task: ${taskKey} | run: ${runKey} (${dbPath})`);
|
|
1280
1283
|
}
|
|
1281
1284
|
|
|
1285
|
+
// F2 (workflow-handoff-integrity v1.9.5) — best-effort auto-advance workflow pointer
|
|
1286
|
+
await maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options, logger, t });
|
|
1287
|
+
|
|
1282
1288
|
if (isDocCreatingAgent(normalizedAgent)) {
|
|
1283
1289
|
backupAiosonDocs(targetDir).catch(() => {});
|
|
1284
1290
|
}
|
|
@@ -1299,6 +1305,150 @@ async function runAgentDone({ args, options = {}, logger, t }) {
|
|
|
1299
1305
|
}
|
|
1300
1306
|
|
|
1301
1307
|
|
|
1308
|
+
/**
|
|
1309
|
+
* maybeAutoAdvanceWorkflow — F2 (workflow-handoff-integrity v1.9.5)
|
|
1310
|
+
*
|
|
1311
|
+
* Best-effort: when a workflow is active for the project AND the calling
|
|
1312
|
+
* agent has produced its canonical artifact on disk, internally invokes
|
|
1313
|
+
* `runWorkflowNext({ complete: <agent> })` so the pointer advances without
|
|
1314
|
+
* requiring every agent prompt to literal-call `aioson workflow:next`.
|
|
1315
|
+
*
|
|
1316
|
+
* Gating (DD-01 — workflow.state.json presence-detection):
|
|
1317
|
+
* - workflow.state.json absent OR `--no-auto-advance` flag → skip (backward-compat)
|
|
1318
|
+
* - workflow.state.json corrupt → log warning, skip (AC-F2-09 graceful degradation)
|
|
1319
|
+
* - agent unknown in handoff-contract CONTRACTS → log warning, skip (AC-F2-10)
|
|
1320
|
+
*
|
|
1321
|
+
* Idempotency (BR-01): `last_workflow_event_at` in workflow.state.json blocks
|
|
1322
|
+
* re-emission within a 1s window.
|
|
1323
|
+
*
|
|
1324
|
+
* Side effects (best-effort, every failure is non-fatal):
|
|
1325
|
+
* - reads `.aioson/context/workflow.state.json`
|
|
1326
|
+
* - writes `last_workflow_event_at` back to that file on success
|
|
1327
|
+
* - calls `runWorkflowNext` with quiet logger + `--json` to suppress prose
|
|
1328
|
+
* - emits ONE concise stdout line on success when not in --json mode
|
|
1329
|
+
*
|
|
1330
|
+
* @param {object} ctx
|
|
1331
|
+
* @param {string} ctx.targetDir Project root.
|
|
1332
|
+
* @param {string} ctx.normalizedAgent Agent name with leading `@`.
|
|
1333
|
+
* @param {object} ctx.options agent:done CLI options.
|
|
1334
|
+
* @param {object} ctx.logger Logger (logger.log + logger.error).
|
|
1335
|
+
* @param {Function} [ctx.t] Translation fn (passed through).
|
|
1336
|
+
* @returns {Promise<{advanced: boolean, skipped?: string, error?: string}>}
|
|
1337
|
+
*/
|
|
1338
|
+
async function maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options = {}, logger, t }) {
|
|
1339
|
+
// DD-01 opt-out — explicit --no-auto-advance disables, regardless of state.
|
|
1340
|
+
if (options['no-auto-advance'] || options.noAutoAdvance) {
|
|
1341
|
+
return { advanced: false, skipped: 'opt-out' };
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const statePath = path.join(targetDir, '.aioson', 'context', 'workflow.state.json');
|
|
1345
|
+
|
|
1346
|
+
// 1. Read workflow.state.json (graceful absent OR corrupt — AC-F2-02 / AC-F2-09).
|
|
1347
|
+
let state;
|
|
1348
|
+
try {
|
|
1349
|
+
const raw = await fs.readFile(statePath, 'utf8');
|
|
1350
|
+
state = JSON.parse(raw);
|
|
1351
|
+
} catch (err) {
|
|
1352
|
+
if (err.code === 'ENOENT') {
|
|
1353
|
+
return { advanced: false, skipped: 'no_active_workflow' };
|
|
1354
|
+
}
|
|
1355
|
+
if (!options.json && logger?.error) {
|
|
1356
|
+
logger.error(`[agent:done] workflow.state.json unreadable (${err.code || err.message}); fallback to backward-compat (no auto-advance)`);
|
|
1357
|
+
}
|
|
1358
|
+
return { advanced: false, skipped: 'state_corrupt', error: err.message };
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// 2. Inactive workflow → skip.
|
|
1362
|
+
if (!state || (!state.featureSlug && state.mode !== 'project') || state.current === null) {
|
|
1363
|
+
return { advanced: false, skipped: 'inactive_workflow' };
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// 3. Idempotency guard (BR-01 — 1s window).
|
|
1367
|
+
const now = Date.now();
|
|
1368
|
+
const lastEventAt = Number(state.last_workflow_event_at) || 0;
|
|
1369
|
+
if (now - lastEventAt < 1000) {
|
|
1370
|
+
return { advanced: false, skipped: 'idempotency_window' };
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// 4. Lookup canonical artifact via handoff-contract (DPC-03 — reuse CONTRACTS map).
|
|
1374
|
+
let artifacts;
|
|
1375
|
+
try {
|
|
1376
|
+
const { getCanonicalArtifactsForAgent } = require('../handoff-contract');
|
|
1377
|
+
artifacts = await getCanonicalArtifactsForAgent(normalizedAgent, targetDir, {
|
|
1378
|
+
mode: state.mode || 'feature',
|
|
1379
|
+
featureSlug: state.featureSlug,
|
|
1380
|
+
classification: state.classification
|
|
1381
|
+
});
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
if (!options.json && logger?.error) {
|
|
1384
|
+
logger.error(`[agent:done] handoff-contract lookup failed (${err.message}); skip auto-advance`);
|
|
1385
|
+
}
|
|
1386
|
+
return { advanced: false, skipped: 'contract_error', error: err.message };
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// AC-F2-10 — agent not registered in CONTRACTS.
|
|
1390
|
+
if (artifacts === null) {
|
|
1391
|
+
if (!options.json && logger?.error) {
|
|
1392
|
+
logger.error(`[agent:done] agent '${normalizedAgent}' not in handoff-contract CONTRACTS map; skip auto-advance`);
|
|
1393
|
+
}
|
|
1394
|
+
return { advanced: false, skipped: 'unknown_agent' };
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Empty array — agent legitimately produces no canonical artifact (e.g. @committer, @dev).
|
|
1398
|
+
// Don't auto-advance; the workflow advances on explicit user action when needed.
|
|
1399
|
+
if (artifacts.length === 0) {
|
|
1400
|
+
return { advanced: false, skipped: 'no_canonical_artifact' };
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// 5. At least one declared artifact must exist on disk before we trust auto-advance.
|
|
1404
|
+
let anyExists = false;
|
|
1405
|
+
for (const artifactPath of artifacts) {
|
|
1406
|
+
try {
|
|
1407
|
+
await fs.access(artifactPath);
|
|
1408
|
+
anyExists = true;
|
|
1409
|
+
break;
|
|
1410
|
+
} catch { /* not found — try next */ }
|
|
1411
|
+
}
|
|
1412
|
+
if (!anyExists) {
|
|
1413
|
+
return { advanced: false, skipped: 'artifact_missing' };
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// 6. Internal invocation of runWorkflowNext (lazy require — circular safety).
|
|
1417
|
+
let result;
|
|
1418
|
+
try {
|
|
1419
|
+
const { runWorkflowNext } = require('./workflow-next');
|
|
1420
|
+
result = await runWorkflowNext({
|
|
1421
|
+
args: [targetDir],
|
|
1422
|
+
options: { complete: normalizedAgent.replace(/^@/, ''), json: true },
|
|
1423
|
+
logger: { log: () => {}, error: () => {}, warn: () => {} },
|
|
1424
|
+
t
|
|
1425
|
+
});
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
if (!options.json && logger?.error) {
|
|
1428
|
+
logger.error(`[agent:done] workflow:next failed (${err.message}); pointer unchanged`);
|
|
1429
|
+
}
|
|
1430
|
+
return { advanced: false, skipped: 'workflow_next_failed', error: err.message };
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// 7. Persist last_workflow_event_at for idempotency (best-effort).
|
|
1434
|
+
try {
|
|
1435
|
+
const refreshedRaw = await fs.readFile(statePath, 'utf8').catch(() => null);
|
|
1436
|
+
const refreshed = refreshedRaw ? JSON.parse(refreshedRaw) : state;
|
|
1437
|
+
refreshed.last_workflow_event_at = now;
|
|
1438
|
+
await fs.writeFile(statePath, `${JSON.stringify(refreshed, null, 2)}\n`);
|
|
1439
|
+
} catch { /* non-fatal */ }
|
|
1440
|
+
|
|
1441
|
+
// 8. Surface concise outcome — single line, AFTER existing standard log (AC-F2-02 preserved).
|
|
1442
|
+
if (!options.json && logger?.log && result?.ok) {
|
|
1443
|
+
const nextStage = result.next || result.nextStage || null;
|
|
1444
|
+
const tag = nextStage ? `→ ${nextStage}` : '(workflow complete)';
|
|
1445
|
+
logger.log(`[agent:done] auto-advanced ${tag}`);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
return { advanced: true, result };
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
|
|
1302
1452
|
async function runRuntimeSessionStart({ args, options = {}, logger, t }) {
|
|
1303
1453
|
const targetDir = resolveTargetDir(args);
|
|
1304
1454
|
const { db, dbPath, runtimeDir } = await openRuntimeDb(targetDir);
|
|
@@ -2074,6 +2224,7 @@ module.exports = {
|
|
|
2074
2224
|
runRuntimeLog,
|
|
2075
2225
|
runAgentDone,
|
|
2076
2226
|
runAgentRecover,
|
|
2227
|
+
maybeAutoAdvanceWorkflow,
|
|
2077
2228
|
runRuntimeSessionStart,
|
|
2078
2229
|
runRuntimeSessionLog,
|
|
2079
2230
|
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 };
|
|
@@ -967,6 +967,57 @@ async function activateStage(targetDir, state, locale, tool, explicitAgent = nul
|
|
|
967
967
|
};
|
|
968
968
|
}
|
|
969
969
|
|
|
970
|
+
/**
|
|
971
|
+
* F3 (workflow-handoff-integrity v1.9.6) — pending-decisions guard.
|
|
972
|
+
*
|
|
973
|
+
* Reads `.aioson/plans/{slug}/manifest.md` frontmatter. If `status` matches
|
|
974
|
+
* `pending-<X>-decisions`, throws a hard error recommending the agent that
|
|
975
|
+
* resolves those decisions. `--force` overrides.
|
|
976
|
+
*
|
|
977
|
+
* Whitelist (DD-02): known agents are [architect, product, pm, qa]. Unknown
|
|
978
|
+
* captured groups still block but are flagged as unrecognized so typos don't
|
|
979
|
+
* silently route to nonexistent agents.
|
|
980
|
+
*
|
|
981
|
+
* Errors:
|
|
982
|
+
* - WORKFLOW_NEXT_PENDING_DECISIONS — pending state detected, advance blocked.
|
|
983
|
+
*
|
|
984
|
+
* @param {string} targetDir Project root.
|
|
985
|
+
* @param {string|null} slug Feature slug (null in project mode → no-op).
|
|
986
|
+
* @param {boolean} force When true, skip the check (--force override).
|
|
987
|
+
* @returns {Promise<void>} Resolves silently when no pending decisions block; throws otherwise.
|
|
988
|
+
*/
|
|
989
|
+
const PENDING_STATE_WHITELIST = ['architect', 'product', 'pm', 'qa'];
|
|
990
|
+
|
|
991
|
+
async function assertManifestNotPending(targetDir, slug, force) {
|
|
992
|
+
if (force) return; // AC-F3-03 — explicit override.
|
|
993
|
+
if (!slug) return; // AC-F3-04 — no feature context, nothing to guard.
|
|
994
|
+
const manifestPath = path.join(targetDir, '.aioson', 'plans', slug, 'manifest.md');
|
|
995
|
+
let content;
|
|
996
|
+
try {
|
|
997
|
+
content = await fs.readFile(manifestPath, 'utf8');
|
|
998
|
+
} catch {
|
|
999
|
+
return; // AC-F3-04 — no manifest (e.g. MICRO without Sheldon stage), skip.
|
|
1000
|
+
}
|
|
1001
|
+
const status = parseFrontmatterValue(content, 'status');
|
|
1002
|
+
if (!status) return; // No status field → nothing to assert.
|
|
1003
|
+
const match = String(status).match(/^pending-(.+)-decisions$/);
|
|
1004
|
+
if (!match) return; // AC-F3-02 — only pending-*-decisions pattern blocks.
|
|
1005
|
+
const captured = match[1].toLowerCase();
|
|
1006
|
+
const known = PENDING_STATE_WHITELIST.includes(captured);
|
|
1007
|
+
const recommendation = known
|
|
1008
|
+
? `Próximo agente recomendado: @${captured}.`
|
|
1009
|
+
: `Estado desconhecido '${captured}' — whitelist atual: ${PENDING_STATE_WHITELIST.map((a) => `@${a}`).join(', ')}.`;
|
|
1010
|
+
const err = new Error(
|
|
1011
|
+
`[workflow:next] Gate blocked: ${slug} manifest tem status 'pending-${captured}-decisions'. ${recommendation} Use --force para override.`
|
|
1012
|
+
);
|
|
1013
|
+
err.code = 'WORKFLOW_NEXT_PENDING_DECISIONS';
|
|
1014
|
+
err.slug = slug;
|
|
1015
|
+
err.pendingState = captured;
|
|
1016
|
+
err.knownState = known;
|
|
1017
|
+
throw err;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
|
|
970
1021
|
async function runWorkflowNext({ args, options, logger, t }) {
|
|
971
1022
|
if (options.status || options.suggest) {
|
|
972
1023
|
const { runWorkflowStatus } = require('./workflow-status');
|
|
@@ -988,6 +1039,17 @@ async function runWorkflowNext({ args, options, logger, t }) {
|
|
|
988
1039
|
let completedStage = null;
|
|
989
1040
|
|
|
990
1041
|
if (options.complete || options['complete-current']) {
|
|
1042
|
+
// F3 (workflow-handoff-integrity v1.9.6) — pending-decisions guard.
|
|
1043
|
+
// Hard error if sheldon manifest has unresolved decisions; --force overrides.
|
|
1044
|
+
try {
|
|
1045
|
+
await assertManifestNotPending(targetDir, state.featureSlug, Boolean(options.force));
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
if (err && err.code === 'WORKFLOW_NEXT_PENDING_DECISIONS') {
|
|
1048
|
+
logErrorLine(err.message);
|
|
1049
|
+
}
|
|
1050
|
+
throw err;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
991
1053
|
let finalized;
|
|
992
1054
|
try {
|
|
993
1055
|
finalized = await finalizeCurrentStage(
|
|
@@ -1274,6 +1336,8 @@ module.exports = {
|
|
|
1274
1336
|
applySkip,
|
|
1275
1337
|
activateStage,
|
|
1276
1338
|
runWorkflowNext,
|
|
1339
|
+
assertManifestNotPending,
|
|
1340
|
+
PENDING_STATE_WHITELIST,
|
|
1277
1341
|
shouldRouteToValidator,
|
|
1278
1342
|
detectUnsubstantiatedCompletions
|
|
1279
1343
|
};
|
package/src/handoff-contract.js
CHANGED
|
@@ -405,6 +405,30 @@ async function getBlockingRevisions(targetDir, featureSlug) {
|
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
/**
|
|
409
|
+
* getCanonicalArtifactsForAgent
|
|
410
|
+
*
|
|
411
|
+
* Public lookup helper used by `runAgentDone` (F2 — workflow-handoff-integrity v1.9.5)
|
|
412
|
+
* to determine which artifact paths an agent is expected to produce. Returns the
|
|
413
|
+
* paths declared by the agent's contract in CONTRACTS, fully resolved against the
|
|
414
|
+
* workflow state.
|
|
415
|
+
*
|
|
416
|
+
* @param {string} agent Agent name (with or without leading `@`).
|
|
417
|
+
* @param {string} targetDir Project root path (absolute).
|
|
418
|
+
* @param {object} state Workflow state: { mode, featureSlug, classification }.
|
|
419
|
+
* @returns {string[]|null} Array of absolute artifact paths, or `null` when the
|
|
420
|
+
* agent is not registered in CONTRACTS. An empty array
|
|
421
|
+
* means the agent produces no canonical artifact (e.g.
|
|
422
|
+
* `@committer`, `@dev`) — auto-emit should be skipped.
|
|
423
|
+
*/
|
|
424
|
+
async function getCanonicalArtifactsForAgent(agent, targetDir, state) {
|
|
425
|
+
const normalizedAgent = String(agent || '').replace(/^@/, '').toLowerCase();
|
|
426
|
+
if (!normalizedAgent) return null;
|
|
427
|
+
const contract = CONTRACTS[normalizedAgent];
|
|
428
|
+
if (!contract) return null;
|
|
429
|
+
return await resolveArtifacts(contract, targetDir, state || {});
|
|
430
|
+
}
|
|
431
|
+
|
|
408
432
|
module.exports = {
|
|
409
433
|
parseFrontmatterValue,
|
|
410
434
|
readProjectClassification,
|
|
@@ -413,5 +437,6 @@ module.exports = {
|
|
|
413
437
|
validateHandoffContract,
|
|
414
438
|
formatContractError,
|
|
415
439
|
getBlockingRevisions,
|
|
440
|
+
getCanonicalArtifactsForAgent,
|
|
416
441
|
CONTRACTS
|
|
417
442
|
};
|