sprag-cli 3.69.0 → 3.71.0
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.
- package/NOTICE +5 -2
- package/README.ko.md +16 -0
- package/README.md +16 -0
- package/bin/cli.js +53 -4
- package/docs/CODEX.ko.md +3 -2
- package/docs/CODEX.md +5 -4
- package/docs/TEAM.ko.md +174 -0
- package/docs/TEAM.md +175 -0
- package/package.json +5 -1
- package/presets/agents.json +118 -0
- package/presets/doc2md/convert.py +1 -1
- package/presets/ratchet-rules.json +12 -2
- package/src/agents.js +3 -1
- package/src/cli-entry.js +40 -0
- package/src/codex/delegation.js +8 -5
- package/src/codex/hooks.js +25 -12
- package/src/codex/panel-setup.js +2 -1
- package/src/codex/panel.js +2 -1
- package/src/codex/route-scan.js +6 -4
- package/src/commands/brief.js +2 -0
- package/src/commands/codex-delegate.js +3 -3
- package/src/commands/codex.js +6 -5
- package/src/commands/harness.js +3 -4
- package/src/commands/install.js +19 -18
- package/src/commands/presets.js +158 -0
- package/src/commands/profile-map.js +2 -2
- package/src/commands/route-scan.js +25 -10
- package/src/commands/team.js +334 -0
- package/src/commands/update-check.js +2 -2
- package/src/cost.js +6 -2
- package/src/cowork-sync.js +5 -4
- package/src/debug.js +3 -2
- package/src/delegation-agents.js +49 -49
- package/src/delegation-guard.js +3 -2
- package/src/doc2md.cjs +4 -3
- package/src/env.cjs +7 -0
- package/src/env.js +19 -0
- package/src/first-run-note.js +3 -2
- package/src/harness-templates.js +10 -0
- package/src/harness.js +4 -14
- package/src/hook.cjs +4 -3
- package/src/model-rules.js +58 -29
- package/src/preset-sign.js +106 -0
- package/src/preset-sync.js +133 -0
- package/src/presets-source.js +209 -0
- package/src/prompt.js +2 -1
- package/src/route-caps.js +3 -3
- package/src/route-inject.js +24 -15
- package/src/route-scan.js +39 -15
- package/src/seed-rules.js +14 -16
- package/src/shared-model-rules.js +1 -1
- package/src/star-note.js +3 -2
- package/src/statusline-guard.js +144 -0
- package/src/stdin-payload.js +33 -4
- package/src/team-prompts.js +353 -0
- package/src/team-ratchet.js +276 -0
- package/src/team-sync.js +625 -0
- package/src/update-check.js +3 -2
package/docs/TEAM.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Team layer
|
|
2
|
+
|
|
3
|
+
[← README](../README.md) · [한국어](./TEAM.ko.md)
|
|
4
|
+
|
|
5
|
+
Sprag is local-only by default. The team layer is an opt-in add-on that lets a group see each member's daily spend and savings in one dashboard, and share the ratchet rules an owner or admin approves. Nothing is sent until you run `sprag team join <token>` or your environment sets `SPRAG_TEAM_TOKEN`. `SPRAG_NO_TEAM_SYNC=1` stops every usage upload, whatever else is configured.
|
|
6
|
+
|
|
7
|
+
## What is sent and what is not
|
|
8
|
+
|
|
9
|
+
After you join, Sprag uploads one row per local calendar day:
|
|
10
|
+
|
|
11
|
+
| Field | Meaning |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Spend (USD) | priced from your local session logs |
|
|
14
|
+
| Savings (USD) | from the routing and doc2md ledgers |
|
|
15
|
+
| Delegated runs | how many runs were delegated that day |
|
|
16
|
+
| Model-family share | the share of those runs that went to each model family |
|
|
17
|
+
| Sprag version | the installed version |
|
|
18
|
+
| Agent | `claude` or `codex` |
|
|
19
|
+
| Display name | the name you chose at join; the default is your OS user name |
|
|
20
|
+
| Operating system | OS name and version (for example macOS 26, Windows 11, Ubuntu 24.04) and CPU architecture (arm64 or x64); sent once after joining and again only if it changes. Nothing that identifies the machine by name: no hostname, no user name, no path |
|
|
21
|
+
|
|
22
|
+
Not sent: prompts, transcripts, file paths, project names, rule text, API keys. The one exception is the opt-in prompt upload of an Enterprise team with prompt capture on, described under "Prompt usage (Enterprise)". A rule leaves your machine only when you run `sprag team propose` yourself.
|
|
23
|
+
|
|
24
|
+
The daily aggregate upload runs at most once every 6 hours after the last successful one, from a detached child process that the SessionStart hook (and the prompt hook, which only reads a small local file) spawns. If a sync fails, for example because the network is down, it is retried after 15, then 30, then 60 minutes (60 at most) instead of waiting 6 hours. The CLI remembers the last day it uploaded and re-sends from the day before it, up to 30 days, so a machine that was offline for days catches up on its first successful sync. Re-sending a day is safe because each day is an upsert. The prompt upload of an Enterprise team with capture on has its own 30-minute timer (see "Prompt usage (Enterprise)"). It never runs on the statusline path.
|
|
25
|
+
|
|
26
|
+
Destination: `https://hiycfjwswtkixyoqkznm.supabase.co/rest/v1/rpc/*` (Supabase, region ap-northeast-2). The calls are the RPCs `team_join`, `ledger_upsert`, `team_rules_get`, `team_rule_propose`, `team_leave`, `prompt_chunk_put` (Enterprise prompt capture only) and `member_set_sync` (the upload-switch status call, see "The upload switch"); `sprag team create` also calls `team_create` with your sign-in session.
|
|
27
|
+
|
|
28
|
+
## Create a team from the CLI
|
|
29
|
+
|
|
30
|
+
You can set up a team without the web dashboard. Signing in uses a 6-digit code emailed to you (valid 15 minutes). Sprag never stores the sign-in session.
|
|
31
|
+
|
|
32
|
+
In a terminal, run one command and answer the prompts (email, code, team name):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
sprag team create
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
From an agent or any shell without a terminal, it takes two steps:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
sprag team create --email you@example.com --name "Acme"
|
|
42
|
+
# the code arrives by email, then:
|
|
43
|
+
sprag team create --email you@example.com --name "Acme" --code 123456
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The first step only sends the code. The second verifies it and creates the team. It then prints the invite token, the member command (`npm i -g sprag-cli@latest && sprag team join <token>`), the organization line (`export SPRAG_TEAM_TOKEN=<token>`) and the dashboard address. Sign in to https://sprag.io/team/ with the same email to manage the team.
|
|
47
|
+
|
|
48
|
+
By default this machine joins as the first member and runs one sync. Pass `--no-join` to skip that. If this machine is already in a team, run `sprag team leave` first or pass `--no-join`.
|
|
49
|
+
|
|
50
|
+
## Join as an individual
|
|
51
|
+
|
|
52
|
+
An owner or admin gives you an invite token. Then:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
sprag team join <token> --name "Mina"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Sprag prints the list of what will be uploaded and asks for confirmation. `--name` sets the display name (default: your OS user name). In a shell without a terminal, pass `--yes` to confirm. If you leave the token out, Sprag reads `SPRAG_TEAM_TOKEN`.
|
|
59
|
+
|
|
60
|
+
Check the result with `sprag team status`.
|
|
61
|
+
|
|
62
|
+
## Enroll an organization
|
|
63
|
+
|
|
64
|
+
To enroll every machine without each person running a command, set `SPRAG_TEAM_TOKEN=<token>` in the environment. Two common places:
|
|
65
|
+
|
|
66
|
+
- a shell profile: `export SPRAG_TEAM_TOKEN=<token>`
|
|
67
|
+
- Claude Code managed settings: `{"env":{"SPRAG_TEAM_TOKEN":"…"}}`
|
|
68
|
+
|
|
69
|
+
The machine joins at the next session start. The first session after that prints a one-line notice that names the team and the two commands that matter: `sprag team status --show-payload` shows what is sent, and `sprag team leave` stops it.
|
|
70
|
+
|
|
71
|
+
`leave` wins over the environment. A machine that left does not rejoin on its own, even with the variable still set. It rejoins only after an owner or admin issues a new token and the member runs `sprag team join` again.
|
|
72
|
+
|
|
73
|
+
## Team rules
|
|
74
|
+
|
|
75
|
+
Rules come at two levels. **Organization rules** apply to every team in the organization; owners and admins write them on the Rules tab of the dashboard. **Team rules** apply to one team; owners and admins approve them there too. An approved team rule can be applied to the whole organization with one click ("Apply to whole organization"): it moves to the organization level and its team copy is retired, so no machine receives it twice.
|
|
76
|
+
|
|
77
|
+
Members can suggest a team rule:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
sprag team propose "Run lint before every commit" --scope ratchet
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`--scope` is `ratchet` (the default) or `model`. A proposal stays pending until an owner or admin approves it.
|
|
84
|
+
|
|
85
|
+
**When a rule arrives.** Approved rules reach a machine at its next session start. A machine checks for new rules at most every hour and uploads usage every 6 hours; `sprag team sync --force` then a new session applies them at once. Both kinds of sync rewrite the rule files as soon as the rules arrive, so the next session reads them without another round trip. `sprag team sync --rules-only` pulls the rules alone and uploads nothing.
|
|
86
|
+
|
|
87
|
+
Sprag writes the rules to `~/.claude/ratchet-team.md` (Codex: `<CODEX_HOME>/ratchet-team.md`), organization rules first and then team rules, each with its model rules in a sub-section, and imports that file from the global harness block of `~/.claude/CLAUDE.md` for as long as the file exists. Do not edit the file by hand, because Sprag rewrites it. `sprag team rules` lists the cached rules as `- [org · model] text` or `- [team · ratchet] text`. `sprag team leave` removes the member key, the cache and the import.
|
|
88
|
+
|
|
89
|
+
## Status, payload and leaving
|
|
90
|
+
|
|
91
|
+
| Command | What it does |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `sprag team join [token] [--name <n>] [--yes]` | Join a team |
|
|
94
|
+
| `sprag team status [--show-payload]` | Team, plan, prompt-capture flag, display name, last sync. `--show-payload` also prints the device info and the exact daily rows that would be uploaded |
|
|
95
|
+
| `sprag team sync [--force] [--rules-only]` | Upload now and pull rules. `--force` ignores the 6-hour interval; `--rules-only` pulls the organization and team rules and uploads nothing |
|
|
96
|
+
| `sprag team rules` | Show the cached organization and team rules |
|
|
97
|
+
| `sprag team propose "<text>" [--scope ratchet\|model]` | Propose a rule to the team |
|
|
98
|
+
| `sprag team leave` | Leave the team and stop uploading |
|
|
99
|
+
|
|
100
|
+
## Organizations and seats
|
|
101
|
+
|
|
102
|
+
Every team belongs to an organization, and seats are bought per organization, not per team. Teams you create without naming an organization get a new one automatically, so existing setups keep working.
|
|
103
|
+
|
|
104
|
+
- **What a seat is.** One machine (one member key) that has not left and either sent anything in the last 30 days or is paused. A machine that has been silent for 30 days or more without being paused holds no seat. Members who left are not counted.
|
|
105
|
+
- **Second machine.** When the same person uses two machines, an owner or admin can merge the second one into the first so the two share one seat. In the dashboard, use "Merge seat" on the member row; "Unmerge" gives it its own seat back. A merged row shows "shares a seat with <name>".
|
|
106
|
+
- **The limit.** Each organization has a seat limit. A join past the limit is refused: `sprag team join` exits 1 with "This organization has no free seat. Ask your organization owner or admin to add seats", and an environment auto-join stays silent and tries again at a later session start. Lowering the limit below current use never removes anyone; it only refuses new joins until Sprag raises it again.
|
|
107
|
+
- **Roles.** There are three permission levels. The superuser is Sprag staff: it sets the plan and the seat limit, suspends organizations, adds owners, and sees every organization at sprag.io/ops/. An owner is a company's top level: it appoints and removes admins, and does everything an admin does. An admin is a company's management level: it creates teams and manages members, rules and seats. The person who created a team has no special rights over it; only the organization's owners and admins and the superuser manage teams. Owners and admins see the seat limit in the dashboard. To add seats, write to sales@sprag.io.
|
|
108
|
+
- **The plan.** Sprag sets the plan (`pro` or `enterprise`) on the organization; owners and admins cannot change it. Prompt capture can be switched on only while the organization is on Enterprise. To change the plan, contact us.
|
|
109
|
+
|
|
110
|
+
Create a team inside an existing organization from the CLI with `sprag team create --org <org-id>`. The command prints the organization name and how many seats are used. `sprag team status` shows the organization name.
|
|
111
|
+
|
|
112
|
+
## The upload switch
|
|
113
|
+
|
|
114
|
+
`SPRAG_NO_TEAM_SYNC=1` (the old name `CTS_NO_TEAM_SYNC` is still read) stops every usage upload. A joined machine sends one status-only call, `member_set_sync`, with no usage data, the first time it sees the switch on, and one more when it is off again. After that it uploads immediately. That call is how the dashboard can tell a paused machine from a silent one. A paused machine keeps its seat until an owner or admin removes it.
|
|
115
|
+
|
|
116
|
+
## The dashboard
|
|
117
|
+
|
|
118
|
+
https://sprag.io/team/ is where an owner or admin signs in with an email code, creates a team and gets the invite token. From there they see per-member spend and savings, manage organization and team rules on the Rules tab, and remove members. The organization panel shows the plan, seats used against the limit, admins and every team. The superuser sees every organization at sprag.io/ops/.
|
|
119
|
+
|
|
120
|
+
A date range control at the top of the dashboard (7d, 30d, 90d, This month, Last month or Custom) sets the period for the members table, the Usage chart and the prompt usage tab. It defaults to the last 30 days, uses your local calendar days with the end date included, and allows 365 days at most. The choice is kept in the page address (`?from=2026-10-01&to=2026-10-18`), so a reload or a shared link shows the same period. The superuser console at /ops/ has the same control for the organization table, the organization header and its prompt usage.
|
|
121
|
+
|
|
122
|
+
### The seat roster
|
|
123
|
+
|
|
124
|
+
Owners and admins get a seat roster on /team. Each row is one machine and shows:
|
|
125
|
+
|
|
126
|
+
- **Upload switch.** On or Off.
|
|
127
|
+
- **Signal.** `ok`: an upload within 48 hours. `late`: the switch is on but there has been no upload for 48 hours or more. `paused`: the switch is off. `silent`: no contact for 30 days or more; the machine holds no seat. `left`: the member ran `sprag team leave`. `removed`: an owner or admin removed it.
|
|
128
|
+
- **Dates and version.** Last upload, last contact, paused since, and the Sprag version.
|
|
129
|
+
|
|
130
|
+
Row actions:
|
|
131
|
+
|
|
132
|
+
- **Remove** frees the seat, and the machine's key stops working.
|
|
133
|
+
- **Restore** brings back only machines that an owner or admin removed, and only within the seat limit.
|
|
134
|
+
- **Seat merge** and unmerge, as described above.
|
|
135
|
+
|
|
136
|
+
Below the roster, an activity log lists joined, paused, resumed, left, removed and restored events, with who did each one: member, owner, admin or superuser.
|
|
137
|
+
|
|
138
|
+
## Plans
|
|
139
|
+
|
|
140
|
+
Free, Pro ($5 per seat per month) and Enterprise ($10 per seat per month, with prompt capture and per-user prompt usage). Volume or annual invoicing is a custom agreement; write to sales@sprag.io. Details are on https://sprag.io/pricing/.
|
|
141
|
+
|
|
142
|
+
## Prompt usage (Enterprise)
|
|
143
|
+
|
|
144
|
+
Owners and admins of an Enterprise organization get a Prompts tab on sprag.io/team: a per-user table of prompts, characters, average length, active days and last prompt, and a searchable prompt list per user. Prompts are stored only for Enterprise teams that have prompt capture switched on; Pro organizations see a locked state.
|
|
145
|
+
|
|
146
|
+
**What is captured.** Only the prompts you typed yourself, from your main Claude Code sessions (`~/.claude/projects/**/*.jsonl`) and Codex sessions (`<CODEX_HOME or ~/.codex>/sessions/**/*.jsonl`). Each prompt goes up as its time, the project folder's name (the last part of the path, never the full path) and its text, cut at 20,000 characters. Not captured: assistant replies, tool output, file contents, subagent transcripts, slash-command wrappers, system reminders, or any file path.
|
|
147
|
+
|
|
148
|
+
**Redaction.** Before upload, obvious secrets are replaced with `[redacted]`: API keys and tokens (`sk-...`, `sk-ant-...`, `ghp_...`, `gho_...`, `github_pat_...`, `AKIA...`, `xox[abpr]-...`), `Bearer` tokens, private key blocks, and `KEY=`, `TOKEN=`, `PASSWORD=` and `SECRET=` assignments. This catches common shapes, not every secret, so do not paste secrets into prompts on a machine with capture on.
|
|
149
|
+
|
|
150
|
+
**No backfill.** The first time a machine sees prompt capture on, it marks the end of every existing transcript and uploads only what you type after that. Nothing written before the policy reached your machine is sent, and what you type while capture is off is never sent later. Transcripts created afterwards are read from their start. When the policy turns off, the machine forgets its place (`prompts-offsets.json`), so switching it on again starts fresh at the end of every file.
|
|
151
|
+
|
|
152
|
+
**Cadence.** Prompts are uploaded about every 30 minutes while Claude Code or Codex is in use; the daily aggregate stays at every 6 hours. A prompt you submit checks a small local timer and, when 30 minutes have passed, starts a detached background upload (no network and no parsing happen in the hook itself). A session start and `sprag team sync --force` also upload. One run sends at most 2,000 prompts or 8 MB; the rest waits for the next sync. The file path is never sent; each file is identified by the first 16 characters of a hash of its path, and a repeated chunk is ignored by the server.
|
|
153
|
+
|
|
154
|
+
**Check it.** `sprag team status` shows "Prompt capture: on" and the time and count of the last prompt upload. `sprag team status --show-payload` also shows how many prompts are waiting and one example (time, project, first 80 characters), never the full text of the pending prompts.
|
|
155
|
+
|
|
156
|
+
**How to stop.** Ask an owner or admin to switch prompt capture off for the team (the next sync stops the upload and clears the position file); set `SPRAG_NO_TEAM_SYNC=1` to stop every upload right away; or run `sprag team leave`.
|
|
157
|
+
|
|
158
|
+
## Privacy notes
|
|
159
|
+
|
|
160
|
+
- Owners and admins see per-member daily totals, not sessions or prompts, unless the organization is on Enterprise and the team has prompt capture switched on.
|
|
161
|
+
- A member sees the same payload with `sprag team status --show-payload`.
|
|
162
|
+
- Prompt capture is an Enterprise policy that an owner or admin can switch on in the dashboard. Members see its state in `sprag team status`. When capture is on, the CLI uploads the prompts you type after the policy reached your machine (see "Prompt usage (Enterprise)").
|
|
163
|
+
- Rule text leaves your machine only through `sprag team propose`.
|
|
164
|
+
|
|
165
|
+
## Files and environment variables
|
|
166
|
+
|
|
167
|
+
| Name | Purpose |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `<user data dir>/team.json` | Membership state and the member key (mode 0600) |
|
|
170
|
+
| `<user data dir>/team-rules.json` | Organization and team rules pulled at the last sync |
|
|
171
|
+
| `<user data dir>/prompts-offsets.json` | Where the prompt uploader stopped in each transcript; exists only while prompt capture is on |
|
|
172
|
+
| `~/.claude/ratchet-team.md` | Approved organization and team rules for Claude Code, imported from `~/.claude/CLAUDE.md` |
|
|
173
|
+
| `<CODEX_HOME>/ratchet-team.md` | The same rules for Codex |
|
|
174
|
+
| `SPRAG_TEAM_TOKEN` | Invite token; enrolls the machine at the next session start |
|
|
175
|
+
| `SPRAG_NO_TEAM_SYNC=1` | Stops every usage upload. A joined machine sends only one status-only call when it first sees the switch on and when it is off again (old name `CTS_NO_TEAM_SYNC` still read) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.71.0",
|
|
4
4
|
"description": "Model-delegation router for Claude Code and Codex: lookups, checks and test runs go to cheaper models, documents go in as compact Markdown, and a live statusline shows spend and savings.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
"deploy": "node scripts/deploy.mjs",
|
|
12
12
|
"docs:statusline": "node scripts/docs-statusline.mjs",
|
|
13
13
|
"postinstall": "node bin/postinstall.js || true",
|
|
14
|
+
"presets:pull": "node scripts/presets-pull.mjs",
|
|
14
15
|
"publish:legacy": "node scripts/publish-legacy.mjs",
|
|
15
16
|
"release:notes": "node scripts/release-notes.mjs",
|
|
16
17
|
"site:docs": "node scripts/build-site-docs.mjs",
|
|
18
|
+
"site:serve": "node scripts/serve-site.mjs",
|
|
17
19
|
"test": "node --test",
|
|
18
20
|
"test:mods": "claude plugin test mod/sprag-doc2md && claude plugin test mod/sprag-delegation-meter && claude plugin test mod/sprag-statusline",
|
|
19
21
|
"version": "node scripts/sync-site-version.mjs"
|
|
@@ -30,6 +32,8 @@
|
|
|
30
32
|
"README.ko.md",
|
|
31
33
|
"docs/CODEX.md",
|
|
32
34
|
"docs/CODEX.ko.md",
|
|
35
|
+
"docs/TEAM.md",
|
|
36
|
+
"docs/TEAM.ko.md",
|
|
33
37
|
"LICENSE",
|
|
34
38
|
"NOTICE"
|
|
35
39
|
],
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": [
|
|
3
|
+
"Delegation-only subagents written to ~/.claude/agents by syncDelegationAgents().",
|
|
4
|
+
"Same shape as the \"agents\" array of a published preset bundle, so the bundled copy",
|
|
5
|
+
"and a remote bundle render through one path. A remote bundle replaces this list",
|
|
6
|
+
"when its signature verifies; otherwise this file is used.",
|
|
7
|
+
"Models are aliases on purpose (a gateway resolves them through ANTHROPIC_DEFAULT_*_MODEL),",
|
|
8
|
+
"except opus-worker, which names the full id: a bare alias resolves to an older model.",
|
|
9
|
+
"Effort follows how the work is verified: low for lookups and command runs, medium for",
|
|
10
|
+
"implementation against tests, high for the larger opus share.",
|
|
11
|
+
"The files are rendered in English (description.en, body.en): they are model-facing",
|
|
12
|
+
"prompts, and the Korean copy is what the /ops console shows next to it."
|
|
13
|
+
],
|
|
14
|
+
"agents": [
|
|
15
|
+
{
|
|
16
|
+
"name": "haiku-explore",
|
|
17
|
+
"model": "haiku",
|
|
18
|
+
"effort": "low",
|
|
19
|
+
"tools": [
|
|
20
|
+
"Read",
|
|
21
|
+
"Grep",
|
|
22
|
+
"Glob",
|
|
23
|
+
"Bash"
|
|
24
|
+
],
|
|
25
|
+
"description": {
|
|
26
|
+
"ko": "위임 작업용 저비용 읽기 전용 탐색, 조회, 상태 확인, 요약 에이전트입니다. MCP 도구는 없습니다.",
|
|
27
|
+
"en": "Cheap read-only exploration, lookup, status checks and summaries for delegated tasks. It has no MCP tools."
|
|
28
|
+
},
|
|
29
|
+
"body": {
|
|
30
|
+
"ko": "맡은 역할: 읽기 전용 탐색, 조회, 상태 확인, 붙여넣은 로그나 텍스트를 두고 묻는 질문에 답합니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.",
|
|
31
|
+
"en": "Your role: read-only exploration, lookups, status checks, and questions about pasted logs or text.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs general-purpose: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "haiku-runner",
|
|
36
|
+
"model": "haiku",
|
|
37
|
+
"effort": "low",
|
|
38
|
+
"tools": [
|
|
39
|
+
"Bash",
|
|
40
|
+
"Read",
|
|
41
|
+
"Grep",
|
|
42
|
+
"Glob"
|
|
43
|
+
],
|
|
44
|
+
"description": {
|
|
45
|
+
"ko": "위임 작업의 빌드·테스트·git 명령을 실행하는 저비용 에이전트입니다. MCP 도구는 없습니다.",
|
|
46
|
+
"en": "Cheap runner for build, test and git commands in delegated tasks. It has no MCP tools."
|
|
47
|
+
},
|
|
48
|
+
"body": {
|
|
49
|
+
"ko": "맡은 역할: 전달받은 빌드·테스트·git 명령을 실행하고 결과를 보고합니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.",
|
|
50
|
+
"en": "Your role: run the build, test, or git commands you are given and report the result.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs general-purpose: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "haiku-translate",
|
|
55
|
+
"model": "haiku",
|
|
56
|
+
"effort": "low",
|
|
57
|
+
"tools": [
|
|
58
|
+
"Read",
|
|
59
|
+
"Grep",
|
|
60
|
+
"Glob",
|
|
61
|
+
"Edit",
|
|
62
|
+
"Write"
|
|
63
|
+
],
|
|
64
|
+
"description": {
|
|
65
|
+
"ko": "위임 작업용 저비용 일괄 번역·정형 텍스트 변환 에이전트입니다. MCP 도구는 없습니다.",
|
|
66
|
+
"en": "Cheap batch translation and structured text conversion for delegated tasks. It has no MCP tools."
|
|
67
|
+
},
|
|
68
|
+
"body": {
|
|
69
|
+
"ko": "맡은 역할: 전달받은 파일이나 텍스트의 일괄 번역과 정형 텍스트 변환입니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.",
|
|
70
|
+
"en": "Your role: batch translation and mechanical text conversion of the files or text you are given.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs general-purpose: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "sonnet-worker",
|
|
75
|
+
"model": "sonnet",
|
|
76
|
+
"effort": "medium",
|
|
77
|
+
"tools": [
|
|
78
|
+
"Read",
|
|
79
|
+
"Grep",
|
|
80
|
+
"Glob",
|
|
81
|
+
"Bash",
|
|
82
|
+
"Edit",
|
|
83
|
+
"Write",
|
|
84
|
+
"NotebookEdit"
|
|
85
|
+
],
|
|
86
|
+
"description": {
|
|
87
|
+
"ko": "여러 단계 작업과 설계가 확정된 구현을 맡는 중간 티어 워커입니다. MCP 도구는 없습니다.",
|
|
88
|
+
"en": "Mid-tier worker for multi-step tasks and implementation handed over with a fixed design. It has no MCP tools."
|
|
89
|
+
},
|
|
90
|
+
"body": {
|
|
91
|
+
"ko": "맡은 역할: 여러 단계로 이어지는 작업과, 호출자가 설계를 확정한 구현을 맡습니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.",
|
|
92
|
+
"en": "Your role: multi-step work and the implementation half of a design the caller already fixed.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs general-purpose: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "opus-worker",
|
|
97
|
+
"model": "claude-opus-5-5",
|
|
98
|
+
"effort": "high",
|
|
99
|
+
"tools": [
|
|
100
|
+
"Read",
|
|
101
|
+
"Grep",
|
|
102
|
+
"Glob",
|
|
103
|
+
"Bash",
|
|
104
|
+
"Edit",
|
|
105
|
+
"Write",
|
|
106
|
+
"NotebookEdit"
|
|
107
|
+
],
|
|
108
|
+
"description": {
|
|
109
|
+
"ko": "Fable 세션용 고성능 워커로, 설계가 확정된 대규모 구현·디버깅·리팩터링을 맡습니다. MCP 도구는 없습니다.",
|
|
110
|
+
"en": "Heavy worker for Fable sessions: large implementation, debugging and refactoring handed over with a fixed design. It has no MCP tools."
|
|
111
|
+
},
|
|
112
|
+
"body": {
|
|
113
|
+
"ko": "맡은 역할: 호출자가 설계를 확정한 대규모 구현과 디버깅입니다. 설계 판단과 되돌릴 수 없는 작업은 호출자에게 넘기십시오.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.",
|
|
114
|
+
"en": "Your role: large implementation and debugging work whose design the caller already fixed. Hand design judgement and irreversible actions back to the caller.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs general-purpose: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
@@ -34,7 +34,7 @@ import zipfile
|
|
|
34
34
|
|
|
35
35
|
MAX_UNCOMPRESSED = 500 * 1024 * 1024
|
|
36
36
|
MAX_RATIO = 200
|
|
37
|
-
ROW_CAP = int(os.environ.get("CTS_DOC2MD_ROW_CAP", "50000"))
|
|
37
|
+
ROW_CAP = int(os.environ.get("SPRAG_DOC2MD_ROW_CAP", os.environ.get("CTS_DOC2MD_ROW_CAP", "50000")))
|
|
38
38
|
ZIP_EXTS = {".pptx", ".xlsx", ".docx"}
|
|
39
39
|
|
|
40
40
|
|
|
@@ -6,31 +6,39 @@
|
|
|
6
6
|
"each answer. Both read this file, so there is one source for the wording.",
|
|
7
7
|
"Each rule carries both languages because the text is injected into the model's",
|
|
8
8
|
"context, where a Korean-only rule would steer an English session into Korean.",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"Each rule carries a stable `id`. It was derived once from the `ko` text",
|
|
10
|
+
"(fix-<sha1(ko)[0:6]>) and is kept when the wording changes, so a rule keeps its",
|
|
11
|
+
"identity, and a user's 'no', across rewording. This file is also the fallback for",
|
|
12
|
+
"the centrally published preset bundle; `npm run presets:pull` refreshes it."
|
|
11
13
|
],
|
|
12
14
|
"rules": [
|
|
13
15
|
{
|
|
16
|
+
"id": "fix-bd7836",
|
|
14
17
|
"ko": "웹 fetch/search에서 robots·UA 차단('unable to fetch' 등)이 발생하면 → curl에 브라우저 UA(Mozilla/5.0 ... Chrome/...) 헤더를 붙여 재시도. reddit 등은 .json 엔드포인트 + UA 조합으로 접근 가능",
|
|
15
18
|
"en": "When a web fetch/search is blocked by robots or user-agent checks ('unable to fetch' and similar) → retry with curl carrying a browser UA header (Mozilla/5.0 ... Chrome/...). Sites like reddit also answer on their .json endpoints with that UA"
|
|
16
19
|
},
|
|
17
20
|
{
|
|
21
|
+
"id": "fix-4168c2",
|
|
18
22
|
"ko": "ratchet 룰 promote 호출 직전 사용자에게 scope(global/project)를 먼저 묻는다 — Bash 환경은 non-TTY라 CLI readline 프롬프트가 안 뜨므로, 호출자(LLM)가 대신 묻고 --global/--project 플래그를 명시해 실행",
|
|
19
23
|
"en": "Ask the user for the scope (global or project) before calling ratchet promote — the Bash environment is non-TTY, so the CLI's readline prompt never appears; the caller (the model) has to ask and pass --global/--project explicitly"
|
|
20
24
|
},
|
|
21
25
|
{
|
|
26
|
+
"id": "fix-ce98b4",
|
|
22
27
|
"ko": "자동화 도구가 grep·정규식으로 찾는 마커 텍스트(헤더·앵커·키워드 라인)는 리팩터링·압축·수정 금지 — 정확 매칭 검출은 한 글자만 바뀌어도 깨짐",
|
|
23
28
|
"en": "Never refactor, compress or reword marker text that an automated tool finds by grep or regex (headings, anchors, keyword lines) — exact-match detection breaks on a single changed character"
|
|
24
29
|
},
|
|
25
30
|
{
|
|
31
|
+
"id": "fix-b1da7a",
|
|
26
32
|
"ko": "자동화 설계에서 vision/screenshot 루프가 보이면 → 호출 가능한 함수·API·MCP tool부터 찾는다. 통제 가능한 내부 도구는 무조건 API, 통제 불가 외부 SaaS만 vision 차선책. 인터페이스를 바꾼 뒤에 모델을 내린다 — 거꾸로 가면 토큰 폭발",
|
|
27
33
|
"en": "When an automation design calls for a vision/screenshot loop → look for a callable function, API or MCP tool first. Anything you control gets an API; vision is the fallback only for external SaaS you cannot change. Fix the interface before lowering the model — doing it the other way round explodes the token cost"
|
|
28
34
|
},
|
|
29
35
|
{
|
|
36
|
+
"id": "fix-1dee5f",
|
|
30
37
|
"ko": "입력 데이터(ground-truth) 변경 시 그에 의존하는 모든 파생 자산을 끝까지 재생성한다 — 중간 단계를 생략하면 stale 산출물이 새 입력 위에 얹혀 결과가 깨짐",
|
|
31
38
|
"en": "When the ground-truth input changes, regenerate every derived asset all the way through — skipping an intermediate step leaves a stale artifact sitting on top of the new input and the result breaks"
|
|
32
39
|
},
|
|
33
40
|
{
|
|
41
|
+
"id": "fix-f558f9",
|
|
34
42
|
"ko": "탐색·조회·상태 확인·명령 실행 같은 단순 요청은 `model: haiku` 서브에이전트로 위임해 상위 모델 토큰을 아낀다 (읽기 전용 preset 에이전트가 이미 있으면 그걸 쓰고, 없으면 티어만 내려서 위임)",
|
|
35
43
|
"en": "Delegate simple requests — lookup, status checks, running commands — to a `model: haiku` subagent to keep the expensive model's tokens for real work (use an existing read-only preset agent when there is one; otherwise delegate with the tier lowered)",
|
|
36
44
|
"agents": [
|
|
@@ -38,10 +46,12 @@
|
|
|
38
46
|
]
|
|
39
47
|
},
|
|
40
48
|
{
|
|
49
|
+
"id": "fix-aa18bc",
|
|
41
50
|
"ko": "지침이나 룰이 지켜지지 않아 같은 품질 문제가 재발하면 문구를 다시 쓰지 말 것 → 그 조항이 기계로 판정 가능한지부터 따지고(문자열 비교나 정규식으로 결정되면 판정 가능), 가능하면 쓰기 시점 훅이나 테스트 같은 차단 지점을 추가한다. 판단이 필요한 잔여 부분만 지침에 남기고 그 경계를 지침에 적는다. 실제 코퍼스에 돌려 오탐 건수를 센 뒤 좁히고, 회귀 테스트를 같은 커밋에 넣는다",
|
|
42
51
|
"en": "When a guideline keeps being violated and the same quality defect recurs, do NOT reword the instruction → first ask whether the clause is machine-decidable (a string comparison or regex settles it), and if so add an enforcement point such as a write-time hook or a test. Leave only the judgement remainder in the prose and state that boundary there. Measure the false-positive count on a real corpus, narrow until it is acceptable, and land a regression test in the same commit"
|
|
43
52
|
},
|
|
44
53
|
{
|
|
54
|
+
"id": "fix-777a15",
|
|
45
55
|
"ko": "여러 파일에 걸친 구현을 맡으면 → 설계와 스펙 확정은 메인 모델이 하고 코드 작성은 서브에이전트에 위임한다(Claude는 `model: sonnet`, Codex는 명시적 매핑을 우선해 해석한 T1을 쓰며 haiku나 T2에는 구현을 맡기지 않는다). 위임 프롬프트에는 설계, 수정할 파일, 제약, 완료 기준, 테스트 명령, 예산, 권장 모델과 그 이유를 적어 다른 문맥 없이 실행할 수 있게 한다. 이미 읽은 파일의 약 50줄 이하 변경이나 의존 단계가 하나뿐인 짧은 구현, 그리고 위임 도구나 하위 모델을 쓸 수 없는 경우는 메인 모델이 직접 처리한다. 위임한 구현은 끝날 때까지 기다리고 같은 구현을 중복 실행하지 않으며, 돌아온 diff와 테스트 결과를 메인 모델이 직접 검증한 뒤에 완료로 보고한다",
|
|
46
56
|
"en": "When an implementation spans several files → the main model settles the design and specification, then delegates the code writing to a subagent (Claude: `model: sonnet`; Codex: its resolved T1, explicit mapping first; never haiku or T2 for implementation). Write the delegation prompt so it runs without other context: design, files to change, constraints, completion criteria, test commands, budget, and the recommended model with a one-line reason. Keep changes of roughly 50 lines or less in files already read, a single short dependency chain, or cases where the subagent tools or the lower model are unavailable on the main model. Wait for the delegated work instead of duplicating it, and verify the returned diff and the actual test results before reporting completion",
|
|
47
57
|
"agents": [
|
package/src/agents.js
CHANGED
|
@@ -26,10 +26,12 @@ const AGENT_MODEL = {
|
|
|
26
26
|
'haiku-runner': 'haiku',
|
|
27
27
|
'haiku-translate': 'haiku',
|
|
28
28
|
'sonnet-worker': 'sonnet',
|
|
29
|
+
'opus-worker': 'opus',
|
|
29
30
|
// A T1 rule names its target by tier alone (`agent: 'sonnet'`). Without these
|
|
30
31
|
// the phrase fell through to the haiku default, and a sonnet rule told the
|
|
31
32
|
// session to delegate to haiku. resolveAgentName maps them to an agent file.
|
|
32
33
|
sonnet: 'sonnet',
|
|
34
|
+
opus: 'opus',
|
|
33
35
|
haiku: 'haiku',
|
|
34
36
|
};
|
|
35
37
|
|
|
@@ -50,7 +52,7 @@ export function agentExists(name, root = process.cwd()) {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/** Agent a bare tier name stands for when that agent's file exists. */
|
|
53
|
-
const TIER_AGENT = { sonnet: 'sonnet-worker', haiku: 'haiku-explore' };
|
|
55
|
+
const TIER_AGENT = { sonnet: 'sonnet-worker', opus: 'opus-worker', haiku: 'haiku-explore' };
|
|
54
56
|
|
|
55
57
|
/**
|
|
56
58
|
* The agent a rule target really names on this machine, or null when none is
|
package/src/cli-entry.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The script a detached child of this CLI must run.
|
|
3
|
+
*
|
|
4
|
+
* The preset sync and the two team syncs start a detached child to do their
|
|
5
|
+
* network work, and they used to pass `process.argv[1]` as its script. Inside
|
|
6
|
+
* the CLI that is bin/cli.js. Inside anything else that imports these modules
|
|
7
|
+
* it is that caller's script: under `node --test` a test file relaunched
|
|
8
|
+
* itself, detached, on every session-start case, and each copy did the same.
|
|
9
|
+
* 4,055 processes in 59 seconds on 2026-10-20, and three memory-exhaustion
|
|
10
|
+
* stalls in two days. So the entry is fixed here from this module's own
|
|
11
|
+
* location, and a spawned child carries a marker that refuses to spawn again.
|
|
12
|
+
*/
|
|
13
|
+
import * as childProcess from 'node:child_process';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { envVar } from './env.js';
|
|
16
|
+
|
|
17
|
+
export const CLI_ENTRY = fileURLToPath(new URL('../bin/cli.js', import.meta.url));
|
|
18
|
+
|
|
19
|
+
/** The marker a detached child carries; read through envVar('DETACHED_CHILD'). */
|
|
20
|
+
export const DETACHED_CHILD_VAR = 'SPRAG_DETACHED_CHILD';
|
|
21
|
+
|
|
22
|
+
export function isDetachedChild(env = process.env) {
|
|
23
|
+
return envVar('DETACHED_CHILD', env) === '1';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The environment for a detached child: the caller's, plus the marker. */
|
|
27
|
+
export function detachedChildEnv(env = process.env) {
|
|
28
|
+
return { ...env, [DETACHED_CHILD_VAR]: '1' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Start `sprag <args>` detached, never awaited, never from a detached child.
|
|
33
|
+
* Returns true when a child was started. Every background refresh goes
|
|
34
|
+
* through here so that no caller can name the script itself.
|
|
35
|
+
*/
|
|
36
|
+
export function spawnDetachedCli(args, { env = process.env, spawn = childProcess.spawn, execPath = process.execPath } = {}) {
|
|
37
|
+
if (isDetachedChild(env)) return false;
|
|
38
|
+
spawn(execPath, [CLI_ENTRY, ...args], { detached: true, stdio: 'ignore', windowsHide: true, env: detachedChildEnv(env) }).unref();
|
|
39
|
+
return true;
|
|
40
|
+
}
|
package/src/codex/delegation.js
CHANGED
|
@@ -36,7 +36,7 @@ export function validateCodexTarget(model, effort) {
|
|
|
36
36
|
|
|
37
37
|
export function validateCodexSharedTarget({ tier, from, model, effort, provider }) {
|
|
38
38
|
validateCodexTarget(model, effort);
|
|
39
|
-
if (!SHARED_RULE_TIERS.includes(tier)) throw new Error('Shared rule tier must be T1 or T2.');
|
|
39
|
+
if (!SHARED_RULE_TIERS.includes(tier)) throw new Error('Shared rule tier must be T1, T1H or T2.');
|
|
40
40
|
if (!safeId(from) || from === model) throw new Error('--from must name a different parent model.');
|
|
41
41
|
if (!safeId(provider)) throw new Error('A shared model mapping requires a Codex provider ID.');
|
|
42
42
|
return { tier, from, model, ...(effort === undefined ? {} : { effort }), provider };
|
|
@@ -78,7 +78,7 @@ function catalogCodexModels(home) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
* The models Codex resolves T2 and
|
|
81
|
+
* The models Codex resolves T2, T1 and T1H to when the user mapped none, the way
|
|
82
82
|
* Claude Code resolves `haiku` and `sonnet`. Candidates are the models that
|
|
83
83
|
* already ran in this user's Codex sessions plus, for the built-in OpenAI
|
|
84
84
|
* provider only, the models Codex's catalog lists for the account (a gateway
|
|
@@ -86,7 +86,8 @@ function catalogCodexModels(home) {
|
|
|
86
86
|
* qualifies only if it is priced on the parent's provider and costs less than
|
|
87
87
|
* the parent, so the hint never names a model the gateway cannot serve. The
|
|
88
88
|
* cheapest is T2 and the next one up is T1; with a single candidate T1 stays
|
|
89
|
-
* on the main agent.
|
|
89
|
+
* on the main agent. T1H is the most expensive cheaper model, and only when
|
|
90
|
+
* there are at least three candidates, so that it differs from T1. Returns { targets, reason }.
|
|
90
91
|
*/
|
|
91
92
|
export function autoCodexSharedTargets({ dir = userDataDir(), home = codexUserDir(), model, provider, now = Date.now() } = {}) {
|
|
92
93
|
if (!safeId(model) || !safeId(provider)) return { targets: [], reason: 'unknown parent model or provider' };
|
|
@@ -104,7 +105,9 @@ export function autoCodexSharedTargets({ dir = userDataDir(), home = codexUserDi
|
|
|
104
105
|
? `${model} is already the cheapest available model on ${provider}`
|
|
105
106
|
: `no cheaper priced model has run in your Codex sessions on ${provider}` };
|
|
106
107
|
}
|
|
107
|
-
const
|
|
108
|
+
const picks = cheaper.slice(0, 2).map((x, i) => ({ tier: i === 0 ? 'T2' : 'T1', x }));
|
|
109
|
+
if (cheaper.length >= 3) picks.push({ tier: 'T1H', x: cheaper.at(-1) });
|
|
110
|
+
const targets = picks.map(({ tier, x }) => ({ tier, from: model, model: x.model, provider, auto: true }));
|
|
108
111
|
return { targets, reason: null };
|
|
109
112
|
}
|
|
110
113
|
|
|
@@ -240,7 +243,7 @@ export function codexDelegateMinContext(cfg) {
|
|
|
240
243
|
// Codex tells the model to set spawn_agent's model only on the user's request,
|
|
241
244
|
// so the shared hint says where this request came from.
|
|
242
245
|
const SHARED_APPROVAL = 'The user approved this policy and its model mapping, so setting model here follows the user\'s request.';
|
|
243
|
-
const SHARED_TIER_GUIDANCE = 'Choose T2 only for a simple bounded lookup or command. Use T1 for multi-step tool chains or work spanning multiple files. If the required tier is not mapped, keep the task on the main agent. Do not delegate design judgement, diagnosis, or irreversible actions.';
|
|
246
|
+
const SHARED_TIER_GUIDANCE = 'Choose T2 only for a simple bounded lookup or command. Use T1 for multi-step tool chains or work spanning multiple files. Use T1H only for large-scope work that T1 would likely get wrong. If the required tier is not mapped, keep the task on the main agent. Do not delegate design judgement, diagnosis, or irreversible actions.';
|
|
244
247
|
const WAIT_TEXT = 'Wait for it to finish (call wait_agent with timeout_ms of at least 120000, and again if it times out) and do not do the delegated task yourself while it runs; then verify its result and report unfinished work. Do not spawn another CLI process. '
|
|
245
248
|
+ 'If sub-agent tools or the target model are unavailable, keep the task on the main agent and state that delegation was unavailable.';
|
|
246
249
|
|
package/src/codex/hooks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { envVar } from '../env.js';
|
|
2
|
+
import { spawnDetachedCli } from '../cli-entry.js';
|
|
1
3
|
import { createRequire } from 'node:module';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
4
|
import { createHash } from 'node:crypto';
|
|
4
5
|
import { mkdirSync, openSync, readFileSync, readSync, closeSync, statSync } from 'node:fs';
|
|
5
6
|
import { join, resolve } from 'node:path';
|
|
@@ -116,9 +117,8 @@ async function routeNotice(root, { spawnRescan = defaultRescan } = {}) {
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
function defaultRescan() {
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
{ detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
120
|
+
if (envVar('NO_ROUTE_SCAN') === '1') return;
|
|
121
|
+
spawnDetachedCli(['route-scan', '--refresh', '--quiet', '--agent', 'codex']);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
@@ -130,9 +130,8 @@ function defaultRescan() {
|
|
|
130
130
|
* of the parent is the earliest point that knows one was spawned.
|
|
131
131
|
*/
|
|
132
132
|
function defaultLedgerRefresh() {
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
{ detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
133
|
+
if (envVar('NO_ROUTE_SCAN') === '1') return;
|
|
134
|
+
spawnDetachedCli(['route-scan', 'savings', '--refresh', '--quiet', '--agent', 'codex']);
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
export function lintCodexTool(payload, { scope = 'all' } = {}) {
|
|
@@ -180,13 +179,25 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
180
179
|
const parts = [];
|
|
181
180
|
// Keep the preset file current with this version's wording before it is read.
|
|
182
181
|
if (event === 'session-start') {
|
|
182
|
+
try { (await import('../preset-sync.js')).spawnPresetSyncIfDue(); } catch (e) { debug('codex:preset-sync', e); }
|
|
183
183
|
try { (await import('../preset-ratchet.js')).preparePresetRatchet({ agent: 'codex' }); } catch (e) { debug('codex:preset', e); }
|
|
184
184
|
}
|
|
185
185
|
const { presetRatchetPath } = await import('../preset-ratchet.js');
|
|
186
|
+
let teamRatchetPath = () => '';
|
|
187
|
+
let teamNotice = null;
|
|
188
|
+
try {
|
|
189
|
+
const tr = await import('../team-ratchet.js');
|
|
190
|
+
teamRatchetPath = tr.teamRatchetPath;
|
|
191
|
+
if (event === 'session-start') {
|
|
192
|
+
tr.syncTeamRatchet({ agent: 'codex' });
|
|
193
|
+
tr.spawnTeamSyncIfDue();
|
|
194
|
+
teamNotice = tr.autoJoinNoticeBlock(undefined, 'en');
|
|
195
|
+
}
|
|
196
|
+
} catch (e) { debug('codex:team-ratchet', e); }
|
|
186
197
|
// Lower priority survives the budget first. Rules and style move to a file
|
|
187
198
|
// when they do not fit; offers and notices are simply left out.
|
|
188
199
|
for (const [scope, ratchet] of [['global', codexHarnessPaths(root, 'global').ratchet],
|
|
189
|
-
['preset', presetRatchetPath({ agent: 'codex' })], ['project', codexHarnessPaths(root, 'project').ratchet]]) {
|
|
200
|
+
['preset', presetRatchetPath({ agent: 'codex' })], ['team', teamRatchetPath({ agent: 'codex' })], ['project', codexHarnessPaths(root, 'project').ratchet]]) {
|
|
190
201
|
try {
|
|
191
202
|
if (statSync(ratchet).size > 32 * 1024) {
|
|
192
203
|
parts.push({ priority: 1, text: `Read the ${scope} Sprag ratchet rules at ${JSON.stringify(ratchet)} before working; they exceed the automatic injection limit.` });
|
|
@@ -194,10 +205,11 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
194
205
|
const text = readFileSync(ratchet, 'utf8').trim();
|
|
195
206
|
// An empty preset file is only its header; injecting it would spend
|
|
196
207
|
// tokens every session on no rules.
|
|
197
|
-
if (text && (scope !== 'preset' || /^- /m.test(text))) parts.push({ priority: 1, spill: true, text: `[Sprag Codex ratchet: ${scope}]\n${text}` });
|
|
208
|
+
if (text && ((scope !== 'preset' && scope !== 'team') || /^- /m.test(text))) parts.push({ priority: 1, spill: true, text: `[Sprag Codex ratchet: ${scope}]\n${text}` });
|
|
198
209
|
}
|
|
199
210
|
} catch (e) { if (e?.code !== 'ENOENT') debug('codex:ratchet', e); }
|
|
200
211
|
}
|
|
212
|
+
if (teamNotice) parts.push({ priority: 5, text: teamNotice });
|
|
201
213
|
if (event === 'subagent-start') parts.push({ priority: 0, text:
|
|
202
214
|
'[Sprag Codex delegation]\nStay within the assigned task and any caller-provided budget. Return findings with file references and actual verification results; state unfinished work. Use only available Codex tools and configured roles. Do not infer a model tier or invent a tool-call budget.' });
|
|
203
215
|
// Codex reads the Korean guide from the global AGENTS.md, so the hook adds it
|
|
@@ -207,7 +219,7 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
207
219
|
try { korean = syncCodexKoreanBlock({ cfg }); } catch (e) { debug('codex:korean-sync', e); }
|
|
208
220
|
if (korean?.action !== 'unchanged') parts.push({ priority: 2, spill: true, text: koreanStyleInjection({ cfg }) });
|
|
209
221
|
parts.push({ priority: 3, spill: true, text: await cohesionInjection({ cfg }) });
|
|
210
|
-
if (
|
|
222
|
+
if (envVar('NO_DOC2MD') !== '1' && cfg?.codex?.doc2md !== false) parts.push({ priority: 4, spill: true, text: codexDocumentNote() });
|
|
211
223
|
// Offers ask the user a question; a `codex exec` run has no one to answer it.
|
|
212
224
|
if (event === 'session-start' && payload.source !== 'compact' && !isCodexExecSession(payload.transcript_path)) {
|
|
213
225
|
if (version) parts.push({ priority: 5, text: updateOffer(version, { agent: 'codex', lang: userLanguage() }) });
|
|
@@ -219,6 +231,7 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
219
231
|
}
|
|
220
232
|
if (event === 'prompt') {
|
|
221
233
|
const parts = [];
|
|
234
|
+
try { (await import('../team-ratchet.js')).spawnTeamSyncOnPrompt(); } catch (e) { debug('codex:team-prompts', e); }
|
|
222
235
|
if (codexDelegateEnabled(cfg)) {
|
|
223
236
|
// Before the hint for this prompt is recorded: a hint from an earlier turn
|
|
224
237
|
// that no child claimed is closed, and children that did run get priced.
|
|
@@ -235,14 +248,14 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
235
248
|
}
|
|
236
249
|
catch (e) { debug('codex:brief', e); }
|
|
237
250
|
}
|
|
238
|
-
if (
|
|
251
|
+
if (envVar('NO_DOC2MD') !== '1' && cfg?.codex?.doc2md !== false) {
|
|
239
252
|
const identity = codexDocumentContext(payload);
|
|
240
253
|
parts.push(doc2md.contextForPrompt(payload, { lang: userLanguage(), agent: 'codex', convert: (source) => convertCodexDocument(source, identity) }));
|
|
241
254
|
}
|
|
242
255
|
return context('UserPromptSubmit', parts.filter(Boolean).join('\n\n'));
|
|
243
256
|
}
|
|
244
257
|
if (event === 'pre-tool') {
|
|
245
|
-
if (
|
|
258
|
+
if (envVar('NO_DOC2MD') !== '1' && cfg?.codex?.doc2md !== false) {
|
|
246
259
|
const result = codexDocumentTool(payload);
|
|
247
260
|
if (result) return result;
|
|
248
261
|
}
|