axstack 0.49.0 → 0.50.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/README.md +5 -7
- package/docs/concepts.md +11 -51
- package/docs/host-operations.md +24 -99
- package/docs/installation.md +27 -52
- package/docs/workflows.md +48 -35
- package/package.json +1 -1
- package/profiles/presets/claude-only.json +0 -37
- package/profiles/presets/codex-only.json +0 -36
- package/profiles/presets/mixed.json +4 -41
- package/skills/axstack/references/automations.md +28 -2
- package/skills/axstack/references/autopilot.md +7 -5
- package/skills/axstack/references/candidate-publication.md +1 -0
- package/skills/axstack/references/contracts.md +56 -7
- package/skills/axstack/references/lifecycle.md +20 -5
- package/skills/axstack/references/pr-shape.md +15 -0
- package/skills/axstack/references/pr-triage-nightly.md +14 -2
- package/skills/axstack/references/review-manager-prompt.md +18 -11
- package/skills/axstack/references/role-roster.md +4 -9
- package/skills/axstack/references/routing.md +3 -7
- package/skills/axstack/references/t3-runtime.md +122 -103
- package/skills/axstack/references/test-audit-weekly.md +14 -2
- package/skills/axstack/references/workspace-hygiene.md +14 -2
- package/skills/axstack/scripts/dispatch-plan.js +10 -27
- package/skills/axstack/scripts/resolve-models.js +9 -18
- package/skills/axstack-brainstorm/references/arena.md +2 -2
- package/skills/axstack-cleanup/SKILL.md +6 -3
- package/skills/axstack-implement/SKILL.md +5 -4
- package/skills/axstack-research/SKILL.md +3 -5
- package/skills/axstack-review/SKILL.md +7 -3
- package/skills/axstack-scout/SKILL.md +2 -12
- package/skills/axstack-watch/SKILL.md +43 -8
- package/skills/axstack-watch/references/escalation.md +12 -5
- package/skills/axstack-watch/references/proven-verification.md +1 -1
- package/skills/axstack-watch/references/readiness.md +13 -2
- package/skills/axstack-watch/references/repair-publication.md +1 -1
- package/skills/axstack-watch/references/watch-runtime.md +46 -33
- package/src/instructions.js +4 -3
- package/src/roles.js +3 -6
- package/skills/axstack/scripts/account-usage.js +0 -79
- package/skills/axstack/scripts/pick-instance.js +0 -57
- package/skills/axstack/scripts/sync-default.js +0 -191
- package/skills/axstack/systemd/axstack-default-sync.service +0 -8
- package/skills/axstack/systemd/axstack-default-sync.timer +0 -11
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import { accountUsage, expandHome } from './account-usage.js';
|
|
3
|
-
|
|
4
|
-
const drivers = { claudeAgent: 'claude', codex: 'codex' };
|
|
5
|
-
function parsePool(value) {
|
|
6
|
-
if (!value) return null;
|
|
7
|
-
const pool = {};
|
|
8
|
-
for (const entry of value.split(',')) {
|
|
9
|
-
const match = /^(claudeAgent|codex)=([^:,=\s]+)(?::([a-z]+))?$/.exec(entry);
|
|
10
|
-
if (!match || Object.hasOwn(pool, match[1])) throw new Error('invalid pool');
|
|
11
|
-
const [, driver, model, effort] = match;
|
|
12
|
-
pool[driver] = { model, ...(effort && { options: [
|
|
13
|
-
{ id: driver === 'codex' ? 'reasoningEffort' : 'effort', value: effort },
|
|
14
|
-
] }) };
|
|
15
|
-
}
|
|
16
|
-
return pool;
|
|
17
|
-
}
|
|
18
|
-
const candidates = (settings, pool) => Object.entries(settings.providerInstances ?? {}).filter(([, instance]) =>
|
|
19
|
-
instance.enabled && Object.hasOwn(pool, instance.driver));
|
|
20
|
-
|
|
21
|
-
export function decide(settings, usage, now, pool = null) {
|
|
22
|
-
const from = settings.defaultModelSelection?.instanceId ?? null;
|
|
23
|
-
const result = { action: 'unchanged', reason: 'unmanaged', from, to: from, instances: [] };
|
|
24
|
-
const driver = settings.providerInstances?.[from]?.driver;
|
|
25
|
-
const pooled = pool !== null;
|
|
26
|
-
pool ??= { [driver]: null };
|
|
27
|
-
if (!Object.hasOwn(drivers, driver) || !Object.hasOwn(pool, driver)) return result;
|
|
28
|
-
const updated = (best, reason) => ({ ...result, action: 'updated', reason, to: best.instanceId,
|
|
29
|
-
selection: pooled ? { instanceId: best.instanceId, ...pool[settings.providerInstances[best.instanceId].driver] }
|
|
30
|
-
: { ...settings.defaultModelSelection, instanceId: best.instanceId } });
|
|
31
|
-
result.instances = candidates(settings, pool).map(([instanceId, instance]) => {
|
|
32
|
-
const account = usage.find((entry) => entry.instanceId === instanceId);
|
|
33
|
-
const window = instance.driver === 'claudeAgent' ? account?.windows?.[1]
|
|
34
|
-
: account?.windows?.[1] ?? account?.windows?.[0];
|
|
35
|
-
const excluded = account?.utilization >= 95 || account?.limitReached || account?.credentialsAbsent
|
|
36
|
-
|| (instanceId === from && (account?.unauthorized || account?.state === 'skipped'));
|
|
37
|
-
const known = account && ['fresh', 'cached'].includes(account.state)
|
|
38
|
-
&& Number.isFinite(account.updatedAt) && now - account.updatedAt < 300000
|
|
39
|
-
&& Number.isFinite(window?.used) && Number.isFinite(window?.seconds) && window.seconds > 0
|
|
40
|
-
&& Number.isFinite(window?.resetAt) && window.resetAt > now
|
|
41
|
-
&& (!pooled || window.seconds >= 604800);
|
|
42
|
-
const remaining = known ? 100 - window.used : null;
|
|
43
|
-
return { instanceId, state: excluded ? 'excluded' : known ? 'eligible' : 'unknown',
|
|
44
|
-
...(pooled && { band: known ? remaining >= 15 ? 2 : remaining >= 10 ? 1 : 0 : null }),
|
|
45
|
-
pace: known ? 100 * (1 - (window.resetAt - now) / (window.seconds * 1000)) - window.used : null };
|
|
46
|
-
});
|
|
47
|
-
const current = result.instances.find((entry) => entry.instanceId === from);
|
|
48
|
-
const best = result.instances.filter((entry) => entry.state === 'eligible')
|
|
49
|
-
.sort((a, b) => (pooled ? b.band - a.band : 0) || b.pace - a.pace)[0];
|
|
50
|
-
const excluded = !current || current.state === 'excluded';
|
|
51
|
-
if (excluded && best) return updated(best, 'current-excluded');
|
|
52
|
-
if (result.instances.some((entry) => entry.state === 'unknown')) return { ...result, reason: 'unknown' };
|
|
53
|
-
if (!best) return { ...result, reason: 'no-eligible' };
|
|
54
|
-
const higherBand = pooled && best.band > current.band;
|
|
55
|
-
if (best.instanceId !== from && (higherBand || best.pace - current.pace >= 10)) {
|
|
56
|
-
return updated(best, higherBand ? 'reserve' : 'pace');
|
|
57
|
-
}
|
|
58
|
-
return { ...result, reason: 'margin' };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Preserve the instance-only edit when the rest of the selection is already identical.
|
|
62
|
-
function selectionBytes(original, selection) {
|
|
63
|
-
const previous = JSON.parse(original).defaultModelSelection;
|
|
64
|
-
const instanceOnly = deepEqual({ ...previous, instanceId: selection.instanceId }, selection);
|
|
65
|
-
let start;
|
|
66
|
-
const tokens = [...original.matchAll(/"(?:\\.|[^"\\])*"|[{}\[\]:,]|[^\s{}\[\]:,]+/g)];
|
|
67
|
-
let depth = 0;
|
|
68
|
-
let selectionDepth = 0;
|
|
69
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
70
|
-
const token = tokens[i][0];
|
|
71
|
-
if (depth === 1 && token.startsWith('"') && JSON.parse(token) === 'defaultModelSelection'
|
|
72
|
-
&& tokens[i + 1]?.[0] === ':' && tokens[i + 2]?.[0] === '{') {
|
|
73
|
-
selectionDepth = depth + 1;
|
|
74
|
-
start = tokens[i + 2].index;
|
|
75
|
-
}
|
|
76
|
-
if (instanceOnly && selectionDepth && depth === selectionDepth && token.startsWith('"') && JSON.parse(token) === 'instanceId'
|
|
77
|
-
&& tokens[i + 1]?.[0] === ':') {
|
|
78
|
-
const value = tokens[i + 2];
|
|
79
|
-
return original.slice(0, value.index) + JSON.stringify(selection.instanceId) + original.slice(value.index + value[0].length);
|
|
80
|
-
}
|
|
81
|
-
if (token === '{' || token === '[') depth++;
|
|
82
|
-
if (token === '}' || token === ']') {
|
|
83
|
-
if (depth === selectionDepth) {
|
|
84
|
-
if (!instanceOnly) return original.slice(0, start) + JSON.stringify(selection) + original.slice(tokens[i].index + 1);
|
|
85
|
-
selectionDepth = 0;
|
|
86
|
-
}
|
|
87
|
-
depth--;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
throw new Error('missing instance string');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function deepEqual(a, b) {
|
|
94
|
-
if (a === b) return true;
|
|
95
|
-
if (!a || !b || typeof a !== 'object' || typeof b !== 'object' || Array.isArray(a) !== Array.isArray(b)) return false;
|
|
96
|
-
const keys = Object.keys(a);
|
|
97
|
-
return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && deepEqual(a[key], b[key]));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function commit(path, originalBytes, nextBytes, selection) {
|
|
101
|
-
const original = JSON.parse(originalBytes);
|
|
102
|
-
const next = JSON.parse(nextBytes);
|
|
103
|
-
const from = original.defaultModelSelection?.instanceId;
|
|
104
|
-
const to = next.defaultModelSelection?.instanceId;
|
|
105
|
-
if (typeof from !== 'string' || typeof to !== 'string') throw new Error('invalid instance string');
|
|
106
|
-
selection ??= { ...original.defaultModelSelection, instanceId: to };
|
|
107
|
-
if (!deepEqual(next.defaultModelSelection, selection)) throw new Error('refusing undecided selection');
|
|
108
|
-
const unchanged = deepEqual(original.defaultModelSelection, selection);
|
|
109
|
-
original.defaultModelSelection = selection;
|
|
110
|
-
if (!deepEqual(original, next)) throw new Error('refusing change outside defaultModelSelection');
|
|
111
|
-
if (unchanged) return 'unchanged';
|
|
112
|
-
const lock = `${path}.axstack-default-sync.lock`;
|
|
113
|
-
try { fs.mkdirSync(lock, { mode: 0o700 }); }
|
|
114
|
-
catch (error) {
|
|
115
|
-
if (error.code !== 'EEXIST') throw error;
|
|
116
|
-
if (Date.now() - fs.lstatSync(lock).mtimeMs <= 60000) return 'locked';
|
|
117
|
-
fs.rmdirSync(lock);
|
|
118
|
-
try { fs.mkdirSync(lock, { mode: 0o700 }); }
|
|
119
|
-
catch (retryError) { if (retryError.code === 'EEXIST') return 'locked'; throw retryError; }
|
|
120
|
-
}
|
|
121
|
-
let temp;
|
|
122
|
-
let fd;
|
|
123
|
-
try {
|
|
124
|
-
const info = fs.lstatSync(path);
|
|
125
|
-
if (!info.isFile() || info.isSymbolicLink()) throw new Error('settings must be a regular file');
|
|
126
|
-
const sibling = `${path}.axstack-default-sync-${crypto.randomUUID()}.tmp`;
|
|
127
|
-
fd = fs.openSync(sibling, 'wx', info.mode & 0o7777);
|
|
128
|
-
temp = sibling;
|
|
129
|
-
fs.fchmodSync(fd, info.mode & 0o7777);
|
|
130
|
-
fs.writeFileSync(fd, nextBytes);
|
|
131
|
-
fs.fsyncSync(fd);
|
|
132
|
-
fs.closeSync(fd);
|
|
133
|
-
fd = undefined;
|
|
134
|
-
// This is the final operation before rename; the remaining sub-ms race is accepted.
|
|
135
|
-
if (!fs.readFileSync(path).equals(Buffer.from(originalBytes))) return 'concurrent-edit';
|
|
136
|
-
fs.renameSync(temp, path);
|
|
137
|
-
temp = undefined;
|
|
138
|
-
if (!fs.readFileSync(path).equals(Buffer.from(nextBytes))) throw new Error('read-back mismatch');
|
|
139
|
-
return 'updated';
|
|
140
|
-
} finally {
|
|
141
|
-
try { if (fd !== undefined) fs.closeSync(fd); }
|
|
142
|
-
finally {
|
|
143
|
-
try { if (temp) fs.unlinkSync(temp); }
|
|
144
|
-
finally { fs.rmdirSync(lock); }
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function main() {
|
|
150
|
-
if (process.env.AXSTACK_SYNC_DEFAULT === '0') {
|
|
151
|
-
console.log(JSON.stringify({ action: 'unchanged', reason: 'disabled', from: null, to: null, instances: [] }));
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
let path = `${process.env.HOME}/.t3/userdata/settings.json`;
|
|
155
|
-
let dryRun = false;
|
|
156
|
-
const args = process.argv.slice(2);
|
|
157
|
-
const seen = new Set();
|
|
158
|
-
for (let i = 0; i < args.length; i++) {
|
|
159
|
-
if (seen.has(args[i])) throw new Error('duplicate flag');
|
|
160
|
-
seen.add(args[i]);
|
|
161
|
-
if (args[i] === '--dry-run') dryRun = true;
|
|
162
|
-
else if (args[i] === '--settings' && args[i + 1] && !args[i + 1].startsWith('--')) path = args[++i];
|
|
163
|
-
else throw new Error('expected [--settings path] [--dry-run]');
|
|
164
|
-
}
|
|
165
|
-
path = expandHome(path);
|
|
166
|
-
const original = fs.readFileSync(path, 'utf8');
|
|
167
|
-
const settings = JSON.parse(original);
|
|
168
|
-
if (!settings || typeof settings !== 'object' || Array.isArray(settings)
|
|
169
|
-
|| (Object.hasOwn(settings, 'providerInstances') && (!settings.providerInstances
|
|
170
|
-
|| typeof settings.providerInstances !== 'object' || Array.isArray(settings.providerInstances)))) throw new Error('invalid settings');
|
|
171
|
-
const driver = settings.providerInstances?.[settings.defaultModelSelection?.instanceId]?.driver;
|
|
172
|
-
const pool = parsePool(process.env.AXSTACK_SYNC_POOL);
|
|
173
|
-
const usage = [];
|
|
174
|
-
if (Object.hasOwn(drivers, driver) && (!pool || Object.hasOwn(pool, driver))) {
|
|
175
|
-
for (const [id, instance] of candidates(settings, pool ?? { [driver]: null })) {
|
|
176
|
-
usage.push(await accountUsage(id, instance, drivers[instance.driver]));
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
let decision = decide(settings, usage, Date.now(), pool);
|
|
180
|
-
if (decision.action === 'updated' && !dryRun) {
|
|
181
|
-
const reason = commit(path, original, selectionBytes(original, decision.selection), decision.selection);
|
|
182
|
-
if (reason !== 'updated') decision = { ...decision, action: 'unchanged', reason, to: decision.from };
|
|
183
|
-
}
|
|
184
|
-
console.log(JSON.stringify(decision));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (import.meta.main) main().catch((error) => {
|
|
188
|
-
console.error(error.message === 'read-back mismatch' ? 'default sync failed: read-back mismatch'
|
|
189
|
-
: 'default sync failed: invalid input/settings or failed write');
|
|
190
|
-
process.exitCode = 1;
|
|
191
|
-
});
|