@technomoron/agent-run 0.99.32 → 0.99.34

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/CHANGES CHANGED
@@ -1,6 +1,47 @@
1
1
  CHANGES
2
2
  =======
3
3
 
4
+ Version 0.99.34 (2026-09-07)
5
+ ----------------------------
6
+ * Keep the agent-brain socket under the configured root so the service and
7
+ MCP clients connect regardless of their environment. Restart the service
8
+ and agent sessions after upgrading.
9
+ * Remove the runtime-directory environment variable from the service helper.
10
+
11
+ Version 0.99.33 (2026-09-06)
12
+ ----------------------------
13
+ * Run the agent-brain MCP server as `agent-brain` from `PATH` and choose its
14
+ socket when the bridge starts, so generated agent configuration no longer
15
+ contains the path of whichever install rendered it. Re-render existing
16
+ profiles with `agent-run update`.
17
+ * Let the git guard through for throwaway test repositories: `commit`, `tag`
18
+ and `push` run normally below a directory holding an `.agent-run-test-repo`
19
+ marker, so a project's own test suite works inside an agent session.
20
+ * Match the git guard on the subcommand instead of any argument, so
21
+ `git log --grep push` or a branch named `commit` is no longer blocked.
22
+ * Fix the Windows guard shims: strip a leading PATH entry only when it is the
23
+ guard directory, resolve the real command explicitly, and refuse recursive
24
+ resolution.
25
+ * Archive closed review findings to a compact history file and add
26
+ `review_history` for recalling them without them appearing in normal
27
+ knowledge retrieval.
28
+ * Keep brain retrieval working when a single knowledge or task file is
29
+ malformed, reporting the broken file instead of failing every read.
30
+ * Rebuild the search index only when the underlying files change, so read-only
31
+ retrieval no longer writes on every call.
32
+ * Run brain Git operations against the real git binary rather than the guard
33
+ shim, and stage `templates/` with `sync save`.
34
+ * Render skills through a loader restricted to the skills directories, so a
35
+ skill template cannot include files elsewhere in the configuration root.
36
+ * Omit disabled MCP servers from Codex and Grok configuration, as Claude and
37
+ Gemini already do.
38
+ * Remove `AGENT_WRAPPER_FORCE_PERMISSIVE`, the undocumented environment
39
+ variable that disabled Claude's permission prompts.
40
+ * Stop overwriting systemd units and helper scripts in the configuration root
41
+ on every run.
42
+ * Remove `scripts/release-check.sh`. `pnpm release` now only creates and pushes
43
+ the tag, and `pnpm run release:preflight` is a clean build plus the tests.
44
+
4
45
  Version 0.99.32 (2026-09-06)
5
46
  ----------------------------
6
47
  * Add severity, an assigned C1/H1/M1/L1 label, and an open/fixed/wontfix state
package/README.md CHANGED
@@ -206,6 +206,23 @@ Codex and Grok and are omitted from Claude and Gemini.
206
206
  `local.md.njk` and `overrides/` are not created unless they contain actual
207
207
  profile-specific configuration.
208
208
 
209
+ ## Guard Shims
210
+
211
+ `blockGitWrite`, `blockPublish`, and `blockGithubRelease` put small `git`, `npm`,
212
+ `pnpm`, and `gh` shims on the launched agent's `PATH`. They refuse
213
+ `git commit`, `git tag`, `git push`, `npm publish`, `pnpm publish`, and
214
+ `gh release create` with exit code 42 and pass everything else through to the
215
+ real command. Set `AGENT_RUN_ALLOW_GIT_WRITE=1`, `AGENT_RUN_ALLOW_PUBLISH=1`, or
216
+ `AGENT_RUN_ALLOW_GITHUB_RELEASE=1` for a single invocation you have reviewed.
217
+
218
+ Throwaway repositories that a test suite creates and deletes are exempt from the
219
+ `git` shim. Create an empty `.agent-run-test-repo` file in the repository, or in
220
+ any directory above it, and `git commit`, `git tag`, and `git push` run normally
221
+ for anything below that marker. The shim reads the marker from the directory
222
+ `git` would work in, following any leading `-C` options. This is what lets
223
+ `pnpm test` run inside an agent session; nothing outside a marked directory is
224
+ affected.
225
+
209
226
  ## Profile Resolution
210
227
 
211
228
  The mapped path is:
@@ -597,11 +614,25 @@ and restore the previous unconfigured-project behavior.
597
614
  reports omitted items. Use `get_knowledge` to read complete items when necessary.
598
615
  Always-recalled knowledge is considered first, then matching file patterns,
599
616
  then lexical results. Search uses FTS ranking with extra weight for titles and tags.
617
+ Queries compare the current files with the index and write only added, changed,
618
+ or removed entries. Unchanged queries do not take a database write lock. Context
619
+ retrieval scans the files once. Search and context MCP tools declare that they
620
+ may write because they can refresh this derived index.
600
621
  Symbols contribute search terms. Manual Markdown changes are read on every query;
601
622
  the index can be deleted while the service is stopped and rebuilt on the next query.
602
623
 
603
624
  ### Knowledge and skills
604
625
 
626
+ Malformed knowledge and task files do not block other entries. When a scan finds
627
+ problems, MCP and CLI list/search responses contain `items` and `brokenFiles`;
628
+ object responses, including context, gain a `brokenFiles` field. Each error lists
629
+ the source path, `status: "broken"`, a reason, and an ID when it can be parsed.
630
+ Responses without errors keep their existing format. Broken entries are excluded
631
+ from search and context, and reads or updates of those entries are refused.
632
+ Correct the file on disk; the next scan picks up the repair automatically.
633
+ Duplicate IDs flag all matching files. External task imports and new review
634
+ numbering stop when broken files would make deduplication or numbering unreliable.
635
+
605
636
  Knowledge is one Markdown file per item, with YAML metadata:
606
637
 
607
638
  ```markdown
@@ -642,7 +673,7 @@ project. The brain-memory skill explains these choices to native agents.
642
673
 
643
674
  Available MCP tools include `get_context`, `search_knowledge`, `get_knowledge`,
644
675
  `remember`, `amend_knowledge`, `promote`, `deprecate_knowledge`, `review_context`,
645
- `review_list`, and `resolve_review`.
676
+ `review_list`, `resolve_review`, `review_history`, and `review_archive`.
646
677
  Writes use atomic file replacement and a shared lock; concurrent writes either
647
678
  complete or report that the caller should retry. After a crashed writer, stop
648
679
  the service, verify the PID in `runtime/brain-write.lock` is no longer running,
@@ -699,8 +730,19 @@ agent-brain review resolve ITEM_ID --revision REVISION --state fixed "Fixed by u
699
730
  `review_list` returns findings in severity then number order, open ones only
700
731
  unless states are given, each with the revision needed to resolve it.
701
732
  `resolve_review` records the state and reason and takes the finding out of active
702
- recall, keeping its text and history. Mark a finding `wontfix` only when the user
703
- has said it is intentional.
733
+ recall. It writes a short record (label, title, outcome, date, reason, and original
734
+ file/revision) to the scope's `review-history.jsonl`, then deletes the full finding
735
+ file. `review-counters.json` preserves numbering. These files participate in brain
736
+ Git sync but are excluded from normal knowledge retrieval.
737
+
738
+ Use MCP `review_history` to recall old fixes, with optional `query`, `scope`, `state`,
739
+ `offset`, and `limit` (default 100). The CLI equivalent is
740
+ `agent-brain review history [query]`. Git retains full details only when the original
741
+ files were committed before cleanup; the MCP history is sufficient for the short fix list.
742
+ Use `review_archive` with a scope, or `agent-brain review archive --scope project`,
743
+ to compact existing files already marked `fixed` or `wontfix`. Deferred findings
744
+ remain open. Mark a finding `wontfix` only when the user explicitly drops it or
745
+ confirms it is intentional. A request to postpone an issue does not close it.
704
746
 
705
747
  Finding labels are indexed, so `search_knowledge` for `H2` finds that finding.
706
748
  Review files written before these fields existed keep parsing; they simply carry
@@ -719,6 +761,13 @@ A project/default skill overrides a global skill with the same name. Add
719
761
  `extends: global:NAME` to include the global skill's instructions before the local
720
762
  instructions. The existing native skill-template system continues to work.
721
763
 
764
+ Skill templates and their includes must stay under `global/skills/` or the active
765
+ project/default profile's `skills/` directory. Use config-root-relative paths such
766
+ as `{% include "global/skills/shared/checks.md" %}`, or `./` paths relative to the
767
+ including template. Nunjucks variables remain available. Symlinks are rejected,
768
+ and each file is limited to 1 MiB. Move snippets and skill overrides stored
769
+ elsewhere into these directories before rendering or loading those skills.
770
+
722
771
  The installed `brain-memory`, `brain-review`, and `todo-manager` skills describe
723
772
  retrieval, persistence, review learning, and task workflows. Review records use
724
773
  the knowledge tools and remain inferred unless confirmed by the user.
@@ -790,9 +839,14 @@ agent-brain sync push --confirmed
790
839
 
791
840
  The command without an action previews Git status and eligible files. Saving
792
841
  includes Markdown knowledge, skills, tasks, project metadata, and `.gitignore`.
842
+ It also includes all regular files under each scope's `templates/` directory,
843
+ including nested Nunjucks templates and supporting files. Template additions,
844
+ edits, and deletions are synced; symlinks are rejected.
793
845
  It excludes indexes, runtime state, secrets, `brain.jsonc`, and native agent
794
846
  configuration. Existing staged changes block a save. Configure the remote and
795
847
  upstream with normal Git commands using your existing credentials.
848
+ Brain Git operations use `AGENT_RUN_REAL_PATH` when available, so agent-session
849
+ Git guards do not intercept configuration sync.
796
850
  After cloning onto another machine, run `agent-brain init` to restore generated
797
851
  profile markers and shared templates, then update source roots in `config.yaml`
798
852
  to match that machine.
@@ -811,12 +865,21 @@ agent-brain serve
811
865
  ```
812
866
 
813
867
  The service hosts SDK Streamable HTTP at `/mcp` using apicore's Fastify instance.
814
- It listens on `$XDG_RUNTIME_DIR/agent-brain.sock`, or
815
- `~/.agent-run/runtime/agent-brain.sock` when that variable is absent. The socket
868
+ It listens on `<config-root>/runtime/agent-brain.sock`, which defaults to
869
+ `~/.agent-run/runtime/agent-brain.sock`. Both the service and clients use the
870
+ configured root regardless of their environment. The socket
816
871
  directory must belong to the current user and have mode `0700`; the socket has
817
872
  mode `0600`. No TCP listener or application user database is created.
818
873
 
819
- `agent-run mcp` bridges native-agent stdio to this socket. Use `--socket PATH`
874
+ Generated native-agent configurations run `agent-brain mcp` through `PATH`, so
875
+ `agent-brain` must be installed and available there. The bridge selects the socket
876
+ when it starts; generated files do not depend on the installation or runtime
877
+ directory used to render them. Re-render existing profiles with `agent-run update`
878
+ to replace older commands that contain absolute installation paths. After
879
+ upgrading from a version that used a different socket location, restart the service and
880
+ agent sessions so they use the socket under the configuration root.
881
+
882
+ `agent-run mcp` also bridges native-agent stdio to this socket. Use `--socket PATH`
820
883
  for a particular service. The bridge verifies the socket's ownership and private
821
884
  permissions. A missing or unreachable socket is an error; start the service
822
885
  before connecting. There is no standalone stdio server or TCP fallback. Apicore
@@ -828,8 +891,9 @@ stopped. Starting a second service on an existing socket fails without replacing
828
891
  the first service's socket.
829
892
 
830
893
  For a user service, copy `ops/systemd/agent-brain.service` to
831
- `~/.config/systemd/user/`, adjust `ExecStart` if your executable is elsewhere, and
832
- run `systemctl --user daemon-reload` followed by
894
+ `~/.config/systemd/user/` and adjust `ExecStart` if your executable is elsewhere.
895
+ Add `--configdir PATH` to `ExecStart` if you use a custom configuration root.
896
+ Run `systemctl --user daemon-reload` followed by
833
897
  `systemctl --user enable --now agent-brain`. Installation is explicit.
834
898
 
835
899
  The supplied service uses `serve --pull`. On startup this pulls the configuration
@@ -863,6 +927,9 @@ Transport and connector implementations follow the
863
927
  ## Systemd Jobs
864
928
 
865
929
  The repo includes optional systemd units for keeping global AI tooling current.
930
+ Run the installer from the agent-run checkout. Setup, generation, and updates do
931
+ not copy systemd units or helper scripts into `.agent-run`. Remove legacy copies
932
+ there once any custom changes have been moved to your maintained scripts.
866
933
 
867
934
  Install the global AI tools updater:
868
935
 
@@ -933,6 +1000,11 @@ After updating `package.json` and `CHANGES`, run:
933
1000
  pnpm release
934
1001
  ```
935
1002
 
936
- This validates the repo, creates the annotated tag matching the package version,
937
- and pushes that tag to `origin`. The tag push triggers the release workflow,
938
- which verifies, packs, publishes to npm, and creates the GitHub release.
1003
+ This creates the annotated tag matching the package version and pushes that tag
1004
+ to `origin`. It stops if the tag already exists, and removes the local tag again
1005
+ if the push fails. The tag push triggers the release workflow, which runs the
1006
+ tests and build, packs, publishes to npm, and creates the GitHub release.
1007
+
1008
+ `pnpm run release:preflight` is the local gate to run first: a clean build and
1009
+ the full test suite. Update `package.json` and `CHANGES` yourself; nothing
1010
+ checks them for you.
package/dist/agent-run.js CHANGED
@@ -1,14 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.parseEditorCommand = exports.resolveProfile = exports.resolveAgentDir = exports.findProjectRoot = exports.defaultConfigRootSearchCandidates = exports.defaultConfigRoot = exports.parseInvocation = void 0;
4
+ exports.parseEditorCommand = exports.resolveProfile = exports.resolveAgentDir = exports.findProjectRoot = exports.defaultConfigRoot = exports.parseInvocation = void 0;
5
5
  const path = require("path");
6
6
  const commands_1 = require("./commands");
7
7
  var cli_1 = require("./cli");
8
8
  Object.defineProperty(exports, "parseInvocation", { enumerable: true, get: function () { return cli_1.parseInvocation; } });
9
9
  var project_1 = require("./project");
10
10
  Object.defineProperty(exports, "defaultConfigRoot", { enumerable: true, get: function () { return project_1.defaultConfigRoot; } });
11
- Object.defineProperty(exports, "defaultConfigRootSearchCandidates", { enumerable: true, get: function () { return project_1.defaultConfigRootSearchCandidates; } });
12
11
  Object.defineProperty(exports, "findProjectRoot", { enumerable: true, get: function () { return project_1.findProjectRoot; } });
13
12
  Object.defineProperty(exports, "resolveAgentDir", { enumerable: true, get: function () { return project_1.resolveAgentDir; } });
14
13
  Object.defineProperty(exports, "resolveProfile", { enumerable: true, get: function () { return project_1.resolveProfile; } });
@@ -51,13 +51,9 @@ exports.claudeAdapter = {
51
51
  generate,
52
52
  spawn(runtime, options) {
53
53
  (0, shared_1.assertRuntimeFile)('claude', runtime.instructionFile, runtime.context.profileDir);
54
- const forcedPermissionArgs = process.env.AGENT_WRAPPER_FORCE_PERMISSIVE === '1' &&
55
- typeof process.getuid === 'function' && process.getuid() !== 0
56
- ? ['--permission-mode', 'bypassPermissions']
57
- : [];
58
54
  const runtimeArgs = options.wrapperArgs.sandboxMode === 'danger'
59
55
  ? ['--dangerously-skip-permissions']
60
- : forcedPermissionArgs;
56
+ : [];
61
57
  return {
62
58
  command: options.binary,
63
59
  args: [
package/dist/brain/cli.js CHANGED
@@ -39,6 +39,8 @@ async function brainMain(argv, wrapperCommand) {
39
39
  'agent-brain deprecate <id> --revision <hash> [reason]',
40
40
  'agent-brain skills [name]',
41
41
  'agent-brain review list [--severity <level>] [--state <state>]',
42
+ 'agent-brain review history [query] [--scope <scope>] [--state fixed|wontfix]',
43
+ 'agent-brain review archive --scope <scope>',
42
44
  'agent-brain review resolve <id> --revision <hash> --state fixed|wontfix [reason]',
43
45
  'agent-brain todo list | get <id> | add --json <task-object>',
44
46
  'agent-brain todo update <id> --revision <hash> --json <changes>',
@@ -100,48 +102,55 @@ async function brainMain(argv, wrapperCommand) {
100
102
  throw new Error('--revision is required; read the item first'); return values.revision; };
101
103
  const json = () => { if (!values.json)
102
104
  throw new Error('--json is required'); return JSON.parse(values.json); };
105
+ const report = (value) => print(store.withDiagnostics(value));
103
106
  try {
104
107
  switch (command) {
105
108
  case 'status':
106
- print({ configRoot: root, enabled: (0, config_1.readBrainConfig)(root)?.enabled ?? false, profile: store.profile, scopes: store.scopes, knowledge: store.allKnowledge().length, todos: (0, todos_1.listTodos)(store).length });
109
+ report({ configRoot: root, enabled: (0, config_1.readBrainConfig)(root)?.enabled ?? false, profile: store.profile, scopes: store.scopes, knowledge: store.allKnowledge().length, todos: (0, todos_1.listTodos)(store).length });
107
110
  break;
108
111
  case 'search':
109
- print(store.search(positionals.join(' ')));
112
+ report(store.search(positionals.join(' ')));
110
113
  break;
111
114
  case 'context':
112
- print(store.context(positionals.join(' ')));
115
+ report(store.context(positionals.join(' ')));
113
116
  break;
114
117
  case 'get':
115
- print(store.get(requireId()));
118
+ report(store.get(requireId()));
116
119
  break;
117
120
  case 'remember':
118
- print(store.remember(store_1.knowledgeInput.parse(json())));
121
+ report(store.remember(store_1.knowledgeInput.parse(json())));
119
122
  break;
120
123
  case 'promote':
121
124
  if (!values.confirmed)
122
125
  throw new Error('Promotion requires --confirmed after explicit user approval');
123
- print(store.promote(requireId(), store_1.scopeSchema.parse(values.scope), requireRevision()));
126
+ report(store.promote(requireId(), store_1.scopeSchema.parse(values.scope), requireRevision()));
124
127
  break;
125
128
  case 'amend':
126
- print(store.amend(requireId(), requireRevision(), store_1.knowledgeChanges.parse(json())));
129
+ report(store.amend(requireId(), requireRevision(), store_1.knowledgeChanges.parse(json())));
127
130
  break;
128
131
  case 'deprecate':
129
- print(store.deprecate(requireId(), requireRevision(), positionals.slice(1).join(' ')));
132
+ report(store.deprecate(requireId(), requireRevision(), positionals.slice(1).join(' ')));
130
133
  break;
131
134
  case 'skills':
132
- print(positionals[0] ? (0, skills_1.getSkill)(store, positionals[0]) : (0, skills_1.listSkills)(store));
135
+ report(positionals[0] ? (0, skills_1.getSkill)(store, positionals[0]) : (0, skills_1.listSkills)(store));
133
136
  break;
134
137
  case 'review': {
135
138
  const operation = positionals.shift() ?? 'list';
136
139
  if (operation === 'list') {
137
- print(store.reviews({
140
+ report(store.reviews({
138
141
  ...(values.severity ? { severity: [store_1.severitySchema.parse(values.severity)] } : {}),
139
142
  ...(values.state ? { state: [store_1.reviewStateSchema.parse(values.state)] } : {})
140
143
  }).map((item) => ({ finding: item.finding, severity: item.severity, state: item.state ?? 'open', title: item.title, id: item.id, revision: item.revision })));
141
144
  }
145
+ else if (operation === 'history') {
146
+ report(store.reviewHistory({ query: positionals.join(' '), ...(values.scope ? { scope: store_1.scopeSchema.parse(values.scope) } : {}), ...(values.state ? { state: zod_1.z.enum(['fixed', 'wontfix']).parse(values.state) } : {}), limit: Number.MAX_SAFE_INTEGER }));
147
+ }
148
+ else if (operation === 'archive') {
149
+ report(store.archiveReviews(store_1.scopeSchema.parse(values.scope)));
150
+ }
142
151
  else if (operation === 'resolve') {
143
152
  const state = zod_1.z.enum(['fixed', 'wontfix']).parse(values.state);
144
- print(store.resolveReview(requireId(), requireRevision(), state, positionals.slice(1).join(' ') || undefined));
153
+ report(store.resolveReview(requireId(), requireRevision(), state, positionals.slice(1).join(' ') || undefined));
145
154
  }
146
155
  else
147
156
  throw new Error('Usage: agent-brain review list | resolve <id> --revision <hash> --state fixed|wontfix');
@@ -150,30 +159,30 @@ async function brainMain(argv, wrapperCommand) {
150
159
  case 'sync': {
151
160
  const action = positionals[0];
152
161
  if (!action) {
153
- print((0, git_1.gitPreview)(store));
162
+ report((0, git_1.gitPreview)(store));
154
163
  break;
155
164
  }
156
165
  if (!['init', 'save', 'pull', 'push'].includes(action))
157
166
  throw new Error('Unknown sync action');
158
167
  if (!values.confirmed)
159
168
  throw new Error('Review agent-brain sync output and obtain authorization before using --confirmed');
160
- print((0, git_1.syncGit)(store, action, values.message));
169
+ report((0, git_1.syncGit)(store, action, values.message));
161
170
  break;
162
171
  }
163
172
  case 'todo': {
164
173
  const operation = positionals.shift();
165
174
  if (operation === 'list')
166
- print((0, todos_1.listTodos)(store));
175
+ report((0, todos_1.listTodos)(store));
167
176
  else if (operation === 'get')
168
- print((0, todos_1.getTodo)(store, requireId()));
177
+ report((0, todos_1.getTodo)(store, requireId()));
169
178
  else if (operation === 'add')
170
- print((0, todos_1.addTodo)(store, todos_1.todoInput.parse(json())));
179
+ report((0, todos_1.addTodo)(store, todos_1.todoInput.parse(json())));
171
180
  else if (operation === 'update')
172
- print((0, todos_1.updateTodo)(store, requireId(), requireRevision(), todos_1.todoChanges.parse(json())));
181
+ report((0, todos_1.updateTodo)(store, requireId(), requireRevision(), todos_1.todoChanges.parse(json())));
173
182
  else if (operation === 'complete')
174
- print((0, todos_1.updateTodo)(store, requireId(), requireRevision(), { status: 'done' }));
183
+ report((0, todos_1.updateTodo)(store, requireId(), requireRevision(), { status: 'done' }));
175
184
  else if (operation === 'import' || operation === 'sync')
176
- print(await (0, connectors_1.syncConnector)(store, requireId(), store_1.scopeSchema.parse(values.scope)));
185
+ report(await (0, connectors_1.syncConnector)(store, requireId(), store_1.scopeSchema.parse(values.scope)));
177
186
  else
178
187
  throw new Error('Unknown todo command; use agent-brain --help');
179
188
  break;
@@ -181,11 +190,11 @@ async function brainMain(argv, wrapperCommand) {
181
190
  case 'project': {
182
191
  const operation = positionals[0];
183
192
  if (operation === 'list')
184
- print((0, config_1.listBrainProjects)(root));
193
+ report((0, config_1.listBrainProjects)(root));
185
194
  else if (operation === 'info')
186
- print({ profile: store.profile, ...(0, projects_1.inspectProject)(cwd) });
195
+ report({ profile: store.profile, ...(0, projects_1.inspectProject)(cwd) });
187
196
  else if (operation === 'bootstrap')
188
- print({ status: 'proposal', message: 'Inspect the listed documents to propose architecture, constraints, conventions, and specs. Confirm proposals before saving authoritative knowledge.', ...(0, projects_1.inspectProject)(cwd) });
197
+ report({ status: 'proposal', message: 'Inspect the listed documents to propose architecture, constraints, conventions, and specs. Confirm proposals before saving authoritative knowledge.', ...(0, projects_1.inspectProject)(cwd) });
189
198
  else
190
199
  throw new Error('Usage: agent-run project list | info | bootstrap');
191
200
  break;
@@ -14,7 +14,7 @@ const yaml_1 = require("yaml");
14
14
  const zod_1 = require("zod");
15
15
  const utils_1 = require("../utils");
16
16
  function defaultSocketPath(configRoot) {
17
- return path.join(process.env.XDG_RUNTIME_DIR ?? path.join(configRoot, 'runtime'), 'agent-brain.sock');
17
+ return path.join(configRoot, 'runtime', 'agent-brain.sock');
18
18
  }
19
19
  exports.brainConfigSchema = zod_1.z.object({
20
20
  enabled: zod_1.z.boolean().default(true),
package/dist/brain/git.js CHANGED
@@ -11,7 +11,7 @@ const store_1 = require("./store");
11
11
  const config_1 = require("./config");
12
12
  function git(root, args) {
13
13
  const result = (0, node_child_process_1.spawnSync)('git', ['-C', root, ...args], { encoding: 'utf8', shell: false, timeout: 60000,
14
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, maxBuffer: 16 * 1024 * 1024 });
14
+ env: { ...process.env, PATH: process.env.AGENT_RUN_REAL_PATH ?? process.env.PATH, GIT_TERMINAL_PROMPT: '0' }, maxBuffer: 16 * 1024 * 1024 });
15
15
  if (result.error || result.status !== 0)
16
16
  throw new Error(`git ${args[0]} failed: ${result.error?.message ?? result.stderr.trim()}`);
17
17
  return result.stdout.trimEnd();
@@ -40,12 +40,18 @@ function syncableFiles(store) {
40
40
  for (const root of roots) {
41
41
  for (const folder of folders)
42
42
  files.push(...store.files(path.join(root, folder)));
43
- for (const name of ['config.yaml', 'project.md']) {
43
+ files.push(...store.files(path.join(root, 'templates'), { includeAllFiles: true }));
44
+ for (const name of ['config.yaml', 'project.md', 'review-history.jsonl', 'review-counters.json']) {
44
45
  const file = store.safePath(root, name);
45
46
  if (fs.existsSync(file))
46
47
  files.push(file);
47
48
  }
48
49
  }
50
+ if (fs.existsSync(path.join(store.configRoot, '.git'))) {
51
+ const removable = roots.flatMap((root) => ['templates', 'reviews'].map((folder) => path.relative(store.configRoot, path.join(root, folder))));
52
+ const deleted = git(store.configRoot, ['ls-files', '--deleted', '-z', '--', ...removable]).split('\0').filter(Boolean);
53
+ files.push(...deleted.map((file) => store.safePath(file)));
54
+ }
49
55
  const ignore = store.safePath('.gitignore');
50
56
  if (fs.existsSync(ignore))
51
57
  files.push(ignore);
@@ -21,7 +21,7 @@ const builtInSkills = {
21
21
  },
22
22
  'brain-review': {
23
23
  description: 'Review changes against project constraints and previous findings, and save confirmed review knowledge.',
24
- content: 'Call review_context with the task and changed paths. Check correctness, security, architecture, and meaningful test coverage against relevant constraints. Report concrete, actionable findings with severity and file references. If authorized to save the review, call remember with type=review. Keep suspected hazards marked inferred. Never convert a finding into an authoritative constraint without user confirmation.'
24
+ content: 'Call review_context with the task and changed paths. Check correctness, security, architecture, and meaningful test coverage against relevant constraints. Report concrete, actionable findings with severity and file references. If authorized to save the review, call remember with type=review. Keep suspected hazards marked inferred. Never convert a finding into an authoritative constraint without user confirmation. Use review_list for open findings. After completing an authorized fix and relevant checks, resolve_review as fixed with a short explanation and verification result; use wontfix only for an explicit user decision to drop the finding. Postponed findings stay open. Closing saves compact history and removes the full finding file. Recall old fixes through review_history, not normal knowledge search. Use review_archive to compact already closed files when cleanup is requested. Do not create separate done lists or full archived review copies.'
25
25
  },
26
26
  'todo-manager': {
27
27
  description: 'Manage local tasks and import externally tracked tasks without duplicates.',
@@ -26,9 +26,11 @@ function createBrainServer(store) {
26
26
  function tool(name, description, shape, readOnly, handler) {
27
27
  const schema = zod_1.z.object(shape).strict();
28
28
  server.registerTool(name, { description, inputSchema: schema, annotations: { readOnlyHint: readOnly, destructiveHint: false, openWorldHint: false } }, async (input) => {
29
+ store.knowledgeErrors.length = 0;
30
+ store.todoErrors.length = 0;
29
31
  try {
30
32
  const result = await handler(zod_1.z.object(shape).strict().parse(input));
31
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
33
+ return { content: [{ type: 'text', text: JSON.stringify(store.withDiagnostics(result)) }] };
32
34
  }
33
35
  catch (error) {
34
36
  return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }] };
@@ -52,11 +54,11 @@ function createBrainServer(store) {
52
54
  });
53
55
  tool('get_context', 'Retrieve relevant constraints and knowledge before substantial work. Check omitted; explicitly fetch required knowledge if the budget excluded it.', {
54
56
  task: zod_1.z.string().max(10000), files: zod_1.z.array(zod_1.z.string()).max(100).optional(), symbols: zod_1.z.array(zod_1.z.string()).max(100).optional()
55
- }, true, ({ task, files, symbols }) => store.context(task, files, symbols));
57
+ }, false, ({ task, files, symbols }) => store.context(task, files, symbols));
56
58
  tool('search_knowledge', 'Search Markdown knowledge in the active scopes using SQLite FTS.', {
57
59
  query: zod_1.z.string().max(10000), scopes: zod_1.z.array(store_1.scopeSchema).optional(), types: zod_1.z.array(store_1.typeSchema).optional(),
58
60
  includeDeprecated: zod_1.z.boolean().optional(), limit: zod_1.z.number().int().min(1).max(100).optional()
59
- }, true, ({ query, ...options }) => store.search(query, options));
61
+ }, false, ({ query, ...options }) => store.search(query, options));
60
62
  tool('get_knowledge', 'Read a complete knowledge item and its current revision.', { id }, true, ({ id }) => store.get(id));
61
63
  tool('remember', 'Persist durable knowledge when authorized by the user. Choose scope and a singular type; the server stores it in the matching directory (for example constraint in constraints/, preference in preferences/). Explicit user instructions have authority=user; deductions remain inferred observations. Global writes require explicit global intent. Use todo tools for tasks; skills and templates are separate files.', store_1.knowledgeInput.shape, false, (input) => store.remember(input));
62
64
  tool('amend_knowledge', 'Revise an active item in place using its last-read revision, keeping its id, file name, scope, type, authority, and history. Use this to correct or extend existing knowledge instead of writing a near-duplicate. Scope, type, authority, and review severity cannot be changed this way.', {
@@ -70,14 +72,21 @@ function createBrainServer(store) {
70
72
  tool('get_skill', 'Load a full skill only when it applies to the current task.', { name: zod_1.z.string().max(100) }, true, ({ name }) => (0, skills_1.getSkill)(store, name));
71
73
  tool('review_context', 'Retrieve constraints, decisions, prior review findings, and path-specific knowledge for a code review.', {
72
74
  task: zod_1.z.string().max(10000), files: zod_1.z.array(zod_1.z.string()).max(100).optional()
73
- }, true, ({ task, files }) => store.context(`review constraint decision ${task}`, files));
74
- tool('review_list', 'List review findings in severity then number order. Open findings only unless states are given. Each finding carries its label, severity, state, and the revision needed to resolve it.', {
75
+ }, false, ({ task, files }) => store.context(`review constraint decision ${task}`, files));
76
+ tool('review_list', 'List review findings in severity then number order. Open findings only unless states are given; archived findings are in review_history. Each finding carries its label, severity, state, and the revision needed to resolve it.', {
75
77
  severity: zod_1.z.array(store_1.severitySchema).optional(), state: zod_1.z.array(store_1.reviewStateSchema).optional()
76
78
  }, true, ({ severity, state }) => store.reviews({ severity, state }).map((item) => ({
77
79
  finding: item.finding, severity: item.severity, state: item.state ?? 'open', title: item.title,
78
80
  applies_to: item.applies_to, id: item.id, revision: item.revision
79
81
  })));
80
- tool('resolve_review', 'Close a review finding as fixed or wontfix, with a reason and the revision you last read. Use wontfix only when the user has said the finding is intentional.', {
82
+ tool('review_history', 'Recall compact closed review records by label, ID, title, or reason. Excluded from normal knowledge retrieval; newest first, with pagination.', {
83
+ scope: store_1.scopeSchema.optional(), query: zod_1.z.string().max(1000).optional(), state: zod_1.z.enum(['fixed', 'wontfix']).optional(),
84
+ offset: zod_1.z.number().int().min(0).optional(), limit: zod_1.z.number().int().min(1).max(1000).optional()
85
+ }, true, (options) => store.reviewHistory(options));
86
+ tool('review_archive', 'Compact already finished review files in one scope into closure history and remove their full files. Leaves unresolved and deferred findings alone.', {
87
+ scope: store_1.scopeSchema
88
+ }, false, ({ scope }) => store.archiveReviews(scope));
89
+ tool('resolve_review', 'Close a review finding as fixed or wontfix, preserving a compact closure record and deleting the full finding file. Supply the revision you last read. Use wontfix only when the user has said the finding is intentional.', {
81
90
  id, revision, state: zod_1.z.enum(['fixed', 'wontfix']), reason: zod_1.z.string().max(2000).optional()
82
91
  }, false, ({ id, revision, state, reason }) => store.resolveReview(id, revision, state, reason));
83
92
  tool('todo_list', 'List structured tasks in the active scopes.', { status: todos_1.todoInput.shape.status.optional() }, true, ({ status }) => (0, todos_1.listTodos)(store).filter((item) => !status || item.status === status));
@@ -19,11 +19,12 @@ function getSkills(store) {
19
19
  const directory = store.scopeDirectory(store.profile ? 'project' : 'default');
20
20
  const env = (0, templates_1.createNunjucksEnv)(store.configRoot);
21
21
  const context = (0, templates_1.buildRenderContext)(store.projectRoot, directory, store.configRoot, (0, manifest_1.normalizeManifest)((0, manifest_1.loadManifest)(store.configRoot, directory, profile), profile), env);
22
+ const skillEnv = (0, templates_1.createSkillNunjucksEnv)(store.configRoot, directory);
22
23
  const skills = new Map();
23
24
  const globalSkills = new Map();
24
25
  for (const { scope, directory } of store.scopes) {
25
26
  for (const file of store.files(path.join(directory, 'skills')).filter((file) => path.basename(file) === 'SKILL.md')) {
26
- const text = env.renderString(store.read(file), context);
27
+ const text = skillEnv.render(path.relative(store.configRoot, file), context);
27
28
  const document = (0, store_1.readMarkdown)(text);
28
29
  const parsed = metadata.parse(document.metadata);
29
30
  if (path.basename(path.dirname(file)) !== parsed.name)