aiden-runtime 4.8.1 → 4.9.1

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 (100) hide show
  1. package/README.md +88 -1
  2. package/dist/cli/v4/aidenCLI.js +37 -6
  3. package/dist/cli/v4/chatSession.js +53 -13
  4. package/dist/cli/v4/commands/daemon.js +53 -3
  5. package/dist/cli/v4/commands/daemonDoctor.js +212 -0
  6. package/dist/cli/v4/commands/daemonStatus.js +45 -26
  7. package/dist/cli/v4/commands/help.js +5 -0
  8. package/dist/cli/v4/commands/hooks.js +466 -0
  9. package/dist/cli/v4/commands/hooksSlash.js +33 -0
  10. package/dist/cli/v4/commands/index.js +13 -1
  11. package/dist/cli/v4/commands/mcp.js +89 -1
  12. package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
  13. package/dist/cli/v4/commands/memory.js +707 -0
  14. package/dist/cli/v4/commands/memorySlash.js +38 -0
  15. package/dist/cli/v4/commands/recovery.js +1 -1
  16. package/dist/cli/v4/commands/skin.js +7 -0
  17. package/dist/cli/v4/commands/theme.js +217 -0
  18. package/dist/cli/v4/commands/trigger.js +1 -1
  19. package/dist/cli/v4/design/tokens.js +52 -4
  20. package/dist/cli/v4/display.js +39 -26
  21. package/dist/cli/v4/replyRenderer.js +6 -5
  22. package/dist/cli/v4/skinEngine.js +67 -0
  23. package/dist/cli/v4/ui/progressBar.js +179 -0
  24. package/dist/cli/v4/util/closestAction.js +48 -0
  25. package/dist/core/v4/aidenAgent.js +45 -2
  26. package/dist/core/v4/daemon/api/runs.js +131 -0
  27. package/dist/core/v4/daemon/bootstrap.js +368 -13
  28. package/dist/core/v4/daemon/db/migrations.js +169 -0
  29. package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
  30. package/dist/core/v4/daemon/incarnationStore.js +47 -0
  31. package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
  32. package/dist/core/v4/daemon/runs/reclaim.js +88 -0
  33. package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
  34. package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
  35. package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
  36. package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
  37. package/dist/core/v4/daemon/spans/spanStore.js +113 -0
  38. package/dist/core/v4/daemon/triggerBus.js +50 -19
  39. package/dist/core/v4/hooks/auditQuery.js +67 -0
  40. package/dist/core/v4/hooks/dispatcher.js +286 -0
  41. package/dist/core/v4/hooks/index.js +46 -0
  42. package/dist/core/v4/hooks/lifecycle.js +27 -0
  43. package/dist/core/v4/hooks/manifest.js +142 -0
  44. package/dist/core/v4/hooks/registry.js +149 -0
  45. package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
  46. package/dist/core/v4/hooks/toolHookGate.js +76 -0
  47. package/dist/core/v4/hooks/trust.js +14 -0
  48. package/dist/core/v4/identity/contextManager.js +83 -0
  49. package/dist/core/v4/identity/daemonId.js +85 -0
  50. package/dist/core/v4/identity/enforcement.js +103 -0
  51. package/dist/core/v4/identity/executionContext.js +153 -0
  52. package/dist/core/v4/identity/hookExecution.js +62 -0
  53. package/dist/core/v4/identity/httpContext.js +68 -0
  54. package/dist/core/v4/identity/ids.js +185 -0
  55. package/dist/core/v4/identity/index.js +60 -0
  56. package/dist/core/v4/identity/subprocessContext.js +98 -0
  57. package/dist/core/v4/identity/traceparent.js +114 -0
  58. package/dist/core/v4/logger/index.js +3 -1
  59. package/dist/core/v4/logger/logger.js +28 -1
  60. package/dist/core/v4/logger/redact.js +149 -0
  61. package/dist/core/v4/logger/sinks/fileSink.js +13 -0
  62. package/dist/core/v4/logger/sinks/stdSink.js +19 -1
  63. package/dist/core/v4/mcp/install/backup.js +78 -0
  64. package/dist/core/v4/mcp/install/clientPaths.js +90 -0
  65. package/dist/core/v4/mcp/install/clients.js +203 -0
  66. package/dist/core/v4/mcp/install/healthCheck.js +83 -0
  67. package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
  68. package/dist/core/v4/mcp/install/profiles.js +109 -0
  69. package/dist/core/v4/mcp/install/wslDetect.js +62 -0
  70. package/dist/core/v4/memory/namespaceRegistry.js +117 -0
  71. package/dist/core/v4/memory/projectRoot.js +76 -0
  72. package/dist/core/v4/memory/reviewer/index.js +162 -0
  73. package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
  74. package/dist/core/v4/memory/reviewer/prompt.js +105 -0
  75. package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
  76. package/dist/core/v4/memoryManager.js +57 -10
  77. package/dist/core/v4/paths.js +2 -0
  78. package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
  79. package/dist/core/v4/theme/bundledThemes.js +106 -0
  80. package/dist/core/v4/theme/themeLoader.js +160 -0
  81. package/dist/core/v4/theme/themeRegistry.js +97 -0
  82. package/dist/core/v4/theme/themeWatcher.js +95 -0
  83. package/dist/core/v4/toolRegistry.js +71 -8
  84. package/dist/core/v4/update/depWarningFilter.js +76 -0
  85. package/dist/core/v4/update/executeInstall.js +41 -35
  86. package/dist/core/v4/update/platformInstructions.js +128 -0
  87. package/dist/moat/approvalEngine.js +4 -0
  88. package/dist/moat/memoryGuard.js +8 -1
  89. package/dist/providers/v4/anthropicAdapter.js +10 -4
  90. package/dist/tools/v4/backends/local.js +19 -2
  91. package/dist/tools/v4/sessions/recallSession.js +6 -1
  92. package/package.json +3 -1
  93. package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
  94. package/themes/default.yaml +52 -0
  95. package/themes/dracula.yaml +32 -0
  96. package/themes/light.yaml +32 -0
  97. package/themes/monochrome.yaml +31 -0
  98. package/themes/tokyo-night.yaml +32 -0
  99. package/dist/core/pluginSystem.js +0 -121
  100. package/dist/tools/v4/ui/_uiSmokeTool.js +0 -60
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/memory/namespaceRegistry.ts — v4.9.0 Slice 11.
10
+ *
11
+ * Generalizes Aiden's two-file memory model (`memory` + `user`) to a
12
+ * registry. Adds a third default namespace — `project` — keyed off
13
+ * the caller's current working directory. The registry is the single
14
+ * authority every other surface (tool schemas, CLI parser, prompt
15
+ * injector, reviewer, backup) consults; adding a namespace is one
16
+ * registration call instead of an N-place patch.
17
+ *
18
+ * Slice 11 ships THREE default namespaces:
19
+ *
20
+ * memory — Aiden environment / project & technical notes. 2200 chars
21
+ * user — User identity / preferences / workflow style. 1375 chars
22
+ * project — Per-project context at `<projectRoot>/.aiden/PROJECT.md`. 1800 chars
23
+ *
24
+ * The `'memory'` vs `'project'` distinction:
25
+ * - `memory` is GLOBAL per-install (Aiden-wide notes).
26
+ * - `project` is PER-DIRECTORY (one PROJECT.md per repo / workdir).
27
+ *
28
+ * Char limit for `project` (1800) sits between `user` (1375) and the
29
+ * legacy `memory` (2200) — project context is verbose but not as
30
+ * verbose as full environment notes.
31
+ */
32
+ var __importDefault = (this && this.__importDefault) || function (mod) {
33
+ return (mod && mod.__esModule) ? mod : { "default": mod };
34
+ };
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getNamespace = getNamespace;
37
+ exports.hasNamespace = hasNamespace;
38
+ exports.listNamespaces = listNamespaces;
39
+ exports.listNamespaceNames = listNamespaceNames;
40
+ exports.registerNamespace = registerNamespace;
41
+ exports._resetNamespacesForTests = _resetNamespacesForTests;
42
+ const node_path_1 = __importDefault(require("node:path"));
43
+ const BUILTIN = [
44
+ {
45
+ name: 'memory',
46
+ label: 'Memory (project & environment)',
47
+ description: 'Global notes about Aiden\'s environment + this project.',
48
+ charLimit: 2200,
49
+ injectIntoPrompt: true,
50
+ promptHeader: 'Project & Environment',
51
+ resolve: (paths) => paths.memoryMd,
52
+ },
53
+ {
54
+ name: 'user',
55
+ label: 'User (identity & preferences)',
56
+ description: 'User identity, preferences, workflow style.',
57
+ charLimit: 1375,
58
+ injectIntoPrompt: true,
59
+ promptHeader: 'User Identity & Preferences',
60
+ resolve: (paths) => paths.userMd,
61
+ },
62
+ {
63
+ name: 'project',
64
+ label: 'Project (per-repo context)',
65
+ description: 'Per-project context at <projectRoot>/.aiden/PROJECT.md.',
66
+ charLimit: 1800,
67
+ injectIntoPrompt: true,
68
+ promptHeader: 'Current Project Context',
69
+ requiresProject: true,
70
+ resolve: (_paths, projectRoot) => {
71
+ if (!projectRoot) {
72
+ throw new Error('project namespace requires a project root (run from inside a git repo or directory with .aiden/PROJECT.md)');
73
+ }
74
+ return node_path_1.default.join(projectRoot, '.aiden', 'PROJECT.md');
75
+ },
76
+ },
77
+ ];
78
+ const REGISTRY = new Map(BUILTIN.map((n) => [n.name, n]));
79
+ /** Throw-on-unknown getter. Callers MUST check `has()` first if the
80
+ * name is user-supplied — `getNamespace` is for trusted callsites. */
81
+ function getNamespace(name) {
82
+ const ns = REGISTRY.get(name);
83
+ if (!ns) {
84
+ const known = Array.from(REGISTRY.keys()).join(', ');
85
+ throw new Error(`unknown memory namespace '${name}' (known: ${known})`);
86
+ }
87
+ return ns;
88
+ }
89
+ /** Soft check — does this name resolve to a registered namespace. */
90
+ function hasNamespace(name) {
91
+ return REGISTRY.has(name);
92
+ }
93
+ /** Iteration order matches registration order. */
94
+ function listNamespaces() {
95
+ return Array.from(REGISTRY.values());
96
+ }
97
+ /** Names only — for tool schema enums + CLI argument validation. */
98
+ function listNamespaceNames() {
99
+ return Array.from(REGISTRY.keys());
100
+ }
101
+ /**
102
+ * Reserved for v4.10+ user-defined namespaces (plugins). Not wired to
103
+ * config.yaml or plugin loaders in this slice; the entry point exists
104
+ * so the runtime can extend without further refactor.
105
+ */
106
+ function registerNamespace(ns) {
107
+ if (REGISTRY.has(ns.name)) {
108
+ throw new Error(`memory namespace '${ns.name}' is already registered`);
109
+ }
110
+ REGISTRY.set(ns.name, ns);
111
+ }
112
+ /** Test seam — restore registry to the three built-ins. */
113
+ function _resetNamespacesForTests() {
114
+ REGISTRY.clear();
115
+ for (const ns of BUILTIN)
116
+ REGISTRY.set(ns.name, ns);
117
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/memory/projectRoot.ts — v4.9.0 Slice 11.
10
+ *
11
+ * Walk-up project-root detection. Returns the first ancestor of
12
+ * `cwd` containing any of the standard project anchors:
13
+ *
14
+ * .git/ most repos
15
+ * package.json Node
16
+ * pyproject.toml Python
17
+ * Cargo.toml Rust
18
+ * go.mod Go
19
+ * .aiden/PROJECT.md explicit Aiden-managed project
20
+ *
21
+ * Returns `null` when the walk reaches the filesystem root without
22
+ * finding any anchor. Result is cached per-process keyed on the
23
+ * normalised cwd; the cache is cleared via `_resetProjectRootCacheForTests`.
24
+ */
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.findProjectRoot = findProjectRoot;
30
+ exports._resetProjectRootCacheForTests = _resetProjectRootCacheForTests;
31
+ const node_fs_1 = __importDefault(require("node:fs"));
32
+ const node_path_1 = __importDefault(require("node:path"));
33
+ const ANCHORS = [
34
+ '.git',
35
+ 'package.json',
36
+ 'pyproject.toml',
37
+ 'Cargo.toml',
38
+ 'go.mod',
39
+ node_path_1.default.join('.aiden', 'PROJECT.md'),
40
+ ];
41
+ const _cache = new Map();
42
+ /**
43
+ * Walk up from `cwd` looking for an anchor. Returns the absolute path
44
+ * of the directory holding the first anchor, or `null` when nothing
45
+ * is found before filesystem root. Cached per `cwd`.
46
+ */
47
+ function findProjectRoot(cwd = process.cwd()) {
48
+ const startedAt = node_path_1.default.resolve(cwd);
49
+ if (_cache.has(startedAt))
50
+ return _cache.get(startedAt);
51
+ let current = startedAt;
52
+ // Cap the walk at 64 levels — pathological symlink loops shouldn't burn.
53
+ for (let i = 0; i < 64; i += 1) {
54
+ for (const anchor of ANCHORS) {
55
+ const probe = node_path_1.default.join(current, anchor);
56
+ try {
57
+ const stat = node_fs_1.default.statSync(probe);
58
+ if (stat.isFile() || stat.isDirectory()) {
59
+ _cache.set(startedAt, current);
60
+ return current;
61
+ }
62
+ }
63
+ catch { /* ENOENT — keep looking */ }
64
+ }
65
+ const parent = node_path_1.default.dirname(current);
66
+ if (parent === current)
67
+ break; // reached filesystem root
68
+ current = parent;
69
+ }
70
+ _cache.set(startedAt, null);
71
+ return null;
72
+ }
73
+ /** Test seam — clear the per-process cache. */
74
+ function _resetProjectRootCacheForTests() {
75
+ _cache.clear();
76
+ }
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/memory/reviewer/index.ts — v4.9.0 Slice 10.
10
+ *
11
+ * Orchestrator for the post-turn memory reviewer. Pure functions +
12
+ * one entry point `runReview(opts)`. The LLM call is injected as a
13
+ * callback so the reviewer is testable + provider-agnostic — the CLI
14
+ * wires the callback to whatever provider the user has configured.
15
+ *
16
+ * Fail-open guarantees (project rule): any error in the reviewer
17
+ * (LLM timeout, parse error, file write error) is caught + logged
18
+ * but NEVER propagated to the user. `runReview` returns a structured
19
+ * outcome envelope; the CLI surfaces it humanely.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.runReview = runReview;
23
+ exports.stripPendingSections = stripPendingSections;
24
+ const prompt_1 = require("./prompt");
25
+ const skipRules_1 = require("./skipRules");
26
+ const pendingStore_1 = require("./pendingStore");
27
+ const memoryManager_1 = require("../../memoryManager");
28
+ const namespaceRegistry_1 = require("../namespaceRegistry");
29
+ /**
30
+ * Run one review pass. Always resolves (never throws). Use the
31
+ * returned outcome envelope to surface humanely.
32
+ */
33
+ async function runReview(opts) {
34
+ const start = Date.now();
35
+ const log = opts.log ?? (() => { });
36
+ try {
37
+ // Build prompt. Strip pending sections from the "live" snapshots so the
38
+ // reviewer doesn't see its own past proposals as duplicates.
39
+ const liveMemory = stripPendingSections(opts.liveMemoryRaw);
40
+ const liveUser = stripPendingSections(opts.liveUserRaw);
41
+ const prompt = (0, prompt_1.buildReviewerPrompt)({
42
+ recentTurns: opts.recentTurns,
43
+ liveMemory,
44
+ liveUser,
45
+ maxCandidates: opts.maxCandidates,
46
+ });
47
+ // LLM call inside a timeout race.
48
+ const raw = await raceTimeout(opts.callLLM(prompt), opts.timeoutMs);
49
+ if (raw === TIMEOUT_SENTINEL) {
50
+ log('warn', '[memory-reviewer] timeout — no candidates produced');
51
+ return { outcome: 'timeout', durationMs: Date.now() - start };
52
+ }
53
+ // Parse + skip-rule validate.
54
+ const { candidates: parsed, parserDrops } = (0, prompt_1.parseReviewerResponse)(raw);
55
+ const liveMemoryEntries = splitLiveEntries(liveMemory);
56
+ const liveUserEntries = splitLiveEntries(liveUser);
57
+ const dropsByClass = {
58
+ sensitive_class: 0, negation: 0, transient: 0, duplicate: 0, char_cap: 0,
59
+ no_project_root: 0, parser: parserDrops,
60
+ };
61
+ // v4.9.0 Slice 11 — per-namespace kept buckets so `project`
62
+ // (and future namespaces) flow through the same path as memory/user.
63
+ const keptByNamespace = new Map();
64
+ for (const c of parsed) {
65
+ let total = 0;
66
+ for (const arr of keptByNamespace.values())
67
+ total += arr.length;
68
+ if (total >= opts.maxCandidates)
69
+ break;
70
+ // Skip-rule: `requiresProject` namespace + no detected root → drop.
71
+ try {
72
+ const ns = (0, namespaceRegistry_1.getNamespace)(c.file);
73
+ if (ns.requiresProject && !opts.projectRoot) {
74
+ dropsByClass.no_project_root += 1;
75
+ log('info', `[memory-reviewer] skipped (no_project_root): "${c.text.slice(0, 60)}"`);
76
+ continue;
77
+ }
78
+ }
79
+ catch { /* unknown namespace — parser already dropped these but defensive */ }
80
+ const live = c.file === 'user' ? liveUserEntries : liveMemoryEntries;
81
+ const decision = (0, skipRules_1.evaluateCandidate)(c.text, live);
82
+ if (decision.drop && decision.klass) {
83
+ dropsByClass[decision.klass] += 1;
84
+ log('info', `[memory-reviewer] skipped (${decision.klass}): "${c.text.slice(0, 60)}"`);
85
+ continue;
86
+ }
87
+ const bucket = keptByNamespace.get(c.file) ?? [];
88
+ bucket.push({ text: c.text, rationale: c.rationale });
89
+ keptByNamespace.set(c.file, bucket);
90
+ }
91
+ // Append into pending sections — one call per namespace.
92
+ const candidatesProposed = [];
93
+ for (const [nsName, kept] of keptByNamespace) {
94
+ let targetPath;
95
+ if (nsName === 'memory')
96
+ targetPath = opts.memoryPath;
97
+ else if (nsName === 'user')
98
+ targetPath = opts.userPath;
99
+ else if (opts.paths) {
100
+ try {
101
+ targetPath = (0, namespaceRegistry_1.getNamespace)(nsName).resolve(opts.paths, opts.projectRoot ?? null);
102
+ }
103
+ catch {
104
+ continue; /* skip — already counted in no_project_root */
105
+ }
106
+ }
107
+ else {
108
+ continue;
109
+ }
110
+ const stamped = await (0, pendingStore_1.appendCandidates)(targetPath, nsName, kept);
111
+ candidatesProposed.push(...stamped);
112
+ }
113
+ log('info', `[memory-reviewer] review complete: proposed=${candidatesProposed.length} ` +
114
+ `parser_drops=${parserDrops} ` +
115
+ `rule_drops=${Object.entries(dropsByClass).filter(([k]) => k !== 'parser').map(([k, v]) => `${k}:${v}`).join(' ')}`);
116
+ return {
117
+ outcome: 'ok',
118
+ candidatesProposed,
119
+ dropsByClass,
120
+ llmCharsIn: prompt.length,
121
+ llmCharsOut: raw.length,
122
+ durationMs: Date.now() - start,
123
+ };
124
+ }
125
+ catch (e) {
126
+ const error = e instanceof Error ? e.message : String(e);
127
+ log('error', `[memory-reviewer] error (fail-open): ${error}`);
128
+ return { outcome: 'error', error, durationMs: Date.now() - start };
129
+ }
130
+ }
131
+ const TIMEOUT_SENTINEL = Symbol('memoryReviewerTimeout');
132
+ async function raceTimeout(p, ms) {
133
+ let timer = null;
134
+ const timeout = new Promise((resolve) => {
135
+ timer = setTimeout(() => resolve(TIMEOUT_SENTINEL), ms);
136
+ });
137
+ try {
138
+ const result = await Promise.race([p, timeout]);
139
+ if (timer)
140
+ clearTimeout(timer);
141
+ return result;
142
+ }
143
+ catch (e) {
144
+ if (timer)
145
+ clearTimeout(timer);
146
+ throw e;
147
+ }
148
+ }
149
+ /**
150
+ * Strip `## Pending review` blocks from a raw file so the reviewer
151
+ * doesn't see its own prior candidates as "live entries".
152
+ */
153
+ function stripPendingSections(raw) {
154
+ if (!raw.includes('## Pending review'))
155
+ return raw;
156
+ return raw.replace(/\n*§?\n*## Pending review[\s\S]*?(?=(?:\n§\n)|$)/g, '').trimEnd();
157
+ }
158
+ function splitLiveEntries(raw) {
159
+ if (!raw.trim())
160
+ return [];
161
+ return raw.split(memoryManager_1.ENTRY_SEPARATOR).map((e) => e.trim()).filter((e) => e.length > 0);
162
+ }
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/memory/reviewer/pendingStore.ts — v4.9.0 Slice 10.
10
+ *
11
+ * Read + append the `## Pending review` markdown section on MEMORY.md /
12
+ * USER.md. The section is delimited by the entry separator (`\n§\n`)
13
+ * before the markdown header, so live entries remain unaffected by
14
+ * pending candidates. Format:
15
+ *
16
+ * <live entries, separated by \n§\n>
17
+ * §
18
+ * ## Pending review (2026-05-22T12:00:00Z)
19
+ * - [ ] mem_<uuidv7> | <text> | <rationale>
20
+ *
21
+ * `parsePending` extracts the candidate list; `appendCandidates`
22
+ * appends one ## Pending review block per call (idempotent on no-op).
23
+ */
24
+ var __importDefault = (this && this.__importDefault) || function (mod) {
25
+ return (mod && mod.__esModule) ? mod : { "default": mod };
26
+ };
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.appendCandidates = appendCandidates;
29
+ exports.listPending = listPending;
30
+ exports.dropCandidate = dropCandidate;
31
+ exports.listAllPending = listAllPending;
32
+ const node_fs_1 = require("node:fs");
33
+ const node_path_1 = __importDefault(require("node:path"));
34
+ const identity_1 = require("../../identity");
35
+ const memoryManager_1 = require("../../memoryManager");
36
+ const PENDING_HEADER_RE = /^## Pending review/m;
37
+ const CANDIDATE_LINE_RE = /^- \[ \] (mem_[0-9a-f]{32})\s*\|\s*([^|]+?)\s*\|\s*(.+)$/;
38
+ /** Read the file (or empty string) without throwing on ENOENT. */
39
+ async function readOrEmpty(path) {
40
+ try {
41
+ return await node_fs_1.promises.readFile(path, 'utf8');
42
+ }
43
+ catch (e) {
44
+ if (e.code === 'ENOENT')
45
+ return '';
46
+ throw e;
47
+ }
48
+ }
49
+ /**
50
+ * Append a batch of candidates to the file as a fresh `## Pending review`
51
+ * block (markdown, after a separator). Atomic write via tmp + rename.
52
+ * Returns the list of memIds that were stamped onto the candidates.
53
+ */
54
+ async function appendCandidates(filePath, file, inputs, proposedAtIso = new Date().toISOString()) {
55
+ if (inputs.length === 0)
56
+ return [];
57
+ const stamped = inputs.map((c) => ({
58
+ memId: (0, identity_1.newMemoryId)(),
59
+ file,
60
+ text: c.text.trim(),
61
+ rationale: c.rationale.trim(),
62
+ proposedAt: proposedAtIso,
63
+ }));
64
+ const block = [
65
+ memoryManager_1.ENTRY_SEPARATOR,
66
+ `## Pending review (${proposedAtIso})`,
67
+ ...stamped.map((c) => `- [ ] ${c.memId} | ${c.text} | ${c.rationale}`),
68
+ '',
69
+ ].join('\n');
70
+ const existing = await readOrEmpty(filePath);
71
+ const next = existing.endsWith('\n') || existing.length === 0
72
+ ? existing + block
73
+ : existing + '\n' + block;
74
+ // Atomic write: ensure parent dir exists (first-run case where
75
+ // memories/ hasn't been created yet), then tmp + rename.
76
+ await node_fs_1.promises.mkdir(node_path_1.default.dirname(filePath), { recursive: true });
77
+ const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
78
+ await node_fs_1.promises.writeFile(tmp, next, 'utf8');
79
+ await node_fs_1.promises.rename(tmp, filePath);
80
+ return stamped;
81
+ }
82
+ /** Parse the `## Pending review` candidates out of a file. */
83
+ async function listPending(filePath) {
84
+ const raw = await readOrEmpty(filePath);
85
+ if (!PENDING_HEADER_RE.test(raw))
86
+ return [];
87
+ // Default file shape: pending lives at end of file. Walk every line
88
+ // checking the candidate line shape — the proposedAt is associated
89
+ // with the most-recent `## Pending review (...)` header above each
90
+ // run of candidate lines.
91
+ const out = [];
92
+ let currentTs = new Date().toISOString();
93
+ for (const line of raw.split(/\r?\n/)) {
94
+ const hMatch = /^## Pending review \(([^)]+)\)\s*$/.exec(line);
95
+ if (hMatch) {
96
+ currentTs = hMatch[1];
97
+ continue;
98
+ }
99
+ const cMatch = CANDIDATE_LINE_RE.exec(line);
100
+ if (cMatch) {
101
+ out.push({
102
+ memId: cMatch[1],
103
+ // Caller knows which file this came from; we set 'memory' as a
104
+ // placeholder so the type checker stays happy — the CLI overrides
105
+ // this from the caller-side file context.
106
+ file: 'memory',
107
+ text: cMatch[2].trim(),
108
+ rationale: cMatch[3].trim(),
109
+ proposedAt: currentTs,
110
+ });
111
+ }
112
+ }
113
+ return out;
114
+ }
115
+ /** Drop ONE candidate by memId from the file's pending blocks. Atomic. */
116
+ async function dropCandidate(filePath, memId) {
117
+ const raw = await readOrEmpty(filePath);
118
+ if (!raw.includes(memId))
119
+ return false;
120
+ const next = raw
121
+ .split(/\r?\n/)
122
+ .filter((line) => !line.includes(memId))
123
+ .join('\n');
124
+ // Clean up orphan `## Pending review (...)` headers whose lines are gone.
125
+ const cleaned = next.replace(/## Pending review \([^)]*\)\s*\n(?=\n*## |\n*§|\n*$)/g, '');
126
+ const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
127
+ await node_fs_1.promises.writeFile(tmp, cleaned, 'utf8');
128
+ await node_fs_1.promises.rename(tmp, filePath);
129
+ return true;
130
+ }
131
+ /** Convenience: list pending across both files. */
132
+ async function listAllPending(memoryPath, userPath) {
133
+ const m = (await listPending(memoryPath)).map((c) => ({ ...c, file: 'memory' }));
134
+ const u = (await listPending(userPath)).map((c) => ({ ...c, file: 'user' }));
135
+ return [...m, ...u];
136
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/memory/reviewer/prompt.ts — v4.9.0 Slice 10.
10
+ *
11
+ * Reviewer system prompt + the on-disk wire format parser. The reviewer
12
+ * LLM is told to emit one candidate per line, pipe-delimited:
13
+ *
14
+ * <file>|<text>|<rationale>
15
+ *
16
+ * where `<file>` is `memory` or `user`. Lines that don't match the
17
+ * shape, or that propose unknown files, are silently dropped — they
18
+ * count toward the `parser_drops` telemetry counter but don't fail
19
+ * the whole review.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.buildReviewerPrompt = buildReviewerPrompt;
23
+ exports.parseReviewerResponse = parseReviewerResponse;
24
+ const namespaceRegistry_1 = require("../namespaceRegistry");
25
+ /**
26
+ * Reviewer system prompt. Conservative, skip-rules-first. The LLM is
27
+ * told to follow these rules; the parser + skipRules drop any
28
+ * candidate that slips through anyway.
29
+ */
30
+ function buildReviewerPrompt(opts) {
31
+ const turnText = opts.recentTurns
32
+ .map((m) => `[${m.role}] ${m.content}`)
33
+ .join('\n');
34
+ return [
35
+ 'You are Aiden\'s post-turn memory reviewer. You read the recent',
36
+ 'conversation and propose AT MOST ' + String(opts.maxCandidates) +
37
+ ' additions to long-term memory.',
38
+ '',
39
+ 'STRICT RULES (a candidate that violates ANY rule is DROPPED):',
40
+ // v4.9.0 Slice 11 — namespaces are now dynamic. The parser drops
41
+ // any candidate whose `<file>` field isn't in the registry; the
42
+ // skip-rule pass drops `project` candidates when no project root
43
+ // is detected.
44
+ ' - <file> must be one of: ' + (0, namespaceRegistry_1.listNamespaceNames)().join(', ') + '.',
45
+ ' `memory` = project / environment / Aiden facts (global).',
46
+ ' `user` = user identity / preferences / workflow style.',
47
+ ' `project` (when available) = current-repo-specific context.',
48
+ ' - NO negations ("user does not X", "no longer Y"). Skip them.',
49
+ ' - NO transient artifacts ("today", "this session", "just now").',
50
+ ' - NO sensitive inference (health, politics, religion, finance,',
51
+ ' sexual orientation, family planning). NEVER propose these.',
52
+ ' - NO duplicates of what is already in memory below.',
53
+ ' - Each candidate <= 200 chars.',
54
+ '',
55
+ 'OUTPUT FORMAT — one candidate per line, exactly:',
56
+ ' <file>|<text>|<rationale>',
57
+ 'where <file> ∈ {memory, user}. No prose, no headers, no markdown.',
58
+ 'If nothing is worth proposing, return an empty response.',
59
+ '',
60
+ '=== CURRENT live MEMORY.md ===',
61
+ opts.liveMemory || '(empty)',
62
+ '',
63
+ '=== CURRENT live USER.md ===',
64
+ opts.liveUser || '(empty)',
65
+ '',
66
+ '=== RECENT CONVERSATION TURNS ===',
67
+ turnText || '(no turns)',
68
+ '',
69
+ 'Now propose candidates:',
70
+ ].join('\n');
71
+ }
72
+ /**
73
+ * Parse the LLM response into structured candidates. Drops lines that
74
+ * don't match `<file>|<text>|<rationale>` or use an unknown file. The
75
+ * caller's skipRules pass over each result for additional validation.
76
+ */
77
+ function parseReviewerResponse(raw) {
78
+ const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
79
+ const out = [];
80
+ let drops = 0;
81
+ for (const line of lines) {
82
+ // Strip leading bullets / numbering the LLM may have added despite instructions.
83
+ const cleaned = line.replace(/^[-*\d.)\s]+/, '');
84
+ const parts = cleaned.split('|').map((p) => p.trim());
85
+ if (parts.length < 3) {
86
+ drops += 1;
87
+ continue;
88
+ }
89
+ const [file, text, ...rest] = parts;
90
+ // v4.9.0 Slice 11 — accept any registered namespace name. The
91
+ // reviewer orchestrator's skip-rule pass drops `project`
92
+ // candidates when no project root resolves.
93
+ if (!(0, namespaceRegistry_1.listNamespaceNames)().includes(file)) {
94
+ drops += 1;
95
+ continue;
96
+ }
97
+ const rationale = rest.join('|').trim();
98
+ if (!text || !rationale) {
99
+ drops += 1;
100
+ continue;
101
+ }
102
+ out.push({ file: file, text, rationale });
103
+ }
104
+ return { candidates: out, parserDrops: drops };
105
+ }