mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-04 07:47:27 +01:00
* docs(designs): gstack field wave plan (2026-10-12) and the shared simulated Capy host fixture Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(doctor): --min-revision, a launch Bun found off PATH, planning-only pins SKIP gstack-doctor --check gains --min-revision <v>|unknown: below it the revision row FAILs with REVISION_BELOW_MINIMUM naming the Capy wrapper or bin/gstack-upgrade; unknown says so and never claims the minimum. The runtime and pins rows name a supported Bun found in $BUN_INSTALL/bin (not on PATH) with the export/GSTACK_LAUNCH fix (gstack_bun_launch_dir, one helper in bin/gstack-bun-version.sh). --for a planning-only workflow (autoplan, the plan reviews, office-hours) reports a repo test-pin mismatch as SKIP pins (planning-only; ...). New result codes UPGRADE_REFUSED, UPGRADE_FETCH_FAILED, REVISION_BELOW_MINIMUM, CAPY_WRAPPER_STALE, INSTALL_UNVERIFIED with troubleshooting anchors; REVISION_UNMET's fix names bin/gstack-upgrade. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(install): install verdict and install admission at skill start bin/gstack-install-verdict.sh (Bash) and lib/install-verdict.ts (TypeScript) are the one reader of the install verdict gstack-capy-install writes to <state root>/install-verdicts/<sha256(checkout realpath)>.json (checkout, HEAD, workflow and required components, planning-only pins, trailer). The source checkout and capy/support.env resolve through gstack_source_root / sourceRoot, which follow an installed root's .source-path. gstack-skill-start, after SESSION_KIND: an unattended session (any host) refuses on a PATH Bun below the supported minimum; with GSTACK_CAPY=1 an unattended or explicit spawned session also refuses without a passing verdict for this checkout, HEAD and workflow (INSTALL_UNVERIFIED). A refusal prints the INSTALL: row, an install-refused instruction block bound to its SESSION_ID, GSTACK_RESULT ... status=refused run=- and exits 3. Ambient spawned sessions warn; GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1 admits and says so. capy/support.env declares GSTACK_MIN_REVISION and GSTACK_CAPY_MIN_WRAPPER. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(capy-install): launch PATH, Bun reuse and CI pins, setup skip on HEAD, verdict, --check The GSTACK_LAUNCH line carries PATH=<dir>:$PATH when the certified Bun is not first on PATH, plus GSTACK_CAPY=1 and GSTACK_INSTALL_VERDICT, and GSTACK_LAUNCH_PATH: <dir>; the doctor runs with that PATH. A supported Bun in $BUN_INSTALL/bin is reused instead of downloaded again. Project pins are read with bin/gstack-runtime-pins.ts (CI lanes included): when every exact pin is gstack's verified version the archive is installed alongside a different PATH Bun; any other exact pin is RUNTIME_PIN_CONFLICT. Both ./setup runs are skipped when this HEAD was set up with the same hosts, model and browser flag and the check passes (never after a fast-forward); a failed setup ends with gstack: fail setup and is retried next run. --check certifies only; GSTACK_CAPY_LOCK_HELD=1 accepts the bootstrapper's held lock; result anchors come from the generated components table; REV_NOTE names bin/gstack-upgrade. Intentional pin change: test/gstack-capy-install.test.ts --revision trailer now names <root>/bin/gstack-upgrade (the old text named a command that never existed). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(upgrade): bin/gstack-upgrade, the unattended fast-forward every fix line names --check fetches and prints current|behind <installed> -> <remote> (<n> commits); the default fast-forwards to the remote default branch tip and reinstalls (bin/gstack-capy-install on a Capy install, else ./setup --refresh-registered), completing a pending reinstall on the next run. It refuses (UPGRADE_REFUSED, exit 3, nothing changed) a dirty tracked byte, a diverged commit, a pinned checkout, the incoming Bun floor and a held .setup-lock; never stashes or resets; an unreachable remote is UPGRADE_FETCH_FAILED. Fetches are receipted (fail-open). The unattended skill-end UPGRADE_AVAILABLE line is followed by UPGRADE_COMMAND naming it. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(capy): vendored Capy skill, frozen bootstrapper and full wrapper bin/gstack-capy-session capy/skill/gstack/ is the Drive skill's source of truth. scripts/install.sh is a frozen bootstrapper: it prints gstack-capy-skill: wrapper <n> (<path>), clones or fetches under .gstack-capy-install.lock, reads the remote capy/support.env (FETCH_HEAD), fast-forwards a clean unpinned checkout below GSTACK_MIN_REVISION (refusing dirty, diverged or pinned checkouts with the exact commands; GSTACK_CAPY_AUTO_UPGRADE=0 disables), and execs the checkout's own bin/gstack-capy-session with GSTACK_CAPY_LOCK_HELD=1 so the held lock is never taken twice. Offline it says the remote minimum is unknown. A Drive copy older than GSTACK_CAPY_MIN_WRAPPER prints an owner action (CAPY_WRAPPER_STALE). SKILL.md: check at session start, install lazily before the first workflow, the GSTACK_LAUNCH line, the trailer rule, the subagent first-report line, gstack-pregate sealed before any push, and the autoplan/DRIVER.md pointer. The context-budget fixture gains the skill's entry. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: Capy install layers, lazy install, snapshot bake, forward-only recovery, raising the minimum docs/capy-install.md describes the three layers (Drive bootstrapper, full wrapper, installer), the lazy install, the GSTACK_LAUNCH line, install admission, baking gstack into a Capy snapshot, and that recovery after an interrupted fast-forward completes or refuses the pending install stages (checking out an older commit is not a rollback). docs/CHANGELOG_STYLE.md (the release doc; there is no docs/RELEASING.md) says when GSTACK_MIN_REVISION may be raised. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * refactor(capy): the stale-copy owner action moves from the frozen bootstrapper to bin/gstack-capy-session The checkout-owned full wrapper reads GSTACK_CAPY_MIN_WRAPPER through gstack_support_env_get(gstack_source_root) and compares it with the bootstrapper revision it was handed, so the rule can change without recopying the Drive skill. The bootstrapper keeps only what a stale checkout cannot do: clone, fetch, read the remote minimum, fast-forward, exec. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * v1.91.96.0: honest install and self-upgrade on Capy; supported minimum 1.91.96.0 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: drop the helper-only Capy host fixture test (test-of-test ratchet); lane tests exercise the helper Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: bound the new sync spawns in the install-verdict and wrapper tests (spawnSync tripwire) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ci(paid-pr-profile): the vendored Capy skill is free-only; free-only files are not prompts needing paid coverage Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore(harness): non-behavioral HARNESS_VERSION decision for the PR-profile free-only rule Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(doctor): lane A runtime and pins cases are platform-neutral (bash paths; a lane-free .bun-version pin) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(designs): field wave plan with amendments and the gate rev 2-3 review record (ARTIFACT_VALID, 3 gate revisions) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy-install): follow a project's exact Bun pin above the minimum when gstack has its verified archive (adds Bun 1.4.3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: the installer follows a verified exact project Bun pin (capy-install, troubleshooting, changelog) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy-install): the install reason names the project's pin and gstack's real minimum Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
233 lines
9.6 KiB
TypeScript
233 lines
9.6 KiB
TypeScript
/**
|
|
* doctor-components — the one component table behind `gstack-doctor --check`.
|
|
*
|
|
* Row ids, `--for <skill>` resolution, `--require <csv>`, the trailer and the
|
|
* `--json` keys all come from here, so a parent agent never maps between
|
|
* vocabularies. The doctor is Bash 3.2 and must report with Bun absent, so it
|
|
* cannot import this file: scripts/gen-doctor-components.ts renders it into
|
|
* bin/gstack-doctor-components.sh (sourced by bin/gstack-doctor-check.sh) and
|
|
* test/doctor-components.test.ts keeps the committed render fresh, the way
|
|
* agents-digest/gstack-AGENTS.md is kept fresh.
|
|
*
|
|
* Row semantics (printed as `PASS | FAIL | SKIP <id>`):
|
|
* core always evaluated and always required (runtime, pins, state root,
|
|
* privacy, cores, revision). `pins` and `state-root` SKIP with a
|
|
* reason when they cannot be evaluated; a SKIP never fails a run.
|
|
* host a host profile. Required when `--for`/`--require` names it, or by
|
|
* default when it is installed from this checkout. With no flag and
|
|
* no host installed, `claude` (setup's default host) is required so
|
|
* an empty machine is never `ok`.
|
|
* component a built or rendered artifact (patch, browse bundle, browser).
|
|
* SKIP (not_applicable) when the selected workflow does not need it.
|
|
* optional present-or-absent helpers (Codex CLI, CSO native helper). SKIP
|
|
* (optional) when absent and not required; FAIL only when required.
|
|
*/
|
|
|
|
import { RESULT_CODES } from './result-codes';
|
|
|
|
export type ComponentKind = 'core' | 'host' | 'component' | 'optional';
|
|
|
|
export interface DoctorComponent {
|
|
id: string;
|
|
title: string;
|
|
kind: ComponentKind;
|
|
}
|
|
|
|
export const DOCTOR_COMPONENTS: readonly DoctorComponent[] = [
|
|
{ id: 'claude', title: 'Claude skills', kind: 'host' },
|
|
{ id: 'codex', title: 'Codex skills', kind: 'host' },
|
|
{ id: 'patch', title: 'Astra behavioral patch in the Codex render', kind: 'component' },
|
|
{ id: 'codex-cli', title: 'Codex CLI (outside voice)', kind: 'optional' },
|
|
{ id: 'browse-bundle', title: 'browse bundle', kind: 'component' },
|
|
{ id: 'browser', title: 'Chromium launch and render', kind: 'component' },
|
|
{ id: 'cso', title: 'CSO native helper', kind: 'optional' },
|
|
{ id: 'runtime', title: 'gstack runtime (Bun)', kind: 'core' },
|
|
{ id: 'pins', title: 'project pins', kind: 'core' },
|
|
{ id: 'state-root', title: 'state root', kind: 'core' },
|
|
{ id: 'privacy', title: 'privacy', kind: 'core' },
|
|
{ id: 'cores', title: 'cores', kind: 'core' },
|
|
{ id: 'revision', title: 'revision', kind: 'core' },
|
|
] as const;
|
|
|
|
export type ComponentId = (typeof DOCTOR_COMPONENTS)[number]['id'];
|
|
|
|
export const COMPONENT_IDS: readonly ComponentId[] = DOCTOR_COMPONENTS.map(c => c.id);
|
|
|
|
/** What a workflow needs beyond the core rows. `optional` rows SKIP when absent. */
|
|
export interface SkillRequirement {
|
|
requires: readonly ComponentId[];
|
|
optional: readonly ComponentId[];
|
|
}
|
|
|
|
const PLANNING: SkillRequirement = { requires: ['claude', 'codex', 'patch'], optional: ['codex-cli'] };
|
|
const BROWSER: SkillRequirement = { requires: ['claude', 'codex', 'patch', 'browse-bundle', 'browser'], optional: ['codex-cli'] };
|
|
const CLAUDE_ONLY: SkillRequirement = { requires: ['claude'], optional: [] };
|
|
|
|
/**
|
|
* Every skill directory in the repo is classified here; test/doctor-components.test.ts
|
|
* fails when a skill is added without a row. Planning skills need both host
|
|
* profiles and the Astra patch (the outside voice reads the Codex render);
|
|
* browser skills add the browse bundle and Chromium; /cso adds the native
|
|
* helper; /codex needs the CLI itself; everything else runs on the Claude
|
|
* profile alone.
|
|
*/
|
|
export const SKILL_REQUIREMENTS: Readonly<Record<string, SkillRequirement>> = {
|
|
autoplan: PLANNING,
|
|
'plan-ceo-review': PLANNING,
|
|
'plan-eng-review': PLANNING,
|
|
'plan-design-review': PLANNING,
|
|
'plan-devex-review': PLANNING,
|
|
'plan-tune': PLANNING,
|
|
'office-hours': PLANNING,
|
|
spec: PLANNING,
|
|
review: PLANNING,
|
|
ship: PLANNING,
|
|
investigate: PLANNING,
|
|
codex: { requires: ['claude', 'codex', 'patch', 'codex-cli'], optional: [] },
|
|
cso: { requires: ['claude', 'cso'], optional: [] },
|
|
browse: BROWSER,
|
|
qa: BROWSER,
|
|
'qa-only': BROWSER,
|
|
'design-review': BROWSER,
|
|
'design-consultation': BROWSER,
|
|
'design-html': BROWSER,
|
|
'devex-review': BROWSER,
|
|
scrape: BROWSER,
|
|
skillify: BROWSER,
|
|
canary: BROWSER,
|
|
benchmark: BROWSER,
|
|
'make-pdf': BROWSER,
|
|
diagram: BROWSER,
|
|
'pair-agent': BROWSER,
|
|
'open-gstack-browser': BROWSER,
|
|
'setup-browser-cookies': BROWSER,
|
|
'connect-chrome': BROWSER,
|
|
'land-and-deploy': BROWSER,
|
|
'benchmark-models': CLAUDE_ONLY,
|
|
careful: CLAUDE_ONLY,
|
|
'claude-code': CLAUDE_ONLY,
|
|
'context-restore': CLAUDE_ONLY,
|
|
'context-save': CLAUDE_ONLY,
|
|
'design-shotgun': CLAUDE_ONLY,
|
|
'deslop-shared-libs': CLAUDE_ONLY,
|
|
'document-generate': CLAUDE_ONLY,
|
|
'document-release': CLAUDE_ONLY,
|
|
'eval-plan': CLAUDE_ONLY,
|
|
freeze: CLAUDE_ONLY,
|
|
'gstack-upgrade': CLAUDE_ONLY,
|
|
guard: CLAUDE_ONLY,
|
|
health: CLAUDE_ONLY,
|
|
'ios-clean': CLAUDE_ONLY,
|
|
'ios-design-review': CLAUDE_ONLY,
|
|
'ios-fix': CLAUDE_ONLY,
|
|
'ios-qa': CLAUDE_ONLY,
|
|
'ios-sync': CLAUDE_ONLY,
|
|
'landing-report': CLAUDE_ONLY,
|
|
learn: CLAUDE_ONLY,
|
|
retro: CLAUDE_ONLY,
|
|
'setup-deploy': CLAUDE_ONLY,
|
|
'setup-gbrain': CLAUDE_ONLY,
|
|
'sync-gbrain': CLAUDE_ONLY,
|
|
'test-audit': CLAUDE_ONLY,
|
|
unfreeze: CLAUDE_ONLY,
|
|
};
|
|
|
|
/**
|
|
* Planning-only workflows: they read and review a plan but never run the
|
|
* target repository's tests, so a repo test-pin mismatch is reported as
|
|
* `SKIP pins (planning-only; ...)` for `--for <skill>` instead of failing.
|
|
* Workflows that run the repo's tests (ship, review, qa, pregate) keep `pins`
|
|
* required.
|
|
*/
|
|
export const PINS_ADVISORY_SKILLS: readonly string[] = [
|
|
'autoplan', 'plan-ceo-review', 'plan-eng-review', 'plan-design-review', 'plan-devex-review', 'office-hours',
|
|
];
|
|
|
|
/** Rows always evaluated and required, in print order. */
|
|
export const CORE_IDS: readonly ComponentId[] = DOCTOR_COMPONENTS.filter(c => c.kind === 'core').map(c => c.id);
|
|
export const OPTIONAL_IDS: readonly ComponentId[] = DOCTOR_COMPONENTS.filter(c => c.kind === 'optional').map(c => c.id);
|
|
export const HOST_IDS: readonly ComponentId[] = DOCTOR_COMPONENTS.filter(c => c.kind === 'host').map(c => c.id);
|
|
|
|
/** The SKIP reason the installer and the doctor print for a lazily installed browser. */
|
|
export const BROWSER_LAZY_REASON = 'lazy; gstack-browser-ensure installs on first use';
|
|
|
|
/** Render the table as Bash 3.2 (sourced by bin/gstack-doctor-check.sh). */
|
|
export function renderDoctorComponentsSh(): string {
|
|
const caseArm = (ids: readonly string[], body: string) => ` ${ids.join('|')}) ${body} ;;`;
|
|
const groups = new Map<string, string[]>();
|
|
for (const [skill, req] of Object.entries(SKILL_REQUIREMENTS)) {
|
|
const key = `${req.requires.join(' ')}\t${req.optional.join(' ')}`;
|
|
groups.set(key, [...(groups.get(key) ?? []), skill]);
|
|
}
|
|
const requireArms: string[] = [];
|
|
const optionalArms: string[] = [];
|
|
for (const [key, skills] of groups) {
|
|
const [requires, optional] = key.split('\t');
|
|
requireArms.push(caseArm(skills, `printf '%s' '${requires}'`));
|
|
optionalArms.push(caseArm(skills, `printf '%s' '${optional}'`));
|
|
}
|
|
return [
|
|
'# shellcheck shell=bash',
|
|
'# gstack-doctor-components.sh — GENERATED from lib/doctor-components.ts and',
|
|
'# lib/result-codes.ts by scripts/gen-doctor-components.ts (bun run gen:skill-docs). Do not edit; the',
|
|
'# freshness test is test/doctor-components.test.ts. Sourced by',
|
|
'# bin/gstack-doctor-check.sh; Bash 3.2 builtins only.',
|
|
'',
|
|
`GSTACK_DOCTOR_COMPONENTS="${COMPONENT_IDS.join(' ')}"`,
|
|
`GSTACK_DOCTOR_CORE="${CORE_IDS.join(' ')}"`,
|
|
`GSTACK_DOCTOR_HOSTS="${HOST_IDS.join(' ')}"`,
|
|
`GSTACK_DOCTOR_OPTIONAL="${OPTIONAL_IDS.join(' ')}"`,
|
|
`GSTACK_DOCTOR_SKILLS="${Object.keys(SKILL_REQUIREMENTS).join(' ')}"`,
|
|
`GSTACK_BROWSER_LAZY_REASON="${BROWSER_LAZY_REASON}"`,
|
|
'',
|
|
'# gstack_doctor_component_title ID — the human title; returns 1 for an unknown id.',
|
|
'gstack_doctor_component_title() {',
|
|
' case "$1" in',
|
|
...DOCTOR_COMPONENTS.map(c => caseArm([c.id], `printf '%s' '${c.title.replace(/'/g, "'\\''")}'`)),
|
|
' *) return 1 ;;',
|
|
' esac',
|
|
'}',
|
|
'',
|
|
'# gstack_doctor_component_kind ID — core | host | component | optional; returns 1 for an unknown id.',
|
|
'gstack_doctor_component_kind() {',
|
|
' case "$1" in',
|
|
...DOCTOR_COMPONENTS.map(c => caseArm([c.id], `printf '%s' '${c.kind}'`)),
|
|
' *) return 1 ;;',
|
|
' esac',
|
|
'}',
|
|
'',
|
|
'# gstack_doctor_skill_requires SKILL — the non-core ids the skill needs; returns 1 for an unknown skill.',
|
|
'gstack_doctor_skill_requires() {',
|
|
' case "$1" in',
|
|
...requireArms,
|
|
' *) return 1 ;;',
|
|
' esac',
|
|
'}',
|
|
'',
|
|
'# gstack_doctor_skill_optional SKILL — ids that SKIP when absent; returns 1 for an unknown skill.',
|
|
'gstack_doctor_skill_optional() {',
|
|
' case "$1" in',
|
|
...optionalArms,
|
|
' *) return 1 ;;',
|
|
' esac',
|
|
'}',
|
|
'',
|
|
'# gstack_doctor_skill_pins_advisory SKILL — true for a planning-only workflow (a repo test-pin mismatch SKIPs pins).',
|
|
'gstack_doctor_skill_pins_advisory() {',
|
|
' case "$1" in',
|
|
caseArm(PINS_ADVISORY_SKILLS, 'return 0'),
|
|
' *) return 1 ;;',
|
|
' esac',
|
|
'}',
|
|
'',
|
|
'# gstack_result_anchor CODE — the docs/troubleshooting.md anchor for a lib/result-codes.ts code; returns 1 for an unknown code.',
|
|
'gstack_result_anchor() {',
|
|
' case "$1" in',
|
|
...Object.entries(RESULT_CODES).map(([code, row]) => caseArm([code], `printf '%s' '${row.anchor}'`)),
|
|
' *) return 1 ;;',
|
|
' esac',
|
|
'}',
|
|
'',
|
|
].join('\n');
|
|
}
|