axstack 0.53.1 → 0.55.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/docs/workflows.md CHANGED
@@ -256,7 +256,10 @@ for explicit ownership transfer.
256
256
  fenced. Fable also serves the bounded triggers in
257
257
  [Standing contracts](../skills/axstack/references/contracts.md).
258
258
  - `axstack-spec` writes observable acceptance, exclusions, decisions, and one
259
- user-approved revision baseline. Linear through the executor MCP is the
259
+ user-approved revision baseline. Its [shared format](../skills/axstack-spec/references/format.md)
260
+ opens with the human decision brief, groups binding requirements by capability,
261
+ and separates evidence/history. Review checks readability alongside completeness.
262
+ Linear through the executor MCP is the
260
263
  default only for `defi-com` repositories; GitHub Issues and repository
261
264
  Markdown are explicit alternatives and the stores for other repositories.
262
265
  A GitHub baseline pins the issue URL and approved body digest. Preflight
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "axstack",
3
- "version": "0.53.1",
3
+ "version": "0.55.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",
@@ -163,6 +163,15 @@
163
163
  "notes": "Read-only UI verifier: uses the configured verifier browser tools against the given build, URL, or artifact; captures screenshots, interactions, accessibility, desktop/mobile, and reduced-motion evidence in the dispatch evidence folder; returns a verdict with evidence paths. Performs the inline page interaction pass for every interactive page, bound to the exact bytes and SHA-256. Never edits source. T3 resolves Claude classes from saved capabilities; a Claude rejection holds.",
164
164
  "modelClass": "sonnet"
165
165
  },
166
+ {
167
+ "id": "axstack-ui-verifier-sonnet",
168
+ "name": "Axstack UI verifier Sonnet",
169
+ "provider": "claude",
170
+ "model": null,
171
+ "modeId": "bypassPermissions",
172
+ "thinkingOptionId": "high",
173
+ "notes": "Intentional single-provider absence: the mixed Sonnet UI pair is unavailable in this preset. Record dual-run verification as unverified without substitution."
174
+ },
166
175
  {
167
176
  "id": "axstack-explore-codebase",
168
177
  "name": "Axstack codebase explorer",
@@ -163,6 +163,15 @@
163
163
  "notes": "Read-only UI verifier: uses the configured verifier browser tools against the given build, URL, or artifact; captures screenshots, interactions, accessibility, desktop/mobile, and reduced-motion evidence in the dispatch evidence folder; returns a verdict with evidence paths. Performs the inline page interaction pass for every interactive page, bound to the exact bytes and SHA-256. Never edits source. Resolve the class from saved T3 capabilities at run start; rejection and unavailable settings hold without substitution.",
164
164
  "modelClass": "sol"
165
165
  },
166
+ {
167
+ "id": "axstack-ui-verifier-sonnet",
168
+ "name": "Axstack UI verifier Sonnet",
169
+ "provider": "codex",
170
+ "model": null,
171
+ "modeId": "full-access",
172
+ "thinkingOptionId": "high",
173
+ "notes": "Intentional single-provider absence: the mixed Sonnet UI pair is unavailable in this preset. Record dual-run verification as unverified without substitution."
174
+ },
166
175
  {
167
176
  "id": "axstack-explore-codebase",
168
177
  "name": "Axstack codebase explorer",
@@ -164,6 +164,15 @@
164
164
  "notes": "Read-only UI verifier: uses the configured verifier browser tools against the given build, URL, or artifact; captures screenshots, interactions, accessibility, desktop/mobile, and reduced-motion evidence in the dispatch evidence folder; returns a verdict with evidence paths. Performs the inline page interaction pass for every interactive page, bound to the exact bytes and SHA-256. Never edits source. Resolve the Terra class from saved T3 capabilities at run start; rejection and unavailable settings hold without substitution.",
165
165
  "modelClass": "terra"
166
166
  },
167
+ {
168
+ "id": "axstack-ui-verifier-sonnet",
169
+ "name": "Axstack UI verifier Sonnet",
170
+ "provider": "claude",
171
+ "modeId": "bypassPermissions",
172
+ "thinkingOptionId": "high",
173
+ "notes": "Read-only independent run 2 verifier for Android and web/PWA: same brief and pinned build, never sees run 1 results. Uses T3 device tools and Playwright MCP under ui-verification.md; returns per-feature evidence in its private folder. Never edits source. T3 resolves Claude classes from saved capabilities; a Claude rejection holds. Missing Sonnet high holds without substitution.",
174
+ "modelClass": "sonnet"
175
+ },
167
176
  {
168
177
  "id": "axstack-explore-codebase",
169
178
  "name": "Axstack codebase explorer",
@@ -29,7 +29,10 @@ paths, and stale references. Bind the result to the exact head and base.
29
29
 
30
30
  For research, reopen cited sources for answer-changing claims before the
31
31
  driver folds verified claims. For a draft spec, compare it with Align decisions
32
- before user approval: flag anything dropped, added, or softened. For tickets,
32
+ before user approval: flag anything dropped, added, or softened. For a draft spec,
33
+ check readability alongside completeness using the
34
+ [shared format](../../axstack-spec/references/format.md#readability-review).
35
+ For tickets,
33
36
  map every spec acceptance item to a capability's acceptance. Before candidate
34
37
  publication, compare the author receipt with its evidence folder: red/green
35
38
  logs exist, and counts, SHAs, and paths match. For release preparation, compare
@@ -0,0 +1,314 @@
1
+ # Pure fleet recommendation API (schema 1)
2
+
3
+ C1a exports `recommend(input, now)` and `parseFleetConfig(configText, configDigest)`
4
+ from `../scripts/fleet-route.js`. Both are synchronous Bun APIs. They perform no
5
+ I/O, use no ambient clock and keep no state. Importing the module launches nothing.
6
+ The CLI wraps this API; bounded collection is separate from pure recommendation.
7
+
8
+ ```js
9
+ import { recommend } from '../scripts/fleet-route.js';
10
+ const recommendation = recommend(snapshot, evaluationEpochMs);
11
+ // The driver preserves this JSON, then independently verifies execution authority.
12
+ ```
13
+
14
+ Inputs are JSON values. The same canonical input and integer `now` produce
15
+ byte-identical `JSON.stringify` output. Neither the input nor nested facts are
16
+ mutated or shared with the output. Times are nonnegative epoch milliseconds.
17
+ Errors throw `Error` with a stable leading code, e.g. `config-digest-mismatch`,
18
+ `schema-version`, `assignment-conflict` or `evaluation-time`. CLI mapping of
19
+ these errors belongs to the adapter; exceptions never authorize fallback routes.
20
+
21
+ ## Configuration bytes and policy
22
+
23
+ `configText` contains the exact reviewed UTF-8 JSON bytes, including whitespace.
24
+ `configDigest` is the run-pinned lowercase SHA-256 hex digest of those bytes.
25
+ `parseFleetConfig` verifies the digest before parsing and returns the validated
26
+ object. Missing/invalid configuration and digest mismatches fail explicitly.
27
+ No account path or fleet identity is built into this API.
28
+
29
+ ### Offline command
30
+
31
+ `bun fleet-route.js recommend --input snapshot.json --now 100000 --json`
32
+ reads saved facts plus `--config <path>` (default:
33
+ `~/.config/haoshoku/fleet-routing.json`). It never samples or dispatches.
34
+ `--config-digest <sha256>` pins reviewed config bytes; otherwise the saved
35
+ input must carry `configDigest`. Any saved `configText`/`configDigest` must
36
+ agree with the file and explicit pin. Saved input may omit `configText`.
37
+ `--task <name>` overrides only the request task preference. Evaluation time
38
+ is required and explicit. `--help` lists flags and side effects.
39
+
40
+ With `--json`, stdout is the complete schema-1 result; errors are always
41
+ `{"error":{"code":"stable-code","message":"detail"}}` on stderr with
42
+ empty stdout. Exit 0 means a recommendation/help, 1 means an input/config
43
+ error, and 2 means explicit no-route JSON (when `--json` is used). Without
44
+ `--json`, stdout is a one-line stage/host/reason summary. Authentication stays
45
+ on the actual destination session: required session capabilities are supplied
46
+ evidence, never inferred from the origin catalog or copied credentials.
47
+
48
+ | Config field | Contract |
49
+ | --- | --- |
50
+ | `schemaVersion` | Exactly `1` |
51
+ | `hosts` | Nonempty array of `{id, environmentId, fallbackLimitBp, capabilities}`; both identities unique |
52
+ | `pool` | Unique subset of host IDs; may be empty |
53
+ | `defaultOrder` | Every host ID exactly once |
54
+ | `taskOrders` | Task-name map of complete preference orders, e.g. `playwright`; unknown tasks use default order |
55
+ | `balanceLimitBp` | Integer 0..10000 |
56
+ | `routes` | Ordered array of unique `{id, originEnvironmentId, environmentId, kind, enabled, evidence, authorProof?}` |
57
+
58
+ Each host's `fallbackLimitBp` is an integer 0..10000. Routes target configured
59
+ host environment IDs. `kind` is `local`, `remote` or `remote-author-coordinator`;
60
+ only `local` has equal origin/destination IDs. Multiple routes to one host are
61
+ allowed; the first proven route in configuration order represents that host.
62
+ Only routes whose origin matches the input origin are considered.
63
+
64
+ An `evidence` value is `{environmentId, pointer, observedAt}`. The environment
65
+ must match the fact's destination host; the pointer is an opaque nonempty
66
+ host-qualified reference and observation cannot be in the future. Evidence is
67
+ supplied provenance, never a permission grant. A capabilities map has arbitrary
68
+ names such as `browser:headed`, `worktree-isolation`, `session:sign-in`; each value
69
+ is `{value: true|false|null, evidence}`. Required capabilities must be true and
70
+ proven in both configuration and the destination snapshot.
71
+
72
+ Coordinator routes are exclusively for `author` requests; readers use local/remote
73
+ routes. Remote authors cannot use `remote` routes. A coordinator route is disabled
74
+ unless explicitly enabled with `authorProof`:
75
+ `{originEnvironmentId, environmentId, originVersions, destinationVersions,
76
+ evidence, gatesPassed: true}`. Both version objects contain exact nonempty
77
+ `{t3, axstack}` strings matching current origin/destination snapshots. This
78
+ records the driver's completed pilot gates; the selector cannot prove them.
79
+ Changed versions invalidate the route. Direct remote worktree delegation is
80
+ unsupported; a local author requires `worktree-isolation` automatically.
81
+
82
+ ## Device and browser examples
83
+
84
+ In account-local `taskOrders`, `device` and `playwright` are examples only;
85
+ the user supplies complete host orders in their reviewed configuration.
86
+ Capability names `device:android-emulator`, `browser:headless` and `desktop:user`
87
+ are examples only, using the existing arbitrary-name capability map.
88
+ A device request can require `device:android-emulator`; a headless web request
89
+ can require `browser:headless`, proven at the destination like any capability.
90
+ `desktop:user` describes desktop safety constraints, rather than a requirement
91
+ that would exclude workers; the driver checks it before executing UI work.
92
+ Host identities, preference orders, thermal thresholds and emulator availability
93
+ remain account-local facts, with current evidence collected before execution.
94
+ These examples add no runtime state, built-in task policy or capability inference.
95
+ Follow [UI verification](ui-verification.md#android-device-and-webpwa-dual-run)
96
+ for serial emulator use, desktop safety and declared thermal prechecks.
97
+
98
+ ## Recommendation snapshot
99
+
100
+ | Input field | Contract |
101
+ | --- | --- |
102
+ | `schemaVersion` | Exactly `1` |
103
+ | `configText`, `configDigest` | Exact bytes and run-pinned digest above |
104
+ | `origin` | `{environmentId, versions: {t3, axstack}}` |
105
+ | `request` | `{task, role, requiredCapabilities, repositoryId, providerInstanceId, model, options, runtimeMode}` |
106
+ | `destinations` | One destination snapshot per environment, described below |
107
+ | `routeFacts` | One current observation per route, described below |
108
+ | `telemetry` | At most one sample per environment; absent samples are unknown |
109
+ | `inventoryStatus` | `known` or `unknown` |
110
+ | `inventoryObservedAt` | Required nonfuture timestamp when known; null/absent allowed when unknown |
111
+ | `assignments` | Array of normalized `{assignmentId, environmentId, state}` |
112
+ | `pendingAssignments` | Array of `{clientRequestId, environmentId, taskId?}` |
113
+ | `exclusions` | `{hosts: {hostId: reason}, routes: {routeId: reason}}`; nonempty reason strings |
114
+
115
+ `role` is `reader` (all nonwriter roles) or `author`. `requiredCapabilities`
116
+ is an array of strings. An authentication requirement names the actual session
117
+ capability; only the host proving that session is eligible. No credential
118
+ transfer occurs. `options` is an object keyed by option ID. The adapter must
119
+ normalize T3 option arrays to this map without dropping requested settings.
120
+
121
+ A destination is `{snapshotId, environmentId, status, observedAt, evidence,
122
+ versions, project, execution, capabilities}`. Status must be `known` to be
123
+ eligible; unreadable/unknown facts exclude it. `project` is `{id, repositoryId}`.
124
+ `execution` is `{available: true, providerInstanceId, model, options, runtimeMode}`:
125
+ these are destination-verified facts, never the origin's model catalog. They
126
+ must match the request exactly (option object key order is immaterial).
127
+ Missing projects and mismatched repository/target settings have distinct reasons.
128
+ For dispatch-plan binding, also supply `capabilitiesDigest`: the lowercase
129
+ SHA-256 of the exact raw saved destination `orchestrator_capabilities` JSON
130
+ bytes. The selector carries this optional fact as `destinationCapabilitiesDigest`;
131
+ the adapter requires it. Collect the catalog on that destination/session, not
132
+ from the origin's catalog. Both current `options.values[]` (strings/booleans)
133
+ and legacy `options.options[].id` advertisements are supported.
134
+
135
+ A route observation is `{routeId, reachable, authorized, expiresAt,
136
+ destinationSnapshotId, evidence}`. Reachability and authorization must be true.
137
+ Remote link expiry must be a future epoch timestamp. The destination snapshot ID
138
+ must match the corresponding destination; mismatches exclude the route.
139
+ The driver refreshes access, project and capability observations before dispatch;
140
+ the selector imposes no invented lifetime on those facts. Load alone has the
141
+ 15-second freshness rule. Duplicate snapshot join keys fail schema validation.
142
+
143
+ ## Assignment normalization
144
+
145
+ The driver queries T3 and supplies origin-qualified IDs (e.g. `origin:task-id`)
146
+ and the environment actually doing the work. Remote receipts may have null
147
+ `childThreadId`; use `remoteChild.environmentId`, not the origin, to normalize
148
+ that assignment. The selector does not parse raw T3 receipts or query inventory.
149
+ A coordinator and its nested author must share one normalized assignment ID.
150
+
151
+ States accepted: `pending`, `queued`, `running`, `waiting`, `blocked`,
152
+ `completed`, `failed`, `cancelled`, `canceled`, `rejected`. The last five are
153
+ terminal and do not count. The adapter normalizes T3-native states to these.
154
+ Duplicate identical inventory IDs count once; conflicting identities/states
155
+ throw. Pending records deduplicate by client request ID and by returned
156
+ origin-qualified `taskId` matching `assignmentId`. Even terminal inventory
157
+ records suppress their reflected pending records. Pending IDs on different
158
+ hosts conflict. Update the existing pending record when T3 returns a task ID;
159
+ remove failed/not-submitted selections, retain ambiguous submissions until
160
+ reconciled. An unknown inventory contributes no stale counts; known pending
161
+ records still count. Counts report inventory status/time and pending provenance.
162
+ These are observations, never reservations or predicted resource costs.
163
+
164
+ ## Bounded collection (C2)
165
+
166
+ Collect with the installed helper (no bin/axstack.js subcommand):
167
+
168
+ ```bash
169
+ bun fleet-route.js sample --fleet manifest.json --config reviewed.json --config-digest <sha256> --json
170
+ bun fleet-route.js sample-recommend --fleet manifest.json --config reviewed.json \
171
+ --config-digest <sha256> --input facts.json --json
172
+ ```
173
+
174
+ `sample` emits `{schemaVersion:1,configDigest,collectedAt,telemetry}`; each sample
175
+ includes its ageMs at collectedAt.
176
+ `sample-recommend` emits `{schemaVersion:1,input,evaluatedAt,recommendation}`;
177
+ its complete `input` and explicit `evaluatedAt` replay through pure `recommend`.
178
+ It replaces saved telemetry, validates all supplied C1 facts before networking,
179
+ and uses the collector clock after collection. It neither refreshes route/project
180
+ facts nor enables a route. Both commands require a run-pinned digest; config
181
+ path defaults as above. They write stdout/stderr only. Sample reports are JSON
182
+ with or without `--json`; this flag is accepted for interface consistency.
183
+
184
+ The caller supplies a separate manifest, never private fleet defaults:
185
+
186
+ ```json
187
+ {"schemaVersion":1,"hosts":[
188
+ {"hostId":"worker-a","environmentId":"env-worker-a","transport":"local"},
189
+ {"hostId":"worker-b","environmentId":"env-worker-b","transport":"ssh","destination":"sample-worker","user":"fixture_user"}
190
+ ]}
191
+ ```
192
+
193
+ Only these keys are accepted. `hosts` has 1..64 entries, unique host IDs and
194
+ SSH destinations; each host/environment pair must match reviewed config.
195
+ At most one entry is local. Unlisted hosts have unknown telemetry. The caller
196
+ must bind the local identity to its collecting host; a manifest grants no T3
197
+ identity or execution authority. Local entries cannot carry SSH fields.
198
+ SSH destinations are lowercase MagicDNS short names (1..63 ASCII letters,
199
+ digits or interior hyphens, starting/ending alphanumeric, not all digits).
200
+ No aliases with punctuation, addresses, FQDNs, user@host, ports, options or
201
+ commands are accepted. Optional users match `[a-z_][a-z0-9_-]{0,31}`.
202
+ Existing SSH configuration/identities and known-host records must already exist;
203
+ no key paths, credentials, enrollment or interactive prompts are accepted.
204
+
205
+ `--interval-ms` defaults to 5000, integer range 5000..60000. All manifest hosts
206
+ start concurrently in one batch; each waits the interval after its first read
207
+ returns, then reads again. `--timeout-ms` defaults to 10000, integer range
208
+ interval+1000..120000, covering both reads, transport and waiting per host.
209
+ Long intervals require an explicit larger timeout. Timed-out/cancelled SSH
210
+ processes are killed and reaped, waits cancelled and listeners/timers removed.
211
+ SIGINT/SIGTERM cancel the owned collection and produce unknown telemetry.
212
+ SSH uses argv, noninteractive strict host-key checks and existing identities;
213
+ the only remote command is a constant read of /proc/stat and /proc/meminfo.
214
+ Output is capped at 64KiB per read; stderr details and credentials are not emitted.
215
+ No agent, daemon, schedule, dispatch or persistent collection state is created.
216
+
217
+ Linux CPU uses the delta of the first eight aggregate /proc/stat counters.
218
+ Busy includes steal and excludes idle/iowait; guest/guest_nice are already in
219
+ user/nice and are not added again. RAM uses MemTotal minus MemAvailable:
220
+ `ramBp` is the upward-rounded mean of the first and latest endpoint pressure,
221
+ `ramLatestBp` is latest pressure. This is an endpoint mean, not continuous RAM
222
+ history. Missing/duplicate/malformed fields, counter regression, zero elapsed
223
+ CPU ticks or changed MemTotal produce unknown, without partial pressure values.
224
+
225
+ Each collected sample contains host/environment IDs, status, source
226
+ `linux-proc-local` or `linux-proc-ssh`, `scope:whole-host`, intervalMs, timeoutMs,
227
+ durationMs, windowStartedAt and receivedAt. Success additionally contains
228
+ windowEndedAt, cpuBp, ramBp and ramLatestBp. `windowKind:collector-bracket`
229
+ means the collector-clock window brackets both reads, transport and the wait;
230
+ remote counter read times cannot be located more precisely within that bracket.
231
+ `durationMs` uses monotonic elapsed time (integer milliseconds), not a claimed
232
+ native history length. Both sources read whole-host Linux counters, never T3
233
+ process metrics or the cached 200ms delta. No fresh 30-second history is claimed.
234
+ Long windows retain their actual duration. All wall timestamps come from the
235
+ collecting host; remote clocks are neither read nor used for age.
236
+
237
+ A backwards collector clock, shortened window or forward jump over one second
238
+ relative to monotonic elapsed time produces collector-clock unknown. Per-host
239
+ statuses timeout, cancelled, malformed, inconsistent-counters, collector-clock
240
+ and failed become C1 unknown reasons. Collection failure does not fabricate an
241
+ access failure or load-based hold. C1 preserves source/window/duration and age
242
+ for unknown telemetry where supplied. Offline historical C1 inputs may omit
243
+ window timestamps and interval; when present, start/end must enclose duration,
244
+ end cannot follow receipt, and interval must be 5000..60000 and <=duration.
245
+ Age is solely evaluation time minus collector receipt, including stale/future
246
+ unknown results. Re-run before dispatch; snapshots reserve no capacity.
247
+
248
+ CPU/RAM basis points must be integers 0..10000. Instead of either `cpuBp` or
249
+ `ramBp`, supply `cpuFraction` or `ramFraction` (finite 0..1). Exactly one
250
+ representation per metric is allowed. Fractions round upward using their
251
+ shortest decimal representation, avoiding binary multiplication overshoot.
252
+ `ramLatestBp`, when supplied, also participates in the maximum pressure so an
253
+ average cannot hide recent RAM pressure. Load is the max of the supplied CPU
254
+ and RAM pressure; no role costs, GPU or personal-use gates exist.
255
+
256
+ Missing, malformed, short-window, non-whole-host, failed and timed-out samples
257
+ become unknown, as do ages >15000ms or <0. Age 15000ms remains measured. Unknown
258
+ load does not exclude an otherwise eligible route. Additional collector fields
259
+ (e.g. remote wall clock or detailed counter provenance) do not override the
260
+ collector receipt timestamp or validated pressure fields.
261
+
262
+ ## Result and downstream binding
263
+
264
+ The result has `schemaVersion`, `configDigest`, `evaluatedAt`,
265
+ `originEnvironmentId`, `request`, `stage`, `reason`, `decisionReason`, `selected`,
266
+ `flags`, `counts`, `eligible`, `excluded`. Stage is exactly `balance`,
267
+ `pool-unknown`, `fallback` or `no-route`. No-route has `selected: null`.
268
+ `reason: author-local-only` explicitly exposes a local-author limitation;
269
+ `decisionReason` retains the underlying load choice.
270
+
271
+ `selected` and each eligible host contain `hostId`, `environmentId`, `projectId`,
272
+ `repositoryId`, `routeId`, `routeKind`, `destinationSnapshotId`,
273
+ `destinationObservedAt`, `destinationEvidence`, `routeEvidence`, `versions`,
274
+ `execution`, `capabilities`, `fallbackLimitBp`, `assignmentCount`, `telemetry`.
275
+ Measured telemetry includes age, CPU/RAM, maximum pressure, source and window;
276
+ unknown telemetry carries its reason and age when available.
277
+
278
+ `counts.byEnvironment` maps configured environments to observed distinct active
279
+ assignment counts. Flags expose `countsUnknown`, `poolEmpty`,
280
+ `poolPartlyUnknown`, `telemetryUnknown`, `overloaded`, `localAuthorOnly`.
281
+ Excluded entries include host/route identity, reason codes, explicit exclusion
282
+ text and available configured/observed evidence. Failed alternative routes may
283
+ appear even when another route on that host is eligible.
284
+
285
+ ### Dispatch-plan adapter
286
+
287
+ Add `--recommendation recommendation.json --fleet-input snapshot.json
288
+ --now 100000 --config-digest <run-pin>` to the existing dispatch-plan flags.
289
+ All four are required together. `--capabilities` names the raw destination
290
+ catalog. The complete selector input includes config bytes; retain the CLI's
291
+ saved input plus the reviewed `configText` if the CLI supplied it from a file.
292
+ Recompute using the same explicit time and task before binding. The adapter
293
+ rejects any recommendation disagreement, missing/mismatched catalog digest,
294
+ unavailable destination model/options, or role/settings/config disagreement.
295
+ It never reinterprets a requested model pin as a different available model.
296
+
297
+ Task plans add `environmentId`/`projectId` to `target`; launch plans add them
298
+ beside `modelSelection`. All requested options survive as T3 option entries.
299
+ Both include `fleetBinding: {configDigest, destinationSnapshotId,
300
+ destinationCapabilitiesDigest}`. This is preserved evidence, not a T3 launch
301
+ parameter. Copy only tool-supported launch fields. Coordinator author routes
302
+ remain explicitly unsupported pending C3 lifecycle enablement.
303
+
304
+ For `--verify`, supply the effective configuration plus independently observed
305
+ `environmentId`, `projectId` and the preserved `fleetBinding` in one JSON
306
+ envelope. All are compared; missing values hold. Do not synthesize observed
307
+ fields from the plan. C3 owns live evidence collection and remote receipts;
308
+ C1b verifies supplied evidence only and cannot authenticate its origin.
309
+ Fleet binding mismatches exit 2; malformed CLI/files exit 1. Existing local
310
+ planning without these flags retains its interface.
311
+
312
+ A recommendation never launches,
313
+ provisions, mutates links, retries submission or grants authority. Call afresh
314
+ before each dispatch; assignment/load snapshots remain subject to races.
@@ -31,6 +31,11 @@
31
31
  review round and bounded research, spec, ticket, receipt, and release claims.
32
32
  - `axstack-ui-verifier`: owns the inline page interaction pass under [UI checks](ui-verification.md)
33
33
  for every interactive page, bound to the exact bytes and SHA-256.
34
+ - `axstack-ui-verifier-sonnet`: `claude/sonnet` high in mixed, read-only;
35
+ independently drives Android and web/PWA run 2 under [UI checks](ui-verification.md#android-device-and-webpwa-dual-run).
36
+ `axstack-ui-verifier` (Terra high in mixed) drives run 1; the driver reconciles.
37
+ `axstack-ui-verifier-sonnet` is intentionally absent in both single-provider presets,
38
+ leaving dual-run verification unverified; record the absence without substitution.
34
39
  - `axstack-research-requirements`/
35
40
  `axstack-research-code`/`axstack-research-web`/
36
41
  `axstack-explore-execution`:
@@ -29,6 +29,8 @@ using saved T3 capabilities JSON; missing or malformed capabilities holds.
29
29
  Claude exact IDs come from capabilities, replacing transcript read-back.
30
30
  Use an explicit model as given; for `model:null` without a class, grok uses the
31
31
  provider's first listed model from saved capabilities and records its exact ID.
32
+ A first listed ID without the `grok-` prefix holds that role with no substitution;
33
+ the prefix checks family, not X access or other live capability.
32
34
  A Codex or Claude role with neither model nor class is an intentional absence
33
35
  and holds.
34
36
  Resume must reuse the saved capabilities and role snapshot with no re-resolution; changes require the user’s explicit decision.
@@ -76,7 +76,7 @@ configuration; unrelated configurations remain eligible.
76
76
  | Roles | Mechanism and permitted workspace | Completion |
77
77
  |---|---|---|
78
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 |
79
+ | Reviewers (peer/authored), release checks, debug investigators, execution investigators (`axstack-explore-execution`), UI verifier candidate checks (`axstack-ui-verifier`, `axstack-ui-verifier-sonnet`) | 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
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
 
@@ -2,8 +2,11 @@
2
2
 
3
3
  Every Playwright, browser, or rendered-UI check outside the two named browser
4
4
  exceptions, including a "confirm it in the browser" step, goes through async
5
- `delegate_task` to `axstack-ui-verifier` from the run's role snapshot. Give it the exact build, URL, or artifact and the private
6
- dispatch's evidence folder. The verifier is read-only: it never edits source.
5
+ `delegate_task` to a configured UI verifier role.
6
+ The configured UI verifier roles are `axstack-ui-verifier` and
7
+ `axstack-ui-verifier-sonnet` from the role snapshot; select them under the
8
+ [dual-run trigger](#android-device-and-webpwa-dual-run) below.
9
+ Give each verifier the exact build, URL, or artifact and the private dispatch's evidence folder. The verifier is read-only: it never edits source.
7
10
  The PR writer remains the sole writer.
8
11
 
9
12
  Before using a PR preview URL, the UI verifier must read [PR previews](preview.md).
@@ -17,12 +20,12 @@ The second named browser exception permits dispatched roles to use
17
20
  `html_preview` as a non-publishing self-check.
18
21
  Dispatched-role self-checks never replace the driver's two previews or the `axstack-ui-verifier` pass.
19
22
  Dispatched roles never call `html_render`.
20
- All other browser checks outside the two named exceptions remain delegated to `axstack-ui-verifier`.
23
+ All other browser checks outside the two named exceptions remain delegated to configured UI verifier roles under the dual-run trigger below.
21
24
 
22
25
  Read the [T3 runtime boundary](t3-runtime.md) before dispatch; for candidate
23
26
  checks use its driver-made disposable detached checkout at the pinned candidate SHA.
24
27
  Use the configured verifier browser and honor the user browser-tool policy.
25
- Candidate browser and visual checks must run in the delegated `axstack-ui-verifier` in its own detached checkout; outputs go to its private evidence folder, never the driver worktree.
28
+ Candidate browser and visual checks must run in a delegated UI verifier role in its own detached checkout; outputs go to its private evidence folder, never the driver worktree.
26
29
 
27
30
  For every interactive page, require a real-browser `axstack-ui-verifier` interaction pass.
28
31
  Give the evidence path and SHA-256 of the exact bytes in the verifier brief.
@@ -47,6 +50,66 @@ Report a broken or stale recipe as a `low` finding naming `axstack-verify mainta
47
50
  Never let a broken or stale recipe waive existing acceptance obligations.
48
51
  Keep browser recipes tool-neutral for the verifier.
49
52
 
53
+ ## Android device and web/PWA dual run
54
+
55
+ Apply the dual run to Android device verification or explicitly requested
56
+ PWA/app feature verification, such as a `verify-<app>` device recipe or a user request.
57
+ Ordinary review and watch browser checks use one `axstack-ui-verifier` pass by default.
58
+ For these Android and web/PWA runs, assign run 1 to
59
+ `axstack-ui-verifier` (Terra high).
60
+ Dispatch independent run 2 to `axstack-ui-verifier-sonnet` with the same brief
61
+ and the same pinned build.
62
+ Never show run 2 the run 1 results.
63
+ Start run 2 only after run 1 completes shutdown; use a fresh delegated session
64
+ and a separate private evidence folder.
65
+ The two-provider check requires the mixed preset; an intentional single-provider
66
+ absence leaves dual-run verification unverified, without substitution.
67
+ This dual run does not add a second pass to the inline-page exception above.
68
+
69
+ Before run 1, record APK SHA-256 (or build SHA) and PWA deploy SHA in the brief.
70
+ Include feature files, prerequisites, target URL and per-feature acceptance checks.
71
+ Select the host through fleet routing using account-local configuration and
72
+ [capability examples](fleet-route-schema.md#device-and-browser-examples).
73
+ When `desktop:user` status is missing or unknown, treat the host as a user desktop.
74
+ On a `desktop:user` host, including that safe default, use only a headless browser and a windowless emulator.
75
+ Never focus windows or switch workspaces on a user desktop.
76
+ Allow only one emulator per host at a time.
77
+ Run a thermal precheck before starting on hosts that declare one; honor their
78
+ account-local start/stop thresholds and hold or route elsewhere if unsafe.
79
+ For iOS without a capable host, report unverified.
80
+
81
+ Each run follows the same lifecycle:
82
+
83
+ 1. Call `device_list` and confirm `agentDeviceInstalled: true` before `device_open`.
84
+ Pass the selected device's `hostId` and `id` to `device_open`.
85
+ 2. Use `device_open` and its returned `agent-device` command to install, launch
86
+ and drive the pinned APK; open the app before snapshots or input.
87
+ Use the returned command's help for supported operations; report unavailable
88
+ install/build capabilities as unverified rather than inventing commands.
89
+ 3. Save a snapshot and `device_screenshot` for each feature end state in private
90
+ evidence; capture actions, resulting states and side effects.
91
+ 4. For web/PWA, use Playwright MCP at 390×844 and 1440×900, driving the same features.
92
+ Capture console errors as route template and code only.
93
+ 5. Return pass, fail or blocked per feature, pinned build identities and evidence
94
+ paths; keep secrets, recovery phrases, tokens and addresses out of reports.
95
+ Follow the app's production-testing policy; unattended production money paths
96
+ stop at read-only actions.
97
+ 6. After each run, including failed attempts, use `device_close` with
98
+ `shutdown: true` and `browser_close` for owned sessions; preserve evidence.
99
+
100
+ The driver reconciles each feature:
101
+
102
+ - When both runs pass, the driver records pass.
103
+ - When both runs fail a feature, the driver records fail and routes it to `axstack-debug`.
104
+ - If runs disagree, the driver holds and requests one Sonnet rerun of that feature.
105
+ Use a fresh delegated session and a separate private evidence folder for the
106
+ Sonnet rerun, with the same brief and same pinned build.
107
+ Never show the Sonnet rerun run 1 or run 2 results.
108
+ If the rerun still disagrees with run 1, report flaky/unverified with all observations.
109
+ If the rerun agrees with run 1, the agreed result stands while the driver records
110
+ the initial disagreement in evidence; an agreed failure routes to `axstack-debug`.
111
+ - Blocked or missing coverage remains unverified, never a pass.
112
+
50
113
  ## Proof standards
51
114
 
52
115
  Apply these standards wherever referenced, including the verifier's candidate checks
@@ -1,6 +1,8 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { bindFleet } from './fleet-dispatch.js';
2
3
 
3
- const flags = ['--role', '--roles', '--capabilities', '--kind', '--verify', '--out'];
4
+ const fleetFlags = ['--recommendation', '--fleet-input', '--now', '--config-digest'];
5
+ const flags = ['--role', '--roles', '--capabilities', '--kind', '--verify', '--out', ...fleetFlags];
4
6
  const nonempty = (value) => typeof value === 'string' && value.trim().length > 0;
5
7
  const help = `Usage: bun dispatch-plan.js --role <id> --roles <roles.json>
6
8
  --capabilities <saved.json> --kind task|launch
@@ -13,6 +15,10 @@ Example: bun dispatch-plan.js --role axstack-author --roles roles.json --capabil
13
15
  --kind task emits target and, for axstack-author, isolation:"worktree"; launch emits modelSelection
14
16
  --verify Compare t3_thread_configuration JSON; print evidence then MATCH or each mismatch
15
17
  --out Save the payload as full JSON here; stdout is an input/path/omitted-count summary
18
+ --recommendation Saved fleet-route recommendation; requires all following fleet flags
19
+ --fleet-input Saved complete selector input, including exact reviewed config bytes
20
+ --now Injected nonnegative epoch-ms evaluation time matching the recommendation
21
+ --config-digest Run-pinned config SHA-256; must match facts and recommendation
16
22
  --help Show all flags and exit codes
17
23
 
18
24
  Read-only inputs: reads supplied JSON, spawns adjacent resolve-models.js, writes stdout/stderr.
@@ -22,6 +28,11 @@ Large verification retains its exit code, prints mismatches and advises --out on
22
28
  An unwritable --out still prints verification mismatches and retains exit 2.
23
29
  An omitted provider label is unknown; exact instance/model/options/mode checks still apply.
24
30
  Never dispatches or changes snapshots. Reuse recorded exact model pins as given.
31
+ Fleet --capabilities is the selected destination's raw saved catalog, digest-bound
32
+ by its destination facts. It is never the origin catalog by default.
33
+ Fleet verification requires environmentId/projectId and fleetBinding alongside
34
+ the effective T3 configuration. C3 collects that host-qualified evidence envelope.
35
+ Coordinator routes remain unsupported here. A plan grants no launch authority.
25
36
  Exit codes: 0 ok, 1 error, 2 hold (including incomplete read-back).
26
37
  `;
27
38
 
@@ -39,9 +50,12 @@ function argumentsFrom(args) {
39
50
  if (flag in options) throw new Error(`duplicate argument ${flag}; pass it once`);
40
51
  options[flag] = value;
41
52
  }
42
- for (const flag of flags.filter((flag) => !['--verify', '--out'].includes(flag))) {
53
+ for (const flag of ['--role', '--roles', '--capabilities', '--kind']) {
43
54
  if (!options[flag]) throw new Error(`expected ${flag}; pass ${flag} <value>`);
44
55
  }
56
+ if (fleetFlags.some((flag) => options[flag])) {
57
+ for (const flag of fleetFlags) if (!options[flag]) throw new Error(`expected ${flag} for fleet binding`);
58
+ }
45
59
  if (!['task', 'launch'].includes(options['--kind'])) throw new Error('expected --kind task|launch; pass --kind task or --kind launch');
46
60
  return options;
47
61
  }
@@ -60,7 +74,7 @@ function normalizedOptions(options) {
60
74
  return JSON.stringify(entries.sort(([left], [right]) => left.localeCompare(right)));
61
75
  }
62
76
 
63
- function verify(configuration, provider, model, options) {
77
+ function verify(configuration, provider, model, options, fleet) {
64
78
  const mismatches = [];
65
79
  const selection = configuration?.modelSelection;
66
80
  const compare = (field, expected, observed, equal = expected === observed) => {
@@ -77,6 +91,10 @@ function verify(configuration, provider, model, options) {
77
91
  compare('options', options, selection?.options,
78
92
  normalizedOptions(options) === normalizedOptions(selection?.options));
79
93
  compare('runtimeMode', 'full-access', configuration?.runtimeMode);
94
+ if (fleet) {
95
+ for (const [field, expected] of Object.entries(fleet.destination)) compare(field, expected, configuration?.[field]);
96
+ for (const [field, expected] of Object.entries(fleet.fleetBinding)) compare(field, expected, configuration?.fleetBinding?.[field]);
97
+ }
80
98
  return { mismatches, ...(selection?.provider == null ? {
81
99
  providerEvidence: `provider: unknown; expected ${JSON.stringify(provider)}; observed missing; `
82
100
  + 'provider binding checked via instanceId and saved capabilities',
@@ -136,16 +154,25 @@ function plan(argv, io) {
136
154
  '--effort', role.thinkingOptionId, ...modelArgs], { stdout: 'pipe', stderr: 'pipe' });
137
155
  if (resolved.stderr.length) io.stderr(resolved.stderr.toString());
138
156
  if (resolved.exitCode !== 0) return resolved.exitCode === 1 ? 2 : 1;
139
- const { model, effortOption, providerInstanceId } = JSON.parse(resolved.stdout.toString());
140
- const options = effortOption ? [effortOption] : [];
157
+ const resolution = JSON.parse(resolved.stdout.toString());
158
+ const { model, effortOption, providerInstanceId } = resolution;
159
+ let fleet;
160
+ if (args['--recommendation']) {
161
+ if (!/^\d+$/.test(args['--now'])) throw new Error('evaluation-time');
162
+ fleet = bindFleet(readJSON(args['--fleet-input']), readJSON(args['--recommendation']),
163
+ args['--config-digest'], Number(args['--now']), readFileSync(args['--capabilities'], 'utf8'),
164
+ resolution, role.id === 'axstack-author');
165
+ }
166
+ const options = fleet?.options ?? (effortOption ? [effortOption] : []);
141
167
  if (args['--verify']) {
142
- const verification = verify(readJSON(args['--verify']), providerInstanceId, model, options);
168
+ const verification = verify(readJSON(args['--verify']), providerInstanceId, model, options, fleet);
143
169
  emit(verification);
144
170
  return verification.mismatches.length ? 2 : 0;
145
171
  }
146
172
  const selection = { [args['--kind'] === 'launch' ? 'instanceId' : 'providerInstanceId']: providerInstanceId,
147
- model, options };
173
+ model, options, ...(args['--kind'] === 'task' ? fleet?.destination : {}) };
148
174
  emit({ [args['--kind'] === 'launch' ? 'modelSelection' : 'target']: selection, runtimeMode: 'full-access',
175
+ ...(fleet ? { fleetBinding: fleet.fleetBinding, ...(args['--kind'] === 'launch' ? fleet.destination : {}) } : {}),
149
176
  ...(args['--kind'] === 'task' && role.id === 'axstack-author' ? { isolation: 'worktree' } : {}) });
150
177
  return 0;
151
178
  }