mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-09 10:18:32 +01:00
Install /captain only on Capy machines (GST-1833) (#4143)
* Load the Capy crew skills only on Capy machines (GST-1833) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Add changelog entry for Capy-only crew skills (GST-1833) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Test that the harness registrar leaves /captain out off Capy Review-Finding: p1/specialist-batched#2 reader=testing Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Say in the skills guide that /captain installs only on Capy machines Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
7cb151bff7
commit
bb3b7d072a
10 files changed
+239
-12
No files matched your search
@@ -205,7 +205,7 @@ The same `/qa` and `/qa-only` commands test CLIs, APIs, jobs, workers and webhoo
|
||||
| `/setup-gbrain` | **GBrain Onboarding** — from zero to running gbrain in under 5 minutes. PGLite local, Supabase existing URL, or auto-provision a new Supabase project via Management API. MCP registration for Claude Code + per-repo trust triad (read-write/read-only/deny). [Full guide](USING_GBRAIN_WITH_GSTACK.md). |
|
||||
| `/sync-gbrain` | **Keep Brain Current** — re-index this repo's code into gbrain via `gbrain sources add` + `gbrain sync --strategy code`, refresh the `## GBrain Search Guidance` block in CLAUDE.md, and auto-remove guidance when the capability check fails. `--incremental` (default), `--full`, `--dry-run`. Idempotent; safe to re-run. |
|
||||
| `/gstack-upgrade` | **Self-Updater** — upgrade gstack to latest. Detects global vs vendored install, syncs both, shows what changed. |
|
||||
| `/captain` | **Crew Controller** — run many coding agents from one controller: lanes send a READY block and never merge, the controller merges through trains (stamp on call, verify, merge with the expected head), keeps the digest, relays approvals in a fixed shape and runs a daily flaky-fix pass. Also answers to `/crew`. [Team guide](docs/captain.md). |
|
||||
| `/captain` | **Crew Controller** (Capy only: [gstack on Capy](#gstack-on-capy)) — run many coding agents from one controller: lanes send a READY block and never merge, the controller merges through trains (stamp on call, verify, merge with the expected head), keeps the digest, relays approvals in a fixed shape and runs a daily flaky-fix pass. Also answers to `/crew`. [Team guide](docs/captain.md). |
|
||||
| `/ios-qa` | **iOS Live-Device QA (v1.43.0.0+)** — drive a real iPhone or iPad over USB CoreDevice via an embedded `StateServer` in the app. Read Swift source, codegen typed `@Observable` accessors, run the agent loop. Optional `--tailnet` flag exposes the device to OpenClaw or any HTTP-capable agent on your Tailscale tailnet so remote agents can run iOS QA without ever touching the hardware. Capability-tier allowlist (observe/interact/mutate/restore), per-device session lock, audit log. |
|
||||
| `/ios-fix`, `/ios-design-review`, `/ios-clean`, `/ios-sync` | iOS bug-fix loop, designer's-eye HIG audit, debug-bridge cleanup, and accessor resync. See `docs/skills.md`. End-to-end walkthrough: [docs/howto-ios-testing-with-gstack.md](docs/howto-ios-testing-with-gstack.md). |
|
||||
|
||||
@@ -225,6 +225,10 @@ The four setup paths, the per-repo trust policy and memory sync, in brief: [docs
|
||||
|
||||
Other references: [docs/gbrain-sync.md](docs/gbrain-sync.md) (sync-specific guide) • [docs/gbrain-sync-errors.md](docs/gbrain-sync-errors.md) (error index)
|
||||
|
||||
## gstack on Capy
|
||||
|
||||
On a [Capy](https://capy.ai) machine, `./setup` also loads gstack's crew tools: `/captain` to run many agent threads from one controller, a merge queue that lands verified pull requests, receipts that tie each merge to the head that was reviewed and tested, and spend caps for `/ship` and `/autoplan`. They live in [crew/](crew/README.md) and load only on Capy, so any other install is unchanged by them.
|
||||
|
||||
## Platform notes
|
||||
|
||||
macOS and Linux get full support. Windows and Gemini models get minimal support: install and the core skills work, and nothing more is promised. See [support tiers](docs/support-tiers.md).
|
||||
|
||||
@@ -347,3 +347,19 @@ gstack_disabled_normalize() {
|
||||
# entries are still removed. Same list as RETIRED_SKILLS in
|
||||
# scripts/discover-skills.ts (test/retired-skills.test.ts).
|
||||
GSTACK_RETIRED_SKILLS="plan-eng-review investigate"
|
||||
|
||||
# GSTACK_CREW_SKILLS — the gstack-on-Capy skills (crew/README.md). Only a Capy
|
||||
# machine registers them; anywhere else they get no entry on any host and an
|
||||
# existing gstack-owned entry is removed, like a retired skill. Same list as
|
||||
# CREW_SKILLS in scripts/discover-skills.ts (test/crew-skills-on-capy.test.ts).
|
||||
GSTACK_CREW_SKILLS="captain"
|
||||
|
||||
# gstack_on_capy — true on a Capy machine: the test gstack-capy-install's bun
|
||||
# shim and gstack-update-channel.sh already use (CAPY_MACHINE_CONFIG set, or
|
||||
# ~/.capy/system present).
|
||||
gstack_on_capy() { [ -n "${CAPY_MACHINE_CONFIG:-}" ] || [ -d "${HOME:-/nonexistent}/.capy/system" ]; }
|
||||
|
||||
# GSTACK_UNREGISTERED_SKILLS — what setup and gstack-relink keep off every host
|
||||
# on this machine: the retired skills, plus the crew skills off Capy.
|
||||
GSTACK_UNREGISTERED_SKILLS="$GSTACK_RETIRED_SKILLS"
|
||||
gstack_on_capy || GSTACK_UNREGISTERED_SKILLS="$GSTACK_RETIRED_SKILLS $GSTACK_CREW_SKILLS"
|
||||
+4
-3
@@ -72,10 +72,11 @@ if [ -f "$SCRIPT_DIR/gstack-install-registry.sh" ]; then
|
||||
. "$SCRIPT_DIR/gstack-install-registry.sh"
|
||||
DISABLED="$(gstack_disabled_normalize "$("$GSTACK_CONFIG" get disabled_skills 2>/dev/null || true)")"
|
||||
fi
|
||||
# Retired skills (GSTACK_RETIRED_SKILLS) lose their entry the same way; only
|
||||
# the user's DISABLED list is reported and passed to the router.
|
||||
# Retired skills, and the crew skills off a Capy machine
|
||||
# (GSTACK_UNREGISTERED_SKILLS), lose their entry the same way; only the user's
|
||||
# DISABLED list is reported and passed to the router.
|
||||
OFF="$DISABLED"
|
||||
[ -z "${GSTACK_RETIRED_SKILLS:-}" ] || OFF="$(gstack_disabled_normalize "$GSTACK_RETIRED_SKILLS $DISABLED")"
|
||||
[ -z "${GSTACK_UNREGISTERED_SKILLS:-}" ] || OFF="$(gstack_disabled_normalize "$GSTACK_UNREGISTERED_SKILLS $DISABLED")"
|
||||
DISABLED_COUNT=0
|
||||
|
||||
# #2569: rendered :user variants (brain-aware blocks) live in an UNTRACKED
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **`/captain` is now installed only on Capy machines.** gstack's crew tools for Capy (the Captain skill, and later the merge queue, receipts and spend caps) are being gathered under `crew/`, which `./setup` and `gstack-relink` load only where `CAPY_MACHINE_CONFIG` is set or `~/.capy/system` exists, the test the Capy installer already used. Everywhere else `/captain` gets no entry on any host, and an entry an earlier install made is removed on the next setup or upgrade; a skill of your own named `captain` is left alone. Every other skill, the install size and the install time are unchanged. The README has a short "gstack on Capy" section, and crew/README.md says what Capy users get.
|
||||
@@ -0,0 +1,37 @@
|
||||
# gstack on Capy
|
||||
|
||||
This folder holds the parts of gstack that only Capy crews use: the tools that
|
||||
run a crew of coding agents under a Captain. It stays in the public repo, but
|
||||
`./setup` loads it only on a Capy machine. Anywhere else, setup skips it and
|
||||
gstack works exactly as it does without it.
|
||||
|
||||
## What Capy users get on top of gstack
|
||||
|
||||
- **Captains.** `/captain` runs many agent threads from one controller: lanes
|
||||
send a READY block and never merge, the controller keeps the digest, relays
|
||||
approvals in a fixed shape and runs a daily flaky-test pass
|
||||
([docs/captain.md](../docs/captain.md)).
|
||||
- **A merge queue.** `gstack-ship-queue` lands verified pull requests alone or
|
||||
in trains, checks the exact head before it merges, and stamps the version
|
||||
only at release time.
|
||||
- **Receipts.** `/ship` leaves a receipt for the head it checked, so the
|
||||
controller can tell that a pull request was reviewed and tested at the commit
|
||||
it merges.
|
||||
- **Budgets.** `/ship` and `/autoplan` meter their own spend against the cap a
|
||||
brief sets, and stop or hand off before they cross it
|
||||
([docs/ship-budget.md](../docs/ship-budget.md)).
|
||||
|
||||
How the whole factory fits together: [capy/README.md](../capy/README.md).
|
||||
|
||||
## How it is loaded
|
||||
|
||||
A Capy machine is one where `CAPY_MACHINE_CONFIG` is set or `~/.capy/system`
|
||||
exists, the same test the Capy installer (`bin/gstack-capy-install`) already
|
||||
uses. `gstack_on_capy` in `bin/gstack-install-registry.sh` is that test, and
|
||||
`GSTACK_CREW_SKILLS` there lists the crew skills. On a Capy machine setup and
|
||||
`gstack-relink` register them like any other skill; elsewhere they get no entry
|
||||
on any host, and an entry an earlier install made is removed.
|
||||
|
||||
The crew tooling moves here in stages. Code that no public skill calls comes
|
||||
first; the steps `/ship`, `/review` and `/autoplan` take only for crews move
|
||||
after they call it through one switch.
|
||||
+2
-2
@@ -45,7 +45,7 @@ Detailed guides for every gstack skill — philosophy, workflow, and examples.
|
||||
| [`/write-skill`](#write-skill) | **Skill Editor** | Write or improve an agent skill without bloat: edit ladder, size per level, skill-to-bin drift check, full diets with a before/after test. |
|
||||
| [`/test-audit`](#test-audit) | **Test Auditor** | Sweep existing tests for low-value, implementation-coupled or duplicate tests. Report-only unless you approve a batch. |
|
||||
| [`/landing-report`](#landing-report) | **Ship Queue Dashboard** | Read-only snapshot of the workspace-aware ship queue. Which version slots are claimed, which sibling workspaces have WIP. |
|
||||
| [`/captain`](#captain) | **Crew Controller** | Run many coding agents from one controller. Lanes build and send a READY block; the controller validates it, merges through trains (stamp on call, verify, merge with the expected head, verify again; `train land` per slot under `changelog_fragments`), keeps the digest, relays approvals in three fixed shapes and runs a daily flaky-fix pass. Also answers to `/crew`. |
|
||||
| [`/captain`](#captain) | **Crew Controller** (Capy only: [crew/](../crew/README.md)) | Run many coding agents from one controller. Lanes build and send a READY block; the controller validates it, merges through trains (stamp on call, verify, merge with the expected head, verify again; `train land` per slot under `changelog_fragments`), keeps the digest, relays approvals in three fixed shapes and runs a daily flaky-fix pass. Also answers to `/crew`. |
|
||||
| [`/benchmark-models`](#benchmark-models) | **Model Benchmark** | Side-by-side cross-model benchmark for skills (Claude vs GPT vs Gemini, plus particle-1 and DeepSeek V4.1 Flash through OpenRouter). Latency, tokens, cost, optional LLM-judged quality. |
|
||||
| [`/eval-plan`](#eval-plan) | **Eval Planner** | Preregister a paid eval (bars, arms, decoys, stop rules), dry-run it for $0 with the stub model, pilot three priced items, then approve the spend from the extrapolation table. |
|
||||
| | | |
|
||||
@@ -1458,7 +1458,7 @@ Remove the `/freeze` boundary, allowing edits everywhere again. The hooks stay r
|
||||
|
||||
One controller, many lanes. `/captain` (also `/crew`) teaches a coordinating agent the
|
||||
operating model that keeps dozens of worker threads merging into one base branch without
|
||||
turning it red.
|
||||
turning it red. `./setup` installs it only on a Capy machine; see [crew/README.md](../crew/README.md).
|
||||
|
||||
```text
|
||||
You: /captain
|
||||
|
||||
@@ -19,7 +19,7 @@ import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { RETIRED_SKILLS } from '../scripts/discover-skills';
|
||||
import { unregisteredSkills } from '../scripts/discover-skills';
|
||||
import { bunIsolationArgs } from './bun-isolation';
|
||||
import { runBashSync } from './script-invocation';
|
||||
import { frontmatterList, routerLine } from './skill-triggers';
|
||||
@@ -234,13 +234,15 @@ function skillDirs(root: string, triggersByName = new Map<string, string[]>()):
|
||||
|
||||
/** The skills a target's payload carries: the host render (made for this checkout as the install root) or the checkout's committed Claude render. */
|
||||
export function sourceSkills(target: Target, checkout: string, scratch: string): SkillEntry[] {
|
||||
if (target.render === 'claude') return skillDirs(checkout).filter(s => s.name !== 'gstack' && !RETIRED_SKILLS.includes(s.name));
|
||||
const off = unregisteredSkills();
|
||||
const registered = (s: SkillEntry) => !off.includes(s.name.replace(/^gstack-/, ''));
|
||||
if (target.render === 'claude') return skillDirs(checkout).filter(s => s.name !== 'gstack' && registered(s));
|
||||
const out = path.join(scratch, 'render');
|
||||
fs.rmSync(out, { recursive: true, force: true });
|
||||
const r = spawnSync(process.execPath, [...bunIsolationArgs(), 'scripts/gen-skill-docs.ts', '--host', target.name, '--install-root', checkout, '--out-dir', out],
|
||||
{ cwd: checkout, encoding: 'utf8', timeout: 300_000 });
|
||||
if (r.status !== 0) throw new HarnessError('HARNESS_RENDER_FAILED', 'could not render ' + target.name + ' skills: ' + ((r.stderr || r.stdout || '').trim().split('\n').slice(-3).join(' | ')));
|
||||
const skills = skillDirs(path.join(out, '.' + target.name, 'skills'), new Map(skillDirs(checkout).map(s => [s.name, s.triggers])));
|
||||
const skills = skillDirs(path.join(out, '.' + target.name, 'skills'), new Map(skillDirs(checkout).map(s => [s.name, s.triggers]))).filter(registered);
|
||||
if (!skills.length) throw new HarnessError('HARNESS_RENDER_FAILED', 'the ' + target.name + ' render produced no skills');
|
||||
return skills;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import type { HostConfig } from './host-config';
|
||||
|
||||
@@ -20,6 +21,23 @@ const SKIP = new Set(['node_modules', '.git', 'dist']);
|
||||
*/
|
||||
export const RETIRED_SKILLS = ['plan-eng-review', 'investigate'];
|
||||
|
||||
/**
|
||||
* The gstack-on-Capy skills (crew/README.md): registered only on a Capy
|
||||
* machine, and kept off every host elsewhere like a retired skill. Same list
|
||||
* as GSTACK_CREW_SKILLS in bin/gstack-install-registry.sh, and the same Capy
|
||||
* test as its gstack_on_capy.
|
||||
*/
|
||||
export const CREW_SKILLS = ['captain'];
|
||||
|
||||
export function onCapyMachine(env: NodeJS.ProcessEnv = process.env, home = os.homedir()): boolean {
|
||||
return Boolean(env.CAPY_MACHINE_CONFIG) || fs.existsSync(path.join(home, '.capy', 'system'));
|
||||
}
|
||||
|
||||
/** What no host registers on this machine: the retired skills, plus the crew skills off Capy. */
|
||||
export function unregisteredSkills(env: NodeJS.ProcessEnv = process.env, home = os.homedir()): string[] {
|
||||
return onCapyMachine(env, home) ? RETIRED_SKILLS : [...RETIRED_SKILLS, ...CREW_SKILLS];
|
||||
}
|
||||
|
||||
/** The generator and its coverage checks use the same include-minus-skip rule. */
|
||||
export function includesSkill(host: HostConfig, skillDir: string): boolean {
|
||||
const { includeSkills, skipSkills } = host.generation;
|
||||
|
||||
@@ -1440,9 +1440,10 @@ fi
|
||||
# membership inline with "${_DISABLED_SKILLS:- }", so a linker extracted on its
|
||||
# own (tests) behaves as "nothing disabled".
|
||||
_USER_DISABLED_SKILLS="$(gstack_disabled_normalize "$("$GSTACK_CONFIG" get disabled_skills 2>/dev/null || true)")"
|
||||
# Retired skills (GSTACK_RETIRED_SKILLS) take the same no-entry path on every
|
||||
# host; only the user's own list is reported and passed to the router.
|
||||
_DISABLED_SKILLS="$(gstack_disabled_normalize "$GSTACK_RETIRED_SKILLS $_USER_DISABLED_SKILLS")"
|
||||
# Retired skills, and the crew skills off a Capy machine
|
||||
# (GSTACK_UNREGISTERED_SKILLS), take the same no-entry path on every host; only
|
||||
# the user's own list is reported and passed to the router.
|
||||
_DISABLED_SKILLS="$(gstack_disabled_normalize "$GSTACK_UNREGISTERED_SKILLS $_USER_DISABLED_SKILLS")"
|
||||
# Every render of the router (committed-path renders of env-var hosts and the
|
||||
# per-install renders) omits disabled skills (C8); the default Claude router is
|
||||
# rendered in place by gstack-relink.
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* gstack on Capy (crew/README.md): the crew skills (/captain) are registered
|
||||
* only on a Capy machine, the same test gstack-capy-install's bun shim uses
|
||||
* (CAPY_MACHINE_CONFIG set, or ~/.capy/system present). Everywhere else setup
|
||||
* and gstack-relink treat them like a retired skill: no entry on any host, and
|
||||
* an existing gstack-owned entry is removed on the next setup or upgrade.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { bunIsolationArgs } from '../lib/bun-isolation';
|
||||
import { CREW_SKILLS, RETIRED_SKILLS, onCapyMachine, unregisteredSkills } from '../scripts/discover-skills';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const REGISTRY = path.join(ROOT, 'bin', 'gstack-install-registry.sh');
|
||||
const BINS = ['gstack-config', 'gstack-relink', 'gstack-patch-names', 'gstack-skill-name.sh', 'gstack-state-root.sh', 'gstack-install-registry.sh'];
|
||||
|
||||
let tmp: string;
|
||||
let home: string;
|
||||
beforeEach(() => {
|
||||
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-crew-skills-'));
|
||||
home = path.join(tmp, 'home');
|
||||
fs.mkdirSync(home);
|
||||
});
|
||||
afterEach(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
|
||||
const onCapy = () => fs.mkdirSync(path.join(home, '.capy', 'system'), { recursive: true });
|
||||
const registryVar = (name: string, env: Record<string, string> = {}) => {
|
||||
const r = spawnSync('bash', ['-c', `. "$1" && printf %s "$${name}"`, '_', REGISTRY], {
|
||||
encoding: 'utf8', timeout: 10_000, env: { ...process.env, HOME: home, CAPY_MACHINE_CONFIG: '', ...env },
|
||||
});
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
return r.stdout.split(/\s+/).filter(Boolean);
|
||||
};
|
||||
|
||||
test('setup, gstack-relink and the harness registrar name the same crew skills', () => {
|
||||
expect(registryVar('GSTACK_CREW_SKILLS')).toEqual(CREW_SKILLS);
|
||||
expect(CREW_SKILLS).toEqual(['captain']);
|
||||
});
|
||||
|
||||
describe('which skills stay unregistered', () => {
|
||||
test('off Capy: the retired skills and the crew skills', () => {
|
||||
expect(registryVar('GSTACK_UNREGISTERED_SKILLS')).toEqual([...RETIRED_SKILLS, ...CREW_SKILLS]);
|
||||
expect(onCapyMachine({}, home)).toBe(false);
|
||||
expect(unregisteredSkills({}, home)).toEqual([...RETIRED_SKILLS, ...CREW_SKILLS]);
|
||||
});
|
||||
|
||||
test('on a Capy machine (~/.capy/system): only the retired skills', () => {
|
||||
onCapy();
|
||||
expect(registryVar('GSTACK_UNREGISTERED_SKILLS')).toEqual(RETIRED_SKILLS);
|
||||
expect(onCapyMachine({}, home)).toBe(true);
|
||||
expect(unregisteredSkills({}, home)).toEqual(RETIRED_SKILLS);
|
||||
});
|
||||
|
||||
test('on a Capy machine (CAPY_MACHINE_CONFIG): only the retired skills', () => {
|
||||
expect(registryVar('GSTACK_UNREGISTERED_SKILLS', { CAPY_MACHINE_CONFIG: '/etc/capy/machine.json' })).toEqual(RETIRED_SKILLS);
|
||||
expect(unregisteredSkills({ CAPY_MACHINE_CONFIG: '/etc/capy/machine.json' }, home)).toEqual(RETIRED_SKILLS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-relink registers /captain only on Capy', () => {
|
||||
let install: string;
|
||||
let skills: string;
|
||||
const relink = () => spawnSync('bash', [path.join(install, 'bin', 'gstack-relink')], {
|
||||
encoding: 'utf8',
|
||||
timeout: 60_000,
|
||||
env: {
|
||||
...process.env, HOME: home, CAPY_MACHINE_CONFIG: '', GSTACK_HOME: path.join(tmp, 'state'), GSTACK_STATE_ROOT: '', GSTACK_STATE_DIR: '',
|
||||
GSTACK_INSTALL_DIR: install, GSTACK_SKILLS_DIR: skills, GSTACK_SETUP_RUNNING: '',
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
install = path.join(tmp, 'gstack');
|
||||
skills = path.join(tmp, 'skills');
|
||||
fs.mkdirSync(path.join(install, 'bin'), { recursive: true });
|
||||
for (const b of BINS) {
|
||||
fs.copyFileSync(path.join(ROOT, 'bin', b), path.join(install, 'bin', b));
|
||||
fs.chmodSync(path.join(install, 'bin', b), 0o755);
|
||||
}
|
||||
for (const skill of ['captain', 'ship']) {
|
||||
fs.mkdirSync(path.join(install, skill));
|
||||
fs.writeFileSync(path.join(install, skill, 'SKILL.md'), `---\nname: ${skill}\ndescription: test\n---\n# ${skill}\n`);
|
||||
}
|
||||
fs.mkdirSync(skills, { recursive: true });
|
||||
});
|
||||
|
||||
test('off Capy there is no captain entry, and the other skills link as before', () => {
|
||||
const r = relink();
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
expect(fs.lstatSync(path.join(skills, 'captain'), { throwIfNoEntry: false })).toBeUndefined();
|
||||
expect(fs.existsSync(path.join(skills, 'ship', 'SKILL.md'))).toBe(true);
|
||||
expect(r.stdout).not.toContain('Disabled (gstack-config disabled_skills');
|
||||
});
|
||||
|
||||
test('on Capy captain links like any skill', () => {
|
||||
onCapy();
|
||||
const r = relink();
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
expect(fs.existsSync(path.join(skills, 'captain', 'SKILL.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(skills, 'ship', 'SKILL.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('an install that leaves Capy loses the gstack-owned captain entry and keeps a user\'s own', () => {
|
||||
onCapy();
|
||||
expect(relink().status).toBe(0);
|
||||
expect(fs.existsSync(path.join(skills, 'captain', 'SKILL.md'))).toBe(true);
|
||||
fs.rmSync(path.join(home, '.capy'), { recursive: true });
|
||||
const r = relink();
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
expect(fs.lstatSync(path.join(skills, 'captain'), { throwIfNoEntry: false })).toBeUndefined();
|
||||
|
||||
fs.mkdirSync(path.join(skills, 'captain'));
|
||||
const own = '---\nname: captain\ndescription: mine\n---\nmy own captain\n';
|
||||
fs.writeFileSync(path.join(skills, 'captain', 'SKILL.md'), own);
|
||||
expect(relink().status).toBe(0);
|
||||
expect(fs.readFileSync(path.join(skills, 'captain', 'SKILL.md'), 'utf8')).toBe(own);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the harness registrar registers /captain only on Capy', () => {
|
||||
const lib = path.join(ROOT, 'lib', 'harness-register.ts');
|
||||
const sourceNames = (env: Record<string, string> = {}) => {
|
||||
const probe = `const { sourceSkills, TARGETS } = await import(${JSON.stringify(lib)}); for (const t of ['dots', 'hermes']) console.log(t + ':' + sourceSkills(TARGETS[t], ${JSON.stringify(ROOT)}, process.env.SCRATCH).map(s => s.name).join(' '));`;
|
||||
const r = spawnSync(process.execPath, [...bunIsolationArgs(), '-e', probe], {
|
||||
encoding: 'utf8', timeout: 120_000, env: { ...process.env, HOME: home, CAPY_MACHINE_CONFIG: '', SCRATCH: path.join(tmp, 'scratch'), ...env },
|
||||
});
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
return Object.fromEntries(r.stdout.trim().split('\n').map(l => [l.slice(0, l.indexOf(':')), l.slice(l.indexOf(':') + 1).split(' ')]));
|
||||
};
|
||||
|
||||
test('off Capy neither the Claude render nor a host render carries captain', () => {
|
||||
const { dots, hermes } = sourceNames();
|
||||
expect(dots).toContain('ship');
|
||||
expect(dots).not.toContain('captain');
|
||||
expect(hermes).toContain('gstack-ship');
|
||||
expect(hermes).not.toContain('gstack-captain');
|
||||
}, 120_000);
|
||||
|
||||
test('on Capy both renders carry captain', () => {
|
||||
const { dots, hermes } = sourceNames({ CAPY_MACHINE_CONFIG: '/etc/capy/machine.json' });
|
||||
expect(dots).toContain('captain');
|
||||
expect(hermes).toContain('gstack-captain');
|
||||
}, 120_000);
|
||||
});
|
||||
Reference in new issue
Block a user