mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-22 21:00:51 +02:00
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:
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"')
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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-"));
|
||||||
|
|||||||
Reference in New Issue
Block a user