axstack 0.56.0 → 0.57.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "axstack",
3
- "version": "0.56.0",
3
+ "version": "0.57.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",
@@ -5,7 +5,9 @@ and bounded follow-ups are defined in [Peer mode](../../axstack-review/SKILL.md#
5
5
 
6
6
  Read the [T3 runtime boundary](t3-runtime.md) before dispatch.
7
7
  Dispatch `axstack-diligence` with async `delegate_task` in the driver worktree,
8
- using a pinned brief and private evidence paths.
8
+ using a pinned brief and private evidence paths. Explicit non-repository
9
+ research/article diligence uses [Local artifacts](local-artifacts.md) with
10
+ local shared isolation and independently reopened sources, without a fake Git repo.
9
11
  It is read-only, never authors or edits, and returns `PASS`, `FINDINGS`, or `UNKNOWN`
10
12
  with locations, observed evidence, and limits. A stale or missing receipt is
11
13
  not a pass. Keep its first pass independent of other reviewers and workers.
@@ -7,6 +7,12 @@ records and forge actions. Helpers validate explicit saved inputs only: no
7
7
  network, clock, dispatch, retry, merge, retained state or aggregate permission grant.
8
8
  Synthetic evidence can pass binding checks; helpers cannot authenticate it.
9
9
 
10
+ For explicit local non-repository Research/Explain/Publish work, apply
11
+ [Local artifacts](local-artifacts.md): its discriminated scope, native read/list
12
+ null-identity proof and owned folders replace only the repository/checkout terms.
13
+ All native execution authority and independent exact-byte review remain required.
14
+ Engineering/code review and remote routes retain their existing rules.
15
+
10
16
  ## Observe before selecting
11
17
 
12
18
  1. Record installed Axstack and both T3 versions, directed origin/target link,
@@ -25,7 +31,7 @@ Synthetic evidence can pass binding checks; helpers cannot authenticate it.
25
31
  3. Keep the model/effort/provider pins from the run; do not re-resolve to another
26
32
  model when quota/auth/timeout fails. Missing native `t3_thread_configuration`
27
33
  means unavailable effective fields stay unknown. Catalog runtimeModes advertise
28
- support; they do not prove effective access. Native task/thread facts supply
34
+ provider restrictions; they do not prove effective access. Native task/thread facts supply
29
35
  only the fields actually returned. A worker assertion is not native readback.
30
36
  4. Normalize current driver assignments and the pending batch as below. Sample
31
37
  telemetry with C2's bounded command, then recompute C1 immediately before each
@@ -70,8 +76,28 @@ read envelope is `{tool,environmentId,args,observedAt,evidence,value}` where arg
70
76
  excludes the forwarding environmentId, and evidence points to the preserved raw
71
77
  native call/result. environment/project/capabilities are read on the selected
72
78
  host; links are read on origin. The capabilities envelope uses `valueText` for
73
- exact raw catalog bytes instead of value. Project repositoryIdentity.canonicalKey
74
- must match the requested repositoryId. Unknown mappings hold rather than guessing.
79
+ exact raw catalog bytes instead of value. For repository scope, project repositoryIdentity.canonicalKey
80
+ must match the requested repositoryId. If project-read identity is null/omitted,
81
+ `projectList` may carry an ordered array of `t3_project_list` read envelopes on
82
+ that same environment. Collect from cursor 0 through native nextCursor:null,
83
+ recording actual cursor/limit arguments (native defaults: 0/20, limit at most 100).
84
+ Every page must join without gaps, extra arguments or duplicate project IDs.
85
+ The selected project's ID and workspaceRoot must equal project-read; every
86
+ available canonicalKey must equal repositoryId, including when both reads have
87
+ identity. In repository scope, missing identity in both holds; local-artifact
88
+ scope instead requires explicit null in both reads. Incomplete pagination and
89
+ any conflict hold for either scope.
90
+
91
+ For observed T3 0.1.5 native Codex/Claude drivers, omitted provider runtimeModes
92
+ means no provider restriction: native resolveRuntimeMode and RuntimePolicy
93
+ preserve the requested mode. Bind this only with catalog runtimeMode equal to
94
+ the request, exactly one selected provider, canRunChildTask:true and an explicit
95
+ empty constraints array. Explicit mode restrictions must include the requested
96
+ mode. Unknown version/driver omission semantics, missing caller authority,
97
+ unavailable providers or restrictions hold. This is preflight support, never
98
+ proof of a child's effective mode; terminal receipts still require actual native
99
+ readback. Preserve the exact catalog bytes/digest; do not synthesize runtimeModes
100
+ or repositoryIdentity in the saved raw reads. Unknown mappings hold rather than guessing.
75
101
  The pure `inspectDestination` API returns per-term pass/fail/unknown with evidence;
76
102
  `dispatch-plan` holds on a failed/unknown term. This is supplied binding proof only.
77
103
 
@@ -80,9 +106,11 @@ recommendation, explicit evaluation time and config pin. Compare its target to t
80
106
  independently selected host/environment/project/model/options/runtimeMode. Copy only
81
107
  native supported fields, never inputs or fleetBinding. Call `delegate_task` with
82
108
  that exact target, full-access, a role-labelled title, stable clientRequestId and
83
- scope. Reviewers require native isolation:"worktree" and pinned candidate/base;
84
- no shell-created child checkout. Other nonwriters write only their named private
85
- evidence directory on that host. Preserve requested arguments and native response.
109
+ scope. Code reviewers require native isolation:"worktree" and pinned candidate/base;
110
+ no shell-created child checkout. Explicit local artifact reviewers instead use
111
+ shared isolation and SHA-256 snapshots under [Local artifacts](local-artifacts.md);
112
+ artifact writers use only their owned artifact/evidence folders. Other nonwriters
113
+ write only their named private evidence directory on that host. Preserve requested arguments and native response.
86
114
 
87
115
  Record taskId on the origin plus remoteChild.environmentId/threadId. The native
88
116
  remoteChild schema need not return runId: normalize it from observed task/thread
@@ -99,7 +127,9 @@ recommendation at the same model/permission pins; record the exclusion evidence.
99
127
 
100
128
  Preserve schemaVersion:1, recommendationJSON (exact bytes), configDigest,
101
129
  attempt (key, exact title, clientRequestId, origin environmentId/threadId/taskId,
102
- candidate/base SHAs), requested target/runtimeMode/isolation and native submission.
130
+ candidate/base SHAs for repositories, artifact SHA-256/null base for local artifacts),
131
+ requested target/runtimeMode/isolation and native submission. Artifact ownership
132
+ and byte readbacks follow [Local artifacts](local-artifacts.md#receipts).
103
133
  The driver's independent fleetAttempt is the expected run-record identity;
104
134
  never copy it from an arriving receipt to make a comparison pass.
105
135
 
@@ -119,6 +149,9 @@ submission.childRunId; never use lexical ordering of IDs or the original summary
119
149
  Require completed/result_available/no pending children/no unanswered questions or
120
150
  approval. Require exactly one AXSTACK-DONE key/candidate/report marker on both origin's
121
151
  latestTerminalSummary and the destination's final current-run agent message.
152
+ For local artifacts only, T3 0.1.5's deduplicated summary uses the exact
153
+ run/byte-bound exception in [Local artifacts](local-artifacts.md#receipts);
154
+ never rewrite raw native fields or extend it to remote/engineering receipts.
122
155
  A completion marker may be followed by the T3
123
156
  `PR <url>, head <sha>, checks and reviews <state>` trailer. Other trailing text
124
157
  or additional AXSTACK markers keep the receipt held.
@@ -131,7 +164,51 @@ For isolated review include checkout with those fields, candidate/base and readb
131
164
  Never infer local file existence from a remote absolute path.
132
165
  Requested settings remain separate from effective native status.providerInstanceId,
133
166
  model/options and thread.providerInstanceId/model/runtimeMode. Missing effective
134
- options or mode is unknown and holds required verification.
167
+ options or mode is unknown and holds required verification, except for the bounded
168
+ T3 0.1.5 initial remote-launch join below.
169
+
170
+ ### T3 0.1.5 initial remote receipt
171
+
172
+ The remote `readRemoteTask` emitter omits options and returns null childRunId and
173
+ latestTerminalRunId. Preserve those raw omissions/nulls. A current family shell
174
+ is current thread metadata, not an immutable settings record for an older run.
175
+ Never use it to authorize a continuation from an origin summary lacking a run ID.
176
+
177
+ For the sole initial remote run, optional receipt `nativeFamily` can corroborate
178
+ settings and run identity. The driver acquires `orchestration.getThreadFamily`
179
+ with exact `{threadId}` on the actual destination using an authenticated native
180
+ RPC read with `orchestration:read` scope. This is metadata access, not agent
181
+ execution or an alternate worker launch path. Record host/environment identity,
182
+ authenticated acquisition provenance, exact request/result, and independently
183
+ observed server version. Do not execute an agent through SSH/CLI to obtain it.
184
+ Temporary read sessions require their authorized lifecycle and revocation; never
185
+ put tokens/tickets into evidence. This contract grants no auth/session authority.
186
+
187
+ The envelope is `{tool:"orchestration.getThreadFamily",transport:"t3-native-rpc",
188
+ environmentId,serverVersion:"0.1.5",args:{threadId},observedAt,evidence,value}`.
189
+ `value` is the unmodified native `{snapshotSequence,threads}` response. Retain
190
+ raw bytes separately, including any diagnostic/revocation trailer; parse only
191
+ the JSON response into a separate file. Never rewrite the raw response as JSON.
192
+ The version must also match selected destination versions; observe the family
193
+ after thread/wait and no later than receipt evaluation. Missing provenance holds.
194
+
195
+ Require a unique child with the selected project, exact title/current run,
196
+ provider/model/options/mode and delegatedFrom origin environment/thread. Every
197
+ returned family member must explicitly have idle/completed status, null active
198
+ run/request, zero pending guest input and an empty pendingBackgroundTasks array.
199
+ Unknown or live descendants hold even when remote status claims no pending child.
200
+
201
+ The fallback requires native thread runCount:1, exactly one recentRuns row at
202
+ ordinal:1, null local child IDs/childRunIds on status/submission, null origin
203
+ latestTerminalRunId, and identical origin/destination final text. Existing
204
+ current-run, completed terminal, marker, question and host/path checks still
205
+ apply. Explicit status options or run IDs cannot be overwritten by family facts.
206
+ T3 remote launch forwards the accepted explicit model selection and uses it for
207
+ both thread creation and the initial message; this is the source-backed scope
208
+ of the join. More runs or missing counts/ordinals hold. Later continuations need
209
+ actual result-run/per-run settings provenance; repeated matching markers or
210
+ current family settings are insufficient. Complete native statuses retain their
211
+ existing run-ID path. Saved inputs cannot authenticate their own provenance.
135
212
 
136
213
  `inspectFleetReceipt(receipt,{attempt,recommendation,recommendationJSON,now})`
137
214
  returns terms, never authority. Existing dispatch-plan --verify can additionally
@@ -112,6 +112,13 @@ for serial emulator use, desktop safety and declared thermal prechecks.
112
112
  | `pendingAssignments` | Array of `{clientRequestId, environmentId, taskId?}` |
113
113
  | `exclusions` | `{hosts: {hostId: reason}, routes: {routeId: reason}}`; nonempty reason strings |
114
114
 
115
+ `request.scope` may be `{kind:"repository"}` with a nonempty repositoryId;
116
+ legacy omitted scope retains that repository requirement. The explicit
117
+ `{kind:"local-artifact", ...}` alternative requires repositoryId:null and the
118
+ workflow, role, native project and ownership fields in [Local artifacts](local-artifacts.md).
119
+ Artifact destination project also carries workspaceRoot; only the bound local
120
+ environment/project/workspace is eligible. Null alone never selects a route.
121
+
115
122
  `role` is `reader` (all nonwriter roles) or `author`. `requiredCapabilities`
116
123
  is an array of strings. An authentication requirement names the actual session
117
124
  capability; only the host proving that session is eligible. No credential
@@ -168,6 +175,11 @@ Optional `nativeInventory` is normalized before counts under
168
175
  Unknown native totals are null; balance ranking uses the separately labelled
169
176
  pendingLowerBound. Counts also retain inventoryReasons/evidence; flags.countsUnknown follows normalized status.
170
177
  Optional `nativeDestination` carries observed native forwarded preflight reads.
178
+ Optional `nativeDestination.projectList` is the complete ordered native project-list
179
+ page envelopes used to join a cold project-read repository identity; preserve
180
+ raw reads and catalogs. Conflicting identities/workspaces or incomplete pages hold.
181
+ T3 0.1.5 native provider mode omissions require the documented caller/provider
182
+ authority evidence, never synthesized catalog fields.
171
183
  The same reference defines terminal receipt validation and its supplied-evidence
172
184
  limitation. Live drivers require these observed facts, not synthetic caller claims.
173
185
  Legacy normalized input remains unchanged. Remote coordinator remains disabled.
@@ -306,7 +318,10 @@ unavailable destination model/options, or role/settings/config disagreement.
306
318
  It never reinterprets a requested model pin as a different available model.
307
319
 
308
320
  Task plans add `environmentId`/`projectId` to `target`; launch plans add them
309
- beside `modelSelection`. All requested options survive as T3 option entries.
321
+ beside `modelSelection`. All requested options survive as T3 option entries. Explicit local-artifact
322
+ plans require nativeDestination, the exact scope role and task kind; they emit
323
+ isolation:"shared" and artifactScope evidence. The latter is never a native
324
+ argument. Artifact readback compares scope and shared isolation as well.
310
325
  Both include `fleetBinding: {configDigest, destinationSnapshotId,
311
326
  destinationCapabilitiesDigest}`. This is preserved evidence, not a T3 launch
312
327
  parameter. Copy only tool-supported launch fields. Coordinator author routes
@@ -29,6 +29,12 @@ Prefer parallel independent bounded work; no redundant workers. Fanout is depend
29
29
  capacity-driven within host/spending limits. [PR shape](pr-shape.md) covers
30
30
  theme/size; queue dependencies under [PR chains](pr-chain.md).
31
31
 
32
+ For explicit non-repository Research/Explain/Publish artifacts, use
33
+ [Local artifacts](local-artifacts.md) for owned shared delegates and SHA-256
34
+ receipts. The reviewer checkout row above applies to code candidates; article
35
+ review retains independent source/fact and exact-byte UI/design checks.
36
+ Null identity does not waive engineering isolation or authorize remote artifacts.
37
+
32
38
  ## Ownership
33
39
 
34
40
  The PR owner is accountable for candidate, fixes, evidence, monitoring.
@@ -0,0 +1,100 @@
1
+ # Local non-repository artifacts
2
+
3
+ Research, Explain and Publish may use an explicitly bound non-repository
4
+ artifact scope. Null repository identity alone never authorizes a route.
5
+ Engineering authors and code reviewers still require a repository and native
6
+ Git worktree isolation. Remote artifacts and remote author/coordinator work
7
+ remain outside this scope.
8
+
9
+ ## Scope and ownership
10
+
11
+ Use `request.scope` below with `repositoryId:null` and `role:reader`.
12
+ Here reader means no product-code authority; an explainer may write only its
13
+ owned article artifacts. `task` remains a load preference, not role authority.
14
+ The scope is evidence for the helpers and brief, never a native T3 argument.
15
+
16
+ ```json
17
+ {
18
+ "kind": "local-artifact",
19
+ "workflow": "explain",
20
+ "roleId": "axstack-explainer",
21
+ "environmentId": "<current environment>",
22
+ "projectId": "<actual parent project>",
23
+ "workspaceRoot": "/scratch",
24
+ "artifactRoot": "/scratch/run/artifacts/author-a1",
25
+ "evidenceRoot": "/scratch/run/evidence/author-a1",
26
+ "reservedRoots": ["/scratch/run/artifacts/other-attempt"]
27
+ }
28
+ ```
29
+
30
+ `workflow` is research, explain or publish. All three permit the configured
31
+ research requirements/code/code-sol/web/web-sol/X and explore-execution/
32
+ explore-execution-sol source roles plus diligence. Explain and Publish also
33
+ permit explainer, explainer-review and the configured UI verifier roles.
34
+ Use their full `axstack-` role IDs; this list does not make an absent seat
35
+ available or change provider/model/effort/options/runtime authority.
36
+ The selected role ID must equal scope.roleId in dispatch-plan.
37
+ No engineering author, code reviewer, owner or arbitrary role can use this scope.
38
+
39
+ The driver binds the native parent project and its real workspaceRoot. Require
40
+ explicit null repositoryIdentity in both project-read and complete project-list
41
+ reads; omitted, conflicting or incomplete observations hold. Native environment,
42
+ project, catalog, provider restrictions, options, runtime and access checks still
43
+ apply. Use only a local route in that environment/project/workspace. Do not
44
+ fabricate a repository ID, initialize Git or attach an unrelated repository.
45
+
46
+ Give every attempt separate owned artifact and evidence directories, strict
47
+ children of the observed workspaceRoot. They must be canonical absolute paths,
48
+ with no traversal, shared-root writes or ancestor/equal overlap. `reservedRoots`
49
+ is the driver's current list of other attempts' owned artifact/evidence roots;
50
+ include pending and retained attempts, never guess an empty inventory.
51
+ The helper checks lexical paths and overlaps; the driver must independently
52
+ verify real paths, no symlink escapes, ownership and exclusive writers on disk.
53
+ Unknown ownership holds. This is supplied evidence, not an authenticated lock.
54
+
55
+ ## Native dispatch and review
56
+
57
+ Use async native `delegate_task` with `isolation:"shared"`, exact local target,
58
+ role-labelled title and stable clientRequestId. Native local projectId must equal
59
+ the parent's project; moving to another local project is unsupported. Preserve
60
+ requested arguments and native effective settings separately. Dispatch-plan
61
+ requires nativeDestination and `--kind task`; it returns artifactScope as evidence
62
+ and shared isolation. Copy only supported native arguments into delegate_task.
63
+ No top-level launch, shell checkout, live policy edit or per-run bypass is needed.
64
+
65
+ Research branches keep independent first passes and cited-source reopening.
66
+ Diligence reopens answer-changing claims; it never becomes the article author.
67
+ The explainer remains sole source writer. Reviewers/verifiers get their own
68
+ byte-identical snapshot in their owned artifact directory; their reports/probes
69
+ stay in their separate evidence directory and never edit the author's original.
70
+ Independent explainer review checks the exact SHA-256 bytes. Before public
71
+ publication require Playwright layout/interaction evidence and independent
72
+ source/fact and design review of those same bytes under Explain/Publish rules.
73
+ Changed bytes invalidate affected reviews and renders; unknown proof holds.
74
+ Shared filesystem access does not permit cross-reading independent first passes.
75
+ Publication remains the explicitly authorized driver's action.
76
+
77
+ ## Receipts
78
+
79
+ Retain the normal native task/thread/run, pending-question and settings checks.
80
+ For artifact receipts, `attempt.candidate` and marker `head` are the exact
81
+ 64-hex artifact SHA-256; `attempt.base` is null. They are not Git revisions.
82
+ Bind `artifactScope` to the requested scope and report to evidenceRoot.
83
+ Supply `artifact:{path,sha256,readback:{path,sha256,evidence}}` for the independently
84
+ hashed owned snapshot. Requested isolation must be shared. Native child worktreePath may be null;
85
+ on T3 0.1.5 it may also inherit the parent's non-Git scratch path. For the latter,
86
+ include `parent` as a saved `t3_thread_read` observation (tool, environmentId,
87
+ args.threadId, observedAt, evidence, value.thread). Bind it to the attempt's
88
+ origin thread, selected project, child parentThreadId/subagent relationship and
89
+ identical canonical path inside workspaceRoot. Verify the actual filesystem;
90
+ this is shared access, not a Git checkout or OS isolation. Checkout metadata
91
+ remains forbidden. Unknown or different parent/path evidence holds.
92
+
93
+ T3 0.1.5 deduplicates local task results: latestTerminalSummary can be null when
94
+ summary contains the same result. For local artifact receipts only, use that
95
+ summary when childRunId and latestTerminalRunId both equal the current completed
96
+ run and its bytes equal the destination final message. An explicit conflicting
97
+ terminal summary, stale run, other version or remote result does not use this
98
+ fallback. Preserve the raw native fields; do not rewrite them. Never synthesize an effective field from requested settings.
99
+ The driver checks actual bytes and readbacks; helpers validate saved data only.
100
+ Keep artifact receipts separate from Git candidate publication/PR/merge receipts.
@@ -6,6 +6,11 @@ native destination. Remote reviewers require T3 worktree isolation; no shell chi
6
6
  checkout. Host names confer no role or permission. Remote authors/coordinator stay
7
7
  disabled; local isolated authors remain supported even overloaded.
8
8
 
9
+ Explicit non-repository Research/Explain/Publish uses the bounded role allowlist
10
+ and local shared-artifact isolation in [Local artifacts](local-artifacts.md).
11
+ This changes no configured seat, source independence or exact-byte review duty.
12
+ Engineering authors and code reviewers cannot acquire this artifact authority.
13
+
9
14
  - Chat drives (no role ID); `axstack-owner` owns one PR and
10
15
  `axstack-author` its sole writer.
11
16
  For own PRs, automatic merge is the default under the
@@ -204,6 +204,13 @@ a candidate follows [UI verification](ui-verification.md).
204
204
 
205
205
  The driver must be the sole run-record writer and enforce one writer per
206
206
  candidate; it never writes tracked candidate source or tests or repairs an author's source.
207
+ For explicit non-repository Research/Explain/Publish only,
208
+ [Local artifacts](local-artifacts.md) supplies the artifact branch of this table:
209
+ source readers, diligence, explainers and article/UI reviewers use local shared
210
+ native delegates with separate owned folders and exact SHA-256 snapshots.
211
+ Its role allowlist excludes engineering authors and code reviewers; their native
212
+ Git worktree rules above remain mandatory. Null repository alone grants nothing.
213
+
207
214
  The driver may write planning artifacts (spec, ticket map) in its own worktree when the selected store is repository Markdown.
208
215
  Repairs return to that author. Missing or idle sessions grant no ownership transfer.
209
216
  The current chat/driver has no role row in any preset.
@@ -24,8 +24,11 @@ All other browser checks outside the two named exceptions remain delegated to co
24
24
 
25
25
  Read the [T3 runtime boundary](t3-runtime.md) before dispatch; for candidate
26
26
  checks use its driver-made disposable detached checkout at the pinned candidate SHA.
27
+ For explicit non-repository article checks, use the local shared delegate and
28
+ owned SHA-256 snapshot in [Local artifacts](local-artifacts.md); no Git candidate
29
+ is fabricated. Engineering candidate checks retain the checkout requirement.
27
30
  Use the configured verifier browser and honor the user browser-tool policy.
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.
31
+ Engineering 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.
29
32
 
30
33
  For every interactive page, require a real-browser `axstack-ui-verifier` interaction pass.
31
34
  Give the evidence path and SHA-256 of the exact bytes in the verifier brief.
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import { inspectFleetReceipt } from './fleet-receipt.js';
3
3
  import { bindFleet } from './fleet-dispatch.js';
4
+ import { canonical } from './fleet-config.js';
4
5
 
5
6
  const fleetFlags = ['--recommendation', '--fleet-input', '--now', '--config-digest'];
6
7
  const flags = ['--role', '--roles', '--capabilities', '--kind', '--verify', '--out', ...fleetFlags];
@@ -34,6 +35,9 @@ by its destination facts. It is never the origin catalog by default.
34
35
  Fleet verification requires environmentId/projectId and fleetBinding alongside
35
36
  the effective T3 configuration. C3 collects that host-qualified evidence envelope.
36
37
  Optional input.nativeDestination validates saved native forwarded reads.
38
+ Explicit local-artifact scope requires those reads and kind task; emits isolation:"shared".
39
+ artifactScope is preserved evidence only, never a native tool argument.
40
+ Artifact verification also requires artifactScope and independently checked isolation.
37
41
  Optional --verify fleetAttempt/fleetReceipt validates host-qualified terminal facts;
38
42
  --out retains per-term receipt evidence. Synthetic bindings cannot authenticate reads.
39
43
  Coordinator routes remain unsupported here. A plan grants no launch authority.
@@ -98,6 +102,11 @@ function verify(configuration, provider, model, options, fleet) {
98
102
  if (fleet) {
99
103
  for (const [field, expected] of Object.entries(fleet.destination)) compare(field, expected, configuration?.[field]);
100
104
  for (const [field, expected] of Object.entries(fleet.fleetBinding)) compare(field, expected, configuration?.fleetBinding?.[field]);
105
+ if (fleet.artifactScope) {
106
+ compare('artifactScope', fleet.artifactScope, configuration?.artifactScope,
107
+ canonical(fleet.artifactScope) === canonical(configuration?.artifactScope));
108
+ compare('isolation', 'shared', configuration?.isolation);
109
+ }
101
110
  }
102
111
  let receiptTerms;
103
112
  if (fleet && configuration?.fleetReceipt !== undefined) {
@@ -175,7 +184,7 @@ function plan(argv, io) {
175
184
  const recommendation = JSON.parse(recommendationJSON);
176
185
  fleet = bindFleet(readJSON(args['--fleet-input']), recommendation,
177
186
  args['--config-digest'], Number(args['--now']), readFileSync(args['--capabilities'], 'utf8'),
178
- resolution, role.id === 'axstack-author');
187
+ resolution, role.id === 'axstack-author', role.id, args['--kind']);
179
188
  fleet.recommendation = recommendation;
180
189
  fleet.recommendationJSON = recommendationJSON;
181
190
  fleet.now = Number(args['--now']);
@@ -189,6 +198,7 @@ function plan(argv, io) {
189
198
  const selection = { [args['--kind'] === 'launch' ? 'instanceId' : 'providerInstanceId']: providerInstanceId,
190
199
  model, options, ...(args['--kind'] === 'task' ? fleet?.destination : {}) };
191
200
  emit({ [args['--kind'] === 'launch' ? 'modelSelection' : 'target']: selection, runtimeMode: 'full-access',
201
+ ...(fleet?.artifactScope ? { artifactScope: fleet.artifactScope, isolation: fleet.isolation } : {}),
192
202
  ...(fleet ? { fleetBinding: fleet.fleetBinding, ...(args['--kind'] === 'launch' ? fleet.destination : {}) } : {}),
193
203
  ...(args['--kind'] === 'task' && role.id === 'axstack-author' ? { isolation: 'worktree' } : {}) });
194
204
  return 0;
@@ -1,3 +1,5 @@
1
+ import { validScope } from './fleet-scope.js';
2
+
1
3
  // Configuration is reviewed data. This module does not read account-local files.
2
4
  export function fail(code) { throw new Error(code); }
3
5
  export const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -58,7 +60,7 @@ export function validateInput(input, now) {
58
60
  const r = input.request;
59
61
  if (!record(r) || !text(r.task) || !['reader', 'author'].includes(r.role)
60
62
  || !Array.isArray(r.requiredCapabilities) || !r.requiredCapabilities.every(text)
61
- || !text(r.repositoryId) || !text(r.providerInstanceId) || !text(r.model)
63
+ || !validScope(r) || !text(r.providerInstanceId) || !text(r.model)
62
64
  || !record(r.options) || !text(r.runtimeMode)) fail('request-schema');
63
65
  for (const key of ['destinations', 'routeFacts', 'telemetry', 'assignments', 'pendingAssignments']) {
64
66
  if (!Array.isArray(input[key])) fail(`${key}-schema`);
@@ -1,4 +1,5 @@
1
1
  // Validate explicit saved native reads. These bindings do not authenticate input.
2
+ import { isArtifact } from './fleet-scope.js';
2
3
  import { canonical, text, time } from './fleet-config.js';
3
4
 
4
5
  export function inspectDestination(observation, recommendation, capabilitiesText, now) {
@@ -7,6 +8,9 @@ export function inspectDestination(observation, recommendation, capabilitiesText
7
8
  state: pass ? 'pass' : missing ? 'unknown' : 'fail', evidence: evidence ?? null });
8
9
  const s = recommendation.selected;
9
10
  const o = observation ?? {};
11
+ const observed = (read, tool, environmentId, args) => read?.tool === tool
12
+ && read?.environmentId === environmentId && canonical(read?.args) === canonical(args)
13
+ && time(read?.observedAt) && read.observedAt <= now && text(read?.evidence);
10
14
  for (const [name, tool, environmentId, args] of [
11
15
  ['environment', 't3_environment_read', s.environmentId, {}],
12
16
  ['project', 't3_project_read', s.environmentId, { projectId: s.projectId }],
@@ -14,22 +18,66 @@ export function inspectDestination(observation, recommendation, capabilitiesText
14
18
  ['links', 't3_environment_links', recommendation.originEnvironmentId, {}],
15
19
  ]) {
16
20
  const read = o[name];
17
- term('destination-observation', read?.tool === tool && read?.environmentId === environmentId
18
- && canonical(read?.args) === canonical(args) && time(read?.observedAt)
19
- && read.observedAt <= now && text(read?.evidence), read);
21
+ term('destination-observation', observed(read, tool, environmentId, args), read);
20
22
  }
21
23
  term('destination-identity', o.environment?.value?.environmentId === s.environmentId,
22
24
  o.environment?.value?.environmentId);
23
25
  term('destination-project', o.project?.value?.id === s.projectId && text(o.project?.value?.workspaceRoot),
24
26
  o.project?.value);
25
- term('destination-repository', o.project?.value?.repositoryIdentity?.canonicalKey === s.repositoryId,
26
- o.project?.value?.repositoryIdentity);
27
+ const project = o.project?.value;
28
+ let listed;
29
+ if (o.projectList !== undefined) {
30
+ const pages = o.projectList;
31
+ const projects = [];
32
+ let cursor = 0;
33
+ let complete = Array.isArray(pages) && pages.length > 0;
34
+ if (complete) for (const page of pages) {
35
+ const args = page?.args;
36
+ const limit = args?.limit ?? 20;
37
+ const rows = page?.value?.projects;
38
+ const next = page?.value?.nextCursor;
39
+ complete &&= cursor !== null && observed(page, 't3_project_list', s.environmentId, {
40
+ ...(args?.cursor === undefined ? {} : { cursor }),
41
+ ...(args?.limit === undefined ? {} : { limit }),
42
+ }) && (args?.cursor ?? 0) === cursor && Number.isInteger(limit) && limit >= 1 && limit <= 100
43
+ && Array.isArray(rows) && rows.length <= limit
44
+ && (next === null || (next === cursor + limit && rows.length === limit));
45
+ if (Array.isArray(rows)) projects.push(...rows);
46
+ cursor = next;
47
+ }
48
+ listed = projects.find((p) => p?.id === s.projectId);
49
+ term('destination-project-list', complete && cursor === null
50
+ && projects.every((p) => text(p?.id)) && new Set(projects.map((p) => p?.id)).size === projects.length
51
+ && listed?.id === s.projectId && listed.id === project?.id && listed?.workspaceRoot === project?.workspaceRoot, pages);
52
+ }
53
+ // ProjectService.listPage resolves the cold identity cache; getById need not.
54
+ const identities = [project?.repositoryIdentity, listed?.repositoryIdentity].filter((v) => v != null);
55
+ if (isArtifact(recommendation.request)) {
56
+ const scope = recommendation.request.scope;
57
+ term('destination-artifact-project', project?.repositoryIdentity === null
58
+ && listed?.repositoryIdentity === null && project?.workspaceRoot === scope.workspaceRoot
59
+ && s.environmentId === scope.environmentId && s.projectId === scope.projectId,
60
+ { project, listed });
61
+ } else {
62
+ term('destination-repository', identities.length > 0
63
+ && identities.every((v) => v.canonicalKey === s.repositoryId), identities,
64
+ identities.length === 0);
65
+ }
27
66
  term('destination-catalog', o.capabilities?.valueText === capabilitiesText, o.capabilities?.valueText);
28
67
  const catalog = JSON.parse(capabilitiesText);
29
- const provider = catalog.providers?.find((v) => v.providerInstanceId === recommendation.request.providerInstanceId);
30
- term('destination-execution', provider?.canRunChildTask === true
31
- && provider?.runtimeModes?.includes(recommendation.request.runtimeMode), provider,
32
- provider?.canRunChildTask == null || !Array.isArray(provider?.runtimeModes));
68
+ const providers = catalog.providers?.filter((v) => v.providerInstanceId === recommendation.request.providerInstanceId);
69
+ const provider = providers?.length === 1 ? providers[0] : undefined;
70
+ const mode = recommendation.request.runtimeMode;
71
+ // T3 0.1.5 resolveRuntimeMode/RuntimePolicy preserve modes without restrictions.
72
+ // Limit omission semantics to the observed version and native drivers we checked.
73
+ const unrestricted = provider?.runtimeModes === undefined && o.environment?.value?.serverVersion === '0.1.5'
74
+ && ['codex', 'claudeAgent'].includes(provider?.driverKind);
75
+ const supported = unrestricted || (Array.isArray(provider?.runtimeModes) && provider.runtimeModes.includes(mode));
76
+ term('destination-execution', provider?.canRunChildTask === true && supported
77
+ && catalog.runtimeMode === mode && Array.isArray(provider?.constraints) && provider.constraints.length === 0,
78
+ { provider, runtimeMode: catalog.runtimeMode, serverVersion: o.environment?.value?.serverVersion },
79
+ providers?.length === 0 || provider?.canRunChildTask == null || catalog.runtimeMode == null
80
+ || provider?.constraints == null || (provider?.runtimeModes === undefined && !unrestricted));
33
81
  const local = s.environmentId === recommendation.originEnvironmentId;
34
82
  const link = o.links?.value?.links?.find((v) => v.environmentId === s.environmentId);
35
83
  term('destination-access', o.links?.value?.environmentId === recommendation.originEnvironmentId
@@ -1,15 +1,22 @@
1
1
  // Bind supplied placement facts to a plan. No reads, dispatch or authority grant.
2
+ import { isArtifact } from './fleet-scope.js';
2
3
  import { inspectDestination } from './fleet-destination.js';
3
4
  import { canonical, record, text } from './fleet-config.js';
4
5
  import { recommend } from './fleet-route.js';
5
6
 
6
- export function bindFleet(input, saved, configDigest, now, capabilitiesText, resolved, author) {
7
+ export function bindFleet(input, saved, configDigest, now, capabilitiesText, resolved, author, roleId, kind) {
7
8
  const hold = (code) => { throw Object.assign(new Error(code), { exitCode: 2 }); };
8
9
  if (saved?.schemaVersion !== 1) hold('schema-version: recommendation');
9
10
  if (configDigest !== input?.configDigest) hold('config-digest-mismatch');
10
11
  let current;
11
12
  try { current = recommend(input, now); } catch (error) { hold(error.message); }
12
13
  if (canonical(saved) !== canonical(current)) hold('recommendation-mismatch');
14
+ const artifact = isArtifact(current.request);
15
+ if (artifact) {
16
+ if (roleId !== current.request.scope.roleId) hold('artifact-role-mismatch');
17
+ if (kind !== 'task') hold('artifact-task-only');
18
+ if (input.nativeDestination === undefined) hold('artifact-native-destination-required');
19
+ }
13
20
  const selected = current.selected;
14
21
  if (!selected) hold('no-executable-route');
15
22
  // Coordinator enablement/lifecycle is a later capability, never inferred here.
@@ -40,7 +47,8 @@ export function bindFleet(input, saved, configDigest, now, capabilitiesText, res
40
47
  return { id, value };
41
48
  });
42
49
  return {
43
- options, destination: { environmentId: selected.environmentId, projectId: selected.projectId },
50
+ options, ...(artifact ? { artifactScope: current.request.scope, isolation: 'shared' } : {}),
51
+ destination: { environmentId: selected.environmentId, projectId: selected.projectId },
44
52
  fleetBinding: { configDigest, destinationSnapshotId: selected.destinationSnapshotId,
45
53
  destinationCapabilitiesDigest: digest },
46
54
  };
@@ -1,3 +1,4 @@
1
+ import { isArtifact } from './fleet-scope.js';
1
2
  import { nativeAssignments } from './fleet-inventory.js';
2
3
  import { canonical, evidence, fail, record, text, time } from './fleet-config.js';
3
4
 
@@ -116,6 +117,8 @@ export function routeReasons(route, host, destination, facts, input, now) {
116
117
  const add = (code) => { if (!reasons.includes(code)) reasons.push(code); };
117
118
  const remote = route.environmentId !== input.origin.environmentId;
118
119
  const r = input.request;
120
+ const artifact = isArtifact(r);
121
+ if (artifact && (remote || route.kind !== 'local' || r.scope.environmentId !== input.origin.environmentId)) add('artifact-local-only');
119
122
  if (Object.hasOwn(input.exclusions.hosts, host.id)) add('excluded-host');
120
123
  if (Object.hasOwn(input.exclusions.routes, route.id)) add('excluded-route');
121
124
  if (!route.enabled) add('route-disabled');
@@ -134,7 +137,9 @@ export function routeReasons(route, host, destination, facts, input, now) {
134
137
  || !evidence(destination.evidence, host.environmentId, now)) add('evidence-unproven');
135
138
  if (!text(destination.snapshotId) || facts?.destinationSnapshotId !== destination.snapshotId) add('target-snapshot-mismatch');
136
139
  if (!record(destination.project) || !text(destination.project.id)) add('project-missing');
137
- else if (destination.project.repositoryId !== r.repositoryId) add('project-mismatch');
140
+ else if (destination.project.repositoryId !== r.repositoryId
141
+ || (artifact && (destination.project.id !== r.scope.projectId
142
+ || destination.project.workspaceRoot !== r.scope.workspaceRoot))) add('project-mismatch');
138
143
  const e = destination.execution;
139
144
  if (!record(e) || e.available !== true) add('execution-unproven');
140
145
  else if (e.providerInstanceId !== r.providerInstanceId || e.model !== r.model
@@ -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
+ }
@@ -1,5 +1,7 @@
1
1
  // Saved evidence validator. A pass binds supplied facts; it cannot authenticate them.
2
2
  import { canonical, record, text, time } from './fleet-config.js';
3
+ import { inside, isArtifact } from './fleet-scope.js';
4
+ import { inspectNativeFamily } from './fleet-native-receipt.js';
3
5
 
4
6
  function options(value) {
5
7
  if (!Array.isArray(value)) return record(value) ? value : null;
@@ -20,6 +22,7 @@ export function inspectFleetReceipt(receipt, expected) {
20
22
  const a = expected.attempt;
21
23
  const s = expected.recommendation.selected;
22
24
  const request = expected.recommendation.request;
25
+ const artifact = isArtifact(request);
23
26
  term('receipt-origin', text(a?.origin?.threadId) && a?.origin?.environmentId === expected.recommendation.originEnvironmentId
24
27
  && same(r.attempt?.origin, a?.origin), r.attempt?.origin,
25
28
  !text(a?.origin?.threadId) || r.attempt?.origin == null);
@@ -57,12 +60,30 @@ export function inspectFleetReceipt(receipt, expected) {
57
60
  && target?.providerInstanceId === request.providerInstanceId && target?.model === request.model
58
61
  && same(options(target?.options), request.options) && r.requested?.runtimeMode === request.runtimeMode,
59
62
  r.requested);
63
+ const native = r.nativeFamily === undefined ? null : inspectNativeFamily(r, expected);
64
+ if (native) terms.push(...native.terms);
65
+ const shell = native?.shell;
66
+ const nativeValid = remote && native?.terms.every((t) => t.state === 'pass');
67
+ if (native) term('receipt-effective', shell?.providerInstanceId === request.providerInstanceId
68
+ && shell?.modelSelection?.instanceId === request.providerInstanceId
69
+ && shell?.modelSelection?.model === request.model
70
+ && Array.isArray(shell?.modelSelection?.options)
71
+ && same(options(shell?.modelSelection?.options), request.options)
72
+ && shell?.runtimeMode === request.runtimeMode, shell?.modelSelection);
73
+ const runs = r.thread?.value?.recentRuns;
74
+ // Without the origin result-run ID, only the sole initial run can own its summary.
75
+ const initial = nativeValid && status?.childThreadId === null && submitted?.childThreadId === null
76
+ && status?.childRunId === null && submitted?.childRunId === null
77
+ && status?.latestTerminalRunId === null && thread?.runCount === 1
78
+ && Array.isArray(runs) && runs.length === 1 && runs[0]?.ordinal === 1;
79
+ const effectiveOptions = status?.options === undefined && initial
80
+ ? shell?.modelSelection?.options : status?.options;
60
81
  term('receipt-effective', status?.providerInstanceId === request.providerInstanceId
61
82
  && status?.model === request.model && thread?.providerInstanceId === request.providerInstanceId
62
- && thread?.model === request.model && same(options(status?.options), request.options)
83
+ && thread?.model === request.model && same(options(effectiveOptions), request.options)
63
84
  && thread?.runtimeMode === request.runtimeMode, { provider: status?.providerInstanceId,
64
- model: status?.model, options: status?.options ?? null, runtimeMode: thread?.runtimeMode },
65
- status?.options == null || thread?.runtimeMode == null);
85
+ model: status?.model, options: effectiveOptions ?? null, runtimeMode: thread?.runtimeMode },
86
+ effectiveOptions == null || thread?.runtimeMode == null);
66
87
  const terminal = r.terminal?.value;
67
88
  term('receipt-terminal', status?.status === 'completed' && status?.latestTerminalStatus === 'completed'
68
89
  && status?.workState === 'result_available' && status?.hasPendingChildRuns === false
@@ -84,9 +105,8 @@ export function inspectFleetReceipt(receipt, expected) {
84
105
  && questions.value.requests.length === 0, questions,
85
106
  questions == null || questions.value?.requests === undefined);
86
107
  const current = thread?.latestRunId;
87
- const runs = r.thread?.value?.recentRuns;
88
108
  const latest = Array.isArray(runs) ? runs.find((v) => v.runId === current) : null;
89
- term('receipt-run', text(current) && status?.latestTerminalRunId === current
109
+ term('receipt-run', text(current) && (status?.latestTerminalRunId === current || initial)
90
110
  && terminal?.threadId === child(status) && terminal?.runId === current
91
111
  && (!terminal?.resultRun || (terminal.resultRun.runId === current && terminal.resultRun.status === 'completed'))
92
112
  && (!remote || (r.remoteChild?.environmentId === s.environmentId
@@ -94,7 +114,7 @@ export function inspectFleetReceipt(receipt, expected) {
94
114
  && latest?.status === 'completed' && Number.isSafeInteger(latest?.ordinal)
95
115
  && runs.every((v) => Number.isSafeInteger(v.ordinal) && v.ordinal <= latest.ordinal),
96
116
  { current, originTerminal: status?.latestTerminalRunId, terminal, runs },
97
- !current || !status?.latestTerminalRunId || !terminal?.runId || !Array.isArray(runs)
117
+ !current || (!status?.latestTerminalRunId && !initial) || !terminal?.runId || !Array.isArray(runs)
98
118
  || (remote && !r.remoteChild?.runId));
99
119
  const marker = `AXSTACK-DONE key=${a?.key} head=${a?.candidate} report=${r.evidence?.path}`;
100
120
  const finalMarker = (value) => {
@@ -109,15 +129,46 @@ export function inspectFleetReceipt(receipt, expected) {
109
129
  };
110
130
  const items = r.thread?.value?.items;
111
131
  const final = Array.isArray(items) ? items.filter((v) => v.runId === current && v.createdBy === 'agent').at(-1) : null;
112
- term('receipt-marker', /^[a-f0-9]{40}$/.test(a?.candidate ?? '')
113
- && finalMarker(status?.latestTerminalSummary) === marker && finalMarker(final?.text) === marker,
114
- { origin: status?.latestTerminalSummary, destination: final?.text });
132
+ // T3 0.1.5 suppresses latestTerminalSummary when it equals summary.
133
+ // Only the same local result run may use that deduplicated value.
134
+ const localSummary = artifact && !remote && s.versions?.t3 === '0.1.5'
135
+ && status?.latestTerminalSummary === null && text(current)
136
+ && status?.childRunId === current && status?.latestTerminalRunId === current
137
+ && status?.status === 'completed' && status?.summary === final?.text;
138
+ const resultSummary = localSummary ? status.summary : status?.latestTerminalSummary;
139
+ term('receipt-marker', (artifact ? /^[a-f0-9]{64}$/ : /^[a-f0-9]{40}$/).test(a?.candidate ?? '')
140
+ && finalMarker(resultSummary) === marker && finalMarker(final?.text) === marker
141
+ && (!initial || resultSummary === final?.text),
142
+ { origin: resultSummary, destination: final?.text });
115
143
  term('receipt-host-path', r.evidence?.hostId === s.hostId && r.evidence?.environmentId === s.environmentId
116
- && absolute(r.evidence?.path) && finalMarker(status?.latestTerminalSummary)?.endsWith(` report=${r.evidence.path}`), r.evidence);
117
- if (a?.title?.startsWith('Reviewer: ')) {
144
+ && absolute(r.evidence?.path) && finalMarker(resultSummary)?.endsWith(` report=${r.evidence.path}`), r.evidence);
145
+ if (artifact) {
146
+ const scope = request.scope;
147
+ const bytes = r.artifact;
148
+ term('receipt-artifact-scope', a?.base === null && same(r.artifactScope, scope)
149
+ && inside(r.evidence?.path, scope.evidenceRoot), r.artifactScope);
150
+ term('receipt-artifact-bytes', bytes?.sha256 === a?.candidate && inside(bytes?.path, scope.artifactRoot)
151
+ && bytes?.readback?.path === bytes.path && bytes?.readback?.sha256 === bytes.sha256
152
+ && text(bytes?.readback?.evidence), bytes);
153
+ // Shared native children can inherit a parent's non-Git scratch directory.
154
+ // Bind that value to an actual parent read; a path alone proves nothing.
155
+ const parent = r.parent;
156
+ const parentThread = parent?.value?.thread;
157
+ const inherited = !remote && s.versions?.t3 === '0.1.5'
158
+ && parent?.tool === 't3_thread_read' && parent.environmentId === a?.origin?.environmentId
159
+ && same(parent.args, { threadId: a?.origin?.threadId })
160
+ && time(parent.observedAt) && parent.observedAt <= expected.now && text(parent.evidence)
161
+ && parentThread?.threadId === a?.origin?.threadId && parentThread?.projectId === s.projectId
162
+ && thread?.parentThreadId === parentThread?.threadId && thread?.relationshipToParent === 'subagent'
163
+ && absolute(parentThread?.worktreePath) && inside(parentThread.worktreePath, scope.workspaceRoot)
164
+ && thread?.worktreePath === parentThread.worktreePath;
165
+ term('receipt-isolation', r.requested?.isolation === 'shared'
166
+ && (thread?.worktreePath === null || inherited) && r.checkout == null,
167
+ { isolation: r.requested?.isolation, worktreePath: thread?.worktreePath, parent });
168
+ } else if (a?.title?.startsWith('Reviewer: ')) {
118
169
  term('receipt-isolation', r.requested?.isolation === 'worktree', r.requested?.isolation);
119
170
  }
120
- if (a?.title?.startsWith('Reviewer: ') || r.checkout != null || thread?.worktreePath != null || r.requested?.isolation === 'worktree') {
171
+ if (!artifact && (a?.title?.startsWith('Reviewer: ') || r.checkout != null || thread?.worktreePath != null || r.requested?.isolation === 'worktree')) {
121
172
  const c = r.checkout;
122
173
  term('receipt-checkout', c?.hostId === s.hostId && c?.environmentId === s.environmentId
123
174
  && absolute(c?.path) && c.path === thread?.worktreePath
@@ -0,0 +1,24 @@
1
+ // Explicit artifact authority, separate from repository engineering authority.
2
+ const sourceRoles = ['axstack-research-requirements', 'axstack-research-code', 'axstack-research-code-sol',
3
+ 'axstack-explore-execution', 'axstack-explore-execution-sol',
4
+ 'axstack-research-web', 'axstack-research-web-sol', 'axstack-research-x', 'axstack-diligence'];
5
+ const articleRoles = ['axstack-explainer', 'axstack-explainer-review', 'axstack-ui-verifier', 'axstack-ui-verifier-sonnet'];
6
+ export const isArtifact = (request) => request?.scope?.kind === 'local-artifact';
7
+ const path = (value) => typeof value === 'string' && value.startsWith('/') && value !== '/'
8
+ && !/[\s\x00-\x1f\x7f]/.test(value) && !value.slice(1).split('/').some((v) => ['', '.', '..'].includes(v));
9
+ export const inside = (value, root) => path(value) && path(root) && value.startsWith(`${root}/`);
10
+ const overlaps = (a, b) => a === b || inside(a, b) || inside(b, a);
11
+
12
+ export function validScope(request) {
13
+ const s = request.scope;
14
+ if (s === undefined) return typeof request.repositoryId === 'string' && request.repositoryId.trim().length > 0;
15
+ if (s?.kind === 'repository') return typeof request.repositoryId === 'string' && request.repositoryId.trim().length > 0;
16
+ if (!isArtifact(request) || request.repositoryId !== null || request.role !== 'reader'
17
+ || !['research', 'explain', 'publish'].includes(s.workflow)
18
+ || ![...sourceRoles, ...(s.workflow === 'research' ? [] : articleRoles)].includes(s.roleId)
19
+ || ![s.environmentId, s.projectId].every((v) => typeof v === 'string' && v.trim().length > 0)
20
+ || !inside(s.artifactRoot, s.workspaceRoot) || !inside(s.evidenceRoot, s.workspaceRoot)
21
+ || overlaps(s.artifactRoot, s.evidenceRoot) || !Array.isArray(s.reservedRoots)
22
+ || s.reservedRoots.some((root) => !path(root) || overlaps(root, s.artifactRoot) || overlaps(root, s.evidenceRoot))) return false;
23
+ return true;
24
+ }
@@ -20,6 +20,10 @@ scope baseline. Ordinary work in the current chat needs no launch preflight;
20
20
  load the [T3 runtime boundary](../axstack/references/t3-runtime.md) only
21
21
  immediately before an actual profile dispatch.
22
22
 
23
+ For explicit local non-repository work, apply
24
+ [Local artifacts](../axstack/references/local-artifacts.md) before native dispatch.
25
+ Preserve this phase's independent source/fact and exact-byte review requirements.
26
+
23
27
  ## 1. Bound the question and evidence
24
28
 
25
29
  1. Identify the audience, question, requested format, theme, and publication
@@ -24,7 +24,12 @@ Interaction checks remain with `axstack-ui-verifier`; source-fidelity review is
24
24
  Use this checklist for every HTML explanation and other visual artifacts where
25
25
  rendering matters.
26
26
 
27
- 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.
27
+ Browser and visual checks use delegated `axstack-ui-verifier`: engineering
28
+ candidates require its own detached checkout; explicit non-repository articles
29
+ use the local shared delegate with an owned exact-byte snapshot under
30
+ [Local artifacts](../../axstack/references/local-artifacts.md). Outputs stay in
31
+ its private evidence folder. Playwright checks and source/design review remain
32
+ bound to the exact final SHA-256 before public publication.
28
33
 
29
34
  1. Identify the final artifact bytes and theme. The explicit user theme wins;
30
35
  otherwise use `prefers-color-scheme`.
@@ -10,6 +10,10 @@ The current T3 driver owns publication; workers return checked artifacts.
10
10
  Load [Standing contracts](../axstack/references/contracts.md) and follow its
11
11
  lifecycle pointers. Keep page bytes, checks and receipts in private run evidence.
12
12
 
13
+ For explicit local non-repository work, apply
14
+ [Local artifacts](../axstack/references/local-artifacts.md) before native dispatch.
15
+ Preserve this phase's independent source/fact and exact-byte review requirements.
16
+
13
17
  ## 1. Bind the request
14
18
 
15
19
  Read host-managed `~/.config/axstack/publish.json` containing string fields
@@ -20,6 +20,10 @@ baseline, ticket map, or author pipeline. Keep it read-only unless requested
20
20
  and separately authorized to change product code or publish; otherwise, neither
21
21
  is part of research.
22
22
 
23
+ For explicit local non-repository work, apply
24
+ [Local artifacts](../axstack/references/local-artifacts.md) before native dispatch.
25
+ Preserve this phase's independent source/fact and exact-byte review requirements.
26
+
23
27
  ## Procedure
24
28
 
25
29
  1. **Frame the question.** Restate the question, what is in scope, what is
@@ -11,6 +11,11 @@ Include [Safe deletion](../axstack/references/workspace-hygiene.md#safe-deletion
11
11
 
12
12
  Manual review keeps the user’s chat and workspace open.
13
13
 
14
+ For non-repository article/source review, follow
15
+ [Local artifacts](../axstack/references/local-artifacts.md); Explain/Publish own
16
+ the independent artifact review. PR/code-review modes below retain repository
17
+ and native worktree requirements; null identity never selects those modes.
18
+
14
19
  Produce evidence-bound findings for an exact revision using the review count
15
20
  and model routing required by its mode. Report within the requested authority;
16
21
  for own PRs, automatic merge is the default under the