strom-research 1.13.2-beta.3 → 1.13.2-beta.5

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.
@@ -1641,12 +1641,15 @@
1641
1641
  "ui.arg.year": "rok",
1642
1642
  "ui.error.arg.extra": "nečekaný argument „{arg}“",
1643
1643
  "ui.error.arg.extra.hint": "strom help {cmd} — hodnotu s mezerami dát do uvozovek",
1644
+ "ui.error.arg.extra.fix.hint": "správně: {fix}",
1645
+ "ui.error.arg.extra.several.hint": "více záznamů: {line}",
1644
1646
  "ui.error.option.is-arg": "{opt} se u {cmd} píše jako argument, ne jako volba",
1645
1647
  "ui.error.option.is-arg.hint": "{cmd} <{arg}>",
1646
1648
  "ui.error.option.unknown": "neznámá volba {opt} u {cmd}",
1647
1649
  "ui.error.option.unknown.hint": "volby: {options} · strom help {path}",
1648
1650
  "ui.error.option.unknown.none": "neznámá volba {opt} u {cmd}",
1649
1651
  "ui.error.option.unknown.none.hint": "{help}",
1652
+ "ui.error.option.unknown.status.hint": "jistota (--status) patří jen k údaji (E…), nikdy ke jménu ani k rodině: strom cite E… S… --status probable|proven, strom event edit E… --status …",
1650
1653
  "ui.error.option.no-value": "{opt} se píše bez hodnoty",
1651
1654
  "ui.error.option.needs-value": "{opt} potřebuje hodnotu {value}",
1652
1655
  "ui.error.option.needs-value.hint": "strom help {path}",
@@ -1641,12 +1641,15 @@
1641
1641
  "ui.arg.year": "Jahr",
1642
1642
  "ui.error.arg.extra": "unerwartetes Argument „{arg}“",
1643
1643
  "ui.error.arg.extra.hint": "strom help {cmd} — Werte mit Leerzeichen in Anführungszeichen",
1644
+ "ui.error.arg.extra.fix.hint": "richtig: {fix}",
1645
+ "ui.error.arg.extra.several.hint": "mehrere Datensätze: {line}",
1644
1646
  "ui.error.option.is-arg": "{opt} ist bei {cmd} ein Argument, keine Option",
1645
1647
  "ui.error.option.is-arg.hint": "{cmd} <{arg}>",
1646
1648
  "ui.error.option.unknown": "unbekannte Option {opt} für {cmd}",
1647
1649
  "ui.error.option.unknown.hint": "Optionen: {options} · strom help {path}",
1648
1650
  "ui.error.option.unknown.none": "unbekannte Option {opt} für {cmd}",
1649
1651
  "ui.error.option.unknown.none.hint": "{help}",
1652
+ "ui.error.option.unknown.status.hint": "eine Sicherheit (--status) gehört nur zu einer Angabe (E…), nie zu einem Namen oder einer Familie: strom cite E… S… --status probable|proven, strom event edit E… --status …",
1650
1653
  "ui.error.option.no-value": "{opt} hat keinen Wert",
1651
1654
  "ui.error.option.needs-value": "{opt} braucht einen Wert {value}",
1652
1655
  "ui.error.option.needs-value.hint": "strom help {path}",
@@ -4,11 +4,14 @@ You work to the Genealogical Proof Standard: search thoroughly, cite every
4
4
  fact, analyse and correlate the evidence, resolve conflicts, and write the
5
5
  conclusion so the next researcher can follow it.
6
6
 
7
+ <!-- for link verify enrich intake locate narrate -->
7
8
  - **Certainty is explicit.** `proven` — you read the original record yourself
8
9
  and it states the fact directly. `probable` — good evidence, but indirect or a
9
10
  single reading of hard handwriting. `possible` — weak evidence. `lead` — a
10
11
  family story, a family tree, an index or a guess: something to check, never a
11
12
  fact. Family trees and memories are leads, always.
13
+ <!-- end -->
14
+ <!-- for link verify enrich intake locate -->
12
15
  - **A namesake is not your person.** Identify by year, place, house, occupation,
13
16
  spouse and parents — the mother's maiden name decides most cases. Two entries
14
17
  that give one name different parents (another mother) are **two people** —
@@ -20,25 +23,36 @@ conclusion so the next researcher can follow it.
20
23
  - **Negative results are results.** Every search is recorded (`strom search add
21
24
  … --result negative`), with exactly what was covered. The most expensive
22
25
  mistake is to search the same book twice.
26
+ <!-- end -->
23
27
  - **Check the premise first.** Before opening anything, read the brief's
24
28
  *Already known* — the searches of the task's books, people and surnames;
25
29
  `strom searched <where>` only for a book, place or years it does not list.
30
+ Work that answers another open task as well closes it with yours:
31
+ `strom session close --done "…" --next "…" --also-done T…`.
32
+ <!-- for link verify enrich intake -->
26
33
  - **Extract everything the first time** you open a record: names, ages, house
27
34
  numbers, occupations, godparents, witnesses, midwife, remarks in the margin.
28
35
  Opening the same page again later costs more than writing it down now.
36
+ <!-- end -->
37
+ <!-- for link verify enrich intake locate -->
29
38
  - **Nothing is deleted.** A wrong fact is retracted with a reason; a changed
30
39
  fact is edited with a reason. Contradictions become conflicts, competing
31
40
  explanations become hypotheses — never silently pick one.
41
+ <!-- end -->
32
42
  - **Write as you go.** Record each finding as soon as you have it. Your context
33
43
  can be cut or summarised at any moment, and a session working alone is
34
44
  stopped at its time limit (the brief says when; near the end strom's output
35
45
  counts down): what is only in the conversation is lost, what is in strom is
36
- not. Going through many images, record each stretch when it is done (every
46
+ not.
47
+ <!-- for link verify enrich intake -->
48
+ Going through many images, record each stretch when it is done (every
37
49
  ten images, each district or year): the finds, and `strom search add …
38
50
  --pages` for what was searched in vain — never all of it at the end.
39
51
  A fact of a person goes on the person (`event add`); a note or the diary is
40
52
  for what is not a fact yet. What you held back while who it was stayed a
41
53
  hypothesis goes on the person when you decide it.
54
+ <!-- end -->
55
+ <!-- for link verify enrich intake locate -->
42
56
  - **Lessons belong where they apply.** A quirk of a register (its calibration,
43
57
  two years per page, a hand that writes 7 like 1) goes to `strom lesson add
44
58
  --on B…`, so whoever opens that book next sees it.
@@ -50,6 +64,8 @@ conclusion so the next researcher can follow it.
50
64
  queue holds back every other task about people off the tree
51
65
  (`strom task list --off-tree`); they come back by themselves once the link
52
66
  is recorded.
67
+ <!-- end -->
68
+ <!-- for link verify enrich intake -->
53
69
  - **Say what a hypothesis would connect.** A hypothesis about a connection —
54
70
  whose child, the same person, a couple, siblings — gets each variant's links
55
71
  when you record or argue it: `strom hypothesis link H… B --child P… --of F…`
@@ -63,11 +79,15 @@ conclusion so the next researcher can follow it.
63
79
  (`strom hypothesis variant H… "<claim>"`). Deciding one whose variants have
64
80
  links names the variant it is decided for:
65
81
  `strom hypothesis decide H… --variant B --decision "…"`.
82
+ <!-- end -->
66
83
  - **Stay within the task.** New questions become new tasks (`strom task add`,
67
- with where and done-when), not detours. A new task whose images are not here
84
+ with where and done-when), not detours.
85
+ <!-- for link verify enrich intake locate -->
86
+ A new task whose images are not here
68
87
  gets them now: through the archive's connector (built first when it has
69
88
  none), or — where the archive allows only that — it waits at once for what
70
89
  the user should download (`strom task wait T… --images B…:<numbers> --on
71
90
  "…"`); a later session would only find that out.
91
+ <!-- end -->
72
92
  - **Hand over cleanly.** Close with a summary of what was proven, what was
73
93
  searched in vain, and the next cheapest step.
@@ -13,7 +13,11 @@ about 80 views write down what you found before you open more.
13
13
 
14
14
  - **Look only through views**: `strom media view B0001:57` (record set and
15
15
  image number) makes a file in `.strom/views/` — open that file. Whole images
16
- come reduced: good for "is our surname on this page?", not for reading.
16
+ (and `--grid`) come reduced: for finding the entry, never for reading it.
17
+ **Transcribe only from a crop** at full resolution (`--crop`, or `--half`
18
+ where the entry fills the page). A whole view unclear, or the entry not where
19
+ expected: `--half both` before calling it not found — a negative search only
20
+ after the pages were seen at full resolution.
17
21
  - **The views of a scan, or of a batch, in one call**, then open all the files
18
22
  it lists together — not one call and one look per view:
19
23
  `strom media view B0001:57-60 --half both` (the two pages of each double
@@ -26,11 +30,13 @@ about 80 views write down what you found before you open more.
26
30
  width, height as fractions) or `--half left|right` for one page of a spread.
27
31
  A crop comes at full resolution (small ones enlarged). `--contrast` for faded
28
32
  ink, `--rotate 90` for sideways pages.
33
+ <!-- for link verify enrich part -->
29
34
  - **Too small to read?** When a crop says it is enlarged, the scan has no more
30
35
  detail there. A connector that can fetch a part of an image sharper says so
31
36
  in that line: `strom fetch <connector> --recordset B0001 --images 57 --crop
32
37
  …` (one request). Then view the same crop again: it comes from the sharper
33
38
  part by itself.
39
+ <!-- end -->
34
40
  - **Browsing a book is a reader's job**: `strom read B0001 --images 40-69
35
41
  --question "…"` — readers in batches of ten, each with the full question.
36
42
  Their reports stay in notes/readings/; `strom read` ends with what they
@@ -44,14 +50,14 @@ about 80 views write down what you found before you open more.
44
50
  It waits for its readers (often ten minutes or more): run it in the
45
51
  foreground and let it finish — your session ends with your turn, and
46
52
  whatever is left running in the background ends with it.
47
- (Your own subagents can read too — on your own model, never a faster one — in batches of at most twelve — but what they
48
- report is lost with your session unless you write it down: record each
49
- report as it comes back, before you send the next readers.) Only the entries
53
+ (A scan-reader subagent where your agent has one, strom-scan-reader, or your
54
+ own subagents — on your own model, never a faster one — read too, in batches
55
+ of at most twelve with the whole question; what they report is lost with
56
+ your session unless you write it down: record each report as it comes back,
57
+ before you send the next readers.) Only the entries
50
58
  that will be cited need your own eyes, at full resolution.
51
59
  - Old handwriting is decoded, not copied: never a weaker model for handwriting,
52
60
  never a guess. "Illegible" is a valid and valuable answer.
53
- - **Extract everything the first time**: names, ages, house numbers,
54
- occupations, godparents, witnesses, midwife, remarks in the margin.
55
61
  - **Report image by image, as you go**: the image and page, what was found (or
56
62
  nothing), what was illegible and where, the hand, how sure each name is.
57
63
  - **Cite the image, and where the entry is on it**: `strom source add …
@@ -61,12 +67,16 @@ about 80 views write down what you found before you open more.
61
67
  numbering jumps) were not searched: say so in `--note` (strom read puts the
62
68
  gaps its readers saw there), or leave them out of the range
63
69
  (`--pages 40-52,55-69`).
70
+ <!-- for verify enrich intake -->
64
71
  - Page ↔ image: `strom recordset calibrate B0001 --point 57=112` (measured on
65
72
  the image, never guessed); then `strom media view B0001 --page 112` works.
66
- - **No images here yet:** a connector for that archive
67
- (`strom connector list`) fetches them through strom:
68
- `strom fetch <connector> <book> --images 40-69 --recordset B0001` — only the
69
- images you need, never a whole book "just in case".
73
+ <!-- end -->
74
+ <!-- for link verify enrich connector -->
75
+ - **No images here yet:** the book's connector fetches them through strom
76
+ (the brief's record set gives the exact `strom fetch`) — only the images you
77
+ need, never a whole book "just in case".
78
+ <!-- end -->
79
+ <!-- for link verify enrich no-connector -->
70
80
  - **The archive has no connector yet: build one — now, you.** The user does
71
81
  not know connectors exist and will not ask for one. Tell them in a sentence
72
82
  ("for this archive I am preparing a downloader; it will fetch only the images
@@ -74,10 +84,14 @@ about 80 views write down what you found before you open more.
74
84
  follow its DISCOVERY.md (the portal's terms and robots.txt first, official
75
85
  exports preferred), test it with `strom connector test`, then fetch. It is
76
86
  built once per archive and serves every later task.
87
+ <!-- end -->
77
88
  - You never download from an archive yourself (curl, a script, your browser
78
- tools) — only through a connector, paced by strom. The user saves images by
89
+ tools) — only through a connector, paced by strom.
90
+ <!-- for link verify enrich no-connector by-hand -->
91
+ The user saves images by
79
92
  hand only where the archive does not allow automation (the connector then
80
- finds books and gives links only) or where a check stops it. Ask with
93
+ finds books and gives links only), where its connector only finds books, or
94
+ where a check stops it. Ask with
81
95
  `strom task wait T… --images B0001:40-69 --on "…"`: strom makes the folder
82
96
  of the inbox they go into, shows it to the user with the book's link, and
83
97
  checks the numbers of what arrives. Write `--on` for the user, in their
@@ -88,3 +102,4 @@ about 80 views write down what you found before you open more.
88
102
  the entry and save that view too (40a.jpg); a full-resolution scan can be
89
103
  ordered from the archive. Then take the next task; when the images are
90
104
  registered the task comes back by itself.
105
+ <!-- end -->
@@ -2,8 +2,11 @@
2
2
 
3
3
  Record a found entry in one batch: write the lines to a file in notes/ and run
4
4
  `strom batch --file notes/<file>` — all or nothing: when a line fails nothing is
5
- written and every failing line is said at once; fix them, run it again.
6
- `#name` labels what a line creates, `@name` uses it later.
5
+ written and every failing line is said at once; fix them in the file with your
6
+ file-editing tool, not with sed or another shell command (the tree's rules let
7
+ the shell run strom, not edit files), run it again. `#name` labels what a line
8
+ creates, `@name` uses it later; a found search comes after the source it found
9
+ (`--found @s`).
7
10
 
8
11
  source add "Baptism of Jan Novák 1885" --kind baptism --recordset B0001 --clip B0001:57@0.05,0.40,0.45,0.18 --locator "pag. 112, entry 2" --language la --information primary --transcript @notes/entry.txt #s
9
12
  event add P0001 CHR --date "25 JUN 1885" --place "Týnec" --house 13 --cite @s --quote "baptizatus est" --with "godparent:Marie Dvořáková" --with "midwife:Anna Nová" --with "officiant:P. Josef Kříž" --status proven
@@ -22,7 +25,8 @@ written and every failing line is said at once; fix them, run it again.
22
25
  - `--transcript` is the entry's words as they stand in it — its own language,
23
26
  spelling and abbreviations, line by line, unread letters `[?]`; the Strom app
24
27
  shows them next to the entry's image. Every entry read gets them.
25
- - A fact already in the tree gets the citation (`cite E…`), not a second fact —
28
+ - A fact already in the tree gets the citation (`cite E…`, `--status` is a
29
+ fact's only — a name or a family cited takes none), not a second fact —
26
30
  and what the record adds to it: `event edit E… --age husband:27 --age wife:17
27
31
  --house 21 --with "witness:…" --with "officiant:…"` (filling in needs no reason).
28
32
  - The names a record gives go on the person: `name add` (a maiden name completes
@@ -16,6 +16,7 @@ import { agentAppUrl, appSite } from "../core/stromapp.js";
16
16
  import { configDir } from "../core/paths.js";
17
17
  import { agentBrowser } from "../core/connector.js";
18
18
  import { CHROME_ALLOW, CHROME_DENY, chromeDomain } from "../core/browser.js";
19
+ import { claudeScanReader, opencodeScanReader, SCAN_READER } from "./scanreader.js";
19
20
  export const MARKER = "<!-- strom: generated above this line (strom agents sync); your own notes below are kept -->";
20
21
  function agentsMd(tree) {
21
22
  const lang = langName(tree.config.lang);
@@ -313,6 +314,8 @@ export function opencodeConfig(tree) {
313
314
  description: "strom run: the agent working alone, nobody to ask — what the rules do not allow is refused",
314
315
  permission: refused(permission),
315
316
  },
317
+ // reading scans: the shell for strom and the file reader, nothing else
318
+ [SCAN_READER]: opencodeScanReader(tree, permission, refused),
316
319
  },
317
320
  };
318
321
  }
@@ -343,6 +346,7 @@ function dropAgentFiles(tree) {
343
346
  ["AGENTS.md", agentsMd(tree)],
344
347
  ["CLAUDE.md", claudeMd(tree)],
345
348
  [path.join(".claude", "settings.json"), JSON.stringify(claudeSettings(tree), null, 2) + "\n"],
349
+ [CLAUDE_SCAN_READER, claudeScanReader(tree)],
346
350
  ["opencode.json", JSON.stringify(opencodeConfig(tree), null, 2) + "\n"],
347
351
  [path.join(".grok", "config.toml"), grokConfig(tree)],
348
352
  [path.join(".grok", "rules", "strom.md"), GROK_RULES],
@@ -370,7 +374,9 @@ function dropAgentFiles(tree) {
370
374
  }
371
375
  return gone;
372
376
  }
373
- export const AGENT_FILES = ["AGENTS.md", "CLAUDE.md", path.join(".claude", "settings.json"), "opencode.json", path.join(".grok", "config.toml"), path.join(".grok", "rules", "strom.md")];
377
+ /** Claude Code's subagent for reading scans, in the tree. */
378
+ export const CLAUDE_SCAN_READER = path.join(".claude", "agents", `${SCAN_READER}.md`);
379
+ export const AGENT_FILES = ["AGENTS.md", "CLAUDE.md", path.join(".claude", "settings.json"), CLAUDE_SCAN_READER, "opencode.json", path.join(".grok", "config.toml"), path.join(".grok", "rules", "strom.md")];
374
380
  export function syncAgentFiles(tree) {
375
381
  // an archive: no agent works on it, and nothing of one shows in its folder (Milan's decision, 2026-10-03) — strom's own
376
382
  // files of the agents go (what a person wrote in them stays); research switched on writes them again
@@ -396,6 +402,7 @@ export function syncAgentFiles(tree) {
396
402
  write("AGENTS.md", agentsMd(tree));
397
403
  write("CLAUDE.md", claudeMd(tree));
398
404
  write(path.join(".claude", "settings.json"), JSON.stringify(claudeSettings(tree), null, 2) + "\n");
405
+ write(CLAUDE_SCAN_READER, claudeScanReader(tree));
399
406
  write("opencode.json", JSON.stringify(opencodeConfig(tree), null, 2) + "\n");
400
407
  write(path.join(".grok", "config.toml"), grokConfig(tree));
401
408
  write(path.join(".grok", "rules", "strom.md"), GROK_RULES);
@@ -10,6 +10,11 @@
10
10
  export const IMAGE_MAX = 1568;
11
11
  /** What the newer models take in whole, and Claude Code, Codex and OpenCode pass on unshrunk. */
12
12
  export const IMAGE_MAX_LARGE = 2000;
13
+ /**
14
+ * The long side of a whole image shown to find an entry on it (strom media view without --half, --crop or --split):
15
+ * the entry is found as surely as at 2000 px for half the tokens; it is read from a crop or a half at the model's size.
16
+ */
17
+ export const OVERVIEW_MAX = 1400;
13
18
  /** A Claude model name or alias ("opus", "claude-opus-4-7", "claude-sonnet-5-5[1m]", "us.anthropic.claude-…"): its family and version. */
14
19
  export function claudeModel(model) {
15
20
  const m = /(opus|sonnet|haiku|fable|mythos)(?:[-.]?(\d{1,2})(?:[-.](\d{1,2})(?!\d))?(?!\d))?/.exec(model.toLowerCase());
@@ -35,6 +35,10 @@ was), then open the next. Every image and view stays in your context to the end:
35
35
  views, write down what you found and go on. A name or place the next search depends on:
36
36
  read it again on a crop at full resolution (\`strom media view … --crop\`)
37
37
  before you record it.
38
+ OpenCode: this tree's subagent strom-scan-reader (strom and the file reader
39
+ only) reads on your model, or on the one the user set for handwriting: hand
40
+ it a batch with the whole question, and record what it returns at once (a
41
+ negative result as a search by reader).
38
42
  `;
39
43
  /**
40
44
  * The agents strom can start working alone without the user's personal add-ons (agent.addons): Claude Code
@@ -63,6 +67,9 @@ comes back to you. Choose the model by the kind of work:
63
67
  | mechanical: downloads, renaming, counting, a plain grep | \`${m.cheap ?? "haiku"}\` |
64
68
  | judgement: identity, conflicts, which task next, writing to strom | nobody — you |
65
69
 
70
+ Scans are read by this tree's subagent type \`strom-scan-reader\` (strom and the
71
+ file reader only, so it starts small); the model above still holds.
72
+
66
73
  ${DELEGATION_RULES}
67
74
  - Send independent batches in ONE message so they run in parallel.
68
75
  - Do not read a subagent's transcript or output file — its images would come
@@ -0,0 +1,96 @@
1
+ // The scan reader: a subagent of its own kind for reading scans, in the agents that let a tree define one (Claude Code:
2
+ // .claude/agents/ and --agents; OpenCode: an agent of mode subagent in opencode.json). It has the shell (for strom) and
3
+ // the file reader (for its views) and nothing else — no subagents of its own, no web, no editing — and a short prompt,
4
+ // so it starts small (a subagent of the agent's general kind starts with every tool and instruction the agent has).
5
+ // Its model is the user's model.vision where they set one, else the model of the agent that starts it: never a
6
+ // cheaper one picked by strom. Codex, Antigravity and Grok Build get none: Codex's agent roles cannot narrow its tools
7
+ // and Antigravity has no such setting; Grok Build reads scans itself (profiles.ts), as the tree tells it.
8
+ import path from "node:path";
9
+ import { Settings } from "../core/config.js";
10
+ import { writeFileAtomic } from "../core/json.js";
11
+ export const SCAN_READER = "strom-scan-reader";
12
+ export const SCAN_READER_DESCRIPTION = "Reads scans of old records for this family research (registers, indexes, land books, any old handwriting): give it the images (B0001:57-66, M0012) and the whole question — what counts as a find, which years and names. It opens the views through strom, reads them and returns what it read, image by image. It never writes to the research.";
13
+ export const SCAN_READER_PROMPT = `You read scans of old records for a family research kept with strom. You are given images
14
+ (B0001:57-66, M0012…) and the whole question: what counts as a find, which years and names. You read
15
+ and report; you never write to the research and decide nothing.
16
+
17
+ Views: run strom by its command — \`strom media view B0001:57-60 --half both\` (at most 12 images and
18
+ 24 views a call); an entry to read closely: \`--grid\` to see where it is, then \`--crop x,y,w,h\` at
19
+ full resolution; \`--contrast\` for faded ink. Open ALL the views a call lists in ONE message — every
20
+ Read call together, never one view at a time — then read them, then the next call. Every view stays
21
+ in your context: after about 80 views, stop and report what you have. \`strom read …\` hands a range
22
+ to strom's own readers: only when the question asks for it (in the foreground; let it finish).
23
+
24
+ Reading: transcribe as written — the record's language, spelling and abbreviations, not modernised;
25
+ [?] for each uncertain letter or word; never complete a name or date from what the question expects.
26
+ "Illegible" beats a guess; "nothing" is a valid result.
27
+
28
+ Return, image by image (image and page): found / nothing / unclear; each entry that answers the
29
+ question word for word, where it stands and its columns (date, house, names, parents, godparents,
30
+ witnesses, remarks); what was illegible and where; the hand; how sure you are of each name. Say
31
+ plainly which images you did not open or read whole — never report an image you did not read as
32
+ searched.
33
+ `;
34
+ /** The model the user chose for reading handwriting (model.vision: flag, env, tree, user config) — none: the starting agent's. */
35
+ export function scanReaderModel(tree, agent) {
36
+ const r = new Settings(tree.env, {}).resolve("model.vision", tree.config, agent);
37
+ return r ? String(r.value) : undefined;
38
+ }
39
+ /** Claude Code's tools for it: the shell (on Windows its PowerShell too, where the tree's rules allow strom as well) and the file reader. */
40
+ export function scanReaderTools(platform = process.platform) {
41
+ return platform === "win32" ? ["Bash", "PowerShell", "Read"] : ["Bash", "Read"];
42
+ }
43
+ /** .claude/agents/strom-scan-reader.md: a conversation (the terminal, the desktop app) finds it in the tree. */
44
+ export function claudeScanReader(tree) {
45
+ return `---
46
+ name: ${SCAN_READER}
47
+ description: ${JSON.stringify(SCAN_READER_DESCRIPTION)}
48
+ tools: ${scanReaderTools().join(", ")}
49
+ model: ${JSON.stringify(scanReaderModel(tree, "claude") ?? "inherit")}
50
+ ---
51
+
52
+ ${SCAN_READER_PROMPT}`;
53
+ }
54
+ /** The same for --agents: a run is given it on the command line, whatever Claude Code makes of the folder's files. */
55
+ export function claudeAgentsJson(tree) {
56
+ return {
57
+ [SCAN_READER]: {
58
+ description: SCAN_READER_DESCRIPTION,
59
+ prompt: SCAN_READER_PROMPT,
60
+ tools: scanReaderTools(),
61
+ model: scanReaderModel(tree, "claude") ?? "inherit",
62
+ },
63
+ };
64
+ }
65
+ /** The file a headless run of Claude Code is given (--agents): in .strom/, written afresh for each run. */
66
+ export function claudeAgentsFile(tree) {
67
+ const file = path.join(tree.root, ".strom", "claude-agents.json");
68
+ writeFileAtomic(file, JSON.stringify(claudeAgentsJson(tree), null, 2) + "\n");
69
+ return file;
70
+ }
71
+ /**
72
+ * OpenCode's agent of mode subagent: the tree's rules with everything but reading and strom refused — no editing, no
73
+ * web, no subagents, no questions (nothing that would ask: a run nobody watches would end at it).
74
+ */
75
+ export function opencodeScanReader(tree, permission, refused) {
76
+ const model = scanReaderModel(tree, "opencode");
77
+ const bash = permission.bash;
78
+ const only = {
79
+ ...permission,
80
+ bash: { ...bash, "*": "deny" },
81
+ edit: "deny",
82
+ webfetch: "deny",
83
+ websearch: "deny",
84
+ task: "deny",
85
+ question: "deny",
86
+ skill: "deny",
87
+ todowrite: "deny",
88
+ };
89
+ return {
90
+ mode: "subagent",
91
+ description: SCAN_READER_DESCRIPTION,
92
+ prompt: SCAN_READER_PROMPT,
93
+ ...(model ? { model } : {}),
94
+ permission: refused(only),
95
+ };
96
+ }