mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 04:10:47 +02:00
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:
@@ -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
|
||||
|
||||
# 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
|
||||
# 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
|
||||
# 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
|
||||
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
|
||||
|
||||
# Verify everything works
|
||||
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" \
|
||||
|| (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 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) {
|
||||
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);
|
||||
}
|
||||
if (!/^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/.test(releaseTag)) {
|
||||
throw new Error(`Invalid runtime release tag: ${releaseTag}`);
|
||||
}
|
||||
|
||||
const targets = [
|
||||
"darwin-arm64",
|
||||
@@ -35,8 +43,8 @@ const capabilityComponents = {
|
||||
ios: ["ios"],
|
||||
};
|
||||
const commonComponents = ["core", "browser-code", "browser-headless", "browser-visible", "design", "diagram", "pdf"];
|
||||
const release = `https://github.com/${repository}/releases/download/v${version}`;
|
||||
const certificateIdentity = `https://github.com/${repository}/.github/workflows/release-artifacts.yml@refs/tags/v${version}`;
|
||||
const release = `https://github.com/${repository}/releases/download/${releaseTag}`;
|
||||
const certificateIdentity = `https://github.com/${repository}/.github/workflows/release-artifacts.yml@refs/tags/${releaseTag}`;
|
||||
const targetRecords = {};
|
||||
|
||||
for (const target of targets) {
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
fc-match -f '%{family[0]}\t%{color}\n' ':lang=und-zsye:charset=1F600' || true
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
run: bunx playwright install chromium
|
||||
run: bunx playwright-core install chromium
|
||||
|
||||
- name: Build binaries
|
||||
run: bun run build
|
||||
|
||||
@@ -2,7 +2,9 @@ name: Release runtime artifacts
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [v2.0.0]
|
||||
tags:
|
||||
- v2.0.0
|
||||
- v2.0.0-rc.*
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -51,8 +53,6 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22.23.1
|
||||
- uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||
|
||||
- name: Install frozen dependencies
|
||||
run: bun install --frozen-lockfile --ignore-scripts
|
||||
shell: bash
|
||||
@@ -70,6 +70,7 @@ jobs:
|
||||
--version 2.0.0 \
|
||||
--install-now \
|
||||
--yes \
|
||||
--browser managed \
|
||||
--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="$GSTACK_HOME/versions/$active_slot"
|
||||
@@ -112,32 +113,28 @@ jobs:
|
||||
"$GSTACK_HOME/bin/browse" stop >/dev/null 2>&1 || true
|
||||
}
|
||||
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 \
|
||||
"$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 \
|
||||
"$GSTACK_HOME/bin/browse" status
|
||||
browser_cleanup
|
||||
trap - EXIT
|
||||
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"
|
||||
for component_dir in "$stage"/*; do
|
||||
test -d "$component_dir" || continue
|
||||
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
|
||||
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
|
||||
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
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
|
||||
with:
|
||||
@@ -169,8 +166,16 @@ jobs:
|
||||
merge-multiple: true
|
||||
- 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
|
||||
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
|
||||
run: |
|
||||
@@ -188,11 +193,13 @@ jobs:
|
||||
- name: Publish immutable release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PRERELEASE_FLAG: ${{ contains(github.ref_name, '-rc.') && '--prerelease' || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release create "$GITHUB_REF_NAME" \
|
||||
--verify-tag \
|
||||
--title "GStack runtime 2.0.0" \
|
||||
--notes "Signed optional runtime artifacts for the six portable GStack skills." \
|
||||
$PRERELEASE_FLAG \
|
||||
--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/*
|
||||
shell: bash
|
||||
|
||||
@@ -65,14 +65,14 @@ jobs:
|
||||
- name: Preview without mutating state
|
||||
run: |
|
||||
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)
|
||||
shell: bash
|
||||
|
||||
- name: Explicitly install the browser capability
|
||||
run: |
|
||||
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/bin/gstack.cmd"
|
||||
shell: bash
|
||||
|
||||
@@ -60,7 +60,7 @@ they contain no copied judgment. Representative mappings:
|
||||
Canonical installation is standards-based:
|
||||
|
||||
```bash
|
||||
npx skills add time-attack/gstack
|
||||
npx skills add time-attack/gstack/skills
|
||||
```
|
||||
|
||||
Delegate host detection, placement, project/global scope, selected-skill
|
||||
|
||||
@@ -13,7 +13,7 @@ mutation boundaries, recommendations, or voice. Compatibility files under
|
||||
Specification, and Full chain. Do not expose its internal aliases as more
|
||||
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
|
||||
path. Pure judgment must work without the optional `bin/gstack` runtime.
|
||||
The host-neutral `./setup` installs only that managed runtime/capability bundle;
|
||||
|
||||
@@ -35,7 +35,7 @@ Install with the standard Agent Skills installer; it owns host detection,
|
||||
scope, destination paths, updates, removal, and selected-skill installation:
|
||||
|
||||
```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
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
@@ -96,7 +96,7 @@ surface.
|
||||
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
|
||||
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.
|
||||
|
||||
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)
|
||||
|
||||
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:
|
||||
the current branch's `./setup` is runtime-only and will not perform the
|
||||
host-specific actions shown below.
|
||||
|
||||
@@ -44,6 +44,30 @@ export function isCustomChromium(): boolean {
|
||||
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.
|
||||
*
|
||||
@@ -358,9 +382,11 @@ export class BrowserManager {
|
||||
// BROWSE_EXTENSIONS_DIR points to an unpacked Chrome extension directory.
|
||||
// Extensions only work in headed mode, so we use an off-screen window.
|
||||
const extensionsDir = process.env.BROWSE_EXTENSIONS_DIR;
|
||||
if (extensionsDir) assertHeadedBrowserProvider();
|
||||
const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth');
|
||||
const launchArgs: string[] = [...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()];
|
||||
let useHeadless = true;
|
||||
const executablePath = configuredChromiumExecutable();
|
||||
|
||||
// Docker/CI/root: Chromium sandbox requires unprivileged user namespaces which
|
||||
// are typically disabled in containers and are never available for the root
|
||||
@@ -387,7 +413,11 @@ export class BrowserManager {
|
||||
|
||||
this.browser = await chromium.launch({
|
||||
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
|
||||
// the Bun→Node process chain (GitHub #276). Disable it — local daemon
|
||||
// browsing user-specified URLs has marginal sandbox benefit. Also disabled
|
||||
@@ -447,6 +477,7 @@ export class BrowserManager {
|
||||
* every action Claude takes in real time.
|
||||
*/
|
||||
async launchHeaded(authToken?: string): Promise<void> {
|
||||
assertHeadedBrowserProvider();
|
||||
// Clear old state before repopulating
|
||||
this.pages.clear();
|
||||
this.tabSessions.clear();
|
||||
@@ -515,7 +546,7 @@ export class BrowserManager {
|
||||
|
||||
// Support custom Chromium binary via GSTACK_CHROMIUM_PATH env var.
|
||||
// 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.
|
||||
// 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
|
||||
*/
|
||||
async handoff(message: string): Promise<string> {
|
||||
assertHeadedBrowserProvider();
|
||||
if (this.connectionMode === 'headed' || this.isHeaded) {
|
||||
return `HANDOFF: Already in headed mode at ${this.getCurrentUrl()}`;
|
||||
}
|
||||
|
||||
+10
-5
@@ -118,7 +118,7 @@ interface ServerState {
|
||||
serverPath: string;
|
||||
binaryVersion?: string;
|
||||
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;
|
||||
/** Xvfb child PID for cleanup on disconnect. */
|
||||
xvfbPid?: number;
|
||||
@@ -431,8 +431,8 @@ async function ensureServer(flags?: GlobalFlags): Promise<ServerState> {
|
||||
// hint. No silent restart — that would drop tab state, cookies, and
|
||||
// logged-in sessions without warning.
|
||||
if (desiredHash && state.configHash && state.configHash !== desiredHash) {
|
||||
console.error(`[browse] existing daemon has different config (proxy/headed mismatch).`);
|
||||
console.error(`[browse] run 'browse disconnect' first to apply --proxy/--headed.`);
|
||||
console.error(`[browse] existing daemon has different config (browser provider, proxy, or headed mode).`);
|
||||
console.error(`[browse] run 'browse disconnect' first to apply the selected browser configuration.`);
|
||||
process.exit(1);
|
||||
}
|
||||
// Same path: existing daemon is plain (no flags) but caller passes
|
||||
@@ -782,7 +782,7 @@ export interface GlobalFlags {
|
||||
proxyUrl: string | null;
|
||||
/** Whether --headed was passed. */
|
||||
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;
|
||||
/** Redacted form of proxyUrl, safe for logs. */
|
||||
redactedProxyUrl: string;
|
||||
@@ -842,7 +842,12 @@ export function extractGlobalFlags(rawArgs: string[], env: NodeJS.ProcessEnv): G
|
||||
args: out,
|
||||
proxyUrl: canonicalProxyUrl,
|
||||
headed,
|
||||
configHash: computeConfigHash({ proxyUrl: canonicalProxyUrl, headed }),
|
||||
configHash: computeConfigHash({
|
||||
proxyUrl: canonicalProxyUrl,
|
||||
headed,
|
||||
browserProvider: env.GSTACK_BROWSER_PROVIDER,
|
||||
browserExecutable: env.GSTACK_CHROMIUM_PATH,
|
||||
}),
|
||||
redactedProxyUrl: redactProxyUrl(canonicalProxyUrl),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* same machine and survives daemon restarts via the state file.
|
||||
*
|
||||
@@ -135,9 +135,18 @@ export function toUpstreamConfig(cfg: ParsedProxyConfig): UpstreamConfig {
|
||||
export function computeConfigHash(opts: {
|
||||
proxyUrl: string | null | undefined;
|
||||
headed: boolean;
|
||||
browserProvider?: string | null;
|
||||
browserExecutable?: string | null;
|
||||
}): string {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -355,11 +355,18 @@ export async function handleWriteCommand(
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Enhanced error guidance: clicking <option> elements always fails (not visible / timeout)
|
||||
const isOption = 'locator' in resolved
|
||||
? await resolved.locator.evaluate(el => el.tagName === 'OPTION').catch(() => false)
|
||||
: await target.locator(resolved.selector).evaluate(
|
||||
el => el.tagName === 'OPTION'
|
||||
).catch(() => false);
|
||||
// Do not start a second auto-wait after the click has already timed out.
|
||||
// Missing selectors used to spend 5s in click(), then block again in
|
||||
// evaluate() until the outer client killed the command. count() is an
|
||||
// immediate query and keeps the helpful option guidance only when one
|
||||
// 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) {
|
||||
throw new Error(
|
||||
`Cannot click <option> elements. Use 'browse select <parent-select> <value>' instead of 'click' for dropdown options.`
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||||
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 { handleReadCommand as _handleReadCommand, parseOutArgs, hasOutArg, resultToString } from '../src/read-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) =>
|
||||
_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) ───
|
||||
describe('parseOutArgs / hasOutArg', () => {
|
||||
test('--out <path> splits the flag from the positional', () => {
|
||||
@@ -459,6 +478,20 @@ describe('Interaction', () => {
|
||||
}
|
||||
}, 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 () => {
|
||||
const result = await handleWriteCommand('hover', ['h1'], bm);
|
||||
expect(result).toContain('Hovered');
|
||||
|
||||
@@ -92,6 +92,47 @@ describe('D2 daemon-mismatch refuse (CLI integration)', () => {
|
||||
}
|
||||
}, 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 () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-mismatch-plain-'));
|
||||
const stateFile = path.join(tmpDir, 'browse.json');
|
||||
|
||||
@@ -186,4 +186,20 @@ describe('extractGlobalFlags', () => {
|
||||
);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"diff": "^9.0.0",
|
||||
"html-to-docx": "1.8.0",
|
||||
"marked": "^18.0.6",
|
||||
"playwright": "^1.58.2",
|
||||
"playwright": "npm:playwright-core@^1.58.2",
|
||||
"sharp": "^0.34.5",
|
||||
"socks": "^2.8.9",
|
||||
"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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -172,17 +172,17 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -266,11 +266,11 @@
|
||||
|
||||
"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-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=="],
|
||||
|
||||
@@ -286,8 +286,6 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -350,7 +348,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -368,7 +366,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -386,7 +384,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -422,9 +420,7 @@
|
||||
|
||||
"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-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
|
||||
"playwright": ["playwright-core@1.61.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -458,7 +454,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -478,7 +474,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -510,9 +506,9 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -542,7 +538,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -554,6 +550,8 @@
|
||||
|
||||
"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/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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -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:
|
||||
|
||||
```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
|
||||
|
||||
@@ -18,7 +18,7 @@ bundle.
|
||||
## Canonical installation
|
||||
|
||||
```bash
|
||||
npx skills add time-attack/gstack
|
||||
npx skills add time-attack/gstack/skills
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"source_path": "office-hours/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
|
||||
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
||||
"normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||
"target": "skills/plan/references/legacy/office-hours.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -1416,7 +1416,7 @@
|
||||
"source_path": "design-consultation/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
|
||||
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
||||
"normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||
"target": "skills/design/references/legacy/design-consultation.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -1591,7 +1591,7 @@
|
||||
"source_path": "design-html/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
|
||||
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30",
|
||||
"normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
|
||||
"target": "skills/design/references/legacy/design-html.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -1820,7 +1820,7 @@
|
||||
"source_path": "design-review/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
|
||||
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
||||
"normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||
"target": "skills/design/references/legacy/design-review.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2162,7 +2162,7 @@
|
||||
"source_path": "qa/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
|
||||
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
||||
"normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||
"target": "skills/qa/references/legacy/qa.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2288,7 +2288,7 @@
|
||||
"source_path": "qa-only/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
|
||||
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
||||
"normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||
"target": "skills/qa/references/legacy/qa-only.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2466,7 +2466,7 @@
|
||||
"source_path": "devex-review/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
|
||||
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1",
|
||||
"normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
|
||||
"target": "skills/qa/references/legacy/devex-review.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2565,7 +2565,7 @@
|
||||
"source_path": "benchmark/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
|
||||
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d",
|
||||
"normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
|
||||
"target": "skills/qa/references/legacy/benchmark.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2631,7 +2631,7 @@
|
||||
"source_path": "canary/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
|
||||
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef",
|
||||
"normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
|
||||
"target": "skills/qa/references/legacy/canary.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2710,7 +2710,7 @@
|
||||
"source_path": "browse/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
|
||||
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b",
|
||||
"normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
|
||||
"target": "skills/qa/references/legacy/browse.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2798,7 +2798,7 @@
|
||||
"source_path": "open-gstack-browser/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
|
||||
"normalized_render_sha256": "df626d71b8cea4a02d2fb7aef3169563dd132bf17a9d6d84f287894cad84d2cf",
|
||||
"normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
|
||||
"target": "skills/qa/references/legacy/open-gstack-browser.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2873,7 +2873,7 @@
|
||||
"source_path": "setup-browser-cookies/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
|
||||
"normalized_render_sha256": "04c161a58c1a9010efe38095b383b0e1d445a2b678e5bf931a1281d45196940d",
|
||||
"normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
|
||||
"target": "skills/qa/references/legacy/setup-browser-cookies.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2934,7 +2934,7 @@
|
||||
"source_path": "pair-agent/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
|
||||
"normalized_render_sha256": "8557ca390d0b6548f956d2c0e9316f1cf137689d4dc17d40a4525d19f22bc457",
|
||||
"normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
|
||||
"target": "skills/qa/references/legacy/pair-agent.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -4136,7 +4136,7 @@
|
||||
"source_path": "land-and-deploy/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
|
||||
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd",
|
||||
"normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
|
||||
"target": "skills/ship/references/legacy/land-and-deploy.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -4552,7 +4552,7 @@
|
||||
"source_path": "gstack-upgrade/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d",
|
||||
"normalized_render_sha256": "a913cf77f76c4d68c576a06190b498e3d8d3b60b85498a68f173b23a7f800828",
|
||||
"normalized_render_sha256": "2c8cf9e505b7da27730bf646a33ee38e8092259f3335a524c71e57ba5b602541",
|
||||
"target": "skills/ship/references/legacy/gstack-upgrade.md",
|
||||
"disposition": "DUPLICATE_INFRASTRUCTURE",
|
||||
"overlays": [
|
||||
@@ -5775,7 +5775,7 @@
|
||||
"owner_tree": "qa",
|
||||
"consumer_tree": "ship",
|
||||
"target": "skills/ship/references/legacy/canary.md",
|
||||
"sha256": "9fcf4fdea7d52113c8f5b5cc81c1fb36df591cf9e07bcc38c4ee106b15245997",
|
||||
"sha256": "155174e829ef7895707ff1fad1f130fc1c7150bad9fb3598428a2efc939c5ad6",
|
||||
"disposition": "SHARED_MODULE"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
counter. They remain byte-equal and retain the existing four-slot format so
|
||||
the 1.x compatibility ship queue does not silently fail open.
|
||||
- `package.json.gstack.runtimeVersion`, `runtime/index.js`,
|
||||
`runtime/install.js`, and every standards-installed bootstrap declare the
|
||||
managed-runtime protocol release `2.0.0`. The official artifact tag and
|
||||
manifest use that value.
|
||||
- `package.json.gstack.runtimeVersion`, `runtime/index.js`, and
|
||||
`runtime/install.js` declare the managed-runtime protocol release `2.0.0`.
|
||||
Each standards-installed bootstrap separately pins one immutable artifact
|
||||
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
|
||||
drifts inside its own namespace.
|
||||
@@ -24,6 +25,14 @@ linux-arm64 linux-x64 (glibc)
|
||||
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
|
||||
count and SHA-256, signs the archive keylessly with Cosign, emits a Sigstore
|
||||
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.
|
||||
|
||||
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
|
||||
slot, and launch-smokes that exact Chromium on every native release runner.
|
||||
The stable capability launcher sets `PLAYWRIGHT_BROWSERS_PATH` to the active
|
||||
|
||||
+16
-4
@@ -57,13 +57,13 @@ PR, or PR-ready claim is authorized by this status.
|
||||
three retained Claude Haiku live samples are classified `REGRESSION`; they
|
||||
are preserved as noisy supplemental evidence, never cherry-picked as a
|
||||
primary gate or represented as green.
|
||||
- [x] The current macOS GStack 2 suite is green: 151 pass / 0 fail and 1,194
|
||||
assertions across 16 files.
|
||||
- [x] The current macOS GStack 2 suite is green: 218 pass / 0 fail and 2,229
|
||||
assertions across 20 files.
|
||||
- [x] Optional host-neutral runtime implemented with canonical paths,
|
||||
repo/worktree state identity, locks, atomic writes, effect claims,
|
||||
doctor/config/state/cleanup, migrations, upgrade/rollback, and uninstall.
|
||||
- [x] Managed runtime installer coverage is green at 25 pass / 0 fail and 341
|
||||
assertions. The deterministic clean macOS arm64 managed-bundle audit records
|
||||
- [x] Managed runtime installer coverage is green at 34 pass / 0 fail. The
|
||||
deterministic clean macOS arm64 managed-bundle audit records
|
||||
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;
|
||||
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
|
||||
browser journey and Sharp full-page screenshot, and uninstalled while
|
||||
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
|
||||
executable under `.gstack-runtime-tools`, records its path/version in the
|
||||
bundle manifest, vendors the tagged license/source notices, and routes the
|
||||
|
||||
@@ -10,7 +10,7 @@ pass from deterministic, offline, or filesystem-only evidence.
|
||||
| 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. |
|
||||
| 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 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. |
|
||||
@@ -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). |
|
||||
| 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). |
|
||||
| `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`. |
|
||||
| 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. |
|
||||
|
||||
@@ -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:
|
||||
|
||||
```bash
|
||||
npx skills add time-attack/gstack
|
||||
npx skills add time-attack/gstack/skills
|
||||
npx skills update # interactive scope
|
||||
npx skills update -p # project installs only
|
||||
npx skills update -g # global installs only
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "benchmark/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
|
||||
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d",
|
||||
"normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
|
||||
"target": "skills/qa/references/legacy/benchmark.md",
|
||||
"overlays": [
|
||||
679
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "browse/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
|
||||
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b",
|
||||
"normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
|
||||
"target": "skills/qa/references/legacy/browse.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "canary/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
|
||||
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef",
|
||||
"normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
|
||||
"target": "skills/qa/references/legacy/canary.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "design-consultation/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
|
||||
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
||||
"normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||
"target": "skills/design/references/legacy/design-consultation.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "design-html/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
|
||||
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30",
|
||||
"normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
|
||||
"target": "skills/design/references/legacy/design-html.md",
|
||||
"overlays": [
|
||||
679
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "design-review/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
|
||||
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
||||
"normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||
"target": "skills/design/references/legacy/design-review.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "devex-review/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
|
||||
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1",
|
||||
"normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
|
||||
"target": "skills/qa/references/legacy/devex-review.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "gstack-upgrade/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d",
|
||||
"normalized_render_sha256": "a913cf77f76c4d68c576a06190b498e3d8d3b60b85498a68f173b23a7f800828",
|
||||
"normalized_render_sha256": "2c8cf9e505b7da27730bf646a33ee38e8092259f3335a524c71e57ba5b602541",
|
||||
"target": "skills/ship/references/legacy/gstack-upgrade.md",
|
||||
"overlays": [
|
||||
679
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "land-and-deploy/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
|
||||
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd",
|
||||
"normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
|
||||
"target": "skills/ship/references/legacy/land-and-deploy.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "office-hours/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
|
||||
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
||||
"normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||
"target": "skills/plan/references/legacy/office-hours.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "open-gstack-browser/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
|
||||
"normalized_render_sha256": "df626d71b8cea4a02d2fb7aef3169563dd132bf17a9d6d84f287894cad84d2cf",
|
||||
"normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
|
||||
"target": "skills/qa/references/legacy/open-gstack-browser.md",
|
||||
"overlays": [
|
||||
679
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "pair-agent/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
|
||||
"normalized_render_sha256": "8557ca390d0b6548f956d2c0e9316f1cf137689d4dc17d40a4525d19f22bc457",
|
||||
"normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
|
||||
"target": "skills/qa/references/legacy/pair-agent.md",
|
||||
"overlays": [
|
||||
679
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "qa-only/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
|
||||
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
||||
"normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||
"target": "skills/qa/references/legacy/qa-only.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "qa/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
|
||||
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
||||
"normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||
"target": "skills/qa/references/legacy/qa.md",
|
||||
"overlays": [
|
||||
679,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"source_path": "setup-browser-cookies/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
|
||||
"normalized_render_sha256": "04c161a58c1a9010efe38095b383b0e1d445a2b678e5bf931a1281d45196940d",
|
||||
"normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
|
||||
"target": "skills/qa/references/legacy/setup-browser-cookies.md",
|
||||
"overlays": [
|
||||
679
|
||||
|
||||
+16
-16
@@ -83,7 +83,7 @@
|
||||
"source_path": "office-hours/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "8568fe73cca76a80805fab3092cacd10db7e1d7f",
|
||||
"normalized_render_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
||||
"normalized_render_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||
"target": "skills/plan/references/legacy/office-hours.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -1416,7 +1416,7 @@
|
||||
"source_path": "design-consultation/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "64af56ecdbd132cb7c28344e8e4ecb2e5dacf811",
|
||||
"normalized_render_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
||||
"normalized_render_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||
"target": "skills/design/references/legacy/design-consultation.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -1591,7 +1591,7 @@
|
||||
"source_path": "design-html/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "3cdec9a14d62d2e046ed924c972efc30a7d43aca",
|
||||
"normalized_render_sha256": "40682d97ac83aa9178487348d5abf176334fd439e2d12f8e5cda1f8b20cd2c30",
|
||||
"normalized_render_sha256": "775dfc9fdcc6b96d6e267f2b8c5e7eedcf8a1d98b764c134d7111a39f3f07301",
|
||||
"target": "skills/design/references/legacy/design-html.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -1820,7 +1820,7 @@
|
||||
"source_path": "design-review/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "bdcda48e29b489a1cc49faa333922412251d4b41",
|
||||
"normalized_render_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
||||
"normalized_render_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||
"target": "skills/design/references/legacy/design-review.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2162,7 +2162,7 @@
|
||||
"source_path": "qa/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "11997f7b878282c34b6bfd3d4b7a8131f9ad4da8",
|
||||
"normalized_render_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
||||
"normalized_render_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||
"target": "skills/qa/references/legacy/qa.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2288,7 +2288,7 @@
|
||||
"source_path": "qa-only/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "75c4123cc5c406ffdd36c71a094335c137135b1e",
|
||||
"normalized_render_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
||||
"normalized_render_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||
"target": "skills/qa/references/legacy/qa-only.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2466,7 +2466,7 @@
|
||||
"source_path": "devex-review/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "081d4f35bbdec0c6b3da8ae71615ec4d41a84551",
|
||||
"normalized_render_sha256": "4a907c759b6cf4202fbacaea504b1eb601a53dd35b206109d6c5105168ade7e1",
|
||||
"normalized_render_sha256": "6b26b22ae5cbe9483a10ad084cd6b1e8ea32d2c01e482f75b9f8b32944287d0e",
|
||||
"target": "skills/qa/references/legacy/devex-review.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2565,7 +2565,7 @@
|
||||
"source_path": "benchmark/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "038f16f5fba4ae4e9eae922e3276bba8ef88149e",
|
||||
"normalized_render_sha256": "05ac1b123a605201546a7e95899a5b55708ca7fbb7569c58b4d755c52b45a92d",
|
||||
"normalized_render_sha256": "5fd14a7da7e31c24451c26d9123fcbcc376b7875c1a723bf7b69a1474e4f1c6d",
|
||||
"target": "skills/qa/references/legacy/benchmark.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2631,7 +2631,7 @@
|
||||
"source_path": "canary/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "d1eb2950aba2fa2b09d90f13143492c60d46793c",
|
||||
"normalized_render_sha256": "89be5f218da2bd812303c87b8c177081727727e5e0d2dc74eb7a73299794d5ef",
|
||||
"normalized_render_sha256": "b7f753ba0b98d8c7378dc797dca5950b14ebe26565bac25b5bbaa56b8ea8e13b",
|
||||
"target": "skills/qa/references/legacy/canary.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2710,7 +2710,7 @@
|
||||
"source_path": "browse/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "9a159e4c9820172c229e2174d4a62a8f9668ab93",
|
||||
"normalized_render_sha256": "1b532bd904b1fa1686113e8c96b70015ea6b2e6df7319a72c299de901fe5e81b",
|
||||
"normalized_render_sha256": "fee4ae0bd69412a6c3b1fd6737064301240b39731695b53fa10096ba48495019",
|
||||
"target": "skills/qa/references/legacy/browse.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2798,7 +2798,7 @@
|
||||
"source_path": "open-gstack-browser/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "ef91a527890a3ac3622cc7dc84bad1ff7b64443b",
|
||||
"normalized_render_sha256": "df626d71b8cea4a02d2fb7aef3169563dd132bf17a9d6d84f287894cad84d2cf",
|
||||
"normalized_render_sha256": "f32ab85292ae920d811f4014480c48941d9b34d3ad8141840b4fd33bfdccc7dd",
|
||||
"target": "skills/qa/references/legacy/open-gstack-browser.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2873,7 +2873,7 @@
|
||||
"source_path": "setup-browser-cookies/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "f812d9f56f27c32fb5f102083bbe418344c1a652",
|
||||
"normalized_render_sha256": "04c161a58c1a9010efe38095b383b0e1d445a2b678e5bf931a1281d45196940d",
|
||||
"normalized_render_sha256": "9e8ee39b557d1fbd032a94f5fbe16b675bdd89fe64b1c85a9af6a2ebe54aa976",
|
||||
"target": "skills/qa/references/legacy/setup-browser-cookies.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -2934,7 +2934,7 @@
|
||||
"source_path": "pair-agent/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "75ed42d590f99c46cd0883c37bb1f2f9f499211c",
|
||||
"normalized_render_sha256": "8557ca390d0b6548f956d2c0e9316f1cf137689d4dc17d40a4525d19f22bc457",
|
||||
"normalized_render_sha256": "e75661246495412102632a49d626bc313875ef479d2c570002ec66a2ccd2757a",
|
||||
"target": "skills/qa/references/legacy/pair-agent.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -4136,7 +4136,7 @@
|
||||
"source_path": "land-and-deploy/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "98976ad020d541d251cc7e34802a13458ddc88e2",
|
||||
"normalized_render_sha256": "6920f3d97ce474b8f20c8b3e38ca9d3c03973e47af33103a60bab7c02eb867bd",
|
||||
"normalized_render_sha256": "405924730c4e328c1a45de26576cda686d0d7da1fc4a36e840458683e1396aa2",
|
||||
"target": "skills/ship/references/legacy/land-and-deploy.md",
|
||||
"disposition": "BUG_FIX",
|
||||
"overlays": [
|
||||
@@ -4552,7 +4552,7 @@
|
||||
"source_path": "gstack-upgrade/SKILL.md.tmpl",
|
||||
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
|
||||
"blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d",
|
||||
"normalized_render_sha256": "a913cf77f76c4d68c576a06190b498e3d8d3b60b85498a68f173b23a7f800828",
|
||||
"normalized_render_sha256": "2c8cf9e505b7da27730bf646a33ee38e8092259f3335a524c71e57ba5b602541",
|
||||
"target": "skills/ship/references/legacy/gstack-upgrade.md",
|
||||
"disposition": "DUPLICATE_INFRASTRUCTURE",
|
||||
"overlays": [
|
||||
@@ -5775,7 +5775,7 @@
|
||||
"owner_tree": "qa",
|
||||
"consumer_tree": "ship",
|
||||
"target": "skills/ship/references/legacy/canary.md",
|
||||
"sha256": "9fcf4fdea7d52113c8f5b5cc81c1fb36df591cf9e07bcc38c4ee106b15245997",
|
||||
"sha256": "155174e829ef7895707ff1fad1f130fc1c7150bad9fb3598428a2efc939c5ad6",
|
||||
"disposition": "SHARED_MODULE"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -57,18 +57,18 @@
|
||||
}
|
||||
},
|
||||
"mechanical_port": {
|
||||
"rendered_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
||||
"rendered_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||
"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."
|
||||
},
|
||||
"candidate": {
|
||||
"target_path": "skills/design/references/legacy/design-consultation.md",
|
||||
"rendered_legacy_body_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
||||
"rendered_legacy_body_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||
"semantic_signature": {
|
||||
"normalized_sha256": "d323457820291635bc4c46e4559ce6f4d194b940607b76208e95df0c86ffcb0b",
|
||||
"normalized_sha256": "13d5aa11be43cf78f7d77b9f8da081c5fedd3b7e767815ff9d650c6bc5d0738b",
|
||||
"headings_sha256": "78cdfd5aa0c0264964542d45190c919c7adb8c01ea68a22d67dd0fad538cc643",
|
||||
"questions_sha256": "91adedef9aa8000a9aa3385381227d149dbc52cbf2d127656bf0697510b62906",
|
||||
"obligations_sha256": "c36974d06c0f66d0a0f2ea3d27282ed785c7cc956cce40a8c8db35492680c972",
|
||||
"obligations_sha256": "fa0722c312610cfe74c70b1e29f447622b360cb894f3c697b3ca959c3a068ddb",
|
||||
"heading_count": 12,
|
||||
"question_count": 2,
|
||||
"obligation_count": 18
|
||||
|
||||
@@ -57,18 +57,18 @@
|
||||
}
|
||||
},
|
||||
"mechanical_port": {
|
||||
"rendered_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
||||
"rendered_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||
"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."
|
||||
},
|
||||
"candidate": {
|
||||
"target_path": "skills/design/references/legacy/design-review.md",
|
||||
"rendered_legacy_body_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
||||
"rendered_legacy_body_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||
"semantic_signature": {
|
||||
"normalized_sha256": "fe15a4fae62fba41432ae18bbf4ef5620058b784b7bf9768304d0d1dd17bf45b",
|
||||
"normalized_sha256": "9d6828dd60fbe4ab9647c5f4456b0953c514ce22ddb1a9c1c3e572c70491f900",
|
||||
"headings_sha256": "739abb73da8446c6133835f56ea0c862a7c1fd57e6702a6afe1ed1fd2fb2b2c8",
|
||||
"questions_sha256": "57579e50936d62734df12299638bdcf8ce412f24b68688b071be156312aafcbc",
|
||||
"obligations_sha256": "e4fe261ecc3956834dfac15cac60fc4b8057ad0c3b37ede06ee4667d61852655",
|
||||
"obligations_sha256": "3e3b3b8a8e43c0120d60f5517cc03aa64bb31bf50adbe418ba89a7ae63557a55",
|
||||
"heading_count": 62,
|
||||
"question_count": 24,
|
||||
"obligation_count": 63
|
||||
|
||||
@@ -58,18 +58,18 @@
|
||||
}
|
||||
},
|
||||
"mechanical_port": {
|
||||
"rendered_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
||||
"rendered_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||
"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."
|
||||
},
|
||||
"candidate": {
|
||||
"target_path": "skills/plan/references/legacy/office-hours.md",
|
||||
"rendered_legacy_body_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
||||
"rendered_legacy_body_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||
"semantic_signature": {
|
||||
"normalized_sha256": "1a5c9dbda769631df4c3e909fde6b97917780f6a7e9eca5a4edc8c2d0f302052",
|
||||
"normalized_sha256": "ebb8816907a17722d1e1d227de782684870805368fcdc07f69347f32d907a9ba",
|
||||
"headings_sha256": "f50aa767e26cdfc3c8fa4c6bdcabd10e061ca0ab59e2aff4c204eaf68bfd1d56",
|
||||
"questions_sha256": "92d854d47b2f92f30cc34a59cfb6f3e18449beddcb8ebd5490162545bc8384c7",
|
||||
"obligations_sha256": "c6f9f220f431b50b7721ac9baa3ae3d7be5de88b489c80a45350028c96a9db84",
|
||||
"obligations_sha256": "a8abcb960325c146cb555a71821d702b593bb7dc4f12646d4ecaea11a9cf6b68",
|
||||
"heading_count": 34,
|
||||
"question_count": 13,
|
||||
"obligation_count": 43
|
||||
|
||||
@@ -57,18 +57,18 @@
|
||||
}
|
||||
},
|
||||
"mechanical_port": {
|
||||
"rendered_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
||||
"rendered_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||
"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."
|
||||
},
|
||||
"candidate": {
|
||||
"target_path": "skills/qa/references/legacy/qa.md",
|
||||
"rendered_legacy_body_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
||||
"rendered_legacy_body_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||
"semantic_signature": {
|
||||
"normalized_sha256": "e7cd5615adaf54413daa97838cb364810317dd7d661cec5cc4ed40eb48192e55",
|
||||
"normalized_sha256": "63135ad3f73ea195fffc535166396bbf66bc223378686670c6d7d362f80e5848",
|
||||
"headings_sha256": "dd0b56f79cee31c4c3c71f32fb0a4438f686c6ae517625a7d59475deecd4c75e",
|
||||
"questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1",
|
||||
"obligations_sha256": "c9a3696553c10b1338568d728f86f47c9a7a06b2defa37bd557c662d4a9086d8",
|
||||
"obligations_sha256": "8c9e044c4224e4c35838d6ae9241405a5df42c2372c2ce272e0a11f37ab68c86",
|
||||
"heading_count": 58,
|
||||
"question_count": 6,
|
||||
"obligation_count": 65
|
||||
|
||||
@@ -57,18 +57,18 @@
|
||||
}
|
||||
},
|
||||
"mechanical_port": {
|
||||
"rendered_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
||||
"rendered_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||
"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."
|
||||
},
|
||||
"candidate": {
|
||||
"target_path": "skills/qa/references/legacy/qa-only.md",
|
||||
"rendered_legacy_body_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
||||
"rendered_legacy_body_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||
"semantic_signature": {
|
||||
"normalized_sha256": "376eff42459f5b8755bd95934cce615db0fca16504c8e82f84b2704c63f62af3",
|
||||
"normalized_sha256": "601eded52ee9e7c5c5ad7c0ce8a7d63377aa64cdaa56a90c0fe40f972939794a",
|
||||
"headings_sha256": "0241f7efa9ffcaef764bd6517099b03394e09f90c8d1f3f015c7c7a135f78201",
|
||||
"questions_sha256": "8fece376011d8a8606bcd05d52332f77b56de551b7f1abde94bff1f33ef0a9c1",
|
||||
"obligations_sha256": "9ea973707574aa3750e38a2920e65072f804fe7cddf789f1710e08b0a44d92d3",
|
||||
"obligations_sha256": "f16cce08f386155cae52758fb3f7235e1c8090ca7d84117caa4761b76927d9d5",
|
||||
"heading_count": 34,
|
||||
"question_count": 6,
|
||||
"obligation_count": 40
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@
|
||||
"diff": "^9.0.0",
|
||||
"html-to-docx": "1.8.0",
|
||||
"marked": "^18.0.6",
|
||||
"playwright": "^1.58.2",
|
||||
"playwright": "npm:playwright-core@^1.58.2",
|
||||
"sharp": "^0.34.5",
|
||||
"socks": "^2.8.9",
|
||||
"xterm": "5",
|
||||
|
||||
@@ -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
@@ -9,10 +9,12 @@ import { setupRuntime } from "./setup.js";
|
||||
import {
|
||||
configGet,
|
||||
configSet,
|
||||
configSetBrowserChoice,
|
||||
configSetNetworkChoice,
|
||||
parseConfigValue,
|
||||
secretSet,
|
||||
} from "./config.js";
|
||||
import { resolveBrowserChoice } from "./browser-choice.mjs";
|
||||
import { discoverProjectIdentity } from "./identity.js";
|
||||
import {
|
||||
beginRun,
|
||||
@@ -229,12 +231,88 @@ async function configCommand({ args, home, cwd, stdout }) {
|
||||
if (action === "set") {
|
||||
const [key, value, ...rest] = tail;
|
||||
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 });
|
||||
const result = await withOwnedRuntimeMutation(home, () => configSet(home, key, parseConfigValue(value)));
|
||||
write(stdout, `${key} = ${typeof result === "string" ? result : JSON.stringify(result)}\n`);
|
||||
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 }) {
|
||||
@@ -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.flags.has("--rollback")) {
|
||||
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`);
|
||||
return 0;
|
||||
}
|
||||
@@ -674,10 +756,22 @@ async function upgradeCommand({ args, home, stdout, installOptions = {} }) {
|
||||
if (!sourceDir || !version) {
|
||||
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({
|
||||
home,
|
||||
sourceDir,
|
||||
version,
|
||||
...(browserChoice ? { browserChoice } : {}),
|
||||
...installOptions,
|
||||
buildMissing: false,
|
||||
rejectSourceRootLink: true,
|
||||
@@ -828,6 +922,7 @@ function usage() {
|
||||
" gstack runtime path <bundle-relative-path>\n" +
|
||||
" gstack config get [key]\n" +
|
||||
" gstack config set <key> <value>\n" +
|
||||
" gstack config browser managed|installed <absolute-path>|clear\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 update <run-id> [--plan <pointer>|--clear-plan] [--stage <stage>] [--depth quick|standard|deep] [--mutation <authority>] [--modules <a,b>] [--push-detour <goal>|--pop-detour]\n" +
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { atomicWriteJson, readJson, withLock } from "./storage.js";
|
||||
import { resolveRuntimePaths } from "./paths.js";
|
||||
import { BROWSER_PROVIDERS } from "./browser-choice.mjs";
|
||||
|
||||
export const DEFAULT_CONFIG = Object.freeze({
|
||||
schemaVersion: 2,
|
||||
@@ -10,6 +11,7 @@ export const DEFAULT_CONFIG = Object.freeze({
|
||||
baseUrl: "https://api.context.dev/v1",
|
||||
validation: Object.freeze({ status: "unverified", checkedAt: null }),
|
||||
}),
|
||||
browser: Object.freeze({ provider: null, executablePath: null }),
|
||||
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) {
|
||||
const paths = resolveRuntimePaths({ home });
|
||||
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");
|
||||
}
|
||||
}
|
||||
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() {
|
||||
@@ -223,6 +262,7 @@ function mergeDefaults(stored) {
|
||||
...stored,
|
||||
network: { ...DEFAULT_CONFIG.network, ...(stored.network ?? {}) },
|
||||
context: { ...DEFAULT_CONFIG.context, ...(stored.context ?? {}) },
|
||||
browser: { ...DEFAULT_CONFIG.browser, ...(stored.browser ?? {}) },
|
||||
cleanup: { ...DEFAULT_CONFIG.cleanup, ...(stored.cleanup ?? {}) },
|
||||
};
|
||||
}
|
||||
|
||||
+71
-11
@@ -10,6 +10,7 @@ import { RUNTIME_SCHEMA_VERSION, RUNTIME_MIGRATION_ID } from "./migrations.js";
|
||||
import { assertManagedHome } from "./managed-home.js";
|
||||
import { recoverPendingUpgrade } from "./upgrade.js";
|
||||
import { bashCandidates } from "./tooling.js";
|
||||
import { resolveBrowserChoice } from "./browser-choice.mjs";
|
||||
import {
|
||||
OPTIONAL_RUNTIME_CAPABILITIES,
|
||||
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 now = options.now ? options.now() : new Date();
|
||||
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)) {
|
||||
throw new TypeError("Expected skill API must be a short version identifier");
|
||||
}
|
||||
@@ -59,12 +61,16 @@ export async function runDoctor(options = {}) {
|
||||
}
|
||||
|
||||
try {
|
||||
const config = await readJson(paths.config);
|
||||
add("config", config?.schemaVersion <= RUNTIME_SCHEMA_VERSION ? "pass" : "fail",
|
||||
`Config schema ${config?.schemaVersion ?? "unknown"}`);
|
||||
const enabled = config?.network?.mode === "context" && config?.network?.consent === true;
|
||||
runtimeConfig = await readJson(paths.config);
|
||||
add("config", runtimeConfig?.schemaVersion <= RUNTIME_SCHEMA_VERSION ? "pass" : "fail",
|
||||
`Config schema ${runtimeConfig?.schemaVersion ?? "unknown"}`);
|
||||
const enabled = runtimeConfig?.network?.mode === "context" && runtimeConfig?.network?.consent === true;
|
||||
add("network", enabled ? "pass" : "warn",
|
||||
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) {
|
||||
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);
|
||||
const selected = new Set(Array.isArray(manifest?.selectedCapabilities) ? manifest.selectedCapabilities : []);
|
||||
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)) {
|
||||
add(`capability:${capability}`, "warn", "not selected");
|
||||
continue;
|
||||
@@ -160,8 +170,24 @@ export async function runDoctor(options = {}) {
|
||||
add(`capability:${capability}`, "fail", "selected but required launcher metadata is missing");
|
||||
continue;
|
||||
}
|
||||
if (capability === "browser") {
|
||||
const browser = await inspectManagedChromium(activeRoot, options.nodeCommand ?? process.env.GSTACK_NODE ?? "node");
|
||||
if (capability === "browser" || capability === "browser-visible") {
|
||||
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);
|
||||
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" };
|
||||
}
|
||||
|
||||
async function inspectManagedChromium(activeRoot, nodeCommand) {
|
||||
async function inspectManagedChromium(activeRoot, nodeCommand, options = {}) {
|
||||
const browserRoot = path.join(activeRoot, ".gstack-runtime-browsers");
|
||||
const modulePath = path.join(activeRoot, "node_modules", "playwright", "index.mjs");
|
||||
const [browserStat, moduleStat] = await Promise.all([
|
||||
@@ -329,16 +355,50 @@ async function inspectManagedChromium(activeRoot, nodeCommand) {
|
||||
const result = await captureCommand(nodeCommand, [
|
||||
"--input-type=module",
|
||||
"--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 } });
|
||||
const version = result.stdout.trim();
|
||||
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) {
|
||||
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() {
|
||||
if (process.platform !== "darwin") return { ok: false, message: "physical-iOS capability requires macOS" };
|
||||
try {
|
||||
@@ -350,7 +410,7 @@ async function inspectXcrun() {
|
||||
}
|
||||
|
||||
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 === "pdf") return typeof launchers["make-pdf"] === "string";
|
||||
if (capability === "diagram") return true;
|
||||
|
||||
+216
-28
@@ -20,6 +20,14 @@ import {
|
||||
} from "./managed-home.js";
|
||||
import { errorWithCode as installError } from "./errors.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;
|
||||
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("extension"),
|
||||
entry("node_modules/playwright"),
|
||||
entry("node_modules/playwright-core"),
|
||||
entry(managedBunRelativePath(), "managed-bun", true),
|
||||
entry(".gstack-runtime-browsers", "browser"),
|
||||
entry("node_modules/diff"),
|
||||
@@ -324,10 +331,11 @@ const CAPABILITY_PATH_PREFIXES = Object.freeze({
|
||||
});
|
||||
|
||||
/** 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 includesBrowserCode = selected.includes("browser") || selected.includes("browser-visible");
|
||||
const entries = DEFAULT_RUNTIME_BUNDLE.filter((item) => {
|
||||
if (options.browserChoice?.provider === "installed" && item.path === ".gstack-runtime-browsers") return false;
|
||||
const owner = capabilityForPath(item.path);
|
||||
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. */
|
||||
export function runtimeComponentsForCapabilities(input = OPTIONAL_RUNTIME_CAPABILITIES) {
|
||||
export function runtimeComponentsForCapabilities(input = OPTIONAL_RUNTIME_CAPABILITIES, options = {}) {
|
||||
const capabilities = normalizeCapabilitySelection(input);
|
||||
const selected = new Set(["core"]);
|
||||
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);
|
||||
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);
|
||||
return `${prefix}-caps-${digest}`;
|
||||
}
|
||||
@@ -368,7 +379,7 @@ export function runtimeSlotVersion(releaseVersion, capabilityIds) {
|
||||
export async function previewManagedRuntime(options = {}) {
|
||||
if (!options.sourceDir) throw installError("sourceDir is required", "INSTALL_SOURCE_REQUIRED");
|
||||
const sourceDir = await resolvePhysicalSource(options.sourceDir);
|
||||
const surface = runtimeSurfaceForCapabilities(options.capabilityIds);
|
||||
const surface = runtimeSurfaceForCapabilities(options.capabilityIds, { browserChoice: options.browserChoice });
|
||||
let bytes = 0;
|
||||
let files = 0;
|
||||
const missing = [];
|
||||
@@ -422,6 +433,7 @@ export async function previewManagedRuntime(options = {}) {
|
||||
return Object.freeze({
|
||||
sourceDir,
|
||||
capabilities: surface.selected,
|
||||
browser: browserChoiceRequired(surface.selected) ? options.browserChoice ?? null : null,
|
||||
components: surface.entries.length,
|
||||
files,
|
||||
bytes,
|
||||
@@ -463,7 +475,7 @@ export async function installManagedRuntime(options = {}) {
|
||||
if (options.requirePackageIdentity) validatePackageIdentity(packageMetadata, version);
|
||||
|
||||
const selectedSurface = options.entries == null
|
||||
? runtimeSurfaceForCapabilities(options.capabilityIds)
|
||||
? runtimeSurfaceForCapabilities(options.capabilityIds, { browserChoice: options.browserChoice })
|
||||
: null;
|
||||
const entries = normalizeEntries(options.entries ?? selectedSurface.entries);
|
||||
const capabilities = normalizeCapabilities(options.capabilities ?? selectedSurface.capabilities, entries);
|
||||
@@ -584,7 +596,15 @@ export async function installManagedRuntime(options = {}) {
|
||||
version,
|
||||
compatibility: RUNTIME_COMPATIBILITY,
|
||||
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),
|
||||
capabilities,
|
||||
stableSourceFiles,
|
||||
@@ -620,6 +640,7 @@ export async function installManagedRuntime(options = {}) {
|
||||
nodeCommand: options.nodeCommand ?? process.env.GSTACK_NODE ?? "node",
|
||||
run: options.runCommand ?? runCommand,
|
||||
commandTimeoutMs: options.commandTimeoutMs,
|
||||
browserChoice: selectedSurface ? options.browserChoice : null,
|
||||
});
|
||||
},
|
||||
beforeActivate: async ({ active, previous, previousExists, destination }) => {
|
||||
@@ -632,6 +653,7 @@ export async function installManagedRuntime(options = {}) {
|
||||
await removeObsoleteLaunchers(paths, snapshot, launcherSurface);
|
||||
const manifestWriter = options.manifestWriter ?? writeInstallManifest;
|
||||
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 }),
|
||||
onRollback: async ({ pointerRollbackError }) => {
|
||||
@@ -898,6 +920,26 @@ export async function smokeRuntimeBundle(directory, options = {}) {
|
||||
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 bunCommand = parsed.bunCommand ?? env.BUN_CMD ?? "bun";
|
||||
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(
|
||||
stdin,
|
||||
options.stderr ?? process.stderr,
|
||||
@@ -924,10 +966,52 @@ export async function runInstallerCli(argv = process.argv.slice(2), options = {}
|
||||
);
|
||||
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);
|
||||
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({
|
||||
sourceDir,
|
||||
capabilityIds,
|
||||
browserChoice,
|
||||
bunCommand,
|
||||
preparedSource: parsed.prepared,
|
||||
runCommand: options.installOptions?.runCommand,
|
||||
@@ -969,9 +1053,10 @@ export async function runInstallerCli(argv = process.argv.slice(2), options = {}
|
||||
const result = await installManagedRuntime({
|
||||
sourceDir,
|
||||
home,
|
||||
version: runtimeSlotVersion(releaseVersion, capabilityIds),
|
||||
version: runtimeSlotVersion(releaseVersion, capabilityIds, { browserChoice }),
|
||||
bunCommand,
|
||||
capabilityIds,
|
||||
browserChoice,
|
||||
buildMissing: parsed.prepared ? false : undefined,
|
||||
nodeCommand: 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(`Runtime home: ${result.home}\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;
|
||||
} catch (error) {
|
||||
@@ -1052,6 +1137,10 @@ export function runtimeReleaseComponentForPath(value) {
|
||||
const relative = component.slice(browserRoot.length);
|
||||
if (relative === ".links" || relative.startsWith(".links/")) return null;
|
||||
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 (/^chromium-\d/.test(top)) return "browser-visible";
|
||||
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 browserStat = await fs.lstat(managedBrowsers).catch(() => null);
|
||||
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 bunStat = await fs.lstat(managedBun).catch(() => null);
|
||||
const hasManagedBun = bunStat?.isFile() && !bunStat.isSymbolicLink();
|
||||
@@ -1574,20 +1712,29 @@ if (/^#!.*\\bbun(?:\\s|$)/.test(header)) {
|
||||
command = process.env.GSTACK_NODE || process.execPath;
|
||||
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, {
|
||||
stdio: "inherit",
|
||||
windowsHide: true,
|
||||
env: {
|
||||
...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 } : {}),
|
||||
},
|
||||
env: childEnv,
|
||||
});
|
||||
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; });
|
||||
@@ -1697,7 +1844,8 @@ function processIsAlive(pid) {
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -1876,6 +2024,7 @@ async function captureInstallSurface(paths, launcherSurface) {
|
||||
const oldManifest = await readJson(manifestPath, null);
|
||||
const oldLaunchers = validateInstallManifestForUninstall(oldManifest);
|
||||
const relativePaths = new Set([
|
||||
"config.json",
|
||||
"runtime-install.json",
|
||||
...oldLaunchers,
|
||||
...launcherRelativePaths(launcherSurface),
|
||||
@@ -2155,7 +2304,10 @@ function parseInstallerArgs(argv) {
|
||||
home: null,
|
||||
version: undefined,
|
||||
bunCommand: undefined,
|
||||
browserProvider: null,
|
||||
browserPath: null,
|
||||
capabilityIds: OPTIONAL_RUNTIME_CAPABILITIES,
|
||||
capabilitiesProvided: false,
|
||||
installMode: null,
|
||||
yes: false,
|
||||
dryRun: false,
|
||||
@@ -2176,20 +2328,34 @@ function parseInstallerArgs(argv) {
|
||||
else if (arg === "--replace-capabilities") result.replaceCapabilities = true;
|
||||
else if (arg === "--install-now") result.installMode = "now";
|
||||
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];
|
||||
if (!value || value.startsWith("--")) throw new TypeError(`Missing value for ${arg}`);
|
||||
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 {
|
||||
const key = { "--source": "sourceDir", "--home": "home", "--version": "version", "--bun": "bunCommand" }[arg];
|
||||
result[key] = value;
|
||||
}
|
||||
} 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.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.dryRun && (result.installMode != null || result.yes)) throw new TypeError("--dry-run cannot be combined with install/consent flags");
|
||||
return result;
|
||||
@@ -2197,12 +2363,13 @@ function parseInstallerArgs(argv) {
|
||||
|
||||
function installerUsage() {
|
||||
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` +
|
||||
`Optional capabilities: ${OPTIONAL_RUNTIME_CAPABILITIES.join(", ")}\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" +
|
||||
"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) {
|
||||
@@ -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) {
|
||||
stdout.write("GStack optional runtime preview\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`);
|
||||
for (const item of preview.materializations) {
|
||||
if (item.kind === "managed-bun-capture") {
|
||||
|
||||
@@ -319,7 +319,7 @@ function validateTransactionPath(value) {
|
||||
throw managedHomeError("Invalid runtime transaction path", "RUNTIME_TRANSACTION_INVALID");
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
+208
-22
@@ -10,11 +10,22 @@ import { createHash } from "node:crypto";
|
||||
import { constants as fsConstants, createReadStream } from "node:fs";
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
applyBrowserProviderToComponents,
|
||||
assertBrowserChoiceSupportsCapabilities,
|
||||
browserChoiceRequired,
|
||||
detectInstalledBrowsers,
|
||||
resolveBrowserChoice,
|
||||
} from "./browser-choice.mjs";
|
||||
|
||||
export const BOOTSTRAP_SCHEMA_VERSION = 2;
|
||||
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 =
|
||||
`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 CAPABILITY_DEPENDENCIES = Object.freeze({
|
||||
browser: Object.freeze([]),
|
||||
@@ -47,9 +58,9 @@ const ALLOWED_DOWNLOAD_HOSTS = new Set([
|
||||
"objects.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 =
|
||||
`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";
|
||||
|
||||
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());
|
||||
return 0;
|
||||
}
|
||||
if (!["preview", "install"].includes(parsed.action)) {
|
||||
throw bootstrapError("Expected `preview` or `install`", "BOOTSTRAP_USAGE");
|
||||
if (!["options", "preview", "install"].includes(parsed.action)) {
|
||||
throw bootstrapError("Expected `options`, `preview`, or `install`", "BOOTSTRAP_USAGE");
|
||||
}
|
||||
|
||||
const platform = options.platform ?? process.platform;
|
||||
if (parsed.capabilities.includes("ios") && platform !== "darwin") {
|
||||
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) {
|
||||
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") {
|
||||
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;
|
||||
}
|
||||
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");
|
||||
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;
|
||||
@@ -90,11 +164,29 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
);
|
||||
const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL;
|
||||
assertOfficialUrl(manifestUrl, { manifest: true });
|
||||
const manifest = await fetchJson(fetch_, manifestUrl);
|
||||
const manifest = await fetchJson(fetch_, manifestUrl, {
|
||||
official: manifestUrl === OFFICIAL_MANIFEST_URL,
|
||||
});
|
||||
validateManifest(manifest, target);
|
||||
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 plan = buildComponentPlan(manifest, target, parsed.capabilities, reusable);
|
||||
const active = await inspectReusableRuntime(home, manifest.version).catch(() => null);
|
||||
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`);
|
||||
else printComponentPlan(io.stdout, plan);
|
||||
if (parsed.action === "preview") return 0;
|
||||
@@ -117,7 +209,7 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
||||
await assertNoLinks(componentRoot);
|
||||
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 {
|
||||
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) {
|
||||
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) {
|
||||
const arg = argv[index];
|
||||
if (["-h", "--help"].includes(arg)) result.help = true;
|
||||
else if (arg === "--yes") result.yes = true;
|
||||
else if (arg === "--json") result.json = true;
|
||||
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];
|
||||
if (!value || value.startsWith("--")) throw bootstrapError(`${arg} requires a value`, "BOOTSTRAP_USAGE");
|
||||
if (arg === "--capability") result.capabilities.push(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");
|
||||
}
|
||||
if (result.help) return result;
|
||||
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");
|
||||
result.capabilities = [...new Set(result.capabilities)].sort();
|
||||
for (const capability of result.capabilities) {
|
||||
@@ -206,7 +322,7 @@ function sameGraph(actual, expected) {
|
||||
return JSON.stringify(normalize(actual)) === JSON.stringify(normalize(expected));
|
||||
}
|
||||
|
||||
function selectedComponents(capabilities) {
|
||||
function selectedComponents(capabilities, browserChoice) {
|
||||
const selected = new Set(["core"]);
|
||||
for (const capability of capabilities) {
|
||||
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();
|
||||
}
|
||||
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable) {
|
||||
const components = selectedComponents(capabilities);
|
||||
function buildComponentPlan(manifest, target, capabilities, reusable, browserChoice) {
|
||||
const components = selectedComponents(capabilities, browserChoice);
|
||||
const retained = new Set(reusable?.components ?? []);
|
||||
const downloads = components
|
||||
.filter((component) => !retained.has(component))
|
||||
@@ -234,6 +368,7 @@ function buildComponentPlan(manifest, target, capabilities, reusable) {
|
||||
target,
|
||||
version: manifest.version,
|
||||
capabilities,
|
||||
browser: browserChoice,
|
||||
components,
|
||||
reusedComponents: components.filter((component) => retained.has(component)),
|
||||
downloads,
|
||||
@@ -244,6 +379,11 @@ function buildComponentPlan(manifest, target, capabilities, reusable) {
|
||||
function printComponentPlan(stdout, plan) {
|
||||
stdout.write(`GStack optional runtime ${plan.version} for ${plan.target}\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`);
|
||||
if (plan.reusedComponents.length) stdout.write(`Reusing: ${plan.reusedComponents.join(", ")}\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);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
||||
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;
|
||||
const components = [...new Set(bundle.runtimeComponents)];
|
||||
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);
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
@@ -277,7 +438,7 @@ async function inspectReusableRuntime(home, version) {
|
||||
await sha256File(file) !== entry.sha256) return null;
|
||||
files.push(relative);
|
||||
}
|
||||
return { root, components, files };
|
||||
return { root, components, files, selectedCapabilities, browserChoice, releaseMatches };
|
||||
}
|
||||
|
||||
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" });
|
||||
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();
|
||||
if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID");
|
||||
return value;
|
||||
@@ -398,9 +567,14 @@ async function installFromSource(source, parsed, options) {
|
||||
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");
|
||||
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 (options.version) args.push("--version", options.version);
|
||||
if (options.prepared) args.push("--prepared");
|
||||
if (options.prepared || options.replaceCapabilities) args.push("--replace-capabilities");
|
||||
await run(options.nodeCommand ?? process.execPath, args);
|
||||
options.stdout.write(`Installed optional capabilities: ${parsed.capabilities.join(", ")}. No coding host was enrolled.\n`);
|
||||
return 0;
|
||||
@@ -521,12 +695,24 @@ function formatBytes(bytes) {
|
||||
}
|
||||
|
||||
function usage() {
|
||||
return "Usage: node runtime-bootstrap.mjs install --capability <name> [--capability <name>...]\n" +
|
||||
" node runtime-bootstrap.mjs install --source <reviewed-checkout> --capability <name>\n\n" +
|
||||
return "Usage: node runtime-bootstrap.mjs options --capability <browser-backed-name>\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" +
|
||||
"--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() {
|
||||
if (!process.argv[1]) return false;
|
||||
const [modulePath, invokedPath] = await Promise.all([
|
||||
|
||||
+68
-2
@@ -2,14 +2,16 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { assertPathInside, resolveRuntimePaths } from "./paths.js";
|
||||
import { atomicWriteJson, pathExists, readJson, renameWithRetry } from "./storage.js";
|
||||
import { atomicWriteFile, atomicWriteJson, pathExists, readJson, renameWithRetry } from "./storage.js";
|
||||
import {
|
||||
assertManagedHome,
|
||||
ensureManagedHome,
|
||||
ensureManagedRuntimeDirectory,
|
||||
recoverRuntimeTransactionUnlocked,
|
||||
RUNTIME_TRANSACTION_FILE,
|
||||
withRuntimeLifecycleLock,
|
||||
} from "./managed-home.js";
|
||||
import { configSetBrowserChoice } from "./config.js";
|
||||
import { errorWithCode as upgradeError } from "./errors.js";
|
||||
import { currentIsoTimestamp as isoNow } from "./time.js";
|
||||
|
||||
@@ -214,6 +216,11 @@ export async function rollbackUpgrade(home, options = {}) {
|
||||
}
|
||||
await assertTreeContainsNoLinks(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 = {
|
||||
schemaVersion: 2,
|
||||
status: "active",
|
||||
@@ -222,11 +229,70 @@ export async function rollbackUpgrade(home, options = {}) {
|
||||
rolledBackFrom: pointer.current ?? null,
|
||||
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;
|
||||
}, 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 = {}) {
|
||||
const recovered = await recoverPendingUpgrade(home, options);
|
||||
return recovered.pointer;
|
||||
|
||||
@@ -38,7 +38,7 @@ CHROMIUM_DIR=$(ls -d "$PW_CACHE"/chromium-*/chrome-mac-arm64 2>/dev/null | sort
|
||||
|
||||
if [ -z "$CHROMIUM_DIR" ]; then
|
||||
echo "ERROR: Playwright Chromium not found in $PW_CACHE"
|
||||
echo "Run: bunx playwright install chromium"
|
||||
echo "Run: bunx playwright-core install chromium"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -386,7 +386,7 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
\`${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\` |`);
|
||||
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.',
|
||||
'',
|
||||
'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');
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
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 {
|
||||
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'));
|
||||
for (const tree of TREE_NAMES) {
|
||||
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', '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', 'browser-choice.mjs'), browserChoice);
|
||||
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', 'execution-result-contract.json'), EXECUTION_RESULT_SCHEMA);
|
||||
|
||||
@@ -188,7 +188,7 @@ export function renderLegacyBody(source: string): string {
|
||||
*/
|
||||
function portLegacyText(value: string, source: string): string {
|
||||
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;
|
||||
|
||||
@@ -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('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('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
|
||||
.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-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('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, '');
|
||||
|
||||
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.',
|
||||
'',
|
||||
'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,
|
||||
].join('\n');
|
||||
|
||||
@@ -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
|
||||
// accounted for the 16 lazy-section checks; the remaining 136 cover runtime
|
||||
// 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 {
|
||||
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'));
|
||||
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('install --capability <name> --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('matching `install` command with the same capabilities and browser flags plus `--yes`'), `${tree} runtime contract lacks explicit approved install invocation`);
|
||||
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('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`);
|
||||
|
||||
@@ -34,7 +34,7 @@ rm -f \
|
||||
|
||||
(
|
||||
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
|
||||
|
||||
@@ -438,6 +438,15 @@ function verifyInstalledCase(
|
||||
const installedSkills = listInstalledSkills(targetRoot);
|
||||
const sortedExpected = [...expectedSkills].sort();
|
||||
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(
|
||||
checks,
|
||||
`${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 discoveryCommand = execute(
|
||||
// Exercise the repository root exactly as the documented
|
||||
// `npx skills add time-attack/gstack` path will after checkout. The
|
||||
// curated projection alone could hide stray root-level SKILL.md files.
|
||||
skillsCliArgv(npxExecutable, ['add', repoRoot, '--list']),
|
||||
// Exercise the documented public source directly. The repository root
|
||||
// also contains opt-in 1.x compatibility aliases; skills@1.5.19 counts
|
||||
// those internal entries before applying an explicit --skill filter.
|
||||
skillsCliArgv(npxExecutable, ['add', path.join(repoRoot, 'skills'), '--list']),
|
||||
controlProject,
|
||||
controlEnv,
|
||||
);
|
||||
@@ -626,7 +635,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
|
||||
entry,
|
||||
scope,
|
||||
sourceKind,
|
||||
sourceArgument: sourceKind === 'source-symlink' ? sourceLink : sourceRoot,
|
||||
sourceArgument: path.join(sourceKind === 'source-symlink' ? sourceLink : sourceRoot, 'skills'),
|
||||
sourceRoot,
|
||||
expectedSkills: PUBLIC_SKILLS,
|
||||
explicitSelection: false,
|
||||
@@ -643,7 +652,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
|
||||
entry: cursor,
|
||||
scope: 'project',
|
||||
sourceKind: 'repository-root',
|
||||
sourceArgument: repoRoot,
|
||||
sourceArgument: path.join(repoRoot, 'skills'),
|
||||
sourceRoot: repoRoot,
|
||||
expectedSkills: COLLISION_SKILLS,
|
||||
explicitSelection: true,
|
||||
@@ -659,7 +668,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
|
||||
entry: codex,
|
||||
scope: 'global',
|
||||
sourceKind: 'path-with-spaces',
|
||||
sourceArgument: sourceRoot,
|
||||
sourceArgument: path.join(sourceRoot, 'skills'),
|
||||
sourceRoot,
|
||||
expectedSkills: COLLISION_SKILLS,
|
||||
explicitSelection: true,
|
||||
@@ -675,7 +684,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
|
||||
entry: openclaw,
|
||||
scope: 'project',
|
||||
sourceKind: 'path-with-spaces',
|
||||
sourceArgument: sourceRoot,
|
||||
sourceArgument: path.join(sourceRoot, 'skills'),
|
||||
sourceRoot,
|
||||
expectedSkills: ['ship'],
|
||||
explicitSelection: true,
|
||||
@@ -747,7 +756,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
repositoryRoot: repoRoot,
|
||||
sourceProjection: 'repository-root-and-canonical-projection',
|
||||
sourceProjection: 'canonical-skills-subpath-and-opt-in-compatibility-root',
|
||||
cli: {
|
||||
executable: npxExecutable,
|
||||
version,
|
||||
|
||||
@@ -25,17 +25,18 @@ for arg in "$@"; do
|
||||
-h|--help)
|
||||
printf '%s\n' \
|
||||
'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]' \
|
||||
'' \
|
||||
'Optional capabilities: browser, design, pdf, diagram, ios (iOS is macOS-only).' \
|
||||
'Without --install-now, non-interactive use previews and installs nothing.' \
|
||||
'--dry-run and --install-later never modify runtime state or host setup.' \
|
||||
'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
|
||||
;;
|
||||
--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") ;;
|
||||
esac
|
||||
@@ -45,7 +46,7 @@ NODE_COMMAND="${GSTACK_NODE:-node}"
|
||||
if ! command -v "$NODE_COMMAND" >/dev/null 2>&1; then
|
||||
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 " npx skills add time-attack/gstack" >&2
|
||||
echo " npx skills add time-attack/gstack/skills" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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`.
|
||||
|
||||
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
|
||||
|
||||
`$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
Reference in New Issue
Block a user