fix: add production runtime RC release channel

Publish signed prerelease artifacts from v2.0.0-rc.* tags, bind bootstrap trust to the immutable RC tag, make missing-release errors actionable, and install the six public skills from the canonical subpath.
This commit is contained in:
Sinabina
2026-07-20 16:51:59 -07:00
parent d6ef673e4d
commit b0047cc525
84 changed files with 367 additions and 140 deletions
@@ -2,11 +2,19 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
const [directory, repository = process.env.GITHUB_REPOSITORY, version = "2.0.0"] = process.argv.slice(2); const [
directory,
repository = process.env.GITHUB_REPOSITORY,
version = "2.0.0",
releaseTag = `v${version}`,
] = process.argv.slice(2);
if (!directory || !repository) { if (!directory || !repository) {
console.error("Usage: create-runtime-release-manifest.mjs <artifact-dir> <owner/repo> [version]"); console.error("Usage: create-runtime-release-manifest.mjs <artifact-dir> <owner/repo> [version] [release-tag]");
process.exit(2); process.exit(2);
} }
if (!/^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/.test(releaseTag)) {
throw new Error(`Invalid runtime release tag: ${releaseTag}`);
}
const targets = [ const targets = [
"darwin-arm64", "darwin-arm64",
@@ -35,8 +43,8 @@ const capabilityComponents = {
ios: ["ios"], ios: ["ios"],
}; };
const commonComponents = ["core", "browser-code", "browser-headless", "browser-visible", "design", "diagram", "pdf"]; const commonComponents = ["core", "browser-code", "browser-headless", "browser-visible", "design", "diagram", "pdf"];
const release = `https://github.com/${repository}/releases/download/v${version}`; const release = `https://github.com/${repository}/releases/download/${releaseTag}`;
const certificateIdentity = `https://github.com/${repository}/.github/workflows/release-artifacts.yml@refs/tags/v${version}`; const certificateIdentity = `https://github.com/${repository}/.github/workflows/release-artifacts.yml@refs/tags/${releaseTag}`;
const targetRecords = {}; const targetRecords = {};
for (const target of targets) { for (const target of targets) {
+7 -3
View File
@@ -2,7 +2,9 @@ name: Release runtime artifacts
on: on:
push: push:
tags: [v2.0.0] tags:
- v2.0.0
- v2.0.0-rc.*
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@@ -170,7 +172,7 @@ jobs:
- uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 - uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
- name: Create strict six-target manifest - name: Create strict six-target manifest
run: node .github/scripts/create-runtime-release-manifest.mjs release-output "$GITHUB_REPOSITORY" 2.0.0 run: node .github/scripts/create-runtime-release-manifest.mjs release-output "$GITHUB_REPOSITORY" 2.0.0 "$GITHUB_REF_NAME"
- name: Checksum and keyless-sign manifest - name: Checksum and keyless-sign manifest
run: | run: |
@@ -188,11 +190,13 @@ jobs:
- name: Publish immutable release assets - name: Publish immutable release assets
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
PRERELEASE_FLAG: ${{ contains(github.ref_name, '-rc.') && '--prerelease' || '' }}
run: | run: |
set -euo pipefail set -euo pipefail
gh release create "$GITHUB_REF_NAME" \ gh release create "$GITHUB_REF_NAME" \
--verify-tag \ --verify-tag \
--title "GStack runtime 2.0.0" \ $PRERELEASE_FLAG \
--title "GStack runtime $GITHUB_REF_NAME" \
--notes "Signed optional runtime artifacts for the six portable GStack skills." \ --notes "Signed optional runtime artifacts for the six portable GStack skills." \
release-output/* release-output/*
shell: bash shell: bash
+1 -1
View File
@@ -60,7 +60,7 @@ they contain no copied judgment. Representative mappings:
Canonical installation is standards-based: Canonical installation is standards-based:
```bash ```bash
npx skills add time-attack/gstack npx skills add time-attack/gstack/skills
``` ```
Delegate host detection, placement, project/global scope, selected-skill Delegate host detection, placement, project/global scope, selected-skill
+1 -1
View File
@@ -13,7 +13,7 @@ mutation boundaries, recommendations, or voice. Compatibility files under
Specification, and Full chain. Do not expose its internal aliases as more Specification, and Full chain. Do not expose its internal aliases as more
top-level modes. top-level modes.
Canonical install is `npx skills add time-attack/gstack`. Standard installers Canonical install is `npx skills add time-attack/gstack/skills`. Standard installers
own host placement and scope. Do not add host-specific install logic to the 2.0 own host placement and scope. Do not add host-specific install logic to the 2.0
path. Pure judgment must work without the optional `bin/gstack` runtime. path. Pure judgment must work without the optional `bin/gstack` runtime.
The host-neutral `./setup` installs only that managed runtime/capability bundle; The host-neutral `./setup` installs only that managed runtime/capability bundle;
+4 -4
View File
@@ -35,7 +35,7 @@ Install with the standard Agent Skills installer; it owns host detection,
scope, destination paths, updates, removal, and selected-skill installation: scope, destination paths, updates, removal, and selected-skill installation:
```bash ```bash
npx skills add time-attack/gstack npx skills add time-attack/gstack/skills
``` ```
That installs the six judgment skills. Install a subset with the installer's That installs the six judgment skills. Install a subset with the installer's
@@ -87,7 +87,7 @@ extension, connector, MCP, settings entry, profile, alternate host, or browser
runtime is configured without consent, and `./setup` is never a GStack 2 runtime is configured without consent, and `./setup` is never a GStack 2
browser setup command. browser setup command.
Install through `npx skills add time-attack/gstack`; do not clone the repository Install through `npx skills add time-attack/gstack/skills`; do not clone the repository
under `.agents/skills/gstack`. A standards installation exposes exactly one under `.agents/skills/gstack`. A standards installation exposes exactly one
canonical QA skill at `.agents/skills/qa/SKILL.md`. The cloned compatibility canonical QA skill at `.agents/skills/qa/SKILL.md`. The cloned compatibility
tree contains legacy GStack 1 entry points and is not the GStack 2 install tree contains legacy GStack 1 entry points and is not the GStack 2 install
@@ -96,7 +96,7 @@ surface.
The npm package is deliberately not the skill installer and does not contain The npm package is deliberately not the skill installer and does not contain
the six skill tree or compiled browser/design/PDF payloads. It is the small the six skill tree or compiled browser/design/PDF payloads. It is the small
host-neutral runtime control/bootstrap surface used by release tooling. New host-neutral runtime control/bootstrap surface used by release tooling. New
users should install skills with `npx skills add time-attack/gstack`; optional users should install skills with `npx skills add time-attack/gstack/skills`; optional
capabilities are downloaded by a skill after consent. capabilities are downloaded by a skill after consent.
Public web research is optional. Context.dev is the only new external service, Public web research is optional. Context.dev is the only new external service,
@@ -161,7 +161,7 @@ archive. It is not the GStack 2 installation or first-run path.
## Legacy 1.x host-specific install (compatibility only) ## Legacy 1.x host-specific install (compatibility only)
Do not use this section for a new GStack 2 install. Use Do not use this section for a new GStack 2 install. Use
`npx skills add time-attack/gstack` above. These instructions remain while old `npx skills add time-attack/gstack/skills` above. These instructions remain while old
commands are documented as opt-in aliases. They describe a 1.x release/tag: commands are documented as opt-in aliases. They describe a 1.x release/tag:
the current branch's `./setup` is runtime-only and will not perform the the current branch's `./setup` is runtime-only and will not perform the
host-specific actions shown below. host-specific actions shown below.
+1 -1
View File
@@ -85,7 +85,7 @@ judgment. This prevents an alias from drifting away from the canonical source.
The standard Agent Skills tree is the unit of distribution: The standard Agent Skills tree is the unit of distribution:
```bash ```bash
npx skills add time-attack/gstack npx skills add time-attack/gstack/skills
``` ```
The installer—not GStack—owns host detection, project/global scope, destination The installer—not GStack—owns host detection, project/global scope, destination
+2 -2
View File
@@ -18,7 +18,7 @@ bundle.
## Canonical installation ## Canonical installation
```bash ```bash
npx skills add time-attack/gstack npx skills add time-attack/gstack/skills
``` ```
The standards installer owns host detection, destination paths, project/global The standards installer owns host detection, destination paths, project/global
@@ -32,7 +32,7 @@ Examples supported by the installer interface:
npx skills add time-attack/gstack/skills --skill qa npx skills add time-attack/gstack/skills --skill qa
# Installer-managed global scope # Installer-managed global scope
npx skills add time-attack/gstack -g npx skills add time-attack/gstack/skills -g
``` ```
Run `npx skills add --help` for the installed CLI version before scripting Run `npx skills add --help` for the installed CLI version before scripting
+1 -1
View File
@@ -4552,7 +4552,7 @@
"source_path": "gstack-upgrade/SKILL.md.tmpl", "source_path": "gstack-upgrade/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d", "blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d",
"normalized_render_sha256": "a913cf77f76c4d68c576a06190b498e3d8d3b60b85498a68f173b23a7f800828", "normalized_render_sha256": "2c8cf9e505b7da27730bf646a33ee38e8092259f3335a524c71e57ba5b602541",
"target": "skills/ship/references/legacy/gstack-upgrade.md", "target": "skills/ship/references/legacy/gstack-upgrade.md",
"disposition": "DUPLICATE_INFRASTRUCTURE", "disposition": "DUPLICATE_INFRASTRUCTURE",
"overlays": [ "overlays": [
+13 -4
View File
@@ -5,10 +5,11 @@ GStack has two explicit version identities during the 2.0 migration:
- `VERSION` and `package.json.version` are the repository/package release - `VERSION` and `package.json.version` are the repository/package release
counter. They remain byte-equal and retain the existing four-slot format so counter. They remain byte-equal and retain the existing four-slot format so
the 1.x compatibility ship queue does not silently fail open. the 1.x compatibility ship queue does not silently fail open.
- `package.json.gstack.runtimeVersion`, `runtime/index.js`, - `package.json.gstack.runtimeVersion`, `runtime/index.js`, and
`runtime/install.js`, and every standards-installed bootstrap declare the `runtime/install.js` declare the managed-runtime protocol release `2.0.0`.
managed-runtime protocol release `2.0.0`. The official artifact tag and Each standards-installed bootstrap separately pins one immutable artifact
manifest use that value. release tag. Candidate bootstraps use `v2.0.0-rc.N`; the manifest and bundle
remain runtime-compatible with `2.0.0`. Stable bootstraps use `v2.0.0`.
They are intentionally different namespaces. CI fails if either identity They are intentionally different namespaces. CI fails if either identity
drifts inside its own namespace. drifts inside its own namespace.
@@ -24,6 +25,14 @@ linux-arm64 linux-x64 (glibc)
windows-arm64 windows-x64 windows-arm64 windows-x64
``` ```
Both `v2.0.0-rc.*` and `v2.0.0` tags use the same build, signing, manifest,
attestation, and smoke path. RC tags publish GitHub prereleases so the exact
fresh-machine production bootstrap can be exercised before the stable tag is
created. Runtime compatibility and release-channel identity are deliberately
separate: archive names and manifest `version` remain `2.0.0`, while URLs and
Sigstore certificate identity bind to the immutable RC or stable tag that
actually published them.
Each archive has one `gstack/` root and no symlinks. CI records an exact byte Each archive has one `gstack/` root and no symlinks. CI records an exact byte
count and SHA-256, signs the archive keylessly with Cosign, emits a Sigstore count and SHA-256, signs the archive keylessly with Cosign, emits a Sigstore
bundle, and also creates a GitHub build-provenance attestation. The release bundle, and also creates a GitHub build-provenance attestation. The release
+1 -1
View File
@@ -10,7 +10,7 @@ Install the canonical Agent Skills source, then use that installer's tracked
source and lock metadata for discovery, updates, and removal: source and lock metadata for discovery, updates, and removal:
```bash ```bash
npx skills add time-attack/gstack npx skills add time-attack/gstack/skills
npx skills update # interactive scope npx skills update # interactive scope
npx skills update -p # project installs only npx skills update -p # project installs only
npx skills update -g # global installs only npx skills update -g # global installs only
+1 -1
View File
@@ -9,7 +9,7 @@
"source_path": "gstack-upgrade/SKILL.md.tmpl", "source_path": "gstack-upgrade/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d", "blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d",
"normalized_render_sha256": "a913cf77f76c4d68c576a06190b498e3d8d3b60b85498a68f173b23a7f800828", "normalized_render_sha256": "2c8cf9e505b7da27730bf646a33ee38e8092259f3335a524c71e57ba5b602541",
"target": "skills/ship/references/legacy/gstack-upgrade.md", "target": "skills/ship/references/legacy/gstack-upgrade.md",
"overlays": [ "overlays": [
679 679
+1 -1
View File
@@ -4552,7 +4552,7 @@
"source_path": "gstack-upgrade/SKILL.md.tmpl", "source_path": "gstack-upgrade/SKILL.md.tmpl",
"base_sha": "bb57306d98c97011b0919c6132705a15b1579781", "base_sha": "bb57306d98c97011b0919c6132705a15b1579781",
"blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d", "blob_sha": "5402a1da3c857cbf50668085fce53172b72bba0d",
"normalized_render_sha256": "a913cf77f76c4d68c576a06190b498e3d8d3b60b85498a68f173b23a7f800828", "normalized_render_sha256": "2c8cf9e505b7da27730bf646a33ee38e8092259f3335a524c71e57ba5b602541",
"target": "skills/ship/references/legacy/gstack-upgrade.md", "target": "skills/ship/references/legacy/gstack-upgrade.md",
"disposition": "DUPLICATE_INFRASTRUCTURE", "disposition": "DUPLICATE_INFRASTRUCTURE",
"overlays": [ "overlays": [
+20 -6
View File
@@ -13,8 +13,12 @@ import { fileURLToPath } from "node:url";
export const BOOTSTRAP_SCHEMA_VERSION = 2; export const BOOTSTRAP_SCHEMA_VERSION = 2;
export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0"; export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0";
// Keep the runtime compatibility version separate from the immutable release
// channel. Release candidates carry the 2.0.0 runtime contract while letting
// fresh-machine production journeys run before the stable v2.0.0 tag exists.
export const BOOTSTRAP_RELEASE_TAG = "v2.0.0-rc.1";
export const OFFICIAL_MANIFEST_URL = export const OFFICIAL_MANIFEST_URL =
`https://github.com/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/gstack-runtime-manifest.json`; `https://github.com/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/gstack-runtime-manifest.json`;
const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]); const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]);
const CAPABILITY_DEPENDENCIES = Object.freeze({ const CAPABILITY_DEPENDENCIES = Object.freeze({
browser: Object.freeze([]), browser: Object.freeze([]),
@@ -47,9 +51,9 @@ const ALLOWED_DOWNLOAD_HOSTS = new Set([
"objects.githubusercontent.com", "objects.githubusercontent.com",
"release-assets.githubusercontent.com", "release-assets.githubusercontent.com",
]); ]);
const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/`; const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/`;
const OFFICIAL_CERTIFICATE_IDENTITY = const OFFICIAL_CERTIFICATE_IDENTITY =
`https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/v${BOOTSTRAP_RUNTIME_VERSION}`; `https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/${BOOTSTRAP_RELEASE_TAG}`;
const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com"; const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
export async function main(argv = process.argv.slice(2), options = {}) { export async function main(argv = process.argv.slice(2), options = {}) {
@@ -90,7 +94,9 @@ export async function main(argv = process.argv.slice(2), options = {}) {
); );
const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL; const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL;
assertOfficialUrl(manifestUrl, { manifest: true }); assertOfficialUrl(manifestUrl, { manifest: true });
const manifest = await fetchJson(fetch_, manifestUrl); const manifest = await fetchJson(fetch_, manifestUrl, {
official: manifestUrl === OFFICIAL_MANIFEST_URL,
});
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
@@ -300,10 +306,18 @@ function sha256File(file) {
}); });
} }
async function fetchJson(fetch_, url) { async function fetchJson(fetch_, url, options = {}) {
const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" }); const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" });
assertFinalDownloadUrl(response.url || url); assertFinalDownloadUrl(response.url || url);
if (!response.ok) throw bootstrapError(`Download failed with HTTP ${response.status}`, "BOOTSTRAP_DOWNLOAD_FAILED"); if (!response.ok) {
if (options.official && response.status === 404) {
throw bootstrapError(
`Official runtime release ${BOOTSTRAP_RELEASE_TAG} is not published at ${url}. No files were downloaded or installed.`,
"BOOTSTRAP_RELEASE_UNAVAILABLE",
);
}
throw bootstrapError(`Manifest download failed with HTTP ${response.status} from ${url}. No files were downloaded or installed.`, "BOOTSTRAP_DOWNLOAD_FAILED");
}
const value = await response.json(); const value = await response.json();
if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID"); if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID");
return value; return value;
+2 -2
View File
@@ -384,7 +384,7 @@ Print this replacement invocation, then dispatch to it exactly:
\`${assignment.replacement}\` \`${assignment.replacement}\`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved \`${assignment.source}\` module. If that dispatcher is not installed, tell the user to install it with \`npx skills add time-attack/gstack --skill ${assignment.tree}\`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved \`${assignment.source}\` module. If that dispatcher is not installed, tell the user to install it with \`npx skills add time-attack/gstack/skills --skill ${assignment.tree}\`.
`); `);
rows.push(`| \`/${assignment.source}\` | \`${assignment.replacement}\` | \`skills/${assignment.tree}/references/legacy/${assignment.source}.md\` |`); rows.push(`| \`/${assignment.source}\` | \`${assignment.replacement}\` | \`skills/${assignment.tree}/references/legacy/${assignment.source}.md\` |`);
aliases.push({ aliases.push({
@@ -483,7 +483,7 @@ function webContextContract(): string {
'', '',
'Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent.', 'Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent.',
'', '',
'Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack` and is never inferred from runtime state.', 'Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack/skills` and is never inferred from runtime state.',
'', '',
].join('\n'); ].join('\n');
} }
+2 -2
View File
@@ -188,7 +188,7 @@ export function renderLegacyBody(source: string): string {
*/ */
function portLegacyText(value: string, source: string): string { function portLegacyText(value: string, source: string): string {
if (source === 'gstack-upgrade') { if (source === 'gstack-upgrade') {
return `# Legacy upgrade compatibility\n\nThe 1.x host-directory detector, vendored-copy synchronizer, and destructive Git replacement blocks were duplicated installation infrastructure. GStack 2 delegates skill placement and updates to the standard Agent Skills installer and manages the optional shared runtime atomically.\n\n- Update selected skills with \`npx skills add time-attack/gstack\` using the user's existing project/global choice. Never infer or enroll a host.\n- Upgrade a complete local runtime package with \`gstack upgrade --source <complete-gstack-package> --version <version>\`.\n- Roll back the runtime with \`gstack upgrade --rollback\`.\n- Run \`gstack doctor\` after either operation.\n- Do not reset, delete, move, or rewrite a host skill directory. Do not infer Context.dev choice or consent.\n\nThis compatibility module contains no specialist judgment; release readiness and rollback judgment remain in the preserved ship modules.\n`; return `# Legacy upgrade compatibility\n\nThe 1.x host-directory detector, vendored-copy synchronizer, and destructive Git replacement blocks were duplicated installation infrastructure. GStack 2 delegates skill placement and updates to the standard Agent Skills installer and manages the optional shared runtime atomically.\n\n- Update selected skills with \`npx skills add time-attack/gstack/skills\` using the user's existing project/global choice. Never infer or enroll a host.\n- Upgrade a complete local runtime package with \`gstack upgrade --source <complete-gstack-package> --version <version>\`.\n- Roll back the runtime with \`gstack upgrade --rollback\`.\n- Run \`gstack doctor\` after either operation.\n- Do not reset, delete, move, or rewrite a host skill directory. Do not infer Context.dev choice or consent.\n\nThis compatibility module contains no specialist judgment; release readiness and rollback judgment remain in the preserved ship modules.\n`;
} }
let body = value; let body = value;
@@ -280,7 +280,7 @@ function portLegacyText(value: string, source: string): string {
.replaceAll('$GSTACK_ROOT/lib/redact-audit-log.ts', '$GSTACK_BIN/gstack-redact-audit-log') .replaceAll('$GSTACK_ROOT/lib/redact-audit-log.ts', '$GSTACK_BIN/gstack-redact-audit-log')
.replaceAll('bun $GSTACK_BIN/gstack-redact-audit-log', '$GSTACK_BIN/gstack-redact-audit-log') .replaceAll('bun $GSTACK_BIN/gstack-redact-audit-log', '$GSTACK_BIN/gstack-redact-audit-log')
.replaceAll('Disk paths stay `$GSTACK_ROOT/[skill-name]/SKILL.md`.', 'Resolve retired names through `references/COMPATIBILITY.md`; skill placement is installer-owned.') .replaceAll('Disk paths stay `$GSTACK_ROOT/[skill-name]/SKILL.md`.', 'Resolve retired names through `references/COMPATIBILITY.md`; skill placement is installer-owned.')
.replaceAll('Tell the user: "Done. Each developer now runs: `cd $GSTACK_ROOT && ./setup --team`"', 'Tell the user: "Done. Each developer installs the selected canonical skills with `npx skills add time-attack/gstack`; the optional runtime remains user-scoped."'); .replaceAll('Tell the user: "Done. Each developer now runs: `cd $GSTACK_ROOT && ./setup --team`"', 'Tell the user: "Done. Each developer installs the selected canonical skills with `npx skills add time-attack/gstack/skills`; the optional runtime remains user-scoped."');
body = body body = body
.replace(/_VENDORED="no"\nif \[ -d "\.agents\/skills\/gstack" \][\s\S]*?echo "VENDORED_GSTACK: \$_VENDORED"/g, '_VENDORED="managed-by-standard-installer"\necho "VENDORED_GSTACK: $_VENDORED"') .replace(/_VENDORED="no"\nif \[ -d "\.agents\/skills\/gstack" \][\s\S]*?echo "VENDORED_GSTACK: \$_VENDORED"/g, '_VENDORED="managed-by-standard-installer"\necho "VENDORED_GSTACK: $_VENDORED"')
+18 -9
View File
@@ -438,6 +438,15 @@ function verifyInstalledCase(
const installedSkills = listInstalledSkills(targetRoot); const installedSkills = listInstalledSkills(targetRoot);
const sortedExpected = [...expectedSkills].sort(); const sortedExpected = [...expectedSkills].sort();
record(checks, `${id}.command`, command.exitCode === 0, `exit=${command.exitCode}; signal=${command.signal ?? 'none'}`); record(checks, `${id}.command`, command.exitCode === 0, `exit=${command.exitCode}; signal=${command.signal ?? 'none'}`);
if (sourceSkillSegments.length === 1 && sourceSkillSegments[0] === 'skills') {
const reported = Number(stripTerminalControls(command.stdout).match(/Found\s+(\d+)\s+skills?/)?.[1]);
record(
checks,
`${id}.public-discovery-count`,
reported === PUBLIC_SKILLS.length,
`expected installer to report 6 public skills; found ${Number.isFinite(reported) ? reported : '(unparsed)'}`,
);
}
record( record(
checks, checks,
`${id}.selected-skills`, `${id}.selected-skills`,
@@ -595,10 +604,10 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
const supportsRemoval = /remove\s+\[skills\]/.test(helpCommand.stdout) && /Remove Options/.test(helpCommand.stdout); const supportsRemoval = /remove\s+\[skills\]/.test(helpCommand.stdout) && /Remove Options/.test(helpCommand.stdout);
const discoveryCommand = execute( const discoveryCommand = execute(
// Exercise the repository root exactly as the documented // Exercise the documented public source directly. The repository root
// `npx skills add time-attack/gstack` path will after checkout. The // also contains opt-in 1.x compatibility aliases; skills@1.5.19 counts
// curated projection alone could hide stray root-level SKILL.md files. // those internal entries before applying an explicit --skill filter.
skillsCliArgv(npxExecutable, ['add', repoRoot, '--list']), skillsCliArgv(npxExecutable, ['add', path.join(repoRoot, 'skills'), '--list']),
controlProject, controlProject,
controlEnv, controlEnv,
); );
@@ -626,7 +635,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
entry, entry,
scope, scope,
sourceKind, sourceKind,
sourceArgument: sourceKind === 'source-symlink' ? sourceLink : sourceRoot, sourceArgument: path.join(sourceKind === 'source-symlink' ? sourceLink : sourceRoot, 'skills'),
sourceRoot, sourceRoot,
expectedSkills: PUBLIC_SKILLS, expectedSkills: PUBLIC_SKILLS,
explicitSelection: false, explicitSelection: false,
@@ -643,7 +652,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
entry: cursor, entry: cursor,
scope: 'project', scope: 'project',
sourceKind: 'repository-root', sourceKind: 'repository-root',
sourceArgument: repoRoot, sourceArgument: path.join(repoRoot, 'skills'),
sourceRoot: repoRoot, sourceRoot: repoRoot,
expectedSkills: COLLISION_SKILLS, expectedSkills: COLLISION_SKILLS,
explicitSelection: true, explicitSelection: true,
@@ -659,7 +668,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
entry: codex, entry: codex,
scope: 'global', scope: 'global',
sourceKind: 'path-with-spaces', sourceKind: 'path-with-spaces',
sourceArgument: sourceRoot, sourceArgument: path.join(sourceRoot, 'skills'),
sourceRoot, sourceRoot,
expectedSkills: COLLISION_SKILLS, expectedSkills: COLLISION_SKILLS,
explicitSelection: true, explicitSelection: true,
@@ -675,7 +684,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
entry: openclaw, entry: openclaw,
scope: 'project', scope: 'project',
sourceKind: 'path-with-spaces', sourceKind: 'path-with-spaces',
sourceArgument: sourceRoot, sourceArgument: path.join(sourceRoot, 'skills'),
sourceRoot, sourceRoot,
expectedSkills: ['ship'], expectedSkills: ['ship'],
explicitSelection: true, explicitSelection: true,
@@ -747,7 +756,7 @@ export function runFullMatrix(options: FullMatrixOptions): InstallMatrixEvidence
platform: process.platform, platform: process.platform,
architecture: process.arch, architecture: process.arch,
repositoryRoot: repoRoot, repositoryRoot: repoRoot,
sourceProjection: 'repository-root-and-canonical-projection', sourceProjection: 'canonical-skills-subpath-and-opt-in-compatibility-root',
cli: { cli: {
executable: npxExecutable, executable: npxExecutable,
version, version,
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Full chain --module autoplan` `$plan --mode Full chain --module autoplan`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `autoplan` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `autoplan` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module benchmark-models` `$qa --mode Report --module benchmark-models`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `benchmark-models` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `benchmark-models` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module benchmark` `$qa --mode Report --module benchmark`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `benchmark` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `benchmark` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module browse` `$qa --mode Report --module browse`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `browse` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `browse` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module canary` `$qa --mode Report --module canary`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `canary` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `canary` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$debug --mode Diagnose-only --module careful` `$debug --mode Diagnose-only --module careful`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `careful` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill debug`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `careful` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill debug`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$review --mode Deep --module claude` `$review --mode Deep --module claude`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `claude` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill review`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `claude` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill review`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$review --mode Deep --module codex` `$review --mode Deep --module codex`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `codex` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill review`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `codex` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill review`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module context-restore` `$plan --mode Discovery --module context-restore`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `context-restore` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `context-restore` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module context-save` `$plan --mode Discovery --module context-save`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `context-save` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `context-save` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$review --mode Security --module cso` `$review --mode Security --module cso`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `cso` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill review`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `cso` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill review`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Generate --module design-consultation` `$design --mode Generate --module design-consultation`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-consultation` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-consultation` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Implement --module design-html` `$design --mode Implement --module design-html`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-html` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-html` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Implement --module design-review` `$design --mode Implement --module design-review`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Explore --module design-shotgun` `$design --mode Explore --module design-shotgun`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-shotgun` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `design-shotgun` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module devex-review` `$qa --mode Report --module devex-review`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `devex-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `devex-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Generate --module diagram` `$design --mode Generate --module diagram`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `diagram` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `diagram` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Prepare --module document-generate` `$ship --mode Prepare --module document-generate`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `document-generate` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `document-generate` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Prepare --module document-release` `$ship --mode Prepare --module document-release`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `document-release` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `document-release` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$debug --mode Diagnose-only --module freeze` `$debug --mode Diagnose-only --module freeze`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `freeze` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill debug`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `freeze` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill debug`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Prepare --module gstack-upgrade` `$ship --mode Prepare --module gstack-upgrade`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `gstack-upgrade` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `gstack-upgrade` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module gstack` `$plan --mode Discovery --module gstack`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `gstack` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `gstack` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$debug --mode Diagnose-only --module guard` `$debug --mode Diagnose-only --module guard`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `guard` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill debug`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `guard` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill debug`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$review --mode Deep --module health` `$review --mode Deep --module health`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `health` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill review`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `health` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill review`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$debug --mode Diagnose-only --module investigate` `$debug --mode Diagnose-only --module investigate`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `investigate` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill debug`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `investigate` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill debug`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Prepare --module ios-clean` `$ship --mode Prepare --module ios-clean`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-clean` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-clean` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Critique --module ios-design-review` `$design --mode Critique --module ios-design-review`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-design-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-design-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$debug --mode Fix --module ios-fix` `$debug --mode Fix --module ios-fix`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-fix` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill debug`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-fix` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill debug`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module ios-qa` `$qa --mode Report --module ios-qa`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-qa` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-qa` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Prepare --module ios-sync` `$ship --mode Prepare --module ios-sync`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-sync` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `ios-sync` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Land --module land-and-deploy` `$ship --mode Land --module land-and-deploy`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `land-and-deploy` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `land-and-deploy` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Prepare --module landing-report` `$ship --mode Prepare --module landing-report`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `landing-report` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill ship`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `landing-report` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module learn` `$plan --mode Discovery --module learn`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `learn` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `learn` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Generate --module make-pdf` `$design --mode Generate --module make-pdf`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `make-pdf` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `make-pdf` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module office-hours` `$plan --mode Discovery --module office-hours`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `office-hours` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `office-hours` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module open-gstack-browser` `$qa --mode Report --module open-gstack-browser`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `open-gstack-browser` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `open-gstack-browser` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module pair-agent` `$qa --mode Report --module pair-agent`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `pair-agent` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill qa`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `pair-agent` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Product --module plan-ceo-review` `$plan --mode Product --module plan-ceo-review`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `plan-ceo-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill plan`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `plan-ceo-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$design --mode Critique --module plan-design-review` `$design --mode Critique --module plan-design-review`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `plan-design-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack --skill design`. Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `plan-design-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill design`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode DX --module plan-devex-review` `$plan --mode DX --module plan-devex-review`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `plan-devex-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-devex-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Engineering --module plan-eng-review` `$plan --mode Engineering --module plan-eng-review`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `plan-eng-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-eng-review` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module plan-tune` `$plan --mode Discovery --module plan-tune`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `plan-tune` 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-tune` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module qa-only` `$qa --mode Report --module qa-only`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `qa-only` 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 `qa-only` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module retro` `$plan --mode Discovery --module retro`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `retro` 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 `retro` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module scrape` `$qa --mode Report --module scrape`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `scrape` 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 `scrape` 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 setup-browser-cookies` `$qa --mode Report --module setup-browser-cookies`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `setup-browser-cookies` 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 `setup-browser-cookies` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$ship --mode Deploy --module setup-deploy` `$ship --mode Deploy --module setup-deploy`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `setup-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 `setup-deploy` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill ship`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module setup-gbrain` `$plan --mode Discovery --module setup-gbrain`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `setup-gbrain` 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 `setup-gbrain` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$qa --mode Report --module skillify` `$qa --mode Report --module skillify`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `skillify` 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 `skillify` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill qa`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Specification --module spec` `$plan --mode Specification --module spec`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `spec` 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 `spec` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$plan --mode Discovery --module sync-gbrain` `$plan --mode Discovery --module sync-gbrain`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `sync-gbrain` 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 `sync-gbrain` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill plan`.
+1 -1
View File
@@ -12,4 +12,4 @@ Print this replacement invocation, then dispatch to it exactly:
`$debug --mode Diagnose-only --module unfreeze` `$debug --mode Diagnose-only --module unfreeze`
Do not reproduce or summarize the retired specialist here. The canonical dispatcher must load its preserved `unfreeze` 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 `unfreeze` module. If that dispatcher is not installed, tell the user to install it with `npx skills add time-attack/gstack/skills --skill debug`.
+1 -1
View File
@@ -7,4 +7,4 @@ When no public-web choice is stored, present: A) Context.dev free setup (recomme
Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent. Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent.
Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack` and is never inferred from runtime state. Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack/skills` and is never inferred from runtime state.
@@ -13,8 +13,12 @@ import { fileURLToPath } from "node:url";
export const BOOTSTRAP_SCHEMA_VERSION = 2; export const BOOTSTRAP_SCHEMA_VERSION = 2;
export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0"; export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0";
// Keep the runtime compatibility version separate from the immutable release
// channel. Release candidates carry the 2.0.0 runtime contract while letting
// fresh-machine production journeys run before the stable v2.0.0 tag exists.
export const BOOTSTRAP_RELEASE_TAG = "v2.0.0-rc.1";
export const OFFICIAL_MANIFEST_URL = export const OFFICIAL_MANIFEST_URL =
`https://github.com/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/gstack-runtime-manifest.json`; `https://github.com/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/gstack-runtime-manifest.json`;
const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]); const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]);
const CAPABILITY_DEPENDENCIES = Object.freeze({ const CAPABILITY_DEPENDENCIES = Object.freeze({
browser: Object.freeze([]), browser: Object.freeze([]),
@@ -47,9 +51,9 @@ const ALLOWED_DOWNLOAD_HOSTS = new Set([
"objects.githubusercontent.com", "objects.githubusercontent.com",
"release-assets.githubusercontent.com", "release-assets.githubusercontent.com",
]); ]);
const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/`; const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/`;
const OFFICIAL_CERTIFICATE_IDENTITY = const OFFICIAL_CERTIFICATE_IDENTITY =
`https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/v${BOOTSTRAP_RUNTIME_VERSION}`; `https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/${BOOTSTRAP_RELEASE_TAG}`;
const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com"; const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
export async function main(argv = process.argv.slice(2), options = {}) { export async function main(argv = process.argv.slice(2), options = {}) {
@@ -90,7 +94,9 @@ export async function main(argv = process.argv.slice(2), options = {}) {
); );
const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL; const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL;
assertOfficialUrl(manifestUrl, { manifest: true }); assertOfficialUrl(manifestUrl, { manifest: true });
const manifest = await fetchJson(fetch_, manifestUrl); const manifest = await fetchJson(fetch_, manifestUrl, {
official: manifestUrl === OFFICIAL_MANIFEST_URL,
});
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
@@ -300,10 +306,18 @@ function sha256File(file) {
}); });
} }
async function fetchJson(fetch_, url) { async function fetchJson(fetch_, url, options = {}) {
const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" }); const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" });
assertFinalDownloadUrl(response.url || url); assertFinalDownloadUrl(response.url || url);
if (!response.ok) throw bootstrapError(`Download failed with HTTP ${response.status}`, "BOOTSTRAP_DOWNLOAD_FAILED"); if (!response.ok) {
if (options.official && response.status === 404) {
throw bootstrapError(
`Official runtime release ${BOOTSTRAP_RELEASE_TAG} is not published at ${url}. No files were downloaded or installed.`,
"BOOTSTRAP_RELEASE_UNAVAILABLE",
);
}
throw bootstrapError(`Manifest download failed with HTTP ${response.status} from ${url}. No files were downloaded or installed.`, "BOOTSTRAP_DOWNLOAD_FAILED");
}
const value = await response.json(); const value = await response.json();
if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID"); if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID");
return value; return value;
+1 -1
View File
@@ -7,4 +7,4 @@ When no public-web choice is stored, present: A) Context.dev free setup (recomme
Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent. Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent.
Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack` and is never inferred from runtime state. Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack/skills` and is never inferred from runtime state.
@@ -13,8 +13,12 @@ import { fileURLToPath } from "node:url";
export const BOOTSTRAP_SCHEMA_VERSION = 2; export const BOOTSTRAP_SCHEMA_VERSION = 2;
export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0"; export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0";
// Keep the runtime compatibility version separate from the immutable release
// channel. Release candidates carry the 2.0.0 runtime contract while letting
// fresh-machine production journeys run before the stable v2.0.0 tag exists.
export const BOOTSTRAP_RELEASE_TAG = "v2.0.0-rc.1";
export const OFFICIAL_MANIFEST_URL = export const OFFICIAL_MANIFEST_URL =
`https://github.com/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/gstack-runtime-manifest.json`; `https://github.com/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/gstack-runtime-manifest.json`;
const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]); const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]);
const CAPABILITY_DEPENDENCIES = Object.freeze({ const CAPABILITY_DEPENDENCIES = Object.freeze({
browser: Object.freeze([]), browser: Object.freeze([]),
@@ -47,9 +51,9 @@ const ALLOWED_DOWNLOAD_HOSTS = new Set([
"objects.githubusercontent.com", "objects.githubusercontent.com",
"release-assets.githubusercontent.com", "release-assets.githubusercontent.com",
]); ]);
const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/`; const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/`;
const OFFICIAL_CERTIFICATE_IDENTITY = const OFFICIAL_CERTIFICATE_IDENTITY =
`https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/v${BOOTSTRAP_RUNTIME_VERSION}`; `https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/${BOOTSTRAP_RELEASE_TAG}`;
const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com"; const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
export async function main(argv = process.argv.slice(2), options = {}) { export async function main(argv = process.argv.slice(2), options = {}) {
@@ -90,7 +94,9 @@ export async function main(argv = process.argv.slice(2), options = {}) {
); );
const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL; const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL;
assertOfficialUrl(manifestUrl, { manifest: true }); assertOfficialUrl(manifestUrl, { manifest: true });
const manifest = await fetchJson(fetch_, manifestUrl); const manifest = await fetchJson(fetch_, manifestUrl, {
official: manifestUrl === OFFICIAL_MANIFEST_URL,
});
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
@@ -300,10 +306,18 @@ function sha256File(file) {
}); });
} }
async function fetchJson(fetch_, url) { async function fetchJson(fetch_, url, options = {}) {
const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" }); const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" });
assertFinalDownloadUrl(response.url || url); assertFinalDownloadUrl(response.url || url);
if (!response.ok) throw bootstrapError(`Download failed with HTTP ${response.status}`, "BOOTSTRAP_DOWNLOAD_FAILED"); if (!response.ok) {
if (options.official && response.status === 404) {
throw bootstrapError(
`Official runtime release ${BOOTSTRAP_RELEASE_TAG} is not published at ${url}. No files were downloaded or installed.`,
"BOOTSTRAP_RELEASE_UNAVAILABLE",
);
}
throw bootstrapError(`Manifest download failed with HTTP ${response.status} from ${url}. No files were downloaded or installed.`, "BOOTSTRAP_DOWNLOAD_FAILED");
}
const value = await response.json(); const value = await response.json();
if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID"); if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID");
return value; return value;
+1 -1
View File
@@ -7,4 +7,4 @@ When no public-web choice is stored, present: A) Context.dev free setup (recomme
Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent. Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent.
Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack` and is never inferred from runtime state. Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack/skills` and is never inferred from runtime state.
@@ -13,8 +13,12 @@ import { fileURLToPath } from "node:url";
export const BOOTSTRAP_SCHEMA_VERSION = 2; export const BOOTSTRAP_SCHEMA_VERSION = 2;
export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0"; export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0";
// Keep the runtime compatibility version separate from the immutable release
// channel. Release candidates carry the 2.0.0 runtime contract while letting
// fresh-machine production journeys run before the stable v2.0.0 tag exists.
export const BOOTSTRAP_RELEASE_TAG = "v2.0.0-rc.1";
export const OFFICIAL_MANIFEST_URL = export const OFFICIAL_MANIFEST_URL =
`https://github.com/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/gstack-runtime-manifest.json`; `https://github.com/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/gstack-runtime-manifest.json`;
const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]); const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]);
const CAPABILITY_DEPENDENCIES = Object.freeze({ const CAPABILITY_DEPENDENCIES = Object.freeze({
browser: Object.freeze([]), browser: Object.freeze([]),
@@ -47,9 +51,9 @@ const ALLOWED_DOWNLOAD_HOSTS = new Set([
"objects.githubusercontent.com", "objects.githubusercontent.com",
"release-assets.githubusercontent.com", "release-assets.githubusercontent.com",
]); ]);
const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/`; const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/`;
const OFFICIAL_CERTIFICATE_IDENTITY = const OFFICIAL_CERTIFICATE_IDENTITY =
`https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/v${BOOTSTRAP_RUNTIME_VERSION}`; `https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/${BOOTSTRAP_RELEASE_TAG}`;
const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com"; const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
export async function main(argv = process.argv.slice(2), options = {}) { export async function main(argv = process.argv.slice(2), options = {}) {
@@ -90,7 +94,9 @@ export async function main(argv = process.argv.slice(2), options = {}) {
); );
const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL; const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL;
assertOfficialUrl(manifestUrl, { manifest: true }); assertOfficialUrl(manifestUrl, { manifest: true });
const manifest = await fetchJson(fetch_, manifestUrl); const manifest = await fetchJson(fetch_, manifestUrl, {
official: manifestUrl === OFFICIAL_MANIFEST_URL,
});
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
@@ -300,10 +306,18 @@ function sha256File(file) {
}); });
} }
async function fetchJson(fetch_, url) { async function fetchJson(fetch_, url, options = {}) {
const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" }); const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" });
assertFinalDownloadUrl(response.url || url); assertFinalDownloadUrl(response.url || url);
if (!response.ok) throw bootstrapError(`Download failed with HTTP ${response.status}`, "BOOTSTRAP_DOWNLOAD_FAILED"); if (!response.ok) {
if (options.official && response.status === 404) {
throw bootstrapError(
`Official runtime release ${BOOTSTRAP_RELEASE_TAG} is not published at ${url}. No files were downloaded or installed.`,
"BOOTSTRAP_RELEASE_UNAVAILABLE",
);
}
throw bootstrapError(`Manifest download failed with HTTP ${response.status} from ${url}. No files were downloaded or installed.`, "BOOTSTRAP_DOWNLOAD_FAILED");
}
const value = await response.json(); const value = await response.json();
if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID"); if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID");
return value; return value;
+1 -1
View File
@@ -7,4 +7,4 @@ When no public-web choice is stored, present: A) Context.dev free setup (recomme
Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent. Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent.
Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack` and is never inferred from runtime state. Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack/skills` and is never inferred from runtime state.
@@ -13,8 +13,12 @@ import { fileURLToPath } from "node:url";
export const BOOTSTRAP_SCHEMA_VERSION = 2; export const BOOTSTRAP_SCHEMA_VERSION = 2;
export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0"; export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0";
// Keep the runtime compatibility version separate from the immutable release
// channel. Release candidates carry the 2.0.0 runtime contract while letting
// fresh-machine production journeys run before the stable v2.0.0 tag exists.
export const BOOTSTRAP_RELEASE_TAG = "v2.0.0-rc.1";
export const OFFICIAL_MANIFEST_URL = export const OFFICIAL_MANIFEST_URL =
`https://github.com/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/gstack-runtime-manifest.json`; `https://github.com/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/gstack-runtime-manifest.json`;
const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]); const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]);
const CAPABILITY_DEPENDENCIES = Object.freeze({ const CAPABILITY_DEPENDENCIES = Object.freeze({
browser: Object.freeze([]), browser: Object.freeze([]),
@@ -47,9 +51,9 @@ const ALLOWED_DOWNLOAD_HOSTS = new Set([
"objects.githubusercontent.com", "objects.githubusercontent.com",
"release-assets.githubusercontent.com", "release-assets.githubusercontent.com",
]); ]);
const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/`; const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/`;
const OFFICIAL_CERTIFICATE_IDENTITY = const OFFICIAL_CERTIFICATE_IDENTITY =
`https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/v${BOOTSTRAP_RUNTIME_VERSION}`; `https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/${BOOTSTRAP_RELEASE_TAG}`;
const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com"; const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
export async function main(argv = process.argv.slice(2), options = {}) { export async function main(argv = process.argv.slice(2), options = {}) {
@@ -90,7 +94,9 @@ export async function main(argv = process.argv.slice(2), options = {}) {
); );
const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL; const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL;
assertOfficialUrl(manifestUrl, { manifest: true }); assertOfficialUrl(manifestUrl, { manifest: true });
const manifest = await fetchJson(fetch_, manifestUrl); const manifest = await fetchJson(fetch_, manifestUrl, {
official: manifestUrl === OFFICIAL_MANIFEST_URL,
});
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
@@ -300,10 +306,18 @@ function sha256File(file) {
}); });
} }
async function fetchJson(fetch_, url) { async function fetchJson(fetch_, url, options = {}) {
const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" }); const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" });
assertFinalDownloadUrl(response.url || url); assertFinalDownloadUrl(response.url || url);
if (!response.ok) throw bootstrapError(`Download failed with HTTP ${response.status}`, "BOOTSTRAP_DOWNLOAD_FAILED"); if (!response.ok) {
if (options.official && response.status === 404) {
throw bootstrapError(
`Official runtime release ${BOOTSTRAP_RELEASE_TAG} is not published at ${url}. No files were downloaded or installed.`,
"BOOTSTRAP_RELEASE_UNAVAILABLE",
);
}
throw bootstrapError(`Manifest download failed with HTTP ${response.status} from ${url}. No files were downloaded or installed.`, "BOOTSTRAP_DOWNLOAD_FAILED");
}
const value = await response.json(); const value = await response.json();
if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID"); if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID");
return value; return value;
+1 -1
View File
@@ -7,4 +7,4 @@ When no public-web choice is stored, present: A) Context.dev free setup (recomme
Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent. Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent.
Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack` and is never inferred from runtime state. Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack/skills` and is never inferred from runtime state.
@@ -13,8 +13,12 @@ import { fileURLToPath } from "node:url";
export const BOOTSTRAP_SCHEMA_VERSION = 2; export const BOOTSTRAP_SCHEMA_VERSION = 2;
export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0"; export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0";
// Keep the runtime compatibility version separate from the immutable release
// channel. Release candidates carry the 2.0.0 runtime contract while letting
// fresh-machine production journeys run before the stable v2.0.0 tag exists.
export const BOOTSTRAP_RELEASE_TAG = "v2.0.0-rc.1";
export const OFFICIAL_MANIFEST_URL = export const OFFICIAL_MANIFEST_URL =
`https://github.com/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/gstack-runtime-manifest.json`; `https://github.com/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/gstack-runtime-manifest.json`;
const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]); const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]);
const CAPABILITY_DEPENDENCIES = Object.freeze({ const CAPABILITY_DEPENDENCIES = Object.freeze({
browser: Object.freeze([]), browser: Object.freeze([]),
@@ -47,9 +51,9 @@ const ALLOWED_DOWNLOAD_HOSTS = new Set([
"objects.githubusercontent.com", "objects.githubusercontent.com",
"release-assets.githubusercontent.com", "release-assets.githubusercontent.com",
]); ]);
const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/`; const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/`;
const OFFICIAL_CERTIFICATE_IDENTITY = const OFFICIAL_CERTIFICATE_IDENTITY =
`https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/v${BOOTSTRAP_RUNTIME_VERSION}`; `https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/${BOOTSTRAP_RELEASE_TAG}`;
const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com"; const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
export async function main(argv = process.argv.slice(2), options = {}) { export async function main(argv = process.argv.slice(2), options = {}) {
@@ -90,7 +94,9 @@ export async function main(argv = process.argv.slice(2), options = {}) {
); );
const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL; const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL;
assertOfficialUrl(manifestUrl, { manifest: true }); assertOfficialUrl(manifestUrl, { manifest: true });
const manifest = await fetchJson(fetch_, manifestUrl); const manifest = await fetchJson(fetch_, manifestUrl, {
official: manifestUrl === OFFICIAL_MANIFEST_URL,
});
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
@@ -300,10 +306,18 @@ function sha256File(file) {
}); });
} }
async function fetchJson(fetch_, url) { async function fetchJson(fetch_, url, options = {}) {
const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" }); const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" });
assertFinalDownloadUrl(response.url || url); assertFinalDownloadUrl(response.url || url);
if (!response.ok) throw bootstrapError(`Download failed with HTTP ${response.status}`, "BOOTSTRAP_DOWNLOAD_FAILED"); if (!response.ok) {
if (options.official && response.status === 404) {
throw bootstrapError(
`Official runtime release ${BOOTSTRAP_RELEASE_TAG} is not published at ${url}. No files were downloaded or installed.`,
"BOOTSTRAP_RELEASE_UNAVAILABLE",
);
}
throw bootstrapError(`Manifest download failed with HTTP ${response.status} from ${url}. No files were downloaded or installed.`, "BOOTSTRAP_DOWNLOAD_FAILED");
}
const value = await response.json(); const value = await response.json();
if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID"); if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID");
return value; return value;
+1 -1
View File
@@ -7,4 +7,4 @@ When no public-web choice is stored, present: A) Context.dev free setup (recomme
Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent. Persist only the explicit choice with `gstack context select host`, `gstack context select local-browser`, or `gstack context select none`. For Context.dev, show `gstack context options`, then use `gstack context setup` and its hidden key prompt; consent and key storage belong to the runtime, never this judgment prompt. Do not infer Context choice or consent.
Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack` and is never inferred from runtime state. Capability-dependent work follows `references/RUNTIME.md`. Pure judgment never requires the runtime. Skill placement remains owned by `npx skills add time-attack/gstack/skills` and is never inferred from runtime state.
@@ -1,5 +1,5 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. --> <!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
<!-- GSTACK2_PROVENANCE source=gstack-upgrade/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=5402a1da3c857cbf50668085fce53172b72bba0d baseline_render_sha256=609971d414a31d49180e2630bd79a10b8d09085203f7e48c03d2ca69c2ef129a ported_render_sha256=a913cf77f76c4d68c576a06190b498e3d8d3b60b85498a68f173b23a7f800828 disposition=DUPLICATE_INFRASTRUCTURE --> <!-- GSTACK2_PROVENANCE source=gstack-upgrade/SKILL.md.tmpl base=bb57306d98c97011b0919c6132705a15b1579781 blob=5402a1da3c857cbf50668085fce53172b72bba0d baseline_render_sha256=609971d414a31d49180e2630bd79a10b8d09085203f7e48c03d2ca69c2ef129a ported_render_sha256=2c8cf9e505b7da27730bf646a33ee38e8092259f3335a524c71e57ba5b602541 disposition=DUPLICATE_INFRASTRUCTURE -->
<!-- GSTACK2_ROUTING replacement=$ship --mode Prepare --module gstack-upgrade visibility=internal depth=standard mutation=installation web=none --> <!-- GSTACK2_ROUTING replacement=$ship --mode Prepare --module gstack-upgrade visibility=internal depth=standard mutation=installation web=none -->
<!-- GSTACK2_LEGACY_BODY_START source=gstack-upgrade --> <!-- GSTACK2_LEGACY_BODY_START source=gstack-upgrade -->
@@ -7,7 +7,7 @@
The 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. The 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.
- Update selected skills with `npx skills add time-attack/gstack` using the user's existing project/global choice. Never infer or enroll a host. - 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.
- Upgrade a complete local runtime package with `gstack upgrade --source <complete-gstack-package> --version <version>`. - Upgrade a complete local runtime package with `gstack upgrade --source <complete-gstack-package> --version <version>`.
- Roll back the runtime with `gstack upgrade --rollback`. - Roll back the runtime with `gstack upgrade --rollback`.
- Run `gstack doctor` after either operation. - Run `gstack doctor` after either operation.
@@ -13,8 +13,12 @@ import { fileURLToPath } from "node:url";
export const BOOTSTRAP_SCHEMA_VERSION = 2; export const BOOTSTRAP_SCHEMA_VERSION = 2;
export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0"; export const BOOTSTRAP_RUNTIME_VERSION = "2.0.0";
// Keep the runtime compatibility version separate from the immutable release
// channel. Release candidates carry the 2.0.0 runtime contract while letting
// fresh-machine production journeys run before the stable v2.0.0 tag exists.
export const BOOTSTRAP_RELEASE_TAG = "v2.0.0-rc.1";
export const OFFICIAL_MANIFEST_URL = export const OFFICIAL_MANIFEST_URL =
`https://github.com/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/gstack-runtime-manifest.json`; `https://github.com/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/gstack-runtime-manifest.json`;
const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]); const CAPABILITIES = new Set(["browser", "browser-visible", "design", "pdf", "diagram", "ios"]);
const CAPABILITY_DEPENDENCIES = Object.freeze({ const CAPABILITY_DEPENDENCIES = Object.freeze({
browser: Object.freeze([]), browser: Object.freeze([]),
@@ -47,9 +51,9 @@ const ALLOWED_DOWNLOAD_HOSTS = new Set([
"objects.githubusercontent.com", "objects.githubusercontent.com",
"release-assets.githubusercontent.com", "release-assets.githubusercontent.com",
]); ]);
const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/`; const OFFICIAL_RELEASE_PREFIX = `/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/`;
const OFFICIAL_CERTIFICATE_IDENTITY = const OFFICIAL_CERTIFICATE_IDENTITY =
`https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/v${BOOTSTRAP_RUNTIME_VERSION}`; `https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/${BOOTSTRAP_RELEASE_TAG}`;
const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com"; const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
export async function main(argv = process.argv.slice(2), options = {}) { export async function main(argv = process.argv.slice(2), options = {}) {
@@ -90,7 +94,9 @@ export async function main(argv = process.argv.slice(2), options = {}) {
); );
const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL; const manifestUrl = options.manifestUrl ?? OFFICIAL_MANIFEST_URL;
assertOfficialUrl(manifestUrl, { manifest: true }); assertOfficialUrl(manifestUrl, { manifest: true });
const manifest = await fetchJson(fetch_, manifestUrl); const manifest = await fetchJson(fetch_, manifestUrl, {
official: manifestUrl === OFFICIAL_MANIFEST_URL,
});
validateManifest(manifest, target); validateManifest(manifest, target);
const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack")); const home = path.resolve(parsed.home ?? process.env.GSTACK_HOME ?? path.join(os.homedir(), ".gstack"));
const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null); const reusable = await inspectReusableRuntime(home, manifest.version).catch(() => null);
@@ -300,10 +306,18 @@ function sha256File(file) {
}); });
} }
async function fetchJson(fetch_, url) { async function fetchJson(fetch_, url, options = {}) {
const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" }); const response = await fetch_(url, { headers: { Accept: "application/json" }, redirect: "follow" });
assertFinalDownloadUrl(response.url || url); assertFinalDownloadUrl(response.url || url);
if (!response.ok) throw bootstrapError(`Download failed with HTTP ${response.status}`, "BOOTSTRAP_DOWNLOAD_FAILED"); if (!response.ok) {
if (options.official && response.status === 404) {
throw bootstrapError(
`Official runtime release ${BOOTSTRAP_RELEASE_TAG} is not published at ${url}. No files were downloaded or installed.`,
"BOOTSTRAP_RELEASE_UNAVAILABLE",
);
}
throw bootstrapError(`Manifest download failed with HTTP ${response.status} from ${url}. No files were downloaded or installed.`, "BOOTSTRAP_DOWNLOAD_FAILED");
}
const value = await response.json(); const value = await response.json();
if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID"); if (!value || typeof value !== "object") throw bootstrapError("Manifest returned invalid JSON", "BOOTSTRAP_MANIFEST_INVALID");
return value; return value;
+7
View File
@@ -29,6 +29,13 @@ afterEach(() => {
}); });
describe('GStack 2 standard installer surface', () => { describe('GStack 2 standard installer surface', () => {
test('documents the canonical public subpath instead of the legacy-bearing repository root', () => {
for (const file of ['AGENTS.md', 'CLAUDE.md', 'README.md']) {
const content = fs.readFileSync(path.join(DEFAULT_REPO_ROOT, file), 'utf8');
expect(content, file).toContain('npx skills add time-attack/gstack/skills');
}
});
test('publishes exactly six uniquely named canonical skills', () => { test('publishes exactly six uniquely named canonical skills', () => {
const result = inspectRepository(DEFAULT_REPO_ROOT); const result = inspectRepository(DEFAULT_REPO_ROOT);
@@ -0,0 +1,66 @@
import { describe, expect, test } from "bun:test";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
const ROOT = path.resolve(import.meta.dir, "..");
const SCRIPT = path.join(ROOT, ".github", "scripts", "create-runtime-release-manifest.mjs");
const WORKFLOW = path.join(ROOT, ".github", "workflows", "release-artifacts.yml");
const TARGETS = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "windows-arm64", "windows-x64"];
const COMMON = ["core", "browser-code", "browser-headless", "browser-visible", "design", "diagram", "pdf"];
async function stageFixture(directory: string) {
for (const target of TARGETS) {
const components = [...COMMON, ...(target.startsWith("darwin-") ? ["ios"] : [])];
for (const component of components) {
const name = `gstack-runtime-2.0.0-${target}-${component}.tar.gz`;
await fs.writeFile(path.join(directory, name), "fixture\n");
await fs.writeFile(path.join(directory, `${name}.sha256`), `${"a".repeat(64)} ${name}\n`);
await fs.writeFile(path.join(directory, `${name}.sigstore.json`), "{}\n");
}
}
}
describe("GStack runtime release channel", () => {
test("release candidates retain runtime compatibility while binding URLs and signatures to the RC tag", async () => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-runtime-release-channel-"));
try {
await stageFixture(directory);
const result = spawnSync(process.execPath, [SCRIPT, directory, "time-attack/gstack", "2.0.0", "v2.0.0-rc.1"], {
encoding: "utf8",
});
expect(result.status).toBe(0);
const manifest = JSON.parse(await fs.readFile(path.join(directory, "gstack-runtime-manifest.json"), "utf8"));
expect(manifest.version).toBe("2.0.0");
expect(manifest.targets["darwin-arm64"].components["browser-visible"]).toMatchObject({
url: "https://github.com/time-attack/gstack/releases/download/v2.0.0-rc.1/gstack-runtime-2.0.0-darwin-arm64-browser-visible.tar.gz",
certificateIdentity: "https://github.com/time-attack/gstack/.github/workflows/release-artifacts.yml@refs/tags/v2.0.0-rc.1",
});
} finally {
await fs.rm(directory, { recursive: true, force: true });
}
});
test("release manifest generation rejects non-runtime tags before reading artifacts", async () => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-runtime-invalid-tag-"));
try {
const result = spawnSync(process.execPath, [SCRIPT, directory, "time-attack/gstack", "2.0.0", "main"], {
encoding: "utf8",
});
expect(result.status).not.toBe(0);
expect(`${result.stdout}${result.stderr}`).toContain("Invalid runtime release tag");
} finally {
await fs.rm(directory, { recursive: true, force: true });
}
});
test("release workflow publishes both RC and stable tags through the same signed manifest path", async () => {
const workflow = await fs.readFile(WORKFLOW, "utf8");
expect(workflow).toContain("v2.0.0-rc.*");
expect(workflow).toContain('2.0.0 "$GITHUB_REF_NAME"');
expect(workflow).toContain("PRERELEASE_FLAG:");
expect(workflow).toContain("--prerelease");
expect(workflow).toContain('gh release create "$GITHUB_REF_NAME"');
});
});
+27 -1
View File
@@ -10,6 +10,7 @@ import { setupRuntime } from "../runtime/setup.js";
import { bashCandidates, resolveBashCommand } from "../runtime/tooling.js"; import { bashCandidates, resolveBashCommand } from "../runtime/tooling.js";
import { import {
BOOTSTRAP_SCHEMA_VERSION, BOOTSTRAP_SCHEMA_VERSION,
BOOTSTRAP_RELEASE_TAG,
BOOTSTRAP_RUNTIME_VERSION, BOOTSTRAP_RUNTIME_VERSION,
CAPABILITY_COMPONENTS, CAPABILITY_COMPONENTS,
COMPONENT_DEPENDENCIES, COMPONENT_DEPENDENCIES,
@@ -27,7 +28,7 @@ function officialManifestFixture(target: string, customize?: (component: string,
.filter((component) => component !== "ios" || target.startsWith("darwin-")) .filter((component) => component !== "ios" || target.startsWith("darwin-"))
.map((component) => { .map((component) => {
const artifact: Record<string, unknown> = { const artifact: Record<string, unknown> = {
url: `https://github.com/time-attack/gstack/releases/download/v${BOOTSTRAP_RUNTIME_VERSION}/${component}.tar.gz`, url: `https://github.com/time-attack/gstack/releases/download/${BOOTSTRAP_RELEASE_TAG}/${component}.tar.gz`,
sha256: "0".repeat(64), sha256: "0".repeat(64),
bytes: 8, bytes: 8,
format: "tar.gz", format: "tar.gz",
@@ -296,6 +297,31 @@ describe("GStack runtime setup UX", () => {
expect(fetches).toBe(0); expect(fetches).toBe(0);
}); });
test("missing official release stops before install with an actionable immutable-tag error", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-bootstrap-missing-release-"));
const output = capture();
let calls = 0;
try {
expect(await bootstrapMain([
"preview", "--capability", "browser-visible", "--home", path.join(root, "home"),
], {
stdout: output.stream,
stderr: output.stream,
fetch: async (url: string) => {
calls += 1;
return { ok: false, status: 404, url };
},
})).toBe(1);
expect(calls).toBe(1);
expect(output.value()).toContain(`Official runtime release ${BOOTSTRAP_RELEASE_TAG} is not published`);
expect(output.value()).toContain(OFFICIAL_MANIFEST_URL);
expect(output.value()).toContain("No files were downloaded or installed");
expect(await fs.readdir(root)).toEqual([]);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("bootstrap executes through a symlinked or aliased filesystem path", async () => { test("bootstrap executes through a symlinked or aliased filesystem path", async () => {
if (process.platform === "win32") return; if (process.platform === "win32") return;
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-bootstrap-link-")); const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-bootstrap-link-"));