Merge codex/gstack-2 into gstack2-runtime-integration

Reconcile the four integrated v2 runtime implementations (unified execution
result contract, execution profiles, capability readiness, GitHub security)
with main's browser-provider hardening.

Conflict resolutions:
- runtimeContract() generator: keep new execution-result + doctor-capability
  paragraphs, adopt main's `[matching browser flags]` fallback wording;
  regenerate the six RUNTIME.md.
- package.json: keep the strict isolated test:gstack2 runner and marked 18.0.6
  security bump; adopt main's playwright-core alias.
- bun.lock: regenerated via bun install.
- release-hardening.test.ts: adopt main's browser-provider assertions
  (resolveServerLaunchTarget, --browser managed smoke loop).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sinabina
2026-07-21 13:15:03 -07:00
co-authored by Claude Opus 4.8
160 changed files with 4397 additions and 522 deletions
+4 -4
View File
@@ -70,11 +70,11 @@ RUN curl --retry 5 --retry-delay 5 --retry-connrefused -fsSL https://bun.sh/inst
RUN npm i -g @anthropic-ai/claude-code RUN npm i -g @anthropic-ai/claude-code
# Playwright system deps (Chromium) — needed for browse E2E tests # Playwright system deps (Chromium) — needed for browse E2E tests
RUN npx playwright install-deps chromium RUN npx playwright-core install-deps chromium
# Linux has neither Helvetica nor Arial. make-pdf's print CSS stacks fall back # Linux has neither Helvetica nor Arial. make-pdf's print CSS stacks fall back
# to Liberation Sans (metric-compatible Arial clone, SIL OFL 1.1) so PDFs don't # to Liberation Sans (metric-compatible Arial clone, SIL OFL 1.1) so PDFs don't
# render in DejaVu Sans. playwright install-deps happens to pull this in today, # render in DejaVu Sans. playwright-core install-deps happens to pull this in today,
# but the dep is implicit and could change — install explicitly so upgrades # but the dep is implicit and could change — install explicitly so upgrades
# can't silently regress rendering. # can't silently regress rendering.
# #
@@ -100,12 +100,12 @@ RUN bun install --frozen-lockfile && rm -rf /tmp/*
# Install Playwright Chromium to a shared location accessible by all users # Install Playwright Chromium to a shared location accessible by all users
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
RUN npx playwright install chromium \ RUN npx playwright-core install chromium \
&& chmod -R a+rX /opt/playwright-browsers && chmod -R a+rX /opt/playwright-browsers
# Verify everything works # Verify everything works
RUN bun --version && node --version && claude --version && jq --version && gh --version \ RUN bun --version && node --version && claude --version && jq --version && gh --version \
&& npx playwright --version \ && npx playwright-core --version \
&& fc-match "Liberation Sans" | grep -qi "Liberation" \ && fc-match "Liberation Sans" | grep -qi "Liberation" \
|| (echo "ERROR: fonts-liberation not installed — make-pdf PDFs will render in DejaVu Sans" && exit 1) || (echo "ERROR: fonts-liberation not installed — make-pdf PDFs will render in DejaVu Sans" && exit 1)
@@ -2,11 +2,19 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
const [directory, repository = process.env.GITHUB_REPOSITORY, version = "2.0.0"] = process.argv.slice(2); const [
directory,
repository = process.env.GITHUB_REPOSITORY,
version = "2.0.0",
releaseTag = `v${version}`,
] = process.argv.slice(2);
if (!directory || !repository) { if (!directory || !repository) {
console.error("Usage: create-runtime-release-manifest.mjs <artifact-dir> <owner/repo> [version]"); console.error("Usage: create-runtime-release-manifest.mjs <artifact-dir> <owner/repo> [version] [release-tag]");
process.exit(2); process.exit(2);
} }
if (!/^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/.test(releaseTag)) {
throw new Error(`Invalid runtime release tag: ${releaseTag}`);
}
const targets = [ const targets = [
"darwin-arm64", "darwin-arm64",
@@ -35,8 +43,8 @@ const capabilityComponents = {
ios: ["ios"], ios: ["ios"],
}; };
const commonComponents = ["core", "browser-code", "browser-headless", "browser-visible", "design", "diagram", "pdf"]; const commonComponents = ["core", "browser-code", "browser-headless", "browser-visible", "design", "diagram", "pdf"];
const release = `https://github.com/${repository}/releases/download/v${version}`; const release = `https://github.com/${repository}/releases/download/${releaseTag}`;
const certificateIdentity = `https://github.com/${repository}/.github/workflows/release-artifacts.yml@refs/tags/v${version}`; const certificateIdentity = `https://github.com/${repository}/.github/workflows/release-artifacts.yml@refs/tags/${releaseTag}`;
const targetRecords = {}; const targetRecords = {};
for (const target of targets) { for (const target of targets) {
+1 -1
View File
@@ -66,7 +66,7 @@ jobs:
fc-match -f '%{family[0]}\t%{color}\n' ':lang=und-zsye:charset=1F600' || true fc-match -f '%{family[0]}\t%{color}\n' ':lang=und-zsye:charset=1F600' || true
- name: Install Playwright Chromium - name: Install Playwright Chromium
run: bunx playwright install chromium run: bunx playwright-core install chromium
- name: Build binaries - name: Build binaries
run: bun run build run: bun run build
+24 -17
View File
@@ -2,7 +2,9 @@ name: Release runtime artifacts
on: on:
push: push:
tags: [v2.0.0] tags:
- v2.0.0
- v2.0.0-rc.*
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@@ -51,8 +53,6 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: 22.23.1 node-version: 22.23.1
- uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
- name: Install frozen dependencies - name: Install frozen dependencies
run: bun install --frozen-lockfile --ignore-scripts run: bun install --frozen-lockfile --ignore-scripts
shell: bash shell: bash
@@ -70,6 +70,7 @@ jobs:
--version 2.0.0 \ --version 2.0.0 \
--install-now \ --install-now \
--yes \ --yes \
--browser managed \
--capabilities "$CAPABILITIES" --capabilities "$CAPABILITIES"
active_slot=$(node -e 'const fs=require("fs"),p=process.argv[1];const v=JSON.parse(fs.readFileSync(p,"utf8")).current;if(typeof v!=="string"||!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(v))process.exit(1);process.stdout.write(v)' "$GSTACK_HOME/versions/current.json") active_slot=$(node -e 'const fs=require("fs"),p=process.argv[1];const v=JSON.parse(fs.readFileSync(p,"utf8")).current;if(typeof v!=="string"||!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(v))process.exit(1);process.stdout.write(v)' "$GSTACK_HOME/versions/current.json")
active="$GSTACK_HOME/versions/$active_slot" active="$GSTACK_HOME/versions/$active_slot"
@@ -112,32 +113,28 @@ jobs:
"$GSTACK_HOME/bin/browse" stop >/dev/null 2>&1 || true "$GSTACK_HOME/bin/browse" stop >/dev/null 2>&1 || true
} }
trap browser_cleanup EXIT trap browser_cleanup EXIT
smoke_url=$(node -e 'const fs=require("fs"),p=require("path").join(process.env.GITHUB_WORKSPACE,".gstack-runtime-smoke.html");fs.writeFileSync(p,"<!doctype html><title>GStack runtime smoke</title>\n");process.stdout.write(require("url").pathToFileURL(p).href)')
PATH="$clean_path" GSTACK_NODE="$node_command" BROWSE_PARENT_PID=0 \ PATH="$clean_path" GSTACK_NODE="$node_command" BROWSE_PARENT_PID=0 \
"$GSTACK_HOME/bin/browse" goto about:blank "$GSTACK_HOME/bin/browse" goto "$smoke_url"
PATH="$clean_path" GSTACK_NODE="$node_command" BROWSE_PARENT_PID=0 \ PATH="$clean_path" GSTACK_NODE="$node_command" BROWSE_PARENT_PID=0 \
"$GSTACK_HOME/bin/browse" status "$GSTACK_HOME/bin/browse" status
browser_cleanup browser_cleanup
trap - EXIT trap - EXIT
stage="$RUNNER_TEMP/runtime-components" stage="$RUNNER_TEMP/runtime-components"
mkdir -p "$stage" "$GITHUB_WORKSPACE/release-output" # GNU tar treats a Windows drive colon in an archive path as a
# remote-host separator. Keep archive output in Git Bash's POSIX view.
release_dir="$(pwd -P)/release-output"
mkdir -p "$stage" "$release_dir"
node .github/scripts/stage-runtime-components.mjs "$active" "$stage" node .github/scripts/stage-runtime-components.mjs "$active" "$stage"
for component_dir in "$stage"/*; do for component_dir in "$stage"/*; do
test -d "$component_dir" || continue test -d "$component_dir" || continue
component=$(basename "$component_dir") component=$(basename "$component_dir")
archive="$GITHUB_WORKSPACE/release-output/gstack-runtime-2.0.0-$TARGET-$component.tar.gz" archive="$release_dir/gstack-runtime-2.0.0-$TARGET-$component.tar.gz"
tar -czf "$archive" -C "$component_dir" gstack tar -czf "$archive" -C "$component_dir" gstack
node -e 'const fs=require("fs"),c=require("crypto"),p=process.argv[1];const b=fs.readFileSync(p);fs.writeFileSync(p+".sha256",c.createHash("sha256").update(b).digest("hex")+" "+require("path").basename(p)+"\n")' "$archive" node -e 'const fs=require("fs"),c=require("crypto"),p=process.argv[1];const b=fs.readFileSync(p);fs.writeFileSync(p+".sha256",c.createHash("sha256").update(b).digest("hex")+" "+require("path").basename(p)+"\n")' "$archive"
done done
shell: bash shell: bash
- name: Keyless-sign component archives
run: |
set -euo pipefail
for archive in release-output/*.tar.gz; do
cosign sign-blob --yes --bundle "$archive.sigstore.json" "$archive"
done
shell: bash
- name: Attest component archive provenance - name: Attest component archive provenance
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2 uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
with: with:
@@ -169,8 +166,16 @@ jobs:
merge-multiple: true merge-multiple: true
- uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 - uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
- name: Keyless-sign component archives
run: |
set -euo pipefail
for archive in release-output/*.tar.gz; do
cosign sign-blob --yes --bundle "$archive.sigstore.json" "$archive"
done
shell: bash
- name: Create strict six-target manifest - name: Create strict six-target manifest
run: node .github/scripts/create-runtime-release-manifest.mjs release-output "$GITHUB_REPOSITORY" 2.0.0 run: node .github/scripts/create-runtime-release-manifest.mjs release-output "$GITHUB_REPOSITORY" 2.0.0 "$GITHUB_REF_NAME"
- name: Checksum and keyless-sign manifest - name: Checksum and keyless-sign manifest
run: | run: |
@@ -188,11 +193,13 @@ jobs:
- name: Publish immutable release assets - name: Publish immutable release assets
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
PRERELEASE_FLAG: ${{ contains(github.ref_name, '-rc.') && '--prerelease' || '' }}
run: | run: |
set -euo pipefail set -euo pipefail
gh release create "$GITHUB_REF_NAME" \ gh release create "$GITHUB_REF_NAME" \
--verify-tag \ --verify-tag \
--title "GStack runtime 2.0.0" \ $PRERELEASE_FLAG \
--notes "Signed optional runtime artifacts for the six portable GStack skills." \ --title "GStack runtime $GITHUB_REF_NAME" \
--notes "Signed optional runtime artifacts for the six portable GStack skills. This release adds an explicit managed-versus-installed Chromium consent gate before browser preview or installation." \
release-output/* release-output/*
shell: bash shell: bash
+2 -2
View File
@@ -65,14 +65,14 @@ jobs:
- name: Preview without mutating state - name: Preview without mutating state
run: | run: |
set -e set -e
bash ./setup --dry-run --capabilities browser bash ./setup --dry-run --capabilities browser --browser managed
test ! -e "$GSTACK_HOME" || (echo "dry-run mutated GSTACK_HOME" && exit 1) test ! -e "$GSTACK_HOME" || (echo "dry-run mutated GSTACK_HOME" && exit 1)
shell: bash shell: bash
- name: Explicitly install the browser capability - name: Explicitly install the browser capability
run: | run: |
set -e set -e
bash ./setup --install-now --yes --capabilities browser bash ./setup --install-now --yes --capabilities browser --browser managed
test -f "$GSTACK_HOME/versions/current.json" test -f "$GSTACK_HOME/versions/current.json"
test -f "$GSTACK_HOME/bin/gstack.cmd" test -f "$GSTACK_HOME/bin/gstack.cmd"
shell: bash shell: bash
+1 -1
View File
@@ -60,7 +60,7 @@ they contain no copied judgment. Representative mappings:
Canonical installation is standards-based: Canonical installation is standards-based:
```bash ```bash
npx skills add time-attack/gstack npx skills add time-attack/gstack/skills
``` ```
Delegate host detection, placement, project/global scope, selected-skill Delegate host detection, placement, project/global scope, selected-skill
+1 -1
View File
@@ -13,7 +13,7 @@ mutation boundaries, recommendations, or voice. Compatibility files under
Specification, and Full chain. Do not expose its internal aliases as more Specification, and Full chain. Do not expose its internal aliases as more
top-level modes. top-level modes.
Canonical install is `npx skills add time-attack/gstack`. Standard installers Canonical install is `npx skills add time-attack/gstack/skills`. Standard installers
own host placement and scope. Do not add host-specific install logic to the 2.0 own host placement and scope. Do not add host-specific install logic to the 2.0
path. Pure judgment must work without the optional `bin/gstack` runtime. path. Pure judgment must work without the optional `bin/gstack` runtime.
The host-neutral `./setup` installs only that managed runtime/capability bundle; The host-neutral `./setup` installs only that managed runtime/capability bundle;
+4 -4
View File
@@ -35,7 +35,7 @@ Install with the standard Agent Skills installer; it owns host detection,
scope, destination paths, updates, removal, and selected-skill installation: scope, destination paths, updates, removal, and selected-skill installation:
```bash ```bash
npx skills add time-attack/gstack npx skills add time-attack/gstack/skills
``` ```
That installs the six judgment skills. Install a subset with the installer's That installs the six judgment skills. Install a subset with the installer's
@@ -87,7 +87,7 @@ extension, connector, MCP, settings entry, profile, alternate host, or browser
runtime is configured without consent, and `./setup` is never a GStack 2 runtime is configured without consent, and `./setup` is never a GStack 2
browser setup command. browser setup command.
Install through `npx skills add time-attack/gstack`; do not clone the repository Install through `npx skills add time-attack/gstack/skills`; do not clone the repository
under `.agents/skills/gstack`. A standards installation exposes exactly one under `.agents/skills/gstack`. A standards installation exposes exactly one
canonical QA skill at `.agents/skills/qa/SKILL.md`. The cloned compatibility canonical QA skill at `.agents/skills/qa/SKILL.md`. The cloned compatibility
tree contains legacy GStack 1 entry points and is not the GStack 2 install tree contains legacy GStack 1 entry points and is not the GStack 2 install
@@ -96,7 +96,7 @@ surface.
The npm package is deliberately not the skill installer and does not contain The npm package is deliberately not the skill installer and does not contain
the six skill tree or compiled browser/design/PDF payloads. It is the small the six skill tree or compiled browser/design/PDF payloads. It is the small
host-neutral runtime control/bootstrap surface used by release tooling. New host-neutral runtime control/bootstrap surface used by release tooling. New
users should install skills with `npx skills add time-attack/gstack`; optional users should install skills with `npx skills add time-attack/gstack/skills`; optional
capabilities are downloaded by a skill after consent. capabilities are downloaded by a skill after consent.
Public web research is optional. Context.dev is the only new external service, Public web research is optional. Context.dev is the only new external service,
@@ -161,7 +161,7 @@ archive. It is not the GStack 2 installation or first-run path.
## Legacy 1.x host-specific install (compatibility only) ## Legacy 1.x host-specific install (compatibility only)
Do not use this section for a new GStack 2 install. Use Do not use this section for a new GStack 2 install. Use
`npx skills add time-attack/gstack` above. These instructions remain while old `npx skills add time-attack/gstack/skills` above. These instructions remain while old
commands are documented as opt-in aliases. They describe a 1.x release/tag: commands are documented as opt-in aliases. They describe a 1.x release/tag:
the current branch's `./setup` is runtime-only and will not perform the the current branch's `./setup` is runtime-only and will not perform the
host-specific actions shown below. host-specific actions shown below.
+34 -2
View File
@@ -44,6 +44,30 @@ export function isCustomChromium(): boolean {
return p.includes('GBrowser') || p.includes('gbrowser'); return p.includes('GBrowser') || p.includes('gbrowser');
} }
/**
* Return the explicitly selected Chromium executable for both headless and
* headed launches. Keeping this opt-in preserves the managed browser fallback
* while allowing the lightweight playwright-core adapter to reuse a system or
* host-managed Chrome without downloading Playwright's browser package.
*/
export function configuredChromiumExecutable(
env: NodeJS.ProcessEnv = process.env,
): string | undefined {
const value = env.GSTACK_CHROMIUM_PATH?.trim();
return value || undefined;
}
/** Installed-system Chromium is supported only for headless automation. */
export function assertHeadedBrowserProvider(
env: NodeJS.ProcessEnv = process.env,
): void {
if (env.GSTACK_BROWSER_PROVIDER === 'installed') {
throw new Error(
'Visible GStack Browser requires managed Chromium; installed Chrome-family browsers are headless-only',
);
}
}
/** /**
* Decide whether Playwright should request Chromium's sandbox. * Decide whether Playwright should request Chromium's sandbox.
* *
@@ -358,9 +382,11 @@ export class BrowserManager {
// BROWSE_EXTENSIONS_DIR points to an unpacked Chrome extension directory. // BROWSE_EXTENSIONS_DIR points to an unpacked Chrome extension directory.
// Extensions only work in headed mode, so we use an off-screen window. // Extensions only work in headed mode, so we use an off-screen window.
const extensionsDir = process.env.BROWSE_EXTENSIONS_DIR; const extensionsDir = process.env.BROWSE_EXTENSIONS_DIR;
if (extensionsDir) assertHeadedBrowserProvider();
const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth'); const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth');
const launchArgs: string[] = [...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()]; const launchArgs: string[] = [...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()];
let useHeadless = true; let useHeadless = true;
const executablePath = configuredChromiumExecutable();
// Docker/CI/root: Chromium sandbox requires unprivileged user namespaces which // Docker/CI/root: Chromium sandbox requires unprivileged user namespaces which
// are typically disabled in containers and are never available for the root // are typically disabled in containers and are never available for the root
@@ -387,7 +413,11 @@ export class BrowserManager {
this.browser = await chromium.launch({ this.browser = await chromium.launch({
headless: useHeadless, headless: useHeadless,
...(useHeadless && managedHeadlessChannel() ? { channel: 'chromium' as const } : {}), ...(executablePath
? { executablePath }
: useHeadless && managedHeadlessChannel()
? { channel: 'chromium' as const }
: {}),
// On Windows, Chromium's sandbox fails when the server is spawned through // On Windows, Chromium's sandbox fails when the server is spawned through
// the Bun→Node process chain (GitHub #276). Disable it — local daemon // the Bun→Node process chain (GitHub #276). Disable it — local daemon
// browsing user-specified URLs has marginal sandbox benefit. Also disabled // browsing user-specified URLs has marginal sandbox benefit. Also disabled
@@ -447,6 +477,7 @@ export class BrowserManager {
* every action Claude takes in real time. * every action Claude takes in real time.
*/ */
async launchHeaded(authToken?: string): Promise<void> { async launchHeaded(authToken?: string): Promise<void> {
assertHeadedBrowserProvider();
// Clear old state before repopulating // Clear old state before repopulating
this.pages.clear(); this.pages.clear();
this.tabSessions.clear(); this.tabSessions.clear();
@@ -515,7 +546,7 @@ export class BrowserManager {
// Support custom Chromium binary via GSTACK_CHROMIUM_PATH env var. // Support custom Chromium binary via GSTACK_CHROMIUM_PATH env var.
// Used by GStack Browser.app to point at the bundled Chromium. // Used by GStack Browser.app to point at the bundled Chromium.
const executablePath = process.env.GSTACK_CHROMIUM_PATH || undefined; const executablePath = configuredChromiumExecutable();
// Rebrand Chromium → GStack Browser in macOS menu bar / Dock / Cmd+Tab. // Rebrand Chromium → GStack Browser in macOS menu bar / Dock / Cmd+Tab.
// Patch the Chromium .app's Info.plist so macOS shows our name. // Patch the Chromium .app's Info.plist so macOS shows our name.
@@ -1557,6 +1588,7 @@ export class BrowserManager {
* If step 2 fails return error, headless browser untouched * If step 2 fails return error, headless browser untouched
*/ */
async handoff(message: string): Promise<string> { async handoff(message: string): Promise<string> {
assertHeadedBrowserProvider();
if (this.connectionMode === 'headed' || this.isHeaded) { if (this.connectionMode === 'headed' || this.isHeaded) {
return `HANDOFF: Already in headed mode at ${this.getCurrentUrl()}`; return `HANDOFF: Already in headed mode at ${this.getCurrentUrl()}`;
} }
+10 -5
View File
@@ -118,7 +118,7 @@ interface ServerState {
serverPath: string; serverPath: string;
binaryVersion?: string; binaryVersion?: string;
mode?: 'launched' | 'headed'; mode?: 'launched' | 'headed';
/** Hash of (proxyUrl + headed flag), used by D2 daemon-mismatch check. */ /** Hash of proxy, headed mode, and browser-provider intent, used by daemon-mismatch checks. */
configHash?: string; configHash?: string;
/** Xvfb child PID for cleanup on disconnect. */ /** Xvfb child PID for cleanup on disconnect. */
xvfbPid?: number; xvfbPid?: number;
@@ -431,8 +431,8 @@ async function ensureServer(flags?: GlobalFlags): Promise<ServerState> {
// hint. No silent restart — that would drop tab state, cookies, and // hint. No silent restart — that would drop tab state, cookies, and
// logged-in sessions without warning. // logged-in sessions without warning.
if (desiredHash && state.configHash && state.configHash !== desiredHash) { if (desiredHash && state.configHash && state.configHash !== desiredHash) {
console.error(`[browse] existing daemon has different config (proxy/headed mismatch).`); console.error(`[browse] existing daemon has different config (browser provider, proxy, or headed mode).`);
console.error(`[browse] run 'browse disconnect' first to apply --proxy/--headed.`); console.error(`[browse] run 'browse disconnect' first to apply the selected browser configuration.`);
process.exit(1); process.exit(1);
} }
// Same path: existing daemon is plain (no flags) but caller passes // Same path: existing daemon is plain (no flags) but caller passes
@@ -782,7 +782,7 @@ export interface GlobalFlags {
proxyUrl: string | null; proxyUrl: string | null;
/** Whether --headed was passed. */ /** Whether --headed was passed. */
headed: boolean; headed: boolean;
/** Hash of (proxy + headed) for daemon-mismatch check. */ /** Hash of proxy, headed mode, and browser-provider intent for daemon-mismatch checks. */
configHash: string; configHash: string;
/** Redacted form of proxyUrl, safe for logs. */ /** Redacted form of proxyUrl, safe for logs. */
redactedProxyUrl: string; redactedProxyUrl: string;
@@ -842,7 +842,12 @@ export function extractGlobalFlags(rawArgs: string[], env: NodeJS.ProcessEnv): G
args: out, args: out,
proxyUrl: canonicalProxyUrl, proxyUrl: canonicalProxyUrl,
headed, headed,
configHash: computeConfigHash({ proxyUrl: canonicalProxyUrl, headed }), configHash: computeConfigHash({
proxyUrl: canonicalProxyUrl,
headed,
browserProvider: env.GSTACK_BROWSER_PROVIDER,
browserExecutable: env.GSTACK_CHROMIUM_PATH,
}),
redactedProxyUrl: redactProxyUrl(canonicalProxyUrl), redactedProxyUrl: redactProxyUrl(canonicalProxyUrl),
}; };
} }
+11 -2
View File
@@ -125,7 +125,7 @@ export function toUpstreamConfig(cfg: ParsedProxyConfig): UpstreamConfig {
} }
/** /**
* Compute a stable hash of (proxyUrl + headed flag) for daemon-mismatch * Compute a stable hash of proxy, headed mode, and browser-provider intent for daemon-mismatch
* detection (D2). The hash is deterministic across CLI invocations on the * detection (D2). The hash is deterministic across CLI invocations on the
* same machine and survives daemon restarts via the state file. * same machine and survives daemon restarts via the state file.
* *
@@ -135,9 +135,18 @@ export function toUpstreamConfig(cfg: ParsedProxyConfig): UpstreamConfig {
export function computeConfigHash(opts: { export function computeConfigHash(opts: {
proxyUrl: string | null | undefined; proxyUrl: string | null | undefined;
headed: boolean; headed: boolean;
browserProvider?: string | null;
browserExecutable?: string | null;
}): string { }): string {
const proxyKey = canonicalizeProxyUrl(opts.proxyUrl); const proxyKey = canonicalizeProxyUrl(opts.proxyUrl);
const input = JSON.stringify({ proxy: proxyKey, headed: opts.headed }); const browserProvider = opts.browserProvider || null;
const browserExecutable = browserProvider === "installed" ? opts.browserExecutable || null : null;
const input = JSON.stringify({
proxy: proxyKey,
headed: opts.headed,
browserProvider,
browserExecutable,
});
return createHash('sha256').update(input).digest('hex').slice(0, 16); return createHash('sha256').update(input).digest('hex').slice(0, 16);
} }
+12 -5
View File
@@ -355,11 +355,18 @@ export async function handleWriteCommand(
} }
} catch (err: any) { } catch (err: any) {
// Enhanced error guidance: clicking <option> elements always fails (not visible / timeout) // Enhanced error guidance: clicking <option> elements always fails (not visible / timeout)
const isOption = 'locator' in resolved // Do not start a second auto-wait after the click has already timed out.
? await resolved.locator.evaluate(el => el.tagName === 'OPTION').catch(() => false) // Missing selectors used to spend 5s in click(), then block again in
: await target.locator(resolved.selector).evaluate( // evaluate() until the outer client killed the command. count() is an
el => el.tagName === 'OPTION' // immediate query and keeps the helpful option guidance only when one
).catch(() => false); // unique element actually exists.
const optionLocator = 'locator' in resolved
? resolved.locator
: target.locator(resolved.selector);
const optionCount = await optionLocator.count().catch(() => 0);
const isOption = optionCount === 1
? await optionLocator.evaluate(el => el.tagName === 'OPTION').catch(() => false)
: false;
if (isOption) { if (isOption) {
throw new Error( throw new Error(
`Cannot click <option> elements. Use 'browse select <parent-select> <value>' instead of 'click' for dropdown options.` `Cannot click <option> elements. Use 'browse select <parent-select> <value>' instead of 'click' for dropdown options.`
+34 -1
View File
@@ -7,7 +7,7 @@
import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { startTestServer } from './test-server'; import { startTestServer } from './test-server';
import { BrowserManager } from '../src/browser-manager'; import { BrowserManager, assertHeadedBrowserProvider, configuredChromiumExecutable } from '../src/browser-manager';
import { resolveServerScript } from '../src/cli'; import { resolveServerScript } from '../src/cli';
import { handleReadCommand as _handleReadCommand, parseOutArgs, hasOutArg, resultToString } from '../src/read-commands'; import { handleReadCommand as _handleReadCommand, parseOutArgs, hasOutArg, resultToString } from '../src/read-commands';
import { handleWriteCommand as _handleWriteCommand } from '../src/write-commands'; import { handleWriteCommand as _handleWriteCommand } from '../src/write-commands';
@@ -23,6 +23,25 @@ const handleReadCommand = (cmd: string, args: string[], b: BrowserManager) =>
const handleWriteCommand = (cmd: string, args: string[], b: BrowserManager) => const handleWriteCommand = (cmd: string, args: string[], b: BrowserManager) =>
_handleWriteCommand(cmd, args, b.getActiveSession(), b); _handleWriteCommand(cmd, args, b.getActiveSession(), b);
describe('configuredChromiumExecutable', () => {
test('returns and trims an explicitly selected system browser', () => {
expect(configuredChromiumExecutable({
GSTACK_CHROMIUM_PATH: ' /Applications/Google Chrome.app/Contents/MacOS/Google Chrome ',
})).toBe('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome');
});
test('keeps the managed-browser path when no override is selected', () => {
expect(configuredChromiumExecutable({})).toBeUndefined();
expect(configuredChromiumExecutable({ GSTACK_CHROMIUM_PATH: ' ' })).toBeUndefined();
});
test('rejects headed launch when setup selected an installed system browser', () => {
expect(() => assertHeadedBrowserProvider({ GSTACK_BROWSER_PROVIDER: 'installed' }))
.toThrow('Visible GStack Browser requires managed Chromium');
expect(() => assertHeadedBrowserProvider({ GSTACK_BROWSER_PROVIDER: 'managed' })).not.toThrow();
});
});
// ─── Pure arg-parser + result-conversion unit tests (no browser) ─── // ─── Pure arg-parser + result-conversion unit tests (no browser) ───
describe('parseOutArgs / hasOutArg', () => { describe('parseOutArgs / hasOutArg', () => {
test('--out <path> splits the flag from the positional', () => { test('--out <path> splits the flag from the positional', () => {
@@ -459,6 +478,20 @@ describe('Interaction', () => {
} }
}, 15000); }, 15000);
test('click on a missing selector does not start a second locator wait', async () => {
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
const started = performance.now();
try {
await handleWriteCommand('click', ['#definitely-missing-regression-node'], bm);
expect(true).toBe(false); // Should not reach here
} catch (err: any) {
expect(err.message).toContain('#definitely-missing-regression-node');
}
// click() intentionally retains Playwright's 5s auto-wait. The regression
// was a second default locator wait that pushed the total beyond 8s.
expect(performance.now() - started).toBeLessThan(6500);
}, 8000);
test('hover works', async () => { test('hover works', async () => {
const result = await handleWriteCommand('hover', ['h1'], bm); const result = await handleWriteCommand('hover', ['h1'], bm);
expect(result).toContain('Hovered'); expect(result).toContain('Hovered');
@@ -92,6 +92,47 @@ describe('D2 daemon-mismatch refuse (CLI integration)', () => {
} }
}, 15000); }, 15000);
test('refuses to reuse a same-version daemon from a different browser provider', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-provider-mismatch-'));
const stateFile = path.join(tmpDir, 'browse.json');
const fakeServer = await startFakeHealthServer('fake-token');
const { computeConfigHash } = await import('../src/proxy-config');
const managedHash = computeConfigHash({
proxyUrl: null,
headed: false,
browserProvider: 'managed',
});
fs.writeFileSync(stateFile, JSON.stringify({
pid: process.pid,
port: fakeServer.port,
token: 'fake-token',
startedAt: new Date().toISOString(),
serverPath: '',
mode: 'launched',
configHash: managedHash,
}, null, 2));
const cliEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined) cliEnv[key] = value;
}
cliEnv.BROWSE_STATE_FILE = stateFile;
cliEnv.GSTACK_BROWSER_PROVIDER = 'installed';
cliEnv.GSTACK_CHROMIUM_PATH = process.execPath;
try {
const result = await runCli(['status'], cliEnv);
expect(result.code).toBe(1);
expect(result.stderr).toContain('different config');
expect(result.stderr).toContain('browse disconnect');
} finally {
await fakeServer.close();
try { fs.unlinkSync(stateFile); } catch { /* ignore */ }
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}, 15000);
test('refuses when existing plain daemon meets a --proxy invocation', async () => { test('refuses when existing plain daemon meets a --proxy invocation', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-mismatch-plain-')); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-mismatch-plain-'));
const stateFile = path.join(tmpDir, 'browse.json'); const stateFile = path.join(tmpDir, 'browse.json');
+16
View File
@@ -186,4 +186,20 @@ describe('extractGlobalFlags', () => {
); );
expect(a.configHash).not.toBe(b.configHash); expect(a.configHash).not.toBe(b.configHash);
}); });
test('configHash changes with browser provider and installed executable', () => {
const managed = extractGlobalFlags(['goto', 'x'], {
GSTACK_BROWSER_PROVIDER: 'managed',
} as NodeJS.ProcessEnv);
const installedA = extractGlobalFlags(['goto', 'x'], {
GSTACK_BROWSER_PROVIDER: 'installed',
GSTACK_CHROMIUM_PATH: '/browser/a',
} as NodeJS.ProcessEnv);
const installedB = extractGlobalFlags(['goto', 'x'], {
GSTACK_BROWSER_PROVIDER: 'installed',
GSTACK_CHROMIUM_PATH: '/browser/b',
} as NodeJS.ProcessEnv);
expect(managed.configHash).not.toBe(installedA.configHash);
expect(installedA.configHash).not.toBe(installedB.configHash);
});
}); });
+27 -27
View File
@@ -10,7 +10,7 @@
"diff": "^9.0.0", "diff": "^9.0.0",
"html-to-docx": "1.8.0", "html-to-docx": "1.8.0",
"marked": "^18.0.6", "marked": "^18.0.6",
"playwright": "^1.58.2", "playwright": "npm:playwright-core@^1.58.2",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"socks": "^2.8.9", "socks": "^2.8.9",
"xterm": "5", "xterm": "5",
@@ -52,13 +52,13 @@
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.112.4", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-7eXJJnrmBI5GMC6drrCiSkycVsT7crRZX3qv5HusLSm+qiILjmtqP7gf+UiT7ASu/7Gdj+Zfl4f2haV8wATKUg=="], "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.112.4", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-7eXJJnrmBI5GMC6drrCiSkycVsT7crRZX3qv5HusLSm+qiILjmtqP7gf+UiT7ASu/7Gdj+Zfl4f2haV8wATKUg=="],
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
"@huggingface/jinja": ["@huggingface/jinja@0.5.7", "", {}, "sha512-OosMEbF/R6zkKNNzqhI7kvKYCpo1F0UeIv46/h4D4UjVEKKd6k3TiV8sgu6fkreX4lbBiRI+lZG8UnXnqVQmEQ=="], "@huggingface/jinja": ["@huggingface/jinja@0.5.9", "", {}, "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw=="],
"@huggingface/tokenizers": ["@huggingface/tokenizers@0.1.3", "", {}, "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA=="], "@huggingface/tokenizers": ["@huggingface/tokenizers@0.1.3", "", {}, "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA=="],
@@ -172,17 +172,17 @@
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"adm-zip": ["adm-zip@0.6.0", "", {}, "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg=="], "adm-zip": ["adm-zip@0.6.0", "", {}, "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg=="],
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
@@ -212,7 +212,7 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
@@ -252,7 +252,7 @@
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
@@ -266,11 +266,11 @@
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="], "express-rate-limit": ["express-rate-limit@8.6.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
@@ -286,8 +286,6 @@
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
@@ -310,7 +308,7 @@
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"hono": ["hono@4.12.25", "", {}, "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ=="], "hono": ["hono@4.12.25", "", {}, "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ=="],
@@ -324,7 +322,7 @@
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="],
"image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="],
@@ -350,7 +348,7 @@
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
@@ -368,7 +366,7 @@
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"marked": ["marked@18.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-MrV5puXBfuiy6wl6DLaq3BtIJQAJToAd5zt/ZKhRfGRAuFPALE7/4Y7jnxRQoEgK/pBgurGqLyAuRgZ2xOjr6w=="], "marked": ["marked@18.0.7", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA=="],
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
@@ -386,7 +384,7 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
@@ -422,9 +420,7 @@
"platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="], "platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="],
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], "playwright": ["playwright-core@1.61.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg=="],
"playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
@@ -440,7 +436,7 @@
"queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
@@ -458,7 +454,7 @@
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
@@ -478,7 +474,7 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
@@ -510,9 +506,9 @@
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
@@ -542,7 +538,7 @@
"xterm-addon-fit": ["xterm-addon-fit@0.8.0", "", { "peerDependencies": { "xterm": "^5.0.0" } }, "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw=="], "xterm-addon-fit": ["xterm-addon-fit@0.8.0", "", { "peerDependencies": { "xterm": "^5.0.0" } }, "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
@@ -554,6 +550,8 @@
"accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"dom-serializer/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], "dom-serializer/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], "dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
@@ -566,6 +564,8 @@
"send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"xmlbuilder2/@oozcitak/dom": ["@oozcitak/dom@1.15.5", "", { "dependencies": { "@oozcitak/infra": "1.0.5", "@oozcitak/url": "1.0.0", "@oozcitak/util": "8.0.0" } }, "sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A=="], "xmlbuilder2/@oozcitak/dom": ["@oozcitak/dom@1.15.5", "", { "dependencies": { "@oozcitak/infra": "1.0.5", "@oozcitak/url": "1.0.0", "@oozcitak/util": "8.0.0" } }, "sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A=="],
+1 -1
View File
@@ -85,7 +85,7 @@ judgment. This prevents an alias from drifting away from the canonical source.
The standard Agent Skills tree is the unit of distribution: The standard Agent Skills tree is the unit of distribution:
```bash ```bash
npx skills add time-attack/gstack npx skills add time-attack/gstack/skills
``` ```
The installer—not GStack—owns host detection, project/global scope, destination The installer—not GStack—owns host detection, project/global scope, destination
+2 -2
View File
@@ -18,7 +18,7 @@ bundle.
## Canonical installation ## Canonical installation
```bash ```bash
npx skills add time-attack/gstack npx skills add time-attack/gstack/skills
``` ```
The standards installer owns host detection, destination paths, project/global The standards installer owns host detection, destination paths, project/global
@@ -32,7 +32,7 @@ Examples supported by the installer interface:
npx skills add time-attack/gstack/skills --skill qa npx skills add time-attack/gstack/skills --skill qa
# Installer-managed global scope # Installer-managed global scope
npx skills add time-attack/gstack -g npx skills add time-attack/gstack/skills -g
``` ```
Run `npx skills add --help` for the installed CLI version before scripting Run `npx skills add --help` for the installed CLI version before scripting
+1 -1
View File
@@ -2,7 +2,7 @@
Parity is executable, not a prose claim. Run `bun run scripts/gstack2/run-parity.ts` or the dedicated Bun tests. Parity is executable, not a prose claim. Run `bun run scripts/gstack2/run-parity.ts` or the dedicated Bun tests.
The pinned release inventory passes **4,833 checks** across 55 specialist sources, 16 carved sections, 25 routing scenarios, 16 regression ports, and **78 assets**. The pinned release inventory passes **4,836 checks** across 55 specialist sources, 16 carved sections, 25 routing scenarios, 16 regression ports, and **78 assets**.
The suite verifies: The suite verifies:
+16 -16
View File
@@ -83,7 +83,7 @@
"source_path": "office-hours/SKILL.md.tmpl", "source_path": "office-hours/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f", "blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052", "normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
"target": "skills/plan/references/legacy/office-hours.md", "target": "skills/plan/references/legacy/office-hours.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -1416,7 +1416,7 @@
"source_path": "design-consultation/SKILL.md.tmpl", "source_path": "design-consultation/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811", "blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b", "normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
"target": "skills/design/references/legacy/design-consultation.md", "target": "skills/design/references/legacy/design-consultation.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -1591,7 +1591,7 @@
"source_path": "design-html/SKILL.md.tmpl", "source_path": "design-html/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca", "blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30", "normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
"target": "skills/design/references/legacy/design-html.md", "target": "skills/design/references/legacy/design-html.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -1820,7 +1820,7 @@
"source_path": "design-review/SKILL.md.tmpl", "source_path": "design-review/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41", "blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b", "normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
"target": "skills/design/references/legacy/design-review.md", "target": "skills/design/references/legacy/design-review.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2162,7 +2162,7 @@
"source_path": "qa/SKILL.md.tmpl", "source_path": "qa/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8", "blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55", "normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
"target": "skills/qa/references/legacy/qa.md", "target": "skills/qa/references/legacy/qa.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2288,7 +2288,7 @@
"source_path": "qa-only/SKILL.md.tmpl", "source_path": "qa-only/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e", "blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3", "normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
"target": "skills/qa/references/legacy/qa-only.md", "target": "skills/qa/references/legacy/qa-only.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2466,7 +2466,7 @@
"source_path": "devex-review/SKILL.md.tmpl", "source_path": "devex-review/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551", "blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1", "normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
"target": "skills/qa/references/legacy/devex-review.md", "target": "skills/qa/references/legacy/devex-review.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2565,7 +2565,7 @@
"source_path": "benchmark/SKILL.md.tmpl", "source_path": "benchmark/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e", "blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d", "normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
"target": "skills/qa/references/legacy/benchmark.md", "target": "skills/qa/references/legacy/benchmark.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2631,7 +2631,7 @@
"source_path": "canary/SKILL.md.tmpl", "source_path": "canary/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c", "blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef", "normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
"target": "skills/qa/references/legacy/canary.md", "target": "skills/qa/references/legacy/canary.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2710,7 +2710,7 @@
"source_path": "browse/SKILL.md.tmpl", "source_path": "browse/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93", "blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b", "normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
"target": "skills/qa/references/legacy/browse.md", "target": "skills/qa/references/legacy/browse.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2798,7 +2798,7 @@
"source_path": "open-gstack-browser/SKILL.md.tmpl", "source_path": "open-gstack-browser/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b", "blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
"normalized_render_sha256": "df626d71b8cea4a02d2fb7aef3169563dd132bf17a9d6d84f287894cad84d2cf", "normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
"target": "skills/qa/references/legacy/open-gstack-browser.md", "target": "skills/qa/references/legacy/open-gstack-browser.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2873,7 +2873,7 @@
"source_path": "setup-browser-cookies/SKILL.md.tmpl", "source_path": "setup-browser-cookies/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652", "blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
"normalized_render_sha256": "04c161a58c1a9010efe38095b383b0e1d445a2b678e5bf931a1281d45196940d", "normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
"target": "skills/qa/references/legacy/setup-browser-cookies.md", "target": "skills/qa/references/legacy/setup-browser-cookies.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2934,7 +2934,7 @@
"source_path": "pair-agent/SKILL.md.tmpl", "source_path": "pair-agent/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c", "blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
"normalized_render_sha256": "8557ca390d0b6548f956d2c0e9316f1cf137689d4dc17d40a4525d19f22bc457", "normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
"target": "skills/qa/references/legacy/pair-agent.md", "target": "skills/qa/references/legacy/pair-agent.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -4136,7 +4136,7 @@
"source_path": "land-and-deploy/SKILL.md.tmpl", "source_path": "land-and-deploy/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2", "blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd", "normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
"target": "skills/ship/references/legacy/land-and-deploy.md", "target": "skills/ship/references/legacy/land-and-deploy.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -4552,7 +4552,7 @@
"source_path": "gstack-upgrade/SKILL.md.tmpl", "source_path": "gstack-upgrade/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d", "blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d",
"normalized_render_sha256": "a913cf77f76c4d68c576a06190b498e3d8d3b60b85498a68f173b23a7f800828", "normalized_render_sha256": "2c8cf9e505b7da27730bf646a33ee38e8092259f3335a524c71e57ba5b602541",
"target": "skills/ship/references/legacy/gstack-upgrade.md", "target": "skills/ship/references/legacy/gstack-upgrade.md",
"disposition": "DUPLICATE_INFRASTRUCTURE", "disposition": "DUPLICATE_INFRASTRUCTURE",
"overlays": [ "overlays": [
@@ -5775,7 +5775,7 @@
"owner_tree": "qa", "owner_tree": "qa",
"consumer_tree": "ship", "consumer_tree": "ship",
"target": "skills/ship/references/legacy/canary.md", "target": "skills/ship/references/legacy/canary.md",
"sha256": "9fcf4fdea7d52113c8f5b5cc81c1fb36df591cf9e07bcc38c4ee106b15245997", "sha256": "155174e829ef7895707ff1fad1f130fc1c7150bad9fb3598428a2efc939c5ad6",
"disposition": "SHARED_MODULE" "disposition": "SHARED_MODULE"
}, },
{ {
+14 -5
View File
@@ -5,10 +5,11 @@ GStack has two explicit version identities during the 2.0 migration:
- `VERSION` and `package.json.version` are the repository/package release - `VERSION` and `package.json.version` are the repository/package release
counter. They remain byte-equal and retain the existing four-slot format so counter. They remain byte-equal and retain the existing four-slot format so
the 1.x compatibility ship queue does not silently fail open. the 1.x compatibility ship queue does not silently fail open.
- `package.json.gstack.runtimeVersion`, `runtime/index.js`, - `package.json.gstack.runtimeVersion`, `runtime/index.js`, and
`runtime/install.js`, and every standards-installed bootstrap declare the `runtime/install.js` declare the managed-runtime protocol release `2.0.0`.
managed-runtime protocol release `2.0.0`. The official artifact tag and Each standards-installed bootstrap separately pins one immutable artifact
manifest use that value. release tag. Candidate bootstraps use `v2.0.0-rc.N`; the manifest and bundle
remain runtime-compatible with `2.0.0`. Stable bootstraps use `v2.0.0`.
They are intentionally different namespaces. CI fails if either identity They are intentionally different namespaces. CI fails if either identity
drifts inside its own namespace. drifts inside its own namespace.
@@ -24,6 +25,14 @@ linux-arm64 linux-x64 (glibc)
windows-arm64 windows-x64 windows-arm64 windows-x64
``` ```
Both `v2.0.0-rc.*` and `v2.0.0` tags use the same build, signing, manifest,
attestation, and smoke path. RC tags publish GitHub prereleases so the exact
fresh-machine production bootstrap can be exercised before the stable tag is
created. Runtime compatibility and release-channel identity are deliberately
separate: archive names and manifest `version` remain `2.0.0`, while URLs and
Sigstore certificate identity bind to the immutable RC or stable tag that
actually published them.
Each archive has one `gstack/` root and no symlinks. CI records an exact byte Each archive has one `gstack/` root and no symlinks. CI records an exact byte
count and SHA-256, signs the archive keylessly with Cosign, emits a Sigstore count and SHA-256, signs the archive keylessly with Cosign, emits a Sigstore
bundle, and also creates a GitHub build-provenance attestation. The release bundle, and also creates a GitHub build-provenance attestation. The release
@@ -31,7 +40,7 @@ manifest contains only official GitHub Release URLs and the fixed workflow
certificate identity. certificate identity.
Browser-capable archives include the Playwright-managed Chromium directory at Browser-capable archives include the Playwright-managed Chromium directory at
`.gstack-runtime-browsers`. The builder runs `playwright install chromium` `.gstack-runtime-browsers`. The builder runs `playwright-core install chromium`
only—never `--with-deps` or `sudo`—copies physical files into the immutable only—never `--with-deps` or `sudo`—copies physical files into the immutable
slot, and launch-smokes that exact Chromium on every native release runner. slot, and launch-smokes that exact Chromium on every native release runner.
The stable capability launcher sets `PLAYWRIGHT_BROWSERS_PATH` to the active The stable capability launcher sets `PLAYWRIGHT_BROWSERS_PATH` to the active
+16 -4
View File
@@ -57,13 +57,13 @@ PR, or PR-ready claim is authorized by this status.
three retained Claude Haiku live samples are classified `REGRESSION`; they three retained Claude Haiku live samples are classified `REGRESSION`; they
are preserved as noisy supplemental evidence, never cherry-picked as a are preserved as noisy supplemental evidence, never cherry-picked as a
primary gate or represented as green. primary gate or represented as green.
- [x] The current macOS GStack 2 suite is green: 151 pass / 0 fail and 1,194 - [x] The current macOS GStack 2 suite is green: 218 pass / 0 fail and 2,229
assertions across 16 files. assertions across 20 files.
- [x] Optional host-neutral runtime implemented with canonical paths, - [x] Optional host-neutral runtime implemented with canonical paths,
repo/worktree state identity, locks, atomic writes, effect claims, repo/worktree state identity, locks, atomic writes, effect claims,
doctor/config/state/cleanup, migrations, upgrade/rollback, and uninstall. doctor/config/state/cleanup, migrations, upgrade/rollback, and uninstall.
- [x] Managed runtime installer coverage is green at 25 pass / 0 fail and 341 - [x] Managed runtime installer coverage is green at 34 pass / 0 fail. The
assertions. The deterministic clean macOS arm64 managed-bundle audit records deterministic clean macOS arm64 managed-bundle audit records
110 components, 1,829 files, 450,044,315 bytes, and 50 capability launchers. 110 components, 1,829 files, 450,044,315 bytes, and 50 capability launchers.
This is a platform-specific bundle measurement, not a universal byte count; This is a platform-specific bundle measurement, not a universal byte count;
platform-native package payloads differ. Setup installs frozen platform-native package payloads differ. Setup installs frozen
@@ -75,6 +75,18 @@ PR, or PR-ready claim is authorized by this status.
production-only install with the development SDK absent, completed a local production-only install with the development SDK absent, completed a local
browser journey and Sharp full-page screenshot, and uninstalled while browser journey and Sharp full-page screenshot, and uninstalled while
preserving state. preserving state.
- [x] Browser-backed setup now fails closed until the user explicitly chooses
GStack-managed Chromium or one detected installed Chromium executable. The
local-only options step performs no network or state mutation; the signed
preview reports exact incremental bytes before a separate install approval;
only a successful install persists the host-neutral choice. Installed-browser
mode keeps the Playwright adapter while omitting managed Chromium payloads.
Visible extension-bearing GStack Browser remains managed-only. Focused setup
UX coverage is green at 22 pass / 0 fail, including no-network refusal,
provider-aware component planning, launcher propagation, and doctor launch.
The official live bootstrap remains pending until the corresponding signed
`v2.0.0-rc.6` component release is published; deterministic local evidence
does not relabel that production gate as passed.
- [x] The current candidate additionally captures a runtime-owned Bun 1.3.14 - [x] The current candidate additionally captures a runtime-owned Bun 1.3.14
executable under `.gstack-runtime-tools`, records its path/version in the executable under `.gstack-runtime-tools`, records its path/version in the
bundle manifest, vendors the tagged license/source notices, and routes the bundle manifest, vendors the tagged license/source notices, and routes the
+3 -2
View File
@@ -10,7 +10,7 @@ pass from deterministic, offline, or filesystem-only evidence.
| Command / probe | Observed result | What it proves / does not prove | | Command / probe | Observed result | What it proves / does not prove |
|---|---|---| |---|---|---|
| Earlier focused macOS `bun test test/gstack2-*.test.ts` candidate run | **Exit 0: 136 pass / 0 fail**, 1,128 assertions across 15 files. Log: `/tmp/gstack2-test-command-candidate-final2.log`. | Historical focused checkpoint retained rather than overwritten. | | Earlier focused macOS `bun test test/gstack2-*.test.ts` candidate run | **Exit 0: 136 pass / 0 fail**, 1,128 assertions across 15 files. Log: `/tmp/gstack2-test-command-candidate-final2.log`. | Historical focused checkpoint retained rather than overwritten. |
| Current macOS `bun run test:gstack2` | **Exit 0: 151 pass / 0 fail**, 1,194 assertions across 16 files. | Current generated freshness, routing, runtime, privacy, installer, upgrade, parity, and adversarial-harness surface is green. Native and broad evidence are recorded separately; this working-tree result is not attributed to the earlier native-CI commit. | | Current macOS direct `bun test --timeout 60000 test/gstack2-*.test.ts` | **Exit 0: 218 pass / 0 fail**, 2,229 assertions across 20 files. | Current routing, runtime, privacy, installer, upgrade, parity, browser-provider setup, and adversarial-harness surface is green. The `test:gstack2` wrapper's generated-cleanliness precheck remains a commit-time gate, so this row records the direct underlying suite rather than relabeling that wrapper. |
| `bun test --timeout 30000 test/gstack2-skills.test.ts test/gstack2-skills-routing.test.ts` after regeneration | **Exit 0: 3 pass / 0 fail**, 81 assertions. | The pinned corpus/parity test and both 25-scenario structured-routing tests are green. This remains structural/fixture evidence, not specialist live execution. | | `bun test --timeout 30000 test/gstack2-skills.test.ts test/gstack2-skills-routing.test.ts` after regeneration | **Exit 0: 3 pass / 0 fail**, 81 assertions. | The pinned corpus/parity test and both 25-scenario structured-routing tests are green. This remains structural/fixture evidence, not specialist live execution. |
| `bun run scripts/gstack2/run-parity.ts`, 2026-07-17 rerun | **Exit 0: 4,681 checks passed**; 55 modules, 16 sections, 25 scenarios, 16 regressions, 78 assets. | Current source/render/provenance/contract/asset/fixture parity is green. It is deterministic parity, not live-host behavior. | | `bun run scripts/gstack2/run-parity.ts`, 2026-07-17 rerun | **Exit 0: 4,681 checks passed**; 55 modules, 16 sections, 25 scenarios, 16 regressions, 78 assets. | Current source/render/provenance/contract/asset/fixture parity is green. It is deterministic parity, not live-host behavior. |
| Earlier regenerated structural parity checkpoint | **Exit 0: 2,403 checks passed** with the then-current 55/16/25/16/45 inventory. | Historical candidate checkpoint before later thin-prelude and asset coverage; superseded by the current 4,681-check rerun. | | Earlier regenerated structural parity checkpoint | **Exit 0: 2,403 checks passed** with the then-current 55/16/25/16/45 inventory. | Historical candidate checkpoint before later thin-prelude and asset coverage; superseded by the current 4,681-check rerun. |
@@ -30,7 +30,8 @@ pass from deterministic, offline, or filesystem-only evidence.
| `gstack context smoke --url https://www.context.dev --json` | **PASS:** the official `/web/scrape/markdown` endpoint returned `ok: true` and credit metadata. | The verified key entered through protected stdin/environment worked. The temporary secrets file was mode `0600`, the isolated home was removed, and the safe output did not contain the key. Committed redacted artifact: [`evals/context-dev/live-smoke-2026-07-17.json`](../../evals/context-dev/live-smoke-2026-07-17.json). | | `gstack context smoke --url https://www.context.dev --json` | **PASS:** the official `/web/scrape/markdown` endpoint returned `ok: true` and credit metadata. | The verified key entered through protected stdin/environment worked. The temporary secrets file was mode `0600`, the isolated home was removed, and the safe output did not contain the key. Committed redacted artifact: [`evals/context-dev/live-smoke-2026-07-17.json`](../../evals/context-dev/live-smoke-2026-07-17.json). |
| Standard installer matrix | **PASS: 510/510 checks**, 18 install cases, two removal cases, `skills` CLI 1.5.19. | Project/global installs pass for seven hosts, including Kimi Code CLI through the standard `.agents/skills` path; selected-skill and opt-in compatibility-alias cases, copies, and hashes pass. This remains installer/filesystem evidence. Committed artifact: [`evals/installation/install-matrix.json`](../../evals/installation/install-matrix.json). | | Standard installer matrix | **PASS: 510/510 checks**, 18 install cases, two removal cases, `skills` CLI 1.5.19. | Project/global installs pass for seven hosts, including Kimi Code CLI through the standard `.agents/skills` path; selected-skill and opt-in compatibility-alias cases, copies, and hashes pass. This remains installer/filesystem evidence. Committed artifact: [`evals/installation/install-matrix.json`](../../evals/installation/install-matrix.json). |
| Standard Agent Skills install + actual runtime-absent Codex invocation | **PASS.** Root `--list` returned exactly six. The selected source was `time-attack/gstack/skills --skill qa`; despite `skills` 1.5.19's pre-filter display counting hidden aliases, exactly one skill (`qa`) installed byte-identically. Codex exited 0 with `gstack` absent, reported `NEEDS_SETUP` and one approval prompt, changed no files, and created no runtime/browser/external-service activity. | Closes the actual-host runtime-absent judgment gate for Codex without overstating seven-host UI coverage. Artifact: [`standard-codex-runtime-absent-2026-07-17.json`](../../evals/installation/standard-codex-runtime-absent-2026-07-17.json). | | Standard Agent Skills install + actual runtime-absent Codex invocation | **PASS.** Root `--list` returned exactly six. The selected source was `time-attack/gstack/skills --skill qa`; despite `skills` 1.5.19's pre-filter display counting hidden aliases, exactly one skill (`qa`) installed byte-identically. Codex exited 0 with `gstack` absent, reported `NEEDS_SETUP` and one approval prompt, changed no files, and created no runtime/browser/external-service activity. | Closes the actual-host runtime-absent judgment gate for Codex without overstating seven-host UI coverage. Artifact: [`standard-codex-runtime-absent-2026-07-17.json`](../../evals/installation/standard-codex-runtime-absent-2026-07-17.json). |
| `bun test test/gstack2-runtime-install.test.ts` | **Exit 0: 25 pass / 0 fail**, 341 assertions. | Managed allowlist, hashes, spaces, source/internal-link rejection, production-only frozen dependencies, deterministic exact Sharp/ngrok platform closure, native-load rollback smoke, rollback/recovery, stable launchers, wrapper neutrality, and state-preserving uninstall pass. | | `bun test --timeout 30000 test/gstack2-runtime-install.test.ts` | **Exit 0: 41 pass / 0 fail.** | Managed allowlist, hashes, spaces, source/internal-link rejection, production-only frozen dependencies, deterministic exact Sharp/ngrok platform closure, native-load rollback smoke, atomic browser-choice rollback/recovery, provider-aware stable launchers, wrapper neutrality, and state-preserving uninstall pass. |
| `bun test --timeout 30000 test/gstack2-runtime-setup-ux.test.ts` | **Exit 0: 29 pass / 0 fail.** | Explicit managed/installed/later browser selection fails closed before network, local options do not mutate, signed previews remain provider-identical across same- and cross-release capability additions, developer-source provider switches replace the exact retained set, visible extension Chromium remains managed-only, and launcher/doctor paths honor the persisted selection. This is deterministic local evidence, not native Windows or every installed Chrome-family build. |
| Deterministic clean macOS arm64 managed runtime bundle audit | **110 components, 1,829 files, 450,044,315 bytes, 50 capability launchers.** | This is a platform-specific bundle measurement, not a universal byte count; platform-native package payloads differ. Setup includes the Sharp/ngrok closure and excludes the development-only Claude Agent SDK. The Hugging Face sidecar is outside the bundle and its package is development-only, so production setup installs neither its inference runtime nor model weights; the L4 capability reports unavailable. The standard skill installer remains Markdown-only. Committed artifact: [`evals/runtime-bundle/darwin-arm64.json`](../../evals/runtime-bundle/darwin-arm64.json); reproduce with `bun run scripts/gstack2/audit-runtime-bundle.ts --output evals/runtime-bundle/darwin-arm64.json`. | | Deterministic clean macOS arm64 managed runtime bundle audit | **110 components, 1,829 files, 450,044,315 bytes, 50 capability launchers.** | This is a platform-specific bundle measurement, not a universal byte count; platform-native package payloads differ. Setup includes the Sharp/ngrok closure and excludes the development-only Claude Agent SDK. The Hugging Face sidecar is outside the bundle and its package is development-only, so production setup installs neither its inference runtime nor model weights; the L4 capability reports unavailable. The standard skill installer remains Markdown-only. Committed artifact: [`evals/runtime-bundle/darwin-arm64.json`](../../evals/runtime-bundle/darwin-arm64.json); reproduce with `bun run scripts/gstack2/audit-runtime-bundle.ts --output evals/runtime-bundle/darwin-arm64.json`. |
| Earlier declared Linux Dev Container plus `bun run test:gstack2` inside it | **Exit 0: 136 pass / 0 fail, 1,127 assertions across 15 files.** Log: `/tmp/gstack2-devcontainer-gate-candidate-final4.log`. | Historical container checkpoint retained rather than overwritten; the current 150-test container result is recorded in the native-CI row below. | | Earlier declared Linux Dev Container plus `bun run test:gstack2` inside it | **Exit 0: 136 pass / 0 fail, 1,127 assertions across 15 files.** Log: `/tmp/gstack2-devcontainer-gate-candidate-final4.log`. | Historical container checkpoint retained rather than overwritten; the current 150-test container result is recorded in the native-CI row below. |
| `scripts/gstack2/runtime-install-smoke.sh` in the clean Linux arm64 container | **Pass:** production-only frozen dependencies installed with the development Agent SDK and Hugging Face/ONNX runtime absent; the managed Anthropic SDK, Sharp, and ngrok imports passed; prebuilt capabilities rebuilt; setup/doctor/version/design/PDF passed; a local-browser journey and Sharp full-page screenshot passed; uninstall preserved state. | Proves a source copy with spaces can build and complete the managed runtime lifecycle without Git history, an executable local-model stack, or Darwin-only iOS artifacts. It is not native Windows evidence. | | `scripts/gstack2/runtime-install-smoke.sh` in the clean Linux arm64 container | **Pass:** production-only frozen dependencies installed with the development Agent SDK and Hugging Face/ONNX runtime absent; the managed Anthropic SDK, Sharp, and ngrok imports passed; prebuilt capabilities rebuilt; setup/doctor/version/design/PDF passed; a local-browser journey and Sharp full-page screenshot passed; uninstall preserved state. | Proves a source copy with spaces can build and complete the managed runtime lifecycle without Git history, an executable local-model stack, or Darwin-only iOS artifacts. It is not native Windows evidence. |
+1 -1
View File
@@ -10,7 +10,7 @@ Install the canonical Agent Skills source, then use that installer's tracked
source and lock metadata for discovery, updates, and removal: source and lock metadata for discovery, updates, and removal:
```bash ```bash
npx skills add time-attack/gstack npx skills add time-attack/gstack/skills
npx skills update # interactive scope npx skills update # interactive scope
npx skills update -p # project installs only npx skills update -p # project installs only
npx skills update -g # global installs only npx skills update -g # global installs only
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "benchmark/SKILL.md.tmpl", "source_path": "benchmark/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e", "blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d", "normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
"target": "skills/qa/references/legacy/benchmark.md", "target": "skills/qa/references/legacy/benchmark.md",
"overlays": [ "overlays": [
679 679
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "browse/SKILL.md.tmpl", "source_path": "browse/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93", "blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b", "normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
"target": "skills/qa/references/legacy/browse.md", "target": "skills/qa/references/legacy/browse.md",
"overlays": [ "overlays": [
679, 679,
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "canary/SKILL.md.tmpl", "source_path": "canary/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c", "blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef", "normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
"target": "skills/qa/references/legacy/canary.md", "target": "skills/qa/references/legacy/canary.md",
"overlays": [ "overlays": [
679, 679,
@@ -9,7 +9,7 @@
"source_path": "design-consultation/SKILL.md.tmpl", "source_path": "design-consultation/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811", "blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b", "normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
"target": "skills/design/references/legacy/design-consultation.md", "target": "skills/design/references/legacy/design-consultation.md",
"overlays": [ "overlays": [
679, 679,
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "design-html/SKILL.md.tmpl", "source_path": "design-html/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca", "blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30", "normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
"target": "skills/design/references/legacy/design-html.md", "target": "skills/design/references/legacy/design-html.md",
"overlays": [ "overlays": [
679 679
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "design-review/SKILL.md.tmpl", "source_path": "design-review/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41", "blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b", "normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
"target": "skills/design/references/legacy/design-review.md", "target": "skills/design/references/legacy/design-review.md",
"overlays": [ "overlays": [
679, 679,
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "devex-review/SKILL.md.tmpl", "source_path": "devex-review/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551", "blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1", "normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
"target": "skills/qa/references/legacy/devex-review.md", "target": "skills/qa/references/legacy/devex-review.md",
"overlays": [ "overlays": [
679, 679,
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "gstack-upgrade/SKILL.md.tmpl", "source_path": "gstack-upgrade/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d", "blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d",
"normalized_render_sha256": "a913cf77f76c4d68c576a06190b498e3d8d3b60b85498a68f173b23a7f800828", "normalized_render_sha256": "2c8cf9e505b7da27730bf646a33ee38e8092259f3335a524c71e57ba5b602541",
"target": "skills/ship/references/legacy/gstack-upgrade.md", "target": "skills/ship/references/legacy/gstack-upgrade.md",
"overlays": [ "overlays": [
679 679
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "land-and-deploy/SKILL.md.tmpl", "source_path": "land-and-deploy/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2", "blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd", "normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
"target": "skills/ship/references/legacy/land-and-deploy.md", "target": "skills/ship/references/legacy/land-and-deploy.md",
"overlays": [ "overlays": [
679, 679,
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "office-hours/SKILL.md.tmpl", "source_path": "office-hours/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f", "blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052", "normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
"target": "skills/plan/references/legacy/office-hours.md", "target": "skills/plan/references/legacy/office-hours.md",
"overlays": [ "overlays": [
679, 679,
@@ -9,7 +9,7 @@
"source_path": "open-gstack-browser/SKILL.md.tmpl", "source_path": "open-gstack-browser/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b", "blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
"normalized_render_sha256": "df626d71b8cea4a02d2fb7aef3169563dd132bf17a9d6d84f287894cad84d2cf", "normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
"target": "skills/qa/references/legacy/open-gstack-browser.md", "target": "skills/qa/references/legacy/open-gstack-browser.md",
"overlays": [ "overlays": [
679 679
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "pair-agent/SKILL.md.tmpl", "source_path": "pair-agent/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c", "blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
"normalized_render_sha256": "8557ca390d0b6548f956d2c0e9316f1cf137689d4dc17d40a4525d19f22bc457", "normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
"target": "skills/qa/references/legacy/pair-agent.md", "target": "skills/qa/references/legacy/pair-agent.md",
"overlays": [ "overlays": [
679 679
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "qa-only/SKILL.md.tmpl", "source_path": "qa-only/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e", "blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3", "normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
"target": "skills/qa/references/legacy/qa-only.md", "target": "skills/qa/references/legacy/qa-only.md",
"overlays": [ "overlays": [
679, 679,
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "qa/SKILL.md.tmpl", "source_path": "qa/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8", "blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55", "normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
"target": "skills/qa/references/legacy/qa.md", "target": "skills/qa/references/legacy/qa.md",
"overlays": [ "overlays": [
679, 679,
@@ -9,7 +9,7 @@
"source_path": "setup-browser-cookies/SKILL.md.tmpl", "source_path": "setup-browser-cookies/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652", "blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
"normalized_render_sha256": "04c161a58c1a9010efe38095b383b0e1d445a2b678e5bf931a1281d45196940d", "normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
"target": "skills/qa/references/legacy/setup-browser-cookies.md", "target": "skills/qa/references/legacy/setup-browser-cookies.md",
"overlays": [ "overlays": [
679 679
+16 -16
View File
@@ -83,7 +83,7 @@
"source_path": "office-hours/SKILL.md.tmpl", "source_path": "office-hours/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f", "blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052", "normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
"target": "skills/plan/references/legacy/office-hours.md", "target": "skills/plan/references/legacy/office-hours.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -1416,7 +1416,7 @@
"source_path": "design-consultation/SKILL.md.tmpl", "source_path": "design-consultation/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811", "blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b", "normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
"target": "skills/design/references/legacy/design-consultation.md", "target": "skills/design/references/legacy/design-consultation.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -1591,7 +1591,7 @@
"source_path": "design-html/SKILL.md.tmpl", "source_path": "design-html/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca", "blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30", "normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
"target": "skills/design/references/legacy/design-html.md", "target": "skills/design/references/legacy/design-html.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -1820,7 +1820,7 @@
"source_path": "design-review/SKILL.md.tmpl", "source_path": "design-review/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41", "blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b", "normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
"target": "skills/design/references/legacy/design-review.md", "target": "skills/design/references/legacy/design-review.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2162,7 +2162,7 @@
"source_path": "qa/SKILL.md.tmpl", "source_path": "qa/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8", "blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55", "normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
"target": "skills/qa/references/legacy/qa.md", "target": "skills/qa/references/legacy/qa.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2288,7 +2288,7 @@
"source_path": "qa-only/SKILL.md.tmpl", "source_path": "qa-only/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e", "blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3", "normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
"target": "skills/qa/references/legacy/qa-only.md", "target": "skills/qa/references/legacy/qa-only.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2466,7 +2466,7 @@
"source_path": "devex-review/SKILL.md.tmpl", "source_path": "devex-review/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551", "blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1", "normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
"target": "skills/qa/references/legacy/devex-review.md", "target": "skills/qa/references/legacy/devex-review.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2565,7 +2565,7 @@
"source_path": "benchmark/SKILL.md.tmpl", "source_path": "benchmark/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e", "blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d", "normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
"target": "skills/qa/references/legacy/benchmark.md", "target": "skills/qa/references/legacy/benchmark.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2631,7 +2631,7 @@
"source_path": "canary/SKILL.md.tmpl", "source_path": "canary/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c", "blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef", "normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
"target": "skills/qa/references/legacy/canary.md", "target": "skills/qa/references/legacy/canary.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2710,7 +2710,7 @@
"source_path": "browse/SKILL.md.tmpl", "source_path": "browse/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93", "blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b", "normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
"target": "skills/qa/references/legacy/browse.md", "target": "skills/qa/references/legacy/browse.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2798,7 +2798,7 @@
"source_path": "open-gstack-browser/SKILL.md.tmpl", "source_path": "open-gstack-browser/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b", "blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
"normalized_render_sha256": "df626d71b8cea4a02d2fb7aef3169563dd132bf17a9d6d84f287894cad84d2cf", "normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
"target": "skills/qa/references/legacy/open-gstack-browser.md", "target": "skills/qa/references/legacy/open-gstack-browser.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2873,7 +2873,7 @@
"source_path": "setup-browser-cookies/SKILL.md.tmpl", "source_path": "setup-browser-cookies/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652", "blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
"normalized_render_sha256": "04c161a58c1a9010efe38095b383b0e1d445a2b678e5bf931a1281d45196940d", "normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
"target": "skills/qa/references/legacy/setup-browser-cookies.md", "target": "skills/qa/references/legacy/setup-browser-cookies.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -2934,7 +2934,7 @@
"source_path": "pair-agent/SKILL.md.tmpl", "source_path": "pair-agent/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c", "blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
"normalized_render_sha256": "8557ca390d0b6548f956d2c0e9316f1cf137689d4dc17d40a4525d19f22bc457", "normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
"target": "skills/qa/references/legacy/pair-agent.md", "target": "skills/qa/references/legacy/pair-agent.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -4136,7 +4136,7 @@
"source_path": "land-and-deploy/SKILL.md.tmpl", "source_path": "land-and-deploy/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2", "blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd", "normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
"target": "skills/ship/references/legacy/land-and-deploy.md", "target": "skills/ship/references/legacy/land-and-deploy.md",
"disposition": "BUG_FIX", "disposition": "BUG_FIX",
"overlays": [ "overlays": [
@@ -4552,7 +4552,7 @@
"source_path": "gstack-upgrade/SKILL.md.tmpl", "source_path": "gstack-upgrade/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d", "blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d",
"normalized_render_sha256": "a913cf77f76c4d68c576a06190b498e3d8d3b60b85498a68f173b23a7f800828", "normalized_render_sha256": "2c8cf9e505b7da27730bf646a33ee38e8092259f3335a524c71e57ba5b602541",
"target": "skills/ship/references/legacy/gstack-upgrade.md", "target": "skills/ship/references/legacy/gstack-upgrade.md",
"disposition": "DUPLICATE_INFRASTRUCTURE", "disposition": "DUPLICATE_INFRASTRUCTURE",
"overlays": [ "overlays": [
@@ -5775,7 +5775,7 @@
"owner_tree": "qa", "owner_tree": "qa",
"consumer_tree": "ship", "consumer_tree": "ship",
"target": "skills/ship/references/legacy/canary.md", "target": "skills/ship/references/legacy/canary.md",
"sha256": "9fcf4fdea7d52113c8f5b5cc81c1fb36df591cf9e07bcc38c4ee106b15245997", "sha256": "155174e829ef7895707ff1fad1f130fc1c7150bad9fb3598428a2efc939c5ad6",
"disposition": "SHARED_MODULE" "disposition": "SHARED_MODULE"
}, },
{ {
@@ -57,18 +57,18 @@
} }
}, },
"mechanical_port": { "mechanical_port": {
"rendered_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b", "rendered_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
"differs_from_baseline": true, "differs_from_baseline": true,
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references." "allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
}, },
"candidate": { "candidate": {
"target_path": "skills/design/references/legacy/design-consultation.md", "target_path": "skills/design/references/legacy/design-consultation.md",
"rendered_legacy_body_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b", "rendered_legacy_body_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
"semantic_signature": { "semantic_signature": {
"normalized_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b", "normalized_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
"headings_sha256": "78cdfd5aa0c0264964542d45190c919c7adb8c01ea68a22d67dd0fad538cc643", "headings_sha256": "78cdfd5aa0c0264964542d45190c919c7adb8c01ea68a22d67dd0fad538cc643",
"questions_sha256": "91adedef9aa8000a9aa3385381227d149dbc52cbf2d127656bf0697510b62906", "questions_sha256": "91adedef9aa8000a9aa3385381227d149dbc52cbf2d127656bf0697510b62906",
"obligations_sha256": "c36974d06c0f66d0a0f2ea3d27282ed785c7cc956cce40a8c8db35492680c972", "obligations_sha256": "fa0722c312610cfe74c70b1e29f447622b360cb894f3c697b3ca959c3a068ddb",
"heading_count": 12, "heading_count": 12,
"question_count": 2, "question_count": 2,
"obligation_count": 18 "obligation_count": 18
@@ -57,18 +57,18 @@
} }
}, },
"mechanical_port": { "mechanical_port": {
"rendered_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b", "rendered_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
"differs_from_baseline": true, "differs_from_baseline": true,
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references." "allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
}, },
"candidate": { "candidate": {
"target_path": "skills/design/references/legacy/design-review.md", "target_path": "skills/design/references/legacy/design-review.md",
"rendered_legacy_body_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b", "rendered_legacy_body_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
"semantic_signature": { "semantic_signature": {
"normalized_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b", "normalized_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
"headings_sha256": "739abb73da8446c6133835f56ea0c862a7c1fd57e6702a6afe1ed1fd2fb2b2c8", "headings_sha256": "739abb73da8446c6133835f56ea0c862a7c1fd57e6702a6afe1ed1fd2fb2b2c8",
"questions_sha256": "57579e50936d62734df12299638bdcf8ce412f24b68688b071be156312aafcbc", "questions_sha256": "57579e50936d62734df12299638bdcf8ce412f24b68688b071be156312aafcbc",
"obligations_sha256": "e4fe261ecc3956834dfac15cac60fc4b8057ad0c3b37ede06ee4667d61852655", "obligations_sha256": "3e3b3b8a8e43c0120d60f5517cc03aa64bb31bf50adbe418ba89a7ae63557a55",
"heading_count": 62, "heading_count": 62,
"question_count": 24, "question_count": 24,
"obligation_count": 63 "obligation_count": 63
@@ -58,18 +58,18 @@
} }
}, },
"mechanical_port": { "mechanical_port": {
"rendered_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052", "rendered_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
"differs_from_baseline": true, "differs_from_baseline": true,
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references." "allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
}, },
"candidate": { "candidate": {
"target_path": "skills/plan/references/legacy/office-hours.md", "target_path": "skills/plan/references/legacy/office-hours.md",
"rendered_legacy_body_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052", "rendered_legacy_body_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
"semantic_signature": { "semantic_signature": {
"normalized_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052", "normalized_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
"headings_sha256": "f50aa767e26cdfc3c8fa4c6bdcabd10e061ca0ab59e2aff4c204eaf68bfd1d56", "headings_sha256": "f50aa767e26cdfc3c8fa4c6bdcabd10e061ca0ab59e2aff4c204eaf68bfd1d56",
"questions_sha256": "92d854d47b2f92f30cc34a59cfb6f3e18449beddcb8ebd5490162545bc8384c7", "questions_sha256": "92d854d47b2f92f30cc34a59cfb6f3e18449beddcb8ebd5490162545bc8384c7",
"obligations_sha256": "c6f9f220f431b50b7721ac9baa3ae3d7be5de88b489c80a45350028c96a9db84", "obligations_sha256": "a8abcb960325c146cb555a71821d702b593bb7dc4f12646d4ecaea11a9cf6b68",
"heading_count": 34, "heading_count": 34,
"question_count": 13, "question_count": 13,
"obligation_count": 43 "obligation_count": 43
@@ -57,18 +57,18 @@
} }
}, },
"mechanical_port": { "mechanical_port": {
"rendered_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55", "rendered_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
"differs_from_baseline": true, "differs_from_baseline": true,
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references." "allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
}, },
"candidate": { "candidate": {
"target_path": "skills/qa/references/legacy/qa.md", "target_path": "skills/qa/references/legacy/qa.md",
"rendered_legacy_body_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55", "rendered_legacy_body_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
"semantic_signature": { "semantic_signature": {
"normalized_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55", "normalized_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
"headings_sha256": "dd0b56f79cee31c4c3c71f32fb0a4438f686c6ae517625a7d59475deecd4c75e", "headings_sha256": "dd0b56f79cee31c4c3c71f32fb0a4438f686c6ae517625a7d59475deecd4c75e",
"questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1", "questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1",
"obligations_sha256": "c9a3696553c10b1338568d728f86f47c9a7a06b2defa37bd557c662d4a9086d8", "obligations_sha256": "8c9e044c4224e4c35838d6ae9241405a5df42c2372c2ce272e0a11f37ab68c86",
"heading_count": 58, "heading_count": 58,
"question_count": 6, "question_count": 6,
"obligation_count": 65 "obligation_count": 65
@@ -57,18 +57,18 @@
} }
}, },
"mechanical_port": { "mechanical_port": {
"rendered_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3", "rendered_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
"differs_from_baseline": true, "differs_from_baseline": true,
"allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references." "allowed_difference": "Canonical GStack 2 carve: exclude the retired shared onboarding wrapper and host hook advisory; resolve retired invocations to six public routes; relocate host/runtime paths; lazy-load pinned carved sections from package-local references."
}, },
"candidate": { "candidate": {
"target_path": "skills/qa/references/legacy/qa-only.md", "target_path": "skills/qa/references/legacy/qa-only.md",
"rendered_legacy_body_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3", "rendered_legacy_body_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
"semantic_signature": { "semantic_signature": {
"normalized_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3", "normalized_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
"headings_sha256": "0241f7efa9ffcaef764bd6517099b03394e09f90c8d1f3f015c7c7a135f78201", "headings_sha256": "0241f7efa9ffcaef764bd6517099b03394e09f90c8d1f3f015c7c7a135f78201",
"questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1", "questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1",
"obligations_sha256": "9ea973707574aa3750e38a2920e65072f804fe7cddf789f1710e08b0a44d92d3", "obligations_sha256": "f16cce08f386155cae52758fb3f7235e1c8090ca7d84117caa4761b76927d9d5",
"heading_count": 34, "heading_count": 34,
"question_count": 6, "question_count": 6,
"obligation_count": 40 "obligation_count": 40
+1 -1
View File
@@ -75,7 +75,7 @@
"diff": "^9.0.0", "diff": "^9.0.0",
"html-to-docx": "1.8.0", "html-to-docx": "1.8.0",
"marked": "^18.0.6", "marked": "^18.0.6",
"playwright": "^1.58.2", "playwright": "npm:playwright-core@^1.58.2",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"socks": "^2.8.9", "socks": "^2.8.9",
"xterm": "5", "xterm": "5",
+154
View File
@@ -0,0 +1,154 @@
import { constants as fsConstants } from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
export const BROWSER_PROVIDERS = Object.freeze(["managed", "installed"]);
const BROWSER_CAPABILITIES = new Set(["browser", "browser-visible", "diagram", "pdf"]);
const NAMED_CANDIDATES = Object.freeze({
darwin: Object.freeze([
["Google Chrome", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
["Google Chrome Beta", "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta"],
["Chromium", "/Applications/Chromium.app/Contents/MacOS/Chromium"],
["Microsoft Edge", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"],
["Brave", "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"],
]),
win32: Object.freeze([
["Google Chrome", ["LOCALAPPDATA", "Google/Chrome/Application/chrome.exe"]],
["Google Chrome", ["PROGRAMFILES", "Google/Chrome/Application/chrome.exe"]],
["Google Chrome", ["PROGRAMFILES(X86)", "Google/Chrome/Application/chrome.exe"]],
["Microsoft Edge", ["PROGRAMFILES(X86)", "Microsoft/Edge/Application/msedge.exe"]],
["Microsoft Edge", ["PROGRAMFILES", "Microsoft/Edge/Application/msedge.exe"]],
["Brave", ["LOCALAPPDATA", "BraveSoftware/Brave-Browser/Application/brave.exe"]],
]),
});
const PATH_CANDIDATES = Object.freeze([
["Google Chrome", "google-chrome"],
["Google Chrome", "google-chrome-stable"],
["Chromium", "chromium"],
["Chromium", "chromium-browser"],
["Microsoft Edge", "microsoft-edge"],
["Microsoft Edge", "microsoft-edge-stable"],
["Brave", "brave-browser"],
]);
export function browserChoiceRequired(capabilities) {
return capabilities.some((capability) => BROWSER_CAPABILITIES.has(capability));
}
export function assertBrowserChoiceSupportsCapabilities(choice, capabilities) {
if (choice?.provider === "installed" && capabilities.includes("browser-visible")) {
throw browserChoiceError(
"Visible GStack Browser requires managed Chromium because installed Chrome-family builds can block automation extension loading; choose `managed` for this capability",
"BROWSER_PROVIDER_UNSUPPORTED",
);
}
return choice;
}
export function applyBrowserProviderToComponents(components, choice) {
if (choice?.provider !== "installed") return Object.freeze([...components].sort());
return Object.freeze(components
.filter((component) => component !== "browser-headless" && component !== "browser-visible")
.sort());
}
export async function detectInstalledBrowsers(options = {}) {
if (Array.isArray(options.candidates)) {
const resolved = [];
for (const candidate of options.candidates) {
const browser = await inspectCandidate(candidate.name, candidate.executablePath, options);
if (browser) resolved.push(browser);
}
return deduplicate(resolved);
}
const platform = options.platform ?? process.platform;
const env = options.env ?? process.env;
const homeDir = options.homeDir ?? os.homedir();
const candidates = [];
if (platform === "darwin") {
for (const [name, executablePath] of NAMED_CANDIDATES.darwin) {
candidates.push({ name, executablePath });
candidates.push({
name,
executablePath: path.join(homeDir, executablePath.replace(/^\/Applications\//, "Applications/")),
});
}
} else if (platform === "win32") {
for (const [name, [variable, suffix]] of NAMED_CANDIDATES.win32) {
const base = env[variable];
if (base) candidates.push({ name, executablePath: path.join(base, ...suffix.split("/")) });
}
} else if (platform === "linux") {
for (const [name, command] of PATH_CANDIDATES) {
for (const directory of String(env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
candidates.push({ name, executablePath: path.join(directory, command) });
}
}
}
const resolved = [];
for (const candidate of candidates) {
const browser = await inspectCandidate(candidate.name, candidate.executablePath, options);
if (browser) resolved.push(browser);
}
return deduplicate(resolved);
}
export async function resolveBrowserChoice(choice, options = {}) {
if (!choice || !BROWSER_PROVIDERS.includes(choice.provider)) {
throw browserChoiceError(
"Choose a browser provider: `managed` downloads GStack's isolated Chromium, while `installed` uses an explicitly selected local Chromium executable",
"BROWSER_CHOICE_REQUIRED",
);
}
if (choice.provider === "managed") {
if (choice.executablePath != null) {
throw browserChoiceError("Managed Chromium cannot include an installed-browser path", "BROWSER_CHOICE_INVALID");
}
return Object.freeze({ provider: "managed", executablePath: null });
}
if (typeof choice.executablePath !== "string" || !path.isAbsolute(choice.executablePath)) {
throw browserChoiceError("Installed browser setup requires an absolute executable path", "BROWSER_PATH_REQUIRED");
}
const inspected = await inspectCandidate(choice.name ?? "Installed Chromium", choice.executablePath, options);
if (!inspected) {
throw browserChoiceError(`Installed browser executable is unavailable or not executable: ${choice.executablePath}`, "BROWSER_PATH_INVALID");
}
return Object.freeze({ provider: "installed", executablePath: inspected.executablePath });
}
async function inspectCandidate(name, executablePath, options) {
if (typeof executablePath !== "string" || !path.isAbsolute(executablePath)) return null;
const fs_ = options.fs ?? fs;
try {
const invocationPath = path.resolve(executablePath);
const physical = await fs_.realpath(invocationPath);
const stat = await fs_.lstat(physical);
if (!stat.isFile() || stat.isSymbolicLink()) return null;
if ((options.platform ?? process.platform) !== "win32") await fs_.access(physical, fsConstants.X_OK);
return Object.freeze({ name, executablePath: invocationPath, physicalPath: physical });
} catch {
return null;
}
}
function deduplicate(candidates) {
const seen = new Set();
return Object.freeze(candidates.flatMap((candidate) => {
const identity = candidate.physicalPath ?? candidate.executablePath;
if (seen.has(identity)) return [];
seen.add(identity);
return [Object.freeze({ name: candidate.name, executablePath: candidate.executablePath })];
}));
}
function browserChoiceError(message, code) {
const error = new Error(message);
error.code = code;
return error;
}
+97 -2
View File
@@ -9,10 +9,12 @@ import { setupRuntime } from "./setup.js";
import { import {
configGet, configGet,
configSet, configSet,
configSetBrowserChoice,
configSetNetworkChoice, configSetNetworkChoice,
parseConfigValue, parseConfigValue,
secretSet, secretSet,
} from "./config.js"; } from "./config.js";
import { resolveBrowserChoice } from "./browser-choice.mjs";
import { discoverProjectIdentity } from "./identity.js"; import { discoverProjectIdentity } from "./identity.js";
import { import {
beginRun, beginRun,
@@ -229,12 +231,88 @@ async function configCommand({ args, home, cwd, stdout }) {
if (action === "set") { if (action === "set") {
const [key, value, ...rest] = tail; const [key, value, ...rest] = tail;
if (!key || value === undefined || rest.length) throw cliError("Usage: gstack config set <key> <value>", "USAGE"); if (!key || value === undefined || rest.length) throw cliError("Usage: gstack config set <key> <value>", "USAGE");
if (key === "browser" || key.startsWith("browser.")) {
throw cliError(
"Browser selection is coherent state; use `gstack config browser managed`, `gstack config browser installed <absolute-path>`, or `gstack config browser clear`.",
"CONFIG_BROWSER_COMMAND_REQUIRED",
);
}
await setupRuntime({ home, cwd }); await setupRuntime({ home, cwd });
const result = await withOwnedRuntimeMutation(home, () => configSet(home, key, parseConfigValue(value))); const result = await withOwnedRuntimeMutation(home, () => configSet(home, key, parseConfigValue(value)));
write(stdout, `${key} = ${typeof result === "string" ? result : JSON.stringify(result)}\n`); write(stdout, `${key} = ${typeof result === "string" ? result : JSON.stringify(result)}\n`);
return 0; return 0;
} }
throw cliError("Usage: gstack config get [key] | gstack config set <key> <value>", "USAGE"); if (action === "browser") {
const [provider, executablePath, ...rest] = tail;
if (rest.length || !["managed", "installed", "clear"].includes(provider) ||
(provider === "installed" ? !executablePath : executablePath != null)) {
throw cliError("Usage: gstack config browser managed | installed <absolute-executable-path> | clear", "USAGE");
}
await setupRuntime({ home, cwd });
const choice = provider === "clear"
? null
: await resolveBrowserChoice({ provider, executablePath });
await assertBrowserChoiceCompatibleWithActiveRuntime(home, choice);
const result = await withOwnedRuntimeMutation(home, () => configSetBrowserChoice(home, choice));
write(stdout, provider === "clear"
? "browser selection cleared\n"
: `browser = ${JSON.stringify(result)}\n`);
return 0;
}
throw cliError("Usage: gstack config get [key] | gstack config set <key> <value> | gstack config browser managed | installed <path> | clear", "USAGE");
}
async function activeRuntimeBrowserChoice(home) {
const paths = resolveRuntimePaths({ home });
const pointer = await readJson(paths.versionPointer, null);
if (typeof pointer?.current !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(pointer.current)) return null;
return runtimeBrowserChoiceAtPath(path.join(paths.versions, pointer.current));
}
async function runtimeBrowserChoiceAtPath(runtimePath) {
const manifest = await readJson(path.join(runtimePath, ".gstack-bundle.json"), null);
if (!manifest || typeof manifest !== "object") return null;
const selected = Array.isArray(manifest.selectedCapabilities) ? manifest.selectedCapabilities : [];
const components = Array.isArray(manifest.runtimeComponents) ? manifest.runtimeComponents : [];
if (!selected.includes("browser") && !selected.includes("browser-visible")) return null;
const explicit = manifest.browserChoice;
const provider = explicit?.provider ?? (
components.includes("browser-headless") || components.includes("browser-visible")
? "managed"
: components.includes("browser-code")
? "installed"
: null
);
return provider ? {
provider,
executablePath: provider === "installed" ? explicit?.executablePath ?? null : null,
visible: selected.includes("browser-visible"),
} : null;
}
async function assertBrowserChoiceCompatibleWithActiveRuntime(home, choice) {
if (!choice) return;
const active = await activeRuntimeBrowserChoice(home);
if (!active) return;
if (choice.provider !== active.provider) {
throw cliError(
`The active runtime was installed for ${active.provider} Chromium. Use the signed capability bootstrap to install a ${choice.provider} browser slot before switching providers.`,
"BROWSER_PROVIDER_SLOT_MISMATCH",
);
}
if (choice.provider === "installed" && active.visible) {
throw cliError("Visible GStack Browser is managed-only; install a managed browser slot before selecting it", "BROWSER_PROVIDER_UNSUPPORTED");
}
}
async function resolvedBrowserChoiceForRuntimePath(runtimePath) {
const choice = await runtimeBrowserChoiceAtPath(runtimePath);
if (!choice) return null;
if (choice.provider === "managed") return { provider: "managed", executablePath: null };
if (typeof choice.executablePath !== "string") {
throw cliError("The rollback slot does not record its installed browser executable", "BROWSER_PATH_REQUIRED");
}
return resolveBrowserChoice({ provider: "installed", executablePath: choice.executablePath });
} }
async function stateCommand({ args, home, cwd, env, stdout, stderr }) { async function stateCommand({ args, home, cwd, env, stdout, stderr }) {
@@ -665,7 +743,11 @@ async function upgradeCommand({ args, home, stdout, installOptions = {} }) {
if (parsed.positionals.length) throw cliError("Upgrade accepts only named options", "USAGE"); if (parsed.positionals.length) throw cliError("Upgrade accepts only named options", "USAGE");
if (parsed.flags.has("--rollback")) { if (parsed.flags.has("--rollback")) {
if (parsed.values.has("--source") || parsed.values.has("--version")) throw cliError("--rollback cannot be combined with staging options", "USAGE"); if (parsed.values.has("--source") || parsed.values.has("--version")) throw cliError("--rollback cannot be combined with staging options", "USAGE");
const pointer = await rollbackUpgrade(home); const pointer = await rollbackUpgrade(home, {
prepareActivation: async (fallbackPath) => ({
browserChoice: await resolvedBrowserChoiceForRuntimePath(fallbackPath),
}),
});
write(stdout, parsed.flags.has("--json") ? `${JSON.stringify(pointer, null, 2)}\n` : `Rolled back to ${pointer.current}\n`); write(stdout, parsed.flags.has("--json") ? `${JSON.stringify(pointer, null, 2)}\n` : `Rolled back to ${pointer.current}\n`);
return 0; return 0;
} }
@@ -674,10 +756,22 @@ async function upgradeCommand({ args, home, stdout, installOptions = {} }) {
if (!sourceDir || !version) { if (!sourceDir || !version) {
throw cliError("Usage: gstack upgrade --source <complete-gstack-package> --version <version> | --rollback", "USAGE"); throw cliError("Usage: gstack upgrade --source <complete-gstack-package> --version <version> | --rollback", "USAGE");
} }
let browserChoice;
if (installOptions.entries == null) {
const configuredBrowser = await configGet(home, "browser");
if (!configuredBrowser?.provider) {
throw cliError(
"Upgrade needs the browser choice that setup normally records. Run `gstack config browser managed` or `gstack config browser installed <absolute-path>` first.",
"BROWSER_CHOICE_REQUIRED",
);
}
browserChoice = await resolveBrowserChoice(configuredBrowser);
}
const result = await installManagedRuntime({ const result = await installManagedRuntime({
home, home,
sourceDir, sourceDir,
version, version,
...(browserChoice ? { browserChoice } : {}),
...installOptions, ...installOptions,
buildMissing: false, buildMissing: false,
rejectSourceRootLink: true, rejectSourceRootLink: true,
@@ -828,6 +922,7 @@ function usage() {
" gstack runtime path <bundle-relative-path>\n" + " gstack runtime path <bundle-relative-path>\n" +
" gstack config get [key]\n" + " gstack config get [key]\n" +
" gstack config set <key> <value>\n" + " gstack config set <key> <value>\n" +
" gstack config browser managed|installed <absolute-path>|clear\n" +
" gstack state inspect [run-id]\n" + " gstack state inspect [run-id]\n" +
" gstack state begin <workflow> [--run-id <id>] [--goal <goal>] [--plan <pointer>] [--stage <stage>] [--depth quick|standard|deep] [--mutation <authority>] [--modules <a,b>]\n" + " gstack state begin <workflow> [--run-id <id>] [--goal <goal>] [--plan <pointer>] [--stage <stage>] [--depth quick|standard|deep] [--mutation <authority>] [--modules <a,b>]\n" +
" gstack state update <run-id> [--plan <pointer>|--clear-plan] [--stage <stage>] [--depth quick|standard|deep] [--mutation <authority>] [--modules <a,b>] [--push-detour <goal>|--pop-detour]\n" + " gstack state update <run-id> [--plan <pointer>|--clear-plan] [--stage <stage>] [--depth quick|standard|deep] [--mutation <authority>] [--modules <a,b>] [--push-detour <goal>|--pop-detour]\n" +
+40
View File
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { atomicWriteJson, readJson, withLock } from "./storage.js"; import { atomicWriteJson, readJson, withLock } from "./storage.js";
import { resolveRuntimePaths } from "./paths.js"; import { resolveRuntimePaths } from "./paths.js";
import { BROWSER_PROVIDERS } from "./browser-choice.mjs";
export const DEFAULT_CONFIG = Object.freeze({ export const DEFAULT_CONFIG = Object.freeze({
schemaVersion: 2, schemaVersion: 2,
@@ -10,6 +11,7 @@ export const DEFAULT_CONFIG = Object.freeze({
baseUrl: "https://api.context.dev/v1", baseUrl: "https://api.context.dev/v1",
validation: Object.freeze({ status: "unverified", checkedAt: null }), validation: Object.freeze({ status: "unverified", checkedAt: null }),
}), }),
browser: Object.freeze({ provider: null, executablePath: null }),
cleanup: Object.freeze({ retentionDays: 30 }), cleanup: Object.freeze({ retentionDays: 30 }),
}); });
@@ -125,6 +127,18 @@ export async function configSetNetworkChoice(home, choice) {
}); });
} }
/** Persist one coherent browser-engine choice or clear it atomically. */
export async function configSetBrowserChoice(home, choice) {
const normalized = choice == null
? { provider: null, executablePath: null }
: { provider: choice.provider, executablePath: choice.executablePath ?? null };
validateBrowserChoice(normalized);
return updateConfig(home, (config) => {
config.browser = normalized;
return { ...config.browser };
});
}
async function updateConfig(home, mutate) { async function updateConfig(home, mutate) {
const paths = resolveRuntimePaths({ home }); const paths = resolveRuntimePaths({ home });
return withLock(path.join(paths.locks, "config.lock"), async () => { return withLock(path.join(paths.locks, "config.lock"), async () => {
@@ -211,6 +225,31 @@ function validateConfig(config) {
throw new TypeError("context.validation.checkedAt must be an ISO timestamp or null"); throw new TypeError("context.validation.checkedAt must be an ISO timestamp or null");
} }
} }
validateBrowserChoice(config.browser ?? { provider: null, executablePath: null });
}
function validateBrowserChoice(browser) {
if (browser == null || typeof browser !== "object" || Array.isArray(browser)) {
throw new TypeError("browser must be an object");
}
const keys = Object.keys(browser).sort();
if (keys.join(",") !== "executablePath,provider") {
throw new TypeError("browser requires exactly provider and executablePath");
}
if (browser.provider == null) {
if (browser.executablePath != null) throw new TypeError("An unselected browser cannot have an executable path");
return;
}
if (!BROWSER_PROVIDERS.includes(browser.provider)) {
throw new TypeError("browser.provider must be `managed`, `installed`, or null");
}
if (browser.provider === "managed" && browser.executablePath != null) {
throw new TypeError("Managed Chromium cannot have an installed executable path");
}
if (browser.provider === "installed" &&
(typeof browser.executablePath !== "string" || !path.isAbsolute(browser.executablePath))) {
throw new TypeError("An installed browser requires an absolute executable path");
}
} }
function cloneDefaultConfig() { function cloneDefaultConfig() {
@@ -223,6 +262,7 @@ function mergeDefaults(stored) {
...stored, ...stored,
network: { ...DEFAULT_CONFIG.network, ...(stored.network ?? {}) }, network: { ...DEFAULT_CONFIG.network, ...(stored.network ?? {}) },
context: { ...DEFAULT_CONFIG.context, ...(stored.context ?? {}) }, context: { ...DEFAULT_CONFIG.context, ...(stored.context ?? {}) },
browser: { ...DEFAULT_CONFIG.browser, ...(stored.browser ?? {}) },
cleanup: { ...DEFAULT_CONFIG.cleanup, ...(stored.cleanup ?? {}) }, cleanup: { ...DEFAULT_CONFIG.cleanup, ...(stored.cleanup ?? {}) },
}; };
} }
+71 -11
View File
@@ -10,6 +10,7 @@ import { RUNTIME_SCHEMA_VERSION, RUNTIME_MIGRATION_ID } from "./migrations.js";
import { assertManagedHome } from "./managed-home.js"; import { assertManagedHome } from "./managed-home.js";
import { recoverPendingUpgrade } from "./upgrade.js"; import { recoverPendingUpgrade } from "./upgrade.js";
import { bashCandidates } from "./tooling.js"; import { bashCandidates } from "./tooling.js";
import { resolveBrowserChoice } from "./browser-choice.mjs";
import { import {
OPTIONAL_RUNTIME_CAPABILITIES, OPTIONAL_RUNTIME_CAPABILITIES,
RUNTIME_CAPABILITY_DEPENDENCIES, RUNTIME_CAPABILITY_DEPENDENCIES,
@@ -31,6 +32,7 @@ export async function runDoctor(options = {}) {
const add = (id, status, message, details) => checks.push({ id, status, message, ...(details ? { details } : {}) }); const add = (id, status, message, details) => checks.push({ id, status, message, ...(details ? { details } : {}) });
const now = options.now ? options.now() : new Date(); const now = options.now ? options.now() : new Date();
const expectedSkillApi = options.expectedSkillApi ?? RUNTIME_COMPATIBILITY.skillApi; const expectedSkillApi = options.expectedSkillApi ?? RUNTIME_COMPATIBILITY.skillApi;
let runtimeConfig = null;
if (typeof expectedSkillApi !== "string" || !/^[0-9A-Za-z][0-9A-Za-z._-]{0,31}$/.test(expectedSkillApi)) { if (typeof expectedSkillApi !== "string" || !/^[0-9A-Za-z][0-9A-Za-z._-]{0,31}$/.test(expectedSkillApi)) {
throw new TypeError("Expected skill API must be a short version identifier"); throw new TypeError("Expected skill API must be a short version identifier");
} }
@@ -59,12 +61,16 @@ export async function runDoctor(options = {}) {
} }
try { try {
const config = await readJson(paths.config); runtimeConfig = await readJson(paths.config);
add("config", config?.schemaVersion <= RUNTIME_SCHEMA_VERSION ? "pass" : "fail", add("config", runtimeConfig?.schemaVersion <= RUNTIME_SCHEMA_VERSION ? "pass" : "fail",
`Config schema ${config?.schemaVersion ?? "unknown"}`); `Config schema ${runtimeConfig?.schemaVersion ?? "unknown"}`);
const enabled = config?.network?.mode === "context" && config?.network?.consent === true; const enabled = runtimeConfig?.network?.mode === "context" && runtimeConfig?.network?.consent === true;
add("network", enabled ? "pass" : "warn", add("network", enabled ? "pass" : "warn",
enabled ? "Context.dev network mode has explicit consent" : "Network access is off (safe default)"); enabled ? "Context.dev network mode has explicit consent" : "Network access is off (safe default)");
const browserProvider = runtimeConfig?.browser?.provider;
add("browser-selection", browserProvider ? "pass" : "warn", browserProvider
? `Browser provider explicitly selected: ${browserProvider}`
: "No browser provider selected; browser-backed skills will ask at first use");
} catch (error) { } catch (error) {
add("config", "fail", `Config cannot be read: ${error.message}`); add("config", "fail", `Config cannot be read: ${error.message}`);
} }
@@ -145,7 +151,11 @@ export async function runDoctor(options = {}) {
add("specialist-tool:python", python.ok ? "pass" : "warn", python.message, python.details); add("specialist-tool:python", python.ok ? "pass" : "warn", python.message, python.details);
const selected = new Set(Array.isArray(manifest?.selectedCapabilities) ? manifest.selectedCapabilities : []); const selected = new Set(Array.isArray(manifest?.selectedCapabilities) ? manifest.selectedCapabilities : []);
const launchers = manifest?.capabilities ?? {}; const launchers = manifest?.capabilities ?? {};
for (const capability of OPTIONAL_RUNTIME_CAPABILITIES) { const capabilitiesToInspect = [
...OPTIONAL_RUNTIME_CAPABILITIES,
...(selected.has("browser-visible") ? ["browser-visible"] : []),
];
for (const capability of capabilitiesToInspect) {
if (!selected.has(capability)) { if (!selected.has(capability)) {
add(`capability:${capability}`, "warn", "not selected"); add(`capability:${capability}`, "warn", "not selected");
continue; continue;
@@ -160,8 +170,24 @@ export async function runDoctor(options = {}) {
add(`capability:${capability}`, "fail", "selected but required launcher metadata is missing"); add(`capability:${capability}`, "fail", "selected but required launcher metadata is missing");
continue; continue;
} }
if (capability === "browser") { if (capability === "browser" || capability === "browser-visible") {
const browser = await inspectManagedChromium(activeRoot, options.nodeCommand ?? process.env.GSTACK_NODE ?? "node"); const visible = capability === "browser-visible";
const browser = visible && runtimeConfig?.browser?.provider !== "managed"
? { ok: false, message: "visible GStack Browser requires the managed Chromium provider" }
: runtimeConfig?.browser?.provider === "installed"
? await inspectInstalledChromium(
activeRoot,
options.nodeCommand ?? process.env.GSTACK_NODE ?? "node",
runtimeConfig.browser,
options,
)
: runtimeConfig?.browser?.provider === "managed"
? await inspectManagedChromium(
activeRoot,
options.nodeCommand ?? process.env.GSTACK_NODE ?? "node",
{ visible },
)
: { ok: false, message: "browser capability is installed, but no browser provider was explicitly selected" };
add(`capability:${capability}`, browser.ok ? "pass" : "fail", browser.message, browser.details); add(`capability:${capability}`, browser.ok ? "pass" : "fail", browser.message, browser.details);
continue; continue;
} }
@@ -314,7 +340,7 @@ async function inspectPython(env) {
return { ok: false, message: "Python 3 is absent; only specialist flows that explicitly request it are unavailable" }; return { ok: false, message: "Python 3 is absent; only specialist flows that explicitly request it are unavailable" };
} }
async function inspectManagedChromium(activeRoot, nodeCommand) { async function inspectManagedChromium(activeRoot, nodeCommand, options = {}) {
const browserRoot = path.join(activeRoot, ".gstack-runtime-browsers"); const browserRoot = path.join(activeRoot, ".gstack-runtime-browsers");
const modulePath = path.join(activeRoot, "node_modules", "playwright", "index.mjs"); const modulePath = path.join(activeRoot, "node_modules", "playwright", "index.mjs");
const [browserStat, moduleStat] = await Promise.all([ const [browserStat, moduleStat] = await Promise.all([
@@ -329,16 +355,50 @@ async function inspectManagedChromium(activeRoot, nodeCommand) {
const result = await captureCommand(nodeCommand, [ const result = await captureCommand(nodeCommand, [
"--input-type=module", "--input-type=module",
"--eval", "--eval",
`const { chromium } = await import(${JSON.stringify(moduleUrl)}); const browser = await chromium.launch({ headless: true }); try { process.stdout.write(browser.version()); } finally { await browser.close(); }`, `const { chromium } = await import(${JSON.stringify(moduleUrl)}); const browser = await chromium.launch(${options.visible ? '{ headless: true, channel: "chromium" }' : "{ headless: true }"}); try { process.stdout.write(browser.version()); } finally { await browser.close(); }`,
], { env: { ...process.env, PLAYWRIGHT_BROWSERS_PATH: browserRoot } }); ], { env: { ...process.env, PLAYWRIGHT_BROWSERS_PATH: browserRoot } });
const version = result.stdout.trim(); const version = result.stdout.trim();
if (!version) return { ok: false, message: "managed Chromium launched without reporting a browser version" }; if (!version) return { ok: false, message: "managed Chromium launched without reporting a browser version" };
return { ok: true, message: `managed headless Chromium ${version} launches and exits cleanly`, details: { browserRoot, version } }; return {
ok: true,
message: `managed ${options.visible ? "visible-capable" : "headless"} Chromium ${version} launches and exits cleanly`,
details: { browserRoot, version },
};
} catch (error) { } catch (error) {
return { ok: false, message: `managed Chromium is not runnable: ${error.message}` }; return { ok: false, message: `managed Chromium is not runnable: ${error.message}` };
} }
} }
async function inspectInstalledChromium(activeRoot, nodeCommand, configured, options = {}) {
const modulePath = path.join(activeRoot, "node_modules", "playwright", "index.mjs");
const moduleStat = await fs.lstat(modulePath).catch(() => null);
if (!moduleStat?.isFile() || moduleStat.isSymbolicLink()) {
return { ok: false, message: "Playwright module for the installed-browser adapter is missing/unsafe" };
}
try {
const choice = await resolveBrowserChoice(configured, {
platform: options.platform,
env: options.env,
homeDir: options.homeDir,
});
const moduleUrl = pathToFileURL(modulePath).href;
const result = await captureCommand(nodeCommand, [
"--input-type=module",
"--eval",
`const { chromium } = await import(${JSON.stringify(moduleUrl)}); const browser = await chromium.launch({ headless: true, executablePath: ${JSON.stringify(choice.executablePath)} }); try { process.stdout.write(browser.version()); } finally { await browser.close(); }`,
]);
const version = result.stdout.trim();
if (!version) return { ok: false, message: "installed Chromium launched without reporting a browser version" };
return {
ok: true,
message: `installed Chromium ${version} launches through the Playwright adapter and exits cleanly`,
details: { provider: "installed", executablePath: choice.executablePath, version },
};
} catch (error) {
return { ok: false, message: `installed Chromium is not runnable through the Playwright adapter: ${error.message}` };
}
}
async function inspectXcrun() { async function inspectXcrun() {
if (process.platform !== "darwin") return { ok: false, message: "physical-iOS capability requires macOS" }; if (process.platform !== "darwin") return { ok: false, message: "physical-iOS capability requires macOS" };
try { try {
@@ -350,7 +410,7 @@ async function inspectXcrun() {
} }
function capabilityLaunchersReady(capability, launchers) { function capabilityLaunchersReady(capability, launchers) {
if (capability === "browser") return typeof launchers.browse === "string"; if (capability === "browser" || capability === "browser-visible") return typeof launchers.browse === "string";
if (capability === "design") return typeof launchers["gstack-design"] === "string"; if (capability === "design") return typeof launchers["gstack-design"] === "string";
if (capability === "pdf") return typeof launchers["make-pdf"] === "string"; if (capability === "pdf") return typeof launchers["make-pdf"] === "string";
if (capability === "diagram") return true; if (capability === "diagram") return true;
+216 -28
View File
@@ -20,6 +20,14 @@ import {
} from "./managed-home.js"; } from "./managed-home.js";
import { errorWithCode as installError } from "./errors.js"; import { errorWithCode as installError } from "./errors.js";
import { currentIsoTimestamp as isoNow } from "./time.js"; import { currentIsoTimestamp as isoNow } from "./time.js";
import { configSetBrowserChoice, loadConfig } from "./config.js";
import {
applyBrowserProviderToComponents,
assertBrowserChoiceSupportsCapabilities,
browserChoiceRequired,
detectInstalledBrowsers,
resolveBrowserChoice,
} from "./browser-choice.mjs";
const INSTALL_SCHEMA_VERSION = 2; const INSTALL_SCHEMA_VERSION = 2;
export const MAX_RUNTIME_BUNDLE_BYTES = 2 * 1024 * 1024 * 1024; export const MAX_RUNTIME_BUNDLE_BYTES = 2 * 1024 * 1024 * 1024;
@@ -266,7 +274,6 @@ export const DEFAULT_RUNTIME_BUNDLE = Object.freeze([
entry("browse/src"), entry("browse/src"),
entry("extension"), entry("extension"),
entry("node_modules/playwright"), entry("node_modules/playwright"),
entry("node_modules/playwright-core"),
entry(managedBunRelativePath(), "managed-bun", true), entry(managedBunRelativePath(), "managed-bun", true),
entry(".gstack-runtime-browsers", "browser"), entry(".gstack-runtime-browsers", "browser"),
entry("node_modules/diff"), entry("node_modules/diff"),
@@ -324,10 +331,11 @@ const CAPABILITY_PATH_PREFIXES = Object.freeze({
}); });
/** Resolve the audited core plus only explicitly selected optional capabilities. */ /** Resolve the audited core plus only explicitly selected optional capabilities. */
export function runtimeSurfaceForCapabilities(input = OPTIONAL_RUNTIME_CAPABILITIES) { export function runtimeSurfaceForCapabilities(input = OPTIONAL_RUNTIME_CAPABILITIES, options = {}) {
const selected = normalizeCapabilitySelection(input); const selected = normalizeCapabilitySelection(input);
const includesBrowserCode = selected.includes("browser") || selected.includes("browser-visible"); const includesBrowserCode = selected.includes("browser") || selected.includes("browser-visible");
const entries = DEFAULT_RUNTIME_BUNDLE.filter((item) => { const entries = DEFAULT_RUNTIME_BUNDLE.filter((item) => {
if (options.browserChoice?.provider === "installed" && item.path === ".gstack-runtime-browsers") return false;
const owner = capabilityForPath(item.path); const owner = capabilityForPath(item.path);
return owner == null || selected.includes(owner) || (owner === "browser" && includesBrowserCode); return owner == null || selected.includes(owner) || (owner === "browser" && includesBrowserCode);
}); });
@@ -339,7 +347,7 @@ export function runtimeSurfaceForCapabilities(input = OPTIONAL_RUNTIME_CAPABILIT
} }
/** Expand logical runtime capabilities into the signed internal components. */ /** Expand logical runtime capabilities into the signed internal components. */
export function runtimeComponentsForCapabilities(input = OPTIONAL_RUNTIME_CAPABILITIES) { export function runtimeComponentsForCapabilities(input = OPTIONAL_RUNTIME_CAPABILITIES, options = {}) {
const capabilities = normalizeCapabilitySelection(input); const capabilities = normalizeCapabilitySelection(input);
const selected = new Set(["core"]); const selected = new Set(["core"]);
for (const capability of capabilities) { for (const capability of capabilities) {
@@ -354,13 +362,16 @@ export function runtimeComponentsForCapabilities(input = OPTIONAL_RUNTIME_CAPABI
} }
} }
} }
return Object.freeze([...selected].sort()); return applyBrowserProviderToComponents([...selected], options.browserChoice);
} }
export function runtimeSlotVersion(releaseVersion, capabilityIds) { export function runtimeSlotVersion(releaseVersion, capabilityIds, options = {}) {
validateVersion(releaseVersion); validateVersion(releaseVersion);
const selected = normalizeCapabilitySelection(capabilityIds); const selected = normalizeCapabilitySelection(capabilityIds);
const digest = createHash("sha256").update(selected.join(",") || "core").digest("hex").slice(0, 12); const browserProvider = browserChoiceRequired(selected)
? options.browserChoice?.provider ?? "legacy-managed"
: "no-browser";
const digest = createHash("sha256").update(`${selected.join(",") || "core"}|${browserProvider}`).digest("hex").slice(0, 12);
const prefix = String(releaseVersion).slice(0, 60); const prefix = String(releaseVersion).slice(0, 60);
return `${prefix}-caps-${digest}`; return `${prefix}-caps-${digest}`;
} }
@@ -368,7 +379,7 @@ export function runtimeSlotVersion(releaseVersion, capabilityIds) {
export async function previewManagedRuntime(options = {}) { export async function previewManagedRuntime(options = {}) {
if (!options.sourceDir) throw installError("sourceDir is required", "INSTALL_SOURCE_REQUIRED"); if (!options.sourceDir) throw installError("sourceDir is required", "INSTALL_SOURCE_REQUIRED");
const sourceDir = await resolvePhysicalSource(options.sourceDir); const sourceDir = await resolvePhysicalSource(options.sourceDir);
const surface = runtimeSurfaceForCapabilities(options.capabilityIds); const surface = runtimeSurfaceForCapabilities(options.capabilityIds, { browserChoice: options.browserChoice });
let bytes = 0; let bytes = 0;
let files = 0; let files = 0;
const missing = []; const missing = [];
@@ -422,6 +433,7 @@ export async function previewManagedRuntime(options = {}) {
return Object.freeze({ return Object.freeze({
sourceDir, sourceDir,
capabilities: surface.selected, capabilities: surface.selected,
browser: browserChoiceRequired(surface.selected) ? options.browserChoice ?? null : null,
components: surface.entries.length, components: surface.entries.length,
files, files,
bytes, bytes,
@@ -463,7 +475,7 @@ export async function installManagedRuntime(options = {}) {
if (options.requirePackageIdentity) validatePackageIdentity(packageMetadata, version); if (options.requirePackageIdentity) validatePackageIdentity(packageMetadata, version);
const selectedSurface = options.entries == null const selectedSurface = options.entries == null
? runtimeSurfaceForCapabilities(options.capabilityIds) ? runtimeSurfaceForCapabilities(options.capabilityIds, { browserChoice: options.browserChoice })
: null; : null;
const entries = normalizeEntries(options.entries ?? selectedSurface.entries); const entries = normalizeEntries(options.entries ?? selectedSurface.entries);
const capabilities = normalizeCapabilities(options.capabilities ?? selectedSurface.capabilities, entries); const capabilities = normalizeCapabilities(options.capabilities ?? selectedSurface.capabilities, entries);
@@ -584,7 +596,15 @@ export async function installManagedRuntime(options = {}) {
version, version,
compatibility: RUNTIME_COMPATIBILITY, compatibility: RUNTIME_COMPATIBILITY,
selectedCapabilities: selectedSurface?.selected ?? null, selectedCapabilities: selectedSurface?.selected ?? null,
runtimeComponents: selectedSurface ? runtimeComponentsForCapabilities(selectedSurface.selected) : null, browserChoice: selectedSurface && browserChoiceRequired(selectedSurface.selected)
? {
provider: options.browserChoice?.provider ?? null,
executablePath: options.browserChoice?.executablePath ?? null,
}
: null,
runtimeComponents: selectedSurface
? runtimeComponentsForCapabilities(selectedSurface.selected, { browserChoice: options.browserChoice })
: null,
components: entries.map(({ path: component }) => component), components: entries.map(({ path: component }) => component),
capabilities, capabilities,
stableSourceFiles, stableSourceFiles,
@@ -620,6 +640,7 @@ export async function installManagedRuntime(options = {}) {
nodeCommand: options.nodeCommand ?? process.env.GSTACK_NODE ?? "node", nodeCommand: options.nodeCommand ?? process.env.GSTACK_NODE ?? "node",
run: options.runCommand ?? runCommand, run: options.runCommand ?? runCommand,
commandTimeoutMs: options.commandTimeoutMs, commandTimeoutMs: options.commandTimeoutMs,
browserChoice: selectedSurface ? options.browserChoice : null,
}); });
}, },
beforeActivate: async ({ active, previous, previousExists, destination }) => { beforeActivate: async ({ active, previous, previousExists, destination }) => {
@@ -632,6 +653,7 @@ export async function installManagedRuntime(options = {}) {
await removeObsoleteLaunchers(paths, snapshot, launcherSurface); await removeObsoleteLaunchers(paths, snapshot, launcherSurface);
const manifestWriter = options.manifestWriter ?? writeInstallManifest; const manifestWriter = options.manifestWriter ?? writeInstallManifest;
installManifest = await manifestWriter(paths, active, launcherSurface, options.now); installManifest = await manifestWriter(paths, active, launcherSurface, options.now);
if (options.browserChoice) await configSetBrowserChoice(home, options.browserChoice);
}, },
afterActivate: async () => fs.rm(path.join(home, RUNTIME_TRANSACTION_FILE), { force: true }), afterActivate: async () => fs.rm(path.join(home, RUNTIME_TRANSACTION_FILE), { force: true }),
onRollback: async ({ pointerRollbackError }) => { onRollback: async ({ pointerRollbackError }) => {
@@ -898,6 +920,26 @@ export async function smokeRuntimeBundle(directory, options = {}) {
cause, cause,
); );
} }
} else if (options.browserChoice?.provider === "installed") {
const playwrightFile = path.join(directory, "node_modules", "playwright", "index.mjs");
const moduleStat = await fs.lstat(playwrightFile).catch(() => null);
if (!moduleStat?.isFile() || moduleStat.isSymbolicLink()) {
throw installError("Playwright adapter for the installed browser is missing or unsafe", "INSTALL_SMOKE_FAILED");
}
const browserChoice = await resolveBrowserChoice(options.browserChoice);
try {
await run(command, [
"--input-type=module",
"--eval",
`const { chromium } = await import(${JSON.stringify(pathToFileURL(playwrightFile).href)}); const browser = await chromium.launch({ headless: true, executablePath: ${JSON.stringify(browserChoice.executablePath)} }); try { if (!browser.version()) throw new Error("browser version unavailable"); } finally { await browser.close(); }`,
], { cwd: directory, capture: true, timeoutMs: Math.max(timeoutMs, 30_000) });
} catch (cause) {
throw installError(
"The selected installed Chromium failed its Playwright launch smoke test; the active runtime and browser selection were not changed",
"INSTALL_SMOKE_FAILED",
cause,
);
}
} }
} }
@@ -916,7 +958,7 @@ export async function runInstallerCli(argv = process.argv.slice(2), options = {}
const stdout = options.stdout ?? process.stdout; const stdout = options.stdout ?? process.stdout;
const bunCommand = parsed.bunCommand ?? env.BUN_CMD ?? "bun"; const bunCommand = parsed.bunCommand ?? env.BUN_CMD ?? "bun";
let capabilityIds = parsed.capabilityIds; let capabilityIds = parsed.capabilityIds;
if (parsed.installMode == null && !parsed.dryRun && stdin.isTTY && !parsed.json) { if (!parsed.capabilitiesProvided && parsed.installMode == null && !parsed.dryRun && stdin.isTTY && !parsed.json) {
const answer = await askInstallerQuestion( const answer = await askInstallerQuestion(
stdin, stdin,
options.stderr ?? process.stderr, options.stderr ?? process.stderr,
@@ -924,10 +966,52 @@ export async function runInstallerCli(argv = process.argv.slice(2), options = {}
); );
capabilityIds = parseCapabilityList(answer || "all"); capabilityIds = parseCapabilityList(answer || "all");
} }
if (parsed.installMode === "later" && !parsed.browserProvider && browserChoiceRequired(capabilityIds)) {
if (parsed.json) {
stdout.write(`${JSON.stringify({ ok: true, action: "install-later", mutated: false, preview: null }, null, 2)}\n`);
} else if (!parsed.quiet) {
stdout.write("No browser provider was selected and no runtime was installed. Judgment-only skills remain usable.\n");
}
return 0;
}
capabilityIds = await mergeActiveCapabilities(home, capabilityIds, parsed.replaceCapabilities); capabilityIds = await mergeActiveCapabilities(home, capabilityIds, parsed.replaceCapabilities);
let browserChoice = null;
if (browserChoiceRequired(capabilityIds)) {
const configured = parsed.browserProvider
? { provider: parsed.browserProvider, executablePath: parsed.browserPath }
: (await loadConfig(home)).browser;
if (configured?.provider) {
browserChoice = await resolveBrowserChoice(configured, {
platform: options.platform,
env,
homeDir: options.homeDir,
});
} else if (stdin.isTTY && !parsed.json && !parsed.dryRun) {
browserChoice = await askBrowserChoice({
input: stdin,
output: options.stderr ?? process.stderr,
platform: options.platform,
env,
homeDir: options.homeDir,
});
if (!browserChoice) {
stdout.write("No browser provider was selected. No runtime was installed; judgment-only skills remain usable.\n");
return 0;
}
} else {
throw installError(
"Browser-backed capabilities require an explicit choice. Use `--browser managed` or `--browser installed --browser-path <absolute-executable-path>`; no browser was downloaded or selected.",
"INSTALL_BROWSER_CHOICE_REQUIRED",
);
}
assertBrowserChoiceSupportsCapabilities(browserChoice, capabilityIds);
} else if (parsed.browserProvider || parsed.browserPath) {
throw installError("Browser options require a browser-backed capability", "INSTALL_BROWSER_CHOICE_UNUSED");
}
const preview = await previewManagedRuntime({ const preview = await previewManagedRuntime({
sourceDir, sourceDir,
capabilityIds, capabilityIds,
browserChoice,
bunCommand, bunCommand,
preparedSource: parsed.prepared, preparedSource: parsed.prepared,
runCommand: options.installOptions?.runCommand, runCommand: options.installOptions?.runCommand,
@@ -969,9 +1053,10 @@ export async function runInstallerCli(argv = process.argv.slice(2), options = {}
const result = await installManagedRuntime({ const result = await installManagedRuntime({
sourceDir, sourceDir,
home, home,
version: runtimeSlotVersion(releaseVersion, capabilityIds), version: runtimeSlotVersion(releaseVersion, capabilityIds, { browserChoice }),
bunCommand, bunCommand,
capabilityIds, capabilityIds,
browserChoice,
buildMissing: parsed.prepared ? false : undefined, buildMissing: parsed.prepared ? false : undefined,
nodeCommand: env.GSTACK_NODE ?? "node", nodeCommand: env.GSTACK_NODE ?? "node",
launcherNodeCommand: env.GSTACK_NODE ?? "node", launcherNodeCommand: env.GSTACK_NODE ?? "node",
@@ -984,7 +1069,7 @@ export async function runInstallerCli(argv = process.argv.slice(2), options = {}
stdout.write(`Installed gstack runtime ${releaseVersion}\n`); stdout.write(`Installed gstack runtime ${releaseVersion}\n`);
stdout.write(`Runtime home: ${result.home}\n`); stdout.write(`Runtime home: ${result.home}\n`);
stdout.write(`Launcher directory: ${path.join(result.home, "bin")}\n`); stdout.write(`Launcher directory: ${path.join(result.home, "bin")}\n`);
stdout.write("Skills are installed separately with: npx skills add time-attack/gstack\n"); stdout.write("Skills are installed separately with: npx skills add time-attack/gstack/skills\n");
} }
return 0; return 0;
} catch (error) { } catch (error) {
@@ -1052,6 +1137,10 @@ export function runtimeReleaseComponentForPath(value) {
const relative = component.slice(browserRoot.length); const relative = component.slice(browserRoot.length);
if (relative === ".links" || relative.startsWith(".links/")) return null; if (relative === ".links" || relative.startsWith(".links/")) return null;
const top = relative.split("/")[0]; const top = relative.split("/")[0];
// Playwright downloads winldd on Windows only to validate browser DLL
// dependencies during installation. It is not required to launch Chromium
// from the completed managed runtime, so keep it out of release artifacts.
if (/^winldd-\d/.test(top)) return null;
if (top.startsWith("chromium_headless_shell-") || top.startsWith("ffmpeg-")) return "browser-headless"; if (top.startsWith("chromium_headless_shell-") || top.startsWith("ffmpeg-")) return "browser-headless";
if (/^chromium-\d/.test(top)) return "browser-visible"; if (/^chromium-\d/.test(top)) return "browser-visible";
throw installError(`Unknown managed browser payload path: ${component}`, "INSTALL_BROWSER_PAYLOAD_INVALID"); throw installError(`Unknown managed browser payload path: ${component}`, "INSTALL_BROWSER_PAYLOAD_INVALID");
@@ -1548,6 +1637,55 @@ if (!stat?.isFile() || stat.isSymbolicLink()) throw new Error("Active capability
const managedBrowsers = path.join(root, ".gstack-runtime-browsers"); const managedBrowsers = path.join(root, ".gstack-runtime-browsers");
const browserStat = await fs.lstat(managedBrowsers).catch(() => null); const browserStat = await fs.lstat(managedBrowsers).catch(() => null);
if (browserStat?.isSymbolicLink()) throw new Error("Managed browser directory is unsafe"); if (browserStat?.isSymbolicLink()) throw new Error("Managed browser directory is unsafe");
const config = await fs.readFile(path.join(home, "config.json"), "utf8")
.then(value => JSON.parse(value), () => null);
const bundle = await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8")
.then(value => JSON.parse(value), () => null);
const browserBacked = relative.startsWith("browse/") || relative.startsWith("make-pdf/");
const selectedCapabilities = Array.isArray(bundle?.selectedCapabilities) ? bundle.selectedCapabilities : [];
const runtimeComponents = Array.isArray(bundle?.runtimeComponents) ? bundle.runtimeComponents : [];
const visibleRequested = relative.startsWith("browse/") && (
args.includes("connect") ||
args.includes("handoff") ||
args.includes("--headed") ||
(args[0] === "pair-agent" && !args.includes("--headless"))
);
const slotProvider = bundle?.browserChoice?.provider ?? (
runtimeComponents.includes("browser-headless") || runtimeComponents.includes("browser-visible")
? "managed"
: runtimeComponents.includes("browser-code")
? "installed"
: null
);
let browserChoice = config?.browser ?? { provider: null, executablePath: null };
if (browserBacked) {
if (!browserChoice?.provider) {
throw new Error("No browser provider is selected; run the signed browser capability bootstrap before launching browser-backed tools");
}
if (slotProvider && browserChoice.provider !== slotProvider) {
throw new Error("The selected browser provider does not match the active runtime slot; run the signed browser capability bootstrap for the selected provider");
}
if (visibleRequested && !selectedCapabilities.includes("browser-visible")) {
if (browserChoice.provider === "installed") {
throw new Error("Visible GStack Browser requires managed Chromium; preview and approve the browser-visible capability first");
}
throw new Error("The active runtime slot does not include visible Chromium; preview and approve the browser-visible capability first");
}
if (browserChoice.provider === "installed") {
if (selectedCapabilities.includes("browser-visible")) {
throw new Error("Visible GStack Browser requires a managed Chromium runtime slot");
}
if (visibleRequested) {
throw new Error("Visible GStack Browser requires managed Chromium; preview and approve the browser-visible capability first");
}
const browserModule = await import(pathToFileURL(path.join(root, "runtime", "browser-choice.mjs")).href);
browserChoice = await browserModule.resolveBrowserChoice(browserChoice);
} else if (browserChoice.provider === "managed") {
if (!browserStat?.isDirectory()) throw new Error("Managed Chromium is missing from the active runtime slot");
} else {
throw new Error("Configured browser provider is invalid");
}
}
const managedBun = path.join(root, ${JSON.stringify(managedBunRelativePath())}); const managedBun = path.join(root, ${JSON.stringify(managedBunRelativePath())});
const bunStat = await fs.lstat(managedBun).catch(() => null); const bunStat = await fs.lstat(managedBun).catch(() => null);
const hasManagedBun = bunStat?.isFile() && !bunStat.isSymbolicLink(); const hasManagedBun = bunStat?.isFile() && !bunStat.isSymbolicLink();
@@ -1574,20 +1712,29 @@ if (/^#!.*\\bbun(?:\\s|$)/.test(header)) {
command = process.env.GSTACK_NODE || process.execPath; command = process.env.GSTACK_NODE || process.execPath;
commandArgs = [target, ...args]; commandArgs = [target, ...args];
} }
const childEnv = {
...process.env,
GSTACK_HOME: process.env.GSTACK_HOME || home,
GSTACK_NODE: process.env.GSTACK_NODE || process.execPath,
GSTACK_BASH: bashCommand,
...(hasManagedBun ? {
BUN_CMD: managedBun,
PATH: path.dirname(managedBun) + path.delimiter + (process.env.PATH || ""),
} : {}),
};
if (browserBacked) {
delete childEnv.PLAYWRIGHT_BROWSERS_PATH;
delete childEnv.GSTACK_CHROMIUM_PATH;
delete childEnv.GSTACK_BROWSER_PROVIDER;
childEnv.GSTACK_BROWSER_PROVIDER = browserChoice.provider;
if (browserChoice.provider === "installed") delete childEnv.BROWSE_EXTENSIONS_DIR;
if (browserChoice.provider === "managed") childEnv.PLAYWRIGHT_BROWSERS_PATH = managedBrowsers;
else childEnv.GSTACK_CHROMIUM_PATH = browserChoice.executablePath;
}
const child = spawn(command, commandArgs, { const child = spawn(command, commandArgs, {
stdio: "inherit", stdio: "inherit",
windowsHide: true, windowsHide: true,
env: { env: childEnv,
...process.env,
GSTACK_HOME: process.env.GSTACK_HOME || home,
GSTACK_NODE: process.env.GSTACK_NODE || process.execPath,
GSTACK_BASH: bashCommand,
...(hasManagedBun ? {
BUN_CMD: managedBun,
PATH: path.dirname(managedBun) + path.delimiter + (process.env.PATH || ""),
} : {}),
...(browserStat?.isDirectory() ? { PLAYWRIGHT_BROWSERS_PATH: managedBrowsers } : {}),
},
}); });
child.once("error", error => { console.error(error.message); process.exitCode = 1; }); child.once("error", error => { console.error(error.message); process.exitCode = 1; });
child.once("exit", (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; }); child.once("exit", (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; });
@@ -1697,7 +1844,8 @@ function processIsAlive(pid) {
} }
function validTransactionPath(value) { function validTransactionPath(value) {
if (value === "runtime-install.json" || (typeof value === "string" && /^bin\\/[A-Za-z0-9._-]+$/.test(value))) return value; if (value === "config.json" || value === "runtime-install.json" ||
(typeof value === "string" && /^bin\\/[A-Za-z0-9._-]+$/.test(value))) return value;
throw new Error("Invalid managed runtime transaction path"); throw new Error("Invalid managed runtime transaction path");
} }
@@ -1876,6 +2024,7 @@ async function captureInstallSurface(paths, launcherSurface) {
const oldManifest = await readJson(manifestPath, null); const oldManifest = await readJson(manifestPath, null);
const oldLaunchers = validateInstallManifestForUninstall(oldManifest); const oldLaunchers = validateInstallManifestForUninstall(oldManifest);
const relativePaths = new Set([ const relativePaths = new Set([
"config.json",
"runtime-install.json", "runtime-install.json",
...oldLaunchers, ...oldLaunchers,
...launcherRelativePaths(launcherSurface), ...launcherRelativePaths(launcherSurface),
@@ -2155,7 +2304,10 @@ function parseInstallerArgs(argv) {
home: null, home: null,
version: undefined, version: undefined,
bunCommand: undefined, bunCommand: undefined,
browserProvider: null,
browserPath: null,
capabilityIds: OPTIONAL_RUNTIME_CAPABILITIES, capabilityIds: OPTIONAL_RUNTIME_CAPABILITIES,
capabilitiesProvided: false,
installMode: null, installMode: null,
yes: false, yes: false,
dryRun: false, dryRun: false,
@@ -2176,20 +2328,34 @@ function parseInstallerArgs(argv) {
else if (arg === "--replace-capabilities") result.replaceCapabilities = true; else if (arg === "--replace-capabilities") result.replaceCapabilities = true;
else if (arg === "--install-now") result.installMode = "now"; else if (arg === "--install-now") result.installMode = "now";
else if (arg === "--install-later") result.installMode = "later"; else if (arg === "--install-later") result.installMode = "later";
else if (["--source", "--home", "--version", "--bun", "--capabilities"].includes(arg)) { else if (["--source", "--home", "--version", "--bun", "--capabilities", "--browser", "--browser-path"].includes(arg)) {
const value = argv[index + 1]; const value = argv[index + 1];
if (!value || value.startsWith("--")) throw new TypeError(`Missing value for ${arg}`); if (!value || value.startsWith("--")) throw new TypeError(`Missing value for ${arg}`);
index += 1; index += 1;
if (arg === "--capabilities") result.capabilityIds = parseCapabilityList(value); if (arg === "--capabilities") {
result.capabilityIds = parseCapabilityList(value);
result.capabilitiesProvided = true;
}
else if (arg === "--browser") result.browserProvider = value;
else if (arg === "--browser-path") result.browserPath = value;
else { else {
const key = { "--source": "sourceDir", "--home": "home", "--version": "version", "--bun": "bunCommand" }[arg]; const key = { "--source": "sourceDir", "--home": "home", "--version": "version", "--bun": "bunCommand" }[arg];
result[key] = value; result[key] = value;
} }
} else { } else {
throw new TypeError(`Unknown setup option: ${arg}. Skill placement is delegated to: npx skills add time-attack/gstack`); throw new TypeError(`Unknown setup option: ${arg}. Skill placement is delegated to: npx skills add time-attack/gstack/skills`);
} }
} }
if (result.installMode === "later" && result.yes) throw new TypeError("--install-later cannot be combined with --yes"); if (result.installMode === "later" && result.yes) throw new TypeError("--install-later cannot be combined with --yes");
if (result.browserProvider != null && !["managed", "installed"].includes(result.browserProvider)) {
throw new TypeError("--browser must be `managed` or `installed`");
}
if (result.browserProvider === "managed" && result.browserPath != null) {
throw new TypeError("--browser-path is valid only with `--browser installed`");
}
if (result.browserPath != null && result.browserProvider !== "installed") {
throw new TypeError("--browser-path requires `--browser installed`");
}
if (result.prepared && result.installMode !== "now") throw new TypeError("--prepared is reserved for an explicit prepared artifact install"); if (result.prepared && result.installMode !== "now") throw new TypeError("--prepared is reserved for an explicit prepared artifact install");
if (result.dryRun && (result.installMode != null || result.yes)) throw new TypeError("--dry-run cannot be combined with install/consent flags"); if (result.dryRun && (result.installMode != null || result.yes)) throw new TypeError("--dry-run cannot be combined with install/consent flags");
return result; return result;
@@ -2197,12 +2363,13 @@ function parseInstallerArgs(argv) {
function installerUsage() { function installerUsage() {
return `Usage: ./setup [--capabilities <list>] [--replace-capabilities] [--dry-run|--install-now [--yes]|--install-later]\n` + return `Usage: ./setup [--capabilities <list>] [--replace-capabilities] [--dry-run|--install-now [--yes]|--install-later]\n` +
` [--browser managed|installed [--browser-path <absolute-path>]]\n` +
` [--home <path>] [--version <version>] [--json] [--quiet]\n\n` + ` [--home <path>] [--version <version>] [--json] [--quiet]\n\n` +
`Optional capabilities: ${OPTIONAL_RUNTIME_CAPABILITIES.join(", ")}\n` + `Optional capabilities: ${OPTIONAL_RUNTIME_CAPABILITIES.join(", ")}\n` +
"Without --install-now, non-interactive use previews and installs nothing.\n" + "Without --install-now, non-interactive use previews and installs nothing.\n" +
"--dry-run and --install-later never modify the runtime, state, or host setup.\n" + "--dry-run and --install-later never modify the runtime, state, or host setup.\n" +
"Installs only the optional host-neutral runtime and selected local capabilities.\n" + "Installs only the optional host-neutral runtime and selected local capabilities.\n" +
"Install the six skills separately with: npx skills add time-attack/gstack\n"; "Install the six skills separately with: npx skills add time-attack/gstack/skills\n";
} }
function parseCapabilityList(value) { function parseCapabilityList(value) {
@@ -2221,9 +2388,30 @@ async function askInstallerQuestion(input, output, prompt) {
} }
} }
async function askBrowserChoice({ input, output, platform, env, homeDir }) {
const installed = await detectInstalledBrowsers({ platform, env, homeDir });
output.write("\nBrowser-backed skills need one explicit browser choice:\n");
output.write(" m) Managed Chromium — isolated and reproducible; its exact download is shown before install.\n");
installed.forEach((browser, index) => {
output.write(` ${index + 1}) ${browser.name}${browser.executablePath} (isolated automation profile; no browser download).\n`);
});
output.write(" l) Later — install nothing.\n");
const answer = (await askInstallerQuestion(input, output, "Select m, a browser number, or l [l]: ")).trim().toLowerCase();
if (!answer || answer === "l" || answer === "later") return null;
if (answer === "m" || answer === "managed") return resolveBrowserChoice({ provider: "managed" });
const selected = installed[Number(answer) - 1];
if (!selected) throw installError("Invalid browser selection", "INSTALL_BROWSER_CHOICE_INVALID");
return resolveBrowserChoice({ provider: "installed", executablePath: selected.executablePath }, { platform, env, homeDir });
}
function printInstallPreview(stdout, preview) { function printInstallPreview(stdout, preview) {
stdout.write("GStack optional runtime preview\n"); stdout.write("GStack optional runtime preview\n");
stdout.write(`Capabilities: ${preview.capabilities.length ? preview.capabilities.join(", ") : "core only"}\n`); stdout.write(`Capabilities: ${preview.capabilities.length ? preview.capabilities.join(", ") : "core only"}\n`);
if (preview.browser?.provider === "managed") {
stdout.write("Browser: managed isolated Chromium (downloaded only after approval).\n");
} else if (preview.browser?.provider === "installed") {
stdout.write(`Browser: installed Chromium at ${preview.browser.executablePath} (launched with an isolated automation profile; no browser download).\n`);
}
stdout.write(`Projected local payload before unknown downloads: ${preview.humanSize} (${preview.files} files, ${preview.components} components)\n`); stdout.write(`Projected local payload before unknown downloads: ${preview.humanSize} (${preview.files} files, ${preview.components} components)\n`);
for (const item of preview.materializations) { for (const item of preview.materializations) {
if (item.kind === "managed-bun-capture") { if (item.kind === "managed-bun-capture") {
+1 -1
View File
@@ -319,7 +319,7 @@ function validateTransactionPath(value) {
throw managedHomeError("Invalid runtime transaction path", "RUNTIME_TRANSACTION_INVALID"); throw managedHomeError("Invalid runtime transaction path", "RUNTIME_TRANSACTION_INVALID");
} }
const normalized = value.replaceAll("\\", "/"); const normalized = value.replaceAll("\\", "/");
if (normalized === "runtime-install.json" || /^bin\/[A-Za-z0-9._-]+$/.test(normalized)) return normalized; if (normalized === "config.json" || normalized === "runtime-install.json" || /^bin\/[A-Za-z0-9._-]+$/.test(normalized)) return normalized;
throw managedHomeError(`Invalid runtime transaction path: ${value}`, "RUNTIME_TRANSACTION_INVALID"); throw managedHomeError(`Invalid runtime transaction path: ${value}`, "RUNTIME_TRANSACTION_INVALID");
} }
+208 -22
View File
@@ -10,11 +10,22 @@ import { createHash } from "node:crypto";
import { constants as fsConstants, createReadStream } from "node:fs"; import { constants as fsConstants, createReadStream } from "node:fs";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import {
applyBrowserProviderToComponents,
assertBrowserChoiceSupportsCapabilities,
browserChoiceRequired,
detectInstalledBrowsers,
resolveBrowserChoice,
} from "./browser-choice.mjs";
export const BOOTSTRAP_SCHEMA_VERSION = 2; export const BOOTSTRAP_SCHEMA_VERSION = 2;
export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0"; export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0";
// Keep the runtime compatibility version separate from the immutable release
// channel. Release candidates carry the 2.0.0 runtime contract while letting
// fresh-machine production journeys run before the stable v2.0.0 tag exists.
export const BOOTSTRAP_RELEASE_TAG = "v2.0.0-rc.6";
export const OFFICIAL_MANIFEST_URL = export const OFFICIAL_MANIFEST_URL =
`https://github.com/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/gstack-runtime-manifest.json`; `https://github.com/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/gstack-runtime-manifest.json`;
const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]); const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]);
const CAPABILITY_DEPENDENCIES = Object.freeze({ const CAPABILITY_DEPENDENCIES = Object.freeze({
browser: Object.freeze([]), browser: Object.freeze([]),
@@ -47,9 +58,9 @@ const ALLOWED_DOWNLOAD_HOSTS = new Set([
"objects.githubusercontent.com", "objects.githubusercontent.com",
"release-assets.githubusercontent.com", "release-assets.githubusercontent.com",
]); ]);
const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/`; const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/`;
const OFFICIAL_CERTIFICATE_IDENTITY = const OFFICIAL_CERTIFICATE_IDENTITY =
`https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/v${BOOTSTRAP_RUNTIME_VERSION}`; `https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/${BOOTSTRAP_RELEASE_TAG}`;
const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com"; const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
export async function main(argv = process.argv.slice(2), options = {}) { export async function main(argv = process.argv.slice(2), options = {}) {
@@ -63,22 +74,85 @@ export async function main(argv = process.argv.slice(2), options = {}) {
io.stdout.write(usage()); io.stdout.write(usage());
return 0; return 0;
} }
if (!["preview", "install"].includes(parsed.action)) { if (!["options", "preview", "install"].includes(parsed.action)) {
throw bootstrapError("Expected `preview` or `install`", "BOOTSTRAP_USAGE"); throw bootstrapError("Expected `options`, `preview`, or `install`", "BOOTSTRAP_USAGE");
} }
const platform = options.platform ?? process.platform; const platform = options.platform ?? process.platform;
if (parsed.capabilities.includes("ios") && platform !== "darwin") { if (parsed.capabilities.includes("ios") && platform !== "darwin") {
throw bootstrapError("The physical-iOS capability is available only on macOS", "BOOTSTRAP_PLATFORM_UNSUPPORTED"); throw bootstrapError("The physical-iOS capability is available only on macOS", "BOOTSTRAP_PLATFORM_UNSUPPORTED");
} }
const requiresBrowser = browserChoiceRequired(parsed.capabilities);
if (parsed.action === "options") {
if (!requiresBrowser) {
throw bootstrapError("Browser options apply only to browser-backed capabilities", "BOOTSTRAP_USAGE");
}
const detected = await detectInstalledBrowsers({
platform,
env: options.env,
homeDir: options.homeDir,
candidates: options.browserCandidates,
});
const installedSupported = !parsed.capabilities.includes("browser-visible");
const installed = detected.map((browser) => ({
...browser,
supported: installedSupported,
...(installedSupported ? {} : { reason: "Visible GStack Browser requires managed Chromium for extension loading" }),
}));
const result = {
managed: {
provider: "managed",
description: "GStack-managed isolated Chromium; exact signed component bytes are shown by preview before consent",
},
installed,
mutated: false,
network: false,
};
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: "options", ...result }, null, 2)}\n`);
else printBrowserOptions(io.stdout, result);
return 0;
}
let browserChoice = null;
if (requiresBrowser) {
browserChoice = await resolveBrowserChoice({
provider: parsed.browserProvider,
executablePath: parsed.browserPath,
}, { platform, env: options.env, homeDir: options.homeDir });
assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
} else if (parsed.browserProvider || parsed.browserPath) {
throw bootstrapError("Browser options require a browser-backed capability", "BOOTSTRAP_USAGE");
}
if (parsed.source) { if (parsed.source) {
const sourceHome = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const active = await inspectReusableRuntime(sourceHome, BOOTSTRAP_RUNTIME_VERSION).catch(() => null);
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; choose a browser provider before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
if (parsed.action === "preview") { if (parsed.action === "preview") {
io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n"); io.stdout.write("Reviewed-source fallback has no signed compressed-byte manifest; the local installer can provide an on-disk preview only.\n");
return 0; return 0;
} }
if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED"); if (!parsed.yes) throw bootstrapError("Installation requires explicit --yes after review", "BOOTSTRAP_CONSENT_REQUIRED");
io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n"); io.stderr.write("Developer-only source install: only continue with a checkout you reviewed and trust.\n");
return await installFromSource(parsed.source, parsed, { ...options, ...io, prepared: false }); return await installFromSource(parsed.source, parsed, {
...options,
...io,
prepared: false,
replaceCapabilities: true,
browserChoice,
});
} }
const fetch_ = options.fetch ?? globalThis.fetch; const fetch_ = options.fetch ?? globalThis.fetch;
@@ -90,11 +164,29 @@ export async function main(argv = process.argv.slice(2), options = {}) {
); );
const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL; const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL;
assertOfficialUrl(manifestUrl, { manifest: true }); assertOfficialUrl(manifestUrl, { manifest: true });
const manifest = await fetchJson(fetch_, manifestUrl); const manifest = await fetchJson(fetch_, manifestUrl, {
official: manifestUrl === OFFICIAL_MANIFEST_URL,
});
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable); const reusable = active?.releaseMatches ? active : null;
if (!browserChoice && active?.browserChoice) {
browserChoice = await resolveBrowserChoice(active.browserChoice, {
platform,
env: options.env,
homeDir: options.homeDir,
});
}
parsed.capabilities = mergeRetainedCapabilities(parsed.capabilities, active, browserChoice);
if (browserChoiceRequired(parsed.capabilities) && !browserChoice) {
throw bootstrapError(
"The active browser capability does not record a reusable browser provider; preview browser setup options before changing this runtime.",
"BOOTSTRAP_BROWSER_CHOICE_REQUIRED",
);
}
if (browserChoice) assertBrowserChoiceSupportsCapabilities(browserChoice, parsed.capabilities);
const plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable, browserChoice);
if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`); if (parsed.json) io.stdout.write(`${JSON.stringify({ ok: true, action: parsed.action, ...plan }, null, 2)}\n`);
else printComponentPlan(io.stdout, plan); else printComponentPlan(io.stdout, plan);
if (parsed.action === "preview") return 0; if (parsed.action === "preview") return 0;
@@ -117,7 +209,7 @@ export async function main(argv = process.argv.slice(2), options = {}) {
await assertNoLinks(componentRoot); await assertNoLinks(componentRoot);
await mergeComponentRoot(componentRoot, root, claimedFiles, item.component); await mergeComponentRoot(componentRoot, root, claimedFiles, item.component);
} }
return await installFromSource(root, parsed, { ...options, ...io, prepared: true, version: manifest.version }); return await installFromSource(root, parsed, { ...options, ...io, prepared: true, version: manifest.version, browserChoice });
} finally { } finally {
await fs.rm(temporary, { recursive: true, force: true }); await fs.rm(temporary, { recursive: true, force: true });
} }
@@ -128,23 +220,47 @@ export async function main(argv = process.argv.slice(2), options = {}) {
} }
function parseArgs(argv) { function parseArgs(argv) {
const result = { action: null, capabilities: [], source: null, home: null, yes: false, json: false, help: false }; const result = {
action: null,
capabilities: [],
source: null,
home: null,
browserProvider: null,
browserPath: null,
yes: false,
json: false,
help: false,
};
for (let index = 0; index < argv.length; index += 1) { for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index]; const arg = argv[index];
if (["-h", "--help"].includes(arg)) result.help = true; if (["-h", "--help"].includes(arg)) result.help = true;
else if (arg === "--yes") result.yes = true; else if (arg === "--yes") result.yes = true;
else if (arg === "--json") result.json = true; else if (arg === "--json") result.json = true;
else if (!result.action && !arg.startsWith("-")) result.action = arg; else if (!result.action && !arg.startsWith("-")) result.action = arg;
else if (["--capability", "--source", "--home"].includes(arg)) { else if (["--capability", "--source", "--home", "--browser", "--browser-path"].includes(arg)) {
const value = argv[++index]; const value = argv[++index];
if (!value || value.startsWith("--")) throw bootstrapError(`${arg} requires a value`, "BOOTSTRAP_USAGE"); if (!value || value.startsWith("--")) throw bootstrapError(`${arg} requires a value`, "BOOTSTRAP_USAGE");
if (arg === "--capability") result.capabilities.push(value); if (arg === "--capability") result.capabilities.push(value);
else if (arg === "--source") result.source = value; else if (arg === "--source") result.source = value;
else result.home = value; else if (arg === "--home") result.home = value;
else if (arg === "--browser") result.browserProvider = value;
else result.browserPath = value;
} else throw bootstrapError(`Unknown option: ${arg}`, "BOOTSTRAP_USAGE"); } else throw bootstrapError(`Unknown option: ${arg}`, "BOOTSTRAP_USAGE");
} }
if (result.help) return result; if (result.help) return result;
if (result.action === "preview" && result.yes) throw bootstrapError("preview cannot be combined with --yes", "BOOTSTRAP_USAGE"); if (result.action === "preview" && result.yes) throw bootstrapError("preview cannot be combined with --yes", "BOOTSTRAP_USAGE");
if (result.action === "options" && (result.yes || result.source || result.browserProvider || result.browserPath)) {
throw bootstrapError("options cannot be combined with install or browser-selection flags", "BOOTSTRAP_USAGE");
}
if (result.browserProvider != null && !["managed", "installed"].includes(result.browserProvider)) {
throw bootstrapError("--browser must be `managed` or `installed`", "BOOTSTRAP_USAGE");
}
if (result.browserProvider === "managed" && result.browserPath != null) {
throw bootstrapError("--browser-path is valid only with `--browser installed`", "BOOTSTRAP_USAGE");
}
if (result.browserPath != null && result.browserProvider !== "installed") {
throw bootstrapError("--browser-path requires `--browser installed`", "BOOTSTRAP_USAGE");
}
if (!result.capabilities.length) throw bootstrapError("At least one --capability is required", "BOOTSTRAP_USAGE"); if (!result.capabilities.length) throw bootstrapError("At least one --capability is required", "BOOTSTRAP_USAGE");
result.capabilities = [...new Set(result.capabilities)].sort(); result.capabilities = [...new Set(result.capabilities)].sort();
for (const capability of result.capabilities) { for (const capability of result.capabilities) {
@@ -206,7 +322,7 @@ function sameGraph(actual, expected) {
return JSON.stringify(normalize(actual)) === JSON.stringify(normalize(expected)); return JSON.stringify(normalize(actual)) === JSON.stringify(normalize(expected));
} }
function selectedComponents(capabilities) { function selectedComponents(capabilities, browserChoice) {
const selected = new Set(["core"]); const selected = new Set(["core"]);
for (const capability of capabilities) { for (const capability of capabilities) {
for (const component of CAPABILITY_COMPONENTS[capability] ?? []) selected.add(component); for (const component of CAPABILITY_COMPONENTS[capability] ?? []) selected.add(component);
@@ -220,11 +336,29 @@ function selectedComponents(capabilities) {
} }
} }
} }
return applyBrowserProviderToComponents([...selected], browserChoice);
}
function mergeRetainedCapabilities(requested, reusable, browserChoice) {
const selected = new Set([
...(Array.isArray(reusable?.selectedCapabilities) ? reusable.selectedCapabilities : []),
...requested,
]);
if (browserChoice?.provider === "installed") selected.delete("browser-visible");
const pending = [...selected];
while (pending.length) {
for (const dependency of CAPABILITY_DEPENDENCIES[pending.pop()] ?? []) {
if (!selected.has(dependency)) {
selected.add(dependency);
pending.push(dependency);
}
}
}
return [...selected].sort(); return [...selected].sort();
} }
function buildComponentPlan(manifest, target, capabilities, reusable) { function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
const components = selectedComponents(capabilities); const components = selectedComponents(capabilities, browserChoice);
const retained = new Set(reusable?.components ?? []); const retained = new Set(reusable?.components ?? []);
const downloads = components const downloads = components
.filter((component) => !retained.has(component)) .filter((component) => !retained.has(component))
@@ -234,6 +368,7 @@ function buildComponentPlan(manifest, target, capabilities, reusable) {
target, target,
version: manifest.version, version: manifest.version,
capabilities, capabilities,
browser: browserChoice,
components, components,
reusedComponents: components.filter((component) => retained.has(component)), reusedComponents: components.filter((component) => retained.has(component)),
downloads, downloads,
@@ -244,6 +379,11 @@ function buildComponentPlan(manifest, target, capabilities, reusable) {
function printComponentPlan(stdout, plan) { function printComponentPlan(stdout, plan) {
stdout.write(`GStack optional runtime ${plan.version} for ${plan.target}\n`); stdout.write(`GStack optional runtime ${plan.version} for ${plan.target}\n`);
stdout.write(`Capabilities: ${plan.capabilities.join(", ")}\n`); stdout.write(`Capabilities: ${plan.capabilities.join(", ")}\n`);
if (plan.browser?.provider === "installed") {
stdout.write(`Browser: installed Chromium at ${plan.browser.executablePath}; isolated automation profile, no Chromium download\n`);
} else if (plan.browser?.provider === "managed") {
stdout.write("Browser: managed isolated Chromium\n");
}
stdout.write(`Components: ${plan.components.join(", ")}\n`); stdout.write(`Components: ${plan.components.join(", ")}\n`);
if (plan.reusedComponents.length) stdout.write(`Reusing: ${plan.reusedComponents.join(", ")}\n`); if (plan.reusedComponents.length) stdout.write(`Reusing: ${plan.reusedComponents.join(", ")}\n`);
stdout.write(`Download: ${plan.downloadBytes} bytes across ${plan.downloads.length} component(s)\n`); stdout.write(`Download: ${plan.downloadBytes} bytes across ${plan.downloads.length} component(s)\n`);
@@ -258,10 +398,31 @@ async function inspectReusableRuntime(home, version) {
const stat = await fs.lstat(root); const stat = await fs.lstat(root);
if (!stat.isDirectory() || stat.isSymbolicLink()) return null; if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8")); const bundle = JSON.parse(await fs.readFile(path.join(root, ".gstack-bundle.json"), "utf8"));
if (bundle?.schemaVersion !== 2 || bundle?.version !== version || !Array.isArray(bundle.runtimeComponents) || const releaseMatches = bundle?.version === version ||
(typeof bundle?.version === "string" && bundle.version.startsWith(`${version}-caps-`));
if (bundle?.schemaVersion !== 2 || typeof bundle.version !== "string" ||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(bundle.version) || !Array.isArray(bundle.runtimeComponents) ||
!Array.isArray(bundle.files)) return null; !Array.isArray(bundle.files)) return null;
const components = [...new Set(bundle.runtimeComponents)]; const components = [...new Set(bundle.runtimeComponents)];
if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null; if (!components.length || components.some((component) => !Object.hasOwn(COMPONENT_DEPENDENCIES, component))) return null;
const selectedCapabilities = Array.isArray(bundle.selectedCapabilities)
? [...new Set(bundle.selectedCapabilities)]
: [];
if (selectedCapabilities.some((capability) => !CAPABILITIES.has(capability))) return null;
let browserChoice = null;
if (browserChoiceRequired(selectedCapabilities)) {
const explicit = bundle.browserChoice;
if (!explicit || !["managed", "installed"].includes(explicit.provider)) return null;
if (explicit.provider === "installed") {
if (selectedCapabilities.includes("browser-visible") ||
typeof explicit.executablePath !== "string" || !path.isAbsolute(explicit.executablePath) ||
components.includes("browser-headless") || components.includes("browser-visible")) return null;
browserChoice = { provider: "installed", executablePath: explicit.executablePath };
} else {
if (!components.includes("browser-headless") && !components.includes("browser-visible")) return null;
browserChoice = { provider: "managed", executablePath: null };
}
}
await assertNoLinks(root); await assertNoLinks(root);
const files = []; const files = [];
const seen = new Set(); const seen = new Set();
@@ -277,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
await sha256File(file) !== entry.sha256) return null; await sha256File(file) !== entry.sha256) return null;
files.push(relative); files.push(relative);
} }
return { root, components, files }; return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
} }
async function seedReusableRuntime(reusable, destination, claimedFiles) { async function seedReusableRuntime(reusable, destination, claimedFiles) {
@@ -300,10 +461,18 @@ function sha256File(file) {
}); });
} }
async function fetchJson(fetch_, url) { async function fetchJson(fetch_, url, options = {}) {
const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" }); const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" });
assertFinalDownloadUrl(response.url || url); assertFinalDownloadUrl(response.url || url);
if (!response.ok) throw bootstrapError(`Download failed with HTTP ${response.status}`, "BOOTSTRAP_DOWNLOAD_FAILED"); if (!response.ok) {
if (options.official && response.status === 404) {
throw bootstrapError(
`Official runtime release ${BOOTSTRAP_RELEASE_TAG} is not published at ${url}. No files were downloaded or installed.`,
"BOOTSTRAP_RELEASE_UNAVAILABLE",
);
}
throw bootstrapError(`Manifest download failed with HTTP ${response.status} from ${url}. No files were downloaded or installed.`, "BOOTSTRAP_DOWNLOAD_FAILED");
}
const value = await response.json(); const value = await response.json();
if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID"); if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID");
return value; return value;
@@ -398,9 +567,14 @@ async function installFromSource(source, parsed, options) {
const stat = await fs.lstat(installer).catch(() => null); const stat = await fs.lstat(installer).catch(() => null);
if (!stat?.isFile() || stat.isSymbolicLink()) throw bootstrapError("Source does not contain a safe runtime installer", "BOOTSTRAP_SOURCE_INVALID"); if (!stat?.isFile() || stat.isSymbolicLink()) throw bootstrapError("Source does not contain a safe runtime installer", "BOOTSTRAP_SOURCE_INVALID");
const args = [installer, "--source", physical, "--install-now", "--yes", "--capabilities", parsed.capabilities.join(",")]; const args = [installer, "--source", physical, "--install-now", "--yes", "--capabilities", parsed.capabilities.join(",")];
if (options.browserChoice) {
args.push("--browser", options.browserChoice.provider);
if (options.browserChoice.executablePath) args.push("--browser-path", options.browserChoice.executablePath);
}
if (parsed.home) args.push("--home", path.resolve(parsed.home)); if (parsed.home) args.push("--home", path.resolve(parsed.home));
if (options.version) args.push("--version", options.version); if (options.version) args.push("--version", options.version);
if (options.prepared) args.push("--prepared"); if (options.prepared) args.push("--prepared");
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
await run(options.nodeCommand ?? process.execPath, args); await run(options.nodeCommand ?? process.execPath, args);
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`); options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
return 0; return 0;
@@ -521,12 +695,24 @@ function formatBytes(bytes) {
} }
function usage() { function usage() {
return "Usage: node runtime-bootstrap.mjs install --capability <name> [--capability <name>...]\n" + return "Usage: node runtime-bootstrap.mjs options --capability <browser-backed-name>\n" +
" node runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name>\n\n" + " node runtime-bootstrap.mjs preview|install --capability <name> [--capability <name>...]\n" +
" --browser managed|installed [--browser-path <absolute-path>] [--yes]\n" +
" node runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> --browser <choice>\n\n" +
"Downloads only a versioned official GStack runtime release and never enrolls a coding host.\n" + "Downloads only a versioned official GStack runtime release and never enrolls a coding host.\n" +
"--source is a developer-only fallback for a checkout you have reviewed and trust.\n"; "--source is a developer-only fallback for a checkout you have reviewed and trust.\n";
} }
function printBrowserOptions(stdout, result) {
stdout.write("GStack browser setup options (no network access and no changes made)\n");
stdout.write(`managed: ${result.managed.description}\n`);
if (!result.installed.length) stdout.write("installed: no supported Chromium executable detected; an absolute path may be supplied explicitly\n");
for (const browser of result.installed) stdout.write(browser.supported
? `installed: ${browser.name}${browser.executablePath}\n`
: `installed (unavailable for this capability): ${browser.name}${browser.executablePath}; ${browser.reason}\n`);
stdout.write("No provider is selected until the user chooses one and separately approves the previewed install.\n");
}
async function isDirectExecution() { async function isDirectExecution() {
if (!process.argv[1]) return false; if (!process.argv[1]) return false;
const [modulePath, invokedPath] = await Promise.all([ const [modulePath, invokedPath] = await Promise.all([
+68 -2
View File
@@ -2,14 +2,16 @@ import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { assertPathInside, resolveRuntimePaths } from "./paths.js"; import { assertPathInside, resolveRuntimePaths } from "./paths.js";
import { atomicWriteJson, pathExists, readJson, renameWithRetry } from "./storage.js"; import { atomicWriteFile, atomicWriteJson, pathExists, readJson, renameWithRetry } from "./storage.js";
import { import {
assertManagedHome, assertManagedHome,
ensureManagedHome, ensureManagedHome,
ensureManagedRuntimeDirectory, ensureManagedRuntimeDirectory,
recoverRuntimeTransactionUnlocked, recoverRuntimeTransactionUnlocked,
RUNTIME_TRANSACTION_FILE,
withRuntimeLifecycleLock, withRuntimeLifecycleLock,
} from "./managed-home.js"; } from "./managed-home.js";
import { configSetBrowserChoice } from "./config.js";
import { errorWithCode as upgradeError } from "./errors.js"; import { errorWithCode as upgradeError } from "./errors.js";
import { currentIsoTimestamp as isoNow } from "./time.js"; import { currentIsoTimestamp as isoNow } from "./time.js";
@@ -214,6 +216,11 @@ export async function rollbackUpgrade(home, options = {}) {
} }
await assertTreeContainsNoLinks(fallbackPath); await assertTreeContainsNoLinks(fallbackPath);
if (options.healthCheck) await options.healthCheck(fallbackPath); if (options.healthCheck) await options.healthCheck(fallbackPath);
const activation = options.prepareActivation
? await options.prepareActivation(fallbackPath)
: null;
const syncBrowserChoice = activation != null &&
Object.prototype.hasOwnProperty.call(activation, "browserChoice");
const rolledBack = { const rolledBack = {
schemaVersion: 2, schemaVersion: 2,
status: "active", status: "active",
@@ -222,11 +229,70 @@ export async function rollbackUpgrade(home, options = {}) {
rolledBackFrom: pointer.current ?? null, rolledBackFrom: pointer.current ?? null,
rolledBackAt: isoNow(options.now), rolledBackAt: isoNow(options.now),
}; };
await atomicWriteJson(paths.versionPointer, rolledBack, { mode: 0o600 }); if (!syncBrowserChoice) {
await atomicWriteJson(paths.versionPointer, rolledBack, { mode: 0o600 });
return rolledBack;
}
const configSnapshot = await snapshotRollbackConfig(paths.config);
const journalPath = path.join(resolved, RUNTIME_TRANSACTION_FILE);
await atomicWriteJson(journalPath, {
schemaVersion: 1,
kind: "gstack-runtime-install-transaction",
status: "prepared",
home: resolved,
version: fallbackVersion,
previousPointerExists: true,
previousPointer: pointer,
files: [configSnapshot == null
? { path: "config.json", existed: false }
: {
path: "config.json",
existed: true,
mode: configSnapshot.mode,
dataBase64: configSnapshot.data.toString("base64"),
}],
preparedAt: isoNow(options.now),
}, { mode: 0o600 });
try {
await configSetBrowserChoice(resolved, activation.browserChoice);
await atomicWriteJson(paths.versionPointer, rolledBack, { mode: 0o600 });
await fs.rm(journalPath, { force: true });
} catch (cause) {
const rollbackErrors = [];
try {
if (configSnapshot == null) await fs.rm(paths.config, { force: true });
else await atomicWriteFile(paths.config, configSnapshot.data, { mode: configSnapshot.mode });
} catch (error) {
rollbackErrors.push(error);
}
try {
await atomicWriteJson(paths.versionPointer, pointer, { mode: 0o600 });
} catch (error) {
rollbackErrors.push(error);
}
if (rollbackErrors.length === 0) await fs.rm(journalPath, { force: true });
const error = upgradeError("Rollback activation failed and the previous runtime was restored", "ROLLBACK_ACTIVATION_FAILED", cause);
if (rollbackErrors.length === 1) error.rollbackError = rollbackErrors[0];
else if (rollbackErrors.length > 1) error.rollbackError = new AggregateError(rollbackErrors, "Runtime rollback restoration was incomplete");
throw error;
}
return rolledBack; return rolledBack;
}, options); }, options);
} }
async function snapshotRollbackConfig(configPath) {
const stat = await fs.lstat(configPath).catch((error) => {
if (error?.code === "ENOENT") return null;
throw error;
});
if (!stat) return null;
if (!stat.isFile() || stat.isSymbolicLink()) {
throw upgradeError("Refusing unsafe browser configuration during rollback", "RUNTIME_TRANSACTION_INVALID");
}
return { data: await fs.readFile(configPath), mode: stat.mode & 0o777 };
}
export async function activeVersion(home, options = {}) { export async function activeVersion(home, options = {}) {
const recovered = await recoverPendingUpgrade(home, options); const recovered = await recoverPendingUpgrade(home, options);
return recovered.pointer; return recovered.pointer;
+1 -1
View File
@@ -38,7 +38,7 @@ CHROMIUM_DIR=$(ls -d "$PW_CACHE"/chromium-*/chrome-mac-arm64 2>/dev/null | sort
if [ -z "$CHROMIUM_DIR" ]; then if [ -z "$CHROMIUM_DIR" ]; then
echo "ERROR: Playwright Chromium not found in $PW_CACHE" echo "ERROR: Playwright Chromium not found in $PW_CACHE"
echo "Run: bunx playwright install chromium" echo "Run: bunx playwright-core install chromium"
exit 1 exit 1
fi fi
+10 -6
View File
@@ -386,7 +386,7 @@ Print this replacement invocation, then dispatch to it exactly:
\`${assignment.replacement}\` \`${assignment.replacement}\`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved \`${assignment.source}\` module. If that dispatcher is not installed, tell the user to install it with \`npx skills add time-attack/gstack --skill ${assignment.tree}\`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved \`${assignment.source}\` module. If that dispatcher is not installed, tell the user to install it with \`npx skills add time-attack/gstack/skills --skill ${assignment.tree}\`.
`); `);
rows.push(`| \`/${assignment.source}\` | \`${assignment.replacement}\` | \`skills/${assignment.tree}/references/legacy/${assignment.source}.md\` |`); rows.push(`| \`/${assignment.source}\` | \`${assignment.replacement}\` | \`skills/${assignment.tree}/references/legacy/${assignment.source}.md\` |`);
aliases.push({ aliases.push({
@@ -485,7 +485,7 @@ function webContextContract(): string {
'', '',
'Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent.', 'Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent.',
'', '',
'Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack` and is never inferred from runtime state.', 'Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack/skills` and is never inferred from runtime state.',
'', '',
].join('\n'); ].join('\n');
} }
@@ -496,15 +496,17 @@ function runtimeContract(): string {
The six Agent Skills are useful without a GStack runtime. Never install, download, build, select, update, or remove runtime capabilities merely because a skill was invoked. The six Agent Skills are useful without a GStack runtime. Never install, download, build, select, update, or remove runtime capabilities merely because a skill was invoked.
Before interactive browser work, read \`references/BROWSER-PROVIDERS.md\` in full. It owns provider detection, consented host setup, the common readiness journey, and the explicit GStack fallback. Skill installation never proves browser readiness. Before interactive browser work, read \`references/BROWSER-PROVIDERS.md\` in full. It owns host-provider detection, consented host setup, and the common readiness journey. The GStack fallback uses the local Playwright adapter with one explicit engine choice; skill installation never proves browser readiness.
When an active specialist first reaches a capability it cannot use, name the exact capability and why it is needed. Offer to continue without it when the judgment-only or host-native path remains valid. Before any network preview, ask whether the user wants to check official setup options and exact sizes. Disclose that an uncached preview makes one public GitHub request for signed manifest metadata and sends no repository content, private URL, file, cookie, token, or credential; then STOP. A cached already-verified manifest may preview offline, but never silently fetch. When an active specialist first reaches a capability it cannot use, name the exact capability and why it is needed. Offer to continue without it when the judgment-only or host-native path remains valid. Before any network preview, ask whether the user wants to check official setup options and exact sizes. Disclose that an uncached preview makes one public GitHub request for signed manifest metadata and sends no repository content, private URL, file, cookie, token, or credential; then STOP. A cached already-verified manifest may preview offline, but never silently fetch.
Only after the user approves that metadata check, run the non-mutating preview from this skill root: \`node references/support/runtime-bootstrap.mjs preview --capability <name>\` (repeat \`--capability\` for additional requested capabilities). It dependency-expands, reports already verified local components, exact missing components, and their summed compressed bytes. It never downloads components or mutates runtime state. Preview consent is not install consent. For a browser-backed capability, first run \`node references/support/runtime-bootstrap.mjs options --capability <name>\`. This local-only command performs no network request or mutation. Show the detected installed Chromium executables plus managed Chromium, explain that either choice uses an isolated automation profile, and STOP for a choice. Never infer or silently record one. Installed Chromium avoids browser-binary downloads; managed Chromium is isolated and reproducible. Internal \`browser-visible\` requires managed Chromium because installed Chrome-family builds can block automation extension loading.
Only after the user approves the metadata check and, when applicable, chooses a browser engine, run the non-mutating preview from this skill root: \`node references/support/runtime-bootstrap.mjs preview --capability <name> --browser managed\` or \`node references/support/runtime-bootstrap.mjs preview --capability <name> --browser installed --browser-path <absolute-path>\` (repeat \`--capability\` for additional requested capabilities). Omit browser flags for capabilities that do not depend on a browser. It dependency-expands, reports already verified local components, exact missing components, and their summed compressed bytes. It never downloads components or mutates runtime state. Preview consent is not install consent.
User-facing setup capabilities are exactly \`browser\`, \`design\`, \`diagram\`, \`pdf\`, and \`ios\`. \`all\` means those five and intentionally excludes visible Chromium. The internal \`browser-visible\` capability is additive and is offered only when a workflow actually reaches a headed browser, extension, or browser-handoff step. Never offer it during ordinary headless QA. User-facing setup capabilities are exactly \`browser\`, \`design\`, \`diagram\`, \`pdf\`, and \`ios\`. \`all\` means those five and intentionally excludes visible Chromium. The internal \`browser-visible\` capability is additive and is offered only when a workflow actually reaches a headed browser, extension, or browser-handoff step. Never offer it during ordinary headless QA.
After showing the complete preview, STOP for explicit approval. Only after approval run \`node references/support/runtime-bootstrap.mjs install --capability <name> --yes\`; install must reprint the identical dependency-closed plan before downloading. Signed internal components are \`core\`, \`browser-code\` (browse code and dependencies), \`browser-headless\` (Playwright headless shell and FFmpeg), \`browser-visible\` (full Chromium), \`design\`, \`diagram\`, \`pdf\`, and \`ios\`. Logical \`browser\` expands to \`browser-code + browser-headless\`; internal \`browser-visible\` expands to \`browser-code + browser-visible\` and does not require headless. Component dependencies are \`browser-code → core\`, \`browser-headless → browser-code\`, and \`browser-visible → browser-code\`. \`diagram\` depends on logical \`browser\`; \`pdf\` depends on \`diagram\`; \`ios\` is Darwin-only. Therefore a first-time headed flow previews \`core + browser-code + browser-visible\`, while an existing verified headless runtime downloads only missing \`browser-visible\`. The manifest schema is v2 with global \`capabilityComponents\` and \`componentDependencies\`, plus \`targets[target].components[id]\` carrying signed exact-byte artifacts. After showing the complete preview, STOP for explicit approval. Only after approval run the matching \`install\` command with the same capabilities and browser flags plus \`--yes\`; install must reprint the identical dependency-closed plan before downloading. The approved browser choice is then persisted atomically in \`$GSTACK_HOME/config.json\`. \`gstack config browser clear\` disables browser-backed launchers; the managed/installed config commands can reselect only a provider compatible with the active slot. Switching providers requires a fresh preview and install so configuration cannot point at a runtime missing that engine. Signed internal components are \`core\`, \`browser-code\` (adapter code and dependencies), \`browser-headless\` (managed Playwright headless shell and FFmpeg), \`browser-visible\` (managed full Chromium), \`design\`, \`diagram\`, \`pdf\`, and \`ios\`. With managed Chromium, logical \`browser\` expands to \`browser-code + browser-headless\`; with an installed browser, the same logical capability downloads \`browser-code\` only and the stable launcher injects the validated executable path. Internal \`browser-visible\` expands to \`browser-code + browser-visible\` and is managed-only. \`diagram\` depends on logical \`browser\`; \`pdf\` depends on \`diagram\`; \`ios\` is Darwin-only. The manifest schema is v2 with global \`capabilityComponents\` and \`componentDependencies\`, plus \`targets[target].components[id]\` carrying signed exact-byte artifacts.
The dependency-free Node 18+ bootstrap accepts only official GStack GitHub Release hosts, validates manifest/runtime/skill API/platform/component metadata and SHA-256, verifies a declared Cosign bundle when Cosign is available, rejects unsafe archive paths/links, and invokes the atomic managed installer. Never run \`./setup\` inside a standard-installed skill directory, enroll a host, or treat skill installation as runtime consent. The dependency-free Node 18+ bootstrap accepts only official GStack GitHub Release hosts, validates manifest/runtime/skill API/platform/component metadata and SHA-256, verifies a declared Cosign bundle when Cosign is available, rejects unsafe archive paths/links, and invokes the atomic managed installer. Never run \`./setup\` inside a standard-installed skill directory, enroll a host, or treat skill installation as runtime consent.
@@ -518,7 +520,7 @@ Every optional-runtime tool result must satisfy \`references/support/execution-r
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. 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 <reviewed-checkout> --capability <name> --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. The developer-only fallback is \`node references/support/runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name> [matching browser flags] --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.
`; `;
} }
@@ -568,6 +570,7 @@ Do not put secrets in run IDs, effect keys, or command arguments. Existing appro
function writeSharedContracts(): void { function writeSharedContracts(): void {
const bootstrap = fs.readFileSync(path.join(ROOT, 'runtime', 'runtime-bootstrap.mjs')); const bootstrap = fs.readFileSync(path.join(ROOT, 'runtime', 'runtime-bootstrap.mjs'));
const browserChoice = fs.readFileSync(path.join(ROOT, 'runtime', 'browser-choice.mjs'));
const browserSmoke = fs.readFileSync(path.join(ROOT, 'runtime', 'browser-provider-smoke.mjs')); const browserSmoke = fs.readFileSync(path.join(ROOT, 'runtime', 'browser-provider-smoke.mjs'));
for (const tree of TREE_NAMES) { for (const tree of TREE_NAMES) {
write(path.join(ROOT, 'skills', tree, 'references', 'EXECUTION-PROFILES.md'), `${GENERATED}\n${renderExecutionProfiles()}`); write(path.join(ROOT, 'skills', tree, 'references', 'EXECUTION-PROFILES.md'), `${GENERATED}\n${renderExecutionProfiles()}`);
@@ -577,6 +580,7 @@ function writeSharedContracts(): void {
write(path.join(ROOT, 'skills', tree, 'references', 'RUNTIME.md'), runtimeContract()); write(path.join(ROOT, 'skills', tree, 'references', 'RUNTIME.md'), runtimeContract());
write(path.join(ROOT, 'skills', tree, 'references', 'BROWSER-PROVIDERS.md'), `${GENERATED}\n${renderBrowserProviderContract()}`); write(path.join(ROOT, 'skills', tree, 'references', 'BROWSER-PROVIDERS.md'), `${GENERATED}\n${renderBrowserProviderContract()}`);
write(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-bootstrap.mjs'), bootstrap); write(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-bootstrap.mjs'), bootstrap);
write(path.join(ROOT, 'skills', tree, 'references', 'support', 'browser-choice.mjs'), browserChoice);
write(path.join(ROOT, 'skills', tree, 'references', 'support', 'browser-provider-smoke.mjs'), browserSmoke); write(path.join(ROOT, 'skills', tree, 'references', 'support', 'browser-provider-smoke.mjs'), browserSmoke);
writeJson(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-contract.json'), RUNTIME_SKILL_CONTRACT); writeJson(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-contract.json'), RUNTIME_SKILL_CONTRACT);
writeJson(path.join(ROOT, 'skills', tree, 'references', 'support', 'execution-result-contract.json'), EXECUTION_RESULT_SCHEMA); writeJson(path.join(ROOT, 'skills', tree, 'references', 'support', 'execution-result-contract.json'), EXECUTION_RESULT_SCHEMA);
+5 -4
View File
@@ -188,7 +188,7 @@ export function renderLegacyBody(source: string): string {
*/ */
function portLegacyText(value: string, source: string): string { function portLegacyText(value: string, source: string): string {
if (source === 'gstack-upgrade') { if (source === 'gstack-upgrade') {
return `# Legacy upgrade compatibility\n\nThe 1.x host-directory detector, vendored-copy synchronizer, and destructive Git replacement blocks were duplicated installation infrastructure. GStack 2 delegates skill placement and updates to the standard Agent Skills installer and manages the optional shared runtime atomically.\n\n- Update selected skills with \`npx skills add time-attack/gstack\` using the user's existing project/global choice. Never infer or enroll a host.\n- Upgrade a complete local runtime package with \`gstack upgrade --source <complete-gstack-package> --version <version>\`.\n- Roll back the runtime with \`gstack upgrade --rollback\`.\n- Run \`gstack doctor\` after either operation.\n- Do not reset, delete, move, or rewrite a host skill directory. Do not infer Context.dev choice or consent.\n\nThis compatibility module contains no specialist judgment; release readiness and rollback judgment remain in the preserved ship modules.\n`; return `# Legacy upgrade compatibility\n\nThe 1.x host-directory detector, vendored-copy synchronizer, and destructive Git replacement blocks were duplicated installation infrastructure. GStack 2 delegates skill placement and updates to the standard Agent Skills installer and manages the optional shared runtime atomically.\n\n- Update selected skills with \`npx skills add time-attack/gstack/skills\` using the user's existing project/global choice. Never infer or enroll a host.\n- Upgrade a complete local runtime package with \`gstack upgrade --source <complete-gstack-package> --version <version>\`.\n- Roll back the runtime with \`gstack upgrade --rollback\`.\n- Run \`gstack doctor\` after either operation.\n- Do not reset, delete, move, or rewrite a host skill directory. Do not infer Context.dev choice or consent.\n\nThis compatibility module contains no specialist judgment; release readiness and rollback judgment remain in the preserved ship modules.\n`;
} }
let body = value; let body = value;
@@ -280,7 +280,7 @@ function portLegacyText(value: string, source: string): string {
.replaceAll('$GSTACK_ROOT/lib/redact-audit-log.ts', '$GSTACK_BIN/gstack-redact-audit-log') .replaceAll('$GSTACK_ROOT/lib/redact-audit-log.ts', '$GSTACK_BIN/gstack-redact-audit-log')
.replaceAll('bun $GSTACK_BIN/gstack-redact-audit-log', '$GSTACK_BIN/gstack-redact-audit-log') .replaceAll('bun $GSTACK_BIN/gstack-redact-audit-log', '$GSTACK_BIN/gstack-redact-audit-log')
.replaceAll('Disk paths stay `$GSTACK_ROOT/[skill-name]/SKILL.md`.', 'Resolve retired names through `references/COMPATIBILITY.md`; skill placement is installer-owned.') .replaceAll('Disk paths stay `$GSTACK_ROOT/[skill-name]/SKILL.md`.', 'Resolve retired names through `references/COMPATIBILITY.md`; skill placement is installer-owned.')
.replaceAll('Tell the user: "Done. Each developer now runs: `cd $GSTACK_ROOT && ./setup --team`"', 'Tell the user: "Done. Each developer installs the selected canonical skills with `npx skills add time-attack/gstack`; the optional runtime remains user-scoped."'); .replaceAll('Tell the user: "Done. Each developer now runs: `cd $GSTACK_ROOT && ./setup --team`"', 'Tell the user: "Done. Each developer installs the selected canonical skills with `npx skills add time-attack/gstack/skills`; the optional runtime remains user-scoped."');
body = body body = body
.replace(/_VENDORED="no"\nif \[ -d "\.agents\/skills\/gstack" \][\s\S]*?echo "VENDORED_GSTACK: \$_VENDORED"/g, '_VENDORED="managed-by-standard-installer"\necho "VENDORED_GSTACK: $_VENDORED"') .replace(/_VENDORED="no"\nif \[ -d "\.agents\/skills\/gstack" \][\s\S]*?echo "VENDORED_GSTACK: \$_VENDORED"/g, '_VENDORED="managed-by-standard-installer"\necho "VENDORED_GSTACK: $_VENDORED"')
@@ -311,7 +311,8 @@ function portLegacyText(value: string, source: string): string {
.replaceAll('bun run $GSTACK_BIN/gstack-next-version', '$GSTACK_BIN/gstack-next-version') .replaceAll('bun run $GSTACK_BIN/gstack-next-version', '$GSTACK_BIN/gstack-next-version')
.replaceAll('bun run $GSTACK_BIN/gstack-version-bump', '$GSTACK_BIN/gstack-version-bump') .replaceAll('bun run $GSTACK_BIN/gstack-version-bump', '$GSTACK_BIN/gstack-version-bump')
.replaceAll('DISCOVER_BIN="bun run $GSTACK_BIN/gstack-global-discover"', 'DISCOVER_BIN="$GSTACK_BIN/gstack-global-discover"') .replaceAll('DISCOVER_BIN="bun run $GSTACK_BIN/gstack-global-discover"', 'DISCOVER_BIN="$GSTACK_BIN/gstack-global-discover"')
.replaceAll('Tell the user: "gstack browse needs a one-time build (~10 seconds). OK to proceed?" Then STOP and wait.', 'Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.') .replaceAll('Tell the user: "gstack browse needs a one-time build (~10 seconds). OK to proceed?" Then STOP and wait.', 'Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.')
.replaceAll('Tell the user: "The optional managed headless browser capability is missing. Do you want to preview its exact dependency-closed component plan and compressed bytes now?" Then STOP and wait.', 'Tell the user: "The browser-backed capability is not ready. Do you want to see the local setup options—GStack-managed Chromium or a detected installed Chromium executable—with no network access or changes?" Then STOP and wait.')
.replace(/^command -v bun >\/dev\/null 2>&1 \|\| echo "redaction scan skipped — bun not on PATH"\n/gm, ''); .replace(/^command -v bun >\/dev\/null 2>&1 \|\| echo "redaction scan skipped — bun not on PATH"\n/gm, '');
body = body body = body
@@ -430,7 +431,7 @@ export function renderPortedLegacyBody(source: string): string {
'', '',
'This workflow may require internal `browser-visible` because it reaches a headed browser, extension, interactive cookie picker, or browser handoff. Do not offer visible Chromium during ordinary headless QA.', 'This workflow may require internal `browser-visible` because it reaches a headed browser, extension, interactive cookie picker, or browser handoff. Do not offer visible Chromium during ordinary headless QA.',
'', '',
'At the first actual visible-browser step, ask whether the user wants to check official setup options and exact sizes. Disclose that an uncached preview makes one public GitHub signed-manifest request and sends no repository/private data, then STOP. Only after that approval run `node references/support/runtime-bootstrap.mjs preview --capability browser-visible`. It expands to `core + browser-code + browser-visible` for a first install, but an existing verified headless runtime downloads only missing `browser-visible`; it never requires `browser-headless`. Show the exact missing components and summed incremental compressed bytes, then STOP again for separate install approval. Only after install approval run `node references/support/runtime-bootstrap.mjs install --capability browser-visible --yes`, recheck readiness, and resume the interrupted step.', 'At the first actual visible-browser step, run the local-only `node references/support/runtime-bootstrap.mjs options --capability browser-visible`, explain that this extension-bearing flow requires managed Chromium because installed Chrome-family builds can block automation extension loading, and ask whether the user wants to check exact official sizes. Disclose that an uncached preview makes one public GitHub signed-manifest request and sends no repository/private data, then STOP. Only after approval run `node references/support/runtime-bootstrap.mjs preview --capability browser-visible --browser managed`. It expands to `core + browser-code + browser-visible` for a first install, but an existing verified headless runtime downloads only missing `browser-visible`; it never requires `browser-headless`. Show the exact missing components and summed incremental compressed bytes, then STOP again for separate install approval. Only after install approval run `node references/support/runtime-bootstrap.mjs install --capability browser-visible --browser managed --yes`, recheck readiness, and resume the interrupted step.',
'', '',
body, body,
].join('\n'); ].join('\n');
+3 -3
View File
@@ -16,7 +16,7 @@ const ALLOWED_DISPOSITIONS = new Set(['VERBATIM_PORT', 'MECHANICAL_PORT', 'JUDGM
// checks to the previously verified 4,681-check corpus. The first update only // checks to the previously verified 4,681-check corpus. The first update only
// accounted for the 16 lazy-section checks; the remaining 136 cover runtime // accounted for the 16 lazy-section checks; the remaining 136 cover runtime
// contracts, retired-invocation guards, and generated package closure. // contracts, retired-invocation guards, and generated package closure.
export const EXPECTED_PARITY_CHECKS = 4833; export const EXPECTED_PARITY_CHECKS = 4836;
function sha256(value: string | Uint8Array): string { function sha256(value: string | Uint8Array): string {
return createHash('sha256').update(value).digest('hex'); return createHash('sha256').update(value).digest('hex');
@@ -373,8 +373,8 @@ export function runParity(): ParityResult {
const packagedBootstrap = fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-bootstrap.mjs')); const packagedBootstrap = fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-bootstrap.mjs'));
check(packagedBootstrap.equals(fs.readFileSync(path.join(ROOT, 'runtime', 'runtime-bootstrap.mjs'))), `${tree} packaged runtime bootstrap drifted from its source`); check(packagedBootstrap.equals(fs.readFileSync(path.join(ROOT, 'runtime', 'runtime-bootstrap.mjs'))), `${tree} packaged runtime bootstrap drifted from its source`);
check(runtimeContract.includes('preview --capability <name>') && runtimeContract.includes('It never downloads components or mutates runtime state.'), `${tree} runtime contract lacks non-mutating exact-byte preview`); check(runtimeContract.includes('preview --capability <name>') && runtimeContract.includes('It never downloads components or mutates runtime state.'), `${tree} runtime contract lacks non-mutating exact-byte preview`);
check(runtimeContract.includes('install --capability <name> --yes'), `${tree} runtime contract lacks explicit approved install invocation`); check(runtimeContract.includes('matching `install` command with the same capabilities and browser flags plus `--yes`'), `${tree} runtime contract lacks explicit approved install invocation`);
check(runtimeContract.includes('Logical `browser` expands to `browser-code + browser-headless`') && runtimeContract.includes('`browser-visible` expands to `browser-code + browser-visible` and does not require headless') && runtimeContract.includes('`pdf` depends on `diagram`'), `${tree} runtime contract omits component dependency closure`); check(runtimeContract.includes('With managed Chromium, logical `browser` expands to `browser-code + browser-headless`') && runtimeContract.includes('Internal `browser-visible` expands to `browser-code + browser-visible` and is managed-only') && runtimeContract.includes('`pdf` depends on `diagram`'), `${tree} runtime contract omits provider-aware component dependency closure`);
check(runtimeContract.includes('`all` means those five and intentionally excludes visible Chromium'), `${tree} runtime contract lets eager setup install visible Chromium`); check(runtimeContract.includes('`all` means those five and intentionally excludes visible Chromium'), `${tree} runtime contract lets eager setup install visible Chromium`);
check(runtimeContract.includes('B=$GSTACK_BIN/browse') && runtimeContract.includes('P=$GSTACK_BIN/make-pdf'), `${tree} runtime contract omits stable launcher bindings`); check(runtimeContract.includes('B=$GSTACK_BIN/browse') && runtimeContract.includes('P=$GSTACK_BIN/make-pdf'), `${tree} runtime contract omits stable launcher bindings`);
check(runtimeContract.includes('BUN_CMD=$GSTACK_BIN/bun'), `${tree} runtime contract omits the managed Bun binding`); check(runtimeContract.includes('BUN_CMD=$GSTACK_BIN/bun'), `${tree} runtime contract omits the managed Bun binding`);
+1 -1
View File
@@ -34,7 +34,7 @@ rm -f \
( (
cd "$REPO" cd "$REPO"
./setup --home "$HOME_DIR" --json ./setup --home "$HOME_DIR" --browser managed --install-now --yes --json
) )
# The optional runtime setup installs only its production/build closure. The # The optional runtime setup installs only its production/build closure. The
+18 -9
View File
@@ -438,6 +438,15 @@ function verifyInstalledCase(
const installedSkills = listInstalledSkills(targetRoot); const installedSkills = listInstalledSkills(targetRoot);
const sortedExpected = [...expectedSkills].sort(); const sortedExpected = [...expectedSkills].sort();
record(checks, `${id}.command`, command.exitCode === 0, `exit=${command.exitCode}; signal=${command.signal ?? 'none'}`); record(checks, `${id}.command`, command.exitCode === 0, `exit=${command.exitCode}; signal=${command.signal ?? 'none'}`);
if (sourceSkillSegments.length === 1 && sourceSkillSegments[0] === 'skills') {
const reported = Number(stripTerminalControls(command.stdout).match(/Found\s+(\d+)\s+skills?/)?.[1]);
record(
checks,
`${id}.public-discovery-count`,
reported === PUBLIC_SKILLS.length,
`expected installer to report 6 public skills; found ${Number.isFinite(reported) ? reported : '(unparsed)'}`,
);
}
record( record(
checks, checks,
`${id}.selected-skills`, `${id}.selected-skills`,
@@ -595,10 +604,10 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
const supportsRemoval = /remove\s+\[skills\]/.test(helpCommand.stdout) && /Remove Options/.test(helpCommand.stdout); const supportsRemoval = /remove\s+\[skills\]/.test(helpCommand.stdout) && /Remove Options/.test(helpCommand.stdout);
const discoveryCommand = execute( const discoveryCommand = execute(
// Exercise the repository root exactly as the documented // Exercise the documented public source directly. The repository root
// `npx skills add time-attack/gstack` path will after checkout. The // also contains opt-in 1.x compatibility aliases; skills@1.5.19 counts
// curated projection alone could hide stray root-level SKILL.md files. // those internal entries before applying an explicit --skill filter.
skillsCliArgv(npxExecutable, ['add', repoRoot, '--list']), skillsCliArgv(npxExecutable, ['add', path.join(repoRoot, 'skills'), '--list']),
controlProject, controlProject,
controlEnv, controlEnv,
); );
@@ -626,7 +635,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
entry, entry,
scope, scope,
sourceKind, sourceKind,
sourceArgument: sourceKind === 'source-symlink' ? sourceLink : sourceRoot, sourceArgument: path.join(sourceKind === 'source-symlink' ? sourceLink : sourceRoot, 'skills'),
sourceRoot, sourceRoot,
expectedSkills: PUBLIC_SKILLS, expectedSkills: PUBLIC_SKILLS,
explicitSelection: false, explicitSelection: false,
@@ -643,7 +652,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
entry: cursor, entry: cursor,
scope: 'project', scope: 'project',
sourceKind: 'repository-root', sourceKind: 'repository-root',
sourceArgument: repoRoot, sourceArgument: path.join(repoRoot, 'skills'),
sourceRoot: repoRoot, sourceRoot: repoRoot,
expectedSkills: COLLISION_SKILLS, expectedSkills: COLLISION_SKILLS,
explicitSelection: true, explicitSelection: true,
@@ -659,7 +668,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
entry: codex, entry: codex,
scope: 'global', scope: 'global',
sourceKind: 'path-with-spaces', sourceKind: 'path-with-spaces',
sourceArgument: sourceRoot, sourceArgument: path.join(sourceRoot, 'skills'),
sourceRoot, sourceRoot,
expectedSkills: COLLISION_SKILLS, expectedSkills: COLLISION_SKILLS,
explicitSelection: true, explicitSelection: true,
@@ -675,7 +684,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
entry: openclaw, entry: openclaw,
scope: 'project', scope: 'project',
sourceKind: 'path-with-spaces', sourceKind: 'path-with-spaces',
sourceArgument: sourceRoot, sourceArgument: path.join(sourceRoot, 'skills'),
sourceRoot, sourceRoot,
expectedSkills: ['ship'], expectedSkills: ['ship'],
explicitSelection: true, explicitSelection: true,
@@ -747,7 +756,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
platform: process.platform, platform: process.platform,
architecture: process.arch, architecture: process.arch,
repositoryRoot: repoRoot, repositoryRoot: repoRoot,
sourceProjection: 'repository-root-and-canonical-projection', sourceProjection: 'canonical-skills-subpath-and-opt-in-compatibility-root',
cli: { cli: {
executable: npxExecutable, executable: npxExecutable,
version, version,
+4 -3
View File
@@ -25,17 +25,18 @@ for arg in "$@"; do
-h|--help) -h|--help)
printf '%s\n' \ printf '%s\n' \
'Usage: ./setup [--capabilities <list>] [--replace-capabilities] [--dry-run|--install-now [--yes]|--install-later]' \ 'Usage: ./setup [--capabilities <list>] [--replace-capabilities] [--dry-run|--install-now [--yes]|--install-later]' \
' [--browser managed|installed [--browser-path <absolute-path>]]' \
' [--home <path>] [--version <version>] [--json] [--quiet]' \ ' [--home <path>] [--version <version>] [--json] [--quiet]' \
'' \ '' \
'Optional capabilities: browser, design, pdf, diagram, ios (iOS is macOS-only).' \ 'Optional capabilities: browser, design, pdf, diagram, ios (iOS is macOS-only).' \
'Without --install-now, non-interactive use previews and installs nothing.' \ 'Without --install-now, non-interactive use previews and installs nothing.' \
'--dry-run and --install-later never modify runtime state or host setup.' \ '--dry-run and --install-later never modify runtime state or host setup.' \
'Installs only the optional host-neutral runtime and selected local capabilities.' \ 'Installs only the optional host-neutral runtime and selected local capabilities.' \
'Skills are installed separately with: npx skills add time-attack/gstack' 'Skills are installed separately with: npx skills add time-attack/gstack/skills'
exit 0 exit 0
;; ;;
--local|--team|--no-team) --local|--team|--no-team)
echo "gstack setup: $arg is deprecated; skill placement is delegated to: npx skills add time-attack/gstack" >&2 echo "gstack setup: $arg is deprecated; skill placement is delegated to: npx skills add time-attack/gstack/skills" >&2
;; ;;
*) ARGS+=("$arg") ;; *) ARGS+=("$arg") ;;
esac esac
@@ -45,7 +46,7 @@ NODE_COMMAND="${GSTACK_NODE:-node}"
if ! command -v "$NODE_COMMAND" >/dev/null 2>&1; then if ! command -v "$NODE_COMMAND" >/dev/null 2>&1; then
echo "gstack setup: Node 18+ is required by the managed runtime launchers." >&2 echo "gstack setup: Node 18+ is required by the managed runtime launchers." >&2
echo "Install Node from https://nodejs.org, or install judgment-only skills with:" >&2 echo "Install Node from https://nodejs.org, or install judgment-only skills with:" >&2
echo " npx skills add time-attack/gstack" >&2 echo " npx skills add time-attack/gstack/skills" >&2
exit 1 exit 1
fi fi
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Full chain --module autoplan` `$plan --mode Full chain --module autoplan`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `autoplan` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `autoplan` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module benchmark-models` `$qa --mode Report --module benchmark-models`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `benchmark-models` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `benchmark-models` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module benchmark` `$qa --mode Report --module benchmark`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `benchmark` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `benchmark` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module browse` `$qa --mode Report --module browse`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `browse` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `browse` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module canary` `$qa --mode Report --module canary`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `canary` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `canary` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$debug --mode Diagnose-only --module careful` `$debug --mode Diagnose-only --module careful`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `careful` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill debug`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `careful` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill debug`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$review --mode Deep --module claude` `$review --mode Deep --module claude`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `claude` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill review`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `claude` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill review`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$review --mode Deep --module codex` `$review --mode Deep --module codex`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `codex` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill review`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `codex` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill review`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module context-restore` `$plan --mode Discovery --module context-restore`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `context-restore` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `context-restore` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module context-save` `$plan --mode Discovery --module context-save`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `context-save` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `context-save` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$review --mode Security --module cso` `$review --mode Security --module cso`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `cso` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill review`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `cso` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill review`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Generate --module design-consultation` `$design --mode Generate --module design-consultation`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-consultation` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-consultation` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Implement --module design-html` `$design --mode Implement --module design-html`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-html` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-html` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Implement --module design-review` `$design --mode Implement --module design-review`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Explore --module design-shotgun` `$design --mode Explore --module design-shotgun`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-shotgun` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-shotgun` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module devex-review` `$qa --mode Report --module devex-review`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `devex-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `devex-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Generate --module diagram` `$design --mode Generate --module diagram`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `diagram` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `diagram` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Prepare --module document-generate` `$ship --mode Prepare --module document-generate`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `document-generate` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `document-generate` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Prepare --module document-release` `$ship --mode Prepare --module document-release`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `document-release` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `document-release` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$debug --mode Diagnose-only --module freeze` `$debug --mode Diagnose-only --module freeze`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `freeze` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill debug`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `freeze` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill debug`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Prepare --module gstack-upgrade` `$ship --mode Prepare --module gstack-upgrade`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `gstack-upgrade` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `gstack-upgrade` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module gstack` `$plan --mode Discovery --module gstack`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `gstack` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `gstack` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$debug --mode Diagnose-only --module guard` `$debug --mode Diagnose-only --module guard`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `guard` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill debug`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `guard` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill debug`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$review --mode Deep --module health` `$review --mode Deep --module health`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `health` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill review`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `health` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill review`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$debug --mode Diagnose-only --module investigate` `$debug --mode Diagnose-only --module investigate`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `investigate` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill debug`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `investigate` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill debug`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Prepare --module ios-clean` `$ship --mode Prepare --module ios-clean`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-clean` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-clean` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Critique --module ios-design-review` `$design --mode Critique --module ios-design-review`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-design-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-design-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$debug --mode Fix --module ios-fix` `$debug --mode Fix --module ios-fix`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-fix` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill debug`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-fix` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill debug`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module ios-qa` `$qa --mode Report --module ios-qa`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-qa` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-qa` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Prepare --module ios-sync` `$ship --mode Prepare --module ios-sync`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-sync` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-sync` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Land --module land-and-deploy` `$ship --mode Land --module land-and-deploy`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `land-and-deploy` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `land-and-deploy` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Prepare --module landing-report` `$ship --mode Prepare --module landing-report`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `landing-report` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `landing-report` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module learn` `$plan --mode Discovery --module learn`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `learn` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `learn` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Generate --module make-pdf` `$design --mode Generate --module make-pdf`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `make-pdf` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `make-pdf` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module office-hours` `$plan --mode Discovery --module office-hours`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `office-hours` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `office-hours` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module open-gstack-browser` `$qa --mode Report --module open-gstack-browser`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `open-gstack-browser` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `open-gstack-browser` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module pair-agent` `$qa --mode Report --module pair-agent`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `pair-agent` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `pair-agent` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Product --module plan-ceo-review` `$plan --mode Product --module plan-ceo-review`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `plan-ceo-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `plan-ceo-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Critique --module plan-design-review` `$design --mode Critique --module plan-design-review`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `plan-design-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `plan-design-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.

Some files were not shown because too many files have changed in this diff Show More