mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 23:14:33 +01:00
v1.91.79.0 feat(eval-plan): /eval-plan tier 1, lane-check, evidence verify, coordinator contract, owner brief, contributor-PR mode, banned terms (multi-agent wave PR E) (#3123)
* v1.91.79.0 feat(eval-plan): /eval-plan tier 1, lane-check, evidence verify, coordinator contract, owner brief, contributor-PR mode, banned terms (multi-agent wave PR E) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(spec,review): fail closed when the coordinator contract cannot be read; restore the omitted-Army sentence The spec --execute spawn captures `gstack-autoplan contract` before piping the archive, so a lane never starts without the contract; the C9 fixture installs its gstack-like checkout (bin, lib, a contract stub) at every host's global root. Review Step 5.8 keeps the pinned 'without claiming specialist coverage' wording. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
ab79e55119
commit
5d647455f2
87 files changed
+4137
-762
No files matched your search
@@ -38,8 +38,16 @@ import { skillCensus } from './helpers/skill-census';
|
||||
* New-skill ratchet: previous ceiling 1,171 + ceil(92 / 4) = 1,194
|
||||
* token-equivalents (4,776 bytes), leaving 4 bytes. Existing descriptions
|
||||
* are unchanged.
|
||||
* ref eval-plan addition on base 7552362 (2026-10-10)
|
||||
* result pre-addition aggregate 4,776 bytes; eval-plan adds 10 bytes
|
||||
* (its description is a `|` block scalar, counted as the other
|
||||
* block-scalar skills are), yielding 4,786 bytes = 1,197
|
||||
* token-equivalents including the root router alias.
|
||||
* New-skill ratchet: previous ceiling 1,194 + ceil(10 / 4) = 1,197
|
||||
* token-equivalents (4,788 bytes), leaving 2 bytes. Existing descriptions
|
||||
* are unchanged.
|
||||
*/
|
||||
const CATALOG_BUDGET_TOKEN_EQUIVALENTS = 1_194;
|
||||
const CATALOG_BUDGET_TOKEN_EQUIVALENTS = 1_197;
|
||||
|
||||
// Largest today: design-consultation at 229 bytes. A description that needs
|
||||
// more than 260 bytes is a body paragraph, not a catalog entry.
|
||||
|
||||
@@ -94,6 +94,10 @@ const MODULE_SINKS = [
|
||||
'lib/gbrain-supabase-provision.ts',
|
||||
// consent-gated engine download (install verb): receipt before the fetch, fail-closed
|
||||
'bin/gstack-design-detect.ts',
|
||||
// Multi-agent wave E3: contributor-mode asks GitHub for the PR author's
|
||||
// repository permission (`gh api .../collaborators/<user>/permission`); the
|
||||
// author login and repo slug leave the machine, sink 'contributor-mode'.
|
||||
'lib/contributor-mode.ts',
|
||||
// The Memorable bridge hook: gstack-owned code that hands each prompt to a
|
||||
// vendor CLI. hosts/ has no curl/fetch for the scanner to see, so the
|
||||
// receipt wiring is pinned here explicitly.
|
||||
|
||||
Vendored
+48
-47
@@ -1,67 +1,68 @@
|
||||
{
|
||||
"_comment": "Context-budget ratchet ceilings (~tokens). Regenerate: bun test/helpers/capture-context-budget.ts. Headroom: alwaysOnTotal x1.05, eagerPerInvocation x1.1. Graded by test/context-budget-ratchet.test.ts via lib/context-bill.ts checkBudget.",
|
||||
"alwaysOnTotal": 7283,
|
||||
"alwaysOnTotal": 7396,
|
||||
"eagerPerInvocation": {
|
||||
"autoplan": 16899,
|
||||
"benchmark": 7850,
|
||||
"autoplan": 17055,
|
||||
"benchmark": 7849,
|
||||
"benchmark-models": 4231,
|
||||
"browse": 8962,
|
||||
"browse": 8961,
|
||||
"browser-skills/hackernews-frontpage": 371,
|
||||
"canary": 13522,
|
||||
"canary": 13520,
|
||||
"careful": 1328,
|
||||
"codex": 15599,
|
||||
"context-restore": 10996,
|
||||
"context-save": 10589,
|
||||
"codex": 15598,
|
||||
"context-restore": 10995,
|
||||
"context-save": 10588,
|
||||
"cso": 4702,
|
||||
"design-consultation": 16273,
|
||||
"design-html": 14556,
|
||||
"design-review": 35821,
|
||||
"design-shotgun": 14981,
|
||||
"design-consultation": 16277,
|
||||
"design-html": 14555,
|
||||
"design-review": 35819,
|
||||
"design-shotgun": 14980,
|
||||
"deslop-shared-libs": 3448,
|
||||
"devex-review": 20206,
|
||||
"devex-review": 20204,
|
||||
"diagram": 4336,
|
||||
"document-generate": 11810,
|
||||
"document-release": 10174,
|
||||
"document-generate": 11809,
|
||||
"document-release": 10235,
|
||||
"eval-plan": 9323,
|
||||
"freeze": 1194,
|
||||
"gstack": 4267,
|
||||
"gstack": 4289,
|
||||
"gstack-upgrade": 5793,
|
||||
"guard": 1082,
|
||||
"health": 11523,
|
||||
"investigate": 12584,
|
||||
"ios-clean": 8282,
|
||||
"ios-design-review": 8478,
|
||||
"ios-fix": 8171,
|
||||
"ios-qa": 11744,
|
||||
"ios-sync": 8297,
|
||||
"land-and-deploy": 19005,
|
||||
"landing-report": 8994,
|
||||
"learn": 8709,
|
||||
"health": 11521,
|
||||
"investigate": 12583,
|
||||
"ios-clean": 8280,
|
||||
"ios-design-review": 8477,
|
||||
"ios-fix": 8170,
|
||||
"ios-qa": 11742,
|
||||
"ios-sync": 8295,
|
||||
"land-and-deploy": 19003,
|
||||
"landing-report": 8993,
|
||||
"learn": 8708,
|
||||
"make-pdf": 5348,
|
||||
"office-hours": 24915,
|
||||
"open-gstack-browser": 4928,
|
||||
"office-hours": 24914,
|
||||
"open-gstack-browser": 4981,
|
||||
"openclaw/skills/gstack-openclaw-ceo-review": 2764,
|
||||
"openclaw/skills/gstack-openclaw-investigate": 1429,
|
||||
"openclaw/skills/gstack-openclaw-office-hours": 4433,
|
||||
"openclaw/skills/gstack-openclaw-retro": 2542,
|
||||
"pair-agent": 11104,
|
||||
"plan-ceo-review": 20885,
|
||||
"plan-design-review": 22326,
|
||||
"plan-devex-review": 17600,
|
||||
"plan-eng-review": 13484,
|
||||
"plan-tune": 14775,
|
||||
"qa": 13498,
|
||||
"qa-only": 11545,
|
||||
"retro": 19034,
|
||||
"review": 21044,
|
||||
"scrape": 7355,
|
||||
"setup-browser-cookies": 3575,
|
||||
"setup-deploy": 11415,
|
||||
"setup-gbrain": 15697,
|
||||
"ship": 21164,
|
||||
"skillify": 11649,
|
||||
"spec": 14923,
|
||||
"sync-gbrain": 15478,
|
||||
"test-audit": 10335,
|
||||
"pair-agent": 11156,
|
||||
"plan-ceo-review": 20974,
|
||||
"plan-design-review": 22321,
|
||||
"plan-devex-review": 17595,
|
||||
"plan-eng-review": 13479,
|
||||
"plan-tune": 14773,
|
||||
"qa": 13497,
|
||||
"qa-only": 11543,
|
||||
"retro": 19033,
|
||||
"review": 21059,
|
||||
"scrape": 7354,
|
||||
"setup-browser-cookies": 3628,
|
||||
"setup-deploy": 11413,
|
||||
"setup-gbrain": 15696,
|
||||
"ship": 21161,
|
||||
"skillify": 11648,
|
||||
"spec": 14922,
|
||||
"sync-gbrain": 15477,
|
||||
"test-audit": 10333,
|
||||
"unfreeze": 457
|
||||
}
|
||||
}
|
||||
+60
-60
@@ -427,7 +427,7 @@ Routine authorization never waives those gates or their required user decisions.
|
||||
**Routine work needs no confirmation:** include uncommitted changes, choose MICRO/PATCH
|
||||
under Step 12, draft CHANGELOG and commits, mark completed TODOs and auto-fix findings.
|
||||
When Step 7 coverage meets its target, report remaining gaps and verify generated
|
||||
tests without another permission question. Step 15 commits those tests.
|
||||
tests without asking again. Step 15 commits those tests.
|
||||
|
||||
**Route:** integrate (1–3) → test and review (4–11.5) → prepare the release
|
||||
(12–15) → verify frozen content (16) → push and publish (17–21).
|
||||
@@ -437,10 +437,10 @@ Steps 12, 17 and 19 prevent duplicate bumps, pushes and PRs, never verification.
|
||||
### Keep state between steps
|
||||
|
||||
Keep one private Markdown **invocation record** outside the product tree and save
|
||||
its absolute path. Use these headings so a paused run can resume:
|
||||
its absolute path, with these headings so a paused run can resume:
|
||||
- **Release:** versions, `BUMP_LEVEL`, reviewed tree and attempt counts.
|
||||
- **Decisions:** each approval's finding, files and authorized action. Reuse it only
|
||||
for that same scope; a repair never resets approvals or expands them.
|
||||
for that same scope; a repair never resets or expands approvals.
|
||||
- **Reviews:** handles, original start tokens, terminal states, outputs and queued fixes.
|
||||
- **Checks:** command/label, result/counts, timestamp, log and consumed inputs.
|
||||
- **Documentation:** candidate/id, attempts used, accepted hashes or named blocked exception.
|
||||
@@ -462,18 +462,18 @@ permission to proceed. Follow the saved work list:
|
||||
1. Start with Steps 1–21 in order, including 6.5, 11.5 and 14.5. Advance only after
|
||||
the current item's gates clear.
|
||||
2. Expand a repair into individual steps and insert them before the still-pending
|
||||
work. This replaces the current item, whose actual result stays in the record.
|
||||
work. This replaces the current item, whose result stays in the record.
|
||||
Add its destination only if not already the next pending step.
|
||||
3. For another repair, repeat rule 2 without discarding pending work.
|
||||
The saved list takes precedence over ordinary next-step
|
||||
sentences inside a repair. A range never adds unlisted steps.
|
||||
The saved list outranks ordinary next-step sentences inside a repair.
|
||||
A range never adds unlisted steps.
|
||||
|
||||
**Example:** Step 11 fixes insert `9 → 10 → 11` before 11.5. A further Step 9 fix
|
||||
affecting 6–8 makes the list `5 → 6 → 7 → 8 → 9 → 10 → 11 → 11.5`.
|
||||
The unchanged release steps follow. STOP and AskUserQuestion gates still apply during repairs.
|
||||
|
||||
Keep the same attempt counts throughout the invocation. A range ending at Step 14
|
||||
does not enter Step 14.5. A range that includes Step 14.5 enters its existing audit
|
||||
does not enter Step 14.5. A range including Step 14.5 enters its existing audit
|
||||
decision, not an unconditional new launch; its initial-plus-ONE limit never resets.
|
||||
Permitted repairs continue in this invocation without restarting /ship.
|
||||
|
||||
@@ -500,6 +500,7 @@ sections. Read a section in full before doing its step; do not work from memory.
|
||||
| writing the CHANGELOG entry (Step 13) | `sections/changelog.md` |
|
||||
| auditing docs before final commit/verification (Step 14.5), on every ship | `sections/documentation.md` |
|
||||
| preparing the title and creating or updating the PR/MR with the verified documentation outcome (Steps 18-19) | `sections/pr-body.md` |
|
||||
| handing the branch to a coordinator instead of opening a PR (--no-pr or an automation lane; Step 18) | `sections/handoff.md` |
|
||||
|
||||
---
|
||||
|
||||
@@ -551,7 +552,7 @@ branch name wherever the instructions say "the base branch" or `<default>`.
|
||||
|
||||
If the ask is App Store/TestFlight distribution, look for an `.xcodeproj`,
|
||||
`.xcworkspace`, or Swift app product. Read `Package.swift` and its entrypoint to
|
||||
distinguish an app from a library/CLI. If unclear, use AskUserQuestion to identify
|
||||
distinguish an app from a library/CLI. If unclear, AskUserQuestion to identify
|
||||
the target and wait before choosing a release path.
|
||||
For a confirmed app, **STOP and Read
|
||||
`sections/apple-release.md` relative to the installed `gstack-ship` SKILL.md directory FIRST**. Store distribution proceeds
|
||||
@@ -563,10 +564,10 @@ repository-landing asks, including on Apple repos.
|
||||
|
||||
1. Save the current branch as `<branch-name>`. If on the base branch or the repo's default branch, **abort**: "You're on the base branch. Ship from a feature branch."
|
||||
|
||||
2. Run `git status` (never use `-uall`). Uncommitted changes are always included — no need to ask.
|
||||
2. Run `git status` (never `-uall`). Uncommitted changes are always included; do not ask.
|
||||
|
||||
3. Run `git fetch origin <base>` before inspecting the diff. If fetch fails, STOP:
|
||||
report the error and restore access before continuing. Then inspect
|
||||
report the error and restore access first. Then inspect
|
||||
`git diff origin/<base> --stat`, untracked files from status, and
|
||||
`git log origin/<base>..HEAD --oneline`.
|
||||
|
||||
@@ -658,7 +659,7 @@ VERDICT: {CLEARED or NOT CLEARED} — {reason}
|
||||
An outside review with status `unverified` or `unavailable` is missing coverage, never a pass.
|
||||
|
||||
For diffs >200 lines (`git diff origin/<base> --stat | tail -1`), recommend
|
||||
`/plan-eng-review` or `/autoplan` for architecture review.
|
||||
`/plan-eng-review` or `/autoplan`.
|
||||
|
||||
For Design Review: run `source <(~/.gemini/antigravity-cli/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)`. If `SCOPE_FRONTEND=true` and no design review exists, mention: "Design Review not run — Step 9 includes the lite check; consider /design-review for a full visual audit."
|
||||
|
||||
@@ -669,16 +670,16 @@ Continue to Step 2 without asking; Step 9 applies the review gates.
|
||||
## Step 2: Distribution Pipeline Check
|
||||
|
||||
Check distribution for new standalone artifacts (CLI binaries, packages, tools),
|
||||
not web services with existing deployment.
|
||||
not web services already deployed.
|
||||
|
||||
1. List candidate distribution paths:
|
||||
```bash
|
||||
git diff origin/<base> --diff-filter=A --name-only | grep -E '(^|/)(cmd/[^/]+/main\.go|bin/[^/]+|Cargo\.toml|setup\.py|package\.json)$' | head -5
|
||||
```
|
||||
Also inspect matching untracked files from Step 1's status. Read each match:
|
||||
a new `package.json` or `Cargo.toml` alone does not establish a publishable
|
||||
artifact. Also inspect existing manifests for newly declared binaries or
|
||||
package exports. Apply the pipeline gate only when a new distributable is present.
|
||||
Include matching untracked files from Step 1's status. Read each match:
|
||||
a new `package.json` or `Cargo.toml` alone is not a publishable artifact;
|
||||
also check existing manifests for newly declared binaries or package
|
||||
exports. Gate only when a new distributable is present.
|
||||
|
||||
2. If new artifact detected, check for a release workflow:
|
||||
```bash
|
||||
@@ -696,7 +697,7 @@ not web services with existing deployment.
|
||||
Ask for unknown targets, registries or access first; never invent credentials.
|
||||
Recheck against the artifact and include the workflow in tests and review.
|
||||
Do not publish a release during `/ship`.
|
||||
5. Otherwise, continue without adding a pipeline.
|
||||
5. Otherwise continue without a pipeline.
|
||||
|
||||
---
|
||||
|
||||
@@ -708,7 +709,7 @@ Merge the base ref fetched in Step 1 so tests and reviews cover the integrated c
|
||||
git merge origin/<base> --no-edit
|
||||
```
|
||||
|
||||
**If there are merge conflicts:** Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution decision, and wait for the answer before editing or continuing.
|
||||
**Merge conflicts:** Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing.
|
||||
|
||||
**If already up to date:** Continue silently.
|
||||
|
||||
@@ -746,7 +747,7 @@ repeat Step 2 (decisions included) on the merged content, then Step 4.
|
||||
1. **Select the two reviews.** Run `~/.gemini/antigravity-cli/skills/gstack/bin/gstack-review-read`.
|
||||
Select this invocation's final Step 9.4 record (`skill:"review"`, `via:"ship"`)
|
||||
and Step 11 native record (`skill:"adversarial-review"`). Match each to its saved
|
||||
handle, original token and source; reject outside-provider or older invocation records.
|
||||
handle, original token and source; reject outside-provider or older records.
|
||||
2. **Compare their content.** Require the native record's `review_binding.state`
|
||||
to be `verified`. All three snapshots must match: its `wtree`, Step 9.4's
|
||||
`review_binding.start_wtree` and `review_binding.end_wtree`. A mismatch or missing
|
||||
@@ -755,7 +756,7 @@ repeat Step 2 (decisions included) on the merged content, then Step 4.
|
||||
Never attach new tokens to old work.
|
||||
3. **Preserve any QA exception.** A named probe-risk exception may leave Step 9.4's
|
||||
root `wtree` absent; item 2 still compares its start/end snapshots. Matching content
|
||||
does not mean the failed or unrun probes passed. Keep Step 9.4's incomplete flags
|
||||
does not mean failed or unrun probes passed. Keep Step 9.4's incomplete flags
|
||||
and the user's exception.
|
||||
4. **Save the evidence.** Save both records and matching **reviewed tree** for
|
||||
Step 16. Continue to Step 12.
|
||||
@@ -792,9 +793,8 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
- **MICRO**: <50 lines, trivial tweaks/config. **PATCH**: 50+ lines, no feature signal.
|
||||
- **MINOR**: ask for any feature signal (new route/page, migration, module) or 500+ lines.
|
||||
**MAJOR**: ask for milestones or breaking changes (`ship: bump patch` never asks).
|
||||
Use AskUserQuestion: recommended
|
||||
level with rationale, smaller level, or cancel. Wait; cancel stops before release
|
||||
writes or push and preserves existing work.
|
||||
AskUserQuestion: recommended level with rationale, smaller level, or cancel.
|
||||
Wait; cancel stops before release writes or push and preserves existing work.
|
||||
Save lowercase `BUMP_LEVEL`. A claimed version may move the next available number
|
||||
forward, but cannot change the chosen MICRO/PATCH/MINOR/MAJOR level.
|
||||
|
||||
@@ -806,7 +806,7 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
**Qualify first:** require successful utility output and a nonempty valid version.
|
||||
`offline:false` qualifies; `offline:true` qualifies only with `fallback:"git"`.
|
||||
Offline output without that fallback, failure, malformed output or an empty version
|
||||
is unusable, even if it contains a version-looking string.
|
||||
is unusable, even when it contains a version-looking string.
|
||||
|
||||
- **Usable candidate:** print warnings and claimed queue. FRESH sets `NEW_VERSION=CANDIDATE_VERSION`.
|
||||
ALREADY_BUMPED compares it with `currentVersion`: if different, ask to rebump
|
||||
@@ -821,7 +821,7 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
```bash
|
||||
bun run ~/.gemini/antigravity-cli/skills/gstack/bin/gstack-version-bump write --version "$NEW_VERSION" --regen-digest
|
||||
```
|
||||
The CLI validates `MAJOR.MINOR.PATCH.MICRO` (or pinned 3-digit semver) and writes
|
||||
The CLI validates `MAJOR.MINOR.PATCH.MICRO` (or pinned 3-digit semver), writes
|
||||
VERSION, the manifest (`--package-json-path` → `.gstack/package-json-path` →
|
||||
`./package.json`) and existing npm lockfiles, never creating one, with the 3-digit
|
||||
translation (`1.67.0.0` → `1.67.0`); VERSION is authoritative. Exit 3 is a half-write:
|
||||
@@ -837,7 +837,7 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
```bash
|
||||
~/.gemini/antigravity-cli/skills/gstack/bin/gstack-decision-log '{"decision":"Ship NEW_VERSION (BUMP_LEVEL)","rationale":"WHY","scope":"repo","source":"skill","confidence":9}' 2>/dev/null || true
|
||||
```
|
||||
Substitute `NEW_VERSION`, `BUMP_LEVEL` and a one-line `WHY` (scope or breaking-change signal); best-effort, non-blocking.
|
||||
Substitute `NEW_VERSION`, `BUMP_LEVEL` and a one-line `WHY` (scope or breaking-change signal); best-effort.
|
||||
|
||||
> **STOP.** Before writing the CHANGELOG entry (Step 13), Read `sections/changelog.md` relative to the installed `gstack-ship` SKILL.md directory and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
@@ -858,7 +858,7 @@ at the bottom. If disorganized, ask: A) Reorganize preserving all content
|
||||
- Step 2: add the approved distribution follow-up as P1 with the missing pipeline and affected artifact.
|
||||
- Step 8: add each approved P1 plan deferral with `Deferred from plan: {plan file path}` and the missing work.
|
||||
- Step 5: retain P0 test-failure entries already written; deduplicate by failure and source, adding missing approved entries with error output and branch.
|
||||
Never turn dropped scope into TODOs or invent unapproved follow-ups. Reuse matching existing entries rather than duplicating them.
|
||||
Never turn dropped scope into TODOs or invent unapproved follow-ups. Reuse matching entries instead of duplicating them.
|
||||
|
||||
**4. Detect completed TODOs:** Compare titles, files and behavior with
|
||||
`git diff origin/<base>`, untracked files and `git log origin/<base>..HEAD --oneline`.
|
||||
@@ -866,9 +866,9 @@ Move proven completions to `## Completed` with `**Completed:** vX.Y.Z (YYYY-MM-D
|
||||
leave uncertain items open.
|
||||
|
||||
**5. Save the summary:** Report additions, deferrals, completions, remaining count and
|
||||
creation/reorganization. If creation was declined or a write failed, warn and retain
|
||||
unsaved follow-ups in Step 19's PR summary. Never claim they were saved;
|
||||
TODO write failures are non-blocking.
|
||||
creation/reorganization. If creation was declined or a write failed, warn and carry
|
||||
unsaved follow-ups in Step 19's PR summary; never claim they were saved. TODO
|
||||
write failures are non-blocking.
|
||||
|
||||
---
|
||||
|
||||
@@ -887,9 +887,9 @@ Make bisectable commits; if already committed, continue to Step 16. Never create
|
||||
|
||||
1. Group changes with their tests, config/routes, views and Step 14.5 docs.
|
||||
Migrations may stand alone or accompany their model.
|
||||
Under 50 lines across fewer than 4 files may use one commit.
|
||||
Under 50 lines across fewer than 4 files may be one commit.
|
||||
2. Order dependencies first: infrastructure → models/services → controllers/views.
|
||||
Each commit must work independently, without broken imports or missing code.
|
||||
Each commit must work on its own, without broken imports or missing code.
|
||||
Group VERSION + CHANGELOG + TODOS.md after the feature commits.
|
||||
3. Use `<type>: <summary>` (feat/fix/chore/refactor/docs) and a brief body.
|
||||
Only the final VERSION/CHANGELOG commit gets the release version and co-author
|
||||
@@ -912,7 +912,7 @@ EOF
|
||||
|
||||
Run stages 1–5 in order; recovery notes below name where to resume. Content
|
||||
changes during or after verification restart at stage 1 and complete all five
|
||||
stages before Step 17. Content-preserving commits keep valid evidence.
|
||||
stages before Step 17. Content-preserving commits keep evidence valid.
|
||||
|
||||
### 1. Finish writers and prepare outputs
|
||||
|
||||
@@ -925,7 +925,7 @@ files and CI. Run them and save results. If none exists, record not applicable a
|
||||
the inspected sources. A missing prerequisite or failed build stops shipping:
|
||||
report **Build failed or prerequisite missing**, with the command, error and needed
|
||||
repair. Never invent a substitute command.
|
||||
**If blocked:** Repair the prerequisite or build, then repeat stage 1. After it passes, continue
|
||||
**If blocked:** Repair it, then repeat stage 1. After it passes, continue
|
||||
to stage 2; treat any content repair as a behavioral change there.
|
||||
|
||||
### 2. Choose the change route
|
||||
@@ -965,8 +965,7 @@ Report changed inputs, blockers and attempts used:
|
||||
|
||||
- **An attempt remains, with changed inputs or an available repair:** insert
|
||||
`14.5 → 15 → 16` before Step 17. Use Blocked recovery with the existing count.
|
||||
Validate the outcome before Step 15,
|
||||
then restart Step 16 stage 1 to regenerate and compare again.
|
||||
Validate the outcome before Step 15, then restart Step 16 stage 1.
|
||||
- **Otherwise:** STOP unless the user accepts
|
||||
the specific named documentation risk and all unwaivable gates clear, under
|
||||
Step 14.5's Blocked recovery rules. Unchanged approved content goes to stage 4;
|
||||
@@ -977,7 +976,7 @@ Never run a third audit. Child return is not acceptance.
|
||||
### 4. Verify the frozen candidate
|
||||
|
||||
Freeze inputs through verification and push. Run declared docs/link/generated-file
|
||||
checks; report unavailable checks.
|
||||
checks; report any that are unavailable.
|
||||
|
||||
**Reuse a check when its inputs match.** Compare hashes or complete bytes of its
|
||||
saved and current consumed files, fixtures, dependencies and execution parameters.
|
||||
@@ -990,8 +989,8 @@ command, result/counts, timestamp and log, never resample. Mandatory reviews sti
|
||||
`--allow-paths` exempts only release metadata. A `package.json` version-only edit
|
||||
can qualify; scripts, dependencies and runtime configuration require live tests.
|
||||
Uncertain edits cannot be exempted. Docs, TODO edits, new/generated tests and fixes
|
||||
make evidence STALE even without a new code review. Use this example only after
|
||||
confirming that every allowed edit is release metadata:
|
||||
make evidence STALE even without a new code review. Use this example only once
|
||||
every allowed edit is release metadata:
|
||||
|
||||
```bash
|
||||
~/.gemini/antigravity-cli/skills/gstack/bin/gstack-evidence check --label tests --expect-cmd '<tests>' --label vitest --expect-cmd '<vitest>' --max-age 24 --allow-paths CHANGELOG.md,VERSION,package.json,agents-digest/gstack-AGENTS.md
|
||||
@@ -1017,7 +1016,7 @@ keeps its existing audit count; it does not authorize a third attempt.
|
||||
### 5. Report, then push
|
||||
|
||||
Commit only approved, verified release changes left uncommitted after Step 15,
|
||||
including generated outputs; use its grouping rules and never create an empty commit.
|
||||
including generated outputs; use its grouping rules; never create an empty commit.
|
||||
Preserve unrelated user files.
|
||||
|
||||
Paste build/docs/test results. Reuse waivers only for the same verified
|
||||
@@ -1031,8 +1030,8 @@ its records:
|
||||
`reason` and logger-stamped `actor`. If there are none, the coverage line
|
||||
(`none detected (...)`, `partial (...)` or `UNAVAILABLE — ...`).
|
||||
- Each test lane's `tests_ran` (N, ZERO-RUN or `count unavailable`).
|
||||
Write this report in the auditor's posture: describe this session's own gate edits
|
||||
as evidence for the reader to examine, not as positions to defend; a kept gate
|
||||
Write this report in the auditor's posture: this session's own gate edits are
|
||||
evidence for the reader to examine, not positions to defend; a kept gate
|
||||
states its reason and nothing more. Otherwise continue to Step 17.
|
||||
|
||||
---
|
||||
@@ -1079,7 +1078,7 @@ Branch on the echoed values:
|
||||
`pre-push.local`; otherwise request manual integration. Dangling
|
||||
symlinks are unmanaged. Never overwrite either policy.
|
||||
2. **`REDACT_PREPUSH` not true AND `PREPUSH_PROMPTED: no`** — one-time
|
||||
offer (fires once EVER, machine-wide). AskUserQuestion:
|
||||
offer (once ever, machine-wide). AskUserQuestion:
|
||||
|
||||
> gstack can install a per-repo git pre-push hook that blocks pushes
|
||||
> containing credentials (API keys, tokens, private keys). It's a
|
||||
@@ -1093,16 +1092,15 @@ Branch on the echoed values:
|
||||
If A: run `~/.gemini/antigravity-cli/skills/gstack/bin/gstack-config set redact_prepush_hook true`
|
||||
then re-run the block and apply the same directory and unmanaged-hook rules above.
|
||||
If B: run `~/.gemini/antigravity-cli/skills/gstack/bin/gstack-config set redact_prepush_hook false`.
|
||||
ALWAYS (after either answer, but NOT if the question itself failed to
|
||||
render — a failed AskUserQuestion must re-offer next time):
|
||||
ALWAYS after either answer (never after a failed question; it must re-offer
|
||||
next time):
|
||||
```bash
|
||||
GSTACK_STATE_ROOT=$(~/.gemini/antigravity-cli/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
|
||||
touch "$GSTACK_STATE_ROOT/.redact-prepush-prompted"
|
||||
```
|
||||
3. **Declined earlier** — continue
|
||||
without comment.
|
||||
3. **Declined earlier** — continue without comment.
|
||||
|
||||
**Open gate findings against an existing ready PR/MR:** when Step 9 left any gate
|
||||
**Open gate findings against an existing ready PR/MR:** when Step 9 left a gate
|
||||
finding `open`, look up an open PR/MR for `<branch-name>` before pushing
|
||||
(`gh pr list --head <branch-name> --state open --json number,isDraft`; GitLab
|
||||
`glab mr list --source-branch <branch-name> --output json`, reading `draft`). A lookup
|
||||
@@ -1116,7 +1114,7 @@ failure STOPs here. If one exists and is not a draft:
|
||||
the `Gate edits: N open` banner. Record B in the invocation record as a
|
||||
human-authorised publication; Step 19 reads it. Never convert a ready PR to a draft.
|
||||
|
||||
A new PR, or an existing draft, needs no question: Step 19 creates or keeps the draft.
|
||||
A new PR or an existing draft needs no question: Step 19 creates or keeps the draft.
|
||||
|
||||
**Idempotency check:** Check if the branch is already pushed and up to date.
|
||||
|
||||
@@ -1132,7 +1130,7 @@ echo "LOCAL: $LOCAL REMOTE: $REMOTE"
|
||||
[ "$LOCAL" = "$REMOTE" ] && echo "ALREADY_PUSHED" || echo "PUSH_NEEDED"
|
||||
```
|
||||
|
||||
If `ALREADY_PUSHED`, skip the push but continue to Step 18. Otherwise push with upstream tracking:
|
||||
`ALREADY_PUSHED`: skip the push, continue to Step 18. Otherwise push with upstream tracking:
|
||||
|
||||
```bash
|
||||
git push -u origin <branch-name>
|
||||
@@ -1142,9 +1140,8 @@ git push -u origin <branch-name>
|
||||
- **Non-fast-forward push:** fetch and inspect the remote, then merge under Step 3's
|
||||
conflict rules. Run Steps 5–16 before returning to Step 17. Never rewrite history.
|
||||
- **Authentication, hook or network failure:** repair the cause, then repeat Step 16
|
||||
even if content is unchanged before returning to Step 17. Never bypass failed guards.
|
||||
Never force-push.
|
||||
Only a successful push or verified `ALREADY_PUSHED` proceeds.
|
||||
even with unchanged content before returning to Step 17. Never bypass failed guards.
|
||||
Never force-push. Only a successful push or verified `ALREADY_PUSHED` proceeds.
|
||||
|
||||
Continue to Step 18. No documentation writer runs after push.
|
||||
|
||||
@@ -1157,19 +1154,23 @@ First look up open PRs/MRs for `<branch-name>` on the detected platform:
|
||||
- GitHub: `gh pr list --head <branch-name> --state open --json number,title,url`
|
||||
- GitLab: `glab mr list --source-branch <branch-name> --output json` (defaults to open).
|
||||
|
||||
A successful empty array means new; one match supplies the existing title/identity.
|
||||
An empty array means new; one match supplies the existing title/identity.
|
||||
Lookup failure or ambiguous matches **STOP** for resolution, never mean no PR.
|
||||
Save the result for Step 19's recheck.
|
||||
|
||||
The pr-body section prepares the title from that result, then publishes.
|
||||
The pr-body section prepares the title from that result, then publishes;
|
||||
`--no-pr` or an automation lane takes the handoff section.
|
||||
|
||||
> **STOP.** Before preparing the title and creating or updating the PR/MR with the verified documentation outcome (Steps 18-19), Read `sections/pr-body.md` relative to the installed `gstack-ship` SKILL.md directory and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
|
||||
> **STOP.** Before handing the branch to a coordinator instead of opening a PR (--no-pr or an automation lane; Step 18), Read `sections/handoff.md` relative to the installed `gstack-ship` SKILL.md directory and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
|
||||
## Step 20: Persist ship metrics
|
||||
|
||||
Log metrics for `/retro` through `gstack-review-log`; it handles project/branch paths,
|
||||
JSON validation, storage and sync. It takes **no path argument**; do not build one.
|
||||
JSON validation, storage and sync and takes **no path argument**.
|
||||
|
||||
```bash
|
||||
~/.gemini/antigravity-cli/skills/gstack/bin/gstack-review-log '{"skill":"ship","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","coverage_pct":COVERAGE_PCT,"coverage_schema":2,"coverage_pct_value":COVERAGE_PCT_VALUE,"weak_gaps":WEAK_GAPS,"tests_extended":TESTS_EXTENDED,"tests_rejected":TESTS_REJECTED,"regression_proof":REGRESSION_PROOF,"plan_items_total":PLAN_TOTAL,"plan_items_done":PLAN_DONE,"verification_result":"VERIFY_RESULT","version":"VERSION","branch":"'"$(git rev-parse --abbrev-ref HEAD)"'"}'
|
||||
@@ -1185,7 +1186,7 @@ Substitute from earlier steps:
|
||||
- **VERIFY_RESULT**: "pass", "fail", or "skipped", set after Step 9 executes Step 8.1's verification list
|
||||
- **VERSION**: `NEW_VERSION` from Step 12; `null` (unquoted) under NO_VERSION
|
||||
|
||||
The shell supplies the branch. Run this automatically, without confirmation.
|
||||
The shell supplies the branch. Run it without confirmation.
|
||||
|
||||
---
|
||||
|
||||
@@ -1206,15 +1207,14 @@ if [ ! -f "$_NUDGE_MARKER" ] && [ "$_QT" = "false" ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
The marker or enabled question_tuning suppresses it. To re-enable, remove
|
||||
`$GSTACK_STATE_ROOT/.plan-tune-nudge-shown` before the next ship.
|
||||
The marker or enabled question_tuning suppresses it; remove the marker to re-enable.
|
||||
|
||||
---
|
||||
|
||||
## Section self-check (before you finish)
|
||||
|
||||
List the applicable Section index entries and confirm each Read. If you worked from
|
||||
memory, STOP, Read the section and redo that step. Use `gstack-version-bump`, never
|
||||
memory, STOP, Read the section and redo that step. Use `gstack-version-bump`; never
|
||||
hand-roll VERSION/package.json writes.
|
||||
|
||||
---
|
||||
|
||||
+60
-60
@@ -432,7 +432,7 @@ Routine authorization never waives those gates or their required user decisions.
|
||||
**Routine work needs no confirmation:** include uncommitted changes, choose MICRO/PATCH
|
||||
under Step 12, draft CHANGELOG and commits, mark completed TODOs and auto-fix findings.
|
||||
When Step 7 coverage meets its target, report remaining gaps and verify generated
|
||||
tests without another permission question. Step 15 commits those tests.
|
||||
tests without asking again. Step 15 commits those tests.
|
||||
|
||||
**Route:** integrate (1–3) → test and review (4–11.5) → prepare the release
|
||||
(12–15) → verify frozen content (16) → push and publish (17–21).
|
||||
@@ -442,10 +442,10 @@ Steps 12, 17 and 19 prevent duplicate bumps, pushes and PRs, never verification.
|
||||
### Keep state between steps
|
||||
|
||||
Keep one private Markdown **invocation record** outside the product tree and save
|
||||
its absolute path. Use these headings so a paused run can resume:
|
||||
its absolute path, with these headings so a paused run can resume:
|
||||
- **Release:** versions, `BUMP_LEVEL`, reviewed tree and attempt counts.
|
||||
- **Decisions:** each approval's finding, files and authorized action. Reuse it only
|
||||
for that same scope; a repair never resets approvals or expands them.
|
||||
for that same scope; a repair never resets or expands approvals.
|
||||
- **Reviews:** handles, original start tokens, terminal states, outputs and queued fixes.
|
||||
- **Checks:** command/label, result/counts, timestamp, log and consumed inputs.
|
||||
- **Documentation:** candidate/id, attempts used, accepted hashes or named blocked exception.
|
||||
@@ -467,18 +467,18 @@ permission to proceed. Follow the saved work list:
|
||||
1. Start with Steps 1–21 in order, including 6.5, 11.5 and 14.5. Advance only after
|
||||
the current item's gates clear.
|
||||
2. Expand a repair into individual steps and insert them before the still-pending
|
||||
work. This replaces the current item, whose actual result stays in the record.
|
||||
work. This replaces the current item, whose result stays in the record.
|
||||
Add its destination only if not already the next pending step.
|
||||
3. For another repair, repeat rule 2 without discarding pending work.
|
||||
The saved list takes precedence over ordinary next-step
|
||||
sentences inside a repair. A range never adds unlisted steps.
|
||||
The saved list outranks ordinary next-step sentences inside a repair.
|
||||
A range never adds unlisted steps.
|
||||
|
||||
**Example:** Step 11 fixes insert `9 → 10 → 11` before 11.5. A further Step 9 fix
|
||||
affecting 6–8 makes the list `5 → 6 → 7 → 8 → 9 → 10 → 11 → 11.5`.
|
||||
The unchanged release steps follow. STOP and AskUserQuestion gates still apply during repairs.
|
||||
|
||||
Keep the same attempt counts throughout the invocation. A range ending at Step 14
|
||||
does not enter Step 14.5. A range that includes Step 14.5 enters its existing audit
|
||||
does not enter Step 14.5. A range including Step 14.5 enters its existing audit
|
||||
decision, not an unconditional new launch; its initial-plus-ONE limit never resets.
|
||||
Permitted repairs continue in this invocation without restarting /ship.
|
||||
|
||||
@@ -505,6 +505,7 @@ sections. Read a section in full before doing its step; do not work from memory.
|
||||
| writing the CHANGELOG entry (Step 13) | `sections/changelog.md` |
|
||||
| auditing docs before final commit/verification (Step 14.5), on every ship | `sections/documentation.md` |
|
||||
| preparing the title and creating or updating the PR/MR with the verified documentation outcome (Steps 18-19) | `sections/pr-body.md` |
|
||||
| handing the branch to a coordinator instead of opening a PR (--no-pr or an automation lane; Step 18) | `sections/handoff.md` |
|
||||
|
||||
---
|
||||
|
||||
@@ -556,7 +557,7 @@ branch name wherever the instructions say "the base branch" or `<default>`.
|
||||
|
||||
If the ask is App Store/TestFlight distribution, look for an `.xcodeproj`,
|
||||
`.xcworkspace`, or Swift app product. Read `Package.swift` and its entrypoint to
|
||||
distinguish an app from a library/CLI. If unclear, use AskUserQuestion to identify
|
||||
distinguish an app from a library/CLI. If unclear, AskUserQuestion to identify
|
||||
the target and wait before choosing a release path.
|
||||
For a confirmed app, **STOP and Read
|
||||
`~/.claude/skills/gstack/ship/sections/apple-release.md` FIRST**. Store distribution proceeds
|
||||
@@ -568,10 +569,10 @@ repository-landing asks, including on Apple repos.
|
||||
|
||||
1. Save the current branch as `<branch-name>`. If on the base branch or the repo's default branch, **abort**: "You're on the base branch. Ship from a feature branch."
|
||||
|
||||
2. Run `git status` (never use `-uall`). Uncommitted changes are always included — no need to ask.
|
||||
2. Run `git status` (never `-uall`). Uncommitted changes are always included; do not ask.
|
||||
|
||||
3. Run `git fetch origin <base>` before inspecting the diff. If fetch fails, STOP:
|
||||
report the error and restore access before continuing. Then inspect
|
||||
report the error and restore access first. Then inspect
|
||||
`git diff origin/<base> --stat`, untracked files from status, and
|
||||
`git log origin/<base>..HEAD --oneline`.
|
||||
|
||||
@@ -663,7 +664,7 @@ VERDICT: {CLEARED or NOT CLEARED} — {reason}
|
||||
An outside review with status `unverified` or `unavailable` is missing coverage, never a pass.
|
||||
|
||||
For diffs >200 lines (`git diff origin/<base> --stat | tail -1`), recommend
|
||||
`/plan-eng-review` or `/autoplan` for architecture review.
|
||||
`/plan-eng-review` or `/autoplan`.
|
||||
|
||||
For Design Review: run `source <(~/.claude/skills/gstack/bin/gstack-diff-scope <base> 2>/dev/null)`. If `SCOPE_FRONTEND=true` and no design review exists, mention: "Design Review not run — Step 9 includes the lite check; consider /design-review for a full visual audit."
|
||||
|
||||
@@ -674,16 +675,16 @@ Continue to Step 2 without asking; Step 9 applies the review gates.
|
||||
## Step 2: Distribution Pipeline Check
|
||||
|
||||
Check distribution for new standalone artifacts (CLI binaries, packages, tools),
|
||||
not web services with existing deployment.
|
||||
not web services already deployed.
|
||||
|
||||
1. List candidate distribution paths:
|
||||
```bash
|
||||
git diff origin/<base> --diff-filter=A --name-only | grep -E '(^|/)(cmd/[^/]+/main\.go|bin/[^/]+|Cargo\.toml|setup\.py|package\.json)$' | head -5
|
||||
```
|
||||
Also inspect matching untracked files from Step 1's status. Read each match:
|
||||
a new `package.json` or `Cargo.toml` alone does not establish a publishable
|
||||
artifact. Also inspect existing manifests for newly declared binaries or
|
||||
package exports. Apply the pipeline gate only when a new distributable is present.
|
||||
Include matching untracked files from Step 1's status. Read each match:
|
||||
a new `package.json` or `Cargo.toml` alone is not a publishable artifact;
|
||||
also check existing manifests for newly declared binaries or package
|
||||
exports. Gate only when a new distributable is present.
|
||||
|
||||
2. If new artifact detected, check for a release workflow:
|
||||
```bash
|
||||
@@ -701,7 +702,7 @@ not web services with existing deployment.
|
||||
Ask for unknown targets, registries or access first; never invent credentials.
|
||||
Recheck against the artifact and include the workflow in tests and review.
|
||||
Do not publish a release during `/ship`.
|
||||
5. Otherwise, continue without adding a pipeline.
|
||||
5. Otherwise continue without a pipeline.
|
||||
|
||||
---
|
||||
|
||||
@@ -713,7 +714,7 @@ Merge the base ref fetched in Step 1 so tests and reviews cover the integrated c
|
||||
git merge origin/<base> --no-edit
|
||||
```
|
||||
|
||||
**If there are merge conflicts:** Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution decision, and wait for the answer before editing or continuing.
|
||||
**Merge conflicts:** Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing.
|
||||
|
||||
**If already up to date:** Continue silently.
|
||||
|
||||
@@ -751,7 +752,7 @@ repeat Step 2 (decisions included) on the merged content, then Step 4.
|
||||
1. **Select the two reviews.** Run `~/.claude/skills/gstack/bin/gstack-review-read`.
|
||||
Select this invocation's final Step 9.4 record (`skill:"review"`, `via:"ship"`)
|
||||
and Step 11 native record (`skill:"adversarial-review"`). Match each to its saved
|
||||
handle, original token and source; reject outside-provider or older invocation records.
|
||||
handle, original token and source; reject outside-provider or older records.
|
||||
2. **Compare their content.** Require the native record's `review_binding.state`
|
||||
to be `verified`. All three snapshots must match: its `wtree`, Step 9.4's
|
||||
`review_binding.start_wtree` and `review_binding.end_wtree`. A mismatch or missing
|
||||
@@ -760,7 +761,7 @@ repeat Step 2 (decisions included) on the merged content, then Step 4.
|
||||
Never attach new tokens to old work.
|
||||
3. **Preserve any QA exception.** A named probe-risk exception may leave Step 9.4's
|
||||
root `wtree` absent; item 2 still compares its start/end snapshots. Matching content
|
||||
does not mean the failed or unrun probes passed. Keep Step 9.4's incomplete flags
|
||||
does not mean failed or unrun probes passed. Keep Step 9.4's incomplete flags
|
||||
and the user's exception.
|
||||
4. **Save the evidence.** Save both records and matching **reviewed tree** for
|
||||
Step 16. Continue to Step 12.
|
||||
@@ -797,9 +798,8 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
- **MICRO**: <50 lines, trivial tweaks/config. **PATCH**: 50+ lines, no feature signal.
|
||||
- **MINOR**: ask for any feature signal (new route/page, migration, module) or 500+ lines.
|
||||
**MAJOR**: ask for milestones or breaking changes (`ship: bump patch` never asks).
|
||||
Use AskUserQuestion: recommended
|
||||
level with rationale, smaller level, or cancel. Wait; cancel stops before release
|
||||
writes or push and preserves existing work.
|
||||
AskUserQuestion: recommended level with rationale, smaller level, or cancel.
|
||||
Wait; cancel stops before release writes or push and preserves existing work.
|
||||
Save lowercase `BUMP_LEVEL`. A claimed version may move the next available number
|
||||
forward, but cannot change the chosen MICRO/PATCH/MINOR/MAJOR level.
|
||||
|
||||
@@ -811,7 +811,7 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
**Qualify first:** require successful utility output and a nonempty valid version.
|
||||
`offline:false` qualifies; `offline:true` qualifies only with `fallback:"git"`.
|
||||
Offline output without that fallback, failure, malformed output or an empty version
|
||||
is unusable, even if it contains a version-looking string.
|
||||
is unusable, even when it contains a version-looking string.
|
||||
|
||||
- **Usable candidate:** print warnings and claimed queue. FRESH sets `NEW_VERSION=CANDIDATE_VERSION`.
|
||||
ALREADY_BUMPED compares it with `currentVersion`: if different, ask to rebump
|
||||
@@ -826,7 +826,7 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
```bash
|
||||
bun run ~/.claude/skills/gstack/bin/gstack-version-bump write --version "$NEW_VERSION" --regen-digest
|
||||
```
|
||||
The CLI validates `MAJOR.MINOR.PATCH.MICRO` (or pinned 3-digit semver) and writes
|
||||
The CLI validates `MAJOR.MINOR.PATCH.MICRO` (or pinned 3-digit semver), writes
|
||||
VERSION, the manifest (`--package-json-path` → `.gstack/package-json-path` →
|
||||
`./package.json`) and existing npm lockfiles, never creating one, with the 3-digit
|
||||
translation (`1.67.0.0` → `1.67.0`); VERSION is authoritative. Exit 3 is a half-write:
|
||||
@@ -842,7 +842,7 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-decision-log '{"decision":"Ship NEW_VERSION (BUMP_LEVEL)","rationale":"WHY","scope":"repo","source":"skill","confidence":9}' 2>/dev/null || true
|
||||
```
|
||||
Substitute `NEW_VERSION`, `BUMP_LEVEL` and a one-line `WHY` (scope or breaking-change signal); best-effort, non-blocking.
|
||||
Substitute `NEW_VERSION`, `BUMP_LEVEL` and a one-line `WHY` (scope or breaking-change signal); best-effort.
|
||||
|
||||
> **STOP.** Before writing the CHANGELOG entry (Step 13), Read `~/.claude/skills/gstack/ship/sections/changelog.md` and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
@@ -863,7 +863,7 @@ at the bottom. If disorganized, ask: A) Reorganize preserving all content
|
||||
- Step 2: add the approved distribution follow-up as P1 with the missing pipeline and affected artifact.
|
||||
- Step 8: add each approved P1 plan deferral with `Deferred from plan: {plan file path}` and the missing work.
|
||||
- Step 5: retain P0 test-failure entries already written; deduplicate by failure and source, adding missing approved entries with error output and branch.
|
||||
Never turn dropped scope into TODOs or invent unapproved follow-ups. Reuse matching existing entries rather than duplicating them.
|
||||
Never turn dropped scope into TODOs or invent unapproved follow-ups. Reuse matching entries instead of duplicating them.
|
||||
|
||||
**4. Detect completed TODOs:** Compare titles, files and behavior with
|
||||
`git diff origin/<base>`, untracked files and `git log origin/<base>..HEAD --oneline`.
|
||||
@@ -871,9 +871,9 @@ Move proven completions to `## Completed` with `**Completed:** vX.Y.Z (YYYY-MM-D
|
||||
leave uncertain items open.
|
||||
|
||||
**5. Save the summary:** Report additions, deferrals, completions, remaining count and
|
||||
creation/reorganization. If creation was declined or a write failed, warn and retain
|
||||
unsaved follow-ups in Step 19's PR summary. Never claim they were saved;
|
||||
TODO write failures are non-blocking.
|
||||
creation/reorganization. If creation was declined or a write failed, warn and carry
|
||||
unsaved follow-ups in Step 19's PR summary; never claim they were saved. TODO
|
||||
write failures are non-blocking.
|
||||
|
||||
---
|
||||
|
||||
@@ -892,9 +892,9 @@ Make bisectable commits; if already committed, continue to Step 16. Never create
|
||||
|
||||
1. Group changes with their tests, config/routes, views and Step 14.5 docs.
|
||||
Migrations may stand alone or accompany their model.
|
||||
Under 50 lines across fewer than 4 files may use one commit.
|
||||
Under 50 lines across fewer than 4 files may be one commit.
|
||||
2. Order dependencies first: infrastructure → models/services → controllers/views.
|
||||
Each commit must work independently, without broken imports or missing code.
|
||||
Each commit must work on its own, without broken imports or missing code.
|
||||
Group VERSION + CHANGELOG + TODOS.md after the feature commits.
|
||||
3. Use `<type>: <summary>` (feat/fix/chore/refactor/docs) and a brief body.
|
||||
Only the final VERSION/CHANGELOG commit gets the release version and co-author
|
||||
@@ -917,7 +917,7 @@ EOF
|
||||
|
||||
Run stages 1–5 in order; recovery notes below name where to resume. Content
|
||||
changes during or after verification restart at stage 1 and complete all five
|
||||
stages before Step 17. Content-preserving commits keep valid evidence.
|
||||
stages before Step 17. Content-preserving commits keep evidence valid.
|
||||
|
||||
### 1. Finish writers and prepare outputs
|
||||
|
||||
@@ -930,7 +930,7 @@ files and CI. Run them and save results. If none exists, record not applicable a
|
||||
the inspected sources. A missing prerequisite or failed build stops shipping:
|
||||
report **Build failed or prerequisite missing**, with the command, error and needed
|
||||
repair. Never invent a substitute command.
|
||||
**If blocked:** Repair the prerequisite or build, then repeat stage 1. After it passes, continue
|
||||
**If blocked:** Repair it, then repeat stage 1. After it passes, continue
|
||||
to stage 2; treat any content repair as a behavioral change there.
|
||||
|
||||
### 2. Choose the change route
|
||||
@@ -970,8 +970,7 @@ Report changed inputs, blockers and attempts used:
|
||||
|
||||
- **An attempt remains, with changed inputs or an available repair:** insert
|
||||
`14.5 → 15 → 16` before Step 17. Use Blocked recovery with the existing count.
|
||||
Validate the outcome before Step 15,
|
||||
then restart Step 16 stage 1 to regenerate and compare again.
|
||||
Validate the outcome before Step 15, then restart Step 16 stage 1.
|
||||
- **Otherwise:** STOP unless the user accepts
|
||||
the specific named documentation risk and all unwaivable gates clear, under
|
||||
Step 14.5's Blocked recovery rules. Unchanged approved content goes to stage 4;
|
||||
@@ -982,7 +981,7 @@ Never run a third audit. Child return is not acceptance.
|
||||
### 4. Verify the frozen candidate
|
||||
|
||||
Freeze inputs through verification and push. Run declared docs/link/generated-file
|
||||
checks; report unavailable checks.
|
||||
checks; report any that are unavailable.
|
||||
|
||||
**Reuse a check when its inputs match.** Compare hashes or complete bytes of its
|
||||
saved and current consumed files, fixtures, dependencies and execution parameters.
|
||||
@@ -995,8 +994,8 @@ command, result/counts, timestamp and log, never resample. Mandatory reviews sti
|
||||
`--allow-paths` exempts only release metadata. A `package.json` version-only edit
|
||||
can qualify; scripts, dependencies and runtime configuration require live tests.
|
||||
Uncertain edits cannot be exempted. Docs, TODO edits, new/generated tests and fixes
|
||||
make evidence STALE even without a new code review. Use this example only after
|
||||
confirming that every allowed edit is release metadata:
|
||||
make evidence STALE even without a new code review. Use this example only once
|
||||
every allowed edit is release metadata:
|
||||
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-evidence check --label tests --expect-cmd '<tests>' --label vitest --expect-cmd '<vitest>' --max-age 24 --allow-paths CHANGELOG.md,VERSION,package.json,agents-digest/gstack-AGENTS.md
|
||||
@@ -1022,7 +1021,7 @@ keeps its existing audit count; it does not authorize a third attempt.
|
||||
### 5. Report, then push
|
||||
|
||||
Commit only approved, verified release changes left uncommitted after Step 15,
|
||||
including generated outputs; use its grouping rules and never create an empty commit.
|
||||
including generated outputs; use its grouping rules; never create an empty commit.
|
||||
Preserve unrelated user files.
|
||||
|
||||
Paste build/docs/test results. Reuse waivers only for the same verified
|
||||
@@ -1036,8 +1035,8 @@ its records:
|
||||
`reason` and logger-stamped `actor`. If there are none, the coverage line
|
||||
(`none detected (...)`, `partial (...)` or `UNAVAILABLE — ...`).
|
||||
- Each test lane's `tests_ran` (N, ZERO-RUN or `count unavailable`).
|
||||
Write this report in the auditor's posture: describe this session's own gate edits
|
||||
as evidence for the reader to examine, not as positions to defend; a kept gate
|
||||
Write this report in the auditor's posture: this session's own gate edits are
|
||||
evidence for the reader to examine, not positions to defend; a kept gate
|
||||
states its reason and nothing more. Otherwise continue to Step 17.
|
||||
|
||||
---
|
||||
@@ -1084,7 +1083,7 @@ Branch on the echoed values:
|
||||
`pre-push.local`; otherwise request manual integration. Dangling
|
||||
symlinks are unmanaged. Never overwrite either policy.
|
||||
2. **`REDACT_PREPUSH` not true AND `PREPUSH_PROMPTED: no`** — one-time
|
||||
offer (fires once EVER, machine-wide). AskUserQuestion:
|
||||
offer (once ever, machine-wide). AskUserQuestion:
|
||||
|
||||
> gstack can install a per-repo git pre-push hook that blocks pushes
|
||||
> containing credentials (API keys, tokens, private keys). It's a
|
||||
@@ -1098,16 +1097,15 @@ Branch on the echoed values:
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-config set redact_prepush_hook true`
|
||||
then re-run the block and apply the same directory and unmanaged-hook rules above.
|
||||
If B: run `~/.claude/skills/gstack/bin/gstack-config set redact_prepush_hook false`.
|
||||
ALWAYS (after either answer, but NOT if the question itself failed to
|
||||
render — a failed AskUserQuestion must re-offer next time):
|
||||
ALWAYS after either answer (never after a failed question; it must re-offer
|
||||
next time):
|
||||
```bash
|
||||
GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
|
||||
touch "$GSTACK_STATE_ROOT/.redact-prepush-prompted"
|
||||
```
|
||||
3. **Declined earlier** — continue
|
||||
without comment.
|
||||
3. **Declined earlier** — continue without comment.
|
||||
|
||||
**Open gate findings against an existing ready PR/MR:** when Step 9 left any gate
|
||||
**Open gate findings against an existing ready PR/MR:** when Step 9 left a gate
|
||||
finding `open`, look up an open PR/MR for `<branch-name>` before pushing
|
||||
(`gh pr list --head <branch-name> --state open --json number,isDraft`; GitLab
|
||||
`glab mr list --source-branch <branch-name> --output json`, reading `draft`). A lookup
|
||||
@@ -1121,7 +1119,7 @@ failure STOPs here. If one exists and is not a draft:
|
||||
the `Gate edits: N open` banner. Record B in the invocation record as a
|
||||
human-authorised publication; Step 19 reads it. Never convert a ready PR to a draft.
|
||||
|
||||
A new PR, or an existing draft, needs no question: Step 19 creates or keeps the draft.
|
||||
A new PR or an existing draft needs no question: Step 19 creates or keeps the draft.
|
||||
|
||||
**Idempotency check:** Check if the branch is already pushed and up to date.
|
||||
|
||||
@@ -1137,7 +1135,7 @@ echo "LOCAL: $LOCAL REMOTE: $REMOTE"
|
||||
[ "$LOCAL" = "$REMOTE" ] && echo "ALREADY_PUSHED" || echo "PUSH_NEEDED"
|
||||
```
|
||||
|
||||
If `ALREADY_PUSHED`, skip the push but continue to Step 18. Otherwise push with upstream tracking:
|
||||
`ALREADY_PUSHED`: skip the push, continue to Step 18. Otherwise push with upstream tracking:
|
||||
|
||||
```bash
|
||||
git push -u origin <branch-name>
|
||||
@@ -1147,9 +1145,8 @@ git push -u origin <branch-name>
|
||||
- **Non-fast-forward push:** fetch and inspect the remote, then merge under Step 3's
|
||||
conflict rules. Run Steps 5–16 before returning to Step 17. Never rewrite history.
|
||||
- **Authentication, hook or network failure:** repair the cause, then repeat Step 16
|
||||
even if content is unchanged before returning to Step 17. Never bypass failed guards.
|
||||
Never force-push.
|
||||
Only a successful push or verified `ALREADY_PUSHED` proceeds.
|
||||
even with unchanged content before returning to Step 17. Never bypass failed guards.
|
||||
Never force-push. Only a successful push or verified `ALREADY_PUSHED` proceeds.
|
||||
|
||||
Continue to Step 18. No documentation writer runs after push.
|
||||
|
||||
@@ -1162,19 +1159,23 @@ First look up open PRs/MRs for `<branch-name>` on the detected platform:
|
||||
- GitHub: `gh pr list --head <branch-name> --state open --json number,title,url`
|
||||
- GitLab: `glab mr list --source-branch <branch-name> --output json` (defaults to open).
|
||||
|
||||
A successful empty array means new; one match supplies the existing title/identity.
|
||||
An empty array means new; one match supplies the existing title/identity.
|
||||
Lookup failure or ambiguous matches **STOP** for resolution, never mean no PR.
|
||||
Save the result for Step 19's recheck.
|
||||
|
||||
The pr-body section prepares the title from that result, then publishes.
|
||||
The pr-body section prepares the title from that result, then publishes;
|
||||
`--no-pr` or an automation lane takes the handoff section.
|
||||
|
||||
> **STOP.** Before preparing the title and creating or updating the PR/MR with the verified documentation outcome (Steps 18-19), Read `~/.claude/skills/gstack/ship/sections/pr-body.md` and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
|
||||
> **STOP.** Before handing the branch to a coordinator instead of opening a PR (--no-pr or an automation lane; Step 18), Read `~/.claude/skills/gstack/ship/sections/handoff.md` and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
|
||||
## Step 20: Persist ship metrics
|
||||
|
||||
Log metrics for `/retro` through `gstack-review-log`; it handles project/branch paths,
|
||||
JSON validation, storage and sync. It takes **no path argument**; do not build one.
|
||||
JSON validation, storage and sync and takes **no path argument**.
|
||||
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-review-log '{"skill":"ship","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","coverage_pct":COVERAGE_PCT,"coverage_schema":2,"coverage_pct_value":COVERAGE_PCT_VALUE,"weak_gaps":WEAK_GAPS,"tests_extended":TESTS_EXTENDED,"tests_rejected":TESTS_REJECTED,"regression_proof":REGRESSION_PROOF,"plan_items_total":PLAN_TOTAL,"plan_items_done":PLAN_DONE,"verification_result":"VERIFY_RESULT","version":"VERSION","branch":"'"$(git rev-parse --abbrev-ref HEAD)"'"}'
|
||||
@@ -1190,7 +1191,7 @@ Substitute from earlier steps:
|
||||
- **VERIFY_RESULT**: "pass", "fail", or "skipped", set after Step 9 executes Step 8.1's verification list
|
||||
- **VERSION**: `NEW_VERSION` from Step 12; `null` (unquoted) under NO_VERSION
|
||||
|
||||
The shell supplies the branch. Run this automatically, without confirmation.
|
||||
The shell supplies the branch. Run it without confirmation.
|
||||
|
||||
---
|
||||
|
||||
@@ -1211,15 +1212,14 @@ if [ ! -f "$_NUDGE_MARKER" ] && [ "$_QT" = "false" ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
The marker or enabled question_tuning suppresses it. To re-enable, remove
|
||||
`$GSTACK_STATE_ROOT/.plan-tune-nudge-shown` before the next ship.
|
||||
The marker or enabled question_tuning suppresses it; remove the marker to re-enable.
|
||||
|
||||
---
|
||||
|
||||
## Section self-check (before you finish)
|
||||
|
||||
List the applicable Section index entries and confirm each Read. If you worked from
|
||||
memory, STOP, Read the section and redo that step. Use `gstack-version-bump`, never
|
||||
memory, STOP, Read the section and redo that step. Use `gstack-version-bump`; never
|
||||
hand-roll VERSION/package.json writes.
|
||||
|
||||
---
|
||||
|
||||
+60
-60
@@ -447,7 +447,7 @@ Routine authorization never waives those gates or their required user decisions.
|
||||
**Routine work needs no confirmation:** include uncommitted changes, choose MICRO/PATCH
|
||||
under Step 12, draft CHANGELOG and commits, mark completed TODOs and auto-fix findings.
|
||||
When Step 7 coverage meets its target, report remaining gaps and verify generated
|
||||
tests without another permission question. Step 15 commits those tests.
|
||||
tests without asking again. Step 15 commits those tests.
|
||||
|
||||
**Route:** integrate (1–3) → test and review (4–11.5) → prepare the release
|
||||
(12–15) → verify frozen content (16) → push and publish (17–21).
|
||||
@@ -457,10 +457,10 @@ Steps 12, 17 and 19 prevent duplicate bumps, pushes and PRs, never verification.
|
||||
### Keep state between steps
|
||||
|
||||
Keep one private Markdown **invocation record** outside the product tree and save
|
||||
its absolute path. Use these headings so a paused run can resume:
|
||||
its absolute path, with these headings so a paused run can resume:
|
||||
- **Release:** versions, `BUMP_LEVEL`, reviewed tree and attempt counts.
|
||||
- **Decisions:** each approval's finding, files and authorized action. Reuse it only
|
||||
for that same scope; a repair never resets approvals or expands them.
|
||||
for that same scope; a repair never resets or expands approvals.
|
||||
- **Reviews:** handles, original start tokens, terminal states, outputs and queued fixes.
|
||||
- **Checks:** command/label, result/counts, timestamp, log and consumed inputs.
|
||||
- **Documentation:** candidate/id, attempts used, accepted hashes or named blocked exception.
|
||||
@@ -482,18 +482,18 @@ permission to proceed. Follow the saved work list:
|
||||
1. Start with Steps 1–21 in order, including 6.5, 11.5 and 14.5. Advance only after
|
||||
the current item's gates clear.
|
||||
2. Expand a repair into individual steps and insert them before the still-pending
|
||||
work. This replaces the current item, whose actual result stays in the record.
|
||||
work. This replaces the current item, whose result stays in the record.
|
||||
Add its destination only if not already the next pending step.
|
||||
3. For another repair, repeat rule 2 without discarding pending work.
|
||||
The saved list takes precedence over ordinary next-step
|
||||
sentences inside a repair. A range never adds unlisted steps.
|
||||
The saved list outranks ordinary next-step sentences inside a repair.
|
||||
A range never adds unlisted steps.
|
||||
|
||||
**Example:** Step 11 fixes insert `9 → 10 → 11` before 11.5. A further Step 9 fix
|
||||
affecting 6–8 makes the list `5 → 6 → 7 → 8 → 9 → 10 → 11 → 11.5`.
|
||||
The unchanged release steps follow. STOP and AskUserQuestion gates still apply during repairs.
|
||||
|
||||
Keep the same attempt counts throughout the invocation. A range ending at Step 14
|
||||
does not enter Step 14.5. A range that includes Step 14.5 enters its existing audit
|
||||
does not enter Step 14.5. A range including Step 14.5 enters its existing audit
|
||||
decision, not an unconditional new launch; its initial-plus-ONE limit never resets.
|
||||
Permitted repairs continue in this invocation without restarting /ship.
|
||||
|
||||
@@ -520,6 +520,7 @@ sections. Read a section in full before doing its step; do not work from memory.
|
||||
| writing the CHANGELOG entry (Step 13) | `sections/changelog.md` |
|
||||
| auditing docs before final commit/verification (Step 14.5), on every ship | `sections/documentation.md` |
|
||||
| preparing the title and creating or updating the PR/MR with the verified documentation outcome (Steps 18-19) | `sections/pr-body.md` |
|
||||
| handing the branch to a coordinator instead of opening a PR (--no-pr or an automation lane; Step 18) | `sections/handoff.md` |
|
||||
|
||||
---
|
||||
|
||||
@@ -571,7 +572,7 @@ branch name wherever the instructions say "the base branch" or `<default>`.
|
||||
|
||||
If the ask is App Store/TestFlight distribution, look for an `.xcodeproj`,
|
||||
`.xcworkspace`, or Swift app product. Read `Package.swift` and its entrypoint to
|
||||
distinguish an app from a library/CLI. If unclear, use AskUserQuestion to identify
|
||||
distinguish an app from a library/CLI. If unclear, AskUserQuestion to identify
|
||||
the target and wait before choosing a release path.
|
||||
For a confirmed app, **STOP and Read
|
||||
`sections/apple-release.md` relative to the installed `gstack-ship` SKILL.md directory FIRST**. Store distribution proceeds
|
||||
@@ -583,10 +584,10 @@ repository-landing asks, including on Apple repos.
|
||||
|
||||
1. Save the current branch as `<branch-name>`. If on the base branch or the repo's default branch, **abort**: "You're on the base branch. Ship from a feature branch."
|
||||
|
||||
2. Run `git status` (never use `-uall`). Uncommitted changes are always included — no need to ask.
|
||||
2. Run `git status` (never `-uall`). Uncommitted changes are always included; do not ask.
|
||||
|
||||
3. Run `git fetch origin <base>` before inspecting the diff. If fetch fails, STOP:
|
||||
report the error and restore access before continuing. Then inspect
|
||||
report the error and restore access first. Then inspect
|
||||
`git diff origin/<base> --stat`, untracked files from status, and
|
||||
`git log origin/<base>..HEAD --oneline`.
|
||||
|
||||
@@ -679,7 +680,7 @@ VERDICT: {CLEARED or NOT CLEARED} — {reason}
|
||||
An outside review with status `unverified` or `unavailable` is missing coverage, never a pass.
|
||||
|
||||
For diffs >200 lines (`git diff origin/<base> --stat | tail -1`), recommend
|
||||
`/plan-eng-review` or `/autoplan` for architecture review.
|
||||
`/plan-eng-review` or `/autoplan`.
|
||||
|
||||
For Design Review: run `source <($GSTACK_ROOT/bin/gstack-diff-scope <base> 2>/dev/null)`. If `SCOPE_FRONTEND=true` and no design review exists, mention: "Design Review not run — Step 9 includes the lite check; consider /design-review for a full visual audit."
|
||||
|
||||
@@ -690,16 +691,16 @@ Continue to Step 2 without asking; Step 9 applies the review gates.
|
||||
## Step 2: Distribution Pipeline Check
|
||||
|
||||
Check distribution for new standalone artifacts (CLI binaries, packages, tools),
|
||||
not web services with existing deployment.
|
||||
not web services already deployed.
|
||||
|
||||
1. List candidate distribution paths:
|
||||
```bash
|
||||
git diff origin/<base> --diff-filter=A --name-only | grep -E '(^|/)(cmd/[^/]+/main\.go|bin/[^/]+|Cargo\.toml|setup\.py|package\.json)$' | head -5
|
||||
```
|
||||
Also inspect matching untracked files from Step 1's status. Read each match:
|
||||
a new `package.json` or `Cargo.toml` alone does not establish a publishable
|
||||
artifact. Also inspect existing manifests for newly declared binaries or
|
||||
package exports. Apply the pipeline gate only when a new distributable is present.
|
||||
Include matching untracked files from Step 1's status. Read each match:
|
||||
a new `package.json` or `Cargo.toml` alone is not a publishable artifact;
|
||||
also check existing manifests for newly declared binaries or package
|
||||
exports. Gate only when a new distributable is present.
|
||||
|
||||
2. If new artifact detected, check for a release workflow:
|
||||
```bash
|
||||
@@ -717,7 +718,7 @@ not web services with existing deployment.
|
||||
Ask for unknown targets, registries or access first; never invent credentials.
|
||||
Recheck against the artifact and include the workflow in tests and review.
|
||||
Do not publish a release during `/ship`.
|
||||
5. Otherwise, continue without adding a pipeline.
|
||||
5. Otherwise continue without a pipeline.
|
||||
|
||||
---
|
||||
|
||||
@@ -729,7 +730,7 @@ Merge the base ref fetched in Step 1 so tests and reviews cover the integrated c
|
||||
git merge origin/<base> --no-edit
|
||||
```
|
||||
|
||||
**If there are merge conflicts:** Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution decision, and wait for the answer before editing or continuing.
|
||||
**Merge conflicts:** Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing.
|
||||
|
||||
**If already up to date:** Continue silently.
|
||||
|
||||
@@ -767,7 +768,7 @@ repeat Step 2 (decisions included) on the merged content, then Step 4.
|
||||
1. **Select the two reviews.** Run `$GSTACK_ROOT/bin/gstack-review-read`.
|
||||
Select this invocation's final Step 9.4 record (`skill:"review"`, `via:"ship"`)
|
||||
and Step 11 native record (`skill:"adversarial-review"`). Match each to its saved
|
||||
handle, original token and source; reject outside-provider or older invocation records.
|
||||
handle, original token and source; reject outside-provider or older records.
|
||||
2. **Compare their content.** Require the native record's `review_binding.state`
|
||||
to be `verified`. All three snapshots must match: its `wtree`, Step 9.4's
|
||||
`review_binding.start_wtree` and `review_binding.end_wtree`. A mismatch or missing
|
||||
@@ -776,7 +777,7 @@ repeat Step 2 (decisions included) on the merged content, then Step 4.
|
||||
Never attach new tokens to old work.
|
||||
3. **Preserve any QA exception.** A named probe-risk exception may leave Step 9.4's
|
||||
root `wtree` absent; item 2 still compares its start/end snapshots. Matching content
|
||||
does not mean the failed or unrun probes passed. Keep Step 9.4's incomplete flags
|
||||
does not mean failed or unrun probes passed. Keep Step 9.4's incomplete flags
|
||||
and the user's exception.
|
||||
4. **Save the evidence.** Save both records and matching **reviewed tree** for
|
||||
Step 16. Continue to Step 12.
|
||||
@@ -814,9 +815,8 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
- **MICRO**: <50 lines, trivial tweaks/config. **PATCH**: 50+ lines, no feature signal.
|
||||
- **MINOR**: ask for any feature signal (new route/page, migration, module) or 500+ lines.
|
||||
**MAJOR**: ask for milestones or breaking changes (`ship: bump patch` never asks).
|
||||
Use AskUserQuestion: recommended
|
||||
level with rationale, smaller level, or cancel. Wait; cancel stops before release
|
||||
writes or push and preserves existing work.
|
||||
AskUserQuestion: recommended level with rationale, smaller level, or cancel.
|
||||
Wait; cancel stops before release writes or push and preserves existing work.
|
||||
Save lowercase `BUMP_LEVEL`. A claimed version may move the next available number
|
||||
forward, but cannot change the chosen MICRO/PATCH/MINOR/MAJOR level.
|
||||
|
||||
@@ -829,7 +829,7 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
**Qualify first:** require successful utility output and a nonempty valid version.
|
||||
`offline:false` qualifies; `offline:true` qualifies only with `fallback:"git"`.
|
||||
Offline output without that fallback, failure, malformed output or an empty version
|
||||
is unusable, even if it contains a version-looking string.
|
||||
is unusable, even when it contains a version-looking string.
|
||||
|
||||
- **Usable candidate:** print warnings and claimed queue. FRESH sets `NEW_VERSION=CANDIDATE_VERSION`.
|
||||
ALREADY_BUMPED compares it with `currentVersion`: if different, ask to rebump
|
||||
@@ -845,7 +845,7 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack;[ -d "$_r/bin" ]||_r=${CODEX_HOME:-~/.codex}/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host codex from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
|
||||
bun run $GSTACK_ROOT/bin/gstack-version-bump write --version "$NEW_VERSION" --regen-digest
|
||||
```
|
||||
The CLI validates `MAJOR.MINOR.PATCH.MICRO` (or pinned 3-digit semver) and writes
|
||||
The CLI validates `MAJOR.MINOR.PATCH.MICRO` (or pinned 3-digit semver), writes
|
||||
VERSION, the manifest (`--package-json-path` → `.gstack/package-json-path` →
|
||||
`./package.json`) and existing npm lockfiles, never creating one, with the 3-digit
|
||||
translation (`1.67.0.0` → `1.67.0`); VERSION is authoritative. Exit 3 is a half-write:
|
||||
@@ -862,7 +862,7 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack;[ -d "$_r/bin" ]||_r=${CODEX_HOME:-~/.codex}/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host codex from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
|
||||
$GSTACK_ROOT/bin/gstack-decision-log '{"decision":"Ship NEW_VERSION (BUMP_LEVEL)","rationale":"WHY","scope":"repo","source":"skill","confidence":9}' 2>/dev/null || true
|
||||
```
|
||||
Substitute `NEW_VERSION`, `BUMP_LEVEL` and a one-line `WHY` (scope or breaking-change signal); best-effort, non-blocking.
|
||||
Substitute `NEW_VERSION`, `BUMP_LEVEL` and a one-line `WHY` (scope or breaking-change signal); best-effort.
|
||||
|
||||
> **STOP.** Before writing the CHANGELOG entry (Step 13), Read `sections/changelog.md` relative to the installed `gstack-ship` SKILL.md directory and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
@@ -883,7 +883,7 @@ at the bottom. If disorganized, ask: A) Reorganize preserving all content
|
||||
- Step 2: add the approved distribution follow-up as P1 with the missing pipeline and affected artifact.
|
||||
- Step 8: add each approved P1 plan deferral with `Deferred from plan: {plan file path}` and the missing work.
|
||||
- Step 5: retain P0 test-failure entries already written; deduplicate by failure and source, adding missing approved entries with error output and branch.
|
||||
Never turn dropped scope into TODOs or invent unapproved follow-ups. Reuse matching existing entries rather than duplicating them.
|
||||
Never turn dropped scope into TODOs or invent unapproved follow-ups. Reuse matching entries instead of duplicating them.
|
||||
|
||||
**4. Detect completed TODOs:** Compare titles, files and behavior with
|
||||
`git diff origin/<base>`, untracked files and `git log origin/<base>..HEAD --oneline`.
|
||||
@@ -891,9 +891,9 @@ Move proven completions to `## Completed` with `**Completed:** vX.Y.Z (YYYY-MM-D
|
||||
leave uncertain items open.
|
||||
|
||||
**5. Save the summary:** Report additions, deferrals, completions, remaining count and
|
||||
creation/reorganization. If creation was declined or a write failed, warn and retain
|
||||
unsaved follow-ups in Step 19's PR summary. Never claim they were saved;
|
||||
TODO write failures are non-blocking.
|
||||
creation/reorganization. If creation was declined or a write failed, warn and carry
|
||||
unsaved follow-ups in Step 19's PR summary; never claim they were saved. TODO
|
||||
write failures are non-blocking.
|
||||
|
||||
---
|
||||
|
||||
@@ -912,9 +912,9 @@ Make bisectable commits; if already committed, continue to Step 16. Never create
|
||||
|
||||
1. Group changes with their tests, config/routes, views and Step 14.5 docs.
|
||||
Migrations may stand alone or accompany their model.
|
||||
Under 50 lines across fewer than 4 files may use one commit.
|
||||
Under 50 lines across fewer than 4 files may be one commit.
|
||||
2. Order dependencies first: infrastructure → models/services → controllers/views.
|
||||
Each commit must work independently, without broken imports or missing code.
|
||||
Each commit must work on its own, without broken imports or missing code.
|
||||
Group VERSION + CHANGELOG + TODOS.md after the feature commits.
|
||||
3. Use `<type>: <summary>` (feat/fix/chore/refactor/docs) and a brief body.
|
||||
Only the final VERSION/CHANGELOG commit gets the release version and co-author
|
||||
@@ -937,7 +937,7 @@ EOF
|
||||
|
||||
Run stages 1–5 in order; recovery notes below name where to resume. Content
|
||||
changes during or after verification restart at stage 1 and complete all five
|
||||
stages before Step 17. Content-preserving commits keep valid evidence.
|
||||
stages before Step 17. Content-preserving commits keep evidence valid.
|
||||
|
||||
### 1. Finish writers and prepare outputs
|
||||
|
||||
@@ -950,7 +950,7 @@ files and CI. Run them and save results. If none exists, record not applicable a
|
||||
the inspected sources. A missing prerequisite or failed build stops shipping:
|
||||
report **Build failed or prerequisite missing**, with the command, error and needed
|
||||
repair. Never invent a substitute command.
|
||||
**If blocked:** Repair the prerequisite or build, then repeat stage 1. After it passes, continue
|
||||
**If blocked:** Repair it, then repeat stage 1. After it passes, continue
|
||||
to stage 2; treat any content repair as a behavioral change there.
|
||||
|
||||
### 2. Choose the change route
|
||||
@@ -990,8 +990,7 @@ Report changed inputs, blockers and attempts used:
|
||||
|
||||
- **An attempt remains, with changed inputs or an available repair:** insert
|
||||
`14.5 → 15 → 16` before Step 17. Use Blocked recovery with the existing count.
|
||||
Validate the outcome before Step 15,
|
||||
then restart Step 16 stage 1 to regenerate and compare again.
|
||||
Validate the outcome before Step 15, then restart Step 16 stage 1.
|
||||
- **Otherwise:** STOP unless the user accepts
|
||||
the specific named documentation risk and all unwaivable gates clear, under
|
||||
Step 14.5's Blocked recovery rules. Unchanged approved content goes to stage 4;
|
||||
@@ -1002,7 +1001,7 @@ Never run a third audit. Child return is not acceptance.
|
||||
### 4. Verify the frozen candidate
|
||||
|
||||
Freeze inputs through verification and push. Run declared docs/link/generated-file
|
||||
checks; report unavailable checks.
|
||||
checks; report any that are unavailable.
|
||||
|
||||
**Reuse a check when its inputs match.** Compare hashes or complete bytes of its
|
||||
saved and current consumed files, fixtures, dependencies and execution parameters.
|
||||
@@ -1015,8 +1014,8 @@ command, result/counts, timestamp and log, never resample. Mandatory reviews sti
|
||||
`--allow-paths` exempts only release metadata. A `package.json` version-only edit
|
||||
can qualify; scripts, dependencies and runtime configuration require live tests.
|
||||
Uncertain edits cannot be exempted. Docs, TODO edits, new/generated tests and fixes
|
||||
make evidence STALE even without a new code review. Use this example only after
|
||||
confirming that every allowed edit is release metadata:
|
||||
make evidence STALE even without a new code review. Use this example only once
|
||||
every allowed edit is release metadata:
|
||||
|
||||
```bash
|
||||
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack;[ -d "$_r/bin" ]||_r=${CODEX_HOME:-~/.codex}/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host codex from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
|
||||
@@ -1043,7 +1042,7 @@ keeps its existing audit count; it does not authorize a third attempt.
|
||||
### 5. Report, then push
|
||||
|
||||
Commit only approved, verified release changes left uncommitted after Step 15,
|
||||
including generated outputs; use its grouping rules and never create an empty commit.
|
||||
including generated outputs; use its grouping rules; never create an empty commit.
|
||||
Preserve unrelated user files.
|
||||
|
||||
Paste build/docs/test results. Reuse waivers only for the same verified
|
||||
@@ -1057,8 +1056,8 @@ its records:
|
||||
`reason` and logger-stamped `actor`. If there are none, the coverage line
|
||||
(`none detected (...)`, `partial (...)` or `UNAVAILABLE — ...`).
|
||||
- Each test lane's `tests_ran` (N, ZERO-RUN or `count unavailable`).
|
||||
Write this report in the auditor's posture: describe this session's own gate edits
|
||||
as evidence for the reader to examine, not as positions to defend; a kept gate
|
||||
Write this report in the auditor's posture: this session's own gate edits are
|
||||
evidence for the reader to examine, not positions to defend; a kept gate
|
||||
states its reason and nothing more. Otherwise continue to Step 17.
|
||||
|
||||
---
|
||||
@@ -1106,7 +1105,7 @@ Branch on the echoed values:
|
||||
`pre-push.local`; otherwise request manual integration. Dangling
|
||||
symlinks are unmanaged. Never overwrite either policy.
|
||||
2. **`REDACT_PREPUSH` not true AND `PREPUSH_PROMPTED: no`** — one-time
|
||||
offer (fires once EVER, machine-wide). AskUserQuestion:
|
||||
offer (once ever, machine-wide). AskUserQuestion:
|
||||
|
||||
> gstack can install a per-repo git pre-push hook that blocks pushes
|
||||
> containing credentials (API keys, tokens, private keys). It's a
|
||||
@@ -1120,17 +1119,16 @@ Branch on the echoed values:
|
||||
If A: run `$GSTACK_ROOT/bin/gstack-config set redact_prepush_hook true`
|
||||
then re-run the block and apply the same directory and unmanaged-hook rules above.
|
||||
If B: run `$GSTACK_ROOT/bin/gstack-config set redact_prepush_hook false`.
|
||||
ALWAYS (after either answer, but NOT if the question itself failed to
|
||||
render — a failed AskUserQuestion must re-offer next time):
|
||||
ALWAYS after either answer (never after a failed question; it must re-offer
|
||||
next time):
|
||||
```bash
|
||||
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack;[ -d "$_r/bin" ]||_r=${CODEX_HOME:-~/.codex}/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host codex from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
|
||||
GSTACK_STATE_ROOT=$($GSTACK_ROOT/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
|
||||
touch "$GSTACK_STATE_ROOT/.redact-prepush-prompted"
|
||||
```
|
||||
3. **Declined earlier** — continue
|
||||
without comment.
|
||||
3. **Declined earlier** — continue without comment.
|
||||
|
||||
**Open gate findings against an existing ready PR/MR:** when Step 9 left any gate
|
||||
**Open gate findings against an existing ready PR/MR:** when Step 9 left a gate
|
||||
finding `open`, look up an open PR/MR for `<branch-name>` before pushing
|
||||
(`gh pr list --head <branch-name> --state open --json number,isDraft`; GitLab
|
||||
`glab mr list --source-branch <branch-name> --output json`, reading `draft`). A lookup
|
||||
@@ -1144,7 +1142,7 @@ failure STOPs here. If one exists and is not a draft:
|
||||
the `Gate edits: N open` banner. Record B in the invocation record as a
|
||||
human-authorised publication; Step 19 reads it. Never convert a ready PR to a draft.
|
||||
|
||||
A new PR, or an existing draft, needs no question: Step 19 creates or keeps the draft.
|
||||
A new PR or an existing draft needs no question: Step 19 creates or keeps the draft.
|
||||
|
||||
**Idempotency check:** Check if the branch is already pushed and up to date.
|
||||
|
||||
@@ -1160,7 +1158,7 @@ echo "LOCAL: $LOCAL REMOTE: $REMOTE"
|
||||
[ "$LOCAL" = "$REMOTE" ] && echo "ALREADY_PUSHED" || echo "PUSH_NEEDED"
|
||||
```
|
||||
|
||||
If `ALREADY_PUSHED`, skip the push but continue to Step 18. Otherwise push with upstream tracking:
|
||||
`ALREADY_PUSHED`: skip the push, continue to Step 18. Otherwise push with upstream tracking:
|
||||
|
||||
```bash
|
||||
git push -u origin <branch-name>
|
||||
@@ -1170,9 +1168,8 @@ git push -u origin <branch-name>
|
||||
- **Non-fast-forward push:** fetch and inspect the remote, then merge under Step 3's
|
||||
conflict rules. Run Steps 5–16 before returning to Step 17. Never rewrite history.
|
||||
- **Authentication, hook or network failure:** repair the cause, then repeat Step 16
|
||||
even if content is unchanged before returning to Step 17. Never bypass failed guards.
|
||||
Never force-push.
|
||||
Only a successful push or verified `ALREADY_PUSHED` proceeds.
|
||||
even with unchanged content before returning to Step 17. Never bypass failed guards.
|
||||
Never force-push. Only a successful push or verified `ALREADY_PUSHED` proceeds.
|
||||
|
||||
Continue to Step 18. No documentation writer runs after push.
|
||||
|
||||
@@ -1185,19 +1182,23 @@ First look up open PRs/MRs for `<branch-name>` on the detected platform:
|
||||
- GitHub: `gh pr list --head <branch-name> --state open --json number,title,url`
|
||||
- GitLab: `glab mr list --source-branch <branch-name> --output json` (defaults to open).
|
||||
|
||||
A successful empty array means new; one match supplies the existing title/identity.
|
||||
An empty array means new; one match supplies the existing title/identity.
|
||||
Lookup failure or ambiguous matches **STOP** for resolution, never mean no PR.
|
||||
Save the result for Step 19's recheck.
|
||||
|
||||
The pr-body section prepares the title from that result, then publishes.
|
||||
The pr-body section prepares the title from that result, then publishes;
|
||||
`--no-pr` or an automation lane takes the handoff section.
|
||||
|
||||
> **STOP.** Before preparing the title and creating or updating the PR/MR with the verified documentation outcome (Steps 18-19), Read `sections/pr-body.md` relative to the installed `gstack-ship` SKILL.md directory and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
|
||||
> **STOP.** Before handing the branch to a coordinator instead of opening a PR (--no-pr or an automation lane; Step 18), Read `sections/handoff.md` relative to the installed `gstack-ship` SKILL.md directory and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
|
||||
## Step 20: Persist ship metrics
|
||||
|
||||
Log metrics for `/retro` through `gstack-review-log`; it handles project/branch paths,
|
||||
JSON validation, storage and sync. It takes **no path argument**; do not build one.
|
||||
JSON validation, storage and sync and takes **no path argument**.
|
||||
|
||||
```bash
|
||||
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack;[ -d "$_r/bin" ]||_r=${CODEX_HOME:-~/.codex}/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host codex from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
|
||||
@@ -1214,7 +1215,7 @@ Substitute from earlier steps:
|
||||
- **VERIFY_RESULT**: "pass", "fail", or "skipped", set after Step 9 executes Step 8.1's verification list
|
||||
- **VERSION**: `NEW_VERSION` from Step 12; `null` (unquoted) under NO_VERSION
|
||||
|
||||
The shell supplies the branch. Run this automatically, without confirmation.
|
||||
The shell supplies the branch. Run it without confirmation.
|
||||
|
||||
---
|
||||
|
||||
@@ -1236,15 +1237,14 @@ if [ ! -f "$_NUDGE_MARKER" ] && [ "$_QT" = "false" ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
The marker or enabled question_tuning suppresses it. To re-enable, remove
|
||||
`$GSTACK_STATE_ROOT/.plan-tune-nudge-shown` before the next ship.
|
||||
The marker or enabled question_tuning suppresses it; remove the marker to re-enable.
|
||||
|
||||
---
|
||||
|
||||
## Section self-check (before you finish)
|
||||
|
||||
List the applicable Section index entries and confirm each Read. If you worked from
|
||||
memory, STOP, Read the section and redo that step. Use `gstack-version-bump`, never
|
||||
memory, STOP, Read the section and redo that step. Use `gstack-version-bump`; never
|
||||
hand-roll VERSION/package.json writes.
|
||||
|
||||
---
|
||||
|
||||
+60
-60
@@ -428,7 +428,7 @@ Routine authorization never waives those gates or their required user decisions.
|
||||
**Routine work needs no confirmation:** include uncommitted changes, choose MICRO/PATCH
|
||||
under Step 12, draft CHANGELOG and commits, mark completed TODOs and auto-fix findings.
|
||||
When Step 7 coverage meets its target, report remaining gaps and verify generated
|
||||
tests without another permission question. Step 15 commits those tests.
|
||||
tests without asking again. Step 15 commits those tests.
|
||||
|
||||
**Route:** integrate (1–3) → test and review (4–11.5) → prepare the release
|
||||
(12–15) → verify frozen content (16) → push and publish (17–21).
|
||||
@@ -438,10 +438,10 @@ Steps 12, 17 and 19 prevent duplicate bumps, pushes and PRs, never verification.
|
||||
### Keep state between steps
|
||||
|
||||
Keep one private Markdown **invocation record** outside the product tree and save
|
||||
its absolute path. Use these headings so a paused run can resume:
|
||||
its absolute path, with these headings so a paused run can resume:
|
||||
- **Release:** versions, `BUMP_LEVEL`, reviewed tree and attempt counts.
|
||||
- **Decisions:** each approval's finding, files and authorized action. Reuse it only
|
||||
for that same scope; a repair never resets approvals or expands them.
|
||||
for that same scope; a repair never resets or expands approvals.
|
||||
- **Reviews:** handles, original start tokens, terminal states, outputs and queued fixes.
|
||||
- **Checks:** command/label, result/counts, timestamp, log and consumed inputs.
|
||||
- **Documentation:** candidate/id, attempts used, accepted hashes or named blocked exception.
|
||||
@@ -463,18 +463,18 @@ permission to proceed. Follow the saved work list:
|
||||
1. Start with Steps 1–21 in order, including 6.5, 11.5 and 14.5. Advance only after
|
||||
the current item's gates clear.
|
||||
2. Expand a repair into individual steps and insert them before the still-pending
|
||||
work. This replaces the current item, whose actual result stays in the record.
|
||||
work. This replaces the current item, whose result stays in the record.
|
||||
Add its destination only if not already the next pending step.
|
||||
3. For another repair, repeat rule 2 without discarding pending work.
|
||||
The saved list takes precedence over ordinary next-step
|
||||
sentences inside a repair. A range never adds unlisted steps.
|
||||
The saved list outranks ordinary next-step sentences inside a repair.
|
||||
A range never adds unlisted steps.
|
||||
|
||||
**Example:** Step 11 fixes insert `9 → 10 → 11` before 11.5. A further Step 9 fix
|
||||
affecting 6–8 makes the list `5 → 6 → 7 → 8 → 9 → 10 → 11 → 11.5`.
|
||||
The unchanged release steps follow. STOP and AskUserQuestion gates still apply during repairs.
|
||||
|
||||
Keep the same attempt counts throughout the invocation. A range ending at Step 14
|
||||
does not enter Step 14.5. A range that includes Step 14.5 enters its existing audit
|
||||
does not enter Step 14.5. A range including Step 14.5 enters its existing audit
|
||||
decision, not an unconditional new launch; its initial-plus-ONE limit never resets.
|
||||
Permitted repairs continue in this invocation without restarting /ship.
|
||||
|
||||
@@ -501,6 +501,7 @@ sections. Read a section in full before doing its step; do not work from memory.
|
||||
| writing the CHANGELOG entry (Step 13) | `sections/changelog.md` |
|
||||
| auditing docs before final commit/verification (Step 14.5), on every ship | `sections/documentation.md` |
|
||||
| preparing the title and creating or updating the PR/MR with the verified documentation outcome (Steps 18-19) | `sections/pr-body.md` |
|
||||
| handing the branch to a coordinator instead of opening a PR (--no-pr or an automation lane; Step 18) | `sections/handoff.md` |
|
||||
|
||||
---
|
||||
|
||||
@@ -552,7 +553,7 @@ branch name wherever the instructions say "the base branch" or `<default>`.
|
||||
|
||||
If the ask is App Store/TestFlight distribution, look for an `.xcodeproj`,
|
||||
`.xcworkspace`, or Swift app product. Read `Package.swift` and its entrypoint to
|
||||
distinguish an app from a library/CLI. If unclear, use AskUserQuestion to identify
|
||||
distinguish an app from a library/CLI. If unclear, AskUserQuestion to identify
|
||||
the target and wait before choosing a release path.
|
||||
For a confirmed app, **STOP and Read
|
||||
`sections/apple-release.md` relative to the installed `gstack-ship` SKILL.md directory FIRST**. Store distribution proceeds
|
||||
@@ -564,10 +565,10 @@ repository-landing asks, including on Apple repos.
|
||||
|
||||
1. Save the current branch as `<branch-name>`. If on the base branch or the repo's default branch, **abort**: "You're on the base branch. Ship from a feature branch."
|
||||
|
||||
2. Run `git status` (never use `-uall`). Uncommitted changes are always included — no need to ask.
|
||||
2. Run `git status` (never `-uall`). Uncommitted changes are always included; do not ask.
|
||||
|
||||
3. Run `git fetch origin <base>` before inspecting the diff. If fetch fails, STOP:
|
||||
report the error and restore access before continuing. Then inspect
|
||||
report the error and restore access first. Then inspect
|
||||
`git diff origin/<base> --stat`, untracked files from status, and
|
||||
`git log origin/<base>..HEAD --oneline`.
|
||||
|
||||
@@ -660,7 +661,7 @@ VERDICT: {CLEARED or NOT CLEARED} — {reason}
|
||||
An outside review with status `unverified` or `unavailable` is missing coverage, never a pass.
|
||||
|
||||
For diffs >200 lines (`git diff origin/<base> --stat | tail -1`), recommend
|
||||
`/plan-eng-review` or `/autoplan` for architecture review.
|
||||
`/plan-eng-review` or `/autoplan`.
|
||||
|
||||
For Design Review: run `source <($GSTACK_ROOT/bin/gstack-diff-scope <base> 2>/dev/null)`. If `SCOPE_FRONTEND=true` and no design review exists, mention: "Design Review not run — Step 9 includes the lite check; consider /design-review for a full visual audit."
|
||||
|
||||
@@ -671,16 +672,16 @@ Continue to Step 2 without asking; Step 9 applies the review gates.
|
||||
## Step 2: Distribution Pipeline Check
|
||||
|
||||
Check distribution for new standalone artifacts (CLI binaries, packages, tools),
|
||||
not web services with existing deployment.
|
||||
not web services already deployed.
|
||||
|
||||
1. List candidate distribution paths:
|
||||
```bash
|
||||
git diff origin/<base> --diff-filter=A --name-only | grep -E '(^|/)(cmd/[^/]+/main\.go|bin/[^/]+|Cargo\.toml|setup\.py|package\.json)$' | head -5
|
||||
```
|
||||
Also inspect matching untracked files from Step 1's status. Read each match:
|
||||
a new `package.json` or `Cargo.toml` alone does not establish a publishable
|
||||
artifact. Also inspect existing manifests for newly declared binaries or
|
||||
package exports. Apply the pipeline gate only when a new distributable is present.
|
||||
Include matching untracked files from Step 1's status. Read each match:
|
||||
a new `package.json` or `Cargo.toml` alone is not a publishable artifact;
|
||||
also check existing manifests for newly declared binaries or package
|
||||
exports. Gate only when a new distributable is present.
|
||||
|
||||
2. If new artifact detected, check for a release workflow:
|
||||
```bash
|
||||
@@ -698,7 +699,7 @@ not web services with existing deployment.
|
||||
Ask for unknown targets, registries or access first; never invent credentials.
|
||||
Recheck against the artifact and include the workflow in tests and review.
|
||||
Do not publish a release during `/ship`.
|
||||
5. Otherwise, continue without adding a pipeline.
|
||||
5. Otherwise continue without a pipeline.
|
||||
|
||||
---
|
||||
|
||||
@@ -710,7 +711,7 @@ Merge the base ref fetched in Step 1 so tests and reviews cover the integrated c
|
||||
git merge origin/<base> --no-edit
|
||||
```
|
||||
|
||||
**If there are merge conflicts:** Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution decision, and wait for the answer before editing or continuing.
|
||||
**Merge conflicts:** Auto-resolve simple ones (VERSION, schema.rb, CHANGELOG ordering). For complex or ambiguous conflicts, **STOP**, show the conflicting choices, use AskUserQuestion for the resolution, and wait for the answer before editing or continuing.
|
||||
|
||||
**If already up to date:** Continue silently.
|
||||
|
||||
@@ -748,7 +749,7 @@ repeat Step 2 (decisions included) on the merged content, then Step 4.
|
||||
1. **Select the two reviews.** Run `$GSTACK_ROOT/bin/gstack-review-read`.
|
||||
Select this invocation's final Step 9.4 record (`skill:"review"`, `via:"ship"`)
|
||||
and Step 11 native record (`skill:"adversarial-review"`). Match each to its saved
|
||||
handle, original token and source; reject outside-provider or older invocation records.
|
||||
handle, original token and source; reject outside-provider or older records.
|
||||
2. **Compare their content.** Require the native record's `review_binding.state`
|
||||
to be `verified`. All three snapshots must match: its `wtree`, Step 9.4's
|
||||
`review_binding.start_wtree` and `review_binding.end_wtree`. A mismatch or missing
|
||||
@@ -757,7 +758,7 @@ repeat Step 2 (decisions included) on the merged content, then Step 4.
|
||||
Never attach new tokens to old work.
|
||||
3. **Preserve any QA exception.** A named probe-risk exception may leave Step 9.4's
|
||||
root `wtree` absent; item 2 still compares its start/end snapshots. Matching content
|
||||
does not mean the failed or unrun probes passed. Keep Step 9.4's incomplete flags
|
||||
does not mean failed or unrun probes passed. Keep Step 9.4's incomplete flags
|
||||
and the user's exception.
|
||||
4. **Save the evidence.** Save both records and matching **reviewed tree** for
|
||||
Step 16. Continue to Step 12.
|
||||
@@ -795,9 +796,8 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
- **MICRO**: <50 lines, trivial tweaks/config. **PATCH**: 50+ lines, no feature signal.
|
||||
- **MINOR**: ask for any feature signal (new route/page, migration, module) or 500+ lines.
|
||||
**MAJOR**: ask for milestones or breaking changes (`ship: bump patch` never asks).
|
||||
Use AskUserQuestion: recommended
|
||||
level with rationale, smaller level, or cancel. Wait; cancel stops before release
|
||||
writes or push and preserves existing work.
|
||||
AskUserQuestion: recommended level with rationale, smaller level, or cancel.
|
||||
Wait; cancel stops before release writes or push and preserves existing work.
|
||||
Save lowercase `BUMP_LEVEL`. A claimed version may move the next available number
|
||||
forward, but cannot change the chosen MICRO/PATCH/MINOR/MAJOR level.
|
||||
|
||||
@@ -810,7 +810,7 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
**Qualify first:** require successful utility output and a nonempty valid version.
|
||||
`offline:false` qualifies; `offline:true` qualifies only with `fallback:"git"`.
|
||||
Offline output without that fallback, failure, malformed output or an empty version
|
||||
is unusable, even if it contains a version-looking string.
|
||||
is unusable, even when it contains a version-looking string.
|
||||
|
||||
- **Usable candidate:** print warnings and claimed queue. FRESH sets `NEW_VERSION=CANDIDATE_VERSION`.
|
||||
ALREADY_BUMPED compares it with `currentVersion`: if different, ask to rebump
|
||||
@@ -826,7 +826,7 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.factory/skills/gstack;[ -d "$_r/bin" ]||_r=~/.factory/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host factory from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
|
||||
bun run $GSTACK_ROOT/bin/gstack-version-bump write --version "$NEW_VERSION" --regen-digest
|
||||
```
|
||||
The CLI validates `MAJOR.MINOR.PATCH.MICRO` (or pinned 3-digit semver) and writes
|
||||
The CLI validates `MAJOR.MINOR.PATCH.MICRO` (or pinned 3-digit semver), writes
|
||||
VERSION, the manifest (`--package-json-path` → `.gstack/package-json-path` →
|
||||
`./package.json`) and existing npm lockfiles, never creating one, with the 3-digit
|
||||
translation (`1.67.0.0` → `1.67.0`); VERSION is authoritative. Exit 3 is a half-write:
|
||||
@@ -843,7 +843,7 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
|
||||
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.factory/skills/gstack;[ -d "$_r/bin" ]||_r=~/.factory/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host factory from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
|
||||
$GSTACK_ROOT/bin/gstack-decision-log '{"decision":"Ship NEW_VERSION (BUMP_LEVEL)","rationale":"WHY","scope":"repo","source":"skill","confidence":9}' 2>/dev/null || true
|
||||
```
|
||||
Substitute `NEW_VERSION`, `BUMP_LEVEL` and a one-line `WHY` (scope or breaking-change signal); best-effort, non-blocking.
|
||||
Substitute `NEW_VERSION`, `BUMP_LEVEL` and a one-line `WHY` (scope or breaking-change signal); best-effort.
|
||||
|
||||
> **STOP.** Before writing the CHANGELOG entry (Step 13), Read `sections/changelog.md` relative to the installed `gstack-ship` SKILL.md directory and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
@@ -864,7 +864,7 @@ at the bottom. If disorganized, ask: A) Reorganize preserving all content
|
||||
- Step 2: add the approved distribution follow-up as P1 with the missing pipeline and affected artifact.
|
||||
- Step 8: add each approved P1 plan deferral with `Deferred from plan: {plan file path}` and the missing work.
|
||||
- Step 5: retain P0 test-failure entries already written; deduplicate by failure and source, adding missing approved entries with error output and branch.
|
||||
Never turn dropped scope into TODOs or invent unapproved follow-ups. Reuse matching existing entries rather than duplicating them.
|
||||
Never turn dropped scope into TODOs or invent unapproved follow-ups. Reuse matching entries instead of duplicating them.
|
||||
|
||||
**4. Detect completed TODOs:** Compare titles, files and behavior with
|
||||
`git diff origin/<base>`, untracked files and `git log origin/<base>..HEAD --oneline`.
|
||||
@@ -872,9 +872,9 @@ Move proven completions to `## Completed` with `**Completed:** vX.Y.Z (YYYY-MM-D
|
||||
leave uncertain items open.
|
||||
|
||||
**5. Save the summary:** Report additions, deferrals, completions, remaining count and
|
||||
creation/reorganization. If creation was declined or a write failed, warn and retain
|
||||
unsaved follow-ups in Step 19's PR summary. Never claim they were saved;
|
||||
TODO write failures are non-blocking.
|
||||
creation/reorganization. If creation was declined or a write failed, warn and carry
|
||||
unsaved follow-ups in Step 19's PR summary; never claim they were saved. TODO
|
||||
write failures are non-blocking.
|
||||
|
||||
---
|
||||
|
||||
@@ -893,9 +893,9 @@ Make bisectable commits; if already committed, continue to Step 16. Never create
|
||||
|
||||
1. Group changes with their tests, config/routes, views and Step 14.5 docs.
|
||||
Migrations may stand alone or accompany their model.
|
||||
Under 50 lines across fewer than 4 files may use one commit.
|
||||
Under 50 lines across fewer than 4 files may be one commit.
|
||||
2. Order dependencies first: infrastructure → models/services → controllers/views.
|
||||
Each commit must work independently, without broken imports or missing code.
|
||||
Each commit must work on its own, without broken imports or missing code.
|
||||
Group VERSION + CHANGELOG + TODOS.md after the feature commits.
|
||||
3. Use `<type>: <summary>` (feat/fix/chore/refactor/docs) and a brief body.
|
||||
Only the final VERSION/CHANGELOG commit gets the release version and co-author
|
||||
@@ -918,7 +918,7 @@ EOF
|
||||
|
||||
Run stages 1–5 in order; recovery notes below name where to resume. Content
|
||||
changes during or after verification restart at stage 1 and complete all five
|
||||
stages before Step 17. Content-preserving commits keep valid evidence.
|
||||
stages before Step 17. Content-preserving commits keep evidence valid.
|
||||
|
||||
### 1. Finish writers and prepare outputs
|
||||
|
||||
@@ -931,7 +931,7 @@ files and CI. Run them and save results. If none exists, record not applicable a
|
||||
the inspected sources. A missing prerequisite or failed build stops shipping:
|
||||
report **Build failed or prerequisite missing**, with the command, error and needed
|
||||
repair. Never invent a substitute command.
|
||||
**If blocked:** Repair the prerequisite or build, then repeat stage 1. After it passes, continue
|
||||
**If blocked:** Repair it, then repeat stage 1. After it passes, continue
|
||||
to stage 2; treat any content repair as a behavioral change there.
|
||||
|
||||
### 2. Choose the change route
|
||||
@@ -971,8 +971,7 @@ Report changed inputs, blockers and attempts used:
|
||||
|
||||
- **An attempt remains, with changed inputs or an available repair:** insert
|
||||
`14.5 → 15 → 16` before Step 17. Use Blocked recovery with the existing count.
|
||||
Validate the outcome before Step 15,
|
||||
then restart Step 16 stage 1 to regenerate and compare again.
|
||||
Validate the outcome before Step 15, then restart Step 16 stage 1.
|
||||
- **Otherwise:** STOP unless the user accepts
|
||||
the specific named documentation risk and all unwaivable gates clear, under
|
||||
Step 14.5's Blocked recovery rules. Unchanged approved content goes to stage 4;
|
||||
@@ -983,7 +982,7 @@ Never run a third audit. Child return is not acceptance.
|
||||
### 4. Verify the frozen candidate
|
||||
|
||||
Freeze inputs through verification and push. Run declared docs/link/generated-file
|
||||
checks; report unavailable checks.
|
||||
checks; report any that are unavailable.
|
||||
|
||||
**Reuse a check when its inputs match.** Compare hashes or complete bytes of its
|
||||
saved and current consumed files, fixtures, dependencies and execution parameters.
|
||||
@@ -996,8 +995,8 @@ command, result/counts, timestamp and log, never resample. Mandatory reviews sti
|
||||
`--allow-paths` exempts only release metadata. A `package.json` version-only edit
|
||||
can qualify; scripts, dependencies and runtime configuration require live tests.
|
||||
Uncertain edits cannot be exempted. Docs, TODO edits, new/generated tests and fixes
|
||||
make evidence STALE even without a new code review. Use this example only after
|
||||
confirming that every allowed edit is release metadata:
|
||||
make evidence STALE even without a new code review. Use this example only once
|
||||
every allowed edit is release metadata:
|
||||
|
||||
```bash
|
||||
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.factory/skills/gstack;[ -d "$_r/bin" ]||_r=~/.factory/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host factory from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
|
||||
@@ -1024,7 +1023,7 @@ keeps its existing audit count; it does not authorize a third attempt.
|
||||
### 5. Report, then push
|
||||
|
||||
Commit only approved, verified release changes left uncommitted after Step 15,
|
||||
including generated outputs; use its grouping rules and never create an empty commit.
|
||||
including generated outputs; use its grouping rules; never create an empty commit.
|
||||
Preserve unrelated user files.
|
||||
|
||||
Paste build/docs/test results. Reuse waivers only for the same verified
|
||||
@@ -1038,8 +1037,8 @@ its records:
|
||||
`reason` and logger-stamped `actor`. If there are none, the coverage line
|
||||
(`none detected (...)`, `partial (...)` or `UNAVAILABLE — ...`).
|
||||
- Each test lane's `tests_ran` (N, ZERO-RUN or `count unavailable`).
|
||||
Write this report in the auditor's posture: describe this session's own gate edits
|
||||
as evidence for the reader to examine, not as positions to defend; a kept gate
|
||||
Write this report in the auditor's posture: this session's own gate edits are
|
||||
evidence for the reader to examine, not positions to defend; a kept gate
|
||||
states its reason and nothing more. Otherwise continue to Step 17.
|
||||
|
||||
---
|
||||
@@ -1087,7 +1086,7 @@ Branch on the echoed values:
|
||||
`pre-push.local`; otherwise request manual integration. Dangling
|
||||
symlinks are unmanaged. Never overwrite either policy.
|
||||
2. **`REDACT_PREPUSH` not true AND `PREPUSH_PROMPTED: no`** — one-time
|
||||
offer (fires once EVER, machine-wide). AskUserQuestion:
|
||||
offer (once ever, machine-wide). AskUserQuestion:
|
||||
|
||||
> gstack can install a per-repo git pre-push hook that blocks pushes
|
||||
> containing credentials (API keys, tokens, private keys). It's a
|
||||
@@ -1101,17 +1100,16 @@ Branch on the echoed values:
|
||||
If A: run `$GSTACK_ROOT/bin/gstack-config set redact_prepush_hook true`
|
||||
then re-run the block and apply the same directory and unmanaged-hook rules above.
|
||||
If B: run `$GSTACK_ROOT/bin/gstack-config set redact_prepush_hook false`.
|
||||
ALWAYS (after either answer, but NOT if the question itself failed to
|
||||
render — a failed AskUserQuestion must re-offer next time):
|
||||
ALWAYS after either answer (never after a failed question; it must re-offer
|
||||
next time):
|
||||
```bash
|
||||
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.factory/skills/gstack;[ -d "$_r/bin" ]||_r=~/.factory/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host factory from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
|
||||
GSTACK_STATE_ROOT=$($GSTACK_ROOT/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
|
||||
touch "$GSTACK_STATE_ROOT/.redact-prepush-prompted"
|
||||
```
|
||||
3. **Declined earlier** — continue
|
||||
without comment.
|
||||
3. **Declined earlier** — continue without comment.
|
||||
|
||||
**Open gate findings against an existing ready PR/MR:** when Step 9 left any gate
|
||||
**Open gate findings against an existing ready PR/MR:** when Step 9 left a gate
|
||||
finding `open`, look up an open PR/MR for `<branch-name>` before pushing
|
||||
(`gh pr list --head <branch-name> --state open --json number,isDraft`; GitLab
|
||||
`glab mr list --source-branch <branch-name> --output json`, reading `draft`). A lookup
|
||||
@@ -1125,7 +1123,7 @@ failure STOPs here. If one exists and is not a draft:
|
||||
the `Gate edits: N open` banner. Record B in the invocation record as a
|
||||
human-authorised publication; Step 19 reads it. Never convert a ready PR to a draft.
|
||||
|
||||
A new PR, or an existing draft, needs no question: Step 19 creates or keeps the draft.
|
||||
A new PR or an existing draft needs no question: Step 19 creates or keeps the draft.
|
||||
|
||||
**Idempotency check:** Check if the branch is already pushed and up to date.
|
||||
|
||||
@@ -1141,7 +1139,7 @@ echo "LOCAL: $LOCAL REMOTE: $REMOTE"
|
||||
[ "$LOCAL" = "$REMOTE" ] && echo "ALREADY_PUSHED" || echo "PUSH_NEEDED"
|
||||
```
|
||||
|
||||
If `ALREADY_PUSHED`, skip the push but continue to Step 18. Otherwise push with upstream tracking:
|
||||
`ALREADY_PUSHED`: skip the push, continue to Step 18. Otherwise push with upstream tracking:
|
||||
|
||||
```bash
|
||||
git push -u origin <branch-name>
|
||||
@@ -1151,9 +1149,8 @@ git push -u origin <branch-name>
|
||||
- **Non-fast-forward push:** fetch and inspect the remote, then merge under Step 3's
|
||||
conflict rules. Run Steps 5–16 before returning to Step 17. Never rewrite history.
|
||||
- **Authentication, hook or network failure:** repair the cause, then repeat Step 16
|
||||
even if content is unchanged before returning to Step 17. Never bypass failed guards.
|
||||
Never force-push.
|
||||
Only a successful push or verified `ALREADY_PUSHED` proceeds.
|
||||
even with unchanged content before returning to Step 17. Never bypass failed guards.
|
||||
Never force-push. Only a successful push or verified `ALREADY_PUSHED` proceeds.
|
||||
|
||||
Continue to Step 18. No documentation writer runs after push.
|
||||
|
||||
@@ -1166,19 +1163,23 @@ First look up open PRs/MRs for `<branch-name>` on the detected platform:
|
||||
- GitHub: `gh pr list --head <branch-name> --state open --json number,title,url`
|
||||
- GitLab: `glab mr list --source-branch <branch-name> --output json` (defaults to open).
|
||||
|
||||
A successful empty array means new; one match supplies the existing title/identity.
|
||||
An empty array means new; one match supplies the existing title/identity.
|
||||
Lookup failure or ambiguous matches **STOP** for resolution, never mean no PR.
|
||||
Save the result for Step 19's recheck.
|
||||
|
||||
The pr-body section prepares the title from that result, then publishes.
|
||||
The pr-body section prepares the title from that result, then publishes;
|
||||
`--no-pr` or an automation lane takes the handoff section.
|
||||
|
||||
> **STOP.** Before preparing the title and creating or updating the PR/MR with the verified documentation outcome (Steps 18-19), Read `sections/pr-body.md` relative to the installed `gstack-ship` SKILL.md directory and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
|
||||
> **STOP.** Before handing the branch to a coordinator instead of opening a PR (--no-pr or an automation lane; Step 18), Read `sections/handoff.md` relative to the installed `gstack-ship` SKILL.md directory and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
|
||||
## Step 20: Persist ship metrics
|
||||
|
||||
Log metrics for `/retro` through `gstack-review-log`; it handles project/branch paths,
|
||||
JSON validation, storage and sync. It takes **no path argument**; do not build one.
|
||||
JSON validation, storage and sync and takes **no path argument**.
|
||||
|
||||
```bash
|
||||
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.factory/skills/gstack;[ -d "$_r/bin" ]||_r=~/.factory/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host factory from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
|
||||
@@ -1195,7 +1196,7 @@ Substitute from earlier steps:
|
||||
- **VERIFY_RESULT**: "pass", "fail", or "skipped", set after Step 9 executes Step 8.1's verification list
|
||||
- **VERSION**: `NEW_VERSION` from Step 12; `null` (unquoted) under NO_VERSION
|
||||
|
||||
The shell supplies the branch. Run this automatically, without confirmation.
|
||||
The shell supplies the branch. Run it without confirmation.
|
||||
|
||||
---
|
||||
|
||||
@@ -1217,15 +1218,14 @@ if [ ! -f "$_NUDGE_MARKER" ] && [ "$_QT" = "false" ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
The marker or enabled question_tuning suppresses it. To re-enable, remove
|
||||
`$GSTACK_STATE_ROOT/.plan-tune-nudge-shown` before the next ship.
|
||||
The marker or enabled question_tuning suppresses it; remove the marker to re-enable.
|
||||
|
||||
---
|
||||
|
||||
## Section self-check (before you finish)
|
||||
|
||||
List the applicable Section index entries and confirm each Read. If you worked from
|
||||
memory, STOP, Read the section and redo that step. Use `gstack-version-bump`, never
|
||||
memory, STOP, Read the section and redo that step. Use `gstack-version-bump`; never
|
||||
hand-roll VERSION/package.json writes.
|
||||
|
||||
---
|
||||
|
||||
+7
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"newModules": [
|
||||
{ "file": "lib/pricing.ts", "movedFrom": "test/helpers/pricing.ts (PRICING, estimateCostUsd; plan E1 moved the table under lib/ so bin/gstack-eval-plan can price arms)" },
|
||||
{ "file": "lib/eval-plan.ts", "movedFrom": "(new: preregistration check, priced arms, $0 dry run, priced pilot ledger behind bin/gstack-eval-plan, plan E1)" },
|
||||
{ "file": "lib/lane-ownership.ts", "movedFrom": "(new: lane ownership check behind bin/gstack-lane-check, plan E2)" },
|
||||
{ "file": "lib/coordinator-contract.ts", "movedFrom": "(new: the inherited coordinator contract text, plan E2)" },
|
||||
{ "file": "lib/owner-brief.ts", "movedFrom": "(new: the one-page owner brief and draft-direction close behind bin/gstack-owner-brief, plan E3)" },
|
||||
{ "file": "lib/contributor-mode.ts", "movedFrom": "(new: maintainer-set detection behind bin/gstack-contributor-mode, plan E3)" },
|
||||
{ "file": "lib/banned-terms.ts", "movedFrom": "(new: .gstack/banned-terms.json scan behind bin/gstack-banned-terms, plan E3)" },
|
||||
{ "file": "scripts/lib/shard-engine.ts", "movedFrom": "scripts/test-strict-output.ts" },
|
||||
{ "file": "autoplan/bin/phase-publication-hook.ts", "movedFrom": "(owner: the /autoplan publication guard; ENG-17 keeps it under the module limit)" },
|
||||
{ "file": "autoplan/bin/guard-reasons.ts", "movedFrom": "(new: guard reason codes, denial text and anchors, ENG-17)" },
|
||||
|
||||
+11
@@ -40,3 +40,14 @@ Later PRs of the wave append their rows; a row is never removed.
|
||||
| p0-4-pipe-hidden-failure: a failing runner behind `\| tail` reported green; a zero-run lane reported green | C8 `lanes` (pipefail) | runner `node --test {files} 2>&1 \| tail -n 1` with a broken `lib/util.js`; runner that prints `# tests 0` | `test/pregate-selection.test.ts` "p0-4-pipe-hidden-failure" |
|
||||
| p0-4-stale-pregate: a pre-gate record reused after the final stamp moved the tree | C8 publication | pregate.json recorded, then `README.md` edited | `test/pregate-invalidation.test.ts` "PREGATE_STALE for both" |
|
||||
| p0-4-dropped-vendored-hunk: a `patchedDependencies` hunk silently dropped on regeneration | C8 `patches` (tier 2, deferred) | — | — (follow-up pre-gate PR) |
|
||||
| p1-8-unpriced-arm: a paid eval ran arms whose price the table did not know, so the admission estimate was $0 | E1 `arms`, `dry-run` | `resolveArms` with a made-up model; `dryRun` with an unpriced arm | `test/pricing-eval-plan.test.ts` "EVAL_PRICE_MISSING", "price_table.missing" |
|
||||
| p1-8-blank-prereg: bars and stop rules were written after the first priced call | E1 `prereg --check` | `preregTemplate()` with every section empty | `test/pricing-eval-plan.test.ts` "empty section fails the check" |
|
||||
| p1-8-malformed-rows: rows without a prompt field reached the paid run; the retry prompt was never read | E1 `dry-run` | `rows.jsonl` with a non-JSON line and a row missing `prompt`; no retry prompt; a 100-token output budget | `test/pricing-eval-plan.test.ts` "flags malformed rows" |
|
||||
| p1-8-pilot-overrun: a run kept reserving calls past its cap; unknown charges were counted as zero | E1 `pilot` | cap `$0.0001`; one attempt settled `unknown` | `test/pricing-eval-plan.test.ts` "EVAL_BUDGET_EXHAUSTED", "verdict unknown" |
|
||||
| p2-9-overlapping-lanes: two lanes edited the same planned file and the second merge rewrote the first | E2 `gstack-lane-check` | fixture repo with `lane-a` (PR #7) and `lane-b` both pushed; planned `src/a.ts`, `docs/**` | `test/lane-coordination.test.ts` "LANE_CONFLICT" |
|
||||
| p2-9-borrowed-evidence: a bundle produced on another machine was cited as a pass here | E2 `gstack-evidence verify` | bundle with no matching ledger record; `--rerun` on a moved tree | `test/lane-coordination.test.ts` "stays unverified", "moved tree refuses" |
|
||||
| p2-9-contract-drift: lane prompts paraphrased the merge conditions and spend caps differently | E2 `{{COORDINATOR_CONTRACT}}` | resolver, `gstack-autoplan contract`, both reviewer prompts, spec's spawn pipe | `test/lane-coordination.test.ts` "same bytes" |
|
||||
| p2-10-buried-gate: a 400-line final report hid the decisions and a critical finding | E3 `gstack-owner-brief render` | nine-item gate with one urgent finding | `test/owner-brief-contributor-banned.test.ts` "one page" |
|
||||
| p2-10-draft-left-standing: `draft direction stands until the owner decides` survived the owner's answer | E3 `gstack-owner-brief close` | plan with three draft lines, one still pending | `test/owner-brief-contributor-banned.test.ts` "close rewrites" |
|
||||
| p2-10-outside-pr-as-code: an outside PR was reviewed as code first and credited without carried-over evidence | E3 `gstack-contributor-mode` | explicit `maintainers`, `read` vs `write` permission, `gh` offline | `test/owner-brief-contributor-banned.test.ts` "contributor mode" |
|
||||
| p2-10-banned-term: a renamed product's old name landed in README while the glossary was allowed | E3 `gstack-banned-terms` | diff touching README, `docs/migration/**`, the glossary and the term file itself | `test/owner-brief-contributor-banned.test.ts` "added lines are scanned" |
|
||||
@@ -13,9 +13,9 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
||||
import { execFile, spawnSync } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'fs';
|
||||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { dirname, join } from 'path';
|
||||
import { createHash } from 'crypto';
|
||||
import { runGeneration } from '../scripts/gen-skill-docs';
|
||||
import { ALL_HOST_CONFIGS } from '../hosts/index';
|
||||
@@ -45,9 +45,11 @@ function mustSh(cwd: string, cmd: string) {
|
||||
/** A directory that looks like a gstack checkout (VERSION, setup, bin/gstack-config). */
|
||||
function makeGstackLike(dir: string, git: boolean) {
|
||||
mkdirSync(join(dir, 'bin'), { recursive: true });
|
||||
mkdirSync(join(dir, 'lib'), { recursive: true });
|
||||
writeFileSync(join(dir, 'VERSION'), '1.0.0.0\n');
|
||||
writeFileSync(join(dir, 'setup'), '#!/bin/sh\necho SETUP_RAN "$@"\n', { mode: 0o755 });
|
||||
writeFileSync(join(dir, 'bin', 'gstack-config'), '#!/bin/sh\necho false\n', { mode: 0o755 });
|
||||
writeFileSync(join(dir, 'bin', 'gstack-autoplan'), '#!/bin/sh\necho "## Coordinator contract (fixture)"\n', { mode: 0o755 });
|
||||
if (git) mustSh(dir, 'git init -q -b main && git add -A && git commit -q -m init');
|
||||
}
|
||||
|
||||
@@ -90,7 +92,7 @@ async function snapshot(dir: string): Promise<string> {
|
||||
return h.digest('hex');
|
||||
}
|
||||
|
||||
interface HostRender { host: string; upgrade: string[]; spec: string[]; localDir: string }
|
||||
interface HostRender { host: string; upgrade: string[]; spec: string[]; localDir: string; globalRoot: string }
|
||||
const hostRenders: HostRender[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -109,7 +111,7 @@ beforeAll(async () => {
|
||||
const spec = bashFences(readFileSync(specPath, 'utf8'));
|
||||
const team = fence(upgrade, 'git rm -r --cached');
|
||||
const localDir = team.match(/git rm -r --cached (\S+)\/ /)![1];
|
||||
hostRenders.push({ host: config.name, upgrade, spec, localDir });
|
||||
hostRenders.push({ host: config.name, upgrade, spec, localDir, globalRoot: config.globalRoot });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -224,6 +226,10 @@ describe.skipIf(IS_WINDOWS)('C9: destructive upgrade/spec fences refuse bad path
|
||||
const team = await run(wd, fence(h.upgrade, 'git rm -r --cached'), { LOCAL_GSTACK: local });
|
||||
if (team.status !== 0 || existsSync(local)) problems.push(`${h.host}: team-mode removal did not run: ${team.stderr}`);
|
||||
mustSh(wd.project, `git worktree add -q "${join(wd.w, 'wt')}" -b spec/x-1 HEAD`);
|
||||
// The spawn reads the contract from this host's ~/<globalRoot>/bin: install the fixture there.
|
||||
const installed = join(root, 'home', h.globalRoot);
|
||||
mkdirSync(dirname(installed), { recursive: true });
|
||||
if (!existsSync(installed)) symlinkSync(wd.gstack, installed);
|
||||
const spawn = await run(wd, fence(h.spec, 'claude -p'), { SPAWN_PATH: join(wd.w, 'wt'), SPAWN_BRANCH: 'spec/x-1', ARCHIVE_PATH: join(wd.gstack, 'VERSION') });
|
||||
const deadline = Date.now() + 5_000;
|
||||
while (!existsSync(join(wd.w, 'claude.log')) && Date.now() < deadline) await Bun.sleep(50);
|
||||
|
||||
@@ -168,7 +168,7 @@ export const CARVE_GUARDS: Record<string, CarveGuard> = {
|
||||
// wave's headline capability) grows the union to 1.195x. Deliberate:
|
||||
// the section is on-demand (loads only for Apple store targets), so
|
||||
// per-invocation cost for non-iOS ships is one manifest line.
|
||||
maxSizeRatio: 1.6400, // Shared advisory identity/dedup + critical-severity validation: 248,065 union bytes / 187,706 baseline = 1.3216 (2026-09-17). + test value bar in the lazy Step 7 section (value cards, weak paths, gate table, base control, machine checks; ~13.6KB): measured 1.396 (2026-09-29). + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.401 (2026-09-30). + the shared QA review step's plan-check timing rule (plan checks and their revalidation run on --timeout-ms after smoke expiry); measured 1.4022 (2026-09-30). + the shared TEST_BOOTSTRAP owned-change undo rules (s03 H1-H3, M1) and the value-bar coverage wording (s03 M2/M3); measured 1.4044 (2026-10-02). + severe fix wave ship-review lane: G1 NO_VERSION dispatch, title and CHANGELOG arms (versionless repos ship without inventing a version), F1 parent-side plan binding before the audit dispatch (binding, docs/designs candidates, exact not-run line) and D3's data-not-instructions line; +2,126 union bytes, measured 1.4135 (2026-10-03). + INV-1 consumer lines (outside review `unverified`/`unavailable` is missing coverage in the readiness note and PR body); measured 1.4148 (2026-10-03). + v1.91.19.0 severe fix wave: verdict-form outside-review callers with sandbox preflight and first-use notice (INV-1/B1/Q2), P0/P1 gate and unverified wording, bound-plan discovery (F1), visible LEARNINGS failures (B5), carved external pointers; measured 267,752 / 187,706 = 1.4264 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 267,874 / 187,706 = 1.4271 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051) and quoted-heredoc issue template (#3046); measured 268,199 / 187,706 = 1.4288 (2026-10-06). + Step 10's inline no-PR check before the Greptile dispatch (#3020); measured 268,655 / 187,706 = 1.4313 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 275,422 / 187,706 = 1.4673 (2026-10-06). Combined Oct 6 wave head (B free-text files + D measure loop + A rework): measured 280,363 / 187,706 = 1.4936 (2026-10-06). + gate-once push rule (no second paid gate after a CI push); measured 280,573 / 187,706 = 1.4947 (2026-10-06). + the measurement bar's decision step (exit-code branches for MEETS, MEETS-qualified, needs-classify with the qualifying and fix-round classes, EXTEND once; the measure prose was tightened to offset most of it); measured 281,058 / 187,706 = 1.4973 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 281,438 / 187,706 = 1.4994 (2026-10-07). + Oct 7 wave CEO-28/DX-11: review prompts ask for a severity per finding and design proposals use the proposal gate; measured 281,556 / 187,706 = 1.5000 (2026-10-07). + Oct 7 wave B4: /ship publishes through gstack-post (title prepared in a file in the pr-body section, compose block plus gstack-post exit-code branches replacing the two scans); measured 281,894 / 187,706 = 1.5018 (2026-10-07). + B2 early Greptile PR (lazy Step 6.5 section: detect, public-repo consent, push, provisional title file, open; Step 10's bounded wait; the stop-leaves-the-draft rule); measured 286,626 / 187,706 = 1.5270 (2026-10-07). B3, the classifier, B4 and B2 merged: measured 287,128 / 187,706 = 1.5297 (2026-10-07). + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site; measured 281,370 / 187,706 = 1.4990 (2026-10-07). Merged with the guard area: measured 287,440 / 187,706 = 1.5313 (2026-10-07). + Oct 8 wave: the defensive-security framing and fixture summary mode reach the security specialist and the Red Team (stated once in the dispatch, referenced by the Red Team); measured 288,783 / 187,706 = 1.5385 (2026-10-08). + gate integrity (HONEST_WORK_GATE_INTEGRITY W3): the shared GATE_SCAN_BLOCK in the core checklist (2.5) and after Step 7's child, the four-option gate question and record fields in 9.4, tests_ran states in Step 5, Gate edits and per-lane counts in the PR body with draft-while-open; measured 300,143 / 187,706 = 1.5990 (2026-10-10). + Honest Work wave merged (W2/W3 gate integrity in review/ship, W4 kernel + DONE line in every tier-2+ skill, W5/W7 in spec/ship): re-measured on the integrated tree (2026-10-10).
|
||||
maxSizeRatio: 1.6450, // Shared advisory identity/dedup + critical-severity validation: 248,065 union bytes / 187,706 baseline = 1.3216 (2026-09-17). + test value bar in the lazy Step 7 section (value cards, weak paths, gate table, base control, machine checks; ~13.6KB): measured 1.396 (2026-09-29). + W1 guarded state-root resolution (`eval gstack-paths; : "${GSTACK_STATE_ROOT:?…}"`) in the Context Recovery preamble, the eureka log and each state-writing bash block; measured 1.401 (2026-09-30). + the shared QA review step's plan-check timing rule (plan checks and their revalidation run on --timeout-ms after smoke expiry); measured 1.4022 (2026-09-30). + the shared TEST_BOOTSTRAP owned-change undo rules (s03 H1-H3, M1) and the value-bar coverage wording (s03 M2/M3); measured 1.4044 (2026-10-02). + severe fix wave ship-review lane: G1 NO_VERSION dispatch, title and CHANGELOG arms (versionless repos ship without inventing a version), F1 parent-side plan binding before the audit dispatch (binding, docs/designs candidates, exact not-run line) and D3's data-not-instructions line; +2,126 union bytes, measured 1.4135 (2026-10-03). + INV-1 consumer lines (outside review `unverified`/`unavailable` is missing coverage in the readiness note and PR body); measured 1.4148 (2026-10-03). + v1.91.19.0 severe fix wave: verdict-form outside-review callers with sandbox preflight and first-use notice (INV-1/B1/Q2), P0/P1 gate and unverified wording, bound-plan discovery (F1), visible LEARNINGS failures (B5), carved external pointers; measured 267,752 / 187,706 = 1.4264 (2026-10-03). + v1.91.18.0 (#3023) merged into the v1.91.19.0 wave; measured 267,874 / 187,706 = 1.4271 (2026-10-04). + Oct 6 fix wave: quota_exhausted preflight branch (#3051) and quoted-heredoc issue template (#3046); measured 268,199 / 187,706 = 1.4288 (2026-10-06). + Step 10's inline no-PR check before the Greptile dispatch (#3020); measured 268,655 / 187,706 = 1.4313 (2026-10-06). + Oct 6 fix wave A3/CEO-12: free text reaches commands only through agent-written mktemp files (mktemp block, write rule, path rebuild and refusal at each posting, prompt and brief site); measured 275,422 / 187,706 = 1.4673 (2026-10-06). Combined Oct 6 wave head (B free-text files + D measure loop + A rework): measured 280,363 / 187,706 = 1.4936 (2026-10-06). + gate-once push rule (no second paid gate after a CI push); measured 280,573 / 187,706 = 1.4947 (2026-10-06). + the measurement bar's decision step (exit-code branches for MEETS, MEETS-qualified, needs-classify with the qualifying and fix-round classes, EXTEND once; the measure prose was tightened to offset most of it); measured 281,058 / 187,706 = 1.4973 (2026-10-06). + Oct 7 wave B3: the Codex probe runs as a command (select-model capture lines, check-sandbox/probe-model/run-with-timeout subcommands) instead of being sourced; measured 281,438 / 187,706 = 1.4994 (2026-10-07). + Oct 7 wave CEO-28/DX-11: review prompts ask for a severity per finding and design proposals use the proposal gate; measured 281,556 / 187,706 = 1.5000 (2026-10-07). + Oct 7 wave B4: /ship publishes through gstack-post (title prepared in a file in the pr-body section, compose block plus gstack-post exit-code branches replacing the two scans); measured 281,894 / 187,706 = 1.5018 (2026-10-07). + B2 early Greptile PR (lazy Step 6.5 section: detect, public-repo consent, push, provisional title file, open; Step 10's bounded wait; the stop-leaves-the-draft rule); measured 286,626 / 187,706 = 1.5270 (2026-10-07). B3, the classifier, B4 and B2 merged: measured 287,128 / 187,706 = 1.5297 (2026-10-07). + Oct 7 wave guard (CEO-20): conditional `run_in_background` request plus the background-run recovery at each foreground dispatch site; measured 281,370 / 187,706 = 1.4990 (2026-10-07). Merged with the guard area: measured 287,440 / 187,706 = 1.5313 (2026-10-07). + Oct 8 wave: the defensive-security framing and fixture summary mode reach the security specialist and the Red Team (stated once in the dispatch, referenced by the Red Team); measured 288,783 / 187,706 = 1.5385 (2026-10-08). + gate integrity (HONEST_WORK_GATE_INTEGRITY W3): the shared GATE_SCAN_BLOCK in the core checklist (2.5) and after Step 7's child, the four-option gate question and record fields in 9.4, tests_ran states in Step 5, Gate edits and per-lane counts in the PR body with draft-while-open; measured 300,143 / 187,706 = 1.5990 (2026-10-10). + Honest Work wave merged (W2/W3 gate integrity in review/ship, W4 kernel + DONE line in every tier-2+ skill, W5/W7 in spec/ship): re-measured on the integrated tree (2026-10-10). + Oct 10 multi-agent wave PR E2 (ship/sections/handoff.md: the no-PR automation handoff, with its Section-index row and STOP pointer in the skeleton), offset by tightening Steps 1–3, 12, 14–17 and 20–21 prose so the skeleton stays under 81,000 bytes (80,997); C-pregate landed 1,123 union bytes in tests.md in between; measured 308,619 / 187,706 = 1.6442 on the integra
Line truncated
|
||||
},
|
||||
'plan-ceo-review': {
|
||||
skill: 'plan-ceo-review',
|
||||
@@ -606,6 +606,7 @@ do not launch the downstream skill or open a browser.`,
|
||||
behavioral: 'prompt',
|
||||
maxSkeletonBytes: 58_400, // + v2.0 {{ASIDE_RESEARCH}} (Aside first, WebSearch fallback); measured 57_356
|
||||
minUnionBytes: 64_500, // measured union 67,430
|
||||
maxSizeRatio: 1.07, // Oct 10 multi-agent wave PR E2: the lazy gate-and-file section renders {{COORDINATOR_CONTRACT}} (the block every spawned lane inherits word for word; `gstack-autoplan contract` prints the same bytes) before the --execute spawn pipe; measured 86,614 / 81,577 = 1.0617 after the contract became a fail-closed `CONTRACT=$(…)` capture (2026-10-10). The skeleton is unchanged (57,107).
|
||||
mustContain: ['HARD GATE', 'dedupe', 'quality gate', 'acceptance criteria', 'archive'],
|
||||
},
|
||||
'setup-gbrain': {
|
||||
|
||||
@@ -52,6 +52,12 @@ export const IDENTIFIER_PLACEHOLDERS: Record<string, IdentifierGrammar> = {
|
||||
'<new-version>': { grammar: /^[0-9]+(?:\.[0-9]+)*$/, what: 'NEW_VERSION from the version bump (dot-separated digits)' },
|
||||
'<issue-number>': { grammar: /^[0-9]*$/, what: 'filed issue number, or empty when none was filed' },
|
||||
'<check-number>': { grammar: NUM, what: 'canary check number' },
|
||||
// /eval-plan (plan E1): the pilot's ledger inputs.
|
||||
'<cap-usd>': { grammar: NUMBER, what: 'spend cap in USD' },
|
||||
'<total-items>': { grammar: NUM, what: 'items in the full eval run' },
|
||||
'<workers>': { grammar: NUM, what: 'concurrent eval workers' },
|
||||
'<attempt-id>': { grammar: ID, what: 'PILOT_ATTEMPT id printed by pilot start' },
|
||||
'<usd|unknown>': { grammar: /^(?:[0-9]+(?:\.[0-9]+)?|unknown)$/, what: 'actual cost in USD from the provider usage field, or unknown' },
|
||||
'<run-id>': { grammar: NUM, what: 'GitHub Actions run id' },
|
||||
'<PID>': { grammar: NUM, what: 'process id printed by an earlier block' },
|
||||
'<retained-owner-token>': { grammar: ID, what: 'freeze owner token printed by acquire' },
|
||||
|
||||
+4
-64
@@ -1,66 +1,6 @@
|
||||
/**
|
||||
* Per-model pricing tables.
|
||||
*
|
||||
* Prices are USD per million tokens as of `as_of`. Update quarterly.
|
||||
* Standard text-token estimates, not billed totals: excludes cache writes,
|
||||
* tool fees, and service-tier or long-context multipliers.
|
||||
* Link to provider pricing pages:
|
||||
* - Anthropic: https://www.anthropic.com/pricing#api
|
||||
* - OpenAI: https://openai.com/api/pricing/
|
||||
* - Google AI: https://ai.google.dev/pricing
|
||||
*
|
||||
* When a model isn't in the table, estimateCost returns 0 with a console warning.
|
||||
* Prefer adding a new row to the table over guessing.
|
||||
* Per-model pricing tables: re-exported from lib/pricing.ts (plan E1), which
|
||||
* documents the billing categories the table excludes. Test and script
|
||||
* importers keep this path; add rows in lib/pricing.ts.
|
||||
*/
|
||||
|
||||
export interface ModelPricing {
|
||||
input_per_mtok: number;
|
||||
output_per_mtok: number;
|
||||
cached_input_per_mtok?: number;
|
||||
as_of: string; // YYYY-MM
|
||||
}
|
||||
|
||||
export const PRICING: Record<string, ModelPricing> = {
|
||||
// Claude (Anthropic)
|
||||
// https://platform.claude.com/docs/en/models/fable-5-1/overview#pricing
|
||||
'claude-fable-5-1': { input_per_mtok: 10.00, output_per_mtok: 50.00, cached_input_per_mtok: 0.25, as_of: '2026-09' },
|
||||
'claude-opus-4-7': { input_per_mtok: 15.00, output_per_mtok: 75.00, as_of: '2026-04' },
|
||||
'claude-sonnet-4-6': { input_per_mtok: 3.00, output_per_mtok: 15.00, as_of: '2026-04' },
|
||||
'claude-haiku-4-5': { input_per_mtok: 1.00, output_per_mtok: 5.00, as_of: '2026-04' },
|
||||
|
||||
// OpenAI (GPT + o-series)
|
||||
// https://developers.openai.com/api/docs/models/gpt-6-astra
|
||||
'gpt-6-astra': { input_per_mtok: 10.00, output_per_mtok: 50.00, cached_input_per_mtok: 1.00, as_of: '2026-09' },
|
||||
'gpt-5.4': { input_per_mtok: 2.50, output_per_mtok: 10.00, as_of: '2026-04' },
|
||||
'gpt-5.4-mini': { input_per_mtok: 0.60, output_per_mtok: 2.40, as_of: '2026-04' },
|
||||
'o3': { input_per_mtok: 15.00, output_per_mtok: 60.00, as_of: '2026-04' },
|
||||
'o4-mini': { input_per_mtok: 1.10, output_per_mtok: 4.40, as_of: '2026-04' },
|
||||
|
||||
// Google
|
||||
'gemini-2.5-pro': { input_per_mtok: 1.25, output_per_mtok: 5.00, as_of: '2026-04' },
|
||||
'gemini-2.5-flash': { input_per_mtok: 0.30, output_per_mtok: 1.20, as_of: '2026-04' },
|
||||
};
|
||||
|
||||
const WARNED = new Set<string>();
|
||||
|
||||
export function estimateCostUsd(
|
||||
tokens: { input: number; output: number; cached?: number },
|
||||
model: string | undefined
|
||||
): number {
|
||||
if (!model) return 0;
|
||||
const row = PRICING[model];
|
||||
if (!row) {
|
||||
if (!WARNED.has(model)) {
|
||||
WARNED.add(model);
|
||||
console.error(`WARN: no pricing for model ${model}; returning 0. Add it to test/helpers/pricing.ts.`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// This helper expects uncached input and cache reads as disjoint counts.
|
||||
// Preserve the historical 10% rate for rows without an explicit cache-read price.
|
||||
const cachedRate = row.cached_input_per_mtok ?? row.input_per_mtok * 0.1;
|
||||
const inputCost = tokens.input * row.input_per_mtok / 1_000_000;
|
||||
const cachedCost = (tokens.cached ?? 0) * cachedRate / 1_000_000;
|
||||
const outputCost = tokens.output * row.output_per_mtok / 1_000_000;
|
||||
return +(inputCost + cachedCost + outputCost).toFixed(6);
|
||||
}
|
||||
export { EXCLUDED_BILLING_CATEGORIES, PRICING, estimateCostUsd, hasPricing, pricingRow, type ModelPricing } from '../../lib/pricing';
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* PR E2: lane ownership (`gstack-lane-check`), `gstack-evidence verify <bundle>`,
|
||||
* and the coordinator contract that every lane prompt inherits byte for byte.
|
||||
*/
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { COORDINATOR_CONTRACT, COORDINATOR_CONTRACT_HEADING } from '../lib/coordinator-contract';
|
||||
import { readBundle, renderVerification, verifyBundle } from '../lib/evidence-bundle';
|
||||
import { laneCheck, renderLaneCheck } from '../lib/lane-ownership';
|
||||
import { RESOLVERS } from '../scripts/resolvers';
|
||||
import { cleanup, makeFixtureRepo, recordAndBundle, runBin, type FixtureRepo } from './helpers/restamp-fixture';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const repos: FixtureRepo[] = [];
|
||||
const fixture = () => { const r = makeFixtureRepo({ prefix: 'gstack-lane-' }); repos.push(r); return r; };
|
||||
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
|
||||
|
||||
function commitOn(repo: FixtureRepo, branch: string, files: Record<string, string>): void {
|
||||
repo.git('checkout', '-q', '-b', branch, 'main');
|
||||
for (const [rel, body] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(repo.work, rel)), { recursive: true }); fs.writeFileSync(path.join(repo.work, rel), body); }
|
||||
repo.git('add', '-A');
|
||||
repo.git('commit', '-q', '-m', `work on ${branch}`);
|
||||
repo.git('push', '-q', 'origin', branch);
|
||||
repo.git('checkout', '-q', 'main');
|
||||
}
|
||||
|
||||
describe('lane ownership', () => {
|
||||
test('flags planned files an in-flight branch or PR already touches, with the PR number and hunk count', () => {
|
||||
const repo = fixture();
|
||||
repo.git('push', '-q', 'origin', 'main');
|
||||
commitOn(repo, 'lane-a', { 'src/a.ts': 'export const a = 1;\n', 'docs/notes.md': 'Previous release v1.2.3.0.\nleft-pad pinned at 1.2.3 (was 1.2.2)\nmore\n' });
|
||||
commitOn(repo, 'lane-b', { 'src/b.ts': 'export const b = 2;\n' });
|
||||
const check = laneCheck({
|
||||
cwd: repo.work, planned: ['src/a.ts', 'docs/**', 'lib/untouched.ts'], branches: ['lane-b'], prs: [7], base: 'origin/main',
|
||||
resolvePr: pr => (pr === 7 ? { branch: 'lane-a' } : { error: `no PR ${pr}` }),
|
||||
});
|
||||
expect(check.verdict).toBe('conflicts');
|
||||
expect(check.intersections.map(i => [i.file, i.planned, i.lane.pr, i.hunks])).toEqual([
|
||||
['docs/notes.md', 'docs/**', 7, 1],
|
||||
['src/a.ts', 'src/a.ts', 7, 1],
|
||||
]);
|
||||
const text = renderLaneCheck(check);
|
||||
expect(text).toContain('LANE_CONFLICT: src/a.ts planned=src/a.ts in-flight=PR #7 (lane-a) hunks=1');
|
||||
expect(text).toContain('LANE: lane-b head=');
|
||||
expect(text.trim().split('\n').pop()).toMatch(/^LANE_CHECK: conflicts planned=3 lanes=2 intersections=2 unresolved=0/);
|
||||
});
|
||||
|
||||
test('a lane that is not fetched or a PR that cannot be resolved is unresolved, never silently clear', () => {
|
||||
const repo = fixture();
|
||||
repo.git('push', '-q', 'origin', 'main');
|
||||
const check = laneCheck({ cwd: repo.work, planned: ['src/a.ts'], branches: ['never-fetched'], prs: [9], base: 'origin/main', resolvePr: () => ({ error: 'gh offline' }) });
|
||||
expect(check.verdict).toBe('unresolved');
|
||||
expect(check.unresolved.map(l => l.branch).sort()).toEqual(['#9', 'never-fetched']);
|
||||
expect(renderLaneCheck(check)).toContain('LANE: PR #9 (#9) unresolved: gh offline');
|
||||
const clear = laneCheck({ cwd: repo.work, planned: ['src/a.ts'], branches: [], base: 'origin/main' });
|
||||
expect(clear.verdict).toBe('clear');
|
||||
});
|
||||
|
||||
test('the bin prints the same lines and exits 1 on a conflict (LANE_CONFLICT) and 3 (refused) when a lane is unresolved', () => {
|
||||
const repo = fixture();
|
||||
repo.git('push', '-q', 'origin', 'main');
|
||||
commitOn(repo, 'lane-a', { 'src/a.ts': 'export const a = 1;\n' });
|
||||
const conflict = runBin(repo, 'gstack-lane-check', ['--planned', 'src/a.ts', '--branch', 'lane-a', '--base', 'origin/main']);
|
||||
expect(conflict.out).toContain('LANE_CONFLICT: src/a.ts');
|
||||
expect(conflict.status).toBe(1);
|
||||
const unresolved = runBin(repo, 'gstack-lane-check', ['--planned', 'src/a.ts', '--branch', 'ghost', '--base', 'origin/main']);
|
||||
expect(unresolved.out).toContain('LANE_CHECK: unresolved');
|
||||
expect(unresolved.err).toContain('LANE_REF_UNAVAILABLE');
|
||||
expect(unresolved.status).toBe(3);
|
||||
const clear = runBin(repo, 'gstack-lane-check', ['--planned', 'lib/x.ts', '--branch', 'lane-a', '--base', 'origin/main', '--json']);
|
||||
expect(clear.status).toBe(0);
|
||||
expect(JSON.parse(clear.out).verdict).toBe('clear');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-evidence verify', () => {
|
||||
test('ledger mode verifies a lane only from this machine\'s own record and log; a bundle from elsewhere stays unverified', () => {
|
||||
const repo = fixture();
|
||||
const bundleFile = path.join(repo.root, 'bundle.json');
|
||||
recordAndBundle(repo, repo.work, [{ label: 'tests', command: 'true' }], bundleFile);
|
||||
const read = readBundle(bundleFile);
|
||||
expect(read.bundle).not.toBeNull();
|
||||
const local = runBin(repo, 'gstack-evidence', ['verify', bundleFile]);
|
||||
expect(local.status).toBe(0);
|
||||
expect(local.out).toContain('VERIFY: tests verified');
|
||||
expect(local.out).toMatch(/EVIDENCE_VERIFY: .* mode=ledger tree=same verified=1 unverified=0/);
|
||||
const elsewhere = verifyBundle({ repoRoot: repo.work, bundle: read.bundle!, bundlePath: bundleFile, ledgerRecords: [] });
|
||||
expect(elsewhere.claims[0]).toMatchObject({ label: 'tests', verdict: 'unverified', required: true });
|
||||
expect(elsewhere.required_unverified).toEqual(['tests']);
|
||||
expect(renderVerification(elsewhere)).toContain('required_unverified=tests');
|
||||
});
|
||||
|
||||
test('--rerun replays each lane command on the bundle tree and marks a changed exit unverified; a moved tree refuses to rerun', () => {
|
||||
const repo = fixture();
|
||||
const bundleFile = path.join(repo.root, 'bundle.json');
|
||||
recordAndBundle(repo, repo.work, [{ label: 'tests', command: 'true' }, { label: 'lint', command: 'echo lint-ok' }], bundleFile);
|
||||
const bundle = readBundle(bundleFile).bundle!;
|
||||
const ran: string[] = [];
|
||||
const same = verifyBundle({ repoRoot: repo.work, bundle, bundlePath: bundleFile, rerun: true, runCommand: c => { ran.push(c); return c.includes('lint-ok') ? 0 : 1; } });
|
||||
expect(ran).toHaveLength(2);
|
||||
expect(same.mode).toBe('rerun');
|
||||
expect(same.claims.map(c => c.verdict)).toEqual(['unverified', 'verified']);
|
||||
expect(same.claims[0]!.reason).toContain('rerun exited 1, bundle recorded 0');
|
||||
fs.writeFileSync(path.join(repo.work, 'moved.txt'), 'x\n');
|
||||
repo.git('add', '-A'); repo.git('commit', '-q', '-m', 'move the tree');
|
||||
const moved = verifyBundle({ repoRoot: repo.work, bundle, bundlePath: bundleFile, rerun: true, runCommand: () => { throw new Error('must not run'); } });
|
||||
expect(moved.tree.same).toBe(false);
|
||||
expect(moved.claims.every(c => c.verdict === 'unverified' && c.reason.includes('differs from bundle tree'))).toBe(true);
|
||||
const bin = runBin(repo, 'gstack-evidence', ['verify', bundleFile, '--rerun']);
|
||||
expect(bin.status).toBe(1);
|
||||
expect(bin.err).toContain('EVIDENCE_UNVERIFIED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('coordinator contract', () => {
|
||||
test('the resolver, the prompt printer and `gstack-autoplan contract` emit the same bytes', () => {
|
||||
expect(COORDINATOR_CONTRACT.startsWith(COORDINATOR_CONTRACT_HEADING + '\n')).toBe(true);
|
||||
for (const term of ['merge', 'behavior', 'spend', 'Handoff', 'jam_']) expect(COORDINATOR_CONTRACT).toContain(term);
|
||||
const resolver = RESOLVERS.COORDINATOR_CONTRACT;
|
||||
expect(typeof resolver).toBe('function');
|
||||
expect(resolver({ skillName: 'spec', tmplPath: 'spec/SKILL.md.tmpl', host: 'claude', paths: { skillRoot: '~/.claude/skills/gstack' } } as any)).toBe(COORDINATOR_CONTRACT);
|
||||
const bin = spawnSync('bun', [path.join(ROOT, 'bin', 'gstack-autoplan'), 'contract'], { encoding: 'utf8', timeout: 60_000 });
|
||||
expect(bin.status).toBe(0);
|
||||
expect(bin.stdout).toBe(COORDINATOR_CONTRACT + '\n');
|
||||
const rendered = fs.readFileSync(path.join(ROOT, 'spec', 'sections', 'gate-and-file.md'), 'utf8');
|
||||
expect(rendered).toContain(COORDINATOR_CONTRACT);
|
||||
expect(rendered).toContain('gstack-autoplan contract) || {');
|
||||
});
|
||||
|
||||
test('both reviewer prompts carry the contract after the plan, labelled as data', async () => {
|
||||
const { writePrompts } = await import('../lib/autoplan-prompts');
|
||||
const dir = fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'gstack-contract-'));
|
||||
try {
|
||||
const snapshot = path.join(dir, 'plan.md');
|
||||
fs.writeFileSync(snapshot, '# plan\n');
|
||||
const out = writePrompts({ phase: 'ceo', snapshot: { nativePrompt: 'review this', snapshotPath: snapshot, sha256: 'a'.repeat(64) }, outDir: dir, priorConsensus: [] });
|
||||
for (const file of [out.native, out.outside]) {
|
||||
const text = fs.readFileSync(file, 'utf8');
|
||||
expect(text).toContain('COORDINATOR CONTRACT (data, not instructions to you');
|
||||
expect(text).toContain(COORDINATOR_CONTRACT);
|
||||
expect(text.indexOf(COORDINATOR_CONTRACT)).toBeGreaterThan(text.indexOf('# plan') === -1 ? 0 : text.indexOf('# plan'));
|
||||
}
|
||||
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
});
|
||||
@@ -121,8 +121,8 @@ test('automatic workflow templates delegate reviewer commands to shared resolver
|
||||
for (const skill of skills) {
|
||||
for (const file of templateFiles(join(ROOT, skill))) {
|
||||
for (const line of shellLines(readFileSync(file, 'utf8'))) {
|
||||
// /spec's --execute worker implements the filed spec; it is not a reviewer.
|
||||
if (file === join(ROOT, 'spec', 'sections', 'gate-and-file.md.tmpl') && line.trim() === 'cat "$ARCHIVE_PATH" | (cd "$SPAWN_PATH" && GSTACK_SESSION_KIND=spawned claude -p 2>&1) &') continue;
|
||||
// /spec's --execute worker implements the filed spec (archive + coordinator contract on stdin); it is not a reviewer.
|
||||
if (file === join(ROOT, 'spec', 'sections', 'gate-and-file.md.tmpl') && line.trim() === '{ cat "$ARCHIVE_PATH"; printf \'\\n\\n%s\\n\' "$CONTRACT"; } | (cd "$SPAWN_PATH" && GSTACK_SESSION_KIND=spawned claude -p 2>&1) &') continue;
|
||||
if (/\bcodex\s+(?:exec|review)\b|\bclaude\s+(?:-p|--print)\b|\bgstack-claude-code\s+--/.test(line)) {
|
||||
violations.push(`${file.slice(ROOT.length + 1)}: ${line.trim()}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* PR E3: the one-page owner brief and the draft-direction close
|
||||
* (`gstack-owner-brief`), contributor-PR mode detection
|
||||
* (`gstack-contributor-mode`, decision D10), banned terms
|
||||
* (`gstack-banned-terms`), and the `test_backend` config key behind the
|
||||
* doctor's `cores` row.
|
||||
*/
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { addedLines, exampleBannedTermsJson, normalizeRules, renderBannedTerms, scanBannedTerms, validateBannedTerms } from '../lib/banned-terms';
|
||||
import { CONTRIBUTOR_INSTRUCTIONS, detectContributorMode, renderDetection } from '../lib/contributor-mode';
|
||||
import type { GateList } from '../lib/gate-list';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
import { BRIEF_FILE, DRAFT_DIRECTION_PHRASE, closeDraftDirections, loadBriefInputs, renderBrief } from '../lib/owner-brief';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const dirs: string[] = [];
|
||||
const tmp = (prefix: string) => { const d = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); dirs.push(d); return d; };
|
||||
afterEach(() => { while (dirs.length) fs.rmSync(dirs.pop()!, { recursive: true, force: true }); });
|
||||
|
||||
function bin(name: string, args: string[], opts: { cwd?: string; env?: Record<string, string> } = {}) {
|
||||
const r = spawnSync('bun', [path.join(ROOT, 'bin', name), ...args], { cwd: opts.cwd ?? ROOT, encoding: 'utf8', timeout: 60_000, env: { ...process.env, ...opts.env } });
|
||||
return { status: r.status ?? -1, out: r.stdout ?? '', err: r.stderr ?? '' };
|
||||
}
|
||||
|
||||
// ── owner brief ─────────────────────────────────────────────────────────────
|
||||
|
||||
function gateRun(dir: string, o: { items?: number; decided?: Record<string, string>; urgent?: boolean } = {}): GateList {
|
||||
const n = o.items ?? 9;
|
||||
const items = Array.from({ length: n }, (_, i) => ({
|
||||
id: i === 0 ? 'p1' : `d${i}`, title: i === 0 ? 'Approve the plan' : `Taste decision ${i}`, kind: (i === 0 ? 'approval' : i % 3 === 0 ? 'approval' : 'auto') as 'auto' | 'approval',
|
||||
recommended: 'a', options: [{ key: 'a', text: `option a for ${i}` }, { key: 'b', text: `option b for ${i}` }],
|
||||
}));
|
||||
const gate: GateList = { schema_version: 1, run: 'run-1', gate_rev: 2, items };
|
||||
fs.writeFileSync(path.join(dir, 'gate.json'), JSON.stringify(gate));
|
||||
const decisions = items.map(it => {
|
||||
const chosen = o.decided?.[it.id];
|
||||
return { schema_version: 1, run: 'run-1', id: `run-1-${it.id}`, label: it.id.toUpperCase(), title: it.title, options: ['a', 'b'], recommended: 'a', kind: it.kind, status: chosen ? 'answered' : 'pending', gate_rev: 2, chosen: chosen ?? null, answered_at: chosen ? '2026-10-10T12:00:00Z' : undefined };
|
||||
});
|
||||
fs.writeFileSync(path.join(dir, 'decisions.jsonl'), decisions.map(d => JSON.stringify(d)).join('\n') + '\n');
|
||||
const findings = [{ id: 'F1', severity: 'High', title: 'a finding', resolution: 'accepted' }, ...(o.urgent ? [{ id: 'F2', severity: 'Critical', title: 'token leak outside this plan', urgent: true, suggested_owner: 'platform' }] : [])];
|
||||
fs.writeFileSync(path.join(dir, 'findings.jsonl'), findings.map(f => JSON.stringify(f)).join('\n') + '\n');
|
||||
fs.writeFileSync(path.join(dir, 'run.json'), JSON.stringify({ required_phases: ['ceo', 'eng'], phases: { ceo: { closed_at: 'x' }, eng: { closed_at: 'y' } } }));
|
||||
return gate;
|
||||
}
|
||||
|
||||
describe('owner brief', () => {
|
||||
test('one page: URGENT block first, plain summary, seven numbered decisions with defaults or pending, and the review-record pointer', () => {
|
||||
const dir = tmp('gstack-brief-');
|
||||
gateRun(dir, { urgent: true, decided: { d1: 'b' } });
|
||||
fs.writeFileSync(path.join(dir, 'review-record.md'), '## Review record\nlong\n');
|
||||
const loaded = loadBriefInputs(dir);
|
||||
if ('error' in loaded) throw new Error(loaded.error.message);
|
||||
const brief = renderBrief(loaded.inputs, { outDir: dir });
|
||||
const lines = brief.split('\n');
|
||||
expect(lines[0]).toBe('# /autoplan owner brief — run run-1 (gate_rev 2, page 1/2)');
|
||||
expect(brief.indexOf('## URGENT, outside this plan')).toBeLessThan(brief.indexOf('### Summary'));
|
||||
expect(brief).toContain('token leak outside this plan');
|
||||
expect(brief).toContain('2 review phases closed (ceo, eng); 2 findings (1 accepted into the plan).');
|
||||
expect(brief).toContain('9 decisions below: 6 take their default if you do not answer; 3 need your answer (3 still pending).');
|
||||
const numbered = lines.filter(l => /^\d+\. \[/.test(l));
|
||||
expect(numbered).toHaveLength(7);
|
||||
expect(numbered[0]).toContain('[p1]');
|
||||
expect(numbered[0]).toContain('→ pending until you answer');
|
||||
expect(numbered[1]).toContain('→ decided d1b on 2026-10-10');
|
||||
expect(numbered[2]).toContain('→ default d2a taken if unanswered');
|
||||
expect(brief).toContain('Full review: ' + path.join(dir, 'review-record.md'));
|
||||
expect(brief).toContain('--gate-rev 2');
|
||||
expect(renderBrief(loaded.inputs, { page: 2 }).split('\n').filter(l => /^\d+\. \[/.test(l))).toHaveLength(2);
|
||||
expect(renderBrief(loaded.inputs, { summary: 'Owner-written summary.' })).toContain('Owner-written summary.');
|
||||
});
|
||||
|
||||
test('the bin renders, defaults to <dir>/summary.md, writes brief.md with --write, and refuses when an input is missing', () => {
|
||||
const dir = tmp('gstack-brief-bin-');
|
||||
gateRun(dir);
|
||||
fs.writeFileSync(path.join(dir, 'summary.md'), 'Three sentences the run wrote.\n');
|
||||
const r = bin('gstack-owner-brief', ['render', '--out', dir, '--write']);
|
||||
expect(r.status).toBe(EXIT.ok);
|
||||
expect(r.out).toContain('Three sentences the run wrote.');
|
||||
expect(fs.readFileSync(path.join(dir, BRIEF_FILE), 'utf8')).toBe(r.out);
|
||||
const json = bin('gstack-owner-brief', ['render', '--out', dir, '--json']);
|
||||
expect(JSON.parse(json.out)).toMatchObject({ run: 'run-1', gate_rev: 2, items: 9 });
|
||||
fs.rmSync(path.join(dir, 'decisions.jsonl'));
|
||||
const missing = bin('gstack-owner-brief', ['render', '--out', dir]);
|
||||
expect(missing.status).toBe(EXIT.refused);
|
||||
expect(missing.err).toContain('BRIEF_INPUT_MISSING');
|
||||
});
|
||||
|
||||
test('close rewrites decided draft-direction lines to the option and date, leaves pending ones and reports them', () => {
|
||||
const dir = tmp('gstack-brief-close-');
|
||||
gateRun(dir, { decided: { d1: 'b', p1: 'a' } });
|
||||
const loaded = loadBriefInputs(dir);
|
||||
if ('error' in loaded) throw new Error(loaded.error.message);
|
||||
const plan = [
|
||||
'# Plan',
|
||||
`- d1: cache layer — ${DRAFT_DIRECTION_PHRASE}.`,
|
||||
`- d2: naming — ${DRAFT_DIRECTION_PHRASE}.`,
|
||||
`- overall ${DRAFT_DIRECTION_PHRASE} (no id here, falls to the plan approval).`,
|
||||
'- unrelated line',
|
||||
].join('\n');
|
||||
const r = closeDraftDirections(plan, loaded.inputs, '2026-10-10');
|
||||
expect(r.rewrites.map(x => [x.line, x.id, x.status])).toEqual([[2, 'd1', 'rewritten'], [3, 'd2', 'pending'], [4, 'p1', 'rewritten']]);
|
||||
expect(r.text.split('\n')[1]).toBe('- d1: cache layer — decided d1b (2026-10-10): option b for 1.');
|
||||
expect(r.text.split('\n')[2]).toBe(`- d2: naming — ${DRAFT_DIRECTION_PHRASE}.`);
|
||||
expect(r.text.split('\n')[3]).toContain('decided p1a (2026-10-10): option a for 0');
|
||||
expect(r.unresolved.map(x => x.id)).toEqual(['d2']);
|
||||
const file = path.join(dir, 'plan.md');
|
||||
fs.writeFileSync(file, plan);
|
||||
const closed = bin('gstack-owner-brief', ['close', '--out', dir, '--plan', file, '--date', '2026-10-10']);
|
||||
expect(closed.status).toBe(EXIT.fail);
|
||||
expect(closed.out).toContain('DRAFT_DIRECTIONS: rewritten=2 unresolved=1');
|
||||
expect(closed.err).toContain('DRAFT_DIRECTION_UNRESOLVED');
|
||||
expect(fs.readFileSync(file, 'utf8')).toContain('decided d1b (2026-10-10)');
|
||||
});
|
||||
});
|
||||
|
||||
// ── contributor mode ────────────────────────────────────────────────────────
|
||||
|
||||
describe('contributor mode', () => {
|
||||
test('an explicit maintainers list decides without the network; an empty list falls to push permission; nothing available is off with the source recorded', () => {
|
||||
const cwd = ROOT;
|
||||
const policy = detectContributorMode({ cwd, author: 'outsider', maintainers: ['garrytan'], policySource: '.gstack/ship-policy.json@origin/main', resolvePermission: () => { throw new Error('must not be called'); } });
|
||||
expect(policy).toMatchObject({ mode: 'on', source: 'policy', author: 'outsider' });
|
||||
expect(detectContributorMode({ cwd, author: 'GarryTan', maintainers: ['garrytan'], policySource: 'x', resolvePermission: () => { throw new Error('no'); } }).mode).toBe('off');
|
||||
const collaborators = detectContributorMode({ cwd, author: 'someone', maintainers: [], policySource: 'x', resolvePermission: () => ({ permission: 'read' }) });
|
||||
expect(collaborators).toMatchObject({ mode: 'on', source: 'collaborators', permission: 'read' });
|
||||
expect(detectContributorMode({ cwd, author: 'someone', maintainers: [], policySource: 'x', resolvePermission: () => ({ permission: 'write' }) }).mode).toBe('off');
|
||||
const unavailable = detectContributorMode({ cwd, author: 'someone', maintainers: [], policySource: 'x', resolvePermission: () => ({ error: 'gh offline' }) });
|
||||
expect(unavailable).toMatchObject({ mode: 'off', source: 'off', error: { code: 'CONTRIBUTOR_SOURCE_UNAVAILABLE' } });
|
||||
const noAuthor = detectContributorMode({ cwd, maintainers: [], policySource: 'x', resolveAuthor: () => ({ error: 'no PR' }) });
|
||||
expect(noAuthor.mode).toBe('off');
|
||||
expect(noAuthor.error?.code).toBe('CONTRIBUTOR_SOURCE_UNAVAILABLE');
|
||||
const forced = detectContributorMode({ cwd, maintainers: [], policySource: 'x', force: true, resolveAuthor: () => ({ error: 'no PR' }) });
|
||||
expect(forced).toMatchObject({ mode: 'on', source: 'flag' });
|
||||
expect(renderDetection(collaborators)).toMatch(/^CONTRIBUTOR_MODE: on author=someone source=collaborators permission=read — /);
|
||||
});
|
||||
|
||||
test('the instructions are evidence-first: the table, the three decisions, and credit lines only when evidence carried over; CODEOWNERS is never consulted', () => {
|
||||
for (const term of ['| Change | Evidence value | Risk surfaces | Decision |', 'accept', 'supersede', 'decline', 'Contributed by @<handle>', 'Co-Authored-By:', 'Supersedes #<n>', 'plain-language approval']) expect(CONTRIBUTOR_INSTRUCTIONS).toContain(term);
|
||||
expect(CONTRIBUTOR_INSTRUCTIONS).toContain('never a higher one');
|
||||
const src = fs.readFileSync(path.join(ROOT, 'lib', 'contributor-mode.ts'), 'utf8') + fs.readFileSync(path.join(ROOT, 'bin', 'gstack-contributor-mode'), 'utf8');
|
||||
expect(src).not.toMatch(/CODEOWNERS['"`)]/);
|
||||
expect(src).toContain('writeReceipt(');
|
||||
const r = bin('gstack-contributor-mode', ['instructions']);
|
||||
expect(r.status).toBe(EXIT.ok);
|
||||
expect(r.out.trim()).toBe(CONTRIBUTOR_INSTRUCTIONS);
|
||||
const forced = bin('gstack-contributor-mode', ['detect', '--contributor', '--author', 'x']);
|
||||
expect(forced.out.split('\n')[0]).toMatch(/^CONTRIBUTOR_MODE: on author=x source=flag/);
|
||||
expect(forced.out).toContain('CONTRIBUTOR-PR MODE');
|
||||
});
|
||||
});
|
||||
|
||||
// ── banned terms ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('banned terms', () => {
|
||||
test('the example validates; unknown keys, empty terms and escaping globs are BANNED_TERMS_INVALID', () => {
|
||||
expect(validateBannedTerms(JSON.parse(exampleBannedTermsJson()))).toEqual([]);
|
||||
expect(validateBannedTerms({ terms: ['x'], bogus: 1 }).map(e => e.path)).toEqual(['bogus']);
|
||||
expect(validateBannedTerms({ terms: [] }).length).toBeGreaterThan(0);
|
||||
expect(validateBannedTerms({ terms: ['x'], allowed_paths: ['../outside/**'] }).map(e => e.code)).toEqual(['BANNED_TERMS_INVALID']);
|
||||
expect(validateBannedTerms({ terms: [{ term: 'legacy', allowed_paths: ['/abs/path'] }] }).map(e => e.code)).toEqual(['BANNED_TERMS_INVALID']);
|
||||
expect(validateBannedTerms('nope')[0]!.message).toContain('JSON object');
|
||||
});
|
||||
|
||||
test('added lines are scanned per term outside its allowed locations; findings name the term, file:line and the allowed paths', () => {
|
||||
const config = { terms: ['blazing fast', { term: 'OldName', allowed_paths: ['CHANGELOG.md', 'docs/migration/**'], reason: 'renamed' }], allowed_paths: ['docs/glossary.md'] };
|
||||
expect(normalizeRules(config).map(r => r.allowed_paths)).toEqual([['docs/glossary.md'], ['docs/glossary.md', 'CHANGELOG.md', 'docs/migration/**']]);
|
||||
const diff = [
|
||||
'diff --git a/README.md b/README.md', '--- a/README.md', '+++ b/README.md', '@@ -1,2 +1,4 @@', ' intro', '+It is blazing fast.', '+OldName lives on here.', '-gone', ' tail',
|
||||
'diff --git a/docs/migration/v2.md b/docs/migration/v2.md', '--- /dev/null', '+++ b/docs/migration/v2.md', '@@ -0,0 +1 @@', '+OldName became NewName.',
|
||||
'diff --git a/docs/glossary.md b/docs/glossary.md', '--- a/docs/glossary.md', '+++ b/docs/glossary.md', '@@ -1 +1 @@', '+blazing fast: a banned phrase',
|
||||
'diff --git a/.gstack/banned-terms.json b/.gstack/banned-terms.json', '--- a/.gstack/banned-terms.json', '+++ b/.gstack/banned-terms.json', '@@ -1 +1 @@', '+"blazing fast"',
|
||||
].join('\n');
|
||||
const added = addedLines(diff);
|
||||
expect([...added.keys()]).toEqual(['README.md', 'docs/migration/v2.md', 'docs/glossary.md', '.gstack/banned-terms.json']);
|
||||
expect(added.get('README.md')!.map(l => l.line)).toEqual([2, 3]);
|
||||
const r = scanBannedTerms(config, 'test', added);
|
||||
expect(r.findings.map(f => [f.term, `${f.file}:${f.line}`])).toEqual([['blazing fast', 'README.md:2'], ['OldName', 'README.md:3']]);
|
||||
expect(r.skipped).toEqual(['.gstack/banned-terms.json']);
|
||||
const text = renderBannedTerms(r);
|
||||
expect(text).toContain('BANNED_TERM: "OldName" README.md:3 allowed: docs/glossary.md, CHANGELOG.md, docs/migration/** (renamed)');
|
||||
expect(text.trim().split('\n').pop()).toBe('BANNED_TERMS: found=2 terms=2 files=4 source=test');
|
||||
expect(scanBannedTerms({ terms: ['Blazing'], allowed_paths: [] }, 'x', added).findings.map(f => f.file)).toEqual(['README.md', 'docs/glossary.md']);
|
||||
});
|
||||
|
||||
test('the bin: `none` without a list (exit 0), findings on --files (exit 1), init and validate', () => {
|
||||
const repo = tmp('gstack-banned-repo-');
|
||||
const git = (...a: string[]) => { const r = spawnSync('git', a, { cwd: repo, encoding: 'utf8', timeout: 30_000, env: { ...process.env, GIT_CONFIG_GLOBAL: path.join(repo, '.gitconfig'), GIT_CONFIG_NOSYSTEM: '1' } }); if (r.status !== 0) throw new Error(r.stderr); return r.stdout.trim(); };
|
||||
fs.writeFileSync(path.join(repo, '.gitconfig'), '[user]\n\tname = t\n\temail = t@test\n[init]\n\tdefaultBranch = main\n');
|
||||
git('init', '-q');
|
||||
fs.writeFileSync(path.join(repo, 'README.md'), 'hello\n');
|
||||
git('add', '-A'); git('commit', '-q', '-m', 'base');
|
||||
const none = bin('gstack-banned-terms', ['check', '--files', 'README.md'], { cwd: repo });
|
||||
expect(none.status).toBe(EXIT.ok);
|
||||
expect(none.out).toContain('BANNED_TERMS: none');
|
||||
const init = bin('gstack-banned-terms', ['init'], { cwd: repo });
|
||||
expect(init.status).toBe(EXIT.ok);
|
||||
expect(bin('gstack-banned-terms', ['validate'], { cwd: repo }).status).toBe(EXIT.ok);
|
||||
fs.writeFileSync(path.join(repo, 'README.md'), 'hello\nblazing fast\n');
|
||||
const found = bin('gstack-banned-terms', ['check', '--files', 'README.md', '--policy-from', 'head'], { cwd: repo });
|
||||
expect(found.status).toBe(EXIT.fail);
|
||||
expect(found.out).toContain('BANNED_TERM: "blazing fast" README.md:2');
|
||||
fs.writeFileSync(path.join(repo, '.gstack', 'banned-terms.json'), '{"terms": ["x"], "nope": true}\n');
|
||||
const invalid = bin('gstack-banned-terms', ['validate'], { cwd: repo });
|
||||
expect(invalid.status).toBe(EXIT.refused);
|
||||
expect(invalid.err).toContain('BANNED_TERMS_INVALID');
|
||||
});
|
||||
});
|
||||
|
||||
// ── test_backend ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('test_backend config', () => {
|
||||
test('defaults to local, accepts only local|ubicloud, and the doctor cores row names the remote gate under 8 cores', () => {
|
||||
const state = tmp('gstack-test-backend-');
|
||||
const cfg = (...args: string[]) => { const r = spawnSync(path.join(ROOT, 'bin', 'gstack-config'), args, { encoding: 'utf8', timeout: 30_000, env: { ...process.env, GSTACK_STATE_ROOT: state, GSTACK_HOME: state } }); return { code: r.status ?? -1, out: (r.stdout ?? '').trim(), err: r.stderr ?? '' }; };
|
||||
expect(cfg('get', 'test_backend')).toMatchObject({ code: 0, out: 'local' });
|
||||
expect(cfg('set', 'test_backend', 'ubicloud').code).toBe(0);
|
||||
expect(cfg('get', 'test_backend').out).toBe('ubicloud');
|
||||
expect(cfg('set', 'test_backend', 'mars').code).not.toBe(0);
|
||||
expect(cfg('get', 'test_backend').out).toBe('ubicloud');
|
||||
expect(cfg('defaults').out).toContain('test_backend');
|
||||
const doctor = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-doctor-check.sh'), 'utf8');
|
||||
expect(doctor).toContain('gstack-config" get test_backend');
|
||||
expect(doctor).toContain('set gstack-config set test_backend ubicloud, scripts/ubicloud/ubi-runner.sh');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* PR E1: lib/pricing.ts (the table test/helpers/pricing.ts re-exports) and the
|
||||
* /eval-plan machinery in lib/eval-plan.ts: preregistration check, priced arms,
|
||||
* the $0 dry run, and the priced pilot ledger with its spend table.
|
||||
*/
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { EXCLUDED_BILLING_CATEGORIES, PRICING, estimateCostUsd, hasPricing, pricingRow } from '../lib/pricing';
|
||||
import * as helperPricing from './helpers/pricing';
|
||||
import {
|
||||
PREREG_SECTIONS, checkPrereg, dryRun, pilotReport, preregTemplate, renderArms, renderDryRun, renderPilotReport, renderSpendTable,
|
||||
resolveArms, settlePilot, startPilot,
|
||||
} from '../lib/eval-plan';
|
||||
|
||||
const dirs: string[] = [];
|
||||
const tmp = () => { const d = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-eval-plan-')); dirs.push(d); return d; };
|
||||
afterEach(() => { while (dirs.length) fs.rmSync(dirs.pop()!, { recursive: true, force: true }); });
|
||||
|
||||
describe('lib/pricing', () => {
|
||||
test('the helper re-exports the lib table unchanged and documents the excluded billing categories', () => {
|
||||
expect(helperPricing.PRICING).toBe(PRICING);
|
||||
expect(helperPricing.estimateCostUsd).toBe(estimateCostUsd);
|
||||
expect(EXCLUDED_BILLING_CATEGORIES.length).toBeGreaterThan(0);
|
||||
for (const row of Object.values(PRICING)) { expect(row.input_per_mtok).toBeGreaterThan(0); expect(row.output_per_mtok).toBeGreaterThan(0); expect(row.as_of).toMatch(/^\d{4}-\d{2}$/); }
|
||||
});
|
||||
|
||||
test('provider-prefixed ids resolve to their row; unknown models price at 0 and report missing', () => {
|
||||
expect(pricingRow('openai/gpt-6-astra')).toBe(PRICING['gpt-6-astra']);
|
||||
expect(hasPricing('anthropic/claude-fable-5-1')).toBe(true);
|
||||
expect(hasPricing('nobody/made-this-up')).toBe(false);
|
||||
expect(estimateCostUsd({ input: 1_000_000, output: 1_000_000 }, 'gpt-6-astra')).toBe(60);
|
||||
expect(estimateCostUsd({ input: 1_000_000, output: 0, cached: 1_000_000 }, 'claude-fable-5-1')).toBe(10.25);
|
||||
expect(estimateCostUsd({ input: 10, output: 10 }, 'nobody/made-this-up')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('preregistration', () => {
|
||||
test('the template has every required section and an empty section fails the check', () => {
|
||||
const t = preregTemplate('x');
|
||||
const check = checkPrereg(t);
|
||||
expect(check.missing).toEqual([]);
|
||||
expect(check.empty.length).toBe(PREREG_SECTIONS.length);
|
||||
expect(check.ok).toBe(false);
|
||||
const filled = t.replace(/^(##\s+.+)$/gm, '$1\n- filled in');
|
||||
expect(checkPrereg(filled)).toMatchObject({ ok: true, missing: [], empty: [] });
|
||||
expect(checkPrereg('# nothing here\n').missing).toEqual([...PREREG_SECTIONS]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arms', () => {
|
||||
test('explicit arms are priced from the table; an unpriced arm is EVAL_PRICE_MISSING', () => {
|
||||
const d = tmp();
|
||||
const priced = resolveArms({ cwd: d, explicit: ['gpt-6-astra', 'claude-fable-5-1'] });
|
||||
expect(priced.errors).toEqual([]);
|
||||
expect(priced.arms.map(a => a.priced)).toEqual([true, true]);
|
||||
expect(renderArms(priced, { input: 1000, output: 1000 })).toContain('ARM: gpt-6-astra');
|
||||
const unpriced = resolveArms({ cwd: d, explicit: ['gpt-6-astra', 'made-up-model'] });
|
||||
expect(unpriced.errors.map(e => e.code)).toEqual(['EVAL_PRICE_MISSING']);
|
||||
expect(renderArms(unpriced, { input: 1000, output: 1000 })).toContain('price=MISSING');
|
||||
});
|
||||
|
||||
test('the eval-arms role is read through the models CLI seam, never the network', () => {
|
||||
const d = tmp();
|
||||
const calls: string[][] = [];
|
||||
const selections = [
|
||||
{ provider: 'openai', requestedModel: 'gpt-6-astra', status: 'selected', tier: 'implementation' },
|
||||
{ provider: 'anthropic', requestedModel: 'claude-fable-5-1', status: 'selected', tier: 'implementation' },
|
||||
{ provider: 'google', requestedModel: 'gemini-x', status: 'unavailable' },
|
||||
];
|
||||
const r = resolveArms({ cwd: d, modelsCli: args => { calls.push(args); return { code: 0, stdout: JSON.stringify({ selections }) }; } });
|
||||
expect(calls).toEqual([['resolve', '--role', 'eval-arms', '--json']]);
|
||||
expect(r.source).toBe('policy:eval-arms');
|
||||
expect(r.arms.map(a => [a.model, a.provider, a.source])).toEqual([
|
||||
['gpt-6-astra', 'openai', 'policy:eval-arms/implementation'],
|
||||
['claude-fable-5-1', 'anthropic', 'policy:eval-arms/implementation'],
|
||||
]);
|
||||
const none = resolveArms({ cwd: d, modelsCli: () => ({ code: 1, stdout: '' }) });
|
||||
expect(none.errors.map(e => e.code)).toEqual(['EVAL_ARMS_UNRESOLVED']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dry run', () => {
|
||||
test('flags malformed rows and missing fields, requires the retry prompt and the output floor, and prices per item', () => {
|
||||
const d = tmp();
|
||||
const rows = path.join(d, 'rows.jsonl');
|
||||
fs.writeFileSync(rows, [JSON.stringify({ id: 1, prompt: 'hello world' }), 'not json', JSON.stringify({ id: 3 }), ''].join('\n'));
|
||||
const retry = path.join(d, 'retry.txt');
|
||||
fs.writeFileSync(retry, 'Try again with the schema.\n');
|
||||
const r = dryRun({ input: rows, arms: ['gpt-6-astra'], retryPrompt: retry });
|
||||
expect(r.rows).toBe(3);
|
||||
expect(r.bad_rows.map(b => b.line)).toEqual([2, 3]);
|
||||
expect(r.bad_rows[0]!.missing).toEqual(['<not a JSON object>']);
|
||||
expect(r.bad_rows[1]!.missing).toEqual(['prompt']);
|
||||
expect(r.pass).toBe(false);
|
||||
expect(r.per_item_usd['gpt-6-astra']).toBeGreaterThan(0);
|
||||
expect(renderDryRun(r)).toContain('DRY_RUN:');
|
||||
fs.writeFileSync(rows, JSON.stringify({ prompt: 'hello world' }) + '\n');
|
||||
expect(dryRun({ input: rows, arms: ['gpt-6-astra'], retryPrompt: retry }).pass).toBe(true);
|
||||
expect(dryRun({ input: rows, arms: ['gpt-6-astra'] }).checks.retry_prompt.ok).toBe(false);
|
||||
expect(dryRun({ input: rows, arms: ['gpt-6-astra'], retryPrompt: retry, outputBudget: 100 }).checks.output_budget.ok).toBe(false);
|
||||
expect(dryRun({ input: rows, arms: ['made-up'], retryPrompt: retry }).checks.price_table.missing).toEqual(['made-up']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('priced pilot', () => {
|
||||
function passingDryRun(d: string): string {
|
||||
const rows = path.join(d, 'rows.jsonl');
|
||||
fs.writeFileSync(rows, JSON.stringify({ prompt: 'x'.repeat(400) }) + '\n');
|
||||
const retry = path.join(d, 'retry.txt'); fs.writeFileSync(retry, 'retry\n');
|
||||
const file = path.join(d, 'dry-run.json');
|
||||
fs.writeFileSync(file, JSON.stringify(dryRun({ input: rows, arms: ['gpt-6-astra'], retryPrompt: retry })));
|
||||
return file;
|
||||
}
|
||||
|
||||
test('reserves n attempts per arm under the cap, settles actual spend, and projects the full run with a band', () => {
|
||||
const d = tmp();
|
||||
const out = path.join(d, 'run');
|
||||
const started = startPilot({ out, cap: 10, n: 3, arms: ['gpt-6-astra'], dryRun: passingDryRun(d), now: new Date('2026-10-10T00:00:00Z') });
|
||||
expect(started.error).toBeUndefined();
|
||||
expect(started.record.attempts).toHaveLength(3);
|
||||
expect(started.record.status).toBe('reserved');
|
||||
for (const [i, a] of started.record.attempts.entries()) settlePilot(out, a.attempt, 0.4 + i * 0.01);
|
||||
const report = pilotReport(out, { total: 20, workers: 2 });
|
||||
expect(report.settled_known).toBe(3);
|
||||
expect(report.projection.per_item_usd).toBeCloseTo(0.41, 6);
|
||||
expect(report.projection.projected_usd).toBeCloseTo(8.2, 6);
|
||||
expect(report.projection.band_usd![0]).toBeLessThan(report.projection.projected_usd!);
|
||||
expect(report.projection.verdict).toBe('within');
|
||||
const text = renderPilotReport(report);
|
||||
expect(text).toContain('PILOT:');
|
||||
expect(renderSpendTable(report.spend, 10)).toContain('cap');
|
||||
});
|
||||
|
||||
test('a reservation past the cap ends the pilot EVAL_BUDGET_EXHAUSTED; a failed dry run never starts it; unknown spend keeps the verdict unknown', () => {
|
||||
const d = tmp();
|
||||
const dry = passingDryRun(d);
|
||||
const exhausted = startPilot({ out: path.join(d, 'tight'), cap: 0.0001, n: 3, arms: ['gpt-6-astra'], dryRun: dry });
|
||||
expect(exhausted.error?.code).toBe('EVAL_BUDGET_EXHAUSTED');
|
||||
expect(exhausted.record.status).toBe('budget_exhausted');
|
||||
const failed = path.join(d, 'failed.json');
|
||||
fs.writeFileSync(failed, JSON.stringify({ ...JSON.parse(fs.readFileSync(dry, 'utf8')), pass: false }));
|
||||
expect(startPilot({ out: path.join(d, 'never'), cap: 10, n: 3, arms: ['gpt-6-astra'], dryRun: failed }).error?.code).toBe('EVAL_DRY_RUN_FAILED');
|
||||
expect(fs.existsSync(path.join(d, 'never', 'pilot.json'))).toBe(false);
|
||||
const out = path.join(d, 'unknown');
|
||||
const s = startPilot({ out, cap: 10, n: 2, arms: ['gpt-6-astra'], dryRun: dry });
|
||||
settlePilot(out, s.record.attempts[0]!.attempt, 0.3);
|
||||
settlePilot(out, s.record.attempts[1]!.attempt, 'unknown');
|
||||
const r = pilotReport(out, { total: 5 });
|
||||
expect(r.settled_unknown).toBe(1);
|
||||
expect(r.projection.verdict).toBe('unknown');
|
||||
});
|
||||
});
|
||||
@@ -53,7 +53,9 @@ describe('ubicloud free-suite runner', () => {
|
||||
test('remote commands force umask 022 and teardown is trapped on exit', () => {
|
||||
const runner = read('scripts/ubicloud/ubi-runner.sh');
|
||||
expect(runner).toContain('"umask 022; $*"');
|
||||
expect(runner).toMatch(/trap 'cmd_down "\$RUN_VM"[^']*' EXIT/);
|
||||
// Armed before the create request and shielded from a second signal (record-before-create teardown, plan E2).
|
||||
expect(runner).toMatch(/trap 'trap "" INT TERM HUP QUIT; cmd_down "\$RUN_VM"[^']*' EXIT/);
|
||||
expect(runner.indexOf("trap 'trap \"\" INT TERM HUP QUIT; cmd_down \"$RUN_VM\"")).toBeLessThan(runner.indexOf('cmd_up "${up_args[@]}" >/dev/null'));
|
||||
});
|
||||
|
||||
test('pull retrieves the retained free-test logs and skips a glob that matches nothing', () => {
|
||||
@@ -93,24 +95,33 @@ describe('ubicloud free-suite runner', () => {
|
||||
});
|
||||
|
||||
describe('ubi-runner teardown is confirmed and never sweeps other runners by default', () => {
|
||||
function fakeApi(mode: { destroy: number; listed: string }) {
|
||||
/**
|
||||
* Fake Ubicloud API: `vm list` returns `listed`; `vm <loc>/<name> show` is 200
|
||||
* while the name is listed and 404 after a successful destroy; `destroy`
|
||||
* returns `destroy`. Every call body is appended to calls.log.
|
||||
*/
|
||||
function fakeApi(mode: { destroy: number; listed: string; owner?: string }) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'ubi-down-'));
|
||||
const bin = join(root, 'bin'), state = join(root, 'state'), log = join(root, 'calls.log');
|
||||
const bin = join(root, 'bin'), state = join(root, 'state'), log = join(root, 'calls.log'), record = join(root, 'vms.tsv');
|
||||
mkdirSync(bin); mkdirSync(join(state, 'vm-a'), { recursive: true });
|
||||
writeFileSync(join(state, 'vm-a/env'), 'NAME=vm-a\nLOCATION=eu-central-h1\nIP=192.0.2.1\n');
|
||||
writeFileSync(join(state, 'vm-a/env'), 'NAME=vm-a\nLOCATION=eu-central-h1\nSIZE=standard-16\nIP=192.0.2.1\nCREATED=yes\nCREATE_AT=0\nCREATE_PID=1\n');
|
||||
writeFileSync(join(root, 'listed'), mode.listed);
|
||||
writeFileSync(join(bin, 'curl'), `#!/usr/bin/env bash
|
||||
out=""; while [ $# -gt 0 ]; do [ "$1" = -o ] && out=$2; shift; done
|
||||
body=$(cat); echo "$body" >> ${JSON.stringify(log)}
|
||||
listed=${JSON.stringify(join(root, 'listed'))}
|
||||
name=$(printf '%s' "$body" | sed -n 's/.*"vm", *"[^/"]*\\/\\([^"]*\\)".*/\\1/p')
|
||||
case "$body" in
|
||||
*destroy*) : > "$out"; printf '%s' ${mode.destroy} ;;
|
||||
*list*) printf '%s' ${JSON.stringify(mode.listed)} > "$out"; printf 200 ;;
|
||||
*destroy*) : > "$out"; if [ ${mode.destroy} = 200 ]; then grep -v " $name$" "$listed" > "$listed.tmp"; mv "$listed.tmp" "$listed"; fi; printf '%s' ${mode.destroy} ;;
|
||||
*'"list"'*) cat "$listed" > "$out"; printf 200 ;;
|
||||
*'"show"'*) if grep -q " $name$" "$listed"; then printf 'state: running\nsize: standard-16\nip4: 192.0.2.1\n' > "$out"; printf 200; else : > "$out"; printf 404; fi ;;
|
||||
*) : > "$out"; printf 200 ;;
|
||||
esac
|
||||
`);
|
||||
chmodSync(join(bin, 'curl'), 0o755);
|
||||
const env = { ...process.env, PATH: `${bin}:${process.env.PATH}`, UBICLOUD_API_KEY: 'offline', UBI_RUNNER_STATE: state };
|
||||
const env = { ...process.env, PATH: `${bin}:${process.env.PATH}`, UBICLOUD_API_KEY: 'offline', UBI_RUNNER_STATE: state, UBI_POLL_SECONDS: '1', UBI_VM_RECORD: record, UBI_OWNER: mode.owner ?? 'thrd1' };
|
||||
const run = (...args: string[]) => Bun.spawnSync(['bash', join(DIR, 'ubi-runner.sh'), ...args], { env, timeout: 90_000 });
|
||||
return { root, run, calls: () => (existsSync(log) ? readFileSync(log, 'utf8') : '') };
|
||||
return { root, run, record, calls: () => (existsSync(log) ? readFileSync(log, 'utf8') : '') };
|
||||
}
|
||||
|
||||
test('down fails when the destroy request fails', () => {
|
||||
@@ -122,12 +133,14 @@ esac
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('down succeeds only once the VM is no longer listed', () => {
|
||||
const f = fakeApi({ destroy: 200, listed: 'eu-central-h1 other-vm\n' });
|
||||
test('down succeeds only once the VM is no longer listed, and forgets its scratch record', () => {
|
||||
const f = fakeApi({ destroy: 200, listed: 'eu-central-h1 vm-a\neu-central-h1 other-vm\n' });
|
||||
try {
|
||||
writeFileSync(f.record, 'vm-a\teu-central-h1\tthrd1\t2026-10-10T00:00:00Z\t1\nubirun-other-1000000000-aa\teu-central-h1\tother\t2026-10-10T00:00:00Z\t2\n');
|
||||
const r = f.run('down', 'vm-a');
|
||||
expect(r.exitCode).toBe(0);
|
||||
expect(r.stderr.toString()).toContain('destroyed eu-central-h1/vm-a');
|
||||
expect(readFileSync(f.record, 'utf8')).toBe('ubirun-other-1000000000-aa\teu-central-h1\tother\t2026-10-10T00:00:00Z\t2\n');
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
@@ -138,4 +151,37 @@ esac
|
||||
expect(f.calls()).toBe('');
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('gc destroys only this owner’s stale VMs: another owner’s and untagged legacy names are never touched', () => {
|
||||
const listed = 'eu-central-h1 ubirun-thrd1-1000000000-aaaa\neu-central-h1 ubirun-other-1000000000-bbbb\neu-central-h1 ubirun-1000000000-cccc\neu-central-h1 ubirun-thrd1-9999999999-dddd\n';
|
||||
const f = fakeApi({ destroy: 200, listed });
|
||||
try {
|
||||
const r = f.run('gc', '1');
|
||||
expect(r.exitCode).toBe(0);
|
||||
const destroyed = f.calls().split('\n').filter(l => l.includes('destroy'));
|
||||
expect(destroyed).toHaveLength(1);
|
||||
expect(destroyed[0]).toContain('ubirun-thrd1-1000000000-aaaa');
|
||||
expect(f.calls()).not.toMatch(/destroy.*ubirun-other/);
|
||||
expect(f.calls()).not.toMatch(/destroy.*ubirun-1000000000-cccc/);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('list --mine shows this owner’s VMs only; owner tags are lowercase letters and digits, 12 max, starting with a letter', () => {
|
||||
const f = fakeApi({ destroy: 200, listed: 'eu-central-h1 ubirun-thrd1-1000000000-aaaa\neu-north-h1 ubirun-other-1000000000-bbbb\neu-central-h1 ubirun-1000000000-cccc\n' });
|
||||
try {
|
||||
const mine = f.run('list', '--mine');
|
||||
expect(mine.exitCode).toBe(0);
|
||||
expect(mine.stdout.toString()).toBe('eu-central-h1 ubirun-thrd1-1000000000-aaaa\n');
|
||||
const owner = Bun.spawnSync(['bash', join(DIR, 'ubi-runner.sh'), 'owner'], { env: { ...process.env, UBI_OWNER: '12-Capy_Thread-ABCDEFGH', UBI_RUNNER_STATE: f.root }, timeout: 10_000 });
|
||||
expect(owner.stdout.toString().trim()).toBe('u12capythrea');
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('up records the VM in the scratch file before the create request is sent', () => {
|
||||
const runner = read('scripts/ubicloud/ubi-runner.sh');
|
||||
const create = runner.indexOf('cli "${args[@]}" >/dev/null 2>"$dir/create.err"');
|
||||
expect(create).toBeGreaterThan(0);
|
||||
expect(runner.indexOf('record_vm "$name" "$location"')).toBeLessThan(create);
|
||||
expect(runner).toContain('$HOME/.capy/work/ubi-runner/vms.tsv');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user