harnex 0.29.1 → 0.30.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e36a59564be22ba55cda2815e3e72b59b887bb2a4e0417be776295ef1cb10b82
4
- data.tar.gz: 0a9b1355a5bc153e4d6fdd6ef79df81f022c5b6a75f97de0a8f91ef52353e526
3
+ metadata.gz: e3869562568a9927b6a413b5536ee9c489940ce0a8cf9ac6c001072b005ae760
4
+ data.tar.gz: 1203d0e9e327d56ccb63a119bc27489c417dce551e6be530772b2477e6f592b2
5
5
  SHA512:
6
- metadata.gz: 1794b1c8190bf40109732a821470d9a24772be9e68e46b9c49732ac46bd24fb73a88d07d0097dece3ec43e3b164932903d69ffda7fa6b4f18b244989990c8a52
7
- data.tar.gz: 8163c5eb86dacb6174868459fcc9bba2fb87b66c503e61a96f38baefbe2ca93423df3a84fd07da626927402fe5432f4f541eec202a7b4cc4fde131e1b9ff7de4
6
+ metadata.gz: 5f445333438f47be5c9f8f8eef6a90c3ce4844d6d4c398ae9f9a1568d4750de3313f741266b325b07d01fb941599473b6b143c827b34aa8608cab47a7f2bd1db
7
+ data.tar.gz: f5bc3dde8e4ab3b666989418293ce304d53dbcbd4752baf7d17ad1afad86e39af2d8b56bcf28ace274caf5440892ac7111e4fb8dfd02581e9de38757a6659d7e
data/CHANGELOG.md CHANGED
@@ -1,8 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.30.1] - 2026-10-28 | 11:16 AM | +04
4
+
5
+ Holm Queue 174 lost a night to dead lanes reading `running` and refusals
6
+ reading "Unknown error": #93.
7
+
8
+ ### Fixed
9
+
10
+ - **Settled lanes no longer read `running` in machine listings** (#93).
11
+ `harnex lanes --json` reports `done`/`failed` independently of
12
+ `collected`; listing does not collect results. The table, which MCP
13
+ `lanes` also returns, keeps its wording, including `finished, uncollected`.
14
+ - **Pi provider errors have actionable outcomes** (#93). Refusal errors
15
+ (`cyber_policy`, content filter/policy, cybersecurity-risk flags) settle as
16
+ `policy_refusal`; retry guidance requires a rephrased brief or another
17
+ route. Pi's exact `Unknown error (no error details in response)` settles as
18
+ `provider_error_unexplained`, with guidance to inspect provider/gateway
19
+ logs before retrying. Neither suggests continuing the same task unchanged.
20
+ Both classes remain intact in owner-watch events and count toward the
21
+ default orchestration failure budget. `provider_capacity` is unchanged.
22
+
23
+ ## [0.30.0] - 2026-10-25 | 02:55 AM | IST
24
+
25
+ Holm's first nights with lanes (Holm Queue 167): #89–#92. Published to RubyGems.
26
+
27
+ ### Fixed
28
+
29
+ - **Lane names collided** (#89). Identical briefs in sibling worktrees of a
30
+ bare repo all became `pi-read-carry-out-brief`, and `harnex result NAME`
31
+ then failed with `several lanes are named …`.
32
+ - A bare layout (`repo/.bare` plus sibling worktrees) is one repo: its main
33
+ checkout is the worktree on the bare repo's `HEAD` branch, so every
34
+ worktree shares one lane namespace and `harnex lanes` shows them all.
35
+ - Generated names skip every name another lane still holds in any repo
36
+ (working, uncollected, or an unlanded worktree); `--name`, `--worktree`
37
+ and `ask --id` refuse a name while another repo's worker runs under it;
38
+ `result`/`reply` find a working lane from anywhere.
39
+ - A lane working in another worktree is named after it
40
+ (`pi-night-1024_s3`); brief openers such as "You are a … lane" and "carry
41
+ out the brief" no longer make the name.
42
+ - `delegate --worktree` refuses a `--cwd` that is already another worktree
43
+ of the repo (it would have branched from the main checkout instead).
44
+ - **The `gpt` agent invented a `worktree`** from brief prose (#89). It now
45
+ sets `worktree`, `name` and `cwd` only from header lines, takes a new
46
+ `name:` header, and passes `cwd:` on to `result` and `reply`.
47
+ - **A lane in place reported other people's commits** (#91). Its footer
48
+ counts only commits that appeared while the worker ran a git command, and
49
+ names the rest: `0 commits (HEAD also moved 6 commits by others)`.
50
+ - **Messages sent straight to a worker were invisible** to the session that
51
+ started its lane (#92). `harnex send` now records who sent a message, and a
52
+ lane's result lists messages from anyone but the lane's owner and its own
53
+ `harnex reply` (`interjected: 1 message … from a terminal (harnex send):
54
+ "…"`). When a message reached the worker after it had answered, the result
55
+ shows the answers it replaced first, the original task's report among them.
56
+
57
+ ### Added
58
+
59
+ - `.harnex/config.json` `lanes` templates put worktree lanes where the repo
60
+ keeps its worktrees (#90): `{"lanes": {"worktree_path": "../{name}",
61
+ "branch": "night/{mm}{dd}_{name}"}}` (`{name}`, `{yyyy}`, `{mm}`, `{dd}`).
62
+ `land` and `discard` follow each lane's own path and branch.
63
+ - MCP `ask` takes `name`.
64
+
3
65
  ## [0.29.1] - 2026-10-23 | 10:31 AM | IST
4
66
 
5
- Local-only fix release for #88: lanes in repos with a phase allowlist.
67
+ Fix release for #88: lanes in repos with a phase allowlist. Published to
68
+ RubyGems on 2026-10-25 | 02:14 AM | IST.
6
69
 
7
70
  ### Fixed
8
71
 
data/TECHNICAL.md CHANGED
@@ -423,7 +423,10 @@ not a new runtime.
423
423
  base sha, owner (`CLAUDE_CODE_SESSION_ID`/`PI_SESSION_ID`), requested model,
424
424
  Pi `agent_session_id`, accumulated cost, launch time and the last turn's result.
425
425
  States: `running` (a turn is out and uncollected), `done`, `failed`, `landed`,
426
- `discarded`.
426
+ `discarded`. `lanes --json` and MCP `lanes` project current settlement from
427
+ task events (or fresh terminal evidence): `done`/`failed` even before
428
+ collection, with separate `collected` and process-liveness `live` fields.
429
+ Listing is read-only; pending results and the table wording are unchanged.
427
430
  - **Launching:** the verbs shell out to the low-level CLI. `delegate` runs
428
431
  `harnex run pi --id NAME --tmux NAME --cwd WORKDIR --root ROOT --auto-stop`
429
432
  with a brief file (`--context-file`); with `--wait-done` it blocks on the
@@ -436,7 +439,12 @@ not a new runtime.
436
439
  <agent_session_id>`, so Pi resumes the conversation.
437
440
  - **Settling:** after a turn, the lane reads the launch's `usage` event (cost,
438
441
  `agent_session_id`) and records the result; the footer adds commits and a
439
- diffstat from git (`base_sha..HEAD`).
442
+ diffstat from git (`base_sha..HEAD`). Pi error text can classify
443
+ `policy_refusal` (retry with a rephrased brief or another route) or
444
+ `provider_error_unexplained` (inspect provider/gateway logs first). These
445
+ failure classes are preserved in `watch --owner`; `provider_capacity`
446
+ retains its adapter-retries-exhausted meaning. Full vocabulary:
447
+ [dispatch telemetry](docs/dispatch-telemetry.md).
440
448
  - **Worktrees:** `.harnex/worktrees/NAME` on branch `harnex/NAME`; `.harnex/`
441
449
  is added to `<git-common-dir>/info/exclude` on the first write.
442
450
  `land` merges with `git merge --no-edit`, leaves conflicts in progress, and
data/agents/gpt.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gpt
3
- description: "Hand a task or question to a GPT model (a Pi worker run through harnex) and get its reply back verbatim. Use when the user asks for GPT, for another model's opinion or review, or to delegate work to Pi/GPT workers; launch several at once for parallel GPT work, and SendMessage the same agent for follow-ups (the worker keeps its context). While it runs, the harnex `lanes` tool or `harnex lanes` shows what each worker is doing right now (its current tool). Questions and reviews go to a read-only worker; a brief that asks to run commands, tests or benchmarks, or to change files, goes to a worker that can (in the current checkout). The prompt may open with header lines, then a blank line, then the brief: `worktree: NAME` (edit in a new git worktree on branch harnex/NAME; afterwards land it with the harnex `land` tool or `harnex land NAME`, which merges and removes the worktree and branch; don't git merge lane branches by hand), `mode: write` (run commands or edit the current checkout in place), `mode: read` (read-only even if the brief mentions commands), `model: provider/model`, `effort: low|medium|high`, `fallback: provider/model` (read-only asks), `cwd: <dir>`. Write a self-contained brief: the GPT worker has not seen this conversation."
3
+ description: "Hand a task or question to a GPT model (a Pi worker run through harnex) and get its reply back verbatim. Use when the user asks for GPT, for another model's opinion or review, or to delegate work to Pi/GPT workers; launch several at once for parallel GPT work, and SendMessage the same agent for follow-ups (the worker keeps its context). While it runs, the harnex `lanes` tool or `harnex lanes` shows what each worker is doing right now (its current tool). Questions and reviews go to a read-only worker; a brief that asks to run commands, tests or benchmarks, or to change files, goes to a worker that can (in the current checkout). The prompt may open with header lines, then a blank line, then the brief: `worktree: NAME` (edit in a new git worktree on branch harnex/NAME; afterwards land it with the harnex `land` tool or `harnex land NAME`, which merges and removes the worktree and branch; don't git merge lane branches by hand), `mode: write` (run commands or edit the current checkout in place), `mode: read` (read-only even if the brief mentions commands), `model: provider/model`, `effort: low|medium|high`, `fallback: provider/model` (read-only asks), `cwd: <dir>` (work in that directory, e.g. a sibling worktree), `name: NAME` (the lane's name; default: the cwd worktree's name or the brief's first words). Write a self-contained brief: the GPT worker has not seen this conversation."
4
4
  tools: mcp__harnex__ask, mcp__harnex__delegate, mcp__harnex__reply, mcp__harnex__result
5
5
  mcpServers:
6
6
  - harnex
@@ -16,11 +16,16 @@ return its text.
16
16
  ## 1. Split the prompt
17
17
 
18
18
  The prompt may open with header lines `key: value` using only these keys:
19
- `worktree`, `mode`, `model`, `effort`, `fallback`, `cwd`. The header ends at
20
- the first line that is not one of them; drop one blank line after it.
19
+ `worktree`, `mode`, `model`, `effort`, `fallback`, `cwd`, `name`. The header
20
+ ends at the first line that is not one of them; drop one blank line after it.
21
21
  Everything after the header is the brief, kept exactly as written. No header
22
22
  lines means the whole prompt is the brief.
23
23
 
24
+ Only header lines set tool arguments. Never take a `worktree`, `name` or
25
+ `cwd` from the brief's wording, even when the brief names a worktree, a
26
+ branch or a directory: the brief goes to the worker as it is, and the worker
27
+ reads it.
28
+
24
29
  ## 2. Call one tool
25
30
 
26
31
  Pick the tool:
@@ -35,23 +40,26 @@ Then:
35
40
 
36
41
  - **`delegate`:** call it with
37
42
  - `task` = the brief;
38
- - `worktree` = NAME, if given;
43
+ - `worktree` = NAME, only if the header has a `worktree:` line;
44
+ - `name` = NAME, if the header has a `name:` line and no `worktree:` line;
39
45
  - and `model`, `effort`, `cwd` if given.
40
46
  - **`ask`:** call it with
41
47
  - `question` = the brief;
42
- - and `model`, `effort`, `cwd` if given, plus `fallback_model` from
48
+ - and `model`, `effort`, `cwd`, `name` if given, plus `fallback_model` from
43
49
  `fallback:`.
44
50
  - **A later message in this conversation** (a follow-up) goes to `reply`, with
45
51
  - `name` = the lane named in the last result's `── NAME:` footer;
46
- - `message` = the new message, exactly as written.
52
+ - `message` = the new message, exactly as written;
53
+ - and `cwd` if the first prompt's header gave one.
47
54
 
48
55
  Pass every string exactly as written.
49
56
 
50
57
  ## 3. Still working
51
58
 
52
59
  If the tool's text ends with a footer saying `still working`, call `result`
53
- with `names` = [that lane name]. Repeat while it says still working, up to 12
54
- times. Never start a second lane for the same brief.
60
+ with `names` = [that lane name], and `cwd` if the header gave one. Repeat
61
+ while it says still working, up to 12 times. Never start a second lane for
62
+ the same brief.
55
63
 
56
64
  ## 4. Return
57
65
 
data/docs/agents.md CHANGED
@@ -59,10 +59,13 @@ Optional header lines, then a blank line, then the brief:
59
59
  | `mode: read` | Read-only, even if the brief mentions commands |
60
60
  | `model: provider/model`, `effort: low\|medium\|high` | Pick the worker's model |
61
61
  | `fallback: provider/model` | Retry a read-only ask on another model |
62
- | `cwd: DIR` | Work in another directory |
62
+ | `cwd: DIR` | Work in another directory, e.g. a sibling worktree (follow-ups stay there) |
63
+ | `name: NAME` | Name the lane (default: the `cwd` worktree's name, else the brief's first words) |
63
64
 
64
65
  Without a header, questions and reviews go to a read-only worker, and a brief
65
- that asks to run something or change files goes to a worker that can.
66
+ that asks to run something or change files goes to a worker that can. Only
67
+ header lines set these: a brief that mentions a worktree or a branch is passed
68
+ on as it is, never turned into a `worktree`.
66
69
  The definition ships with harnex as [`agents/gpt.md`](../agents/gpt.md).
67
70
 
68
71
  ### The CLI from Claude's Bash tool
@@ -50,6 +50,28 @@ The effective value is the first-class `harnex run --phase TEXT` value, or
50
50
  The allowlist must be an array of non-empty strings. Policy must be `warn` or
51
51
  `reject`.
52
52
 
53
+ ## Lane worktrees
54
+
55
+ `harnex delegate --worktree NAME` creates `.harnex/worktrees/NAME` on branch
56
+ `harnex/NAME`. Repositories that keep worktrees elsewhere can set templates:
57
+
58
+ ```json
59
+ {
60
+ "lanes": {
61
+ "worktree_path": "../night-{mm}{dd}_{name}",
62
+ "branch": "night/{mm}{dd}_{name}"
63
+ }
64
+ }
65
+ ```
66
+
67
+ - Placeholders: `{name}` (the lane), `{yyyy}`, `{mm}`, `{dd}` (the day the
68
+ lane starts). Each template must contain `{name}`.
69
+ - `worktree_path` is relative to the main checkout; `branch` must be a valid
70
+ branch name.
71
+ - Either key may be left out to keep its default. `land` and `discard` follow
72
+ each lane's recorded path and branch, so changing the templates does not
73
+ strand lanes already started. See [Lanes](lanes.md).
74
+
53
75
  ## Events, output, and receipt retention
54
76
 
55
77
  Per-session event JSONL, output transcripts, and generated proof receipts live
@@ -345,10 +345,16 @@ facts prove the delta, not semantic quality or human authorship.
345
345
  - **Completion:** `completed_with_proof`, `completed_with_activity`.
346
346
  - **Proof defects:** `completed_no_activity`, `report_missing`,
347
347
  `report_invalid`, `report_rejected`.
348
- - **Process:** `task_failed` (the turn failed), `provider_capacity` (the turn
349
- failed after the adapter's own provider retries ran out, from Pi's
350
- structured `auto_retry_end success:false`; capacity or rate limits rather
351
- than the work), `boot_failure` (structured
348
+ - **Provider:** `provider_capacity` (the turn failed after the adapter's own
349
+ provider retries ran out, from Pi's structured `auto_retry_end success:false`;
350
+ capacity or rate limits rather than the work), `policy_refusal` (Pi's error
351
+ text names `cyber_policy`, content filter/policy, or a cybersecurity-risk
352
+ flag; retry needs a rephrased brief or another route),
353
+ `provider_error_unexplained` (Pi's exact `Unknown error (no error details in
354
+ response)`; inspect provider/gateway logs before retrying). Explicit error
355
+ classes take precedence over retry exhaustion; they do not infer a refusal
356
+ from an unexplained error. `watch --owner` carries these classes unchanged.
357
+ - **Process:** `task_failed` (the turn failed), `boot_failure` (structured
352
358
  transport died before its first turn), `permission_prompt` (ended on a
353
359
  trust/confirmation prompt the adapter recognizes), `timeout` (`--max-runtime`
354
360
  stopped running work, or exit 124), `stopped_by_caller` (a manual stop
@@ -375,7 +381,8 @@ initial workers do not join families. Start rows are not counted. An optional
375
381
  adds a `failure_budget` verdict and exits 3 once N fatal failures are counted
376
382
  (1 is an error, 2 a usage error; a missing `--dispatch` file is an error).
377
383
  By default the fatal classes are `completed_no_activity`, `task_failed`,
378
- `provider_capacity`, `boot_failure`, `permission_prompt`, `timeout`, and `process_failed`; proof
384
+ `provider_capacity`, `policy_refusal`, `provider_error_unexplained`,
385
+ `boot_failure`, `permission_prompt`, `timeout`, and `process_failed`; proof
379
386
  defects, caller stops and `unknown` are not fatal.
380
387
  Harnex supplies the ledger only: the caller decides what to do on a breach.
381
388
 
data/docs/lanes.md CHANGED
@@ -5,7 +5,8 @@ its follow-ups. You start one with `harnex ask` or `harnex delegate`, collect
5
5
  it with `harnex result`, continue it with `harnex reply`, and finish it with
6
6
  `harnex land` or `harnex discard`. Every lane has a name, runs in a visible
7
7
  tmux window named after it, and leaves a small record so any later command can
8
- find it by name from anywhere in the repository.
8
+ find it by name from anywhere in the repository (from any worktree of it, a
9
+ bare-repo layout included).
9
10
 
10
11
  Lanes are the agent-facing layer of harnex. Everything here is also available
11
12
  as native tools through [`harnex mcp`](mcp.md), and the low-level `run`, `send`,
@@ -65,10 +66,19 @@ cost, commits and the next commands.
65
66
  `harnex/NAME` from `HEAD`. The worker is told to commit there. Parallel lanes
66
67
  never collide, and your checkout is untouched until you land. If your checkout
67
68
  has uncommitted changes, harnex warns that the worktree starts from `HEAD`.
68
- - **Without `--worktree`** the worker works in the current directory, which is
69
- right for tasks that only run or inspect things (tests, a benchmark, a long
70
- command). `--name NAME` names the lane; otherwise harnex derives a name from
71
- the task (`pi-slow-bench`).
69
+ A repo that keeps its worktrees elsewhere says so in `.harnex/config.json`
70
+ (see [Where things live](#where-things-live)).
71
+ - **Without `--worktree`** the worker works in the current directory, or in
72
+ `--cwd DIR`, which is right for tasks that only run or inspect things (tests,
73
+ a benchmark, a long command) and for a worktree you made yourself.
74
+ - **Names.** `--name NAME` names the lane. Otherwise a lane working in another
75
+ worktree of the repo is named after it (`--cwd ../night-1024_s3` gives
76
+ `pi-night-1024_s3`), and any other lane after the task's first words
77
+ (`pi-slow-bench`, then `pi-slow-bench-2`). Generated names skip every name
78
+ another lane still holds anywhere (working, uncollected, or a worktree not
79
+ yet landed), and a name you choose is refused while another repo's worker
80
+ is running under it, so `harnex result NAME` and `reply NAME` find a
81
+ working lane from anywhere.
72
82
  - **`--file PATH`** reads a long brief from a file. Briefs must be
73
83
  self-contained: the worker has not seen your conversation. Name the files, the
74
84
  expected behavior and how to verify it.
@@ -126,6 +136,11 @@ or bare `land`.
126
136
  `harnex discard NAME` stops the lane if it is still working and deletes its
127
137
  worktree and branch without merging.
128
138
 
139
+ **Lanes in place.** For a lane without a worktree, the footer counts only the
140
+ commits the worker made (harnex watches `HEAD` around each git command the
141
+ worker runs). Commits someone else made in the same checkout meanwhile are
142
+ named apart: `0 commits (HEAD also moved 3 commits by others)`.
143
+
129
144
  ## Parallel lanes
130
145
 
131
146
  ```bash
@@ -153,6 +168,26 @@ mid-task as a follow-up; a lane that has finished is relaunched on its own Pi
153
168
  session (`pi --session`), so it remembers everything from the earlier turns.
154
169
  The footer's cost is the lane's total so far.
155
170
 
171
+ **Messages from someone else.** Anyone can `harnex send` to a lane's worker
172
+ (the owner from another terminal, a buddy). The lane's result then says so,
173
+ so the session that started the lane is not left guessing:
174
+
175
+ ```text
176
+ [earlier reply this turn, before another message reached the worker]
177
+ Review: PASS (0 P1, 2 P2) ...
178
+
179
+ [latest reply]
180
+ Understood; the report is now in /tmp/review.md.
181
+
182
+ ── review: done in 6m10s · gpt-6.1-sol · $0.41 · 0 commits
183
+ interjected: 1 message reached the worker from outside this lane (the reply above may answer it; all: `harnex events --id review`):
184
+ 23:26:04 from a terminal (harnex send): "Write the report to /tmp/review.md instead"
185
+ ```
186
+
187
+ The worker's earlier answers in the same turn (the original task's report
188
+ among them) come first; your own `harnex reply` follow-ups are never listed as
189
+ interjections.
190
+
156
191
  ## When a model fails
157
192
 
158
193
  ```text
@@ -164,7 +199,14 @@ The footer's cost is the lane's total so far.
164
199
  For provider and model failures (not found, rate limited, out of capacity) the
165
200
  footer suggests continuing on Pi's default model. `reply --model` resumes the
166
201
  same session, so the task is not lost. For `ask`, `--fallback-model` does this
167
- automatically.
202
+ automatically on `provider_capacity` only.
203
+
204
+ A provider refusal is different: `policy_refusal` means the provider refused
205
+ the content, so a retry needs a rephrased brief or another route. Do not send
206
+ "Continue the same task" unchanged. `provider_error_unexplained` means Pi
207
+ reported exactly `Unknown error (no error details in response)`; read the
208
+ provider/gateway logs before retrying. Harnex does not infer a policy refusal
209
+ when Pi supplies no details.
168
210
 
169
211
  ## Watching a worker
170
212
 
@@ -179,7 +221,12 @@ truncate done 2m03s gpt-5.1 harnex/truncate harnex land tr
179
221
  ```
180
222
 
181
223
  A working lane shows the tool it is running right now (tool names only; harnex
182
- never records tool arguments). `harnex pane --id NAME --lines 40` captures the
224
+ never records tool arguments). `harnex lanes --json` and MCP `lanes` return
225
+ current settlement (`done`/`failed` even before collection), a separate
226
+ `collected` boolean, and process-liveness `live`. `collected: false` means
227
+ `harnex result` still needs to collect the turn; listing does not do that.
228
+ The table keeps `finished, uncollected` for those finished workers.
229
+ `harnex pane --id NAME --lines 40` captures the
183
230
  worker's tmux window, and the window itself is always there to watch.
184
231
 
185
232
  ## Where things live
@@ -188,7 +235,18 @@ worker's tmux window, and the window itself is always there to watch.
188
235
  repo, worktree, branch, model, owner, Pi session, cost, last result). Closed
189
236
  lanes are pruned after 30 days; open lanes never are.
190
237
  - **Worktrees:** `.harnex/worktrees/NAME` inside your repository, on branch
191
- `harnex/NAME`.
238
+ `harnex/NAME`. To keep them beside the main checkout instead, set templates
239
+ in the repo's `.harnex/config.json` (`{name}`, `{yyyy}`, `{mm}`, `{dd}`;
240
+ the path is relative to the main checkout). `land` and `discard` follow the
241
+ lane's own path and branch:
242
+
243
+ ```json
244
+ { "lanes": { "worktree_path": "../night-{mm}{dd}_{name}", "branch": "night/{mm}{dd}_{name}" } }
245
+ ```
246
+
247
+ - **Bare-repo layouts** (`repo/.bare` plus sibling worktrees): the main
248
+ checkout is the worktree on the bare repo's `HEAD` branch (`master`), and
249
+ every worktree of the repo shares its lanes.
192
250
  - **`.harnex/`** is added to the clone's `.git/info/exclude` the first time
193
251
  harnex writes there, so lane worktrees and dispatch rows never show up as
194
252
  untracked work. Nothing tracked changes.
data/docs/mcp.md CHANGED
@@ -17,8 +17,12 @@ servers when a session starts. `harnex doctor` reports the registrations under
17
17
 
18
18
  ## Tools
19
19
 
20
- Each tool runs the matching CLI command and returns its text: the worker's
21
- reply followed by the `── NAME:` footer.
20
+ Each tool runs the matching CLI command and returns its text: normally the
21
+ worker's reply followed by the `── NAME:` footer. `lanes` returns the
22
+ `harnex lanes` table, with each worker's live activity; a settled worker reads
23
+ "finished, uncollected" until its result is collected. Scripts use
24
+ `harnex lanes --json`, whose `state` is `done`/`failed` for a settled worker
25
+ (never `running`), with separate `collected` and `live` fields.
22
26
 
23
27
  | Tool | Arguments | Does |
24
28
  | --- | --- | --- |
@@ -28,7 +32,7 @@ reply followed by the `── NAME:` footer.
28
32
  | `reply` | `name`, `message`; `model`, `wait`, `cwd`, `wait_minutes` | Follow-up to the same worker, which keeps its context |
29
33
  | `land` | `names` (optional); `cwd` | Merge worktree lanes, remove worktrees and branches |
30
34
  | `discard` | `names`; `cwd` | Drop lanes without merging |
31
- | `lanes` | `all`; `cwd` | Open lanes, what each worker is doing, the next step |
35
+ | `lanes` | `all`; `cwd` | Open lane JSON: settled state, collection, liveness, model and branch |
32
36
 
33
37
  Arguments are forgiving: `prompt`, `brief` or `message` work for `task`;
34
38
  `prompt` or `query` for `question`; `lane` or `id` for `name`. `cwd` defaults
data/docs/pi-rpc.md CHANGED
@@ -107,6 +107,15 @@ Harnex maps Pi events as follows:
107
107
  | `agent_settled` without an authoritative final stop reason | fail closed |
108
108
  | EOF, malformed JSON, or request timeout | disconnect/failure |
109
109
 
110
+ A final Pi `error` with refusal text (`cyber_policy`, content filter/policy,
111
+ "flagged for possible cybersecurity risk") settles as `policy_refusal`.
112
+ A retry needs a rephrased brief or another route, not the same task unchanged.
113
+ The exact `Unknown error (no error details in response)` settles as
114
+ `provider_error_unexplained`: inspect provider/gateway logs before retrying;
115
+ it is not evidence of a refusal. Both override exhausted adapter retries;
116
+ otherwise `provider_capacity` keeps its existing retry-exhaustion behavior.
117
+ These classes survive status, dispatch rows and `watch --owner` events.
118
+
110
119
  A forced send while Pi is busy is translated to a `prompt` with
111
120
  `streamingBehavior="steer"`; ordinary sends wait for the settled prompt state.
112
121
  This avoids sending a plain prompt that current Pi correctly rejects while
@@ -238,7 +238,10 @@ harnex watch --owner --stall-after 25m
238
238
  `session_id`, `repo`, `ts`, `work_state`, plus `outcome_class`,
239
239
  `receipt_path`, `elapsed_s`, `stall`, `reason` where they apply, and a
240
240
  readable `msg`. No heartbeats, so Claude Code's `Monitor` wakes only when
241
- something needs a decision.
241
+ something needs a decision. Provider failures preserve `policy_refusal`
242
+ (rephrase the brief or use another route), `provider_error_unexplained`
243
+ (read provider/gateway logs before retrying), and `provider_capacity`
244
+ (adapter retries exhausted) as distinct outcome classes.
242
245
  - **Restarts:** a restarted stream re-announces `started` for running
243
246
  dispatches and delivers only results it has not delivered before (cursor
244
247
  under the owner's notifications directory). Delivery is not acceptance:
data/guides/06_claude.md CHANGED
@@ -70,7 +70,12 @@ harnex reply word-count "Also add TextKit.char_count(text) with a test."
70
70
  A worker still on its turn takes the message as a follow-up. A finished lane
71
71
  is relaunched on its own Pi session, so the worker keeps everything it knew.
72
72
  After a provider-capacity failure, reply "Continue the same task from where
73
- you stopped", optionally with `--model OTHER`.
73
+ you stopped", optionally with `--model OTHER`. Do not do that for
74
+ `policy_refusal`: the provider refused the content, so retry with a rephrased
75
+ brief or another route. For `provider_error_unexplained`, read the
76
+ provider/gateway logs before retrying. `harnex lanes --json` and MCP `lanes`
77
+ separate settled `state` (`done`/`failed`) from `collected` and process `live`;
78
+ uncollected results are still available through `result`.
74
79
 
75
80
  ## Rules For Claude Code
76
81
 
@@ -112,6 +112,9 @@ module Harnex
112
112
 
113
113
  def register_lane(id, model)
114
114
  cwd = File.expand_path(@options[:cwd] || Dir.pwd)
115
+ if (other = Lanes.live_holder(id))
116
+ raise OptionParser::InvalidArgument, "a worker named #{id} is working in #{other['repo_root']}; pick another --id"
117
+ end
115
118
  Lanes.save(
116
119
  "name" => id, "kind" => "ask", "repo_root" => Lanes.main_root(cwd) || cwd, "run_root" => run_root,
117
120
  "cwd" => cwd, "read_only" => !@options[:write], "owner" => Harnex.ambient_owner_id,
@@ -204,7 +207,8 @@ module Harnex
204
207
  answer = full_answer(turn)
205
208
  case exit_code
206
209
  when 0
207
- @out.puts answer.to_s
210
+ lane = answered_lane(result["id"])
211
+ @out.puts(lane ? Lanes.render_answer(lane, turn.merge("final_message" => answer.to_s, "final_message_truncated" => false)) : answer.to_s)
208
212
  @err.puts("harnex ask: #{result['id']} answered in #{elapsed.round}s (model #{turn['model'] || 'Pi default'})")
209
213
  when Waiter::DONE_EXIT_TIMEOUT
210
214
  @err.puts("harnex ask: #{result['id']} is still working; keep waiting with: #{resume_command(result['id'])}")
@@ -212,10 +216,18 @@ module Harnex
212
216
  @out.puts answer unless answer.to_s.empty?
213
217
  reason = turn["last_error"] || turn["outcome_class"] || turn["status"] || result["error"] || "no result"
214
218
  @err.puts("harnex ask: #{result['id'] || 'the worker'} did not answer (#{reason})")
219
+ hint = Lanes.provider_failure_hint("outcome" => turn["outcome_class"])
220
+ @err.puts("next: #{hint}") if hint
215
221
  end
216
222
  exit_code
217
223
  end
218
224
 
225
+ def answered_lane(id)
226
+ id && Lanes.resolve(id, from: File.expand_path(@options[:cwd] || Dir.pwd))
227
+ rescue OptionParser::InvalidArgument
228
+ nil
229
+ end
230
+
219
231
  def full_answer(turn)
220
232
  path = turn["final_message_path"].to_s
221
233
  return File.read(path).chomp if turn["final_message_truncated"] && !path.empty? && File.file?(path)
@@ -17,8 +17,12 @@ module Harnex
17
17
  Options:
18
18
  --worktree NAME Work in a new git worktree (.harnex/worktrees/NAME, branch
19
19
  harnex/NAME); finish with `harnex land NAME` or
20
- `harnex discard NAME`
21
- --name NAME Lane name without a worktree (default: from the task)
20
+ `harnex discard NAME`. The repo's .harnex/config.json
21
+ can move it: {"lanes": {"worktree_path": "../{name}",
22
+ "branch": "night/{mm}{dd}_{name}"}}
23
+ --name NAME Lane name without a worktree (default: the --cwd
24
+ worktree's name, else from the task). A working
25
+ lane's name is unique across every repo
22
26
  --cwd DIR Directory the worker works in (default: current directory)
23
27
  --file PATH Read the task from PATH
24
28
  --model NAME Pi model (provider/model); default: Pi's own default
@@ -86,9 +90,10 @@ module Harnex
86
90
  root = Lanes.main_root(workdir)
87
91
  raise OptionParser::InvalidArgument, "--worktree needs a git repository (#{workdir} is not in one)" if @options[:worktree] && root.nil?
88
92
 
93
+ refuse_nested_worktree!(root, workdir) if @options[:worktree]
89
94
  Lanes.sweep_merged(root)
90
95
  root ||= workdir
91
- name = Lanes.validate_name!(@options[:worktree] || @options[:name] || Lanes.generate_name(root, task))
96
+ name = Lanes.validate_name!(@options[:worktree] || @options[:name] || Lanes.generate_name(root, task, cwd: workdir))
92
97
  ensure_free!(root, name)
93
98
  lane = {
94
99
  "name" => name, "kind" => "delegate", "repo_root" => root, "run_root" => root, "cwd" => workdir,
@@ -125,11 +130,27 @@ module Harnex
125
130
  "lane #{name} already exists (#{existing['state']}): follow up with `harnex reply #{name} \"...\"`, " \
126
131
  "or `harnex land #{name}` / `harnex discard #{name}` first"
127
132
  end
133
+ if (other = Lanes.live_holder(name)) && other["repo_root"] != root
134
+ raise OptionParser::InvalidArgument,
135
+ "a worker named #{name} is working in #{other['repo_root']}; working lanes' names are unique, " \
136
+ "so pick another name (or wait: `harnex result #{name}`)"
137
+ end
128
138
  return unless Harnex.read_registry(root, name)
129
139
 
130
140
  raise OptionParser::InvalidArgument, "a harnex session named #{name} is running; pick another name"
131
141
  end
132
142
 
143
+ # #89: --cwd in a sibling worktree plus --worktree would branch a new
144
+ # worktree from the main checkout, not from --cwd; say so instead.
145
+ def refuse_nested_worktree!(root, workdir)
146
+ top = Lanes.toplevel(workdir)
147
+ return if top.nil? || root.nil? || Lanes.same_dir?(top, root)
148
+
149
+ raise OptionParser::InvalidArgument,
150
+ "--cwd #{top} is already a worktree of #{root}: drop --worktree to work there in place, " \
151
+ "or drop --cwd to start a new worktree from the main checkout"
152
+ end
153
+
133
154
  def warn_uncommitted(root)
134
155
  dirty = Lanes.dirty_paths(root)
135
156
  return if dirty.empty?
@@ -34,7 +34,7 @@ module Harnex
34
34
  Lanes.sweep_merged(Lanes.main_root(Dir.pwd))
35
35
  lanes = selected
36
36
  if @argv.include?("--json")
37
- @out.puts JSON.generate(lanes.map { |lane| lane.merge("live" => Lanes.live?(lane)) })
37
+ @out.puts JSON.generate(lanes.map { |lane| Lanes.snapshot(lane) })
38
38
  elsif lanes.empty?
39
39
  @out.puts "no lanes#{@argv.include?('--all') ? '' : ' in this repo'} (start one with `harnex delegate`)"
40
40
  else
@@ -51,7 +51,8 @@ module Harnex
51
51
  Worker outcome classes (dispatch row outcome.class):
52
52
  completed completed_with_proof completed_with_activity
53
53
  proof completed_no_activity* report_missing report_invalid report_rejected
54
- process task_failed* provider_capacity* boot_failure* permission_prompt*
54
+ provider provider_capacity* policy_refusal* provider_error_unexplained*
55
+ process task_failed* boot_failure* permission_prompt*
55
56
  timeout* process_failed* stopped_by_caller
56
57
  fallback unknown
57
58
  The budget is a ledger, not a breaker: enforcement stays with the caller.
@@ -61,7 +61,8 @@ module Harnex
61
61
  def follow_up(lane, message)
62
62
  argv = @harnex_command + ["send", "--id", lane["name"], "--repo", lane["run_root"], "--message", message]
63
63
  argv += ["--wait-done"] + (@options[:timeout] ? ["--timeout", @options[:timeout].to_s] : []) if @options[:wait]
64
- stdout, stderr, status = Open3.capture3(*argv)
64
+ # Marked as the lane's own follow-up, not an interjection (#92).
65
+ stdout, stderr, status = Open3.capture3({ "HARNEX_SEND_VIA" => "reply" }, *argv)
65
66
  result = Lanes.last_json(stdout)
66
67
  raise Lanes::LaunchError, "harnex could not reach lane #{lane['name']}: #{stderr.strip.lines.last}" if result.empty?
67
68
 
@@ -71,7 +71,7 @@ module Harnex
71
71
  text = if @options[:json]
72
72
  JSON.generate(lane: lane["name"], exit_code: exit_code, state: lane["state"], result: turn)
73
73
  elsif lane["kind"] == "ask" && exit_code.zero?
74
- Lanes.final_message(turn)
74
+ Lanes.render_answer(lane, turn)
75
75
  else
76
76
  Lanes.render(lane, turn, exit_code)
77
77
  end
@@ -141,7 +141,8 @@ module Harnex
141
141
  text: text,
142
142
  submit: @options[:submit],
143
143
  enter_only: @options[:submit_only],
144
- force: @options[:force]
144
+ force: @options[:force],
145
+ from: Harnex.sender_identity
145
146
  )
146
147
 
147
148
  Net::HTTP.start(
@@ -488,7 +488,8 @@ module Harnex
488
488
  }
489
489
  payload[:turn] = event["turn"] if event["turn"]
490
490
  payload[:follow_ups] = event["follow_ups"] if event["follow_ups"]
491
- %w[end_sha commits turn_commits model final_message final_message_path final_message_truncated].each do |key|
491
+ %w[end_sha commits turn_commits model final_message final_message_path final_message_truncated
492
+ earlier_replies].each do |key|
492
493
  payload[key.to_sym] = event[key] if event.key?(key)
493
494
  end
494
495
  exit_code = 0