axstack 0.9.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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/bin/axstack.js +396 -0
  4. package/docs/installation.md +239 -0
  5. package/docs/workflows.md +220 -0
  6. package/package.json +40 -0
  7. package/profiles/presets/claude-only.json +194 -0
  8. package/profiles/presets/codex-only.json +194 -0
  9. package/profiles/presets/mixed.json +194 -0
  10. package/skills/axstack/SKILL.md +81 -0
  11. package/skills/axstack/references/automations.md +368 -0
  12. package/skills/axstack/references/candidate-publication.md +45 -0
  13. package/skills/axstack/references/contracts.md +102 -0
  14. package/skills/axstack/references/lifecycle.md +137 -0
  15. package/skills/axstack/references/orca-runtime.md +109 -0
  16. package/skills/axstack/references/pr-shape.md +39 -0
  17. package/skills/axstack/references/routing.md +129 -0
  18. package/skills/axstack/references/run-record.md +109 -0
  19. package/skills/axstack-align/SKILL.md +121 -0
  20. package/skills/axstack-audit/SKILL.md +137 -0
  21. package/skills/axstack-audit/references/record.md +28 -0
  22. package/skills/axstack-debug/SKILL.md +157 -0
  23. package/skills/axstack-debug/references/packet.md +80 -0
  24. package/skills/axstack-explain/SKILL.md +66 -0
  25. package/skills/axstack-explain/references/visual-qa.md +15 -0
  26. package/skills/axstack-implement/SKILL.md +164 -0
  27. package/skills/axstack-improve/SKILL.md +69 -0
  28. package/skills/axstack-relay/SKILL.md +102 -0
  29. package/skills/axstack-research/SKILL.md +57 -0
  30. package/skills/axstack-research/references/checklist.md +25 -0
  31. package/skills/axstack-review/SKILL.md +343 -0
  32. package/skills/axstack-spec/SKILL.md +67 -0
  33. package/skills/axstack-tickets/SKILL.md +86 -0
  34. package/skills/axstack-watch/SKILL.md +160 -0
  35. package/skills/axstack-watch/references/repair-publication.md +69 -0
  36. package/skills/axstack-watch/references/watch-runtime.md +60 -0
  37. package/src/capabilities.js +138 -0
  38. package/src/claude-settings.js +230 -0
  39. package/src/installer.js +980 -0
  40. package/src/instructions.js +100 -0
  41. package/src/locations.js +43 -0
  42. package/src/manifest.js +251 -0
  43. package/src/posixpath.js +108 -0
  44. package/src/roles.js +142 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Axat Bhardwaj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # Axstack
2
+
3
+ Axstack is a standalone toolkit for finishing agreed engineering work with less
4
+ supervision while retaining independent review. Chat drives execution; a Bun
5
+ CLI installs owned skills and role data and checks capabilities. Axstack has no
6
+ daemon, scheduler, runtime database, or workflow state machine.
7
+
8
+ Orca is the only supported active runtime. Its installed, version-matched
9
+ `orchestration` and `orca-cli` guides own worktrees, sessions, supervised
10
+ dispatch, messages, settlement, and handoff mechanics. Axstack owns scope,
11
+ role choices, evidence, review policy, and one private derived run record.
12
+
13
+ ## How a run works
14
+
15
+ Invoke `axstack` or the needed phase directly: `axstack-align`,
16
+ `axstack-spec`, `axstack-tickets`, `axstack-implement`, `axstack-review`, and
17
+ `axstack-watch`. Direct `axstack-research`, `axstack-explain`,
18
+ `axstack-improve`, and `axstack-debug` routes need no spec ceremony. `axstack-relay` remains an
19
+ optional inline route for explicit messages and authorized notifications; an
20
+ unavailable or legacy-runtime-only relay falls back to the current conversation
21
+ without changing authority.
22
+
23
+ 1. Classify new engineering work as substantial, small, or unclear with a brief
24
+ reason. Small, bounded one-PR work uses the current request or selected issue
25
+ as a snapshotted small-change intent;
26
+ substantial or stacked work needs an approved spec and matching ticket map.
27
+ 2. The current chat drives on whatever model runs it; there is no driver
28
+ profile. Bind each ready task to the selected role snapshot and an authoritative Orca
29
+ Run, Task, and Dispatch. Exactly one writer owns a candidate at a time.
30
+ 3. Peer PRs receive both configured independent reviewer roles. Authored PRs
31
+ receive one eligible reviewer from the selected preset's explicit mapping
32
+ and actual author provenance. Every review binds the exact head and base.
33
+ Stable IDs are `axstack-reviewer-primary` and `axstack-reviewer-secondary`.
34
+ 4. Accepted repairs return to the same author where its session and evidence
35
+ remain valid. The human merges by default, bottom-up for a stack.
36
+ 5. Full handoff requires explicit recipient acceptance of the exact scope and
37
+ authority before ownership changes. Ordinary resume reconciles the current
38
+ owner instead of replacing it.
39
+
40
+ Active PR fanout is dependency- and capacity-driven; there is no fixed count.
41
+ Each PR has one theme and a measured size under the shared
42
+ [PR-shape policy](skills/axstack/references/pr-shape.md). Routine shape and
43
+ fanout choices remain autonomous inside approved scope. Material scope,
44
+ serious-risk, unavailable-model, and human-merge holds remain explicit.
45
+ The autonomous driver records the rationale band's cohesion rationale; the exception band needs a
46
+ reasonable split attempt and full exception record. Size alone never requires
47
+ user approval.
48
+
49
+ ## Install from source
50
+
51
+ Requirements: Bun >=1.3.14, Git, `gh`, the `gh stack` extension, and a running
52
+ Orca with its runtime-owned guides. Filesystem access uses Bun's implementation
53
+ of `node:fs` and `node:fs/promises`; there are no runtime dependencies.
54
+
55
+ ```sh
56
+ bun bin/axstack.js check --bundle . [--harness claude|codex]
57
+ bun bin/axstack.js install --bundle . --skills-dir <dir> --instructions <file> --preset mixed [--yes]
58
+ bun bin/axstack.js install --bundle . --skills-dir <dir> --preset mixed
59
+ bun bin/axstack.js uninstall --skills-dir <dir> --instructions <file>
60
+ ```
61
+
62
+ Use `--harness codex` or `--harness claude` only for a verified default skill
63
+ directory. `--claude-settings` and `--no-claude-settings` manage the existing
64
+ Claude Code subagent default transaction; they do not configure Orca roles.
65
+ See [installation details](docs/installation.md).
66
+
67
+ `--instructions` manages one versioned Axstack block in `AGENTS.md` or
68
+ `CLAUDE.md`. Harness defaults resolve those files automatically. The block
69
+ points at the installed entry skill and requires delegated work to use visible
70
+ Orca orchestration rather than a harness-native subagent tool. Text and file
71
+ mode outside the markers are preserved; edited, malformed, unowned, or unsafe
72
+ targets are reported without normal-path adoption. Install exits nonzero when
73
+ an instruction conflict is preserved, while clean and idempotent installs exit
74
+ successfully.
75
+
76
+ The public bundle preserves three canonical 21-role inputs:
77
+ [mixed](profiles/presets/mixed.json),
78
+ [codex-only](profiles/presets/codex-only.json), and
79
+ [claude-only](profiles/presets/claude-only.json). Each is exactly
80
+ `{ "version": 1, "roles": [...] }`. Installation writes the selected snapshot
81
+ to `<skills-dir>/axstack/roles.json` as
82
+ `{ "version": 1, "preset": "<name>", "roles": [...] }` under normal ownership
83
+ hashes. An edited installed role file is preserved.
84
+
85
+ Mixed configures independent Astra and Fable advisers at high. Single-provider
86
+ presets preserve both adviser IDs and mark the unavailable one with `model:
87
+ null` inside that preset's provider bounds; installation remains ready, while
88
+ Align and Spec hold because both receipts are required. The mixed
89
+ `axstack-checker` model is also intentionally `null`; that role stays held
90
+ instead of inheriting a provider default. The single-provider presets configure
91
+ the checker. Preset changes affect new runs only. Stored model, effort, and permission
92
+ fields are declared intent until actual Orca launch receipts establish effective
93
+ behavior; installation never proves provider availability or permission parity.
94
+ Subscription availability and quota never select a fallback model.
95
+ End-to-end compatibility remains unverified without matching runtime receipts.
96
+
97
+ ## Runtime evidence and holds
98
+
99
+ Native Orca exercises have returned Codex and Claude worker completions,
100
+ same-terminal follow-up, separate worktree placement, settlement cleanup,
101
+ `user_takeover` retention, and recovery from `consumer_fenced`. These are
102
+ bounded runtime facts, not proof that every role or harness is compatible.
103
+
104
+ Input acceptance is not agent readiness. A trust prompt was observed after an
105
+ accepted launch, so startup recovery must inspect the existing attempt, never
106
+ answer trust or permission prompts on the worker's behalf, and never create a
107
+ duplicate writer. A `worker_done` advances work only when its Task and Dispatch
108
+ match the active attempt and its revision evidence verifies.
109
+
110
+ The user lifted the native-watch hold by decision on 2026-09-16. Orca's native automation
111
+ schema exposes a provider but cannot pin model, effort, or permission, so the
112
+ driver automation records its model identity every tick and the watchdog treats
113
+ a mismatch as a safety hold. Axstack uses no historical fallback and introduces
114
+ no custom scheduler. The five-minute driver, hourly read-only watchdog, quiet
115
+ healthy ticks, and shared 24-hour deadline remain the acceptance contract.
116
+
117
+ Mobile completion/reply behavior remains unverified. Structural checks and
118
+ qualitative scenario evaluation are not live runtime proof.
119
+
120
+ ## Historical migration boundary
121
+
122
+ Older releases used Paseo for orchestration and could leave profile ownership
123
+ provenance or retired skills behind. That state is historical and inert in the
124
+ Orca runtime. Migration preserves user-edited and unknown assets and records
125
+ legacy ownership without reading, writing, or deleting live host configuration.
126
+ Use the explicit migration guidance in [installation](docs/installation.md);
127
+ release installation, host cutover, and old-timer cleanup need separate
128
+ authorization.
129
+
130
+ ## License
131
+
132
+ [MIT](LICENSE) © 2026 Axat Bhardwaj.
package/bin/axstack.js ADDED
@@ -0,0 +1,396 @@
1
+ #!/usr/bin/env bun
2
+ // axstack — installation/setup CLI. Installs owned skill bundles and selected
3
+ // role data, configures Claude settings, and checks host capabilities. Chat
4
+ // CLI performs installation bookkeeping only (no runtime, scheduler, or
5
+ // model calls).
6
+ import { readFileSync } from 'node:fs';
7
+ import { join, resolve } from '../src/posixpath.js';
8
+ import {
9
+ checkInstructionBinding,
10
+ installBundle,
11
+ uninstallBundle,
12
+ validateBundle,
13
+ } from '../src/installer.js';
14
+ import { BUN_FLOOR, checkCapabilities, meetsFloor, runRealCheck } from '../src/capabilities.js';
15
+ import { harnessLocations } from '../src/locations.js';
16
+
17
+ // import.meta.dir is already a filesystem path (no URL conversion, so
18
+ // spaces/percent/hash characters survive verbatim).
19
+ const HERE = import.meta.dir;
20
+ const PACKAGE_ROOT = resolve(HERE, '..');
21
+
22
+ function homeDir() {
23
+ // Tilde expansion needs a known absolute home; with HOME missing, empty,
24
+ // or relative the destination is unknown, so refuse even with --yes.
25
+ const home = Bun.env.HOME;
26
+ if (!home || !home.startsWith('/')) {
27
+ throw new Error('HOME is missing, empty, or not absolute; pass explicit paths instead of ~');
28
+ }
29
+ return home;
30
+ }
31
+
32
+ function packageVersion() {
33
+ try {
34
+ return JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
35
+ } catch {
36
+ return 'unknown';
37
+ }
38
+ }
39
+
40
+ const HELP = `axstack — Axstack setup CLI (installation bookkeeping only)
41
+
42
+ Usage:
43
+ axstack install --preset <mixed|codex-only|claude-only> --bundle <dir> --skills-dir <dir> [--instructions <file>] [--claude-settings <file>|--no-claude-settings] [--harness <name>] [--force] [--yes]
44
+ axstack check [--bundle <dir>] [--instructions <file>] [--skills-dir <dir>|--harness <name>]
45
+ axstack uninstall --skills-dir <dir> [--instructions <file>] [--claude-settings <file>|--no-claude-settings] [--harness <name>] [--force] [--yes]
46
+ axstack --help | --version
47
+
48
+ Commands:
49
+ install Validate a skill bundle, then copy owned skills and selected role
50
+ data and record ownership hashes.
51
+ check Probe Bun, Git, gh stack, and resolved Orca runtime/guide
52
+ capabilities. Probes do not prove model or execution compatibility.
53
+ uninstall Remove only unchanged Axstack-owned assets. User edits survive.
54
+
55
+ Flags:
56
+ --bundle <dir> Bundle root holding skills/axstack-*/SKILL.md and
57
+ profiles/presets/*.json role data. Defaults to the package root.
58
+ --preset <name> Required routing preset: mixed, codex-only, or claude-only.
59
+ Aliases: codex = codex-only; claude = claude-only.
60
+ --skills-dir <dir> Explicit install target (required). Overrides --harness.
61
+ --instructions <file>
62
+ Instruction file to receive the owned routing block.
63
+ Harness defaults: ~/.claude/CLAUDE.md for Claude and
64
+ $CODEX_HOME/AGENTS.md for Codex (default ~/.codex).
65
+ --claude-settings <file>
66
+ Manage the Claude Code user settings file at this path.
67
+ This forces testable availability without installing Claude.
68
+ --no-claude-settings
69
+ Skip Claude Code user-settings management.
70
+ --harness <name> Known harness (${harnessLocations().map((h) => h.harness).join(', ')}).
71
+ Grok has no verified auto-discovery: --skills-dir is required.
72
+ --force Overwrite/remove user-edited owned assets and take
73
+ ownership of unknown files. Off by default.
74
+ --yes Confirm writes inside your home directory. Temp dirs
75
+ outside home never need this.
76
+
77
+ Migration:
78
+ --profile is obsolete. Axstack installs the selected preset as
79
+ <skills-dir>/axstack/roles.json and never reads or writes Paseo config.
80
+
81
+ Safety:
82
+ Bundles with symlinks, absolute paths or '..' escapes are rejected before
83
+ any write. Unknown pre-existing files are never silently overwritten.
84
+ Partial failures roll back files created in that run.
85
+ `;
86
+
87
+ function parseArgs(argv) {
88
+ const out = { command: null, flags: {} };
89
+ const rest = [...argv];
90
+ if (rest.length === 0 || rest[0] === '--help' || rest[0] === '-h') {
91
+ out.command = 'help';
92
+ return out;
93
+ }
94
+ if (rest[0] === '--version' || rest[0] === '-V') {
95
+ out.command = 'version';
96
+ return out;
97
+ }
98
+ out.command = rest.shift();
99
+ const wantsValue = new Set([
100
+ '--bundle', '--skills-dir', '--instructions', '--harness', '--preset', '--claude-settings',
101
+ ]);
102
+ while (rest.length > 0) {
103
+ const tok = rest.shift();
104
+ if (tok === '--profile') {
105
+ throw new Error(
106
+ '--profile is obsolete in the Orca-only installer; role data is installed as axstack/roles.json and legacy Paseo cleanup is separate',
107
+ );
108
+ }
109
+ if (wantsValue.has(tok)) {
110
+ const val = rest.shift();
111
+ if (val === undefined || val.startsWith('--')) {
112
+ throw new Error(`${tok} requires a value (got ${val ?? 'nothing'}); refusing`);
113
+ }
114
+ out.flags[tok.slice(2)] = val;
115
+ } else if (tok === '--force') out.flags.force = true;
116
+ else if (tok === '--yes') out.flags.yes = true;
117
+ else if (tok === '--no-claude-settings') out.flags['no-claude-settings'] = true;
118
+ else if (tok === '--help' || tok === '-h') out.command = 'help';
119
+ else throw new Error(`unknown argument: ${tok}`);
120
+ }
121
+ return out;
122
+ }
123
+
124
+ function defaultClaudeSettingsPath() {
125
+ if (Bun.env.CLAUDE_CONFIG_DIR) {
126
+ return join(resolve(Bun.env.CLAUDE_CONFIG_DIR), 'settings.json');
127
+ }
128
+ const home = Bun.env.HOME;
129
+ return home?.startsWith('/') ? join(home, '.claude', 'settings.json') : null;
130
+ }
131
+
132
+ function isInsideRawHome(path) {
133
+ const home = Bun.env.HOME;
134
+ if (!home?.startsWith('/') || !path) return false;
135
+ const absoluteHome = resolve(home);
136
+ const absolutePath = resolve(path);
137
+ return absolutePath === absoluteHome || absolutePath.startsWith(`${absoluteHome}/`);
138
+ }
139
+
140
+ function resolveClaudeOption(flags, command) {
141
+ if (flags['claude-settings'] && flags['no-claude-settings']) {
142
+ throw new Error('--claude-settings and --no-claude-settings cannot be used together');
143
+ }
144
+ if (flags['no-claude-settings']) {
145
+ return { available: false, settingsPath: null, skip: true, reason: 'disabled by --no-claude-settings' };
146
+ }
147
+ if (flags['claude-settings']) {
148
+ return { available: true, settingsPath: expandHome(flags['claude-settings']), explicit: true };
149
+ }
150
+
151
+ const settingsPath = defaultClaudeSettingsPath();
152
+ const available = Bun.which('claude') !== null;
153
+ if (command === 'install' && available && !flags.yes && isInsideRawHome(settingsPath)) {
154
+ return {
155
+ available: false,
156
+ settingsPath: null,
157
+ reason: 'default settings path is inside HOME; re-run with --yes',
158
+ };
159
+ }
160
+ return {
161
+ available,
162
+ settingsPath: available ? settingsPath : null,
163
+ reason: available ? undefined : 'Claude Code is not available',
164
+ };
165
+ }
166
+
167
+ function printClaudeSettings(report, { uninstall = false } = {}) {
168
+ if (!report) return;
169
+ if (report.status === 'skipped') {
170
+ console.log(`claude settings: skipped — ${report.reason}`);
171
+ } else if (!uninstall && report.status === 'set') {
172
+ console.log(`claude settings: set env.CLAUDE_CODE_SUBAGENT_MODEL=opus in ${report.path}`);
173
+ } else if (!uninstall && report.status === 'unchanged') {
174
+ console.log('claude settings: unchanged (owned)');
175
+ } else if (!uninstall && report.status === 'preserved') {
176
+ console.log(`claude settings: preserved existing value ${JSON.stringify(report.value)} (not owned)`);
177
+ } else if (uninstall && report.status === 'removed') {
178
+ console.log(`claude settings: removed env.CLAUDE_CODE_SUBAGENT_MODEL from ${report.path}`);
179
+ } else if (uninstall && report.status === 'retained') {
180
+ console.log('claude settings: unchanged (owned by another skills install)');
181
+ } else if (uninstall && report.status === 'preserved') {
182
+ console.log(`claude settings: preserved edited value ${JSON.stringify(report.value)} (ownership released)`);
183
+ } else if (uninstall && report.status === 'released') {
184
+ console.log('claude settings: ownership released (bound settings file missing)');
185
+ }
186
+ }
187
+
188
+ function expandHome(p) {
189
+ if (p === '~') return homeDir();
190
+ if (p.startsWith('~/')) return join(homeDir(), p.slice(2));
191
+ return p;
192
+ }
193
+
194
+ function resolveHarnessTarget(harness) {
195
+ const table = harnessLocations();
196
+ const entry = table.find((h) => h.harness === harness);
197
+ if (!entry) throw new Error(`unknown harness: ${harness} (known: ${table.map((h) => h.harness).join(', ')})`);
198
+ if (entry.discovery === 'unverified') {
199
+ throw new Error(
200
+ `harness '${harness}' has no verified auto-discovery; pass an explicit --skills-dir override`,
201
+ );
202
+ }
203
+ if (entry.harness === 'codex') {
204
+ // User skills live under $CODEX_HOME/skills; CODEX_HOME defaults to ~/.codex.
205
+ const home = Bun.env.CODEX_HOME ? expandHome(Bun.env.CODEX_HOME) : join(homeDir(), '.codex');
206
+ return join(home, 'skills');
207
+ }
208
+ return expandHome(entry.skillsDir);
209
+ }
210
+
211
+ function resolveHarnessInstructions(harness) {
212
+ if (harness === 'claude') return join(homeDir(), '.claude', 'CLAUDE.md');
213
+ if (harness === 'codex') {
214
+ const codexHome = Bun.env.CODEX_HOME
215
+ ? expandHome(Bun.env.CODEX_HOME)
216
+ : join(homeDir(), '.codex');
217
+ return join(codexHome, 'AGENTS.md');
218
+ }
219
+ return null;
220
+ }
221
+
222
+ function printCheckReport(report) {
223
+ console.log('Capability check:');
224
+ for (const c of report.checks) {
225
+ console.log(` [${c.ok ? 'ok' : 'MISSING'}] ${c.label}: ${c.detail}`);
226
+ }
227
+ if (report.gaps.length > 0) {
228
+ console.log('Gaps detected:');
229
+ for (const g of report.gaps) console.log(` - ${g}`);
230
+ } else {
231
+ console.log('No gaps detected.');
232
+ }
233
+ console.log('Probe limits:');
234
+ for (const line of report.limitations) console.log(` - ${line}`);
235
+ }
236
+
237
+ function checkRuntimeFloor() {
238
+ if (!meetsFloor(Bun.version, BUN_FLOOR)) {
239
+ console.error(`axstack: requires Bun >= ${BUN_FLOOR} (running ${Bun.version})`);
240
+ process.exitCode = 1;
241
+ return false;
242
+ }
243
+ return true;
244
+ }
245
+
246
+ async function main() {
247
+ if (!checkRuntimeFloor()) return;
248
+ let parsed;
249
+ try {
250
+ parsed = parseArgs(Bun.argv.slice(2));
251
+ } catch (err) {
252
+ console.error(`axstack: ${err.message}`);
253
+ console.error(HELP);
254
+ process.exitCode = 1;
255
+ return;
256
+ }
257
+ const { command, flags } = parsed;
258
+
259
+ if (command === 'help') {
260
+ console.log(HELP);
261
+ return;
262
+ }
263
+ if (command === 'version') {
264
+ console.log(`axstack ${packageVersion()}`);
265
+ return;
266
+ }
267
+
268
+ try {
269
+ if (command === 'install') {
270
+ let skillsDir = flags['skills-dir'] ? expandHome(flags['skills-dir']) : null;
271
+ if (!skillsDir && flags.harness) skillsDir = resolveHarnessTarget(flags.harness);
272
+ const instructionsPath = flags.instructions
273
+ ? expandHome(flags.instructions)
274
+ : flags.harness
275
+ ? resolveHarnessInstructions(flags.harness)
276
+ : null;
277
+ const summary = await installBundle({
278
+ bundleDir: flags.bundle ? resolve(flags.bundle) : PACKAGE_ROOT,
279
+ skillsDir,
280
+ preset: flags.preset,
281
+ instructionsPath,
282
+ force: !!flags.force,
283
+ yes: !!flags.yes,
284
+ claude: resolveClaudeOption(flags, 'install'),
285
+ log: (m) => { if (m !== 'plan complete') console.log(m); },
286
+ });
287
+ const changed =
288
+ summary.added.length + summary.updated.length + summary.removed.length +
289
+ (['created', 'updated'].includes(summary.instructions?.status) ? 1 : 0);
290
+ if (changed === 0) {
291
+ console.log('Install complete: no changes (idempotent, everything unchanged).');
292
+ } else {
293
+ if (summary.added.length) console.log(`added: ${summary.added.join(', ')}`);
294
+ if (summary.updated.length) console.log(`updated: ${summary.updated.join(', ')}`);
295
+ if (summary.removed.length) console.log(`removed: ${summary.removed.join(', ')}`);
296
+ if (summary.unchanged.length) console.log(`unchanged: ${summary.unchanged.join(', ')}`);
297
+ }
298
+ if (summary.preserved.length) {
299
+ console.log(`preserved user edits (use --force to overwrite): ${summary.preserved.join(', ')}`);
300
+ }
301
+ if (summary.stale.length) console.log(`stale owned files left on disk: ${summary.stale.join(', ')}`);
302
+ if (summary.instructions) {
303
+ const i = summary.instructions;
304
+ if (i.status === 'conflict') {
305
+ console.log(`instruction conflict: ${i.path} preserved (${i.reason})`);
306
+ process.exitCode = 1;
307
+ } else {
308
+ console.log(`instruction ${i.status}: ${i.path}`);
309
+ }
310
+ }
311
+ if (summary.notes?.length) {
312
+ for (const note of summary.notes) console.log(`note: ${note}`);
313
+ }
314
+ if (summary.roles) {
315
+ const p = summary.roles;
316
+ console.log(
317
+ p.ready
318
+ ? `preset ${summary.preset}: ready`
319
+ : `preset ${summary.preset}: NOT ready — ${p.gaps.join('; ')}`,
320
+ );
321
+ if (!p.ready) process.exitCode = 1;
322
+ } else {
323
+ console.log('role data was not installed; readiness is unverified.');
324
+ }
325
+ printClaudeSettings(summary.claudeSettings);
326
+ return;
327
+ }
328
+ if (command === 'check') {
329
+ const report = await checkCapabilities(runRealCheck);
330
+ printCheckReport(report);
331
+ if (flags.bundle) {
332
+ const bundle = await validateBundle(resolve(flags.bundle));
333
+ const presetNames = Object.keys(bundle.presets);
334
+ console.log(
335
+ `bundle ok: ${bundle.files.length} skill files, ` +
336
+ (presetNames.length > 0 ? `role presets ${presetNames.join(', ')}` : 'no role presets'),
337
+ );
338
+ }
339
+ if (flags.instructions || flags.harness) {
340
+ let skillsDir = flags['skills-dir'] ? expandHome(flags['skills-dir']) : null;
341
+ if (!skillsDir && flags.harness) skillsDir = resolveHarnessTarget(flags.harness);
342
+ const instructionsPath = flags.instructions
343
+ ? expandHome(flags.instructions)
344
+ : resolveHarnessInstructions(flags.harness);
345
+ const instruction = await checkInstructionBinding({ skillsDir, instructionsPath });
346
+ console.log(
347
+ `instruction ${instruction.status}: ${instruction.path}` +
348
+ (instruction.reason ? ` (${instruction.reason})` : ''),
349
+ );
350
+ if (instruction.status !== 'owned') process.exitCode = 1;
351
+ }
352
+ if (report.gaps.length > 0) process.exitCode = 1;
353
+ return;
354
+ }
355
+ if (command === 'uninstall') {
356
+ let skillsDir = flags['skills-dir'] ? expandHome(flags['skills-dir']) : null;
357
+ if (!skillsDir && flags.harness) skillsDir = resolveHarnessTarget(flags.harness);
358
+ const instructionsPath = flags.instructions
359
+ ? expandHome(flags.instructions)
360
+ : flags.harness
361
+ ? resolveHarnessInstructions(flags.harness)
362
+ : null;
363
+ const summary = await uninstallBundle({
364
+ skillsDir,
365
+ instructionsPath,
366
+ force: !!flags.force,
367
+ yes: !!flags.yes,
368
+ claude: resolveClaudeOption(flags, 'uninstall'),
369
+ log: (m) => console.log(m),
370
+ });
371
+ if (summary.note) console.log(summary.note);
372
+ if (summary.removed.length) console.log(`removed: ${summary.removed.join(', ')}`);
373
+ if (summary.preserved.length) {
374
+ console.log(`preserved user edits (use --force to remove): ${summary.preserved.join(', ')}`);
375
+ }
376
+ if (summary.instructions) {
377
+ const i = summary.instructions;
378
+ if (i.status === 'conflict' || i.status === 'preserved') {
379
+ console.log(`instruction ${i.status}: ${i.path} preserved (${i.reason})`);
380
+ } else {
381
+ console.log(`instruction ${i.status}: ${i.path}`);
382
+ }
383
+ }
384
+ printClaudeSettings(summary.claudeSettings, { uninstall: true });
385
+ return;
386
+ }
387
+ console.error(`axstack: unknown command: ${command}`);
388
+ console.error(HELP);
389
+ process.exitCode = 1;
390
+ } catch (err) {
391
+ console.error(`axstack: ${err.message}`);
392
+ process.exitCode = 1;
393
+ }
394
+ }
395
+
396
+ await main();