@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
@@ -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
  };
@@ -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
  };
@@ -26,6 +26,15 @@ module.exports = {
26
26
  'aioson context:pack [path] [--agent=<agent>] [--goal=<text>] [--module=<module-or-folder>] [--max-files=8] [--json] [--locale=en]',
27
27
  help_context_load:
28
28
  'aioson context:load [path] --target=<rule|brain>:<slug> --agent=<name> [--batch="slug1,slug2"] [--feature=<slug>] [--classification=<MICRO|SMALL|MEDIUM>] [--verbose] [--json] [--locale=en]',
29
+ help_chain_audit:
30
+ 'aioson chain:audit <file> [path] [--limit=N] [--feature=<slug>] [--json] [--locale=en]',
31
+ chain_audit: {
32
+ file_required: 'chain:audit requires a file path. Usage: aioson chain:audit <file> [--limit=N] [--feature=<slug>] [--json]',
33
+ runtime_unavailable: 'chain:audit runtime db unavailable: {error}',
34
+ query_failed: 'chain:audit failed to query chain_edges: {error}',
35
+ no_impacts: 'chain:audit {file} → no impacts detected ({duration}ms)',
36
+ results_header: 'chain:audit {file} → {count} impact(s) ({duration}ms):'
37
+ },
29
38
  context_load: {
30
39
  target_required: 'context:load requires --target=<rule|brain>:<slug>.',
31
40
  agent_required: 'context:load requires --agent=<name>.',
@@ -27,6 +27,15 @@ module.exports = {
27
27
  'aioson context:pack [path] [--agent=<agente>] [--goal=<texto>] [--module=<modulo-o-carpeta>] [--max-files=8] [--json] [--locale=es]',
28
28
  help_context_load:
29
29
  'aioson context:load [path] --target=<rule|brain>:<slug> --agent=<nombre> [--batch="slug1,slug2"] [--feature=<slug>] [--classification=<MICRO|SMALL|MEDIUM>] [--verbose] [--json] [--locale=es]',
30
+ help_chain_audit:
31
+ 'aioson chain:audit <archivo> [path] [--limit=N] [--feature=<slug>] [--json] [--locale=es]',
32
+ chain_audit: {
33
+ file_required: 'chain:audit requiere una ruta de archivo. Uso: aioson chain:audit <archivo> [--limit=N] [--feature=<slug>] [--json]',
34
+ runtime_unavailable: 'chain:audit runtime db no disponible: {error}',
35
+ query_failed: 'chain:audit falló al consultar chain_edges: {error}',
36
+ no_impacts: 'chain:audit {file} → ningún impacto detectado ({duration}ms)',
37
+ results_header: 'chain:audit {file} → {count} impacto(s) ({duration}ms):'
38
+ },
30
39
  context_load: {
31
40
  target_required: 'context:load requiere --target=<rule|brain>:<slug>.',
32
41
  agent_required: 'context:load requiere --agent=<nombre>.',
@@ -27,6 +27,15 @@ module.exports = {
27
27
  'aioson context:pack [path] [--agent=<agent>] [--goal=<texte>] [--module=<module-ou-dossier>] [--max-files=8] [--json] [--locale=fr]',
28
28
  help_context_load:
29
29
  'aioson context:load [path] --target=<rule|brain>:<slug> --agent=<nom> [--batch="slug1,slug2"] [--feature=<slug>] [--classification=<MICRO|SMALL|MEDIUM>] [--verbose] [--json] [--locale=fr]',
30
+ help_chain_audit:
31
+ 'aioson chain:audit <fichier> [path] [--limit=N] [--feature=<slug>] [--json] [--locale=fr]',
32
+ chain_audit: {
33
+ file_required: 'chain:audit exige un chemin de fichier. Usage : aioson chain:audit <fichier> [--limit=N] [--feature=<slug>] [--json]',
34
+ runtime_unavailable: 'chain:audit runtime db indisponible : {error}',
35
+ query_failed: 'chain:audit échec de la requête chain_edges : {error}',
36
+ no_impacts: 'chain:audit {file} → aucun impact détecté ({duration}ms)',
37
+ results_header: 'chain:audit {file} → {count} impact(s) ({duration}ms) :'
38
+ },
30
39
  context_load: {
31
40
  target_required: 'context:load exige --target=<rule|brain>:<slug>.',
32
41
  agent_required: 'context:load exige --agent=<nom>.',
@@ -27,6 +27,15 @@ module.exports = {
27
27
  'aioson context:pack [path] [--agent=<agente>] [--goal=<texto>] [--module=<modulo-ou-pasta>] [--max-files=8] [--json] [--locale=pt-BR]',
28
28
  help_context_load:
29
29
  'aioson context:load [path] --target=<rule|brain>:<slug> --agent=<nome> [--batch="slug1,slug2"] [--feature=<slug>] [--classification=<MICRO|SMALL|MEDIUM>] [--verbose] [--json] [--locale=pt-BR]',
30
+ help_chain_audit:
31
+ 'aioson chain:audit <arquivo> [path] [--limit=N] [--feature=<slug>] [--json] [--locale=pt-BR]',
32
+ chain_audit: {
33
+ file_required: 'chain:audit exige um caminho de arquivo. Uso: aioson chain:audit <arquivo> [--limit=N] [--feature=<slug>] [--json]',
34
+ runtime_unavailable: 'chain:audit runtime db indisponível: {error}',
35
+ query_failed: 'chain:audit falhou ao consultar chain_edges: {error}',
36
+ no_impacts: 'chain:audit {file} → nenhum impacto detectado ({duration}ms)',
37
+ results_header: 'chain:audit {file} → {count} impacto(s) ({duration}ms):'
38
+ },
30
39
  context_load: {
31
40
  target_required: 'context:load exige --target=<rule|brain>:<slug>.',
32
41
  agent_required: 'context:load exige --agent=<nome>.',
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * agent-semantic-diff — pure helpers for detecting semantic drift between
5
+ * workspace `.aioson/agents/{agent}.md` and template `template/.aioson/agents/{agent}.md`.
6
+ *
7
+ * F4 / T5 (workflow-handoff-integrity v1.9.8). Designed to catch the 981a8fd-style
8
+ * migration where the workspace agent prompt was updated but the template was not.
9
+ * The existing `sync-agents-preflight#checkParity` only inspects the `## Feature dossier`
10
+ * section length — this helper extends that lens to headers, section content, and
11
+ * frontmatter.
12
+ *
13
+ * Three diff strategies (per DD-03):
14
+ * - diffHeaders — section list (presence + order) at `##` and `###` levels
15
+ * - diffSectionContent — hash-based diff of section bodies (catches content drift)
16
+ * - diffFrontmatter — field-level YAML-ish frontmatter comparison
17
+ *
18
+ * Plain text body diff is deliberately skipped — too noisy for typos/cosmetic edits.
19
+ */
20
+
21
+ const crypto = require('node:crypto');
22
+
23
+ // ─── Frontmatter ─────────────────────────────────────────────────────────────
24
+
25
+ function extractFrontmatter(content) {
26
+ const match = String(content || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
27
+ if (!match) return null;
28
+ const out = {};
29
+ for (const line of match[1].split(/\r?\n/)) {
30
+ const idx = line.indexOf(':');
31
+ if (idx === -1) continue;
32
+ const key = line.slice(0, idx).trim();
33
+ if (!key) continue;
34
+ out[key] = line.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
35
+ }
36
+ return out;
37
+ }
38
+
39
+ function diffFrontmatter(workspaceContent, templateContent) {
40
+ const ws = extractFrontmatter(workspaceContent);
41
+ const tpl = extractFrontmatter(templateContent);
42
+ if (ws === null && tpl === null) return null; // both have no frontmatter
43
+ const missingInTemplate = [];
44
+ const missingInWorkspace = [];
45
+ const valueChanged = [];
46
+ const wsObj = ws || {};
47
+ const tplObj = tpl || {};
48
+ for (const key of Object.keys(wsObj)) {
49
+ if (!(key in tplObj)) {
50
+ missingInTemplate.push(key);
51
+ } else if (wsObj[key] !== tplObj[key]) {
52
+ valueChanged.push({ key, workspace: wsObj[key], template: tplObj[key] });
53
+ }
54
+ }
55
+ for (const key of Object.keys(tplObj)) {
56
+ if (!(key in wsObj)) missingInWorkspace.push(key);
57
+ }
58
+ if (missingInTemplate.length + missingInWorkspace.length + valueChanged.length === 0) return null;
59
+ return { missingInTemplate, missingInWorkspace, valueChanged };
60
+ }
61
+
62
+ // ─── Headers ─────────────────────────────────────────────────────────────────
63
+
64
+ /**
65
+ * Extract ## and ### headers in document order.
66
+ * Skips anything inside fenced code blocks.
67
+ */
68
+ function extractHeaders(content) {
69
+ const lines = String(content || '').split(/\r?\n/);
70
+ const headers = [];
71
+ let inFence = false;
72
+ for (const line of lines) {
73
+ if (/^```/.test(line.trim())) {
74
+ inFence = !inFence;
75
+ continue;
76
+ }
77
+ if (inFence) continue;
78
+ const m = line.match(/^(##{1,2})\s+(.+?)\s*$/);
79
+ if (m) headers.push(m[2].trim());
80
+ }
81
+ return headers;
82
+ }
83
+
84
+ function diffHeaders(workspaceContent, templateContent) {
85
+ const ws = extractHeaders(workspaceContent);
86
+ const tpl = extractHeaders(templateContent);
87
+ const wsSet = new Set(ws);
88
+ const tplSet = new Set(tpl);
89
+ const missingInTemplate = ws.filter((h) => !tplSet.has(h));
90
+ const missingInWorkspace = tpl.filter((h) => !wsSet.has(h));
91
+ // Order check: of the headers present in both, do they appear in the same sequence?
92
+ const common = ws.filter((h) => tplSet.has(h));
93
+ const commonInTpl = tpl.filter((h) => wsSet.has(h));
94
+ const reordered = common.length === commonInTpl.length
95
+ && common.some((h, i) => h !== commonInTpl[i]);
96
+ if (missingInTemplate.length + missingInWorkspace.length === 0 && !reordered) return null;
97
+ return { missingInTemplate, missingInWorkspace, reordered };
98
+ }
99
+
100
+ // ─── Section content (hash-based) ────────────────────────────────────────────
101
+
102
+ /**
103
+ * Split content into Map<header, body>. Body is normalized (trimmed lines,
104
+ * collapsed whitespace) before hashing to avoid cosmetic-only false positives.
105
+ */
106
+ function extractSections(content) {
107
+ const lines = String(content || '').split(/\r?\n/);
108
+ const sections = new Map();
109
+ let current = '__preamble__';
110
+ let body = [];
111
+ let inFence = false;
112
+ for (const line of lines) {
113
+ if (/^```/.test(line.trim())) {
114
+ inFence = !inFence;
115
+ body.push(line);
116
+ continue;
117
+ }
118
+ const headerMatch = !inFence && line.match(/^(##{1,2})\s+(.+?)\s*$/);
119
+ if (headerMatch) {
120
+ sections.set(current, body.join('\n'));
121
+ current = headerMatch[2].trim();
122
+ body = [];
123
+ continue;
124
+ }
125
+ body.push(line);
126
+ }
127
+ sections.set(current, body.join('\n'));
128
+ return sections;
129
+ }
130
+
131
+ function normalizeBody(body) {
132
+ return String(body || '')
133
+ .split(/\r?\n/)
134
+ .map((l) => l.replace(/\s+/g, ' ').trim())
135
+ .filter(Boolean)
136
+ .join('\n');
137
+ }
138
+
139
+ function hashBody(body) {
140
+ return crypto.createHash('sha256').update(normalizeBody(body)).digest('hex').slice(0, 16);
141
+ }
142
+
143
+ function diffSectionContent(workspaceContent, templateContent) {
144
+ const ws = extractSections(workspaceContent);
145
+ const tpl = extractSections(templateContent);
146
+ const diverged = [];
147
+ for (const [header, wsBody] of ws.entries()) {
148
+ if (!tpl.has(header)) continue; // missing-header case handled by diffHeaders
149
+ const wsHash = hashBody(wsBody);
150
+ const tplHash = hashBody(tpl.get(header));
151
+ if (wsHash !== tplHash) {
152
+ diverged.push({ header, workspaceHash: wsHash, templateHash: tplHash });
153
+ }
154
+ }
155
+ return diverged.length > 0 ? diverged : null;
156
+ }
157
+
158
+ // ─── Aggregate runner ────────────────────────────────────────────────────────
159
+
160
+ /**
161
+ * Run all three diff strategies on a single agent file pair.
162
+ * Returns null when no drift is detected, otherwise an issue object.
163
+ */
164
+ function diffAgentFile(workspaceContent, templateContent) {
165
+ // AC-T5-08: missing-on-one-side detection.
166
+ if (!workspaceContent && !templateContent) return null;
167
+ if (!workspaceContent || !templateContent) {
168
+ return {
169
+ missingFile: !workspaceContent ? 'workspace' : 'template',
170
+ missingInTemplate: [], missingInWorkspace: [], reordered: false,
171
+ divergedSections: [],
172
+ frontmatter: null
173
+ };
174
+ }
175
+ const headers = diffHeaders(workspaceContent, templateContent);
176
+ const sections = diffSectionContent(workspaceContent, templateContent);
177
+ const frontmatter = diffFrontmatter(workspaceContent, templateContent);
178
+ if (!headers && !sections && !frontmatter) return null;
179
+ return {
180
+ missingFile: null,
181
+ missingInTemplate: headers?.missingInTemplate || [],
182
+ missingInWorkspace: headers?.missingInWorkspace || [],
183
+ reordered: headers?.reordered || false,
184
+ divergedSections: sections || [],
185
+ frontmatter: frontmatter || null
186
+ };
187
+ }
188
+
189
+ module.exports = {
190
+ extractFrontmatter,
191
+ extractHeaders,
192
+ extractSections,
193
+ diffFrontmatter,
194
+ diffHeaders,
195
+ diffSectionContent,
196
+ diffAgentFile,
197
+ hashBody,
198
+ normalizeBody
199
+ };