@jaimevalasek/aioson 1.9.3 → 1.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +237 -0
- package/README.md +44 -1
- package/package.json +1 -1
- package/src/cli.js +50 -1
- package/src/commands/chain-audit.js +156 -0
- package/src/commands/op-capture.js +146 -0
- package/src/commands/op-forget.js +54 -0
- package/src/commands/op-identity.js +145 -0
- package/src/commands/op-list.js +105 -0
- package/src/commands/op-migrate.js +158 -0
- package/src/commands/op-promote.js +66 -0
- package/src/commands/op-reinforce.js +73 -0
- package/src/commands/op-show.js +71 -0
- package/src/commands/op-stubs.js +67 -0
- package/src/commands/preflight.js +6 -2
- package/src/commands/runtime.js +178 -0
- package/src/commands/state-save.js +61 -0
- package/src/commands/sync-agents-preflight.js +117 -3
- package/src/commands/workflow-next.js +64 -0
- package/src/handoff-contract.js +25 -0
- package/src/i18n/messages/en.js +9 -0
- package/src/i18n/messages/es.js +9 -0
- package/src/i18n/messages/fr.js +9 -0
- package/src/i18n/messages/pt-BR.js +9 -0
- package/src/lib/agent-semantic-diff.js +199 -0
- package/src/neural-chain-agent-ingest.js +400 -0
- package/src/neural-chain-config.js +95 -0
- package/src/neural-chain-git-ingest.js +280 -0
- package/src/neural-chain-migration.js +61 -0
- package/src/neural-chain-noise-file.js +332 -0
- package/src/neural-chain-sanitize.js +0 -0
- package/src/neural-chain-telemetry.js +90 -0
- package/src/operator-memory/conflict.js +202 -0
- package/src/operator-memory/decay.js +157 -0
- package/src/operator-memory/decision.js +274 -0
- package/src/operator-memory/identity.js +109 -0
- package/src/operator-memory/index-md.js +170 -0
- package/src/operator-memory/loader.js +106 -0
- package/src/operator-memory/proposal.js +179 -0
- package/src/operator-memory/prune.js +81 -0
- package/src/operator-memory/slug.js +90 -0
- package/src/operator-memory/storage.js +121 -0
- package/src/preflight-engine.js +91 -1
- package/src/runtime-store.js +2 -0
- package/template/.aioson/agents/dev.md +1 -1
- package/template/.aioson/agents/deyvin.md +3 -3
- package/template/.aioson/agents/neo.md +23 -1
- package/template/.aioson/agents/product.md +1 -1
- package/template/.aioson/agents/setup.md +1 -1
- package/template/.aioson/docs/deyvin/pair-execution.md +1 -1
- package/template/.aioson/skills/process/decision-presentation/SKILL.md +9 -0
- package/template/AGENTS.md +23 -0
- package/template/CLAUDE.md +23 -0
- package/template/agents/_shared/memory-capture-directive.md +115 -0
|
@@ -0,0 +1,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 };
|