@selesai/code 0.13.9 → 0.13.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/core/agent-session.d.ts +6 -0
- package/dist/core/agent-session.js +5 -3
- package/dist/extensions/handoff-new.test.ts +26 -1
- package/dist/extensions/handoff-new.ts +5 -0
- package/dist/extensions/pi-subagents/README.md +0 -6
- package/dist/extensions/pi-subagents/docs/agents.md +1 -114
- package/dist/extensions/pi-subagents/src/agents/builtin-names.ts +1 -12
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +3 -19
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-contract.ts +8 -37
- package/dist/extensions/pi-subagents/src/shared/types.ts +2 -2
- package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +2 -38
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +0 -49
- package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +0 -18
- package/dist/modes/rpc/rpc-client.d.ts +51 -7
- package/dist/modes/rpc/rpc-client.js +57 -11
- package/dist/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/modes/rpc/rpc-mode.js +199 -3
- package/dist/modes/rpc/rpc-types.d.ts +91 -1
- package/dist/skills/improve-codebase/SKILL.md +1 -1
- package/dist/skills/planger/SKILL.md +1 -1
- package/dist/skills/workflow/SKILL.md +9 -15
- package/docs/rpc.md +170 -0
- package/package.json +1 -1
- package/dist/extensions/pi-subagents/agents/architect.md +0 -170
- package/dist/extensions/pi-subagents/agents/builder.md +0 -37
- package/dist/extensions/pi-subagents/agents/claude-code-writer.md +0 -15
- package/dist/extensions/pi-subagents/agents/claude-code.md +0 -15
- package/dist/extensions/pi-subagents/agents/codex-exec-writer.md +0 -15
- package/dist/extensions/pi-subagents/agents/codex-exec.md +0 -15
- package/dist/extensions/pi-subagents/agents/commentator.md +0 -37
- package/dist/extensions/pi-subagents/agents/cursor-agent-writer.md +0 -14
- package/dist/extensions/pi-subagents/agents/cursor-agent.md +0 -14
- package/dist/extensions/pi-subagents/agents/explorer.md +0 -32
- package/dist/extensions/pi-subagents/agents/recapper.md +0 -31
- package/dist/extensions/pi-subagents/src/runs/shared/claude-code-adapter.ts +0 -129
- package/dist/extensions/pi-subagents/src/runs/shared/codex-exec-adapter.ts +0 -129
- package/dist/extensions/pi-subagents/src/runs/shared/cursor-agent-adapter.ts +0 -114
- package/dist/extensions/pi-subagents/test/integration/claude-code-smoke.test.ts +0 -52
- package/dist/extensions/pi-subagents/test/integration/claude-code-writer-smoke.test.ts +0 -55
- package/dist/extensions/pi-subagents/test/integration/codex-exec-smoke.test.ts +0 -53
- package/dist/extensions/pi-subagents/test/integration/codex-exec-writer-smoke.test.ts +0 -57
- package/dist/extensions/pi-subagents/test/integration/cursor-agent-smoke.test.ts +0 -59
- package/dist/extensions/pi-subagents/test/integration/cursor-agent-writer-smoke.test.ts +0 -62
- package/dist/extensions/pi-subagents/test/unit/claude-code-adapter.test.ts +0 -245
- package/dist/extensions/pi-subagents/test/unit/codex-exec-adapter.test.ts +0 -192
- package/dist/extensions/pi-subagents/test/unit/cursor-agent-adapter.test.ts +0 -259
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.10] - 2026-09-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **RPC rev-3 interactive-TUI parity batch.** New RPC commands `navigate_tree`, `list_sessions`, `rename_session`, `delete_session`, and `set_session_models`; new option fields for `prompt.queueWhileCompacting`, `cycle_model.direction`, `get_available_models.refresh`, and theme passthrough for `export_html`/`share_gist`.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **handoff-new carries the session name.** The new session started by `/handoff-new` (and RPC `handoff_new`) now inherits the current session's name as its starting name.
|
|
12
|
+
- **pi-subagents uses only built-in agents.** The bundled pi-subagents extension now ships only the built-in agents (`advisor`, `delegate`, `oracle`, `researcher`, `reviewer`, `scout`, `worker`) and drops the custom agent definitions and Claude Code / Codex / Cursor agent adapters, so subagents work in any host without external CLIs.
|
|
13
|
+
|
|
5
14
|
## [0.13.9] - 2026-09-01
|
|
6
15
|
|
|
7
16
|
### Changed
|
|
@@ -108,6 +108,12 @@ export type AgentSessionEvent = Exclude<AgentEvent, {
|
|
|
108
108
|
delta: string;
|
|
109
109
|
} | {
|
|
110
110
|
type: "image_captioning_start";
|
|
111
|
+
} | {
|
|
112
|
+
type: "session_tree";
|
|
113
|
+
newLeafId: string | null;
|
|
114
|
+
oldLeafId: string | null;
|
|
115
|
+
summaryEntry?: BranchSummaryEntry | null;
|
|
116
|
+
fromExtension?: boolean;
|
|
111
117
|
} | {
|
|
112
118
|
type: "image_captioning_end";
|
|
113
119
|
ok: boolean;
|
|
@@ -2793,14 +2793,16 @@ export class AgentSession {
|
|
|
2793
2793
|
// Update agent state
|
|
2794
2794
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
2795
2795
|
this.agent.state.messages = sessionContext.messages;
|
|
2796
|
-
// Emit session_tree event
|
|
2797
|
-
|
|
2796
|
+
// Emit session_tree event (extension runners + RPC stdout subscribers)
|
|
2797
|
+
const treeEvent = {
|
|
2798
2798
|
type: "session_tree",
|
|
2799
2799
|
newLeafId: this.sessionManager.getLeafId(),
|
|
2800
2800
|
oldLeafId,
|
|
2801
2801
|
summaryEntry,
|
|
2802
2802
|
fromExtension: summaryText ? fromExtension : undefined,
|
|
2803
|
-
}
|
|
2803
|
+
};
|
|
2804
|
+
await this._extensionRunner.emit(treeEvent);
|
|
2805
|
+
this._emit(treeEvent);
|
|
2804
2806
|
// Emit to custom tools
|
|
2805
2807
|
return { editorText, cancelled: false, summaryEntry };
|
|
2806
2808
|
}
|
|
@@ -138,11 +138,13 @@ describe("handoff-new extension", () => {
|
|
|
138
138
|
model?: any;
|
|
139
139
|
customResult?: string | null;
|
|
140
140
|
mode?: string;
|
|
141
|
+
sessionName?: string;
|
|
141
142
|
} = {}) {
|
|
142
|
-
const calls: { newSession: any; userMessage: string | null; notify: { msg: string; kind: string }[] } = {
|
|
143
|
+
const calls: { newSession: any; userMessage: string | null; notify: { msg: string; kind: string }[]; setupName: string | null } = {
|
|
143
144
|
newSession: null,
|
|
144
145
|
userMessage: null,
|
|
145
146
|
notify: [],
|
|
147
|
+
setupName: null,
|
|
146
148
|
};
|
|
147
149
|
const ctx: any = {
|
|
148
150
|
mode: opts.mode ?? "tui",
|
|
@@ -150,6 +152,7 @@ describe("handoff-new extension", () => {
|
|
|
150
152
|
sessionManager: {
|
|
151
153
|
getBranch: () => (opts.branch === undefined ? [userEntry("we decided to build X")] : opts.branch),
|
|
152
154
|
getSessionFile: () => "/tmp/old.json",
|
|
155
|
+
getSessionName: () => opts.sessionName ?? undefined,
|
|
153
156
|
},
|
|
154
157
|
modelRegistry: {
|
|
155
158
|
async getApiKeyAndHeaders() {
|
|
@@ -165,6 +168,12 @@ describe("handoff-new extension", () => {
|
|
|
165
168
|
},
|
|
166
169
|
async newSession(options: any) {
|
|
167
170
|
calls.newSession = options;
|
|
171
|
+
if (options?.setup) {
|
|
172
|
+
const mgr: any = { named: [] as string[] };
|
|
173
|
+
mgr.appendSessionInfo = (name: string) => mgr.named.push(name);
|
|
174
|
+
await options.setup(mgr);
|
|
175
|
+
calls.setupName = mgr.named.at(-1) ?? null;
|
|
176
|
+
}
|
|
168
177
|
if (options?.withSession) {
|
|
169
178
|
await options.withSession(ctx);
|
|
170
179
|
}
|
|
@@ -183,6 +192,20 @@ describe("handoff-new extension", () => {
|
|
|
183
192
|
expect(calls.newSession.cancelled).toBeUndefined();
|
|
184
193
|
});
|
|
185
194
|
|
|
195
|
+
it("session name carries over to the new session", async () => {
|
|
196
|
+
const { commands } = createPiHarness();
|
|
197
|
+
const { ctx, calls } = createCtx({ sessionName: "db migration" });
|
|
198
|
+
await commands.get("handoff-new")!.handler("goal", ctx);
|
|
199
|
+
expect(calls.setupName).toBe("db migration");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("no session name leaves the new session unnamed for auto-naming", async () => {
|
|
203
|
+
const { commands } = createPiHarness();
|
|
204
|
+
const { ctx, calls } = createCtx({ sessionName: undefined });
|
|
205
|
+
await commands.get("handoff-new")!.handler("goal", ctx);
|
|
206
|
+
expect(calls.setupName).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
186
209
|
it("no goal arg uses DEFAULT_GOAL", async () => {
|
|
187
210
|
const { commands } = createPiHarness();
|
|
188
211
|
const { ctx, calls } = createCtx();
|
|
@@ -275,6 +298,7 @@ describe("handoff-new extension", () => {
|
|
|
275
298
|
sessionManager: {
|
|
276
299
|
getBranch: () => [userEntry("we decided to build X")],
|
|
277
300
|
getSessionFile: () => "/tmp/old.json",
|
|
301
|
+
getSessionName: () => undefined,
|
|
278
302
|
},
|
|
279
303
|
modelRegistry: {
|
|
280
304
|
async getApiKeyAndHeaders() {
|
|
@@ -372,6 +396,7 @@ describe("handoff-new extension", () => {
|
|
|
372
396
|
sessionManager: {
|
|
373
397
|
getBranch: () => [userEntry("we decided to build X")],
|
|
374
398
|
getSessionFile: () => "/tmp/old.json",
|
|
399
|
+
getSessionName: () => undefined,
|
|
375
400
|
},
|
|
376
401
|
modelRegistry: {
|
|
377
402
|
async getApiKeyAndHeaders() {
|
|
@@ -110,6 +110,11 @@ async function handoffNew(args: string, ctx: ExtensionCommandContext) {
|
|
|
110
110
|
|
|
111
111
|
const newSessionResult = await ctx.newSession({
|
|
112
112
|
parentSession: currentSessionFile,
|
|
113
|
+
// Carry the current session's name over to the new session as the starting point.
|
|
114
|
+
setup: (replacementManager) => {
|
|
115
|
+
const name = ctx.sessionManager.getSessionName();
|
|
116
|
+
if (name) replacementManager.appendSessionInfo(name);
|
|
117
|
+
},
|
|
113
118
|
withSession: async (replacementCtx) => {
|
|
114
119
|
await replacementCtx.sendUserMessage(result);
|
|
115
120
|
},
|
|
@@ -62,12 +62,6 @@ The extension ships with agents you can use immediately:
|
|
|
62
62
|
| `reviewer` | Code review and small fixes against the task/plan, tests, edge cases, and simplicity. |
|
|
63
63
|
| `oracle` | A second opinion before acting. Challenges assumptions without editing. |
|
|
64
64
|
| `delegate` | A lightweight general delegate that behaves close to the parent session. |
|
|
65
|
-
| `architect` | Read-only architecture and implementation planning. |
|
|
66
|
-
| `builder` | Mutation-capable scoped implementation. |
|
|
67
|
-
| `commentator` | Read-only evidence-based review. |
|
|
68
|
-
| `explorer` | Read-only local codebase reconnaissance. |
|
|
69
|
-
| `recapper` | Read-only handoff and context synthesis. |
|
|
70
|
-
| `codex-exec`, `claude-code`, `cursor-agent` | External CLI runners with their own adapter contracts. |
|
|
71
65
|
|
|
72
66
|
Rule of thumb: `scout` before you understand the code, `researcher` before you trust external facts, `worker` to implement, `reviewer` to check, and `oracle` when the decision itself feels risky.
|
|
73
67
|
|
|
@@ -59,120 +59,7 @@ The Pi async run remains the source of truth for status, artifacts, wake/wait, m
|
|
|
59
59
|
|
|
60
60
|
External CLI agents use their own runner contract. Do not pass native Pi child options such as model override, structured output, acceptance/agent contract, tool budgets, fast mode, fork context, skills, or native Pi tools unless the adapter explicitly implements them.
|
|
61
61
|
|
|
62
|
-
The
|
|
63
|
-
|
|
64
|
-
| Profile | Access | Sandbox |
|
|
65
|
-
|---|---|---|
|
|
66
|
-
| `codex-exec` | Read-only analysis | `read-only` |
|
|
67
|
-
| `codex-exec-writer` | Explicit workspace edits | `workspace-write` |
|
|
68
|
-
|
|
69
|
-
Neither adapter uses full access, approval or sandbox bypasses, automatic approval review, or additional writable roots. User profiles cannot add argv. The `codex-exec` selection identity is reserved for the read-only adapter.
|
|
70
|
-
|
|
71
|
-
Run it asynchronously:
|
|
72
|
-
|
|
73
|
-
```text
|
|
74
|
-
Use codex-exec to analyze this change without editing files.
|
|
75
|
-
|
|
76
|
-
Use codex-exec-writer to make the requested workspace changes.
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
The adapter validates `codex --version` and `codex exec --help` only when a run launches. Discovery, list, status, and native Pi launches do not probe Codex. JSONL, stderr, and stdout are untrusted. A run succeeds only after bounded valid JSONL contains one `turn.completed` event and the bounded final-message artifact is present.
|
|
80
|
-
|
|
81
|
-
Maintainers can collect real smoke evidence without making it part of the normal test suite:
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
SELESAI_SUBAGENTS_CODEX_EXEC_SMOKE=1 \
|
|
85
|
-
SELESAI_SUBAGENTS_CODEX_EXEC_SMOKE_REPORT=/tmp/pi-subagents-codex-exec-smoke.json \
|
|
86
|
-
node --experimental-strip-types --import ./test/support/register-loader.mjs \
|
|
87
|
-
--test test/integration/codex-exec-smoke.test.ts
|
|
88
|
-
|
|
89
|
-
SELESAI_SUBAGENTS_CODEX_EXEC_WRITER_SMOKE=1 \
|
|
90
|
-
SELESAI_SUBAGENTS_CODEX_EXEC_WRITER_SMOKE_REPORT=/tmp/pi-subagents-codex-exec-writer-smoke.json \
|
|
91
|
-
node --experimental-strip-types --import ./test/support/register-loader.mjs \
|
|
92
|
-
--test test/integration/codex-exec-writer-smoke.test.ts
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
The read-only smoke must report `writeCanaryExists: false`. The writer smoke must report `writeCanaryMatches: true`. Both reports include startup duration and terminal proof without raw protocol output, prompts, or credentials.
|
|
96
|
-
|
|
97
|
-
The built-in `claude-code` and `claude-code-writer` profiles are the supported Claude Code one-shot modes. Both require an installed Claude Code CLI that is already authenticated through its normal local login. Claude Code 2.1.150 needs the user setting source for normal OAuth/keychain authentication, so both adapters load user settings but exclude project and local settings. User-level Claude Code settings and hooks are therefore an operator-trusted prerequisite. Review or disable unsafe user hooks before using either profile.
|
|
98
|
-
|
|
99
|
-
| Profile | Access | Permission mode | Built-in tools |
|
|
100
|
-
|---|---|---|---|
|
|
101
|
-
| `claude-code` | Handoff-only read-only advice | `plan` | none |
|
|
102
|
-
| `claude-code-writer` | Explicit workspace file edits | `acceptEdits` | `Read,Write,Edit,Glob,Grep` |
|
|
103
|
-
|
|
104
|
-
Both adapters own `claude -p` argv with stream JSON, strict empty MCP configuration, user-only setting sources, no session persistence, disabled slash commands, and disabled Chrome integration. The writer mode does not include Bash or any permission bypass. Neither mode uses `--bare`, which does not read normal OAuth/keychain authentication. Neither mode requires `--safe-mode`, which is absent from the installed 2.1.150 help. User profiles cannot add argv. Selecting the code-owned `claude-code-writer` adapter identity is the only way to opt into its write tools; the read-only adapter cannot be widened with user argv.
|
|
105
|
-
|
|
106
|
-
Run it asynchronously:
|
|
107
|
-
|
|
108
|
-
```text
|
|
109
|
-
Use claude-code to analyze this handoff without editing files.
|
|
110
|
-
|
|
111
|
-
Use claude-code-writer to make the requested file changes.
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
The adapter validates `claude --version` and `claude --help` only when a run launches. Discovery, list, status, and native Pi launches do not probe Claude Code or authentication. JSONL, stderr, and stdout are untrusted. A run succeeds only after bounded valid JSONL contains exactly one successful terminal `result` with non-empty final text. Missing or revoked local authentication, limit stops, malformed JSON, duplicate terminal results, and EOF before a terminal result fail closed.
|
|
115
|
-
|
|
116
|
-
Maintainers can opt in to separate read-only and writer canaries:
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
SELESAI_SUBAGENTS_CLAUDE_CODE_SMOKE=1 \
|
|
120
|
-
SELESAI_SUBAGENTS_CLAUDE_CODE_SMOKE_REPORT=/tmp/pi-subagents-claude-code-smoke.json \
|
|
121
|
-
node --experimental-strip-types --import ./test/support/register-loader.mjs \
|
|
122
|
-
--test test/integration/claude-code-smoke.test.ts
|
|
123
|
-
|
|
124
|
-
SELESAI_SUBAGENTS_CLAUDE_CODE_WRITER_SMOKE=1 \
|
|
125
|
-
SELESAI_SUBAGENTS_CLAUDE_CODE_WRITER_SMOKE_REPORT=/tmp/pi-subagents-claude-code-writer-smoke.json \
|
|
126
|
-
node --experimental-strip-types --import ./test/support/register-loader.mjs \
|
|
127
|
-
--test test/integration/claude-code-writer-smoke.test.ts
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Both smoke reports record `authentication: "existing-cli-required"`, `settingSources: "user"`, and `userSettingsTrust: "required"` without recording credential details. For read-only, confirm `terminalState` is `completed` and `writeCanaryExists` is `false`. For writer, confirm `terminalState` is `completed` and `writeCanaryMatches` is `true`. `durationMs` records cold process time. If authentication is missing or revoked, repair the normal local Claude Code login and rerun the smoke. Reports do not contain raw protocol output or credentials.
|
|
131
|
-
|
|
132
|
-
The built-in `cursor-agent` and `cursor-agent-writer` profiles are the supported Cursor CLI one-shot modes. Both require an installed Cursor CLI and either `CURSOR_API_KEY` or an existing local login.
|
|
133
|
-
|
|
134
|
-
| Profile | Access | Cursor mode |
|
|
135
|
-
|---|---|---|
|
|
136
|
-
| `cursor-agent` | Read-only analysis | `ask` |
|
|
137
|
-
| `cursor-agent-writer` | Explicit workspace edits | non-interactive print |
|
|
138
|
-
|
|
139
|
-
Both adapters use stream JSON, the enabled sandbox, and the primary workspace. They write the full handoff to a private `0600` file in a private temporary directory. Process argv contains only a short instruction with that path. The temporary directory is added as a workspace root only when it is outside the primary workspace. The prompt file and directory are removed after completion, failure, or stop.
|
|
140
|
-
|
|
141
|
-
The adapters do not pass force, yolo, auto-review, MCP approval, plugin, session resume, continue, worktree, or workspace trust flags. User profiles cannot add argv or workspace roots. The `cursor-agent` selection identity is reserved for the read-only adapter.
|
|
142
|
-
|
|
143
|
-
Launch preflight validates `cursor-agent --version` and `cursor-agent --help` only when a run starts. Discovery, list, status, and native Pi launches do not execute Cursor or probe authentication. A run succeeds only when bounded valid JSONL ends with one successful `result` event that has non-empty final text. Error events, failed results, malformed JSON, output after the terminal event, and EOF before a result fail closed.
|
|
144
|
-
|
|
145
|
-
These headless smokes rely on saved workspace trust. Cursor documents no passive command that checks workspace trust, so the smoke cannot verify it before launch. The operator must use Cursor's interactive trust flow for the exact disposable workspace and the exact derived prompt directory, `<state-root>/external-0.cursor-prompt`. Keep that prompt directory after the trust step. It must be empty, owned by the operator who runs the smoke, and must not be a symlink. The harness preserves this directory but creates its private handoff with exclusive `0600` access and removes the handoff after every run. Repeat the trust setup if either exact path changes.
|
|
146
|
-
|
|
147
|
-
The smoke requires two existing, separate operator-managed directories and an explicit disposable-workspace attestation:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
export SELESAI_SUBAGENTS_CURSOR_SMOKE_WORKSPACE=/tmp/pi-subagents-cursor-smoke-workspace
|
|
151
|
-
export SELESAI_SUBAGENTS_CURSOR_SMOKE_STATE_ROOT=/tmp/pi-subagents-cursor-smoke-state
|
|
152
|
-
export SELESAI_SUBAGENTS_CURSOR_SMOKE_DISPOSABLE=1
|
|
153
|
-
mkdir -p "$SELESAI_SUBAGENTS_CURSOR_SMOKE_WORKSPACE" "$SELESAI_SUBAGENTS_CURSOR_SMOKE_STATE_ROOT"
|
|
154
|
-
mkdir -p "$SELESAI_SUBAGENTS_CURSOR_SMOKE_STATE_ROOT/external-0.cursor-prompt"
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Do not place a file at `pi-subagents-cursor-write-canary.txt` in the workspace or any file, including `handoff.txt`, in the prompt directory. The harness refuses the pre-existing canary and any non-empty prompt directory. It does not delete the workspace, state root, or operator-owned prompt directory. It removes only its canary and private handoff file.
|
|
158
|
-
|
|
159
|
-
Maintainers can then run separate read-only and writer canaries:
|
|
160
|
-
|
|
161
|
-
```bash
|
|
162
|
-
SELESAI_SUBAGENTS_CURSOR_AGENT_SMOKE=1 \
|
|
163
|
-
SELESAI_SUBAGENTS_CURSOR_AGENT_SMOKE_REPORT=/tmp/pi-subagents-cursor-agent-smoke.json \
|
|
164
|
-
node --experimental-strip-types --import ./test/support/register-loader.mjs \
|
|
165
|
-
--test test/integration/cursor-agent-smoke.test.ts
|
|
166
|
-
|
|
167
|
-
SELESAI_SUBAGENTS_CURSOR_AGENT_WRITER_SMOKE=1 \
|
|
168
|
-
SELESAI_SUBAGENTS_CURSOR_AGENT_WRITER_SMOKE_REPORT=/tmp/pi-subagents-cursor-agent-writer-smoke.json \
|
|
169
|
-
node --experimental-strip-types --import ./test/support/register-loader.mjs \
|
|
170
|
-
--test test/integration/cursor-agent-writer-smoke.test.ts
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
The read-only smoke must report `writeCanaryExists: false`. The writer smoke must report `writeCanaryMatches: true`. Both reports record `workspaceTrust: "operator-managed-saved"`, confirm that the external prompt root was added, and include startup duration and terminal proof without raw protocol output, prompts, or credentials. A trust-required error remains terminal; the harness does not retry with a trust, force, or yolo flag.
|
|
174
|
-
|
|
175
|
-
Native `oracle` runs inside Pi and can use its configured read tools. The Claude profiles send the assembled prompt to the local Claude Code CLI through stdin. An external-job agent sends the assembled prompt to its registered provider. Provider options and a prompt digest are persisted in Pi run state. The prompt text is delivered through the local host bridge to the provider and is not stored in the public result payload. Do not place secrets in advisory prompts unless the target provider is approved to receive them.
|
|
62
|
+
The bundled code-owned external adapters (`codex-exec`, `claude-code`, `cursor-agent` and their writer variants) were removed in favor of the built-in Pi agents. A custom agent with `runner.type: external-cli` (no adapter) still runs the generic one-shot stdin contract: `command` is executed with the prompt on stdin, and the bounded final output is treated as untrusted text.
|
|
176
63
|
|
|
177
64
|
### External-job state table
|
|
178
65
|
|
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
export const BUILTIN_AGENT_NAMES = [
|
|
2
|
-
"architect",
|
|
3
2
|
"advisor",
|
|
4
|
-
"builder",
|
|
5
|
-
"claude-code",
|
|
6
|
-
"claude-code-writer",
|
|
7
|
-
"codex-exec",
|
|
8
|
-
"codex-exec-writer",
|
|
9
|
-
"commentator",
|
|
10
|
-
"cursor-agent",
|
|
11
|
-
"cursor-agent-writer",
|
|
12
3
|
"delegate",
|
|
13
|
-
"explorer",
|
|
14
4
|
"oracle",
|
|
15
|
-
"recapper",
|
|
16
5
|
"researcher",
|
|
17
6
|
"reviewer",
|
|
18
7
|
"scout",
|
|
19
8
|
"worker",
|
|
20
|
-
] as const;
|
|
9
|
+
] as const;
|
|
@@ -5,7 +5,7 @@ import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
|
|
|
5
5
|
|
|
6
6
|
const CUSTOM_TOOL_DESCRIPTION_FILE = "subagent-tool-description.md";
|
|
7
7
|
const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
|
|
8
|
-
const EXTERNAL_CLI_RUNNER_GUIDANCE = "External CLI agents (
|
|
8
|
+
const EXTERNAL_CLI_RUNNER_GUIDANCE = "External CLI agents (custom external-cli runners; there are no bundled adapter agents) use their own runner contract and do not support native Pi child options such as model override, structured output, acceptance/agent contract, tool budget, fast mode, fork context, skills, or native Pi tools unless the runner explicitly implements them.";
|
|
9
9
|
const WORKFLOW_RESUME_KEY_GUIDANCE = "Each workflow key identifies one result lane: use a new stable workflow key for every distinct retained resume pass; same-key calls are reused only when launch parameters are identical, and incompatible parameters are rejected.";
|
|
10
10
|
const WORKFLOW_OUTPUT_BINDING_GUIDANCE = "For durable workflow child files, set output on runs.run/runs.all; task filename prose is not an output declaration, and return the child's outputReference, outputPathMapping, or artifactPaths instead of inventing a literal path.";
|
|
11
11
|
const WORKFLOW_LANES_GUIDANCE = "For bounded parallel sequential chains, use runs.lanes([{key,stages:[{key,agent,task},{key,resume:'previous',task},...]}]); first stages run together, later stages sequence per lane, and the bounded board reports lane-local failures. Only an explicit structuredOutput.verdict === 'blocked' blocks a successful stage; reviewer prose is not parsed.";
|
|
@@ -144,9 +144,6 @@ import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
|
|
|
144
144
|
import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, PI_AGGREGATE_EVENT_PROJECTOR, projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleState, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
|
|
145
145
|
import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
|
|
146
146
|
import { buildExternalCliPrompt, runExternalCli } from "../shared/external-cli-runner.ts";
|
|
147
|
-
import { resolveClaudeCodeLaunch } from "../shared/claude-code-adapter.ts";
|
|
148
|
-
import { resolveCodexExecLaunch } from "../shared/codex-exec-adapter.ts";
|
|
149
|
-
import { resolveCursorAgentLaunch } from "../shared/cursor-agent-adapter.ts";
|
|
150
147
|
import { resolveExternalCliRunnerStatus } from "../shared/external-cli-contract.ts";
|
|
151
148
|
import { runExternalJob } from "../shared/external-job-runner.ts";
|
|
152
149
|
import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
|
|
@@ -1480,28 +1477,15 @@ async function runSingleStepInner(
|
|
|
1480
1477
|
|
|
1481
1478
|
if (step.runner?.type === "external-cli") {
|
|
1482
1479
|
const externalCwd = step.cwd ?? ctx.cwd;
|
|
1483
|
-
const
|
|
1484
|
-
? resolveCodexExecLaunch({ adapter: step.runner.adapter, command: step.runner.command, asyncDir: path.dirname(ctx.outputFile), stepIndex: ctx.flatIndex })
|
|
1485
|
-
: step.runner.adapter === "claude-code" || step.runner.adapter === "claude-code-writer"
|
|
1486
|
-
? resolveClaudeCodeLaunch({ adapter: step.runner.adapter, command: step.runner.command })
|
|
1487
|
-
: step.runner.adapter === "cursor-agent" || step.runner.adapter === "cursor-agent-writer"
|
|
1488
|
-
? resolveCursorAgentLaunch({ adapter: step.runner.adapter, command: step.runner.command, cwd: externalCwd, asyncDir: path.dirname(ctx.outputFile), stepIndex: ctx.flatIndex })
|
|
1489
|
-
: undefined;
|
|
1490
|
-
const runner = resolveExternalCliRunnerStatus({ ...step.runner, ...(adapterLaunch ? { args: adapterLaunch.args } : {}) });
|
|
1480
|
+
const runner = resolveExternalCliRunnerStatus({ ...step.runner });
|
|
1491
1481
|
const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
|
|
1492
1482
|
const external = await runExternalCli(omitUndefinedProperties({
|
|
1493
|
-
command:
|
|
1494
|
-
args:
|
|
1483
|
+
command: runner.command,
|
|
1484
|
+
args: runner.args,
|
|
1495
1485
|
cwd: externalCwd,
|
|
1496
1486
|
prompt: buildExternalCliPrompt(step.systemPrompt ?? "", task),
|
|
1497
1487
|
asyncDir: path.dirname(ctx.outputFile),
|
|
1498
1488
|
stepIndex: ctx.flatIndex,
|
|
1499
|
-
environment: adapterLaunch?.environment,
|
|
1500
|
-
preflight: adapterLaunch?.preflight,
|
|
1501
|
-
parser: adapterLaunch?.parser,
|
|
1502
|
-
finalOutputPath: adapterLaunch?.finalOutputPath,
|
|
1503
|
-
promptFilePath: adapterLaunch?.promptFilePath,
|
|
1504
|
-
temporaryDirectories: adapterLaunch?.temporaryDirectories,
|
|
1505
1489
|
registerTimeout: ctx.registerTimeout,
|
|
1506
1490
|
registerStop: ctx.registerStop,
|
|
1507
1491
|
timeoutMessage: ctx.timeoutMessage,
|
|
@@ -22,43 +22,27 @@ const PROMPT_FILE_UNSUPPORTED = {
|
|
|
22
22
|
resume: "The one-shot prompt-file adapter does not retain a durable external session identity.",
|
|
23
23
|
} as const;
|
|
24
24
|
|
|
25
|
-
export const CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS = [
|
|
26
|
-
"codex-exec",
|
|
27
|
-
"codex-exec-writer",
|
|
28
|
-
"claude-code",
|
|
29
|
-
"claude-code-writer",
|
|
30
|
-
"cursor-agent",
|
|
31
|
-
"cursor-agent-writer",
|
|
32
|
-
] as const;
|
|
25
|
+
export const CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS = [] as const;
|
|
33
26
|
export type CodeOwnedExternalCliAdapterId = typeof CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS[number];
|
|
34
27
|
|
|
35
28
|
const CODE_OWNED_EXTERNAL_CLI_ADAPTER_ID_SET = new Set<string>(CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS);
|
|
36
|
-
export const CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL =
|
|
29
|
+
export const CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL = "(none)";
|
|
37
30
|
|
|
38
31
|
export function isCodeOwnedExternalCliAdapterId(value: unknown): value is CodeOwnedExternalCliAdapterId {
|
|
39
32
|
return typeof value === "string" && CODE_OWNED_EXTERNAL_CLI_ADAPTER_ID_SET.has(value);
|
|
40
33
|
}
|
|
41
34
|
|
|
42
|
-
const RESERVED_READ_ONLY_ADAPTERS = [
|
|
43
|
-
{ name: "claude-code", writer: "claude-code-writer", access: "file-write" },
|
|
44
|
-
{ name: "codex-exec", writer: "codex-exec-writer", access: "workspace-write" },
|
|
45
|
-
{ name: "cursor-agent", writer: "cursor-agent-writer", access: "workspace-write" },
|
|
46
|
-
] as const;
|
|
35
|
+
const RESERVED_READ_ONLY_ADAPTERS = [] as const;
|
|
47
36
|
|
|
48
37
|
export function validateCodeOwnedProfileRunner(
|
|
49
|
-
|
|
38
|
+
_agent: {
|
|
50
39
|
name: string;
|
|
51
40
|
localName?: string;
|
|
52
41
|
aliases?: readonly string[];
|
|
53
42
|
runner?: { type: string; adapter?: string };
|
|
54
43
|
},
|
|
55
44
|
): string | undefined {
|
|
56
|
-
|
|
57
|
-
for (const adapter of RESERVED_READ_ONLY_ADAPTERS) {
|
|
58
|
-
if (selectionNames.includes(adapter.name) && !(agent.runner?.type === "external-cli" && agent.runner.adapter === adapter.name)) {
|
|
59
|
-
return `Selection name '${adapter.name}' is reserved for the read-only '${adapter.name}' adapter. Use '${adapter.writer}' for explicit ${adapter.access} access.`;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
45
|
+
// With no code-owned adapter profiles bundled, no selection names are reserved.
|
|
62
46
|
return undefined;
|
|
63
47
|
}
|
|
64
48
|
|
|
@@ -81,26 +65,13 @@ export function resolveExternalCliRunnerStatus(input: {
|
|
|
81
65
|
promptDelivery?: "stdin";
|
|
82
66
|
capabilities?: ExternalCliCapabilityNarrowing;
|
|
83
67
|
}): ExternalCliRunnerStatus {
|
|
84
|
-
const
|
|
85
|
-
const codexExecWriter = input.adapter === "codex-exec-writer";
|
|
86
|
-
const claudeCode = input.adapter === "claude-code";
|
|
87
|
-
const claudeCodeWriter = input.adapter === "claude-code-writer";
|
|
88
|
-
const cursorAgent = input.adapter === "cursor-agent";
|
|
89
|
-
const cursorAgentWriter = input.adapter === "cursor-agent-writer";
|
|
90
|
-
const cursor = cursorAgent || cursorAgentWriter;
|
|
91
|
-
const unsupported = cursor ? PROMPT_FILE_UNSUPPORTED : UNSUPPORTED;
|
|
68
|
+
const unsupported = UNSUPPORTED;
|
|
92
69
|
return {
|
|
93
70
|
type: "external-cli",
|
|
94
71
|
command: input.command,
|
|
95
72
|
args: input.args ?? [],
|
|
96
|
-
promptDelivery:
|
|
97
|
-
adapter: { id: input.adapter ?? "external-cli", version: 1, executionMode:
|
|
98
|
-
...(codexExec ? { safety: { sandbox: "read-only" as const, approvalPolicy: "never" as const, ephemeral: true as const } } : {}),
|
|
99
|
-
...(codexExecWriter ? { safety: { access: "workspace-write" as const, sandbox: "workspace-write" as const, approvalPolicy: "never" as const, ephemeral: true as const } } : {}),
|
|
100
|
-
...(claudeCode ? { safety: { access: "read-only" as const, authentication: "existing-cli-required" as const, permissionMode: "plan" as const, tools: "none" as const, mcp: "empty-strict" as const, settingSources: "user" as const, userSettingsTrust: "required" as const, sessionPersistence: false as const } } : {}),
|
|
101
|
-
...(claudeCodeWriter ? { safety: { access: "workspace-write" as const, authentication: "existing-cli-required" as const, permissionMode: "acceptEdits" as const, tools: "Read,Write,Edit,Glob,Grep" as const, mcp: "empty-strict" as const, settingSources: "user" as const, userSettingsTrust: "required" as const, sessionPersistence: false as const } } : {}),
|
|
102
|
-
...(cursorAgent ? { safety: { access: "read-only" as const, authentication: "cursor-api-key-or-existing-login" as const, mode: "ask" as const, sandbox: "enabled" as const, workspaceTrust: "existing-required" as const, sessionReuse: false as const } } : {}),
|
|
103
|
-
...(cursorAgentWriter ? { safety: { access: "workspace-write" as const, authentication: "cursor-api-key-or-existing-login" as const, mode: "print" as const, sandbox: "enabled" as const, workspaceTrust: "existing-required" as const, sessionReuse: false as const } } : {}),
|
|
73
|
+
promptDelivery: input.promptDelivery ?? "stdin",
|
|
74
|
+
adapter: { id: input.adapter ?? "external-cli", version: 1, executionMode: "one-shot-stdin" },
|
|
104
75
|
capabilities: {
|
|
105
76
|
stop: true,
|
|
106
77
|
steer: false,
|
|
@@ -1652,7 +1652,7 @@ export type AgentRunnerConfig =
|
|
|
1652
1652
|
| { type: "pi" }
|
|
1653
1653
|
| {
|
|
1654
1654
|
type: "external-cli";
|
|
1655
|
-
adapter?:
|
|
1655
|
+
adapter?: never;
|
|
1656
1656
|
command: string;
|
|
1657
1657
|
args?: string[];
|
|
1658
1658
|
promptDelivery?: "stdin";
|
|
@@ -1678,7 +1678,7 @@ export interface ExternalCliCapabilities {
|
|
|
1678
1678
|
}
|
|
1679
1679
|
|
|
1680
1680
|
export interface ExternalCliReceiptMetadata {
|
|
1681
|
-
adapter: { id: "external-cli" | "
|
|
1681
|
+
adapter: { id: "external-cli" | "grok-build"; version: 1; executionMode: "one-shot-stdin" | "one-shot-prompt-file" };
|
|
1682
1682
|
capabilities: ExternalCliCapabilities;
|
|
1683
1683
|
safety?:
|
|
1684
1684
|
| { sandbox: "read-only"; approvalPolicy: "never"; ephemeral: true }
|
|
@@ -127,7 +127,7 @@ function parseExternalCliReceiptMetadata(value: unknown, key: string, source: st
|
|
|
127
127
|
const adapterRecord = adapter as Record<string, unknown>;
|
|
128
128
|
const unknownAdapter = Object.keys(adapterRecord).filter((field) => !["id", "version", "executionMode"].includes(field));
|
|
129
129
|
if (unknownAdapter.length > 0) throw new Error(`${label}.adapter has unsupported fields: ${unknownAdapter.join(", ")}.`);
|
|
130
|
-
if ((adapterRecord.id !== "external-cli" && adapterRecord.id !== "
|
|
130
|
+
if ((adapterRecord.id !== "external-cli" && adapterRecord.id !== "grok-build") || adapterRecord.version !== 1 || adapterRecord.executionMode !== (adapterRecord.id === "grok-build" ? "one-shot-prompt-file" : "one-shot-stdin")) throw new Error(`${label}.adapter is invalid.`);
|
|
131
131
|
const capabilities = metadata.capabilities;
|
|
132
132
|
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities)) throw new Error(`${label}.capabilities must be an object.`);
|
|
133
133
|
const capabilityRecord = capabilities as Record<string, unknown>;
|
|
@@ -137,43 +137,7 @@ function parseExternalCliReceiptMetadata(value: unknown, key: string, source: st
|
|
|
137
137
|
if (capabilityRecord[capability] !== expected) throw new Error(`${label}.capabilities.${capability} is invalid.`);
|
|
138
138
|
}
|
|
139
139
|
const safety = metadata.safety;
|
|
140
|
-
if (adapterRecord.id === "
|
|
141
|
-
if (!safety || typeof safety !== "object" || Array.isArray(safety)) throw new Error(`${label}.safety is missing.`);
|
|
142
|
-
const safetyRecord = safety as Record<string, unknown>;
|
|
143
|
-
const unknownSafety = Object.keys(safetyRecord).filter((field) => !["sandbox", "approvalPolicy", "ephemeral"].includes(field));
|
|
144
|
-
if (unknownSafety.length > 0) throw new Error(`${label}.safety has unsupported fields: ${unknownSafety.join(", ")}.`);
|
|
145
|
-
if (safetyRecord.sandbox !== "read-only" || safetyRecord.approvalPolicy !== "never" || safetyRecord.ephemeral !== true) throw new Error(`${label}.safety is invalid.`);
|
|
146
|
-
} else if (adapterRecord.id === "codex-exec-writer") {
|
|
147
|
-
if (!safety || typeof safety !== "object" || Array.isArray(safety)) throw new Error(`${label}.safety is missing.`);
|
|
148
|
-
const safetyRecord = safety as Record<string, unknown>;
|
|
149
|
-
const unknownSafety = Object.keys(safetyRecord).filter((field) => !["access", "sandbox", "approvalPolicy", "ephemeral"].includes(field));
|
|
150
|
-
if (unknownSafety.length > 0) throw new Error(`${label}.safety has unsupported fields: ${unknownSafety.join(", ")}.`);
|
|
151
|
-
if (safetyRecord.access !== "workspace-write" || safetyRecord.sandbox !== "workspace-write" || safetyRecord.approvalPolicy !== "never" || safetyRecord.ephemeral !== true) throw new Error(`${label}.safety is invalid.`);
|
|
152
|
-
} else if (adapterRecord.id === "claude-code") {
|
|
153
|
-
if (!safety || typeof safety !== "object" || Array.isArray(safety)) throw new Error(`${label}.safety is missing.`);
|
|
154
|
-
const safetyRecord = safety as Record<string, unknown>;
|
|
155
|
-
const legacy = safetyRecord.authentication === undefined;
|
|
156
|
-
const unknownSafety = Object.keys(safetyRecord).filter((field) => !(legacy
|
|
157
|
-
? ["permissionMode", "tools", "mcp", "settingSources", "sessionPersistence"]
|
|
158
|
-
: ["access", "authentication", "permissionMode", "tools", "mcp", "settingSources", "userSettingsTrust", "sessionPersistence"]).includes(field));
|
|
159
|
-
if (unknownSafety.length > 0) throw new Error(`${label}.safety has unsupported fields: ${unknownSafety.join(", ")}.`);
|
|
160
|
-
if (legacy) {
|
|
161
|
-
if (safetyRecord.permissionMode !== "plan" || safetyRecord.tools !== "none" || safetyRecord.mcp !== "empty-strict" || safetyRecord.settingSources !== "none" || safetyRecord.sessionPersistence !== false) throw new Error(`${label}.safety is invalid.`);
|
|
162
|
-
} else if (safetyRecord.access !== "read-only" || safetyRecord.authentication !== "existing-cli-required" || safetyRecord.permissionMode !== "plan" || safetyRecord.tools !== "none" || safetyRecord.mcp !== "empty-strict" || safetyRecord.settingSources !== "user" || safetyRecord.userSettingsTrust !== "required" || safetyRecord.sessionPersistence !== false) throw new Error(`${label}.safety is invalid.`);
|
|
163
|
-
} else if (adapterRecord.id === "claude-code-writer") {
|
|
164
|
-
if (!safety || typeof safety !== "object" || Array.isArray(safety)) throw new Error(`${label}.safety is missing.`);
|
|
165
|
-
const safetyRecord = safety as Record<string, unknown>;
|
|
166
|
-
const unknownSafety = Object.keys(safetyRecord).filter((field) => !["access", "authentication", "permissionMode", "tools", "mcp", "settingSources", "userSettingsTrust", "sessionPersistence"].includes(field));
|
|
167
|
-
if (unknownSafety.length > 0) throw new Error(`${label}.safety has unsupported fields: ${unknownSafety.join(", ")}.`);
|
|
168
|
-
if (safetyRecord.access !== "workspace-write" || safetyRecord.authentication !== "existing-cli-required" || safetyRecord.permissionMode !== "acceptEdits" || safetyRecord.tools !== "Read,Write,Edit,Glob,Grep" || safetyRecord.mcp !== "empty-strict" || safetyRecord.settingSources !== "user" || safetyRecord.userSettingsTrust !== "required" || safetyRecord.sessionPersistence !== false) throw new Error(`${label}.safety is invalid.`);
|
|
169
|
-
} else if (adapterRecord.id === "cursor-agent" || adapterRecord.id === "cursor-agent-writer") {
|
|
170
|
-
if (!safety || typeof safety !== "object" || Array.isArray(safety)) throw new Error(`${label}.safety is missing.`);
|
|
171
|
-
const safetyRecord = safety as Record<string, unknown>;
|
|
172
|
-
const unknownSafety = Object.keys(safetyRecord).filter((field) => !["access", "authentication", "mode", "sandbox", "workspaceTrust", "sessionReuse"].includes(field));
|
|
173
|
-
if (unknownSafety.length > 0) throw new Error(`${label}.safety has unsupported fields: ${unknownSafety.join(", ")}.`);
|
|
174
|
-
const writer = adapterRecord.id === "cursor-agent-writer";
|
|
175
|
-
if (safetyRecord.access !== (writer ? "workspace-write" : "read-only") || safetyRecord.authentication !== "cursor-api-key-or-existing-login" || safetyRecord.mode !== (writer ? "print" : "ask") || safetyRecord.sandbox !== "enabled" || safetyRecord.workspaceTrust !== "existing-required" || safetyRecord.sessionReuse !== false) throw new Error(`${label}.safety is invalid.`);
|
|
176
|
-
} else if (adapterRecord.id === "grok-build") {
|
|
140
|
+
if (adapterRecord.id === "grok-build") {
|
|
177
141
|
if (!safety || typeof safety !== "object" || Array.isArray(safety)) throw new Error(`${label}.safety is missing.`);
|
|
178
142
|
const safetyRecord = safety as Record<string, unknown>;
|
|
179
143
|
const unknownSafety = Object.keys(safetyRecord).filter((field) => !["access", "authentication", "permissionMode", "tools", "deniedTools", "sandbox", "webSearch", "subagents", "config", "updates", "sessionPersistence"].includes(field));
|
|
@@ -108,55 +108,6 @@ describe("agent management config parsing", () => {
|
|
|
108
108
|
assert.equal(JSON.stringify(capabilities).includes("SYSTEM_PROMPT_SENTINEL"), false);
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
it("rejects management attempts to widen the reserved read-only Claude profile", () => {
|
|
112
|
-
const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
|
|
113
|
-
const writerRunner = { type: "external-cli", adapter: "claude-code-writer", command: "claude" };
|
|
114
|
-
const unsafeCreate = handleCreate({ config: { name: "claude-code", description: "Unsafe shadow", scope: "project", runner: writerRunner } }, ctx);
|
|
115
|
-
assert.equal(unsafeCreate.isError, true);
|
|
116
|
-
assert.match(readText(unsafeCreate), /reserved for the read-only 'claude-code' adapter/);
|
|
117
|
-
const unsafeLocalName = handleCreate({ config: { name: "claude-code", package: "custom", description: "Unsafe local name", scope: "project", runner: writerRunner } }, ctx);
|
|
118
|
-
assert.equal(unsafeLocalName.isError, true);
|
|
119
|
-
assert.match(readText(unsafeLocalName), /Selection name 'claude-code' is reserved/);
|
|
120
|
-
const unsafeAlias = handleCreate({ config: { name: "aliased-writer", aliases: ["claude-code"], description: "Unsafe alias", scope: "project", runner: writerRunner } }, ctx);
|
|
121
|
-
assert.equal(unsafeAlias.isError, true);
|
|
122
|
-
assert.match(readText(unsafeAlias), /Selection name 'claude-code' is reserved/);
|
|
123
|
-
|
|
124
|
-
const readOnlyCreate = handleCreate({ config: { name: "claude-code", description: "Narrow shadow", scope: "project", runner: { type: "external-cli", adapter: "claude-code", command: "claude" } } }, ctx);
|
|
125
|
-
assert.equal(readOnlyCreate.isError, false);
|
|
126
|
-
const unsafeUpdate = handleUpdate({ agent: "claude-code", agentScope: "project", config: { runner: writerRunner } }, ctx);
|
|
127
|
-
assert.equal(unsafeUpdate.isError, true);
|
|
128
|
-
assert.match(readText(unsafeUpdate), /reserved for the read-only 'claude-code' adapter/);
|
|
129
|
-
assert.match(fs.readFileSync(path.join(tempDir, ".selesai", "agents", "claude-code.md"), "utf-8"), /adapter: claude-code\n/);
|
|
130
|
-
|
|
131
|
-
const writerCreate = handleCreate({ config: { name: "custom-writer", description: "Writer", scope: "project", runner: writerRunner } }, ctx);
|
|
132
|
-
assert.equal(writerCreate.isError, false);
|
|
133
|
-
const unsafeAliasUpdate = handleUpdate({ agent: "custom-writer", agentScope: "project", config: { aliases: ["claude-code"] } }, ctx);
|
|
134
|
-
assert.equal(unsafeAliasUpdate.isError, true);
|
|
135
|
-
assert.match(readText(unsafeAliasUpdate), /Selection name 'claude-code' is reserved/);
|
|
136
|
-
const unsafeRename = handleUpdate({ agent: "custom-writer", agentScope: "project", config: { name: "claude-code" } }, ctx);
|
|
137
|
-
assert.equal(unsafeRename.isError, true);
|
|
138
|
-
assert.match(readText(unsafeRename), /reserved for the read-only 'claude-code' adapter/);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("rejects create, update, alias, and rename widening for Codex and Cursor", () => {
|
|
142
|
-
const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
|
|
143
|
-
for (const [readOnly, writer, command] of [["codex-exec", "codex-exec-writer", "codex"], ["cursor-agent", "cursor-agent-writer", "cursor-agent"]] as const) {
|
|
144
|
-
const writerRunner = { type: "external-cli", adapter: writer, command };
|
|
145
|
-
assert.equal(handleCreate({ config: { name: readOnly, description: "Unsafe shadow", scope: "project", runner: writerRunner } }, ctx).isError, true);
|
|
146
|
-
assert.equal(handleCreate({ config: { name: readOnly, package: `custom-${readOnly}`, description: "Unsafe local name", scope: "project", runner: writerRunner } }, ctx).isError, true);
|
|
147
|
-
assert.equal(handleCreate({ config: { name: `${readOnly}-alias`, aliases: [readOnly], description: "Unsafe alias", scope: "project", runner: writerRunner } }, ctx).isError, true);
|
|
148
|
-
|
|
149
|
-
const readOnlyCreate = handleCreate({ config: { name: readOnly, description: "Narrow shadow", scope: "project", runner: { type: "external-cli", adapter: readOnly, command } } }, ctx);
|
|
150
|
-
assert.equal(readOnlyCreate.isError, false);
|
|
151
|
-
assert.equal(handleUpdate({ agent: readOnly, agentScope: "project", config: { runner: writerRunner } }, ctx).isError, true);
|
|
152
|
-
|
|
153
|
-
const customName = `custom-${writer}`;
|
|
154
|
-
assert.equal(handleCreate({ config: { name: customName, description: "Writer", scope: "project", runner: writerRunner } }, ctx).isError, false);
|
|
155
|
-
assert.equal(handleUpdate({ agent: customName, agentScope: "project", config: { aliases: [readOnly] } }, ctx).isError, true);
|
|
156
|
-
assert.equal(handleUpdate({ agent: customName, agentScope: "project", config: { name: readOnly } }, ctx).isError, true);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
111
|
it("lists valid agents and diagnoses malformed agent definitions", () => {
|
|
161
112
|
const agentsDir = path.join(tempDir, ".selesai", "agents");
|
|
162
113
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
@@ -255,24 +255,6 @@ describe("runtime agent registration", () => {
|
|
|
255
255
|
});
|
|
256
256
|
|
|
257
257
|
it("fails closed for builtin and duplicate runtime identities", () => {
|
|
258
|
-
assert.throws(
|
|
259
|
-
() => registerAgent({ pi, name: "claude-code", definition: { description: "Unsafe", systemPrompt: "Write.", runner: { type: "external-cli", adapter: "claude-code-writer", command: "claude" } } }),
|
|
260
|
-
/reserved for the read-only 'claude-code' adapter/,
|
|
261
|
-
);
|
|
262
|
-
assert.throws(
|
|
263
|
-
() => registerAgent({ pi, name: "runtime-writer", definition: { description: "Unsafe alias", systemPrompt: "Write.", aliases: ["claude-code"], runner: { type: "external-cli", adapter: "claude-code-writer", command: "claude" } } }),
|
|
264
|
-
/Selection name 'claude-code' is reserved/,
|
|
265
|
-
);
|
|
266
|
-
for (const [readOnly, writer, command] of [["codex-exec", "codex-exec-writer", "codex"], ["cursor-agent", "cursor-agent-writer", "cursor-agent"]] as const) {
|
|
267
|
-
assert.throws(
|
|
268
|
-
() => registerAgent({ pi, name: readOnly, definition: { description: "Unsafe", systemPrompt: "Write.", runner: { type: "external-cli", adapter: writer, command } } }),
|
|
269
|
-
/reserved for the read-only/,
|
|
270
|
-
);
|
|
271
|
-
assert.throws(
|
|
272
|
-
() => registerAgent({ pi, name: `runtime-${writer}`, definition: { description: "Unsafe alias", systemPrompt: "Write.", aliases: [readOnly], runner: { type: "external-cli", adapter: writer, command } } }),
|
|
273
|
-
/Selection name .* is reserved/,
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
258
|
assert.throws(
|
|
277
259
|
() => registerAgent({ pi, name: "worker", definition: { description: "Bad", systemPrompt: "Bad." } }),
|
|
278
260
|
/Worker|builtin agent 'worker'|collides with builtin agent 'worker'/i,
|