axstack 0.30.0 → 0.31.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 CHANGED
@@ -15,7 +15,7 @@ coordination. You can start at the phase you need.
15
15
  | Failure mode | How Axstack responds |
16
16
  | --- | --- |
17
17
  | Wrong thing built | Align rounds clarify the request; every brainstorm runs a light arena across configured families. Use judges only for Rung 2 hard-to-reverse choices. |
18
- | Nobody really reviewed it | Strict TDD checks behavior first; mixed authored review is cross-provider, and mixed peer review uses two isolated Sol-high sessions at the exact revision. |
18
+ | Nobody really reviewed it | Strict TDD checks behavior first; mixed authored review is cross-provider, and mixed peer review pairs Sol high with Opus low in isolated sessions at the exact revision. |
19
19
  | Design rot | The design lens sketches boundaries before a build; Improve surfaces evidenced changes later. |
20
20
  | Agents left a mess | T3 makes delegation visible, one writer owns each PR, cleanup stays bounded, and watched own PRs merge under guarded rules. |
21
21
 
package/docs/workflows.md CHANGED
@@ -119,7 +119,7 @@ role.
119
119
 
120
120
  | Preset | Author | Ordered peer reviewers | Astra / Opus advisers | Auditor |
121
121
  | --- | --- | --- | --- | --- |
122
- | `mixed` | Sol high | Sol high; Sol high | Astra high / Opus xhigh | Sonnet high + Sol high |
122
+ | `mixed` | Sol high | Sol high; Opus low | Astra high / Opus xhigh | Sonnet high + Sol high |
123
123
  | `codex-only` | Sol high | Sol high; Luna xhigh | Astra high / unavailable | Luna xhigh + Sol high |
124
124
  | `claude-only` | Opus medium | Opus medium; Sonnet high | unavailable / Opus xhigh | Sonnet high (Sol absent) |
125
125
 
@@ -147,7 +147,8 @@ later installation changes without re-resolution.
147
147
 
148
148
  Peer roles use the stable IDs `axstack-reviewer-primary` and
149
149
  `axstack-reviewer-peer`; their provider/class mappings come only from the
150
- selected preset. The mixed peer coordinator and review-lane binding
150
+ selected preset. The mixed cross-provider peer pair is Sol high followed by
151
+ Opus low. The mixed peer coordinator and review-lane binding
151
152
  `axstack-owner` uses Sol high and never reviews. `axstack-reviewer-secondary`
152
153
  is used in the authored mapping and, alongside `axstack-reviewer-primary`,
153
154
  in codebase findings mode.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "axstack",
3
- "version": "0.30.0",
3
+ "version": "0.31.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",
@@ -43,7 +43,7 @@
43
43
  "provider": "codex",
44
44
  "modeId": "full-access",
45
45
  "thinkingOptionId": "high",
46
- "notes": "Primary reviewer in the ordered mixed peer pair: Sol high followed by Sol high. Eligible authored reviewer for an Opus-authored candidate. Never author or owner; review exact SHA and base across all six angles and acceptance. Peer first pass stays isolated.",
46
+ "notes": "Primary reviewer in the ordered mixed peer pair: Sol high followed by Opus low. Eligible authored reviewer for an Opus-authored candidate. Never author or owner; review exact SHA and base across all six angles and acceptance. Peer first pass stays isolated.",
47
47
  "modelClass": "sol"
48
48
  },
49
49
  {
@@ -58,11 +58,11 @@
58
58
  {
59
59
  "id": "axstack-reviewer-peer",
60
60
  "name": "Axstack reviewer (peer)",
61
- "provider": "codex",
62
- "modeId": "full-access",
63
- "thinkingOptionId": "high",
64
- "notes": "Second reviewer in the ordered mixed peer pair: Sol high followed by Sol high. Never author or owner; review exact SHA and base across all six angles and acceptance. Peer first pass stays isolated.",
65
- "modelClass": "sol"
61
+ "provider": "claude",
62
+ "modeId": "bypassPermissions",
63
+ "thinkingOptionId": "low",
64
+ "notes": "Second reviewer in the ordered mixed peer pair: Sol high followed by Opus low. Never author or owner; review exact SHA and base across all six angles and acceptance. Peer first pass stays isolated. T3 resolves Claude classes from saved capabilities; a Claude rejection holds.",
65
+ "modelClass": "opus"
66
66
  },
67
67
  {
68
68
  "id": "axstack-diligence",
@@ -7,7 +7,8 @@
7
7
  Only the recorded owning watch thread merges, including `axstack-owner`
8
8
  for authorized standalone maintenance. Other roles never acquire merge authority.
9
9
  - `axstack-reviewer-primary` and `axstack-reviewer-peer` are the ordered
10
- peer pair. Peer review uses both; authored review uses this table:
10
+ peer pair. In `mixed`, the cross-provider pair is Sol high followed by Opus low.
11
+ Peer review uses both; authored review uses this table:
11
12
 
12
13
  | Preset | Author class | Reviewer (class/effort) |
13
14
  | --- | --- | --- |
@@ -0,0 +1,134 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ const flags = ['--role', '--roles', '--capabilities', '--picker', '--kind', '--verify'];
4
+ const nonempty = (value) => typeof value === 'string' && value.trim().length > 0;
5
+ const help = `Usage: bun dispatch-plan.js --role <id> --roles <roles.json>
6
+ --capabilities <saved.json> --picker <picker.json> --kind task|launch
7
+ [--verify <configuration.json>]
8
+
9
+ --role Role ID from the supplied snapshot
10
+ --roles Installed-style roles.json (version 1, roles array)
11
+ --capabilities Saved orchestrator_capabilities JSON; reuse the run's saved snapshot
12
+ --picker Saved picker JSON; uses chosenInstanceId
13
+ --kind task emits target; launch emits modelSelection
14
+ --verify Compare t3_thread_configuration JSON; print MATCH or each mismatch
15
+ --help Show all flags and exit codes
16
+
17
+ Read-only: reads supplied JSON, spawns adjacent resolve-models.js, writes stdout/stderr.
18
+ Never dispatches or changes snapshots. Reuse recorded exact model pins as given.
19
+ Exit codes: 0 ok, 1 error, 2 hold (including incomplete read-back).
20
+ `;
21
+
22
+ function hold(message) {
23
+ throw Object.assign(new Error(message), { exitCode: 2 });
24
+ }
25
+
26
+ function argumentsFrom(args) {
27
+ const options = {};
28
+ for (let i = 0; i < args.length; i += 2) {
29
+ const flag = args[i];
30
+ if (!flags.includes(flag)) throw new Error(`unexpected argument ${flag}`);
31
+ const value = args[i + 1];
32
+ if (!nonempty(value) || value.startsWith('--')) throw new Error(`expected value after ${flag}`);
33
+ if (flag in options) throw new Error(`duplicate argument ${flag}`);
34
+ options[flag] = value;
35
+ }
36
+ for (const flag of flags.filter((flag) => flag !== '--verify')) {
37
+ if (!options[flag]) throw new Error(`expected ${flag}`);
38
+ }
39
+ if (!['task', 'launch'].includes(options['--kind'])) throw new Error('expected --kind task|launch');
40
+ return options;
41
+ }
42
+
43
+ const readJSON = (path) => JSON.parse(readFileSync(path, 'utf8'));
44
+
45
+ function normalizedOptions(options) {
46
+ const entries = Array.isArray(options) ? options.map((option) => [option?.id, option?.value])
47
+ : options && typeof options === 'object' ? Object.entries(options) : null;
48
+ if (!entries || entries.some(([id, value]) => typeof id !== 'string' || !id.trim()
49
+ || !['string', 'boolean'].includes(typeof value))
50
+ || new Set(entries.map(([id]) => id)).size !== entries.length) return null;
51
+ return JSON.stringify(entries.sort(([left], [right]) => left.localeCompare(right)));
52
+ }
53
+
54
+ function verify(configuration, instanceId, provider, model, options) {
55
+ const mismatches = [];
56
+ const selection = configuration?.modelSelection;
57
+ const compare = (field, expected, observed, equal = expected === observed) => {
58
+ if (!equal) mismatches.push(`${field}: ${observed == null ? 'unknown' : 'fail'}; `
59
+ + `expected ${JSON.stringify(expected)}; observed ${JSON.stringify(observed) ?? 'missing'}`);
60
+ };
61
+ // T3 may omit the legacy provider field: the exact instance ID is its binding.
62
+ if (selection?.provider != null) {
63
+ compare('provider', provider, selection.provider,
64
+ (selection.provider === 'claude' ? 'claudeAgent' : selection.provider) === provider);
65
+ }
66
+ compare('instanceId', instanceId, selection?.instanceId);
67
+ compare('model', model, selection?.model);
68
+ compare('options', options, selection?.options,
69
+ normalizedOptions(options) === normalizedOptions(selection?.options));
70
+ compare('runtimeMode', 'full-access', configuration?.runtimeMode);
71
+ console.log(mismatches.length ? mismatches.join('\n') : 'MATCH');
72
+ return mismatches.length ? 2 : 0;
73
+ }
74
+
75
+ function main() {
76
+ if (process.argv.slice(2).includes('--help')) {
77
+ process.stdout.write(help);
78
+ return 0;
79
+ }
80
+ const args = argumentsFrom(process.argv.slice(2));
81
+ const snapshot = readJSON(args['--roles']);
82
+ if (snapshot?.version !== 1 || !Array.isArray(snapshot.roles)) {
83
+ throw new Error('roles snapshot must have version 1 and a roles array');
84
+ }
85
+ const roles = snapshot.roles.filter((role) => role?.id === args['--role']);
86
+ if (roles.length !== 1) hold(`role ${args['--role']} unavailable or ambiguous`);
87
+ const role = roles[0];
88
+ if (!nonempty(role.provider)) hold(`provider unavailable for role ${role.id}`);
89
+ if (role.modeId !== 'full-access' && !(role.provider === 'claude' && role.modeId === 'bypassPermissions')) {
90
+ hold(`mode ${role.modeId ?? 'missing'} unavailable for role ${role.id}`);
91
+ }
92
+ if (!nonempty(role.thinkingOptionId)) hold(`effort unavailable for role ${role.id}`);
93
+ if (!nonempty(role.model) && !nonempty(role.modelClass) && role.model !== null) {
94
+ hold(`model unavailable for role ${role.id}`);
95
+ }
96
+ const picker = readJSON(args['--picker']);
97
+ if (!nonempty(picker?.chosenInstanceId)) hold('picker chosen instance unavailable');
98
+ const modelArgs = typeof role.model === 'string' ? ['--model', role.model]
99
+ : role.modelClass ? ['--class', role.modelClass] : ['--model', String(role.model)];
100
+ const resolved = Bun.spawnSync([process.execPath, `${import.meta.dir}/resolve-models.js`,
101
+ '--provider', role.provider, '--capabilities', args['--capabilities'],
102
+ '--effort', role.thinkingOptionId, ...modelArgs], { stdout: 'pipe', stderr: 'pipe' });
103
+ if (resolved.stderr.length) process.stderr.write(resolved.stderr);
104
+ if (resolved.exitCode !== 0) return resolved.exitCode === 1 ? 2 : 1;
105
+ const { model, effortOption, providerInstanceId } = JSON.parse(resolved.stdout.toString());
106
+ const chosen = readJSON(args['--capabilities']).providers
107
+ .filter((provider) => provider.providerInstanceId === picker.chosenInstanceId);
108
+ if (chosen.length !== 1 || chosen[0].driverKind !== providerInstanceId
109
+ || (['grok', 'antigravity'].includes(role.provider) && picker.chosenInstanceId !== providerInstanceId)) {
110
+ hold(`picker instance ${picker.chosenInstanceId} does not bind provider ${role.provider}`);
111
+ }
112
+ const selectedModel = chosen[0].models?.find((entry) => entry.id === model);
113
+ if (!selectedModel) hold(`model ${model} unavailable on instance ${picker.chosenInstanceId}`);
114
+ if (effortOption && !selectedModel.options?.some((option) => option.id === effortOption.id
115
+ && option.options?.some((value) => value.id === effortOption.value))) {
116
+ hold(`effort ${effortOption.value} unavailable on instance ${picker.chosenInstanceId}`);
117
+ }
118
+ const options = effortOption ? [effortOption] : [];
119
+ if (args['--verify']) {
120
+ return verify(readJSON(args['--verify']), picker.chosenInstanceId, providerInstanceId, model, options);
121
+ }
122
+ const selection = { [args['--kind'] === 'launch' ? 'instanceId' : 'providerInstanceId']: picker.chosenInstanceId,
123
+ model, options };
124
+ console.log(JSON.stringify({ [args['--kind'] === 'launch' ? 'modelSelection' : 'target']: selection,
125
+ runtimeMode: 'full-access' }));
126
+ return 0;
127
+ }
128
+
129
+ try {
130
+ process.exitCode = main();
131
+ } catch (error) {
132
+ console.error(`dispatch-plan ${error.exitCode === 2 ? 'hold' : 'error'}: ${error.message}`);
133
+ process.exitCode = error.exitCode ?? 1;
134
+ }
@@ -223,7 +223,8 @@ This section applies to peer and authored PR modes.
223
223
  - **Peer:** exactly two independent final reviewers,
224
224
  `axstack-reviewer-primary` and `axstack-reviewer-peer` from the routing snapshot.
225
225
  Send both the identical six-angle brief, with no first-pass cross-read or children.
226
- Mixed peer reviewers share a model, with independence from separate sessions, the identical brief and an isolated first pass.
226
+ The mixed peer pair is cross-provider (Sol high + Opus low), with independence
227
+ still from separate sessions, the identical brief and an isolated first pass.
227
228
  - **Authored:** exactly one eligible independent reviewer from this complete
228
229
  mapping:
229
230