@jaimevalasek/aioson 1.20.0 → 1.21.3

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 (34) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/docs/pt/living-memory/reflexao-in-harness.md +2 -0
  3. package/package.json +1 -1
  4. package/src/cli.js +5 -0
  5. package/src/commands/context-health.js +35 -2
  6. package/src/commands/feature-close.js +36 -0
  7. package/src/commands/install.js +5 -0
  8. package/src/commands/memory-archive.js +193 -193
  9. package/src/commands/memory-reflect-commit.js +28 -4
  10. package/src/commands/memory-restore.js +177 -177
  11. package/src/commands/memory-search.js +135 -135
  12. package/src/commands/memory-trim.js +191 -0
  13. package/src/constants.js +1 -0
  14. package/src/current-state-trim.js +170 -0
  15. package/src/doctor.js +17 -0
  16. package/src/i18n/messages/en.js +16 -0
  17. package/src/i18n/messages/es.js +16 -0
  18. package/src/i18n/messages/fr.js +16 -0
  19. package/src/i18n/messages/pt-BR.js +16 -0
  20. package/src/install-wizard.js +3 -2
  21. package/src/lib/tool-capabilities.js +67 -64
  22. package/src/memory-reflect-engine.js +10 -4
  23. package/src/permissions-generator.js +3 -0
  24. package/template/.aioson/agents/architect.md +3 -0
  25. package/template/.aioson/agents/committer.md +1 -1
  26. package/template/.aioson/agents/dev.md +2 -2
  27. package/template/.aioson/agents/deyvin.md +2 -1
  28. package/template/.aioson/agents/pentester.md +1 -0
  29. package/template/.aioson/agents/qa.md +4 -0
  30. package/template/.aioson/agents/sheldon.md +1 -0
  31. package/template/.aioson/agents/tester.md +2 -0
  32. package/template/.aioson/config/autonomy-protocol.json +1 -0
  33. package/template/.aioson/design-docs/agent-loading-contract.md +138 -0
  34. package/template/.aioson/docs/quality/code-health-analysis.md +79 -0
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // aioson memory:reflect-commit [.] --agent=<name> [--output=<path-to-json>] [--json]
3
+ // aioson memory:reflect-commit [.] --agent=<name> [--output=<path-to-json>] [--json] [--dry-run]
4
4
  //
5
5
  // Reads the reflect manifest written by memory:reflect-prepare, accepts the
6
6
  // agent's reflected output (as a JSON map of relative path → new content),
@@ -63,13 +63,14 @@ async function emitEvent(targetDir, agent, type, message, payload) {
63
63
  async function runMemoryReflectCommit({ args, options = {}, logger }) {
64
64
  const targetDir = path.resolve(process.cwd(), args?.[0] || '.');
65
65
  const agent = String(options.agent || '').trim() || 'unknown';
66
+ const dryRun = Boolean(options['dry-run'] || options.dryRun);
66
67
 
67
68
  const manifestPath = path.join(targetDir, REFLECT_PROMPT_RELATIVE);
68
69
  let manifest;
69
70
  try {
70
71
  manifest = await readJsonFile(manifestPath);
71
72
  } catch {
72
- const message = `manifest not found at ${path.relative(targetDir, manifestPath)} — run memory:reflect-prepare first`;
73
+ const message = `manifest not found at ${path.relative(targetDir, manifestPath)} — it may have been consumed by a previous successful reflect-commit; run memory:reflect-prepare to generate a new one`;
73
74
  if (!options.json) logger.log(`✗ ${message}`);
74
75
  return { ok: false, error: 'missing_manifest', message };
75
76
  }
@@ -102,9 +103,11 @@ async function runMemoryReflectCommit({ args, options = {}, logger }) {
102
103
  // verify that every resolved absolute path stays under the project's
103
104
  // bootstrap directory. validate() already rejects absolute paths and
104
105
  // `..` segments, but this is the second wall in case the manifest's
105
- // allowed_paths is ever extended beyond bootstrap/.
106
+ // allowed_paths is ever extended beyond bootstrap/. Run this for BOTH
107
+ // dry-run and real commits so a dry-run validates exactly what a real
108
+ // commit would do.
106
109
  const bootstrapRoot = path.resolve(targetDir, '.aioson/context/bootstrap');
107
- const written = [];
110
+ const planned = [];
108
111
  for (const [relPath, content] of Object.entries(files)) {
109
112
  const absPath = path.resolve(targetDir, relPath);
110
113
  if (!absPath.startsWith(bootstrapRoot + path.sep) && absPath !== bootstrapRoot) {
@@ -116,6 +119,27 @@ async function runMemoryReflectCommit({ args, options = {}, logger }) {
116
119
  });
117
120
  return { ok: false, error: 'path_escape', message: msg };
118
121
  }
122
+ planned.push({ relPath, absPath, content });
123
+ }
124
+
125
+ // --dry-run: validation + path containment already passed. Report what WOULD
126
+ // be written and stop — no disk writes, and crucially the manifest is NOT
127
+ // consumed, so a real commit can still follow. A dry-run must never mutate
128
+ // state (this is the bug fix: previously --dry-run was ignored and ran the
129
+ // full destructive commit).
130
+ if (dryRun) {
131
+ const wouldWrite = planned.map((p) => p.relPath);
132
+ if (!options.json) {
133
+ logger.log('• reflect-commit DRY RUN — no files written, manifest preserved');
134
+ for (const w of wouldWrite) logger.log(` - would write: ${w}`);
135
+ }
136
+ await emitEvent(targetDir, agent, 'memory_reflect_dry_run',
137
+ `dry-run validated ${wouldWrite.length} file(s)`, { would_write: wouldWrite });
138
+ return { ok: true, dryRun: true, would_write: wouldWrite };
139
+ }
140
+
141
+ const written = [];
142
+ for (const { relPath, absPath, content } of planned) {
119
143
  // SF-project-22: scrub zero-width / bidi / HTML-comment injection carriers
120
144
  // from the LLM-authored content before it lands in bootstrap. Path
121
145
  // containment is already enforced above; this is the content layer of the
@@ -1,177 +1,177 @@
1
- 'use strict';
2
-
3
- /**
4
- * aioson memory:restore --id=<rule|learning|brain>:<slug> [--reason="<text>"] [--dry-run] [--json]
5
- *
6
- * Tier-2 human-actioned command. Moves an archived artifact back to its
7
- * active path and records an `event_type='restored'` entry with a fresh
8
- * `start_at` (PMD-10). History is preserved: the prior `archived` entry
9
- * keeps its `end_at`.
10
- *
11
- * Refuses to run when `process.env.AIOSON_RUNTIME_HOOK === '1'` (BR-ALL-01).
12
- * Emits `aioson notify --level=warn` BEFORE mutation (BR-ALL-06).
13
- */
14
-
15
- const path = require('node:path');
16
- const { openRuntimeDb } = require('../runtime-store');
17
- const { runNotify } = require('./notify');
18
- const {
19
- parseTargetId,
20
- normalizeKind,
21
- restoreTarget,
22
- TARGET_TYPES
23
- } = require('../learning-loop-archive');
24
-
25
- function tFn(t, key, params) {
26
- if (typeof t === 'function') {
27
- try { return t(key, params || {}); } catch { /* fall through */ }
28
- }
29
- return null;
30
- }
31
-
32
- function isHookContext() {
33
- return process.env.AIOSON_RUNTIME_HOOK === '1';
34
- }
35
-
36
- async function runMemoryRestore({ args, options = {}, logger, t }) {
37
- const targetDir = path.resolve(process.cwd(), args && args[0] ? args[0] : '.');
38
- const wantJson = Boolean(options.json);
39
- const dryRun = Boolean(options['dry-run'] || options.dryRun);
40
- const log = (msg) => { if (logger && typeof logger.log === 'function') logger.log(msg); };
41
-
42
- if (isHookContext()) {
43
- const msg = tFn(t, 'memory_restore.hook_blocked')
44
- || 'memory:restore cannot be invoked from a runtime hook (BR-ALL-01: tier-2 requires human action).';
45
- if (wantJson) return { ok: false, reason: 'hook_blocked' };
46
- log(msg);
47
- return { ok: false, reason: 'hook_blocked' };
48
- }
49
-
50
- const rawId = options.id || options.target || '';
51
- const reason = options.reason ? String(options.reason).trim() : '';
52
-
53
- if (!rawId) {
54
- const msg = tFn(t, 'memory_restore.id_required')
55
- || 'memory:restore requires --id=<rule|learning|brain>:<slug>.';
56
- if (wantJson) return { ok: false, reason: 'missing_id' };
57
- log(msg);
58
- return { ok: false, reason: 'missing_id' };
59
- }
60
-
61
- const parsed = parseTargetId(rawId);
62
- const kind = normalizeKind(parsed.kind);
63
- if (!kind || !TARGET_TYPES.has(kind) || !parsed.slug) {
64
- const msg = tFn(t, 'memory_restore.invalid_id', { value: rawId })
65
- || `memory:restore invalid --id value: "${rawId}". Expected rule|learning|brain:<slug>.`;
66
- if (wantJson) return { ok: false, reason: 'invalid_id', value: rawId };
67
- log(msg);
68
- return { ok: false, reason: 'invalid_id' };
69
- }
70
-
71
- const notifyMessage = tFn(t, 'memory_restore.notify_template', {
72
- kind,
73
- slug: parsed.slug,
74
- reason: reason || 'restoring archived artifact'
75
- }) || `restoring ${kind} "${parsed.slug}"${reason ? `: ${reason}` : ''}`;
76
- let notifyResult;
77
- try {
78
- notifyResult = await runNotify({
79
- args: [targetDir],
80
- options: {
81
- level: 'warn',
82
- topic: 'memory',
83
- message: notifyMessage,
84
- agent: 'memory-restore',
85
- json: wantJson ? true : undefined
86
- },
87
- logger: logger || { log: () => {} }
88
- });
89
- } catch (err) {
90
- if (wantJson) return { ok: false, reason: 'notify_failed', error: String(err && err.message || err) };
91
- log(`memory:restore notify failed: ${err && err.message ? err.message : err}`);
92
- return { ok: false, reason: 'notify_failed' };
93
- }
94
- if (notifyResult && notifyResult.ok === false) {
95
- if (wantJson) return { ok: false, reason: 'notify_blocked', exitCode: notifyResult.exitCode };
96
- log('memory:restore aborted: tier-2 notify returned non-zero exit code.');
97
- return { ok: false, reason: 'notify_blocked' };
98
- }
99
-
100
- let dbHandle;
101
- try {
102
- dbHandle = await openRuntimeDb(targetDir);
103
- } catch (err) {
104
- if (wantJson) return { ok: false, reason: 'runtime_db_unavailable', error: String(err && err.message || err) };
105
- log(`memory:restore runtime db unavailable: ${err && err.message ? err.message : err}`);
106
- return { ok: false, reason: 'runtime_db_unavailable' };
107
- }
108
-
109
- const { db } = dbHandle;
110
- let outcome;
111
- try {
112
- outcome = restoreTarget(db, {
113
- targetDir,
114
- kind,
115
- slug: parsed.slug,
116
- reason: reason || null,
117
- actor: 'human',
118
- featureSlug: options.feature ? String(options.feature).trim() : null,
119
- dryRun
120
- });
121
- } finally {
122
- db.close();
123
- }
124
-
125
- if (!outcome.ok) {
126
- if (wantJson) return outcome;
127
- if (outcome.reason === 'target_not_archived') {
128
- const msg = tFn(t, 'memory_restore.target_not_archived', { kind, slug: parsed.slug })
129
- || `memory:restore: ${kind} "${parsed.slug}" not found in archive.`;
130
- log(msg);
131
- } else if (outcome.reason === 'target_already_active') {
132
- const msg = tFn(t, 'memory_restore.target_already_active', { kind, slug: parsed.slug })
133
- || `memory:restore: ${kind} "${parsed.slug}" already active. No-op.`;
134
- log(msg);
135
- } else if (outcome.reason === 'target_not_found') {
136
- const msg = tFn(t, 'memory_restore.target_not_found', { kind, slug: parsed.slug })
137
- || `memory:restore: ${kind} "${parsed.slug}" not found.`;
138
- log(msg);
139
- } else {
140
- log(`memory:restore failed: ${outcome.reason}${outcome.error ? ' — ' + outcome.error : ''}`);
141
- }
142
- return outcome;
143
- }
144
-
145
- if (wantJson) {
146
- return {
147
- ok: true,
148
- dry_run: Boolean(outcome.dryRun),
149
- kind,
150
- slug: parsed.slug,
151
- source_path: outcome.sourcePath,
152
- dest_path: outcome.destPath,
153
- restored_entry_id: outcome.restoredEntryId || null,
154
- start_at: outcome.startAt || null
155
- };
156
- }
157
-
158
- if (outcome.dryRun) {
159
- const msg = tFn(t, 'memory_restore.dry_run_summary', {
160
- kind,
161
- slug: parsed.slug,
162
- source: outcome.sourcePath || kind,
163
- dest: outcome.destPath || '(no dest)'
164
- }) || `memory:restore [dry-run]: would move ${outcome.sourcePath || kind} → ${outcome.destPath || '(no dest)'}.`;
165
- log(msg);
166
- } else {
167
- const msg = tFn(t, 'memory_restore.restored_success', {
168
- kind,
169
- slug: parsed.slug,
170
- dest: outcome.destPath || ''
171
- }) || `memory:restore ✓ ${kind} "${parsed.slug}" restored${outcome.destPath ? ' to ' + outcome.destPath : ''}.`;
172
- log(msg);
173
- }
174
- return outcome;
175
- }
176
-
177
- module.exports = { runMemoryRestore };
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson memory:restore --id=<rule|learning|brain>:<slug> [--reason="<text>"] [--dry-run] [--json]
5
+ *
6
+ * Tier-2 human-actioned command. Moves an archived artifact back to its
7
+ * active path and records an `event_type='restored'` entry with a fresh
8
+ * `start_at` (PMD-10). History is preserved: the prior `archived` entry
9
+ * keeps its `end_at`.
10
+ *
11
+ * Refuses to run when `process.env.AIOSON_RUNTIME_HOOK === '1'` (BR-ALL-01).
12
+ * Emits `aioson notify --level=warn` BEFORE mutation (BR-ALL-06).
13
+ */
14
+
15
+ const path = require('node:path');
16
+ const { openRuntimeDb } = require('../runtime-store');
17
+ const { runNotify } = require('./notify');
18
+ const {
19
+ parseTargetId,
20
+ normalizeKind,
21
+ restoreTarget,
22
+ TARGET_TYPES
23
+ } = require('../learning-loop-archive');
24
+
25
+ function tFn(t, key, params) {
26
+ if (typeof t === 'function') {
27
+ try { return t(key, params || {}); } catch { /* fall through */ }
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function isHookContext() {
33
+ return process.env.AIOSON_RUNTIME_HOOK === '1';
34
+ }
35
+
36
+ async function runMemoryRestore({ args, options = {}, logger, t }) {
37
+ const targetDir = path.resolve(process.cwd(), args && args[0] ? args[0] : '.');
38
+ const wantJson = Boolean(options.json);
39
+ const dryRun = Boolean(options['dry-run'] || options.dryRun);
40
+ const log = (msg) => { if (logger && typeof logger.log === 'function') logger.log(msg); };
41
+
42
+ if (isHookContext()) {
43
+ const msg = tFn(t, 'cli.memory_restore.hook_blocked')
44
+ || 'memory:restore cannot be invoked from a runtime hook (BR-ALL-01: tier-2 requires human action).';
45
+ if (wantJson) return { ok: false, reason: 'hook_blocked' };
46
+ log(msg);
47
+ return { ok: false, reason: 'hook_blocked' };
48
+ }
49
+
50
+ const rawId = options.id || options.target || '';
51
+ const reason = options.reason ? String(options.reason).trim() : '';
52
+
53
+ if (!rawId) {
54
+ const msg = tFn(t, 'cli.memory_restore.id_required')
55
+ || 'memory:restore requires --id=<rule|learning|brain>:<slug>.';
56
+ if (wantJson) return { ok: false, reason: 'missing_id' };
57
+ log(msg);
58
+ return { ok: false, reason: 'missing_id' };
59
+ }
60
+
61
+ const parsed = parseTargetId(rawId);
62
+ const kind = normalizeKind(parsed.kind);
63
+ if (!kind || !TARGET_TYPES.has(kind) || !parsed.slug) {
64
+ const msg = tFn(t, 'cli.memory_restore.invalid_id', { value: rawId })
65
+ || `memory:restore invalid --id value: "${rawId}". Expected rule|learning|brain:<slug>.`;
66
+ if (wantJson) return { ok: false, reason: 'invalid_id', value: rawId };
67
+ log(msg);
68
+ return { ok: false, reason: 'invalid_id' };
69
+ }
70
+
71
+ const notifyMessage = tFn(t, 'cli.memory_restore.notify_template', {
72
+ kind,
73
+ slug: parsed.slug,
74
+ reason: reason || 'restoring archived artifact'
75
+ }) || `restoring ${kind} "${parsed.slug}"${reason ? `: ${reason}` : ''}`;
76
+ let notifyResult;
77
+ try {
78
+ notifyResult = await runNotify({
79
+ args: [targetDir],
80
+ options: {
81
+ level: 'warn',
82
+ topic: 'memory',
83
+ message: notifyMessage,
84
+ agent: 'memory-restore',
85
+ json: wantJson ? true : undefined
86
+ },
87
+ logger: logger || { log: () => {} }
88
+ });
89
+ } catch (err) {
90
+ if (wantJson) return { ok: false, reason: 'notify_failed', error: String(err && err.message || err) };
91
+ log(`memory:restore notify failed: ${err && err.message ? err.message : err}`);
92
+ return { ok: false, reason: 'notify_failed' };
93
+ }
94
+ if (notifyResult && notifyResult.ok === false) {
95
+ if (wantJson) return { ok: false, reason: 'notify_blocked', exitCode: notifyResult.exitCode };
96
+ log('memory:restore aborted: tier-2 notify returned non-zero exit code.');
97
+ return { ok: false, reason: 'notify_blocked' };
98
+ }
99
+
100
+ let dbHandle;
101
+ try {
102
+ dbHandle = await openRuntimeDb(targetDir);
103
+ } catch (err) {
104
+ if (wantJson) return { ok: false, reason: 'runtime_db_unavailable', error: String(err && err.message || err) };
105
+ log(`memory:restore runtime db unavailable: ${err && err.message ? err.message : err}`);
106
+ return { ok: false, reason: 'runtime_db_unavailable' };
107
+ }
108
+
109
+ const { db } = dbHandle;
110
+ let outcome;
111
+ try {
112
+ outcome = restoreTarget(db, {
113
+ targetDir,
114
+ kind,
115
+ slug: parsed.slug,
116
+ reason: reason || null,
117
+ actor: 'human',
118
+ featureSlug: options.feature ? String(options.feature).trim() : null,
119
+ dryRun
120
+ });
121
+ } finally {
122
+ db.close();
123
+ }
124
+
125
+ if (!outcome.ok) {
126
+ if (wantJson) return outcome;
127
+ if (outcome.reason === 'target_not_archived') {
128
+ const msg = tFn(t, 'cli.memory_restore.target_not_archived', { kind, slug: parsed.slug })
129
+ || `memory:restore: ${kind} "${parsed.slug}" not found in archive.`;
130
+ log(msg);
131
+ } else if (outcome.reason === 'target_already_active') {
132
+ const msg = tFn(t, 'cli.memory_restore.target_already_active', { kind, slug: parsed.slug })
133
+ || `memory:restore: ${kind} "${parsed.slug}" already active. No-op.`;
134
+ log(msg);
135
+ } else if (outcome.reason === 'target_not_found') {
136
+ const msg = tFn(t, 'cli.memory_restore.target_not_found', { kind, slug: parsed.slug })
137
+ || `memory:restore: ${kind} "${parsed.slug}" not found.`;
138
+ log(msg);
139
+ } else {
140
+ log(`memory:restore failed: ${outcome.reason}${outcome.error ? ' — ' + outcome.error : ''}`);
141
+ }
142
+ return outcome;
143
+ }
144
+
145
+ if (wantJson) {
146
+ return {
147
+ ok: true,
148
+ dry_run: Boolean(outcome.dryRun),
149
+ kind,
150
+ slug: parsed.slug,
151
+ source_path: outcome.sourcePath,
152
+ dest_path: outcome.destPath,
153
+ restored_entry_id: outcome.restoredEntryId || null,
154
+ start_at: outcome.startAt || null
155
+ };
156
+ }
157
+
158
+ if (outcome.dryRun) {
159
+ const msg = tFn(t, 'cli.memory_restore.dry_run_summary', {
160
+ kind,
161
+ slug: parsed.slug,
162
+ source: outcome.sourcePath || kind,
163
+ dest: outcome.destPath || '(no dest)'
164
+ }) || `memory:restore [dry-run]: would move ${outcome.sourcePath || kind} → ${outcome.destPath || '(no dest)'}.`;
165
+ log(msg);
166
+ } else {
167
+ const msg = tFn(t, 'cli.memory_restore.restored_success', {
168
+ kind,
169
+ slug: parsed.slug,
170
+ dest: outcome.destPath || ''
171
+ }) || `memory:restore ✓ ${kind} "${parsed.slug}" restored${outcome.destPath ? ' to ' + outcome.destPath : ''}.`;
172
+ log(msg);
173
+ }
174
+ return outcome;
175
+ }
176
+
177
+ module.exports = { runMemoryRestore };