diff --git a/AGENTS.md b/AGENTS.md index c6abb1fd5..0b6fc7e70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,6 +66,7 @@ Invoke them by name (e.g., `/office-hours`). | `/health` | Code quality dashboard (type checker, linter, tests, dead code). | | `/benchmark` | Performance regression detection (page load, Core Web Vitals). | | `/benchmark-models` | Cross-model benchmark for skills (Claude, GPT, Gemini side-by-side). | +| `/eval-plan` | Preregister a paid eval (bars, arms, decoys, stop rules), dry-run it for $0, pilot three priced items, then approve the spend. | | `/cso` | Supported security findings with explicit coverage. Static assessment remains available without catalog profiles; contained runtime/scanner execution requires matching qualified profiles, and no catalog has one yet (scanner build inputs are reviewed and await their qualification run; runtime qualification needs a private evaluator that is still being built), so `/cso` runs static assessment only. Runtime-tested bundles authenticate separate external assertions. Project-test completion remains `self_reported` because target code controls the test process; `tested` is reserved for a future target-independent completion witness. | | `/setup-gbrain` | Set up gbrain for cross-machine session memory sync. | | `/sync-gbrain` | Keep gbrain current with this repo's code; refresh agent search guidance in CLAUDE.md. | diff --git a/CHANGELOG.md b/CHANGELOG.md index baa2138eb..56640ee0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.91.79.0] - 2026-10-11 + +**`/eval-plan` preregisters paid evals for your product, coordination tools keep parallel lanes from colliding, and the autoplan owner sees a one-page brief instead of the full review (multi-agent wave PR E, plan items E1 tier 1, E2, E3).** + +- **`/eval-plan` (tier 1).** `gstack-eval-plan checklist` renders the preregistration checklist (bars, arms, decoys, stop rules, held-out exposure); `dry-run` runs the `$0` stub model over real input shapes and checks the price-table row, the output budget for reasoning models and the retry prompt's actual text; `pilot` runs n≈3 through `lib/spend-ledger.ts` and prints the extrapolation with an error band before the approval ask; `arms` resolves `gstack-models resolve --role eval-arms`. `PRICING` moves to `lib/pricing.ts` with its excluded billing categories documented; a model without a verified row fails closed with `EVAL_PRICE_MISSING`. Tier-3 steps are named in the skill as `tier 3, after P7`. +- **Coordination.** `gstack-lane-check` diffs in-flight branches and PRs against planned files and reports each intersection with the PR number and hunk count (`LANE_CONFLICT`). `gstack-evidence verify ` reruns probes and marks each claim `verified` or `unverified`. `{{COORDINATOR_CONTRACT}}` (merge conditions, behavior-change notices, spend caps, handoff format) renders into autoplan and the spec `--execute` spawn, and `gstack-autoplan contract` prints the identical bytes. `scripts/ubicloud/ubi-runner.sh` tags VMs by owner, adds `list --mine`, `usage`, `records` and own-VMs-only `gc`, and records a VM before creating it. `test_backend` (`local` | `ubicloud`) joins the config and the doctor `cores` row recommends `ubicloud` under 8 cores. Ship gains the Step 18-H no-PR handoff section; cross-thread messages carry full thread ids. +- **What the owner sees.** autoplan Phase 4 writes `review-record.md` and presents `gstack-owner-brief render`: the `URGENT, outside this plan` block first, a plain summary, then the numbered decisions seven per page with stable ids, `auto` items showing the default taken and `approval` items pending; `close --plan` rewrites every `draft direction stands until the owner decides` line to the decided option and date. `/review --contributor` (auto-detected via `gstack-contributor-mode detect`: `maintainers` in `.gstack/ship-policy.json`, else push permission, else off, source recorded) reviews outside PRs as evidence first with an accept/supersede/decline table and emits credit lines only when a diagnosis or test carried over. `gstack-banned-terms check` runs on every `/review` and `/document-release` diff from `.gstack/banned-terms.json`, naming the term, file and allowed locations. + ## [1.91.78.0] - 2026-10-11 **Plan reviews check reality before they check taste, `/review` and `/cso` look sideways from every finding, and `/investigate` ships recipes for flakes, polluters, races, eval bisects and backlog triage.** diff --git a/CLAUDE.md b/CLAUDE.md index 7c3eb56b0..a3ed96bc2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -146,7 +146,7 @@ a skill's own bash. **Size budgets:** generated SKILL.md files warn above 160KB (~40K tokens); `test/catalog-budget.test.ts` caps the always-loaded skill catalog at -`CATALOG_BUDGET_TOKEN_EQUIVALENTS` (1,194 today; each new skill ratchets it); and +`CATALOG_BUDGET_TOKEN_EQUIVALENTS` (1,197 today; each new skill ratchets it); and `test/context-budget-ratchet.test.ts` pins per-skill token ceilings against `test/fixtures/context-budget.json` (for legitimate growth or a landed reduction, re-run `bun test/helpers/capture-context-budget.ts` and commit the diff --git a/README.md b/README.md index 4d493ceb2..2ad75ca8f 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ When qualified CSO runtime images are published, setup gives each automatic prel Open Claude Code and paste this. Claude does the rest. -> Install gstack: run **`git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup`** then add a "gstack" section to CLAUDE.md that says to use the /browse skill from gstack for all web browsing, never use mcp\_\_claude-in-chrome\_\_\* tools, and lists the available skills: /office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review, /design-consultation, /design-shotgun, /design-html, /review, /deslop-shared-libs, /test-audit, /ship, /land-and-deploy, /canary, /benchmark, /browse, /connect-chrome, /qa, /qa-only, /design-review, /scrape, /setup-browser-cookies, /setup-deploy, /setup-gbrain, /retro, /investigate, /document-release, /document-generate, /codex, /cso, /autoplan, /plan-devex-review, /devex-review, /careful, /freeze, /guard, /unfreeze, /gstack-upgrade, /learn. Then ask the user if they also want to add gstack to the current project so teammates get it. +> Install gstack: run **`git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup`** then add a "gstack" section to CLAUDE.md that says to use the /browse skill from gstack for all web browsing, never use mcp\_\_claude-in-chrome\_\_\* tools, and lists the available skills: /office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review, /design-consultation, /design-shotgun, /design-html, /review, /deslop-shared-libs, /test-audit, /ship, /eval-plan, /land-and-deploy, /canary, /benchmark, /browse, /connect-chrome, /qa, /qa-only, /design-review, /scrape, /setup-browser-cookies, /setup-deploy, /setup-gbrain, /retro, /investigate, /document-release, /document-generate, /codex, /cso, /autoplan, /plan-devex-review, /devex-review, /careful, /freeze, /guard, /unfreeze, /gstack-upgrade, /learn. Then ask the user if they also want to add gstack to the current project so teammates get it. ### Step 2: Team mode — auto-update for shared repos (recommended) @@ -304,6 +304,7 @@ Each skill feeds into the next. `/office-hours` writes a design doc that `/plan- | `/land-and-deploy` | **Release Engineer** | Merge the PR, wait for CI and deploy, verify production health. One command from "approved" to "verified in production." | | `/canary` | **SRE** | Post-deploy monitoring loop. Watches for console errors, performance regressions, and page failures. | | `/benchmark` | **Performance Engineer** | Baseline page load times, Core Web Vitals, and resource sizes. Compare before/after on every PR. | +| `/eval-plan` | **Eval Planner** | Preregister a paid eval before the first priced call: bars, arms, decoys and stop rules, a $0 stub-model dry run over the real inputs, and a three-item priced pilot whose extrapolation and spend table gate the approval. | | `/document-release` | **Technical Writer** | Audit changed behavior against project docs on every ship, before final verification and publication. Also runs standalone. Shows updated, reviewed/current or blocked docs and any remaining gaps. | | `/document-generate` | **Documentation Author** | Generate missing docs from scratch using the Diataxis framework. Researches the codebase first, then writes reference / how-to / tutorial / explanation docs that actually match the code. Invokable standalone or chained from `/document-release` when the coverage map finds gaps. Learn more: [tutorial](docs/tutorial-document-generate.md) • [how-to](docs/howto-document-a-shipped-feature.md) • [why Diataxis](docs/explanation-diataxis-in-gstack.md). | | `/retro` | **Eng Manager** | Team-aware weekly retro. Per-person breakdowns, shipping streaks, test health trends, growth opportunities. `/retro global` runs across all your projects and AI tools (Claude Code, Codex, Gemini). | @@ -772,7 +773,7 @@ linked in, never deleted. ## gstack Use /browse from gstack for all web browsing. Never use mcp__claude-in-chrome__* tools. Available skills: /office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review, -/design-consultation, /design-shotgun, /design-html, /review, /deslop-shared-libs, /test-audit, /ship, /land-and-deploy, +/design-consultation, /design-shotgun, /design-html, /review, /deslop-shared-libs, /test-audit, /ship, /eval-plan, /land-and-deploy, /canary, /benchmark, /browse, /open-gstack-browser, /qa, /qa-only, /design-review, /scrape, /setup-browser-cookies, /setup-deploy, /setup-gbrain, /sync-gbrain, /retro, /investigate, /document-release, /document-generate, /codex, /cso, /autoplan, /pair-agent, /careful, /freeze, diff --git a/SKILL.md b/SKILL.md index 0ed1f7ed5..2508bc9a9 100644 --- a/SKILL.md +++ b/SKILL.md @@ -226,6 +226,7 @@ Generic “second opinion”, “outside review”, or “cross-model review” - User asks to share the browser with another agent, "pair OpenClaw/Codex with my browser" → invoke `/pair-agent` (fallback browser) - User asks to codify or save the last `/scrape` as a reusable skill → invoke `/skillify` (fallback browser) - User asks about page speed, web performance regression → invoke `/benchmark`; model or skill benchmarks → invoke `/benchmark-models` +- User asks to plan, preregister, price or pilot a paid eval → invoke `/eval-plan` - User asks what gstack has learned, "show learnings" → invoke `/learn` - User asks to tune question sensitivity, "stop asking me that" → invoke `/plan-tune` - User asks for code quality dashboard, "health check" → invoke `/health` diff --git a/SKILL.md.tmpl b/SKILL.md.tmpl index 62ece1c35..8065cd9c7 100644 --- a/SKILL.md.tmpl +++ b/SKILL.md.tmpl @@ -94,6 +94,7 @@ quality gates that produce better results than answering inline. - User asks to share the browser with another agent, "pair OpenClaw/Codex with my browser" → invoke `/pair-agent` (fallback browser) - User asks to codify or save the last `/scrape` as a reusable skill → invoke `/skillify` (fallback browser) - User asks about page speed, web performance regression → invoke `/benchmark`; model or skill benchmarks → invoke `/benchmark-models` +- User asks to plan, preregister, price or pilot a paid eval → invoke `/eval-plan` - User asks what gstack has learned, "show learnings" → invoke `/learn` - User asks to tune question sensitivity, "stop asking me that" → invoke `/plan-tune` - User asks for code quality dashboard, "health check" → invoke `/health` diff --git a/VERSION b/VERSION index 0bb1d7e3e..e3ebf8e19 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.78.0 +1.91.79.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 6c8ba724c..5c975937d 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.78.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.79.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/autoplan/SKILL.md b/autoplan/SKILL.md index 503fcee76..eaf9b03c2 100644 --- a/autoplan/SKILL.md +++ b/autoplan/SKILL.md @@ -452,7 +452,7 @@ DESIGN=$(~/.claude/skills/gstack/bin/gstack-design-doc-find "$SLUG" "$BRANCH") [ -n "$DESIGN" ] && echo "Design doc found: $DESIGN" || echo "No design doc found" ``` If a design doc exists, read it; its problem statement, constraints and chosen -approach feed the review pipeline. +approach feed the pipeline. ## Prerequisite Skill Offer @@ -515,7 +515,7 @@ If none was produced (user may have cancelled), proceed with standard review. # /autoplan — Auto-Review Pipeline Read every CEO, design, DX and eng section from disk at full interactive depth. -The 6 principles answer intermediate questions; taste goes to one final approval gate. +The 6 principles answer intermediate questions; taste goes to one final gate. --- @@ -540,9 +540,9 @@ sections. Read a section in full before doing its step; do not work from memory. 1. **Choose completeness** — Ship the whole thing; pick the approach covering more edge cases. 2. **Boil lakes** — Fix everything in the blast radius (files this plan modifies + direct importers). Auto-approve expansions in blast radius AND < 1 day CC effort (< 5 files, no new infra). -3. **Pragmatic** — Two options fix the same thing? Pick the cleaner one, in 5 seconds, not 5 minutes. +3. **Pragmatic** — Two options fix the same thing? Pick the cleaner one, in seconds. 4. **DRY** — Duplicates existing functionality? Reject; reuse what exists. -5. **Explicit over clever** — 10-line obvious fix > 200-line abstraction; pick what a new contributor reads in 30 seconds. +5. **Explicit over clever** — 10-line obvious fix > 200-line abstraction; pick what a new contributor reads at a glance. 6. **Bias toward action** — Merge > review cycles > stale deliberation. Flag concerns, don't block. **Conflict resolution (context-dependent tiebreakers):** @@ -556,10 +556,10 @@ sections. Read a section in full before doing its step; do not work from memory. Every auto-decision is classified: -**Mechanical** — one clearly right answer. Auto-decide silently. Examples: run the -outside reviewer when enabled (yes), run evals (yes), reduce scope on a complete plan (no). +**Mechanical** — one clearly right answer. Auto-decide silently (run the enabled +outside reviewer: yes; run evals: yes; reduce scope on a complete plan: no). -**Taste** — reasonable people could disagree. Auto-decide with recommendation; surface at the final gate. Sources: +**Taste** — reasonable people could disagree. Auto-decide with a recommendation; surface at the final gate. Sources: 1. **Close approaches** — top two are both viable with different tradeoffs. 2. **Borderline scope** — in blast radius but 3-5 files, or ambiguous radius. 3. **Codex disagreements** — the outside reviewer recommends differently and has a valid point. @@ -568,7 +568,7 @@ outside reviewer when enabled (yes), run evals (yes), reduce scope on a complete user's stated direction: merge, split, add or remove features/skills/workflows. NEVER auto-decide these. At the final approval gate give the original direction, proposed change, reasoning, blind spots and cost of being wrong (Phase 4 gate item); -flag agreed security/feasibility risks. The user's direction stands unless they approve the change. +flag agreed security/feasibility risks. The user's direction stands until they approve the change. --- @@ -591,20 +591,20 @@ Keep ONE phase active, completing these gates in order: 4. At the phase's exit, load its `phase-close` section afresh. Execute its numbered operations: prepare the current packet, Read it completely, reconcile it semantically, then SEND the parent completion message. Publication is its own - operation there; an earlier Read is not this close. + operation; an earlier Read is not this close. 5. Only after the message has been sent may the driver load/create/dispatch the next phase. Then continue to the next phase's tool calls in the same turn; after Eng, proceed to final synthesis/approval. Use the declared skip rule for an inapplicable phase; do not load its review or close steps. -Phase notifications, skips included, are progress updates: do not end the turn -or wait for a "continue" reply at these boundaries. +Phase notifications, skips included, are progress updates: never end the turn +or wait for a "continue" reply there. A missing gate keeps the current phase open, even if a reviewer finished. Read requests/self-reports and INPUT hashes do not prove uptake or review quality. Never draft future-phase reviews or outputs. Headings/promises are not completion. After compaction, reload current phase instructions/skill/sections, then reconcile saved artifacts and sent conversation messages separately. If closing, reload `phase-close` and resume its first incomplete numbered operation; -regenerate and reread the full packet if the implementation or accepted decisions changed: +regenerate and reread the full packet when the implementation or accepted decisions changed: - If a verified phase lacks its announcement, resume the close procedure at step 6 (Publish) before advancing. - If its reviewer is pending, wait for that same reviewer. - If native dispatch has not happened, finish any incomplete preliminary work before recovering a voice input. @@ -614,7 +614,7 @@ regenerate and reread the full packet if the implementation or accepted decision `nativePrompt` is the file's review body, not the Agent prompt. Resume at the first incomplete gate. Pending is not unavailable. Never skip native passes/required sections for time, -context pressure or your own review. Missing outside coverage does not block native +context pressure or your own review. Missing outside coverage never blocks native completion; report accurately. Never read raw agent transcripts. --- @@ -626,7 +626,7 @@ full interactive depth; answer each question using the 6 principles. **Default resolution: the recommended option.** Take `(recommended)` or the mode's context default. Use the 6 principles for missing recommendations/ties. On principle -disagreement, take the recommendation and surface the disagreement as Taste at the final gate. +disagreement, take the recommendation and surface it as Taste at the final gate. **Never auto-decide User Challenges:** both models agree to change the user's direction/settled decisions, or a premise is clearly wrong. Ask once at the Final @@ -638,7 +638,7 @@ record ALL accepted obligations below and run `amend-input` before continuing. Missing deliverables leave the review incomplete. No summary substitutes or one-line sections; under 3 sentences likely means -compression. "No issues found" needs 1-2 sentences on what was examined and why nothing was flagged. +compression. "No issues found" needs 1-2 sentences on what was examined and why. Explain inapplicability with evidence; skip only under Phase 0's list. Never abort or redirect to interactive review: the user chose /autoplan. @@ -649,8 +649,8 @@ or redirect to interactive review: the user chose /autoplan. ``` Phase: `ceo|design|dx|eng`. Record accepted requirements here; -no analysis/severity/verdict/consensus. No accepted requirements: `None: reason`. -On a rerun, carry forward unchanged accepted requirements; do not replace them with None. +no analysis/severity/verdict/consensus. None accepted: `None: reason`. +On a rerun, carry forward unchanged accepted requirements; never replace them with None. `amend` checks exact retention atomically; full readback; None unchanged. Baseline edits: `create`'s `baselineEdits`. Prior blocks immutable; state replacements in current block. Reconcile all decisions with readback. @@ -672,14 +672,14 @@ Prefix every Codex prompt: Absolute paths: SOURCE_PLAN (input), ACTIVE_PLAN (harness-assigned plan, else SOURCE_PLAN). Save plan amendments and review artifacts to ACTIVE_PLAN. -Send phase announcements and the final approval request in the conversation. +Send phase announcements and the final approval request (the brief) in the conversation. Resolve SNAPSHOT_TOOL once: ```bash bun -e 'console.log(require("fs").realpathSync(process.argv[1]))' "$HOME/.claude/skills/gstack/bin/gstack-autoplan-snapshot.ts" ``` -Fresh RESTORE_PATH, beside its phase artifacts in the project's git-excluded `.gstack/tmp/autoplan/`: +Fresh RESTORE_PATH, beside its phase artifacts in the git-excluded `.gstack/tmp/autoplan/`: ```bash SLUG=$(~/.claude/skills/gstack/bin/gstack-slug --get SLUG 2>/dev/null) _AP="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/autoplan"; mkdir -p "$_AP" && chmod 700 "$_AP" @@ -698,7 +698,7 @@ bun "" init "" "" "" Run init as its own Bash call with literal absolute paths: no variables, substitutions, chaining, pipes or redirects, which the guard cannot bind. Use returned paths/`scope`; never hand-wrap. init backs up SOURCE_PLAN exactly, -then initializes ACTIVE_PLAN atomically without losing requirements. +then initializes ACTIVE_PLAN atomically, keeping every requirement. Reviewers get only `## Implementation plan`; analysis, structured inputs included, stays in `## Review record`. On helper errors, stop; no stderr hiding/grep fallback. Re-run: copy RESTORE_PATH's bytes to SOURCE_PLAN, then /autoplan. @@ -708,7 +708,7 @@ Re-run: copy RESTORE_PATH's bytes to SOURCE_PLAN, then /autoplan. - Read CLAUDE.md, TODOS.md, git log -30, git diff against the base branch --stat - Discover design docs: `~/.claude/skills/gstack/bin/gstack-design-doc-find "$SLUG" "$BRANCH"` (prints the doc path, or nothing) - Detect UI scope: grep the plan for view/rendering terms (component, screen, form, - button, modal, layout, dashboard, sidebar, nav, dialog). Require 2+ matches. Exclude + button, modal, layout, dashboard, sidebar, nav, dialog). Require 2+ matches, excluding false positives ("page" alone, "UI" in acronyms). - Use init's full-input `scope`. For changed input or semantic enabling flags, rerun: ```bash @@ -718,8 +718,7 @@ bun "" scope "" terms. The threshold is 2+ term matches (occurrences, not distinct terms). Also enable DX when the product is a developer tool (developers install, integrate or build on it) or an AI agent is the primary user: add `--developer-tool` or `--agent-primary`. These flags only enable DX; no - context label negates a positive result. Skip DX only when the result is false and neither - trigger applies. + context label negates a positive result. Skip DX only when both are false. ### Step 3: Locate review skills; load each at phase entry @@ -735,7 +734,7 @@ directory, never cwd/runtime assets. Missing skill: report the phase and setup repair; never substitute a harness or claim completion. Read skills/sections only at their triggers, never prefetch future phases. Load -the tasks aggregator at Phase 4. Run all applicable skills and lazy sections fully. +the tasks aggregator at Phase 4. Run every applicable skill and lazy section fully. **Section skip list — when following a loaded skill file, SKIP these sections (they are already handled by /autoplan):** @@ -753,7 +752,7 @@ the tasks aggregator at Phase 4. Run all applicable skills and lazy sections ful - Outside Voice — Independent Plan Challenge - Design Outside Voices (independent) -Follow ONLY the review-specific methodology, sections, and required outputs. +Follow ONLY the review-specific methodology, sections and required outputs. Output: "Here's what I'm working with: [plan summary]. UI scope: [yes/no]. DX scope: [yes/no]. Review skills load at each phase entry; starting the full pipeline with auto-decisions." @@ -818,7 +817,7 @@ only CEO). Missing voices: N/A, never CONFIRMED. Skipped scope stays skipped. ## Phase 2: Design Review (conditional — skip if no UI scope) -**Skip condition:** If UI scope was NOT detected in Phase 0, skip this phase +**Skip condition:** No UI scope detected in Phase 0: skip this phase entirely — do NOT read its section. Send: "Phase 2 skipped — no UI scope detected." Record the skip in ACTIVE_PLAN; it is not a completed review. @@ -829,7 +828,7 @@ Record the skip in ACTIVE_PLAN; it is not a completed review. ## Phase 2.5: DX Review (conditional — skip if no developer-facing scope) -**Skip condition:** If DX scope was NOT detected in Phase 0, skip this phase +**Skip condition:** No DX scope detected in Phase 0: skip this phase entirely — do NOT read its section. Send: "Phase 2.5 skipped — no developer-facing scope detected." Record the skip in ACTIVE_PLAN; it is not a completed review. @@ -847,7 +846,7 @@ Record the skip in ACTIVE_PLAN; it is not a completed review. ## Decision Audit Trail -Immediately after each auto-decision, append one row to the plan file using Edit: +Right after each auto-decision, append one row to the plan file using Edit: ```markdown @@ -861,7 +860,7 @@ Immediately after each auto-decision, append one row to the plan file using Edit ## Pre-Gate Verification -Check the plan and conversation for every applicable deliverable: +Check the plan and conversation for each applicable deliverable: | Phase | Required outputs | |---|---| @@ -872,8 +871,8 @@ Check the plan and conversation for every applicable deliverable: Per phase, verify native and outside voice results or explicit unavailable/skipped status, cross-phase themes and one Decision Audit Trail row per auto-decision. -Produce missing outputs before the gate; after at most 2 repair attempts, warn at -the gate with each still-incomplete item. +Produce missing outputs before the gate; after at most 2 repair attempts, warn +there with each incomplete item. Reality rows: each phase's `PLAN_REALITY: … verdict=complete` from `~/.claude/skills/gstack/bin/gstack-plan-reality check --phase `. A missing applicable row is `incomplete` @@ -886,15 +885,15 @@ in an unattended run, never a warning; interactive runs follow the repair rule a > **STOP.** Before presenting the Final Approval Gate (Phase 4) — the aggregator computes $AGGREGATED_TASKS that the gate message substitutes, Read `~/.claude/skills/gstack/autoplan/sections/tasks-aggregator.md` and execute it > in full. Do not work from memory — that section is the source of truth for this step. -**STOP here and present the final state to the user.** +**STOP here and present the one-page brief to the user.** Write `$_AP/gate.json` (shape: `gstack-gate --help`): one item per User Challenge (`uc`, approval; `why` = you said / both recommend / why / blind spots / cost if wrong), per taste decision (`d`, auto; `why` = principle and the alternative's impact) and `plan` (approval: a) approve as-is (recommended) b) with overrides c) interrogate d) revise e) reject). Run `~/.claude/skills/gstack/bin/gstack-gate render $_AP/gate.json` -and append its `decisions` output to `$_AP/decisions.jsonl`. Present this message, -ending with the rendered list: +and append its `decisions` output to `$_AP/decisions.jsonl`. Write the full review +to `$_AP/review-record.md` and its Plan Summary alone to `$_AP/summary.md`: ``` ## /autoplan Review Complete @@ -907,7 +906,7 @@ findings outside this plan, or `None.`); also written above `## Implementation p [1-3 sentence summary] ### Decisions Made: [N] total ([M] auto-decided, [K] taste choices, [J] user challenges) -Auto-decided: see Decision Audit Trail in the plan file. The rest: the gate list below. +Auto-decided: see the plan file's Decision Audit Trail; the rest are the brief's decisions. ### Review Scores CEO, Design, DX and Eng: phase summary plus Codex, Claude @@ -915,7 +914,7 @@ and consensus status; say skipped where a phase did not run. Then the CYCLE line from `~/.claude/skills/gstack/bin/gstack-autoplan-timing summary --run --out "$_AP"`. ### Cross-Phase Themes -List concerns independently raised in 2+ phases. If none: "No cross-phase themes — each phase's concerns were distinct." +Concerns independently raised in 2+ phases, or "No cross-phase themes — each phase's concerns were distinct." ### Deferred to TODOS.md [Items auto-deferred with reasons] @@ -924,6 +923,10 @@ List concerns independently raised in 2+ phases. If none: "No cross-phase themes [Substitute $AGGREGATED_TASKS. If empty: "_No per-phase task lists found in $TASKS_DIR for branch $BRANCH._"] ``` +Present `~/.claude/skills/gstack/bin/gstack-owner-brief render --out "$_AP" --write` +verbatim (URGENT block, plain summary, numbered decisions seven per page with each +auto default, review-record path; `--page N` for later pages), never the full review. + Parse the reply: `~/.claude/skills/gstack/bin/gstack-gate parse $_AP/gate.json --reply "" --gate-rev `; unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `all` or `plana` without overrides = A; `d` overrides = B; `uc` overrides = B2; `planc`/`pland`/`plane` @@ -931,8 +934,8 @@ without overrides = A; `d` overrides = B; `uc` overrides = B2; `planc`/`pl with its `GSTACK_RESULT` line and never approve. **Option handling:** -- A: mark APPROVED, write review logs, suggest /ship -- B: ask which overrides, apply, then follow D's affected-phase rerun rule (including Eng last) before re-presenting the gate. Counts toward the same 3-cycle cap as D. +- A: mark APPROVED, run `~/.claude/skills/gstack/bin/gstack-owner-brief close --out "$_AP" --plan ` (draft-direction lines become the decided option and date), write review logs, suggest /ship +- B: ask which overrides, apply, then follow D's affected-phase rerun rule (Eng last) before re-presenting the gate; same 3-cycle cap as D. - B2: accept/reject User Challenges one at a time; rejected ones preserve the user's direction. Re-run Eng, then re-present the gate. - C: answer freeform, re-present the gate - D: make changes, re-run affected phases (scope→1, design→2, dx→2.5, test plan→3, arch→3; a re-run of any earlier phase re-runs Eng after it — the gate always reviews the final plan). Max 3 cycles. @@ -944,15 +947,15 @@ record verbatim into fenced history in Review record, keeping its original sourc Create a fresh amendment checkpoint. New baseline edits use `create`'s `baselineEdits.record` and `sourceSha256`; the review projection hash is not baseline identity. Carry forward unchanged accepted requirements. Never replay old replacements -or rewrite a historical source SHA. This starts a new phase invocation; compaction +or rewrite a historical source SHA. This is a new phase invocation; compaction resumes the existing one and its checkpoint. Eng still runs last. --- ## Completion: Write Review Logs -On approval, log each completed review for /ship's dashboard. Replace TIMESTAMP, -STATUS and N with actual values. STATUS is "clean" or "issues_open". +On approval, log each completed review for /ship's dashboard, substituting +TIMESTAMP, STATUS ("clean" or "issues_open") and N. ```bash COMMIT=$(git rev-parse --short HEAD 2>/dev/null) @@ -972,7 +975,7 @@ If Phase 2.5 ran (DX scope): ``` Dual voice logs: one record per PHASE (`ceo`, `design`, `dx`, `eng`) with that -phase's status/counts. Generate one AUTOPLAN_RUN_ID and share it with TIMESTAMP. +phase's status/counts, sharing one AUTOPLAN_RUN_ID and TIMESTAMP. ```bash ~/.claude/skills/gstack/bin/gstack-review-log '{"skill":"autoplan-voices","run_id":"AUTOPLAN_RUN_ID","timestamp":"'"$TIMESTAMP"'","status":"STATUS","source":"SOURCE","host":"claude","outside_provider":"codex","outside_status":"OUTSIDE_STATUS","phase":"PHASE","via":"autoplan","consensus_confirmed":N,"consensus_disagree":N,"commit":"'"$COMMIT"'"}' ``` @@ -985,8 +988,8 @@ skipped. Never carry success across phases/runs; preserve modelUsage. Retain the historical review-log skill ID; add `"host":"claude","outside_provider":"codex","outside_status":"completed|unavailable|disabled|skipped","phase":"autoplan"`. Record differing attempt outcomes separately. `source:"codex"` requires completed CLI output; native uses `source:"in-host"` (historical `source:"claude"`: native Claude). Availability/native fallback is not outside completion. Preserve all reported modelUsage; unknown model identity stays unknown. Under `GSTACK_CODEX_NO_SANDBOX=1` add `"codex_sandbox":"danger-full-access"`. Present a phase coverage table (CEO, design, DX, eng): host, outside provider/status, -native completion, findings, partial coverage. Replace N with actual counts. +native completion, findings, partial coverage, with actual counts. **Implementation model:** relay model/source from `"$HOME/.claude/skills/gstack/bin/gstack-models" resolve --role implementation --provider anthropic`. gstack cannot change this session. Recommend only; no spawn or config edits unless asked. On error, relay its repair, not a model. [Policy setup](https://github.com/garrytan/gstack/blob/main/docs/model-policy.md). -Suggest next step: `/ship` when ready to create the PR. +Suggest `/ship` when ready to create the PR. diff --git a/autoplan/SKILL.md.tmpl b/autoplan/SKILL.md.tmpl index 56ca5f494..bfefd212a 100644 --- a/autoplan/SKILL.md.tmpl +++ b/autoplan/SKILL.md.tmpl @@ -44,14 +44,14 @@ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-br {{DESIGN_DOC_DISCOVERY}} ``` If a design doc exists, read it; its problem statement, constraints and chosen -approach feed the review pipeline. +approach feed the pipeline. {{BENEFITS_FROM}} # /autoplan — Auto-Review Pipeline Read every CEO, design, DX and eng section from disk at full interactive depth. -The 6 principles answer intermediate questions; taste goes to one final approval gate. +The 6 principles answer intermediate questions; taste goes to one final gate. --- @@ -63,9 +63,9 @@ The 6 principles answer intermediate questions; taste goes to one final approval 1. **Choose completeness** — Ship the whole thing; pick the approach covering more edge cases. 2. **Boil lakes** — Fix everything in the blast radius (files this plan modifies + direct importers). Auto-approve expansions in blast radius AND < 1 day CC effort (< 5 files, no new infra). -3. **Pragmatic** — Two options fix the same thing? Pick the cleaner one, in 5 seconds, not 5 minutes. +3. **Pragmatic** — Two options fix the same thing? Pick the cleaner one, in seconds. 4. **DRY** — Duplicates existing functionality? Reject; reuse what exists. -5. **Explicit over clever** — 10-line obvious fix > 200-line abstraction; pick what a new contributor reads in 30 seconds. +5. **Explicit over clever** — 10-line obvious fix > 200-line abstraction; pick what a new contributor reads at a glance. 6. **Bias toward action** — Merge > review cycles > stale deliberation. Flag concerns, don't block. **Conflict resolution (context-dependent tiebreakers):** @@ -79,10 +79,10 @@ The 6 principles answer intermediate questions; taste goes to one final approval Every auto-decision is classified: -**Mechanical** — one clearly right answer. Auto-decide silently. Examples: run the -outside reviewer when enabled (yes), run evals (yes), reduce scope on a complete plan (no). +**Mechanical** — one clearly right answer. Auto-decide silently (run the enabled +outside reviewer: yes; run evals: yes; reduce scope on a complete plan: no). -**Taste** — reasonable people could disagree. Auto-decide with recommendation; surface at the final gate. Sources: +**Taste** — reasonable people could disagree. Auto-decide with a recommendation; surface at the final gate. Sources: 1. **Close approaches** — top two are both viable with different tradeoffs. 2. **Borderline scope** — in blast radius but 3-5 files, or ambiguous radius. 3. **{{OUTSIDE_LABEL}} disagreements** — the outside reviewer recommends differently and has a valid point. @@ -91,7 +91,7 @@ outside reviewer when enabled (yes), run evals (yes), reduce scope on a complete user's stated direction: merge, split, add or remove features/skills/workflows. NEVER auto-decide these. At the final approval gate give the original direction, proposed change, reasoning, blind spots and cost of being wrong (Phase 4 gate item); -flag agreed security/feasibility risks. The user's direction stands unless they approve the change. +flag agreed security/feasibility risks. The user's direction stands until they approve the change. --- @@ -114,20 +114,20 @@ Keep ONE phase active, completing these gates in order: 4. At the phase's exit, load its `phase-close` section afresh. Execute its numbered operations: prepare the current packet, Read it completely, reconcile it semantically, then SEND the parent completion message. Publication is its own - operation there; an earlier Read is not this close. + operation; an earlier Read is not this close. 5. Only after the message has been sent may the driver load/create/dispatch the next phase. Then continue to the next phase's tool calls in the same turn; after Eng, proceed to final synthesis/approval. Use the declared skip rule for an inapplicable phase; do not load its review or close steps. -Phase notifications, skips included, are progress updates: do not end the turn -or wait for a "continue" reply at these boundaries. +Phase notifications, skips included, are progress updates: never end the turn +or wait for a "continue" reply there. A missing gate keeps the current phase open, even if a reviewer finished. Read requests/self-reports and INPUT hashes do not prove uptake or review quality. Never draft future-phase reviews or outputs. Headings/promises are not completion. After compaction, reload current phase instructions/skill/sections, then reconcile saved artifacts and sent conversation messages separately. If closing, reload `phase-close` and resume its first incomplete numbered operation; -regenerate and reread the full packet if the implementation or accepted decisions changed: +regenerate and reread the full packet when the implementation or accepted decisions changed: - If a verified phase lacks its announcement, resume the close procedure at step 6 (Publish) before advancing. - If its reviewer is pending, wait for that same reviewer. - If native dispatch has not happened, finish any incomplete preliminary work before recovering a voice input. @@ -137,7 +137,7 @@ regenerate and reread the full packet if the implementation or accepted decision `nativePrompt` is the file's review body, not the Agent prompt. Resume at the first incomplete gate. Pending is not unavailable. Never skip native passes/required sections for time, -context pressure or your own review. Missing outside coverage does not block native +context pressure or your own review. Missing outside coverage never blocks native completion; report accurately. Never read raw agent transcripts. --- @@ -149,7 +149,7 @@ full interactive depth; answer each question using the 6 principles. **Default resolution: the recommended option.** Take `(recommended)` or the mode's context default. Use the 6 principles for missing recommendations/ties. On principle -disagreement, take the recommendation and surface the disagreement as Taste at the final gate. +disagreement, take the recommendation and surface it as Taste at the final gate. **Never auto-decide User Challenges:** both models agree to change the user's direction/settled decisions, or a premise is clearly wrong. Ask once at the Final @@ -161,7 +161,7 @@ record ALL accepted obligations below and run `amend-input` before continuing. Missing deliverables leave the review incomplete. No summary substitutes or one-line sections; under 3 sentences likely means -compression. "No issues found" needs 1-2 sentences on what was examined and why nothing was flagged. +compression. "No issues found" needs 1-2 sentences on what was examined and why. Explain inapplicability with evidence; skip only under Phase 0's list. Never abort or redirect to interactive review: the user chose /autoplan. @@ -172,8 +172,8 @@ or redirect to interactive review: the user chose /autoplan. ``` Phase: `ceo|design|dx|eng`. Record accepted requirements here; -no analysis/severity/verdict/consensus. No accepted requirements: `None: reason`. -On a rerun, carry forward unchanged accepted requirements; do not replace them with None. +no analysis/severity/verdict/consensus. None accepted: `None: reason`. +On a rerun, carry forward unchanged accepted requirements; never replace them with None. `amend` checks exact retention atomically; full readback; None unchanged. Baseline edits: `create`'s `baselineEdits`. Prior blocks immutable; state replacements in current block. Reconcile all decisions with readback. @@ -195,11 +195,11 @@ Prefix every {{OUTSIDE_LABEL}} prompt: Absolute paths: SOURCE_PLAN (input), ACTIVE_PLAN (harness-assigned plan, else SOURCE_PLAN). Save plan amendments and review artifacts to ACTIVE_PLAN. -Send phase announcements and the final approval request in the conversation. +Send phase announcements and the final approval request (the brief) in the conversation. Resolve SNAPSHOT_TOOL once: {{AUTOPLAN_SNAPSHOT_TOOL}} -Fresh RESTORE_PATH, beside its phase artifacts in the project's git-excluded `.gstack/tmp/autoplan/`: +Fresh RESTORE_PATH, beside its phase artifacts in the git-excluded `.gstack/tmp/autoplan/`: ```bash {{SLUG_EVAL}} _AP="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/tmp/autoplan"; mkdir -p "$_AP" && chmod 700 "$_AP" @@ -218,7 +218,7 @@ bun "" init "" "" "" Run init as its own Bash call with literal absolute paths: no variables, substitutions, chaining, pipes or redirects, which the guard cannot bind. Use returned paths/`scope`; never hand-wrap. init backs up SOURCE_PLAN exactly, -then initializes ACTIVE_PLAN atomically without losing requirements. +then initializes ACTIVE_PLAN atomically, keeping every requirement. Reviewers get only `## Implementation plan`; analysis, structured inputs included, stays in `## Review record`. On helper errors, stop; no stderr hiding/grep fallback. Re-run: copy RESTORE_PATH's bytes to SOURCE_PLAN, then /autoplan. @@ -228,7 +228,7 @@ Re-run: copy RESTORE_PATH's bytes to SOURCE_PLAN, then /autoplan. - Read CLAUDE.md, TODOS.md, git log -30, git diff against the base branch --stat - Discover design docs: `~/.claude/skills/gstack/bin/gstack-design-doc-find "$SLUG" "$BRANCH"` (prints the doc path, or nothing) - Detect UI scope: grep the plan for view/rendering terms (component, screen, form, - button, modal, layout, dashboard, sidebar, nav, dialog). Require 2+ matches. Exclude + button, modal, layout, dashboard, sidebar, nav, dialog). Require 2+ matches, excluding false positives ("page" alone, "UI" in acronyms). - Use init's full-input `scope`. For changed input or semantic enabling flags, rerun: ```bash @@ -238,8 +238,7 @@ bun "" scope "" terms. The threshold is 2+ term matches (occurrences, not distinct terms). Also enable DX when the product is a developer tool (developers install, integrate or build on it) or an AI agent is the primary user: add `--developer-tool` or `--agent-primary`. These flags only enable DX; no - context label negates a positive result. Skip DX only when the result is false and neither - trigger applies. + context label negates a positive result. Skip DX only when both are false. ### Step 3: Locate review skills; load each at phase entry @@ -255,7 +254,7 @@ directory, never cwd/runtime assets. Missing skill: report the phase and setup repair; never substitute a harness or claim completion. Read skills/sections only at their triggers, never prefetch future phases. Load -the tasks aggregator at Phase 4. Run all applicable skills and lazy sections fully. +the tasks aggregator at Phase 4. Run every applicable skill and lazy section fully. **Section skip list — when following a loaded skill file, SKIP these sections (they are already handled by /autoplan):** @@ -273,7 +272,7 @@ the tasks aggregator at Phase 4. Run all applicable skills and lazy sections ful - Outside Voice — Independent Plan Challenge - Design Outside Voices (independent) -Follow ONLY the review-specific methodology, sections, and required outputs. +Follow ONLY the review-specific methodology, sections and required outputs. Output: "Here's what I'm working with: [plan summary]. UI scope: [yes/no]. DX scope: [yes/no]. Review skills load at each phase entry; starting the full pipeline with auto-decisions." @@ -297,7 +296,7 @@ only CEO). Missing voices: N/A, never CONFIRMED. Skipped scope stays skipped. ## Phase 2: Design Review (conditional — skip if no UI scope) -**Skip condition:** If UI scope was NOT detected in Phase 0, skip this phase +**Skip condition:** No UI scope detected in Phase 0: skip this phase entirely — do NOT read its section. Send: "Phase 2 skipped — no UI scope detected." Record the skip in ACTIVE_PLAN; it is not a completed review. @@ -307,7 +306,7 @@ Record the skip in ACTIVE_PLAN; it is not a completed review. ## Phase 2.5: DX Review (conditional — skip if no developer-facing scope) -**Skip condition:** If DX scope was NOT detected in Phase 0, skip this phase +**Skip condition:** No DX scope detected in Phase 0: skip this phase entirely — do NOT read its section. Send: "Phase 2.5 skipped — no developer-facing scope detected." Record the skip in ACTIVE_PLAN; it is not a completed review. @@ -323,7 +322,7 @@ Record the skip in ACTIVE_PLAN; it is not a completed review. ## Decision Audit Trail -Immediately after each auto-decision, append one row to the plan file using Edit: +Right after each auto-decision, append one row to the plan file using Edit: ```markdown @@ -337,7 +336,7 @@ Immediately after each auto-decision, append one row to the plan file using Edit ## Pre-Gate Verification -Check the plan and conversation for every applicable deliverable: +Check the plan and conversation for each applicable deliverable: | Phase | Required outputs | |---|---| @@ -348,8 +347,8 @@ Check the plan and conversation for every applicable deliverable: Per phase, verify native and outside voice results or explicit unavailable/skipped status, cross-phase themes and one Decision Audit Trail row per auto-decision. -Produce missing outputs before the gate; after at most 2 repair attempts, warn at -the gate with each still-incomplete item. +Produce missing outputs before the gate; after at most 2 repair attempts, warn +there with each incomplete item. {{PLAN_REALITY_ROWS:pregate}} @@ -359,15 +358,15 @@ the gate with each still-incomplete item. {{SECTION:tasks-aggregator}} -**STOP here and present the final state to the user.** +**STOP here and present the one-page brief to the user.** Write `$_AP/gate.json` (shape: `gstack-gate --help`): one item per User Challenge (`uc`, approval; `why` = you said / both recommend / why / blind spots / cost if wrong), per taste decision (`d`, auto; `why` = principle and the alternative's impact) and `plan` (approval: a) approve as-is (recommended) b) with overrides c) interrogate d) revise e) reject). Run `~/.claude/skills/gstack/bin/gstack-gate render $_AP/gate.json` -and append its `decisions` output to `$_AP/decisions.jsonl`. Present this message, -ending with the rendered list: +and append its `decisions` output to `$_AP/decisions.jsonl`. Write the full review +to `$_AP/review-record.md` and its Plan Summary alone to `$_AP/summary.md`: ``` ## /autoplan Review Complete @@ -380,7 +379,7 @@ findings outside this plan, or `None.`); also written above `## Implementation p [1-3 sentence summary] ### Decisions Made: [N] total ([M] auto-decided, [K] taste choices, [J] user challenges) -Auto-decided: see Decision Audit Trail in the plan file. The rest: the gate list below. +Auto-decided: see the plan file's Decision Audit Trail; the rest are the brief's decisions. ### Review Scores CEO, Design, DX and Eng: phase summary plus {{OUTSIDE_LABEL}}, {{NATIVE_LABEL}} @@ -388,7 +387,7 @@ and consensus status; say skipped where a phase did not run. Then the CYCLE line from `~/.claude/skills/gstack/bin/gstack-autoplan-timing summary --run --out "$_AP"`. ### Cross-Phase Themes -List concerns independently raised in 2+ phases. If none: "No cross-phase themes — each phase's concerns were distinct." +Concerns independently raised in 2+ phases, or "No cross-phase themes — each phase's concerns were distinct." ### Deferred to TODOS.md [Items auto-deferred with reasons] @@ -397,6 +396,10 @@ List concerns independently raised in 2+ phases. If none: "No cross-phase themes [Substitute $AGGREGATED_TASKS. If empty: "_No per-phase task lists found in $TASKS_DIR for branch $BRANCH._"] ``` +Present `~/.claude/skills/gstack/bin/gstack-owner-brief render --out "$_AP" --write` +verbatim (URGENT block, plain summary, numbered decisions seven per page with each +auto default, review-record path; `--page N` for later pages), never the full review. + Parse the reply: `~/.claude/skills/gstack/bin/gstack-gate parse $_AP/gate.json --reply "" --gate-rev `; unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `all` or `plana` without overrides = A; `d` overrides = B; `uc` overrides = B2; `planc`/`pland`/`plane` @@ -404,8 +407,8 @@ without overrides = A; `d` overrides = B; `uc` overrides = B2; `planc`/`pl with its `GSTACK_RESULT` line and never approve. **Option handling:** -- A: mark APPROVED, write review logs, suggest /ship -- B: ask which overrides, apply, then follow D's affected-phase rerun rule (including Eng last) before re-presenting the gate. Counts toward the same 3-cycle cap as D. +- A: mark APPROVED, run `~/.claude/skills/gstack/bin/gstack-owner-brief close --out "$_AP" --plan ` (draft-direction lines become the decided option and date), write review logs, suggest /ship +- B: ask which overrides, apply, then follow D's affected-phase rerun rule (Eng last) before re-presenting the gate; same 3-cycle cap as D. - B2: accept/reject User Challenges one at a time; rejected ones preserve the user's direction. Re-run Eng, then re-present the gate. - C: answer freeform, re-present the gate - D: make changes, re-run affected phases (scope→1, design→2, dx→2.5, test plan→3, arch→3; a re-run of any earlier phase re-runs Eng after it — the gate always reviews the final plan). Max 3 cycles. @@ -417,15 +420,15 @@ record verbatim into fenced history in Review record, keeping its original sourc Create a fresh amendment checkpoint. New baseline edits use `create`'s `baselineEdits.record` and `sourceSha256`; the review projection hash is not baseline identity. Carry forward unchanged accepted requirements. Never replay old replacements -or rewrite a historical source SHA. This starts a new phase invocation; compaction +or rewrite a historical source SHA. This is a new phase invocation; compaction resumes the existing one and its checkpoint. Eng still runs last. --- ## Completion: Write Review Logs -On approval, log each completed review for /ship's dashboard. Replace TIMESTAMP, -STATUS and N with actual values. STATUS is "clean" or "issues_open". +On approval, log each completed review for /ship's dashboard, substituting +TIMESTAMP, STATUS ("clean" or "issues_open") and N. ```bash COMMIT=$(git rev-parse --short HEAD 2>/dev/null) @@ -445,7 +448,7 @@ If Phase 2.5 ran (DX scope): ``` Dual voice logs: one record per PHASE (`ceo`, `design`, `dx`, `eng`) with that -phase's status/counts. Generate one AUTOPLAN_RUN_ID and share it with TIMESTAMP. +phase's status/counts, sharing one AUTOPLAN_RUN_ID and TIMESTAMP. ```bash ~/.claude/skills/gstack/bin/gstack-review-log '{"skill":"autoplan-voices","run_id":"AUTOPLAN_RUN_ID","timestamp":"'"$TIMESTAMP"'","status":"STATUS","source":"SOURCE","host":"{{HOST_ID}}","outside_provider":"{{OUTSIDE_PROVIDER}}","outside_status":"OUTSIDE_STATUS","phase":"PHASE","via":"autoplan","consensus_confirmed":N,"consensus_disagree":N,"commit":"'"$COMMIT"'"}' ``` @@ -458,8 +461,8 @@ skipped. Never carry success across phases/runs; preserve modelUsage. {{OUTSIDE_PROVENANCE:autoplan}} Present a phase coverage table (CEO, design, DX, eng): host, outside provider/status, -native completion, findings, partial coverage. Replace N with actual counts. +native completion, findings, partial coverage, with actual counts. {{IMPLEMENTATION_MODEL_HANDOFF}} -Suggest next step: `/ship` when ready to create the PR. +Suggest `/ship` when ready to create the PR. diff --git a/autoplan/sections/ceo-phase.md b/autoplan/sections/ceo-phase.md index 70a76bd0a..e517cb61e 100644 --- a/autoplan/sections/ceo-phase.md +++ b/autoplan/sections/ceo-phase.md @@ -4,7 +4,7 @@ Before dispatch, Read `methodologyPath` from `bun "" methodology Execute in this order: Step 0 (including its completed Spec Review Loop) → Claude CEO voice → Codex CEO voice → consensus → Review Sections → saved summary → phase -announcement. Dispatching a reviewer does not complete its step. +announcement. Dispatching a reviewer never completes its step. **Override rules:** - Mode selection: SELECTIVE EXPANSION @@ -50,11 +50,11 @@ Supply the complete CEO scope summary and `` to the loaded Spec Review Loop. The checkpoint is immutable prior state; never supply it as the current working plan. A failed preparation is an input failure, not a completed spec review. Keep the loop's existing stop conditions and three-launch cap. After the loop, -create a fresh snapshot below for both voices; it does not replace the amendment checkpoint. +create a fresh snapshot below for both voices; the amendment checkpoint stays. Step 0.5 (Dual Voices): After Step 0's Spec Review Loop, consume the native CEO review, then the available outside voice (P6). Present both completed results -before consensus; always run the native pass. +before consensus; the native pass always runs. **Bind phase input:** Run; use `snapshotPath` as `` for both voices: ```bash @@ -137,19 +137,19 @@ echo 'OUTSIDE_STATUS: completed provider=codex host=claude' Use Bash `timeout: 600000`; show the full response in a `tool-output` fence. Require successful execution and valid markers. Refusal, empty/malformed output, missing score/severity/completion markers, timeout or CLI failure means `outside_status: unavailable`. P0/P1 findings block like native ones; `OUTSIDE_STATUS: unverified` is missing coverage. Use the caller's fallback; missing coverage is never clean/PASS. After either outcome, delete only your private prompt; scratch cleanup is automatic. -Failed/incomplete outside review → unavailable; disabled → skip outside. Both retain the native pass. +Failed/incomplete outside review → unavailable; disabled → skip outside. Both keep the native pass. Retain the historical review-log skill ID; add `"host":"claude","outside_provider":"codex","outside_status":"completed|unavailable|disabled|skipped","phase":"ceo"`. Record differing attempt outcomes separately. `source:"codex"` requires completed CLI output; native uses `source:"in-host"` (historical `source:"claude"`: native Claude). Availability/native fallback is not outside completion. Preserve all reported modelUsage; unknown model identity stays unknown. Under `GSTACK_CODEX_NO_SANDBOX=1` add `"codex_sandbox":"danger-full-access"`. - **Error handling:** Codex auth/timeout/empty → proceed with - Claude subagent only, tagged `[single-model]`. If Claude subagent also fails → + **Error handling:** Codex auth/timeout/empty → proceed with the + Claude subagent only, tagged `[single-model]`. If it also fails → "Outside voices unavailable — continuing with primary review." **Degradation matrix:** Both fail → "single-reviewer mode". Codex only → tag `[codex-only]`. Subagent only → tag `[subagent-only]`. -- Strategy choices: if the outside reviewer disagrees with a premise or scope decision with valid - strategic reason → TASTE DECISION. If both models agree the user's stated structure +- Strategy choices: the outside reviewer disagrees with a premise or scope decision for a valid + strategic reason → TASTE DECISION. Both models agree the user's stated structure should change (merge, split, add, remove) → USER CHALLENGE (never auto-decided). Produce the CEO consensus table from the completed results: @@ -171,7 +171,7 @@ Native findings stay separate; disagreements → taste; flag single-voice critic Sections 1-11 — for EACH section, run the evaluation criteria from the loaded skill file: - Sections WITH findings: full analysis, auto-decide each issue, log to audit trail - Sections with NO findings: 1-2 sentences stating what was examined and why nothing - was flagged. NEVER compress a section to just its name in a table row. + was flagged. NEVER compress a section to its name in a table row. - Section 11 (Design): run only if UI scope was detected in Phase 0 **Mandatory outputs from Phase 1:** @@ -182,8 +182,8 @@ Sections 1-11 — for EACH section, run the evaluation criteria from the loaded - "What already exists" section mapping sub-problems to existing code - Error & Rescue Registry table (from Section 2) - Failure Modes Registry table (from review sections) -- Dream state delta (where this plan leaves us vs 12-month ideal) -- Completion Summary (the full summary table from the CEO skill) +- Dream state delta (where this plan leaves us vs the 12-month ideal) +- Completion Summary (the CEO skill's full summary table) **Close this phase:** diff --git a/autoplan/sections/ceo-phase.md.tmpl b/autoplan/sections/ceo-phase.md.tmpl index 168ec0681..a4e0e1311 100644 --- a/autoplan/sections/ceo-phase.md.tmpl +++ b/autoplan/sections/ceo-phase.md.tmpl @@ -2,7 +2,7 @@ Before dispatch, Read {{AUTOPLAN_REVIEW_FILE:plan-ceo-review:with-sections}} per Execute in this order: Step 0 (including its completed Spec Review Loop) → {{NATIVE_LABEL}} CEO voice → {{OUTSIDE_LABEL}} CEO voice → consensus → Review Sections → saved summary → phase -announcement. Dispatching a reviewer does not complete its step. +announcement. Dispatching a reviewer never completes its step. **Override rules:** - Mode selection: SELECTIVE EXPANSION @@ -48,11 +48,11 @@ Supply the complete CEO scope summary and `` to the loaded Spec Review Loop. The checkpoint is immutable prior state; never supply it as the current working plan. A failed preparation is an input failure, not a completed spec review. Keep the loop's existing stop conditions and three-launch cap. After the loop, -create a fresh snapshot below for both voices; it does not replace the amendment checkpoint. +create a fresh snapshot below for both voices; the amendment checkpoint stays. Step 0.5 (Dual Voices): After Step 0's Spec Review Loop, consume the native CEO review, then the available outside voice (P6). Present both completed results -before consensus; always run the native pass. +before consensus; the native pass always runs. **Bind phase input:** Run; use `snapshotPath` as `` for both voices: ```bash @@ -92,19 +92,19 @@ IMPORTANT: Do NOT read or execute any SKILL.md files or paths containing skills/ {{OUTSIDE_INVOCATION:autoplan}} -Failed/incomplete outside review → unavailable; disabled → skip outside. Both retain the native pass. +Failed/incomplete outside review → unavailable; disabled → skip outside. Both keep the native pass. {{OUTSIDE_PROVENANCE:ceo}} - **Error handling:** {{OUTSIDE_LABEL}} auth/timeout/empty → proceed with - {{NATIVE_LABEL}} subagent only, tagged `[single-model]`. If {{NATIVE_LABEL}} subagent also fails → + **Error handling:** {{OUTSIDE_LABEL}} auth/timeout/empty → proceed with the + {{NATIVE_LABEL}} subagent only, tagged `[single-model]`. If it also fails → "Outside voices unavailable — continuing with primary review." **Degradation matrix:** Both fail → "single-reviewer mode". {{OUTSIDE_LABEL}} only → tag `[{{OUTSIDE_PROVIDER}}-only]`. Subagent only → tag `[subagent-only]`. -- Strategy choices: if the outside reviewer disagrees with a premise or scope decision with valid - strategic reason → TASTE DECISION. If both models agree the user's stated structure +- Strategy choices: the outside reviewer disagrees with a premise or scope decision for a valid + strategic reason → TASTE DECISION. Both models agree the user's stated structure should change (merge, split, add, remove) → USER CHALLENGE (never auto-decided). Produce the CEO consensus table from the completed results: @@ -126,7 +126,7 @@ Native findings stay separate; disagreements → taste; flag single-voice critic Sections 1-11 — for EACH section, run the evaluation criteria from the loaded skill file: - Sections WITH findings: full analysis, auto-decide each issue, log to audit trail - Sections with NO findings: 1-2 sentences stating what was examined and why nothing - was flagged. NEVER compress a section to just its name in a table row. + was flagged. NEVER compress a section to its name in a table row. - Section 11 (Design): run only if UI scope was detected in Phase 0 **Mandatory outputs from Phase 1:** @@ -135,8 +135,8 @@ Sections 1-11 — for EACH section, run the evaluation criteria from the loaded - "What already exists" section mapping sub-problems to existing code - Error & Rescue Registry table (from Section 2) - Failure Modes Registry table (from review sections) -- Dream state delta (where this plan leaves us vs 12-month ideal) -- Completion Summary (the full summary table from the CEO skill) +- Dream state delta (where this plan leaves us vs the 12-month ideal) +- Completion Summary (the CEO skill's full summary table) **Close this phase:** diff --git a/autoplan/sections/design-phase.md b/autoplan/sections/design-phase.md index f8e043365..403669ed0 100644 --- a/autoplan/sections/design-phase.md +++ b/autoplan/sections/design-phase.md @@ -96,7 +96,7 @@ echo 'OUTSIDE_STATUS: completed provider=codex host=claude' Use Bash `timeout: 600000`; show the full response in a `tool-output` fence. Require successful execution and valid markers. Refusal, empty/malformed output, missing score/severity/completion markers, timeout or CLI failure means `outside_status: unavailable`. P0/P1 findings block like native ones; `OUTSIDE_STATUS: unverified` is missing coverage. Use the caller's fallback; missing coverage is never clean/PASS. After either outcome, delete only your private prompt; scratch cleanup is automatic. -Failed/incomplete outside review → unavailable; disabled → skip outside. Both retain the native pass. +Failed/incomplete outside review → unavailable; disabled → skip outside. Both keep the native pass. Retain the historical review-log skill ID; add `"host":"claude","outside_provider":"codex","outside_status":"completed|unavailable|disabled|skipped","phase":"design"`. Record differing attempt outcomes separately. `source:"codex"` requires completed CLI output; native uses `source:"in-host"` (historical `source:"claude"`: native Claude). Availability/native fallback is not outside completion. Preserve all reported modelUsage; unknown model identity stays unknown. Under `GSTACK_CODEX_NO_SANDBOX=1` add `"codex_sandbox":"danger-full-access"`. diff --git a/autoplan/sections/design-phase.md.tmpl b/autoplan/sections/design-phase.md.tmpl index 6f77e0c92..d39e93e31 100644 --- a/autoplan/sections/design-phase.md.tmpl +++ b/autoplan/sections/design-phase.md.tmpl @@ -51,7 +51,7 @@ IMPORTANT: Do NOT read or execute any SKILL.md files or paths containing skills/ {{OUTSIDE_INVOCATION:autoplan}} -Failed/incomplete outside review → unavailable; disabled → skip outside. Both retain the native pass. +Failed/incomplete outside review → unavailable; disabled → skip outside. Both keep the native pass. {{OUTSIDE_PROVENANCE:design}} diff --git a/autoplan/sections/dx-phase.md b/autoplan/sections/dx-phase.md index 73b0e2510..b5c64b78c 100644 --- a/autoplan/sections/dx-phase.md +++ b/autoplan/sections/dx-phase.md @@ -5,12 +5,12 @@ Before dispatch, Read `methodologyPath` from `bun "" methodology **Override rules:** - Mode selection: DX POLISH - Persona: infer from README/docs, pick the most common developer type (P6) -- Competitive benchmark: research through Aside per the loaded skill's "Web research runs in Aside" section (WebSearch when Aside is not ready); use the reference benchmarks when neither is available (P1) +- Competitive benchmark: research through Aside per the loaded skill's "Web research runs in Aside" section (WebSearch when Aside is not ready); reference benchmarks when neither is available (P1) - Magical moment: pick the lowest-effort delivery vehicle that achieves the competitive tier (P5) -- Getting started friction: always optimize toward fewer steps (P5, simpler over clever) +- Getting started friction: always optimize toward fewer steps (P5) - Error message quality: always require problem + cause + fix (P1, completeness) - API/CLI naming: consistency wins over cleverness (P5) -- DX taste decisions (e.g., opinionated defaults vs flexibility): mark TASTE DECISION +- DX taste decisions (opinionated defaults vs flexibility): mark TASTE DECISION - Dual voices: always run BOTH Claude subagent AND Codex if available (P6). **Bind phase input:** Run; use `snapshotPath` as `` for both voices: @@ -100,7 +100,7 @@ echo 'OUTSIDE_STATUS: completed provider=codex host=claude' Use Bash `timeout: 600000`; show the full response in a `tool-output` fence. Require successful execution and valid markers. Refusal, empty/malformed output, missing score/severity/completion markers, timeout or CLI failure means `outside_status: unavailable`. P0/P1 findings block like native ones; `OUTSIDE_STATUS: unverified` is missing coverage. Use the caller's fallback; missing coverage is never clean/PASS. After either outcome, delete only your private prompt; scratch cleanup is automatic. -Failed/incomplete outside review → unavailable; disabled → skip outside. Both retain the native pass. +Failed/incomplete outside review → unavailable; disabled → skip outside. Both keep the native pass. Retain the historical review-log skill ID; add `"host":"claude","outside_provider":"codex","outside_status":"completed|unavailable|disabled|skipped","phase":"dx"`. Record differing attempt outcomes separately. `source:"codex"` requires completed CLI output; native uses `source:"in-host"` (historical `source:"claude"`: native Claude). Availability/native fallback is not outside completion. Preserve all reported modelUsage; unknown model identity stays unknown. Under `GSTACK_CODEX_NO_SANDBOX=1` add `"codex_sandbox":"danger-full-access"`. diff --git a/autoplan/sections/dx-phase.md.tmpl b/autoplan/sections/dx-phase.md.tmpl index 150a7aee1..0d4c7fee6 100644 --- a/autoplan/sections/dx-phase.md.tmpl +++ b/autoplan/sections/dx-phase.md.tmpl @@ -3,12 +3,12 @@ Before dispatch, Read {{AUTOPLAN_REVIEW_FILE:plan-devex-review:with-sections}} p **Override rules:** - Mode selection: DX POLISH - Persona: infer from README/docs, pick the most common developer type (P6) -- Competitive benchmark: research through Aside per the loaded skill's "Web research runs in Aside" section (WebSearch when Aside is not ready); use the reference benchmarks when neither is available (P1) +- Competitive benchmark: research through Aside per the loaded skill's "Web research runs in Aside" section (WebSearch when Aside is not ready); reference benchmarks when neither is available (P1) - Magical moment: pick the lowest-effort delivery vehicle that achieves the competitive tier (P5) -- Getting started friction: always optimize toward fewer steps (P5, simpler over clever) +- Getting started friction: always optimize toward fewer steps (P5) - Error message quality: always require problem + cause + fix (P1, completeness) - API/CLI naming: consistency wins over cleverness (P5) -- DX taste decisions (e.g., opinionated defaults vs flexibility): mark TASTE DECISION +- DX taste decisions (opinionated defaults vs flexibility): mark TASTE DECISION - Dual voices: always run BOTH {{NATIVE_LABEL}} subagent AND {{OUTSIDE_LABEL}} if available (P6). **Bind phase input:** Run; use `snapshotPath` as `` for both voices: @@ -55,7 +55,7 @@ IMPORTANT: Do NOT read or execute any SKILL.md files or paths containing skills/ {{OUTSIDE_INVOCATION:autoplan}} -Failed/incomplete outside review → unavailable; disabled → skip outside. Both retain the native pass. +Failed/incomplete outside review → unavailable; disabled → skip outside. Both keep the native pass. {{OUTSIDE_PROVENANCE:dx}} diff --git a/autoplan/sections/eng-phase.md b/autoplan/sections/eng-phase.md index 1c521fd3e..118514059 100644 --- a/autoplan/sections/eng-phase.md +++ b/autoplan/sections/eng-phase.md @@ -89,7 +89,7 @@ echo 'OUTSIDE_STATUS: completed provider=codex host=claude' Use Bash `timeout: 600000`; show the full response in a `tool-output` fence. Require successful execution and valid markers. Refusal, empty/malformed output, missing score/severity/completion markers, timeout or CLI failure means `outside_status: unavailable`. P0/P1 findings block like native ones; `OUTSIDE_STATUS: unverified` is missing coverage. Use the caller's fallback; missing coverage is never clean/PASS. After either outcome, delete only your private prompt; scratch cleanup is automatic. -Failed/incomplete outside review → unavailable; disabled → skip outside. Both retain the native pass. +Failed/incomplete outside review → unavailable; disabled → skip outside. Both keep the native pass. Retain the historical review-log skill ID; add `"host":"claude","outside_provider":"codex","outside_status":"completed|unavailable|disabled|skipped","phase":"eng"`. Record differing attempt outcomes separately. `source:"codex"` requires completed CLI output; native uses `source:"in-host"` (historical `source:"claude"`: native Claude). Availability/native fallback is not outside completion. Preserve all reported modelUsage; unknown model identity stays unknown. Under `GSTACK_CODEX_NO_SANDBOX=1` add `"codex_sandbox":"danger-full-access"`. diff --git a/autoplan/sections/eng-phase.md.tmpl b/autoplan/sections/eng-phase.md.tmpl index f434bdccc..0e358a2f7 100644 --- a/autoplan/sections/eng-phase.md.tmpl +++ b/autoplan/sections/eng-phase.md.tmpl @@ -44,7 +44,7 @@ IMPORTANT: Do NOT read or execute any SKILL.md files or paths containing skills/ {{OUTSIDE_INVOCATION:autoplan}} -Failed/incomplete outside review → unavailable; disabled → skip outside. Both retain the native pass. +Failed/incomplete outside review → unavailable; disabled → skip outside. Both keep the native pass. {{OUTSIDE_PROVENANCE:eng}} diff --git a/autoplan/sections/phase-close.md b/autoplan/sections/phase-close.md index b6e71d147..d4e7f60d0 100644 --- a/autoplan/sections/phase-close.md +++ b/autoplan/sections/phase-close.md @@ -4,7 +4,7 @@ Read this section afresh when the current phase's review work finishes. Use the phase, amendment checkpoint and methodology path bound at that phase's exit. This procedure owns readback, verification and publication as separate operations. On hosts that inline sections, reread this close block in the installed Autoplan -SKILL.md at each exit; those hosts do not have a separate phase-close.md file. +SKILL.md at each exit; they have no separate phase-close.md file. 1. **Finish and save the review.** Require the phase's full methodology/section Reads, required outputs, successful writes and terminal reviewer results. @@ -12,8 +12,8 @@ SKILL.md at each exit; those hosts do not have a separate phase-close.md file. reviewer keeps the phase open. Apply the phase's failure policy to failed native attempts; unavailable/disabled voices receive no completion credit. Record wall time: `~/.claude/skills/gstack/bin/gstack-autoplan-timing close --run - --phase --out "$_AP" --outside-s --native-s ` (timing.json; the - analytics append is skipped with a printed line on an ephemeral state root). + --phase --out "$_AP" --outside-s --native-s ` (timing.json; an + ephemeral state root skips the analytics append with a printed line). 2. **Reconcile accepted requirements.** Record every accepted behavior, condition, test and manual checklist in this phase's accepted block. Taste remains provisional; User Challenges preserve the original requirements. A `None` @@ -30,7 +30,7 @@ preparation does not perform them. 4. **Read the complete current packet.** For every returned `readRanges` entry, issue a Read of `closePacketPath` with that entry's exact `offset` and `limit`. Finish all ranges through EOF. A Read of only the edited tail does not satisfy - this step; previous snapshots do not satisfy it. If a result is truncated, read + this step; neither do previous snapshots. If a result is truncated, read its missing ranges. If a Read fails, repair it and finish the missing ranges. Do not advance on a request without its result. After the final successful Read, perform step 5 here. diff --git a/autoplan/sections/phase-close.md.tmpl b/autoplan/sections/phase-close.md.tmpl index 68d9586d9..2ea1081f9 100644 --- a/autoplan/sections/phase-close.md.tmpl +++ b/autoplan/sections/phase-close.md.tmpl @@ -2,7 +2,7 @@ Read this section afresh when the current phase's review work finishes. Use the phase, amendment checkpoint and methodology path bound at that phase's exit. This procedure owns readback, verification and publication as separate operations. On hosts that inline sections, reread this close block in the installed Autoplan -SKILL.md at each exit; those hosts do not have a separate phase-close.md file. +SKILL.md at each exit; they have no separate phase-close.md file. 1. **Finish and save the review.** Require the phase's full methodology/section Reads, required outputs, successful writes and terminal reviewer results. @@ -10,8 +10,8 @@ SKILL.md at each exit; those hosts do not have a separate phase-close.md file. reviewer keeps the phase open. Apply the phase's failure policy to failed native attempts; unavailable/disabled voices receive no completion credit. Record wall time: `~/.claude/skills/gstack/bin/gstack-autoplan-timing close --run - --phase --out "$_AP" --outside-s --native-s ` (timing.json; the - analytics append is skipped with a printed line on an ephemeral state root). + --phase --out "$_AP" --outside-s --native-s ` (timing.json; an + ephemeral state root skips the analytics append with a printed line). 2. **Reconcile accepted requirements.** Record every accepted behavior, condition, test and manual checklist in this phase's accepted block. Taste remains provisional; User Challenges preserve the original requirements. A `None` @@ -28,7 +28,7 @@ preparation does not perform them. 4. **Read the complete current packet.** For every returned `readRanges` entry, issue a Read of `closePacketPath` with that entry's exact `offset` and `limit`. Finish all ranges through EOF. A Read of only the edited tail does not satisfy - this step; previous snapshots do not satisfy it. If a result is truncated, read + this step; neither do previous snapshots. If a result is truncated, read its missing ranges. If a Read fails, repair it and finish the missing ranges. Do not advance on a request without its result. After the final successful Read, perform step 5 here. diff --git a/bin/gstack-autoplan b/bin/gstack-autoplan index 96d5078ad..3b8c81f86 100755 --- a/bin/gstack-autoplan +++ b/bin/gstack-autoplan @@ -13,6 +13,7 @@ * gstack-autoplan status --out [--json] query: the manifest, no GSTACK_RESULT line * gstack-autoplan export --out [--json] rewrite plan.md, review-record.md, findings/tasks.jsonl and run.json hashes * gstack-autoplan release --out --attempt drop a reservation for an attempt that never ran + * gstack-autoplan contract print the coordinator contract every lane prompt inherits (plan E2) * * Logic: lib/autoplan-run.ts (next/submit/resume), lib/autoplan-gate.ts (close, gate, answer), * lib/autoplan-export.ts, lib/autoplan-prompts.ts, lib/spend-ledger.ts. Codes: lib/result-codes.ts. @@ -28,6 +29,7 @@ import { EXIT, exitTableHelp } from '../lib/headless-artifacts'; import { createRunner, familyConflict, type RunnerId } from '../lib/outside-voice-runner'; import { resultLine } from '../lib/result-codes'; import { release } from '../lib/spend-ledger'; +import { COORDINATOR_CONTRACT } from '../lib/coordinator-contract'; const HELP = `gstack-autoplan — unattended /autoplan: next/submit loop, answer, resume (docs/unattended.md) @@ -42,6 +44,7 @@ const HELP = `gstack-autoplan — unattended /autoplan: next/submit loop, answer next in a loop, only when a runner executes both voices here; --deadline cancels the children it started answer --out --gate-rev --reply "" [--json] resume --out [--json] status --out [--json] export --out release --out --attempt + contract print lib/coordinator-contract.ts (the block every lane and reviewer prompt inherits word for word) The first next needs --plan. The native voice is dispatched by the parent from the printed prompt file (a subagent on the parent's model); the outside voice runs through gstack-outside-voice or a cross-family subagent. Spend by host subagents is unknown. @@ -149,6 +152,7 @@ async function main(argv: string[]): Promise { const [cmd, ...rest] = argv; const json = rest.includes('--json'); if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { process.stdout.write(HELP + '\n'); return cmd ? EXIT.ok : EXIT.usage; } + if (cmd === 'contract') { process.stdout.write(COORDINATOR_CONTRACT + '\n'); return EXIT.ok; } const outFlag = flag(rest, '--out'); if (!outFlag) return usage(`${cmd} needs --out `); const out = path.resolve(outFlag); diff --git a/bin/gstack-banned-terms b/bin/gstack-banned-terms new file mode 100755 index 000000000..2708bb4a9 --- /dev/null +++ b/bin/gstack-banned-terms @@ -0,0 +1,90 @@ +#!/usr/bin/env bun +/** + * gstack-banned-terms — terms a repository forbids outside named locations (plan E3). + * + * gstack-banned-terms [check] [--base ] [--diff-base ] [--files ] [--policy-from base|head] [--json] + * gstack-banned-terms init [--force] + * gstack-banned-terms validate + * + * `check` scans the lines the branch adds against the base (or whole files with + * --files) for every term in .gstack/banned-terms.json outside its allowed + * paths. Each finding names the term, file:line and the allowed locations; the + * last line is `BANNED_TERMS: ok | found= | none`. Exit 0 ok or none, 1 found, + * 3 when the file is invalid. Logic: lib/banned-terms.ts. Called from /review and /document-release. + */ +import { spawnSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { BANNED_TERMS_PATH, addedLines, exampleBannedTermsJson, loadBannedTerms, renderBannedTerms, scanBannedTerms, validateBannedTerms, wholeFiles } from '../lib/banned-terms'; +import { EXIT, exitTableHelp } from '../lib/headless-artifacts'; +import { resultLine } from '../lib/result-codes'; +import { resolveBaseBranch } from '../lib/ship-policy'; + +const HELP = `gstack-banned-terms — scan a diff for banned terms outside their allowed locations (docs/ship-policy.md#banned-terms) + + check [--base ] [--diff-base ] [--files ] [--policy-from base|head] [--json] + added lines since the merge base with (default origin/HEAD), or whole --files; the term list + is read from the committed copy on the base (default) or the working tree (--policy-from head) + init [--force] write a commented ${BANNED_TERMS_PATH} example + validate schema and containment check of the working-tree file + +${exitTableHelp()}`; + +function flag(args: string[], name: string): string | undefined { + const i = args.indexOf(name); + return i >= 0 ? args[i + 1] : undefined; +} + +function main(argv: string[]): number { + if (argv.includes('--help') || argv.includes('-h')) { process.stdout.write(HELP + '\n'); return EXIT.ok; } + const cmd = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'check'; + const rest = cmd === argv[0] ? argv.slice(1) : argv; + const root = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8', timeout: 15_000 }); + if (root.status !== 0) { process.stderr.write('gstack-banned-terms: not inside a git repository\n'); return EXIT.usage; } + const repoRoot = root.stdout.trim(); + const file = path.join(repoRoot, BANNED_TERMS_PATH); + + if (cmd === 'init') { + if (fs.existsSync(file) && !rest.includes('--force')) { process.stderr.write(`gstack-banned-terms: ${BANNED_TERMS_PATH} exists (pass --force to overwrite)\n`); return EXIT.refused; } + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, exampleBannedTermsJson()); + process.stdout.write(`BANNED_TERMS_INIT: ${BANNED_TERMS_PATH}\n`); + return EXIT.ok; + } + if (cmd === 'validate') { + if (!fs.existsSync(file)) { process.stdout.write(`BANNED_TERMS: none (no ${BANNED_TERMS_PATH})\n`); return EXIT.ok; } + let parsed: unknown; + try { parsed = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e: any) { process.stderr.write(resultLine('gstack-banned-terms validate', 'BANNED_TERMS_INVALID', e.message) + '\n'); return EXIT.refused; } + const errors = validateBannedTerms(parsed); + for (const e of errors) process.stderr.write(resultLine('gstack-banned-terms validate', e.code, `${e.message} at ${e.path || ''}`) + '\n'); + if (errors.length) return EXIT.refused; + process.stdout.write(`BANNED_TERMS_VALID: ${BANNED_TERMS_PATH}\n`); + return EXIT.ok; + } + if (cmd !== 'check') { process.stderr.write(`gstack-banned-terms: unknown command ${cmd}\n${HELP}\n`); return EXIT.usage; } + + const json = rest.includes('--json'); + const base = resolveBaseBranch(repoRoot, flag(rest, '--base')); + const policyFrom = flag(rest, '--policy-from') ?? 'base'; + let load = loadBannedTerms(repoRoot, policyFrom === 'head' ? undefined : base); + if (policyFrom !== 'head' && 'absent' in load) load = loadBannedTerms(repoRoot); + if ('absent' in load) { process.stdout.write(json ? JSON.stringify({ source: load.source, absent: true, findings: [] }) + '\n' : `BANNED_TERMS: none (no ${BANNED_TERMS_PATH} on ${policyFrom === 'head' ? 'the working tree' : base})\n`); return EXIT.ok; } + if ('errors' in load) { for (const e of load.errors) process.stderr.write(resultLine('gstack-banned-terms', e.code, `${e.message} at ${e.path || ''} (${load.source})`) + '\n'); return EXIT.refused; } + + const filesFlag = flag(rest, '--files'); + let files; + if (filesFlag) files = wholeFiles(repoRoot, filesFlag.split(',').map(s => s.trim()).filter(Boolean)); + else { + const diffBase = flag(rest, '--diff-base') ?? (() => { const mb = spawnSync('git', ['merge-base', base, 'HEAD'], { cwd: repoRoot, encoding: 'utf8', timeout: 30_000 }); return mb.status === 0 ? mb.stdout.trim() : undefined; })(); + if (!diffBase) { process.stderr.write(`gstack-banned-terms: no merge base with ${base}; pass --diff-base or --files \n`); return EXIT.usage; } + const diff = spawnSync('git', ['diff', '--unified=0', '--no-color', diffBase], { cwd: repoRoot, encoding: 'utf8', timeout: 120_000, maxBuffer: 256 * 1024 * 1024 }); + if (diff.status !== 0) { process.stderr.write(`gstack-banned-terms: git diff ${diffBase} failed\n`); return EXIT.fail; } + files = addedLines(diff.stdout); + } + const result = scanBannedTerms(load.config, load.source, files); + if (json) process.stdout.write(JSON.stringify(result) + '\n'); else process.stdout.write(renderBannedTerms(result)); + if (result.findings.length) { process.stderr.write(resultLine('gstack-banned-terms', 'BANNED_TERM_FOUND', `${result.findings.length} finding(s)`) + '\n'); return EXIT.fail; } + return EXIT.ok; +} + +process.exitCode = main(process.argv.slice(2)); diff --git a/bin/gstack-config b/bin/gstack-config index bb7067d67..ee7685b2e 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -221,6 +221,11 @@ CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on ne # ship_rerun_backend: local # local | ubicloud — where free-suite flake reruns # # run; ubicloud also needs UBICLOUD_API_KEY. # # Invalid values are REJECTED (value kept). +# test_backend: local # local | ubicloud — where the full unit and E2E +# # tiers run (plan E2). The gstack-doctor cores row +# # recommends ubicloud under 8 cores; ubicloud +# # needs UBICLOUD_API_KEY and scripts/ubicloud/. +# # Invalid values are REJECTED (value kept). # ship_greptile_early: true # true | false — when the repo uses Greptile, /ship # # pushes and opens a draft PR after its free tests # # so Greptile reviews in parallel (a public repo @@ -301,6 +306,7 @@ lookup_default() { ship_measure_sweep_cases) echo "5" ;; ship_measure_sweep_budget_usd) echo "150" ;; ship_rerun_backend) echo "local" ;; # local | ubicloud (Ubicloud spend is opt-in) + test_backend) echo "local" ;; # local | ubicloud — full unit/E2E tiers (plan E2) plan_review_tier) echo "frontier" ;; # frontier | smart | host — lib/model-policy.ts reads it strictly implementation_tier) echo "smart" ;; # frontier | smart model_frontier_claude|model_frontier_openai|model_smart_claude|model_smart_openai) echo "" ;; # empty = shipped catalog ID (gstack-models list) @@ -748,9 +754,9 @@ case "${1:-}" in echo "Error: ship_greptile_early '$VALUE' not recognized. Valid values: true, false. Existing value left unchanged." >&2 exit 1 fi ;; - ship_rerun_backend) + ship_rerun_backend|test_backend) if [ "$VALUE" != "local" ] && [ "$VALUE" != "ubicloud" ]; then - echo "Error: ship_rerun_backend '$VALUE' not recognized. Valid values: local, ubicloud. Existing value left unchanged." >&2 + echo "Error: $KEY '$VALUE' not recognized. Valid values: local, ubicloud. Existing value left unchanged." >&2 exit 1 fi ;; esac @@ -939,7 +945,7 @@ case "${1:-}" in timeline_stop_hook design_detector design_detector_install_prompted memorable_recall \ browse_extension_id ship_measure_rule_trials ship_measure_behavior_panels \ ship_measure_judge_outputs ship_measure_ask_per_trial_usd ship_measure_budget_usd \ - ship_measure_max_rounds ship_measure_sweep_cases ship_measure_sweep_budget_usd ship_rerun_backend \ + ship_measure_max_rounds ship_measure_sweep_cases ship_measure_sweep_budget_usd ship_rerun_backend test_backend \ plan_review_tier implementation_tier model_frontier_claude model_frontier_openai \ model_smart_claude model_smart_openai; do gstack_config_select "$KEY" @@ -964,7 +970,7 @@ case "${1:-}" in timeline_stop_hook design_detector design_detector_install_prompted memorable_recall \ browse_extension_id ship_measure_rule_trials ship_measure_behavior_panels \ ship_measure_judge_outputs ship_measure_ask_per_trial_usd ship_measure_budget_usd \ - ship_measure_max_rounds ship_measure_sweep_cases ship_measure_sweep_budget_usd ship_rerun_backend \ + ship_measure_max_rounds ship_measure_sweep_cases ship_measure_sweep_budget_usd ship_rerun_backend test_backend \ plan_review_tier implementation_tier model_frontier_claude model_frontier_openai \ model_smart_claude model_smart_openai; do printf ' %-24s %s\n' "$KEY:" "$(lookup_default "$KEY")" diff --git a/bin/gstack-contributor-mode b/bin/gstack-contributor-mode new file mode 100755 index 000000000..db0b450d4 --- /dev/null +++ b/bin/gstack-contributor-mode @@ -0,0 +1,65 @@ +#!/usr/bin/env bun +/** + * gstack-contributor-mode — contributor-PR mode detection for /review (plan E3, decision D10). + * + * gstack-contributor-mode detect [--author ] [--pr ] [--contributor] [--base ] [--policy-from base|head] [--json] + * gstack-contributor-mode instructions + * + * Prints `CONTRIBUTOR_MODE: on|off author= source=flag|policy|collaborators|off` + * then, when on, the evidence-first instructions /review follows (the table and the + * credit-line rule). Maintainers come from .gstack/ship-policy.json on the base + * branch; without a list, repository push permission through the collaborators API; + * without either, off with the source recorded (CONTRIBUTOR_SOURCE_UNAVAILABLE on + * stderr). CODEOWNERS is never read. Exit 0 always for a completed detection. + * Logic: lib/contributor-mode.ts. + */ +import { spawnSync } from 'node:child_process'; +import { CONTRIBUTOR_INSTRUCTIONS, detectContributorMode, renderDetection } from '../lib/contributor-mode'; +import { EXIT, exitTableHelp } from '../lib/headless-artifacts'; +import { resultLine } from '../lib/result-codes'; +import { isUnavailable, loadPolicyFromBase, loadPolicyFromHead, resolveBaseBranch } from '../lib/ship-policy'; + +const HELP = `gstack-contributor-mode — is the PR author outside the maintainer set? (docs/ship-policy.md#contributor-mode) + + detect [--author ] [--pr ] [--contributor] [--base ] [--policy-from base|head] [--json] + --contributor forces the mode on (source=flag); --author skips the gh lookup + instructions print the evidence-first review instructions without detecting + +${exitTableHelp()}`; + +function flag(args: string[], name: string): string | undefined { + const i = args.indexOf(name); + return i >= 0 ? args[i + 1] : undefined; +} + +function main(argv: string[]): number { + if (argv.includes('--help') || argv.includes('-h')) { process.stdout.write(HELP + '\n'); return EXIT.ok; } + const [cmd, ...rest] = argv; + if (cmd === 'instructions') { process.stdout.write(CONTRIBUTOR_INSTRUCTIONS + '\n'); return EXIT.ok; } + if (cmd !== 'detect') { process.stderr.write(`gstack-contributor-mode: unknown command ${cmd ?? ''}\n${HELP}\n`); return EXIT.usage; } + const root = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8', timeout: 15_000 }); + if (root.status !== 0) { process.stderr.write('gstack-contributor-mode: not inside a git repository\n'); return EXIT.usage; } + const cwd = root.stdout.trim(); + const prText = flag(rest, '--pr'); + const pr = prText !== undefined ? Number(prText) : undefined; + if (pr !== undefined && (!Number.isInteger(pr) || pr <= 0)) { process.stderr.write('gstack-contributor-mode: --pr needs a positive number\n'); return EXIT.usage; } + + let maintainers: string[] = []; let policySource = 'no ship policy'; + if ((flag(rest, '--policy-from') ?? 'base') === 'head') { const l = loadPolicyFromHead(cwd); maintainers = l.policy.maintainers; policySource = l.source.label; } + else { + const base = resolveBaseBranch(cwd, flag(rest, '--base')); + const l = loadPolicyFromBase(cwd, base); + if (!isUnavailable(l)) { maintainers = l.policy.maintainers; policySource = l.source.label; } + else policySource = `policy source unavailable (${l.detail})`; + } + const d = detectContributorMode({ cwd, author: flag(rest, '--author'), pr, force: rest.includes('--contributor'), maintainers, policySource }); + if (rest.includes('--json')) process.stdout.write(JSON.stringify(d) + '\n'); + else { + process.stdout.write(renderDetection(d) + '\n'); + if (d.mode === 'on') process.stdout.write('\n' + CONTRIBUTOR_INSTRUCTIONS + '\n'); + } + if (d.error) process.stderr.write(resultLine('gstack-contributor-mode', d.error.code, d.error.message) + '\n'); + return EXIT.ok; +} + +process.exitCode = main(process.argv.slice(2)); diff --git a/bin/gstack-doctor-check.sh b/bin/gstack-doctor-check.sh index 181d454af..a622ba92d 100644 --- a/bin/gstack-doctor-check.sh +++ b/bin/gstack-doctor-check.sh @@ -300,15 +300,20 @@ _dc_check_privacy() { _dc_row PASS privacy "telemetry=$(_dc_cfg telemetry off) artifacts_sync=$(_dc_cfg artifacts_sync_mode off) codex_reviews=$(_dc_cfg codex_reviews enabled) update_check=$(_dc_cfg update_check true)" } +# cores: the CPU count plus the configured test_backend (gstack-config, local | +# ubicloud); under 8 cores with test_backend=local the row recommends ubicloud +# for the full unit and E2E tiers (plan E2). Always PASS: a small machine is a +# fact, not a broken install. _dc_check_cores() { - local n + local n backend n=$(nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 0) n=$(printf '%s' "$n" | clamp '0-9' 6) - case "$n" in ''|0) _dc_row PASS cores "unknown CPU count"; return ;; esac - if [ "$n" -lt 8 ]; then - _dc_row PASS cores "$n (under 8: run full unit and E2E tiers on a remote gate, e.g. test_backend=ubicloud)" + backend=$("$BIN/gstack-config" get test_backend 2>/dev/null | --init [--title ] + * gstack-eval-plan arms [--arms ] [--policy-doc ] [--input-tokens N] [--output-tokens N] [--json] + * gstack-eval-plan dry-run --input --arms [--fields a,b] [--output-budget N] + * [--retry-prompt ] [--max-rows N] [--out ] [--json] + * gstack-eval-plan pilot start --out --cap --arms --dry-run [--n 3] + * gstack-eval-plan pilot settle --out --attempt --usd + * gstack-eval-plan pilot release --out --attempt + * gstack-eval-plan pilot report --out --total [--workers W] [--json] + * + * The dry run spends nothing (stub model) and prints the checklist: price-table + * entry per arm, output budget sized for reasoning models, the retry prompt's + * exact text. The pilot reserves through lib/spend-ledger.ts (estimated admission, + * lock-shared refusal), settles actual cost, and `report` prints the spend table + * (spent, reserved, unknown charges, worst-case overrun) and the extrapolation + * with its error band before the approval ask. Exit 0 ok, 1 fail (incomplete + * prereg, failed check, exceeded cap), 2 usage, 3 refused. Logic: lib/eval-plan.ts. + */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { checkPrereg, DEFAULT_TOKENS, dryRun, pilotReport, preregTemplate, releasePilot, renderArms, renderDryRun, renderPilotReport, resolveArms, settlePilot, startPilot } from '../lib/eval-plan'; +import { EXIT, exitTableHelp } from '../lib/headless-artifacts'; +import { resultLine } from '../lib/result-codes'; +import { SpendCapExceeded } from '../lib/spend-ledger'; + +const HELP = `gstack-eval-plan — preregistration, priced arms, $0 dry run, priced pilot (eval-plan/SKILL.md) + + prereg --check the five sections exist and are filled: Bars, Arms, Decoys, Stop rules, Held-out exposure + prereg --init [--title ] write the template + arms [--arms m,m] [--policy-doc f] [--input-tokens N] [--output-tokens N] [--json] + arms from \`gstack-models resolve --role eval-arms\`, else the repo policy doc, each priced per item + dry-run --input rows.jsonl --arms m,m [--fields a,b] [--output-budget N] [--retry-prompt f] [--max-rows N] [--out dir] [--json] + stub model over the real rows; checklist: price table, output budget, retry prompt text + pilot start --out dir --cap usd --arms m,m --dry-run dry-run.json [--n 3] + pilot settle --out dir --attempt id --usd pilot release --out dir --attempt id + pilot report --out dir --total items [--workers W] [--json] spend table + extrapolation with a 95% band, then the approval ask + +${exitTableHelp()}`; + +function flag(args: string[], name: string): string | undefined { + const i = args.indexOf(name); + return i >= 0 ? args[i + 1] : undefined; +} +function num(args: string[], name: string, fallback?: number): number | undefined { + const v = flag(args, name); + if (v === undefined) return fallback; + const n = Number(v); + if (!Number.isFinite(n) || n < 0) throw new Error(`${name} needs a non-negative number`); + return n; +} +function usage(message: string): number { process.stderr.write(`gstack-eval-plan: ${message}\n${HELP}\n`); return EXIT.usage; } +const csv = (v: string | undefined) => (v ?? '').split(',').map(s => s.trim()).filter(Boolean); + +function doPrereg(rest: string[]): number { + const init = flag(rest, '--init'); + if (init) { + if (fs.existsSync(init) && !rest.includes('--force')) { process.stderr.write(`gstack-eval-plan: ${init} exists (pass --force)\n`); return EXIT.refused; } + fs.mkdirSync(path.dirname(path.resolve(init)), { recursive: true }); + fs.writeFileSync(init, preregTemplate(flag(rest, '--title'))); + process.stdout.write(`PREREG_INIT: ${init}\n`); + return EXIT.ok; + } + const file = flag(rest, '--check'); + if (!file || !fs.existsSync(file)) return usage('prereg needs --check or --init '); + const r = checkPrereg(fs.readFileSync(file, 'utf8')); + if (rest.includes('--json')) process.stdout.write(JSON.stringify({ file, ...r }) + '\n'); + else process.stdout.write(`PREREG: ${r.ok ? 'ok' : 'incomplete'} present=${r.present.length}/5${r.missing.length ? ` missing=${r.missing.join(',')}` : ''}${r.empty.length ? ` empty=${r.empty.join(',')}` : ''} file=${file}\n`); + if (!r.ok) { process.stderr.write(resultLine('gstack-eval-plan prereg', 'PREREG_INCOMPLETE', [...r.missing.map(m => `missing ${m}`), ...r.empty.map(e => `empty ${e}`)].join('; ')) + '\n'); return EXIT.fail; } + return EXIT.ok; +} + +function doArms(rest: string[]): number { + const tokens = { input: num(rest, '--input-tokens', DEFAULT_TOKENS.input)!, output: num(rest, '--output-tokens', DEFAULT_TOKENS.output)! }; + const r = resolveArms({ cwd: process.cwd(), explicit: csv(flag(rest, '--arms')), policyDoc: flag(rest, '--policy-doc'), tokens }); + if (rest.includes('--json')) process.stdout.write(JSON.stringify({ ...r, tokens }) + '\n'); else process.stdout.write(renderArms(r, tokens)); + for (const e of r.errors) process.stderr.write(resultLine('gstack-eval-plan arms', e.code, e.message) + '\n'); + return r.errors.length ? EXIT.fail : EXIT.ok; +} + +function doDryRun(rest: string[]): number { + const input = flag(rest, '--input'); + const arms = csv(flag(rest, '--arms')); + if (!input || !fs.existsSync(input)) return usage('dry-run needs --input '); + if (!arms.length) return usage('dry-run needs --arms '); + const r = dryRun({ input, arms, fields: csv(flag(rest, '--fields')), outputBudget: num(rest, '--output-budget'), retryPrompt: flag(rest, '--retry-prompt'), maxRows: num(rest, '--max-rows') }); + const out = flag(rest, '--out'); + if (out) { fs.mkdirSync(out, { recursive: true }); fs.writeFileSync(path.join(out, 'dry-run.json'), JSON.stringify(r, null, 2) + '\n'); } + if (rest.includes('--json')) process.stdout.write(JSON.stringify(r) + '\n'); else process.stdout.write(renderDryRun(r)); + if (out) process.stdout.write(`DRY_RUN_RECORD: ${path.join(out, 'dry-run.json')}\n`); + if (!r.pass) { + const why = [r.rows === 0 ? 'no rows' : '', r.bad_rows.length ? `${r.bad_rows.length} bad rows` : '', !r.checks.price_table.ok ? `price missing: ${r.checks.price_table.missing.join(',') || 'no arms'}` : '', !r.checks.output_budget.ok ? 'output budget below the reasoning floor' : '', !r.checks.retry_prompt.ok ? 'retry prompt missing' : ''].filter(Boolean).join('; '); + process.stderr.write(resultLine('gstack-eval-plan dry-run', r.checks.price_table.ok ? 'EVAL_DRY_RUN_FAILED' : 'EVAL_PRICE_MISSING', why) + '\n'); + return EXIT.fail; + } + return EXIT.ok; +} + +function doPilot(rest: string[]): number { + const [sub, ...args] = rest; + const out = flag(args, '--out'); + if (!out) return usage(`pilot ${sub ?? ''} needs --out `); + if (sub === 'start') { + const cap = num(args, '--cap'); const arms = csv(flag(args, '--arms')); const dry = flag(args, '--dry-run'); + if (cap === undefined || !arms.length || !dry || !fs.existsSync(dry)) return usage('pilot start needs --cap --arms --dry-run '); + const r = startPilot({ out, cap, n: num(args, '--n', 3)!, arms, dryRun: dry }); + if (r.error && !r.record) { process.stderr.write(resultLine('gstack-eval-plan pilot start', r.error.code, r.error.message) + '\n'); return EXIT.refused; } + for (const a of r.record.attempts) process.stdout.write(`PILOT_ATTEMPT: ${a.attempt} model=${a.model} estimate=$${a.estimate_usd.toFixed(4)}\n`); + process.stdout.write(`PILOT: status=${r.record.status} reserved=${r.record.attempts.length} cap=$${cap.toFixed(2)} ledger=${path.join(out, 'spend.json')}\n`); + if (r.error) { process.stderr.write(resultLine('gstack-eval-plan pilot start', r.error.code, r.error.message) + '\n'); return EXIT.fail; } + return EXIT.ok; + } + if (sub === 'settle' || sub === 'release') { + const attempt = flag(args, '--attempt'); + if (!attempt) return usage(`pilot ${sub} needs --attempt `); + if (sub === 'release') { const l = releasePilot(out, attempt); process.stdout.write(`RELEASED: ${attempt} reserved=${l.reserved.length}\n`); return EXIT.ok; } + const usd = flag(args, '--usd'); + if (usd === undefined) return usage('pilot settle needs --usd '); + const value = usd === 'unknown' ? 'unknown' as const : Number(usd); + if (value !== 'unknown' && (!Number.isFinite(value) || value < 0)) return usage('--usd needs a non-negative number or unknown'); + const l = settlePilot(out, attempt, value); + process.stdout.write(`SETTLED: ${attempt} usd=${usd} spent=$${l.spent_usd.toFixed(4)} open=${l.reserved.length}\n`); + return EXIT.ok; + } + if (sub === 'report') { + const total = num(args, '--total'); + if (total === undefined) return usage('pilot report needs --total '); + if (!fs.existsSync(path.join(out, 'pilot.json'))) return usage(`${out} has no pilot.json; run pilot start first`); + const r = pilotReport(out, { total, workers: num(args, '--workers') }); + if (args.includes('--json')) process.stdout.write(JSON.stringify(r) + '\n'); else process.stdout.write(renderPilotReport(r)); + if (r.record.status === 'budget_exhausted' || r.spend.worst_case_overrun_usd > 0) { process.stderr.write(resultLine('gstack-eval-plan pilot report', 'EVAL_BUDGET_EXHAUSTED', `worst case $${r.spend.worst_case_usd.toFixed(4)} vs cap $${r.record.cap_usd.toFixed(2)}`) + '\n'); return EXIT.fail; } + return r.projection.verdict === 'exceeds' ? EXIT.fail : EXIT.ok; + } + return usage(`unknown pilot command ${sub ?? ''}`); +} + +function main(argv: string[]): number { + const [cmd, ...rest] = argv; + if (!cmd || cmd === '--help' || cmd === '-h') { process.stdout.write(HELP + '\n'); return cmd ? EXIT.ok : EXIT.usage; } + try { + if (cmd === 'prereg') return doPrereg(rest); + if (cmd === 'arms') return doArms(rest); + if (cmd === 'dry-run') return doDryRun(rest); + if (cmd === 'pilot') return doPilot(rest); + return usage(`unknown command ${cmd}`); + } catch (e: any) { + if (e instanceof SpendCapExceeded) { process.stderr.write(resultLine(`gstack-eval-plan ${cmd}`, 'EVAL_BUDGET_EXHAUSTED', e.message) + '\n'); return EXIT.fail; } + process.stderr.write(`gstack-eval-plan ${cmd}: ${e?.message ?? e}\n`); + return e?.message?.includes('needs') ? EXIT.usage : EXIT.fail; + } +} + +process.exitCode = main(process.argv.slice(2)); diff --git a/bin/gstack-evidence b/bin/gstack-evidence index b3844620c..f56231557 100755 --- a/bin/gstack-evidence +++ b/bin/gstack-evidence @@ -9,6 +9,7 @@ * gstack-evidence bundle --out [--label ]... [--require ] [--selection L=]... * [--run-url L=]... [--import ] [--base ] (plan C3; lib/evidence-bundle.ts) * gstack-evidence ancestor ... | --from-policy [--base ] (plan C7) + * gstack-evidence verify [--rerun] [--out ] [--json] (plan E2) * * `run` is a TRANSPARENT wrapper: it streams the child's output through * unchanged, tees it to a 0600 log (2MB cap with a truncation marker), and @@ -52,7 +53,7 @@ import { spawnSync } from "child_process"; import { appendJsonl, readJsonl } from "../lib/jsonl-store"; import { resolveStateRoot } from "../lib/state-root"; import { scan, applyRedactions } from "../lib/redact-engine"; -import { buildBundle, checkAncestors } from "../lib/evidence-bundle"; +import { buildBundle, checkAncestors, readBundle, renderVerification, verifyBundle } from "../lib/evidence-bundle"; import { resultLine } from "../lib/result-codes"; import { isUnavailable, loadPolicyFromBase, resolveBaseBranch } from "../lib/ship-policy"; import { versionAtRef } from "../lib/restamp"; @@ -725,6 +726,27 @@ function cmdAncestor(argv: string[]): number { return results.every((r) => r.ok) ? 0 : 1; } +/** E2: rerun each lane's probe on the bundle's tree, or mark each claim from this machine's ledger. */ +function cmdVerify(argv: string[]): number { + const file = argv.find((a, i) => !a.startsWith("--") && argv[i - 1] !== "--out"); + if (!file) { console.error("usage: gstack-evidence verify [--rerun] [--out ] [--json]"); return 2; } + const read = readBundle(file); + if (!read.bundle) { console.error(resultLine("gstack-evidence verify", read.error!.code, read.error!.message)); return 1; } + const repoRoot = git(["rev-parse", "--show-toplevel"]) ?? process.cwd(); + let records: Record[] = []; + try { records = readJsonl>(ledgerPath().file); } catch { records = []; } + const v = verifyBundle({ repoRoot, bundle: read.bundle, bundlePath: file, rerun: argv.includes("--rerun"), ledgerRecords: records }); + const outIdx = argv.indexOf("--out"); + if (outIdx >= 0 && argv[outIdx + 1]) { + const { writeFileSync } = require("fs") as typeof import("fs"); + mkdirpSync(dirname(argv[outIdx + 1]!)); + writeFileSync(argv[outIdx + 1]!, JSON.stringify({ ...read.bundle, verification: v }, null, 2) + "\n"); + } + if (argv.includes("--json")) console.log(JSON.stringify(v)); else process.stdout.write(renderVerification(v)); + for (const c of v.claims) if (c.verdict === "unverified" && c.required) console.error(resultLine("gstack-evidence verify", "EVIDENCE_UNVERIFIED", `${c.label}: ${c.reason}`)); + return v.required_unverified.length ? 1 : 0; +} + const [, , sub, ...rest] = process.argv; try { if (sub === "run") { @@ -735,8 +757,10 @@ try { process.exit(cmdBundle(rest)); } else if (sub === "ancestor") { process.exit(cmdAncestor(rest)); + } else if (sub === "verify") { + process.exit(cmdVerify(rest)); } else { - console.error("usage: gstack-evidence run|check|bundle|ancestor ..."); + console.error("usage: gstack-evidence run|check|bundle|ancestor|verify ..."); process.exit(2); } } catch (e: any) { diff --git a/bin/gstack-lane-check b/bin/gstack-lane-check new file mode 100755 index 000000000..d9ca5692e --- /dev/null +++ b/bin/gstack-lane-check @@ -0,0 +1,62 @@ +#!/usr/bin/env bun +/** + * gstack-lane-check — planned files versus in-flight lanes (plan E2). + * + * gstack-lane-check --planned [,...] [--planned-file ] [--branch ]... [--pr ]... [--base ] [--json] + * + * Diffs each in-flight branch or pull request against its merge base with the + * base branch and prints one `LANE_CONFLICT:` line per planned file it already + * changes (PR number, branch, hunk count), then `LANE_CHECK: clear | conflicts | unresolved`. + * Exit 0 clear, 1 conflicts, 3 when a lane could not be resolved and nothing conflicts. + * Logic: lib/lane-ownership.ts. Codes: LANE_CONFLICT, LANE_REF_UNAVAILABLE (lib/result-codes.ts). + */ +import { spawnSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import { EXIT, exitTableHelp } from '../lib/headless-artifacts'; +import { laneCheck, renderLaneCheck } from '../lib/lane-ownership'; +import { resultLine } from '../lib/result-codes'; + +const HELP = `gstack-lane-check — one owner per file across in-flight lanes + + --planned files or globs this lane plans to touch (repeatable; \`lib/**\` and \`*.ts\` wildcards) + --planned-file one planned path per line (# comments allowed) + --branch an in-flight branch (fetched: origin/ or local) + --pr an in-flight pull request; its head branch comes from \`gh pr view\` + --base the base to diff against (default origin/HEAD, else origin/main) + --json the whole check as one JSON object + +${exitTableHelp()}`; + +function main(argv: string[]): number { + if (argv.includes('--help') || argv.includes('-h')) { process.stdout.write(HELP + '\n'); return EXIT.ok; } + const planned: string[] = []; const branches: string[] = []; const prs: number[] = []; + let base: string | undefined; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]!; + if (a === '--planned') planned.push(...(argv[++i] ?? '').split(',')); + else if (a === '--planned-file') { + const file = argv[++i]; + if (!file || !fs.existsSync(file)) { process.stderr.write(`gstack-lane-check: --planned-file ${file ?? ''} not found\n`); return EXIT.usage; } + planned.push(...fs.readFileSync(file, 'utf8').split('\n').map(l => l.replace(/#.*$/, '').trim()).filter(Boolean)); + } + else if (a === '--branch') branches.push(argv[++i] ?? ''); + else if (a === '--pr') { const n = Number(argv[++i]); if (!Number.isInteger(n) || n <= 0) { process.stderr.write('gstack-lane-check: --pr needs a positive number\n'); return EXIT.usage; } prs.push(n); } + else if (a === '--base') base = argv[++i]; + else if (a !== '--json') { process.stderr.write(`gstack-lane-check: unknown argument ${a}\n${HELP}\n`); return EXIT.usage; } + } + if (planned.filter(Boolean).length === 0) { process.stderr.write(`gstack-lane-check: --planned or --planned-file is required\n${HELP}\n`); return EXIT.usage; } + if (branches.filter(Boolean).length === 0 && prs.length === 0) { process.stderr.write(`gstack-lane-check: name at least one --branch or --pr\n${HELP}\n`); return EXIT.usage; } + const root = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8', timeout: 15_000 }); + if (root.status !== 0) { process.stderr.write('gstack-lane-check: not inside a git repository\n'); return EXIT.usage; } + const check = laneCheck({ cwd: root.stdout.trim(), planned, branches: branches.filter(Boolean), prs, base }); + if (argv.includes('--json')) process.stdout.write(JSON.stringify(check) + '\n'); + else process.stdout.write(renderLaneCheck(check)); + for (const lane of check.unresolved) process.stderr.write(resultLine('gstack-lane-check', 'LANE_REF_UNAVAILABLE', lane.pr !== null ? `PR #${lane.pr}` : lane.branch) + '\n'); + if (check.intersections.length) { + process.stderr.write(resultLine('gstack-lane-check', 'LANE_CONFLICT', `${check.intersections.length} planned file(s) already in flight`) + '\n'); + return EXIT.fail; + } + return check.unresolved.length ? EXIT.refused : EXIT.ok; +} + +process.exitCode = main(process.argv.slice(2)); diff --git a/bin/gstack-owner-brief b/bin/gstack-owner-brief new file mode 100755 index 000000000..85fa74c0a --- /dev/null +++ b/bin/gstack-owner-brief @@ -0,0 +1,73 @@ +#!/usr/bin/env bun +/** + * gstack-owner-brief — the one-page owner brief at the autoplan gate (plan E3). + * + * gstack-owner-brief render --out [--page N] [--summary ] [--write] [--json] + * gstack-owner-brief close --out --plan [--date YYYY-MM-DD] [--json] + * + * `render` prints the plain-language summary (--summary , else /summary.md + * when present, else counted from the artifacts), the URGENT block and the numbered + * decisions (gate list, seven per page, stable ids; auto items show the default + * taken if unanswered, approval items stay pending) and points at review-record.md + * for the full review; --write also saves brief.md in the run directory. + * `close` rewrites every `draft direction stands until the owner decides` line in + * the plan to the decided option and date; pending lines stay and are reported + * (DRAFT_DIRECTION_UNRESOLVED, exit 1). Logic: lib/owner-brief.ts. + */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { EXIT, exitTableHelp } from '../lib/headless-artifacts'; +import { BRIEF_FILE, closeDraftDirectionsFile, loadBriefInputs, renderBrief } from '../lib/owner-brief'; +import { resultLine } from '../lib/result-codes'; + +const HELP = `gstack-owner-brief — one page for the owner: summary, URGENT block, numbered decisions (docs/unattended.md#owner-brief) + + render --out [--page N] [--summary ] [--write] [--json] + close --out --plan [--date YYYY-MM-DD] [--json] + +${exitTableHelp()}`; + +function flag(args: string[], name: string): string | undefined { + const i = args.indexOf(name); + return i >= 0 ? args[i + 1] : undefined; +} + +function main(argv: string[]): number { + const [cmd, ...rest] = argv; + if (!cmd || cmd === '--help' || cmd === '-h') { process.stdout.write(HELP + '\n'); return cmd ? EXIT.ok : EXIT.usage; } + const out = flag(rest, '--out'); + if (!out) { process.stderr.write(`gstack-owner-brief: ${cmd} needs --out \n${HELP}\n`); return EXIT.usage; } + const loaded = loadBriefInputs(path.resolve(out)); + if ('error' in loaded) { process.stderr.write(resultLine(`gstack-owner-brief ${cmd}`, loaded.error.code, loaded.error.message) + '\n'); return loaded.error.code === 'BRIEF_INPUT_MISSING' ? EXIT.refused : EXIT.fail; } + const json = rest.includes('--json'); + + if (cmd === 'render') { + const pageText = flag(rest, '--page'); + if (pageText !== undefined && !/^[0-9]+$/.test(pageText)) { process.stderr.write('gstack-owner-brief: --page needs a number\n'); return EXIT.usage; } + const summaryFile = flag(rest, '--summary') ?? (fs.existsSync(path.join(path.resolve(out), 'summary.md')) ? path.join(path.resolve(out), 'summary.md') : undefined); + const summary = summaryFile ? fs.readFileSync(summaryFile, 'utf8') : undefined; + const brief = renderBrief(loaded.inputs, { page: pageText ? Number(pageText) : 1, summary, outDir: path.resolve(out) }); + if (rest.includes('--write')) fs.writeFileSync(path.join(path.resolve(out), BRIEF_FILE), brief); + if (json) process.stdout.write(JSON.stringify({ run: loaded.inputs.gate.run, gate_rev: loaded.inputs.gate.gate_rev, items: loaded.inputs.gate.items.length, brief }) + '\n'); + else process.stdout.write(brief); + return EXIT.ok; + } + if (cmd === 'close') { + const plan = flag(rest, '--plan'); + if (!plan || !fs.existsSync(plan)) { process.stderr.write(`gstack-owner-brief: close needs --plan \n`); return EXIT.usage; } + const date = flag(rest, '--date'); + if (date !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(date)) { process.stderr.write('gstack-owner-brief: --date needs YYYY-MM-DD\n'); return EXIT.usage; } + const result = closeDraftDirectionsFile(plan, loaded.inputs, date); + if (json) process.stdout.write(JSON.stringify({ plan, rewrites: result.rewrites, unresolved: result.unresolved.length }) + '\n'); + else { + for (const r of result.rewrites) process.stdout.write(`DRAFT_DIRECTION: line ${r.line} ${r.id ?? '(no id)'} ${r.status}\n`); + process.stdout.write(`DRAFT_DIRECTIONS: rewritten=${result.rewrites.length - result.unresolved.length} unresolved=${result.unresolved.length} plan=${plan}\n`); + } + for (const r of result.unresolved) process.stderr.write(resultLine('gstack-owner-brief close', 'DRAFT_DIRECTION_UNRESOLVED', `line ${r.line}: ${r.status === 'unknown_id' ? 'no decision id on the line and no plan approval item' : `${r.id} pending`}`) + '\n'); + return result.unresolved.length ? EXIT.fail : EXIT.ok; + } + process.stderr.write(`gstack-owner-brief: unknown command ${cmd}\n${HELP}\n`); + return EXIT.usage; +} + +process.exitCode = main(process.argv.slice(2)); diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 34653bcbb..e84f7cd36 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -54,6 +54,7 @@ gstack/ ├── review/ # PR review skill (checklist.md is hand-written; design-checklist.md is GENERATED from lib/design-catalog.ts) ├── deslop-shared-libs/ # Recommendations-only audit for worthwhile shared-code extractions ├── test-audit/ # Report-first sweep for low-value tests (test value bar, audit mode) +├── eval-plan/ # Preregistered paid evals: prereg, arms, $0 dry run, priced pilot (bin/gstack-eval-plan) ├── plan-ceo-review/ # /plan-ceo-review skill ├── plan-eng-review/ # /plan-eng-review skill ├── autoplan/ # /autoplan skill (auto-review pipeline: CEO → design → DX → eng, eng always last) diff --git a/docs/TESTING_INTERNALS.md b/docs/TESTING_INTERNALS.md index b63047d5e..ea4f9e105 100644 --- a/docs/TESTING_INTERNALS.md +++ b/docs/TESTING_INTERNALS.md @@ -908,12 +908,39 @@ the CSO private-state checks), AppArmor blocks the unprivileged user namespaces Chromium's sandbox needs, and `clang` and `python3-venv` are absent (required by the Dia readiness and Python runner tests). -VMs are named `ubirun--`. Every exit path destroys the client's -own VM, and `down` fails unless the VM is confirmed gone. Nothing sweeps stale -VMs by default, because the project quota is shared with other agents' runners; -set `UBI_GC_HOURS` to a positive number (or run `gc HOURS`) to opt in. For other commands, use the runner directly: -`scripts/ubicloud/ubi-runner.sh run --setup