axstack 0.55.0 → 0.56.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/docs/concepts.md +3 -1
  2. package/docs/installation.md +5 -2
  3. package/docs/workflows.md +3 -1
  4. package/package.json +1 -1
  5. package/skills/axstack/references/fleet-dispatch.md +228 -0
  6. package/skills/axstack/references/fleet-route-schema.md +16 -0
  7. package/skills/axstack/references/fleet-verification.md +102 -0
  8. package/skills/axstack/references/lifecycle.md +19 -2
  9. package/skills/axstack/references/role-roster.md +6 -0
  10. package/skills/axstack/references/routing.md +12 -0
  11. package/skills/axstack/references/run-record.md +83 -0
  12. package/skills/axstack/references/t3-runtime.md +173 -6
  13. package/skills/axstack/references/workspace-hygiene.md +8 -0
  14. package/skills/axstack/scripts/dispatch-plan.js +19 -2
  15. package/skills/axstack/scripts/fleet-destination.js +78 -0
  16. package/skills/axstack/scripts/fleet-dispatch.js +6 -0
  17. package/skills/axstack/scripts/fleet-facts.js +12 -1
  18. package/skills/axstack/scripts/fleet-inventory.js +76 -0
  19. package/skills/axstack/scripts/fleet-native-receipt.js +35 -0
  20. package/skills/axstack/scripts/fleet-receipt.js +149 -0
  21. package/skills/axstack/scripts/fleet-route.js +4 -3
  22. package/skills/axstack-cleanup/SKILL.md +21 -0
  23. package/skills/axstack-implement/SKILL.md +12 -5
  24. package/skills/axstack-watch/SKILL.md +3 -0
  25. package/skills/axstack-watch/references/watch-runtime.md +18 -5
  26. package/skills/voa/SKILL.md +96 -0
  27. package/skills/voa/references/observations.md +79 -0
  28. package/skills/voa/references/recovery.md +94 -0
  29. package/skills/voa/references/verification.md +35 -0
  30. package/skills/voa/scripts/cleanup.js +39 -0
  31. package/skills/voa/scripts/evaluate.js +122 -0
  32. package/skills/voa/scripts/observations.js +68 -0
  33. package/skills/voa/scripts/records.js +48 -0
  34. package/skills/voa/scripts/voa.js +43 -0
  35. package/src/installer.js +3 -2
  36. package/src/instructions.js +11 -3
@@ -71,6 +71,125 @@ The first dispatch for each (provider, model, effort) must reach a running or
71
71
  completed run before its siblings launch. Rejection stops siblings of that
72
72
  configuration; unrelated configurations remain eligible.
73
73
 
74
+ ## Coordinating multiple drivers
75
+
76
+ The Coordinator owns the overall outcome and coordinates work across projects.
77
+ A Driver owns delivery within one project: implementation, review, merge and deployment.
78
+ A Worker performs a bounded task for a Driver, including authoring, research or review.
79
+ Parent describes a relationship, not a role name.
80
+
81
+ A logical Coordinator can coordinate multiple project-bound
82
+ drivers, each using T3 delegation for multiple scoped Workers. Reuse a suitable
83
+ current Coordinator. Suggest a new Coordinator only when useful.
84
+ Every top-level thread launch requires an explicit user request, including a
85
+ new Coordinator, project driver or launch fallback. On resume, preserve existing
86
+ explicit accepted authorization within its scope. This skill never grants
87
+ blanket launch permission. Missing launch authorization holds only the affected
88
+ launch. After a blocked launch, continue independent authorized work, subject
89
+ to its dependencies and scoped holds under [Autopilot](autopilot.md#advance-and-hold).
90
+
91
+ For cross-project work, use the correct project-bound driver, selecting the
92
+ projectId and workspaceStrategy in t3_thread_launch when an authorized launch
93
+ is needed. Reuse an existing appropriate driver after reconciling its ownership
94
+ and scope, and verify the resulting project/worktree binding before dispatch.
95
+ A local delegate_task cannot select another project. Never retry the same
96
+ invalid project request. A routing failure holds that route and its dependent
97
+ work, rather than the whole authorized run.
98
+
99
+ Record the logical coordinator link separately from native parentThreadId,
100
+ including when null. The Coordinator remains the logical role when T3 records project Drivers as
101
+ separate top-level threads with null parentThreadId; native ancestry does not
102
+ change the recorded coordination ownership. For each driver, record its
103
+ exact link, project, worktree, scope, owner and record pointer. Record acceptance
104
+ and completion handbacks, dependencies, held state and resume condition. An
105
+ acceptance handback binds the driver identity to accepted scope and authority;
106
+ completion handbacks carry exact revisions and verified receipt pointers under
107
+ [Lifecycle](lifecycle.md#receipts-bind-each-decision-to-evidence). Reconcile native
108
+ state before advancing dependencies, rather than accepting a message alone.
109
+
110
+ Each driver owns its own PR mutations and watches and is the sole writer of its
111
+ own run record. The Coordinator owns aggregate coordination and its aggregate record,
112
+ using driver handbacks rather than editing their records or taking over their PRs.
113
+ Never allow competing writers for a candidate or record across drivers.
114
+ Existing author exclusivity and accepted-transfer rules still apply.
115
+
116
+ This is an optional coordination shape: single-driver runs remain unchanged.
117
+ The Coordinator and Drivers have no default model role in presets. Existing model,
118
+ permission and remote-author restrictions remain in force, with no substitution
119
+ or remote-author enablement. Coordination adds no daemon, scheduler or runtime DB.
120
+
121
+ ## Waiting turns and diagnosis
122
+
123
+ Before ending a waiting turn, follow [Waiting-turn and closing records](run-record.md#waiting-turn-and-closing-records).
124
+ The Driver diagnoses stale reviews, failing checks and relevant upstream fixes
125
+ immediately before ending a waiting turn. Inspect the blocker itself and record
126
+ its cause or a specific unexplained blocker with `diagnosis: none` and an already
127
+ authorized read-only diagnostic next action. Timers never replace reasoning.
128
+ Diagnosing a blocker preserves downstream human, auth, release and serious-risk
129
+ holds; it grants no dismissal, approval or merge rights.
130
+
131
+ ## Bounded recovery boundary
132
+
133
+ Ordinary status and periodic status sweeps remain read-only. Recovery requires
134
+ an explicit user recovery invocation naming scope or an authorized schedule
135
+ bound to a Coordinator-designated and recorded existing recovery thread.
136
+ Agent messages never activate it. The accepted implementation follow-up binds
137
+ the bounded synthetic/owned-resource canary to expiry, rollback and exact-ID
138
+ cleanup. Run-scoped activation requires passing canary receipts plus recorded
139
+ recovery host/thread, schedule ID, scope, model/effort, finite budget, expiry and
140
+ cleanup owner. Record the schedule ID at creation and verify its binding before
141
+ any send. The already authorized bounded canary and run-scoped activation need
142
+ no repeat approval when those conditions pass. A persistent shared service
143
+ requires separate explicit user authorization. Reading these instructions alone
144
+ grants no new top-level launch or activation authority. Keep finite pass limits
145
+ shorter than the 30-minute cadence, included owners and coverage cursor bound;
146
+ no catch-up loop. VOA implementation/source ownership is separate.
147
+
148
+ Read the current owner record under [Waiting-turn and closing records](run-record.md#waiting-turn-and-closing-records).
149
+ Match exact owner host/environment/thread and accepted ownership, not title,
150
+ parentThreadId or age. Unknown ownership/history/queue/activity, incomplete
151
+ records or unavailable delivery/idempotency proof remain report-only.
152
+ Any active run or active/pending child work makes the owner active: skip it.
153
+ Delegated owners are status-only under their parent, with no continuation to
154
+ either parent or child. Failed/interrupted execution and completed work with
155
+ cleanup remaining are report-only; never relaunch or redo product work.
156
+ Held queues, pending requests, parked user-reply holds and unknown request/queue
157
+ state are report-only. Only the user resumes a held queue. Explicit stops,
158
+ pauses and snoozes skip recovery. These guards take precedence over diagnosis.
159
+ Credentials, session/pairing, permission and new-thread authorization requests
160
+ are report-only, with no diagnosis exception.
161
+
162
+ A diagnosis: none record is eligible only for an idle owner with a specific
163
+ unexplained non-human blocker and an already authorized read-only diagnostic
164
+ next action. This may coexist with a downstream forge-resolved user-merge,
165
+ npm-stage or human-review hold, including an auth-code PR. A human gate alone
166
+ or an already diagnosed human-only hold receives zero continuations. Diagnosis
167
+ never clears or answers that gate. The other eligible case is a named external
168
+ dependency proven cleared, with the owner's authorized next action revalidated.
169
+ Inspect the blocker itself; silence and a completed turn prove neither stall
170
+ nor completion. Verify native PR watch support for the exact external dependency
171
+ before promising its wake. Prefer native events. An owner wake suppresses
172
+ recovery only before the recorded nextEligibleCheck or while same-key wake
173
+ delivery is queued or running in the owner thread. An armed unfired watch or
174
+ schedule, or one unable to observe the named blocker, never suppresses eligible
175
+ recovery. Unsupported events permit checking only the named external condition
176
+ under the recorded canary and activation conditions.
177
+
178
+ Send one bounded continuation with its condition key to the exact existing
179
+ top-level owning Coordinator/Driver. Revalidate all guards, owner history,
180
+ queue and deduplication receipts immediately before delivery. Include structured
181
+ evidence references by URL/ID/SHA and a recovery summary, with the recorded next
182
+ action: “Revalidate; act only within your existing authorized next action.”
183
+ Exclude quoted third-party/tool/webhook text. A recovery continuation is never
184
+ a user reply, answer, approval or authorization. The receiving owner independently
185
+ revalidates evidence and retains its original authority and holds. Recovery
186
+ never answers pending questions or performs forge mutations, delegation,
187
+ thread creation/settlement/cancel/interrupt, settings changes, or other owners'
188
+ queue/watch/schedule edits. Its writes are bounded continuations, own-thread
189
+ receipts and only its separately authorized schedule/duplicate-message cleanup.
190
+ Cross-host sends remain report-only until installed C4 instructions and current
191
+ directed C5 write-link proof exist; never redirect or relay outside native T3.
192
+
74
193
  ## Role dispatch by permitted writes
75
194
 
76
195
  | Roles | Mechanism and permitted workspace | Completion |
@@ -183,6 +302,13 @@ After each delegated completion the driver must check its own `HEAD` and
183
302
  `git status --porcelain` against their pre-dispatch values: both stay unchanged,
184
303
  including untracked entries. Any change is a hold before advancing that work.
185
304
 
305
+ Native delegate completion owns the wake for `delegate_task` children. Never
306
+ status-check running delegate_task children on scheduled wakes. Never create a
307
+ schedule merely because delegate_task children exist. A lost completion wake
308
+ remains active/unknown in read-only status, rather than triggering polling,
309
+ silence-based cancellation or a replacement author. Native completion receipts
310
+ are reconciled on delivery, not sought through hourly status checks.
311
+
186
312
  ## Launch recovery, repairs and questions
187
313
 
188
314
  For a lost delegated dispatch or repair response, retry with the same stable
@@ -233,12 +359,13 @@ A replacement after terminal failure must use `a<n+1>`, a new branch and title;
233
359
  the failed attempt's branch is kept until salvage. Unknown liveness holds
234
360
  replacement; reconcile the old writer before admitting another.
235
361
 
236
- With an unsettled launched thread, the driver turn must end only while a bound
237
- `schedule_task` with `bindToCurrentThread:true` is armed at the default
238
- `everyMs:3600000` or for its 5-minute recovery cases in
362
+ The bound lifecycle rule below applies only to owned launched-fallback Writers
363
+ awaiting completion. With an unsettled launched thread, the driver turn must end only while a bound
364
+ `schedule_task` with `bindToCurrentThread:true` is armed at the 5-minute lifecycle
365
+ cadence `everyMs:300000` under
239
366
  [Chat-run watch runtime](../../axstack-watch/references/watch-runtime.md#chat-run-watch),
240
- with its ID recorded. Each wake must reconcile all unsettled runs, including a
241
- writer that died without sending; failed runs hold incomplete work. The watch
367
+ with its ID recorded. Each wake must reconcile unsettled launched-fallback Writer
368
+ runs, including a writer that died without sending; failed runs hold incomplete work. The watch
242
369
  inherits the driver model/workspace and adds no runtime of Axstack's own.
243
370
  T3 queues a scheduled wake even while the driver is busy, so copies pile up.
244
371
  After a provider error, server restart or Stop, T3 holds the queue: held
@@ -324,7 +451,7 @@ An open PR left unwatched by a user Stop never triggers the 5-minute recovery ca
324
451
  After T3 auto-settles a multi-PR driver on one PR merge, it re-arms remaining
325
452
  open members through the recovery steps before ending the turn.
326
453
  If rate-limited, use the 5-minute schedule and next-wake recovery above.
327
- The schedule still reconciles author and review tasks, readiness, and release.
454
+ The schedule reconciles delivered receipts, readiness and release, plus launched-fallback Writer runs.
328
455
  When a PR merges or closes, or its watch is torn down, call `unwatch_pull_request`
329
456
  and keep its link.
330
457
 
@@ -409,3 +536,43 @@ For own PRs, automatic merge is the default under the
409
536
  [watch predicate](../../axstack-watch/SKILL.md#5-state-readiness-precisely).
410
537
  The recorded owning watch thread merges; a missing or idle owner requires
411
538
  lifecycle reconciliation and explicit transfer first.
539
+
540
+ ## Fleet nonwriter boundary (C3a)
541
+
542
+ Before dispatch, the driver must capture capability, project, settings and access
543
+ through native forwarded APIs on the selected destination, following
544
+ [Fleet dispatch and receipts](fleet-dispatch.md). Origin catalogs and caller
545
+ assertions alone cannot establish destination readiness. Preserve exact catalog
546
+ bytes/digest, explicit observation/evaluation times, config digest and recommendation.
547
+
548
+ `t3_thread_configuration` is currently unavailable in the advertised tools.
549
+ Unavailable effective fields remain unknown; the driver must not invent an API,
550
+ readiness or effective settings. Use advertised dispatch/thread facts, keep requested
551
+ and effective values separate, and hold any required unobserved field.
552
+
553
+ For ambiguous submission, reconcile the exact clientRequestId/attempt and remote task
554
+ before any retry. Unknown liveness prohibits replacement. A confirmed not-submitted
555
+ failure may exclude that route and recompute from current facts; it never permits
556
+ model or permission substitution. No hidden SSH agent or new top-level thread is
557
+ an execution fallback; a new thread requires an explicit user request.
558
+
559
+ The driver must read task_status on the origin and terminal/thread facts on the
560
+ actual destination for the latest run. Pending children or an unanswered question
561
+ prevent success even after a completed turn. Read the native pending-question list
562
+ with `t3_pending_request` action `list` on that destination; the older
563
+ `t3_pending_request_list` spelling above is not an advertised callable tool.
564
+ A null local childThreadId, idle state, silence or old summary proves no completion.
565
+
566
+ The remote author coordinator remains DISABLED: C3b's all-refs-unchanged pilot
567
+ failed because of T3 checkpoint refs. Drivers must not enable or rerun the pilot
568
+ under C3a. Direct remote worktree authors remain ineligible on T3 v0.1.5;
569
+ local isolated authors remain the fallback even when overloaded. C4 personal
570
+ policy/Haoshoku changes and C6 deployment/live acceptance remain later phases.
571
+
572
+ Remote nonwriters use native `delegate_task` with exact selected target.environmentId,
573
+ projectId, providerInstanceId, model, target.options and runtimeMode. Remote reviewers
574
+ use T3 `isolation:"worktree"`; never make their child checkouts in the shell.
575
+ Use no raw agent CLI over SSH. SSH is restricted to telemetry and authorized
576
+ artifact/commit transport. Follow host-qualified archive/readback/retirement in
577
+ [Fleet dispatch and receipts](fleet-dispatch.md#retrieval-and-retirement).
578
+ Only the driver publishes, links, watches or merges PRs.
@@ -1,5 +1,13 @@
1
1
  # Workspace hygiene for driver-owned T3 runs
2
2
 
3
+ For remote nonwriters, apply [host-qualified retrieval and retirement](fleet-dispatch.md#retrieval-and-retirement).
4
+ Resolve task status on origin and thread/run/settlement on the actual destination.
5
+ Archive and read back exact evidence on origin before retiring only the owned
6
+ T3-created reviewer checkout. Bind host/environment/path and candidate/base SHA;
7
+ never use an origin filesystem path as a destination path. Unknown liveness,
8
+ ownership or user takeover preserves that resource. Validation precedes deletion
9
+ in separate sequential calls; preserve unknown ignored artifacts.
10
+
3
11
  This is a prompt contract, not a cleanup daemon. Follow [T3 runtime](t3-runtime.md)
4
12
  for native identities, terminal run evidence and schema. Dispatched workers never
5
13
  sweep or remove another session. A scheduled pass with recorded cleanup authority
@@ -1,4 +1,5 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { inspectFleetReceipt } from './fleet-receipt.js';
2
3
  import { bindFleet } from './fleet-dispatch.js';
3
4
 
4
5
  const fleetFlags = ['--recommendation', '--fleet-input', '--now', '--config-digest'];
@@ -32,6 +33,9 @@ Fleet --capabilities is the selected destination's raw saved catalog, digest-bou
32
33
  by its destination facts. It is never the origin catalog by default.
33
34
  Fleet verification requires environmentId/projectId and fleetBinding alongside
34
35
  the effective T3 configuration. C3 collects that host-qualified evidence envelope.
36
+ Optional input.nativeDestination validates saved native forwarded reads.
37
+ Optional --verify fleetAttempt/fleetReceipt validates host-qualified terminal facts;
38
+ --out retains per-term receipt evidence. Synthetic bindings cannot authenticate reads.
35
39
  Coordinator routes remain unsupported here. A plan grants no launch authority.
36
40
  Exit codes: 0 ok, 1 error, 2 hold (including incomplete read-back).
37
41
  `;
@@ -95,7 +99,15 @@ function verify(configuration, provider, model, options, fleet) {
95
99
  for (const [field, expected] of Object.entries(fleet.destination)) compare(field, expected, configuration?.[field]);
96
100
  for (const [field, expected] of Object.entries(fleet.fleetBinding)) compare(field, expected, configuration?.fleetBinding?.[field]);
97
101
  }
98
- return { mismatches, ...(selection?.provider == null ? {
102
+ let receiptTerms;
103
+ if (fleet && configuration?.fleetReceipt !== undefined) {
104
+ receiptTerms = inspectFleetReceipt(configuration.fleetReceipt, {
105
+ attempt: configuration.fleetAttempt, recommendation: fleet.recommendation,
106
+ recommendationJSON: fleet.recommendationJSON, now: fleet.now,
107
+ });
108
+ for (const term of receiptTerms) if (term.state !== 'pass') mismatches.push(`${term.code}: ${term.state}; evidence ${JSON.stringify(term.evidence)}`);
109
+ }
110
+ return { mismatches, ...(receiptTerms ? { receiptTerms } : {}), ...(selection?.provider == null ? {
99
111
  providerEvidence: `provider: unknown; expected ${JSON.stringify(provider)}; observed missing; `
100
112
  + 'provider binding checked via instanceId and saved capabilities',
101
113
  } : {}) };
@@ -159,9 +171,14 @@ function plan(argv, io) {
159
171
  let fleet;
160
172
  if (args['--recommendation']) {
161
173
  if (!/^\d+$/.test(args['--now'])) throw new Error('evaluation-time');
162
- fleet = bindFleet(readJSON(args['--fleet-input']), readJSON(args['--recommendation']),
174
+ const recommendationJSON = readFileSync(args['--recommendation'], 'utf8');
175
+ const recommendation = JSON.parse(recommendationJSON);
176
+ fleet = bindFleet(readJSON(args['--fleet-input']), recommendation,
163
177
  args['--config-digest'], Number(args['--now']), readFileSync(args['--capabilities'], 'utf8'),
164
178
  resolution, role.id === 'axstack-author');
179
+ fleet.recommendation = recommendation;
180
+ fleet.recommendationJSON = recommendationJSON;
181
+ fleet.now = Number(args['--now']);
165
182
  }
166
183
  const options = fleet?.options ?? (effortOption ? [effortOption] : []);
167
184
  if (args['--verify']) {
@@ -0,0 +1,78 @@
1
+ // Validate explicit saved native reads. These bindings do not authenticate input.
2
+ import { canonical, text, time } from './fleet-config.js';
3
+
4
+ export function inspectDestination(observation, recommendation, capabilitiesText, now) {
5
+ const terms = [];
6
+ const term = (code, pass, evidence, missing = evidence == null) => terms.push({ code,
7
+ state: pass ? 'pass' : missing ? 'unknown' : 'fail', evidence: evidence ?? null });
8
+ const s = recommendation.selected;
9
+ const o = observation ?? {};
10
+ const observed = (read, tool, environmentId, args) => read?.tool === tool
11
+ && read?.environmentId === environmentId && canonical(read?.args) === canonical(args)
12
+ && time(read?.observedAt) && read.observedAt <= now && text(read?.evidence);
13
+ for (const [name, tool, environmentId, args] of [
14
+ ['environment', 't3_environment_read', s.environmentId, {}],
15
+ ['project', 't3_project_read', s.environmentId, { projectId: s.projectId }],
16
+ ['capabilities', 'orchestrator_capabilities', s.environmentId, {}],
17
+ ['links', 't3_environment_links', recommendation.originEnvironmentId, {}],
18
+ ]) {
19
+ const read = o[name];
20
+ term('destination-observation', observed(read, tool, environmentId, args), read);
21
+ }
22
+ term('destination-identity', o.environment?.value?.environmentId === s.environmentId,
23
+ o.environment?.value?.environmentId);
24
+ term('destination-project', o.project?.value?.id === s.projectId && text(o.project?.value?.workspaceRoot),
25
+ o.project?.value);
26
+ const project = o.project?.value;
27
+ let listed;
28
+ if (o.projectList !== undefined) {
29
+ const pages = o.projectList;
30
+ const projects = [];
31
+ let cursor = 0;
32
+ let complete = Array.isArray(pages) && pages.length > 0;
33
+ if (complete) for (const page of pages) {
34
+ const args = page?.args;
35
+ const limit = args?.limit ?? 20;
36
+ const rows = page?.value?.projects;
37
+ const next = page?.value?.nextCursor;
38
+ complete &&= cursor !== null && observed(page, 't3_project_list', s.environmentId, {
39
+ ...(args?.cursor === undefined ? {} : { cursor }),
40
+ ...(args?.limit === undefined ? {} : { limit }),
41
+ }) && (args?.cursor ?? 0) === cursor && Number.isInteger(limit) && limit >= 1 && limit <= 100
42
+ && Array.isArray(rows) && rows.length <= limit
43
+ && (next === null || (next === cursor + limit && rows.length === limit));
44
+ if (Array.isArray(rows)) projects.push(...rows);
45
+ cursor = next;
46
+ }
47
+ listed = projects.find((p) => p?.id === s.projectId);
48
+ term('destination-project-list', complete && cursor === null
49
+ && projects.every((p) => text(p?.id)) && new Set(projects.map((p) => p?.id)).size === projects.length
50
+ && listed?.id === s.projectId && listed.id === project?.id && listed?.workspaceRoot === project?.workspaceRoot, pages);
51
+ }
52
+ // ProjectService.listPage resolves the cold identity cache; getById need not.
53
+ const identities = [project?.repositoryIdentity, listed?.repositoryIdentity].filter((v) => v != null);
54
+ term('destination-repository', identities.length > 0
55
+ && identities.every((v) => v.canonicalKey === s.repositoryId), identities,
56
+ identities.length === 0);
57
+ term('destination-catalog', o.capabilities?.valueText === capabilitiesText, o.capabilities?.valueText);
58
+ const catalog = JSON.parse(capabilitiesText);
59
+ const providers = catalog.providers?.filter((v) => v.providerInstanceId === recommendation.request.providerInstanceId);
60
+ const provider = providers?.length === 1 ? providers[0] : undefined;
61
+ const mode = recommendation.request.runtimeMode;
62
+ // T3 0.1.5 resolveRuntimeMode/RuntimePolicy preserve modes without restrictions.
63
+ // Limit omission semantics to the observed version and native drivers we checked.
64
+ const unrestricted = provider?.runtimeModes === undefined && o.environment?.value?.serverVersion === '0.1.5'
65
+ && ['codex', 'claudeAgent'].includes(provider?.driverKind);
66
+ const supported = unrestricted || (Array.isArray(provider?.runtimeModes) && provider.runtimeModes.includes(mode));
67
+ term('destination-execution', provider?.canRunChildTask === true && supported
68
+ && catalog.runtimeMode === mode && Array.isArray(provider?.constraints) && provider.constraints.length === 0,
69
+ { provider, runtimeMode: catalog.runtimeMode, serverVersion: o.environment?.value?.serverVersion },
70
+ providers?.length === 0 || provider?.canRunChildTask == null || catalog.runtimeMode == null
71
+ || provider?.constraints == null || (provider?.runtimeModes === undefined && !unrestricted));
72
+ const local = s.environmentId === recommendation.originEnvironmentId;
73
+ const link = o.links?.value?.links?.find((v) => v.environmentId === s.environmentId);
74
+ term('destination-access', o.links?.value?.environmentId === recommendation.originEnvironmentId
75
+ && (local || (link?.status === 'reachable' && link?.access === recommendation.request.runtimeMode
76
+ && Date.parse(link?.expiresAt) > now)), local ? o.links?.value : link);
77
+ return terms;
78
+ }
@@ -1,4 +1,5 @@
1
1
  // Bind supplied placement facts to a plan. No reads, dispatch or authority grant.
2
+ import { inspectDestination } from './fleet-destination.js';
2
3
  import { canonical, record, text } from './fleet-config.js';
3
4
  import { recommend } from './fleet-route.js';
4
5
 
@@ -16,6 +17,11 @@ export function bindFleet(input, saved, configDigest, now, capabilitiesText, res
16
17
  const digest = selected.destinationCapabilitiesDigest;
17
18
  if (typeof digest !== 'string' || !/^[a-f0-9]{64}$/.test(digest)) hold('capabilities-digest-missing');
18
19
  if (new Bun.CryptoHasher('sha256').update(capabilitiesText).digest('hex') !== digest) hold('capabilities-digest-mismatch');
20
+ if (input.nativeDestination !== undefined) {
21
+ const mismatch = inspectDestination(input.nativeDestination, current, capabilitiesText, now)
22
+ .find((term) => term.state !== 'pass');
23
+ if (mismatch) hold(mismatch.code);
24
+ }
19
25
  const request = current.request;
20
26
  const { model, providerInstanceId, effortOption } = resolved;
21
27
  if (request.providerInstanceId !== providerInstanceId || request.model !== model
@@ -1,10 +1,21 @@
1
+ import { nativeAssignments } from './fleet-inventory.js';
1
2
  import { canonical, evidence, fail, record, text, time } from './fleet-config.js';
2
3
 
3
4
  const terminal = new Set(['completed', 'failed', 'cancelled', 'canceled', 'rejected']);
4
5
  const states = new Set([...terminal, 'pending', 'queued', 'running', 'waiting', 'blocked']);
5
6
  const qualified = (value) => text(value) && /^.+:.+$/.test(value);
6
7
 
7
- export function assignmentCounts(input, hosts) {
8
+ export function assignmentCounts(input, hosts, now) {
9
+ if (input.nativeInventory !== undefined) {
10
+ const normalized = nativeAssignments(input.nativeInventory, hosts, now);
11
+ const counts = assignmentCounts(normalized, hosts);
12
+ return { ...counts, inventoryReasons: normalized.inventoryReasons,
13
+ inventoryEvidence: normalized.inventoryEvidence,
14
+ ...(normalized.inventoryStatus === 'unknown' ? {
15
+ pendingLowerBound: counts.byEnvironment,
16
+ byEnvironment: Object.fromEntries(hosts.map((h) => [h.environmentId, null])),
17
+ } : {}) };
18
+ }
8
19
  const byEnvironment = Object.fromEntries(hosts.map((h) => [h.environmentId, 0]));
9
20
  const inventory = new Map();
10
21
  const clients = new Map();
@@ -0,0 +1,76 @@
1
+ // Normalize saved native observations only. No discovery, reservations or dispatch.
2
+ import { canonical, text, time } from './fleet-config.js';
3
+
4
+ const active = new Set(['queued', 'running', 'waiting']);
5
+ const terminal = new Set(['completed', 'failed', 'cancelled', 'interrupted']);
6
+ // JSON tuple encoding avoids collisions between IDs containing punctuation.
7
+ const identity = (environmentId, taskId) => JSON.stringify([environmentId, taskId]);
8
+
9
+ export function nativeAssignments(snapshot, hosts, now) {
10
+ const reasons = new Set();
11
+ const unknown = (code) => reasons.add(code);
12
+ const environments = new Set(hosts.map((h) => h.environmentId));
13
+ const tasks = new Map();
14
+ const assignments = new Map();
15
+ const pending = new Map();
16
+ if (snapshot?.complete !== true || !time(snapshot?.observedAt) || !time(now) || snapshot.observedAt > now || !text(snapshot?.evidence)
17
+ || !Array.isArray(snapshot?.tasks) || !Array.isArray(snapshot?.pending)) unknown('inventory-incomplete');
18
+ for (const item of Array.isArray(snapshot?.tasks) ? snapshot.tasks : []) {
19
+ if (!text(item?.origin?.environmentId) || !text(item?.origin?.threadId) || !text(item?.task?.taskId)) {
20
+ unknown('origin-unknown'); continue;
21
+ }
22
+ const key = identity(item.origin.environmentId, item.task.taskId);
23
+ const previous = tasks.get(key);
24
+ if (previous && canonical(previous) !== canonical(item)) unknown('assignment-conflict');
25
+ tasks.set(key, item);
26
+ }
27
+ for (const [key, item] of tasks) {
28
+ const task = item.task;
29
+ const environmentId = task.remoteChild?.environmentId
30
+ ?? (text(task.childThreadId) ? item.origin.environmentId : null);
31
+ if (!text(environmentId)) { unknown('destination-unknown'); continue; }
32
+ if (!environments.has(environmentId)) { unknown('host-unmapped'); continue; }
33
+ if (task.remoteChild && !text(task.remoteChild.threadId)) { unknown('destination-unknown'); continue; }
34
+ if (!active.has(task.status) && !terminal.has(task.status)) { unknown('task-state-unknown'); continue; }
35
+ let rootKey = key;
36
+ if (item.assignmentRoot) {
37
+ rootKey = identity(item.assignmentRoot.environmentId, item.assignmentRoot.taskId);
38
+ const root = tasks.get(rootKey);
39
+ if (!root || root.assignmentRoot) { unknown('assignment-root-unknown'); continue; }
40
+ const rootEnvironment = root.task.remoteChild?.environmentId
41
+ ?? (text(root.task.childThreadId) ? root.origin.environmentId : null);
42
+ if (rootEnvironment !== environmentId) { unknown('assignment-conflict'); continue; }
43
+ }
44
+ const previous = assignments.get(rootKey);
45
+ const state = active.has(task.status) || task.hasPendingChildRuns === true
46
+ || task.workState === 'waiting_for_children' ? 'running' : 'completed';
47
+ // A terminal coordinator with live descendants remains active.
48
+ assignments.set(rootKey, { assignmentId: `task:${rootKey}`, environmentId,
49
+ state: previous?.state === 'running' ? 'running' : state });
50
+ }
51
+ for (const item of Array.isArray(snapshot?.pending) ? snapshot.pending : []) {
52
+ if (!text(item?.origin?.environmentId) || !text(item?.origin?.threadId) || !text(item?.clientRequestId)) {
53
+ unknown('origin-unknown'); continue;
54
+ }
55
+ if (!environments.has(item.environmentId)) { unknown('host-unmapped'); continue; }
56
+ const key = JSON.stringify([item.origin.environmentId, item.origin.threadId, item.clientRequestId]);
57
+ const value = { clientRequestId: key, environmentId: item.environmentId };
58
+ if (item.taskId !== undefined) {
59
+ if (!text(item.taskId)) { unknown('origin-unknown'); continue; }
60
+ const taskKey = identity(item.origin.environmentId, item.taskId);
61
+ const known = tasks.get(taskKey);
62
+ const rootKey = known?.assignmentRoot
63
+ ? identity(known.assignmentRoot.environmentId, known.assignmentRoot.taskId) : taskKey;
64
+ value.taskId = `task:${rootKey}`;
65
+ const reflected = assignments.get(rootKey);
66
+ if (reflected && reflected.environmentId !== item.environmentId) unknown('assignment-conflict');
67
+ }
68
+ const previous = pending.get(key);
69
+ if (previous && canonical(previous) !== canonical(value)) unknown('assignment-conflict');
70
+ pending.set(key, value);
71
+ }
72
+ return { inventoryStatus: reasons.size ? 'unknown' : 'known',
73
+ inventoryObservedAt: snapshot?.observedAt ?? null, inventoryReasons: [...reasons],
74
+ inventoryEvidence: snapshot?.evidence ?? null,
75
+ assignments: [...assignments.values()], pendingAssignments: [...pending.values()] };
76
+ }
@@ -0,0 +1,35 @@
1
+ // T3 0.1.5 remote status omits options and a result-run ID. Never synthesize them.
2
+ import { canonical, text, time } from './fleet-config.js';
3
+
4
+ export function inspectNativeFamily(receipt, expected) {
5
+ const r = receipt;
6
+ const n = r.nativeFamily;
7
+ const s = expected.recommendation.selected;
8
+ const a = expected.attempt;
9
+ const child = r.status?.value?.remoteChild?.threadId;
10
+ const current = r.thread?.value?.thread?.latestRunId;
11
+ const terms = [];
12
+ const term = (pass, evidence) => terms.push({ code: 'receipt-native-family',
13
+ state: pass ? 'pass' : evidence == null ? 'unknown' : 'fail', evidence: evidence ?? null });
14
+ term(n?.tool === 'orchestration.getThreadFamily' && n?.transport === 't3-native-rpc'
15
+ && n?.environmentId === s.environmentId && n?.serverVersion === '0.1.5'
16
+ && s.versions?.t3 === n.serverVersion && canonical(n?.args) === canonical({ threadId: child })
17
+ && time(n?.observedAt) && n.observedAt <= expected.now
18
+ && n.observedAt >= r.thread?.observedAt && n.observedAt >= r.terminal?.observedAt
19
+ && text(n?.evidence), n);
20
+ const rows = n?.value?.threads;
21
+ const matches = Array.isArray(rows) ? rows.filter((v) => v?.id === child) : [];
22
+ term(time(n?.value?.snapshotSequence) && Array.isArray(rows)
23
+ && rows.every((v) => text(v?.id)) && new Set(rows.map((v) => v.id)).size === rows.length
24
+ && matches.length === 1, n?.value);
25
+ const shell = matches[0];
26
+ term(shell?.id === child && shell?.projectId === s.projectId && shell?.title === a?.title
27
+ && shell?.latestRunId === current && text(current)
28
+ && shell?.delegatedFrom?.environmentId === a?.origin?.environmentId
29
+ && shell?.delegatedFrom?.threadId === a?.origin?.threadId, shell);
30
+ term(shell?.status === 'completed' && Array.isArray(rows)
31
+ && rows.every((v) => ['completed', 'idle'].includes(v?.status) && v?.activeRunId === null
32
+ && Array.isArray(v?.pendingBackgroundTasks) && v.pendingBackgroundTasks.length === 0
33
+ && v?.pendingRuntimeRequest === null && v?.pendingGuestMessageCount === 0), rows);
34
+ return { terms, shell };
35
+ }