From c271815130f0ad9e777101b0d3a0014c5fd03307 Mon Sep 17 00:00:00 2001 From: Sinabina Date: Mon, 20 Jul 2026 16:22:24 -0700 Subject: [PATCH 1/2] feat(gstack2): add capability readiness process --- README.md | 8 +- docs/gstack-2/CAPABILITY-READINESS.md | 75 ++++++++++++++ runtime/cli.js | 25 +++-- runtime/doctor.js | 97 +++++++++++++++++++ scripts/gstack2/generate-skill-tree.ts | 2 + skills/debug/references/RUNTIME.md | 2 + skills/design/references/RUNTIME.md | 2 + skills/plan/references/RUNTIME.md | 2 + skills/qa/references/RUNTIME.md | 2 + skills/review/references/RUNTIME.md | 2 + skills/ship/references/RUNTIME.md | 2 + test/gstack2-capability-readiness.test.ts | 113 ++++++++++++++++++++++ 12 files changed, 324 insertions(+), 8 deletions(-) create mode 100644 docs/gstack-2/CAPABILITY-READINESS.md create mode 100644 test/gstack2-capability-readiness.test.ts diff --git a/README.md b/README.md index ed00ef336f..dc368a644f 100644 --- a/README.md +++ b/README.md @@ -611,8 +611,12 @@ Data is stored in [Supabase](https://supabase.com) (open source Firebase alterna ## Troubleshooting -For GStack 2, first run `gstack doctor` (or `gstack doctor --json`) when the -optional runtime is installed. For skill discovery problems, use the standard +For GStack 2, run `gstack doctor --capability browser|design|diagram|pdf|ios` +(optionally `--json`) for a focused, non-mutating readiness result. It reports +pure-judgment availability, platform support, preview consent, install consent, +and runtime readiness separately; see +[`docs/gstack-2/CAPABILITY-READINESS.md`](docs/gstack-2/CAPABILITY-READINESS.md). +For skill discovery problems, use the standard installer's list and reinstall commands; do not manually copy host paths. The entries below apply to legacy 1.x installations. diff --git a/docs/gstack-2/CAPABILITY-READINESS.md b/docs/gstack-2/CAPABILITY-READINESS.md new file mode 100644 index 0000000000..c52f11fb88 --- /dev/null +++ b/docs/gstack-2/CAPABILITY-READINESS.md @@ -0,0 +1,75 @@ +# Capability readiness + +GStack's six public skills provide pure judgment without the optional runtime. +When a workflow needs executable evidence, inspect one of the five user-facing +capabilities without changing the machine: + +```bash +gstack doctor --capability browser +gstack doctor --capability design --json +gstack doctor --capability diagram +gstack doctor --capability pdf +gstack doctor --capability ios +``` + +The focused result deliberately keeps five questions separate: + +| Axis | Meaning | +|---|---| +| `judgment` | `available` even when no optional runtime exists. | +| `platform` | `supported` or `unsupported`; physical iOS is macOS-only. | +| `consent.preview` | Whether consent is required before an uncached public signed-manifest metadata request. Doctor never makes that request or grants consent. | +| `consent.install` | Separate consent required only after the complete preview. Doctor never installs or records consent. | +| `readiness` | Whether the selected local runtime capability is usable now. | + +Readiness has exactly five states: + +- `ready`: capability and managed-runtime checks passed. +- `degraded`: the capability passed, but the managed runtime reported a warning + such as recovery from an interrupted upgrade. +- `unavailable`: the capability is not selected, no runtime is active, or runtime + inspection could not establish availability. +- `unsupported`: the platform cannot provide the capability with GStack's + existing architecture. +- `failed`: an installed/selected capability failed its readiness evidence. + +The JSON form follows the runtime's result convention: `ok` is true only for +`ready` and `degraded`; a non-ready focused doctor exits 1. Usage errors still +exit 2 through the standard CLI error envelope. + +## Consent and setup flow + +```text +pure judgment (always available) + | + v +capability doctor (local, non-mutating) + | + +-- ready/degraded --> capability-dependent work + | + +-- unsupported ----> judgment-only work or supported platform + | + +-- unavailable/failed + | + v + ask for preview consent + | + v + packaged bootstrap preview (no install) + | + v + ask for install consent + | + v + packaged bootstrap install + | + v + capability doctor again +``` + +Preview consent is not install consent. Deferring either does not block pure +judgment. Follow the packaged bootstrap contract for exact dependency closure, +compressed bytes, signature checks, and atomic installation. Do not run setup +from a standards-installed skill directory, add a browser provider, replace the +local Chromium/Playwright backend, or replace the physical-iOS +DebugBridge/CoreDevice harness. diff --git a/runtime/cli.js b/runtime/cli.js index 620b8803a2..73cb30c73f 100644 --- a/runtime/cli.js +++ b/runtime/cli.js @@ -25,7 +25,13 @@ import { runExternalEffect, updateRunWorkflow, } from "./state.js"; -import { runDoctor, formatDoctor } from "./doctor.js"; +import { + CAPABILITY_READINESS_CAPABILITIES, + capabilityReadiness, + formatCapabilityReadiness, + runDoctor, + formatDoctor, +} from "./doctor.js"; import { cleanupRuntime } from "./cleanup.js"; import { ContextClient, @@ -152,11 +158,18 @@ async function initCommand({ args, home, cwd, stdout, legacyAlias = false }) { } async function doctorCommand({ args, home, cwd, stdout }) { - const parsed = parseFlags(args, new Set(["--json", "--skill-api"])); + const parsed = parseFlags(args, new Set(["--json", "--skill-api", "--capability"])); if (parsed.positionals.length) throw cliError("Doctor accepts only named options", "USAGE"); + const capability = parsed.values.get("--capability"); + if (capability && !CAPABILITY_READINESS_CAPABILITIES.includes(capability)) { + throw cliError(`Unknown capability: ${capability}. Choose ${CAPABILITY_READINESS_CAPABILITIES.join(", ")}.`, "USAGE"); + } const report = await runDoctor({ home, cwd, expectedSkillApi: parsed.values.get("--skill-api") }); - write(stdout, parsed.flags.has("--json") ? `${JSON.stringify(report, null, 2)}\n` : formatDoctor(report)); - return report.ok ? 0 : 1; + const result = capability ? capabilityReadiness(report, capability) : report; + write(stdout, parsed.flags.has("--json") + ? `${JSON.stringify(result, null, 2)}\n` + : capability ? formatCapabilityReadiness(result) : formatDoctor(result)); + return result.ok ? 0 : 1; } async function configCommand({ args, home, cwd, stdout }) { @@ -630,7 +643,7 @@ function parseFlags(args, allowed) { continue; } if (!allowed.has(arg)) throw cliError(`Unknown option: ${arg}`, "USAGE"); - if (["--source", "--version", "--url", "--older-than-hours", "--max-pages", "--max-depth", "--max-links", "--skill-api"].includes(arg)) { + if (["--source", "--version", "--url", "--older-than-hours", "--max-pages", "--max-depth", "--max-links", "--skill-api", "--capability"].includes(arg)) { const value = args[++index]; if (value == null || value.startsWith("--")) throw cliError(`${arg} requires a value`, "USAGE"); values.set(arg, value); @@ -707,7 +720,7 @@ function usage() { "Usage:\n" + " gstack init # initialize state only\n" + " gstack setup # compatibility alias for init\n" + - " gstack doctor [--skill-api ] [--json]\n" + + " gstack doctor [--capability browser|design|diagram|pdf|ios] [--skill-api ] [--json]\n" + " gstack paths [--json|--shell]\n" + " gstack runtime path \n" + " gstack config get [key]\n" + diff --git a/runtime/doctor.js b/runtime/doctor.js index 1f9601bada..81b9a4440b 100644 --- a/runtime/doctor.js +++ b/runtime/doctor.js @@ -17,6 +17,14 @@ import { managedBunRelativePath, } from "./install.js"; +export const CAPABILITY_READINESS_CAPABILITIES = Object.freeze([ + "browser", + "design", + "diagram", + "pdf", + "ios", +]); + export async function runDoctor(options = {}) { const paths = resolveRuntimePaths(options); const checks = []; @@ -183,6 +191,82 @@ export async function runDoctor(options = {}) { }; } +export function capabilityReadiness(report, capability, options = {}) { + if (!CAPABILITY_READINESS_CAPABILITIES.includes(capability)) { + throw new TypeError(`Unknown capability: ${capability}`); + } + const platform = options.platform ?? process.platform; + const checkedAt = report.checkedAt; + const judgment = { + status: "available", + message: "Pure-judgment skill guidance is available without the optional runtime.", + }; + if (capability === "ios" && platform !== "darwin") { + return { + ok: false, + capability, + checkedAt, + judgment, + platform: { status: "unsupported", platform, message: "Physical iOS requires macOS and the existing CoreDevice harness." }, + consent: { + preview: { status: "not-applicable", granted: false }, + install: { status: "not-applicable", granted: false }, + }, + readiness: { status: "unsupported", message: "The runtime capability is unsupported on this platform." }, + nextAction: "Continue with pure judgment or move the physical-iOS workflow to macOS.", + }; + } + + const capabilityCheck = report.checks.find((check) => check.id === `capability:${capability}`); + const runtimeCheck = report.checks.find((check) => check.id === "managed-runtime"); + let status; + if (capabilityCheck?.status === "pass" && runtimeCheck?.status === "pass") status = "ready"; + else if (capabilityCheck?.status === "pass") status = "degraded"; + else if (capabilityCheck?.status === "fail") status = "failed"; + else status = "unavailable"; + + const needsSetup = status === "unavailable" || status === "failed"; + const messages = { + ready: "The selected capability passed its runtime readiness checks.", + degraded: "The capability check passed, but the managed runtime reported a degraded condition.", + unavailable: "The optional runtime capability is not installed or cannot currently be inspected.", + failed: "The selected capability is installed but failed readiness checks.", + }; + return { + ok: status === "ready" || status === "degraded", + capability, + checkedAt, + judgment, + platform: { status: "supported", platform }, + consent: { + preview: { + status: needsSetup ? "required" : "not-required", + granted: false, + message: needsSetup + ? "Consent is required before an uncached signed-manifest metadata preview; this command does not grant it." + : "No setup preview is needed for the current readiness state.", + }, + install: { + status: needsSetup ? "required-after-preview" : "not-required", + granted: false, + message: needsSetup + ? "Install consent is separate and may be requested only after the complete preview; this command does not grant it." + : "No install is needed for the current readiness state.", + }, + }, + readiness: { + status, + message: messages[status], + evidence: [runtimeCheck, capabilityCheck].filter(Boolean), + }, + nextAction: needsSetup + ? `Ask for preview consent, then run the packaged bootstrap preview for ${capability}; continue judgment-only work if setup is deferred.` + : status === "degraded" + ? "Review the managed-runtime warning before capability-dependent evidence work." + : "Proceed with capability-dependent work.", + }; +} + async function inspectRuntimeBun(activeRoot, manifest) { const relative = managedBunRelativePath(); const declared = manifest?.tools?.bun; @@ -328,3 +412,16 @@ export function formatDoctor(report) { for (const check of report.checks) lines.push(`${symbol[check.status]} ${check.id}: ${check.message}`); return `${lines.join("\n")}\n`; } + +export function formatCapabilityReadiness(report) { + const lines = [ + `gstack capability readiness: ${report.capability}`, + `judgment: ${report.judgment.status}`, + `platform: ${report.platform.status}`, + `preview consent: ${report.consent.preview.status}`, + `install consent: ${report.consent.install.status}`, + `readiness: ${report.readiness.status}`, + `next: ${report.nextAction}`, + ]; + return `${lines.join("\n")}\n`; +} diff --git a/scripts/gstack2/generate-skill-tree.ts b/scripts/gstack2/generate-skill-tree.ts index 90159925e1..42991c4de3 100644 --- a/scripts/gstack2/generate-skill-tree.ts +++ b/scripts/gstack2/generate-skill-tree.ts @@ -512,6 +512,8 @@ Some retained helpers are shell scripts. \`gstack doctor\` verifies Bash and, on The package/runtime compatibility tuple is \`schemaVersion=1\`, \`runtimeVersion=2.0.0\`, and \`skillApi=2.0\`; the machine-readable copy is \`references/support/runtime-contract.json\`. An incompatible active runtime is unavailable, not permission to upgrade it. +Use \`gstack doctor --capability browser|design|diagram|pdf|ios\` (optionally \`--json\`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly \`ready\`, \`degraded\`, \`unavailable\`, \`unsupported\`, or \`failed\`. Doctor never grants or persists consent, previews metadata, or installs anything. \`unavailable\` means setup may be offered; \`failed\` means selected runtime evidence failed; \`unsupported\` is a platform boundary; and \`degraded\` means the capability passed while the managed runtime has a warning. + The developer-only fallback is \`node references/support/runtime-bootstrap.mjs install --source --capability --yes\`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. `; } diff --git a/skills/debug/references/RUNTIME.md b/skills/debug/references/RUNTIME.md index 6ce746ba03..d6983af9d4 100644 --- a/skills/debug/references/RUNTIME.md +++ b/skills/debug/references/RUNTIME.md @@ -21,4 +21,6 @@ Some retained helpers are shell scripts. `gstack doctor` verifies Bash and, on W The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2.0.0`, and `skillApi=2.0`; the machine-readable copy is `references/support/runtime-contract.json`. An incompatible active runtime is unavailable, not permission to upgrade it. +Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning. + The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source --capability --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. diff --git a/skills/design/references/RUNTIME.md b/skills/design/references/RUNTIME.md index 6ce746ba03..d6983af9d4 100644 --- a/skills/design/references/RUNTIME.md +++ b/skills/design/references/RUNTIME.md @@ -21,4 +21,6 @@ Some retained helpers are shell scripts. `gstack doctor` verifies Bash and, on W The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2.0.0`, and `skillApi=2.0`; the machine-readable copy is `references/support/runtime-contract.json`. An incompatible active runtime is unavailable, not permission to upgrade it. +Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning. + The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source --capability --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. diff --git a/skills/plan/references/RUNTIME.md b/skills/plan/references/RUNTIME.md index 6ce746ba03..d6983af9d4 100644 --- a/skills/plan/references/RUNTIME.md +++ b/skills/plan/references/RUNTIME.md @@ -21,4 +21,6 @@ Some retained helpers are shell scripts. `gstack doctor` verifies Bash and, on W The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2.0.0`, and `skillApi=2.0`; the machine-readable copy is `references/support/runtime-contract.json`. An incompatible active runtime is unavailable, not permission to upgrade it. +Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning. + The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source --capability --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. diff --git a/skills/qa/references/RUNTIME.md b/skills/qa/references/RUNTIME.md index 6ce746ba03..d6983af9d4 100644 --- a/skills/qa/references/RUNTIME.md +++ b/skills/qa/references/RUNTIME.md @@ -21,4 +21,6 @@ Some retained helpers are shell scripts. `gstack doctor` verifies Bash and, on W The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2.0.0`, and `skillApi=2.0`; the machine-readable copy is `references/support/runtime-contract.json`. An incompatible active runtime is unavailable, not permission to upgrade it. +Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning. + The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source --capability --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. diff --git a/skills/review/references/RUNTIME.md b/skills/review/references/RUNTIME.md index 6ce746ba03..d6983af9d4 100644 --- a/skills/review/references/RUNTIME.md +++ b/skills/review/references/RUNTIME.md @@ -21,4 +21,6 @@ Some retained helpers are shell scripts. `gstack doctor` verifies Bash and, on W The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2.0.0`, and `skillApi=2.0`; the machine-readable copy is `references/support/runtime-contract.json`. An incompatible active runtime is unavailable, not permission to upgrade it. +Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning. + The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source --capability --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. diff --git a/skills/ship/references/RUNTIME.md b/skills/ship/references/RUNTIME.md index 6ce746ba03..d6983af9d4 100644 --- a/skills/ship/references/RUNTIME.md +++ b/skills/ship/references/RUNTIME.md @@ -21,4 +21,6 @@ Some retained helpers are shell scripts. `gstack doctor` verifies Bash and, on W The package/runtime compatibility tuple is `schemaVersion=1`, `runtimeVersion=2.0.0`, and `skillApi=2.0`; the machine-readable copy is `references/support/runtime-contract.json`. An incompatible active runtime is unavailable, not permission to upgrade it. +Use `gstack doctor --capability browser|design|diagram|pdf|ios` (optionally `--json`) for a non-mutating, capability-specific readiness result. Its independent axes must remain distinct: pure judgment availability, platform support, preview consent, install consent, and runtime readiness. Readiness is exactly `ready`, `degraded`, `unavailable`, `unsupported`, or `failed`. Doctor never grants or persists consent, previews metadata, or installs anything. `unavailable` means setup may be offered; `failed` means selected runtime evidence failed; `unsupported` is a platform boundary; and `degraded` means the capability passed while the managed runtime has a warning. + The developer-only fallback is `node references/support/runtime-bootstrap.mjs install --source --capability --yes`; show its trust warning and use it only when the user explicitly selects a checkout they reviewed. If the packaged bootstrap is unavailable, stop capability setup instead of guessing a checkout-relative command. Deferring installation records no consent and must not block pure judgment. diff --git a/test/gstack2-capability-readiness.test.ts b/test/gstack2-capability-readiness.test.ts new file mode 100644 index 0000000000..9691adfc6b --- /dev/null +++ b/test/gstack2-capability-readiness.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, test } from "bun:test"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { main } from "../runtime/cli.js"; +import { capabilityReadiness, formatCapabilityReadiness } from "../runtime/doctor.js"; +import { setupRuntime } from "../runtime/setup.js"; + +function report(checks: Array>) { + return { + ok: !checks.some((check) => check.status === "fail"), + home: "/fixture/.gstack", + checkedAt: "2026-07-20T00:00:00.000Z", + checks, + }; +} + +function capture() { + let value = ""; + return { stream: { write: (chunk: string) => { value += chunk; } }, value: () => value }; +} + +describe("capability readiness", () => { + test("keeps pure judgment, preview consent, and install consent separate when unavailable", () => { + const result = capabilityReadiness(report([ + { id: "managed-runtime", status: "fail", message: "No active managed runtime" }, + { id: "capability:pdf", status: "warn", message: "not installed" }, + ]), "pdf"); + + expect(result).toMatchObject({ + ok: false, + judgment: { status: "available" }, + platform: { status: "supported" }, + consent: { + preview: { status: "required", granted: false }, + install: { status: "required-after-preview", granted: false }, + }, + readiness: { status: "unavailable" }, + }); + }); + + test("distinguishes ready, degraded, and failed runtime states", () => { + const ready = capabilityReadiness(report([ + { id: "managed-runtime", status: "pass", message: "active" }, + { id: "capability:design", status: "pass", message: "runnable" }, + ]), "design"); + const degraded = capabilityReadiness(report([ + { id: "managed-runtime", status: "warn", message: "recovered" }, + { id: "capability:design", status: "pass", message: "runnable" }, + ]), "design"); + const failed = capabilityReadiness(report([ + { id: "managed-runtime", status: "pass", message: "active" }, + { id: "capability:diagram", status: "fail", message: "launcher metadata missing" }, + ]), "diagram"); + + expect(ready).toMatchObject({ ok: true, readiness: { status: "ready" } }); + expect(degraded).toMatchObject({ ok: true, readiness: { status: "degraded" } }); + expect(failed).toMatchObject({ ok: false, readiness: { status: "failed" } }); + expect(failed.consent.install.status).toBe("required-after-preview"); + }); + + test("reports physical iOS as unsupported without turning off pure judgment", () => { + const result = capabilityReadiness(report([]), "ios", { platform: "linux" }); + expect(result).toMatchObject({ + ok: false, + judgment: { status: "available" }, + platform: { status: "unsupported", platform: "linux" }, + consent: { + preview: { status: "not-applicable" }, + install: { status: "not-applicable" }, + }, + readiness: { status: "unsupported" }, + }); + }); + + test("human output exposes every state axis", () => { + const result = capabilityReadiness(report([ + { id: "managed-runtime", status: "pass", message: "active" }, + { id: "capability:browser", status: "pass", message: "launched" }, + ]), "browser"); + expect(formatCapabilityReadiness(result)).toContain("judgment: available"); + expect(formatCapabilityReadiness(result)).toContain("preview consent: not-required"); + expect(formatCapabilityReadiness(result)).toContain("install consent: not-required"); + expect(formatCapabilityReadiness(result)).toContain("readiness: ready"); + }); + + test("doctor capability JSON is non-mutating and uses the unified result envelope", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-capability-doctor-")); + const home = path.join(root, "home"); + const stdout = capture(); + const stderr = capture(); + try { + await setupRuntime({ home, cwd: root }); + const exit = await main(["doctor", "--capability", "pdf", "--json"], { + cwd: root, + env: { ...process.env, GSTACK_HOME: home }, + stdout: stdout.stream, + stderr: stderr.stream, + }); + const result = JSON.parse(stdout.value()); + expect(exit).toBe(1); + expect(stderr.value()).toBe(""); + expect(result).toMatchObject({ + ok: false, + capability: "pdf", + judgment: { status: "available" }, + readiness: { status: "unavailable" }, + }); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); +}); From e6f0018b7393a9439107308389ec4607807292c6 Mon Sep 17 00:00:00 2001 From: Sinabina Date: Tue, 21 Jul 2026 13:59:29 -0700 Subject: [PATCH 2/2] fix(gstack2): treat hard runtime failure as not-ready in capability readiness A capability whose binary launches while the managed runtime hard-fails (e.g. skill-API mismatch) was reported as `degraded`/ok:true/exit 0, diverging from plain `gstack doctor` (ok:false/exit 1) for the identical report. Split the branch so runtime `warn` stays `degraded` and runtime `fail` maps to `failed`, and add a regression test for the runtime-fail + capability-pass case. Co-Authored-By: Claude Opus 4.8 (1M context) --- runtime/doctor.js | 3 ++- test/gstack2-capability-readiness.test.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/runtime/doctor.js b/runtime/doctor.js index 70fcff94b2..21ab58c985 100644 --- a/runtime/doctor.js +++ b/runtime/doctor.js @@ -247,7 +247,8 @@ export function capabilityReadiness(report, capability, options = {}) { const runtimeCheck = report.checks.find((check) => check.id === "managed-runtime"); let status; if (capabilityCheck?.status === "pass" && runtimeCheck?.status === "pass") status = "ready"; - else if (capabilityCheck?.status === "pass") status = "degraded"; + else if (capabilityCheck?.status === "pass" && runtimeCheck?.status === "warn") status = "degraded"; + else if (capabilityCheck?.status === "pass") status = "failed"; else if (capabilityCheck?.status === "fail") status = "failed"; else status = "unavailable"; diff --git a/test/gstack2-capability-readiness.test.ts b/test/gstack2-capability-readiness.test.ts index 9691adfc6b..d6787e2464 100644 --- a/test/gstack2-capability-readiness.test.ts +++ b/test/gstack2-capability-readiness.test.ts @@ -52,11 +52,18 @@ describe("capability readiness", () => { { id: "managed-runtime", status: "pass", message: "active" }, { id: "capability:diagram", status: "fail", message: "launcher metadata missing" }, ]), "diagram"); + // A hard runtime failure under a launchable capability is not a warning: + // it must not report ok:true, matching plain `gstack doctor`'s exit code. + const runtimeFailed = capabilityReadiness(report([ + { id: "managed-runtime", status: "fail", message: "incompatible skill API" }, + { id: "capability:browser", status: "pass", message: "launched" }, + ]), "browser"); expect(ready).toMatchObject({ ok: true, readiness: { status: "ready" } }); expect(degraded).toMatchObject({ ok: true, readiness: { status: "degraded" } }); expect(failed).toMatchObject({ ok: false, readiness: { status: "failed" } }); expect(failed.consent.install.status).toBe("required-after-preview"); + expect(runtimeFailed).toMatchObject({ ok: false, readiness: { status: "failed" } }); }); test("reports physical iOS as unsupported without turning off pure judgment", () => {