@jaimevalasek/aioson 1.9.3 → 1.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +237 -0
  2. package/README.md +44 -1
  3. package/package.json +1 -1
  4. package/src/cli.js +50 -1
  5. package/src/commands/chain-audit.js +156 -0
  6. package/src/commands/op-capture.js +146 -0
  7. package/src/commands/op-forget.js +54 -0
  8. package/src/commands/op-identity.js +145 -0
  9. package/src/commands/op-list.js +105 -0
  10. package/src/commands/op-migrate.js +158 -0
  11. package/src/commands/op-promote.js +66 -0
  12. package/src/commands/op-reinforce.js +73 -0
  13. package/src/commands/op-show.js +71 -0
  14. package/src/commands/op-stubs.js +67 -0
  15. package/src/commands/preflight.js +6 -2
  16. package/src/commands/runtime.js +178 -0
  17. package/src/commands/state-save.js +61 -0
  18. package/src/commands/sync-agents-preflight.js +117 -3
  19. package/src/commands/workflow-next.js +64 -0
  20. package/src/handoff-contract.js +25 -0
  21. package/src/i18n/messages/en.js +9 -0
  22. package/src/i18n/messages/es.js +9 -0
  23. package/src/i18n/messages/fr.js +9 -0
  24. package/src/i18n/messages/pt-BR.js +9 -0
  25. package/src/lib/agent-semantic-diff.js +199 -0
  26. package/src/neural-chain-agent-ingest.js +400 -0
  27. package/src/neural-chain-config.js +95 -0
  28. package/src/neural-chain-git-ingest.js +280 -0
  29. package/src/neural-chain-migration.js +61 -0
  30. package/src/neural-chain-noise-file.js +332 -0
  31. package/src/neural-chain-sanitize.js +0 -0
  32. package/src/neural-chain-telemetry.js +90 -0
  33. package/src/operator-memory/conflict.js +202 -0
  34. package/src/operator-memory/decay.js +157 -0
  35. package/src/operator-memory/decision.js +274 -0
  36. package/src/operator-memory/identity.js +109 -0
  37. package/src/operator-memory/index-md.js +170 -0
  38. package/src/operator-memory/loader.js +106 -0
  39. package/src/operator-memory/proposal.js +179 -0
  40. package/src/operator-memory/prune.js +81 -0
  41. package/src/operator-memory/slug.js +90 -0
  42. package/src/operator-memory/storage.js +121 -0
  43. package/src/preflight-engine.js +91 -1
  44. package/src/runtime-store.js +2 -0
  45. package/template/.aioson/agents/dev.md +1 -1
  46. package/template/.aioson/agents/deyvin.md +3 -3
  47. package/template/.aioson/agents/neo.md +23 -1
  48. package/template/.aioson/agents/product.md +1 -1
  49. package/template/.aioson/agents/setup.md +1 -1
  50. package/template/.aioson/docs/deyvin/pair-execution.md +1 -1
  51. package/template/.aioson/skills/process/decision-presentation/SKILL.md +9 -0
  52. package/template/AGENTS.md +23 -0
  53. package/template/CLAUDE.md +23 -0
  54. package/template/agents/_shared/memory-capture-directive.md +115 -0
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson op:capture — record a standing-decision signal (Phase 2, v1.13.0).
5
+ *
6
+ * Usage:
7
+ * aioson op:capture --signal=authorization --quote="..." --proposal="..." --source-agent=dev
8
+ *
9
+ * Flow (architecture-operator-memory.md § Phase 2 capture pipeline):
10
+ * 1. Validate --signal in {authorization, exclusion, correction, confirmation}
11
+ * 2. Resolve identity (Phase 1)
12
+ * 3. deriveSlug(--proposal) — deterministic kebab + truncate
13
+ * 4. Check proposals/{slug}.md:
14
+ * - absent: write proposal with detected_count=1, exit 0 silent
15
+ * - present: detected_count++
16
+ * - count >= 2: promoteProposal (atomic) + stdout audit line
17
+ * - count < 2: update proposal, exit 0 silent
18
+ * 5. Telemetry op_capture before file write
19
+ */
20
+
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+ const { resolveIdentity } = require('../operator-memory/identity');
24
+ const { ensureStorageTree, recordIdentityActivity, openIndexDb, getStorageRoot } = require('../operator-memory/storage');
25
+ const { deriveSlug, fingerprintProposal } = require('../operator-memory/slug');
26
+ const { captureSignal, readProposal, VALID_SIGNAL_TYPES } = require('../operator-memory/proposal');
27
+ const { promoteProposal } = require('../operator-memory/decision');
28
+ const { emitDossierEvent } = require('../lib/dossier-telemetry');
29
+
30
+ const PROMOTION_THRESHOLD = 2;
31
+
32
+ function existsCheckFactory(identity) {
33
+ return (slug) => {
34
+ const existing = readProposal(identity, slug);
35
+ if (!existing) return null;
36
+ return existing.proposal_fingerprint || fingerprintProposal(existing.proposal || '');
37
+ };
38
+ }
39
+
40
+ async function runOpCapture({ args = [], options = {}, logger }) {
41
+ const targetDir = process.cwd();
42
+ const helpRequested = options.help === true || args.includes('--help') || args.includes('-h');
43
+ if (helpRequested) {
44
+ const msg = `op:capture — capture a standing-decision signal into the proposals queue.
45
+ Usage:
46
+ aioson op:capture --signal=<type> --quote=<verbatim> --proposal=<paraphrase> --source-agent=<agent>
47
+ Signal types: ${VALID_SIGNAL_TYPES.join(', ')}
48
+ First detection writes to proposals/{slug}.md. Second detection promotes to decisions/{slug}.md atomically.`;
49
+ if (options.json) return { ok: true, help: true };
50
+ if (logger) logger.log(msg);
51
+ return { ok: true };
52
+ }
53
+
54
+ const signal = options.signal;
55
+ const quote = options.quote;
56
+ const proposal = options.proposal;
57
+ const sourceAgent = options['source-agent'] || options.sourceAgent || 'unknown';
58
+
59
+ if (!signal || !proposal) {
60
+ const err = `op:capture — required: --signal=<type> --proposal=<paraphrase>. Got signal=${signal}, proposal=${proposal ? 'present' : 'missing'}.`;
61
+ if (options.json) return { ok: false, error: err };
62
+ if (logger && logger.error) logger.error(err);
63
+ return { ok: false, exitCode: 1, error: err };
64
+ }
65
+ if (!VALID_SIGNAL_TYPES.includes(signal)) {
66
+ const err = `op:capture — invalid --signal='${signal}'. Must be one of: ${VALID_SIGNAL_TYPES.join(', ')}.`;
67
+ if (options.json) return { ok: false, error: err };
68
+ if (logger && logger.error) logger.error(err);
69
+ return { ok: false, exitCode: 1, error: err };
70
+ }
71
+
72
+ const resolved = resolveIdentity();
73
+ ensureStorageTree(resolved.identity);
74
+ const db = openIndexDb();
75
+ try {
76
+ recordIdentityActivity(db, { identity: resolved.identity, source: resolved.source });
77
+ } finally {
78
+ db.close();
79
+ }
80
+
81
+ const slug = deriveSlug(proposal, existsCheckFactory(resolved.identity));
82
+
83
+ await emitDossierEvent(targetDir, {
84
+ agent: 'op-capture',
85
+ type: 'op_capture',
86
+ summary: `${signal}: ${slug}`,
87
+ meta: { identity_prefix: resolved.identity.slice(0, 8), signal_type: signal, slug, source_agent: sourceAgent }
88
+ });
89
+
90
+ let result;
91
+ try {
92
+ result = captureSignal({
93
+ identity: resolved.identity,
94
+ slug,
95
+ signal_type: signal,
96
+ quote,
97
+ proposal,
98
+ source_agent: sourceAgent
99
+ });
100
+ } catch (err) {
101
+ const errMsg = `op:capture failed: ${err.message}`;
102
+ if (options.json) return { ok: false, error: errMsg };
103
+ if (logger && logger.error) logger.error(errMsg);
104
+ return { ok: false, exitCode: 1, error: errMsg };
105
+ }
106
+
107
+ const count = result.proposal.detected_count;
108
+
109
+ if (count >= PROMOTION_THRESHOLD) {
110
+ // Promote to decision
111
+ let decision;
112
+ try {
113
+ decision = promoteProposal({ identity: resolved.identity, proposal: result.proposal });
114
+ } catch (err) {
115
+ const errMsg = `op:capture promotion failed: ${err.message}`;
116
+ if (options.json) return { ok: false, error: errMsg };
117
+ if (logger && logger.error) logger.error(errMsg);
118
+ return { ok: false, exitCode: 1, error: errMsg };
119
+ }
120
+
121
+ await emitDossierEvent(targetDir, {
122
+ agent: 'op-capture',
123
+ type: 'op_promote',
124
+ summary: `promoted ${slug} (${signal})`,
125
+ meta: { identity_prefix: resolved.identity.slice(0, 8), slug, signal_type: signal, category: decision.category }
126
+ });
127
+
128
+ const auditLine = `✔ Memory: '${proposal}'. aioson op:forget ${slug} p/ desfazer.`;
129
+ if (options.json) {
130
+ return { ok: true, promoted: true, slug, identity: resolved.identity, category: decision.category };
131
+ }
132
+ if (logger) logger.log(auditLine);
133
+ return { ok: true, promoted: true, slug };
134
+ }
135
+
136
+ // First detection (or below threshold) — silent
137
+ if (options.json) {
138
+ return { ok: true, promoted: false, slug, detected_count: count, identity: resolved.identity };
139
+ }
140
+ return { ok: true, promoted: false, slug, detected_count: count };
141
+ }
142
+
143
+ module.exports = {
144
+ runOpCapture,
145
+ PROMOTION_THRESHOLD
146
+ };
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson op:forget <slug> — soft-delete a decision or proposal to history/.
5
+ * Phase 2 (v1.13.0).
6
+ */
7
+
8
+ const path = require('node:path');
9
+ const { resolveIdentity } = require('../operator-memory/identity');
10
+ const { ensureStorageTree } = require('../operator-memory/storage');
11
+ const { forgetEntry } = require('../operator-memory/decision');
12
+ const { emitDossierEvent } = require('../lib/dossier-telemetry');
13
+
14
+ async function runOpForget({ 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:forget <slug> — soft-delete a decision or proposal to history/. Idempotent.');
21
+ return { ok: true };
22
+ }
23
+
24
+ if (!slug) {
25
+ const err = 'op:forget — required argument: <slug>. Usage: aioson op:forget <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 result = forgetEntry(resolved.identity, slug);
34
+
35
+ await emitDossierEvent(targetDir, {
36
+ agent: 'op-forget',
37
+ type: 'op_forget',
38
+ summary: `${result.mode}: ${slug}`,
39
+ meta: { identity_prefix: resolved.identity.slice(0, 8), slug, mode: result.mode }
40
+ });
41
+
42
+ if (options.json) {
43
+ return { ok: true, mode: result.mode, archived: result.archivedPath };
44
+ }
45
+ if (result.mode === 'noop') {
46
+ if (logger) logger.log(`op:forget — '${slug}' not found (idempotent no-op).`);
47
+ } else {
48
+ const archivedRel = result.archivedPath ? path.basename(result.archivedPath) : null;
49
+ if (logger) logger.log(`op:forget — '${slug}' archived as history/${archivedRel} (${result.mode}).`);
50
+ }
51
+ return { ok: true, mode: result.mode };
52
+ }
53
+
54
+ module.exports = { runOpForget };
@@ -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 };