mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
* fix(capy): the install trailer says behind=<n> and its fix, so a stale install shows in every relayed trailer (field list 2 item 13, #2378) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy): a lease records its holder's start identity, so a pid reused after a wake is reclaimed; a skipped session-start upgrade ends fail behind, never ok (GBRA-105, GBRA-106) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): stale needs proof (unreadable or malformed identities keep the lease), ps identity is timezone-independent, and reclaim never deletes a concurrent renewal Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(autoplan): enforced round cap and spend budget, deferred close, public export without machine paths, opt-in durable checkpoints (field list 2 W5-W7) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): one directory lock for publish, prune and reclaim; reclaim rereads the record judged stale, so a renewal is never deleted or hidden Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): the directory lock names its holder; broken only when the holder is dead or its pid reused, unlock removes only its own lock, touch exits 3 under a live holder Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): breakers serialize on .lock.break and reread the lock before removing it; gstack-skill-start reports a lease the held lock kept out Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(lease): name the double-crash limit of breaking a dead lock Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(capy): --for review-voice install profile, no-Drive bootstrap command, reviewer prompt trailer (field list 2 W11) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit89bf556c11) * test: pin review-voice row, helpers row and the no-Drive fallback in existing install tests Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit7f6c70459a) * test: the DRIVER template names the reviewer trailer relay (pregate lane) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit2545d9a6d2) * test(context-budget): capy skill eager ceiling for the review-voice subagent paragraph (W11; ratchet protocol, capture value) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(eval-plan): prereg diagnostics dry run (W8) and .gstack/eval-models.json lint (W9), WIP Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commitb509096218) * feat(eval-plan): W8/W9 tests, docs/eval-models.md, troubleshooting anchors, /eval-plan steps Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit9335e45de2) * test(eval-plan): eval-plan/SKILL.md names only defined codes and accepted flags Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commita7a974d268) * test(review-voice): the launch line points at the verdict's bun-only directory (#3233's launch contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(third-party-actions): the Aside drive-offer detector matches the product name, not the English idiom (paid run 38988927989 tpa-absent-linux false positive) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): export refuses artifact paths outside the run before writing; a deferred close keeps earlier undecided findings; reopening CEO grants Eng its rerun (review of #3234) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(autoplan-budget): bind the reservation expectation to lib/autoplan-budget's default estimate (test-of-test ratchet) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(evals): the docs observer accepts the spawned-prefixed gstack-skill-start --help read Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): curate capy-review-voice out of the Windows subset (POSIX launcher contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: changelog entry for field list 2 wave A Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: stamp v1.91.115.0 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>
283 lines
13 KiB
TypeScript
283 lines
13 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, wrapper). `pins`, `state-root` and
|
|
* `wrapper` SKIP with a reason when they cannot be evaluated; a
|
|
* SKIP never fails a run. `wrapper` WARNs (an owner action, never
|
|
* a failure) for a stale Capy skill copy the run did not use.
|
|
* 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.
|
|
*
|
|
* Install profiles (INSTALL_PROFILES) are SKILL_REQUIREMENTS rows that name an
|
|
* install shape instead of a skill directory. `--for review-voice` is the one
|
|
* today: a dispatched reviewer's machine gets a clone or fast-forward and Bun
|
|
* at gstack's own minimum (the project's pins are never applied, so `pins`
|
|
* SKIPs), no ./setup and no host profiles, and the trailer certifies only the
|
|
* read-only helpers (`helpers`). Its verdict covers no host, so admission
|
|
* refuses every workflow with a `--for <skill>` fix.
|
|
*/
|
|
|
|
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: 'helpers', title: 'read-only helpers (gstack-pregate sealed, gstack-plan-reality, gstack-artifact, gstack-autoplan reads)', kind: 'component' },
|
|
{ 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' },
|
|
{ id: 'wrapper', title: 'Capy skill copy (bootstrapper 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: [] };
|
|
const REVIEW_VOICE: SkillRequirement = { requires: ['helpers'], optional: [] };
|
|
|
|
/** SKILL_REQUIREMENTS rows that are install profiles, not skill directories (see the header). */
|
|
export const INSTALL_PROFILES: readonly string[] = ['review-voice'];
|
|
|
|
/** The read-only helpers the `helpers` row launches (`<bin> --help` loads each module graph with the launch Bun). */
|
|
export const REVIEW_VOICE_HELPERS: readonly string[] = ['gstack-pregate', 'gstack-plan-reality', 'gstack-artifact', 'gstack-autoplan'];
|
|
|
|
/** The checkout root the Capy skill bootstrapper resolves by default (GSTACK_CAPY_ROOT unset). */
|
|
export const CAPY_INSTALL_ROOT = '$HOME/.local/share/gstack';
|
|
export const GSTACK_REPO_URL = 'https://github.com/garrytan/gstack.git';
|
|
|
|
/**
|
|
* The install command for a machine without the Drive skill: clone into ROOT
|
|
* when no checkout is there, then run the checkout's own bootstrapper (the
|
|
* file the Drive skill holds) by absolute path with that root, so the floor
|
|
* check and the fast-forward still run and the caller's working directory,
|
|
* and so its project, is kept. No `; ` inside, so it can ride a fix clause.
|
|
*/
|
|
export function capyBootstrapCommand(skill: string, root: string = CAPY_INSTALL_ROOT): string {
|
|
return `test -f "${root}/setup" || git clone --quiet --single-branch --depth 1 ${GSTACK_REPO_URL} "${root}" && GSTACK_CAPY_ROOT="${root}" bash "${root}/capy/skill/gstack/scripts/install.sh"${skill ? ` --for ${skill}` : ''}`;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
'review-voice': REVIEW_VOICE,
|
|
};
|
|
|
|
/**
|
|
* 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_HELPERS="${REVIEW_VOICE_HELPERS.join(' ')}"`,
|
|
'',
|
|
'# 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_doctor_install_profile NAME — true for an install profile (helper runtime: no ./setup, no host profiles, project pins not applied).',
|
|
'gstack_doctor_install_profile() {',
|
|
' case "$1" in',
|
|
caseArm(INSTALL_PROFILES, 'return 0'),
|
|
' *) return 1 ;;',
|
|
' esac',
|
|
'}',
|
|
'',
|
|
'# gstack_capy_bootstrap_cmd ROOT [SKILL] — the install command for a machine without the Drive skill (capyBootstrapCommand).',
|
|
'gstack_capy_bootstrap_cmd() {',
|
|
` printf 'test -f "%s/setup" || git clone --quiet --single-branch --depth 1 %s "%s" && GSTACK_CAPY_ROOT="%s" bash "%s/capy/skill/gstack/scripts/install.sh"' "$1" '${GSTACK_REPO_URL}' "$1" "$1" "$1"`,
|
|
` [ -z "\${2:-}" ] || printf ' --for %s' "$2"`,
|
|
'}',
|
|
'',
|
|
'# 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');
|
|
}
|