Skip to content

feat(agent): COD-549 launch workers in existing project directories - #708

Merged
Ark0N merged 4 commits into
Ark0N:masterfrom
aakhter:pr/COD-549-project-workers
Oct 27, 2026
Merged

Ark0N merged 4 commits into
Ark0N:masterfrom
aakhter:pr/COD-549-project-workers

Conversation

@aakhter

@aakhter aakhter commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

codeman agent spawn requires a case name even when a worker should use an existing project. Allow an omitted case to use the caller's cwd, and add --working-dir for another existing local directory. Named cases keep their current quick-start behavior.

A directory launch creates the session with POST /api/v1/sessions (workingDir set) and starts it with the mode's start route (POST /api/v1/sessions/:id/interactive or /shell), keeping parent lineage and the server's ownership checks. Real project directories receive no agent-case marker. A rejected or ambiguous start returns the created session id, one valid JSON document with --json, and an error exit, without retrying.

Refused locally with exit 4, before any request:

  • an unknown --mode (named-case spawns included; noted in the changeset)
  • an empty or whitespace-only --working-dir, which would otherwise resolve to the cwd
  • a bare spawn whose cwd is the home directory (realpath compare, so a symlinked HOME matches): it would install Codeman's hooks into ~/.claude/settings.local.json, the same reason POST /api/sessions skips workspace hooks without a workingDir. An explicit --working-dir ~ is still allowed.

spawn "" (for example spawn "$CASE" with CASE unset) is a case name, not "use cwd": it reaches quick-start, which refuses it, as on master.

Directory-launch --json output: { sessionId, workingDir, created: true }, plus ready when a readiness wait ran, readinessError when that call failed, and startupError when the start failed. The same sentence appears in docs/api-reference.md, the README, the wiki page and the changeset, and a test keeps the four in step.

Docs: api-reference, README, docs/wiki/Driving-Codeman-From-An-Agent.md, the skills/codeman verbs and messaging references, SKILL.md and its synced plugin mirror, the cli-agent.ts @fileoverview, and a minor changeset.

Verification:

  • test/cli-agent-project-spawn.test.ts and test/cli-agent.test.ts: 111 tests, including the real commander parser and HTTP transport for spawn "", --working-dir "" and a HOME cwd. Each guard was reverted to confirm its tests fail.
  • The agent CLI, MCP, skill, plugin and session-route tests (16 files, 463 tests) pass.
  • Typecheck, lint, format, frontend syntax, browser excludes and the plugin mirror check pass.

Paths must exist on the CLI host; normal server path and ownership validation still apply. This contribution is independent of the readiness fix (#709) and the offline-history command (#707).

@Ark0N

Ark0N commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Thanks for this, aakhter. This PR lets codeman agent spawn start a worker in the current directory, or in an existing directory given with --working-dir, through POST /api/v1/sessions plus the mode's start route, while named cases keep using quick-start.

I ran it against a real in-process server as well as your tests: claude and shell workers land in the given directory with the parent lineage intact and a started pane, no agent-case marker is written, and a CLI that is not installed is refused before any session exists. Typecheck, lint, format and the agent CLI, MCP, skill and plugin tests all pass.

A few small things:

  1. src/cli-agent.ts:542 and :544: options.caseName is tested for truthiness, so codeman agent spawn "" (for example spawn "$CASE" with $CASE unset) now launches a worker in the caller's cwd and exits 0. On master that request reached quick-start with caseName: "" and the schema refused it. Please branch on options.caseName !== undefined in both places so only an omitted case means "use cwd", and add a test for the empty string.
  2. Docs that still describe the old mapping: docs/api-reference.md:594 says spawn is POST /api/v1/quick-start and that --json prints the envelope's data; the codeman agent block in README.md (line 984) shows only spawn scratch-1; and the @fileoverview in src/cli-agent.ts (line 5) lists the endpoints the client uses. One sentence each covering POST /api/v1/sessions, POST /api/v1/sessions/:id/interactive|shell and the directory-launch JSON shape is enough.
  3. test/cli-agent-project-spawn.test.ts:38: every new test sets ready: false, but --ready is the default. Please add one test with ready: true on claude that pins the order create, /interactive, then wait-output on the created id, and one with json: false for a failed start (id alone on stdout, message on stderr, exit 1).

Nits:

  • src/cli-agent.ts:541: the unknown-mode check now also refuses named-case spawns locally (exit 4 instead of the server's 400 and exit 1). That is fine by itself, and it deserves a line in the changeset, since the description says named cases are unchanged.
  • src/cli-agent.ts:570: POST /api/sessions always answers { session: { id } }, so the created?.id fallback never runs against a real server, and the three tests that feed ok({ id: OTHER }) exercise only that fallback. Dropping it and using the real shape in the fakes keeps the tests aligned with the route.
  • skills/codeman/SKILL.md:44 (and the plugin mirror): the new paragraph's first line is about 170 characters and the rest is hard-wrapped. A reflow of both copies keeps them identical.

No need to push again for these: I will apply them on top when I merge, unless you would rather push them yourself first.

…ment the directory launch

`codeman agent spawn ""` (for example `spawn "$CASE"` with CASE unset)
used to launch a worker in the caller's cwd. Both case checks now test
`caseName !== undefined`, so an empty name reaches quick-start and is
refused by its schema as before.

Drop the `{ id }` fallback on the create answer: `POST /api/sessions`
answers `{ session: { id } }`, and the test fakes now use that shape.

Tests: an empty case name (unit and through the real argument parser
and HTTP transport), the claude `--ready` order create, /interactive,
wait-output on the created id, and a non-JSON failed start (id alone on
stdout, reason on stderr, exit 1).

Docs: the spawn mapping and directory-launch JSON shape in
docs/api-reference.md, the README and wiki `codeman agent` sections, the
cli-agent @fileoverview and the skill reference tables; reflow the new
SKILL.md paragraph (plugin mirror synced); the changeset notes the
local exit-4 refusal of an unknown mode for named cases.
@aakhter

aakhter commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review and for testing it against a real server. I pushed all of it (c5ffea0), so there is nothing left for you to apply at merge.

  1. Empty case: both checks now use options.caseName !== undefined, so spawn "" goes to quick-start with caseName: "" and the schema refuses it, as on master. Two unit tests cover it (the empty name alone, and the empty name with --working-dir, which is refused like any case plus --working-dir). Two more tests run spawn "" and a bare spawn through the real commander parser and HTTP transport against a local server, which checks that commander really passes "" through.
  2. Docs: docs/api-reference.md, the README codeman agent block and its rules paragraph, and the @fileoverview now describe POST /api/v1/sessions followed by /interactive or /shell, plus the directory-launch --json shape { sessionId, workingDir, created: true, ready } (with startupError on a failed start). I made the same update in the wiki page and in the skill's reference/verbs.md and reference/messaging.md tables, which still said quick-start was the only spawn path.
  3. Tests: a ready: true claude test checks the order create, /interactive, then wait-output on the created id, and a json: false failed-start test checks that stdout has only the id, the reason goes to stderr, the exit code is 1 and no readiness wait runs.

Nits: the changeset now says an unknown --mode is refused locally with exit 4 for named cases too. I removed the created?.id fallback, the fakes now use { session: { id } }, and a new test checks that a bare { id } answer counts as a failure and starts nothing. I reflowed the SKILL.md paragraph and resynced the plugin mirror (sync-plugin.mjs --check is clean).

Typecheck, lint, format and the agent CLI, MCP, skill and plugin tests (201) all pass.

@Ark0N

Ark0N commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Thanks for the quick and thorough follow-up, aakhter. With c5ffea0, codeman agent spawn starts a worker in the current directory or in an existing directory given with --working-dir, through POST /api/v1/sessions plus the mode's start route, while named cases keep using quick-start. I checked every point from the first round and all of them landed: the empty case name now reaches quick-start, the docs and skill tables describe the directory launch, and the ready: true and json: false tests pin the order and the output. Typecheck, lint, format, the plugin mirror and the agent CLI, MCP, skill and plugin tests all pass.

One small thing is left, the flag version of the empty-case issue:

  1. src/cli-agent.ts:548: options.workingDir ?? process.cwd() only falls back on undefined, and commander passes --working-dir "" through as an empty string, which resolve() turns into the cwd. So codeman agent spawn --working-dir "$WT" --mode codex with $WT empty (a git worktree add that failed, say) starts an editing worker in the caller's own checkout and exits 0, the opposite of the separate-worktree advice in SKILL.md. Please refuse an empty (or whitespace-only) --working-dir with exit 4 before any request, with one unit test (agentSpawn(t.d, { ...base, workingDir: '' }) returns EXIT.refused with no calls) and one through the real parser (runSpawn(['--working-dir', '']) sends nothing).

Nit:

  • src/cli-agent.ts:379: the emitJson comment says the --json output is never a reshaped copy of the envelope's data, but a directory spawn now prints { sessionId, workingDir, created: true, ready }. One clause naming that exception keeps the comment true.

No need to push again for these: I will apply them on top when I merge, unless you would rather push them yourself first.

`codeman agent spawn --working-dir "$WT"` with WT empty (a failed
`git worktree add`, say) resolved '' to the caller's cwd and started an
editing worker in the caller's own checkout. An empty or
whitespace-only --working-dir now refuses with exit 4 before any
request.

The emitJson comment names the directory-spawn exception to "the
envelope's data": `{ sessionId, workingDir, created: true, ready }`.

Tests: unit (`''`, spaces, tab: exit 4, no calls) and through the real
argument parser and HTTP transport (nothing sent, exit 4).

Docs: README, api-reference, wiki, SKILL.md (plugin mirror synced) and
the changeset note the refusal.
@aakhter

aakhter commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. I pushed both so nothing is left to apply at merge:

  1. An empty or whitespace-only --working-dir now refuses with exit 4 before any request. Tests: agentSpawn with '', spaces and a tab returns EXIT.refused with no calls, and runSpawn(['--working-dir', '']) (and ' ') sends nothing and exits 4. README, api-reference, wiki, SKILL.md and the changeset mention it.

Nit: the emitJson comment now names the directory-spawn exception, { sessionId, workingDir, created: true, ready } (plus startupError on a failed start).

@Ark0N

Ark0N commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Thanks for the second follow-up, aakhter. With b26234d, codeman agent spawn starts a worker in the current directory, or in an existing directory given with --working-dir, through POST /api/v1/sessions plus the mode's start route, while named cases keep using quick-start.

Both points from the last round landed: an empty or whitespace-only --working-dir refuses with exit 4 before any request, and the emitJson comment names the directory-spawn exception. I also ran the directory launch against a real in-process server: shell and claude workers start in the given directory with the parent lineage and a live pane, and no agent-case marker is written. Typecheck, lint, format, frontend syntax, the plugin mirror check and the agent CLI, MCP, skill and plugin tests (202) pass, and CI is green.

One small thing is left:

  1. src/cli-agent.ts:558: a bare codeman agent spawn run from a session whose cwd is $HOME installs Codeman's hooks into ~/.claude/settings.local.json (and, with agent skill injection on, the skill into ~/.claude/skills, the user-level location every Claude session loads). POST /api/sessions skips workspace hooks when the caller names no workingDir to avoid exactly that (src/web/routes/session-routes.ts:1268-1274: the cwd fallback is $HOME under installer-created services), but the CLI resolves its own cwd and always sends it as an explicit workingDir, so the server cannot tell the two apart. I reproduced it against a real server: chdir to HOME, agentSpawn with no case and no flag, exit 0, and the settings file appears with the hooks block. A session created without a workingDir runs in the server's cwd, so its agent reaches this without any cd. Please refuse (exit 4, before any request) when the directory came from cwd and its realpath equals the realpath of os.homedir(), with a message to pass --working-dir ~ explicitly, plus one unit test. An explicit --working-dir ~ stays allowed, the same as POST /api/sessions with workingDir set to HOME today.

Nit:

  • docs/api-reference.md:594 lists readinessError in the directory-spawn --json shape while the README, the wiki page and the changeset do not, and ready itself is absent with --no-ready or for a mode without a ready mark (JSON.stringify drops undefined). One clause in each place ("ready when a readiness wait ran, readinessError when that call failed") keeps them exact.

No need to push again for these: I will apply them on top when I merge, unless you would rather push them yourself first.

`codeman agent spawn` always sends its directory as an explicit
workingDir, so the server cannot apply its no-workingDir rule and
installs workspace hooks (and an injected skill) into ~/.claude when the
caller's cwd is HOME. A bare spawn whose cwd resolves to the home
directory now refuses with exit 4 before any request and names
`--working-dir ~`; an explicit `--working-dir ~` is still sent. Paths
are compared by realpath, so a symlinked HOME matches, and an
unresolvable HOME or cwd falls back to the resolved path without
throwing.

The directory-spawn --json shape now reads the same in the README, the
API reference, the wiki page and the changeset: `ready` when a
readiness wait ran, `readinessError` when that call failed, and
`startupError` when the start failed. A test pins all four.
@aakhter

aakhter commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, Ark0N. I pushed both items myself as 003398e, so there is nothing for you to apply at merge.

  1. HOME cwd (src/cli-agent.ts): when a bare codeman agent spawn (no case, no --working-dir) runs from a cwd whose realpath equals realpath(os.homedir()), it now refuses with exit 4 before any request: "refusing: the current directory is your home directory; pass --working-dir ~ to start a worker there on purpose". An explicit --working-dir ~ is still sent, the same as POST /api/sessions with workingDir set to HOME. Both sides go through realpath, so a symlinked HOME matches the directory it points to. If either path cannot be resolved, the check compares the plain resolved paths, and it returns false when HOME is empty or homedir() throws. It never throws. Tests in test/cli-agent-project-spawn.test.ts: "refuses a cwd that is HOME with exit 4 before any request and names the explicit flag", "compares realpaths, so a symlinked HOME still matches the cwd it points at", "allows an explicit --working-dir that is HOME, as POST /api/sessions does", "allows a cwd below HOME", "never throws when HOME cannot be resolved, and compares the resolved paths instead", and one more through the real commander parser and an HTTP server ("bare spawn from a HOME cwd sends nothing and exits 4; --working-dir <HOME> is sent").

Nit, --json shape: README, docs/api-reference.md, the wiki page and the changeset now all use the same wording: { sessionId, workingDir, created: true }, plus ready when a readiness wait ran, readinessError when that call failed, and startupError when the start failed. The emitJson JSDoc matches. A new test ("the directory-spawn --json shape is documented the same way everywhere") checks that all four files contain the clause. The skill's reference docs and the plugin mirror never describe the shape, so nothing changed there.

Same-class sweep: spawn is the only place in cli-agent.ts that turns an implicit value into an explicit workingDir. The MCP server sends no directory. The --dir options defaulting to process.cwd() on codeman session start, start and task add in cli.ts call the in-process session manager and task queue, not the HTTP route, so the server's no-workingDir rule doesn't apply to them, and they are unchanged by this PR. I documented the HOME refusal next to the empty --working-dir rule in the README, the API reference, the wiki, the changeset, the --working-dir help text and skills/codeman/SKILL.md (plugin mirror synced).

Trade-off: the refusal applies to every mode, not just claude. Skill injection doesn't depend on the mode, and the rule is easier to predict this way. A shell worker in HOME just needs --working-dir ~.

Mutation checks: turning the guard off fails 3 tests. Applying the guard to the explicit flag too fails 2. Replacing realpath with resolve fails the symlink test. Reverting the README clause fails the doc test. Gates: typecheck, lint, format:check, check:frontend-syntax, check:public-assets, check:browser-excludes and the plugin mirror check all pass. Agent CLI, MCP, skill, plugin and session-routes tests: 16 files, 463 tests, all passing.

@Ark0N

Ark0N commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Thanks for the third follow-up, aakhter. With 003398e, codeman agent spawn starts a worker in the current directory, or in an existing directory given with --working-dir, through POST /api/v1/sessions plus the mode's start route. A bare spawn from a HOME cwd now refuses with exit 4, and named cases keep using quick-start.

The HOME guard does what the last round asked: the realpath comparison, the explicit --working-dir ~ escape and the tests through the real parser all check out. Typecheck, lint, format, frontend syntax, browser excludes, the plugin mirror check and the agent CLI, MCP, skill and plugin tests (11 files, 226 tests) pass.

One thing needs fixing before merge:

  1. test/cli-agent-project-spawn.test.ts:347: the doc-sync test reads .changeset/project-worker-directories.md. At release time npm run version-packages runs changeset version, which consumes and deletes every changeset, so on the release commit this test throws ENOENT and npm test fails on master. I reproduced it by removing the file: 1 failed | 29 passed. Please drop the changeset from that list and keep README, the API reference and the wiki. Changesets are short-lived by design, so a test should never read one.

Smaller things:

  1. CLAUDE.md:132: the Agent CLI paragraph still describes spawn only as quick-start and lists the guards that live in code. One clause covering the directory path (POST /api/v1/sessions plus /interactive or /shell, no Agent-Origin header, so no marker) and its refusals (an empty --working-dir, and a bare spawn from a HOME cwd, needed because the CLI always sends an explicit workingDir, so the server's no-workingDir hooks skip cannot apply) keeps it true. The invariants list in the @fileoverview at src/cli-agent.ts:16 needs the same, and should name test/cli-agent-project-spawn.test.ts alongside test/cli-agent.test.ts.
  2. test/cli-agent-project-spawn.test.ts:322: allows a cwd below HOME stubs HOME to a temp dir but leaves the cwd at the repo root, which is not under it. Mock the cwd to a subdirectory of the temp HOME, so the test covers the case the guard must leave alone (an agent in ~/src/app).
  3. docs/wiki/Driving-Codeman-From-An-Agent.md:59: the sentence groups the empty case name with the empty --working-dir, so "(exit 4, before any request)" reads as covering both. spawn "" reaches quick-start and exits 1, as your own test pins. Splitting the sentence fixes it.

No need to push again for these: I will apply them on top when I merge, unless you would rather push them yourself first.

@Ark0N
Ark0N merged commit 003398e into Ark0N:master Oct 27, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Oct 27, 2026
- test/cli-agent-project-spawn.test.ts:345: the doc-sync test no longer reads .changeset/project-worker-directories.md (changeset version deletes it at release); README, API reference and wiki stay.
- CLAUDE.md:132 + src/cli-agent.ts @fileoverview (invariant 5): the directory spawn (POST /api/v1/sessions plus /interactive or /shell, no Agent-Origin header, so no marker) and its refusals (empty --working-dir, bare spawn from a HOME cwd); the invariants now name test/cli-agent-project-spawn.test.ts too.
- test/cli-agent-project-spawn.test.ts:322: "allows a cwd below HOME" mocks the cwd to <temp HOME>/src/app, so it covers the case the guard must leave alone.
- docs/wiki/Driving-Codeman-From-An-Agent.md:59: split the sentence so exit 4 covers only the empty --working-dir; spawn "" reaches quick-start and exits 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 27, 2026
Ark0N pushed a commit that referenced this pull request Oct 27, 2026
…and guarded submission

Conflicts resolved by keeping both sides:
- CLAUDE.md (Agent CLI): #694's opening (an HTTP client, the experimental
  agent-session-routes behind agentCoordinationEnabled) with #708's clause
  on the directory spawn and its two refusals.
- docs/api-reference.md (codeman agent): #708's spawn paragraph and --json
  shape, with #694's inspect/stage/submit in the verb list and the guarded
  endpoints named before "adds no route for the other verbs".
- src/cli-agent.ts (@fileoverview): #708's quick-start / directory spawn
  routes and #694's experimental routes in one endpoint list; the two
  node:fs/promises imports (#708's realpath+stat, #694's open+stat) merged
  into one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 27, 2026
Replaces the six contributor changesets (#694, #707, #708, #709, #718,
#713) with one consolidated minor changeset covering all 14 PRs, with the
Thanks section first and the release-review fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Ark0N

Ark0N commented Oct 27, 2026

Copy link
Copy Markdown
Owner

Merged in 1.46.0 (https://github.com/Ark0N/Codeman/releases/tag/codeman%401.46.0). Thank you for the HOME guard round; the realpath comparison and the explicit --working-dir ~ escape were exactly right. At merge I applied the four items: the doc-sync test no longer reads the changeset (releases delete it), the CLAUDE.md Agent CLI paragraph and the cli-agent.ts overview cover the directory spawn and its refusals, the "below HOME" test now runs from a cwd inside the temp HOME, and the wiki sentence about exit 4 is split.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants