axstack 0.54.0 → 0.56.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.
Files changed (50) 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/profiles/presets/claude-only.json +9 -0
  6. package/profiles/presets/codex-only.json +9 -0
  7. package/profiles/presets/mixed.json +9 -0
  8. package/skills/axstack/references/fleet-dispatch.md +166 -0
  9. package/skills/axstack/references/fleet-route-schema.md +325 -0
  10. package/skills/axstack/references/fleet-verification.md +102 -0
  11. package/skills/axstack/references/lifecycle.md +19 -2
  12. package/skills/axstack/references/role-roster.md +11 -0
  13. package/skills/axstack/references/routing.md +14 -0
  14. package/skills/axstack/references/run-record.md +83 -0
  15. package/skills/axstack/references/t3-runtime.md +174 -7
  16. package/skills/axstack/references/ui-verification.md +67 -4
  17. package/skills/axstack/references/workspace-hygiene.md +8 -0
  18. package/skills/axstack/scripts/dispatch-plan.js +52 -8
  19. package/skills/axstack/scripts/fleet-config.js +78 -0
  20. package/skills/axstack/scripts/fleet-destination.js +39 -0
  21. package/skills/axstack/scripts/fleet-dispatch.js +47 -0
  22. package/skills/axstack/scripts/fleet-facts.js +161 -0
  23. package/skills/axstack/scripts/fleet-inventory.js +76 -0
  24. package/skills/axstack/scripts/fleet-proc.js +31 -0
  25. package/skills/axstack/scripts/fleet-receipt.js +130 -0
  26. package/skills/axstack/scripts/fleet-route-cli.js +78 -0
  27. package/skills/axstack/scripts/fleet-route.js +86 -0
  28. package/skills/axstack/scripts/fleet-sample-cli.js +81 -0
  29. package/skills/axstack/scripts/fleet-sample-io.js +59 -0
  30. package/skills/axstack/scripts/fleet-sample-manifest.js +34 -0
  31. package/skills/axstack/scripts/fleet-sample.js +48 -0
  32. package/skills/axstack/scripts/resolve-models.js +8 -4
  33. package/skills/axstack-cleanup/SKILL.md +21 -0
  34. package/skills/axstack-implement/SKILL.md +12 -5
  35. package/skills/axstack-review/references/review-candidate.md +1 -1
  36. package/skills/axstack-verify/SKILL.md +17 -6
  37. package/skills/axstack-watch/SKILL.md +3 -0
  38. package/skills/axstack-watch/references/watch-runtime.md +18 -5
  39. package/skills/voa/SKILL.md +96 -0
  40. package/skills/voa/references/observations.md +79 -0
  41. package/skills/voa/references/recovery.md +94 -0
  42. package/skills/voa/references/verification.md +35 -0
  43. package/skills/voa/scripts/cleanup.js +39 -0
  44. package/skills/voa/scripts/evaluate.js +122 -0
  45. package/skills/voa/scripts/observations.js +68 -0
  46. package/skills/voa/scripts/records.js +48 -0
  47. package/skills/voa/scripts/voa.js +43 -0
  48. package/src/installer.js +3 -2
  49. package/src/instructions.js +11 -3
  50. package/src/roles.js +2 -2
@@ -0,0 +1,130 @@
1
+ // Saved evidence validator. A pass binds supplied facts; it cannot authenticate them.
2
+ import { canonical, record, text, time } from './fleet-config.js';
3
+
4
+ function options(value) {
5
+ if (!Array.isArray(value)) return record(value) ? value : null;
6
+ if (value.some((v) => !text(v?.id) || !['string', 'boolean'].includes(typeof v.value))
7
+ || new Set(value.map((v) => v.id)).size !== value.length) return null;
8
+ return Object.fromEntries(value.map((v) => [v.id, v.value]));
9
+ }
10
+ const same = (a, b) => a !== undefined && b !== undefined && canonical(a) === canonical(b);
11
+ const absolute = (path) => text(path) && path.startsWith('/') && !path.split('/').some((p) => p === '..' || p === '.');
12
+
13
+ export function inspectFleetReceipt(receipt, expected) {
14
+ const terms = [];
15
+ const term = (code, pass, observed, missing = observed === undefined || observed === null) => {
16
+ terms.push({ code, state: pass ? 'pass' : missing ? 'unknown' : 'fail', evidence: observed ?? null });
17
+ };
18
+ const r = receipt ?? {};
19
+ term('receipt-version', r.schemaVersion === 1, r.schemaVersion);
20
+ const a = expected.attempt;
21
+ const s = expected.recommendation.selected;
22
+ const request = expected.recommendation.request;
23
+ term('receipt-origin', text(a?.origin?.threadId) && a?.origin?.environmentId === expected.recommendation.originEnvironmentId
24
+ && same(r.attempt?.origin, a?.origin), r.attempt?.origin,
25
+ !text(a?.origin?.threadId) || r.attempt?.origin == null);
26
+ term('receipt-attempt', text(a?.key) && text(a?.clientRequestId) && text(a?.origin?.taskId)
27
+ && same(r.attempt, a), r.attempt);
28
+ term('receipt-recommendation', r.recommendationJSON === expected.recommendationJSON, r.recommendationJSON);
29
+ term('receipt-config', r.configDigest === expected.recommendation.configDigest, r.configDigest);
30
+ const status = r.status?.value;
31
+ const submitted = r.submission;
32
+ const thread = r.thread?.value?.thread;
33
+ const remote = s.environmentId !== expected.recommendation.originEnvironmentId;
34
+ const child = (v) => remote ? v?.remoteChild?.threadId : v?.childThreadId;
35
+ const env = (v) => remote ? v?.remoteChild?.environmentId : a?.origin?.environmentId;
36
+ term('receipt-destination', env(status) === s.environmentId && env(submitted) === s.environmentId
37
+ && text(child(status)) && child(status) === child(submitted)
38
+ && r.thread?.environmentId === s.environmentId && thread?.threadId === child(status),
39
+ { submitted: submitted?.remoteChild ?? submitted?.childThreadId,
40
+ status: status?.remoteChild ?? status?.childThreadId, thread: r.thread?.environmentId });
41
+ term('receipt-title', text(a?.title) && thread?.title === a.title, thread?.title);
42
+ term('receipt-project', thread?.projectId === s.projectId, thread?.projectId);
43
+ for (const [name, tool, environmentId, args] of [
44
+ ['status', 'task_status', a?.origin?.environmentId, { taskId: a?.origin?.taskId }],
45
+ ['thread', 't3_thread_read', s.environmentId, { threadId: child(status) }],
46
+ ['terminal', 't3_thread_wait', s.environmentId, { threadId: child(status), runId: thread?.latestRunId }],
47
+ ]) {
48
+ const read = r[name];
49
+ term('receipt-observation', read?.tool === tool && read?.environmentId === environmentId
50
+ && same(read?.args, args) && time(read?.observedAt) && read.observedAt <= expected.now
51
+ && text(read?.evidence), read);
52
+ }
53
+ term('receipt-origin', status?.taskId === a?.origin?.taskId && submitted?.taskId === a?.origin?.taskId,
54
+ status?.taskId);
55
+ const target = r.requested?.target;
56
+ term('receipt-requested', target?.environmentId === s.environmentId && target?.projectId === s.projectId
57
+ && target?.providerInstanceId === request.providerInstanceId && target?.model === request.model
58
+ && same(options(target?.options), request.options) && r.requested?.runtimeMode === request.runtimeMode,
59
+ r.requested);
60
+ term('receipt-effective', status?.providerInstanceId === request.providerInstanceId
61
+ && status?.model === request.model && thread?.providerInstanceId === request.providerInstanceId
62
+ && thread?.model === request.model && same(options(status?.options), request.options)
63
+ && 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);
66
+ const terminal = r.terminal?.value;
67
+ term('receipt-terminal', status?.status === 'completed' && status?.latestTerminalStatus === 'completed'
68
+ && status?.workState === 'result_available' && status?.hasPendingChildRuns === false
69
+ && ['completed', 'idle'].includes(thread?.status) && thread?.activeRunId === null
70
+ && terminal?.status === 'completed' && terminal?.timedOut === false, { status, terminal },
71
+ status?.status == null || status?.workState == null || status?.hasPendingChildRuns == null
72
+ || status?.latestTerminalStatus == null || terminal?.status == null || terminal?.timedOut == null
73
+ || thread?.activeRunId === undefined);
74
+ term('receipt-question', status?.hasPendingQuestion === false && status?.pendingApproval === null
75
+ && thread?.pendingRequestCount === 0, { hasPendingQuestion: status?.hasPendingQuestion,
76
+ pendingApproval: status?.pendingApproval, pendingRequestCount: thread?.pendingRequestCount },
77
+ status?.hasPendingQuestion == null || status?.pendingApproval === undefined || thread?.pendingRequestCount == null);
78
+ const questions = r.questions;
79
+ term('receipt-question', questions?.tool === 't3_pending_request'
80
+ && questions?.environmentId === s.environmentId
81
+ && same(questions?.args, { action: 'list', threadId: child(status) })
82
+ && time(questions?.observedAt) && questions.observedAt <= expected.now
83
+ && text(questions?.evidence) && Array.isArray(questions?.value?.requests)
84
+ && questions.value.requests.length === 0, questions,
85
+ questions == null || questions.value?.requests === undefined);
86
+ const current = thread?.latestRunId;
87
+ const runs = r.thread?.value?.recentRuns;
88
+ const latest = Array.isArray(runs) ? runs.find((v) => v.runId === current) : null;
89
+ term('receipt-run', text(current) && status?.latestTerminalRunId === current
90
+ && terminal?.threadId === child(status) && terminal?.runId === current
91
+ && (!terminal?.resultRun || (terminal.resultRun.runId === current && terminal.resultRun.status === 'completed'))
92
+ && (!remote || (r.remoteChild?.environmentId === s.environmentId
93
+ && r.remoteChild?.threadId === child(status) && r.remoteChild?.runId === current))
94
+ && latest?.status === 'completed' && Number.isSafeInteger(latest?.ordinal)
95
+ && runs.every((v) => Number.isSafeInteger(v.ordinal) && v.ordinal <= latest.ordinal),
96
+ { current, originTerminal: status?.latestTerminalRunId, terminal, runs },
97
+ !current || !status?.latestTerminalRunId || !terminal?.runId || !Array.isArray(runs)
98
+ || (remote && !r.remoteChild?.runId));
99
+ const marker = `AXSTACK-DONE key=${a?.key} head=${a?.candidate} report=${r.evidence?.path}`;
100
+ const finalMarker = (value) => {
101
+ if (typeof value !== 'string') return null;
102
+ const lines = value.trim().split('\n');
103
+ const markers = lines.filter((line) => line.startsWith('AXSTACK-'));
104
+ if (markers.length !== 1) return null;
105
+ const trailer = lines.slice(lines.indexOf(markers[0]) + 1);
106
+ return trailer.length === 0 || (trailer.length === 1
107
+ && /^PR https?:\/\/\S+, head [a-f0-9]{7,40}, checks and reviews .+$/.test(trailer[0]))
108
+ ? markers[0] : null;
109
+ };
110
+ const items = r.thread?.value?.items;
111
+ 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 });
115
+ 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: ')) {
118
+ term('receipt-isolation', r.requested?.isolation === 'worktree', r.requested?.isolation);
119
+ }
120
+ if (a?.title?.startsWith('Reviewer: ') || r.checkout != null || thread?.worktreePath != null || r.requested?.isolation === 'worktree') {
121
+ const c = r.checkout;
122
+ term('receipt-checkout', c?.hostId === s.hostId && c?.environmentId === s.environmentId
123
+ && absolute(c?.path) && c.path === thread?.worktreePath
124
+ && /^[a-f0-9]{40}$/.test(a?.base ?? '') && c?.base === a.base && c?.candidate === a.candidate
125
+ && c?.readback?.hostId === s.hostId && c?.readback?.environmentId === s.environmentId
126
+ && c?.readback?.path === c.path && c?.readback?.base === a.base && c?.readback?.candidate === a.candidate
127
+ && text(c?.readback?.evidence), c);
128
+ }
129
+ return terms;
130
+ }
@@ -0,0 +1,78 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { sampleMain, sampleHelp } from './fleet-sample-cli.js';
3
+ import { recommend, parseFleetConfig } from './fleet-route.js';
4
+
5
+ const help = `Usage: bun fleet-route.js recommend --input <snapshot.json> --now <epoch-ms> [--json]
6
+ [--task <name>] [--config <fleet-routing.json>] [--config-digest <sha256>]
7
+
8
+ --input Saved schema-1 facts; no collection or dispatch
9
+ --now Required nonnegative integer evaluation time; no ambient clock
10
+ --task Override the saved request's task preference
11
+ --config Reviewed account-local config (default: ~/.config/haoshoku/fleet-routing.json)
12
+ --config-digest Run-pinned SHA-256; otherwise required in saved input.configDigest
13
+ --json Emit complete recommendation JSON; errors are JSON on stderr
14
+ --help Show this interface
15
+
16
+ Reads only the supplied input and config. Writes stdout/stderr only.
17
+ No network, authentication, dispatch, config changes or retained state.
18
+ Required session capabilities must be proven on the actual destination session;
19
+ an origin catalog or transferred credentials cannot establish them.
20
+ Exclusions with reasons are supplied in input.exclusions.hosts/routes.
21
+ Use sample or sample-recommend for bounded collection.
22
+ Exit codes: 0 recommendation/help, 1 input/config error, 2 no executable route.
23
+ `;
24
+
25
+ const fail = (code) => { throw new Error(code); };
26
+ function readInput(path) {
27
+ let value;
28
+ try { value = readFileSync(path, 'utf8'); } catch { fail('missing-input'); }
29
+ try { return JSON.parse(value); } catch { fail('input-json'); }
30
+ }
31
+
32
+ function defaultConfigPath() {
33
+ return process.env.HOME ? `${process.env.HOME}/.config/haoshoku/fleet-routing.json` : null;
34
+ }
35
+
36
+ export async function main(argv) {
37
+ try {
38
+ if (argv.length === 1 && argv[0] === '--help') {
39
+ process.stdout.write(help + sampleHelp);
40
+ return 0;
41
+ }
42
+ if (['sample', 'sample-recommend'].includes(argv[0])) return await sampleMain(argv, defaultConfigPath());
43
+ if (argv[0] !== 'recommend') fail('unexpected-command');
44
+ const args = {};
45
+ for (let i = 1; i < argv.length; i++) {
46
+ const flag = argv[i];
47
+ if (!['--input', '--now', '--task', '--config', '--config-digest', '--json'].includes(flag)) fail('unexpected-argument');
48
+ if (Object.hasOwn(args, flag)) fail('duplicate-argument');
49
+ if (flag === '--json') { args[flag] = true; continue; }
50
+ const value = argv[++i];
51
+ if (!value?.trim() || value.startsWith('--')) fail('missing-argument');
52
+ args[flag] = value;
53
+ }
54
+ if (!args['--input'] || !args['--now']) fail('missing-argument');
55
+ if (!/^\d+$/.test(args['--now'])) fail('evaluation-time');
56
+ const now = Number(args['--now']);
57
+ const input = readInput(args['--input']);
58
+ if (!input || typeof input !== 'object' || Array.isArray(input) || input.schemaVersion !== 1) fail('schema-version: input');
59
+ let configText;
60
+ const configPath = args['--config'] ?? defaultConfigPath();
61
+ try {
62
+ if (!configPath) fail('missing-config');
63
+ configText = readFileSync(configPath, 'utf8');
64
+ } catch { fail('missing-config'); }
65
+ const configDigest = args['--config-digest'] ?? input.configDigest;
66
+ parseFleetConfig(configText, configDigest);
67
+ if (input.configDigest !== undefined && input.configDigest !== configDigest) fail('config-digest-mismatch');
68
+ if (input.configText !== undefined && input.configText !== configText) fail('config-snapshot-mismatch');
69
+ const result = recommend({ ...input, configText, configDigest,
70
+ request: args['--task'] ? { ...input.request, task: args['--task'] } : input.request }, now);
71
+ process.stdout.write(args['--json'] ? `${JSON.stringify(result)}\n`
72
+ : `${result.stage}: ${result.selected?.hostId ?? 'no host'} (${result.reason})\n`);
73
+ return result.selected ? 0 : 2;
74
+ } catch (error) {
75
+ process.stderr.write(`${JSON.stringify({ error: { code: error.message.split(':')[0], message: error.message } })}\n`);
76
+ return 1;
77
+ }
78
+ }
@@ -0,0 +1,86 @@
1
+ // Pure supplied-facts API. No clock, filesystem, networking or dispatch.
2
+ import { parseFleetConfig, validateInput } from './fleet-config.js';
3
+ import { assignmentCounts, routeReasons, telemetry } from './fleet-facts.js';
4
+ export { parseFleetConfig } from './fleet-config.js';
5
+
6
+ export function recommend(input, now) {
7
+ validateInput(input, now);
8
+ const config = parseFleetConfig(input.configText, input.configDigest);
9
+ const counts = assignmentCounts(input, config.hosts, now);
10
+ const preference = Object.hasOwn(config.taskOrders, input.request.task)
11
+ ? config.taskOrders[input.request.task] : config.defaultOrder;
12
+ const eligible = [];
13
+ const excluded = [];
14
+ for (const hostId of preference) {
15
+ const host = config.hosts.find((h) => h.id === hostId);
16
+ const destination = input.destinations.find((d) => d.environmentId === host.environmentId);
17
+ const routes = config.routes.filter((r) => r.environmentId === host.environmentId
18
+ && r.originEnvironmentId === input.origin.environmentId);
19
+ let accepted = null;
20
+ for (const route of routes) {
21
+ const facts = input.routeFacts.find((f) => f.routeId === route.id);
22
+ const reasons = routeReasons(route, host, destination, facts, input, now);
23
+ if (reasons.length) {
24
+ excluded.push({ hostId, environmentId: host.environmentId, routeId: route.id, reasons,
25
+ exclusion: (Object.hasOwn(input.exclusions.hosts, hostId) ? input.exclusions.hosts[hostId] : null)
26
+ ?? (Object.hasOwn(input.exclusions.routes, route.id) ? input.exclusions.routes[route.id] : null),
27
+ evidence: { configuredRoute: route.evidence ?? null, observedRoute: facts?.evidence ?? null,
28
+ destination: destination?.evidence ?? null, destinationSnapshotId: destination?.snapshotId ?? null,
29
+ configuredCapabilities: host.capabilities, observedCapabilities: destination?.capabilities ?? null } });
30
+ } else if (!accepted) {
31
+ accepted = { hostId, environmentId: host.environmentId, projectId: destination.project.id,
32
+ repositoryId: destination.project.repositoryId, routeId: route.id, routeKind: route.kind,
33
+ destinationSnapshotId: destination.snapshotId, destinationObservedAt: destination.observedAt,
34
+ ...(destination.capabilitiesDigest !== undefined
35
+ ? { destinationCapabilitiesDigest: destination.capabilitiesDigest } : {}),
36
+ destinationEvidence: destination.evidence, routeEvidence: facts.evidence, versions: destination.versions,
37
+ execution: destination.execution, capabilities: destination.capabilities,
38
+ fallbackLimitBp: host.fallbackLimitBp, assignmentCount: counts.byEnvironment[host.environmentId],
39
+ telemetry: telemetry(input.telemetry.find((s) => s.environmentId === host.environmentId), now) };
40
+ }
41
+ }
42
+ if (accepted) eligible.push(accepted);
43
+ if (!routes.length) excluded.push({ hostId, environmentId: host.environmentId, routeId: null, reasons: ['route-missing'], exclusion: null });
44
+ }
45
+ const pool = eligible.filter((h) => config.pool.includes(h.hostId));
46
+ const measured = (h) => h.telemetry.status === 'measured';
47
+ const rank = (h) => preference.indexOf(h.hostId);
48
+ const byLoad = (a, b) => a.telemetry.loadBp - b.telemetry.loadBp || rank(a) - rank(b);
49
+ const rankingCounts = counts.pendingLowerBound ?? counts.byEnvironment;
50
+ const under = pool.filter((h) => measured(h) && h.telemetry.loadBp <= config.balanceLimitBp);
51
+ let stage; let reason; let selected = null;
52
+ if (!eligible.length) { stage = 'no-route'; reason = 'no-executable-route'; }
53
+ else if (under.length) {
54
+ stage = 'balance'; reason = 'pool-under-limit';
55
+ selected = under.sort((a, b) => rankingCounts[a.environmentId] - rankingCounts[b.environmentId] || byLoad(a, b))[0];
56
+ } else if (pool.length && pool.every((h) => !measured(h))) {
57
+ stage = 'pool-unknown'; reason = 'pool-all-unknown'; selected = pool[0];
58
+ } else {
59
+ stage = 'fallback';
60
+ selected = eligible.find((h) => measured(h) && h.telemetry.loadBp <= h.fallbackLimitBp);
61
+ if (selected) reason = 'first-within-limit';
62
+ else {
63
+ const observed = eligible.filter(measured).sort(byLoad);
64
+ selected = observed[0] ?? eligible[0];
65
+ reason = observed.length ? 'least-loaded-over-limit' : 'all-telemetry-unknown';
66
+ }
67
+ }
68
+ const flags = {
69
+ countsUnknown: counts.inventoryStatus === 'unknown', poolEmpty: pool.length === 0,
70
+ poolPartlyUnknown: pool.some(measured) && pool.some((h) => !measured(h)),
71
+ telemetryUnknown: eligible.some((h) => !measured(h)),
72
+ overloaded: selected ? measured(selected) && selected.telemetry.loadBp > selected.fallbackLimitBp : false,
73
+ localAuthorOnly: input.request.role === 'author' && eligible.length > 0
74
+ && eligible.every((h) => h.environmentId === input.origin.environmentId),
75
+ };
76
+ // Copy supplied nested facts so callers cannot mutate input through the result.
77
+ return structuredClone({ schemaVersion: 1, configDigest: input.configDigest, evaluatedAt: now,
78
+ originEnvironmentId: input.origin.environmentId, request: input.request,
79
+ stage, reason: flags.localAuthorOnly ? 'author-local-only' : reason, decisionReason: reason,
80
+ selected, flags, counts, eligible, excluded });
81
+ }
82
+
83
+ if (import.meta.main) {
84
+ const { main } = await import('./fleet-route-cli.js');
85
+ process.exitCode = await main(process.argv.slice(2));
86
+ }
@@ -0,0 +1,81 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { parseFleetConfig, recommend } from './fleet-route.js';
3
+ import { sampleFleet } from './fleet-sample.js';
4
+ import { sampleOptions, validateManifest } from './fleet-sample-manifest.js';
5
+ export const sampleHelp = `
6
+ Usage: bun fleet-route.js sample --fleet <manifest.json> --config-digest <sha256> [--config <path>] [--json]
7
+ bun fleet-route.js sample-recommend --fleet <manifest.json> --input <snapshot.json>
8
+ --config-digest <sha256> [--config <path>] [--json]
9
+ --fleet Schema-1 manifest; exact host/environment joins; local or SSH transport
10
+ --interval-ms Integer 5000..60000; default 5000; minimum gap between two reads
11
+ --timeout-ms Per-host whole-collection deadline; default 10000; interval+1000..120000
12
+ --config Same reviewed config/default path as recommend; explicit digest required
13
+ --input sample-recommend only: C1 access/project/capability/assignment facts
14
+ --json Full sample report or {input,evaluatedAt,recommendation} replay envelope
15
+
16
+ Sample commands read Linux /proc/stat and /proc/meminfo locally or through bounded,
17
+ noninteractive SSH argv to validated MagicDNS short names with existing identities.
18
+ They use collector receipt time, collect hosts concurrently, and write stdout/stderr only.
19
+ Window duration brackets transport plus reads and interval; it is not native history.
20
+ Unknown telemetry never grants a route. No dispatch, config mutation or retained state.
21
+ Exit codes: 0 sample/recommendation, 1 input/config error, 2 no executable route.
22
+ `;
23
+ const fail = (code) => { throw Error(code); };
24
+ function read(path, name, json = true) {
25
+ let text;
26
+ try { text = readFileSync(path, 'utf8'); } catch { fail(`missing-${name}`); }
27
+ if (!json) return text;
28
+ try { return JSON.parse(text); } catch { fail(`${name}-json`); }
29
+ }
30
+ export async function sampleMain(argv, defaultConfigPath) {
31
+ const command = argv[0]; const args = {};
32
+ const flags = ['--fleet', '--config', '--config-digest', '--interval-ms', '--timeout-ms', '--json',
33
+ ...(command === 'sample-recommend' ? ['--input', '--task'] : [])];
34
+ for (let i = 1; i < argv.length; i++) {
35
+ const flag = argv[i];
36
+ if (!flags.includes(flag)) fail('unexpected-argument');
37
+ if (Object.hasOwn(args, flag)) fail('duplicate-argument');
38
+ if (flag === '--json') { args[flag] = true; continue; }
39
+ const value = argv[++i];
40
+ if (!value?.trim() || value.startsWith('--')) fail('missing-argument');
41
+ args[flag] = value;
42
+ }
43
+ if (!args['--fleet'] || (command === 'sample-recommend' && !args['--input'])) fail('missing-argument');
44
+ const numeric = (name, fallback, reason) => {
45
+ if (args[name] === undefined) return fallback;
46
+ if (!/^\d+$/.test(args[name])) fail(reason);
47
+ return Number(args[name]);
48
+ };
49
+ const options = sampleOptions({ intervalMs: numeric('--interval-ms', 5000, 'sample-interval'),
50
+ timeoutMs: numeric('--timeout-ms', 10000, 'sample-timeout') });
51
+ const configPath = args['--config'] ?? defaultConfigPath;
52
+ const configText = read(configPath, 'config', false);
53
+ const configDigest = args['--config-digest'];
54
+ const config = parseFleetConfig(configText, configDigest);
55
+ const manifest = validateManifest(read(args['--fleet'], 'manifest'), config.hosts);
56
+ let input;
57
+ if (command === 'sample-recommend') {
58
+ input = read(args['--input'], 'input');
59
+ if (input?.configDigest !== undefined && input.configDigest !== configDigest) fail('config-digest-mismatch');
60
+ if (input?.configText !== undefined && input.configText !== configText) fail('config-snapshot-mismatch');
61
+ input = { ...input, configText, configDigest, request: args['--task']
62
+ ? { ...input.request, task: args['--task'] } : input?.request };
63
+ recommend(input, Date.now()); // Validate supplied security facts before any network read.
64
+ }
65
+ const controller = new AbortController();
66
+ const cancel = () => controller.abort();
67
+ process.once('SIGINT', cancel); process.once('SIGTERM', cancel);
68
+ let samples;
69
+ try { samples = await sampleFleet(manifest, config.hosts, options, { signal: controller.signal }); }
70
+ finally { process.removeListener('SIGINT', cancel); process.removeListener('SIGTERM', cancel); }
71
+ const evaluatedAt = Date.now();
72
+ if (command === 'sample') {
73
+ process.stdout.write(`${JSON.stringify({ ...samples, telemetry: samples.telemetry.map((s) => ({ ...s, ageMs: evaluatedAt - s.receivedAt })),
74
+ configDigest, collectedAt: evaluatedAt })}\n`);
75
+ return 0;
76
+ }
77
+ input = { ...input, telemetry: samples.telemetry };
78
+ const recommendation = recommend(input, evaluatedAt);
79
+ process.stdout.write(`${JSON.stringify({ schemaVersion: 1, input, evaluatedAt, recommendation })}\n`);
80
+ return recommendation.selected ? 0 : 2;
81
+ }
@@ -0,0 +1,59 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ export const PROC_COMMAND = "cat /proc/stat && printf '\\n---AXSTACK-MEM---\\n' && cat /proc/meminfo";
3
+ export function sshArgv(host) {
4
+ return ['ssh', '-T', '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=yes', '-o', 'IdentitiesOnly=yes',
5
+ '-o', 'ConnectTimeout=3', '-o', 'ConnectionAttempts=1', '-o', 'ServerAliveInterval=2',
6
+ '-o', 'ServerAliveCountMax=1', '-o', 'ControlMaster=no', '-o', 'ControlPath=none',
7
+ '-o', 'ClearAllForwardings=yes', '-o', 'PermitLocalCommand=no',
8
+ ...(host.user ? ['-l', host.user] : []), '--', host.destination, PROC_COMMAND];
9
+ }
10
+ export function wait(ms, signal) {
11
+ return new Promise((resolve, reject) => {
12
+ const cancel = () => { clearTimeout(timer); signal.removeEventListener('abort', cancel); reject(signal.reason); };
13
+ const timer = setTimeout(() => { signal.removeEventListener('abort', cancel); resolve(); }, ms);
14
+ signal.addEventListener('abort', cancel, { once: true });
15
+ if (signal.aborted) cancel();
16
+ });
17
+ }
18
+ async function limited(stream, signal) {
19
+ const reader = stream.getReader(); const chunks = []; let size = 0;
20
+ const cancel = () => { void reader.cancel(); };
21
+ signal.addEventListener('abort', cancel, { once: true });
22
+ try {
23
+ while (true) {
24
+ if (signal.aborted) throw signal.reason;
25
+ const { done, value } = await reader.read();
26
+ if (done) break;
27
+ size += value.byteLength;
28
+ if (size > 65536) throw Error('malformed');
29
+ chunks.push(value);
30
+ }
31
+ return new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks));
32
+ } finally {
33
+ signal.removeEventListener('abort', cancel);
34
+ await reader.cancel();
35
+ reader.releaseLock();
36
+ }
37
+ }
38
+ export async function readProc(host, signal, spawn = Bun.spawn) {
39
+ if (host.transport === 'local') {
40
+ const stat = await readFile('/proc/stat', { encoding: 'utf8', signal });
41
+ const mem = await readFile('/proc/meminfo', { encoding: 'utf8', signal });
42
+ return `${stat}\n---AXSTACK-MEM---\n${mem}`;
43
+ }
44
+ const child = spawn(sshArgv(host), { stdin: 'ignore', stdout: 'pipe', stderr: 'ignore' });
45
+ const cancel = () => child.kill('SIGKILL');
46
+ signal.addEventListener('abort', cancel, { once: true });
47
+ try {
48
+ if (signal.aborted) { cancel(); throw signal.reason; }
49
+ const output = await limited(child.stdout, signal);
50
+ const code = await child.exited;
51
+ if (signal.aborted) throw signal.reason;
52
+ if (code !== 0) throw Error('failed');
53
+ return output;
54
+ } finally {
55
+ signal.removeEventListener('abort', cancel);
56
+ child.kill('SIGKILL');
57
+ await child.exited;
58
+ }
59
+ }
@@ -0,0 +1,34 @@
1
+ import { record } from './fleet-config.js';
2
+ const fail = (reason) => { throw Error(reason); };
3
+ const keys = (obj, allowed) => record(obj) && Object.keys(obj).every((k) => allowed.includes(k));
4
+ export function validateManifest(manifest, hosts) {
5
+ if (manifest?.schemaVersion !== 1) fail('schema-version: manifest');
6
+ if (!keys(manifest, ['schemaVersion', 'hosts']) || !Array.isArray(manifest.hosts)
7
+ || !manifest.hosts.length || manifest.hosts.length > 64) fail('manifest-schema');
8
+ const ids = new Set(); const destinations = new Set(); let local = false;
9
+ for (const h of manifest.hosts) {
10
+ if (!keys(h, ['hostId', 'environmentId', 'transport', 'destination', 'user'])) fail('manifest-schema');
11
+ if (!hosts.some((c) => c.id === h.hostId && c.environmentId === h.environmentId)) fail('manifest-host');
12
+ if (ids.has(h.hostId)) fail('manifest-duplicate');
13
+ ids.add(h.hostId);
14
+ if (!['local', 'ssh'].includes(h.transport)) fail('manifest-transport');
15
+ if (h.transport === 'local') {
16
+ if (h.destination !== undefined || h.user !== undefined) fail('manifest-schema');
17
+ if (local) fail('manifest-duplicate');
18
+ local = true;
19
+ } else {
20
+ // MagicDNS short names only. SSH aliases, IPs, ports and shell syntax are rejected.
21
+ if (typeof h.destination !== 'string' || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(h.destination)
22
+ || /^\d+$/.test(h.destination)) fail('manifest-destination');
23
+ if (h.user !== undefined && (typeof h.user !== 'string' || !/^[a-z_][a-z0-9_-]{0,31}$/.test(h.user))) fail('manifest-user');
24
+ if (destinations.has(h.destination)) fail('manifest-duplicate');
25
+ destinations.add(h.destination);
26
+ }
27
+ }
28
+ return manifest;
29
+ }
30
+ export function sampleOptions({ intervalMs = 5000, timeoutMs = 10000 } = {}) {
31
+ if (!Number.isSafeInteger(intervalMs) || intervalMs < 5000 || intervalMs > 60000) fail('sample-interval');
32
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < intervalMs + 1000 || timeoutMs > 120000) fail('sample-timeout');
33
+ return { intervalMs, timeoutMs };
34
+ }
@@ -0,0 +1,48 @@
1
+ import { parseProc, pressure } from './fleet-proc.js';
2
+ import { validateManifest, sampleOptions } from './fleet-sample-manifest.js';
3
+ import { readProc, wait } from './fleet-sample-io.js';
4
+ export { parseProc, pressure, validateManifest };
5
+
6
+ // Read functions and clocks are injectable for deterministic offline verification.
7
+ export async function sampleFleet(manifest, hosts, options, dependencies = {}) {
8
+ validateManifest(manifest, hosts);
9
+ const { intervalMs, timeoutMs } = sampleOptions(options);
10
+ const { clock = Date.now, monotonic = () => performance.now(), read = readProc, wait: pause = wait,
11
+ signal } = dependencies;
12
+ const telemetry = await Promise.all(manifest.hosts.map(async (host) => {
13
+ const controller = new AbortController();
14
+ const cancel = () => controller.abort(Error('cancelled'));
15
+ signal?.addEventListener('abort', cancel, { once: true });
16
+ if (signal?.aborted) cancel();
17
+ const timer = setTimeout(() => controller.abort(Error('timeout')), timeoutMs);
18
+ const windowStartedAt = clock(); const started = monotonic();
19
+ const source = host.transport === 'local' ? 'linux-proc-local' : 'linux-proc-ssh';
20
+ const provenance = { hostId: host.hostId, environmentId: host.environmentId, scope: 'whole-host', source,
21
+ intervalMs, timeoutMs, windowStartedAt, windowKind: 'collector-bracket' };
22
+ const check = () => { if (controller.signal.aborted) throw controller.signal.reason; };
23
+ try {
24
+ check();
25
+ const first = parseProc(await read(host, controller.signal));
26
+ check();
27
+ await pause(intervalMs, controller.signal);
28
+ check();
29
+ const last = parseProc(await read(host, controller.signal));
30
+ check();
31
+ const windowEndedAt = clock(); const durationMs = Math.floor(monotonic() - started);
32
+ if (durationMs >= timeoutMs) throw Error('timeout');
33
+ if (durationMs < intervalMs || windowEndedAt < windowStartedAt
34
+ || windowEndedAt - windowStartedAt < durationMs
35
+ || windowEndedAt - windowStartedAt > durationMs + 1000) throw Error('collector-clock');
36
+ return { ...provenance, status: 'ok', ...pressure(first, last), durationMs,
37
+ windowEndedAt, receivedAt: windowEndedAt };
38
+ } catch (error) {
39
+ const reason = controller.signal.aborted ? controller.signal.reason?.message : error.message;
40
+ return { ...provenance, status: ['timeout', 'cancelled', 'malformed', 'inconsistent-counters', 'collector-clock']
41
+ .includes(reason) ? reason : 'failed', durationMs: Math.max(0, Math.floor(monotonic() - started)), receivedAt: clock() };
42
+ } finally {
43
+ clearTimeout(timer);
44
+ signal?.removeEventListener('abort', cancel);
45
+ }
46
+ }));
47
+ return { schemaVersion: 1, telemetry };
48
+ }
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
3
3
  const bindings = { codex: 'codex', claude: 'claudeAgent', grok: 'grok' };
4
4
  const effortIds = { codex: 'reasoningEffort', claude: 'effort', grok: 'reasoningEffort' };
5
5
  const nonempty = (value) => typeof value === 'string' && value.trim().length > 0;
6
+ const optionValues = (option) => option.values ?? option.options?.map((value) => value.id);
6
7
 
7
8
  function argumentsFrom(args) {
8
9
  const options = { excluded: [] };
@@ -37,9 +38,11 @@ function providerFrom(catalog, provider) {
37
38
  }
38
39
  const instance = matches[0];
39
40
  if (!Array.isArray(instance.models) || instance.models.some((model) =>
40
- !model || !nonempty(model.id) || !Array.isArray(model.options) || model.options.some((option) =>
41
+ !model || !nonempty(model.id) || !Array.isArray(model.options ?? []) || (model.options ?? []).some((option) =>
41
42
  !option || !nonempty(option.id) || (option.options !== undefined &&
42
- (!Array.isArray(option.options) || option.options.some((value) => !value || !nonempty(value.id))))))) {
43
+ (!Array.isArray(option.options) || option.options.some((value) => !value || !nonempty(value.id))))
44
+ || (option.values !== undefined && (!Array.isArray(option.values)
45
+ || option.values.some((value) => typeof value !== 'boolean' && !nonempty(value))))))) {
43
46
  throw new Error('malformed capabilities models or options');
44
47
  }
45
48
  return instance;
@@ -58,6 +61,7 @@ function modelFrom(models, { provider, model: pin, class: modelClass, excluded }
58
61
  }
59
62
  const model = models[0];
60
63
  if (!model || excluded.includes(model.id)) throw new Error('missing first listed model');
64
+ if (!model.id.startsWith('grok-')) throw new Error(`first listed model ${model.id} is not grok-family`);
61
65
  return model;
62
66
  }
63
67
  if (!['codex', 'claude'].includes(provider) || !/^[a-z]+$/.test(modelClass)) {
@@ -90,8 +94,8 @@ try {
90
94
  if ((options.class === 'terra' || /^gpt-\d+(?:\.\d+)*-terra$/.test(model.id)) && effort !== 'high') {
91
95
  throw new Error('Terra requires exactly high effort');
92
96
  }
93
- const effortOptions = model.options.filter((option) => option.id === effortIds[provider]);
94
- if (effortOptions.length !== 1 || !effortOptions[0].options?.some((value) => value.id === effort)
97
+ const effortOptions = (model.options ?? []).filter((option) => option.id === effortIds[provider]);
98
+ if (effortOptions.length !== 1 || !optionValues(effortOptions[0])?.includes(effort)
95
99
  || (provider === 'grok' && effort === 'max')) {
96
100
  throw new Error(`unsupported effort ${effort} for ${provider}/${model.id}`);
97
101
  }
@@ -43,6 +43,27 @@ Preserve unknown files, unmerged author work, ambiguous publication, and evidenc
43
43
  Do not force removal, bulk-clean, edit a runtime database, or add a scheduler,
44
44
  daemon or state machine. Report other projects' resources; never touch them.
45
45
 
46
+ ## Wait registrations and owned wakes
47
+
48
+ Follow [Bounded recovery boundary](../axstack/references/t3-runtime.md#bounded-recovery-boundary)
49
+ and [Waiting-turn and closing records](../axstack/references/run-record.md#waiting-turn-and-closing-records)
50
+ when ending an owner condition. On completion/cancellation/handoff, reconcile
51
+ ownership and delete only run-owned schedule IDs with absence readback.
52
+ Stop owned watches and clear only identified owned duplicate scheduled messages,
53
+ recording exact IDs, scope, owner, lifetime and receipts. Preserve user messages
54
+ and shared persistent schedules. Run completion unregisters only its own
55
+ registration and preserves the shared service and other registrations.
56
+ Without separately authorized shared-service lifetime, recovery ends at run
57
+ close, empty scope or expiry, whichever comes first. Empty scope means no open
58
+ wait registrations, rather than no eligible action in this pass. After expiry,
59
+ perform cleanup-only with zero sends even if deletion failed.
60
+ Unknown deletion or queue state is a scoped cleanup finding, never a reason to
61
+ redo product work. Keep that resource ID and its cleanup owner until reconciled.
62
+ Verify Stop/restart/provider-failure queue behavior before promising a timer
63
+ wake; a timer cannot override a held queue. Use advertised interval/fixed-time/
64
+ webhook operations, never an invented dedicated one-shot trigger. Recovery owns
65
+ only its own resources; each Driver cleans its own watches, queues and schedules.
66
+
46
67
  ## Reconcile each candidate
47
68
 
48
69
  Take a fresh native inventory and bind every candidate to its exact projectId, attempt key, taskId/childThreadId/childRunId
@@ -55,6 +55,8 @@ For substantive delegated or resumable work, use the shared
55
55
  [run record](../axstack/references/run-record.md). Reconcile it on restart with
56
56
  the approved scope, T3 threads/runs and forge state, exact revisions, tickets, and watches.
57
57
  Reuse valid owners and authors; ambiguity holds a replacement writer.
58
+ When a logical Coordinator coordinates project drivers, follow
59
+ [Coordinating multiple drivers](../axstack/references/t3-runtime.md#coordinating-multiple-drivers).
58
60
 
59
61
  At execution start, bind work to the T3 driver thread and one authoritative
60
62
  dispatch attempt. Preserve the actual thread/task/run IDs and process completion
@@ -281,8 +283,10 @@ For each PR:
281
283
  Load and follow [Round-3 step-back](references/step-back.md).
282
284
  Load and follow [Review-round budgets](../axstack/references/run-record.md#review-round-budgets) before dispatching any repair from the step-back.
283
285
 
284
- The T3 run watch reconciles every unsettled dispatch attempt; the bounded
285
- forge check wait is the only other implementation wait.
286
+ The T3 run watch reconciles unsettled launched-fallback Writer runs; native
287
+ completion delivers delegated receipts. Never status-check running delegate_task
288
+ children on scheduled wakes. The bounded forge check wait is the only other
289
+ implementation wait.
286
290
  After verified readback of every own PR publication, the driver arms or joins
287
291
  its maintain-mode chat-run watch under [Autopilot](../axstack/references/autopilot.md).
288
292
  That watch owns the bound T3 schedule wake; its runtime sets the cadence.
@@ -290,10 +294,13 @@ A turn with unsettled launched threads must end only under the bound-watch rule
290
294
  With settled threads, end a turn only when every required PR is `merge-ready` or `held`.
291
295
  Follow [Route each wake](../axstack-watch/SKILL.md#4-route-each-wake).
292
296
  Follow [Advised holds](../axstack/references/contracts.md#advised-holds) when raising eligible holds.
293
- Raise user-decision and serious-risk holds in the T3 driver thread and end the
294
- turn waiting on the decision. Serious risks are raised immediately.
297
+ Raise user-decision and serious-risk holds in the owning T3 driver thread and
298
+ end the turn waiting on the decision when all remaining authorized work is held.
299
+ Continue independent authorized work under Autopilot's scoped-hold rules.
300
+ Serious risks are raised immediately.
295
301
  After bounded safe recovery, raise a genuine blocked operation that still needs
296
- the user in the T3 driver thread, and the driver ends its turn waiting on it.
302
+ the user in the owning T3 driver thread, and the driver ends its turn waiting on it
303
+ when all remaining authorized work is held.
297
304
 
298
305
  Merge-ready opens the merge boundary. Own PRs default to automatic merge under
299
306
  [watch §5](../axstack-watch/SKILL.md#5-state-readiness-precisely).
@@ -137,7 +137,7 @@ This section applies to peer and authored PR modes.
137
137
  For an `axstack-verify create` PR, the reviewer drives the candidate's recipe because the base has none.
138
138
  The author never performs this review drive.
139
139
  The reviewer drives each affected feature and records that drive as the `Final-head check`.
140
- Delegate browser drives to `axstack-ui-verifier` with the feature file path.
140
+ Delegate browser drives to `axstack-ui-verifier` with the feature file path; device drives follow [UI verification](../../axstack/references/ui-verification.md#android-device-and-webpwa-dual-run)'s dual-run trigger.
141
141
  Running the applicable surface counts as spin-up; a CLI run qualifies.
142
142
  Use loopback and isolated instances under [axstack-verify](../../axstack-verify/SKILL.md#isolate-every-attempt).
143
143