Skip to content

Commit 63ec5c1

Browse files
Merge pull request #591 from corbitsdev/cl-6952-remove-the-static-per-package-writepaths-lock-detect-concurrent-lane
Remove static writePaths lock; detect concurrent lane overlap at spawn
2 parents 91639eb + e1812fd commit 63ec5c1

20 files changed

Lines changed: 59 additions & 369 deletions

docs/ARCHITECTURE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ Profiles with `orchestrator: true` may themselves call `task` (one hop only): ne
229229

230230
#### Closed director fleet (`src/agent/directors/`)
231231

232-
Every shipped specialist is a **director package** — a prompt-first `DirectorPackage` (system prompt, tool envelope, spawn rights, nudge budget, report contract, optional `writePaths`, `modelRole`) registered in a **closed** set of 16 ids. There is no catch-all worker: `task` without `agent` or non-general `intent`, and `task(intent="general")`, fail closed so the primary reclassifies. Nested directors with a spawn allowlist reject off-list children at `createTaskTool` (not prompt-only). Skywalker is the primary session identity: `task(agent="skywalker")` is refused, and `directorProfiles()` omits it from the spawn catalog.
232+
Every shipped specialist is a **director package** — a prompt-first `DirectorPackage` (system prompt, tool envelope, spawn rights, nudge budget, report contract, `modelRole`) registered in a **closed** set of 16 ids. There is no catch-all worker: `task` without `agent` or non-general `intent`, and `task(intent="general")`, fail closed so the primary reclassifies. Nested directors with a spawn allowlist reject off-list children at `createTaskTool` (not prompt-only). Skywalker is the primary session identity: `task(agent="skywalker")` is refused, and `directorProfiles()` omits it from the spawn catalog.
233233

234234
**Primary**
235235

@@ -285,7 +285,7 @@ Every shipped specialist is a **director package** — a prompt-first `DirectorP
285285
| greybeard | intern, explore, critique only |
286286
| All other directors | no `task` |
287287

288-
**Tool envelopes** prefer small `tools.allow` mounts over deny-everything. Shipped docs/design directors (shakespeare, brand-reviewer, bruckheimer) mount write tools with **no** package `writePaths`. Lane routing is spawn policy (shakespeare = P/A/I docs, brand-reviewer = DESIGN.md, bruckheimer = product discovery), not a file lock. Optional `writePaths` still exists; the permission gate enforces it when a profile sets it.
288+
**Tool envelopes** prefer small `tools.allow` mounts over deny-everything. Shipped docs/design directors (shakespeare, brand-reviewer, bruckheimer) mount write tools with no path-level lock. Lane routing is spawn policy (shakespeare = P/A/I docs, brand-reviewer = DESIGN.md, bruckheimer = product discovery), not a file lock. There is no static per-package write-path declaration (CL-6952 removed it — no shipped director ever set one); instead the task tool records, without blocking, when two concurrently running dispatches land on the same cwd (see `intervention-log.ts`'s `conflict` class).
289289

290290
**Typical chain:** bruckheimer → plan → greybeard → build (+ intern) → critique (+ optional neckbeard), with skywalker coordinating throughout.
291291

@@ -299,7 +299,7 @@ Data-only agent plugins (`src/plugins/data-only-agent.ts`) synthesize `agentPlug
299299

300300
### System Prompt (`src/agent/prompts.ts`)
301301

302-
The primary session identity is **Skywalker** (`buildChatRole` → `createSkywalkerSystemPrompt`). Product name remains Corbits Code; when asked its name, the primary answers Skywalker. Role: orchestrate — classify, DIY tiny/single-file/one-route product edits, dispatch closed directors via `task` for substantial work, track the fleet, synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary session (CORE and `SKYWALKER_TOOLS`) so Skywalker can DIY bounded edits; spawn remains the default for substantial, multi-file, parallel, or specialist work (hard cap 4 workers). Shell file-writes stay denied by auto-shell policy. MCP tools are not re-filtered by a product-write deny list (that list is gone). Leaf `writePaths` only apply to path-keyed product tools when a profile sets them. A frontier model already knows how to code; the static prompt carries harness-specific facts and the closed-fleet orchestration policy. The base is three individually-exported sections:
302+
The primary session identity is **Skywalker** (`buildChatRole` → `createSkywalkerSystemPrompt`). Product name remains Corbits Code; when asked its name, the primary answers Skywalker. Role: orchestrate — classify, DIY tiny/single-file/one-route product edits, dispatch closed directors via `task` for substantial work, track the fleet, synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary session (CORE and `SKYWALKER_TOOLS`) so Skywalker can DIY bounded edits; spawn remains the default for substantial, multi-file, parallel, or specialist work (hard cap 4 workers). Shell file-writes stay denied by auto-shell policy. MCP tools are not re-filtered by a product-write deny list (that list is gone). There is no static per-leaf write-path lock; concurrent lanes sharing a cwd are instead flagged (not blocked) as a `conflict` intervention. A frontier model already knows how to code; the static prompt carries harness-specific facts and the closed-fleet orchestration policy. The base is three individually-exported sections:
303303

304304
- `buildChatRole` — Skywalker primary identity (orchestrate; DIY tiny/bounded product edits; spawn for substantial work).
305305
- `buildHarnessFacts` — the non-derivable rules: shell file-writes are blocked, path tools are the DIY surface on primary (spawn build/docs directors for substantial work), dependency installs and off-limits paths need approval, images are native multimodal input, only core tools are resident (load the rest via `tool_search`; use `search_agents` before dispatching specialists), workflows run only from slash-command steps, and session memory lives at `.corbits/MEMORY.md`.

docs/IMPLEMENTATION.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ src/
8888
hooks.ts Lifecycle hooks: discovery, turn collector, run summary
8989
subagent/
9090
index.ts Sub-agent spawn + SubAgentDirector
91-
task-tool.ts task() — resolveDirector first; writePaths on child
91+
task-tool.ts task() — resolveDirector first; concurrent-lane-overlap check on spawn
9292
session-store.ts Retained child session transcripts for observe UI
93-
identity-context.ts ALS: worker cwd + optional writePaths for gate
93+
identity-context.ts ALS: worker description + cwd for gate attribution
9494
config/
9595
index.ts Config resolution (settings files + flags) (was config.ts)
9696
settings.ts Settings schema, validators, loaders, resolveProvider
@@ -106,8 +106,7 @@ src/
106106
classify.ts Tool tier + approval-request construction
107107
command.ts Chained-command split + command scopes
108108
auto-shell-policy.ts Auto-mode run_shell deny/ask rule table
109-
gate.ts Permission gate evaluation (+ director writePaths)
110-
write-path-policy.ts Basename/glob match for worker write allowlists
109+
gate.ts Permission gate evaluation
111110
matcher.ts Approval glob matching
112111
store.ts Per-directory approval persistence
113112
types.ts Approval / scope / request / outcome types
@@ -155,14 +154,14 @@ docs/
155154
Sixteen packages under `src/agent/directors/<id>/` register in `DIRECTOR_REGISTRY` (`registry.ts`). Wire path:
156155

157156
1. `task(agent=…)` / `task(intent=…)``resolveDirector` in `task-tool.ts` before tools and system prompt are built. Bare `task` (neither field) and `intent=general` fail closed.
158-
2. `packageToProfile` maps envelope (`tools.allow`/`deny`) to `AgentProfile.capabilities`, `spawn.maySpawn``orchestrator`, and optional `writePaths`. System prompts are prefixed with a stable identity block (`formatDirectorSystemPrompt`: agent id, model role, optional skills).
157+
2. `packageToProfile` maps envelope (`tools.allow`/`deny`) to `AgentProfile.capabilities` and `spawn.maySpawn``orchestrator`. System prompts are prefixed with a stable identity block (`formatDirectorSystemPrompt`: agent id, model role, optional skills).
159158
3. Nested spawn: packages with `spawn.allowlist` forward that list into nested `task` (`spawnAllowlist` on nestedDispatch). Off-list `agent` is refused. `task(agent=skywalker)` is refused (primary is not a spawned worker). Primary omits the list so plugin profiles stay reachable.
160159
4. `directorProfiles()` is the spawn catalog (`default-agents.ts`) — closed set minus skywalker; plugin agent profiles still load and can override by id.
161-
5. Primary chat role is Skywalker: `buildChatRole()``createSkywalkerSystemPrompt()`. Product mutation tools (`write_file` / `edit_file` / `delete_file`) live in CORE (and `SKYWALKER_TOOLS`) so they are advertised on the primary without a `tool_search` round-trip. DIY tiny/bounded edits on the parent; spawn build/docs directors for substantial work — a prompt judgment call, not a toolset strip. `PRIMARY_DENIED_PRODUCT_TOOLS` is gone. Shell file-writes stay denied; MCP tools are not re-filtered by a product-write deny list. Optional `writePaths` (when a profile sets it) only gate path-keyed product tools.
160+
5. Primary chat role is Skywalker: `buildChatRole()``createSkywalkerSystemPrompt()`. Product mutation tools (`write_file` / `edit_file` / `delete_file`) live in CORE (and `SKYWALKER_TOOLS`) so they are advertised on the primary without a `tool_search` round-trip. DIY tiny/bounded edits on the parent; spawn build/docs directors for substantial work — a prompt judgment call, not a toolset strip. `PRIMARY_DENIED_PRODUCT_TOOLS` is gone. Shell file-writes stay denied; MCP tools are not re-filtered by a product-write deny list. There is no static per-profile write-path lock (CL-6952).
162161

163162
**Codex tool proxies.** When the active provider is Codex (`isCodexProviderName`), `createAgentToolset` and `runSubAgent` mount `apply_patch`, `shell`, and `update_plan` stringTools from `createCodexToolProxies`, all forwarding through the same posix `ToolRunner` seam (`runTool`) so permission plugins still apply. `apply_patch` parses the Codex envelope and forwards each op (`write_file` / `delete_file` / `read_file`). `shell` — the native Codex name is `shell`, not `exec_command`, per the pinned base-instructions text quoted in `codex-responses-adapter.ts`'s bridge message — normalizes Codex's `command` (string or `["bash","-lc",script]`-style argv array), `workdir`, and `timeout_ms` onto `run_shell`'s `{command, cwd?, timeout?}` and is gated by `allowShellFromCapabilities` (mirrors `allowDeleteFromCapabilities` against `run_shell`). `update_plan` maps Codex's `plan: [{step, status}]` onto `manage_tasks(action: "create")`; `pending`/`in_progress`/`completed` map to `todo`/`doing`/`done` — `manage_tasks`'s `cancelled` status has no Codex equivalent and is never produced by this proxy. Primary strips `apply_patch` after mount (Corbits DIY stays on `write_file` / `edit_file` / `delete_file`); `shell` and `update_plan` stay on primary (same classification as `run_shell` / `manage_tasks`). Build and docs leaf allowlists (`BUILD_TOOLS` / `DOCS_TOOLS`) include `apply_patch` so Codex workers keep the proxy after the capability filter. `CORE_TOOL_NAMES` does not list it.
164163

165-
6. Shipped directors omit `writePaths`. The optional field is still enforced in the permission gate via ALS identity (`identity-context.ts` + `write-path-policy.ts`) when a plugin/custom profile sets it.
164+
6. There is no static write-path declaration on packages or profiles (CL-6952 removed it — no shipped director ever set one). Instead, `task-tool.ts` tracks each running dispatch by cwd; a new dispatch that lands on the same cwd as a still-running lane records a `concurrent-lane-overlap` entry in `intervention-log.ts` (class `conflict`). This is advisory only — it never blocks the spawn, since cwd overlap does not prove the two lanes touch the same files.
166165
7. Spawn effort: pin > package `modelRole` default (`defaultEffortForDirector`; intern=low; plan/review/orchestrator=high; implement/explore/docs/test=medium) > orchestrator/worker binary > parent inheritance. Optional skills are listed in the identity header for awareness; workers do not mount `use_skill` (guidance is baked into package system prompts). Primary mounts `use_skill` for its own skill list.
167166

168167
Intent defaults: `intent=implement` → director `build`; explore/plan → same-named director; review → critique; general → error. Spawn: skywalker full fleet; greybeard intern/explore/critique only; all other directors no `task`. Live `<env>` injects cwd, platform, arch, runtime, date, and git status on every chat and worker prompt.

docs/PRODUCT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Capabilities beyond the core toolset are opt-in plugins, enabled per workspace t
142142

143143
## Multi-agent (sub-agents)
144144

145-
The primary session is always **orchestrator** (single-agent mode is gone). Its identity is **Skywalker** (product name remains Corbits Code; when asked its name, answer Skywalker): classify work, DIY tiny/single-file/one-route product edits, dispatch a **closed fleet of 16 directors** for substantial work, track the fleet, and synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary (CORE / `SKYWALKER_TOOLS`) — path tools are the DIY surface; spawn remains the default for substantial, multi-file, parallel, or specialist work (hard cap 4). Shell file-writes stay denied. MCP tools are not re-filtered by a product-write deny list (that list is gone). Shipped directors have no package `writePaths`; the optional field still constrains path-keyed product tools (not shell) when a profile sets it. Yolo / skip-permissions still bypasses the write-path gate when enabled. Operator slash recipes (`/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`) tell Skywalker which directors to spawn for substantial work; tiny/bounded edits may run on the primary.
145+
The primary session is always **orchestrator** (single-agent mode is gone). Its identity is **Skywalker** (product name remains Corbits Code; when asked its name, answer Skywalker): classify work, DIY tiny/single-file/one-route product edits, dispatch a **closed fleet of 16 directors** for substantial work, track the fleet, and synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary (CORE / `SKYWALKER_TOOLS`) — path tools are the DIY surface; spawn remains the default for substantial, multi-file, parallel, or specialist work (hard cap 4). Shell file-writes stay denied. MCP tools are not re-filtered by a product-write deny list (that list is gone). There is no static per-package write-path declaration (CL-6952 removed it — no shipped director ever set one). A concurrent dispatch landing on the same working directory as another still-running lane is recorded as a `conflict` intervention, not blocked. Operator slash recipes (`/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`) tell Skywalker which directors to spawn for substantial work; tiny/bounded edits may run on the primary.
146146

147147
| Lane | Directors |
148148
| --------- | ---------------------------------------------------------------------------------- |

src/agent/directors/brand-reviewer/package.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,10 @@ describe("brandReviewerPackage", () => {
2121
expect(brandReviewerPackage.spawn.maySpawn).toBe(false);
2222
});
2323

24-
test("tools.allow includes write tools; writePaths is omitted", () => {
24+
test("tools.allow includes write tools", () => {
2525
const allow = brandReviewerPackage.tools?.allow ?? [];
2626
expect(allow).toContain("write_file");
2727
expect(allow).toContain("edit_file");
28-
expect(brandReviewerPackage.writePaths).toBeUndefined();
2928
});
3029

3130
test("systemPrompt mentions DESIGN.md", () => {

src/agent/directors/bruckheimer/package.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@ describe("bruckheimerPackage", () => {
2222
expect(bruckheimerPackage.spawn.maySpawn).toBe(false);
2323
});
2424

25-
test("tools.allow includes write tools; writePaths is omitted", () => {
25+
test("tools.allow includes write tools", () => {
2626
const allow = bruckheimerPackage.tools?.allow ?? [];
2727
expect(allow).toContain("write_file");
2828
expect(allow).toContain("edit_file");
29-
expect(bruckheimerPackage.writePaths).toBeUndefined();
3029
});
3130

3231
test("modelRole is docs", () => {

src/agent/directors/registry.test.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ describe("director registry", () => {
104104
expect(grey.maxTurns).toBe(DIRECTOR_REGISTRY.greybeard.nudge?.maxTurns);
105105

106106
const shakespeare = packageToProfile(DIRECTOR_REGISTRY.shakespeare);
107-
expect(shakespeare.writePaths).toBeUndefined();
108107
expect(shakespeare.capabilities?.mode).toBe("allow");
109108
expect(shakespeare.capabilities?.tools).toContain("write_file");
110109
});
@@ -144,13 +143,6 @@ describe("director registry", () => {
144143
}
145144
});
146145

147-
test("no shipped director in DIRECTOR_IDS has a non-empty writePaths", () => {
148-
for (const id of DIRECTOR_IDS) {
149-
const paths = DIRECTOR_REGISTRY[id].writePaths;
150-
expect(paths === undefined || paths.length === 0).toBe(true);
151-
}
152-
});
153-
154146
test("build mounts product writes; intern is shell-only; other leaves do not spawn", () => {
155147
expect(DIRECTOR_REGISTRY.build.tools?.allow).toEqual(
156148
expect.arrayContaining(["write_file", "edit_file", "delete_file", "apply_patch"]),
@@ -178,15 +170,6 @@ describe("director registry", () => {
178170
expect(s.spawn.allowlist).toHaveLength(15);
179171
});
180172

181-
test("writePaths guards: a director with non-empty writePaths never allows run_shell", () => {
182-
for (const id of DIRECTOR_IDS) {
183-
const pkg = DIRECTOR_REGISTRY[id];
184-
if (!pkg.writePaths || pkg.writePaths.length === 0) continue;
185-
const allow = pkg.tools?.allow ?? [];
186-
expect(allow).not.toContain("run_shell");
187-
}
188-
});
189-
190173
test("every director profile declares matching agent id in system prompt", () => {
191174
for (const id of DIRECTOR_IDS) {
192175
const profile = packageToProfile(DIRECTOR_REGISTRY[id]);

src/agent/directors/registry.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,6 @@ export function packageToProfile(pkg: DirectorPackage): AgentProfile {
127127
orchestrator: pkg.spawn.maySpawn,
128128
...(pkg.nudge?.maxTurns !== undefined ? { maxTurns: pkg.nudge.maxTurns } : {}),
129129
...(capabilities !== undefined ? { capabilities } : {}),
130-
...(pkg.writePaths !== undefined && pkg.writePaths.length > 0
131-
? { writePaths: [...pkg.writePaths] }
132-
: {}),
133130
};
134131
}
135132

src/agent/directors/shakespeare/package.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,10 @@ describe("shakespearePackage", () => {
3232
expect(shakespearePackage.spawn.maySpawn).toBe(false);
3333
});
3434

35-
test("tools.allow includes write tools; writePaths is omitted", () => {
35+
test("tools.allow includes write tools", () => {
3636
const allow = shakespearePackage.tools?.allow ?? [];
3737
expect(allow).toContain("write_file");
3838
expect(allow).toContain("edit_file");
39-
expect(shakespearePackage.writePaths).toBeUndefined();
4039
});
4140

4241
test("modelRole is docs", () => {

src/agent/directors/tool-sets.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@ export const BUILD_TOOLS = [
3434

3535
/**
3636
* Docs leaves: read/search/lsp/web + file writes — no run_shell, no delete_file.
37-
* Envelope policy, not a writePaths lock: docs leaves omit shell so they cannot
38-
* mutate via the terminal. Optional package writePaths, when a profile sets it,
39-
* is still enforced by the permission gate on path-keyed write tools.
37+
* Envelope policy only: docs leaves omit shell so they cannot mutate via the
38+
* terminal. There is no separate path-level lock on top of the tool envelope.
4039
*
4140
* Composed from READ_TOOLS minus run_shell so it tracks the read surface
4241
* automatically; only the write tools are added explicitly. `apply_patch` is

src/agent/directors/types.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,6 @@ export interface DirectorPackage {
6464
/** Optional skills the worker may load dynamically (ordered). */
6565
readonly optionalSkills?: readonly string[];
6666
readonly tools?: ToolEnvelope;
67-
/**
68-
* Authz write-path allowlist for write_file/edit_file/delete_file.
69-
* Enforced by the permission gate (not prompt policy). A bare filename (no
70-
* slash) matches only at the workspace root; a glob matches the resolved
71-
* workspace-relative path; anything outside the worker cwd is denied. yolo
72-
* mode bypasses this gate. Omitted = no path lock (tool allow/deny alone
73-
* decides whether writes exist).
74-
*/
75-
readonly writePaths?: readonly string[];
7667
readonly spawn: SpawnRights;
7768
readonly nudge?: NudgePolicy;
7869
readonly modelRole: ModelRole;

0 commit comments

Comments
 (0)