Commit b7475f0
authored
feat(scouts): isolated scout fleet with hooks, intra-scout subagents, knowledge index, and fleet ops (#46)
* feat: scouts — fleet of focused discovery watchers
Adds a `ralph scout` command that runs a fleet of `watch` discovery
loops, one per focused interest area, with namespaced results and
per-scout manifests. Each scout is an isolated sensor for one topic
cluster: no cross-contamination between, say, ai-security and
ai-eval.
New surface:
- `ralph scout run [--name <n>]` — deploy one or all scouts
- `ralph scout ls` — list configured scouts
- `ralph scout results [name]` — recent runs, per scout or fleet-wide
- `ralph scout init <name>` — scaffold a new scout with an empty
manifest
Layout:
- `scouts/<name>/manifest.json` per scout (same shape as
`watch-manifest.json`, with its own topics/languages/thresholds)
- `results/<scout>/pw-YYYYMMDD-HHmm/` — namespaced run output
- Five scouts seeded: agent-sdks, ai-eval, ai-security, computer-use,
inference-infra
Under the hood:
- `SCOUTS_DIR` constant in `src/utils/paths.ts`
- `runWatch` accepts `manifestPath` and `scoutName` options; results
namespacing flows from `scoutName`
- `watch` gains `--manifest` and `--scout` flags so it can be pointed
at a per-scout manifest directly
- `readManifest`, `listRuns` exported for the scout command
- Steering gets a manifest-lifecycle section documenting auto-add,
freshness tracking, pruning suggestions, and the discoveryLog cap
Gitignore the 105MB compiled `/ralph-for-kiro` binary so it doesn't
sneak into commits; `results/` and `watch-manifest.json` were already
ignored.
Verified: typecheck, lint (biome 2.4.12), 76/76 tests.
* feat(hooks): agentSpawn + stop lifecycle hooks with per-turn sidecars
Wires Kiro CLI's `agentSpawn` and `stop` hooks (added in Kiro 1.24)
into every Ralph agent config so each turn drops a structured
sidecar under the run's `iterations/` directory alongside the
session-derived markdown.
Why: the current runner relies on `<promise>COMPLETE</promise>`
in Kiro's SQLite session history to detect turn end. That works,
but leaves no per-turn audit trail when a run stalls — the
`pw-20260331-0300` run we dug up earlier had `status: running`
and iteration 0 with no way to tell whether kiro-cli ever spawned
an agent at all. The hook sidecars close that gap without replacing
the existing completion check.
Contract for hook scripts — passed via child-process env vars, not
stdin, because the hook's stdin-JSON payload shape varies across
Kiro versions:
RALPH_RUN_DIR absolute path to `results/<scout>/pw-*` or
`results/pw-*` (for non-scout watches)
RALPH_ITERATION 1-based iteration this kiro-cli invocation runs
RALPH_SCOUT_NAME scout name, empty string for non-scout watches
Scripts:
- `.kiro/hooks/on-agent-spawn.sh` — writes
`iterations/NN-spawn.json` at turn start
- `.kiro/hooks/on-stop.sh` — writes
`iterations/NN-turn.json` at turn end
Implementation:
- `KiroClient.runChat(prompt, hookEnv?)` layers RALPH_* onto
`process.env` and spawns kiro-cli with the merged env
- `LoopConfig` gains nullable `runDir` + `scoutName` fields
- `runWatch` threads `resultsPath` + `scoutName` into the loop
config
- `installHookScripts()` helper stamps the two hook scripts
chmod 0o755 under `.kiro/hooks/`; both `ralph init` and
`ralph watch init` call it so new installs ship with hooks
- Hook scripts bundled as text via Bun's loader; `*.sh` module
decl added to types
Tests: 8 new in tests/hooks.test.ts covering env build, script
installation + idempotency + executable bits, and LoopConfig
round-trip for runDir/scoutName. 84/84 pass, typecheck clean,
biome clean.
* feat(scouts): per-scout .kiro/ tree with OS-process isolation
Each scout now gets its own `.kiro/` tree under `scouts/<name>/.kiro/`
— agents, steering, hooks, settings, session history — and kiro-cli
is spawned with cwd = `scouts/<name>/` so Kiro picks up the per-scout
config instead of the shared repo-root one. Scouts on divergent
topics (ai-security, ai-eval, ...) cannot cross-contaminate because
they never share a session, a steering doc, or a SQLite DB.
Changes:
- `ensureScoutKiroTree(scoutDir)` helper (src/core/scout-init.ts)
stamps the per-scout tree idempotently on every run, reusing
the existing agent JSON + hook installer. Agent config is
re-written on each run so bumps propagate; steering is
preserved once customized.
- `LoopConfig` gains `scoutCwd` (nullable). When set, the loop
runner passes it through to `KiroClient.runChat` as the spawn
cwd.
- `KiroClient.runChat` signature widened from (prompt, hookEnv?)
to (prompt, { hookEnv?, cwd? }) — the subprocess cwd override
goes through this path.
- `runWatch` detects scout runs, resolves an absolute results
path (hooks must write there regardless of subprocess cwd),
calls `ensureScoutKiroTree`, and passes `scoutCwd` into the
loop config.
Biome note: turned off `complexity/useLiteralKeys` in biome.json
because it conflicts with TS strict's
`noPropertyAccessFromIndexSignature` on `NodeJS.ProcessEnv`. TS
strict is the authority.
Tests: 2 new in tests/scout-init.test.ts covering tree shape,
executable bits, agent/steering content, and idempotency under
user customization. 86/86 pass.
* feat(scouts): intra-scout probe-topic subagent for parallel per-topic dives
Inside a scout, the project-watcher agent now delegates per-topic
deep dives to a new probe-topic subagent via the `use_subagent`
tool's `InvokeSubagents` command — one subagent invocation per
manifest topic, all in parallel. Each probe runs in isolated
context and writes a single Markdown report to
`<run-dir>/probes/<topic>.md`. The parent watcher reads probe
outputs and owns the synthesis step (summary.md, manifest
updates).
Cross-scout contamination stays impossible because the probe-topic
subagent only gets spawned from a scout's kiro-cli subprocess,
which is already isolated by cwd under `scouts/<name>/`. Subagents
share the parent's session — but the parent session only sees one
scout's manifest because of M2a's process boundary.
Config changes:
- src/data/probe-topic.json — new subagent config, narrow
allowedTools (read, @brave-search, @Tavily, @exa), points at
probe-topic.md steering
- src/data/probe-topic.md — subagent steering: one-topic,
read-only-manifest, structured report contract
- src/data/project-watcher.json and .kiro/agents/project-watcher.json
gain availableAgents=["probe-*"] and trustedAgents=["probe-topic"]
(Kiro 1.25 glob whitelist — nothing else can be spawned)
- src/data/watcher-context.md documents the fan-out pattern
with a copy-pasteable use_subagent InvokeSubagents example;
skip when topic count ≤ 1
Implementation:
- ensureScoutKiroTree() now stamps probe-topic.{json,md} into
each scout's .kiro/{agents,steering}/ alongside project-watcher.
Steering is preserved on user customization; agent config is
re-written to propagate defaults.
Grounded the schema via `kiro-cli chat --no-interactive` tool
introspection — the real tool is `use_subagent` with
{command:"InvokeSubagents", content:{subagents:[{query, agent_name?}]}};
agent_name matches the `name` field of the agent JSON.
Tests: scout-init.test.ts extended to assert probe-topic config +
steering land in the scout's .kiro/, and that the parent's
availableAgents/trustedAgents glob whitelist is present. 86/86 pass.
* feat(scouts): per-scout knowledge-base index + scout-cwd-relative resources
Each scout's project-watcher agent now gets a `scout-history`
knowledge-base `resources` entry scoped to
`results/<this-scout>/**/summary.md` — so prior findings feed the
retrieval layer but an `ai-security` scout can't see `ai-eval`'s
history, and vice versa. Bright line stays at the scout boundary.
Also fixes the `resources` paths. The default agent config ships
with paths relative to the repo-root cwd
(`file://../../watch-manifest.json`,
`file://../ralph-loop.local.json`). When scout isolation puts kiro-cli
in `scouts/<name>/`, those are wrong. scout-init rewrites them to
`file://manifest.json` and `file://.kiro/ralph-loop.local.json`
before stamping the per-scout config, and the repo-root fallback
keeps its old paths intact.
Steering: adds a short section documenting Kiro 1.26 `@path` inline
refs as the cheaper alternative to `fs_read` for known files
(`@manifest.json`, `@.kiro/ralph-loop.local.json`), and explains the
`scout-history` knowledge resource as the natural-language
retrieval path for past iterations.
Decision deferred: global compaction settings
(`compaction.excludeContextWindowPercent` etc.) live in
`~/.kiro/settings/cli.json` per Kiro's 1.24 docs — they're a user-home
setting, not an agent-config field, so they can't be isolated per
scout via cwd. Out of scope for the wrapper; users who want them run
`kiro-cli settings compaction.excludeContextWindowPercent 20` once
at their home level.
Tests: scout-init.test.ts asserts the knowledge-base entry shape,
type="knowledgeBase", and that `include` is scoped to the scout's
own results/ subtree. 86/86 pass.
* feat(scouts): fleet ops — parallel run, status, tail
Three fleet-level commands for operating multiple isolated scouts:
- `ralph scout run --concurrency N` — drain pattern with N
parallel workers, each pulling the next scout off a shared queue
and running it to completion before pulling the next. One
scout's failure is caught per-item so the fleet doesn't abort.
Default concurrency=1 preserves the old sequential behavior.
- `ralph scout status` — one line per scout showing the latest
run's task ID, status, iteration count, wall-clock duration,
and repos-discovered count. Fleet-wide glance.
- `ralph scout tail <name>` — follow the in-flight run of a
named scout by polling the `iterations/` directory for new
hook sidecars (NN-spawn.json / NN-turn.json from M1). Prints
a timestamped line per new sidecar, and stops automatically
when `status.json` flips out of `running`. Configurable poll
interval via `--interval <ms>` (default 2000).
Each parallel worker still runs its scout as a separate kiro-cli
child process with its own cwd under `scouts/<name>/` (M2a), so
concurrency does not compromise isolation — each worker is a fully
independent OS process.
Tests: 4 new in tests/scout-concurrency.test.ts covering the
drain pattern — sequential timing at concurrency=1, parallel
speedup at concurrency=N, one-failure-doesn't-abort invariant,
and input-order preservation in results regardless of finish
order. 90/90 pass.
* fix(tests): drop TOCTOU access+stat pattern flagged by CodeQL
CodeQL's js/file-system-race rule fires when a test calls access(p)
then opens/stats the same path — the file may be deleted between the
two calls. Replaced with a single stat() that throws ENOENT if the
file doesn't exist, which fails the test cleanly without the race.
* fix(tests): use Bun.file() for single-handle reads to clear CodeQL TOCTOU
CodeQL's js/file-system-race fires whenever the same path is passed to
two separate fs calls (stat + readFile, access + stat, ...). The tests
legitimately want to check mode AND content of a file the test itself
just wrote. Switching to Bun.file(path) gives a single handle from
which we can derive stat() and text()/json(), which satisfies the rule
without weakening the assertions.1 parent 86d93c3 commit b7475f0
34 files changed
Lines changed: 1742 additions & 47 deletions
File tree
- .kiro
- agents
- hooks
- steering
- scouts
- agent-sdks
- ai-eval
- ai-security
- computer-use
- inference-infra
- src
- commands
- core
- data
- hooks
- schemas
- types
- utils
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
642 | 642 | | |
643 | 643 | | |
644 | 644 | | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
3 | | - | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
20 | 22 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
153 | 156 | | |
154 | 157 | | |
155 | 158 | | |
156 | 159 | | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
157 | 202 | | |
158 | 203 | | |
159 | 204 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
12 | 15 | | |
13 | 16 | | |
14 | 17 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
0 commit comments