@zhalker/gentle-shell-codegraph-subagents 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +61 -3
- package/extensions/index.ts +99 -0
- package/package.json +39 -6
- package/skills/codegraph-workflow/SKILL.md +45 -0
- package/src/agents.ts +116 -0
- package/src/apply.ts +119 -0
- package/src/autoindex.ts +167 -0
- package/src/daemonclient.ts +122 -0
- package/src/initlock.ts +230 -0
- package/src/mcp.ts +93 -0
- package/src/prefetch.ts +167 -0
- package/src/skill-reminder.ts +33 -0
- package/src/warmup.ts +86 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zhalker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,61 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @zhalker/gentle-shell-codegraph-subagents
|
|
2
|
+
|
|
3
|
+
Pi package that keeps CodeGraph reachable from gentle-shell subagents. On every session start it checks the global agent definitions and `mcp.json` and fixes what gentle-ai or the gentle-pi package left behind. Nothing is rewritten when everything is already right.
|
|
4
|
+
|
|
5
|
+
> **This package edits files in your Pi configuration.** On every session start it rewrites the agent definitions in `~/.pi/agent/agents/` and `~/.pi/agent/subagents/` and the `codegraph` entry in `~/.pi/agent/mcp.json`. It keeps a one-time backup of each file it changes as `<file>.pre-codegraph-subagents.bak`. Read "What it does" before installing, and see "Uninstall" to go back.
|
|
6
|
+
|
|
7
|
+
Upstream issue: Gentleman-Programming/gentle-shell#2804. License: MIT.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
In `~/.pi/agent/agents/` and `~/.pi/agent/subagents/` (or `$PI_CODING_AGENT_DIR`):
|
|
12
|
+
|
|
13
|
+
- Removes the blocks gentle-ai writes between `<!-- gentle-ai:pi-codegraph-tool -->` / `<!-- gentle-ai:pi-codegraph-guidance -->` and `<!-- /gentle-ai:pi-codegraph -->`.
|
|
14
|
+
- Removes the dead `mcp` entry (the retired `pi-mcp-adapter` proxy) and the `"*": false` entry from every `tools:` list.
|
|
15
|
+
- Adds `mcp__codegraph__codegraph_explore` to `gentle-ai-explore`, `gentle-ai-verify`, `gentle-ai-worker`, `jd-fix-agent`, `jd-judge-a` and `jd-judge-b`, removes the `codegraph` extension tool from `gentle-ai-explore`, and rewrites that agent's own CodeGraph instructions to use the MCP tool. Adds a routing marker to `gentle-ai-worker` for its first-prompt skill reminder.
|
|
16
|
+
- Leaves the `review-*` agents without CodeGraph.
|
|
17
|
+
|
|
18
|
+
The package also provides the `codegraph-workflow` skill. When Pi loads this packaged skill, the extension adds a reminder before the first user prompt of the parent session and the first prompt of `gentle-ai-worker` children. It does not add the reminder to other children. Pi handles skill name collisions; if Pi selects a different skill with that name, the extension does not direct the model to it as if it were the packaged skill. A local package is discovered in place rather than copied to the user's skills directory.
|
|
19
|
+
|
|
20
|
+
In `mcp.json`: selects `"direct"` for `codegraph_explore` under `toolExposure`, without changing the server-wide exposure. Explicit tool settings (including matching patterns), disabled servers, and hidden servers are preserved. At session start Pi warns when the CodeGraph MCP server is not configured, without creating it, or when direct subagent access may be unavailable; it also warns if a hidden server has an explicit direct explore exception, which Pi can still expose. Trusted project MCP configuration and servers registered by extensions are considered. A configuration change may require a reload before Pi uses it.
|
|
21
|
+
|
|
22
|
+
In a subagent that only reads and holds `mcp__codegraph__codegraph_explore` (the explorer), a `before_agent_start` hook also runs `codegraph_explore` for its task once before the first model call and hands the output to the model as a message, bounded to 20 s and 12,000 characters. It asks the project's running CodeGraph daemon directly (the same text as the CLI, locally ~75 ms against ~590 ms); when there is no usable daemon it runs `codegraph explore <task>` instead, and it never starts a daemon. If the project has no `.codegraph/` index, the package's init lock is held, or neither way works, nothing is added and the run continues as before.
|
|
23
|
+
|
|
24
|
+
In the directory where Pi starts, a new session also looks after the CodeGraph index, in the background (the session never waits for it):
|
|
25
|
+
|
|
26
|
+
- If `.codegraph/` exists, it runs `codegraph sync`. The CodeGraph file watcher does not work on SMB mounts, so without this the index stays as it was.
|
|
27
|
+
- If there is no `.codegraph/`, but there is a `.git` and one of `composer.json`, `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`, `Gemfile`, `pom.xml`, `build.gradle` or `build.gradle.kts`, it runs `codegraph init -y`. In any other directory nothing is created.
|
|
28
|
+
- When there is an index (existing, or just created), it then warms the project's CodeGraph daemon: a short MCP client runs one small `codegraph_explore` and disconnects, and the daemon stays up for as long as your session keeps it. The first real call of the orchestrator and of every subagent is then fast (locally 620-650 ms cold against ~70 ms warm). The explorer's pre-fetch asks that same daemon, so it gets the same speed-up.
|
|
29
|
+
- Pi shows a notice when each step starts ("syncing the CodeGraph index...", "creating the CodeGraph index of this project...", "warming up CodeGraph...") and another when it ends ("CodeGraph index synced", "CodeGraph index created", "CodeGraph ready"), or a warning if it failed.
|
|
30
|
+
- Concurrent rewrites of agent files and `mcp.json` are tolerated (an existing backup is kept, and transient collisions are retried). A per-project lock in the user's temp folder coordinates package-started initializations: a session that finds it held skips init, sync and warm-up, and the explorer skips its prefetch. When a session exits during init, the package also checks CodeGraph's writer lock. On Windows it conservatively holds the package lock for 60 seconds after spawning the shell, covering the period before CodeGraph writes its own lock. Locks are treated as stale after 11 minutes.
|
|
31
|
+
- Subagent children and `/reload` do nothing; startup, new, resume and fork do.
|
|
32
|
+
|
|
33
|
+
Before the first change to a file it saves the original next to it as `<file>.pre-codegraph-subagents.bak`; later changes never overwrite that backup.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
pi install npm:@zhalker/gentle-shell-codegraph-subagents
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Changes apply on the next session start. Reinstalling, or running `gentle-ai sync` again, is corrected the same way on the following start.
|
|
42
|
+
|
|
43
|
+
## Uninstall
|
|
44
|
+
|
|
45
|
+
Remove the package, then copy each `*.pre-codegraph-subagents.bak` over its original if you want the previous files back.
|
|
46
|
+
|
|
47
|
+
## Known limits
|
|
48
|
+
|
|
49
|
+
- Only the global agent directory is handled, not `<project>/.pi/agents/`.
|
|
50
|
+
- The warm-up has only been measured on local disks, not on a network share. It never kills the processes it starts, because the daemon is shared with your own sessions.
|
|
51
|
+
- CodeGraph's own `init` can hang when two start at once, or when a daemon opens the project just after the index is created. The package lock only coordinates sessions sharing the same user temp folder; it cannot prevent access by a terminal, another tool, another user, or Pi's own MCP connection. On Windows, if the session and shell exit before CodeGraph writes its own lock and that takes more than 60 seconds, the package cannot tell that init is still running. The 11-minute stale-lock limit can also expire while a process remains alive.
|
|
52
|
+
- The index sync is a background command: a prefetch that runs before it finishes can see the previous index. The sync only looks at the directory where Pi starts, not at parents or subfolders. Treat prefetched results as navigation hints, not authoritative evidence: they can be stale, incomplete, truncated or incorrect; verify findings against source files.
|
|
53
|
+
- Asking the daemon directly relies on two internal details of CodeGraph: the registry in `~/.codegraph/daemons/` and the hello line with `protocol: 1`. If either changes, or the daemon answers anything unexpected, the CLI is used, as before. A daemon that is there but does not answer within 20 s is not retried through the CLI.
|
|
54
|
+
- The explorer prefetch requires the launcher to set `GENTLE_PI_AGENTS_CHILD=1` and `GENTLE_PI_AGENTS_REQUESTED_TOOLS` to the child's declared comma-separated tool list. It recognizes a read-only child with the CodeGraph MCP tool and no writing tools; this declaration does not prove the MCP tool actually connected. Without these launcher variables the prefetch is skipped. The first 300 sanitized characters of the task become the query, so verbose instructions can broaden the result; the returned context is capped at 12,000 characters.
|
|
55
|
+
- The rewrite of the explorer's own instructions matches known sentences; if gentle-pi rewords them, they are left as they are.
|
|
56
|
+
- The `codegraph` MCP server has to be configured or registered by an extension; the package never creates it. When Pi is launched from a GUI or service, its `PATH` may differ from your shell. An absolute MCP server command can fix the server launch, but this package's `init`, `sync` and CLI fallback still need `codegraph` on Pi's `PATH`.
|
|
57
|
+
- Tested with Pi 1.4.0 and node 24.
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
`npm test` runs the unit tests with Node's built-in runner.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { applyCodegraphSubagents, readExposureWarning, readMissingServerWarning } from "../src/apply.ts";
|
|
5
|
+
import { prepareProject, runCodegraph, type SessionStartReason } from "../src/autoindex.ts";
|
|
6
|
+
import { exploreViaDaemon } from "../src/daemonclient.ts";
|
|
7
|
+
import { realInitLock } from "../src/initlock.ts";
|
|
8
|
+
import { PREFETCH_TIMEOUT_MS, exploreDaemonFirst, exploreWithCodegraph, prefetchForExplorer } from "../src/prefetch.ts";
|
|
9
|
+
import { warmUpDaemon } from "../src/warmup.ts";
|
|
10
|
+
import { chooseSkillReminder, SKILL_REMINDER_TYPE } from "../src/skill-reminder.ts";
|
|
11
|
+
|
|
12
|
+
// Starts the index sync (or first init) and then the daemon warm-up in the background; the session never waits for them.
|
|
13
|
+
function startAutoIndex(reason: SessionStartReason, ctx: ExtensionContext): void {
|
|
14
|
+
const say = (text: string, level: "info" | "warning") => {
|
|
15
|
+
if (ctx.hasUI) ctx.ui.notify(`gentle-shell-codegraph-subagents: ${text}`, level);
|
|
16
|
+
};
|
|
17
|
+
prepareProject({
|
|
18
|
+
cwd: ctx.cwd,
|
|
19
|
+
env: process.env,
|
|
20
|
+
reason,
|
|
21
|
+
exists: existsSync,
|
|
22
|
+
run: runCodegraph,
|
|
23
|
+
lock: realInitLock,
|
|
24
|
+
warm: (cwd) => warmUpDaemon(cwd),
|
|
25
|
+
onStart: (action) => say(action === "init" ? "creating the CodeGraph index of this project..." : "syncing the CodeGraph index...", "info"),
|
|
26
|
+
onIndexed: (result) => {
|
|
27
|
+
if (result.joined) say("another session is creating the CodeGraph index of this project", "info");
|
|
28
|
+
else if (!result.ok) say(`codegraph ${result.action} failed in ${ctx.cwd}`, "warning");
|
|
29
|
+
else say(result.action === "init" ? "CodeGraph index created" : "CodeGraph index synced", "info");
|
|
30
|
+
},
|
|
31
|
+
onWarmStart: () => say("warming up CodeGraph...", "info"),
|
|
32
|
+
})
|
|
33
|
+
.then((result) => {
|
|
34
|
+
if (result.warm === "ok") say("CodeGraph ready", "info");
|
|
35
|
+
else if (result.warm === "failed") say(`CodeGraph warm-up failed in ${ctx.cwd}`, "warning");
|
|
36
|
+
})
|
|
37
|
+
.catch(() => {
|
|
38
|
+
// The session may already be gone; nothing to report to.
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Reapplied on every session start so a reinstall (pi install, gentle-shell install)
|
|
43
|
+
// or a gentle-ai sync that rewrote the agent files is corrected on the next launch.
|
|
44
|
+
export default function (pi: ExtensionAPI) {
|
|
45
|
+
let firstPromptSeen = false;
|
|
46
|
+
pi.on("session_start", async (event, ctx) => {
|
|
47
|
+
startAutoIndex(event.reason, ctx);
|
|
48
|
+
try {
|
|
49
|
+
const { changed, errors } = applyCodegraphSubagents(getAgentDir());
|
|
50
|
+
if (!ctx.hasUI) return;
|
|
51
|
+
if (changed.length > 0) ctx.ui.notify(`gentle-shell-codegraph-subagents: updated ${changed.length} file(s)`, "info");
|
|
52
|
+
if (errors.length > 0) ctx.ui.notify(`gentle-shell-codegraph-subagents: ${errors.length} file(s) skipped: ${errors[0]}`, "warning");
|
|
53
|
+
const registered = pi.getMcpServers?.().some((server) => server.name === "codegraph") ?? false;
|
|
54
|
+
if (process.env.GENTLE_PI_AGENTS_CHILD !== "1" && readMissingServerWarning(getAgentDir(), ctx.cwd, ctx.isProjectTrusted(), registered)) {
|
|
55
|
+
ctx.ui.notify("gentle-shell-codegraph-subagents: CodeGraph MCP server was not found in the current configuration. Subagents may not have direct access; configure the server to enable it.", "warning");
|
|
56
|
+
}
|
|
57
|
+
const exposure = readExposureWarning(getAgentDir(), ctx.cwd, ctx.isProjectTrusted());
|
|
58
|
+
if (exposure === "hidden-with-direct") ctx.ui.notify("gentle-shell-codegraph-subagents: CodeGraph server is hidden, but its direct explore exception is configured to expose that tool. Your settings were preserved.", "warning");
|
|
59
|
+
else if (exposure) ctx.ui.notify(`gentle-shell-codegraph-subagents: CodeGraph explore is configured as ${exposure}; direct subagent access may be unavailable. Your setting was preserved.`, "warning");
|
|
60
|
+
} catch {
|
|
61
|
+
// Never block a session over this.
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Only the explorer child: hand it `codegraph explore` for its task before its first model call.
|
|
66
|
+
// Without .codegraph, the CLI, or in time, the run goes on exactly as before.
|
|
67
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
68
|
+
try {
|
|
69
|
+
const alreadySent = firstPromptSeen || ctx.sessionManager.getBranch().some((entry) => entry.type === "custom_message" && entry.customType === SKILL_REMINDER_TYPE);
|
|
70
|
+
const reminder = chooseSkillReminder({
|
|
71
|
+
expectedPath: join(import.meta.dirname, "..", "skills", "codegraph-workflow", "SKILL.md"),
|
|
72
|
+
skills: event.systemPromptOptions?.skills ?? [],
|
|
73
|
+
child: process.env.GENTLE_PI_AGENTS_CHILD === "1",
|
|
74
|
+
systemPrompt: event.systemPrompt,
|
|
75
|
+
alreadySent,
|
|
76
|
+
isUserPrompt: event.messageType === "user",
|
|
77
|
+
});
|
|
78
|
+
if (event.messageType === "user") firstPromptSeen = true;
|
|
79
|
+
const prompt = event.messageType === "user" ? event.prompt : undefined;
|
|
80
|
+
const content = await prefetchForExplorer({
|
|
81
|
+
env: process.env,
|
|
82
|
+
prompt,
|
|
83
|
+
cwd: process.cwd(),
|
|
84
|
+
exists: existsSync,
|
|
85
|
+
initRunning: (cwd) => realInitLock.isHeld(cwd),
|
|
86
|
+
explore: (query, cwd) =>
|
|
87
|
+
exploreDaemonFirst(query, cwd, {
|
|
88
|
+
daemon: (q, c) => exploreViaDaemon(c, q, PREFETCH_TIMEOUT_MS),
|
|
89
|
+
cli: exploreWithCodegraph,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
if (reminder) return { message: { customType: SKILL_REMINDER_TYPE, content: [reminder, content].filter(Boolean).join("\n\n"), display: false } };
|
|
93
|
+
if (content === undefined) return undefined;
|
|
94
|
+
return { message: { customType: "codegraph-prefetch", content, display: false } };
|
|
95
|
+
} catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@zhalker/gentle-shell-codegraph-subagents",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@zhalker/gentle-shell-codegraph-subagents",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Pi package that keeps CodeGraph reachable from gentle-shell subagents: normalizes agent tool lists and the codegraph MCP exposure on every session start, and pre-fetches CodeGraph for the explorer.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "zhalker",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi",
|
|
11
|
+
"codegraph",
|
|
12
|
+
"subagents",
|
|
13
|
+
"gentle-shell",
|
|
14
|
+
"gentle-ai",
|
|
15
|
+
"mcp"
|
|
16
|
+
],
|
|
17
|
+
"files": [
|
|
18
|
+
"extensions",
|
|
19
|
+
"src",
|
|
20
|
+
"skills",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"pi": {
|
|
25
|
+
"extensions": [
|
|
26
|
+
"./extensions/index.ts"
|
|
27
|
+
],
|
|
28
|
+
"skills": [
|
|
29
|
+
"./skills"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "node --test \"test/*.test.ts\"",
|
|
34
|
+
"prepublishOnly": "npm test"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codegraph-workflow
|
|
3
|
+
description: "Trigger: investigate, analyze, refactor, fix, or review code. Use CodeGraph to navigate indexed code before broad symbol searches."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CodeGraph workflow
|
|
7
|
+
|
|
8
|
+
## Activation Contract
|
|
9
|
+
|
|
10
|
+
Use this workflow for code investigation, analysis, refactoring, fixes,
|
|
11
|
+
or review. For unrelated tasks, do not invoke CodeGraph just because
|
|
12
|
+
this skill is available.
|
|
13
|
+
|
|
14
|
+
## Hard Rules
|
|
15
|
+
|
|
16
|
+
- When an applicable index and a CodeGraph tool are available, use
|
|
17
|
+
CodeGraph before broad symbol searches.
|
|
18
|
+
- Do not run `codegraph index` before every query.
|
|
19
|
+
- Do not initialize or update an index independently of the package's
|
|
20
|
+
startup policy or explicit user authorization.
|
|
21
|
+
- Treat CodeGraph results as navigation hints, not authoritative source.
|
|
22
|
+
Confirm relevant behavior and findings against the source files.
|
|
23
|
+
|
|
24
|
+
## Decision Gates
|
|
25
|
+
|
|
26
|
+
- For exact strings, filenames, configuration, or a known file, use the
|
|
27
|
+
appropriate file or search tool directly.
|
|
28
|
+
- If CodeGraph is unavailable, fails, or has no applicable index, continue
|
|
29
|
+
with the available read and search tools. Do not repeatedly retry it.
|
|
30
|
+
- If indexing or synchronization is still running, do not assume the
|
|
31
|
+
results are current. Do not start another index operation.
|
|
32
|
+
- When working across projects or worktrees, use only an index and a
|
|
33
|
+
project path the current tool and the user's scope permit.
|
|
34
|
+
|
|
35
|
+
## Delegation
|
|
36
|
+
|
|
37
|
+
- If a subagent cannot use CodeGraph, the parent may query it and include
|
|
38
|
+
relevant findings and paths in the handoff.
|
|
39
|
+
- Respect each subagent's tool restrictions. Do not require `bash` from
|
|
40
|
+
a read-only explorer.
|
|
41
|
+
|
|
42
|
+
## Output Contract
|
|
43
|
+
|
|
44
|
+
When relevant, report unavailable or potentially stale index evidence and
|
|
45
|
+
distinguish CodeGraph findings from behavior verified in source.
|
package/src/agents.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { WORKER_MARKER } from "./skill-reminder.ts";
|
|
2
|
+
|
|
3
|
+
export const CODEGRAPH_MCP_TOOL = "mcp__codegraph__codegraph_explore";
|
|
4
|
+
|
|
5
|
+
const EXPLORER = "gentle-ai-explore";
|
|
6
|
+
const AGENTS_WITH_MCP_TOOL = new Set([
|
|
7
|
+
"gentle-ai-explore",
|
|
8
|
+
"gentle-ai-verify",
|
|
9
|
+
"gentle-ai-worker",
|
|
10
|
+
"jd-fix-agent",
|
|
11
|
+
"jd-judge-a",
|
|
12
|
+
"jd-judge-b",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const FRONTMATTER = /^(---\r?\n)([\s\S]*?)(\r?\n---)([\s\S]*)$/;
|
|
16
|
+
const GENTLE_AI_BLOCK = /<!-- gentle-ai:pi-codegraph-(?:tool|guidance) -->[\s\S]*?<!-- \/gentle-ai:pi-codegraph -->/g;
|
|
17
|
+
const MARKER = "\u0000";
|
|
18
|
+
|
|
19
|
+
function unquote(value: string): string {
|
|
20
|
+
const v = value.trim();
|
|
21
|
+
return v.length >= 2 && (v[0] === '"' || v[0] === "'") && v[v.length - 1] === v[0] ? v.slice(1, -1) : v;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The gentle-pi agent files ship a `- "*": false` list entry; Pi reads it as a tool named `*": false`.
|
|
25
|
+
function isBrokenEntry(item: string): boolean {
|
|
26
|
+
return /\*["']?\s*:\s*false/.test(item);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const EXPLORER_STRUCTURAL_LINE = "- For structural questions, use the cwd-scoped `codegraph` tool";
|
|
30
|
+
const EXPLORER_INDEX_LINE = "- `codegraph` may create or update only the current workspace `.codegraph/` index";
|
|
31
|
+
const EXPLORER_READ_ONLY_PHRASE = "Other than the explicit `.codegraph/` index exception, read and search only.";
|
|
32
|
+
|
|
33
|
+
// The explorer loses its `codegraph` extension tool, so the lines that tell it to use or initialize that tool would send it nowhere.
|
|
34
|
+
function rewriteExplorerInstructions(body: string, eol: string): string {
|
|
35
|
+
if (!body.includes("codegraph")) return body;
|
|
36
|
+
const out: string[] = [];
|
|
37
|
+
for (const line of body.split(eol)) {
|
|
38
|
+
if (line.startsWith(EXPLORER_STRUCTURAL_LINE)) {
|
|
39
|
+
out.push(`- For structural questions, use the \`${CODEGRAPH_MCP_TOOL}\` tool before broad filesystem searches. It reads the existing \`.codegraph/\` index and cannot create one; when the project has none, use \`read\`, \`grep\`, and \`find\` and say so in the handoff.`);
|
|
40
|
+
} else if (line.startsWith(EXPLORER_INDEX_LINE)) {
|
|
41
|
+
continue;
|
|
42
|
+
} else {
|
|
43
|
+
out.push(line.replace(EXPLORER_READ_ONLY_PHRASE, "Read and search only."));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out.join(eol);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function removeBlocks(body: string, eol: string): string {
|
|
50
|
+
if (!body.includes("gentle-ai:pi-codegraph")) return body;
|
|
51
|
+
const marked = body.replace(GENTLE_AI_BLOCK, MARKER);
|
|
52
|
+
return marked.replace(/\s*\u0000(?:\s*\u0000)*\s*/g, (match, offset: number) => {
|
|
53
|
+
if (offset === 0) return "";
|
|
54
|
+
return offset + match.length >= marked.length ? eol : eol + eol;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
function editTools(front: string, eol: string): string {
|
|
60
|
+
const lines = front.split(/\r?\n/);
|
|
61
|
+
const nameLine = lines.find((line) => /^name:/.test(line));
|
|
62
|
+
const name = nameLine ? unquote(nameLine.slice("name:".length)) : "";
|
|
63
|
+
const at = lines.findIndex((line) => /^tools:/.test(line));
|
|
64
|
+
if (at === -1) return front;
|
|
65
|
+
|
|
66
|
+
const rest = lines[at].slice("tools:".length).trim();
|
|
67
|
+
let style: "bracket" | "csv" | "list";
|
|
68
|
+
let items: string[];
|
|
69
|
+
let consumed = 1;
|
|
70
|
+
let indent = " ";
|
|
71
|
+
if (rest.startsWith("[")) {
|
|
72
|
+
style = "bracket";
|
|
73
|
+
items = rest.replace(/^\[/, "").replace(/\]$/, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
74
|
+
} else if (rest.length > 0) {
|
|
75
|
+
style = "csv";
|
|
76
|
+
items = rest.split(",").map((s) => s.trim()).filter(Boolean);
|
|
77
|
+
} else {
|
|
78
|
+
style = "list";
|
|
79
|
+
items = [];
|
|
80
|
+
for (let i = at + 1; i < lines.length; i++) {
|
|
81
|
+
const m = /^(\s*)-\s+(.*)$/.exec(lines[i]);
|
|
82
|
+
if (!m) break;
|
|
83
|
+
if (items.length === 0) indent = m[1];
|
|
84
|
+
items.push(m[2].trim());
|
|
85
|
+
consumed++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (items.length === 0) return front;
|
|
89
|
+
|
|
90
|
+
let next = items.filter((item) => unquote(item) !== "mcp" && !isBrokenEntry(item));
|
|
91
|
+
if (name === EXPLORER) next = next.filter((item) => unquote(item) !== "codegraph");
|
|
92
|
+
if (AGENTS_WITH_MCP_TOOL.has(name) && !next.some((item) => unquote(item) === CODEGRAPH_MCP_TOOL)) next.push(CODEGRAPH_MCP_TOOL);
|
|
93
|
+
// An empty list would hand the agent Pi's default tools; keep what was there instead.
|
|
94
|
+
if (next.length === 0 || next.join("\n") === items.join("\n")) return front;
|
|
95
|
+
|
|
96
|
+
const replacement =
|
|
97
|
+
style === "list" ? ["tools:", ...next.map((item) => `${indent}- ${item}`)]
|
|
98
|
+
: style === "bracket" ? [`tools: [${next.join(", ")}]`]
|
|
99
|
+
: [`tools: ${next.join(", ")}`];
|
|
100
|
+
return [...lines.slice(0, at), ...replacement, ...lines.slice(at + consumed)].join(eol);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Returns the agent definition with the CodeGraph wiring applied; identical text when nothing changes. */
|
|
104
|
+
export function normalizeAgent(text: string): string {
|
|
105
|
+
const match = FRONTMATTER.exec(text);
|
|
106
|
+
if (!match) return text;
|
|
107
|
+
const [, open, front, close, body] = match;
|
|
108
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
109
|
+
const name = /^name:\s*(.+)$/m.exec(front)?.[1];
|
|
110
|
+
const cleaned = removeBlocks(body, eol);
|
|
111
|
+
let rewritten = name !== undefined && unquote(name) === EXPLORER ? rewriteExplorerInstructions(cleaned, eol) : cleaned;
|
|
112
|
+
if (name !== undefined && unquote(name) === "gentle-ai-worker" && !rewritten.includes(WORKER_MARKER)) {
|
|
113
|
+
rewritten = rewritten.trimEnd() + eol + eol + WORKER_MARKER + eol;
|
|
114
|
+
}
|
|
115
|
+
return open + editTools(front, eol) + close + rewritten;
|
|
116
|
+
}
|
package/src/apply.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { normalizeAgent } from "./agents.ts";
|
|
4
|
+
import { inspectEffectiveMcpExposure, isCodegraphServerMissing, normalizeMcpConfig } from "./mcp.ts";
|
|
5
|
+
|
|
6
|
+
export interface ApplyResult {
|
|
7
|
+
/** Files that were rewritten. */
|
|
8
|
+
changed: string[];
|
|
9
|
+
/** One message per file that could not be processed. */
|
|
10
|
+
errors: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const BACKUP_SUFFIX = ".pre-codegraph-subagents.bak";
|
|
14
|
+
|
|
15
|
+
/** Reads the effective file settings only; untrusted project configuration is not loaded by Pi. */
|
|
16
|
+
export function readExposureWarning(agentDir: string, cwd: string, projectTrusted: boolean): string | undefined {
|
|
17
|
+
const read = (path: string): string | undefined => {
|
|
18
|
+
try { return readFileSync(path, "utf8"); } catch { return undefined; }
|
|
19
|
+
};
|
|
20
|
+
const global = read(join(agentDir, "mcp.json")) ?? "{}";
|
|
21
|
+
const project = projectTrusted ? read(join(cwd, ".pi", "mcp.json")) : undefined;
|
|
22
|
+
return inspectEffectiveMcpExposure(global, project);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Reports absent file configuration without creating it; registered servers also count. */
|
|
26
|
+
export function readMissingServerWarning(agentDir: string, cwd: string, projectTrusted: boolean, registered = false): boolean {
|
|
27
|
+
const read = (path: string): string | undefined => {
|
|
28
|
+
try { return readFileSync(path, "utf8"); } catch (error) {
|
|
29
|
+
return codeOf(error) === "ENOENT" ? undefined : "invalid";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const global = read(join(agentDir, "mcp.json")) ?? "{}";
|
|
33
|
+
const project = projectTrusted ? read(join(cwd, ".pi", "mcp.json")) : undefined;
|
|
34
|
+
return isCodegraphServerMissing(global, project, registered);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Write beside the target and rename, so a reader never sees a half-written file.
|
|
38
|
+
function writeAtomic(file: string, text: string): void {
|
|
39
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
40
|
+
try {
|
|
41
|
+
writeFileSync(temp, text);
|
|
42
|
+
renameSync(temp, file);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
try { unlinkSync(temp); } catch { /* nothing to clean */ }
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Several sessions can start at the same moment and rewrite the same files: on Windows a rename over a file
|
|
50
|
+
// another session is replacing fails with one of these, and a retry finds the work already done.
|
|
51
|
+
const TRANSIENT_CODES = new Set(["EPERM", "EBUSY", "EACCES", "ENOENT"]);
|
|
52
|
+
const ATTEMPTS = 8;
|
|
53
|
+
|
|
54
|
+
const codeOf = (error: unknown): string | undefined => (error as { code?: string } | null)?.code;
|
|
55
|
+
|
|
56
|
+
function pauseBriefly(): void {
|
|
57
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10 + Math.floor(Math.random() * 30));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The backup holds the original, so whoever gets there first wins and the others leave it alone.
|
|
61
|
+
function saveBackupOnce(file: string, original: string): void {
|
|
62
|
+
try {
|
|
63
|
+
writeFileSync(file + BACKUP_SUFFIX, original, { flag: "wx" });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (codeOf(error) !== "EEXIST") throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function alreadyNormalized(file: string, normalize: (text: string) => string): boolean {
|
|
70
|
+
try {
|
|
71
|
+
const text = readFileSync(file, "utf8");
|
|
72
|
+
return normalize(text) === text;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function rewrite(file: string, normalize: (text: string) => string, result: ApplyResult): void {
|
|
79
|
+
let failure: unknown;
|
|
80
|
+
for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
|
|
81
|
+
try {
|
|
82
|
+
const before = readFileSync(file, "utf8");
|
|
83
|
+
const after = normalize(before);
|
|
84
|
+
// Already right: nothing to do, or another session just did it.
|
|
85
|
+
if (after === before) return;
|
|
86
|
+
saveBackupOnce(file, before);
|
|
87
|
+
writeAtomic(file, after);
|
|
88
|
+
result.changed.push(file);
|
|
89
|
+
return;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
failure = error;
|
|
92
|
+
if (!TRANSIENT_CODES.has(codeOf(error) ?? "")) break;
|
|
93
|
+
pauseBriefly();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (alreadyNormalized(file, normalize)) return;
|
|
97
|
+
result.errors.push(`${file}: ${failure instanceof Error ? failure.message : String(failure)}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Applies the CodeGraph wiring to the global agent definitions and to mcp.json
|
|
102
|
+
* under `agentDir`. Idempotent and never throws: failures are returned in `errors`.
|
|
103
|
+
*/
|
|
104
|
+
export function applyCodegraphSubagents(agentDir: string): ApplyResult {
|
|
105
|
+
const result: ApplyResult = { changed: [], errors: [] };
|
|
106
|
+
for (const folder of ["agents", "subagents"]) {
|
|
107
|
+
const dir = join(agentDir, folder);
|
|
108
|
+
let entries: string[];
|
|
109
|
+
try {
|
|
110
|
+
entries = readdirSync(dir).filter((entry) => entry.toLowerCase().endsWith(".md")).sort();
|
|
111
|
+
} catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
for (const entry of entries) rewrite(join(dir, entry), normalizeAgent, result);
|
|
115
|
+
}
|
|
116
|
+
const mcp = join(agentDir, "mcp.json");
|
|
117
|
+
if (existsSync(mcp)) rewrite(mcp, normalizeMcpConfig, result);
|
|
118
|
+
return result;
|
|
119
|
+
}
|