axstack 0.55.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.
- package/docs/concepts.md +3 -1
- package/docs/installation.md +5 -2
- package/docs/workflows.md +3 -1
- package/package.json +1 -1
- package/skills/axstack/references/fleet-dispatch.md +166 -0
- package/skills/axstack/references/fleet-route-schema.md +11 -0
- package/skills/axstack/references/fleet-verification.md +102 -0
- package/skills/axstack/references/lifecycle.md +19 -2
- package/skills/axstack/references/role-roster.md +6 -0
- package/skills/axstack/references/routing.md +12 -0
- package/skills/axstack/references/run-record.md +83 -0
- package/skills/axstack/references/t3-runtime.md +173 -6
- package/skills/axstack/references/workspace-hygiene.md +8 -0
- package/skills/axstack/scripts/dispatch-plan.js +19 -2
- package/skills/axstack/scripts/fleet-destination.js +39 -0
- package/skills/axstack/scripts/fleet-dispatch.js +6 -0
- package/skills/axstack/scripts/fleet-facts.js +12 -1
- package/skills/axstack/scripts/fleet-inventory.js +76 -0
- package/skills/axstack/scripts/fleet-receipt.js +130 -0
- package/skills/axstack/scripts/fleet-route.js +4 -3
- package/skills/axstack-cleanup/SKILL.md +21 -0
- package/skills/axstack-implement/SKILL.md +12 -5
- package/skills/axstack-watch/SKILL.md +3 -0
- package/skills/axstack-watch/references/watch-runtime.md +18 -5
- package/skills/voa/SKILL.md +96 -0
- package/skills/voa/references/observations.md +79 -0
- package/skills/voa/references/recovery.md +94 -0
- package/skills/voa/references/verification.md +35 -0
- package/skills/voa/scripts/cleanup.js +39 -0
- package/skills/voa/scripts/evaluate.js +122 -0
- package/skills/voa/scripts/observations.js +68 -0
- package/skills/voa/scripts/records.js +48 -0
- package/skills/voa/scripts/voa.js +43 -0
- package/src/installer.js +3 -2
- package/src/instructions.js +11 -3
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const HOSTS = ['io', 'iobook', 'iobox', 'iosatellite', 'axat-vps'];
|
|
2
|
+
export const activeRuns = new Set(['queued', 'preparing', 'starting', 'running', 'waiting']);
|
|
3
|
+
export const pointer = (value) => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9:/._#?=&%+@-]{0,1023}$/.test(value);
|
|
4
|
+
export const instant = (value) => typeof value === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(value)
|
|
5
|
+
&& Number.isFinite(Date.parse(value));
|
|
6
|
+
const threadLink = (value) => typeof value === 'string' && (pointer(value) && value.startsWith('t3-thread://')
|
|
7
|
+
|| value.startsWith('<t3-thread://') && value.endsWith('>') && pointer(value.slice(1, -1)));
|
|
8
|
+
export const identity = (owner) => owner && HOSTS.includes(owner.host)
|
|
9
|
+
&& ['environmentId', 'threadId'].every((key) => pointer(owner[key])) && threadLink(owner.threadLink);
|
|
10
|
+
export const sameOwner = (a, b) => identity(a) && identity(b)
|
|
11
|
+
&& ['host', 'environmentId', 'threadId', 'threadLink'].every((key) => a[key] === b[key]);
|
|
12
|
+
export const sameKey = (a, b) => Array.isArray(a) && Array.isArray(b) && a.length === 3 && b.length === 3
|
|
13
|
+
&& a.every((value, index) => value === b[index]);
|
|
14
|
+
export const validKey = (key) => Array.isArray(key) && key.length === 3 && HOSTS.includes(key[0]) && key.every(pointer);
|
|
15
|
+
export const term = (name, state, evidence) => ({ term: name, state, evidence });
|
|
16
|
+
|
|
17
|
+
export function threadStatus(thread, at) {
|
|
18
|
+
let state = 'unknown';
|
|
19
|
+
if (Number.isInteger(thread.pendingRequests) && thread.pendingRequests > 0) state = 'needs-you';
|
|
20
|
+
else if (activeRuns.has(thread.run)) state = 'working';
|
|
21
|
+
else if (['failed', 'interrupted'].includes(thread.run)) state = 'failed';
|
|
22
|
+
else if (['completed', 'idle'].includes(thread.run) && thread.pendingRequests === 0) state = 'idle';
|
|
23
|
+
const last = Array.isArray(thread.messages) ? thread.messages.filter((m) => m.role === 'assistant').at(-1) : null;
|
|
24
|
+
return {
|
|
25
|
+
state, lastRun: thread.run ?? null, pendingRequests: thread.pendingRequests ?? null,
|
|
26
|
+
idleHours: instant(thread.updatedAt) ? Math.max(0, Math.round((at - Date.parse(thread.updatedAt)) / 360000) / 10) : null,
|
|
27
|
+
branch: thread.branch ?? null, prs: Array.isArray(thread.prs) ? thread.prs : [],
|
|
28
|
+
lastMessage: typeof last?.text === 'string' ? last.text.replace(/\s+/g, ' ').slice(0, 300) : '',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalize(input) {
|
|
33
|
+
if (input?.schemaVersion !== 1 || !instant(input.at) || !instant(input.collectedAt)
|
|
34
|
+
|| !Array.isArray(input.hosts)) throw Error('need schemaVersion 1, ISO at/collectedAt and hosts array');
|
|
35
|
+
const budget = input.budget;
|
|
36
|
+
if (!budget || !['maxOwners', 'maxMessages'].every((key) => Number.isInteger(budget[key])
|
|
37
|
+
&& budget[key] > 0 && budget[key] <= 1000) || !instant(budget.deadlineAt)) throw Error('invalid finite budget');
|
|
38
|
+
const at = Date.parse(input.at);
|
|
39
|
+
const duration = Date.parse(budget.deadlineAt) - Date.parse(input.collectedAt);
|
|
40
|
+
const inTime = at >= Date.parse(input.collectedAt) && at < Date.parse(budget.deadlineAt)
|
|
41
|
+
&& duration > 0 && duration < 1800000;
|
|
42
|
+
const coverage = [];
|
|
43
|
+
const threads = [];
|
|
44
|
+
let next = null;
|
|
45
|
+
for (const host of HOSTS) {
|
|
46
|
+
const matches = input.hosts.filter((row) => row.host === host);
|
|
47
|
+
const row = matches.length === 1 ? matches[0] : null;
|
|
48
|
+
const known = row?.reachable === true && row?.paginationComplete === true
|
|
49
|
+
&& pointer(row.environmentId) && pointer(row.evidence) && Array.isArray(row.threads);
|
|
50
|
+
coverage.push(term(host, known ? 'pass' : 'unknown', row?.evidence ?? 'missing/unreachable/incomplete inventory'));
|
|
51
|
+
const rows = Array.isArray(row?.threads) ? row.threads : [];
|
|
52
|
+
const remaining = inTime ? budget.maxOwners - threads.length : 0;
|
|
53
|
+
const processed = rows.slice(0, remaining);
|
|
54
|
+
if (processed.length < rows.length && next === null) next = { host, rowIndex: processed.length };
|
|
55
|
+
const counts = new Map();
|
|
56
|
+
for (const thread of processed) counts.set(thread.threadId, (counts.get(thread.threadId) ?? 0) + 1);
|
|
57
|
+
// Observed rows on partial hosts remain visible, but cannot prove recovery readiness.
|
|
58
|
+
for (const thread of processed) {
|
|
59
|
+
const owner = { host, environmentId: row.environmentId, threadId: thread.threadId, threadLink: thread.threadLink };
|
|
60
|
+
threads.push({ owner, inventoryKnown: known && processed.length === rows.length && counts.get(thread.threadId) === 1,
|
|
61
|
+
raw: thread, ...threadStatus(thread, at) });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const observed = input.hosts.reduce((sum, row) => sum + (Array.isArray(row.threads) ? row.threads.length : 0), 0);
|
|
65
|
+
coverage.push(term('pass-budget', inTime && observed <= threads.length ? 'pass' : 'unknown',
|
|
66
|
+
`${threads.length}/${observed} observed rows; deadline ${budget.deadlineAt}`));
|
|
67
|
+
return { at, coverage, threads, cursor: { processedOwners: threads.length, next } };
|
|
68
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { identity, instant, pointer, sameOwner } from './observations.js';
|
|
2
|
+
|
|
3
|
+
const text = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
4
|
+
const referenceList = (value) => Array.isArray(value) && value.length > 0 && value.every(pointer);
|
|
5
|
+
const waitKinds = new Set(['active-worker', 'own-pr-ci-review', 'external-dependency', 'human-approval',
|
|
6
|
+
'explicit-pause', 'failed-interrupted', 'completed-cleanup', 'unknown']);
|
|
7
|
+
|
|
8
|
+
export function currentRecord(thread, owner, maxMessages) {
|
|
9
|
+
if (thread.historyKnown !== true || !Array.isArray(thread.messages) || thread.messages.length > maxMessages) return null;
|
|
10
|
+
// Native roles and authorship are supplied by the caller, never reconstructed from text.
|
|
11
|
+
const last = thread.messages.at(-1);
|
|
12
|
+
if (last?.role !== 'assistant' || last.source !== 'native' || !sameOwner(last.author, owner) || !pointer(last.id)
|
|
13
|
+
|| !pointer(thread.latestRunId) || last.runId !== thread.latestRunId
|
|
14
|
+
|| typeof last.text !== 'string') return null;
|
|
15
|
+
let record = null;
|
|
16
|
+
for (const match of last.text.matchAll(/(?:^|\n)```json\s*\n([\s\S]*?)\n```(?=\n|$)/g)) {
|
|
17
|
+
try { record = JSON.parse(match[1]); } catch { record = null; }
|
|
18
|
+
}
|
|
19
|
+
if (record?.schemaVersion !== 1 || !sameOwner(record.owner, owner) || !pointer(record.conditionId)
|
|
20
|
+
|| !text(record.diagnosis) || !text(record.authority?.scope) || !pointer(record.authority?.evidence)
|
|
21
|
+
|| !text(record.nextAction?.action) || !text(record.nextAction?.mode) || !pointer(record.nextAction?.evidence)
|
|
22
|
+
|| !text(record.wake?.mechanism) || !(record.wake.id === null || pointer(record.wake.id))
|
|
23
|
+
|| !text(record.wake.scope) || !text(record.lifetime?.scope) || !instant(record.lifetime.expiresAt)
|
|
24
|
+
|| !identity(record.cleanupOwner)) return null;
|
|
25
|
+
if (record.recordType === 'closing') {
|
|
26
|
+
return ['resolved', 'completed', 'cancelled', 'accepted-handoff'].includes(record.outcome)
|
|
27
|
+
&& referenceList(record.evidence) && Array.isArray(record.cleanup) ? record : null;
|
|
28
|
+
}
|
|
29
|
+
if (record.recordType !== 'waiting-turn' || !waitKinds.has(record.waitKind)
|
|
30
|
+
|| !text(record.dependency?.kind) || !pointer(record.dependency?.id) || !referenceList(record.dependency?.evidence)
|
|
31
|
+
|| !instant(record.lastMeaningfulProgress?.at) || !pointer(record.lastMeaningfulProgress?.evidence)
|
|
32
|
+
|| typeof record.pause !== 'boolean' || !(record.snoozeUntil === null || instant(record.snoozeUntil))
|
|
33
|
+
|| !(record.nextEligibleCheck === null || instant(record.nextEligibleCheck))) return null;
|
|
34
|
+
return record;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function condition(record, blocker, at) {
|
|
38
|
+
if (record?.recordType !== 'waiting-turn' || !blocker
|
|
39
|
+
|| blocker.nonHuman !== true || blocker.kind !== record.dependency.kind || blocker.id !== record.dependency.id
|
|
40
|
+
|| !['check', 'review', 'upstream-pr', 'dependency'].includes(blocker.kind)
|
|
41
|
+
|| !pointer(blocker.evidence) || !instant(blocker.inspectedAt)
|
|
42
|
+
|| Date.parse(blocker.inspectedAt) > at || Date.parse(blocker.inspectedAt) < at - 1800000) return null;
|
|
43
|
+
if (record.waitKind === 'external-dependency' && blocker.state === 'cleared') return 'dependency-cleared';
|
|
44
|
+
if (['own-pr-ci-review', 'external-dependency', 'human-approval'].includes(record.waitKind)
|
|
45
|
+
&& record.diagnosis === 'none' && blocker.state === 'unexplained'
|
|
46
|
+
&& record.nextAction.mode === 'read-only') return 'missing-diagnosis';
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { evaluate } from './evaluate.js';
|
|
3
|
+
|
|
4
|
+
const HELP = `VOA saved-observation preview (Bun >=1.3.14)
|
|
5
|
+
Usage: bun voa.js [--mode status|recovery|cleanup] --input <saved.json>
|
|
6
|
+
--mode Defaults to read-only status. Recovery/cleanup require explicit scope evidence.
|
|
7
|
+
--input One saved schemaVersion:1 JSON file, at most 8 MiB.
|
|
8
|
+
--help Show flags and side effects.
|
|
9
|
+
Reads only the given file; writes JSON to stdout and errors to stderr.
|
|
10
|
+
No collection, network, sends, dispatch, scheduling, workflow selection, deletion or state.
|
|
11
|
+
Terms are pass/fail/unknown/n/a with evidence, never an aggregate authority verdict.
|
|
12
|
+
Exit: 0 complete report (including failed criteria), 1 invalid input, 2 incomplete/unknown.
|
|
13
|
+
See ../references/observations.md and ../references/recovery.md.
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export async function main(args) {
|
|
17
|
+
if (args.length === 1 && args[0] === '--help') { process.stdout.write(HELP); return 0; }
|
|
18
|
+
let mode = 'status';
|
|
19
|
+
let path;
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
22
|
+
const flag = args[index];
|
|
23
|
+
const value = args[index + 1];
|
|
24
|
+
if (!['--mode', '--input'].includes(flag) || !value || value.startsWith('--') || seen.has(flag)) {
|
|
25
|
+
throw Error('use --help; unknown, repeated or missing flag value');
|
|
26
|
+
}
|
|
27
|
+
seen.add(flag);
|
|
28
|
+
if (flag === '--mode') mode = value;
|
|
29
|
+
else path = value;
|
|
30
|
+
}
|
|
31
|
+
if (!path) throw Error('--input is required');
|
|
32
|
+
const file = Bun.file(path);
|
|
33
|
+
if (file.size > 8 * 1024 * 1024) throw Error('input exceeds 8 MiB');
|
|
34
|
+
const result = evaluate(await file.json(), mode);
|
|
35
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
36
|
+
const terms = [...result.coverage, ...result.owners.flatMap((owner) => owner.terms), ...(result.cleanup?.terms ?? [])];
|
|
37
|
+
return terms.some((entry) => entry.state === 'unknown') ? 2 : 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (import.meta.main) {
|
|
41
|
+
try { process.exitCode = await main(Bun.argv.slice(2)); }
|
|
42
|
+
catch (error) { process.stderr.write(`voa: ${error.message}\n`); process.exitCode = 1; }
|
|
43
|
+
}
|
package/src/installer.js
CHANGED
|
@@ -239,10 +239,11 @@ export async function validateBundle(bundleDir, selectedPreset = null) {
|
|
|
239
239
|
throw new Error('bundle skills/ must be a real directory, not a symlink');
|
|
240
240
|
}
|
|
241
241
|
const entries = await readdir(skillsRoot, { withFileTypes: true });
|
|
242
|
+
const isShippedSkill = (name) => name.startsWith('axstack') || name === 'voa';
|
|
242
243
|
// Symlinked (or non-directory) top-level skill entries must be rejected,
|
|
243
244
|
// not silently skipped: Dirent.isDirectory() is false for symlinks.
|
|
244
245
|
for (const entry of entries) {
|
|
245
|
-
if (!entry.name
|
|
246
|
+
if (!isShippedSkill(entry.name)) continue;
|
|
246
247
|
const entryStat = await lstat(join(skillsRoot, entry.name));
|
|
247
248
|
if (entryStat.isSymbolicLink() || !entryStat.isDirectory()) {
|
|
248
249
|
throw new Error(
|
|
@@ -250,7 +251,7 @@ export async function validateBundle(bundleDir, selectedPreset = null) {
|
|
|
250
251
|
);
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
|
-
const skillDirs = entries.filter((e) => e.isDirectory() && e.name
|
|
254
|
+
const skillDirs = entries.filter((e) => e.isDirectory() && isShippedSkill(e.name));
|
|
254
255
|
if (skillDirs.length === 0) {
|
|
255
256
|
throw new Error('bundle contains no axstack-* skill directories under skills/');
|
|
256
257
|
}
|
package/src/instructions.js
CHANGED
|
@@ -11,10 +11,18 @@ export function renderInstructionBlock() {
|
|
|
11
11
|
'`axstack-implement` loops author -> review -> repair until every PR is merge-ready.',
|
|
12
12
|
'Route every subagent, delegated worker, reviewer, and cross-harness dispatch through T3 Code orchestration using the `t3-code` MCP.',
|
|
13
13
|
'Use `delegate_task` for every role, including authors with `isolation:"worktree"`.',
|
|
14
|
-
'Use `t3_thread_launch` for top-level drivers.',
|
|
15
|
-
'Use `t3_thread_launch` as a rare author fallback only when worktree isolation is unavailable and record each use in the run record.',
|
|
14
|
+
'Use `t3_thread_launch` for top-level drivers only on explicit user request for a new thread.',
|
|
15
|
+
'Use `t3_thread_launch` as a rare author fallback only when worktree isolation is unavailable following an explicit user request for a new thread and record each use in the run record.',
|
|
16
16
|
'Follow `references/t3-runtime.md` in the installed `axstack` skill for the runtime contract.',
|
|
17
|
-
'
|
|
17
|
+
'Before every subsequent dispatch of delegated workers and reviewers and every explicitly authorized top-level launch, follow `references/fleet-dispatch.md` and `references/fleet-route-schema.md` in the installed `axstack` skill with current routing facts.',
|
|
18
|
+
'Refresh routing facts immediately before dispatch and run `bun <installed-axstack>/scripts/fleet-route.js recommend --input <facts.json> --now <epoch-ms> --config ~/.config/haoshoku/fleet-routing.json --config-digest <run-pinned-sha256> --json` with the account-local policy and run-pinned digest.',
|
|
19
|
+
'Bind the selected destination to explicit `environmentId`, `projectId`, provider, model, options and `runtimeMode` from current destination observations.',
|
|
20
|
+
'Never let native main/UI balancing override the explicit worker destination.',
|
|
21
|
+
'Hold routing failures, unknown execution authority and no-route results, using only a proven supported local isolated fallback within existing authority.',
|
|
22
|
+
'Absence of facts never authorizes a route.',
|
|
23
|
+
'Keep remote-author-coordinator off pending successful version-bound lifecycle gates.',
|
|
24
|
+
'The user authorizes Axstack drivers in T3 to run full-access and create delegated writers and worktrees within approved scope.',
|
|
25
|
+
'Existing exact scoped authorization needs no additional permission flow.',
|
|
18
26
|
'Do not use a harness native subagent tool for delegated work.',
|
|
19
27
|
END,
|
|
20
28
|
].join('\n');
|