Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ codeoid routine add <name> --prompt … [--cron|--every|--at] [--check <cmd>] #
codeoid routine add <name> --loop --prompt … --verify "id: cmd" [--test] [--attempts N] [--attempt-minutes N] [--run-minutes N] [--budget N] # start a loop (--check = gate, --verify = its acceptance checks)
codeoid routine ls | show | runs | run [--wait] | edit | pause | resume | rm | cancel <runId> | token | preview
codeoid worktree trust [dir] [--off] [--yes] # trust the setup command of a repo's .codeoid/setup.yaml, pinned to its blob (docs/CONFIGURATION.md)
codeoid workstream ls | show <id> | match [dir] [--workstream <id>] # named lines of work and what a folder resolves to (docs/workstreams-design.md)
codeoid new <n> <d> --workstream <id> # a session's home workstream; default: the folder's, else workstreams.default
```

`/mcp/control` (off by default, `mcpControl.*`) lets external agents drive
Expand Down Expand Up @@ -516,6 +518,25 @@ On Claude the PreToolUse hook routes high and not-understood ones to the gate pa
Autonomous mode never waits on a person: the OS sandbox (#448) is its boundary (it doesn't start without one that runs, or the owner's own off). So in autonomous mode a shell command of any level runs (`#riskAsks` is false there; the level goes to the audit, `session.auto_approve … risk=high`), and a write runs where the sandbox lets it land (a host-side file tool beyond its spec is refused there, `#outsideSandbox`). What the sandbox can't be left to hold is refused at once, never asked (`Session#autonomousRefusal`, shared by the gate and the tool_start card; `#refuseAutonomous`: the reason to the agent and on the card, an info line `autonomous.refused`, audit `session.autonomous_refuse kind=…`, no budget spent): `unsandboxed` (`#unsandboxedRequest`), `held_write` (`#heldWrite`), `claude_settings` (`#writesClaudeSettingsOnNamedAccount`), `credential_dir` (a secret-path `ask`, or a shell risk carrying one on a session that isn't sandboxed: `ls ~` runs inside one), `unresolved_path` (`isUnresolvedPathTool`). Credential reads stay refused in every mode. Elicitation tools and fleet send-class dispatch still ask (so they are all an unattended run parks on). When the budget runs out (park) the session reverts to guarded and says so (`#noteAutonomousBudgetSpent`: info line `budget.spent`, `SessionInfo.autonomousBudgetSpent` until a mode is set, `modeSummary` in `codeoid ls`). Guarded and interactive are unchanged. `approvals.askOnUnknown` is inert. Relative paths are judged from the cwd the backend reports (`ToolApprovalFn`'s `shellCwd`: Claude's PreToolUse hook `cwd`, kept for every tool, since the CLI resolves Bash's targets and a Grep/Glob relative `path` from its persisted cwd; null = unknown, asks). Claude's `Monitor` command is a shell call, `PowerShell` never understood. Claude's CLI gets `CDPATH=` and opencode's shells have none, so a bare `cd` goes where the shell reader places it. Codex autonomous asks nothing (#442).
A command that could read another process's environment (a `/proc/<pid>/environ` however written, what a search of /proc hands on, `ps` unless its options and the line's variables prove it shows none) is refused as a credential read on every backend, failing closed (`refuseEnvironRead`, shell-risk.ts); a word's possible values are its `shape` (parse.ts).

Workstreams (`src/daemon/workstreams/`, docs/workstreams-design.md): a session's
home workstream is decided once by `WorkstreamService` (`forCreate`: explicit >
folder match > `workstreams.default`; `forResume`: a stored binding sticks, a
pre-workstreams session is homed once by its folder, `source: "migrated"`) and
kept by the Session in `TranscriptMeta.workstream`, written in BOTH meta saves
and validated when read (`readWorkstreamBinding`). Matching is pure
(`match.ts`: an exclusive claim outranks a suggestion, then the most specific
RULE wins, never the match location; a path rule beats a remote rule;
`upstream`-only matches only suggest); `folder.ts` reads a repository's remotes
from its git config without running git (non-blocking opens: a planted FIFO
must not hang the daemon) and gives a folder's two spellings only — as given
and real — never links found by scanning; spellings that disagree leave no
home. A stored binding that is malformed fails closed (`source: "none"`), never
re-matched; the resume pre-pass (`forResume`) lets forks and children inherit,
and the decided home is written with `TranscriptStore.patchMeta` after resume
succeeded, touching no other field. Every `new Session` site in SessionManager passes
`#workstreamOpts(...)`; children and forks inherit, the conductor's is reserved.
With no workstreams configured no binding exists and nothing changes.

Routines (`src/daemon/routines/`, docs/routines.md) run sessions on a schedule
or an API call. `RoutineService` owns the scheduler and runs; it reaches
sessions only through the `RoutineHost` seam that `SessionManager` implements,
Expand Down
46 changes: 46 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,52 @@ Prefer a command that runs no repository scripts, such as `bun install --ignore-
A setup failure is reported as an infrastructure problem, not the agent's; a routine run fails before its agent starts.
Recipes aren't supported on Windows.

## Workstreams

A workstream is a named line of work: a company, a client, a project, a team, a personal setup.
Each session belongs to exactly one, its home, and the home decides what the session is given.
The design is [workstreams-design.md](workstreams-design.md).

```jsonc
"workstreams": {
"default": "personal", // home for a folder nothing matches; leave out to have none
"isolateGlobalServers": false, // on: a session gets only its workstream's servers (W1)
"items": [
{ "id": "acme", "name": "Acme", "claim": "exclusive",
"match": { "paths": ["~/Workspace/acme-*"], "remotes": ["github.com/acme/*"] } },
{ "id": "research", "match": { "paths": ["~/Workspace/research/**"] } },
{ "id": "personal" }
]
}
```

A session's home is decided at create: `--workstream <id>` (or the web picker), else the folder's, else `default`.
`codeoid workstream match <dir>` says what a folder resolves to and which rule decided it; `codeoid workstream ls` and `show <id>` list the configuration.

Matching:

- A path rule matches the folder it names and everything below it; `~` is your home; `x`, `x/` and `x/**` are the same rule.
A folder matches as you gave it and as its real path, and a rule whose fixed part (`~/Workspace` in `~/Workspace/acme-*`) is a symlink matches through it too.
Nothing else: links are never searched for. If the path you gave and the real path match different workstreams, the folder gets no home until you choose one.
- A remote rule matches the repository's `origin` (or `upstream`) as `host/owner/repo`, read from the repository's git config without running git.
An ssh `Host` alias in `~/.ssh/config` reads as its `HostName`.
A match through `upstream` alone only suggests the workstream.
- An exclusive claim beats any suggestion. Then the most specific rule wins: any path rule beats any remote rule, then the rule with more literal folders, then more literal characters.
Two workstreams tied on a folder give it no home until you choose one. Remote rules are case-insensitive.

`claim: "exclusive"` means the folder runs only with its own workstream: choosing another one for a session there, or forking a session of another workstream into it, is refused.
A delegated token can confirm a folder's own workstream but not choose another; every choice is in the audit log (`session.workstream`).
`claim: "suggest"` (the default) only pre-selects it.

A session keeps its home.
A restart never matches the folder again, so changing the rules affects new sessions only.
A session from before workstreams were configured is given its folder's home once, the first time it resumes (`source: migrated`).
A home deleted from the config leaves its sessions marked orphaned; removing the whole `workstreams` block turns workstreams off instead.
A session whose stored home is unreadable or invalid gets no home (it is never re-matched to its folder).

This is phase W0: a session's workstream is decided, kept and shown, and nothing it is given changes yet.
Servers, credentials, git identity and memory follow in later phases (the design's §13).

## Signing a backend in, instead of pasting a key

Settings → **Claude** → **Sign in with Claude** runs the backend's own login
Expand Down
Loading
Loading