From 4aebb1d6356bb4d73d782ac432c2f0621f29f041 Mon Sep 17 00:00:00 2001 From: garrytan Date: Wed, 28 Oct 2026 01:08:12 +0000 Subject: [PATCH] wip: harness hosts experimental, troubleshooting codes, kit Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- docs/troubleshooting.md | 56 +++++++++++++++++++++++++++++++++++ scripts/host-config.ts | 10 ------- test/agents-digest.test.ts | 26 ++-------------- test/host-conformance.test.ts | 3 +- 4 files changed, 61 insertions(+), 34 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 7ff082041..33b274c06 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -5053,3 +5053,59 @@ a registered, unlocked worktree whose directory is already gone (`git worktree prune`); a locked one, or a `broken` row whose `.git` link is missing, reads `review` instead. `clean,ignored:` means the tree is clean but holds gitignored entries (`.env`, build output) that go with the worktree. + + +## Agent harnesses (gstack-harness) + +`bin/gstack-harness` registers gstack's skills with the agent harness gstack is installed into (OpenClaw, Hermes, a Dot, QM, any other) and records every file it writes in `$GSTACK_STATE_ROOT/harness/.ledger`. Each error line ends with its code and a link here. Per-harness install, verify and uninstall steps: [docs/hosts.md](hosts.md#agent-harnesses). + + +### `gstack-harness: unknown harness ""` (HARNESS_UNKNOWN_TARGET) + +The name is not one of openclaw, hermes, dots, generic, qm, muse or grok. For a harness gstack does not know, use `generic` with `--skills-dir` or `--context`, or `--print` to get the router and block to paste in. + + +### `gstack-harness: has no default skills folder` (HARNESS_NO_DESTINATION) + +`generic` has no default location, and gstack never guesses at your context files. Pass `--skills-dir ` (an Agent Skills folder the harness reads) or `--context ` (a file it loads every session), or `--print` to write nothing. + + +### `gstack-harness: Hermes does not support --skills router` (HARNESS_MODE_UNSUPPORTED) + +Hermes opens only skills it has indexed, so a router that points at unlisted files would not load. Drop `--skills` to take the harness's default (Hermes: all; OpenClaw, dots and generic: router). + + +### `gstack-harness: refusing to write into : ` (HARNESS_UNSAFE_DESTINATION) + +The destination is your home folder, the filesystem root, inside the gstack checkout, a symlink, or a path with a tab or newline. Nothing was written. Pick the harness's own skills folder, for example `--skills-dir ~/.openclaw/skills`. + + +### `gstack-harness: already exists and gstack did not write it` / `a skill named already exists at ` (HARNESS_UNOWNED_ENTRY) + +gstack only replaces files its ledger says it wrote. A folder or a skill with the same name is yours (or another tool's), and harnesses resolve skills by name, so gstack stops instead of shadowing it. Rename or move that entry, then register again. + + +### `gstack-harness: could not render skills: ...` (HARNESS_RENDER_FAILED) + +The harness's skills are rendered from this checkout with `scripts/gen-skill-docs.ts`. Run `bun install` in the checkout and register again; the last lines of the generator's output are in the message. + + +### `gstack-harness: router description is bytes` / `context block is bytes` (HARNESS_BUDGET_EXCEEDED) + +The router's description (at most 300 bytes) and the context block (at most 600 bytes) go into every session, so gstack refuses to grow them. This is a gstack bug: report it with the byte count. + + +### `gstack-harness: no registration named ""` (HARNESS_NOT_REGISTERED) + +There is no ledger for that harness or id under `$GSTACK_STATE_ROOT/harness`. `gstack-harness status` lists every registration on this machine with its id. + + +### `gstack-harness: no QM admin credential in the environment` / `... needs --yes` (HARNESS_QM_CREDENTIALS) + +Registering gstack in QM publishes an org skill pack, so it needs an org admin's credential and an explicit `--yes`. Set `GSTACK_QM_URL` and `GSTACK_QM_ADMIN_TOKEN` (or `GSTACK_QM_SIGNING_SECRET` with `GSTACK_QM_ADMIN_ACTOR`) in the environment and run `gstack-harness register qm --yes`. gstack never prompts for, prints or stores the credential; the message shows the exact request instead. + + +### `gstack-harness: /v1/admin/skill-packs... returned HTTP ` / `... failed: timed out after 60 s` (HARNESS_QM_API) + +QM's admin API refused or did not answer. gstack lists packs before it creates one and retries once, so rerunning never makes a second pack. Check the URL is QM core and the credential is an org admin; `gstack-harness status` shows whether a pack id was recorded. + diff --git a/scripts/host-config.ts b/scripts/host-config.ts index 382982fdc..6336231ed 100644 --- a/scripts/host-config.ts +++ b/scripts/host-config.ts @@ -124,13 +124,6 @@ export interface HostConfig { install: { /** How skills are linked into the host dir. */ linkingStrategy: 'real-dir-symlink' | 'symlink-generated'; - /** - * Instruction-only fallback tier: a committed rules digest this host's - * users can hand-copy when there is no full install arm. Delivery is - * print-path + user-performed copy ONLY — setup must never write or - * overwrite a user's own AGENTS.md. See scripts/gen-agents-digest.ts. - */ - instructionTier?: { rulesFile: string }; /** * 'harness': setup's arm for this host is `gstack-harness register ` * (lib/harness-register.ts): the host's render copied into the harness's own @@ -198,9 +191,6 @@ export function validateHostConfig(config: HostConfig, validResolverNames?: Read if (config.install.registration === 'harness' && config.tier !== 'experimental') { errors.push('install.registration harness needs tier experimental (setup registers it through gstack-harness)'); } - if (config.tier === 'instruction-only' && !config.install.instructionTier && config.name !== 'slate' && config.name !== 'gbrain') { - errors.push('instruction-only hosts must declare install.instructionTier (what setup tells the user to copy)'); - } const modelError = validateModel(config.defaultModel); if (modelError) { errors.push(`defaultModel ${modelError}`); diff --git a/test/agents-digest.test.ts b/test/agents-digest.test.ts index 6eed47262..4cfe545bb 100644 --- a/test/agents-digest.test.ts +++ b/test/agents-digest.test.ts @@ -55,16 +55,9 @@ describe('agents-digest', () => { test('setup never copies the digest onto a user AGENTS.md (print-path only)', () => { const setup = fs.readFileSync(path.join(ROOT, 'setup'), 'utf-8'); - // The explainer arms print the digest path, anchored to the script's own - // directory — a $(pwd)-relative path prints a nonexistent file whenever - // setup is invoked from anywhere but the repo root. - const explainer = setup.match(/^print_instruction_tier\(\) \{([\s\S]*?)^\}/m)?.[1]; - expect(explainer).toBeDefined(); - // Informational hosts resolve their own anchor before install preflight. - // Bind the printed path to that assignment, without fixing its local name. - const anchor = explainer!.match(/^\s*([A-Za-z_]\w*)="\$\(cd "\$\(dirname "\$0"\)" && pwd -P\)"$/m)?.[1]; - expect(anchor).toBeDefined(); - expect(explainer).toContain(`$${anchor}/${DIGEST_RELPATH}`); + // OpenClaw and Hermes register skills through gstack-harness now (GSTA-135), + // so no setup arm prints the digest; a $(pwd)-relative digest path would + // name a nonexistent file whenever setup runs from outside the repo root. expect(setup).not.toMatch(/\$\(pwd\)\/agents-digest/); // …and no line may write to an AGENTS.md destination. Covers direct // write verbs (cp/ln/mv/tee/install/rsync/dd/truncate), > and >> @@ -101,17 +94,4 @@ describe('agents-digest', () => { expect(resolver).toContain(shared); } }); - - test('instruction-tier hosts declare the digest in host config — and the declaration tracks the real path', () => { - // This is what makes instructionTier.rulesFile load-bearing instead of - // config nobody reads: the declared string must equal the generator's - // DIGEST_RELPATH, and setup's explainer must print that same path. If - // the digest ever moves, all three surfaces fail together. - const setup = fs.readFileSync(path.join(ROOT, 'setup'), 'utf-8'); - expect(setup).toContain(DIGEST_RELPATH); - for (const host of ['openclaw', 'hermes']) { - const src = fs.readFileSync(path.join(ROOT, 'hosts', `${host}.ts`), 'utf-8'); - expect(src).toContain(`instructionTier: { rulesFile: '${DIGEST_RELPATH}' }`); - } - }); }); diff --git a/test/host-conformance.test.ts b/test/host-conformance.test.ts index 469a92272..ca136f179 100644 --- a/test/host-conformance.test.ts +++ b/test/host-conformance.test.ts @@ -35,7 +35,8 @@ const DISCOVERY: Record = { /** Every directory any host (or a shared agents layout) reads; .gemini/skills is Gemini CLI's. */ const ALL_DISCOVERY_DIRS = [...Object.values(DISCOVERY), '.agents/skills', '.copilot/skills', '.hermes/skills', '.gemini/skills']; -const installable = ALL_HOST_CONFIGS.filter(c => c.tier !== 'instruction-only').map(c => c.name); +/** Harness hosts (install.registration 'harness') register through gstack-harness: test/harness-register.test.ts certifies them. */ +const installable = ALL_HOST_CONFIGS.filter(c => c.tier !== 'instruction-only' && c.install.registration !== 'harness').map(c => c.name); const instructionOnly = ALL_HOST_CONFIGS.filter(c => c.tier === 'instruction-only').map(c => c.name); function skillNames(dir: string): string[] {