@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/README.md +44 -1
  3. package/package.json +1 -1
  4. package/src/cli.js +45 -1
  5. package/src/commands/op-capture.js +146 -0
  6. package/src/commands/op-forget.js +54 -0
  7. package/src/commands/op-identity.js +145 -0
  8. package/src/commands/op-list.js +105 -0
  9. package/src/commands/op-migrate.js +158 -0
  10. package/src/commands/op-promote.js +66 -0
  11. package/src/commands/op-reinforce.js +73 -0
  12. package/src/commands/op-show.js +71 -0
  13. package/src/commands/op-stubs.js +67 -0
  14. package/src/commands/preflight.js +6 -2
  15. package/src/commands/runtime.js +151 -0
  16. package/src/commands/state-save.js +61 -0
  17. package/src/commands/sync-agents-preflight.js +117 -3
  18. package/src/commands/workflow-next.js +64 -0
  19. package/src/handoff-contract.js +25 -0
  20. package/src/lib/agent-semantic-diff.js +199 -0
  21. package/src/operator-memory/conflict.js +202 -0
  22. package/src/operator-memory/decay.js +157 -0
  23. package/src/operator-memory/decision.js +274 -0
  24. package/src/operator-memory/identity.js +109 -0
  25. package/src/operator-memory/index-md.js +170 -0
  26. package/src/operator-memory/loader.js +106 -0
  27. package/src/operator-memory/proposal.js +179 -0
  28. package/src/operator-memory/prune.js +81 -0
  29. package/src/operator-memory/slug.js +90 -0
  30. package/src/operator-memory/storage.js +121 -0
  31. package/src/preflight-engine.js +91 -1
  32. package/template/.aioson/agents/dev.md +1 -1
  33. package/template/.aioson/agents/deyvin.md +3 -3
  34. package/template/.aioson/agents/manifests/pm.manifest.json +2 -1
  35. package/template/.aioson/agents/neo.md +1 -1
  36. package/template/.aioson/agents/orchestrator.md +4 -3
  37. package/template/.aioson/agents/pm.md +58 -6
  38. package/template/.aioson/agents/product.md +1 -1
  39. package/template/.aioson/agents/setup.md +1 -1
  40. package/template/.aioson/docs/deyvin/pair-execution.md +1 -1
  41. package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +2 -2
  42. package/template/.aioson/skills/process/decision-presentation/SKILL.md +9 -0
  43. package/template/AGENTS.md +23 -0
  44. package/template/CLAUDE.md +23 -0
  45. package/template/agents/_shared/memory-capture-directive.md +115 -0
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson op:identity — operator-memory identity resolution (Phase 1, v1.12.0).
5
+ *
6
+ * Subcommands:
7
+ * show (default) — resolve current identity + emit summary
8
+ * set <id> — Phase 1 stub. Full impl in Phase 5; prints guidance to use env override.
9
+ *
10
+ * Usage:
11
+ * aioson op:identity show
12
+ * aioson op:identity show --json
13
+ * aioson op:identity set ci-bot-shared (Phase 1: prints guidance only)
14
+ * AIOSON_OPERATOR_ID=ci-bot aioson op:identity show
15
+ */
16
+
17
+ const {
18
+ resolveIdentity,
19
+ validateOverride,
20
+ OVERRIDE_REGEX
21
+ } = require('../operator-memory/identity');
22
+ const {
23
+ ensureStorageTree,
24
+ openIndexDb,
25
+ recordIdentityActivity,
26
+ getStorageRoot
27
+ } = require('../operator-memory/storage');
28
+ const { emitDossierEvent } = require('../lib/dossier-telemetry');
29
+
30
+ function parseSubcommand(args) {
31
+ // parser.js strips the command name, so args contains positional args only.
32
+ // Filter out a leading '.' (legacy convention from other AIOSON commands that
33
+ // take a project path; op:* commands are machine-local so '.' is ignored).
34
+ const positional = (args || []).filter((a) => typeof a === 'string' && !a.startsWith('-') && a !== '.');
35
+ const sub = positional[0] || 'show';
36
+ const setValue = sub === 'set' ? positional[1] : null;
37
+ return { sub, setValue };
38
+ }
39
+
40
+ async function runOpIdentity({ args = [], options = {}, logger }) {
41
+ const targetDir = process.cwd();
42
+ const { sub, setValue } = parseSubcommand(args);
43
+
44
+ if (sub !== 'show' && sub !== 'set') {
45
+ const err = `op:identity — unknown subcommand '${sub}'. Use: show | set <id>`;
46
+ if (options.json) return { ok: false, error: err };
47
+ if (logger) logger.error(err);
48
+ return { ok: false, error: err, exitCode: 1 };
49
+ }
50
+
51
+ if (sub === 'set') {
52
+ if (!setValue) {
53
+ const err = `op:identity set — missing <id>. Usage: aioson op:identity set <id>`;
54
+ if (options.json) return { ok: false, error: err };
55
+ if (logger) logger.error(err);
56
+ return { ok: false, error: err, exitCode: 1 };
57
+ }
58
+ const validation = validateOverride(setValue);
59
+ if (!validation.ok) {
60
+ const err = `op:identity set — '${setValue}' invalid (${validation.reason}; expected ${OVERRIDE_REGEX}, no reserved prefix _* or aioson-*).`;
61
+ if (options.json) return { ok: false, error: err };
62
+ if (logger) logger.error(err);
63
+ return { ok: false, error: err, exitCode: 1 };
64
+ }
65
+ // Phase 5 full impl: process.env mutation works only within this Node process.
66
+ // For shell-session persistence the user must export AIOSON_OPERATOR_ID. This
67
+ // command initializes the identity storage tree for the new id so subsequent
68
+ // op:* invocations in the same process see a ready identity.
69
+ process.env.AIOSON_OPERATOR_ID = setValue;
70
+ ensureStorageTree(setValue);
71
+ let db;
72
+ try {
73
+ db = openIndexDb();
74
+ recordIdentityActivity(db, { identity: setValue, source: 'override' });
75
+ } finally {
76
+ if (db) { try { db.close(); } catch { /* ignore */ } }
77
+ }
78
+ const msg = `op:identity set — process env AIOSON_OPERATOR_ID=${setValue} (this process only). Persist via shell:\n export AIOSON_OPERATOR_ID=${setValue}\n # or .bashrc / equivalent`;
79
+ if (options.json) {
80
+ return {
81
+ ok: true,
82
+ identity: setValue,
83
+ source: 'override',
84
+ storage_root: getStorageRoot(setValue),
85
+ persistence: 'process_env_only',
86
+ shell_export: `export AIOSON_OPERATOR_ID=${setValue}`
87
+ };
88
+ }
89
+ if (logger) logger.log(msg);
90
+ return { ok: true };
91
+ }
92
+
93
+ // sub === 'show'
94
+ const resolved = resolveIdentity();
95
+ ensureStorageTree(resolved.identity);
96
+
97
+ let db;
98
+ try {
99
+ db = openIndexDb();
100
+ recordIdentityActivity(db, { identity: resolved.identity, source: resolved.source });
101
+ } finally {
102
+ if (db) {
103
+ try { db.close(); } catch { /* swallow */ }
104
+ }
105
+ }
106
+
107
+ if (resolved.source === 'anonymous-fallback') {
108
+ await emitDossierEvent(targetDir, {
109
+ agent: 'op-identity',
110
+ type: 'op_identity_unresolved',
111
+ summary: 'git email unavailable; using _anonymous bucket',
112
+ meta: { identity: resolved.identity, source: resolved.source }
113
+ });
114
+ }
115
+
116
+ const storageRoot = getStorageRoot(resolved.identity);
117
+
118
+ const result = {
119
+ ok: true,
120
+ identity: resolved.identity,
121
+ source: resolved.source,
122
+ storage_root: storageRoot,
123
+ warning: resolved.warning
124
+ };
125
+
126
+ if (options.json) return result;
127
+
128
+ if (resolved.warning && logger && logger.error) {
129
+ logger.error(`⚠ ${resolved.warning}`);
130
+ }
131
+ const sourceLabel = resolved.source === 'override'
132
+ ? '(override via AIOSON_OPERATOR_ID)'
133
+ : resolved.source === 'anonymous-fallback'
134
+ ? '(anonymous fallback — set git config user.email to scope memory)'
135
+ : '(git-email-hash)';
136
+ if (logger) {
137
+ logger.log(`op:identity — ${resolved.identity} ${sourceLabel}`);
138
+ logger.log(`storage_root: ${storageRoot}`);
139
+ }
140
+ return result;
141
+ }
142
+
143
+ module.exports = {
144
+ runOpIdentity
145
+ };
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson op:list — list active decisions (Phase 3, v1.14.0).
5
+ *
6
+ * Output formats:
7
+ * --format=table (default): human-readable columns
8
+ * --format=json: machine-readable JSON
9
+ *
10
+ * Flags:
11
+ * --proposals show pending proposals instead of decisions
12
+ * --include-archived include MEMORY-archive.md entries
13
+ */
14
+
15
+ const fs = require('node:fs');
16
+ const path = require('node:path');
17
+ const { resolveIdentity } = require('../operator-memory/identity');
18
+ const { ensureStorageTree, getStorageRoot } = require('../operator-memory/storage');
19
+ const { loadMemoryIndex } = require('../operator-memory/index-md');
20
+ const { readDecision } = require('../operator-memory/decision');
21
+
22
+ async function runOpList({ args = [], options = {}, logger }) {
23
+ if (options.help === true || args.includes('--help') || args.includes('-h')) {
24
+ if (logger) logger.log('op:list [--proposals] [--include-archived] [--format=table|json] — list active decisions.');
25
+ return { ok: true };
26
+ }
27
+
28
+ const format = options.format || 'table';
29
+ const showProposals = Boolean(options.proposals);
30
+ const includeArchived = Boolean(options['include-archived']);
31
+
32
+ const resolved = resolveIdentity();
33
+ ensureStorageTree(resolved.identity);
34
+ const root = getStorageRoot(resolved.identity);
35
+
36
+ let items = [];
37
+ if (showProposals) {
38
+ const proposalsDir = path.join(root, 'proposals');
39
+ if (fs.existsSync(proposalsDir)) {
40
+ const files = fs.readdirSync(proposalsDir).filter((f) => f.endsWith('.md'));
41
+ const { readProposal } = require('../operator-memory/proposal');
42
+ for (const f of files) {
43
+ const slug = f.slice(0, -3);
44
+ const p = readProposal(resolved.identity, slug);
45
+ if (p) items.push({ slug, ...p });
46
+ }
47
+ }
48
+ } else {
49
+ const active = loadMemoryIndex(resolved.identity, 'active');
50
+ if (active && active.entries) {
51
+ // Enrich with full decision data so --format=json carries category + body summary
52
+ for (const entry of active.entries) {
53
+ const d = readDecision(resolved.identity, entry.slug);
54
+ if (d) items.push({ slug: entry.slug, title: entry.title, ...d });
55
+ }
56
+ }
57
+ if (includeArchived) {
58
+ const archive = loadMemoryIndex(resolved.identity, 'archive');
59
+ if (archive && archive.entries) {
60
+ for (const entry of archive.entries) {
61
+ const d = readDecision(resolved.identity, entry.slug);
62
+ if (d) items.push({ slug: entry.slug, title: entry.title, tier: 'archive', ...d });
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ if (format === 'json' || options.json) {
69
+ const result = {
70
+ ok: true,
71
+ identity: resolved.identity,
72
+ identity_source: resolved.source,
73
+ tier: showProposals ? 'proposals' : (includeArchived ? 'active+archive' : 'active'),
74
+ count: items.length,
75
+ items
76
+ };
77
+ if (options.json) {
78
+ return result;
79
+ }
80
+ if (logger) logger.log(JSON.stringify(result, null, 2));
81
+ return result;
82
+ }
83
+
84
+ if (items.length === 0) {
85
+ const msg = showProposals
86
+ ? `op:list — no pending proposals for identity ${resolved.identity}.`
87
+ : `op:list — no decisions for identity ${resolved.identity}.`;
88
+ if (logger) logger.log(msg);
89
+ return { ok: true, count: 0 };
90
+ }
91
+
92
+ if (logger) {
93
+ logger.log(`op:list — ${items.length} ${showProposals ? 'proposal(s)' : 'decision(s)'} for ${resolved.identity}:`);
94
+ logger.log('');
95
+ for (const item of items) {
96
+ const dateOnly = String(item.last_reinforced || item.last_detected || '').slice(0, 10);
97
+ const category = item.category || (showProposals ? 'proposal' : 'default');
98
+ const tier = item.tier === 'archive' ? '[archive] ' : '';
99
+ logger.log(` ${tier}${item.slug.padEnd(40)} ${item.signal_type.padEnd(14)} ${category.padEnd(10)} ${dateOnly}`);
100
+ }
101
+ }
102
+ return { ok: true, count: items.length };
103
+ }
104
+
105
+ module.exports = { runOpList };
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson op:migrate — explicit one-shot migration from `.aioson/context/user-profile.md`
5
+ * (Phase 5, v1.16.0). PMD-10: deprecation tied to feature.md status, not version.
6
+ *
7
+ * Idempotent: re-runs after first successful migration skip silently (checks
8
+ * deprecated_by frontmatter field on user-profile.md).
9
+ *
10
+ * Field mapping (known 8 dimensions, conservative subset):
11
+ * autonomy_preference → category=identity, signal_type=authorization
12
+ * communication_style → category=identity, signal_type=authorization
13
+ * feedback_density → category=identity, signal_type=authorization
14
+ * correction_tolerance → category=identity, signal_type=correction
15
+ * tool_authorization → category=tooling, signal_type=authorization
16
+ * workflow_strictness → category=autonomy, signal_type=authorization
17
+ * ui_style → category=identity, signal_type=authorization
18
+ * verbosity → category=identity, signal_type=authorization
19
+ *
20
+ * Unknown fields in user-profile.md are preserved (not migrated).
21
+ */
22
+
23
+ const fs = require('node:fs');
24
+ const path = require('node:path');
25
+ const { resolveIdentity } = require('../operator-memory/identity');
26
+ const { ensureStorageTree } = require('../operator-memory/storage');
27
+ const { captureSignal } = require('../operator-memory/proposal');
28
+ const { readDecision, promoteProposal } = require('../operator-memory/decision');
29
+ const { deriveSlug } = require('../operator-memory/slug');
30
+ const { emitDossierEvent } = require('../lib/dossier-telemetry');
31
+
32
+ const KNOWN_FIELDS = {
33
+ autonomy_preference: { category: 'identity', signal_type: 'authorization' },
34
+ communication_style: { category: 'identity', signal_type: 'authorization' },
35
+ feedback_density: { category: 'identity', signal_type: 'authorization' },
36
+ correction_tolerance: { category: 'identity', signal_type: 'correction' },
37
+ tool_authorization: { category: 'tooling', signal_type: 'authorization' },
38
+ workflow_strictness: { category: 'autonomy', signal_type: 'authorization' },
39
+ ui_style: { category: 'identity', signal_type: 'authorization' },
40
+ verbosity: { category: 'identity', signal_type: 'authorization' }
41
+ };
42
+
43
+ function parseUserProfileFrontmatter(content) {
44
+ const m = content.match(/^---\n([\s\S]*?)\n---/);
45
+ if (!m) return null;
46
+ const out = {};
47
+ for (const line of m[1].split('\n')) {
48
+ const fm = line.match(/^([a-z_]+):\s*(.*)$/);
49
+ if (fm) {
50
+ let v = fm[2].trim();
51
+ if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1);
52
+ if (v.startsWith("'") && v.endsWith("'")) v = v.slice(1, -1);
53
+ out[fm[1]] = v;
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+
59
+ function rewriteUserProfileWithDeprecation(filePath, fm) {
60
+ const content = fs.readFileSync(filePath, 'utf8');
61
+ const m = content.match(/^---\n([\s\S]*?)\n---/);
62
+ if (!m) return false;
63
+ if (fm.deprecated_by === 'operator-memory') return false; // already deprecated
64
+ const updatedFm = { ...fm, deprecated_by: 'operator-memory', deprecated_at: new Date().toISOString() };
65
+ const fmLines = Object.entries(updatedFm).map(([k, v]) => `${k}: ${v}`);
66
+ const newContent = `---\n${fmLines.join('\n')}\n---\n${content.slice(m[0].length)}`;
67
+ const tmp = `${filePath}.tmp`;
68
+ fs.writeFileSync(tmp, newContent, 'utf8');
69
+ fs.renameSync(tmp, filePath);
70
+ return true;
71
+ }
72
+
73
+ async function runOpMigrate({ args = [], options = {}, logger }) {
74
+ const targetDir = process.cwd();
75
+ const profilePath = path.join(targetDir, '.aioson', 'context', 'user-profile.md');
76
+
77
+ if (options.help === true || args.includes('--help') || args.includes('-h')) {
78
+ if (logger) logger.log('op:migrate — explicit one-shot import from .aioson/context/user-profile.md into operator-memory. Idempotent.');
79
+ return { ok: true };
80
+ }
81
+
82
+ if (!fs.existsSync(profilePath)) {
83
+ const msg = 'op:migrate — no .aioson/context/user-profile.md to migrate (skipped).';
84
+ if (options.json) return { ok: true, migrated: 0, skipped: 0, reason: 'no_user_profile' };
85
+ if (logger) logger.log(msg);
86
+ return { ok: true, migrated: 0 };
87
+ }
88
+
89
+ const content = fs.readFileSync(profilePath, 'utf8');
90
+ const fm = parseUserProfileFrontmatter(content);
91
+ if (!fm) {
92
+ if (options.json) return { ok: false, error: 'user-profile.md has no parseable frontmatter' };
93
+ if (logger && logger.error) logger.error('op:migrate — user-profile.md has no parseable frontmatter');
94
+ return { ok: false, exitCode: 1 };
95
+ }
96
+
97
+ if (fm.deprecated_by === 'operator-memory') {
98
+ const msg = 'op:migrate — user-profile.md already deprecated (idempotent skip).';
99
+ if (options.json) return { ok: true, migrated: 0, skipped: 0, reason: 'already_deprecated' };
100
+ if (logger) logger.log(msg);
101
+ return { ok: true, migrated: 0, idempotent: true };
102
+ }
103
+
104
+ const resolved = resolveIdentity();
105
+ ensureStorageTree(resolved.identity);
106
+
107
+ let migrated = 0;
108
+ let skipped = 0;
109
+ const results = [];
110
+
111
+ for (const [field, value] of Object.entries(fm)) {
112
+ if (!KNOWN_FIELDS[field]) continue; // unknown field — preserve in user-profile.md
113
+ if (!value || value === 'null' || value === '') { skipped += 1; continue; }
114
+ const config = KNOWN_FIELDS[field];
115
+ const proposalText = `${field}: ${value}`;
116
+ const slug = deriveSlug(proposalText);
117
+
118
+ const existing = readDecision(resolved.identity, slug);
119
+ if (existing) {
120
+ skipped += 1;
121
+ results.push({ field, slug, action: 'skipped_existing' });
122
+ continue;
123
+ }
124
+
125
+ // Capture + immediately promote (one-shot, skip 2x threshold)
126
+ const cap = captureSignal({
127
+ identity: resolved.identity,
128
+ slug,
129
+ signal_type: config.signal_type,
130
+ quote: `(migrated from user-profile.md: ${field})`,
131
+ proposal: proposalText,
132
+ source_agent: 'migrate'
133
+ });
134
+ promoteProposal({ identity: resolved.identity, proposal: { ...cap.proposal, detected_count: 2 } });
135
+ migrated += 1;
136
+ results.push({ field, slug, action: 'migrated', category: config.category });
137
+
138
+ await emitDossierEvent(targetDir, {
139
+ agent: 'op-migrate',
140
+ type: 'op_migrate',
141
+ summary: `migrated ${field} → ${slug}`,
142
+ meta: { identity_prefix: resolved.identity.slice(0, 8), field, slug, category: config.category }
143
+ });
144
+ }
145
+
146
+ rewriteUserProfileWithDeprecation(profilePath, fm);
147
+
148
+ if (options.json) {
149
+ return { ok: true, migrated, skipped, results, identity: resolved.identity };
150
+ }
151
+ if (logger) {
152
+ logger.log(`op:migrate — imported ${migrated} field(s) from user-profile.md (${skipped} skipped).`);
153
+ if (migrated > 0) logger.log('user-profile.md frontmatter marked deprecated_by: operator-memory');
154
+ }
155
+ return { ok: true, migrated, skipped };
156
+ }
157
+
158
+ module.exports = { runOpMigrate, KNOWN_FIELDS };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson op:promote <slug> — manually promote a proposal (skip 2x threshold).
5
+ * Phase 2 (v1.13.0).
6
+ */
7
+
8
+ const { resolveIdentity } = require('../operator-memory/identity');
9
+ const { ensureStorageTree } = require('../operator-memory/storage');
10
+ const { readProposal } = require('../operator-memory/proposal');
11
+ const { promoteProposal } = require('../operator-memory/decision');
12
+ const { emitDossierEvent } = require('../lib/dossier-telemetry');
13
+
14
+ async function runOpPromote({ args = [], options = {}, logger }) {
15
+ const targetDir = process.cwd();
16
+ const positional = (args || []).filter((a) => typeof a === 'string' && !a.startsWith('-') && a !== '.');
17
+ const slug = positional[0];
18
+
19
+ if (options.help === true || args.includes('--help') || args.includes('-h')) {
20
+ if (logger) logger.log('op:promote <slug> — manually promote a pending proposal to a decision.');
21
+ return { ok: true };
22
+ }
23
+
24
+ if (!slug) {
25
+ const err = 'op:promote — required argument: <slug>. Usage: aioson op:promote <slug>';
26
+ if (options.json) return { ok: false, error: err };
27
+ if (logger && logger.error) logger.error(err);
28
+ return { ok: false, exitCode: 1, error: err };
29
+ }
30
+
31
+ const resolved = resolveIdentity();
32
+ ensureStorageTree(resolved.identity);
33
+ const proposal = readProposal(resolved.identity, slug);
34
+ if (!proposal) {
35
+ const err = `op:promote — proposal '${slug}' not found.`;
36
+ if (options.json) return { ok: false, error: err };
37
+ if (logger && logger.error) logger.error(err);
38
+ return { ok: false, exitCode: 1, error: err };
39
+ }
40
+
41
+ let decision;
42
+ try {
43
+ decision = promoteProposal({ identity: resolved.identity, proposal });
44
+ } catch (err) {
45
+ const errMsg = `op:promote failed: ${err.message}`;
46
+ if (options.json) return { ok: false, error: errMsg };
47
+ if (logger && logger.error) logger.error(errMsg);
48
+ return { ok: false, exitCode: 1, error: errMsg };
49
+ }
50
+
51
+ await emitDossierEvent(targetDir, {
52
+ agent: 'op-promote',
53
+ type: 'op_promote',
54
+ summary: `manual promote ${slug}`,
55
+ meta: { identity_prefix: resolved.identity.slice(0, 8), slug, signal_type: decision.signal_type, category: decision.category, mode: 'manual' }
56
+ });
57
+
58
+ const auditLine = `✔ Memory: '${proposal.proposal}'. aioson op:forget ${slug} p/ desfazer.`;
59
+ if (options.json) {
60
+ return { ok: true, slug, identity: resolved.identity, category: decision.category };
61
+ }
62
+ if (logger) logger.log(auditLine);
63
+ return { ok: true, slug };
64
+ }
65
+
66
+ module.exports = { runOpPromote };
@@ -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
+ };