axstack 0.50.2 → 0.52.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/README.md +4 -0
- package/docs/installation.md +5 -4
- package/docs/workflows.md +8 -6
- package/package.json +1 -1
- package/profiles/presets/codex-only.json +2 -2
- package/profiles/presets/mixed.json +8 -8
- package/skills/axstack/references/default-routing.md +166 -0
- package/skills/axstack/references/role-roster.md +5 -4
- package/skills/axstack/references/t3-runtime.md +13 -7
- package/skills/axstack/references/thread-title-instructions.txt +3 -0
- package/skills/axstack/scripts/default-policy.js +134 -0
- package/skills/axstack/scripts/default-settings.js +36 -0
- package/skills/axstack/scripts/driver-history.js +70 -0
- package/skills/axstack/scripts/hub-secret.js +27 -0
- package/skills/axstack/scripts/hub-usage.js +93 -0
- package/skills/axstack/scripts/sync-default.js +76 -0
- package/skills/axstack/systemd/axstack-default-sync.service +9 -0
- package/skills/axstack/systemd/axstack-default-sync.timer +10 -0
- package/skills/axstack-audit/SKILL.md +1 -1
- package/skills/axstack-implement/SKILL.md +1 -1
- package/skills/axstack-improve/SKILL.md +2 -2
- package/skills/axstack-research/SKILL.md +2 -2
- package/skills/axstack-review/references/review-candidate.md +1 -1
- package/src/roles.js +3 -3
package/README.md
CHANGED
|
@@ -174,3 +174,7 @@ Regenerate `bun.lock` with the minimum Bun version in `package.json.engines`
|
|
|
174
174
|
## License
|
|
175
175
|
|
|
176
176
|
[MIT](LICENSE) © 2026 Axat Bhardwaj.
|
|
177
|
+
|
|
178
|
+
The [opt-in saved New-chat default script](skills/axstack/references/default-routing.md)
|
|
179
|
+
uses subscription capacity and preserves project overrides; its five-minute timer
|
|
180
|
+
is never enabled by installation. Role-labelled title instructions need T3 >=0.1.3.
|
package/docs/installation.md
CHANGED
|
@@ -294,10 +294,11 @@ missing either holds this role without substitution, including Haiku 4.5.
|
|
|
294
294
|
The `codex-only` assignments for these roles are unchanged.
|
|
295
295
|
Scout adds `axstack-explore-codebase-luna` at Codex Luna xhigh in `mixed` and
|
|
296
296
|
`codex-only`. Luna has an explicit null model in `claude-only`.
|
|
297
|
-
The
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
297
|
+
The `-sol` research-code and explore-execution pair seats use Terra high in
|
|
298
|
+
`mixed` and Sol high in `codex-only`; the auditor pair stays Sol high in both.
|
|
299
|
+
Their role IDs remain unchanged. `claude-only` records intentional absences.
|
|
300
|
+
The paired seats run independently on one brief and the driver reconciles
|
|
301
|
+
their findings.
|
|
301
302
|
|
|
302
303
|
The mixed checker uses the Claude Sonnet class at low effort, resolved from
|
|
303
304
|
saved capabilities. Mixed `axstack-research-x` uses `grok` with
|
package/docs/workflows.md
CHANGED
|
@@ -140,14 +140,15 @@ and `codex-only`. In `claude-only`, Luna is intentionally absent.
|
|
|
140
140
|
The mixed X route uses Grok.
|
|
141
141
|
The mixed `axstack-auditor` uses Haiku 5.5 xhigh, pinned to `claude-haiku-5-5`;
|
|
142
142
|
missing that ID or xhigh effort holds without substitution. The mixed
|
|
143
|
-
`axstack-explainer-review` uses
|
|
143
|
+
`axstack-explainer-review` uses Terra medium; `axstack-explainer` pins
|
|
144
144
|
`claude-sonnet-5-5` high with class `sonnet`. Both roles cover every Explain
|
|
145
145
|
visual, including chat Mermaid and anatomy figures.
|
|
146
146
|
In `claude-only`, `axstack-auditor` uses the Claude Sonnet class at high effort.
|
|
147
|
-
The
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
The `-sol` research-code and explore-execution seats use Terra high in `mixed`
|
|
148
|
+
and Sol high in `codex-only`; the auditor pair stays Sol high in both.
|
|
149
|
+
`claude-only` records each as intentionally absent. Their stable role IDs remain
|
|
150
|
+
unchanged. Dispatch the base and paired seats independently on the same brief,
|
|
151
|
+
then reconcile their findings per claim without averaging.
|
|
151
152
|
|
|
152
153
|
The installed `<skills-dir>/axstack/roles.json` adds the selected preset name:
|
|
153
154
|
`{ "version": 1, "preset": "<name>", "roles": [...] }`. The runtime reads it
|
|
@@ -162,7 +163,8 @@ Peer roles use the stable IDs `axstack-reviewer-primary` and
|
|
|
162
163
|
`axstack-reviewer-peer`; their provider/class mappings come only from the
|
|
163
164
|
selected preset and the ordered reviewer table above. The mixed peer coordinator and review-lane binding
|
|
164
165
|
`axstack-owner` uses Sol high and never reviews. `axstack-reviewer-secondary`
|
|
165
|
-
|
|
166
|
+
uses Terra xhigh for Sol-authored candidates in `codex-only`. It is used
|
|
167
|
+
in the authored mapping and, alongside `axstack-reviewer-primary`,
|
|
166
168
|
in codebase findings mode.
|
|
167
169
|
|
|
168
170
|
The unavailable adviser in each single-provider preset stays explicitly
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "axstack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "Axstack installer and setup CLI: installs owned chat skills and role data, configures supported harness settings, and checks T3 Code capabilities.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"provider": "codex",
|
|
53
53
|
"modeId": "full-access",
|
|
54
54
|
"thinkingOptionId": "xhigh",
|
|
55
|
-
"notes": "
|
|
56
|
-
"modelClass": "
|
|
55
|
+
"notes": "Terra xhigh authored reviewer for a Sol-authored candidate. Never author or owner; review exact SHA and base across all six angles and acceptance.",
|
|
56
|
+
"modelClass": "terra"
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
"id": "axstack-reviewer-peer",
|
|
@@ -102,12 +102,12 @@
|
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
104
|
"id": "axstack-research-code-sol",
|
|
105
|
-
"name": "Axstack research code
|
|
105
|
+
"name": "Axstack research code Terra",
|
|
106
106
|
"provider": "codex",
|
|
107
107
|
"modeId": "full-access",
|
|
108
108
|
"thinkingOptionId": "high",
|
|
109
|
-
"notes": "Independent
|
|
110
|
-
"modelClass": "
|
|
109
|
+
"notes": "Independent Terra high research code investigator: verifies code behavior and executable evidence on the same bounded brief without cross-reading. Reports findings for driver reconciliation.",
|
|
110
|
+
"modelClass": "terra"
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
"id": "axstack-research-web",
|
|
@@ -143,8 +143,8 @@
|
|
|
143
143
|
"provider": "codex",
|
|
144
144
|
"modeId": "full-access",
|
|
145
145
|
"thinkingOptionId": "medium",
|
|
146
|
-
"notes": "Independent
|
|
147
|
-
"modelClass": "
|
|
146
|
+
"notes": "Independent Terra medium explanation reviewer: always reviews every visual. Reviews consequential or complex driver-written prose or on request. Checks the exact artifact for text and source fidelity. The browser interaction pass belongs to axstack-ui-verifier. Any artifact change invalidates its review. Resolve the class from saved T3 capabilities at run start; rejection and unavailable settings hold without substitution. Design review follows design.md hard rules and FRONTEND-DESIGN.md (calibration defaults, restraint and quality floor) for every inline HTML page and every Publish article adaptation. For inline pages the reviewer itself renders exact bytes with html_preview at 728px dark and 360px light as a non-publishing self-check. For Publish the reviewer opens axstack-ui-verifier PNGs at 728px dark and 360px light plus 390px mobile and 1440px desktop, covering all four widths in both themes with paths bound to the artifact SHA-256 and recorded before review. Inspect the 1440px desktop screenshot for balanced composition, intentional horizontal margins, bounded prose measure, typography, section spacing and chart legibility. Inspect the 360px and 390px mobile screenshots for gutters, readable text and chart labels. A page failing to look decent is a blocking design finding, repaired by the same author within the two-round visual limit. If only design findings remain after the second round, deliver the visual with each finding named. Never describe a remaining finding as fixed or a clean pass. Apply Delivery's normal holds for other failures and unknown renders. Chat Mermaid and anatomy keep their existing review.",
|
|
147
|
+
"modelClass": "terra"
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
150
|
"id": "axstack-ui-verifier",
|
|
@@ -185,12 +185,12 @@
|
|
|
185
185
|
},
|
|
186
186
|
{
|
|
187
187
|
"id": "axstack-explore-execution-sol",
|
|
188
|
-
"name": "Axstack execution explorer
|
|
188
|
+
"name": "Axstack execution explorer Terra",
|
|
189
189
|
"provider": "codex",
|
|
190
190
|
"modeId": "full-access",
|
|
191
191
|
"thinkingOptionId": "high",
|
|
192
|
-
"notes": "Independent
|
|
193
|
-
"modelClass": "
|
|
192
|
+
"notes": "Independent Terra high execution explorer: runs authorized bounded runtime checks on the same brief without cross-reading. Reports findings for driver reconciliation.",
|
|
193
|
+
"modelClass": "terra"
|
|
194
194
|
},
|
|
195
195
|
{
|
|
196
196
|
"id": "axstack-auditor",
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Saved New-chat default (opt-in)
|
|
2
|
+
|
|
3
|
+
Run these commands from the installed Axstack skill directory (containing
|
|
4
|
+
`scripts/` and `systemd/`). `bun scripts/sync-default.js --help` lists all inputs
|
|
5
|
+
and writes. Dry-run is the default. `--apply` changes only the global saved
|
|
6
|
+
`defaultModelSelection`; existing chats, dispatch roles and provider settings
|
|
7
|
+
are untouched. Manual New chat may retain a draft model choice; project overrides
|
|
8
|
+
still win. Output lists saved project override IDs. On older T3 settings where
|
|
9
|
+
`projectSettingsFolded` is false/absent, legacy project defaults may also live in
|
|
10
|
+
T3's project records and are not enumerated by this script; inspect Project Settings.
|
|
11
|
+
There is no per-start hook or guarantee that a client has refreshed its saved default.
|
|
12
|
+
|
|
13
|
+
## Inputs and policy
|
|
14
|
+
|
|
15
|
+
The live path reads the one enabled `cliproxy` source from settings, using its
|
|
16
|
+
URL and management key in memory. When settings contain the literal masked key
|
|
17
|
+
`••••••`, resolve only `secrets/usage-limit-source-<base64url(sourceId)>.bin` beside
|
|
18
|
+
the explicit settings file, using T3's UTF-8/base64url naming. Inline keys still work.
|
|
19
|
+
The settings object stays masked; only a temporary hub config is hydrated. Secret
|
|
20
|
+
files must be owned private regular files, with no symlinks or shared writable
|
|
21
|
+
parent; malformed, missing or unsafe keys hold with a sanitized reason. Neither
|
|
22
|
+
secret bytes nor raw settings/headers/responses are logged or persisted. Like installed T3 0.1.3 `makeCliproxyApi`, it
|
|
23
|
+
GETs `v0/management/auth-files`, then POSTs `api-call` envelopes requesting GET
|
|
24
|
+
Anthropic OAuth `/api/oauth/usage` or Codex `/backend-api/wham/usage`. Hub-side
|
|
25
|
+
`$TOKEN$` substitution keeps account tokens out of the script. It never requests
|
|
26
|
+
account credential contents, reset credits or reset-quota operations. HTTPS is required except
|
|
27
|
+
loopback fixtures, redirects are refused, requests time out after 15 seconds,
|
|
28
|
+
and at most 32 accounts are queried in batches of four. A source/account failure
|
|
29
|
+
holds; reports contain only policy diagnostics, never raw settings or responses.
|
|
30
|
+
No live requests were needed to develop the adapter.
|
|
31
|
+
|
|
32
|
+
The default model catalog comes from existing T3 `caches/<instanceId>.json`
|
|
33
|
+
(sibling to `userdata`), with ready/enabled/installed status, matching driver,
|
|
34
|
+
model slug and effort descriptor. Catalogs older than 24 hours hold; the script
|
|
35
|
+
never refreshes providers. Exactly one enabled instance per target driver is
|
|
36
|
+
required because usage sources do not establish which of several instances
|
|
37
|
+
shares which accounts. Refresh catalogs in T3 if held. These checks establish
|
|
38
|
+
catalog evidence, not successful provider execution.
|
|
39
|
+
|
|
40
|
+
Routes: Claude `claude-opus-5-5` medium; Codex `gpt-6-astra` medium;
|
|
41
|
+
Codex `gpt-6.1-sol` xhigh. The configured instance IDs are discovered, not renamed.
|
|
42
|
+
Both source and account telemetry must be no older than five minutes, with
|
|
43
|
+
valid applicable session/weekly percentages and durations. A supplied reset must
|
|
44
|
+
be a valid future date within its window duration. Missing,
|
|
45
|
+
stale, malformed or ambiguous data holds the entire decision, without failover.
|
|
46
|
+
A successful Codex response containing only a seven-day primary window (secondary
|
|
47
|
+
absent/null) is authoritative single-window telemetry: the adapter explicitly marks
|
|
48
|
+
session n/a. It does not synthesize session capacity. A plain incomplete snapshot
|
|
49
|
+
without this provenance still holds; weekly is always required. An account needs
|
|
50
|
+
remaining quota in every applicable window. Null or absent reset timing is valid for
|
|
51
|
+
Claude session/weekly/scoped Opus and Codex windows. The adapter omits the missing
|
|
52
|
+
reset; it never invents a timestamp. Unknown reset timing alone does **not** mean
|
|
53
|
+
unused or not started: the reported used percentage still governs remaining quota,
|
|
54
|
+
same-account eligibility and Astra's reserve, including exhaustion at 100% used.
|
|
55
|
+
For pressure, an untimed window earns zero elapsed-window credit, a conservative
|
|
56
|
+
lower bound, while its full reported usage is subtracted. `untimedWindows` reports
|
|
57
|
+
these windows (including relevant Opus scope); n/a session windows are separate.
|
|
58
|
+
Malformed non-null dates, stale telemetry and missing applicable windows still hold. A blocked account does not
|
|
59
|
+
exhaust other accounts. Opus scoped weekly limits also constrain eligibility.
|
|
60
|
+
|
|
61
|
+
Installed T3 0.1.3 client `main-lZWM_z1N.js` functions `g7`/`_7` display **equal
|
|
62
|
+
account arithmetic means**, not a weighted quota total: each account contributes
|
|
63
|
+
one percentage. Codex groups by window kind; Claude uses kind plus window ID.
|
|
64
|
+
A weekly-exhausted account contributes 100% session usage. Different resets stay
|
|
65
|
+
per account; T3 lists refill events scaled by member count, rather than inventing
|
|
66
|
+
one pool reset. An n/a session contributes neither a zero nor 100% observation;
|
|
67
|
+
only accounts with an applicable session enter its mean, and an all-n/a session
|
|
68
|
+
is reported as null with status n/a. This script uses that unrounded mean for the reserve check:
|
|
69
|
+
Astra is excluded if either applicable Codex window has less than 25% remaining; exactly
|
|
70
|
+
25% is eligible. Opus has no Codex reserve exclusion. Percentages do not establish
|
|
71
|
+
absolute tokens or relative subscription sizes. Duplicate identities hold
|
|
72
|
+
instead of silently adding capacity from overlapping sources.
|
|
73
|
+
|
|
74
|
+
Pressure is the worse session/weekly mean of elapsed-window percentage minus
|
|
75
|
+
used percentage, higher meaning more underused capacity. As in T3's pace display,
|
|
76
|
+
a weekly-blocked synthetic session has no duration and is omitted from pace,
|
|
77
|
+
while still contributing 100% to remaining-capacity aggregation. Reset timing is
|
|
78
|
+
computed per account before averaging. Similar pressure is within five percentage
|
|
79
|
+
points; an existing eligible default stays unless a challenger is at least ten
|
|
80
|
+
points better or a fair tie-break is justified.
|
|
81
|
+
|
|
82
|
+
The timer automatically reads the existing `statev2.sqlite` beside settings using
|
|
83
|
+
Bun's built-in SQLite read-only mode. It queries only recent thread metadata from
|
|
84
|
+
`orchestration_v2_projection_threads` across all projects, including archived and
|
|
85
|
+
soft-deleted records still present. It creates no DB, migrates nothing, and issues
|
|
86
|
+
no T3 auth session. Symlink/unsafe DB or WAL/shared-memory paths hold history;
|
|
87
|
+
a WAL without existing shared memory is refused to avoid creating sidecars.
|
|
88
|
+
SQLite's normal transient reader locks are required. Metadata reads are capped at
|
|
89
|
+
50,000 rows. Missing, incompatible, incomplete or malformed history reports an
|
|
90
|
+
explicit unavailable reason, without pretending the counts are all zero.
|
|
91
|
+
|
|
92
|
+
This route avoids installed CLI limitations: 0.1.0 has no thread-list command;
|
|
93
|
+
0.1.3's thread list exhausts pagination but issues/revokes an auth session, and
|
|
94
|
+
its project command has no list subcommand. No CLI auth or settings updates are
|
|
95
|
+
needed. `--history-db` selects another existing projection DB; `--history-file`
|
|
96
|
+
overrides the DB with a saved native thread-list array or complete result object.
|
|
97
|
+
A non-null cursor (including zero), hasMore, total mismatch, duplicates or missing
|
|
98
|
+
manual identity makes the inventory unavailable.
|
|
99
|
+
|
|
100
|
+
Count identifiable top-level `createdBy:user` starts from web **or mobile** in the
|
|
101
|
+
preceding seven days, using native `providerInstanceId` and `model` fields (legacy
|
|
102
|
+
modelSelection snapshots also work). Two extra starts can break a pressure tie.
|
|
103
|
+
Projection metadata describes the **current saved model** of a thread created in
|
|
104
|
+
that period, not an immutable record of its original model; hard-deleted/pruned
|
|
105
|
+
records and manual model changes limit fidelity. The source/coverage is reported.
|
|
106
|
+
A supplied project-only list remains caller-scoped, not global history.
|
|
107
|
+
|
|
108
|
+
When history is unavailable, similarly pressured eligible models receive fixed
|
|
109
|
+
six-hour UTC slots in Opus/Astra/Sol order. The target is stable within a slot;
|
|
110
|
+
pressure hysteresis still protects a current default outside a fair tie. This
|
|
111
|
+
prevents indefinite starvation without timer state, a new database, per-tick
|
|
112
|
+
rotation, or any claim of equal chat counts. Quota eligibility/reserves always win.
|
|
113
|
+
|
|
114
|
+
## Run or enable
|
|
115
|
+
|
|
116
|
+
First inspect a dry-run, including any hold and project overrides:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
bun scripts/sync-default.js
|
|
120
|
+
bun scripts/sync-default.js --apply
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`--usage-file` accepts an array of normalized T3 `UsageLimitSourceSnapshot`
|
|
124
|
+
objects, including per-account `usageLimits`. `--catalog-dir` selects provider
|
|
125
|
+
caches. `--now <ISO>` is fixture-only and requires `--usage-file`.
|
|
126
|
+
Settings symlinks are refused. Apply creates a sibling lock directory and unique
|
|
127
|
+
temporary file, preserves mode, fsyncs, compares current bytes/inode/mode before
|
|
128
|
+
atomic rename, then reads back. A non-cooperating writer still has a small
|
|
129
|
+
compare-to-rename race; this is not a transaction with T3's settings watcher.
|
|
130
|
+
An existing lock holds; after a crash inspect ownership/process state before
|
|
131
|
+
manually removing that exact lock. Dry-run creates no lock or file.
|
|
132
|
+
|
|
133
|
+
The five-minute user timer is a separate explicit opt-in. It runs one bounded
|
|
134
|
+
process, never an agent. It is not enabled by the installer. After accepting its
|
|
135
|
+
writes, install the shipped units:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
mkdir -p ~/.config/systemd/user
|
|
139
|
+
cp systemd/axstack-default-sync.{service,timer} ~/.config/systemd/user/
|
|
140
|
+
systemctl --user daemon-reload
|
|
141
|
+
systemctl --user enable --now axstack-default-sync.timer
|
|
142
|
+
journalctl --user -u axstack-default-sync.service -n 20
|
|
143
|
+
# Stop future updates:
|
|
144
|
+
systemctl --user disable --now axstack-default-sync.timer
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Exit 2 means held; inspect the sanitized reason. Systemd may show that bounded
|
|
148
|
+
run as failed and will try again at the next timer tick. No automatic retry loop,
|
|
149
|
+
provider failover, account routing, or live service upgrade is included.
|
|
150
|
+
|
|
151
|
+
## Titles
|
|
152
|
+
|
|
153
|
+
On **T3 server >=0.1.3** paste [the short instructions](thread-title-instructions.txt)
|
|
154
|
+
into global Settings → Threads → title instructions, or use the supported
|
|
155
|
+
`server.updateSettings` patch containing only `threadTitleInstructions`.
|
|
156
|
+
Preserve `textGenerationModelSelection`.
|
|
157
|
+
This script never writes title settings; 0.1.0 does not support that field.
|
|
158
|
+
Do not upgrade or restart T3 as part of enabling this script.
|
|
159
|
+
|
|
160
|
+
These are best-effort generation instructions; deterministic child titles come
|
|
161
|
+
from [runtime naming guidance](t3-runtime.md). Supply `Worker: <dispatch-key>`,
|
|
162
|
+
`Reviewer: <dispatch-key>`, `Researcher: <dispatch-key>` or `Subagent: <dispatch-key>`
|
|
163
|
+
explicitly, with a first message. Regeneration and empty-then-human-fed threads
|
|
164
|
+
can invoke generation again; preserve the complete prior title and `Needs you:`.
|
|
165
|
+
Verify New chat selection, settings watcher refresh and title regeneration on the
|
|
166
|
+
actual supported client/server before claiming live behavior.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
| --- | --- | --- |
|
|
16
16
|
| `mixed` | `codex/sol` | `axstack-reviewer-secondary` (`claude/opus` high) |
|
|
17
17
|
| `mixed` | `claude/opus` | `axstack-reviewer-primary` (`codex/sol` high) |
|
|
18
|
-
| `codex-only` | `codex/sol` | `axstack-reviewer-secondary` (`codex/
|
|
18
|
+
| `codex-only` | `codex/sol` | `axstack-reviewer-secondary` (`codex/terra` xhigh) |
|
|
19
19
|
| `claude-only` | `claude/opus` | `axstack-reviewer-secondary` (`claude/sonnet` high) |
|
|
20
20
|
- `axstack-advisor-astra`/`axstack-advisor-opus` advise and author candidates;
|
|
21
21
|
`axstack-arena-candidate-grok` adds a family.
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
`axstack-auditor` audits; `axstack-checker` reports discrepancies.
|
|
25
25
|
- `axstack-explainer` authors every visual Explain delivers, including pages,
|
|
26
26
|
chat Mermaid and anatomy figures; mixed pins Sonnet 5.5 high exactly.
|
|
27
|
-
- `axstack-explainer-review` always reviews every visual; `codex/
|
|
27
|
+
- `axstack-explainer-review` always reviews every visual; `codex/terra` medium in mixed.
|
|
28
28
|
Consequential or complex driver-written prose keeps its review rule; every
|
|
29
29
|
whiteboard defense is consequential, in chat too.
|
|
30
30
|
- `axstack-diligence`: read-only [diligence checks](diligence.md) for every authored PR
|
|
@@ -45,9 +45,10 @@
|
|
|
45
45
|
`axstack-explore-codebase-luna` per facet in mixed (Haiku high + Luna
|
|
46
46
|
xhigh) and codex-only (Sol high + Luna xhigh). Claude-only requires
|
|
47
47
|
the Haiku high base seat per facet.
|
|
48
|
-
- Sol pairs `axstack-auditor-sol`/`axstack-research-code-sol`/
|
|
48
|
+
- Sol pairs (stable `-sol` IDs) `axstack-auditor-sol`/`axstack-research-code-sol`/
|
|
49
49
|
`axstack-explore-execution-sol`: `codex/sol` high in
|
|
50
|
-
mixed/codex-only; intentionally absent in claude-only.
|
|
50
|
+
mixed/codex-only; intentionally absent in claude-only. In mixed,
|
|
51
|
+
research-code and explore-execution use `codex/terra` high instead. Dispatch each
|
|
51
52
|
independently from its base seat on the same bounded brief without
|
|
52
53
|
cross-reading. The driver reconciles findings per claim, never averages.
|
|
53
54
|
Record intentional absence and continue with Sonnet alone.
|
|
@@ -75,9 +75,9 @@ configuration; unrelated configurations remain eligible.
|
|
|
75
75
|
|
|
76
76
|
| Roles | Mechanism and permitted workspace | Completion |
|
|
77
77
|
|---|---|---|
|
|
78
|
-
| Advisers, research, read-only explorers, explainers, diligence, checker, auditor, arena prose candidates and judges, escalation | Must use async `delegate_task` in the driver worktree, title = dispatch key; tracked and untracked files stay untouched; writes only `<run>/evidence/<key>/` | Native notification followed by persisted `task_status` |
|
|
79
|
-
| Reviewers (peer/authored), release checks, debug investigators, execution investigators (`axstack-explore-execution`), UI verifier candidate checks (`axstack-ui-verifier`) | Must use async `delegate_task`, title = dispatch key; driver makes a disposable detached checkout of candidate SHA and pinned base with `git worktree add --detach <run>/checkouts/<key> <sha>` (plus pinned debug patch); brief requires `cd` into it; only disposable probes write there, outputs go to `<run>/evidence/<key>/` | Same delegated terminal checks |
|
|
80
|
-
| Author and repairs | Fresh authors must use async `delegate_task` with `isolation:"worktree"`, title = dispatch key, stable `clientRequestId` in their own worktree, kept until PR merges or closes; repairs use `t3_thread_send` to the same child | Delegated terminal checks plus verified candidate |
|
|
78
|
+
| Advisers, research, read-only explorers, explainers, diligence, checker, auditor, arena prose candidates and judges, escalation | Must use async `delegate_task` in the driver worktree, title = role-labelled dispatch key; tracked and untracked files stay untouched; writes only `<run>/evidence/<key>/` | Native notification followed by persisted `task_status` |
|
|
79
|
+
| Reviewers (peer/authored), release checks, debug investigators, execution investigators (`axstack-explore-execution`), UI verifier candidate checks (`axstack-ui-verifier`) | Must use async `delegate_task`, title = role-labelled dispatch key; driver makes a disposable detached checkout of candidate SHA and pinned base with `git worktree add --detach <run>/checkouts/<key> <sha>` (plus pinned debug patch); brief requires `cd` into it; only disposable probes write there, outputs go to `<run>/evidence/<key>/` | Same delegated terminal checks |
|
|
80
|
+
| Author and repairs | Fresh authors must use async `delegate_task` with `isolation:"worktree"`, title = role-labelled dispatch key, stable `clientRequestId` in their own worktree, kept until PR merges or closes; repairs use `t3_thread_send` to the same child | Delegated terminal checks plus verified candidate |
|
|
81
81
|
| Owner | Driver thread in Driver worktree; never writes tracked candidate source or tests; planning artifacts allowed only for repository Markdown; scope, integration, forge mutations and record | No worker launch |
|
|
82
82
|
|
|
83
83
|
The UI verifier checkout row applies to candidate checks, while a page without
|
|
@@ -89,15 +89,21 @@ The driver may write planning artifacts (spec, ticket map) in its own worktree w
|
|
|
89
89
|
Repairs return to that author. Missing or idle sessions grant no ownership transfer.
|
|
90
90
|
The current chat/driver has no role row in any preset.
|
|
91
91
|
|
|
92
|
-
At launch and rename, visible driver thread titles express a plain outcome
|
|
93
|
-
at most 30 characters.
|
|
92
|
+
At launch and rename, visible driver thread titles express a plain outcome with
|
|
93
|
+
`Driver:` prefix, at most 30 characters including the prefix.
|
|
94
94
|
Visible driver thread titles use no repo, issue or size prefix.
|
|
95
|
-
Hidden children keep dispatch-key titles.
|
|
95
|
+
Hidden children keep role-labelled dispatch-key titles.
|
|
96
96
|
D9 applies to driver threads only, while flagged unbound pass threads retain the
|
|
97
97
|
`Needs you: <kind> <repo>#<n>` (or `<lane>`) rename under Automations.
|
|
98
98
|
|
|
99
99
|
The dispatch key must be `<run>:<role>:<task>:a<n>`, recorded before launch and
|
|
100
|
-
|
|
100
|
+
preserved byte-for-byte after its role prefix in the exact whole T3 title.
|
|
101
|
+
Use `Worker: <dispatch-key>` for authors, `Reviewer: <dispatch-key>` for reviewers,
|
|
102
|
+
`Researcher: <dispatch-key>` for research, and `Subagent: <dispatch-key>` for other children.
|
|
103
|
+
Record both the key and complete display title; compare the exact complete title
|
|
104
|
+
during recovery. Substring matches do not establish identity. Branch and task
|
|
105
|
+
identity continue to use the original dispatch key. On title regeneration preserve
|
|
106
|
+
explicit role-labelled dispatch keys and `Needs you:` notices unchanged.
|
|
101
107
|
Each dispatch binds the approved spec or small-change intent, brief, authority, role snapshot, base and candidate to its dispatch key.
|
|
102
108
|
Every dispatch brief must state that dispatched roles never call `html_render`.
|
|
103
109
|
Dispatched roles may call `html_preview` within [UI verification](ui-verification.md).
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Return only the title. For a new ordinary manual chat, use "Driver: " followed by a short sentence-case outcome naming the actual request. Aim for 30 characters including the prefix. No quotes, emoji, final punctuation, repository, issue or size prefix. Do not invent scope or turn a question into a repair task.
|
|
2
|
+
Use an explicit delegated role from the supplied context only: Worker:, Reviewer:, Researcher:, or Subagent:. Merely discussing a role does not assign it.
|
|
3
|
+
On regeneration preserve an existing role prefix and every explicit dispatch/tracking key byte-for-byte. Return a complete explicit role-labelled title containing a dispatch key unchanged, regardless of length. Preserve Needs you: notices and identifying details unchanged.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { historyCounts } from './driver-history.js';
|
|
2
|
+
const routes = [
|
|
3
|
+
['claudeAgent', 'claude-opus-5-5', 'effort', 'medium'],
|
|
4
|
+
['codex', 'gpt-6-astra', 'reasoningEffort', 'medium'],
|
|
5
|
+
['codex', 'gpt-6.1-sol', 'reasoningEffort', 'xhigh'],
|
|
6
|
+
];
|
|
7
|
+
const mean = (values) => values.reduce((a, b) => a + b, 0) / values.length;
|
|
8
|
+
const fresh = (date, now, age) => Number.isFinite(Date.parse(date)) && now >= Date.parse(date) && now - Date.parse(date) <= age;
|
|
9
|
+
const same = (a, b) => a?.instanceId === b?.instanceId && a?.model === b?.model;
|
|
10
|
+
|
|
11
|
+
// Unknown reset timing earns no elapsed-window allowance; reported usage remains authoritative.
|
|
12
|
+
function elapsed(window, now, minutes) {
|
|
13
|
+
if (window.resetsAt == null) return 0;
|
|
14
|
+
const reset = typeof window.resetsAt === 'string' ? Date.parse(window.resetsAt) : NaN;
|
|
15
|
+
if (!Number.isFinite(reset) || reset <= now || reset - now > minutes * 60000) throw Error('malformed reset timing');
|
|
16
|
+
return 1 - (reset - now) / (minutes * 60000);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pool(accounts, driver, now) {
|
|
20
|
+
const members = accounts.filter((a) => a.driver === driver);
|
|
21
|
+
if (!members.length) throw Error(`missing ${driver} accounts`);
|
|
22
|
+
const rows = members.map((a) => {
|
|
23
|
+
const u = a.usageLimits;
|
|
24
|
+
if (u?.unavailable || !fresh(u?.checkedAt, now, 300000) || !Array.isArray(u?.windows)) throw Error(`unknown ${driver} telemetry`);
|
|
25
|
+
const windows = ['session', 'weekly'].map((kind) => {
|
|
26
|
+
const found = u.windows.filter((w) => w.kind === kind && !(driver === 'claudeAgent' && w.id?.startsWith('seven_day_')));
|
|
27
|
+
if (!found.length && driver === 'codex' && kind === 'session'
|
|
28
|
+
&& u.notApplicable?.session === 'verified Codex weekly-only response') return null;
|
|
29
|
+
if (found.length !== 1) throw Error(`missing or ambiguous ${driver} ${kind} window`);
|
|
30
|
+
const w = found[0];
|
|
31
|
+
if (!Number.isFinite(w.usedPercent) || w.usedPercent < 0 || w.usedPercent > 100
|
|
32
|
+
|| !Number.isFinite(w.windowDurationMins) || w.windowDurationMins <= 0) throw Error(`malformed ${driver} ${kind} window`);
|
|
33
|
+
return { used: w.usedPercent, elapsed: elapsed(w, now, w.windowDurationMins), resetKnown: w.resetsAt != null };
|
|
34
|
+
});
|
|
35
|
+
const scoped = u.windows.filter((w) => w.id?.startsWith('seven_day_') && /opus/i.test(w.label ?? ''));
|
|
36
|
+
for (const w of scoped) {
|
|
37
|
+
if (!Number.isFinite(w.usedPercent) || w.usedPercent < 0 || w.usedPercent > 100) throw Error('unknown Opus scoped telemetry');
|
|
38
|
+
elapsed(w, now, 10080);
|
|
39
|
+
}
|
|
40
|
+
const untimedWindows = windows.filter((w) => w?.resetKnown === false).length
|
|
41
|
+
+ scoped.filter((w) => w.resetsAt == null).length;
|
|
42
|
+
const viable = windows.every((w) => w === null || w.used < 100) && scoped.every((w) => w.usedPercent < 100);
|
|
43
|
+
// Match T3's pool display: a weekly-blocked account contributes 100% session used.
|
|
44
|
+
if (windows[0] && windows[1].used >= 100) windows[0] = { used: 100, elapsed: null };
|
|
45
|
+
return { windows, viable, untimedWindows };
|
|
46
|
+
});
|
|
47
|
+
const applicable = [0, 1].map((i) => rows.flatMap((r) => r.windows[i] ? [r.windows[i]] : []));
|
|
48
|
+
const remaining = applicable.map((ws) => ws.length ? 100 - mean(ws.map((w) => w.used)) : null);
|
|
49
|
+
const pace = applicable.flatMap((ws) => {
|
|
50
|
+
const timed = ws.filter((w) => w.elapsed !== null);
|
|
51
|
+
return timed.length ? [mean(timed.map((w) => w.elapsed * 100 - w.used))] : [];
|
|
52
|
+
});
|
|
53
|
+
return { sessionRemaining: remaining[0], weeklyRemaining: remaining[1],
|
|
54
|
+
sessionStatus: applicable[0].length ? 'applicable' : 'n/a', sessionAccounts: applicable[0].length,
|
|
55
|
+
eligibleAccounts: rows.filter((r) => r.viable).length,
|
|
56
|
+
untimedWindows: rows.reduce((sum, r) => sum + r.untimedWindows, 0), score: Math.min(...pace) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function candidates(settings, catalogs, now) {
|
|
60
|
+
return routes.map(([driver, model, option, effort]) => {
|
|
61
|
+
const instances = Object.entries(settings.providerInstances ?? {}).filter(([, p]) => p.enabled === true && p.driver === driver);
|
|
62
|
+
if (instances.length !== 1) throw Error(`missing or ambiguous enabled ${driver} instance`);
|
|
63
|
+
const instanceId = instances[0][0];
|
|
64
|
+
const found = catalogs.filter((c) => c.instanceId === instanceId);
|
|
65
|
+
const c = found[0];
|
|
66
|
+
const m = c?.models?.find((m) => m.slug === model);
|
|
67
|
+
if (found.length !== 1 || c.driver !== driver || c.enabled !== true || c.installed !== true || c.status !== 'ready'
|
|
68
|
+
|| !fresh(c.checkedAt, now, 86400000)
|
|
69
|
+
|| !m?.capabilities?.optionDescriptors?.some((o) => o.id === option && o.options?.some((v) => v.id === effort))) {
|
|
70
|
+
throw Error(`unverified ${driver} model catalog`);
|
|
71
|
+
}
|
|
72
|
+
return { driver, selection: { instanceId, model, options: [{ id: option, value: effort }] } };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function decide(settings, snapshots, catalogs, now, history = null) {
|
|
77
|
+
const result = { action: 'held', selection: settings.defaultModelSelection,
|
|
78
|
+
projectOverrides: Object.entries(settings.projectSettingsOverrides ?? {})
|
|
79
|
+
.filter(([, p]) => p?.defaultModelSelection !== undefined).map(([id]) => id),
|
|
80
|
+
projectOverrideCoverage: settings.projectSettingsFolded === true ? 'saved settings overrides'
|
|
81
|
+
: 'saved settings overrides only; legacy project records not inspected',
|
|
82
|
+
fairness: 'history unavailable; six-hour UTC tie rotation (not equal chat counts)' };
|
|
83
|
+
try {
|
|
84
|
+
if (!Number.isFinite(now) || !Array.isArray(snapshots) || !snapshots.length || !Array.isArray(catalogs)) throw Error('missing input');
|
|
85
|
+
const all = [];
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
for (const s of snapshots) {
|
|
88
|
+
if (s.error === 'hub management key missing or unsafe') throw Error('hub management key missing or unsafe');
|
|
89
|
+
if (s.kind !== 'cliproxy' || s.error || !fresh(s.checkedAt, now, 300000) || !Array.isArray(s.accounts)) throw Error('unknown source telemetry');
|
|
90
|
+
for (const a of s.accounts) {
|
|
91
|
+
if (!['codex', 'claudeAgent'].includes(a.driver)) continue;
|
|
92
|
+
// Multiple sources may expose one account. Hold rather than double-count capacity.
|
|
93
|
+
const key = `${a.driver}:${a.workspaceId ?? a.email ?? a.id}`;
|
|
94
|
+
if (!a.id || seen.has(key)) throw Error('ambiguous duplicate account');
|
|
95
|
+
seen.add(key); all.push(a);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const choices = candidates(settings, catalogs, now);
|
|
99
|
+
result.pools = Object.fromEntries(['claudeAgent', 'codex'].map((d) => [d, pool(all, d, now)]));
|
|
100
|
+
const eligible = choices.filter((c) => result.pools[c.driver].eligibleAccounts > 0
|
|
101
|
+
&& (c.selection.model !== 'gpt-6-astra'
|
|
102
|
+
|| Math.min(...[result.pools.codex.sessionRemaining, result.pools.codex.weeklyRemaining].filter((v) => v !== null)) >= 25));
|
|
103
|
+
if (!eligible.length) throw Error('both pools exhausted; no account has both windows available');
|
|
104
|
+
const best = Math.max(...eligible.map((c) => result.pools[c.driver].score));
|
|
105
|
+
const tied = eligible.filter((c) => best - result.pools[c.driver].score <= 5);
|
|
106
|
+
const current = eligible.find((c) => same(c.selection, result.selection));
|
|
107
|
+
result.history = historyCounts(history, choices, now);
|
|
108
|
+
const counts = result.history.status === 'available' ? result.history.counts : null;
|
|
109
|
+
let chosen;
|
|
110
|
+
if (counts) {
|
|
111
|
+
result.fairness = 'seven-day manual driver starts';
|
|
112
|
+
const count = (c) => counts[c.selection.model];
|
|
113
|
+
const least = Math.min(...tied.map(count));
|
|
114
|
+
// A difference of one is normal; avoid shifting on every timer tick.
|
|
115
|
+
if (!current || best - result.pools[current.driver].score >= 10 || count(current) - least >= 2) {
|
|
116
|
+
chosen = tied.find((c) => count(c) === least);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!chosen && current && best - result.pools[current.driver].score < 10) {
|
|
120
|
+
// Fixed UTC slots prevent starvation without persistent timer state or per-tick rotation.
|
|
121
|
+
const rotation = Math.floor(now / (6 * 3600000)) % choices.length;
|
|
122
|
+
const target = [...choices.slice(rotation), ...choices.slice(0, rotation)].find((c) => tied.includes(c));
|
|
123
|
+
chosen = !counts && tied.includes(current) ? target : current;
|
|
124
|
+
}
|
|
125
|
+
if (!chosen) {
|
|
126
|
+
const index = choices.findIndex((c) => same(c.selection, result.selection));
|
|
127
|
+
chosen = [...choices.slice(index + 1), ...choices.slice(0, index + 1)].find((c) => tied.includes(c));
|
|
128
|
+
}
|
|
129
|
+
const equal = JSON.stringify(chosen.selection) === JSON.stringify(result.selection);
|
|
130
|
+
return { ...result, action: equal ? 'unchanged' : 'update', reason: equal ? 'stable default' : 'capacity or fair tie-break', selection: chosen.selection };
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return { ...result, reason: error.message };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
// Cooperating writers share this lock; non-cooperating T3 writes are compared immediately before rename.
|
|
4
|
+
export function commit(path, original, selection) {
|
|
5
|
+
const lock = `${path}.axstack-default-sync.lock`;
|
|
6
|
+
try { fs.mkdirSync(lock, { mode: 0o700 }); }
|
|
7
|
+
catch (error) { if (error.code === 'EEXIST') return 'locked'; throw error; }
|
|
8
|
+
let temp;
|
|
9
|
+
let fd;
|
|
10
|
+
try {
|
|
11
|
+
const info = fs.lstatSync(path);
|
|
12
|
+
if (!info.isFile() || info.isSymbolicLink()) throw Error('settings must be a regular file; symlinks are refused');
|
|
13
|
+
if (fs.readFileSync(path, 'utf8') !== original) return 'concurrent-edit';
|
|
14
|
+
const settings = JSON.parse(original);
|
|
15
|
+
settings.defaultModelSelection = selection;
|
|
16
|
+
const next = `${JSON.stringify(settings, null, 2)}\n`;
|
|
17
|
+
if (JSON.stringify(JSON.parse(original).defaultModelSelection) === JSON.stringify(selection)) return 'unchanged';
|
|
18
|
+
temp = `${path}.axstack-default-sync-${crypto.randomUUID()}.tmp`;
|
|
19
|
+
fd = fs.openSync(temp, 'wx', info.mode & 0o7777);
|
|
20
|
+
fs.fchmodSync(fd, info.mode & 0o7777);
|
|
21
|
+
fs.writeFileSync(fd, next);
|
|
22
|
+
fs.fsyncSync(fd);
|
|
23
|
+
fs.closeSync(fd); fd = undefined;
|
|
24
|
+
const latest = fs.lstatSync(path);
|
|
25
|
+
if (latest.ino !== info.ino || latest.dev !== info.dev || latest.mode !== info.mode
|
|
26
|
+
|| fs.readFileSync(path, 'utf8') !== original) return 'concurrent-edit';
|
|
27
|
+
// A non-cooperating writer still has a small compare-to-rename race; no file API provides CAS.
|
|
28
|
+
fs.renameSync(temp, path); temp = undefined;
|
|
29
|
+
if (fs.readFileSync(path, 'utf8') !== next) throw Error('settings read-back mismatch');
|
|
30
|
+
return 'updated';
|
|
31
|
+
} finally {
|
|
32
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
33
|
+
if (temp) fs.unlinkSync(temp);
|
|
34
|
+
fs.rmdirSync(lock);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
const week = 7 * 86400000;
|
|
4
|
+
|
|
5
|
+
// Read existing projections across all projects; never create a DB, migrate, or use auth sessions.
|
|
6
|
+
export function readHistory(settingsPath, now, explicitPath) {
|
|
7
|
+
const path = explicitPath ?? `${settingsPath.slice(0, settingsPath.lastIndexOf('/'))}/statev2.sqlite`;
|
|
8
|
+
let db;
|
|
9
|
+
try {
|
|
10
|
+
const info = fs.lstatSync(path);
|
|
11
|
+
if (!info.isFile() || info.isSymbolicLink() || info.uid !== process.getuid?.() || (info.mode & 0o022)) throw Error();
|
|
12
|
+
for (const suffix of ['-wal', '-shm']) {
|
|
13
|
+
try {
|
|
14
|
+
const s = fs.lstatSync(`${path}${suffix}`);
|
|
15
|
+
if (!s.isFile() || s.isSymbolicLink() || s.uid !== info.uid || (s.mode & 0o022)) throw Error();
|
|
16
|
+
} catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
17
|
+
}
|
|
18
|
+
if (fs.existsSync(`${path}-wal`) && !fs.existsSync(`${path}-shm`)) {
|
|
19
|
+
return { unavailable: 'T3 history WAL has no existing shared memory; refusing sidecar creation' };
|
|
20
|
+
}
|
|
21
|
+
db = new Database(path, { readonly: true });
|
|
22
|
+
const threads = db.query(`SELECT thread_id AS threadId, created_at AS createdAt,
|
|
23
|
+
provider_instance_id AS providerInstanceId,
|
|
24
|
+
json_extract(payload_json, '$.createdBy') AS createdBy,
|
|
25
|
+
json_extract(payload_json, '$.creationSource') AS creationSource,
|
|
26
|
+
json_extract(payload_json, '$.modelSelection.model') AS model,
|
|
27
|
+
json_extract(payload_json, '$.lineage.parentThreadId') AS parentThreadId,
|
|
28
|
+
json_type(payload_json, '$.lineage.parentThreadId') AS parentType
|
|
29
|
+
FROM orchestration_v2_projection_threads WHERE created_at >= ? AND created_at <= ?
|
|
30
|
+
ORDER BY created_at, thread_id LIMIT 50001`).all(new Date(now - week).toISOString(), new Date(now).toISOString());
|
|
31
|
+
if (threads.length > 50000) return { unavailable: 'T3 history exceeds bounded inventory limit' };
|
|
32
|
+
if (threads.some((t) => t.createdBy === 'user' && ['web', 'mobile'].includes(t.creationSource)
|
|
33
|
+
&& !['null', 'text'].includes(t.parentType))) return { unavailable: 'T3 history lacks parent identity' };
|
|
34
|
+
return { threads, complete: true, total: threads.length,
|
|
35
|
+
source: 'T3 projection database: all projects, current saved models of threads created in seven days' };
|
|
36
|
+
} catch {
|
|
37
|
+
return { unavailable: 'T3 history database unavailable, unsafe or incompatible' };
|
|
38
|
+
} finally { db?.close(); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function historyCounts(history, choices, now) {
|
|
42
|
+
const unknown = (reason) => ({ status: 'unavailable', reason });
|
|
43
|
+
if (history?.unavailable) {
|
|
44
|
+
const reasons = ['T3 history database unavailable, unsafe or incompatible', 'T3 history exceeds bounded inventory limit',
|
|
45
|
+
'T3 history lacks parent identity', 'T3 history WAL has no existing shared memory; refusing sidecar creation',
|
|
46
|
+
'history file unavailable or malformed'];
|
|
47
|
+
return unknown(reasons.includes(history.unavailable) ? history.unavailable : 'T3 history unavailable; check database or supplied snapshot');
|
|
48
|
+
}
|
|
49
|
+
const threads = Array.isArray(history) ? history : history?.threads;
|
|
50
|
+
if (!Array.isArray(threads)) return unknown('no complete T3 history inventory');
|
|
51
|
+
if (history.hasMore === true || history.nextCursor != null
|
|
52
|
+
|| (history.total !== undefined && history.total !== threads.length)) return unknown('incomplete or paginated T3 history');
|
|
53
|
+
const counts = Object.fromEntries(choices.map((c) => [c.selection.model, 0]));
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const t of threads) {
|
|
56
|
+
const id = t.threadId ?? t.id;
|
|
57
|
+
const at = Date.parse(t.createdAt);
|
|
58
|
+
if (typeof id !== 'string' || !id || seen.has(id) || !Number.isFinite(at)
|
|
59
|
+
|| typeof t.createdBy !== 'string' || typeof t.creationSource !== 'string') return unknown('malformed T3 history record');
|
|
60
|
+
seen.add(id);
|
|
61
|
+
if (t.createdBy !== 'user' || !['web', 'mobile'].includes(t.creationSource) || at > now || now - at > week) continue;
|
|
62
|
+
if (!Object.hasOwn(t, 'parentThreadId')) return unknown('manual history lacks parent identity');
|
|
63
|
+
if (t.parentThreadId !== null) continue;
|
|
64
|
+
const instance = t.providerInstanceId ?? t.modelSelection?.instanceId;
|
|
65
|
+
const model = t.model ?? t.modelSelection?.model;
|
|
66
|
+
if (typeof instance !== 'string' || typeof model !== 'string') return unknown('manual history lacks provider/model identity');
|
|
67
|
+
if (choices.some((c) => c.selection.instanceId === instance && c.selection.model === model)) counts[model]++;
|
|
68
|
+
}
|
|
69
|
+
return { status: 'available', counts, source: history.source ?? 'saved thread list; caller-supplied scope' };
|
|
70
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
// Resolve only T3's masked usage-source field; never hydrate or persist the settings object.
|
|
4
|
+
export function managementKey(value, sourceId, settingsPath) {
|
|
5
|
+
try {
|
|
6
|
+
if (value === '••••••') {
|
|
7
|
+
if (typeof settingsPath !== 'string' || !settingsPath.includes('/') || typeof sourceId !== 'string'
|
|
8
|
+
|| !sourceId || sourceId.length > 512 || Buffer.from(sourceId, 'utf8').toString('utf8') !== sourceId) throw Error();
|
|
9
|
+
const directory = `${settingsPath.slice(0, settingsPath.lastIndexOf('/'))}/secrets`;
|
|
10
|
+
const parent = fs.lstatSync(directory);
|
|
11
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== process.getuid?.() || (parent.mode & 0o022)) throw Error();
|
|
12
|
+
const path = `${directory}/usage-limit-source-${Buffer.from(sourceId, 'utf8').toString('base64url')}.bin`;
|
|
13
|
+
const fd = fs.openSync(path, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
14
|
+
try {
|
|
15
|
+
const info = fs.fstatSync(fd);
|
|
16
|
+
if (!info.isFile() || info.uid !== process.getuid?.() || (info.mode & 0o077) || info.nlink !== 1
|
|
17
|
+
|| info.size < 1 || info.size > 4096) throw Error();
|
|
18
|
+
value = new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(fd));
|
|
19
|
+
} finally { fs.closeSync(fd); }
|
|
20
|
+
}
|
|
21
|
+
// HTTP bearer keys must be bounded, unmasked printable ASCII without whitespace/control bytes.
|
|
22
|
+
if (typeof value !== 'string' || !/^[!-~]{1,4096}$/.test(value)) throw Error();
|
|
23
|
+
return value;
|
|
24
|
+
} catch {
|
|
25
|
+
throw Error('hub management key missing or unsafe');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { managementKey } from './hub-secret.js';
|
|
2
|
+
// Read-only subset of T3 0.1.3 makeCliproxyApi. Never fetch token files or reset credits.
|
|
3
|
+
async function management(config, path, body) {
|
|
4
|
+
const base = new URL(config.url.endsWith('/') ? config.url : `${config.url}/`);
|
|
5
|
+
if (base.protocol !== 'https:' && !(base.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(base.hostname))) {
|
|
6
|
+
throw Error('hub requires HTTPS or loopback HTTP');
|
|
7
|
+
}
|
|
8
|
+
if (base.username || base.password || !config.managementKey) throw Error('invalid hub configuration');
|
|
9
|
+
const response = await fetch(new URL(`v0/management/${path}`, base), {
|
|
10
|
+
method: body === undefined ? 'GET' : 'POST', redirect: 'error', signal: AbortSignal.timeout(15000),
|
|
11
|
+
headers: { Authorization: `Bearer ${config.managementKey}`, 'Content-Type': 'application/json' },
|
|
12
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
13
|
+
});
|
|
14
|
+
if (!response.ok) throw Error('hub management request refused');
|
|
15
|
+
return response.json();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function accountUsage(config, account, now) {
|
|
19
|
+
const checkedAt = new Date(now).toISOString();
|
|
20
|
+
const driver = account.provider === 'claude' ? 'claudeAgent' : 'codex';
|
|
21
|
+
const base = { id: account.id, driver,
|
|
22
|
+
...(account.email ? { email: account.email } : {}),
|
|
23
|
+
...(account.id_token?.chatgpt_account_id ? { workspaceId: account.id_token.chatgpt_account_id } : {}) };
|
|
24
|
+
try {
|
|
25
|
+
if (typeof account.auth_index !== 'string' || !account.auth_index) throw Error('invalid auth index');
|
|
26
|
+
const header = driver === 'codex' ? { Authorization: 'Bearer $TOKEN$', 'Content-Type': 'application/json',
|
|
27
|
+
'OpenAI-Beta': 'codex-1', Originator: 'Codex Desktop',
|
|
28
|
+
...(base.workspaceId ? { 'Chatgpt-Account-Id': base.workspaceId } : {}) }
|
|
29
|
+
: { Authorization: 'Bearer $TOKEN$', 'anthropic-beta': 'oauth-2025-04-20', 'User-Agent': 'claude-code/2.1.273' };
|
|
30
|
+
const response = await management(config, 'api-call', { auth_index: account.auth_index, method: 'GET',
|
|
31
|
+
url: driver === 'codex' ? 'https://chatgpt.com/backend-api/wham/usage' : 'https://api.anthropic.com/api/oauth/usage', header });
|
|
32
|
+
if (!Number.isInteger(response.status_code) || response.status_code < 200 || response.status_code >= 300 || typeof response.body !== 'string') throw Error('provider refused');
|
|
33
|
+
const usage = JSON.parse(response.body);
|
|
34
|
+
if (!usage || typeof usage !== 'object' || usage.error || usage.errors) throw Error('provider payload error');
|
|
35
|
+
const windows = [];
|
|
36
|
+
if (driver === 'codex') {
|
|
37
|
+
for (const key of ['primary_window', 'secondary_window']) {
|
|
38
|
+
const w = usage.rate_limit?.[key];
|
|
39
|
+
if (w?.reset_at != null && (typeof w.reset_at !== 'number' || !Number.isFinite(w.reset_at))) throw Error('invalid Codex reset');
|
|
40
|
+
const minutes = w?.limit_window_seconds / 60;
|
|
41
|
+
const kind = minutes >= 43200 ? 'monthly' : minutes >= 10080 ? 'weekly' : 'session';
|
|
42
|
+
if (w) windows.push({ id: key === 'primary_window' ? 'primary' : 'secondary', kind, label: kind, usedPercent: w.used_percent,
|
|
43
|
+
...(typeof w.reset_at === 'number' ? { resetsAt: new Date(w.reset_at * 1000).toISOString() } : {}),
|
|
44
|
+
windowDurationMins: w.limit_window_seconds / 60 });
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
for (const [key, kind, duration] of [['five_hour', 'session', 300], ['seven_day', 'weekly', 10080]]) {
|
|
48
|
+
const w = usage[key];
|
|
49
|
+
if (w) windows.push({ id: key, kind, label: key, usedPercent: w.utilization,
|
|
50
|
+
...(w.resets_at == null ? {} : { resetsAt: w.resets_at }), windowDurationMins: duration });
|
|
51
|
+
}
|
|
52
|
+
for (const w of usage.limits ?? []) {
|
|
53
|
+
if (w.kind === 'weekly_scoped') windows.push({ id: `seven_day_${String(w.scope?.model?.display_name ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '_')}`, kind: 'weekly',
|
|
54
|
+
label: `Weekly · ${w.scope?.model?.display_name ?? ''}`, usedPercent: w.percent, ...(w.resets_at == null ? {} : { resetsAt: w.resets_at }), windowDurationMins: 10080 });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const weeklyOnly = driver === 'codex' && usage.rate_limit?.primary_window?.limit_window_seconds === 604800
|
|
58
|
+
&& usage.rate_limit.secondary_window == null && windows.length === 1 && windows[0].kind === 'weekly';
|
|
59
|
+
return { ...base, usageLimits: { checkedAt, windows,
|
|
60
|
+
...(weeklyOnly ? { notApplicable: { session: 'verified Codex weekly-only response' } } : {}) } };
|
|
61
|
+
} catch {
|
|
62
|
+
return { ...base, usageLimits: { checkedAt, windows: [], unavailable: { reason: 'probeFailed' } } };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function readHub(settings, now = Date.now(), settingsPath) {
|
|
67
|
+
const sources = Object.entries(settings.usageLimitSources ?? {}).filter(([, s]) => s.enabled === true && s.kind === 'cliproxy');
|
|
68
|
+
if (sources.length !== 1) throw Error('expected exactly one enabled cliproxy usage source');
|
|
69
|
+
const snapshots = [];
|
|
70
|
+
for (const [id, source] of sources) {
|
|
71
|
+
let config;
|
|
72
|
+
try { config = { ...source, managementKey: managementKey(source.managementKey, id, settingsPath) }; }
|
|
73
|
+
catch {
|
|
74
|
+
snapshots.push({ id, kind: 'cliproxy', checkedAt: new Date(now).toISOString(), accounts: [], error: 'hub management key missing or unsafe' });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const raw = await management(config, 'auth-files');
|
|
79
|
+
if (!Array.isArray(raw.files)) throw Error('invalid account list');
|
|
80
|
+
const files = raw.files.filter((a) => !a.disabled && ['claude', 'codex'].includes(a.provider));
|
|
81
|
+
// Bound work and duration; a large hub needs a separately accepted batching policy.
|
|
82
|
+
if (files.length > 32) throw Error('hub account limit exceeded');
|
|
83
|
+
const accounts = [];
|
|
84
|
+
for (let i = 0; i < files.length; i += 4) {
|
|
85
|
+
accounts.push(...await Promise.all(files.slice(i, i + 4).map((a) => accountUsage(config, a, now))));
|
|
86
|
+
}
|
|
87
|
+
snapshots.push({ id, kind: 'cliproxy', checkedAt: new Date(now).toISOString(), accounts });
|
|
88
|
+
} catch {
|
|
89
|
+
snapshots.push({ id, kind: 'cliproxy', checkedAt: new Date(now).toISOString(), accounts: [], error: 'hub usage unavailable' });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return snapshots;
|
|
93
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { decide } from './default-policy.js';
|
|
3
|
+
import { commit } from './default-settings.js';
|
|
4
|
+
import { readHistory } from './driver-history.js';
|
|
5
|
+
import { readHub } from './hub-usage.js';
|
|
6
|
+
export { decide } from './default-policy.js';
|
|
7
|
+
|
|
8
|
+
const help = `Usage: bun sync-default.js [--settings path] [--dry-run | --apply]
|
|
9
|
+
--usage-file path T3 UsageLimitSourceSnapshot array (otherwise read configured hub)
|
|
10
|
+
--catalog-dir path T3 provider cache directory (default: T3 caches beside userdata)
|
|
11
|
+
--history-file path Complete saved native T3 thread-list JSON (overrides database read)
|
|
12
|
+
--history-db path Existing T3 projection DB (default: statev2.sqlite beside settings)
|
|
13
|
+
--now ISO Deterministic clock for fixtures; cannot combine with live hub
|
|
14
|
+
--help Print this help
|
|
15
|
+
Dry-run is the default and writes nothing. Reads settings, provider model caches,
|
|
16
|
+
existing T3 projection history read-only (no create/migrate/auth updates), optional snapshots,
|
|
17
|
+
or configured cliproxy auth-files/api-call usage endpoints.
|
|
18
|
+
Masked hub keys read ONLY userdata/secrets/usage-limit-source-<base64url(sourceId)>.bin
|
|
19
|
+
beside --settings, in memory. Secret symlinks, unsafe permissions and malformed keys hold.
|
|
20
|
+
--apply writes ONLY global defaultModelSelection in --settings (default ~/.t3/userdata/settings.json).
|
|
21
|
+
Apply creates a sibling .axstack-default-sync.lock directory and unique .tmp file,
|
|
22
|
+
fsyncs and atomically renames settings, preserves mode, compares concurrent edits
|
|
23
|
+
and reads back; removes its lock/temp. Existing locks hold (no stale-lock stealing).
|
|
24
|
+
Symlinks refused. Other settings, project overrides, threads, roles and credentials
|
|
25
|
+
are never modified. Does not install/enable timers or write title instructions.
|
|
26
|
+
Exit: 0 decision/unchanged, 1 input/I/O error, 2 held. Output contains no secrets.`;
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
const options = {};
|
|
30
|
+
const values = ['--settings', '--usage-file', '--catalog-dir', '--history-file', '--history-db', '--now'];
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
for (let i = 0; i < args.length; i++) {
|
|
33
|
+
const flag = args[i];
|
|
34
|
+
if (flag === '--help') { console.log(help); return; }
|
|
35
|
+
if (Object.hasOwn(options, flag)) throw Error('duplicate flag');
|
|
36
|
+
if (['--apply', '--dry-run'].includes(flag)) options[flag] = true;
|
|
37
|
+
else if (values.includes(flag) && args[i + 1] && !args[i + 1].startsWith('--')) options[flag] = args[++i];
|
|
38
|
+
else throw Error('invalid arguments');
|
|
39
|
+
}
|
|
40
|
+
if (options['--apply'] && options['--dry-run']) throw Error('conflicting modes');
|
|
41
|
+
if (options['--now'] && !options['--usage-file']) throw Error('--now requires --usage-file');
|
|
42
|
+
const requested = options['--settings'] ?? `${process.env.HOME}/.t3/userdata/settings.json`;
|
|
43
|
+
const stat = fs.lstatSync(requested);
|
|
44
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw Error('settings must be a regular file');
|
|
45
|
+
const path = fs.realpathSync(requested);
|
|
46
|
+
const original = fs.readFileSync(path, 'utf8');
|
|
47
|
+
const settings = JSON.parse(original);
|
|
48
|
+
const now = options['--now'] ? Date.parse(options['--now']) : Date.now();
|
|
49
|
+
const cache = options['--catalog-dir'] ?? `${path.slice(0, path.lastIndexOf('/'))}/../caches`;
|
|
50
|
+
const catalogs = [];
|
|
51
|
+
for (const [id, p] of Object.entries(settings.providerInstances ?? {})) {
|
|
52
|
+
if (p.enabled !== true || !['claudeAgent', 'codex'].includes(p.driver)) continue;
|
|
53
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) throw Error('invalid instance ID');
|
|
54
|
+
try { catalogs.push(JSON.parse(fs.readFileSync(`${cache}/${id}.json`, 'utf8'))); }
|
|
55
|
+
catch { /* Policy reports an unknown catalog; never guess a model. */ }
|
|
56
|
+
}
|
|
57
|
+
const usage = options['--usage-file'] ? JSON.parse(fs.readFileSync(options['--usage-file'], 'utf8')) : await readHub(settings, now, path);
|
|
58
|
+
let history;
|
|
59
|
+
if (options['--history-file']) {
|
|
60
|
+
try { history = JSON.parse(fs.readFileSync(options['--history-file'], 'utf8')); }
|
|
61
|
+
catch { history = { unavailable: 'history file unavailable or malformed' }; }
|
|
62
|
+
} else history = readHistory(path, now, options['--history-db']);
|
|
63
|
+
const result = decide(settings, usage, catalogs, now, history);
|
|
64
|
+
if (result.action === 'update' && options['--apply']) {
|
|
65
|
+
const status = commit(path, original, result.selection);
|
|
66
|
+
result.action = status === 'updated' ? 'updated' : 'held';
|
|
67
|
+
if (status !== 'updated') result.reason = status;
|
|
68
|
+
}
|
|
69
|
+
result.mode = options['--apply'] ? 'apply' : 'dry-run';
|
|
70
|
+
console.log(JSON.stringify(result));
|
|
71
|
+
if (result.action === 'held') process.exitCode = 2;
|
|
72
|
+
}
|
|
73
|
+
if (import.meta.main) main().catch(() => {
|
|
74
|
+
console.error('default sync failed: invalid input or settings I/O; no sensitive details logged');
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Choose the saved T3 New-chat default from subscription capacity
|
|
3
|
+
|
|
4
|
+
[Service]
|
|
5
|
+
Type=oneshot
|
|
6
|
+
Environment=PATH=%h/.bun/bin:/usr/local/bin:/usr/bin:/bin
|
|
7
|
+
UMask=0077
|
|
8
|
+
TimeoutStartSec=180
|
|
9
|
+
ExecStart=/usr/bin/env bun %h/.agents/skills/axstack/scripts/sync-default.js --apply
|
|
@@ -109,7 +109,7 @@ counts with denominators plus the evidence behind the count:
|
|
|
109
109
|
| --- | --- | --- |
|
|
110
110
|
| `mixed` | `codex/sol` | `axstack-reviewer-secondary` (`claude/opus` high) |
|
|
111
111
|
| `mixed` | `claude/opus` | `axstack-reviewer-primary` (`codex/sol` high) |
|
|
112
|
-
| `codex-only` | `codex/sol` | `axstack-reviewer-secondary` (`codex/
|
|
112
|
+
| `codex-only` | `codex/sol` | `axstack-reviewer-secondary` (`codex/terra` xhigh) |
|
|
113
113
|
| `claude-only` | `claude/opus` | `axstack-reviewer-secondary` (`claude/sonnet` high) |
|
|
114
114
|
- Simplification applicability determinations evidenced / total candidate diffs,
|
|
115
115
|
and complete simplification receipts / total candidates, broken down as
|
|
@@ -65,7 +65,7 @@ Immediately before an actual role dispatch, read and follow the
|
|
|
65
65
|
[T3 runtime boundary](../axstack/references/t3-runtime.md). Ordinary local
|
|
66
66
|
reading and writing does not require that launch reference.
|
|
67
67
|
|
|
68
|
-
Authors must use async `delegate_task` with `isolation:"worktree"`, title = dispatch key
|
|
68
|
+
Authors must use async `delegate_task` with `isolation:"worktree"`, title = role-labelled dispatch key
|
|
69
69
|
and stable `clientRequestId` under the runtime contract's fresh-checkout bootstrap,
|
|
70
70
|
same-child repair and recorded launch fallback rules.
|
|
71
71
|
|
|
@@ -22,8 +22,8 @@ materially helps. Do not create a new profile.
|
|
|
22
22
|
When dispatching `axstack-research-code`, dispatch `axstack-research-code-sol`
|
|
23
23
|
independently on the same bounded brief without cross-reading. The driver
|
|
24
24
|
reconciles findings per claim and never averages them. Record an intentionally
|
|
25
|
-
absent
|
|
26
|
-
|
|
25
|
+
absent `-sol` pair and proceed with the base seat alone. A configured optional
|
|
26
|
+
`-sol` pair that fails to launch is fenced, recorded `absent (<reason>)`, and
|
|
27
27
|
named once in the next read-back, then skipped without relay or substitution.
|
|
28
28
|
In mixed fan-out retain a Codex and a Claude seat or hold the affected work.
|
|
29
29
|
|
|
@@ -55,13 +55,13 @@ is part of research.
|
|
|
55
55
|
|
|
56
56
|
- `axstack-research-requirements`: requirements and intent.
|
|
57
57
|
- `axstack-research-code`: code behavior.
|
|
58
|
-
- `axstack-research-code-sol`: independent Sol
|
|
58
|
+
- `axstack-research-code-sol`: independent code investigation (Terra high in mixed, Sol high in codex-only).
|
|
59
59
|
- `axstack-research-web`: web and external sources.
|
|
60
60
|
- `axstack-research-x`: X (Twitter) posts and threads via Grok; cite post URLs and dates.
|
|
61
61
|
- For broad codebase mapping, use `axstack-scout`.
|
|
62
62
|
Never dispatch `axstack-explore-codebase` directly for broad mapping.
|
|
63
63
|
- `axstack-explore-execution`: execution and runtime traces.
|
|
64
|
-
- `axstack-explore-execution-sol`: independent Sol
|
|
64
|
+
- `axstack-explore-execution-sol`: independent execution investigation (Terra high in mixed, Sol high in codex-only).
|
|
65
65
|
|
|
66
66
|
When dispatching `axstack-research-code` or `axstack-explore-execution`,
|
|
67
67
|
dispatch its `-sol` pair independently on the same bounded brief without
|
|
@@ -36,7 +36,7 @@ This section applies to peer and authored PR modes.
|
|
|
36
36
|
| --- | --- | --- |
|
|
37
37
|
| `mixed` | `codex/sol` | `axstack-reviewer-secondary` (`claude/opus` high) |
|
|
38
38
|
| `mixed` | `claude/opus` | `axstack-reviewer-primary` (`codex/sol` high) |
|
|
39
|
-
| `codex-only` | `codex/sol` | `axstack-reviewer-secondary` (`codex/
|
|
39
|
+
| `codex-only` | `codex/sol` | `axstack-reviewer-secondary` (`codex/terra` xhigh) |
|
|
40
40
|
| `claude-only` | `claude/opus` | `axstack-reviewer-secondary` (`claude/sonnet` high) |
|
|
41
41
|
|
|
42
42
|
The diligence receipt is separate and does not count as a reviewer receipt.
|
package/src/roles.js
CHANGED
|
@@ -6,12 +6,12 @@ const PROVIDER_BOUNDS = Object.freeze({
|
|
|
6
6
|
|
|
7
7
|
const CLASS_PROVIDERS = Object.freeze({
|
|
8
8
|
fable: 'claude', opus: 'claude', sonnet: 'claude', haiku: 'claude',
|
|
9
|
-
astra: 'codex', sol: 'codex', luna: 'codex',
|
|
9
|
+
astra: 'codex', sol: 'codex', luna: 'codex', terra: 'codex',
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
export function deriveModelClass(model) {
|
|
13
13
|
if (typeof model !== 'string') return null;
|
|
14
|
-
return model.match(/^gpt-(\d+(?:\.\d+)*)-(astra|sol|luna)$/)?.[2]
|
|
14
|
+
return model.match(/^gpt-(\d+(?:\.\d+)*)-(astra|sol|luna|terra)$/)?.[2]
|
|
15
15
|
?? model.match(/^claude-(fable|opus|sonnet|haiku)-/)?.[1]
|
|
16
16
|
?? null;
|
|
17
17
|
}
|
|
@@ -22,7 +22,7 @@ const AUTHORED_ROUTES = Object.freeze({
|
|
|
22
22
|
'claude/opus': ['axstack-reviewer-primary', 'codex/sol', 'high'],
|
|
23
23
|
},
|
|
24
24
|
'codex-only': {
|
|
25
|
-
'codex/sol': ['axstack-reviewer-secondary', 'codex/
|
|
25
|
+
'codex/sol': ['axstack-reviewer-secondary', 'codex/terra', 'xhigh'],
|
|
26
26
|
},
|
|
27
27
|
'claude-only': {
|
|
28
28
|
'claude/opus': ['axstack-reviewer-secondary', 'claude/sonnet', 'high'],
|