axstack 0.33.0 → 0.34.1

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 pairs Sol high with Opus low in isolated 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 medium 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
 
@@ -147,6 +147,24 @@ Eligible own PRs use guarded automatic merge; see [merge boundaries, exclusions,
147
147
  See [PR scope and sizing](skills/axstack/references/pr-shape.md) for candidate
148
148
  shape and [Releases](https://github.com/axatbhardwaj/axstack/releases) for release history.
149
149
 
150
+ ## Development
151
+
152
+ ```sh
153
+ bun install --frozen-lockfile
154
+ bun run lint
155
+ bun run typecheck
156
+ git config core.hooksPath .husky
157
+ ```
158
+
159
+ The hook is opt-in per clone via `git config core.hooksPath .husky` and requires
160
+ `bun install`. Without dev tools it prints a setup hint and skips the checks.
161
+ The hook runs lint and typecheck only. Biome formatting is optional for editors
162
+ or manual use; commits do not enforce formatting. TypeScript checks JavaScript
163
+ in `src/` and `bin/` without emitting files. All tooling is development-only and
164
+ is excluded from packed assets by `package.json.files`.
165
+ Regenerate `bun.lock` with the minimum Bun version in `package.json.engines`
166
+ (currently 1.3.14); newer Bun versions can write an incompatible lockfile format.
167
+
150
168
  ## License
151
169
 
152
170
  [MIT](LICENSE) © 2026 Axat Bhardwaj.
package/bin/axstack.js CHANGED
@@ -26,7 +26,7 @@ function homeDir() {
26
26
  // Tilde expansion needs a known absolute home; with HOME missing, empty,
27
27
  // or relative the destination is unknown, so refuse even with --yes.
28
28
  const home = Bun.env.HOME;
29
- if (!home || !home.startsWith('/')) {
29
+ if (!home?.startsWith('/')) {
30
30
  throw new Error('HOME is missing, empty, or not absolute; pass explicit paths instead of ~');
31
31
  }
32
32
  return home;
@@ -298,6 +298,13 @@ async function main() {
298
298
  const legacyCodexManifest = legacyCodexDir
299
299
  ? await readLegacySkillsManifest(legacyCodexDir)
300
300
  : null;
301
+ /**
302
+ * @type {Awaited<ReturnType<typeof installBundle>> & {
303
+ * legacyCodex?: {removed: string[], preserved: string[], missing: string[], skipped?: boolean,
304
+ * held?: boolean, failed?: boolean, failure?: boolean, reason?: string},
305
+ * addedRoleIds?: string[], removedRoleIds?: string[], changedRoleModels?: string[], notes?: string[]
306
+ * }}
307
+ */
301
308
  const summary = await installBundle({
302
309
  bundleDir: flags.bundle ? resolve(flags.bundle) : PACKAGE_ROOT,
303
310
  skillsDir,
package/docs/workflows.md CHANGED
@@ -119,11 +119,11 @@ role.
119
119
 
120
120
  | Preset | Author | Ordered peer reviewers | Astra / Opus advisers | Auditor |
121
121
  | --- | --- | --- | --- | --- |
122
- | `mixed` | Sol high | Sol high; Opus low | Astra high / Opus xhigh | Sonnet high + Sol high |
122
+ | `mixed` | Sol high | Sol high; Opus medium | Astra high / Opus xhigh | Haiku xhigh + 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
 
126
- In `mixed` and `claude-only`, `axstack-auditor`, `axstack-research-requirements`,
126
+ In `mixed` and `claude-only`, `axstack-research-requirements`,
127
127
  `axstack-research-code`, `axstack-research-web`, and `axstack-explore-execution`
128
128
  use the Claude Sonnet class at high effort. `axstack-explore-codebase` and
129
129
  `axstack-monitor` adopt Haiku 5.5 high, with class `haiku` and exact pin
@@ -131,6 +131,9 @@ use the Claude Sonnet class at high effort. `axstack-explore-codebase` and
131
131
  holds those roles without substitution. `codex-only` keeps its Codex
132
132
  assignments for those roles. Mixed web-google and X retain their source-specific
133
133
  Antigravity and Grok routes.
134
+ The mixed `axstack-auditor` uses Haiku 5.5 xhigh, pinned to `claude-haiku-5-5`;
135
+ missing that ID or xhigh effort holds without substitution. The mixed
136
+ `axstack-explainer-review` uses Sol medium; `axstack-explainer` stays Sonnet high.
134
137
  The new `-sol` auditor, research-code, and explore-execution seats use Sol high
135
138
  in `mixed` and `codex-only`; `claude-only` records each as intentionally absent.
136
139
  Dispatch the base and Sol seats independently on the same brief, then reconcile
@@ -148,7 +151,7 @@ later installation changes without re-resolution.
148
151
  Peer roles use the stable IDs `axstack-reviewer-primary` and
149
152
  `axstack-reviewer-peer`; their provider/class mappings come only from the
150
153
  selected preset. The mixed cross-provider peer pair is Sol high followed by
151
- Opus low. The mixed peer coordinator and review-lane binding
154
+ Opus medium. The mixed peer coordinator and review-lane binding
152
155
  `axstack-owner` uses Sol high and never reviews. `axstack-reviewer-secondary`
153
156
  is used in the authored mapping and, alongside `axstack-reviewer-primary`,
154
157
  in codebase findings mode.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "axstack",
3
- "version": "0.33.0",
3
+ "version": "0.34.1",
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",
@@ -32,8 +32,15 @@
32
32
  "docs/*.md"
33
33
  ],
34
34
  "scripts": {
35
+ "lint": "biome lint . --error-on-warnings",
36
+ "typecheck": "bun node_modules/typescript/bin/tsc --noEmit --project jsconfig.json",
35
37
  "test": "bun test",
36
38
  "test:all": "bun test"
37
39
  },
38
- "license": "MIT"
40
+ "license": "MIT",
41
+ "devDependencies": {
42
+ "@biomejs/biome": "2.5.15",
43
+ "@types/bun": "1.4.2",
44
+ "typescript": "7.0.2"
45
+ }
39
46
  }
@@ -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 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.",
46
+ "notes": "Primary reviewer in the ordered mixed peer pair: Sol high followed by Opus medium. 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
  {
@@ -51,7 +51,7 @@
51
51
  "name": "Axstack reviewer (secondary)",
52
52
  "provider": "claude",
53
53
  "modeId": "bypassPermissions",
54
- "thinkingOptionId": "medium",
54
+ "thinkingOptionId": "high",
55
55
  "notes": "Eligible authored reviewer for a Sol-authored candidate. Never author or owner; review exact SHA and base across all six angles and acceptance. T3 resolves Claude classes from saved capabilities; a Claude rejection holds.",
56
56
  "modelClass": "opus"
57
57
  },
@@ -60,8 +60,8 @@
60
60
  "name": "Axstack reviewer (peer)",
61
61
  "provider": "claude",
62
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.",
63
+ "thinkingOptionId": "medium",
64
+ "notes": "Second reviewer in the ordered mixed peer pair: Sol high followed by Opus medium. 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
65
  "modelClass": "opus"
66
66
  },
67
67
  {
@@ -150,9 +150,9 @@
150
150
  "name": "Axstack explainer reviewer",
151
151
  "provider": "codex",
152
152
  "modeId": "full-access",
153
- "thinkingOptionId": "xhigh",
154
- "notes": "Independent explanation reviewer: reviews consequential or complex claims, archify output, or on request. Checks the exact artifact for text and source fidelity. The rendered pass belongs to axstack-ui-verifier. Any artifact change invalidates its review. Resolve the class from saved T3 capabilities at run start; rejection and unavailable settings hold without substitution.",
155
- "modelClass": "luna"
153
+ "thinkingOptionId": "medium",
154
+ "notes": "Independent Sol medium explanation reviewer: reviews consequential or complex claims, archify output, or on request. Checks the exact artifact for text and source fidelity. The rendered pass belongs to axstack-ui-verifier. Any artifact change invalidates its review. Resolve the class from saved T3 capabilities at run start; rejection and unavailable settings hold without substitution.",
155
+ "modelClass": "sol"
156
156
  },
157
157
  {
158
158
  "id": "axstack-ui-verifier",
@@ -206,9 +206,10 @@
206
206
  "name": "Axstack auditor",
207
207
  "provider": "claude",
208
208
  "modeId": "bypassPermissions",
209
- "thinkingOptionId": "high",
210
- "notes": "Sonnet high read-only end-of-run and checkpoint auditor. Collects scope and outcome evidence with counts and denominators and reports PASS, FAIL, or UNKNOWN without inventing numbers. Never edits, merges, activates, or audits itself. T3 resolves Claude classes from saved capabilities; a Claude rejection holds.",
211
- "modelClass": "sonnet"
209
+ "thinkingOptionId": "xhigh",
210
+ "notes": "Haiku 5.5 xhigh read-only end-of-run and checkpoint auditor. Collects scope and outcome evidence with counts and denominators and reports PASS, FAIL, or UNKNOWN without inventing numbers. Never edits, merges, activates, or audits itself. T3 uses the exact claude-haiku-5-5 pin from saved capabilities; a Claude rejection holds. Missing model or xhigh effort holds without substitution.",
211
+ "modelClass": "haiku",
212
+ "model": "claude-haiku-5-5"
212
213
  },
213
214
  {
214
215
  "id": "axstack-auditor-sol",
@@ -7,12 +7,12 @@
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. In `mixed`, the cross-provider pair is Sol high followed by Opus low.
10
+ peer pair. In `mixed`, the cross-provider pair is Sol high followed by Opus medium.
11
11
  Peer review uses both; authored review uses this table:
12
12
 
13
13
  | Preset | Author class | Reviewer (class/effort) |
14
14
  | --- | --- | --- |
15
- | `mixed` | `codex/sol` | `axstack-reviewer-secondary` (`claude/opus` medium) |
15
+ | `mixed` | `codex/sol` | `axstack-reviewer-secondary` (`claude/opus` high) |
16
16
  | `mixed` | `claude/opus` | `axstack-reviewer-primary` (`codex/sol` high) |
17
17
  | `codex-only` | `codex/sol` | `axstack-reviewer-secondary` (`codex/luna` xhigh) |
18
18
  | `claude-only` | `claude/opus` | `axstack-reviewer-secondary` (`claude/sonnet` high) |
@@ -24,15 +24,18 @@
24
24
  `axstack-auditor` audits; `axstack-checker` reports discrepancies.
25
25
  - `axstack-explainer` authors archify only on an explicit viewer request.
26
26
  - `axstack-explainer-review` reviews consequential or complex claims, archify
27
- output, or on request.
27
+ output, or on request; `codex/sol` medium in mixed.
28
28
  - `axstack-diligence`: read-only [diligence checks](diligence.md) for every PR
29
29
  review round and bounded research, spec, ticket, receipt, and release claims.
30
30
  - `axstack-ui-verifier`: owns the inline page interaction pass under [UI checks](ui-verification.md)
31
31
  for every interactive page, bound to the exact bytes and SHA-256.
32
- - `axstack-auditor`/`axstack-research-requirements`/
32
+ - `axstack-research-requirements`/
33
33
  `axstack-research-code`/`axstack-research-web`/
34
34
  `axstack-explore-execution`:
35
35
  `claude/sonnet` high in mixed/claude-only.
36
+ - `axstack-auditor`: `claude/haiku` xhigh in mixed, pinned to
37
+ `claude-haiku-5-5`. Missing that exact model or xhigh effort holds without
38
+ substitution. `claude/sonnet` high in claude-only; `codex/luna` xhigh in codex-only.
36
39
  - `axstack-explore-codebase`/`axstack-monitor`: `claude/haiku` high in
37
40
  mixed/claude-only, pinned to `claude-haiku-5-5`. Missing that exact model
38
41
  or high effort holds these roles without substitution.
@@ -41,7 +44,7 @@
41
44
  - Sol pairs `axstack-auditor-sol`/`axstack-research-code-sol`/
42
45
  `axstack-explore-execution-sol`: `codex/sol` high in
43
46
  mixed/codex-only; intentionally absent in claude-only. Dispatch each
44
- independently from its Sonnet seat on the same bounded brief without
47
+ independently from its base seat on the same bounded brief without
45
48
  cross-reading. The driver reconciles findings per claim, never averages.
46
49
  Record intentional absence and continue with Sonnet alone.
47
50
  - Optional seats: `axstack-arena-candidate-grok`,
@@ -54,7 +54,7 @@ function fetchCurrent(args) {
54
54
  }
55
55
 
56
56
  function nodes(connection, field) {
57
- if (!connection || connection.pageInfo?.hasNextPage !== false || !Array.isArray(connection.nodes)) {
57
+ if (connection?.pageInfo?.hasNextPage !== false || !Array.isArray(connection.nodes)) {
58
58
  throw new Error(`incomplete ${field} connection`);
59
59
  }
60
60
  return connection.nodes;
@@ -89,12 +89,19 @@ const definitions = [
89
89
  ['security.pr-tests', 'Governance/Security'], ['security.status-checks', 'Governance/Security'], ['security.updates', 'Governance/Security'], ['security.secrets', 'Governance/Security'],
90
90
  ];
91
91
 
92
- function packageManager(files) {
92
+ function packageManager(files, pkg) {
93
93
  const locks = [
94
94
  ['bun', ['bun.lock', 'bun.lockb']], ['npm', ['package-lock.json', 'npm-shrinkwrap.json']],
95
95
  ['pnpm', ['pnpm-lock.yaml']], ['yarn', ['yarn.lock']],
96
96
  ].filter(([, names]) => names.some(files.has));
97
- if (!locks.length) return { reason: 'no lockfile' };
97
+ if (!locks.length) {
98
+ const dependencyFields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies', 'bundledDependencies', 'bundleDependencies', 'workspaces'];
99
+ if (!files.has('pnpm-workspace.yaml') && dependencyFields.every((field) => !pkg?.[field] || Object.keys(pkg[field]).length === 0)) {
100
+ if (typeof pkg?.packageManager === 'string' && /^bun@[^@\s]+$/.test(pkg.packageManager)) return { name: 'bun', paths: ['package.json: packageManager'] };
101
+ if (pkg?.packageManager === undefined && typeof pkg?.engines?.bun === 'string' && pkg.engines.bun.trim()) return { name: 'bun', paths: ['package.json: engines.bun'] };
102
+ }
103
+ return { reason: 'no lockfile' };
104
+ }
98
105
  if (locks.length > 1) return { reason: 'ambiguous lockfile' };
99
106
  const [name, paths] = locks[0];
100
107
  if (name === 'yarn' && !/^__metadata:/m.test(files.read('yarn.lock'))) return { reason: 'unsupported package manager' };
@@ -150,7 +157,7 @@ function assess(files) {
150
157
  }
151
158
  configured('style.pre-commit', has(/(^|\/)(\.pre-commit-config\.ya?ml|lefthook\.ya?ml|\.lefthook\.ya?ml|\.husky\/pre-commit)$/).concat(pkg.husky?.hooks?.['pre-commit'] ? ['package.json'] : []));
152
159
  configured('env.lockfile', has(/^(bun\.lockb?|package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|yarn\.lock)$/));
153
- const manager = packageManager(files);
160
+ const manager = packageManager(files, pkg);
154
161
  declaredCommands(files, pkg, manager, set, invalidPackage);
155
162
  configured('env.toolchain', has(/^(\.tool-versions|mise\.toml|\.mise\.toml|\.nvmrc|\.devcontainer\.json|\.devcontainer\/(devcontainer\.json|[^/]+\/devcontainer\.json))$/).concat(pkg.engines && Object.keys(pkg.engines).length ? ['package.json'] : []));
156
163
  configured('testing.files', has(/(^|\/)((tests?|__tests__)\/.*\.[cm]?[jt]sx?|[^/]+\.(test|spec)\.[cm]?[jt]sx?)$/));
@@ -168,7 +175,7 @@ function assess(files) {
168
175
  const steps = Object.values(workflow?.jobs ?? {}).flatMap((job) => job?.steps ?? []);
169
176
  const runs = steps.map((step) => step?.run).filter((run) => typeof run === 'string');
170
177
  if (steps.some((step) => typeof step?.uses === 'string' && /^github\/codeql-action\//.test(step.uses))) codeql.push(path);
171
- const invokes = (run, name) => new RegExp(`(?:^|[\\n;&|])\\s*${name}\\s+${name === 'bun' ? 'run\\s+' : '(?:run\\s+)?'}test(?:\\s|[;&|]|$)`).test(run);
178
+ const invokes = (run, name) => new RegExp(`(?:^|[\\n;&|])\\s*${name}\\s+(?:run\\s+)?test(?:\\s|[;&|]|$)`).test(run);
172
179
  if (pr && runs.some((run) => ['bun', 'npm', 'pnpm', 'yarn'].some((name) => invokes(run, name)))) prCandidate = true;
173
180
  if (pr && manager.name && runs.some((run) => invokes(run, manager.name))) prTests = true;
174
181
  } catch { invalid = true; }
@@ -194,19 +201,43 @@ export function summarizePillars(criteria) {
194
201
  return result;
195
202
  }
196
203
 
204
+ function parsePages(text) {
205
+ const pages = [];
206
+ let start = -1, depth = 0, quoted = false, escaped = false;
207
+ for (let i = 0; i < text.length; i++) {
208
+ const char = text[i];
209
+ if (start < 0) {
210
+ if (/\s/.test(char)) continue;
211
+ if (char !== '{' && char !== '[') throw new Error('invalid gh page');
212
+ start = i;
213
+ }
214
+ if (quoted) {
215
+ if (escaped) escaped = false;
216
+ else if (char === '\\') escaped = true;
217
+ else if (char === '"') quoted = false;
218
+ } else if (char === '"') quoted = true;
219
+ else if (char === '{' || char === '[') depth++;
220
+ else if (char === '}' || char === ']') depth--;
221
+ if (depth === 0) {
222
+ pages.push(JSON.parse(text.slice(start, i + 1)));
223
+ start = -1;
224
+ }
225
+ }
226
+ if (start >= 0 || !pages.length) throw new Error('incomplete gh response');
227
+ return pages;
228
+ }
229
+
197
230
  function forgeStatus(origin) {
198
231
  const match = /^https:\/\/github\.com\/([\w.-]+\/[\w.-]+)$/.exec(origin);
199
232
  const evidence = [];
200
233
  const result = (status, reason) => ({ status, reason, evidence: evidence.length ? evidence : ['origin remote'] });
201
234
  if (!match) return result('unknown', 'no GitHub origin');
202
235
  function api(endpoint) {
203
- const argv = [process.env.GH || 'gh', 'api', '--hostname', 'github.com', '--method', 'GET', '--paginate', '--slurp', endpoint];
204
- evidence.push(`gh api --method GET --paginate --slurp ${endpoint}`);
236
+ const argv = [process.env.GH || 'gh', 'api', '--hostname', 'github.com', '--method', 'GET', '--paginate', endpoint];
237
+ evidence.push(`gh api --method GET --paginate ${endpoint}`);
205
238
  const response = Bun.spawnSync(argv, { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' });
206
239
  if (response.exitCode !== 0) throw new Error(response.stderr.toString().trim());
207
- const pages = JSON.parse(response.stdout.toString());
208
- if (!Array.isArray(pages) || !pages.length) throw new Error('incomplete gh response');
209
- return pages;
240
+ return parsePages(response.stdout.toString());
210
241
  }
211
242
  try {
212
243
  const metadata = api(`repos/${match[1]}`);
@@ -268,7 +299,7 @@ async function report(args, paths) {
268
299
  if (args['--run']) {
269
300
  let pkg = {};
270
301
  try { pkg = JSON.parse(files.read('package.json') || '{}'); } catch { /* Static criteria already record invalid JSON. */ }
271
- execution = await executeDeclared(repo, revision, paths, criteria, packageManager(files), pkg, {
302
+ execution = await executeDeclared(repo, revision, paths, criteria, packageManager(files, pkg), pkg, {
272
303
  timeoutMs: Number(args['--timeout-ms'] ?? 120000), logBytes: Number(args['--log-bytes'] ?? 1048576),
273
304
  });
274
305
  header.network = execution.network;
@@ -28,8 +28,8 @@ immediately before an actual auditor profile or session dispatch. Ordinary
28
28
  audit reading and record writing do not load it, and the auditor never
29
29
  dispatches.
30
30
 
31
- Core owns the `axstack-auditor` profile (claude/sonnet high in
32
- mixed/claude-only; codex/luna xhigh in codex-only) and its invocation.
31
+ Core owns the `axstack-auditor` profile (claude/haiku xhigh in mixed,
32
+ claude/sonnet high in claude-only, codex/luna xhigh in codex-only) and its invocation.
33
33
  This skill governs what that auditor reads, measures, and proposes.
34
34
  Dispatch `axstack-auditor` and `axstack-auditor-sol` independently on the same
35
35
  bounded brief, without cross-reading. The driver reconciles findings per claim;
@@ -107,7 +107,7 @@ counts with denominators plus the evidence behind the count:
107
107
 
108
108
  | Preset | Author class | Reviewer (class/effort) |
109
109
  | --- | --- | --- |
110
- | `mixed` | `codex/sol` | `axstack-reviewer-secondary` (`claude/opus` medium) |
110
+ | `mixed` | `codex/sol` | `axstack-reviewer-secondary` (`claude/opus` high) |
111
111
  | `mixed` | `claude/opus` | `axstack-reviewer-primary` (`codex/sol` high) |
112
112
  | `codex-only` | `codex/sol` | `axstack-reviewer-secondary` (`codex/luna` xhigh) |
113
113
  | `claude-only` | `claude/opus` | `axstack-reviewer-secondary` (`claude/sonnet` high) |
@@ -223,14 +223,14 @@ 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
- The mixed peer pair is cross-provider (Sol high + Opus low), with independence
226
+ The mixed peer pair is cross-provider (Sol high + Opus medium), with independence
227
227
  still from separate sessions, the identical brief and an isolated first pass.
228
228
  - **Authored:** exactly one eligible independent reviewer from this complete
229
229
  mapping:
230
230
 
231
231
  | Preset | Actual author provider/class | Reviewer role (configured class/effort) |
232
232
  | --- | --- | --- |
233
- | `mixed` | `codex/sol` | `axstack-reviewer-secondary` (`claude/opus` medium) |
233
+ | `mixed` | `codex/sol` | `axstack-reviewer-secondary` (`claude/opus` high) |
234
234
  | `mixed` | `claude/opus` | `axstack-reviewer-primary` (`codex/sol` high) |
235
235
  | `codex-only` | `codex/sol` | `axstack-reviewer-secondary` (`codex/luna` xhigh) |
236
236
  | `claude-only` | `claude/opus` | `axstack-reviewer-secondary` (`claude/sonnet` high) |
package/src/archify.js CHANGED
@@ -19,6 +19,7 @@ export function archifyGit(path, ...args) {
19
19
 
20
20
  function verifySha(record, actual) {
21
21
  if (actual === record.sha) return;
22
+ /** @type {Error & {code?: string}} */
22
23
  const error = new Error(`archify SHA mismatch: expected ${record.sha}, got ${actual}`);
23
24
  error.code = 'ARCHIFY_SHA_MISMATCH';
24
25
  throw error;
@@ -31,7 +32,8 @@ async function ownersSnapshot(path) {
31
32
  });
32
33
  if (st && (!st.isFile() || st.isSymbolicLink())) throw new Error(`unsafe archify owners file: ${path}`);
33
34
  const bytes = st ? await readFile(path) : null;
34
- const owners = bytes ? JSON.parse(bytes) : [];
35
+ // JSON.parse coerces its argument to a string; preserve the existing Buffer coercion.
36
+ const owners = bytes ? /** @type {(text: {toString(): string}) => unknown} */ (JSON.parse)(bytes) : [];
35
37
  if (!Array.isArray(owners) || owners.some((owner) => typeof owner !== 'string' || !owner.startsWith('/'))) {
36
38
  throw new Error(`invalid archify owners file: ${path}`);
37
39
  }
@@ -184,7 +184,7 @@ export async function planUninstallClaudeSettings({ claude, skillsRoot, boundPat
184
184
  }
185
185
 
186
186
  const owned = sidecar.keys[CLAUDE_SETTING_PATH];
187
- if (!owned || !owned.owners.includes(skillsRoot)) {
187
+ if (!owned?.owners.includes(skillsRoot)) {
188
188
  return {
189
189
  report: { status: 'skipped', reason: 'this skills directory does not own the Claude setting' },
190
190
  writes: [],
package/src/installer.js CHANGED
@@ -179,7 +179,7 @@ function homeDir() {
179
179
  // aliased HOME) compare equal to canonicalized targets; an unresolvable
180
180
  // HOME also yields null and fails closed rather than bypassing the guard.
181
181
  const home = Bun.env.HOME;
182
- if (!home || !home.startsWith('/')) return null;
182
+ if (!home?.startsWith('/')) return null;
183
183
  try {
184
184
  return realpathSync(home);
185
185
  } catch {
@@ -238,7 +238,7 @@ export async function validateBundle(bundleDir, selectedPreset = null) {
238
238
  if (skillsStat.isSymbolicLink() || !skillsStat.isDirectory()) {
239
239
  throw new Error('bundle skills/ must be a real directory, not a symlink');
240
240
  }
241
- let entries = await readdir(skillsRoot, { withFileTypes: true });
241
+ const entries = await readdir(skillsRoot, { withFileTypes: true });
242
242
  // Symlinked (or non-directory) top-level skill entries must be rejected,
243
243
  // not silently skipped: Dirent.isDirectory() is false for symlinks.
244
244
  for (const entry of entries) {
@@ -380,6 +380,11 @@ async function walkSkills(dir, skillsRoot, out) {
380
380
  // never followed or truncated. After the rename, existing permissions are
381
381
  // preserved; new files take an explicit `mode` (e.g. 0600 for configs) or
382
382
  // the umask default. Guards pre-existing entries, not active races.
383
+ /**
384
+ * @param {string} dest
385
+ * @param {string | Uint8Array} bytes
386
+ * @param {{mode?: number}} [options]
387
+ */
383
388
  export async function writeAtomic(dest, bytes, { mode } = {}) {
384
389
  await mkdir(dirname(dest), { recursive: true });
385
390
  let existingMode = null;
@@ -408,6 +413,7 @@ export async function writeAtomic(dest, bytes, { mode } = {}) {
408
413
  }
409
414
  }
410
415
 
416
+ /** @param {Pick<InstallerOptions, 'skillsDir' | 'instructionsPath'>} [options] */
411
417
  export async function checkInstructionBinding({ skillsDir, instructionsPath } = {}) {
412
418
  if (!skillsDir) throw new Error('instruction check requires --skills-dir <dir> or --harness <name>');
413
419
  if (!instructionsPath) throw new Error('instruction check requires --instructions <file> or a supported harness');
@@ -467,6 +473,21 @@ async function assertFileSnapshot(dest, expected) {
467
473
  }
468
474
  }
469
475
 
476
+ /**
477
+ * @typedef {{available: boolean, settingsPath?: string, skip?: boolean, reason?: string}} ClaudeOption
478
+ * @typedef {Object} InstallerOptions
479
+ * @property {string} [skillsDir]
480
+ * @property {string} [instructionsPath]
481
+ * @property {boolean} [force]
482
+ * @property {boolean} [yes]
483
+ * @property {ClaudeOption} [claude]
484
+ * @property {(message: string) => void} [log]
485
+ */
486
+
487
+ /**
488
+ * @param {InstallerOptions & {bundleDir?: string, preset?: string, toolsDir?: string,
489
+ * inheritedInstructions?: {path: string, hash: string}}} [options]
490
+ */
470
491
  export async function installBundle({
471
492
  bundleDir,
472
493
  skillsDir,
@@ -857,6 +878,7 @@ export async function installBundle({
857
878
  }
858
879
  }
859
880
 
881
+ /** @param {InstallerOptions} [options] */
860
882
  export async function uninstallBundle({
861
883
  skillsDir,
862
884
  instructionsPath = null,
@@ -1110,6 +1132,10 @@ export async function uninstallBundle({
1110
1132
  // is present and byte-for-byte verified. This deliberately handles skill
1111
1133
  // payloads only: instruction files, Claude settings, and historical profile
1112
1134
  // ownership may belong to another harness and remain bound to the old manifest.
1135
+ /**
1136
+ * @param {{canonicalSkillsDir?: string, legacySkillsDir?: string, yes?: boolean,
1137
+ * acceptedInstructionTransfer?: {path: string, legacyHash: string, canonicalHash: string}}} [options]
1138
+ */
1113
1139
  export async function retireLegacySkills({
1114
1140
  canonicalSkillsDir,
1115
1141
  legacySkillsDir,
package/src/posixpath.js CHANGED
@@ -65,7 +65,8 @@ export function resolveFrom(cwd, ...paths) {
65
65
  }
66
66
 
67
67
  export function resolve(...paths) {
68
- return resolveFrom(Bun.cwd, ...paths);
68
+ // Bun.cwd is a runtime string missing from the current Bun declarations.
69
+ return resolveFrom(/** @type {typeof Bun & {cwd: string}} */ (Bun).cwd, ...paths);
69
70
  }
70
71
 
71
72
  export function relative(from, to) {
package/src/roles.js CHANGED
@@ -18,7 +18,7 @@ export function deriveModelClass(model) {
18
18
 
19
19
  const AUTHORED_ROUTES = Object.freeze({
20
20
  mixed: {
21
- 'codex/sol': ['axstack-reviewer-secondary', 'claude/opus', 'medium'],
21
+ 'codex/sol': ['axstack-reviewer-secondary', 'claude/opus', 'high'],
22
22
  'claude/opus': ['axstack-reviewer-primary', 'codex/sol', 'high'],
23
23
  },
24
24
  'codex-only': {