v1.91.151.0 fix: contributor ports — plan-eng-review structure id, context checkpoint gaps, first-deploy how-to (#3299)

* fix(plan-eng-review): register the structure question id so never-ask can fire

The complexity gate's Original/Smaller arrangement question had no
registered question_id, so each run invented a per-plan slug (one install
logged ten different ids for it). --check matches ids exactly, so a stored
never-ask could never fire and the question was asked every time.

- Register plan-eng-review-structure as a two-way routing question.
- The template names that exact id and says AUTO_DECIDE picks the
  recommended arrangement; the preamble's check-first rule does the rest.
  Wording is byte-neutral against the parity budget and the per-host pins.
- --check falls back to $GSTACK_HOME/global-question-preferences.json, the
  file the question-preference hook already reads, when the project file
  has no entry for the id, so one setting covers every repo and both
  layers agree. A project entry still wins, the one-way and split-chain
  guards run first, a malformed or non-object global file is ignored with
  a NOTE, and --write stays per-project.
- Tests for the fallback and the template contract; two wording pins
  updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit fb0334eb5c)

* fix(context): pin English provenance markers and ask when branch cannot discriminate

Addresses three gaps from #3217:

1. context-save: markers must stay the five English literals; translated
   tokens break restore sorting. Decisions Made claims about file contents
   need evidence or an assumed Remaining Work item.
2. context-restore: unknown/translated parenthesised markers sort under
   Verify first as (path assumed).
3. When CURRENT_BRANCH cannot discriminate and recent candidates have
   different titles, ask instead of silently loading newest.

Pins the new contract in the provenance prompt-byte test.

Fixes #3217

(cherry picked from commit 2a02647fd4)

* docs: add a neutral how-to for bringing up a first deploy (#3150)

/setup-deploy stops at "Nothing is deployed yet" and writes nothing,
because it has no production URL or deploy trigger to record. The docs
said nothing about the step in between. The maintainers chose to keep
that stop vendor-neutral and asked for a neutral docs page instead
(#3150, option 2).

docs/howto-bring-up-a-first-deploy.md walks through the first deploy:
where the app runs and how deploys start, the services it needs, env
and secrets, DNS and TLS, checking that it works, and running
/setup-deploy again. It names no hosting provider or deploy tool. The
README Docs table and the /setup-deploy section of docs/skills.md link
to it. The stop's own text is unchanged.

Fixes #3150

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5c5d8ec96f)

* docs(changelog): contributor fixes from #3249, #3244, #3264 under [Unreleased]

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: Trevor Bezdek <bezdekt@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Chao Chen <209239467+surlymochan@users.noreply.github.com>
Co-authored-by: y$un_ <forrest.sun527@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-19 19:44:10 +00:00
1 parent 635535d3c1
commit afac8dfd87
19 files changed
+286 -19

No files matched your search

+10
View File
@@ -1,5 +1,15 @@
# Changelog
## [1.91.151.0] - 2026-10-19
**Three contributor fixes: a saved "never ask" now sticks for /plan-eng-review's structure question, /context-save and /context-restore close three checkpoint gaps, and a new how-to covers the first production deploy.** Ported from fork PRs so they can run the paid gate; each commit keeps its author.
### Fixed
- **/plan-eng-review's complexity-gate structure question has a registered id (`plan-eng-review-structure`).** Each run used to invent its own slug, so a `never-ask` saved through /plan-tune never matched the next run and the question came back every time. On `AUTO_DECIDE` the recommended arrangement is taken; feature cuts are still asked one by one. `gstack-question-preference --check` now also falls back to `$GSTACK_HOME/global-question-preferences.json` (the file the question-preference hook already reads) when the project file has no entry; a project entry still wins and the one-way-door and split-chain guards run first. From #3249 by @bezdekt.
- **/context-save keeps provenance markers as English literals and backs file claims in Decisions Made; /context-restore treats a translated or unknown marker as `(path assumed)` and asks when no branch can pick between recent checkpoints with different titles.** Fixes #3217. From #3244 by @surlymochan.
### Added
- **`docs/howto-bring-up-a-first-deploy.md`:** a vendor-neutral walkthrough for when /setup-deploy stops at "Nothing is deployed yet" (host, services, env and secrets, DNS, verification, and what to have ready when you rerun /setup-deploy). Linked from the README and docs/skills.md. Fixes #3150. From #3264 by @szsunyuan.
## [1.91.150.0] - 2026-10-19
**The paid evals that still pinned older models now run on the current generation, and two cases that went red on the move are repaired at their cause.** Garry's direction (2026-10-18) is to stop testing older frontier models, so these files are re-baselined as a fresh measured reference rather than held to a threshold change.
+1
View File
@@ -656,6 +656,7 @@ Other references: [docs/gbrain-sync.md](docs/gbrain-sync.md) (sync-specific guid
|-----|---------------|
| [Skill Deep Dives](docs/skills.md) | Philosophy, examples, and workflow for every skill (includes Greptile integration) |
| [Diagrams & Document Formats](docs/howto-diagrams-and-formats.md) | Mermaid/excalidraw fences in PDFs, image sizing and safety defaults, `--to html\|docx`, `/diagram` triplets |
| [First Production Deploy](docs/howto-bring-up-a-first-deploy.md) | What to do when `/setup-deploy` says nothing is deployed yet: hosting, env and secrets, DNS, checking it works, then back to `/setup-deploy` |
| [Builder Ethos](ETHOS.md) | Builder philosophy: Boil the Ocean, Search Before Building, three layers of knowledge |
| [Using GBrain with GStack](USING_GBRAIN_WITH_GSTACK.md) | Every path, flag, bin helper, and troubleshooting step for `/setup-gbrain` |
| [GBrain Sync](docs/gbrain-sync.md) | Cross-machine memory setup, privacy modes, troubleshooting |
+1 -1
View File
@@ -1 +1 @@
1.91.150.0
1.91.151.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.150.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.151.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+16 -2
View File
@@ -2,6 +2,9 @@
# gstack-question-preference — read/write/check explicit per-question preferences.
#
# Preference file: ~/.gstack/projects/{SLUG}/question-preferences.json
# Global fallback: ~/.gstack/global-question-preferences.json, the file the
# question-preference hook reads (--check only; a project entry for the same
# id wins). Hand-edited; --write stays per-project.
# Schema: { "<question_id>": "always-ask" | "never-ask" | "ask-only-for-one-way" }
#
# Subcommands:
@@ -42,6 +45,7 @@ GSTACK_HOME="$_gstack_sr_root"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
SLUG="${SLUG:-unknown}"
PREF_FILE="$GSTACK_HOME/projects/$SLUG/question-preferences.json"
GLOBAL_PREF_FILE="$GSTACK_HOME/global-question-preferences.json"
EVENT_FILE="$GSTACK_HOME/projects/$SLUG/question-events.jsonl"
mkdir -p "$GSTACK_HOME/projects/$SLUG"
@@ -101,13 +105,23 @@ do_check() {
fi
ensure_file
cd "$ROOT_DIR"
PREF_FILE_PATH="$PREF_FILE" QID="$QID" QSUMMARY="$QSUMMARY" bun -e "
PREF_FILE_PATH="$PREF_FILE" GLOBAL_PREF_FILE_PATH="$GLOBAL_PREF_FILE" QID="$QID" QSUMMARY="$QSUMMARY" bun -e "
import('./scripts/one-way-doors.ts').then((oneway) => {
const fs = require('fs');
const qid = process.env.QID;
const summary = process.env.QSUMMARY || undefined;
const prefs = JSON.parse(fs.readFileSync(process.env.PREF_FILE_PATH, 'utf-8'));
const pref = prefs[qid];
let globalPrefs = {};
try {
if (fs.existsSync(process.env.GLOBAL_PREF_FILE_PATH)) {
const parsed = JSON.parse(fs.readFileSync(process.env.GLOBAL_PREF_FILE_PATH, 'utf-8'));
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('not an object');
globalPrefs = parsed;
}
} catch (e) {
console.error('NOTE: global-question-preferences.json is unreadable; ignoring it.');
}
const pref = qid in prefs ? prefs[qid] : globalPrefs[qid];
// Always check one-way status first — safety overrides preferences.
// summary (when piped) lets the keyword net catch ad-hoc destructive
+10
View File
@@ -509,6 +509,12 @@ has written to). Prefixed lines are never candidates for "latest":
- Otherwise: load the **first plain path line returned by Step 1 above**, which
is the newest `YYYYMMDD-HHMMSS` checkpoint of this project for the current
branch, or, if the current branch has none, the newest across all branches.
- **No useful branch discriminator.** When `CURRENT_BRANCH` is empty, `HEAD`, or
otherwise cannot discriminate (cwd is not a git repo, or branch is unknown), do
not silently load newest if the recent candidates (e.g. last 24 hours, or the
three newest) carry **different titles**. Show those titles with timestamps via
AskUserQuestion, mark newest as the recommended option, and wait. Only auto-load
newest when every recent candidate shares one title (same topic).
- **Do not silently take an older checkpoint.** If Step 1 printed `NEWER_TASK`,
or a `BELOW_CWD`/`POINTER` file is newer than the first plain path line (or
there is none), show both (title, branch, saved time, and `project_root` for a
@@ -534,6 +540,10 @@ checkpoint(s) from another project; only this project's are listed." If it print
order, and drop nothing. Put an item under **Verify first** when it:
- ends in `(path assumed)` or `(code read)`;
- ends in `(path run)` but its text reports a failure;
- ends in a parenthesised trailing marker that is **not** one of the five English
literals `(path run)`, `(path read)`, `(path assumed)`, `(target state checked)`,
`(code read)` — treat a translated or unknown marker as `(path assumed)` (→ Verify
first), never as unmarked Next steps;
- has no marker and is a writing step (migration, sync, insert, import, a dialog that
writes) or names a concrete path (a runnable command, CLI flag or switch, config key
or value, or file or directory path).
+10
View File
@@ -176,6 +176,12 @@ has written to). Prefixed lines are never candidates for "latest":
- Otherwise: load the **first plain path line returned by Step 1 above**, which
is the newest `YYYYMMDD-HHMMSS` checkpoint of this project for the current
branch, or, if the current branch has none, the newest across all branches.
- **No useful branch discriminator.** When `CURRENT_BRANCH` is empty, `HEAD`, or
otherwise cannot discriminate (cwd is not a git repo, or branch is unknown), do
not silently load newest if the recent candidates (e.g. last 24 hours, or the
three newest) carry **different titles**. Show those titles with timestamps via
AskUserQuestion, mark newest as the recommended option, and wait. Only auto-load
newest when every recent candidate shares one title (same topic).
- **Do not silently take an older checkpoint.** If Step 1 printed `NEWER_TASK`,
or a `BELOW_CWD`/`POINTER` file is newer than the first plain path line (or
there is none), show both (title, branch, saved time, and `project_root` for a
@@ -201,6 +207,10 @@ checkpoint(s) from another project; only this project's are listed." If it print
order, and drop nothing. Put an item under **Verify first** when it:
- ends in `(path assumed)` or `(code read)`;
- ends in `(path run)` but its text reports a failure;
- ends in a parenthesised trailing marker that is **not** one of the five English
literals `(path run)`, `(path read)`, `(path assumed)`, `(target state checked)`,
`(code read)` — treat a translated or unknown marker as `(path assumed)` (→ Verify
first), never as unmarked Next steps;
- has no marker and is a writing step (migration, sync, insert, import, a dialog that
writes) or names a concrete path (a runnable command, CLI flag or switch, config key
or value, or file or directory path).
+15
View File
@@ -435,6 +435,21 @@ depends on the target's state, not on the code that writes. Examples:
`Open. Run the suite with CONFIG=staging. (path run) exit 0` and
`Open. Switch B reads config Y. (path assumed)`.
**Markers stay English literals.** `/context-restore` sorts on the exact strings
`(path run)`, `(path read)`, `(path assumed)`, `(target state checked)`, and
`(code read)`. Never translate them, even when the rest of the checkpoint is in
another language. After drafting Remaining Work, scan each item that should have
a marker: if it ends in a parenthesised token that is not one of those five, or
has no marker when the table requires one, rewrite that item before writing the
file (preferred), or add a Remaining Work item that names the bad line so restore
puts it under Verify first.
**Decisions Made and file claims.** Provenance above covers Remaining Work only.
Any statement in Decisions Made about the *contents* of a concrete file (item
counts, line counts, section names) must be backed by a `grep`/`wc`/`Read` in this
turn, or rewritten with an honest `(path assumed)` note in Remaining Work that
restore will verify first. Do not record an unchecked file claim as fact.
If the user provided a title, use it. Otherwise, infer a concise title (3-6 words)
from the work being done.
+15
View File
@@ -103,6 +103,21 @@ depends on the target's state, not on the code that writes. Examples:
`Open. Run the suite with CONFIG=staging. (path run) exit 0` and
`Open. Switch B reads config Y. (path assumed)`.
**Markers stay English literals.** `/context-restore` sorts on the exact strings
`(path run)`, `(path read)`, `(path assumed)`, `(target state checked)`, and
`(code read)`. Never translate them, even when the rest of the checkpoint is in
another language. After drafting Remaining Work, scan each item that should have
a marker: if it ends in a parenthesised token that is not one of those five, or
has no marker when the table requires one, rewrite that item before writing the
file (preferred), or add a Remaining Work item that names the bad line so restore
puts it under Verify first.
**Decisions Made and file claims.** Provenance above covers Remaining Work only.
Any statement in Decisions Made about the *contents* of a concrete file (item
counts, line counts, section names) must be backed by a `grep`/`wc`/`Read` in this
turn, or rewritten with an honest `(path assumed)` note in Remaining Work that
restore will verify first. Do not record an unchecked file claim as fact.
If the user provided a title, use it. Otherwise, infer a concise title (3-6 words)
from the work being done.
+119
View File
@@ -0,0 +1,119 @@
# How to bring up your first production deploy
Use this when `/setup-deploy` stops with "Nothing is deployed yet". It walks through a first production deploy, so you finish with a live URL that `/setup-deploy` can record and `/land-and-deploy` can verify. It names no hosting provider or deploy tool; use whichever fits your project.
`/setup-deploy` writes nothing at that stop. It configures how `/land-and-deploy` checks a deploy, so it needs a production deploy that already exists.
## Prerequisites
- The app builds and starts locally with the commands production will run.
- The code is in its GitHub repository. `/setup-deploy` and `/land-and-deploy` read it through the GitHub CLI, so `gh auth status` should pass.
- An account with the hosting provider you choose, with billing set up if it needs it.
- Optional: a domain you control, if users won't use the host's default address.
## Steps
### 1. Choose where the app runs
Pick a host for the kind of app you have: static files, a long-running server or container, or functions. List what the app needs at runtime: the language runtime and its version, a database or other services, background jobs, and any disk that has to survive a restart.
Then choose how production deploys start. `/land-and-deploy` only watches a trigger that already exists; it never creates one. When `/setup-deploy` asks how deploys are triggered, the choices are:
- automatically on push to the default branch
- a CI workflow in the repository
- a deploy script or CLI command
- manually
A trigger that runs on merge to the default branch keeps `/land-and-deploy` hands-off. With a command trigger, nothing deploys until the command runs: `/land-and-deploy` asks you to run it after the merge, or runs it only with your explicit approval.
### 2. Create the services the app depends on
Create production instances of every service the app uses, such as its database, cache, file storage, email or payments, separate from your development ones. Run the database schema setup or migrations against production before the app serves traffic.
### 3. Set environment variables and secrets
1. List every variable the app reads, at build time and at runtime.
2. Set each one in the host's environment or secrets settings.
3. Keep secret values out of the repository. Commit only an example file with placeholder values, such as `.env.example`.
4. Enter secret values yourself. Don't paste them into a chat with your agent.
Some hosts inject variables only at runtime. A value the build needs, such as a public client-side key, has to be available when the build runs.
### 4. Run the first deploy
Start a deploy with the trigger you chose: push to the default branch, run the workflow, or run the command. Watch the build and release logs until they finish, and note the address the host assigns. If the build fails, fix it and deploy again before you go on.
### 5. Point your domain at it (optional)
1. At your DNS provider, add the records the host asks for: usually a CNAME record for a subdomain, or A and AAAA records for the root domain.
2. Wait until the name resolves:
```bash
dig +short app.example.com
```
3. Check that HTTPS works for the domain with a valid certificate:
```bash
curl -sI https://app.example.com | head -1
```
Decide which address is the production URL, your domain or the host's default address. That is the URL you give `/setup-deploy`.
### 6. Check that it works
1. Open the production URL in a browser and walk through one real flow, such as signing in or loading data.
2. If the app has a health endpoint, check that it returns a 2xx status:
```bash
curl -s -o /dev/null -w "%{http_code}\n" https://app.example.com/health
```
A useful health endpoint exercises the app, for example by reaching its database, instead of returning a fixed page.
3. Make sure you can tell which revision is live: a status command that prints the deployed commit, or a page or endpoint that shows it. `/land-and-deploy` treats a page that loads as site health, not as proof that the merged commit is live.
4. Check the host's logs for errors after your first requests.
### 7. Hand the deploy back to gstack
Run `/setup-deploy` again. If the project now has a config file for a platform it recognizes, it starts from that platform's settings; otherwise it asks. Either way, have these answers ready:
- how deploys are triggered (step 1)
- the production URL (step 5)
- how to check that a deploy succeeded: a health check URL, a status command, the CI workflow's status, or just checking that the URL loads
- the merge method your repository allows (squash, merge or rebase), and any command to run before merging
It writes the answers to the `## Deploy Configuration` section of the project's CLAUDE.md, then tries the health check and the status command once. From then on, `/land-and-deploy` uses that section to merge your PR, watch the deploy and check production. Its first run walks you through a dry run of the pipeline before anything irreversible happens.
## Verification
- `/setup-deploy` ends with `DEPLOY CONFIGURATION — COMPLETE`.
- The project's CLAUDE.md has the section, with your production URL:
```bash
grep -A 20 "## Deploy Configuration" CLAUDE.md
```
- Its health check did not print `UNREACHABLE`, and its status command did not print `COMMAND_FAILED`.
## Troubleshooting
**`/setup-deploy` stops at "Nothing is deployed yet" again.**
That answer means there is no production deploy to record. Once the app is live, pick the answer that says how it deploys: on push, through a CI workflow, with a script or command, or by hand.
**The health check fails or prints `UNREACHABLE`.**
Open the URL yourself. A 404 means the health path doesn't exist, so give `/setup-deploy` a path that returns 2xx, or the plain production URL. `/setup-deploy` keeps the configuration even when this check fails, so you can fix the URL later by running it again or by editing the section in CLAUDE.md.
**The app works on the host's address but not on your domain.**
Check the DNS records (`dig +short <domain>`) and the certificate (`curl -sI https://<domain>`). New records can take a while to resolve everywhere.
**The build works locally but fails on the host.**
Compare the host's build environment with yours. A missing build-time variable or a different runtime version is the usual cause.
**You deploy by running a command by hand.**
Record it as the deploy trigger. After each merge, `/land-and-deploy` tells you production hasn't moved yet and asks you to run it.
## Related
- [`/setup-deploy`](./skills.md#setup-deploy) and [`/land-and-deploy`](./skills.md#land-and-deploy): what each skill does
- [`/canary`](./skills.md#canary): post-deploy monitoring once production is live
- Reference: [`setup-deploy/SKILL.md`](../setup-deploy/SKILL.md), [`land-and-deploy/SKILL.md`](../land-and-deploy/SKILL.md)
+2
View File
@@ -1162,6 +1162,8 @@ Claude: Detected: Fly.io (fly.toml found)
Written to CLAUDE.md. Run /land-and-deploy when ready.
```
If you answer "Nothing is deployed yet", `/setup-deploy` stops and writes nothing, because there is no production deploy to record. [How to bring up your first production deploy](./howto-bring-up-a-first-deploy.md) covers that first step; run `/setup-deploy` again once the app is live.
---
## `/codex`
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.150",
"version": "1.91.151",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+7 -6
View File
@@ -441,18 +441,19 @@ Initial scope selectors need no grid or **pre-answer** ledger write. Ask and
wait before changes.
1. Explain the complexity. Ask each proposed feature cut/deferral separately;
wait before changing scope. With no proposed cuts, keep the feature list and
go directly to the structure question.
2. Always ask the structure question when this gate trips, even with no cuts.
Compare only the file/class arrangement. Use labels `Original arrangement`
and `Smaller arrangement`; put files/classes in each description. Both retain
wait before changing scope.
2. Always resolve the structure question when this gate trips, even with no cuts,
as question_id `plan-eng-review-structure` (no per-plan slug); `AUTO_DECIDE`
picks the recommendation. Compare only the file/class arrangement, labeled
`Original arrangement` and `Smaller arrangement`, with files/classes per
option. Both retain
the same approved feature list, contracts and approved
security/error/test/performance fixes. Include `Pending remedies not decided here: <ids>` in the
question; unapproved fixes stay pending. If no smaller arrangement preserves
these commitments, explain that and offer confirmation of the original
arrangement or a pause to investigate a smaller one. Wait for the answer.
A pause leaves the arrangement undecided: investigate only the agreed question,
then return to this structure selector. Do not continue to C until it is settled.
then return to the selector. Do not continue to C until it is settled.
3. Save the actual feature and structure answers as one scope record: `feature
answers: <refs>; structure: <A/B + ref>; accepted scope: <exact scope>;
pending remedies: <ids or none>`.
@@ -339,18 +339,19 @@ Initial scope selectors need no grid or **pre-answer** ledger write. Ask and
wait before changes.
1. Explain the complexity. Ask each proposed feature cut/deferral separately;
wait before changing scope. With no proposed cuts, keep the feature list and
go directly to the structure question.
2. Always ask the structure question when this gate trips, even with no cuts.
Compare only the file/class arrangement. Use labels `Original arrangement`
and `Smaller arrangement`; put files/classes in each description. Both retain
wait before changing scope.
2. Always resolve the structure question when this gate trips, even with no cuts,
as question_id `plan-eng-review-structure` (no per-plan slug); `AUTO_DECIDE`
picks the recommendation. Compare only the file/class arrangement, labeled
`Original arrangement` and `Smaller arrangement`, with files/classes per
option. Both retain
the same approved feature list, contracts and approved
security/error/test/performance fixes. Include `Pending remedies not decided here: <ids>` in the
question; unapproved fixes stay pending. If no smaller arrangement preserves
these commitments, explain that and offer confirmation of the original
arrangement or a pause to investigate a smaller one. Wait for the answer.
A pause leaves the arrangement undecided: investigate only the agreed question,
then return to this structure selector. Do not continue to C until it is settled.
then return to the selector. Do not continue to C until it is settled.
3. Save the actual feature and structure answers as one scope record: `feature
answers: <refs>; structure: <A/B + ref>; accepted scope: <exact scope>;
pending remedies: <ids or none>`.
+8
View File
@@ -336,6 +336,14 @@ export const QUESTIONS = {
signal_key: 'scope-appetite',
description: "Plan touches 8+ files — reduce scope or hold?",
},
'plan-eng-review-structure': {
id: 'plan-eng-review-structure',
skill: 'plan-eng-review',
category: 'routing',
door_type: 'two-way',
options: ['original-arrangement', 'smaller-arrangement'],
description: "Complexity gate — Original arrangement or Smaller arrangement (file/class layout only)?",
},
'plan-eng-review-test-gap': {
id: 'plan-eng-review-test-gap',
skill: 'plan-eng-review',
@@ -37,6 +37,8 @@ describe('Remaining Work provenance contract', () => {
expect(table).toContain('`Open.`');
expect(table).toContain('`[done]`');
expect(table.indexOf('a writing step')).toBeLessThan(table.indexOf('names a concrete path'));
expect(table).toContain('**Markers stay English literals.**');
expect(table).toContain('**Decisions Made and file claims.**');
const format = between(save, '### Remaining Work', '### Notes');
expect(format).toContain('`Open.`');
});
@@ -49,6 +51,8 @@ describe('Remaining Work provenance contract', () => {
for (const marker of ['(path assumed)', '(code read)']) expect(verifyFirst).toContain(`\`${marker}\``);
expect(verifyFirst).toContain('reports a failure');
expect(verifyFirst).toContain('has no marker');
expect(verifyFirst).toContain('translated or unknown marker');
expect(restore).toContain('**No useful branch discriminator.**');
const nextSteps = between(rules, 'Every other item goes under **Next steps**', 'Verifying means');
for (const marker of ['(path run)', '(path read)', '(target state checked)']) expect(nextSteps).toContain(`\`${marker}\``);
expect(rules).toContain(
+7 -1
View File
@@ -76,10 +76,16 @@ describe('engineering review routing contracts', () => {
test('high complexity stops before Section 1 and asks cuts and structure separately', () => {
expectMentions(compact(complexity), [['stop', 'classes/services', 'section']], 'compact(complexity)');
ordered(compact(complexity), ['1. Explain the complexity', 'Ask each proposed feature cut/deferral separately',
'2. Always ask the structure question', '3. Save the actual feature and structure answers']);
'2. Always resolve the structure question', '3. Save the actual feature and structure answers']);
expect(compact(complexity)).toContain('Pending remedies not decided here: <ids>');
});
test('the structure question uses its registered id and honors a stored preference', () => {
ordered(compact(complexity), ['2. Always resolve the structure question',
'as question_id `plan-eng-review-structure` (no per-plan slug)', '`AUTO_DECIDE` picks the recommendation',
'Compare only the file/class arrangement']);
});
test('no safe smaller arrangement does not authorize scope cuts or bypass the pause', () => {
ordered(compact(complexity), ['If no smaller arrangement preserves these commitments',
'A pause leaves the arrangement undecided', 'Do not continue to C until it is settled']);
+51
View File
@@ -132,6 +132,57 @@ describe('--check with preferences set', () => {
});
});
// The global file ($GSTACK_HOME/global-question-preferences.json, the same file
// the question-preference hook reads) is a hand-edited fallback for --check
// only: one setting covers every project, a project entry for the same id
// wins, and the one-way / split-chain guards still run.
describe('--check global preference fallback', () => {
function plantGlobal(contents: string) {
fs.writeFileSync(path.join(tmpHome, 'global-question-preferences.json'), contents);
}
test('global never-ask with no project entry → AUTO_DECIDE', () => {
plantGlobal(JSON.stringify({ 'plan-eng-review-structure': 'never-ask' }));
const r = run('--check', 'plan-eng-review-structure');
expect(r.status).toBe(0);
expect(r.stdout.trim()).toBe('AUTO_DECIDE');
});
test('project entry wins over the global entry', () => {
plantGlobal(JSON.stringify({ 'plan-eng-review-structure': 'never-ask' }));
run('--write', JSON.stringify({ question_id: 'plan-eng-review-structure', preference: 'always-ask', source: 'plan-tune' }));
const r = run('--check', 'plan-eng-review-structure');
expect(r.stdout.trim()).toBe('ASK_NORMALLY');
});
test('global never-ask on a one-way id → ASK_NORMALLY with safety note', () => {
plantGlobal(JSON.stringify({ 'ship-test-failure-triage': 'never-ask' }));
const r = run('--check', 'ship-test-failure-triage');
expect(r.stdout).toContain('ASK_NORMALLY');
expect(r.stdout).toContain('one-way door overrides');
});
test('global never-ask on a split-chain id → ASK_NORMALLY', () => {
plantGlobal(JSON.stringify({ 'plan-eng-review-split-option-a': 'never-ask' }));
const r = run('--check', 'plan-eng-review-split-option-a');
expect(r.stdout.split('\n')[0]).toBe('ASK_NORMALLY');
});
test.each(['{not json', 'null', '["never-ask"]'])('unusable global file %p is ignored with a note', (contents) => {
plantGlobal(contents);
const r = run('--check', 'plan-eng-review-structure');
expect(r.status).toBe(0);
expect(r.stdout.trim()).toBe('ASK_NORMALLY');
expect(r.stderr).toContain('global-question-preferences.json is unreadable');
});
test('--write stays per-project and never touches the global file', () => {
plantGlobal('{}');
run('--write', JSON.stringify({ question_id: 'plan-eng-review-structure', preference: 'never-ask', source: 'plan-tune' }));
expect(fs.readFileSync(path.join(tmpHome, 'global-question-preferences.json'), 'utf-8')).toBe('{}');
});
});
// #2024: the keyword net only fires when the question TEXT reaches the
// classifier. --summary-stdin pipes it (stdin, not argv — summaries carry
// quotes/newlines/shell metacharacters). Without the summary, an unregistered
+1 -1
View File
@@ -319,7 +319,7 @@ test('Eng loads its one remedy procedure before Scope Challenge findings and ret
const scope = compactProse(sections.slice(sections.indexOf('## Scope Challenge'), sections.indexOf('## Review Sections')));
expectAll(scope, [/decision-brief format for this complexity gate/i,
/ask each proposed feature cut\/deferral separately; wait before changing scope/i,
/unapproved fixes stay pending/i, /always ask the structure question when this gate trips/i,
/unapproved fixes stay pending/i, /always resolve the structure question when this gate trips/i,
/offer confirmation of the original arrangement or a pause to investigate a smaller one\. wait for the answer/i,
/apply only accepted scope changes/i, /"No issues found" for an empty list/i, /findings and scope answers approve no remedies/i]);
const complexityRule = at(scope, /initial scope selectors need no grid or \*\*pre-answer\*\* ledger write/i);