scout-essentials 1.9.1 → 1.9.3

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +18 -1
  3. data/Rakefile +9 -0
  4. data/VERSION +1 -1
  5. data/doc/Improvements.md +20 -0
  6. data/doc/StartHere.md +7 -4
  7. data/doc/developer/AnnotationSystem.md +10 -15
  8. data/doc/developer/Configuration.md +20 -30
  9. data/doc/developer/CoreUtilities.md +52 -22
  10. data/doc/developer/DesignPrinciples.md +7 -9
  11. data/doc/developer/DocumentationPractices.md +476 -0
  12. data/doc/developer/ErrorHandling.md +11 -20
  13. data/doc/developer/LockingAndConcurrency.md +20 -13
  14. data/doc/developer/StreamingModel.md +13 -14
  15. data/doc/developer/Testing.md +389 -0
  16. data/doc/user/AnnotatingData.md +9 -13
  17. data/doc/user/CommandLineOptions.md +9 -12
  18. data/doc/user/Cookbook.md +7 -10
  19. data/doc/user/HandlingStreams.md +22 -29
  20. data/doc/user/LoggingAndProgress.md +9 -11
  21. data/doc/user/ProducingResources.md +0 -1
  22. data/doc/user/RemoteData.md +10 -13
  23. data/doc/user/RunningCommands.md +12 -17
  24. data/doc/user/WorkingWithFiles.md +10 -2
  25. data/lib/scout/config.rb +1 -0
  26. data/lib/scout/indiferent_hash.rb +8 -2
  27. data/lib/scout/log/fingerprint.rb +1 -1
  28. data/lib/scout/named_array.rb +4 -0
  29. data/lib/scout/open/final.rb +1 -1
  30. data/lib/scout/open/stream.rb +14 -13
  31. data/lib/scout/path/util.rb +1 -1
  32. data/lib/scout/testing/availability.rb +264 -0
  33. data/lib/scout/testing/scout_state.rb +193 -0
  34. data/lib/scout/testing.rb +82 -0
  35. data/research/doc-audit-findings.md +7 -5
  36. data/research/doc-structure.md +89 -0
  37. data/research/lock-namespace-blindness.md +73 -0
  38. data/research/probing-guide.md +311 -0
  39. data/research/testing-availability-scout-ai-handoff.md +235 -0
  40. data/research/testing-availability-scout-rig-handoff.md +355 -0
  41. data/scout-essentials.gemspec +24 -3
  42. data/test/scout/annotation/test_dup_clone.rb +31 -0
  43. data/test/scout/indiferent_hash/test_case_insensitive_staleness.rb +27 -0
  44. data/test/scout/indiferent_hash/test_string2hash_false.rb +13 -0
  45. data/test/scout/misc/test_timespan.rb +30 -0
  46. data/test/scout/open/test_compression_detection.rb +32 -0
  47. data/test/scout/open/test_forced_gzip_broken.rb +25 -0
  48. data/test/scout/test_named_array_resolution.rb +49 -0
  49. data/test/scout/test_testing.rb +195 -0
  50. data/test/scout/test_tmpfile_cleanup.rb +23 -0
  51. data/test/scout/testing/test_availability.rb +222 -0
  52. data/test/scout/testing/test_state_bindings.rb +163 -0
  53. data/test/test_helper.rb +26 -12
  54. metadata +23 -2
@@ -0,0 +1,476 @@
1
+ # Documentation Practices
2
+
3
+ This page defines the normative practices for running documentation
4
+ campaigns in the Scout-AI ecosystem (`scout-essentials`, `scout-gear`,
5
+ `scout-ai`) with CortexCoder, Cortex, and ChatAnalyst. It is written for
6
+ campaign Managers planning a documentation review, CortexCoder agents owning
7
+ a subsystem study, Critic/verifier agents gating results, and any agent
8
+ resuming an interrupted campaign. Every practice below carries an evidence
9
+ label; thin or single-reviewer evidence is marked as such and never
10
+ presented as verified. The content is promoted from
11
+ `research/doc-practices-guide.md`, which was retired after Critic-verified
12
+ promotion; its unique associated-elements inventory is preserved in the
13
+ Cortex closure artifact `doc-guide/promotion-record.md`.
14
+
15
+ ## Notation and evidence grades
16
+
17
+ Citations use the compact session keys below. Each advice item carries a
18
+ grade marker showing how strong its evidence is; **no item's grade may be
19
+ read as stronger than marked**.
20
+
21
+ - `[V]` — the cited message was re-read directly (via
22
+ `message_index`/`message_content` or the persisted chat) by the round-3
23
+ extraction that produced this page.
24
+ - `[R1]` — inherited from a round-1 review artifact; not re-verified.
25
+ - `[R2]` — inherited from the round-2 area map; not re-verified.
26
+ - `[REPT]` — ChatAnalyst report accounting only. Reported magnitudes,
27
+ never practice. In particular, token totals are accounting figures; do
28
+ not cite them as behavioral evidence.
29
+
30
+ Session keys (root chats): `ES-ROOT#N`, `GE-ROOT#N`, `SA-ROOT#N` are
31
+ message *N* of the scout-essentials, scout-gear, and scout-ai root
32
+ sessions. Society refs `<root>.files/agent.society/<Agent>/<conversation>/
33
+ agent.chat#N` are abbreviated: essentials `AH` (essentials-annotation-hash),
34
+ `OCS` (essentials-open-cmd-stream), `PRL`
35
+ (essentials-persist-resource-log), `COF` (cortexcoder_open_finish), `OCC`
36
+ (open_cmd_closeout); gear `SCC` (sgdoc-conc), `SKB` (sgdoc-kb), `DC-cli`
37
+ (docconsol-cli), `GE` (gear_entity), `STUB(<conv>)`; scout-ai `SAD`
38
+ (scoutai-subsys-agent-delegation), `STATE` (docrev-state), `B0B4`/`B5B7`
39
+ (docrev-exec-*), `CRITIC` (Critic/docrev-verify), `SUBSYS-*`, `PROMO-*`.
40
+
41
+ ## Areas
42
+
43
+ ### A1 — Campaign architecture: phases, root-vs-society, actor roles
44
+
45
+ Split the campaign into phases with distinct modes and name the boundary
46
+ explicitly. Study phases operate under an explicit "do not modify the
47
+ repository" scope and flip to writes only after a state probe confirms
48
+ authority (SA-ROOT#11 [V]); a phase-1 scope that leaves README untouched is
49
+ a *task scope*, not an omission, and is lifted only by user authorization
50
+ (GE-ROOT#146→#147 [V]).
51
+
52
+ - Do: make one delegation unit = one subsystem in one named conversation;
53
+ dispatch in waves and verify artifacts with `cortex_list` before
54
+ consolidating (SA-ROOT#11 [V]; R1-s D2 [R1]).
55
+ - Do: keep the roles distinct — Manager plans, CortexCoder owns subsystems,
56
+ Critic gates, Searcher/Worker support (SA-ROOT#68 [V]).
57
+ - Don't: keep the root hands-on once work parallelizes. A root doing the
58
+ first investigation itself is fine for a single artifact, but parallel
59
+ branches dispatched outside the parent transcript leave provenance gaps
60
+ (see A8) — this is what happened in the essentials session
61
+ (ES-ROOT#118/#148 [V]).
62
+
63
+ Context: the practice evolved across the three sessions — essentials
64
+ pioneered a root-hands-on root, gear systematized Manager-run batch
65
+ campaigns plus promotion, scout-ai governed phases behind independent
66
+ verification.
67
+
68
+ ### A2 — Docs-first, gap-derived, labeled evidence
69
+
70
+ Review maintained documentation and prior Cortex artifacts *first*, derive
71
+ an explicit gap list, and probe only genuine gaps (STUB(gear_cli)#11 [V];
72
+ GE-ROOT#51 [R1]; SA-ROOT#11 [V]). Never probe what the documentation
73
+ already answers.
74
+
75
+ - Do: label every statement `[doc]`/`[code]`/`[verified]` (plus
76
+ `[artifact]` when prior Cortex work exists) and list doc–code mismatches
77
+ separately with `file:line`. The labels are what make later promotion
78
+ auditable (SA-ROOT#11 [V]).
79
+ - Do: make claims executable: each claim cites an executed probe and its
80
+ property-job receipt, never a transcribed number — "Claims must come from
81
+ executed probes, not transcription" was hard-coded in every essentials
82
+ packet (AH#11 [V]); the finished form is a bold claim plus an Evidence
83
+ paragraph naming result keys and the receipt JSON (ES-ROOT#118 [V]).
84
+ - Do: treat a probe failure as a bug in the probe — fix and re-run, never
85
+ transcribe. The annotation branch looped a failing probe through twelve
86
+ versions until it ran (AH#299/#305→#481 [V]); a delegation study drove
87
+ its probe past v40 the same way (SAD#768/#769 [V]).
88
+ - Don't: write unlabeled claims into durable artifacts (SA-ROOT#11 [V]).
89
+
90
+ ### A3 — Delegation packets, parallel ownership, reconciliation
91
+
92
+ - Do: send five-section self-contained packets — Introduction / Current
93
+ state / Design / Task / Output required — carrying the checkout path,
94
+ HEAD, existing artifacts *with versions*, property digest, naming
95
+ prefixes, and a structured reply contract (AH#11 [V]; SA-ROOT#11 [V];
96
+ GE-ROOT#55 [V], #219 [V]). Self-contained packets are what make stub
97
+ re-dispatch cheap (A5).
98
+ - Do: name one canonical exemplar as the template and partition claim
99
+ territory. Every essentials packet named `path_map.md` the
100
+ "citation-style exemplar" and forbade duplicating it (AH#11 [V]);
101
+ scout-ai packets listed persisted siblings as "read if useful for
102
+ cross-refs, do not duplicate" plus explicit ownership splits
103
+ (R1-s D3 [SA-ROOT#18/#24/#27/#33/#35] [R1]).
104
+ - Do: run parallel owners in batches of three with a cross-edit
105
+ prohibition, and appoint a dedicated reconciler for shared files. The
106
+ reconciler reviews every modified path, fixes shared files, stages and
107
+ commits without pushing (GE-ROOT#51/53/55 [V]; GE-ROOT#219 [V]).
108
+ - Do: serialize long delegations that share an uncommitted tree (gear
109
+ phase-2 ran one at a time against survey-listed conflict risks — R1-g F7
110
+ [R1]); split big close-outs into ordered packets (scout-ai B0–B4 then
111
+ B5–B7, SA-ROOT#61 [V]).
112
+ - Do: for a dead branch, send a closeout-only packet that says "Do NOT redo
113
+ exploration", enumerates the existing probes *and their receipts*, and
114
+ includes a scratch-cleanup step (ES-ROOT#301 [V]).
115
+ - Don't: re-type an "established pattern (follow it exactly)" block into
116
+ every packet — that is a brief's job. The essentials campaign paid that
117
+ cost three times (AH#11, OCS#11, PRL#11 [V]; see A7).
118
+
119
+ ### A4 — Persistence ladder & promotion governance
120
+
121
+ The persistence ladder as practiced: `tmp/` scratch → `probe/` Cortex
122
+ artifacts → claims artifacts → `research/` → `doc/`, with scratch deleted
123
+ after promotion (ES-ROOT#82→#88, #141–#143 [R1]; explicit
124
+ probe→`research/`→`doc/` mapping GE-ROOT#148 [R1]; scout-ai B-series
125
+ SA-ROOT#61 [V]).
126
+
127
+ - Do: promote into `doc/` only under explicit user authorization
128
+ (GE-ROOT#147 [V]).
129
+ - Do: survey before batching. One bounded read-only recon (git state,
130
+ per-subsystem status, conflict risks) can re-cut the whole plan — the
131
+ gear survey found all eight subsystems already had uncommitted doc edits
132
+ and shrank the batches accordingly (GE-ROOT#198/#199 [R1]; final report
133
+ GE-ROOT#225 [V]); in scout-ai the read-only state probe overturned the
134
+ inventory with 20/21 doc files already edited (SA-ROOT#61 [V]).
135
+ - Do: distill before delete. Retirement is a transfer: bounded diff of the
136
+ doomed file against `doc/` and the probe-verified counterpart, promote
137
+ the unique verified facts, drop the unverifiable, then delete
138
+ (SA-ROOT#61 B3/B4/B5 [V]; R1-s D7 [R1]).
139
+ - Do: use decision-bound retention instead of blanket deletion: when a
140
+ file's fate belongs to an open decision, keep it, add a dated retention
141
+ note, and flag it for the maintainer in the final report
142
+ (SA-ROOT#83 [V]).
143
+ - Do: pre-plan commit granularity and per-batch invariants; one retirement
144
+ per commit; deletions never mixed into the commit under test
145
+ (SA-ROOT#61 [V]). Name the record of truth per change class — the
146
+ Improvements ledger records promotions, commit messages record
147
+ retirements — and state the split explicitly (SA-ROOT#69 [V];
148
+ SA-ROOT#83 [V]).
149
+ - Do: route live code defects to the issue ledger with receipts, never
150
+ document broken behavior as normal (SCC#84 [V]); the gear campaign filed
151
+ 48 receipt-cited items with 6 deliberate non-filings, each with a
152
+ recorded reason (GE-ROOT#225 [V]).
153
+ - Do: delete scratch after promotion, verifying each file is superseded
154
+ and leaving unrelated pre-existing `tmp/` material alone (81 files
155
+ deleted, older material kept; ES-ROOT#310 [V]).
156
+
157
+ ### A5 — Interruption, hang, and recovery
158
+
159
+ Distinguish two failure families. A native-blocking hang (`sem_wait`,
160
+ fork+join, cross-pipe misuse) returns `exit_status:-1` with null stdout
161
+ only after the ~3600 s harness kill (SCC#83 [V]); a Cortex-side property
162
+ exception returns fast and is not a hang (OCS#507 [V]).
163
+
164
+ - Do: diagnose a suspect probe standalone under a short shell timeout
165
+ before re-executing it through the entity property (COF#11 step 2 [R1]).
166
+ - Do: salvage dead branches with a bounded closeout packet, never
167
+ re-exploration (ES-ROOT#301 [V]; outcome ES-ROOT#310 [V]).
168
+ - Do: on interruption, trust durable artifacts over memory — "a missing
169
+ transcript cannot establish execution; recover confidence from durable
170
+ outputs" (SA-ROOT#47 [V]) — and resume a mutation campaign only after a
171
+ fresh read-only state probe that closes every planning gap and persists
172
+ a state artifact (STATE#13 [V]; outcome SA-ROOT#83 [V]).
173
+ - Expect: zero-tool stub residue. A dispatch lands (full packet, zero
174
+ inferences, zero tool calls) and execution never starts; the gear
175
+ campaign accumulated five such conversations before the subsystem landed
176
+ elsewhere (STUB(gear_cli)#11 [V] + chat_overview [V]; R1-g F2 [R1]).
177
+ Recovery is re-dispatch — cheap precisely because packets are
178
+ self-contained.
179
+
180
+ ### A6 — Artifact iteration & honest-limits hygiene
181
+
182
+ - Do: write once, refine with small targeted edits, and treat version
183
+ history as the audit trail (`path_map.md` v4 — ES-ROOT#118 [V] plus
184
+ three edits [R1]; annotation claims v6 with one probe at v12 — AH#481
185
+ [V]; scout-ai state artifact closed v3 [R1]).
186
+ - Do: re-read the current version before `cortex_edit` — editing from
187
+ memory fails with "text not found" (ES-ROOT#127/#128 [V]; recovered by
188
+ re-read then re-edit [R1]).
189
+ - Do: encode environment and divergences inside the deliverable: record
190
+ which version observations ran against (installed 1.9.0 gem vs 1.9.1
191
+ checkout in the essentials closeout) and keep both values as citation
192
+ when a sandbox diverges from the host (ES-ROOT#310 [V]).
193
+ - Do: ship a "not probed / why" section in every branch report (AH#481 §5
194
+ [V]; PRL#1661 §5 [V]) and declare missing result keys instead of
195
+ inventing values — demand this verbatim in closeout packets
196
+ (ES-ROOT#301 [V]).
197
+ - Do: strike through stale lines rather than deleting them
198
+ (SA-ROOT#81 [R1]); leave `# ScoutCoder:` gap notes in code where
199
+ undocumented behavior was discovered, as `Observation/probe` does for the
200
+ undocumented `.meta` sidecar layout (ES-ROOT#100 [V]).
201
+
202
+ ### A7 — Cortex workspace usage patterns
203
+
204
+ - Do: recall first at every phase boundary — `cortex_list`/`cortex_search`
205
+ before any dispatch (GE-ROOT#3–#49 [R1]; R1-s [R1]; SA-ROOT#47 [V]).
206
+ - Do: recall before recompute: when a prior campaign already covered 7/8
207
+ subsystems, re-scope the "fresh" instruction to gap-filling and do not
208
+ attribute orphaned prior work to the current campaign (GE-ROOT#42 [V]).
209
+ - Do: use named conversations as durable delegation units and *reuse* an
210
+ executor conversation for follow-ups that depend on its accumulated
211
+ conventions — a one-line repair went back to `docrev-exec-b5b7` "because
212
+ it knows the pattern" (R1-s D9 [SA-ROOT#70/#71/#80] [R1]).
213
+ - Do: define one shared entity property as the probe interface and treat
214
+ the `properties/` registry as the execution ledger: define once with a
215
+ smoke test (ES-ROOT#100 [V]), share across branches, mine for diagnosis
216
+ ("28/28 entries", COF#16/#17 [R1]).
217
+ - Don't: confuse the `ask` tool's `conversation` parameter with the Cortex
218
+ conversations namespace — verify deliverables via `cortex_list prefix=`
219
+ (R1-g F3 [R1]). Don't invoke `Agent/<brief>` before the brief exists
220
+ ("No briefs exist yet"; fall back to the default agent)
221
+ (GE#217/#219 [V]).
222
+ - Gap (thin): the essentials campaign created no conversations, briefs, or
223
+ lists and re-typed its "established pattern" into every packet (AH#11
224
+ [V]) where a brief would have been cheaper. The brief counterfactual
225
+ lives in `guides/briefer-usage.md` and
226
+ `harness/briefed-agent-efficiency.md` — cross-linked artifacts, not chat
227
+ evidence of these campaigns.
228
+
229
+ ### A8 — Provenance, receipts, attribution
230
+
231
+ - Do: route delegation through recorded `ask` calls so `agent_meta`
232
+ receipts survive. The gear root's 12 asks embed 3,111 receipt records
233
+ deduplicated against saved society logs with 0 conflicts (R1-g [R1]);
234
+ scout-ai's asks each returned receipts and the Critic job produced the
235
+ session's single `agent_job` edge (R1-s [R1]).
236
+ - Don't: dispatch work outside the persisted transcript. The essentials
237
+ root launched its three parallel branches and the first closeout with no
238
+ manager turn around them — only one `ask` is recorded (ES-ROOT#301 [V])
239
+ — leaving 54.97 M of the 59.09 M reported tokens unattributed to any
240
+ recorded delegation (chat_report [REPT V]; R1-e D12 [R1]).
241
+ - Do: cite `property_job` receipts (JSON name + definition version +
242
+ digest) per claim; this is the citation currency of every claims
243
+ artifact (ES-ROOT#118 [V]; "Never transcribe numbers without a probe
244
+ receipt", GE-ROOT#51 [R1]).
245
+ - Do: audit citation integrity against the registry before closing —
246
+ "verify every number you write against the actual artifacts/registry";
247
+ the gear synthesis chased a reported sixth mismatch into a transcription
248
+ error and closed at 238 claims / 101 unique receipts / 0 broken
249
+ (GE-ROOT#146 [V]; R1-g D2 [R1]).
250
+ - Expect: interrupted runs drop root segments while society children
251
+ persist — a 14-message zero-call stub holding the full packet
252
+ (PROMO-DOC#13 [V], chat_overview [V]) with no root ask for it
253
+ (R1-s F4 [R1]). Budget a state-reconstruction pass; durable artifacts
254
+ make it cheap (A5).
255
+
256
+ ### A9 — Sandbox, path, and exec friction
257
+
258
+ - Do: state the concrete `/bulk/...` checkout path in packets. `~/git/<repo>`
259
+ does not exist inside bwrap while doc tooling advertises it; the
260
+ divergence is resolved with hypothesis-driven probes plus the designated
261
+ `sandbox_paths` diagnostic (STATE#19/#20/#30/#31 [V]); subsequent
262
+ packets carried the `/bulk` path explicitly (R1-s F1 [R1]).
263
+ - Do: reformulate bash commands against sandbox-visible paths when bwrap
264
+ rejects one (`exit_status:-1`, "Process failed - bwrap …" on an
265
+ unreadable target; DC-cli#77 [V], #1040 [V]). Redirect-heavy one-liners
266
+ fail more often under bwrap (R1-s F2 [R1]).
267
+ - Do: guard every risky subprocess with a shell `timeout`, and know the
268
+ limit: a SIGTERM-based timeout does not interrupt native-blocking
269
+ primitives (`sem_wait` on a missing name), so a "fixed" script can still
270
+ burn the full 3600 s (SCC#82/#83 [V]; use `timeout -s KILL` per R2 §2a
271
+ [R2]). The full friction table is under *Friction table* below.
272
+
273
+ ### A10 — Cost, cache, and context economics
274
+
275
+ All totals in this section are `[REPT]` accounting — reported magnitudes,
276
+ never practice (see *Caveats and evidence limits*). Only the essentials
277
+ total was re-anchored: deduplicated 59,088,981 tt (pt 58,627,925; cct
278
+ 56,841,472 ≈ 96.9 % of pt) over 1,346 calls / 990 events (chat_report +
279
+ chat_tool_calls [REPT V]). scout-ai ≈ 460.6 M tt over 6,989 calls
280
+ [REPT R1]; gear ≈ 775 M tt over 12,254 calls [REPT R1] — both inherited,
281
+ not re-anchored.
282
+
283
+ - Do: prefer cheap local probes; `bash` dominates every session's tool mix
284
+ (essentials 877/1,346 [REPT V]; gear 9,502/12,254 [R1]) and long
285
+ cache-friendly conversations made the economics work.
286
+ - Do: read artifacts page-wise even mid-task — every scout-ai consumption
287
+ used `cortex_read` with `start_line`/`lines` (SUBSYS-INDEX#16/#34 [V];
288
+ R1-s D12 [R1]).
289
+ - Don't: grow single-branch chats without bound — one branch reached 1,662
290
+ messages / 550 calls [R1], the shape that invites compaction (inferred
291
+ risk; no compaction events were observed — R1-e D11 [R1]).
292
+
293
+ ### A11 — Campaign outputs & verification gates
294
+
295
+ - Do: independently re-verify load-bearing claims with fresh minimal code
296
+ in an isolated process before finalizing (ES-ROOT#136/#137 [V]; reported
297
+ at ES-ROOT#148 [V]).
298
+ - Do: gate the final report behind an independent verifier with concrete
299
+ numbered checks, its own methods, a PASS/NEEDS_WORK/BLOCKED verdict, and
300
+ a required "smallest next action". The scout-ai Critic found one real
301
+ defect (13 stale gemspec entries the promotion commits had missed); the
302
+ Manager chose the smallest repair, sent it to the same executor
303
+ conversation, and re-validated (SA-ROOT#68/#69/#72 [V]). Don't expand
304
+ repair scope beyond the verdict.
305
+ - Do: run the test suite and record counts per subsystem before commit —
306
+ including an explicit "no dedicated suite exists" — then review every
307
+ modified path, reconcile shared files, stage intentionally, commit
308
+ without pushing (GE-ROOT#219 [V]; GE-ROOT#225 [V]; R1-g D7 [R1]).
309
+ - Do: audit citation integrity as a closing gate (GE-ROOT#146 [V]) and let
310
+ the verifier expose implicit batch invariants — the gemspec defect
311
+ existed because "deletion must sweep file lists" had never been written
312
+ down (SA-ROOT#69 [V]); encode it as a standard retirement step
313
+ (R1-s F5 [R1]).
314
+
315
+ Context: the verification practice progressed from essentials' spot
316
+ re-verification, to gear's reconciler audit plus test gate, to scout-ai's
317
+ independent Critic with a smallest-repair loop.
318
+
319
+ ## Failure-mode playbook
320
+
321
+ **Native-blocking hang: shell timeout is insufficient.** A planning
322
+ discipline that mandates `timeout 15` wrappers on forking probes
323
+ (SCC#60 [V]) is necessary but not sufficient. In the gear chain, a guard
324
+ with `timeout 120` correctly tripped (`exit=124`) and let the agent
325
+ diagnose its own cross-pipe bug (SCC#76/#77 [V], #78 [V]); after the fix,
326
+ `timeout 180` still hung for the hour — `exit_status:-1`, null stdout,
327
+ bwrap "exceeded 3600 seconds", timestamps 19:43:02.736→20:43:02.778
328
+ (SCC#82/#83 [V]) — because the blocker was
329
+ `ScoutSemaphore.wait_semaphore` (native `sem_wait`), which SIGTERM cannot
330
+ interrupt. Recipes: short `timeout` per invocation; `timeout -s KILL` for
331
+ native blockers [R2]; read `exit_status:-1` + null stdout + a ~3600 s
332
+ timestamp gap as a harness kill, not a quick failure.
333
+
334
+ **Hang recovery: staged, time-boxed, split probes.** After a kill returns,
335
+ wrap each individual script in its own timeout and split verification into
336
+ small scripts so one hang does not block everything: a deterministic
337
+ script with no waits plus a time-boxed script with waits — "even if B
338
+ hangs, A's evidence is in hand" (SCC#84 [V]). Then `pkill` cleanup and a
339
+ `/dev/shm` semaphore check — both themselves messy under bwrap (exit 143;
340
+ SCC#85/#86 [V]). Inbox advice posted during a blocked call is delivered
341
+ only when the call returns; it cannot interrupt it
342
+ (harness-findings/bash-tool-blocking-hangs.md [R1], cited for taxonomy
343
+ only — its message indices are unreliable; see caveats).
344
+
345
+ **Zero-tool stub residue.** A dispatch lands (full packet, zero
346
+ inferences, zero calls) and execution never starts; the gear campaign
347
+ accumulated five such conversations (STUB(gear_cli)#11 [V]; R1-g F2
348
+ [R1]). Recovery is re-dispatch. Whether stub prompts ever reached a model
349
+ anywhere is unproven (see caveats).
350
+
351
+ **Interruption resume: state artifacts plus read-only state probe.** On
352
+ "You were interrupted, please finish up" with no memory of prior work:
353
+ recall the workspace (SA-ROOT#47 [V]), run a dedicated read-only probe
354
+ closing every planning gap (git state, checkout identity, byte counts,
355
+ fix-sweep presence), persist a state artifact, and only then resume
356
+ mutation (STATE#13 [V]; outcome SA-ROOT#83 [V]). In scout-ai this probe
357
+ overturned the inventory — 20/21 doc files were already edited
358
+ (SA-ROOT#61 [V]).
359
+
360
+ **Dead-branch closeout.** Diagnose from the registry (executions with
361
+ forced updates, no claims artifact), then send a closeout-only packet: "Do
362
+ NOT redo exploration", enumerate probes and receipts, require scratch
363
+ cleanup with per-file supersession checks (ES-ROOT#301 [V];
364
+ ES-ROOT#310 [V]). Standalone-under-timeout diagnosis: COF#11 step 2 [R1].
365
+
366
+ **Cortex-side property exception is not a hang.** A fast-failing
367
+ `cortex_entity_property` ("Property execution raised: no implicit
368
+ conversion of Step into Integer", ~0.2 s, from the entity-task rescue)
369
+ blocked a branch at its tail (OCS#500→#507→#521 [V]). What happened after
370
+ the last fast edit (OCS#521, 18:14:05.202Z [V]) is unobservable in the
371
+ transcript (see caveats).
372
+
373
+ **Dispatch-provenance gap.** Work dispatched outside recorded calls leaves
374
+ society children hanging off log edges with no parent record and most cost
375
+ unattributable (essentials: 0 `agent_job` edges, 5 log edges, one recorded
376
+ `ask`; ES-ROOT#301 [V]; chat_report [REPT V]). Recovery is prevention:
377
+ route delegation through `ask`/`cortex_continue`.
378
+
379
+ ## Friction table
380
+
381
+ First recovery move per observed harness/tool friction. Symptom → first
382
+ recovery move, with citation and grade:
383
+
384
+ | Friction | Symptom | First recovery move |
385
+ | --- | --- | --- |
386
+ | `search` path rejected | allowlist exception naming the path | `pwd`, then bash (ES-ROOT#16–#20 [R1]) |
387
+ | `cortex_edit` text-not-found | exception names the missing text | `cortex_read` current version, re-edit (ES-ROOT#127/#128 [V]) |
388
+ | `list_directory` recursive+stats | 142,439 chars truncated at the 100k cap | bash `ls`-based listing (COF#12/#13 [R1]) |
389
+ | `cortex_read type=properties` | ".json" suffix rejected | `cortex_list type=properties` prefix listing (OCC#68/#69, COF#16/#17 [R1]) |
390
+ | bwrap path divergence | `~/git/...` absent, `/home/.../git` ENOENT | hypothesis probes + `sandbox_paths`; carry the `/bulk` path in packets (STATE#19/#20/#30/#31 [V]) |
391
+ | bash under bwrap | `exit_status:-1`, "Process failed - bwrap…" | reformulate against sandbox-visible paths (DC-cli#77, #1040 [V]) |
392
+ | native-blocking hang | `-1` + null stdout after ~3600 s | `timeout -s KILL`; staged split probes (SCC#82/#83, #84 [V]) |
393
+ | `patch` auto-strip under bwrap | strips −p0…−p4 fail, hunk context lost | re-anchor the edit manually (DC-cli#1039/#1040 [R1]; tool identity not re-verified — see caveats) |
394
+ | test-unit multi-file | only the first file executes | one file per runner invocation (STATE#81–#85 [R1]) |
395
+ | `pkill` under bwrap | exit 143, argv elided | verify stray processes/semaphores another way (SCC#85/#86 [V]) |
396
+ | `Agent/<brief>` without a brief | "No briefs exist yet" | continue with the default agent (GE#217/#219 [V]) |
397
+ | ask-vs-Cortex namespace | "No conversation named …" | verify via `cortex_list prefix=` (R1-g F3 [R1]) |
398
+
399
+ ## Caveats and evidence limits
400
+
401
+ These limits are Critic-mandated and binding on this page. They are limits
402
+ of the evidence, never practices to follow.
403
+
404
+ 1. **Message-index mismatch in the bash-hang finding.**
405
+ `harness-findings/bash-tool-blocking-hangs.md` is cited on this page
406
+ **only for its hang taxonomy and its inbox-advice limit**; its message
407
+ indices are unreliable and must not be trusted: its `#52→#53` is in
408
+ fact a fast monitor.rb read pair and `#822→#823` a ConcurrencyModel
409
+ grep at ~22:47Z; the real hang chain is #76/#77 and #82/#83; the
410
+ claimed 22:54:57Z fifth hang was not located.
411
+ 2. **Unresolvable artifact set.** The `scout-gear/*` campaign artifact set
412
+ (overview, 8 claims artifacts, consolidation series) is cited
413
+ throughout `GE-ROOT` and R1 but was **not resolvable via `cortex_list`
414
+ in this workspace's readable maps at extraction time** — verify with
415
+ `cortex_list prefix=` before reuse.
416
+ 3. **The patch-tool friction row is R1-confirmed only.** The re-read of
417
+ DC-cli#1040 confirmed a bwrap process failure but not that the failing
418
+ tool was `patch`; the row stays `[R1]` and is not upgraded.
419
+ 4. Token totals for scout-gear (~775 M tt) and scout-ai (~460.6 M tt) are
420
+ R1/REPT figures; only the essentials total was re-anchored (R2 §7.7).
421
+ Reported magnitudes only — never practice.
422
+ 5. Open threads that stand from the round-2 map: the unobserved
423
+ post-#521 fate of the essentials Open/CMD/Stream branch; the sgdoc-kb
424
+ interior (structural only); whether stub prompts ever reached a model;
425
+ ES-ROOT#302 (552 KB) unread; PRL/AH/COF/OCC interiors R1-sampled only;
426
+ `Searcher/default` and `CortexCoder/default` structurally accounted
427
+ only.
428
+ 6. The "no implicit conversion of Step into Integer" Cortex entity-task
429
+ error deserves its own harness finding (R2 §7.3) — it blocked a branch
430
+ and is distinct from the fork-hang family.
431
+ 7. Essentials compaction risk is inferred, not observed (R1-e D11 [R1]).
432
+
433
+ ## Provenance
434
+
435
+ This page is promoted from `research/doc-practices-guide.md` (491 lines),
436
+ which was retired in a follow-up commit after Critic-verified promotion
437
+ (931f728, PASS 97/100); retirement followed the distill-before-delete
438
+ rule in A4.
439
+
440
+ The guide was produced by a staged process: (1) a chat manifest fixed
441
+ session identity, path caveats, and the guide host repo; (2) three
442
+ independent ChatAnalyst reviewers — one per session, fresh conversations,
443
+ none seeing another's output — produced the round-1 reviews; (3) a distinct
444
+ round-2 mapper reconciled them into the eleven-area taxonomy (A1–A11) with
445
+ gap-closing direct reads; (4) a Critic gate passed that map (PASS, 88/100);
446
+ (5) a round-3 extraction (briefed CortexCoder, conversation `doc-guide`)
447
+ read the artifacts page-wise, spot-verified citations, and wrote the single
448
+ guide file. The guide itself was then Critic-reviewed (93/100) before this
449
+ promotion.
450
+
451
+ Working-review substrate (Cortex logical paths; working artifacts retained
452
+ for provenance, not re-inlined here): `doc-guide/chat-manifest.md`,
453
+ `doc-guide/R1-dimensions-essentials.md`,
454
+ `doc-guide/R1-dimensions-gear.md`,
455
+ `doc-guide/R1-dimensions-scout-ai.md`, `doc-guide/R2-area-map.md`,
456
+ `search/doc-guide-source-map.md`.
457
+
458
+ Underlying sessions (exact spellings; the `~/git/...` form does not
459
+ resolve):
460
+
461
+ - `~/chats/scout-essentials/cortex_coder` — 311 root msgs / 6 chats /
462
+ 1,346 tool calls (09-07→09-08); pioneer root-hands-on campaign.
463
+ - `~/chats/scout-gear/cortex_coder` — 226 root msgs / 41 chats / 12,254
464
+ tool calls (09-08→09-10); Manager-orchestrated gap-fill then
465
+ user-authorized consolidation.
466
+ - `~/chats/scout-ai/cortex_coder` — 84 root msgs / 22 chats / 6,989 tool
467
+ calls (09-10→09-12); eleven labeled subsystem studies, interrupted
468
+ promotion, governed close-out (B0–B7, 22 commits) behind an independent
469
+ Critic gate.
470
+
471
+ Cross-linked campaign artifacts named by the guide (probe sets, claims
472
+ exemplars, harness findings, the scout-ai subsystem index and
473
+ promotion-state pair) remain in the Cortex workspace; the guide's
474
+ §17 associated-elements inventory is preserved verbatim in the Cortex
475
+ closure artifact `doc-guide/promotion-record.md` (campaign closure
476
+ record), which is the authoritative inventory.
@@ -9,8 +9,6 @@ file), `lib/scout/cmd.rb:22-31` (`CMD::Timeout`),
9
9
 
10
10
  ## The taxonomy
11
11
 
12
- Verified by `tmp/rewrite_D/probe_14_exceptions_full.rb`:
13
-
14
12
  ```text
15
13
  ScoutDeprecated parent=StandardError
16
14
  ScoutException parent=StandardError
@@ -42,8 +40,7 @@ not in the raise/catch taxonomy at all.
42
40
 
43
41
  Failures derive from `StandardError` and behave normally. Control-flow
44
42
  signals derive straight from `Exception`, so a bare `rescue =>` (which
45
- means `rescue StandardError`) will **not** see them — verified
46
- `probe_14`:
43
+ means `rescue StandardError`) will **not** see them:
47
44
 
48
45
  ```ruby
49
46
  raise DontClose.new("payload")
@@ -51,7 +48,8 @@ raise DontClose.new("payload")
51
48
  # rescue Exception -> catches
52
49
  ```
53
50
 
54
- Probe results:
51
+ The rescue behaviour follows directly from the superclass — each of
52
+ them extends `Exception`, not `StandardError`:
55
53
 
56
54
  ```text
57
55
  DontClose caught by 'rescue =>' => NOT caught; needs 'rescue Exception'
@@ -90,7 +88,7 @@ raised from `ConcurrentStream#join_threads` when a thread's value is a
90
88
  `Process::Status` that did not succeed (unless `no_fail`).
91
89
 
92
90
  `CMD::Timeout < ProcessFailed` (`lib/scout/cmd.rb:22-31`) adds `command`
93
- and `timeout` readers and composes the message through `super` — verified:
91
+ and `timeout` readers and composes the message through `super`:
94
92
 
95
93
  ```text
96
94
  CMD::Timeout message => "Process 12 failed - command 'sleep 3 ' exceeded timeout of 0.1 seconds"
@@ -118,9 +116,8 @@ exception = (AbortedStream === content and content.exception) ? content.exceptio
118
116
  ```
119
117
 
120
118
  So the caller of `sensible_write` sees the root cause that made the
121
- producer abort, rather than a secondary wrapper. Verified by
122
- `research/behavior-probes.md` P25/P33 and `tmp/rewrite_B/probe_12_swallow.rb`
123
- (`exists after Aborted => false`).
119
+ producer abort, rather than a secondary wrapper (`exists after Aborted
120
+ => false`).
124
121
 
125
122
  The `ensure` always removes the temp file and unlocks a held
126
123
  `Lockfile` (`lib/scout/open/stream.rb:165-171`).
@@ -129,8 +126,6 @@ The `ensure` always removes the temp file and unlocks a held
129
126
 
130
127
  `Misc.insist` (`lib/scout/misc/insist.rb`) retries the block while the
131
128
  code inside raises `TryAgain`; `StopInsist` and `Aborted` break out.
132
- Verified `tmp/rewrite_B/probe_01` / `probe_10_misc.rb` and
133
- `tmp/rewrite_D/probe_04_exceptions.rb`:
134
129
 
135
130
  ```ruby
136
131
  tries = 0
@@ -161,13 +156,11 @@ end
161
156
 
162
157
  - `Persist.persist(..., :canfail => true)` — exceptions from the block are
163
158
  caught; the persist file is removed and `nil` is returned
164
- (`lib/scout/persist.rb:63-100`; verified `tmp/rewrite_D/probe_03_locks.rb`:
165
- `persist canfail => nil`, `file removed: true`).
159
+ (`lib/scout/persist.rb:63-100`; `persist canfail => nil`, `file removed: true`).
166
160
  - `ConcurrentStream` `no_fail: true` — a non-success
167
161
  `Process::Status` from a joined thread is logged at low level instead of
168
162
  raising `ConcurrentStreamProcessFailed`
169
- (`lib/scout/concurrent_stream.rb:113`; also `research/behavior-probes.md`
170
- P26).
163
+ (`lib/scout/concurrent_stream.rb:113`).
171
164
  - `CMD`'s `no_fail` similarly suppresses both `ProcessFailed` from a thread
172
165
  join and exceptions during join.
173
166
 
@@ -181,11 +174,9 @@ happily write empty output unless the producer itself is aborted.
181
174
  `ensure` that removes both. But cleanup is not uniform across the gem:
182
175
 
183
176
  - **`TmpFile.with_file` leaks the temp file when the block raises** — there
184
- is no `ensure` around the `yield`
185
- (`lib/scout/tmpfile.rb:34-48`; verified
186
- `tmp/rewrite_D/probe_05_tmpfile_leak.rb`: the created tmp file is still
187
- present after the block raises `RuntimeError`). Wrap your own `begin/ensure`
188
- around `with_file` if the block can fail.
177
+ is no `ensure` around the `yield` (`lib/scout/tmpfile.rb:34-48`); the
178
+ created tmp file is still present after the block raises `RuntimeError`.
179
+ Wrap your own `begin/ensure` around `with_file` if the block can fail.
189
180
  - `Open.sensible_write` deletes the *target* on failure but only the
190
181
  *temp* file, never other files written by a streaming producer.
191
182
  - `Misc.insist` retries without undoing side effects already produced by
@@ -18,7 +18,7 @@ end
18
18
 
19
19
  `Open.init_lock` (called when the file is loaded) overrides the library
20
20
  defaults, so the effective settings here are **not** the upstream ones
21
- (probe_08):
21
+ :
22
22
 
23
23
  | setting | library default | effective here |
24
24
  |---|---|---|
@@ -34,7 +34,7 @@ attempts sleeps 4 s.
34
34
  ## What a lock looks like on disk
35
35
 
36
36
  Acquiring `<file>` creates `<file>.lock` and a hidden sibling. The payload
37
- written into the lock is four lines (probe_08):
37
+ written into the lock is four lines :
38
38
 
39
39
  ```text
40
40
  /tmp/…/data.txt.lock contents:
@@ -53,6 +53,15 @@ temporary file in the same directory and then **hard-linked** to
53
53
  recognise locks left behind by dead processes and remove them. Once the
54
54
  original temp name is unlinked, `nlink` on the surviving `.lock` is 1.
55
55
 
56
+ The liveness test behind both `sweep` and the stale-age stealing is
57
+ `Process.kill(0, pid)` (`alive?`, `lib/scout/open/lock/lockfile.rb:379`),
58
+ which resolves pids in the **caller's PID namespace**. In a bubblewrap or
59
+ container namespace a host pid is simply absent ⇒ reported dead while its
60
+ process is alive ⇒ the lock can be swept or stolen from a live holder.
61
+ Never share a lock directory across PID namespaces: pin `HOME` (and the
62
+ `tmp` path map) per sandbox. See
63
+ [lock-namespace-blindness](../../research/lock-namespace-blindness.md).
64
+
56
65
  ## `Open.lock` and `LockInterrupted`
57
66
 
58
67
  ```ruby
@@ -75,7 +84,7 @@ raised over the block's own result.
75
84
  ## The three lock namespaces
76
85
 
77
86
  Each subsystem locks in its own directory so the names never collide
78
- (probe_02/probe_03):
87
+ :
79
88
 
80
89
  | who | directory | name shape |
81
90
  |---|---|---|
@@ -83,19 +92,19 @@ Each subsystem locks in its own directory so the names never collide
83
92
  | `Resource#lock_dir` | `$HOME/.scout/tmp/produce_locks` | `TmpFile` digest of the resource path |
84
93
  | `Open.sensible_write` | `$HOME/.scout/tmp/sensible_write_locks` | digest of the output path |
85
94
 
86
- Verified strings:
95
+ The three directories, with an example of each lock file name:
87
96
 
88
97
  ```text
89
98
  Persist.lock_dir => $HOME/.scout/tmp/persist_locks
90
- persist lock file => .../persist_locks/probeD3.persist
99
+ persist lock file => .../persist_locks/<key>.persist
91
100
  Resource#lock_dir => $HOME/.scout/tmp/produce_locks
92
- produce lock file => .../produce_locks/·home·mvazque2·.scout·etc·probeD2
101
+ produce lock file => .../produce_locks/<flattened·produce·path>
93
102
  sensible_write lock dir => $HOME/.scout/tmp/sensible_write_locks
94
103
  ```
95
104
 
96
- (The `·home·mvazque2·...` name is literal `TmpFile.tmp_for_file` output —
97
- each `/` in the probe path was flattened to `·` — and is kept verbatim; the
98
- home-directory component is a machine-specific example value.)
105
+ (The `·flattened·` component is `TmpFile.tmp_for_file` output: each `/`
106
+ in the produce path is flattened to `·` so the lock file name stays a
107
+ single path component.)
99
108
 
100
109
  (See [PersistenceAndResources.md](PersistenceAndResources.md) for how
101
110
  `persist` uses its lock, and [StreamingModel.md](StreamingModel.md) for
@@ -106,10 +115,10 @@ home-directory component is a machine-specific example value.)
106
115
  `Persist.persist(..., :persist_type/:type)` can return a stream that must
107
116
  stay locked while the consumer reads it. Raising `KeepLocked.new(res)` from
108
117
  inside the persist block leaves the lock held; the caller gets the stream
109
- back. probe_03:
118
+ back:
110
119
 
111
120
  ```ruby
112
- res = Persist.persist("probeD3", :text, :persist => false) do
121
+ res = Persist.persist("my-key", :text, :persist => false) do
113
122
  raise KeepLocked.new("payload")
114
123
  end
115
124
  res # => "payload"
@@ -132,8 +141,6 @@ File.exist?(lock_path) # => true (lock still held)
132
141
 
133
142
  ## Thread-safety caveats
134
143
 
135
- Verified by `tmp/rewrite_D/probe_09_threads_fork.rb`:
136
-
137
144
  - `Log::LAST` is a shared top-level `String` — a write from one thread is
138
145
  visible to the next with no synchronisation.
139
146
  - `Scout::Config` holds **no mutex**; concurrent `set`/`get`/`with_config`