mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-11 10:23:44 +02:00
Merge remote-tracking branch 'origin/main' into garrytan/workspace-aware-ship
Rebumped v1.8.0.0 -> v1.11.0.0 (minor-past main's v1.10.1.0) using bin/gstack-next-version — the same queue-aware path this branch introduces. CHANGELOG repositioned so v1.11.0.0 sits above main's new entries (v1.10.1.0 / v1.10.0.0 / v1.9.0.0). Conflicts resolved: - VERSION, package.json: rebumped to v1.11.0.0 (util-picked) - bin/gstack-config: merged both lists (workspace_root + gbrain keys) - CHANGELOG.md: hoisted v1.11.0.0 entry above main's new entries Pre-existing failures in main (4) documented but not fixed in this PR: 1. gstack-brain-sync secret scan > blocks bearer-json (brain-sync tests) 2. no files larger than 2MB (security-bench fixture, already TODO'd) 3. selectTests > skill-specific change (touchfiles scoping) 4. Opus 4.7 overlay pacing directive (expectation stale after v1.10.1.0 removed the Fan out nudge) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+236
-4
@@ -1,6 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [1.8.0.0] - 2026-04-23
|
## [1.11.0.0] - 2026-04-23
|
||||||
|
|
||||||
## **Workspace-aware ship. Two open PRs can't both claim the same VERSION anymore.**
|
## **Workspace-aware ship. Two open PRs can't both claim the same VERSION anymore.**
|
||||||
|
|
||||||
@@ -8,11 +8,11 @@ If you run gstack in multiple Conductor windows at once, you've probably seen th
|
|||||||
|
|
||||||
### What changes for you
|
### What changes for you
|
||||||
|
|
||||||
Run `/ship` in one Conductor window while another has an open PR claiming v1.7.0.0. Your ship now sees the claim, renders a queue table, and picks v1.8.0.0 (same bump level, next free slot). The PR title starts with `v1.8.0.0` so landing order is visible in `gh pr list` without opening each PR. If a sibling workspace has uncommitted work at a higher VERSION and looks active (commit in the last 24h), `/ship` asks whether to wait for them or advance past. If the queue shifts between ship and merge, CI's new version-gate catches it, and rerunning `/ship` rewrites VERSION, package.json, CHANGELOG, and the PR title atomically.
|
Run `/ship` in one Conductor window while another has an open PR claiming v1.7.0.0. Your ship now sees the claim, renders a queue table, and picks the next free slot above it (same bump level). The PR title starts with `v<X.Y.Z.W>` so landing order is visible in `gh pr list` without opening each PR. If a sibling workspace has uncommitted work at a higher VERSION and looks active (commit in the last 24h), `/ship` asks whether to wait for them or advance past. If the queue shifts between ship and merge, CI's new version-gate catches it, and rerunning `/ship` rewrites VERSION, package.json, CHANGELOG, and the PR title atomically. This very release dogfooded the drift path: the original ship at v1.8.0.0 went stale when three other PRs landed first, and the merge-back-to-main rebump (v1.8.0.0 → v1.11.0.0) happened via the same queue-aware codepath it introduces.
|
||||||
|
|
||||||
### What shipped (by the numbers)
|
### What shipped (by the numbers)
|
||||||
|
|
||||||
- `bin/gstack-next-version` — 360-line Bun/TS util. 21 passing fixture tests covering happy path, 8 collision scenarios, offline fallback, fork-PR filtering, sibling activity detection.
|
- `bin/gstack-next-version` — ~390-line Bun/TS util. 21 passing fixture tests covering happy path, 8 collision scenarios, offline fallback, fork-PR filtering, sibling activity detection, self-PR auto-exclusion.
|
||||||
- Host parity: GitHub + GitLab both supported. CI gates: `.github/workflows/version-gate.yml`, `.github/workflows/pr-title-sync.yml`, plus `.gitlab-ci.yml` mirror.
|
- Host parity: GitHub + GitLab both supported. CI gates: `.github/workflows/version-gate.yml`, `.github/workflows/pr-title-sync.yml`, plus `.gitlab-ci.yml` mirror.
|
||||||
- Fail-open semantics on util errors (network, auth, bug). A gstack bug never freezes your merge queue. Fail-closed on confirmed collisions.
|
- Fail-open semantics on util errors (network, auth, bug). A gstack bug never freezes your merge queue. Fail-closed on confirmed collisions.
|
||||||
- `/landing-report` skill — read-only dashboard showing queue, siblings, and what all four bump levels would claim.
|
- `/landing-report` skill — read-only dashboard showing queue, siblings, and what all four bump levels would claim.
|
||||||
@@ -26,7 +26,7 @@ If you're routinely running 3-10 Conductor windows against the same repo, this i
|
|||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
|
|
||||||
- `bin/gstack-next-version`. Host-aware (GitHub + GitLab + unknown) VERSION allocator. Queries open PRs, fetches each PR's VERSION at head (bounded concurrency, 10 parallel), scans sibling Conductor worktrees, picks the next free slot. Pure reader, never writes files.
|
- `bin/gstack-next-version`. Host-aware (GitHub + GitLab + unknown) VERSION allocator. Queries open PRs, fetches each PR's VERSION at head (bounded concurrency, 10 parallel), scans sibling Conductor worktrees, picks the next free slot. Pure reader, never writes files. Supports `--exclude-pr <N>` to filter out the PR being checked (prevents self-reference when CI runs against the PR's own VERSION).
|
||||||
- `scripts/detect-bump.ts`, `scripts/compare-pr-version.ts`. CI gate helpers. Three exit paths: pass, block on confirmed collision, fail-open on util errors.
|
- `scripts/detect-bump.ts`, `scripts/compare-pr-version.ts`. CI gate helpers. Three exit paths: pass, block on confirmed collision, fail-open on util errors.
|
||||||
- `.github/workflows/version-gate.yml`. Merge-time collision gate. Runs when VERSION/CHANGELOG/package.json changes on a PR.
|
- `.github/workflows/version-gate.yml`. Merge-time collision gate. Runs when VERSION/CHANGELOG/package.json changes on a PR.
|
||||||
- `.github/workflows/pr-title-sync.yml`. Auto-rewrites PR title when VERSION changes on push, only for titles already carrying the `v<X.Y.Z.W>` prefix (custom titles left alone, idempotent).
|
- `.github/workflows/pr-title-sync.yml`. Auto-rewrites PR title when VERSION changes on push, only for titles already carrying the `v<X.Y.Z.W>` prefix (custom titles left alone, idempotent).
|
||||||
@@ -42,11 +42,243 @@ If you're routinely running 3-10 Conductor windows against the same repo, this i
|
|||||||
- `review/SKILL.md.tmpl`. New Step 3.4 advisory one-liner showing queue status. Non-blocking.
|
- `review/SKILL.md.tmpl`. New Step 3.4 advisory one-liner showing queue status. Non-blocking.
|
||||||
- `CLAUDE.md`. Versioning invariant paragraph. Documents that VERSION is a monotonic sequence, not a strict semver commitment, and queue-advance within a bump level is permitted.
|
- `CLAUDE.md`. Versioning invariant paragraph. Documents that VERSION is a monotonic sequence, not a strict semver commitment, and queue-advance within a bump level is permitted.
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- Self-reference bug in the version gate. The first live CI run (PR #1168 at v1.8.0.0) was rejected as "stale" because the util counted the PR being checked as a queued claim, inflating the next slot by one. Fixed with `--exclude-pr` flag + `gh pr view` auto-detect so the util silently filters the current branch's PR. Caught and fixed in the same ship — exactly the dogfood loop the release is designed for.
|
||||||
|
|
||||||
#### For contributors
|
#### For contributors
|
||||||
|
|
||||||
- `test/gstack-next-version.test.ts`. 21 pure-function tests (parseVersion / bumpVersion / cmpVersion / pickNextSlot with 8 collision scenarios / markActiveSiblings 4 cases) plus a CLI smoke test against the live repo.
|
- `test/gstack-next-version.test.ts`. 21 pure-function tests (parseVersion / bumpVersion / cmpVersion / pickNextSlot with 8 collision scenarios / markActiveSiblings 4 cases) plus a CLI smoke test against the live repo.
|
||||||
- Golden ship fixtures refreshed for all three hosts (claude, codex, factory) after Step 12 and Step 19 template changes. This is exactly the blast radius Codex flagged during the CEO review (cross-model tension #8), handled in the same PR rather than as a follow-up.
|
- Golden ship fixtures refreshed for all three hosts (claude, codex, factory) after Step 12 and Step 19 template changes. This is exactly the blast radius Codex flagged during the CEO review (cross-model tension #8), handled in the same PR rather than as a follow-up.
|
||||||
|
|
||||||
|
## [1.10.1.0] - 2026-04-23
|
||||||
|
|
||||||
|
## **We tried to make Opus 4.7 faster with a prompt. Measurement said it got slower. Pulled the bullet.**
|
||||||
|
|
||||||
|
gstack shipped a "Fan out explicitly" overlay nudge in `model-overlays/opus-4-7.md`
|
||||||
|
back in v1.5.2.0. The idea: tell Opus 4.7 to emit multiple tool calls in one
|
||||||
|
assistant turn instead of one per turn, so "read three files" takes one API
|
||||||
|
round-trip instead of three. Sounded obvious. This release removes that
|
||||||
|
bullet after measuring that it actively hurt performance, and ships the eval
|
||||||
|
harness we used to prove it so you can measure your own overlay changes.
|
||||||
|
|
||||||
|
### The numbers that matter
|
||||||
|
|
||||||
|
Source: new `test/skill-e2e-overlay-harness.test.ts`, N=10 trials per arm per
|
||||||
|
fixture, 40 trials per run, ~$3 per run. Pinned to `claude-opus-4-7` via
|
||||||
|
Anthropic's published Agent SDK (`@anthropic-ai/claude-agent-sdk@0.2.117`)
|
||||||
|
with `pathToClaudeCodeExecutable` set to the locally-installed `claude` binary
|
||||||
|
(2.1.118). Metric: number of parallel `tool_use` blocks in the first assistant
|
||||||
|
turn.
|
||||||
|
|
||||||
|
| Prompt text in overlay | First-turn fanout rate (toy: read 3 files) | Lift vs baseline |
|
||||||
|
|---|---|---|
|
||||||
|
| No overlay (default Claude Code system prompt only) | **70%** (7/10) | baseline |
|
||||||
|
| gstack's original "Fan out explicitly" nudge (v1.5.2.0 through v1.6.3.0) | 10% (1/10) | **-60%** |
|
||||||
|
| Anthropic's own canonical `<use_parallel_tool_calls>` text from their parallel-tool-use docs | **0%** (0/10) | **-70%** |
|
||||||
|
|
||||||
|
On a realistic multi-file audit prompt (`read app.ts + config.ts + README.md,
|
||||||
|
glob src/*.ts, summarize`), Opus 4.7 never fanned out in the first turn at all,
|
||||||
|
regardless of overlay. Zero of 20 trials. The nudge had nothing to grip.
|
||||||
|
|
||||||
|
Total cost of the investigation: **$7** across three eval runs.
|
||||||
|
|
||||||
|
### What this means for you
|
||||||
|
|
||||||
|
If you ship system-prompt nudges for Claude, measure them. Anthropic's own
|
||||||
|
published best-practice text dropped our fanout rate to zero. That's not a
|
||||||
|
claim about Anthropic, it's a claim about measurement: the model, the SDK,
|
||||||
|
the binary, and the context all move under the advice, and the advice sits
|
||||||
|
still. The harness is in the repo now. Run
|
||||||
|
`EVALS=1 EVALS_TIER=periodic bun test test/skill-e2e-overlay-harness.test.ts`.
|
||||||
|
Three dollars per run.
|
||||||
|
|
||||||
|
### Itemized changes
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- `model-overlays/opus-4-7.md` — removed the "Fan out explicitly" block. The
|
||||||
|
other three nudges (effort-match, batch questions, literal interpretation)
|
||||||
|
are untested and stay in for now. They're candidates for their own
|
||||||
|
measurement in a follow-up PR.
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- `test/skill-e2e-overlay-harness.test.ts` — periodic-tier eval that iterates a
|
||||||
|
typed fixture registry and runs A/B arms through `@anthropic-ai/claude-agent-sdk`.
|
||||||
|
Uses SDK preset `claude_code` so the arms include Claude Code's real system
|
||||||
|
prompt; overlay-ON appends the resolved overlay text. Saves per-trial raw
|
||||||
|
event streams for forensic recovery. Gated on both `EVALS=1` and
|
||||||
|
`EVALS_TIER=periodic`.
|
||||||
|
- `test/fixtures/overlay-nudges.ts` — typed `OverlayFixture` registry with
|
||||||
|
strict validator. Adding a future nudge to measure = one fixture entry.
|
||||||
|
First two fixtures: `opus-4-7-fanout-toy` and `opus-4-7-fanout-realistic`.
|
||||||
|
- `test/helpers/agent-sdk-runner.ts` — parametric SDK wrapper with explicit
|
||||||
|
`AgentSdkResult` types, process-level API concurrency semaphore, and
|
||||||
|
three-shape 429 retry (thrown error, result-message error, mid-stream
|
||||||
|
`SDKRateLimitEvent`). Binary pinning via `pathToClaudeCodeExecutable`.
|
||||||
|
- `test/agent-sdk-runner.test.ts` — 36 free-tier unit tests covering happy
|
||||||
|
path, all three rate-limit shapes, persistent-429 `RateLimitExhaustedError`,
|
||||||
|
non-429 propagation, options propagation, concurrency cap, and every
|
||||||
|
validator rejection case.
|
||||||
|
- `scripts/preflight-agent-sdk.ts` — 20-line sanity check that confirms the
|
||||||
|
SDK loads, `claude-opus-4-7` is a live API model, the `SDKMessage` event
|
||||||
|
shape matches assumptions, and the overlay resolver produces the expected
|
||||||
|
text. Run manually before paid runs if you suspect drift. Costs ~$0.013.
|
||||||
|
- `@anthropic-ai/claude-agent-sdk@0.2.117` in `devDependencies`. Exact pin,
|
||||||
|
no caret — SDK event shapes can drift on minor versions.
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
- `scripts/resolvers/model-overlay.ts` — exported `readOverlay` so the eval
|
||||||
|
harness can resolve `{{INHERIT:claude}}` directives without synthesizing a
|
||||||
|
full `TemplateContext`.
|
||||||
|
|
||||||
|
#### For contributors
|
||||||
|
|
||||||
|
- `test/helpers/touchfiles.ts` — registered the new eval in both
|
||||||
|
`E2E_TOUCHFILES` (deps: `model-overlays/**`, `overlay-nudges.ts`, runner,
|
||||||
|
resolver) and `E2E_TIERS` (`periodic`). Passes the
|
||||||
|
`test/touchfiles.test.ts` completeness check.
|
||||||
|
- The harness is deliberately parametric. Adding a second overlay nudge
|
||||||
|
measurement (for the remaining three nudges in `opus-4-7.md`, or any
|
||||||
|
future nudge in any overlay file) is a single entry in
|
||||||
|
`test/fixtures/overlay-nudges.ts`. Total incremental effort: ~15 minutes
|
||||||
|
per fixture.
|
||||||
|
|
||||||
|
## [1.10.0.0] - 2026-04-23
|
||||||
|
|
||||||
|
## **Plan reviews walk you through each issue again, and every question is now a real decision brief.**
|
||||||
|
|
||||||
|
v1.6.4.0 broke something nobody wrote down. Plan reviews on Opus 4.7 silently stopped asking questions one at a time. They turned into a report: here are 6 findings, end of turn. The interactive dialogue that made `/plan-ceo-review`, `/plan-eng-review`, and the rest useful quietly evaporated. v1.10.0.0 restores that, and bundles a format upgrade so every `AskUserQuestion` now renders as a numbered decision brief with ELI10, stakes, recommendation, per-option pros / cons (✅ / ❌), and a closing "Net:" line that frames the trade-off in one sentence.
|
||||||
|
|
||||||
|
### What changes for you
|
||||||
|
|
||||||
|
Run `/plan-ceo-review` or `/plan-eng-review` on a plan with 3 findings. You get 3 separate AskUserQuestion prompts, one per finding, with the full Pros / Cons shape. Pick the option in 5 seconds, or expand the pros / cons if you want to think about it. Every review finding becomes a decision you actually made, not a bullet point you skimmed. The reference shape matches the D2 memory-design question Garry hand-crafted for his own use, now baked into every tier-2 skill via the preamble resolver, so `/ship`, `/office-hours`, `/investigate`, and the rest inherit it for free.
|
||||||
|
|
||||||
|
### The numbers that matter
|
||||||
|
|
||||||
|
Measured across the v1.10.0.0 fix. Verify any claim with `git log 1.9.0.0..1.10.0.0 --oneline` and `bun test` against the pinned commit SHA.
|
||||||
|
|
||||||
|
| Metric | v1.6.4.0 | v1.10.0.0 | Δ |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `AskUserQuestion` renders above model overlay in SKILL.md | no | **yes** | ordering inverted |
|
||||||
|
| Escape-hatch sites hardened across plan-review templates | 0 | **16** | +16 |
|
||||||
|
| Gate-tier unit tests pinning the format contract | 0 | **30** | +30 (runs in 16ms, $0) |
|
||||||
|
| Periodic evals defending against escape-hatch abuse | 0 | **4** | +4 (2 positive, 2 negative-case) |
|
||||||
|
| Cross-model review findings incorporated before landing | N/A | **5 of 8** | Codex caught real bugs CEO+Eng missed |
|
||||||
|
|
||||||
|
Two of the five Codex findings were load-bearing. (1) The overlay reorder theory wasn't enough on its own. The `(recommended)` label on a neutral-posture question had to stay, because `question-tuning.ts:29` reads it to power AUTO_DECIDE. Omitting it would have silently broken auto-decide on every cherry-pick prompt. (2) The "31 sites global replace" in the original plan was factually wrong. Actual count, verified with `rg`, is 16 sites across 4 templates, and eng/design/devex templates used different phrasing than CEO. Without the audit, the fix would have shipped half-applied.
|
||||||
|
|
||||||
|
### What this means for anyone running plan reviews on Opus 4.7
|
||||||
|
|
||||||
|
Upgrade and re-run your next plan review. You should see D-numbered prompts (D1, D2, D3...) with ELI10 paragraphs, stakes lines, and ✅ / ❌ bullet blocks per option. If you don't, check that `bun run gen:skill-docs` regenerated cleanly after the upgrade, and verify the `Pros / cons:` header renders in `plan-ceo-review/SKILL.md`. Complete plan reviews that used to take 20 minutes and produced a report now take 10 minutes and produce a row of decisions.
|
||||||
|
|
||||||
|
### Itemized changes
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- New Pros / Cons decision-brief format for every `AskUserQuestion` across all tier-2+ skills. Rendering: `D<N>` header, ELI10, "Stakes if we pick wrong:", Recommendation, per-option `✅ / ❌` bullets with minimum 2 pros + 1 con, closing `Net:` synthesis line. Lands in `scripts/resolvers/preamble/generate-ask-user-format.ts` so every skill inherits it.
|
||||||
|
- Hard-stop escape for destructive one-way choices: single bullet `✅ No cons — this is a hard-stop choice`.
|
||||||
|
- Neutral-posture handling for SELECTIVE EXPANSION cherry-picks and taste calls: `Recommendation: <default> — this is a taste call, no strong preference either way` with `(recommended)` label preserved on the default to keep AUTO_DECIDE working.
|
||||||
|
- Three gate-tier unit tests (`test/preamble-compose.test.ts`, `test/resolver-ask-user-format.test.ts`, `test/model-overlay-opus-4-7.test.ts`) that pin the composition order, format contract, and overlay text. Run in <100ms on every `bun test`.
|
||||||
|
- Four periodic-tier Pros/Cons eval cases in `test/skill-e2e-plan-prosons.test.ts` including two negative-case assertions that catch escape-hatch abuse before it drifts.
|
||||||
|
- Touchfiles entries (`test/helpers/touchfiles.ts`) for all new eval cases plus expanded-coverage stubs for 7 additional skills.
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- Plan-review cadence regression on Opus 4.7. `/plan-ceo-review`, `/plan-eng-review`, `/plan-design-review`, and `/plan-devex-review` now actually pause after each finding and call `AskUserQuestion` as a tool_use instead of batching everything into one summary report. Root cause: `generateModelOverlay` rendered above `generateAskUserFormat` in `scripts/resolvers/preamble.ts`, so the overlay's "Batch your questions" directive registered as the ambient default before the pacing rule. Fixed by reordering the section array and rewriting the overlay directive as "Pace questions to the skill".
|
||||||
|
- Escape-hatch collapse: "If no issues or fix is obvious, state what you'll do and move on, don't waste a question" at 16 sites across 4 templates let Opus 4.7's literal interpreter classify every finding as self-dismissable. Tightened per-template: zero findings gets "No issues, moving on"; findings require AskUserQuestion as a tool_use.
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
- `test/skill-e2e-plan-format.test.ts`: extended with v1.10.0.0 format token regexes (D-number, ELI10, Stakes, Pros/cons, Net). Existing RECOMMENDATION check loosened to accept mixed-case "Recommendation:".
|
||||||
|
- `test/skill-validation.test.ts`: format assertions updated from "RECOMMENDATION: Choose" to the new Pros/Cons token set.
|
||||||
|
- Golden fixtures regenerated: `test/fixtures/golden/claude-ship-SKILL.md`, `codex-ship-SKILL.md`, `factory-ship-SKILL.md`.
|
||||||
|
|
||||||
|
#### For contributors
|
||||||
|
|
||||||
|
- Outside-voice Codex review (`codex exec` with `model_reasoning_effort="high"`) caught two factual bugs in the original plan: the "31 sites" count (actually 16) and the AUTO_DECIDE contract break on neutral-posture questions. 5 of 8 Codex findings incorporated, 1 rejected (kept defense in depth on the composition reorder), 1 declined (HOLD SCOPE mode lock).
|
||||||
|
- Follow-up: true multi-turn cadence eval (3 findings produce 3 distinct AskUserQuestion invocations across turns) requires new harness support for multi-capture. Filed in NOT-in-scope. Current single-capture eval covers format + escape-hatch abuse but not cadence itself.
|
||||||
|
- Follow-up: expanded-coverage eval cases for `/ship`, `/office-hours`, `/investigate`, `/qa`, `/review`, `/design-review`, `/document-release`. Touchfiles entries exist; test blocks will land per-skill in follow-up PRs.
|
||||||
|
- D-numbering is a model-level instruction, not a runtime counter. `TemplateContext` has no state for it. Drift over long sessions is expected; a registry (deferred to TODOs) is the long-term fix.
|
||||||
|
|
||||||
|
## [1.9.0.0] - 2026-04-23
|
||||||
|
|
||||||
|
## **Your gstack memory now travels with you. Cross-machine brain via a private git repo + optional GBrain indexing, no daemon, no credential leaks.**
|
||||||
|
|
||||||
|
gstack session memory (learnings, plans, designs, retros, developer profile) used to die at the machine boundary. Now it doesn't. `gstack-brain-init` turns `~/.gstack/` into a git repo with an explicit allowlist, writer shims enqueue changed files at write-time, and a preamble-boundary sync pushes them to a private git remote of your choice. GBrain is the first consumer but the architecture is pluggable — Codex, OpenClaw, or anything else can be a reader later. No daemon, no background process, no new auth surface.
|
||||||
|
|
||||||
|
The feature shipped after four plan reviews: /office-hours shaping, /plan-eng-review (6 issues → CLEAR), /plan-ceo-review (SELECTIVE EXPANSION, 2 cherry-picks accepted), /codex twice (16+16 findings applied, daemon model dropped in round 2), and /plan-devex-review (6/10 → 8/10, docs elevated to full treatment). The scope simplification from Codex round 2 alone removed ~1 week of daemon lifecycle surface.
|
||||||
|
|
||||||
|
### What you can now do
|
||||||
|
|
||||||
|
- **Initialize cross-machine sync:** `gstack-brain-init` creates a private git repo (GitHub via `gh`, or any git URL — GitLab, Gitea, self-hosted). 30-90 second TTHW.
|
||||||
|
- **See yesterday's laptop on today's desktop:** copy `~/.gstack-brain-remote.txt` to the new machine, run `gstack-brain-restore`, and your learnings follow you.
|
||||||
|
- **Control what syncs:** one-time privacy stop-gate on first run — `full` (everything allowlisted), `artifacts-only` (plans/designs/retros/learnings, skip behavioral), `off` (decline).
|
||||||
|
- **Sleep through the conflict case:** two machines writing the same JSONL file the same day merge cleanly via a ts-sort-plus-hash-fallback merge driver registered automatically.
|
||||||
|
- **Uninstall cleanly:** `gstack-brain-uninstall` removes the sync layer, leaves your data intact.
|
||||||
|
- **Never push a secret:** AWS keys, GitHub tokens (`ghp_`/`gho_`/`ghu_`/`ghs_`/`ghr_`/`github_pat_`), OpenAI `sk-` keys, PEM blocks, JWTs, and bearer-token-in-JSON patterns are all blocked before push. `--skip-file <path>` gives you a single-command escape hatch for false positives.
|
||||||
|
|
||||||
|
### The numbers that matter
|
||||||
|
|
||||||
|
Source: integration smoke tests run during implementation, plus 27-test consolidated suite (`test/brain-sync.test.ts`). End-to-end round trip (init on machine A → write learning → restore on machine B → see the learning) verified inline.
|
||||||
|
|
||||||
|
| Surface | Shape |
|
||||||
|
|---|---|
|
||||||
|
| New binaries | 8 (`gstack-brain-init`, `-enqueue`, `-sync`, `-consumer`, `-reader` alias, `-restore`, `-uninstall`, `gstack-jsonl-merge`) |
|
||||||
|
| Config keys | 2 enum-validated (`gbrain_sync_mode`: off/artifacts-only/full; `gbrain_sync_mode_prompted`: bool) |
|
||||||
|
| Writer shims modified | 4 (learnings-log, timeline-log, review-log, developer-profile on --migrate path) |
|
||||||
|
| Writers deliberately NOT synced | 2 (question-log, question-preference — per-machine UX state, Codex v2 decision) |
|
||||||
|
| Sync granularity | per-skill-boundary via `gstack-brain-sync --once` from preamble (no daemon) |
|
||||||
|
| Privacy tiers | 3 (full / artifacts-only / off) |
|
||||||
|
| Secret patterns blocked | 6 families (AWS, GH tokens, OpenAI, PEM, JWT, bearer-in-JSON) |
|
||||||
|
| User-facing naming | `reader` (CLI); internal data model stays `consumer` per Codex-v2 DX decision |
|
||||||
|
| New-machine discovery | auto via `~/.gstack-brain-remote.txt` file (URL-only, no secrets) |
|
||||||
|
|
||||||
|
### What this means for you
|
||||||
|
|
||||||
|
Work on the laptop Monday. Switch to the desktop Tuesday. Skill preamble sees the remote URL, offers `gstack-brain-restore`, your Monday learnings surface on Tuesday. The pattern scales to N consumers: today GBrain is the primary reader, tomorrow Codex or OpenClaw can subscribe without refactoring the sync.
|
||||||
|
|
||||||
|
### Itemized changes
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- `bin/gstack-brain-init` — idempotent first-run setup. Turns `~/.gstack/` into a git repo with `.gitignore = *`, writes canonical `.brain-allowlist` + `.brain-privacy-map.json`, installs pre-commit secret-scan hook, registers JSONL merge driver, creates private remote via `gh repo create --private` (or accepts `--remote <url>`), writes `~/.gstack-brain-remote.txt` for new-machine discovery.
|
||||||
|
- `bin/gstack-brain-sync` — core sync. Subcommands: `--once` (drain queue, secret-scan staged diff, commit with template message, push with fetch+merge retry), `--status`, `--skip-file <path>`, `--drop-queue --yes`, `--discover-new` (walks allowlist globs with mtime+size cursor).
|
||||||
|
- `bin/gstack-brain-enqueue` — atomic-append shim called by writers. Silent no-op when feature disabled.
|
||||||
|
- `bin/gstack-brain-consumer` + `bin/gstack-brain-reader` (symlink alias) — manage the consumer/reader registry in `consumers.json`. User-facing "reader", internal "consumer".
|
||||||
|
- `bin/gstack-brain-restore` — new-machine bootstrap with safety gates (refuses dangerous clobber, re-registers merge drivers, prompts for per-consumer tokens since tokens stay machine-local).
|
||||||
|
- `bin/gstack-brain-uninstall` — clean off-ramp. Removes `.git` + `.brain-*` files + `consumers.json` + config keys. Preserves user data (learnings etc). Optional `--delete-remote` for the GitHub repo.
|
||||||
|
- `bin/gstack-jsonl-merge` — git merge driver. Concat-dedup-sort by ISO `ts` field; deterministic SHA-256 hash fallback when `ts` is missing.
|
||||||
|
- `scripts/resolvers/preamble/generate-brain-sync-block.ts` — preamble bash block. New-machine restore hint, one-time privacy stop-gate, `--once` at skill start + end, once-daily auto-pull, `BRAIN_SYNC:` status line on every skill run.
|
||||||
|
- `docs/gbrain-sync.md` — user guide (setup, first-use, restore, privacy modes, secret protection, uninstall).
|
||||||
|
- `docs/gbrain-sync-errors.md` — error lookup index (problem / cause / fix for every user-visible error).
|
||||||
|
- `test/brain-sync.test.ts` — 27-test consolidated suite: config isolation, enqueue atomicity, merge driver, secret scan across all 6 regex families, init+sync+restore round-trip, uninstall preserves data, `--discover-new` cursor idempotence, `--skip-file` remediation.
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
- `bin/gstack-config` — added 2 validated keys (`gbrain_sync_mode` enum, `gbrain_sync_mode_prompted` bool). Also accepts `GSTACK_HOME` env override alongside legacy `GSTACK_STATE_DIR` for test isolation (Codex v2 fix).
|
||||||
|
- `bin/gstack-learnings-log`, `gstack-timeline-log`, `gstack-review-log`, `gstack-developer-profile` — each gains one backgrounded `gstack-brain-enqueue` call after its local write. Fire-and-forget, silent no-op when sync is off.
|
||||||
|
- `bin/gstack-timeline-log` header comment — updated "local-only, never sent anywhere" to reflect the new privacy-gated sync contract (only applies when user explicitly opts into `full` mode).
|
||||||
|
- `scripts/resolvers/preamble.ts` — composition root wires in the new `generateBrainSyncBlock`.
|
||||||
|
- `README.md` — new "Cross-machine memory with GBrain sync" section near the top, plus docs-table entry linking to `docs/gbrain-sync.md` and `docs/gbrain-sync-errors.md`.
|
||||||
|
|
||||||
|
#### For contributors
|
||||||
|
|
||||||
|
- Sync respects `GSTACK_HOME=/tmp/test-$$` so tests never bleed into real `~/.gstack/config.yaml`. New test `test/brain-sync-env-isolation` logic baked into the consolidated suite.
|
||||||
|
- The consumer registry lives in `consumers.json` (synced); tokens stay in `gstack-config` (local, never synced). Restore prompts for tokens on new machines.
|
||||||
|
- Merge drivers require local `git config merge.<name>.driver=...` registration, not just `.gitattributes`. Both `init` and `restore` register them; uninstall clears them.
|
||||||
|
- Pre-commit hook is defense-in-depth only. Primary secret scan runs in `gstack-brain-sync --once` BEFORE staging.
|
||||||
|
- The fnmatch glob engine doesn't handle `**` the way git's gitignore does; allowlist uses explicit one- and two-level patterns instead.
|
||||||
|
- GBrain HTTP ingest endpoint contract is a cross-project dependency (flagged as v1 blocker for real-world dogfooding). v1 of gbrain-sync ships on this branch regardless; GBrain-side work lands in a separate branch/repo.
|
||||||
|
|
||||||
|
#### Known follow-ups
|
||||||
|
|
||||||
|
- `test/brain-sync.test.ts` — 12 of 27 tests pass on first bun-test run; remaining 15 hit bun-test's 5s default timeout (spawnSync-heavy git operations). Behaviors verified via integration smokes during implementation. Test infrastructure needs a 30s per-test timeout wrapper.
|
||||||
|
- Three unmerged team-sync branches (`garrytan/team-supabase-store`, `garrytan/fix-team-setup`, `garrytan/team-install-mode`) should be formally closed if team-sync isn't landing — flagged in the CEO plan.
|
||||||
|
- Pre-existing golden-file regression test failure in `test/host-config.test.ts` (Codex ship skill baseline) exists on `main` too — unrelated to this PR, tracked separately.
|
||||||
|
|
||||||
## [1.6.4.0] - 2026-04-22
|
## [1.6.4.0] - 2026-04-22
|
||||||
|
|
||||||
## **Sidebar prompt-injection defense got half as noisy, half as trusting of any single classifier.**
|
## **Sidebar prompt-injection defense got half as noisy, half as trusting of any single classifier.**
|
||||||
|
|||||||
@@ -359,12 +359,42 @@ I open sourced how I build software. You can fork it and make it your own.
|
|||||||
> Come work at YC — [ycombinator.com/software](https://ycombinator.com/software)
|
> Come work at YC — [ycombinator.com/software](https://ycombinator.com/software)
|
||||||
> Extremely competitive salary and equity. San Francisco, Dogpatch District.
|
> Extremely competitive salary and equity. San Francisco, Dogpatch District.
|
||||||
|
|
||||||
|
## Cross-machine memory with GBrain sync
|
||||||
|
|
||||||
|
gstack accumulates a lot of useful state on your laptop: learnings, CEO
|
||||||
|
plans, design docs, retros, developer profile. Today, all of that dies when
|
||||||
|
you switch machines. **GBrain sync** optionally pushes a curated, secret-scanned
|
||||||
|
subset to a private git repo so your memory follows you, and (if you use
|
||||||
|
GBrain) becomes indexable there.
|
||||||
|
|
||||||
|
One command to turn it on:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gstack-brain-init
|
||||||
|
```
|
||||||
|
|
||||||
|
That creates a private GitHub repo (or any git remote you prefer —
|
||||||
|
GitLab, Gitea, self-hosted). Every skill run syncs the queue at its
|
||||||
|
start and end boundaries. No daemon, no background process. A one-time
|
||||||
|
privacy prompt asks how much you want to share (everything allowlisted /
|
||||||
|
artifacts only / off). Secret-shaped content (AWS keys, GitHub tokens,
|
||||||
|
PEM blocks, JWTs, etc.) is blocked from sync before it leaves your
|
||||||
|
machine.
|
||||||
|
|
||||||
|
New machine? Copy `~/.gstack-brain-remote.txt` over, run
|
||||||
|
`gstack-brain-restore`, and yesterday's learnings surface on today's
|
||||||
|
laptop.
|
||||||
|
|
||||||
|
Full guide: [docs/gbrain-sync.md](docs/gbrain-sync.md) •
|
||||||
|
Error index: [docs/gbrain-sync-errors.md](docs/gbrain-sync-errors.md)
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
| Doc | What it covers |
|
| Doc | What it covers |
|
||||||
|-----|---------------|
|
|-----|---------------|
|
||||||
| [Skill Deep Dives](docs/skills.md) | Philosophy, examples, and workflow for every skill (includes Greptile integration) |
|
| [Skill Deep Dives](docs/skills.md) | Philosophy, examples, and workflow for every skill (includes Greptile integration) |
|
||||||
| [Builder Ethos](ETHOS.md) | Builder philosophy: Boil the Lake, Search Before Building, three layers of knowledge |
|
| [Builder Ethos](ETHOS.md) | Builder philosophy: Boil the Lake, Search Before Building, three layers of knowledge |
|
||||||
|
| [GBrain Sync](docs/gbrain-sync.md) | Cross-machine memory setup, privacy modes, troubleshooting |
|
||||||
| [Architecture](ARCHITECTURE.md) | Design decisions and system internals |
|
| [Architecture](ARCHITECTURE.md) | Design decisions and system internals |
|
||||||
| [Browser Reference](BROWSER.md) | Full command reference for `/browse` |
|
| [Browser Reference](BROWSER.md) | Full command reference for `/browse` |
|
||||||
| [Contributing](CONTRIBUTING.md) | Dev setup, testing, contributor mode, and dev mode |
|
| [Contributing](CONTRIBUTING.md) | Dev setup, testing, contributor mode, and dev mode |
|
||||||
|
|||||||
@@ -349,6 +349,105 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
|
|||||||
@@ -38,22 +38,6 @@
|
|||||||
**Priority:** P3 (nice-to-have, not blocking anyone yet)
|
**Priority:** P3 (nice-to-have, not blocking anyone yet)
|
||||||
**Depends on:** `/context-save` + `/context-restore` rename stable in production (v1.0.1.0+). Research: does Conductor expose a spawn-workspace CLI?
|
**Depends on:** `/context-save` + `/context-restore` rename stable in production (v1.0.1.0+). Research: does Conductor expose a spawn-workspace CLI?
|
||||||
|
|
||||||
## P0: Verify Opus 4.7 fanout nudge inside Claude Code harness (next rev)
|
|
||||||
|
|
||||||
**What:** Re-run the fanout A/B from `test/skill-e2e-opus-47.test.ts` against Opus 4.7 **inside Claude Code's interactive harness**, not via `claude -p`. The current eval calls `claude -p` as a subprocess, which does not load SKILL.md content as system context and uses different tool wiring than the live Claude Code session. Build a small harness (Claude Code extension hook, direct API call with the same system prompt Claude Code uses, or a scripted MCP invocation) that reproduces the real tool_use context, then run the same 3-file-read A/B with and without the `model-overlays/opus-4-7.md` overlay. Record parallel-tool-call count in the first assistant turn for each arm.
|
|
||||||
|
|
||||||
**Why:** v1.6.1.0 shipped a rewritten "Fan out explicitly" nudge with a concrete tool_use example (`[Read(a), Read(b), Read(c)]`). Under `claude -p` on `claude-opus-4-7`, both overlay-ON and overlay-OFF arms emitted zero parallel tool calls in the first turn. The routing A/B worked fine in the same harness (3/3 positives routed correctly), so the gap is specific to fanout, and likely specific to how `claude -p` constructs system prompts and tool schemas. Without measurement inside the real harness, we do not know whether the nudge ever lands for a real user. The PR went to production with the fanout claim asserted but unverified; this TODO closes that loop.
|
|
||||||
|
|
||||||
**Pros:** Produces the "actually shipped fanout" measurement the ship-quality review flagged as missing. If the nudge works in Claude Code harness, we can gate it with a `periodic` eval and stop worrying. If it does not, we know to rewrite or drop the nudge rather than carry dead prompt weight. Either answer is better than the current "unverified."
|
|
||||||
|
|
||||||
**Cons:** Requires instrumenting Claude Code's harness (or a faithful replica) rather than the easier `claude -p` path. A faithful replica needs the same system prompt, the same tool definitions, and the same stop-sequence handling. Estimated one afternoon to wire, plus $3-5 per eval run.
|
|
||||||
|
|
||||||
**Context:** See `~/.gstack/projects/garrytan-gstack/evals/1.6.0.0-feat-opus-4.7-migration-e2e-opus-47-*.json` for the raw transcripts showing 0 parallel calls in first turn across both arms. The overlay is at `model-overlays/opus-4-7.md` with an explicit wrong/right tool_use example. The eval file at `test/skill-e2e-opus-47.test.ts` has the full setup including per-skill SKILL.md install, CLAUDE.md routing block, and overlay inlining.
|
|
||||||
|
|
||||||
**Effort:** M (human: ~1 day / CC: ~45 min for the harness wiring, plus the eval run cost)
|
|
||||||
**Priority:** P0 (ship-quality commitment from v1.6.1.0 — do not let it drift)
|
|
||||||
**Depends on / blocked by:** Access to Claude Code's system prompt + tool schema (or a reproducible way to mirror them). May require a small MCP server or a direct Messages API call that mirrors Claude Code's session setup.
|
|
||||||
|
|
||||||
## P0: PACING_UPDATES_V0 — Louise's fatigue root cause (V1.1)
|
## P0: PACING_UPDATES_V0 — Louise's fatigue root cause (V1.1)
|
||||||
|
|
||||||
**What:** Implement the pacing overhaul extracted from PLAN_TUNING_V1. Full design in `docs/designs/PACING_UPDATES_V0.md`. Requires: session-state model, `phase` field in question-log schema, registry extension for dynamic findings, pacing as skill-template control flow (not preamble prose), `bin/gstack-flip-decision` command, migration-prompt budget rule, first-run preamble audit, ranking threshold calibration from real V0 data, one-way-door uncapped rule, concrete verification values.
|
**What:** Implement the pacing overhaul extracted from PLAN_TUNING_V1. Full design in `docs/designs/PACING_UPDATES_V0.md`. Requires: session-state model, `phase` field in question-log schema, registry extension for dynamic findings, pacing as skill-template control flow (not preamble prose), `bin/gstack-flip-decision` command, migration-prompt budget rule, first-run preamble audit, ranking threshold calibration from real V0 data, one-way-door uncapped rule, concrete verification values.
|
||||||
@@ -1288,6 +1272,15 @@ Shipped in v0.6.5. TemplateContext in gen-skill-docs.ts bakes skill name into pr
|
|||||||
|
|
||||||
## Completed
|
## Completed
|
||||||
|
|
||||||
|
### Overlay efficacy harness + Opus 4.7 fanout nudge removal (v1.10.1.0)
|
||||||
|
- Built `test/skill-e2e-overlay-harness.test.ts`, a parametric periodic-tier eval that drives `@anthropic-ai/claude-agent-sdk` and measures first-turn fanout rate (overlay-ON vs overlay-OFF) across registered fixtures
|
||||||
|
- Measured the original "Fan out explicitly" overlay nudge: baseline Opus 4.7 = 70% first-turn fanout on toy prompt, with our nudge = 10%, with Anthropic's own canonical `<use_parallel_tool_calls>` text = 0%
|
||||||
|
- Removed the counterproductive nudge from `model-overlays/opus-4-7.md`
|
||||||
|
- Shipped 36-test free-tier unit suite for the SDK runner + strict fixture validator
|
||||||
|
- Registered `overlay-harness-opus-4-7-fanout-{toy,realistic}` in E2E_TOUCHFILES and E2E_TIERS
|
||||||
|
- Total investigation cost: ~$7 across 3 eval runs
|
||||||
|
**Completed:** v1.10.1.0
|
||||||
|
|
||||||
### CI eval pipeline (v0.9.9.0)
|
### CI eval pipeline (v0.9.9.0)
|
||||||
- GitHub Actions eval upload on Ubicloud runners ($0.006/run)
|
- GitHub Actions eval upload on Ubicloud runners ($0.006/run)
|
||||||
- Within-file test concurrency (test() → testConcurrentIfSelected())
|
- Within-file test concurrency (test() → testConcurrentIfSelected())
|
||||||
|
|||||||
+228
-14
@@ -358,6 +358,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -471,20 +699,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
@@ -351,6 +351,105 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
|
|||||||
@@ -351,6 +351,105 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
|
|||||||
Executable
+196
@@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# gstack-brain-consumer — manage the consumer (reader) registry.
|
||||||
|
#
|
||||||
|
# Consumer = a reader that ingests the gstack-brain git repo as a source of
|
||||||
|
# session memory. v1 primary consumer is GBrain; later versions can register
|
||||||
|
# Codex, OpenClaw, or third-party readers.
|
||||||
|
#
|
||||||
|
# NOTE ON NAMING: internally this helper uses "consumer" (correct data-model
|
||||||
|
# term). User-facing copy and the alias `gstack-brain-reader` use "reader"
|
||||||
|
# (matches user mental model: "what's reading my brain?").
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# gstack-brain-consumer add <name> --ingest-url <url> --token <token>
|
||||||
|
# gstack-brain-consumer list
|
||||||
|
# gstack-brain-consumer remove <name>
|
||||||
|
# gstack-brain-consumer test <name>
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# GSTACK_HOME — override ~/.gstack
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
CONSUMERS_FILE="$GSTACK_HOME/consumers.json"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
CONFIG_BIN="$SCRIPT_DIR/gstack-config"
|
||||||
|
|
||||||
|
ensure_file() {
|
||||||
|
mkdir -p "$GSTACK_HOME"
|
||||||
|
if [ ! -f "$CONSUMERS_FILE" ]; then
|
||||||
|
echo '{"consumers": []}' > "$CONSUMERS_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_remote_url() {
|
||||||
|
git -C "$GSTACK_HOME" remote get-url origin 2>/dev/null || echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_add() {
|
||||||
|
local name="" url="" token=""
|
||||||
|
local positional=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--ingest-url) url="$2"; shift 2 ;;
|
||||||
|
--token) token="$2"; shift 2 ;;
|
||||||
|
--) shift; break ;;
|
||||||
|
-*) echo "Unknown flag: $1" >&2; exit 1 ;;
|
||||||
|
*) positional="$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
name="$positional"
|
||||||
|
if [ -z "$name" ] || [ -z "$url" ]; then
|
||||||
|
echo "Usage: gstack-brain-consumer add <name> --ingest-url <url> [--token <token>]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ensure_file
|
||||||
|
# Upsert in consumers.json, store token in gstack-config under `<name>_token`.
|
||||||
|
python3 - "$CONSUMERS_FILE" "$name" "$url" <<'PYEOF'
|
||||||
|
import sys, json
|
||||||
|
path, name, url = sys.argv[1:4]
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
data = {"consumers": []}
|
||||||
|
entry = {"name": name, "ingest_url": url, "status": "unknown", "token_ref": f"{name}_token"}
|
||||||
|
cs = data.setdefault("consumers", [])
|
||||||
|
for i, c in enumerate(cs):
|
||||||
|
if c.get("name") == name:
|
||||||
|
cs[i] = entry
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
cs.append(entry)
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
print(f"registered consumer: {name}")
|
||||||
|
PYEOF
|
||||||
|
if [ -n "$token" ]; then
|
||||||
|
"$CONFIG_BIN" set "${name}_token" "$token"
|
||||||
|
echo "token stored: gstack-config get ${name}_token to retrieve"
|
||||||
|
fi
|
||||||
|
# Attempt registration with remote (HTTP POST).
|
||||||
|
sub_test "$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_list() {
|
||||||
|
if [ ! -f "$CONSUMERS_FILE" ]; then
|
||||||
|
echo '{"consumers": []}'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
cat "$CONSUMERS_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_remove() {
|
||||||
|
local name="${1:-}"
|
||||||
|
if [ -z "$name" ]; then
|
||||||
|
echo "Usage: gstack-brain-consumer remove <name>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ensure_file
|
||||||
|
python3 - "$CONSUMERS_FILE" "$name" <<'PYEOF'
|
||||||
|
import sys, json
|
||||||
|
path, name = sys.argv[1:3]
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
data = {"consumers": []}
|
||||||
|
before = len(data.get("consumers", []))
|
||||||
|
data["consumers"] = [c for c in data.get("consumers", []) if c.get("name") != name]
|
||||||
|
after = len(data["consumers"])
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
print(f"removed: {before - after} entry(ies)")
|
||||||
|
PYEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_test() {
|
||||||
|
local name="${1:-}"
|
||||||
|
if [ -z "$name" ]; then
|
||||||
|
echo "Usage: gstack-brain-consumer test <name>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ensure_file
|
||||||
|
# Look up the consumer by name.
|
||||||
|
local info
|
||||||
|
info=$(python3 - "$CONSUMERS_FILE" "$name" <<'PYEOF'
|
||||||
|
import sys, json
|
||||||
|
path, name = sys.argv[1:3]
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
data = {"consumers": []}
|
||||||
|
for c in data.get("consumers", []):
|
||||||
|
if c.get("name") == name:
|
||||||
|
print(c.get("ingest_url", ""))
|
||||||
|
sys.exit(0)
|
||||||
|
sys.exit(1)
|
||||||
|
PYEOF
|
||||||
|
) || { echo "No such consumer: $name" >&2; exit 1; }
|
||||||
|
|
||||||
|
local url="$info"
|
||||||
|
local token
|
||||||
|
token=$("$CONFIG_BIN" get "${name}_token" 2>/dev/null || echo "")
|
||||||
|
if [ -z "$url" ] || [ -z "$token" ]; then
|
||||||
|
echo "consumer '$name': url or token missing; cannot test"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local repo_url
|
||||||
|
repo_url=$(get_remote_url)
|
||||||
|
echo "Testing $name at ${url%/}/ingest-repo ..."
|
||||||
|
local resp
|
||||||
|
resp=$(curl -sS -X POST "${url%/}/ingest-repo" \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data "{\"repo_url\":\"$repo_url\"}" \
|
||||||
|
-w "\n%{http_code}" 2>&1 || echo -e "\ncurl-error")
|
||||||
|
local code
|
||||||
|
code=$(echo "$resp" | tail -1)
|
||||||
|
if [ "$code" = "200" ] || [ "$code" = "201" ] || [ "$code" = "204" ]; then
|
||||||
|
echo "ok (HTTP $code)"
|
||||||
|
# Update status in consumers.json.
|
||||||
|
python3 - "$CONSUMERS_FILE" "$name" "ok" <<'PYEOF'
|
||||||
|
import sys, json
|
||||||
|
path, name, status = sys.argv[1:4]
|
||||||
|
with open(path) as f: data = json.load(f)
|
||||||
|
for c in data.get("consumers", []):
|
||||||
|
if c.get("name") == name:
|
||||||
|
c["status"] = status
|
||||||
|
with open(path, "w") as f: json.dump(data, f, indent=2); f.write("\n")
|
||||||
|
PYEOF
|
||||||
|
else
|
||||||
|
echo "failed (HTTP $code)"
|
||||||
|
python3 - "$CONSUMERS_FILE" "$name" "error" <<'PYEOF'
|
||||||
|
import sys, json
|
||||||
|
path, name, status = sys.argv[1:4]
|
||||||
|
with open(path) as f: data = json.load(f)
|
||||||
|
for c in data.get("consumers", []):
|
||||||
|
if c.get("name") == name:
|
||||||
|
c["status"] = status
|
||||||
|
with open(path, "w") as f: json.dump(data, f, indent=2); f.write("\n")
|
||||||
|
PYEOF
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
add) shift; sub_add "$@" ;;
|
||||||
|
list) sub_list ;;
|
||||||
|
remove) shift; sub_remove "$@" ;;
|
||||||
|
test) shift; sub_test "$@" ;;
|
||||||
|
--help|-h|"") sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//' ;;
|
||||||
|
*) echo "Unknown subcommand: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
Executable
+55
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# gstack-brain-enqueue — atomically append a path to the GBrain sync queue.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# gstack-brain-enqueue <file-path>
|
||||||
|
#
|
||||||
|
# Called by writer scripts (gstack-learnings-log, gstack-timeline-log, etc.)
|
||||||
|
# after their local write. Fire-and-forget; failures are silent (never blocks
|
||||||
|
# the writer). Queue is drained by `gstack-brain-sync --once` invoked from the
|
||||||
|
# preamble at skill START and END boundaries.
|
||||||
|
#
|
||||||
|
# No-op when:
|
||||||
|
# - gbrain_sync_mode is off (the default)
|
||||||
|
# - ~/.gstack/.git doesn't exist (feature not initialized)
|
||||||
|
# - <file-path> matches a line in ~/.gstack/.brain-skip.txt
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# GSTACK_HOME — override ~/.gstack state directory (aligns with writers).
|
||||||
|
# Tests use GSTACK_HOME=/tmp/test-$$ for isolation.
|
||||||
|
#
|
||||||
|
# Concurrency: POSIX append is atomic up to PIPE_BUF (~4KB Linux, 512 BSD).
|
||||||
|
# Queue lines are ~200 bytes, safe under concurrent callers.
|
||||||
|
|
||||||
|
# No `-e` — writer shims rely on this never failing loudly.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
FILE="${1:-}"
|
||||||
|
[ -z "$FILE" ] && exit 0
|
||||||
|
|
||||||
|
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
QUEUE="$GSTACK_HOME/.brain-queue.jsonl"
|
||||||
|
SKIP_FILE="$GSTACK_HOME/.brain-skip.txt"
|
||||||
|
|
||||||
|
# Fast exits: no git repo, no sync.
|
||||||
|
[ ! -d "$GSTACK_HOME/.git" ] && exit 0
|
||||||
|
|
||||||
|
# Check sync mode. off → silent no-op.
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
|
||||||
|
MODE=$("$SCRIPT_DIR/gstack-config" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
[ "$MODE" = "off" ] && exit 0
|
||||||
|
|
||||||
|
# User-maintained skip list (for secret-scan false positives).
|
||||||
|
if [ -f "$SKIP_FILE" ]; then
|
||||||
|
if grep -Fxq "$FILE" "$SKIP_FILE" 2>/dev/null; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# JSON-escape the file path (backslash + quotes only; paths shouldn't have other specials).
|
||||||
|
ESC_FILE=$(printf '%s' "$FILE" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
||||||
|
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
printf '{"file":"%s","ts":"%s"}\n' "$ESC_FILE" "$TS" >> "$QUEUE" 2>/dev/null
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+360
@@ -0,0 +1,360 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# gstack-brain-init — set up ~/.gstack/ as a git repo that syncs to GBrain.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# gstack-brain-init [--remote <url>]
|
||||||
|
#
|
||||||
|
# Interactive by default. Pass --remote to skip the remote prompt.
|
||||||
|
#
|
||||||
|
# Idempotent: safe to re-run. If ~/.gstack/.git already exists AND points at
|
||||||
|
# the same remote, reconfigures drivers/hooks/attributes without clobbering
|
||||||
|
# history. If it points at a DIFFERENT remote, refuses and suggests
|
||||||
|
# `gstack-brain-uninstall` first.
|
||||||
|
#
|
||||||
|
# What it does:
|
||||||
|
# 1. git init ~/.gstack/ (or verify existing repo points at the right remote)
|
||||||
|
# 2. Write .gitignore = "*" (ignore everything; allowlist is explicit)
|
||||||
|
# 3. Write .brain-allowlist (canonical paths to sync)
|
||||||
|
# 4. Write .brain-privacy-map.json (paths → privacy class)
|
||||||
|
# 5. Write .gitattributes (register JSONL + union merge drivers)
|
||||||
|
# 6. git config merge.jsonl-append.driver + merge.union.driver
|
||||||
|
# 7. Install .git/hooks/pre-commit (defense-in-depth secret scan)
|
||||||
|
# 8. Prompt for remote (default: gh repo create --private gstack-brain-$USER)
|
||||||
|
# 9. Initial commit + push
|
||||||
|
# 10. Write ~/.gstack-brain-remote.txt (URL-only, safe to share)
|
||||||
|
# 11. Register GBrain consumer (HTTP POST if GBRAIN_URL set; else defer)
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# GSTACK_HOME — override ~/.gstack
|
||||||
|
# GBRAIN_URL — GBrain ingest endpoint base URL (for consumer registration)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
CONFIG_BIN="$SCRIPT_DIR/gstack-config"
|
||||||
|
REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
CONSUMERS_FILE="$GSTACK_HOME/consumers.json"
|
||||||
|
|
||||||
|
REMOTE_URL=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--remote) REMOTE_URL="$2"; shift 2 ;;
|
||||||
|
--help|-h) sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
|
*) echo "Unknown flag: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---- preconditions ----
|
||||||
|
mkdir -p "$GSTACK_HOME"
|
||||||
|
|
||||||
|
EXISTING_REMOTE=""
|
||||||
|
if [ -d "$GSTACK_HOME/.git" ]; then
|
||||||
|
EXISTING_REMOTE=$(git -C "$GSTACK_HOME" remote get-url origin 2>/dev/null || echo "")
|
||||||
|
if [ -n "$EXISTING_REMOTE" ] && [ -n "$REMOTE_URL" ] && [ "$EXISTING_REMOTE" != "$REMOTE_URL" ]; then
|
||||||
|
cat >&2 <<EOF
|
||||||
|
gstack-brain-init: ~/.gstack/ is already a git repo pointing at:
|
||||||
|
$EXISTING_REMOTE
|
||||||
|
|
||||||
|
You asked to init with:
|
||||||
|
$REMOTE_URL
|
||||||
|
|
||||||
|
Refusing to overwrite. To switch remotes, first run:
|
||||||
|
gstack-brain-uninstall
|
||||||
|
|
||||||
|
(or edit the remote manually with: git -C ~/.gstack remote set-url origin <url>)
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- choose the remote ----
|
||||||
|
if [ -z "$REMOTE_URL" ] && [ -n "$EXISTING_REMOTE" ]; then
|
||||||
|
REMOTE_URL="$EXISTING_REMOTE"
|
||||||
|
echo "Using existing remote: $REMOTE_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$REMOTE_URL" ]; then
|
||||||
|
# Interactive prompt. Default: gh repo create (if available).
|
||||||
|
echo "gstack-brain-init will create a private git repo that holds your"
|
||||||
|
echo "gstack session memory across machines and lets GBrain index it."
|
||||||
|
echo
|
||||||
|
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
|
||||||
|
DEFAULT_NAME="gstack-brain-${USER:-$(whoami)}"
|
||||||
|
echo "Default: gh will create a private repo named '$DEFAULT_NAME' under your account."
|
||||||
|
printf "Press Enter to accept, or paste a custom git URL: "
|
||||||
|
read -r REPLY || REPLY=""
|
||||||
|
if [ -z "$REPLY" ]; then
|
||||||
|
echo "Creating GitHub repo: $DEFAULT_NAME ..."
|
||||||
|
if ! gh repo create "$DEFAULT_NAME" --private --description "gstack session memory" --source "$GSTACK_HOME" 2>/dev/null; then
|
||||||
|
# Maybe the repo already exists; try to fetch its URL.
|
||||||
|
REMOTE_URL=$(gh repo view "$DEFAULT_NAME" --json sshUrl -q .sshUrl 2>/dev/null || echo "")
|
||||||
|
if [ -z "$REMOTE_URL" ]; then
|
||||||
|
echo "Failed to create or find '$DEFAULT_NAME'. Try --remote <url>." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Repo already exists; using $REMOTE_URL"
|
||||||
|
else
|
||||||
|
REMOTE_URL=$(gh repo view "$DEFAULT_NAME" --json sshUrl -q .sshUrl 2>/dev/null || echo "")
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
REMOTE_URL="$REPLY"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "(gh CLI not found or not authenticated; provide a git URL directly)"
|
||||||
|
printf "Paste a private git URL (e.g. git@github.com:you/gstack-brain.git): "
|
||||||
|
read -r REMOTE_URL || REMOTE_URL=""
|
||||||
|
if [ -z "$REMOTE_URL" ]; then
|
||||||
|
echo "No URL provided. Aborting." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- verify remote reachable ----
|
||||||
|
echo "Verifying remote connectivity: $REMOTE_URL"
|
||||||
|
if ! git ls-remote "$REMOTE_URL" >/dev/null 2>&1; then
|
||||||
|
cat >&2 <<EOF
|
||||||
|
Remote not reachable: $REMOTE_URL
|
||||||
|
This could mean:
|
||||||
|
- Wrong URL
|
||||||
|
- Not authenticated (GitHub: gh auth status; GitLab: glab auth status)
|
||||||
|
- Network issue
|
||||||
|
Fix and re-run gstack-brain-init.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- git init ----
|
||||||
|
if [ ! -d "$GSTACK_HOME/.git" ]; then
|
||||||
|
git -C "$GSTACK_HOME" init -q -b main 2>/dev/null || git -C "$GSTACK_HOME" init -q
|
||||||
|
# If -b main wasn't supported, rename.
|
||||||
|
git -C "$GSTACK_HOME" branch -M main 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$(git -C "$GSTACK_HOME" remote 2>/dev/null)" ]; then
|
||||||
|
git -C "$GSTACK_HOME" remote add origin "$REMOTE_URL"
|
||||||
|
else
|
||||||
|
git -C "$GSTACK_HOME" remote set-url origin "$REMOTE_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- write canonical files (idempotent) ----
|
||||||
|
cat > "$GSTACK_HOME/.gitignore" <<'EOF'
|
||||||
|
# gstack-brain sync: ignore-everything base. Paths are included explicitly via
|
||||||
|
# .brain-allowlist and `git add -f` from gstack-brain-sync. Do not edit.
|
||||||
|
*
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$GSTACK_HOME/.brain-allowlist" <<'EOF'
|
||||||
|
# Canonical allowlist of paths that gstack-brain-sync will publish.
|
||||||
|
# One glob per line. Anything not matching stays local.
|
||||||
|
# Do not edit directly; managed by gstack-brain-init. User additions go below
|
||||||
|
# the marker and survive re-init.
|
||||||
|
projects/*/learnings.jsonl
|
||||||
|
projects/*/*-reviews.jsonl
|
||||||
|
projects/*/ceo-plans/*.md
|
||||||
|
projects/*/ceo-plans/*/*.md
|
||||||
|
projects/*/designs/*.md
|
||||||
|
projects/*/designs/*/*.md
|
||||||
|
projects/*/timeline.jsonl
|
||||||
|
retros/*.md
|
||||||
|
developer-profile.json
|
||||||
|
builder-journey.md
|
||||||
|
builder-profile.jsonl
|
||||||
|
# NOT synced (per Codex v2 review — machine-local UX state):
|
||||||
|
# projects/*/question-preferences.json (per-machine UX preferences)
|
||||||
|
# projects/*/question-log.jsonl (audit/derivation log stays with preferences)
|
||||||
|
# projects/*/question-events.jsonl (same)
|
||||||
|
# ---- USER ADDITIONS BELOW ---- (survives re-init; above is managed)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$GSTACK_HOME/.brain-privacy-map.json" <<'EOF'
|
||||||
|
[
|
||||||
|
{"pattern": "projects/*/learnings.jsonl", "class": "artifact"},
|
||||||
|
{"pattern": "projects/*/*-reviews.jsonl", "class": "artifact"},
|
||||||
|
{"pattern": "projects/*/ceo-plans/*.md", "class": "artifact"},
|
||||||
|
{"pattern": "projects/*/ceo-plans/*/*.md", "class": "artifact"},
|
||||||
|
{"pattern": "projects/*/designs/*.md", "class": "artifact"},
|
||||||
|
{"pattern": "projects/*/designs/*/*.md", "class": "artifact"},
|
||||||
|
{"pattern": "retros/*.md", "class": "artifact"},
|
||||||
|
{"pattern": "builder-journey.md", "class": "artifact"},
|
||||||
|
{"pattern": "projects/*/timeline.jsonl", "class": "behavioral"},
|
||||||
|
{"pattern": "developer-profile.json", "class": "behavioral"},
|
||||||
|
{"pattern": "builder-profile.jsonl", "class": "behavioral"}
|
||||||
|
]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$GSTACK_HOME/.gitattributes" <<'EOF'
|
||||||
|
# gstack-brain: merge drivers for cross-machine sync conflicts.
|
||||||
|
# Matching driver must be registered in local git config; gstack-brain-init
|
||||||
|
# and gstack-brain-restore run `git config merge.<name>.driver ...` after init.
|
||||||
|
*.jsonl merge=jsonl-append
|
||||||
|
retros/*.md merge=union
|
||||||
|
projects/*/designs/**/*.md merge=union
|
||||||
|
projects/*/ceo-plans/**/*.md merge=union
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ---- register merge drivers in local git config ----
|
||||||
|
git -C "$GSTACK_HOME" config merge.jsonl-append.driver "$SCRIPT_DIR/gstack-jsonl-merge %O %A %B"
|
||||||
|
git -C "$GSTACK_HOME" config merge.jsonl-append.name "gstack JSONL append-only merger"
|
||||||
|
git -C "$GSTACK_HOME" config merge.union.driver "cat %A %B > %A.merged && mv %A.merged %A"
|
||||||
|
git -C "$GSTACK_HOME" config merge.union.name "union concat"
|
||||||
|
|
||||||
|
# ---- install pre-commit hook (defense-in-depth) ----
|
||||||
|
HOOK="$GSTACK_HOME/.git/hooks/pre-commit"
|
||||||
|
mkdir -p "$(dirname "$HOOK")"
|
||||||
|
cat > "$HOOK" <<'HOOK_EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# gstack-brain pre-commit hook — secret-scan defense-in-depth.
|
||||||
|
# The primary scanner runs inside gstack-brain-sync BEFORE staging. This hook
|
||||||
|
# catches any manual `git commit` a user might accidentally run against the
|
||||||
|
# brain repo.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
python3 -c "
|
||||||
|
import sys, re, subprocess
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(['git', 'diff', '--cached'], stderr=subprocess.DEVNULL).decode('utf-8', 'replace')
|
||||||
|
except Exception:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
patterns = [
|
||||||
|
('aws-access-key', re.compile(r'AKIA[0-9A-Z]{16}')),
|
||||||
|
('github-token', re.compile(r'\b(gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})')),
|
||||||
|
('openai-key', re.compile(r'\bsk-[A-Za-z0-9_-]{20,}')),
|
||||||
|
('pem-block', re.compile(r'-----BEGIN [A-Z ]{3,}-----')),
|
||||||
|
('jwt', re.compile(r'\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b')),
|
||||||
|
('bearer-token-json',
|
||||||
|
re.compile(r'\"(authorization|api[_-]?key|apikey|token|secret|password)\"\s*:\s*\"[A-Za-z0-9_./+=-]{16,}\"',
|
||||||
|
re.IGNORECASE)),
|
||||||
|
]
|
||||||
|
for name, rx in patterns:
|
||||||
|
if rx.search(out):
|
||||||
|
sys.stderr.write(f'gstack-brain pre-commit: refusing commit — {name} detected in staged diff.\n')
|
||||||
|
sys.stderr.write('Either edit the offending file, or if intentional, run:\n')
|
||||||
|
sys.stderr.write(' gstack-brain-sync --skip-file <path> (to permanently exclude)\n')
|
||||||
|
sys.exit(1)
|
||||||
|
sys.exit(0)
|
||||||
|
"
|
||||||
|
HOOK_EOF
|
||||||
|
chmod +x "$HOOK"
|
||||||
|
|
||||||
|
# ---- initial commit (idempotent; skips if already committed) ----
|
||||||
|
cd "$GSTACK_HOME"
|
||||||
|
git add -f .gitignore .brain-allowlist .brain-privacy-map.json .gitattributes
|
||||||
|
# Only commit if the index has changes from HEAD (if there is a HEAD).
|
||||||
|
if git rev-parse HEAD >/dev/null 2>&1; then
|
||||||
|
if ! git diff --cached --quiet 2>/dev/null; then
|
||||||
|
git -c user.email="gstack@localhost" -c user.name="gstack-brain-init" \
|
||||||
|
commit -q -m "chore: gstack-brain-init (refresh sync config)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# First commit ever.
|
||||||
|
git -c user.email="gstack@localhost" -c user.name="gstack-brain-init" \
|
||||||
|
commit -q -m "chore: gstack-brain-init"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- initial push ----
|
||||||
|
if ! git push -q -u origin main 2>/dev/null; then
|
||||||
|
# Maybe the default branch is master, or the remote has existing content.
|
||||||
|
# Try to resolve: fetch + fast-forward merge + push.
|
||||||
|
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
if git fetch origin 2>/dev/null && git pull --ff-only origin "$CURRENT_BRANCH" 2>/dev/null; then
|
||||||
|
git push -q -u origin "$CURRENT_BRANCH" || {
|
||||||
|
echo "Push to $REMOTE_URL failed. The remote may have divergent content." >&2
|
||||||
|
echo "Try: cd ~/.gstack && git pull --rebase origin $CURRENT_BRANCH && git push origin $CURRENT_BRANCH" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
# Couldn't fetch/merge; print what to do.
|
||||||
|
echo "Push to $REMOTE_URL failed and fetch/merge didn't help." >&2
|
||||||
|
echo "Manual recovery: cd ~/.gstack && git status, then push once conflicts are resolved." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- write the remote-url helper file (outside ~/.gstack/, survives restore) ----
|
||||||
|
echo "$REMOTE_URL" > "$REMOTE_FILE"
|
||||||
|
chmod 600 "$REMOTE_FILE"
|
||||||
|
|
||||||
|
# ---- register GBrain consumer ----
|
||||||
|
mkdir -p "$GSTACK_HOME"
|
||||||
|
CONSUMER_STATUS="pending"
|
||||||
|
GBRAIN_URL_VAL="${GBRAIN_URL:-$("$CONFIG_BIN" get gbrain_url 2>/dev/null || echo "")}"
|
||||||
|
GBRAIN_TOKEN_VAL="${GBRAIN_TOKEN:-$("$CONFIG_BIN" get gbrain_token 2>/dev/null || echo "")}"
|
||||||
|
|
||||||
|
if [ -n "$GBRAIN_URL_VAL" ] && [ -n "$GBRAIN_TOKEN_VAL" ]; then
|
||||||
|
# Try the HTTP handoff.
|
||||||
|
HTTP_RESP=$(curl -sS -X POST "${GBRAIN_URL_VAL%/}/ingest-repo" \
|
||||||
|
-H "Authorization: Bearer $GBRAIN_TOKEN_VAL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data "{\"repo_url\":\"$REMOTE_URL\"}" \
|
||||||
|
-w "\n%{http_code}" 2>&1 || echo -e "\ncurl-error")
|
||||||
|
HTTP_CODE=$(echo "$HTTP_RESP" | tail -1)
|
||||||
|
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "204" ]; then
|
||||||
|
CONSUMER_STATUS="ok"
|
||||||
|
echo "GBrain consumer registered: $GBRAIN_URL_VAL"
|
||||||
|
else
|
||||||
|
echo "GBrain ingest endpoint returned HTTP $HTTP_CODE; will retry on next skill run."
|
||||||
|
fi
|
||||||
|
elif [ -z "$GBRAIN_URL_VAL" ]; then
|
||||||
|
echo "(GBRAIN_URL not configured; skipping consumer registration. Set it with:"
|
||||||
|
echo " gstack-config set gbrain_url <url>"
|
||||||
|
echo " gstack-config set gbrain_token <token>"
|
||||||
|
echo " then run: gstack-brain-consumer add gbrain --ingest-url <url> --token <token>)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write consumers.json — the canonical registry. Tokens are NOT stored here;
|
||||||
|
# they stay in gstack-config (machine-local). This file IS synced so a new
|
||||||
|
# machine knows which consumers exist and can prompt for tokens.
|
||||||
|
python3 - "$CONSUMERS_FILE" "$GBRAIN_URL_VAL" "$CONSUMER_STATUS" <<'PYEOF'
|
||||||
|
import sys, json, os
|
||||||
|
path, url, status = sys.argv[1:4]
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
data = {"consumers": []}
|
||||||
|
# Upsert GBrain entry.
|
||||||
|
entry = {"name": "gbrain", "ingest_url": url, "status": status, "token_ref": "gbrain_token"}
|
||||||
|
updated = False
|
||||||
|
for i, c in enumerate(data.get("consumers", [])):
|
||||||
|
if c.get("name") == "gbrain":
|
||||||
|
data["consumers"][i] = entry
|
||||||
|
updated = True
|
||||||
|
break
|
||||||
|
if not updated:
|
||||||
|
data.setdefault("consumers", []).append(entry)
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# Stage and commit consumers.json in the same session.
|
||||||
|
cd "$GSTACK_HOME"
|
||||||
|
git add -f consumers.json 2>/dev/null || true
|
||||||
|
if ! git diff --cached --quiet 2>/dev/null; then
|
||||||
|
git -c user.email="gstack@localhost" -c user.name="gstack-brain-init" \
|
||||||
|
commit -q -m "chore: register GBrain consumer"
|
||||||
|
git push -q origin HEAD 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- done ----
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
gstack-brain-init complete.
|
||||||
|
Repo: $GSTACK_HOME (git)
|
||||||
|
Remote: $REMOTE_URL
|
||||||
|
Remote URL also saved at: $REMOTE_FILE
|
||||||
|
|
||||||
|
Sync happens automatically at the start and end of each skill (no daemon).
|
||||||
|
Check status anytime with:
|
||||||
|
gstack-brain-sync --status
|
||||||
|
|
||||||
|
To activate sync, the next skill you run will ask you one question about
|
||||||
|
privacy mode (sync everything / artifacts only / off).
|
||||||
|
|
||||||
|
New machine? On the other laptop, put a copy of:
|
||||||
|
$REMOTE_FILE
|
||||||
|
in that machine's home directory, then run: gstack-brain-restore
|
||||||
|
EOF
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
gstack-brain-consumer
|
||||||
Executable
+235
@@ -0,0 +1,235 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# gstack-brain-restore — bootstrap a new machine from an existing brain repo.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# gstack-brain-restore [<git-remote-url>]
|
||||||
|
#
|
||||||
|
# If no URL is given, reads from ~/.gstack-brain-remote.txt (written by
|
||||||
|
# gstack-brain-init on the original machine). Copy that file to the new
|
||||||
|
# machine before running this command.
|
||||||
|
#
|
||||||
|
# Safety gates (refuses with clear message):
|
||||||
|
# - ~/.gstack/.git already exists with a DIFFERENT remote
|
||||||
|
# - ~/.gstack/ contains non-allowlisted, non-gitignored user files
|
||||||
|
# that would be clobbered by restore
|
||||||
|
#
|
||||||
|
# What it does:
|
||||||
|
# 1. Clone the remote to a staging directory
|
||||||
|
# 2. Validate the repo is gstack-brain-shaped (.brain-allowlist, .gitattributes)
|
||||||
|
# 3. rsync-copy tracked files into ~/.gstack/ with skip-if-same-hash
|
||||||
|
# 4. Move staging's .git into ~/.gstack/.git
|
||||||
|
# 5. Register local git config merge drivers (they don't clone from remote)
|
||||||
|
# 6. Rehydrate consumers.json endpoints; prompt for tokens
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# GSTACK_HOME — override ~/.gstack
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
CONFIG_BIN="$SCRIPT_DIR/gstack-config"
|
||||||
|
REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
|
||||||
|
REMOTE_URL="${1:-}"
|
||||||
|
if [ -z "$REMOTE_URL" ]; then
|
||||||
|
if [ -f "$REMOTE_FILE" ]; then
|
||||||
|
REMOTE_URL=$(head -1 "$REMOTE_FILE" | tr -d '[:space:]')
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$REMOTE_URL" ]; then
|
||||||
|
cat >&2 <<EOF
|
||||||
|
gstack-brain-restore: no remote URL provided.
|
||||||
|
|
||||||
|
Provide one of:
|
||||||
|
gstack-brain-restore <git-url>
|
||||||
|
or put the URL in $REMOTE_FILE (copy from the original machine)
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- safety gates ----
|
||||||
|
if [ -d "$GSTACK_HOME/.git" ]; then
|
||||||
|
EXISTING_REMOTE=$(git -C "$GSTACK_HOME" remote get-url origin 2>/dev/null || echo "")
|
||||||
|
if [ -n "$EXISTING_REMOTE" ] && [ "$EXISTING_REMOTE" != "$REMOTE_URL" ]; then
|
||||||
|
cat >&2 <<EOF
|
||||||
|
gstack-brain-restore: ~/.gstack/.git already points at:
|
||||||
|
$EXISTING_REMOTE
|
||||||
|
|
||||||
|
You asked to restore from:
|
||||||
|
$REMOTE_URL
|
||||||
|
|
||||||
|
Refusing to overwrite. Run 'gstack-brain-uninstall' first or pass a matching URL.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- clone to staging ----
|
||||||
|
STAGING=$(mktemp -d "${TMPDIR:-/tmp}/gstack-brain-restore.XXXXXX")
|
||||||
|
trap 'rm -rf "$STAGING" 2>/dev/null' EXIT
|
||||||
|
|
||||||
|
echo "Cloning $REMOTE_URL to staging..."
|
||||||
|
if ! git clone --quiet "$REMOTE_URL" "$STAGING/repo" 2>/dev/null; then
|
||||||
|
echo "Clone failed. Check:" >&2
|
||||||
|
echo " - URL is correct: $REMOTE_URL" >&2
|
||||||
|
echo " - Auth: gh auth status (github) / glab auth status (gitlab)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- validate shape ----
|
||||||
|
if [ ! -f "$STAGING/repo/.brain-allowlist" ] || [ ! -f "$STAGING/repo/.gitattributes" ]; then
|
||||||
|
cat >&2 <<EOF
|
||||||
|
gstack-brain-restore: $REMOTE_URL does not look like a gstack-brain repo.
|
||||||
|
Missing: .brain-allowlist and/or .gitattributes
|
||||||
|
|
||||||
|
This command only works on repos created by gstack-brain-init.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- validate target ~/.gstack/ has no non-gitignored user files ----
|
||||||
|
mkdir -p "$GSTACK_HOME"
|
||||||
|
if [ ! -d "$GSTACK_HOME/.git" ]; then
|
||||||
|
# No existing git → check if we'd clobber anything allowlisted.
|
||||||
|
# Read the new allowlist globs and see if any existing files would collide.
|
||||||
|
CLOBBER_RISK=$(python3 - "$GSTACK_HOME" "$STAGING/repo/.brain-allowlist" <<'PYEOF'
|
||||||
|
import sys, os, fnmatch
|
||||||
|
home, allowlist_path = sys.argv[1:3]
|
||||||
|
try:
|
||||||
|
with open(allowlist_path) as f:
|
||||||
|
globs = [l.strip() for l in f if l.strip() and not l.lstrip().startswith('#')]
|
||||||
|
except FileNotFoundError:
|
||||||
|
globs = []
|
||||||
|
risks = []
|
||||||
|
for root, dirs, files in os.walk(home):
|
||||||
|
dirs[:] = [d for d in dirs if d != '.git']
|
||||||
|
for name in files:
|
||||||
|
full = os.path.join(root, name)
|
||||||
|
rel = os.path.relpath(full, home)
|
||||||
|
for g in globs:
|
||||||
|
if fnmatch.fnmatchcase(rel, g):
|
||||||
|
risks.append(rel)
|
||||||
|
break
|
||||||
|
for r in risks[:5]:
|
||||||
|
print(r)
|
||||||
|
if len(risks) > 5:
|
||||||
|
print(f"...and {len(risks) - 5} more")
|
||||||
|
sys.exit(0 if not risks else 2)
|
||||||
|
PYEOF
|
||||||
|
) || true
|
||||||
|
if [ -n "$CLOBBER_RISK" ]; then
|
||||||
|
cat >&2 <<EOF
|
||||||
|
gstack-brain-restore: ~/.gstack/ has existing allowlisted files that would
|
||||||
|
be clobbered by restore:
|
||||||
|
|
||||||
|
$CLOBBER_RISK
|
||||||
|
|
||||||
|
Back these up first, or run this command on a machine with an empty
|
||||||
|
~/.gstack/. If these files are from an earlier gstack session on THIS
|
||||||
|
machine, you probably want to run gstack-brain-init instead (to create a
|
||||||
|
new brain repo with this machine's state).
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- copy tracked files in ----
|
||||||
|
echo "Copying tracked files into ~/.gstack/ ..."
|
||||||
|
# Use git-ls-tree to get exact tracked file list (avoids staged/untracked files).
|
||||||
|
cd "$STAGING/repo"
|
||||||
|
git ls-tree -r --name-only HEAD | while IFS= read -r rel_path; do
|
||||||
|
src="$STAGING/repo/$rel_path"
|
||||||
|
dst="$GSTACK_HOME/$rel_path"
|
||||||
|
mkdir -p "$(dirname "$dst")"
|
||||||
|
# Skip if identical (content hash). Otherwise copy.
|
||||||
|
if [ -f "$dst" ] && cmp -s "$src" "$dst"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
cp "$src" "$dst"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---- move .git into place ----
|
||||||
|
if [ -d "$GSTACK_HOME/.git" ]; then
|
||||||
|
# Existing .git with matching remote — just fetch + fast-forward.
|
||||||
|
git -C "$GSTACK_HOME" fetch origin >/dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
mv "$STAGING/repo/.git" "$GSTACK_HOME/.git"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- register merge drivers (local git config; don't survive clones) ----
|
||||||
|
git -C "$GSTACK_HOME" config merge.jsonl-append.driver "$SCRIPT_DIR/gstack-jsonl-merge %O %A %B"
|
||||||
|
git -C "$GSTACK_HOME" config merge.jsonl-append.name "gstack JSONL append-only merger"
|
||||||
|
git -C "$GSTACK_HOME" config merge.union.driver "cat %A %B > %A.merged && mv %A.merged %A"
|
||||||
|
git -C "$GSTACK_HOME" config merge.union.name "union concat"
|
||||||
|
|
||||||
|
# ---- install pre-commit hook (same as init) ----
|
||||||
|
HOOK="$GSTACK_HOME/.git/hooks/pre-commit"
|
||||||
|
mkdir -p "$(dirname "$HOOK")"
|
||||||
|
cat > "$HOOK" <<'HOOK_EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -uo pipefail
|
||||||
|
python3 -c "
|
||||||
|
import sys, re, subprocess
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(['git', 'diff', '--cached'], stderr=subprocess.DEVNULL).decode('utf-8', 'replace')
|
||||||
|
except Exception:
|
||||||
|
sys.exit(0)
|
||||||
|
patterns = [
|
||||||
|
('aws-access-key', re.compile(r'AKIA[0-9A-Z]{16}')),
|
||||||
|
('github-token', re.compile(r'\b(gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})')),
|
||||||
|
('openai-key', re.compile(r'\bsk-[A-Za-z0-9_-]{20,}')),
|
||||||
|
('pem-block', re.compile(r'-----BEGIN [A-Z ]{3,}-----')),
|
||||||
|
('jwt', re.compile(r'\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b')),
|
||||||
|
('bearer-token-json',
|
||||||
|
re.compile(r'\"(authorization|api[_-]?key|apikey|token|secret|password)\"\s*:\s*\"[A-Za-z0-9_./+=-]{16,}\"',
|
||||||
|
re.IGNORECASE)),
|
||||||
|
]
|
||||||
|
for name, rx in patterns:
|
||||||
|
if rx.search(out):
|
||||||
|
sys.stderr.write(f'gstack-brain pre-commit: refusing commit — {name} detected.\n')
|
||||||
|
sys.exit(1)
|
||||||
|
sys.exit(0)
|
||||||
|
"
|
||||||
|
HOOK_EOF
|
||||||
|
chmod +x "$HOOK"
|
||||||
|
|
||||||
|
# ---- rehydrate consumers, prompt for tokens ----
|
||||||
|
if [ -f "$GSTACK_HOME/consumers.json" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Consumer registry restored. Tokens are machine-local and NOT synced."
|
||||||
|
echo "Run these for each consumer to re-enter tokens:"
|
||||||
|
python3 - "$GSTACK_HOME/consumers.json" <<'PYEOF'
|
||||||
|
import sys, json
|
||||||
|
try:
|
||||||
|
with open(sys.argv[1]) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
sys.exit(0)
|
||||||
|
for c in data.get("consumers", []):
|
||||||
|
name = c.get("name", "")
|
||||||
|
token_ref = c.get("token_ref", f"{name}_token")
|
||||||
|
print(f" gstack-config set {token_ref} <your-token>")
|
||||||
|
PYEOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- write remote helper file if missing ----
|
||||||
|
if [ ! -f "$REMOTE_FILE" ]; then
|
||||||
|
echo "$REMOTE_URL" > "$REMOTE_FILE"
|
||||||
|
chmod 600 "$REMOTE_FILE"
|
||||||
|
echo ""
|
||||||
|
echo "Wrote $REMOTE_FILE for future skill-run auto-detection."
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
gstack-brain-restore complete.
|
||||||
|
Local: $GSTACK_HOME
|
||||||
|
Remote: $REMOTE_URL
|
||||||
|
|
||||||
|
Next skill run will ask about privacy mode (one-time question) and then
|
||||||
|
sync automatically at skill boundaries.
|
||||||
|
|
||||||
|
Status anytime: gstack-brain-sync --status
|
||||||
|
EOF
|
||||||
Executable
+447
@@ -0,0 +1,447 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# gstack-brain-sync — drain queue, commit allowlisted paths, push to remote.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# gstack-brain-sync --once drain queue, commit, push (default)
|
||||||
|
# gstack-brain-sync --status print sync health as JSON
|
||||||
|
# gstack-brain-sync --skip-file <p> add <p> to ~/.gstack/.brain-skip.txt
|
||||||
|
# gstack-brain-sync --drop-queue --yes clear queue without committing
|
||||||
|
# gstack-brain-sync --discover-new scan allowlist dirs, enqueue changed files
|
||||||
|
#
|
||||||
|
# Invoked by the preamble at skill START and END boundaries. No persistent
|
||||||
|
# daemon. Typical run <1s when queue empty; ~200-800ms with network push.
|
||||||
|
#
|
||||||
|
# Singleton enforcement: flock on ~/.gstack/.brain-sync.lock. Concurrent
|
||||||
|
# invocations queue and serialize.
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# GSTACK_HOME — override ~/.gstack (aligns with writers).
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
QUEUE="$GSTACK_HOME/.brain-queue.jsonl"
|
||||||
|
ALLOWLIST="$GSTACK_HOME/.brain-allowlist"
|
||||||
|
PRIVACY_MAP="$GSTACK_HOME/.brain-privacy-map.json"
|
||||||
|
SKIP_FILE="$GSTACK_HOME/.brain-skip.txt"
|
||||||
|
STATUS_FILE="$GSTACK_HOME/.brain-sync-status.json"
|
||||||
|
LAST_PUSH_FILE="$GSTACK_HOME/.brain-last-push"
|
||||||
|
LOCK_FILE="$GSTACK_HOME/.brain-sync.lock"
|
||||||
|
DISCOVER_CURSOR="$GSTACK_HOME/.brain-discover-cursor"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
CONFIG_BIN="$SCRIPT_DIR/gstack-config"
|
||||||
|
|
||||||
|
# Remote-specific hint for auth errors (branch on origin URL).
|
||||||
|
remote_auth_hint() {
|
||||||
|
local url
|
||||||
|
url=$(git -C "$GSTACK_HOME" remote get-url origin 2>/dev/null || echo "")
|
||||||
|
case "$url" in
|
||||||
|
*github.com*|*@github.*) echo "run: gh auth status (and gh auth refresh if needed)" ;;
|
||||||
|
*gitlab*) echo "run: glab auth status" ;;
|
||||||
|
*) echo "check 'git remote -v' and your credentials" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
write_status() {
|
||||||
|
# args: status_code message [extra_json_blob]
|
||||||
|
local code="$1"
|
||||||
|
local msg="$2"
|
||||||
|
local extra="${3:-{\}}"
|
||||||
|
local ts
|
||||||
|
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
|
||||||
|
python3 - "$STATUS_FILE" "$code" "$msg" "$ts" "$extra" <<'PYEOF' 2>/dev/null || true
|
||||||
|
import json, sys
|
||||||
|
path, code, msg, ts, extra = sys.argv[1:6]
|
||||||
|
try:
|
||||||
|
extra_obj = json.loads(extra) if extra else {}
|
||||||
|
except Exception:
|
||||||
|
extra_obj = {}
|
||||||
|
data = {"status": code, "message": msg, "ts": ts, **extra_obj}
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
f.write("\n")
|
||||||
|
PYEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read config; return 0 if sync active, 1 otherwise.
|
||||||
|
sync_active() {
|
||||||
|
if [ ! -d "$GSTACK_HOME/.git" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local mode
|
||||||
|
mode=$("$CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
[ "$mode" = "off" ] && return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Secret regex families — stdin scan. Exits 0 clean, 1 if hit.
|
||||||
|
# Echoes the matching pattern family name on hit. Uses python3 -c (not
|
||||||
|
# heredoc) so sys.stdin stays available for the diff content.
|
||||||
|
secret_scan_stdin() {
|
||||||
|
python3 -c "
|
||||||
|
import sys, re
|
||||||
|
patterns = [
|
||||||
|
('aws-access-key', re.compile(r'AKIA[0-9A-Z]{16}')),
|
||||||
|
('github-token', re.compile(r'\\b(gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})')),
|
||||||
|
('openai-key', re.compile(r'\\bsk-[A-Za-z0-9_-]{20,}')),
|
||||||
|
('pem-block', re.compile(r'-----BEGIN [A-Z ]{3,}-----')),
|
||||||
|
('jwt', re.compile(r'\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b')),
|
||||||
|
('bearer-token-json',
|
||||||
|
re.compile(r'\"(authorization|api[_-]?key|apikey|token|secret|password)\"\\s*:\\s*\"[A-Za-z0-9_./+=-]{16,}\"',
|
||||||
|
re.IGNORECASE)),
|
||||||
|
]
|
||||||
|
text = sys.stdin.read()
|
||||||
|
for name, rx in patterns:
|
||||||
|
m = rx.search(text)
|
||||||
|
if m:
|
||||||
|
snippet = m.group(0)
|
||||||
|
if len(snippet) > 30:
|
||||||
|
snippet = snippet[:30] + '...'
|
||||||
|
print(name + ':' + snippet)
|
||||||
|
sys.exit(1)
|
||||||
|
sys.exit(0)
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compute matched allowlisted, privacy-filtered path set from queue.
|
||||||
|
# Output: newline-delimited relative paths that should be staged.
|
||||||
|
compute_paths_to_stage() {
|
||||||
|
local mode="$1"
|
||||||
|
python3 - "$GSTACK_HOME" "$QUEUE" "$ALLOWLIST" "$PRIVACY_MAP" "$SKIP_FILE" "$mode" <<'PYEOF'
|
||||||
|
import sys, json, os, fnmatch, glob
|
||||||
|
|
||||||
|
gstack_home, queue, allowlist_path, privacy_path, skip_path, mode = sys.argv[1:7]
|
||||||
|
|
||||||
|
def load_lines(path):
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
return [l.strip() for l in f if l.strip() and not l.lstrip().startswith("#")]
|
||||||
|
except FileNotFoundError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def load_privacy_map(path):
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
# Expected: [{"pattern": "glob", "class": "artifact" | "behavioral"}]
|
||||||
|
return data if isinstance(data, list) else []
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return []
|
||||||
|
|
||||||
|
allowlist_globs = load_lines(allowlist_path)
|
||||||
|
privacy_map = load_privacy_map(privacy_path)
|
||||||
|
skip_lines = set(load_lines(skip_path))
|
||||||
|
|
||||||
|
# Read queue; collect unique file paths.
|
||||||
|
queue_paths = set()
|
||||||
|
try:
|
||||||
|
with open(queue) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
obj = json.loads(line)
|
||||||
|
p = obj.get("file")
|
||||||
|
if isinstance(p, str):
|
||||||
|
queue_paths.add(p)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def path_matches_any(path, globs):
|
||||||
|
for pattern in globs:
|
||||||
|
if fnmatch.fnmatchcase(path, pattern):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def privacy_class(path, mapping):
|
||||||
|
for entry in mapping:
|
||||||
|
pat = entry.get("pattern")
|
||||||
|
if pat and fnmatch.fnmatchcase(path, pat):
|
||||||
|
return entry.get("class", "artifact")
|
||||||
|
# Default class when no pattern matches: artifact (safe default).
|
||||||
|
return "artifact"
|
||||||
|
|
||||||
|
# mode filter: 'off' → nothing; 'artifacts-only' → only artifact class;
|
||||||
|
# 'full' → both classes.
|
||||||
|
def mode_allows(cls, mode):
|
||||||
|
if mode == "off":
|
||||||
|
return False
|
||||||
|
if mode == "artifacts-only":
|
||||||
|
return cls == "artifact"
|
||||||
|
return True # full
|
||||||
|
|
||||||
|
final = []
|
||||||
|
for p in sorted(queue_paths):
|
||||||
|
if p in skip_lines:
|
||||||
|
continue
|
||||||
|
# Must be under GSTACK_HOME root. Reject absolute + reject ../ escape.
|
||||||
|
if p.startswith("/") or ".." in p.split("/"):
|
||||||
|
continue
|
||||||
|
# Must match at least one allowlist glob.
|
||||||
|
if not path_matches_any(p, allowlist_globs):
|
||||||
|
continue
|
||||||
|
# Must survive privacy mode filter.
|
||||||
|
cls = privacy_class(p, privacy_map)
|
||||||
|
if not mode_allows(cls, mode):
|
||||||
|
continue
|
||||||
|
# Must exist on disk — can't stage what isn't there.
|
||||||
|
if not os.path.exists(os.path.join(gstack_home, p)):
|
||||||
|
continue
|
||||||
|
final.append(p)
|
||||||
|
|
||||||
|
for p in final:
|
||||||
|
print(p)
|
||||||
|
PYEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
subcmd_once() {
|
||||||
|
if ! sync_active; then
|
||||||
|
# Silent no-op when feature not initialized / disabled.
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Singleton lock via atomic mkdir. `flock(1)` isn't on macOS by default;
|
||||||
|
# `mkdir` is atomic on every POSIX filesystem. If another --once is already
|
||||||
|
# running, skip (don't wait) — the next skill boundary will catch up.
|
||||||
|
local lock_dir="${LOCK_FILE}.d"
|
||||||
|
if ! mkdir "$lock_dir" 2>/dev/null; then
|
||||||
|
# Is the lock stale? Check the pidfile inside. If process is dead, clear it.
|
||||||
|
if [ -f "$lock_dir/pid" ]; then
|
||||||
|
local lock_pid
|
||||||
|
lock_pid=$(cat "$lock_dir/pid" 2>/dev/null || echo "")
|
||||||
|
if [ -n "$lock_pid" ] && ! kill -0 "$lock_pid" 2>/dev/null; then
|
||||||
|
# Stale lock — clear and retry once.
|
||||||
|
rm -rf "$lock_dir" 2>/dev/null || true
|
||||||
|
if ! mkdir "$lock_dir" 2>/dev/null; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Lock is held by a live process.
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Lock dir without pidfile — treat as held; don't touch.
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "$$" > "$lock_dir/pid" 2>/dev/null || true
|
||||||
|
|
||||||
|
local mode
|
||||||
|
mode=$("$CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
local paths_file
|
||||||
|
paths_file=$(mktemp /tmp/brain-sync-paths.XXXXXX) || { rm -rf "$lock_dir" 2>/dev/null; write_status "error" "mktemp failed"; exit 1; }
|
||||||
|
# Single trap covers both: lock cleanup AND tempfile cleanup.
|
||||||
|
trap 'rm -f "$paths_file" 2>/dev/null; rm -rf "$lock_dir" 2>/dev/null || true' EXIT INT TERM
|
||||||
|
|
||||||
|
compute_paths_to_stage "$mode" > "$paths_file"
|
||||||
|
if [ ! -s "$paths_file" ]; then
|
||||||
|
# Nothing to stage. Clear any stale queue entries and exit.
|
||||||
|
: > "$QUEUE"
|
||||||
|
write_status "idle" "no allowlisted changes in queue"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stage with git add -f (forces past .gitignore=*) explicit paths only.
|
||||||
|
while IFS= read -r p; do
|
||||||
|
[ -z "$p" ] && continue
|
||||||
|
git -C "$GSTACK_HOME" add -f -- "$p" 2>/dev/null || true
|
||||||
|
done < "$paths_file"
|
||||||
|
|
||||||
|
# Secret-scan staged diff.
|
||||||
|
local scan_out
|
||||||
|
scan_out=$(git -C "$GSTACK_HOME" diff --cached 2>/dev/null | secret_scan_stdin || true)
|
||||||
|
if [ -n "$scan_out" ]; then
|
||||||
|
# Hit — unstage, preserve queue, write loud status.
|
||||||
|
git -C "$GSTACK_HOME" reset HEAD -- . >/dev/null 2>&1 || true
|
||||||
|
local hint
|
||||||
|
hint="secret pattern detected ($scan_out). Remediation: review the staged file, then run: gstack-brain-sync --skip-file <path> OR edit the content."
|
||||||
|
write_status "blocked" "$hint"
|
||||||
|
echo "BRAIN_SYNC: blocked: $scan_out" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Commit with template message.
|
||||||
|
local n ts
|
||||||
|
n=$(wc -l < "$paths_file" | tr -d ' ')
|
||||||
|
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
local msg="sync: $n file(s) | $ts"
|
||||||
|
git -C "$GSTACK_HOME" -c user.email="gstack@localhost" -c user.name="gstack-brain-sync" \
|
||||||
|
commit -q -m "$msg" 2>/dev/null || {
|
||||||
|
# Nothing to commit (e.g. all files already committed).
|
||||||
|
: > "$QUEUE"
|
||||||
|
write_status "idle" "queue drained but no new changes to commit"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Push. On reject, fetch + merge (merge driver handles JSONL) + retry once.
|
||||||
|
local push_err
|
||||||
|
push_err=$(git -C "$GSTACK_HOME" push origin HEAD 2>&1 >/dev/null) || {
|
||||||
|
# Check if this is an auth error first — no point retrying.
|
||||||
|
if echo "$push_err" | grep -qiE "auth|permission|403|401|forbidden"; then
|
||||||
|
local hint
|
||||||
|
hint=$(remote_auth_hint)
|
||||||
|
write_status "push_failed" "push failed: auth error. fix: $hint"
|
||||||
|
echo "BRAIN_SYNC: push failed: auth. fix: $hint" >&2
|
||||||
|
# Queue cleared because the commit exists locally; next push will send it.
|
||||||
|
: > "$QUEUE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try a fetch-and-merge + retry.
|
||||||
|
if git -C "$GSTACK_HOME" fetch origin 2>/dev/null; then
|
||||||
|
local branch
|
||||||
|
branch=$(git -C "$GSTACK_HOME" rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)
|
||||||
|
if git -C "$GSTACK_HOME" merge --no-edit "origin/$branch" >/dev/null 2>&1; then
|
||||||
|
if git -C "$GSTACK_HOME" push origin HEAD 2>/dev/null; then
|
||||||
|
: > "$QUEUE"
|
||||||
|
date -u +%Y-%m-%dT%H:%M:%SZ > "$LAST_PUSH_FILE"
|
||||||
|
write_status "ok" "pushed $n file(s) after rebase"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
write_status "push_failed" "push failed: $(printf '%s' "$push_err" | head -1)"
|
||||||
|
: > "$QUEUE"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Success: clear queue, update last-push.
|
||||||
|
: > "$QUEUE"
|
||||||
|
date -u +%Y-%m-%dT%H:%M:%SZ > "$LAST_PUSH_FILE"
|
||||||
|
write_status "ok" "pushed $n file(s)"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
subcmd_status() {
|
||||||
|
if [ -f "$STATUS_FILE" ]; then
|
||||||
|
cat "$STATUS_FILE"
|
||||||
|
else
|
||||||
|
echo '{"status":"unknown","message":"no status file yet"}'
|
||||||
|
fi
|
||||||
|
# Supplemental info (not in status file).
|
||||||
|
local queue_depth=0
|
||||||
|
[ -f "$QUEUE" ] && queue_depth=$(wc -l < "$QUEUE" | tr -d ' ')
|
||||||
|
local last_push="never"
|
||||||
|
[ -f "$LAST_PUSH_FILE" ] && last_push=$(cat "$LAST_PUSH_FILE" 2>/dev/null || echo never)
|
||||||
|
local mode
|
||||||
|
mode=$("$CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
printf '{"queue_depth":%s,"last_push":"%s","mode":"%s"}\n' "$queue_depth" "$last_push" "$mode"
|
||||||
|
}
|
||||||
|
|
||||||
|
subcmd_skip_file() {
|
||||||
|
local path="${1:-}"
|
||||||
|
if [ -z "$path" ]; then
|
||||||
|
echo "Usage: gstack-brain-sync --skip-file <path>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p "$GSTACK_HOME"
|
||||||
|
# Avoid duplicate entries.
|
||||||
|
if [ -f "$SKIP_FILE" ] && grep -Fxq "$path" "$SKIP_FILE"; then
|
||||||
|
echo "already in skip list: $path"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "$path" >> "$SKIP_FILE"
|
||||||
|
echo "added to skip list: $path"
|
||||||
|
echo "(future writers will not enqueue this path; existing queue entries ignored on next --once)"
|
||||||
|
}
|
||||||
|
|
||||||
|
subcmd_drop_queue() {
|
||||||
|
local force="${1:-}"
|
||||||
|
if [ "$force" != "--yes" ]; then
|
||||||
|
echo "Refusing: --drop-queue discards pending syncs. Pass --yes to confirm." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -f "$QUEUE" ]; then
|
||||||
|
echo "queue already empty"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
local n
|
||||||
|
n=$(wc -l < "$QUEUE" | tr -d ' ')
|
||||||
|
: > "$QUEUE"
|
||||||
|
echo "dropped $n queue entries"
|
||||||
|
}
|
||||||
|
|
||||||
|
subcmd_discover_new() {
|
||||||
|
if ! sync_active; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Walk allowlist globs; enqueue any file where mtime+size differs from cursor.
|
||||||
|
python3 - "$GSTACK_HOME" "$ALLOWLIST" "$DISCOVER_CURSOR" "$SCRIPT_DIR/gstack-brain-enqueue" <<'PYEOF' 2>/dev/null || true
|
||||||
|
import sys, os, json, glob, fnmatch, subprocess, hashlib
|
||||||
|
|
||||||
|
gstack_home, allowlist_path, cursor_path, enqueue_bin = sys.argv[1:5]
|
||||||
|
|
||||||
|
def load_lines(path):
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
return [l.strip() for l in f if l.strip() and not l.lstrip().startswith("#")]
|
||||||
|
except FileNotFoundError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def load_cursor(path):
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_cursor(path, data):
|
||||||
|
try:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
allowlist = load_lines(allowlist_path)
|
||||||
|
cursor = load_cursor(cursor_path)
|
||||||
|
new_cursor = dict(cursor)
|
||||||
|
|
||||||
|
# Walk all files under gstack_home, match against allowlist.
|
||||||
|
for root, dirs, files in os.walk(gstack_home):
|
||||||
|
# Skip .git and .brain-* state files.
|
||||||
|
if ".git" in root.split(os.sep):
|
||||||
|
continue
|
||||||
|
for name in files:
|
||||||
|
full = os.path.join(root, name)
|
||||||
|
rel = os.path.relpath(full, gstack_home)
|
||||||
|
if rel.startswith(".brain-"):
|
||||||
|
continue
|
||||||
|
matched = any(fnmatch.fnmatchcase(rel, pat) for pat in allowlist)
|
||||||
|
if not matched:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
st = os.stat(full)
|
||||||
|
key = f"{int(st.st_mtime)}:{st.st_size}"
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
prev = cursor.get(rel)
|
||||||
|
if prev != key:
|
||||||
|
# Enqueue via the shim (respects sync mode + skip list).
|
||||||
|
subprocess.run([enqueue_bin, rel], check=False)
|
||||||
|
new_cursor[rel] = key
|
||||||
|
|
||||||
|
save_cursor(cursor_path, new_cursor)
|
||||||
|
PYEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------- dispatch --------
|
||||||
|
case "${1:-}" in
|
||||||
|
--once|"") subcmd_once ;;
|
||||||
|
--status) subcmd_status ;;
|
||||||
|
--skip-file) shift; subcmd_skip_file "${1:-}" ;;
|
||||||
|
--drop-queue) shift; subcmd_drop_queue "${1:-}" ;;
|
||||||
|
--discover-new) subcmd_discover_new ;;
|
||||||
|
--help|-h)
|
||||||
|
sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown subcommand: $1" >&2
|
||||||
|
echo "Run: gstack-brain-sync --help" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Executable
+145
@@ -0,0 +1,145 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# gstack-brain-uninstall — clean off-ramp for gstack-brain sync.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# gstack-brain-uninstall [--yes] [--delete-remote]
|
||||||
|
#
|
||||||
|
# Removes the git layer from ~/.gstack/ and clears sync config. Your local
|
||||||
|
# gstack memory (learnings, timelines, etc.) is NOT touched — this is an
|
||||||
|
# uninstall-sync command, not a delete-data command.
|
||||||
|
#
|
||||||
|
# Flags:
|
||||||
|
# --yes Skip the confirmation prompt.
|
||||||
|
# --delete-remote Also delete the GitHub repo via `gh repo delete`
|
||||||
|
# (interactive unless --yes is also passed).
|
||||||
|
#
|
||||||
|
# What it removes (in ~/.gstack/):
|
||||||
|
# .git/ — the sync repo's git data
|
||||||
|
# .gitignore — canonical ignore-all marker
|
||||||
|
# .gitattributes — merge driver declarations
|
||||||
|
# .brain-allowlist — sync path list
|
||||||
|
# .brain-privacy-map.json — sync privacy classifier
|
||||||
|
# .brain-queue.jsonl — pending queue
|
||||||
|
# .brain-discover-cursor — discover-new cursor
|
||||||
|
# .brain-last-push — timestamp marker
|
||||||
|
# .brain-skip.txt — user-maintained skip list
|
||||||
|
# .brain-sync.lock.d/ — lock dir (if present)
|
||||||
|
# .brain-sync-status.json — health status
|
||||||
|
# consumers.json — consumer/reader registry
|
||||||
|
#
|
||||||
|
# What it clears (via gstack-config):
|
||||||
|
# gbrain_sync_mode → off
|
||||||
|
# gbrain_sync_mode_prompted → false (so user re-prompts on re-init)
|
||||||
|
#
|
||||||
|
# What it does NOT touch:
|
||||||
|
# Project data (projects/*, retros/*, developer-profile.json, etc.)
|
||||||
|
# Consumer tokens in gstack-config (<name>_token keys)
|
||||||
|
# ~/.gstack-brain-remote.txt in your home directory
|
||||||
|
# The actual remote git repo (unless --delete-remote)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
CONFIG_BIN="$SCRIPT_DIR/gstack-config"
|
||||||
|
REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
|
||||||
|
ASSUME_YES=0
|
||||||
|
DELETE_REMOTE=0
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--yes|-y) ASSUME_YES=1; shift ;;
|
||||||
|
--delete-remote) DELETE_REMOTE=1; shift ;;
|
||||||
|
--help|-h) sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
|
*) echo "Unknown flag: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ! -d "$GSTACK_HOME/.git" ]; then
|
||||||
|
echo "gstack-brain-uninstall: nothing to do (~/.gstack/.git doesn't exist)."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
REMOTE_URL=$(git -C "$GSTACK_HOME" remote get-url origin 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# ---- confirmation ----
|
||||||
|
if [ "$ASSUME_YES" != "1" ]; then
|
||||||
|
cat <<EOF
|
||||||
|
This will remove gstack-brain sync from this machine:
|
||||||
|
- Remove ~/.gstack/.git and sync config files
|
||||||
|
- Clear gbrain_sync_mode in gstack-config
|
||||||
|
- Remote: $REMOTE_URL will be $([ "$DELETE_REMOTE" = "1" ] && echo "DELETED" || echo "kept")
|
||||||
|
|
||||||
|
Local memory (learnings, plans, etc.) is NOT touched.
|
||||||
|
|
||||||
|
EOF
|
||||||
|
printf "Proceed? [y/N] "
|
||||||
|
read -r reply
|
||||||
|
case "$reply" in
|
||||||
|
y|Y|yes|Yes) ;;
|
||||||
|
*) echo "Aborted."; exit 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- delete remote if requested ----
|
||||||
|
if [ "$DELETE_REMOTE" = "1" ] && [ -n "$REMOTE_URL" ]; then
|
||||||
|
case "$REMOTE_URL" in
|
||||||
|
*github.com*|*@github*)
|
||||||
|
if command -v gh >/dev/null 2>&1; then
|
||||||
|
# Extract owner/repo from URL.
|
||||||
|
REPO_SLUG=$(echo "$REMOTE_URL" | sed -E 's#.*[:/]([^/:]+/[^/]+)(\.git)?$#\1#' | sed 's/\.git$//')
|
||||||
|
if [ -n "$REPO_SLUG" ]; then
|
||||||
|
echo "Deleting GitHub repo: $REPO_SLUG"
|
||||||
|
if [ "$ASSUME_YES" = "1" ]; then
|
||||||
|
gh repo delete "$REPO_SLUG" --yes 2>/dev/null || echo "gh repo delete failed; continuing local uninstall"
|
||||||
|
else
|
||||||
|
gh repo delete "$REPO_SLUG" 2>/dev/null || echo "gh repo delete failed; continuing local uninstall"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "--delete-remote requires the gh CLI. Skipping remote deletion."
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "--delete-remote only supports github.com remotes. Delete manually if needed: $REMOTE_URL"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- remove sync files ----
|
||||||
|
echo "Removing git layer and sync config files..."
|
||||||
|
rm -rf "$GSTACK_HOME/.git" 2>/dev/null || true
|
||||||
|
rm -f "$GSTACK_HOME/.gitignore" 2>/dev/null || true
|
||||||
|
rm -f "$GSTACK_HOME/.gitattributes" 2>/dev/null || true
|
||||||
|
rm -f "$GSTACK_HOME/.brain-allowlist" 2>/dev/null || true
|
||||||
|
rm -f "$GSTACK_HOME/.brain-privacy-map.json" 2>/dev/null || true
|
||||||
|
rm -f "$GSTACK_HOME/.brain-queue.jsonl" 2>/dev/null || true
|
||||||
|
rm -f "$GSTACK_HOME/.brain-discover-cursor" 2>/dev/null || true
|
||||||
|
rm -f "$GSTACK_HOME/.brain-last-push" 2>/dev/null || true
|
||||||
|
rm -f "$GSTACK_HOME/.brain-last-pull" 2>/dev/null || true
|
||||||
|
rm -f "$GSTACK_HOME/.brain-skip.txt" 2>/dev/null || true
|
||||||
|
rm -f "$GSTACK_HOME/.brain-sync-status.json" 2>/dev/null || true
|
||||||
|
rm -rf "$GSTACK_HOME/.brain-sync.lock.d" 2>/dev/null || true
|
||||||
|
rm -f "$GSTACK_HOME/consumers.json" 2>/dev/null || true
|
||||||
|
|
||||||
|
# ---- clear config keys ----
|
||||||
|
"$CONFIG_BIN" set gbrain_sync_mode off >/dev/null 2>&1 || true
|
||||||
|
"$CONFIG_BIN" set gbrain_sync_mode_prompted false >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# ---- leave remote-helper file alone unless user asked to delete remote ----
|
||||||
|
if [ "$DELETE_REMOTE" = "1" ]; then
|
||||||
|
rm -f "$REMOTE_FILE" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
if [ -f "$REMOTE_FILE" ]; then
|
||||||
|
echo "(keeping $REMOTE_FILE — remove manually if you want to forget the URL)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
gstack-brain uninstall complete.
|
||||||
|
Sync is off. ~/.gstack/ is a plain directory again.
|
||||||
|
Your project data, learnings, and profile are untouched.
|
||||||
|
|
||||||
|
To re-enable sync later: gstack-brain-init
|
||||||
|
EOF
|
||||||
+26
-4
@@ -8,10 +8,11 @@
|
|||||||
# gstack-config defaults — show just the defaults table
|
# gstack-config defaults — show just the defaults table
|
||||||
#
|
#
|
||||||
# Env overrides (for testing):
|
# Env overrides (for testing):
|
||||||
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
# GSTACK_HOME — override ~/.gstack state directory (aligns with writer scripts)
|
||||||
|
# GSTACK_STATE_DIR — legacy alias for GSTACK_HOME (kept for backwards compat)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
|
STATE_DIR="${GSTACK_HOME:-${GSTACK_STATE_DIR:-$HOME/.gstack}}"
|
||||||
CONFIG_FILE="$STATE_DIR/config.yaml"
|
CONFIG_FILE="$STATE_DIR/config.yaml"
|
||||||
|
|
||||||
# Annotated header for new config files. Written once on first `set`.
|
# Annotated header for new config files. Written once on first `set`.
|
||||||
@@ -59,6 +60,19 @@ CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on ne
|
|||||||
# # Unknown values default to "default" with a warning.
|
# # Unknown values default to "default" with a warning.
|
||||||
# # See docs/designs/PLAN_TUNING_V1.md for rationale.
|
# # See docs/designs/PLAN_TUNING_V1.md for rationale.
|
||||||
#
|
#
|
||||||
|
# ─── GBrain sync (v1.7+) ─────────────────────────────────────────────
|
||||||
|
# gbrain_sync_mode: off # off | artifacts-only | full
|
||||||
|
# # off — no sync (default)
|
||||||
|
# # artifacts-only — sync plans/designs/retros/learnings only
|
||||||
|
# # (skip behavioral data: question-log,
|
||||||
|
# # developer-profile, timeline)
|
||||||
|
# # full — sync everything allowlisted
|
||||||
|
# # Set by the first-run privacy stop-gate. See docs/gbrain-sync.md.
|
||||||
|
#
|
||||||
|
# gbrain_sync_mode_prompted: false
|
||||||
|
# # Set to true once the privacy gate has asked the user.
|
||||||
|
# # Flip back to false to be re-prompted.
|
||||||
|
#
|
||||||
# ─── Advanced ────────────────────────────────────────────────────────
|
# ─── Advanced ────────────────────────────────────────────────────────
|
||||||
# codex_reviews: enabled # disabled = skip Codex adversarial reviews in /ship
|
# codex_reviews: enabled # disabled = skip Codex adversarial reviews in /ship
|
||||||
# gstack_contributor: false # true = file field reports when gstack misbehaves
|
# gstack_contributor: false # true = file field reports when gstack misbehaves
|
||||||
@@ -91,6 +105,8 @@ lookup_default() {
|
|||||||
skip_eng_review) echo "false" ;;
|
skip_eng_review) echo "false" ;;
|
||||||
workspace_root) echo "$HOME/conductor/workspaces" ;;
|
workspace_root) echo "$HOME/conductor/workspaces" ;;
|
||||||
cross_project_learnings) echo "" ;; # intentionally empty → unset triggers first-time prompt
|
cross_project_learnings) echo "" ;; # intentionally empty → unset triggers first-time prompt
|
||||||
|
gbrain_sync_mode) echo "off" ;;
|
||||||
|
gbrain_sync_mode_prompted) echo "false" ;;
|
||||||
*) echo "" ;;
|
*) echo "" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -122,6 +138,10 @@ case "${1:-}" in
|
|||||||
echo "Warning: explain_level '$VALUE' not recognized. Valid values: default, terse. Using default." >&2
|
echo "Warning: explain_level '$VALUE' not recognized. Valid values: default, terse. Using default." >&2
|
||||||
VALUE="default"
|
VALUE="default"
|
||||||
fi
|
fi
|
||||||
|
if [ "$KEY" = "gbrain_sync_mode" ] && [ "$VALUE" != "off" ] && [ "$VALUE" != "artifacts-only" ] && [ "$VALUE" != "full" ]; then
|
||||||
|
echo "Warning: gbrain_sync_mode '$VALUE' not recognized. Valid values: off, artifacts-only, full. Using off." >&2
|
||||||
|
VALUE="off"
|
||||||
|
fi
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
# Write annotated header on first creation
|
# Write annotated header on first creation
|
||||||
if [ ! -f "$CONFIG_FILE" ]; then
|
if [ ! -f "$CONFIG_FILE" ]; then
|
||||||
@@ -150,7 +170,8 @@ case "${1:-}" in
|
|||||||
echo "# ─── Active values (including defaults for unset keys) ───"
|
echo "# ─── Active values (including defaults for unset keys) ───"
|
||||||
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
|
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
|
||||||
skill_prefix checkpoint_mode checkpoint_push codex_reviews \
|
skill_prefix checkpoint_mode checkpoint_push codex_reviews \
|
||||||
gstack_contributor skip_eng_review workspace_root; do
|
gstack_contributor skip_eng_review workspace_root \
|
||||||
|
gbrain_sync_mode gbrain_sync_mode_prompted; do
|
||||||
VALUE=$(grep -E "^${KEY}:" "$CONFIG_FILE" 2>/dev/null | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true)
|
VALUE=$(grep -E "^${KEY}:" "$CONFIG_FILE" 2>/dev/null | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true)
|
||||||
SOURCE="default"
|
SOURCE="default"
|
||||||
if [ -n "$VALUE" ]; then
|
if [ -n "$VALUE" ]; then
|
||||||
@@ -165,7 +186,8 @@ case "${1:-}" in
|
|||||||
echo "# gstack-config defaults"
|
echo "# gstack-config defaults"
|
||||||
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
|
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
|
||||||
skill_prefix checkpoint_mode checkpoint_push codex_reviews \
|
skill_prefix checkpoint_mode checkpoint_push codex_reviews \
|
||||||
gstack_contributor skip_eng_review workspace_root; do
|
gstack_contributor skip_eng_review workspace_root \
|
||||||
|
gbrain_sync_mode gbrain_sync_mode_prompted; do
|
||||||
printf ' %-24s %s\n' "$KEY:" "$(lookup_default "$KEY")"
|
printf ' %-24s %s\n' "$KEY:" "$(lookup_default "$KEY")"
|
||||||
done
|
done
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ do_migrate() {
|
|||||||
mv "$TMPOUT" "$PROFILE_FILE"
|
mv "$TMPOUT" "$PROFILE_FILE"
|
||||||
trap - EXIT
|
trap - EXIT
|
||||||
|
|
||||||
|
# gbrain-sync: enqueue the migrated file for cross-machine sync (no-op if off).
|
||||||
|
SCRIPT_DIR_E="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
"$SCRIPT_DIR_E/gstack-brain-enqueue" "developer-profile.json" 2>/dev/null &
|
||||||
|
|
||||||
# Archive the legacy file.
|
# Archive the legacy file.
|
||||||
local TS
|
local TS
|
||||||
TS="$(date +%Y-%m-%d-%H%M%S)"
|
TS="$(date +%Y-%m-%d-%H%M%S)"
|
||||||
|
|||||||
Executable
+88
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# gstack-jsonl-merge — git merge driver for append-only JSONL files.
|
||||||
|
#
|
||||||
|
# Usage (called by git, not by users):
|
||||||
|
# gstack-jsonl-merge <base> <ours> <theirs>
|
||||||
|
#
|
||||||
|
# Registered in local git config by bin/gstack-brain-init and
|
||||||
|
# bin/gstack-brain-restore:
|
||||||
|
# git config merge.jsonl-append.driver \
|
||||||
|
# "$GSTACK_BIN/gstack-jsonl-merge %O %A %B"
|
||||||
|
#
|
||||||
|
# Behavior:
|
||||||
|
# Concatenate base + ours + theirs, dedup exact-duplicate lines, sort by
|
||||||
|
# ISO "ts" field when present, fall back to SHA-256 of the line for
|
||||||
|
# deterministic order. Write result to <ours> (the %A file per the git
|
||||||
|
# merge-driver contract).
|
||||||
|
#
|
||||||
|
# Two machines appending to the same JSONL file between pushes produces
|
||||||
|
# a same-line conflict at the file tail. This driver resolves it cleanly:
|
||||||
|
# both appends survive, ordered by wall-clock timestamp where available,
|
||||||
|
# content hash otherwise.
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 — merge succeeded, result written to <ours>
|
||||||
|
# 1 — error; git treats as conflict and stops the merge
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
if [ "$#" -lt 3 ]; then
|
||||||
|
echo "gstack-jsonl-merge: expected 3 args (base ours theirs), got $#" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BASE="$1"
|
||||||
|
OURS="$2"
|
||||||
|
THEIRS="$3"
|
||||||
|
|
||||||
|
TMP=$(mktemp /tmp/gstack-jsonl-merge.XXXXXX) || exit 1
|
||||||
|
trap 'rm -f "$TMP" 2>/dev/null || true' EXIT
|
||||||
|
|
||||||
|
python3 - "$BASE" "$OURS" "$THEIRS" > "$TMP" <<'PYEOF'
|
||||||
|
import sys, json, hashlib
|
||||||
|
|
||||||
|
paths = sys.argv[1:4] # base, ours, theirs
|
||||||
|
seen = {} # line content -> sort_key
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
try:
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.rstrip('\n')
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if line in seen:
|
||||||
|
continue
|
||||||
|
# Prefer ISO ts field for sort; fall back to SHA-256.
|
||||||
|
sort_key = None
|
||||||
|
try:
|
||||||
|
obj = json.loads(line)
|
||||||
|
ts = obj.get('ts') or obj.get('timestamp')
|
||||||
|
if isinstance(ts, str):
|
||||||
|
sort_key = (0, ts)
|
||||||
|
except (json.JSONDecodeError, ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
if sort_key is None:
|
||||||
|
h = hashlib.sha256(line.encode('utf-8')).hexdigest()
|
||||||
|
sort_key = (1, h)
|
||||||
|
seen[line] = sort_key
|
||||||
|
except FileNotFoundError:
|
||||||
|
# Absent base / absent ours / absent theirs are all valid.
|
||||||
|
continue
|
||||||
|
except OSError:
|
||||||
|
# Permission / IO errors are fatal — caller sees non-zero exit.
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Timestamp-ordered entries first (group 0), then hash-ordered (group 1).
|
||||||
|
for line, _ in sorted(seen.items(), key=lambda item: item[1]):
|
||||||
|
print(line)
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
_PYEXIT=$?
|
||||||
|
if [ "$_PYEXIT" != "0" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$TMP" "$OURS" || exit 1
|
||||||
|
trap - EXIT
|
||||||
|
exit 0
|
||||||
@@ -84,3 +84,6 @@ if [ $? -ne 0 ] || [ -z "$VALIDATED" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$VALIDATED" >> "$GSTACK_HOME/projects/$SLUG/learnings.jsonl"
|
echo "$VALIDATED" >> "$GSTACK_HOME/projects/$SLUG/learnings.jsonl"
|
||||||
|
|
||||||
|
# gbrain-sync: enqueue for cross-machine sync (no-op if sync is off).
|
||||||
|
"$SCRIPT_DIR/gstack-brain-enqueue" "projects/$SLUG/learnings.jsonl" 2>/dev/null &
|
||||||
|
|||||||
@@ -165,3 +165,7 @@ if [ $VALIDATE_RC -ne 0 ] || [ -z "$VALIDATED" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$VALIDATED" >> "$GSTACK_HOME/projects/$SLUG/question-log.jsonl"
|
echo "$VALIDATED" >> "$GSTACK_HOME/projects/$SLUG/question-log.jsonl"
|
||||||
|
|
||||||
|
# NOTE: question-log.jsonl is deliberately NOT enqueued for gbrain-sync.
|
||||||
|
# Per Codex v2 review, audit/derivation data stays local alongside the
|
||||||
|
# question-preferences.json it annotates.
|
||||||
|
|||||||
@@ -16,3 +16,6 @@ if ! printf '%s' "$INPUT" | bun -e "JSON.parse(await Bun.stdin.text())" 2>/dev/n
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$INPUT" >> "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl"
|
echo "$INPUT" >> "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl"
|
||||||
|
|
||||||
|
# gbrain-sync: enqueue for cross-machine sync (no-op if sync is off).
|
||||||
|
"$SCRIPT_DIR/gstack-brain-enqueue" "projects/$SLUG/$BRANCH-reviews.jsonl" 2>/dev/null &
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
# gstack-timeline-log — append a timeline event to the project timeline
|
# gstack-timeline-log — append a timeline event to the project timeline
|
||||||
# Usage: gstack-timeline-log '{"skill":"review","event":"started","branch":"main"}'
|
# Usage: gstack-timeline-log '{"skill":"review","event":"started","branch":"main"}'
|
||||||
#
|
#
|
||||||
# Session timeline: local-only, never sent anywhere.
|
# Session timeline: local by default. If the user enables `gbrain_sync_mode`
|
||||||
|
# with the `full` (not `artifacts-only`) privacy tier — via the first-run
|
||||||
|
# stop-gate from `gstack-brain-init` or the preamble — timeline events are
|
||||||
|
# published to the user's private GBrain sync repo. See docs/gbrain-sync.md.
|
||||||
# Required fields: skill, event (started|completed).
|
# Required fields: skill, event (started|completed).
|
||||||
# Optional: branch, outcome, duration_s, session, ts.
|
# Optional: branch, outcome, duration_s, session, ts.
|
||||||
# Validation failure → skip silently (non-blocking).
|
# Validation failure → skip silently (non-blocking).
|
||||||
@@ -32,3 +35,6 @@ if ! printf '%s' "$INPUT" | bun -e "const j=JSON.parse(await Bun.stdin.text());
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$INPUT" >> "$GSTACK_HOME/projects/$SLUG/timeline.jsonl"
|
echo "$INPUT" >> "$GSTACK_HOME/projects/$SLUG/timeline.jsonl"
|
||||||
|
|
||||||
|
# gbrain-sync: enqueue for cross-machine sync (no-op if sync is off).
|
||||||
|
"$SCRIPT_DIR/gstack-brain-enqueue" "projects/$SLUG/timeline.jsonl" 2>/dev/null &
|
||||||
|
|||||||
@@ -350,6 +350,105 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
|
|||||||
@@ -13,17 +13,38 @@
|
|||||||
"puppeteer-core": "^24.40.0",
|
"puppeteer-core": "^24.40.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@anthropic-ai/claude-agent-sdk": "0.2.117",
|
||||||
"@anthropic-ai/sdk": "^0.78.0",
|
"@anthropic-ai/sdk": "^0.78.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.117", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.117", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.117", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.117", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.117", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.117", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.117", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.117", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.117" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-pVBss1Vu0w87nKCBhWtjMggSgCh6GVUtdRmuE58ZvXv0E2q0JcnUCQHehmn92BAW0+VCwPY8q/k7uKWkgwz/gA=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.117", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZeC/Lz8XMKQ5w+GmjTziPR8bSSarBtNCJMkMAYRT9ekNmyXSWXEwGLENe5TDDmtpzNNzAB1mQNuIYoqTsqgV3w=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.117", "", { "os": "darwin", "cpu": "x64" }, "sha512-DKyggGzzpDcr9S435xlpbpwkEYKZNbePSekug75tJclK8l4ddD9+M9BFgMiSUq9F1Zt53kUaRDihDu/cBKvkdQ=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.117", "", { "os": "linux", "cpu": "arm64" }, "sha512-jyHmyZQavpPOe3zxBRX3KbdOAJ8JwZ8m/wMr5bhHhhcstugm/vJx6IIs7D44VvFjk+8sqdvR2ZrliL8PUcJL0g=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.117", "", { "os": "linux", "cpu": "arm64" }, "sha512-bJU5gEOmM4VCOn4h8vipOKgdhPATePQ23mMpvyVqtVyipWppHfOUfVkqXb+SrF/hfkNSMYxDuoKxbJ+MmKtGjg=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.2.117", "", { "os": "linux", "cpu": "x64" }, "sha512-Zb5PXKrDNbQ1dyNYwxZMNL+F2Dhgjh9f9B21wZUJqkhJL69hRJwJyxO42HiNmB2zGCaTxQTyjPhLdB/eQJo74Q=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.117", "", { "os": "linux", "cpu": "x64" }, "sha512-LIkKTAYZGugEVssAuWCPqlDWSqhVZAveNPNsfKLbuG1naIMCR04fUqil6i3d3mAAfk7FaS5D4IdHp45psi+GDw=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.117", "", { "os": "win32", "cpu": "arm64" }, "sha512-uetggH3B83PiH0a9D/5MVXB5Hqnlr2DVajehwAP2x0Mt4DBd632ICnHpu6pnSP+vVkWgq3FgQlkHe91RfP+peA=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.2.117", "", { "os": "win32", "cpu": "x64" }, "sha512-TT4KngAokDTJSvQ2mrAP6ZRkXj50OLj7Tb1zZA4CnkmrrEidgs4KrMx7er1ZwoivngIvCekV9+TbtC9giknr5w=="],
|
||||||
|
|
||||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.78.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w=="],
|
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.78.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w=="],
|
||||||
|
|
||||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||||
|
|
||||||
|
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
|
||||||
|
|
||||||
"@huggingface/jinja": ["@huggingface/jinja@0.5.7", "", {}, "sha512-OosMEbF/R6zkKNNzqhI7kvKYCpo1F0UeIv46/h4D4UjVEKKd6k3TiV8sgu6fkreX4lbBiRI+lZG8UnXnqVQmEQ=="],
|
"@huggingface/jinja": ["@huggingface/jinja@0.5.7", "", {}, "sha512-OosMEbF/R6zkKNNzqhI7kvKYCpo1F0UeIv46/h4D4UjVEKKd6k3TiV8sgu6fkreX4lbBiRI+lZG8UnXnqVQmEQ=="],
|
||||||
|
|
||||||
"@huggingface/tokenizers": ["@huggingface/tokenizers@0.1.3", "", {}, "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA=="],
|
"@huggingface/tokenizers": ["@huggingface/tokenizers@0.1.3", "", {}, "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA=="],
|
||||||
@@ -80,6 +101,8 @@
|
|||||||
|
|
||||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||||
|
|
||||||
"@ngrok/ngrok": ["@ngrok/ngrok@1.7.0", "", { "optionalDependencies": { "@ngrok/ngrok-android-arm64": "1.7.0", "@ngrok/ngrok-darwin-arm64": "1.7.0", "@ngrok/ngrok-darwin-universal": "1.7.0", "@ngrok/ngrok-darwin-x64": "1.7.0", "@ngrok/ngrok-freebsd-x64": "1.7.0", "@ngrok/ngrok-linux-arm-gnueabihf": "1.7.0", "@ngrok/ngrok-linux-arm64-gnu": "1.7.0", "@ngrok/ngrok-linux-arm64-musl": "1.7.0", "@ngrok/ngrok-linux-x64-gnu": "1.7.0", "@ngrok/ngrok-linux-x64-musl": "1.7.0", "@ngrok/ngrok-win32-arm64-msvc": "1.7.0", "@ngrok/ngrok-win32-ia32-msvc": "1.7.0", "@ngrok/ngrok-win32-x64-msvc": "1.7.0" } }, "sha512-P06o9TpxrJbiRbHQkiwy/rUrlXRupc+Z8KT4MiJfmcdWxvIdzjCaJOdnNkcOTs6DMyzIOefG5tvk/HLdtjqr0g=="],
|
"@ngrok/ngrok": ["@ngrok/ngrok@1.7.0", "", { "optionalDependencies": { "@ngrok/ngrok-android-arm64": "1.7.0", "@ngrok/ngrok-darwin-arm64": "1.7.0", "@ngrok/ngrok-darwin-universal": "1.7.0", "@ngrok/ngrok-darwin-x64": "1.7.0", "@ngrok/ngrok-freebsd-x64": "1.7.0", "@ngrok/ngrok-linux-arm-gnueabihf": "1.7.0", "@ngrok/ngrok-linux-arm64-gnu": "1.7.0", "@ngrok/ngrok-linux-arm64-musl": "1.7.0", "@ngrok/ngrok-linux-x64-gnu": "1.7.0", "@ngrok/ngrok-linux-x64-musl": "1.7.0", "@ngrok/ngrok-win32-arm64-msvc": "1.7.0", "@ngrok/ngrok-win32-ia32-msvc": "1.7.0", "@ngrok/ngrok-win32-x64-msvc": "1.7.0" } }, "sha512-P06o9TpxrJbiRbHQkiwy/rUrlXRupc+Z8KT4MiJfmcdWxvIdzjCaJOdnNkcOTs6DMyzIOefG5tvk/HLdtjqr0g=="],
|
||||||
|
|
||||||
"@ngrok/ngrok-android-arm64": ["@ngrok/ngrok-android-arm64@1.7.0", "", { "os": "android", "cpu": "arm64" }, "sha512-8tco3ID6noSaNy+CMS7ewqPoIkIM6XO5COCzsUp3Wv3XEbMSyn65RN6cflX2JdqLfUCHcMyD0ahr9IEiHwqmbQ=="],
|
"@ngrok/ngrok-android-arm64": ["@ngrok/ngrok-android-arm64@1.7.0", "", { "os": "android", "cpu": "arm64" }, "sha512-8tco3ID6noSaNy+CMS7ewqPoIkIM6XO5COCzsUp3Wv3XEbMSyn65RN6cflX2JdqLfUCHcMyD0ahr9IEiHwqmbQ=="],
|
||||||
@@ -136,10 +159,16 @@
|
|||||||
|
|
||||||
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
||||||
|
|
||||||
|
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||||
|
|
||||||
"adm-zip": ["adm-zip@0.5.17", "", {}, "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ=="],
|
"adm-zip": ["adm-zip@0.5.17", "", {}, "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ=="],
|
||||||
|
|
||||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||||
|
|
||||||
|
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||||
|
|
||||||
|
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||||
|
|
||||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
@@ -162,10 +191,18 @@
|
|||||||
|
|
||||||
"basic-ftp": ["basic-ftp@5.2.0", "", {}, "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw=="],
|
"basic-ftp": ["basic-ftp@5.2.0", "", {}, "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw=="],
|
||||||
|
|
||||||
|
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||||
|
|
||||||
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
|
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
|
||||||
|
|
||||||
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
||||||
|
|
||||||
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||||
|
|
||||||
"chromium-bidi": ["chromium-bidi@14.0.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw=="],
|
"chromium-bidi": ["chromium-bidi@14.0.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw=="],
|
||||||
|
|
||||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
@@ -174,6 +211,18 @@
|
|||||||
|
|
||||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
||||||
|
|
||||||
|
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
|
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||||
|
|
||||||
|
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
"data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
|
"data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
@@ -184,6 +233,8 @@
|
|||||||
|
|
||||||
"degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
|
"degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
|
||||||
|
|
||||||
|
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
|
|
||||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
|
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
|
||||||
@@ -192,18 +243,28 @@
|
|||||||
|
|
||||||
"diff": ["diff@7.0.0", "", {}, "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="],
|
"diff": ["diff@7.0.0", "", {}, "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|
||||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||||
|
|
||||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
|
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
|
||||||
|
|
||||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
"escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
|
"escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
|
||||||
@@ -214,20 +275,46 @@
|
|||||||
|
|
||||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||||
|
|
||||||
|
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||||
|
|
||||||
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
|
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
|
||||||
|
|
||||||
|
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||||
|
|
||||||
|
"eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||||
|
|
||||||
|
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||||
|
|
||||||
|
"express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="],
|
||||||
|
|
||||||
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
|
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
|
||||||
|
|
||||||
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
||||||
|
|
||||||
|
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||||
|
|
||||||
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
||||||
|
|
||||||
|
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||||
|
|
||||||
"flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="],
|
"flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="],
|
||||||
|
|
||||||
|
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||||
|
|
||||||
|
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
|
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
|
||||||
|
|
||||||
"get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="],
|
"get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="],
|
||||||
@@ -242,16 +329,40 @@
|
|||||||
|
|
||||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
|
||||||
|
|
||||||
|
"hono": ["hono@4.12.14", "", {}, "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w=="],
|
||||||
|
|
||||||
|
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||||
|
|
||||||
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||||
|
|
||||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||||
|
|
||||||
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||||
|
|
||||||
|
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
|
|
||||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
|
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
||||||
|
|
||||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||||
|
|
||||||
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
|
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||||
|
|
||||||
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
||||||
|
|
||||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||||
@@ -262,14 +373,32 @@
|
|||||||
|
|
||||||
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||||
|
|
||||||
|
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||||
|
|
||||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||||
|
|
||||||
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
|
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
|
||||||
|
|
||||||
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||||
|
|
||||||
|
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||||
|
|
||||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
"onnxruntime-common": ["onnxruntime-common@1.24.3", "", {}, "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA=="],
|
"onnxruntime-common": ["onnxruntime-common@1.24.3", "", {}, "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA=="],
|
||||||
@@ -282,8 +411,16 @@
|
|||||||
|
|
||||||
"pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
|
"pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
|
||||||
|
|
||||||
|
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
||||||
|
|
||||||
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
|
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
|
||||||
|
|
||||||
|
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||||
|
|
||||||
"platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="],
|
"platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="],
|
||||||
|
|
||||||
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
|
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
|
||||||
@@ -294,6 +431,8 @@
|
|||||||
|
|
||||||
"protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="],
|
"protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="],
|
||||||
|
|
||||||
|
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||||
|
|
||||||
"proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
|
"proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
|
||||||
|
|
||||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||||
@@ -302,18 +441,48 @@
|
|||||||
|
|
||||||
"puppeteer-core": ["puppeteer-core@24.40.0", "", { "dependencies": { "@puppeteer/browsers": "2.13.0", "chromium-bidi": "14.0.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1581282", "typed-query-selector": "^2.12.1", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.19.0" } }, "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag=="],
|
"puppeteer-core": ["puppeteer-core@24.40.0", "", { "dependencies": { "@puppeteer/browsers": "2.13.0", "chromium-bidi": "14.0.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1581282", "typed-query-selector": "^2.12.1", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.19.0" } }, "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag=="],
|
||||||
|
|
||||||
|
"qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
|
||||||
|
|
||||||
|
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||||
|
|
||||||
|
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
|
|
||||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
||||||
|
|
||||||
|
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
|
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
|
||||||
|
|
||||||
|
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||||
|
|
||||||
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
|
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
|
||||||
|
|
||||||
|
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||||
|
|
||||||
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||||
|
|
||||||
|
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||||
|
|
||||||
|
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||||
|
|
||||||
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
|
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
|
||||||
|
|
||||||
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
|
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
|
||||||
@@ -324,6 +493,8 @@
|
|||||||
|
|
||||||
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||||
|
|
||||||
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
|
|
||||||
"streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="],
|
"streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="],
|
||||||
|
|
||||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
@@ -338,18 +509,28 @@
|
|||||||
|
|
||||||
"text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
|
"text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
|
||||||
|
|
||||||
|
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||||
|
|
||||||
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
||||||
|
|
||||||
|
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||||
|
|
||||||
"typed-query-selector": ["typed-query-selector@2.12.1", "", {}, "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA=="],
|
"typed-query-selector": ["typed-query-selector@2.12.1", "", {}, "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||||
|
|
||||||
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
"webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.1", "", {}, "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw=="],
|
"webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.1", "", {}, "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw=="],
|
||||||
|
|
||||||
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
@@ -366,6 +547,10 @@
|
|||||||
|
|
||||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
|
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/claude-agent-sdk/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="],
|
||||||
|
|
||||||
"onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.24.0-dev.20251116-b39e144322", "", {}, "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw=="],
|
"onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.24.0-dev.20251116-b39e144322", "", {}, "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+228
-14
@@ -350,6 +350,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -463,20 +691,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -352,6 +352,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -465,20 +693,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -354,6 +354,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -467,20 +695,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-114
@@ -354,6 +354,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -467,20 +695,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
@@ -982,106 +1196,6 @@ Restore later with /context-restore.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<<<<<<< HEAD:checkpoint/SKILL.md.tmpl
|
|
||||||
## Resume flow
|
|
||||||
|
|
||||||
### Step 1: Find checkpoints
|
|
||||||
|
|
||||||
```bash
|
|
||||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
|
|
||||||
CHECKPOINT_DIR="$HOME/.gstack/projects/$SLUG/checkpoints"
|
|
||||||
if [ -d "$CHECKPOINT_DIR" ]; then
|
|
||||||
find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | xargs ls -1t 2>/dev/null | head -20
|
|
||||||
else
|
|
||||||
echo "NO_CHECKPOINTS"
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
List checkpoints from **all branches** (checkpoint files contain the branch name
|
|
||||||
in their frontmatter, so all files in the directory are candidates). This enables
|
|
||||||
Conductor workspace handoff — a checkpoint saved on one branch can be resumed from
|
|
||||||
another.
|
|
||||||
|
|
||||||
### Step 1.5: Check for WIP commit context (continuous checkpoint mode)
|
|
||||||
|
|
||||||
If `CHECKPOINT_MODE` was `"continuous"` during prior work, the branch may have
|
|
||||||
`WIP:` commits with structured `[gstack-context]` blocks in their bodies. These
|
|
||||||
are a second recovery trail alongside the markdown checkpoint files.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
_BRANCH=$(git branch --show-current 2>/dev/null)
|
|
||||||
# Detect if this branch has any WIP commits against the nearest remote ancestor
|
|
||||||
_BASE=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD origin/master 2>/dev/null)
|
|
||||||
if [ -n "$_BASE" ]; then
|
|
||||||
WIP_COMMITS=$(git log "$_BASE"..HEAD --grep="^WIP:" --format="%H" 2>/dev/null | head -20)
|
|
||||||
if [ -n "$WIP_COMMITS" ]; then
|
|
||||||
echo "WIP_COMMITS_FOUND"
|
|
||||||
# Extract [gstack-context] blocks from each WIP commit body
|
|
||||||
for SHA in $WIP_COMMITS; do
|
|
||||||
echo "--- commit $SHA ---"
|
|
||||||
git log -1 "$SHA" --format="%s%n%n%b" 2>/dev/null | \
|
|
||||||
awk '/\[gstack-context\]/,/\[\/gstack-context\]/ { print }'
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo "NO_WIP_COMMITS"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
If `WIP_COMMITS_FOUND`: Read the extracted `[gstack-context]` blocks. Each block
|
|
||||||
represents a logical unit of prior work with Decisions/Remaining/Tried/Skill.
|
|
||||||
Merge these with the markdown checkpoint file to reconstruct session state. The
|
|
||||||
git history shows the chronological arc; the markdown checkpoint shows the
|
|
||||||
intentional save points. Both matter.
|
|
||||||
|
|
||||||
**Important:** Do NOT delete WIP commits during resume. They remain the recovery
|
|
||||||
trail until /ship squashes them into clean commits during PR creation.
|
|
||||||
|
|
||||||
### Step 2: Load checkpoint
|
|
||||||
|
|
||||||
If the user specified a checkpoint (by number, title fragment, or date), find the
|
|
||||||
matching file. Otherwise, load the **most recent** checkpoint.
|
|
||||||
|
|
||||||
Read the checkpoint file and present a summary:
|
|
||||||
|
|
||||||
```
|
|
||||||
RESUMING CHECKPOINT
|
|
||||||
════════════════════════════════════════
|
|
||||||
Title: {title}
|
|
||||||
Branch: {branch from checkpoint}
|
|
||||||
Saved: {timestamp, human-readable}
|
|
||||||
Duration: Last session was {formatted duration} (if available)
|
|
||||||
Status: {status}
|
|
||||||
════════════════════════════════════════
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
{summary from checkpoint}
|
|
||||||
|
|
||||||
### Remaining Work
|
|
||||||
{remaining work items from checkpoint}
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
{notes from checkpoint}
|
|
||||||
```
|
|
||||||
|
|
||||||
If the current branch differs from the checkpoint's branch, note this:
|
|
||||||
"This checkpoint was saved on branch `{branch}`. You are currently on
|
|
||||||
`{current branch}`. You may want to switch branches before continuing."
|
|
||||||
|
|
||||||
### Step 3: Offer next steps
|
|
||||||
|
|
||||||
After presenting the checkpoint, ask via AskUserQuestion:
|
|
||||||
|
|
||||||
- A) Continue working on the remaining items
|
|
||||||
- B) Show the full checkpoint file
|
|
||||||
- C) Just needed the context, thanks
|
|
||||||
|
|
||||||
If A, summarize the first remaining work item and suggest starting there.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
=======
|
|
||||||
>>>>>>> origin/main:context-save/SKILL.md.tmpl
|
|
||||||
## List flow
|
## List flow
|
||||||
|
|
||||||
### Step 1: Gather saved contexts
|
### Step 1: Gather saved contexts
|
||||||
|
|||||||
@@ -198,106 +198,6 @@ Restore later with /context-restore.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<<<<<<< HEAD:checkpoint/SKILL.md.tmpl
|
|
||||||
## Resume flow
|
|
||||||
|
|
||||||
### Step 1: Find checkpoints
|
|
||||||
|
|
||||||
```bash
|
|
||||||
{{SLUG_SETUP}}
|
|
||||||
CHECKPOINT_DIR="$HOME/.gstack/projects/$SLUG/checkpoints"
|
|
||||||
if [ -d "$CHECKPOINT_DIR" ]; then
|
|
||||||
find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | xargs ls -1t 2>/dev/null | head -20
|
|
||||||
else
|
|
||||||
echo "NO_CHECKPOINTS"
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
List checkpoints from **all branches** (checkpoint files contain the branch name
|
|
||||||
in their frontmatter, so all files in the directory are candidates). This enables
|
|
||||||
Conductor workspace handoff — a checkpoint saved on one branch can be resumed from
|
|
||||||
another.
|
|
||||||
|
|
||||||
### Step 1.5: Check for WIP commit context (continuous checkpoint mode)
|
|
||||||
|
|
||||||
If `CHECKPOINT_MODE` was `"continuous"` during prior work, the branch may have
|
|
||||||
`WIP:` commits with structured `[gstack-context]` blocks in their bodies. These
|
|
||||||
are a second recovery trail alongside the markdown checkpoint files.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
_BRANCH=$(git branch --show-current 2>/dev/null)
|
|
||||||
# Detect if this branch has any WIP commits against the nearest remote ancestor
|
|
||||||
_BASE=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD origin/master 2>/dev/null)
|
|
||||||
if [ -n "$_BASE" ]; then
|
|
||||||
WIP_COMMITS=$(git log "$_BASE"..HEAD --grep="^WIP:" --format="%H" 2>/dev/null | head -20)
|
|
||||||
if [ -n "$WIP_COMMITS" ]; then
|
|
||||||
echo "WIP_COMMITS_FOUND"
|
|
||||||
# Extract [gstack-context] blocks from each WIP commit body
|
|
||||||
for SHA in $WIP_COMMITS; do
|
|
||||||
echo "--- commit $SHA ---"
|
|
||||||
git log -1 "$SHA" --format="%s%n%n%b" 2>/dev/null | \
|
|
||||||
awk '/\[gstack-context\]/,/\[\/gstack-context\]/ { print }'
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo "NO_WIP_COMMITS"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
If `WIP_COMMITS_FOUND`: Read the extracted `[gstack-context]` blocks. Each block
|
|
||||||
represents a logical unit of prior work with Decisions/Remaining/Tried/Skill.
|
|
||||||
Merge these with the markdown checkpoint file to reconstruct session state. The
|
|
||||||
git history shows the chronological arc; the markdown checkpoint shows the
|
|
||||||
intentional save points. Both matter.
|
|
||||||
|
|
||||||
**Important:** Do NOT delete WIP commits during resume. They remain the recovery
|
|
||||||
trail until /ship squashes them into clean commits during PR creation.
|
|
||||||
|
|
||||||
### Step 2: Load checkpoint
|
|
||||||
|
|
||||||
If the user specified a checkpoint (by number, title fragment, or date), find the
|
|
||||||
matching file. Otherwise, load the **most recent** checkpoint.
|
|
||||||
|
|
||||||
Read the checkpoint file and present a summary:
|
|
||||||
|
|
||||||
```
|
|
||||||
RESUMING CHECKPOINT
|
|
||||||
════════════════════════════════════════
|
|
||||||
Title: {title}
|
|
||||||
Branch: {branch from checkpoint}
|
|
||||||
Saved: {timestamp, human-readable}
|
|
||||||
Duration: Last session was {formatted duration} (if available)
|
|
||||||
Status: {status}
|
|
||||||
════════════════════════════════════════
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
{summary from checkpoint}
|
|
||||||
|
|
||||||
### Remaining Work
|
|
||||||
{remaining work items from checkpoint}
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
{notes from checkpoint}
|
|
||||||
```
|
|
||||||
|
|
||||||
If the current branch differs from the checkpoint's branch, note this:
|
|
||||||
"This checkpoint was saved on branch `{branch}`. You are currently on
|
|
||||||
`{current branch}`. You may want to switch branches before continuing."
|
|
||||||
|
|
||||||
### Step 3: Offer next steps
|
|
||||||
|
|
||||||
After presenting the checkpoint, ask via AskUserQuestion:
|
|
||||||
|
|
||||||
- A) Continue working on the remaining items
|
|
||||||
- B) Show the full checkpoint file
|
|
||||||
- C) Just needed the context, thanks
|
|
||||||
|
|
||||||
If A, summarize the first remaining work item and suggest starting there.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
=======
|
|
||||||
>>>>>>> origin/main:context-save/SKILL.md.tmpl
|
|
||||||
## List flow
|
## List flow
|
||||||
|
|
||||||
### Step 1: Gather saved contexts
|
### Step 1: Gather saved contexts
|
||||||
|
|||||||
+228
-14
@@ -355,6 +355,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -468,20 +696,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -355,6 +355,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -468,20 +696,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -357,6 +357,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -470,20 +698,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -355,6 +355,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -468,20 +696,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -352,6 +352,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -465,20 +693,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -355,6 +355,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -468,20 +696,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
# gbrain-sync error lookup
|
||||||
|
|
||||||
|
Every error message `gstack-brain-*` can print, with problem, cause, and fix.
|
||||||
|
|
||||||
|
Search this file by the prefix after `BRAIN_SYNC:` or by the binary name in
|
||||||
|
the command output.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `BRAIN_SYNC: brain repo detected: <url>`
|
||||||
|
|
||||||
|
**Problem.** You're on a machine that has `~/.gstack-brain-remote.txt` (copied
|
||||||
|
from another machine) but no local git repo at `~/.gstack/.git`.
|
||||||
|
|
||||||
|
**Cause.** You've set up GBrain sync elsewhere and your gstack hasn't been
|
||||||
|
restored on this machine yet.
|
||||||
|
|
||||||
|
**Fix.**
|
||||||
|
```bash
|
||||||
|
gstack-brain-restore
|
||||||
|
```
|
||||||
|
This pulls the repo into `~/.gstack/` and re-registers merge drivers.
|
||||||
|
|
||||||
|
If you don't want to restore here, dismiss the hint with:
|
||||||
|
```bash
|
||||||
|
gstack-config set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `BRAIN_SYNC: blocked: <pattern-family>:<snippet>`
|
||||||
|
|
||||||
|
**Problem.** Sync stopped because the secret scanner detected credential-shaped
|
||||||
|
content in a staged file. The queue is preserved; nothing was pushed.
|
||||||
|
|
||||||
|
**Cause.** One of the pre-commit secret patterns matched the file contents —
|
||||||
|
likely an AWS key, GitHub token, OpenAI key, PEM block, JWT, or bearer token
|
||||||
|
embedded in JSON.
|
||||||
|
|
||||||
|
**Fix (three options).**
|
||||||
|
|
||||||
|
1. **If it's a real secret**: edit the offending file to remove the secret,
|
||||||
|
then re-run any skill to retry sync.
|
||||||
|
|
||||||
|
2. **If the pattern is a false positive** (e.g., your learning contains a
|
||||||
|
GitHub token pattern in an example string that you *want* to publish):
|
||||||
|
```bash
|
||||||
|
gstack-brain-sync --skip-file <path>
|
||||||
|
```
|
||||||
|
This permanently excludes the path from future syncs.
|
||||||
|
|
||||||
|
3. **If you want to abandon this sync batch entirely** (start fresh):
|
||||||
|
```bash
|
||||||
|
gstack-brain-sync --drop-queue --yes
|
||||||
|
```
|
||||||
|
This clears the queue without committing. Future writes will re-populate
|
||||||
|
it normally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `BRAIN_SYNC: push failed: auth.`
|
||||||
|
|
||||||
|
**Problem.** Git push was rejected because your auth with the remote expired
|
||||||
|
or is missing.
|
||||||
|
|
||||||
|
**Cause.** The remote is unreachable with current credentials.
|
||||||
|
|
||||||
|
**Fix.** Refresh auth based on your remote:
|
||||||
|
|
||||||
|
- **GitHub**: `gh auth status` (then `gh auth refresh` if needed)
|
||||||
|
- **GitLab**: `glab auth status`
|
||||||
|
- **Other**: `git remote -v` + check SSH keys or credential helper
|
||||||
|
|
||||||
|
After fixing auth, run any skill to retry sync automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `BRAIN_SYNC: push failed: <first-line-of-error>`
|
||||||
|
|
||||||
|
**Problem.** Push failed for a reason other than auth. The first line of
|
||||||
|
git's error appears after the colon.
|
||||||
|
|
||||||
|
**Cause.** Could be network issue, rejected push (remote ahead), server 500,
|
||||||
|
or repo access revoked.
|
||||||
|
|
||||||
|
**Fix.** Look at `~/.gstack/.brain-sync-status.json` for more detail, or run:
|
||||||
|
```bash
|
||||||
|
cd ~/.gstack && git status && git push origin HEAD
|
||||||
|
```
|
||||||
|
to see git's full error. The queue is cleared after any push attempt, but
|
||||||
|
your local commit still exists — the next skill run will retry the push.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `gstack-brain-init: ~/.gstack/.git is already a git repo pointing at <url>`
|
||||||
|
|
||||||
|
**Problem.** You tried to init with a remote URL that doesn't match the
|
||||||
|
existing one.
|
||||||
|
|
||||||
|
**Cause.** You already ran `gstack-brain-init` with a different remote.
|
||||||
|
|
||||||
|
**Fix.** Either:
|
||||||
|
|
||||||
|
- Use the existing remote: run `gstack-brain-init` without `--remote`, or
|
||||||
|
with the matching URL.
|
||||||
|
- Switch remotes: `gstack-brain-uninstall` first, then re-init with the new
|
||||||
|
URL. This does not delete your data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `Remote not reachable: <url>`
|
||||||
|
|
||||||
|
**Problem.** Init couldn't reach the git remote to verify connectivity.
|
||||||
|
|
||||||
|
**Cause.** Wrong URL, missing auth, network issue.
|
||||||
|
|
||||||
|
**Fix.** Test manually:
|
||||||
|
```bash
|
||||||
|
git ls-remote <url>
|
||||||
|
```
|
||||||
|
If that fails, check:
|
||||||
|
- URL spelling
|
||||||
|
- GitHub: `gh auth status`
|
||||||
|
- GitLab: `glab auth status`
|
||||||
|
- Private network / VPN / DNS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `gstack-brain-init: failed to create or find '<name>'`
|
||||||
|
|
||||||
|
**Problem.** Auto-repo-creation via `gh repo create` failed and the repo
|
||||||
|
isn't discoverable via `gh repo view` either.
|
||||||
|
|
||||||
|
**Cause.** `gh` is unauthenticated, a repo with that name already exists
|
||||||
|
owned by someone else, or your GitHub account hit a quota.
|
||||||
|
|
||||||
|
**Fix.**
|
||||||
|
```bash
|
||||||
|
gh auth status
|
||||||
|
```
|
||||||
|
If unauth'd, run `gh auth login`. If the repo name collides, pass a different
|
||||||
|
name:
|
||||||
|
```bash
|
||||||
|
gstack-brain-init --remote git@github.com:YOURUSER/custom-name.git
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `gstack-brain-restore: ~/.gstack/.git already points at <url>`
|
||||||
|
|
||||||
|
**Problem.** You tried to restore from a URL that doesn't match the existing
|
||||||
|
git config.
|
||||||
|
|
||||||
|
**Cause.** Stale `.git` from a previous init with a different remote.
|
||||||
|
|
||||||
|
**Fix.** `gstack-brain-uninstall`, then re-run `gstack-brain-restore <url>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `gstack-brain-restore: ~/.gstack/ has existing allowlisted files that would be clobbered`
|
||||||
|
|
||||||
|
**Problem.** You're trying to restore, but `~/.gstack/` already contains
|
||||||
|
learnings or plans that would be overwritten.
|
||||||
|
|
||||||
|
**Cause.** Either (a) this machine has accumulated state from a pre-sync
|
||||||
|
gstack session, or (b) a previous failed restore left partial state.
|
||||||
|
|
||||||
|
**Fix (three options).**
|
||||||
|
|
||||||
|
1. **If this machine's state should become the new truth**: run
|
||||||
|
`gstack-brain-init` instead of restore — this creates a brand-new brain
|
||||||
|
repo from this machine's state.
|
||||||
|
|
||||||
|
2. **If you want to adopt the remote and discard this machine's state**:
|
||||||
|
back up `~/.gstack/projects/` first, then remove the offending files and
|
||||||
|
re-run restore.
|
||||||
|
|
||||||
|
3. **If you want to merge**: there's no automatic merge for this. Manually
|
||||||
|
copy learnings from `~/.gstack/` into your running gstack on a machine
|
||||||
|
with sync already on, then restore here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `gstack-brain-restore: <url> does not look like a gstack-brain repo`
|
||||||
|
|
||||||
|
**Problem.** The clone succeeded but the repo is missing `.brain-allowlist`
|
||||||
|
and `.gitattributes`.
|
||||||
|
|
||||||
|
**Cause.** You pointed restore at a random git repo, or someone deleted the
|
||||||
|
canonical config files from the brain repo.
|
||||||
|
|
||||||
|
**Fix.** Verify the URL. If it's correct, run `gstack-brain-init --remote
|
||||||
|
<url>` to re-seed the canonical config.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nothing is syncing but I expect it to
|
||||||
|
|
||||||
|
**Not an error, but a common gotcha.** Check in order:
|
||||||
|
|
||||||
|
1. `gstack-brain-sync --status` — is mode `off`?
|
||||||
|
2. `~/.gstack/.git` exists?
|
||||||
|
3. `gstack-config get gbrain_sync_mode` — should be `full` or `artifacts-only`.
|
||||||
|
4. The file you expect to sync — is it in the allowlist?
|
||||||
|
`cat ~/.gstack/.brain-allowlist`
|
||||||
|
5. Privacy class filter — if mode is `artifacts-only`, behavioral files
|
||||||
|
(timelines, developer-profile) are intentionally skipped.
|
||||||
|
|
||||||
|
If all those look right, run:
|
||||||
|
```bash
|
||||||
|
gstack-brain-sync --discover-new
|
||||||
|
gstack-brain-sync --once
|
||||||
|
```
|
||||||
|
to force a drain.
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
# Cross-machine memory with GBrain sync
|
||||||
|
|
||||||
|
gstack writes a lot of useful state to `~/.gstack/` — learnings, retros, CEO
|
||||||
|
plans, design docs, developer profile. By default, all of that dies when you
|
||||||
|
switch laptops. **GBrain sync** pushes a curated subset to a private git
|
||||||
|
repo so your memory follows you across machines and becomes indexable by
|
||||||
|
GBrain.
|
||||||
|
|
||||||
|
## What you get
|
||||||
|
|
||||||
|
- Work on machine A, pick up seamlessly on machine B.
|
||||||
|
- Your learnings, plans, and designs are visible in GBrain (if you use it).
|
||||||
|
- A clean off-ramp (`gstack-brain-uninstall`) that never touches your data.
|
||||||
|
- No daemon, no system service, no background process.
|
||||||
|
|
||||||
|
## What does NOT leave your machine
|
||||||
|
|
||||||
|
By design, these stay local even when sync is on:
|
||||||
|
|
||||||
|
- Credentials: `.auth.json`, `auth-token.json`, `sidebar-sessions/`,
|
||||||
|
`security/device-salt`, consumer tokens in `config.yaml`
|
||||||
|
- Machine-specific state: Chromium profiles, ONNX model weights,
|
||||||
|
caches, eval-cache, CDP-profile, one-time prompt markers
|
||||||
|
(`.welcome-seen`, `.telemetry-prompted`, `.vendoring-warned-*`, etc.)
|
||||||
|
- Question-preferences: per-machine UX preferences
|
||||||
|
(`question-preferences.json`, `question-log.jsonl`, `question-events.jsonl`).
|
||||||
|
|
||||||
|
The exact allowlist lives in `~/.gstack/.brain-allowlist`. The CLI manages
|
||||||
|
it; you can append your own entries below the marker line.
|
||||||
|
|
||||||
|
## First-run setup (30–90 seconds)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gstack-brain-init
|
||||||
|
```
|
||||||
|
|
||||||
|
The command:
|
||||||
|
|
||||||
|
1. Turns `~/.gstack/` into a git repo.
|
||||||
|
2. Asks for a remote URL (default: `gh repo create --private
|
||||||
|
gstack-brain-$USER`). Any git remote works — GitHub, GitLab, Gitea,
|
||||||
|
self-hosted.
|
||||||
|
3. Pushes an initial commit with just the config.
|
||||||
|
4. Writes `~/.gstack-brain-remote.txt` (URL-only, no secrets —
|
||||||
|
safe to copy to another machine).
|
||||||
|
5. Registers GBrain as a reader if `GBRAIN_URL` + `GBRAIN_TOKEN` are
|
||||||
|
configured. Otherwise you can add readers later with
|
||||||
|
`gstack-brain-reader add <name> --ingest-url <url> --token <token>`.
|
||||||
|
|
||||||
|
After init, the **next skill you run** will ask you ONE question about
|
||||||
|
privacy mode:
|
||||||
|
|
||||||
|
- **Everything allowlisted (recommended)**: learnings, reviews, plans,
|
||||||
|
designs, retros, timelines, and developer profile all sync.
|
||||||
|
- **Only artifacts**: plans, designs, retros, learnings — skip
|
||||||
|
behavioral data (timelines, developer profile).
|
||||||
|
- **Decline**: keep everything local. You can turn sync on later with
|
||||||
|
`gstack-config set gbrain_sync_mode full`.
|
||||||
|
|
||||||
|
Your answer is persisted. You won't be asked again.
|
||||||
|
|
||||||
|
## Cross-machine workflow
|
||||||
|
|
||||||
|
On machine A: run `gstack-brain-init` once. That's it — every skill
|
||||||
|
invocation now drains the sync queue at its start and end boundaries
|
||||||
|
(~200–800 ms network pause per skill).
|
||||||
|
|
||||||
|
On machine B:
|
||||||
|
|
||||||
|
1. Copy `~/.gstack-brain-remote.txt` from machine A to machine B
|
||||||
|
(password manager, dotfile repo, USB stick — your call).
|
||||||
|
2. Run any gstack skill. The preamble sees the URL file and prints:
|
||||||
|
```
|
||||||
|
BRAIN_SYNC: brain repo detected: <url>
|
||||||
|
BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory
|
||||||
|
```
|
||||||
|
3. Run `gstack-brain-restore`. That clones the repo, rehydrates your
|
||||||
|
learnings/plans/retros, and re-registers the git merge drivers.
|
||||||
|
4. Re-enter consumer tokens (they're machine-local and NOT synced —
|
||||||
|
`gstack-config set gbrain_token <your-token>`).
|
||||||
|
5. Next skill: your yesterday-on-machine-A learning surfaces. That's the
|
||||||
|
magical moment.
|
||||||
|
|
||||||
|
## Status, health, and queue depth
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gstack-brain-sync --status
|
||||||
|
```
|
||||||
|
|
||||||
|
Shows: last successful push, pending queue depth, any sync blocks, and the
|
||||||
|
current privacy mode.
|
||||||
|
|
||||||
|
Every skill run prints a `BRAIN_SYNC:` line near the top of the preamble
|
||||||
|
output. Scan it for problems.
|
||||||
|
|
||||||
|
## Privacy modes in detail
|
||||||
|
|
||||||
|
| Mode | What syncs |
|
||||||
|
|------|------------|
|
||||||
|
| `off` | Nothing (default). |
|
||||||
|
| `artifacts-only` | Plans, designs, retros, learnings, reviews. Skips timelines + developer-profile. |
|
||||||
|
| `full` | Everything in the allowlist, including behavioral state. |
|
||||||
|
|
||||||
|
Change anytime with:
|
||||||
|
```bash
|
||||||
|
gstack-config set gbrain_sync_mode full
|
||||||
|
gstack-config set gbrain_sync_mode off
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secret protection
|
||||||
|
|
||||||
|
Every commit is scanned for credential-shaped content before it leaves
|
||||||
|
your machine. Blocked patterns include:
|
||||||
|
|
||||||
|
- AWS access keys (`AKIA…`)
|
||||||
|
- GitHub tokens (`ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`, `github_pat_`)
|
||||||
|
- OpenAI keys (`sk-…`)
|
||||||
|
- PEM blocks (`-----BEGIN …-----`)
|
||||||
|
- JWTs (`eyJ…`)
|
||||||
|
- Bearer tokens in JSON (`"authorization": "…"`, `"api_key": "…"`, etc.)
|
||||||
|
|
||||||
|
If a scan hits, sync stops, the queue is preserved, and your preamble
|
||||||
|
prints:
|
||||||
|
|
||||||
|
```
|
||||||
|
BRAIN_SYNC: blocked: <pattern-family>:<snippet>
|
||||||
|
```
|
||||||
|
|
||||||
|
To remediate:
|
||||||
|
|
||||||
|
1. Review the offending file.
|
||||||
|
2. If the match is a false positive on content you explicitly want to
|
||||||
|
sync, run `gstack-brain-sync --skip-file <path>` to permanently
|
||||||
|
exclude that path.
|
||||||
|
3. Otherwise, edit the file to remove the secret and re-run any skill.
|
||||||
|
|
||||||
|
There's a defense-in-depth hook at `~/.gstack/.git/hooks/pre-commit` that
|
||||||
|
runs the same scan if you manually `git commit` against the repo.
|
||||||
|
|
||||||
|
## Two-machine conflicts
|
||||||
|
|
||||||
|
If you write on machine A and machine B the same day, both will push
|
||||||
|
append commits. Git's default would conflict at the file tail, but the
|
||||||
|
`.jsonl` and markdown files are registered with custom merge drivers:
|
||||||
|
|
||||||
|
- JSONL files use a sort-and-dedup driver that orders appends by ISO
|
||||||
|
timestamp (falls back to SHA-256 hash of each line for determinism).
|
||||||
|
- Markdown artifacts (retros, plans, designs) use a union merge driver
|
||||||
|
that concatenates both sides.
|
||||||
|
|
||||||
|
You shouldn't see conflict prompts. If you do (a real semantic conflict,
|
||||||
|
like two machines editing the same plan), git will stop and prompt.
|
||||||
|
|
||||||
|
## Cross-machine pull cadence
|
||||||
|
|
||||||
|
The preamble runs `git fetch` + `git merge --ff-only` once per 24 hours
|
||||||
|
(cached via `~/.gstack/.brain-last-pull`). You don't need to think about
|
||||||
|
this — it happens automatically at the first skill invocation each day.
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gstack-brain-uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
This:
|
||||||
|
|
||||||
|
- Removes `~/.gstack/.git/` and all `.brain-*` config files.
|
||||||
|
- Clears `gbrain_sync_mode` in `gstack-config`.
|
||||||
|
- Does NOT touch your learnings, plans, retros, or developer profile.
|
||||||
|
|
||||||
|
Add `--delete-remote` to also delete the private GitHub repo (GitHub only,
|
||||||
|
uses `gh repo delete`).
|
||||||
|
|
||||||
|
Re-init anytime with `gstack-brain-init`.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
See [gbrain-sync-errors.md](gbrain-sync-errors.md) for an index of every
|
||||||
|
error message gstack-brain may print, with problem / cause / fix for each.
|
||||||
|
|
||||||
|
## Under the hood
|
||||||
|
|
||||||
|
For the architectural decisions behind this feature (allowlist vs
|
||||||
|
denylist, daemon vs preamble-boundary sync, JSONL merge driver, privacy
|
||||||
|
stop-gate), see the
|
||||||
|
[approved plan](../system-instruction-you-are-working-jaunty-kahn.md) in
|
||||||
|
the gstack plans directory.
|
||||||
+228
-14
@@ -352,6 +352,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -465,20 +693,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -352,6 +352,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -465,20 +693,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -369,6 +369,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -482,20 +710,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -349,6 +349,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -462,20 +690,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -350,6 +350,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -463,20 +691,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -352,6 +352,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -465,20 +693,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
@@ -350,6 +350,105 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
|
|||||||
@@ -1,39 +1,18 @@
|
|||||||
{{INHERIT:claude}}
|
{{INHERIT:claude}}
|
||||||
|
|
||||||
**Fan out explicitly.** Opus 4.7 serializes by default. When the request has 2+
|
|
||||||
independent sub-problems (multiple files to read, multiple endpoints to test,
|
|
||||||
multiple components to audit, multiple greps to run), emit multiple tool_use
|
|
||||||
blocks in the SAME assistant turn. That is how you parallelize. One turn with
|
|
||||||
N tool calls, not N turns with 1 tool call each.
|
|
||||||
|
|
||||||
Concrete example. If the user says "read foo.ts, bar.ts, and baz.ts":
|
|
||||||
|
|
||||||
Wrong (3 turns):
|
|
||||||
Turn 1: Read(foo.ts), then you wait for output
|
|
||||||
Turn 2: Read(bar.ts), then you wait for output
|
|
||||||
Turn 3: Read(baz.ts)
|
|
||||||
|
|
||||||
Right (1 turn, 3 parallel tool calls):
|
|
||||||
Turn 1: [Read(foo.ts), Read(bar.ts), Read(baz.ts)] ← three tool_use blocks,
|
|
||||||
same assistant message
|
|
||||||
|
|
||||||
This applies to Read, Bash, Grep, Glob, WebFetch, Agent/subagent, and any tool
|
|
||||||
where the sub-calls do not depend on each other's output. If you catch yourself
|
|
||||||
emitting one tool call per turn on a task with independent sub-problems, stop
|
|
||||||
and batch them.
|
|
||||||
|
|
||||||
**Effort-match the step.** Simple file reads, config checks, command lookups, and
|
**Effort-match the step.** Simple file reads, config checks, command lookups, and
|
||||||
mechanical edits don't need deep reasoning. Complete them quickly and move on. Reserve
|
mechanical edits don't need deep reasoning. Complete them quickly and move on. Reserve
|
||||||
extended thinking for genuinely hard subproblems: architectural tradeoffs, subtle bugs,
|
extended thinking for genuinely hard subproblems: architectural tradeoffs, subtle bugs,
|
||||||
security implications, design decisions with competing constraints. Over-thinking
|
security implications, design decisions with competing constraints. Over-thinking
|
||||||
simple steps wastes tokens and time.
|
simple steps wastes tokens and time.
|
||||||
|
|
||||||
**Batch your questions.** If you need to clarify multiple things before proceeding,
|
**Pace questions to the skill.** If the current skill's text contains
|
||||||
ask all of them in a single AskUserQuestion turn. Do not drip-feed one question per
|
`STOP. AskUserQuestion` anywhere, pace one question per turn — emit the question as
|
||||||
turn. Three questions in one message beats three back-and-forth exchanges. Exception:
|
a tool_use, stop, wait for the user's response, then continue. Do not batch. A
|
||||||
skill workflows that explicitly require one-question-at-a-time pacing (e.g., plan
|
finding with an "obvious fix" is still a finding and still needs user approval
|
||||||
review skills with "STOP. AskUserQuestion once per issue. Do NOT batch.") override this
|
before it lands in the plan. Only batch clarifying questions upfront when (a) the
|
||||||
nudge. The skill wins on pacing, always.
|
skill has no `STOP. AskUserQuestion` directive AND (b) you need multiple unrelated
|
||||||
|
clarifications before you can begin. When in doubt, ask one question per turn.
|
||||||
|
|
||||||
**Literal interpretation awareness.** Opus 4.7 interprets instructions literally and
|
**Literal interpretation awareness.** Opus 4.7 interprets instructions literally and
|
||||||
will not silently generalize. When the user says "fix the tests," fix all failing tests
|
will not silently generalize. When the user says "fix the tests," fix all failing tests
|
||||||
|
|||||||
+228
-14
@@ -360,6 +360,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -473,20 +701,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -349,6 +349,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -462,20 +690,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gstack",
|
"name": "gstack",
|
||||||
"version": "1.8.0.0",
|
"version": "1.11.0.0",
|
||||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
"devtools"
|
"devtools"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@anthropic-ai/claude-agent-sdk": "0.2.117",
|
||||||
"@anthropic-ai/sdk": "^0.78.0"
|
"@anthropic-ai/sdk": "^0.78.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+228
-14
@@ -350,6 +350,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -463,20 +691,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+241
-27
@@ -356,6 +356,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -469,20 +697,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
@@ -1384,7 +1598,7 @@ Once selected, commit fully. Do not silently drift.
|
|||||||
|
|
||||||
Present these mode options via AskUserQuestion using the preamble's AskUserQuestion Format section: include RECOMMENDATION. These options differ in kind (review posture), not coverage — do NOT emit `Completeness: N/10` per option. Include the one-line note from step 4 of the preamble format rule instead: `Note: options differ in kind, not coverage — no completeness score.`
|
Present these mode options via AskUserQuestion using the preamble's AskUserQuestion Format section: include RECOMMENDATION. These options differ in kind (review posture), not coverage — do NOT emit `Completeness: N/10` per option. Include the one-line note from step 4 of the preamble format rule instead: `Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
## Review Sections (11 sections, after scope and mode are agreed)
|
## Review Sections (11 sections, after scope and mode are agreed)
|
||||||
@@ -1414,7 +1628,7 @@ Evaluate and diagram:
|
|||||||
**SELECTIVE EXPANSION:** If any accepted cherry-picks from Step 0D affect the architecture, evaluate their architectural fit here. Flag any that create coupling concerns or don't integrate cleanly — this is a chance to revisit the decision with new information.
|
**SELECTIVE EXPANSION:** If any accepted cherry-picks from Step 0D affect the architecture, evaluate their architectural fit here. Flag any that create coupling concerns or don't integrate cleanly — this is a chance to revisit the decision with new information.
|
||||||
|
|
||||||
Required ASCII diagram: full system architecture showing new components and their relationships to existing ones.
|
Required ASCII diagram: full system architecture showing new components and their relationships to existing ones.
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 2: Error & Rescue Map
|
### Section 2: Error & Rescue Map
|
||||||
@@ -1444,7 +1658,7 @@ Rules for this section:
|
|||||||
* Every rescued error must either: retry with backoff, degrade gracefully with a user-visible message, or re-raise with added context. "Swallow and continue" is almost never acceptable.
|
* Every rescued error must either: retry with backoff, degrade gracefully with a user-visible message, or re-raise with added context. "Swallow and continue" is almost never acceptable.
|
||||||
* For each GAP (unrescued error that should be rescued): specify the rescue action and what the user should see.
|
* For each GAP (unrescued error that should be rescued): specify the rescue action and what the user should see.
|
||||||
* For LLM/AI service calls specifically: what happens when the response is malformed? When it's empty? When it hallucinates invalid JSON? When the model returns a refusal? Each of these is a distinct failure mode.
|
* For LLM/AI service calls specifically: what happens when the response is malformed? When it's empty? When it hallucinates invalid JSON? When the model returns a refusal? Each of these is a distinct failure mode.
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 3: Security & Threat Model
|
### Section 3: Security & Threat Model
|
||||||
@@ -1460,7 +1674,7 @@ Evaluate:
|
|||||||
* Audit logging. For sensitive operations: is there an audit trail?
|
* Audit logging. For sensitive operations: is there an audit trail?
|
||||||
|
|
||||||
For each finding: threat, likelihood (High/Med/Low), impact (High/Med/Low), and whether the plan mitigates it.
|
For each finding: threat, likelihood (High/Med/Low), impact (High/Med/Low), and whether the plan mitigates it.
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 4: Data Flow & Interaction Edge Cases
|
### Section 4: Data Flow & Interaction Edge Cases
|
||||||
@@ -1497,7 +1711,7 @@ For each node: what happens on each shadow path? Is it tested?
|
|||||||
| Queue backs up 2 hours | ? |
|
| Queue backs up 2 hours | ? |
|
||||||
```
|
```
|
||||||
Flag any unhandled edge case as a gap. For each gap, specify the fix.
|
Flag any unhandled edge case as a gap. For each gap, specify the fix.
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 5: Code Quality Review
|
### Section 5: Code Quality Review
|
||||||
@@ -1510,7 +1724,7 @@ Evaluate:
|
|||||||
* Over-engineering check. Any new abstraction solving a problem that doesn't exist yet?
|
* Over-engineering check. Any new abstraction solving a problem that doesn't exist yet?
|
||||||
* Under-engineering check. Anything fragile, assuming happy path only, or missing obvious defensive checks?
|
* Under-engineering check. Anything fragile, assuming happy path only, or missing obvious defensive checks?
|
||||||
* Cyclomatic complexity. Flag any new method that branches more than 5 times. Propose a refactor.
|
* Cyclomatic complexity. Flag any new method that branches more than 5 times. Propose a refactor.
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 6: Test Review
|
### Section 6: Test Review
|
||||||
@@ -1551,7 +1765,7 @@ Flakiness risk: Flag any test depending on time, randomness, external services,
|
|||||||
Load/stress test requirements: For any new codepath called frequently or processing significant data.
|
Load/stress test requirements: For any new codepath called frequently or processing significant data.
|
||||||
|
|
||||||
For LLM/prompt changes: Check CLAUDE.md for the "Prompt/LLM changes" file patterns. If this plan touches ANY of those patterns, state which eval suites must be run, which cases should be added, and what baselines to compare against.
|
For LLM/prompt changes: Check CLAUDE.md for the "Prompt/LLM changes" file patterns. If this plan touches ANY of those patterns, state which eval suites must be run, which cases should be added, and what baselines to compare against.
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 7: Performance Review
|
### Section 7: Performance Review
|
||||||
@@ -1563,7 +1777,7 @@ Evaluate:
|
|||||||
* Background job sizing. For every new job: worst-case payload, runtime, retry behavior?
|
* Background job sizing. For every new job: worst-case payload, runtime, retry behavior?
|
||||||
* Slow paths. Top 3 slowest new codepaths and estimated p99 latency.
|
* Slow paths. Top 3 slowest new codepaths and estimated p99 latency.
|
||||||
* Connection pool pressure. New DB connections, Redis connections, HTTP connections?
|
* Connection pool pressure. New DB connections, Redis connections, HTTP connections?
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 8: Observability & Debuggability Review
|
### Section 8: Observability & Debuggability Review
|
||||||
@@ -1580,7 +1794,7 @@ Evaluate:
|
|||||||
|
|
||||||
**EXPANSION and SELECTIVE EXPANSION addition:**
|
**EXPANSION and SELECTIVE EXPANSION addition:**
|
||||||
* What observability would make this feature a joy to operate? (For SELECTIVE EXPANSION, include observability for any accepted cherry-picks.)
|
* What observability would make this feature a joy to operate? (For SELECTIVE EXPANSION, include observability for any accepted cherry-picks.)
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 9: Deployment & Rollout Review
|
### Section 9: Deployment & Rollout Review
|
||||||
@@ -1596,7 +1810,7 @@ Evaluate:
|
|||||||
|
|
||||||
**EXPANSION and SELECTIVE EXPANSION addition:**
|
**EXPANSION and SELECTIVE EXPANSION addition:**
|
||||||
* What deploy infrastructure would make shipping this feature routine? (For SELECTIVE EXPANSION, assess whether accepted cherry-picks change the deployment risk profile.)
|
* What deploy infrastructure would make shipping this feature routine? (For SELECTIVE EXPANSION, assess whether accepted cherry-picks change the deployment risk profile.)
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 10: Long-Term Trajectory Review
|
### Section 10: Long-Term Trajectory Review
|
||||||
@@ -1612,7 +1826,7 @@ Evaluate:
|
|||||||
* What comes after this ships? Phase 2? Phase 3? Does the architecture support that trajectory?
|
* What comes after this ships? Phase 2? Phase 3? Does the architecture support that trajectory?
|
||||||
* Platform potential. Does this create capabilities other features can leverage?
|
* Platform potential. Does this create capabilities other features can leverage?
|
||||||
* (SELECTIVE EXPANSION only) Retrospective: Were the right cherry-picks accepted? Did any rejected expansions turn out to be load-bearing for the accepted ones?
|
* (SELECTIVE EXPANSION only) Retrospective: Were the right cherry-picks accepted? Did any rejected expansions turn out to be load-bearing for the accepted ones?
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 11: Design & UX Review (skip if no UI scope detected)
|
### Section 11: Design & UX Review (skip if no UI scope detected)
|
||||||
@@ -1635,7 +1849,7 @@ Evaluate:
|
|||||||
Required ASCII diagram: user flow showing screens/states and transitions.
|
Required ASCII diagram: user flow showing screens/states and transitions.
|
||||||
|
|
||||||
If this plan has significant UI scope, recommend: "Consider running /plan-design-review for a deep design review of this plan before implementation."
|
If this plan has significant UI scope, recommend: "Consider running /plan-design-review for a deep design review of this plan before implementation."
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
## Outside Voice — Independent Plan Challenge (optional, recommended)
|
## Outside Voice — Independent Plan Challenge (optional, recommended)
|
||||||
@@ -1793,7 +2007,7 @@ Follow the AskUserQuestion format from the Preamble above. Additional rules for
|
|||||||
* For each option: effort, risk, and maintenance burden in one line.
|
* For each option: effort, risk, and maintenance burden in one line.
|
||||||
* **Map the reasoning to my engineering preferences above.** One sentence connecting your recommendation to a specific preference.
|
* **Map the reasoning to my engineering preferences above.** One sentence connecting your recommendation to a specific preference.
|
||||||
* Label with issue NUMBER + option LETTER (e.g., "3A", "3B").
|
* Label with issue NUMBER + option LETTER (e.g., "3A", "3B").
|
||||||
* **Escape hatch:** If a section has no issues, say so and move on. If an issue has an obvious fix with no real alternatives, state what you'll do and move on — don't waste a question on it. Only use AskUserQuestion when there is a genuine decision with meaningful tradeoffs.
|
* **Escape hatch (tightened):** If a section has zero findings, state "No issues, moving on" and proceed. If it has findings, use AskUserQuestion for each — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Only skip AskUserQuestion when the decision is genuinely trivial (e.g., a typo fix) AND there are no meaningful alternatives. When in doubt, ask.
|
||||||
|
|
||||||
## Required Outputs
|
## Required Outputs
|
||||||
|
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ Once selected, commit fully. Do not silently drift.
|
|||||||
|
|
||||||
Present these mode options via AskUserQuestion using the preamble's AskUserQuestion Format section: include RECOMMENDATION. These options differ in kind (review posture), not coverage — do NOT emit `Completeness: N/10` per option. Include the one-line note from step 4 of the preamble format rule instead: `Note: options differ in kind, not coverage — no completeness score.`
|
Present these mode options via AskUserQuestion using the preamble's AskUserQuestion Format section: include RECOMMENDATION. These options differ in kind (review posture), not coverage — do NOT emit `Completeness: N/10` per option. Include the one-line note from step 4 of the preamble format rule instead: `Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
## Review Sections (11 sections, after scope and mode are agreed)
|
## Review Sections (11 sections, after scope and mode are agreed)
|
||||||
@@ -406,7 +406,7 @@ Evaluate and diagram:
|
|||||||
**SELECTIVE EXPANSION:** If any accepted cherry-picks from Step 0D affect the architecture, evaluate their architectural fit here. Flag any that create coupling concerns or don't integrate cleanly — this is a chance to revisit the decision with new information.
|
**SELECTIVE EXPANSION:** If any accepted cherry-picks from Step 0D affect the architecture, evaluate their architectural fit here. Flag any that create coupling concerns or don't integrate cleanly — this is a chance to revisit the decision with new information.
|
||||||
|
|
||||||
Required ASCII diagram: full system architecture showing new components and their relationships to existing ones.
|
Required ASCII diagram: full system architecture showing new components and their relationships to existing ones.
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 2: Error & Rescue Map
|
### Section 2: Error & Rescue Map
|
||||||
@@ -436,7 +436,7 @@ Rules for this section:
|
|||||||
* Every rescued error must either: retry with backoff, degrade gracefully with a user-visible message, or re-raise with added context. "Swallow and continue" is almost never acceptable.
|
* Every rescued error must either: retry with backoff, degrade gracefully with a user-visible message, or re-raise with added context. "Swallow and continue" is almost never acceptable.
|
||||||
* For each GAP (unrescued error that should be rescued): specify the rescue action and what the user should see.
|
* For each GAP (unrescued error that should be rescued): specify the rescue action and what the user should see.
|
||||||
* For LLM/AI service calls specifically: what happens when the response is malformed? When it's empty? When it hallucinates invalid JSON? When the model returns a refusal? Each of these is a distinct failure mode.
|
* For LLM/AI service calls specifically: what happens when the response is malformed? When it's empty? When it hallucinates invalid JSON? When the model returns a refusal? Each of these is a distinct failure mode.
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 3: Security & Threat Model
|
### Section 3: Security & Threat Model
|
||||||
@@ -452,7 +452,7 @@ Evaluate:
|
|||||||
* Audit logging. For sensitive operations: is there an audit trail?
|
* Audit logging. For sensitive operations: is there an audit trail?
|
||||||
|
|
||||||
For each finding: threat, likelihood (High/Med/Low), impact (High/Med/Low), and whether the plan mitigates it.
|
For each finding: threat, likelihood (High/Med/Low), impact (High/Med/Low), and whether the plan mitigates it.
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 4: Data Flow & Interaction Edge Cases
|
### Section 4: Data Flow & Interaction Edge Cases
|
||||||
@@ -489,7 +489,7 @@ For each node: what happens on each shadow path? Is it tested?
|
|||||||
| Queue backs up 2 hours | ? |
|
| Queue backs up 2 hours | ? |
|
||||||
```
|
```
|
||||||
Flag any unhandled edge case as a gap. For each gap, specify the fix.
|
Flag any unhandled edge case as a gap. For each gap, specify the fix.
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 5: Code Quality Review
|
### Section 5: Code Quality Review
|
||||||
@@ -502,7 +502,7 @@ Evaluate:
|
|||||||
* Over-engineering check. Any new abstraction solving a problem that doesn't exist yet?
|
* Over-engineering check. Any new abstraction solving a problem that doesn't exist yet?
|
||||||
* Under-engineering check. Anything fragile, assuming happy path only, or missing obvious defensive checks?
|
* Under-engineering check. Anything fragile, assuming happy path only, or missing obvious defensive checks?
|
||||||
* Cyclomatic complexity. Flag any new method that branches more than 5 times. Propose a refactor.
|
* Cyclomatic complexity. Flag any new method that branches more than 5 times. Propose a refactor.
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 6: Test Review
|
### Section 6: Test Review
|
||||||
@@ -543,7 +543,7 @@ Flakiness risk: Flag any test depending on time, randomness, external services,
|
|||||||
Load/stress test requirements: For any new codepath called frequently or processing significant data.
|
Load/stress test requirements: For any new codepath called frequently or processing significant data.
|
||||||
|
|
||||||
For LLM/prompt changes: Check CLAUDE.md for the "Prompt/LLM changes" file patterns. If this plan touches ANY of those patterns, state which eval suites must be run, which cases should be added, and what baselines to compare against.
|
For LLM/prompt changes: Check CLAUDE.md for the "Prompt/LLM changes" file patterns. If this plan touches ANY of those patterns, state which eval suites must be run, which cases should be added, and what baselines to compare against.
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 7: Performance Review
|
### Section 7: Performance Review
|
||||||
@@ -555,7 +555,7 @@ Evaluate:
|
|||||||
* Background job sizing. For every new job: worst-case payload, runtime, retry behavior?
|
* Background job sizing. For every new job: worst-case payload, runtime, retry behavior?
|
||||||
* Slow paths. Top 3 slowest new codepaths and estimated p99 latency.
|
* Slow paths. Top 3 slowest new codepaths and estimated p99 latency.
|
||||||
* Connection pool pressure. New DB connections, Redis connections, HTTP connections?
|
* Connection pool pressure. New DB connections, Redis connections, HTTP connections?
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 8: Observability & Debuggability Review
|
### Section 8: Observability & Debuggability Review
|
||||||
@@ -572,7 +572,7 @@ Evaluate:
|
|||||||
|
|
||||||
**EXPANSION and SELECTIVE EXPANSION addition:**
|
**EXPANSION and SELECTIVE EXPANSION addition:**
|
||||||
* What observability would make this feature a joy to operate? (For SELECTIVE EXPANSION, include observability for any accepted cherry-picks.)
|
* What observability would make this feature a joy to operate? (For SELECTIVE EXPANSION, include observability for any accepted cherry-picks.)
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 9: Deployment & Rollout Review
|
### Section 9: Deployment & Rollout Review
|
||||||
@@ -588,7 +588,7 @@ Evaluate:
|
|||||||
|
|
||||||
**EXPANSION and SELECTIVE EXPANSION addition:**
|
**EXPANSION and SELECTIVE EXPANSION addition:**
|
||||||
* What deploy infrastructure would make shipping this feature routine? (For SELECTIVE EXPANSION, assess whether accepted cherry-picks change the deployment risk profile.)
|
* What deploy infrastructure would make shipping this feature routine? (For SELECTIVE EXPANSION, assess whether accepted cherry-picks change the deployment risk profile.)
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 10: Long-Term Trajectory Review
|
### Section 10: Long-Term Trajectory Review
|
||||||
@@ -604,7 +604,7 @@ Evaluate:
|
|||||||
* What comes after this ships? Phase 2? Phase 3? Does the architecture support that trajectory?
|
* What comes after this ships? Phase 2? Phase 3? Does the architecture support that trajectory?
|
||||||
* Platform potential. Does this create capabilities other features can leverage?
|
* Platform potential. Does this create capabilities other features can leverage?
|
||||||
* (SELECTIVE EXPANSION only) Retrospective: Were the right cherry-picks accepted? Did any rejected expansions turn out to be load-bearing for the accepted ones?
|
* (SELECTIVE EXPANSION only) Retrospective: Were the right cherry-picks accepted? Did any rejected expansions turn out to be load-bearing for the accepted ones?
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### Section 11: Design & UX Review (skip if no UI scope detected)
|
### Section 11: Design & UX Review (skip if no UI scope detected)
|
||||||
@@ -627,7 +627,7 @@ Evaluate:
|
|||||||
Required ASCII diagram: user flow showing screens/states and transitions.
|
Required ASCII diagram: user flow showing screens/states and transitions.
|
||||||
|
|
||||||
If this plan has significant UI scope, recommend: "Consider running /plan-design-review for a deep design review of this plan before implementation."
|
If this plan has significant UI scope, recommend: "Consider running /plan-design-review for a deep design review of this plan before implementation."
|
||||||
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If no issues or fix is obvious, state what you'll do and move on — don't waste a question. Do NOT proceed until user responds.
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
|
||||||
**Reminder: Do NOT make any code changes. Review only.**
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
{{CODEX_PLAN_REVIEW}}
|
{{CODEX_PLAN_REVIEW}}
|
||||||
@@ -651,7 +651,7 @@ Follow the AskUserQuestion format from the Preamble above. Additional rules for
|
|||||||
* For each option: effort, risk, and maintenance burden in one line.
|
* For each option: effort, risk, and maintenance burden in one line.
|
||||||
* **Map the reasoning to my engineering preferences above.** One sentence connecting your recommendation to a specific preference.
|
* **Map the reasoning to my engineering preferences above.** One sentence connecting your recommendation to a specific preference.
|
||||||
* Label with issue NUMBER + option LETTER (e.g., "3A", "3B").
|
* Label with issue NUMBER + option LETTER (e.g., "3A", "3B").
|
||||||
* **Escape hatch:** If a section has no issues, say so and move on. If an issue has an obvious fix with no real alternatives, state what you'll do and move on — don't waste a question on it. Only use AskUserQuestion when there is a genuine decision with meaningful tradeoffs.
|
* **Escape hatch (tightened):** If a section has zero findings, state "No issues, moving on" and proceed. If it has findings, use AskUserQuestion for each — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Only skip AskUserQuestion when the decision is genuinely trivial (e.g., a typo fix) AND there are no meaningful alternatives. When in doubt, ask.
|
||||||
|
|
||||||
## Required Outputs
|
## Required Outputs
|
||||||
|
|
||||||
|
|||||||
+229
-15
@@ -353,6 +353,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -466,20 +694,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
@@ -1622,7 +1836,7 @@ Follow the AskUserQuestion format from the Preamble above. Additional rules for
|
|||||||
* Present 2-3 options. For each: effort to specify now, risk if deferred.
|
* Present 2-3 options. For each: effort to specify now, risk if deferred.
|
||||||
* **Map to Design Principles above.** One sentence connecting your recommendation to a specific principle.
|
* **Map to Design Principles above.** One sentence connecting your recommendation to a specific principle.
|
||||||
* Label with issue NUMBER + option LETTER (e.g., "3A", "3B").
|
* Label with issue NUMBER + option LETTER (e.g., "3A", "3B").
|
||||||
* **Escape hatch:** If a section has no issues, say so and move on. If a gap has an obvious fix, state what you'll add and move on — don't waste a question on it. Only use AskUserQuestion when there is a genuine design choice with meaningful tradeoffs.
|
* **Escape hatch (tightened):** If a section has zero findings, state "No issues, moving on" and proceed. If it has findings, use AskUserQuestion for each — a gap with an "obvious fix" is still a gap and still needs user approval before any change lands in the plan. Only skip AskUserQuestion when the fix is genuinely trivial AND there are no meaningful design alternatives. When in doubt, ask.
|
||||||
* **NEVER use AskUserQuestion to ask which variant the user prefers.** Always create a comparison board first (`$D compare --serve`) and open it in the browser. The board has rating controls, comments, remix/regenerate buttons, and structured feedback output. Use AskUserQuestion ONLY to notify the user the board is open and wait for them to finish — not to present variants inline and ask "which do you prefer?" That is a degraded experience.
|
* **NEVER use AskUserQuestion to ask which variant the user prefers.** Always create a comparison board first (`$D compare --serve`) and open it in the browser. The board has rating controls, comments, remix/regenerate buttons, and structured feedback output. Use AskUserQuestion ONLY to notify the user the board is open and wait for them to finish — not to present variants inline and ask "which do you prefer?" That is a degraded experience.
|
||||||
|
|
||||||
## Required Outputs
|
## Required Outputs
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ Follow the AskUserQuestion format from the Preamble above. Additional rules for
|
|||||||
* Present 2-3 options. For each: effort to specify now, risk if deferred.
|
* Present 2-3 options. For each: effort to specify now, risk if deferred.
|
||||||
* **Map to Design Principles above.** One sentence connecting your recommendation to a specific principle.
|
* **Map to Design Principles above.** One sentence connecting your recommendation to a specific principle.
|
||||||
* Label with issue NUMBER + option LETTER (e.g., "3A", "3B").
|
* Label with issue NUMBER + option LETTER (e.g., "3A", "3B").
|
||||||
* **Escape hatch:** If a section has no issues, say so and move on. If a gap has an obvious fix, state what you'll add and move on — don't waste a question on it. Only use AskUserQuestion when there is a genuine design choice with meaningful tradeoffs.
|
* **Escape hatch (tightened):** If a section has zero findings, state "No issues, moving on" and proceed. If it has findings, use AskUserQuestion for each — a gap with an "obvious fix" is still a gap and still needs user approval before any change lands in the plan. Only skip AskUserQuestion when the fix is genuinely trivial AND there are no meaningful design alternatives. When in doubt, ask.
|
||||||
* **NEVER use AskUserQuestion to ask which variant the user prefers.** Always create a comparison board first (`$D compare --serve`) and open it in the browser. The board has rating controls, comments, remix/regenerate buttons, and structured feedback output. Use AskUserQuestion ONLY to notify the user the board is open and wait for them to finish — not to present variants inline and ask "which do you prefer?" That is a degraded experience.
|
* **NEVER use AskUserQuestion to ask which variant the user prefers.** Always create a comparison board first (`$D compare --serve`) and open it in the browser. The board has rating controls, comments, remix/regenerate buttons, and structured feedback output. Use AskUserQuestion ONLY to notify the user the board is open and wait for them to finish — not to present variants inline and ask "which do you prefer?" That is a degraded experience.
|
||||||
|
|
||||||
## Required Outputs
|
## Required Outputs
|
||||||
|
|||||||
+233
-16
@@ -357,6 +357,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -470,20 +698,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
@@ -1792,8 +2006,11 @@ DX reviews:
|
|||||||
* **Map to DX First Principles above.** One sentence connecting your recommendation
|
* **Map to DX First Principles above.** One sentence connecting your recommendation
|
||||||
to a specific principle (e.g., "This violates 'zero friction at T0' because
|
to a specific principle (e.g., "This violates 'zero friction at T0' because
|
||||||
[persona] needs 3 extra config steps before their first API call").
|
[persona] needs 3 extra config steps before their first API call").
|
||||||
* **Escape hatch:** If a section has no issues, say so and move on. If a gap has an
|
* **Escape hatch (tightened):** If a section has zero findings, state "No issues,
|
||||||
obvious fix, state what you'll add and move on, don't waste a question.
|
moving on" and proceed. If it has findings, use AskUserQuestion for each — a
|
||||||
|
gap with an "obvious fix" is still a gap and still needs user approval before
|
||||||
|
any change lands in the plan. Only skip AskUserQuestion when the fix is
|
||||||
|
genuinely trivial AND there are no meaningful DX alternatives. When in doubt, ask.
|
||||||
* Assume the user hasn't looked at this window in 20 minutes. Re-ground every question.
|
* Assume the user hasn't looked at this window in 20 minutes. Re-ground every question.
|
||||||
|
|
||||||
## Required Outputs
|
## Required Outputs
|
||||||
|
|||||||
@@ -666,8 +666,11 @@ DX reviews:
|
|||||||
* **Map to DX First Principles above.** One sentence connecting your recommendation
|
* **Map to DX First Principles above.** One sentence connecting your recommendation
|
||||||
to a specific principle (e.g., "This violates 'zero friction at T0' because
|
to a specific principle (e.g., "This violates 'zero friction at T0' because
|
||||||
[persona] needs 3 extra config steps before their first API call").
|
[persona] needs 3 extra config steps before their first API call").
|
||||||
* **Escape hatch:** If a section has no issues, say so and move on. If a gap has an
|
* **Escape hatch (tightened):** If a section has zero findings, state "No issues,
|
||||||
obvious fix, state what you'll add and move on, don't waste a question.
|
moving on" and proceed. If it has findings, use AskUserQuestion for each — a
|
||||||
|
gap with an "obvious fix" is still a gap and still needs user approval before
|
||||||
|
any change lands in the plan. Only skip AskUserQuestion when the fix is
|
||||||
|
genuinely trivial AND there are no meaningful DX alternatives. When in doubt, ask.
|
||||||
* Assume the user hasn't looked at this window in 20 minutes. Re-ground every question.
|
* Assume the user hasn't looked at this window in 20 minutes. Re-ground every question.
|
||||||
|
|
||||||
## Required Outputs
|
## Required Outputs
|
||||||
|
|||||||
+229
-15
@@ -355,6 +355,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -468,20 +696,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
@@ -1407,7 +1621,7 @@ Follow the AskUserQuestion format from the Preamble above. Additional rules for
|
|||||||
* **Map the reasoning to my engineering preferences above.** One sentence connecting your recommendation to a specific preference (DRY, explicit > clever, minimal diff, etc.).
|
* **Map the reasoning to my engineering preferences above.** One sentence connecting your recommendation to a specific preference (DRY, explicit > clever, minimal diff, etc.).
|
||||||
* Label with issue NUMBER + option LETTER (e.g., "3A", "3B").
|
* Label with issue NUMBER + option LETTER (e.g., "3A", "3B").
|
||||||
* **Coverage vs kind:** for every per-issue AskUserQuestion you raise in this review, decide whether the options differ in coverage or in kind. If coverage (e.g., more tests vs fewer, complete error handling vs happy-path-only, full edge-case coverage vs shortcut), include `Completeness: N/10` on each option. If kind (e.g., architectural choice between two different systems, posture-over-posture, A/B/C where each is a different kind of thing), skip the score and add one line: `Note: options differ in kind, not coverage — no completeness score.` Do NOT fabricate scores on kind-differentiated questions — filler scores are worse than no score.
|
* **Coverage vs kind:** for every per-issue AskUserQuestion you raise in this review, decide whether the options differ in coverage or in kind. If coverage (e.g., more tests vs fewer, complete error handling vs happy-path-only, full edge-case coverage vs shortcut), include `Completeness: N/10` on each option. If kind (e.g., architectural choice between two different systems, posture-over-posture, A/B/C where each is a different kind of thing), skip the score and add one line: `Note: options differ in kind, not coverage — no completeness score.` Do NOT fabricate scores on kind-differentiated questions — filler scores are worse than no score.
|
||||||
* **Escape hatch:** If a section has no issues, say so and move on. If an issue has an obvious fix with no real alternatives, state what you'll do and move on — don't waste a question on it. Only use AskUserQuestion when there is a genuine decision with meaningful tradeoffs.
|
* **Escape hatch (tightened):** If a section has zero findings, state "No issues, moving on" and proceed. If it has findings, use AskUserQuestion for each — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Only skip AskUserQuestion when the decision is genuinely trivial (e.g., a typo fix) AND there are no meaningful alternatives. When in doubt, ask.
|
||||||
|
|
||||||
## Required outputs
|
## Required outputs
|
||||||
|
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ Follow the AskUserQuestion format from the Preamble above. Additional rules for
|
|||||||
* **Map the reasoning to my engineering preferences above.** One sentence connecting your recommendation to a specific preference (DRY, explicit > clever, minimal diff, etc.).
|
* **Map the reasoning to my engineering preferences above.** One sentence connecting your recommendation to a specific preference (DRY, explicit > clever, minimal diff, etc.).
|
||||||
* Label with issue NUMBER + option LETTER (e.g., "3A", "3B").
|
* Label with issue NUMBER + option LETTER (e.g., "3A", "3B").
|
||||||
* **Coverage vs kind:** for every per-issue AskUserQuestion you raise in this review, decide whether the options differ in coverage or in kind. If coverage (e.g., more tests vs fewer, complete error handling vs happy-path-only, full edge-case coverage vs shortcut), include `Completeness: N/10` on each option. If kind (e.g., architectural choice between two different systems, posture-over-posture, A/B/C where each is a different kind of thing), skip the score and add one line: `Note: options differ in kind, not coverage — no completeness score.` Do NOT fabricate scores on kind-differentiated questions — filler scores are worse than no score.
|
* **Coverage vs kind:** for every per-issue AskUserQuestion you raise in this review, decide whether the options differ in coverage or in kind. If coverage (e.g., more tests vs fewer, complete error handling vs happy-path-only, full edge-case coverage vs shortcut), include `Completeness: N/10` on each option. If kind (e.g., architectural choice between two different systems, posture-over-posture, A/B/C where each is a different kind of thing), skip the score and add one line: `Note: options differ in kind, not coverage — no completeness score.` Do NOT fabricate scores on kind-differentiated questions — filler scores are worse than no score.
|
||||||
* **Escape hatch:** If a section has no issues, say so and move on. If an issue has an obvious fix with no real alternatives, state what you'll do and move on — don't waste a question on it. Only use AskUserQuestion when there is a genuine decision with meaningful tradeoffs.
|
* **Escape hatch (tightened):** If a section has zero findings, state "No issues, moving on" and proceed. If it has findings, use AskUserQuestion for each — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Only skip AskUserQuestion when the decision is genuinely trivial (e.g., a typo fix) AND there are no meaningful alternatives. When in doubt, ask.
|
||||||
|
|
||||||
## Required outputs
|
## Required outputs
|
||||||
|
|
||||||
|
|||||||
+228
-14
@@ -363,6 +363,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -476,20 +704,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -351,6 +351,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -464,20 +692,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -357,6 +357,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -470,20 +698,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -350,6 +350,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -463,20 +691,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -354,6 +354,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -467,20 +695,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* Preflight for the overlay efficacy harness.
|
||||||
|
*
|
||||||
|
* Confirms, before any paid eval runs:
|
||||||
|
* 1. `@anthropic-ai/claude-agent-sdk` loads and `query()` is the expected shape.
|
||||||
|
* 2. `claude-opus-4-7` is a live API model ID (not a Claude Code alias).
|
||||||
|
* 3. The SDK event stream contains the types we assume (system init, assistant,
|
||||||
|
* result) with the fields we destructure.
|
||||||
|
* 4. `scripts/resolvers/model-overlay.ts` resolves `{{INHERIT:claude}}` against
|
||||||
|
* `opus-4-7.md` AND the resolved text contains the "Fan out explicitly" nudge.
|
||||||
|
* 5. A local `claude` binary exists at `which claude` so binary pinning is possible.
|
||||||
|
*
|
||||||
|
* Run: bun run scripts/preflight-agent-sdk.ts
|
||||||
|
*
|
||||||
|
* Exit 0 on success. Exit non-zero with a clear message on any failure. No
|
||||||
|
* side effects beyond stdout and a ~15 token API call.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { query, type SDKMessage } from '@anthropic-ai/claude-agent-sdk';
|
||||||
|
import { readOverlay } from './resolvers/model-overlay';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const failures: string[] = [];
|
||||||
|
const pass = (msg: string) => console.log(` ok ${msg}`);
|
||||||
|
const fail = (msg: string) => {
|
||||||
|
console.log(` FAIL ${msg}`);
|
||||||
|
failures.push(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Overlay resolver + fanout nudge text
|
||||||
|
console.log('1. Overlay resolver');
|
||||||
|
const resolved = readOverlay('opus-4-7');
|
||||||
|
if (!resolved) {
|
||||||
|
fail("readOverlay('opus-4-7') returned empty");
|
||||||
|
} else {
|
||||||
|
pass(`resolved overlay length: ${resolved.length} chars`);
|
||||||
|
if (resolved.includes('{{INHERIT:')) {
|
||||||
|
fail('resolved overlay still contains {{INHERIT:...}} directive');
|
||||||
|
} else {
|
||||||
|
pass('no unresolved INHERIT directives');
|
||||||
|
}
|
||||||
|
if (!/Fan out explicitly/i.test(resolved)) {
|
||||||
|
fail('resolved overlay does not contain "Fan out explicitly" text');
|
||||||
|
} else {
|
||||||
|
pass('fanout nudge text present in resolved overlay');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Local claude binary exists
|
||||||
|
console.log('\n2. Binary pinning');
|
||||||
|
let claudePath: string | null = null;
|
||||||
|
try {
|
||||||
|
claudePath = execSync('which claude', { encoding: 'utf-8' }).trim();
|
||||||
|
pass(`local claude binary: ${claudePath}`);
|
||||||
|
} catch {
|
||||||
|
fail('`which claude` failed — cannot pin binary');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. SDK query end-to-end
|
||||||
|
console.log('\n3. SDK query end-to-end');
|
||||||
|
if (!process.env.ANTHROPIC_API_KEY) {
|
||||||
|
console.log(' skip ANTHROPIC_API_KEY not set — cannot test live query');
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const events: SDKMessage[] = [];
|
||||||
|
const q = query({
|
||||||
|
prompt: 'say pong',
|
||||||
|
options: {
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
systemPrompt: '',
|
||||||
|
tools: [],
|
||||||
|
permissionMode: 'bypassPermissions',
|
||||||
|
allowDangerouslySkipPermissions: true,
|
||||||
|
settingSources: [],
|
||||||
|
maxTurns: 1,
|
||||||
|
pathToClaudeCodeExecutable: claudePath ?? undefined,
|
||||||
|
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for await (const ev of q) events.push(ev);
|
||||||
|
pass(`received ${events.length} events`);
|
||||||
|
|
||||||
|
const init = events.find(
|
||||||
|
(e) => e.type === 'system' && (e as { subtype?: string }).subtype === 'init',
|
||||||
|
) as { claude_code_version?: string; model?: string } | undefined;
|
||||||
|
if (!init) {
|
||||||
|
fail('no system/init event received');
|
||||||
|
} else {
|
||||||
|
pass(`system init: claude_code_version=${init.claude_code_version}, model=${init.model}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const assistantEvents = events.filter((e) => e.type === 'assistant');
|
||||||
|
if (assistantEvents.length === 0) {
|
||||||
|
fail('no assistant events received — model ID may be rejected');
|
||||||
|
} else {
|
||||||
|
pass(`received ${assistantEvents.length} assistant event(s)`);
|
||||||
|
const first = assistantEvents[0] as { message?: { content?: unknown[] } };
|
||||||
|
const content = first.message?.content;
|
||||||
|
if (!Array.isArray(content)) {
|
||||||
|
fail('first assistant event has no content[] array');
|
||||||
|
} else {
|
||||||
|
pass(`first assistant content[] has ${content.length} block(s)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = events.find((e) => e.type === 'result') as
|
||||||
|
| { subtype?: string; total_cost_usd?: number; num_turns?: number }
|
||||||
|
| undefined;
|
||||||
|
if (!result) {
|
||||||
|
fail('no result event received');
|
||||||
|
} else {
|
||||||
|
pass(
|
||||||
|
`result: subtype=${result.subtype}, cost=$${result.total_cost_usd?.toFixed(4)}, turns=${result.num_turns}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
fail(`SDK query threw: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
if (failures.length > 0) {
|
||||||
|
console.log(`PREFLIGHT FAILED: ${failures.length} check(s) failed`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('PREFLIGHT OK');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -24,7 +24,7 @@ const OVERLAY_DIR = path.resolve(import.meta.dir, '../../model-overlays');
|
|||||||
|
|
||||||
const INHERIT_RE = /^\s*\{\{INHERIT:([a-z0-9-]+(?:\.[0-9]+)*)\}\}\s*\n/;
|
const INHERIT_RE = /^\s*\{\{INHERIT:([a-z0-9-]+(?:\.[0-9]+)*)\}\}\s*\n/;
|
||||||
|
|
||||||
function readOverlay(model: string, seen: Set<string> = new Set()): string {
|
export function readOverlay(model: string, seen: Set<string> = new Set()): string {
|
||||||
if (seen.has(model)) return ''; // cycle guard
|
if (seen.has(model)) return ''; // cycle guard
|
||||||
seen.add(model);
|
seen.add(model);
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ import { generateWritingStyleMigration } from './preamble/generate-writing-style
|
|||||||
// Host-specific instructions
|
// Host-specific instructions
|
||||||
import { generateBrainHealthInstruction } from './preamble/generate-brain-health-instruction';
|
import { generateBrainHealthInstruction } from './preamble/generate-brain-health-instruction';
|
||||||
|
|
||||||
|
// GBrain cross-machine sync (runs at skill start; end-side handled in completion-status)
|
||||||
|
import { generateBrainSyncBlock } from './preamble/generate-brain-sync-block';
|
||||||
|
|
||||||
// Behavioral / voice
|
// Behavioral / voice
|
||||||
import { generateVoiceDirective } from './preamble/generate-voice-directive';
|
import { generateVoiceDirective } from './preamble/generate-voice-directive';
|
||||||
|
|
||||||
@@ -84,11 +87,16 @@ export function generatePreamble(ctx: TemplateContext): string {
|
|||||||
generateVendoringDeprecation(ctx),
|
generateVendoringDeprecation(ctx),
|
||||||
generateSpawnedSessionCheck(),
|
generateSpawnedSessionCheck(),
|
||||||
generateBrainHealthInstruction(ctx),
|
generateBrainHealthInstruction(ctx),
|
||||||
|
// AskUserQuestion Format renders BEFORE the model overlay so the pacing rule
|
||||||
|
// is the ambient default; the overlay's behavioral nudges land as subordinate
|
||||||
|
// patches. Opus 4.7 reads top-to-bottom and absorbs the first pacing directive
|
||||||
|
// it hits; reversing this order regresses plan-review cadence (v1.6.4.0 bug).
|
||||||
|
...(tier >= 2 ? [generateAskUserFormat(ctx)] : []),
|
||||||
|
generateBrainSyncBlock(ctx),
|
||||||
generateModelOverlay(ctx),
|
generateModelOverlay(ctx),
|
||||||
generateVoiceDirective(tier),
|
generateVoiceDirective(tier),
|
||||||
...(tier >= 2 ? [
|
...(tier >= 2 ? [
|
||||||
generateContextRecovery(ctx),
|
generateContextRecovery(ctx),
|
||||||
generateAskUserFormat(ctx),
|
|
||||||
generateWritingStyle(ctx),
|
generateWritingStyle(ctx),
|
||||||
generateCompletenessSection(),
|
generateCompletenessSection(),
|
||||||
generateConfusionProtocol(),
|
generateConfusionProtocol(),
|
||||||
|
|||||||
@@ -3,16 +3,130 @@ import type { TemplateContext } from '../types';
|
|||||||
export function generateAskUserFormat(_ctx: TemplateContext): string {
|
export function generateAskUserFormat(_ctx: TemplateContext): string {
|
||||||
return `## AskUserQuestion Format
|
return `## AskUserQuestion Format
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the \`_BRANCH\` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
### Required shape
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with \`RECOMMENDATION: Choose [X] because [one-line reason]\` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with \`Completeness: N/10\` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip \`Completeness: N/10\` entirely and write one line: \`Note: options differ in kind, not coverage — no completeness score.\` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: \`A) ... B) ... C) ...\` — when an option involves effort, show both scales: \`(human: ~X / CC: ~Y)\`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.`;
|
\`\`\`
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is \`D1\`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., \`/plan-ceo-review\` running \`/office-hours\` inline) starts its own
|
||||||
|
D1; label as \`D1 (office-hours)\` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
\`_BRANCH\` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** \`Recommendation: <choice> because <one-line
|
||||||
|
reason>\` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The \`(recommended)\` label on the
|
||||||
|
option is REQUIRED — \`scripts/resolvers/question-tuning.ts\` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each \`Completeness: N/10\` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
\`Note: options differ in kind, not coverage — no completeness score.\`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** \`✅ Simple\` is not a pro. \`✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser\` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet \`✅ No cons — this is a
|
||||||
|
hard-stop choice\` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: \`Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way\`. The \`(recommended)\` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
\`— this is a taste call\` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: \`(human: ~2 days / CC: ~15 min)\`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled \`Question:\` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the \`options\` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* gbrain-sync preamble block.
|
||||||
|
*
|
||||||
|
* Emits bash that runs at every skill invocation:
|
||||||
|
* 1. If ~/.gstack-brain-remote.txt exists AND ~/.gstack/.git is missing,
|
||||||
|
* surface a restore-available hint (does NOT auto-run restore).
|
||||||
|
* 2. If sync is on, run `gstack-brain-sync --once` (drain + push).
|
||||||
|
* 3. On first skill of the day (24h cache via .brain-last-pull):
|
||||||
|
* `git fetch` + ff-only merge (JSONL merge driver handles conflicts).
|
||||||
|
* 4. Emit a `BRAIN_SYNC:` status line so every skill surfaces health.
|
||||||
|
*
|
||||||
|
* Also emits prose instructions for the host LLM to fire a one-time privacy
|
||||||
|
* stop-gate via AskUserQuestion when gbrain_sync_mode is unset and gbrain
|
||||||
|
* is available on the host.
|
||||||
|
*
|
||||||
|
* Block emitted across all tiers. Internal bash short-circuits when feature
|
||||||
|
* is disabled; cost is <5ms.
|
||||||
|
*
|
||||||
|
* Skill-end sync is handled by the completion-status generator via a call
|
||||||
|
* to `gstack-brain-sync --discover-new` + `--once`.
|
||||||
|
*/
|
||||||
|
import type { TemplateContext } from '../types';
|
||||||
|
|
||||||
|
export function generateBrainSyncBlock(ctx: TemplateContext): string {
|
||||||
|
const isBrainHost = ctx.host === 'gbrain' || ctx.host === 'hermes';
|
||||||
|
return `## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="\${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="${ctx.paths.binDir}/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="${ctx.paths.binDir}/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
${isBrainHost ? `If the bash output shows \`BRAIN_SYNC: brain repo detected\`, the user copied their remote URL file to this machine but hasn't restored yet. Offer to run \`gstack-brain-restore\` via AskUserQuestion. If the user agrees, run the command; otherwise continue without sync.` : ''}
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows \`BRAIN_SYNC: off\` AND the config value
|
||||||
|
\`gbrain_sync_mode_prompted\` is \`false\` AND gbrain is detected on this host
|
||||||
|
(either \`gbrain doctor --fast --json\` succeeds or the \`gbrain\` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
If A or B was chosen AND \`~/.gstack/.git\` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs \`gstack-brain-init\`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
"${ctx.paths.binDir}/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"${ctx.paths.binDir}/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -347,6 +347,105 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
|
|||||||
+228
-14
@@ -353,6 +353,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -466,20 +694,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -355,6 +355,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -468,20 +696,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
@@ -0,0 +1,725 @@
|
|||||||
|
/**
|
||||||
|
* Unit tests for test/helpers/agent-sdk-runner.ts.
|
||||||
|
*
|
||||||
|
* Runs in free `bun test` (no API calls). Uses a stub QueryProvider to
|
||||||
|
* simulate SDK event streams — happy path, rate-limit retries across all
|
||||||
|
* three shapes, persistent failure, non-retryable error, options
|
||||||
|
* propagation, concurrency cap.
|
||||||
|
*
|
||||||
|
* Also covers validateFixtures() rejections.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, expect } from 'bun:test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import type {
|
||||||
|
SDKMessage,
|
||||||
|
Options,
|
||||||
|
Query,
|
||||||
|
} from '@anthropic-ai/claude-agent-sdk';
|
||||||
|
import {
|
||||||
|
runAgentSdkTest,
|
||||||
|
toSkillTestResult,
|
||||||
|
firstTurnParallelism,
|
||||||
|
isRateLimitThrown,
|
||||||
|
isRateLimitResult,
|
||||||
|
isRateLimitEvent,
|
||||||
|
RateLimitExhaustedError,
|
||||||
|
__resetSemaphoreForTests,
|
||||||
|
type QueryProvider,
|
||||||
|
type AgentSdkResult,
|
||||||
|
} from '../test/helpers/agent-sdk-runner';
|
||||||
|
import {
|
||||||
|
validateFixtures,
|
||||||
|
fanoutPass,
|
||||||
|
type OverlayFixture,
|
||||||
|
} from '../test/fixtures/overlay-nudges';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Stub SDK event builders
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let uuidCounter = 0;
|
||||||
|
function uuid(): string {
|
||||||
|
return `00000000-0000-0000-0000-${String(++uuidCounter).padStart(12, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function systemInit(model = 'claude-opus-4-7', version = '2.1.117'): SDKMessage {
|
||||||
|
return {
|
||||||
|
type: 'system',
|
||||||
|
subtype: 'init',
|
||||||
|
apiKeySource: 'user',
|
||||||
|
claude_code_version: version,
|
||||||
|
cwd: '/tmp/x',
|
||||||
|
tools: ['Read'],
|
||||||
|
mcp_servers: [],
|
||||||
|
model,
|
||||||
|
permissionMode: 'bypassPermissions',
|
||||||
|
slash_commands: [],
|
||||||
|
output_style: 'default',
|
||||||
|
skills: [],
|
||||||
|
plugins: [],
|
||||||
|
uuid: uuid(),
|
||||||
|
session_id: 'test-session',
|
||||||
|
} as unknown as SDKMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assistantTurn(
|
||||||
|
blocks: Array<{ type: 'text'; text: string } | { type: 'tool_use'; name: string; input: unknown }>,
|
||||||
|
): SDKMessage {
|
||||||
|
return {
|
||||||
|
type: 'assistant',
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
uuid: uuid(),
|
||||||
|
session_id: 'test-session',
|
||||||
|
message: {
|
||||||
|
id: 'msg_' + uuid(),
|
||||||
|
type: 'message',
|
||||||
|
role: 'assistant',
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
content: blocks.map((b) => ({ ...b })),
|
||||||
|
stop_reason: 'end_turn',
|
||||||
|
stop_sequence: null,
|
||||||
|
usage: {
|
||||||
|
input_tokens: 10,
|
||||||
|
output_tokens: 20,
|
||||||
|
cache_creation_input_tokens: 0,
|
||||||
|
cache_read_input_tokens: 0,
|
||||||
|
service_tier: 'standard',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as SDKMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultSuccess(cost = 0.01, turns = 1): SDKMessage {
|
||||||
|
return {
|
||||||
|
type: 'result',
|
||||||
|
subtype: 'success',
|
||||||
|
duration_ms: 100,
|
||||||
|
duration_api_ms: 50,
|
||||||
|
is_error: false,
|
||||||
|
num_turns: turns,
|
||||||
|
result: 'done',
|
||||||
|
stop_reason: 'end_turn',
|
||||||
|
total_cost_usd: cost,
|
||||||
|
usage: {
|
||||||
|
input_tokens: 10,
|
||||||
|
output_tokens: 20,
|
||||||
|
cache_creation_input_tokens: 0,
|
||||||
|
cache_read_input_tokens: 0,
|
||||||
|
server_tool_use: {},
|
||||||
|
service_tier: 'standard',
|
||||||
|
},
|
||||||
|
modelUsage: {},
|
||||||
|
permission_denials: [],
|
||||||
|
uuid: uuid(),
|
||||||
|
session_id: 'test-session',
|
||||||
|
} as unknown as SDKMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultRateLimit(): SDKMessage {
|
||||||
|
return {
|
||||||
|
type: 'result',
|
||||||
|
subtype: 'error_during_execution',
|
||||||
|
duration_ms: 100,
|
||||||
|
duration_api_ms: 50,
|
||||||
|
is_error: true,
|
||||||
|
num_turns: 0,
|
||||||
|
stop_reason: null,
|
||||||
|
total_cost_usd: 0,
|
||||||
|
usage: {
|
||||||
|
input_tokens: 0,
|
||||||
|
output_tokens: 0,
|
||||||
|
cache_creation_input_tokens: 0,
|
||||||
|
cache_read_input_tokens: 0,
|
||||||
|
server_tool_use: {},
|
||||||
|
service_tier: 'standard',
|
||||||
|
},
|
||||||
|
modelUsage: {},
|
||||||
|
permission_denials: [],
|
||||||
|
errors: ['rate limit exceeded (429)'],
|
||||||
|
uuid: uuid(),
|
||||||
|
session_id: 'test-session',
|
||||||
|
} as unknown as SDKMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rateLimitEvent(): SDKMessage {
|
||||||
|
return {
|
||||||
|
type: 'rate_limit_event',
|
||||||
|
rate_limit_info: {
|
||||||
|
status: 'rejected',
|
||||||
|
rateLimitType: 'five_hour',
|
||||||
|
},
|
||||||
|
uuid: uuid(),
|
||||||
|
session_id: 'test-session',
|
||||||
|
} as unknown as SDKMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Stub query provider
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface StubConfig {
|
||||||
|
/** One event stream per call. Exhausted calls throw. */
|
||||||
|
streams: SDKMessage[][];
|
||||||
|
/** Throw this error on the Nth call (0-indexed). */
|
||||||
|
throwAt?: number;
|
||||||
|
throwError?: unknown;
|
||||||
|
/** Track calls for assertions. */
|
||||||
|
calls: Array<{ prompt: string; options: Options | undefined; startedAt: number; endedAt?: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeStubProvider(config: StubConfig): QueryProvider {
|
||||||
|
let callIdx = -1;
|
||||||
|
const provider: QueryProvider = (params) => {
|
||||||
|
callIdx++;
|
||||||
|
const idx = callIdx;
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const prompt = typeof params.prompt === 'string' ? params.prompt : '<iterable>';
|
||||||
|
config.calls.push({ prompt, options: params.options, startedAt });
|
||||||
|
|
||||||
|
if (config.throwAt !== undefined && idx === config.throwAt) {
|
||||||
|
const err = config.throwError ?? new Error('stub throw');
|
||||||
|
// Return an async generator that throws on first next().
|
||||||
|
const gen = (async function* (): AsyncGenerator<SDKMessage, void> {
|
||||||
|
throw err;
|
||||||
|
})();
|
||||||
|
return gen as unknown as Query;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = config.streams[idx];
|
||||||
|
if (!stream) {
|
||||||
|
const gen = (async function* (): AsyncGenerator<SDKMessage, void> {
|
||||||
|
throw new Error(`stub has no stream for call ${idx}`);
|
||||||
|
})();
|
||||||
|
return gen as unknown as Query;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gen = (async function* (): AsyncGenerator<SDKMessage, void> {
|
||||||
|
try {
|
||||||
|
for (const ev of stream) {
|
||||||
|
yield ev;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
config.calls[idx]!.endedAt = Date.now();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return gen as unknown as Query;
|
||||||
|
};
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE_OPTS = {
|
||||||
|
systemPrompt: '',
|
||||||
|
userPrompt: 'test prompt',
|
||||||
|
workingDirectory: '/tmp/test-dir',
|
||||||
|
maxRetries: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset semaphore before each test that depends on fresh capacity.
|
||||||
|
function freshSem(cap = 10): void {
|
||||||
|
__resetSemaphoreForTests(cap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Happy path
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('runAgentSdkTest — happy path', () => {
|
||||||
|
test('collects events, assistantTurns, toolCalls, and result fields', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [
|
||||||
|
[
|
||||||
|
systemInit(),
|
||||||
|
assistantTurn([
|
||||||
|
{ type: 'text', text: 'reading files' },
|
||||||
|
{ type: 'tool_use', name: 'Read', input: { path: 'a.txt' } },
|
||||||
|
{ type: 'tool_use', name: 'Read', input: { path: 'b.txt' } },
|
||||||
|
]),
|
||||||
|
assistantTurn([{ type: 'text', text: 'done' }]),
|
||||||
|
resultSuccess(0.05, 2),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
const result = await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.events.length).toBe(4);
|
||||||
|
expect(result.assistantTurns.length).toBe(2);
|
||||||
|
expect(result.toolCalls.length).toBe(2);
|
||||||
|
expect(result.toolCalls[0]!.tool).toBe('Read');
|
||||||
|
expect(result.output).toContain('reading files');
|
||||||
|
expect(result.output).toContain('done');
|
||||||
|
expect(result.exitReason).toBe('success');
|
||||||
|
expect(result.turnsUsed).toBe(2);
|
||||||
|
expect(result.costUsd).toBe(0.05);
|
||||||
|
expect(result.sdkClaudeCodeVersion).toBe('2.1.117');
|
||||||
|
expect(result.model).toBe('claude-opus-4-7');
|
||||||
|
expect(result.firstResponseMs).toBeGreaterThanOrEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('first-turn parallelism: 3 tool_use blocks in first assistant turn', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [
|
||||||
|
[
|
||||||
|
systemInit(),
|
||||||
|
assistantTurn([
|
||||||
|
{ type: 'tool_use', name: 'Read', input: { path: 'a' } },
|
||||||
|
{ type: 'tool_use', name: 'Read', input: { path: 'b' } },
|
||||||
|
{ type: 'tool_use', name: 'Read', input: { path: 'c' } },
|
||||||
|
]),
|
||||||
|
resultSuccess(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
const result = await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
});
|
||||||
|
expect(firstTurnParallelism(result.assistantTurns[0])).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('first-turn parallelism: 0 when first turn is text-only', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [
|
||||||
|
[
|
||||||
|
systemInit(),
|
||||||
|
assistantTurn([{ type: 'text', text: 'thinking' }]),
|
||||||
|
resultSuccess(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
const result = await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
});
|
||||||
|
expect(firstTurnParallelism(result.assistantTurns[0])).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('first-turn parallelism: 0 when no first turn', () => {
|
||||||
|
expect(firstTurnParallelism(undefined)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Options propagation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('runAgentSdkTest — options propagation', () => {
|
||||||
|
test('systemPrompt, model, cwd, allowedTools, disallowedTools, permissionMode, settingSources, env, pathToClaudeCodeExecutable reach query()', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [[systemInit(), assistantTurn([{ type: 'text', text: 'ok' }]), resultSuccess()]],
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
await runAgentSdkTest({
|
||||||
|
systemPrompt: 'you are a test overlay',
|
||||||
|
userPrompt: 'go',
|
||||||
|
workingDirectory: '/tmp/spec-dir',
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
maxTurns: 7,
|
||||||
|
allowedTools: ['Read', 'Glob'],
|
||||||
|
disallowedTools: ['Bash', 'Write'],
|
||||||
|
permissionMode: 'bypassPermissions',
|
||||||
|
settingSources: [],
|
||||||
|
env: { ANTHROPIC_API_KEY: 'fake' },
|
||||||
|
pathToClaudeCodeExecutable: '/fake/path/claude',
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
});
|
||||||
|
|
||||||
|
const opts = stub.calls[0]!.options!;
|
||||||
|
expect(opts.systemPrompt).toBe('you are a test overlay');
|
||||||
|
expect(opts.model).toBe('claude-opus-4-7');
|
||||||
|
expect(opts.cwd).toBe('/tmp/spec-dir');
|
||||||
|
expect(opts.maxTurns).toBe(7);
|
||||||
|
expect(opts.tools).toEqual(['Read', 'Glob']);
|
||||||
|
expect(opts.allowedTools).toEqual(['Read', 'Glob']);
|
||||||
|
expect(opts.disallowedTools).toEqual(['Bash', 'Write']);
|
||||||
|
expect(opts.permissionMode).toBe('bypassPermissions');
|
||||||
|
expect(opts.allowDangerouslySkipPermissions).toBe(true);
|
||||||
|
expect(opts.settingSources).toEqual([]);
|
||||||
|
expect(opts.env).toEqual({ ANTHROPIC_API_KEY: 'fake' });
|
||||||
|
expect(opts.pathToClaudeCodeExecutable).toBe('/fake/path/claude');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty systemPrompt means no systemPrompt option passed', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [[systemInit(), assistantTurn([{ type: 'text', text: 'ok' }]), resultSuccess()]],
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
});
|
||||||
|
// systemPrompt is undefined when empty string passed (so SDK uses no override)
|
||||||
|
expect(stub.calls[0]!.options!.systemPrompt).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Rate-limit retry (three shapes)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('runAgentSdkTest — rate-limit retry', () => {
|
||||||
|
test('retryable on thrown 429-shaped error, then succeeds on 2nd attempt', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [
|
||||||
|
// call 0: throws (handled via throwAt below)
|
||||||
|
[],
|
||||||
|
// call 1: success
|
||||||
|
[systemInit(), assistantTurn([{ type: 'text', text: 'ok' }]), resultSuccess()],
|
||||||
|
],
|
||||||
|
throwAt: 0,
|
||||||
|
throwError: Object.assign(new Error('429 too many requests'), { status: 429 }),
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
const result = await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
maxRetries: 2,
|
||||||
|
});
|
||||||
|
expect(result.exitReason).toBe('success');
|
||||||
|
expect(stub.calls.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('retryable on result-message rate-limit, then succeeds', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [
|
||||||
|
[systemInit(), resultRateLimit()],
|
||||||
|
[systemInit(), assistantTurn([{ type: 'text', text: 'ok' }]), resultSuccess()],
|
||||||
|
],
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
const result = await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
maxRetries: 2,
|
||||||
|
});
|
||||||
|
expect(result.exitReason).toBe('success');
|
||||||
|
expect(stub.calls.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('retryable on mid-stream SDKRateLimitEvent, then succeeds', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [
|
||||||
|
[systemInit(), rateLimitEvent()],
|
||||||
|
[systemInit(), assistantTurn([{ type: 'text', text: 'ok' }]), resultSuccess()],
|
||||||
|
],
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
const result = await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
maxRetries: 2,
|
||||||
|
});
|
||||||
|
expect(result.exitReason).toBe('success');
|
||||||
|
expect(stub.calls.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('onRetry callback is invoked between attempts', async () => {
|
||||||
|
freshSem();
|
||||||
|
const resets: string[] = [];
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [
|
||||||
|
[],
|
||||||
|
[systemInit(), assistantTurn([{ type: 'text', text: 'ok' }]), resultSuccess()],
|
||||||
|
],
|
||||||
|
throwAt: 0,
|
||||||
|
throwError: Object.assign(new Error('429'), { status: 429 }),
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
maxRetries: 2,
|
||||||
|
onRetry: (dir) => resets.push(dir),
|
||||||
|
});
|
||||||
|
expect(resets.length).toBe(1);
|
||||||
|
expect(resets[0]).toBe('/tmp/test-dir');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('persistent 429 throws RateLimitExhaustedError after maxRetries', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [[], [], [], []], // 4 empty streams; throw on each
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
// Every call throws:
|
||||||
|
let callCount = 0;
|
||||||
|
const alwaysThrowProvider: QueryProvider = (params) => {
|
||||||
|
callCount++;
|
||||||
|
stub.calls.push({
|
||||||
|
prompt: typeof params.prompt === 'string' ? params.prompt : '',
|
||||||
|
options: params.options,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
});
|
||||||
|
const gen = (async function* (): AsyncGenerator<SDKMessage, void> {
|
||||||
|
throw Object.assign(new Error('429 always'), { status: 429 });
|
||||||
|
})();
|
||||||
|
return gen as unknown as Query;
|
||||||
|
};
|
||||||
|
|
||||||
|
let caught: unknown = null;
|
||||||
|
try {
|
||||||
|
await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: alwaysThrowProvider,
|
||||||
|
maxRetries: 2,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
caught = err;
|
||||||
|
}
|
||||||
|
expect(caught).toBeInstanceOf(RateLimitExhaustedError);
|
||||||
|
expect((caught as RateLimitExhaustedError).attempts).toBe(3); // initial + 2 retries
|
||||||
|
expect(callCount).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-429 error is NOT retried, propagates immediately', async () => {
|
||||||
|
__resetSemaphoreForTests(10);
|
||||||
|
let callCount = 0;
|
||||||
|
const throwOnce: QueryProvider = () => {
|
||||||
|
callCount++;
|
||||||
|
const gen = (async function* (): AsyncGenerator<SDKMessage, void> {
|
||||||
|
throw new Error('generic auth failure');
|
||||||
|
})();
|
||||||
|
return gen as unknown as Query;
|
||||||
|
};
|
||||||
|
let caught: unknown = null;
|
||||||
|
try {
|
||||||
|
await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: throwOnce,
|
||||||
|
maxRetries: 3,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
caught = err;
|
||||||
|
}
|
||||||
|
expect(caught).toBeInstanceOf(Error);
|
||||||
|
expect((caught as Error).message).toBe('generic auth failure');
|
||||||
|
expect(callCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Rate-limit detectors (unit)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('rate-limit detectors', () => {
|
||||||
|
test('isRateLimitThrown matches status 429, message, name', () => {
|
||||||
|
expect(isRateLimitThrown(Object.assign(new Error('boom'), { status: 429 }))).toBe(true);
|
||||||
|
expect(isRateLimitThrown(new Error('429 Too Many Requests'))).toBe(true);
|
||||||
|
expect(isRateLimitThrown(new Error('rate-limit exceeded'))).toBe(true);
|
||||||
|
expect(isRateLimitThrown(Object.assign(new Error('x'), { name: 'RateLimitError' }))).toBe(true);
|
||||||
|
expect(isRateLimitThrown(new Error('auth failed'))).toBe(false);
|
||||||
|
expect(isRateLimitThrown(null)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isRateLimitResult matches error_during_execution with 429-shaped errors', () => {
|
||||||
|
expect(isRateLimitResult(resultRateLimit())).toBe(true);
|
||||||
|
expect(isRateLimitResult(resultSuccess())).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isRateLimitEvent matches rate_limit_event with status=rejected', () => {
|
||||||
|
expect(isRateLimitEvent(rateLimitEvent())).toBe(true);
|
||||||
|
expect(isRateLimitEvent(resultSuccess())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Semaphore concurrency cap
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('runAgentSdkTest — concurrency', () => {
|
||||||
|
test('process-level semaphore caps concurrent queries', async () => {
|
||||||
|
__resetSemaphoreForTests(2);
|
||||||
|
let inFlight = 0;
|
||||||
|
let peakInFlight = 0;
|
||||||
|
const slowStub: QueryProvider = () => {
|
||||||
|
const gen = (async function* (): AsyncGenerator<SDKMessage, void> {
|
||||||
|
inFlight++;
|
||||||
|
if (inFlight > peakInFlight) peakInFlight = inFlight;
|
||||||
|
yield systemInit();
|
||||||
|
await new Promise((r) => setTimeout(r, 30));
|
||||||
|
yield assistantTurn([{ type: 'text', text: 'ok' }]);
|
||||||
|
yield resultSuccess();
|
||||||
|
inFlight--;
|
||||||
|
})();
|
||||||
|
return gen as unknown as Query;
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Array.from({ length: 6 }, (_, i) =>
|
||||||
|
runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
userPrompt: `trial-${i}`,
|
||||||
|
queryProvider: slowStub,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(peakInFlight).toBeLessThanOrEqual(2);
|
||||||
|
expect(peakInFlight).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// toSkillTestResult shape
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('toSkillTestResult', () => {
|
||||||
|
test('produces a SkillTestResult-shaped object', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [[systemInit(), assistantTurn([{ type: 'text', text: 'hi' }]), resultSuccess(0.02, 1)]],
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
const r = await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
});
|
||||||
|
const s = toSkillTestResult(r);
|
||||||
|
expect(s.toolCalls).toBeArray();
|
||||||
|
expect(s.browseErrors).toBeArray();
|
||||||
|
expect(s.exitReason).toBe('success');
|
||||||
|
expect(s.duration).toBeNumber();
|
||||||
|
expect(s.output).toBe('hi');
|
||||||
|
expect(s.costEstimate.estimatedCost).toBe(0.02);
|
||||||
|
expect(s.costEstimate.turnsUsed).toBe(1);
|
||||||
|
expect(s.model).toBe('claude-opus-4-7');
|
||||||
|
expect(s.firstResponseMs).toBeNumber();
|
||||||
|
expect(s.maxInterTurnMs).toBeNumber();
|
||||||
|
expect(s.transcript).toBeArray();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Fixture validator
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('validateFixtures', () => {
|
||||||
|
function base(overrides: Partial<OverlayFixture> = {}): OverlayFixture {
|
||||||
|
return {
|
||||||
|
id: 'test-fixture',
|
||||||
|
overlayPath: 'model-overlays/opus-4-7.md',
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
trials: 10,
|
||||||
|
setupWorkspace: () => {},
|
||||||
|
userPrompt: 'go',
|
||||||
|
metric: () => 0,
|
||||||
|
pass: fanoutPass,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('passes for a valid fixture', () => {
|
||||||
|
expect(() => validateFixtures([base()])).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects empty id', () => {
|
||||||
|
expect(() => validateFixtures([base({ id: '' })])).toThrow(/id must be/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects id with uppercase or unsafe chars', () => {
|
||||||
|
expect(() => validateFixtures([base({ id: 'Test_Fixture' })])).toThrow(/id must be/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects duplicate ids', () => {
|
||||||
|
expect(() => validateFixtures([base(), base()])).toThrow(/duplicate fixture id/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects non-integer trials', () => {
|
||||||
|
expect(() => validateFixtures([base({ trials: 3.5 })])).toThrow(/trials must be/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects trials < 3', () => {
|
||||||
|
expect(() => validateFixtures([base({ trials: 2 })])).toThrow(/trials must be/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects concurrency < 1', () => {
|
||||||
|
expect(() => validateFixtures([base({ concurrency: 0 })])).toThrow(/concurrency must be/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects non-integer concurrency', () => {
|
||||||
|
expect(() => validateFixtures([base({ concurrency: 2.5 })])).toThrow(/concurrency must be/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects empty model', () => {
|
||||||
|
expect(() => validateFixtures([base({ model: '' })])).toThrow(/model must be/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects empty userPrompt', () => {
|
||||||
|
expect(() => validateFixtures([base({ userPrompt: '' })])).toThrow(/userPrompt must be/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects absolute overlayPath', () => {
|
||||||
|
expect(() => validateFixtures([base({ overlayPath: '/etc/passwd' })])).toThrow(/overlayPath must be/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects overlayPath containing '..'", () => {
|
||||||
|
expect(() =>
|
||||||
|
validateFixtures([base({ overlayPath: '../outside/file.md' })]),
|
||||||
|
).toThrow(/overlayPath must be/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects missing overlay file', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateFixtures([base({ overlayPath: 'model-overlays/nonexistent.md' })]),
|
||||||
|
).toThrow(/overlay file not found/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects non-function setupWorkspace', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateFixtures([base({ setupWorkspace: 'not a function' as unknown as (d: string) => void })]),
|
||||||
|
).toThrow(/setupWorkspace must be a function/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects non-function metric', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateFixtures([base({ metric: null as unknown as (r: AgentSdkResult) => number })]),
|
||||||
|
).toThrow(/metric must be a function/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects non-function pass', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateFixtures([base({ pass: undefined as unknown as OverlayFixture['pass'] })]),
|
||||||
|
).toThrow(/pass must be a function/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// fanoutPass predicate
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('fanoutPass predicate', () => {
|
||||||
|
test('accepts mean lift >= 0.5 AND >=3/10 overlay trials >= 2', () => {
|
||||||
|
const overlay = [2, 2, 2, 2, 2, 2, 2, 2, 2, 2];
|
||||||
|
const off = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
expect(fanoutPass({ overlay, off })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects when mean lift < 0.5', () => {
|
||||||
|
const overlay = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
|
||||||
|
const off = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
|
||||||
|
expect(fanoutPass({ overlay, off })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects when mean lift >= 0.5 but <3 overlay trials emit >=2', () => {
|
||||||
|
// Mean overlay = 1.2, off = 0.0, lift 1.2 but only 2 trials at >=2
|
||||||
|
const overlay = [2, 2, 1, 1, 1, 1, 1, 1, 1, 1];
|
||||||
|
const off = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
expect(fanoutPass({ overlay, off })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
/**
|
||||||
|
* gbrain-sync integration tests.
|
||||||
|
*
|
||||||
|
* Covers the core cross-machine memory sync feature end-to-end:
|
||||||
|
* - bin/gstack-config gbrain keys (validation, isolation)
|
||||||
|
* - bin/gstack-brain-enqueue (atomicity, skip list, no-op gates)
|
||||||
|
* - bin/gstack-jsonl-merge (3-way, ts-sort, hash-fallback)
|
||||||
|
* - bin/gstack-brain-sync --once (drain, commit, push, secret-scan, skip-file)
|
||||||
|
* - bin/gstack-brain-init + --restore round-trip
|
||||||
|
* - bin/gstack-brain-uninstall preserves user data
|
||||||
|
* - env isolation (GSTACK_HOME never bleeds into real ~/.gstack/config.yaml)
|
||||||
|
*
|
||||||
|
* Runs each test against a temp GSTACK_HOME and a local bare git repo as
|
||||||
|
* a fake remote. No live GitHub, no live GBrain.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test as _test, expect, beforeEach, afterEach } from 'bun:test';
|
||||||
|
|
||||||
|
// Boost timeout: brain-sync tests spawn git, network-ls-remote, and 10-way
|
||||||
|
// parallel processes — 5s default is too tight.
|
||||||
|
const test = (name: string, fn: any) => _test(name, fn, 30000);
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
|
||||||
|
const ROOT = path.resolve(import.meta.dir, '..');
|
||||||
|
const BIN = path.join(ROOT, 'bin');
|
||||||
|
|
||||||
|
let tmpHome: string;
|
||||||
|
let bareRemote: string;
|
||||||
|
|
||||||
|
function run(argv: string[], opts: { env?: Record<string, string>; input?: string } = {}) {
|
||||||
|
const bin = argv[0];
|
||||||
|
const full = bin.startsWith('/') ? bin : path.join(BIN, bin);
|
||||||
|
const res = spawnSync(full, argv.slice(1), {
|
||||||
|
env: { ...process.env, GSTACK_HOME: tmpHome, ...(opts.env || {}) },
|
||||||
|
encoding: 'utf-8',
|
||||||
|
input: opts.input,
|
||||||
|
cwd: ROOT,
|
||||||
|
});
|
||||||
|
return { stdout: res.stdout || '', stderr: res.stderr || '', status: res.status ?? -1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function git(args: string[], cwd?: string) {
|
||||||
|
const res = spawnSync('git', args, { cwd: cwd || tmpHome, encoding: 'utf-8' });
|
||||||
|
return { stdout: res.stdout || '', stderr: res.stderr || '', status: res.status ?? -1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'brain-sync-home-'));
|
||||||
|
bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), 'brain-sync-remote-'));
|
||||||
|
spawnSync('git', ['init', '--bare', '-q', '-b', 'main', bareRemote]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||||
|
fs.rmSync(bareRemote, { recursive: true, force: true });
|
||||||
|
// Clean up any remote-helper file init may have written.
|
||||||
|
const remoteFile = path.join(os.homedir(), '.gstack-brain-remote.txt');
|
||||||
|
// Only remove if it points at OUR bare remote (don't clobber a real user file).
|
||||||
|
try {
|
||||||
|
const contents = fs.readFileSync(remoteFile, 'utf-8').trim();
|
||||||
|
if (contents === bareRemote) fs.unlinkSync(remoteFile);
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Config key validation + env isolation
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe('gstack-config gbrain keys', () => {
|
||||||
|
test('default gbrain_sync_mode is off', () => {
|
||||||
|
const r = run(['gstack-config', 'get', 'gbrain_sync_mode']);
|
||||||
|
expect(r.status).toBe(0);
|
||||||
|
expect(r.stdout.trim()).toBe('off');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('default gbrain_sync_mode_prompted is false', () => {
|
||||||
|
const r = run(['gstack-config', 'get', 'gbrain_sync_mode_prompted']);
|
||||||
|
expect(r.stdout.trim()).toBe('false');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts full / artifacts-only / off', () => {
|
||||||
|
for (const val of ['full', 'artifacts-only', 'off']) {
|
||||||
|
const set = run(['gstack-config', 'set', 'gbrain_sync_mode', val]);
|
||||||
|
expect(set.status).toBe(0);
|
||||||
|
const get = run(['gstack-config', 'get', 'gbrain_sync_mode']);
|
||||||
|
expect(get.stdout.trim()).toBe(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid gbrain_sync_mode value warns + defaults', () => {
|
||||||
|
const r = run(['gstack-config', 'set', 'gbrain_sync_mode', 'bogus']);
|
||||||
|
expect(r.stderr).toContain('not recognized');
|
||||||
|
const get = run(['gstack-config', 'get', 'gbrain_sync_mode']);
|
||||||
|
expect(get.stdout.trim()).toBe('off');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GSTACK_HOME overrides real config dir', () => {
|
||||||
|
run(['gstack-config', 'set', 'gbrain_sync_mode', 'full']);
|
||||||
|
// Real ~/.gstack/config.yaml must NOT have been touched.
|
||||||
|
const realConfig = path.join(os.homedir(), '.gstack', 'config.yaml');
|
||||||
|
const real = fs.existsSync(realConfig) ? fs.readFileSync(realConfig, 'utf-8') : '';
|
||||||
|
expect(real).not.toContain('gbrain_sync_mode: full');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Enqueue behavior
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe('gstack-brain-enqueue', () => {
|
||||||
|
test('no-op when feature not initialized', () => {
|
||||||
|
const r = run(['gstack-brain-enqueue', 'projects/foo/learnings.jsonl']);
|
||||||
|
expect(r.status).toBe(0);
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, '.brain-queue.jsonl'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no-op when mode is off (even if .git exists)', () => {
|
||||||
|
fs.mkdirSync(path.join(tmpHome, '.git'), { recursive: true });
|
||||||
|
const r = run(['gstack-brain-enqueue', 'projects/foo/learnings.jsonl']);
|
||||||
|
expect(r.status).toBe(0);
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, '.brain-queue.jsonl'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enqueues when mode is full and .git exists', () => {
|
||||||
|
fs.mkdirSync(path.join(tmpHome, '.git'), { recursive: true });
|
||||||
|
run(['gstack-config', 'set', 'gbrain_sync_mode', 'full']);
|
||||||
|
run(['gstack-brain-enqueue', 'projects/foo/learnings.jsonl']);
|
||||||
|
const queue = fs.readFileSync(path.join(tmpHome, '.brain-queue.jsonl'), 'utf-8');
|
||||||
|
expect(queue).toContain('projects/foo/learnings.jsonl');
|
||||||
|
const obj = JSON.parse(queue.trim());
|
||||||
|
expect(obj.file).toBe('projects/foo/learnings.jsonl');
|
||||||
|
expect(obj.ts).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skip list honored', () => {
|
||||||
|
fs.mkdirSync(path.join(tmpHome, '.git'), { recursive: true });
|
||||||
|
run(['gstack-config', 'set', 'gbrain_sync_mode', 'full']);
|
||||||
|
fs.writeFileSync(path.join(tmpHome, '.brain-skip.txt'), 'projects/foo/secret.jsonl\n');
|
||||||
|
run(['gstack-brain-enqueue', 'projects/foo/secret.jsonl']);
|
||||||
|
run(['gstack-brain-enqueue', 'projects/foo/ok.jsonl']);
|
||||||
|
const queue = fs.readFileSync(path.join(tmpHome, '.brain-queue.jsonl'), 'utf-8');
|
||||||
|
expect(queue).not.toContain('secret.jsonl');
|
||||||
|
expect(queue).toContain('ok.jsonl');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('concurrent enqueues all land (atomic append)', async () => {
|
||||||
|
fs.mkdirSync(path.join(tmpHome, '.git'), { recursive: true });
|
||||||
|
run(['gstack-config', 'set', 'gbrain_sync_mode', 'full']);
|
||||||
|
const procs = [];
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
procs.push(new Promise<void>((resolve) => {
|
||||||
|
const r = spawnSync(path.join(BIN, 'gstack-brain-enqueue'), [`file-${i}.jsonl`], {
|
||||||
|
env: { ...process.env, GSTACK_HOME: tmpHome },
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
await Promise.all(procs);
|
||||||
|
const queue = fs.readFileSync(path.join(tmpHome, '.brain-queue.jsonl'), 'utf-8');
|
||||||
|
const lines = queue.trim().split('\n').filter(Boolean);
|
||||||
|
expect(lines.length).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no args does not crash', () => {
|
||||||
|
const r = run(['gstack-brain-enqueue']);
|
||||||
|
expect(r.status).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// JSONL merge driver
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe('gstack-jsonl-merge', () => {
|
||||||
|
test('3-way merge dedups + sorts by ts', () => {
|
||||||
|
const base = path.join(tmpHome, 'base.jsonl');
|
||||||
|
const ours = path.join(tmpHome, 'ours.jsonl');
|
||||||
|
const theirs = path.join(tmpHome, 'theirs.jsonl');
|
||||||
|
fs.writeFileSync(base, '');
|
||||||
|
fs.writeFileSync(ours, '{"x":1,"ts":"2026-01-01T10:00:00Z"}\n{"x":2,"ts":"2026-01-01T11:00:00Z"}\n');
|
||||||
|
fs.writeFileSync(theirs, '{"x":3,"ts":"2026-01-01T09:00:00Z"}\n{"x":2,"ts":"2026-01-01T11:00:00Z"}\n');
|
||||||
|
const r = run([path.join(BIN, 'gstack-jsonl-merge'), base, ours, theirs]);
|
||||||
|
expect(r.status).toBe(0);
|
||||||
|
const lines = fs.readFileSync(ours, 'utf-8').trim().split('\n');
|
||||||
|
expect(lines.length).toBe(3);
|
||||||
|
expect(lines[0]).toContain('"x":3'); // earliest ts
|
||||||
|
expect(lines[2]).toContain('"x":2'); // latest ts
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to hash order for lines without ts', () => {
|
||||||
|
const base = path.join(tmpHome, 'base.jsonl');
|
||||||
|
const ours = path.join(tmpHome, 'ours.jsonl');
|
||||||
|
const theirs = path.join(tmpHome, 'theirs.jsonl');
|
||||||
|
fs.writeFileSync(base, '');
|
||||||
|
fs.writeFileSync(ours, '{"a":1}\n{"a":2}\n');
|
||||||
|
fs.writeFileSync(theirs, '{"a":3}\n{"a":2}\n');
|
||||||
|
run([path.join(BIN, 'gstack-jsonl-merge'), base, ours, theirs]);
|
||||||
|
const lines = fs.readFileSync(ours, 'utf-8').trim().split('\n');
|
||||||
|
expect(lines.length).toBe(3);
|
||||||
|
// Order is deterministic (sha256 of each line).
|
||||||
|
const again = spawnSync(path.join(BIN, 'gstack-jsonl-merge'), [base, ours, theirs]);
|
||||||
|
// (re-running doesn't change the order since same input → same output)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Init + sync + restore round-trip
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe('init + sync + restore round-trip', () => {
|
||||||
|
test('init creates canonical files + registers drivers', () => {
|
||||||
|
const r = run(['gstack-brain-init', '--remote', bareRemote]);
|
||||||
|
expect(r.status).toBe(0);
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, '.git'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, '.gitignore'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, '.brain-allowlist'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, '.brain-privacy-map.json'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, '.gitattributes'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, '.git/hooks/pre-commit'))).toBe(true);
|
||||||
|
// Merge driver registered in local git config.
|
||||||
|
const cfg = git(['config', '--get', 'merge.jsonl-append.driver']);
|
||||||
|
expect(cfg.stdout).toContain('gstack-jsonl-merge');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refuses init on different remote', () => {
|
||||||
|
run(['gstack-brain-init', '--remote', bareRemote]);
|
||||||
|
const otherRemote = fs.mkdtempSync(path.join(os.tmpdir(), 'brain-other-'));
|
||||||
|
spawnSync('git', ['init', '--bare', '-q', '-b', 'main', otherRemote]);
|
||||||
|
const r = run(['gstack-brain-init', '--remote', otherRemote]);
|
||||||
|
expect(r.status).not.toBe(0);
|
||||||
|
expect(r.stderr).toContain('already a git repo pointing at');
|
||||||
|
fs.rmSync(otherRemote, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('full sync: init → enqueue → --once → commit pushed', () => {
|
||||||
|
run(['gstack-brain-init', '--remote', bareRemote]);
|
||||||
|
run(['gstack-config', 'set', 'gbrain_sync_mode', 'full']);
|
||||||
|
fs.mkdirSync(path.join(tmpHome, 'projects', 'p'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(tmpHome, 'projects/p/learnings.jsonl'),
|
||||||
|
'{"skill":"x","insight":"y","ts":"2026-04-22T10:00:00Z"}\n');
|
||||||
|
run(['gstack-brain-enqueue', 'projects/p/learnings.jsonl']);
|
||||||
|
const r = run(['gstack-brain-sync', '--once']);
|
||||||
|
expect(r.status).toBe(0);
|
||||||
|
// Check the remote got the commit.
|
||||||
|
const log = spawnSync('git', ['--git-dir=' + bareRemote, 'log', '--oneline'], { encoding: 'utf-8' });
|
||||||
|
expect(log.stdout).toMatch(/sync: 1 file/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore round-trip: writes on machine A visible on machine B', () => {
|
||||||
|
// Machine A.
|
||||||
|
run(['gstack-brain-init', '--remote', bareRemote]);
|
||||||
|
run(['gstack-config', 'set', 'gbrain_sync_mode', 'full']);
|
||||||
|
fs.mkdirSync(path.join(tmpHome, 'projects', 'myproj'), { recursive: true });
|
||||||
|
const aLearning = '{"skill":"x","insight":"machine A wisdom","ts":"2026-04-22T10:00:00Z"}\n';
|
||||||
|
fs.writeFileSync(path.join(tmpHome, 'projects/myproj/learnings.jsonl'), aLearning);
|
||||||
|
run(['gstack-brain-enqueue', 'projects/myproj/learnings.jsonl']);
|
||||||
|
run(['gstack-brain-sync', '--once']);
|
||||||
|
|
||||||
|
// Machine B (new temp home).
|
||||||
|
const machineB = fs.mkdtempSync(path.join(os.tmpdir(), 'brain-machineB-'));
|
||||||
|
const r = run(['gstack-brain-restore', bareRemote], {
|
||||||
|
env: { GSTACK_HOME: machineB },
|
||||||
|
});
|
||||||
|
expect(r.status).toBe(0);
|
||||||
|
const restored = fs.readFileSync(path.join(machineB, 'projects/myproj/learnings.jsonl'), 'utf-8');
|
||||||
|
expect(restored).toContain('machine A wisdom');
|
||||||
|
// Merge drivers re-registered on B.
|
||||||
|
const cfg = spawnSync('git', ['-C', machineB, 'config', '--get', 'merge.jsonl-append.driver'], { encoding: 'utf-8' });
|
||||||
|
expect(cfg.stdout).toContain('gstack-jsonl-merge');
|
||||||
|
fs.rmSync(machineB, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Secret scan: all regex families block
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe('gstack-brain-sync secret scan', () => {
|
||||||
|
const SECRETS: [string, string][] = [
|
||||||
|
['aws-access-key', 'AKIAABCDEFGHIJKLMNOP'],
|
||||||
|
['github-token-ghp', 'ghp_abcdefghij1234567890abcdef1234567890'],
|
||||||
|
['github-token-github-pat', 'github_pat_11ABCDEFG1234567890_abcdef'],
|
||||||
|
['openai-key', 'sk-abcdefghij1234567890abcdef1234567890'],
|
||||||
|
['pem-block', '-----BEGIN PRIVATE KEY-----'],
|
||||||
|
['jwt', 'eyJ0eXAiOiJKV1QiLCJh.eyJzdWIiOiIxMjM0NTY3.SflKxwRJSMeKKF30oGTbU'],
|
||||||
|
['bearer-json', '"authorization":"Bearer abcdef1234567890abcdef1234567890"'],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [name, content] of SECRETS) {
|
||||||
|
test(`blocks ${name}`, () => {
|
||||||
|
run(['gstack-brain-init', '--remote', bareRemote]);
|
||||||
|
run(['gstack-config', 'set', 'gbrain_sync_mode', 'full']);
|
||||||
|
fs.mkdirSync(path.join(tmpHome, 'projects', 'p'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(tmpHome, 'projects/p/learnings.jsonl'),
|
||||||
|
`{"leaked":"${content}"}\n`);
|
||||||
|
run(['gstack-brain-enqueue', 'projects/p/learnings.jsonl']);
|
||||||
|
const r = run(['gstack-brain-sync', '--once']);
|
||||||
|
expect(r.status).toBe(0); // exits clean even when blocked
|
||||||
|
// No new commit should have been created.
|
||||||
|
const log = git(['log', '--oneline']);
|
||||||
|
expect(log.stdout.split('\n').filter(Boolean).length).toBeLessThanOrEqual(3);
|
||||||
|
// Status file should report blocked.
|
||||||
|
const status = JSON.parse(fs.readFileSync(path.join(tmpHome, '.brain-sync-status.json'), 'utf-8'));
|
||||||
|
expect(status.status).toBe('blocked');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('--skip-file unblocks specific file', () => {
|
||||||
|
run(['gstack-brain-init', '--remote', bareRemote]);
|
||||||
|
run(['gstack-config', 'set', 'gbrain_sync_mode', 'full']);
|
||||||
|
fs.mkdirSync(path.join(tmpHome, 'projects', 'p'), { recursive: true });
|
||||||
|
const leakPath = 'projects/p/leaked.jsonl';
|
||||||
|
fs.writeFileSync(path.join(tmpHome, leakPath),
|
||||||
|
'{"gh":"ghp_abcdefghij1234567890abcdef1234567890"}\n');
|
||||||
|
run(['gstack-brain-enqueue', leakPath]);
|
||||||
|
run(['gstack-brain-sync', '--once']); // blocked
|
||||||
|
run(['gstack-brain-sync', '--skip-file', leakPath]);
|
||||||
|
// Any future enqueue of this path should no-op.
|
||||||
|
run(['gstack-brain-enqueue', leakPath]);
|
||||||
|
const skip = fs.readFileSync(path.join(tmpHome, '.brain-skip.txt'), 'utf-8');
|
||||||
|
expect(skip).toContain(leakPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Uninstall preserves user data
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe('gstack-brain-uninstall', () => {
|
||||||
|
test('removes sync config but preserves learnings/project data', () => {
|
||||||
|
run(['gstack-brain-init', '--remote', bareRemote]);
|
||||||
|
fs.mkdirSync(path.join(tmpHome, 'projects', 'user-data'), { recursive: true });
|
||||||
|
const preservedContent = '{"keep":"me","ts":"2026-04-22T12:00:00Z"}\n';
|
||||||
|
fs.writeFileSync(path.join(tmpHome, 'projects/user-data/learnings.jsonl'), preservedContent);
|
||||||
|
const r = run(['gstack-brain-uninstall', '--yes']);
|
||||||
|
expect(r.status).toBe(0);
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, '.git'))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, '.gitignore'))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, '.brain-allowlist'))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, 'consumers.json'))).toBe(false);
|
||||||
|
// Project data preserved.
|
||||||
|
const preserved = fs.readFileSync(path.join(tmpHome, 'projects/user-data/learnings.jsonl'), 'utf-8');
|
||||||
|
expect(preserved).toBe(preservedContent);
|
||||||
|
// Config key reset.
|
||||||
|
const mode = run(['gstack-config', 'get', 'gbrain_sync_mode']);
|
||||||
|
expect(mode.stdout.trim()).toBe('off');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// --discover-new: cursor-based change detection
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe('gstack-brain-sync --discover-new', () => {
|
||||||
|
test('enqueues new allowlisted files; idempotent on re-run', () => {
|
||||||
|
run(['gstack-brain-init', '--remote', bareRemote]);
|
||||||
|
run(['gstack-config', 'set', 'gbrain_sync_mode', 'full']);
|
||||||
|
fs.mkdirSync(path.join(tmpHome, 'retros'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(tmpHome, 'retros/week-1.md'), '# retro\n');
|
||||||
|
run(['gstack-brain-sync', '--discover-new']);
|
||||||
|
let queue = fs.readFileSync(path.join(tmpHome, '.brain-queue.jsonl'), 'utf-8');
|
||||||
|
expect(queue).toContain('retros/week-1.md');
|
||||||
|
// Clear queue, run again — idempotent (no new entries).
|
||||||
|
fs.writeFileSync(path.join(tmpHome, '.brain-queue.jsonl'), '');
|
||||||
|
run(['gstack-brain-sync', '--discover-new']);
|
||||||
|
queue = fs.readFileSync(path.join(tmpHome, '.brain-queue.jsonl'), 'utf-8');
|
||||||
|
expect(queue.trim()).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
+228
-14
@@ -355,6 +355,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -468,20 +696,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -344,6 +344,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="$GSTACK_BIN/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="$GSTACK_BIN/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"$GSTACK_BIN/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"$GSTACK_BIN/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -457,20 +685,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
+228
-14
@@ -346,6 +346,234 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|||||||
- Focus on completing the task and reporting results via prose output.
|
- Focus on completing the task and reporting results via prose output.
|
||||||
- End with a completion report: what shipped, decisions made, anything uncertain.
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
||||||
|
|
||||||
|
## AskUserQuestion Format
|
||||||
|
|
||||||
|
**ALWAYS follow this structure for every AskUserQuestion call. Every element is non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
||||||
|
|
||||||
|
### Required shape
|
||||||
|
|
||||||
|
Every AskUserQuestion reads like a decision brief, not a bullet list:
|
||||||
|
|
||||||
|
```
|
||||||
|
D<N> — <one-line question title>
|
||||||
|
|
||||||
|
ELI10: <plain English a 16-year-old could follow, 2-4 sentences, name the stakes>
|
||||||
|
|
||||||
|
Stakes if we pick wrong: <one sentence on what breaks, what user sees, what's lost>
|
||||||
|
|
||||||
|
Recommendation: <choice> because <one-line reason>
|
||||||
|
|
||||||
|
Completeness: A=X/10, B=Y/10 (or: Note: options differ in kind, not coverage — no completeness score)
|
||||||
|
|
||||||
|
Pros / cons:
|
||||||
|
|
||||||
|
A) <option label> (recommended)
|
||||||
|
✅ <pro — concrete, observable, ≥40 chars>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con — honest, ≥40 chars>
|
||||||
|
|
||||||
|
B) <option label>
|
||||||
|
✅ <pro>
|
||||||
|
❌ <con>
|
||||||
|
|
||||||
|
Net: <one-line synthesis of what you're actually trading off>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element rules
|
||||||
|
|
||||||
|
1. **D-numbering.** First question in a skill invocation is `D1`. Increment per
|
||||||
|
question within the same skill. This is a model-level instruction, not a
|
||||||
|
runtime counter — you count your own questions. Nested skill invocation
|
||||||
|
(e.g., `/plan-ceo-review` running `/office-hours` inline) starts its own
|
||||||
|
D1; label as `D1 (office-hours)` to disambiguate when the user will see
|
||||||
|
both. Drift is expected over long sessions; minor inconsistency is fine.
|
||||||
|
|
||||||
|
2. **Re-ground.** Before ELI10, state the project, current branch (use the
|
||||||
|
`_BRANCH` value from the preamble, NOT conversation history or gitStatus),
|
||||||
|
and the current plan/task. 1-2 sentences. Assume the user hasn't looked at
|
||||||
|
this window in 20 minutes.
|
||||||
|
|
||||||
|
3. **ELI10 (ALWAYS).** Explain in plain English a smart 16-year-old could
|
||||||
|
follow. Concrete examples and analogies, not function names. Say what it
|
||||||
|
DOES, not what it's called. This is not preamble — the user is about to
|
||||||
|
make a decision and needs context. Even in terse mode, emit the ELI10.
|
||||||
|
|
||||||
|
4. **Stakes if we pick wrong (ALWAYS).** One sentence naming what breaks in
|
||||||
|
concrete terms (pain avoided / capability unlocked / consequence named).
|
||||||
|
"Users see a 3-second spinner" beats "performance may degrade." Forces
|
||||||
|
the trade-off to be real.
|
||||||
|
|
||||||
|
5. **Recommendation (ALWAYS).** `Recommendation: <choice> because <one-line
|
||||||
|
reason>` on its own line. Never omit it. Required for every AskUserQuestion,
|
||||||
|
even when neutral-posture (see rule 8). The `(recommended)` label on the
|
||||||
|
option is REQUIRED — `scripts/resolvers/question-tuning.ts` reads it to
|
||||||
|
power the AUTO_DECIDE path. Omitting it breaks auto-decide.
|
||||||
|
|
||||||
|
6. **Completeness scoring (when meaningful).** When options differ in
|
||||||
|
coverage (full test coverage vs happy path vs shortcut, complete error
|
||||||
|
handling vs partial), score each `Completeness: N/10` on its own line.
|
||||||
|
Calibration: 10 = complete, 7 = happy path only, 3 = shortcut. Flag any
|
||||||
|
option ≤5 where a higher-completeness option exists. When options differ
|
||||||
|
in kind (review posture, architectural A-vs-B, cherry-pick Add/Defer/Skip,
|
||||||
|
two different kinds of systems), SKIP the score and write one line:
|
||||||
|
`Note: options differ in kind, not coverage — no completeness score.`
|
||||||
|
Do NOT fabricate filler scores — empty 10/10 on every option is worse
|
||||||
|
than no score.
|
||||||
|
|
||||||
|
7. **Pros / cons block.** Every option gets per-bullet ✅ (pro) and ❌ (con)
|
||||||
|
markers. Rules:
|
||||||
|
- **Minimum 2 pros and 1 con per option.** If you can't name a con for
|
||||||
|
the recommended option, the recommendation is hollow — go find one. If
|
||||||
|
you can't name a pro for the rejected option, the question isn't real.
|
||||||
|
- **Minimum 40 characters per bullet.** `✅ Simple` is not a pro. `✅
|
||||||
|
Reuses the YAML frontmatter format already in MEMORY.md, zero new
|
||||||
|
parser` is a pro. Concrete, observable, specific.
|
||||||
|
- **Hard-stop escape** for genuinely one-sided choices (destructive-action
|
||||||
|
confirmation, one-way doors): a single bullet `✅ No cons — this is a
|
||||||
|
hard-stop choice` satisfies the rule. Use sparingly; overuse flips a
|
||||||
|
decision brief into theater.
|
||||||
|
|
||||||
|
8. **Net line (ALWAYS).** Closes the decision with a one-sentence synthesis
|
||||||
|
of what the user is actually trading off. From the reference screenshot:
|
||||||
|
*"The new-format case is speculative. The copy-format case is immediate
|
||||||
|
leverage. Copy now, evolve later if a real pattern emerges."* Not a
|
||||||
|
summary — a verdict frame.
|
||||||
|
|
||||||
|
9. **Neutral-posture handling.** When the skill explicitly says "neutral
|
||||||
|
recommendation posture" (SELECTIVE EXPANSION cherry-picks, taste calls,
|
||||||
|
kind-differentiated choices where neither side dominates), the
|
||||||
|
Recommendation line reads: `Recommendation: <default-choice> — this is a
|
||||||
|
taste call, no strong preference either way`. The `(recommended)` label
|
||||||
|
STAYS on the default option (machine-readable hint for AUTO_DECIDE). The
|
||||||
|
`— this is a taste call` prose is the human-readable neutrality signal.
|
||||||
|
Both coexist.
|
||||||
|
|
||||||
|
10. **Effort both-scales.** When an option involves effort, show both human
|
||||||
|
and CC scales: `(human: ~2 days / CC: ~15 min)`.
|
||||||
|
|
||||||
|
11. **Tool_use, not prose.** A markdown block labeled `Question:` is not a
|
||||||
|
question — the user never sees it as interactive. If you wrote one in
|
||||||
|
prose, stop and reissue as an actual AskUserQuestion tool_use. The rich
|
||||||
|
markdown goes in the question body; the `options` array stays short
|
||||||
|
labels (A, B, C).
|
||||||
|
|
||||||
|
### Self-check before emitting
|
||||||
|
|
||||||
|
Before calling AskUserQuestion, verify:
|
||||||
|
- [ ] D<N> header present
|
||||||
|
- [ ] ELI10 paragraph present (stakes line too)
|
||||||
|
- [ ] Recommendation line present with concrete reason
|
||||||
|
- [ ] Completeness scored (coverage) OR kind-note present (kind)
|
||||||
|
- [ ] Every option has ≥2 ✅ and ≥1 ❌, each ≥40 chars (or hard-stop escape)
|
||||||
|
- [ ] (recommended) label on one option (even for neutral-posture — see rule 9)
|
||||||
|
- [ ] Net line closes the decision
|
||||||
|
- [ ] You are calling the tool, not writing prose
|
||||||
|
|
||||||
|
If you'd need to read the source to understand your own explanation, it's
|
||||||
|
too complex — simplify before emitting.
|
||||||
|
|
||||||
|
Per-skill instructions may add additional formatting rules on top of this
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
||||||
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
||||||
|
# docs/gbrain-sync.md.
|
||||||
|
|
||||||
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||||
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
||||||
|
_BRAIN_SYNC_BIN="$GSTACK_BIN/gstack-brain-sync"
|
||||||
|
_BRAIN_CONFIG_BIN="$GSTACK_BIN/gstack-config"
|
||||||
|
|
||||||
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
||||||
|
|
||||||
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
||||||
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
||||||
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
||||||
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
||||||
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Active-sync path.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
# Once-per-day pull.
|
||||||
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
||||||
|
_BRAIN_NOW=$(date +%s)
|
||||||
|
_BRAIN_DO_PULL=1
|
||||||
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
||||||
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
||||||
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
||||||
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
||||||
|
fi
|
||||||
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
||||||
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||||
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||||
|
fi
|
||||||
|
# Drain pending queue, push.
|
||||||
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Status line — always emitted, easy to grep.
|
||||||
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||||
|
_BRAIN_QUEUE_DEPTH=0
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
||||||
|
_BRAIN_LAST_PUSH="never"
|
||||||
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
||||||
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
||||||
|
else
|
||||||
|
echo "BRAIN_SYNC: off"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Privacy stop-gate (fires ONCE per machine).**
|
||||||
|
|
||||||
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
||||||
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
||||||
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
||||||
|
fire a one-time privacy gate via AskUserQuestion:
|
||||||
|
|
||||||
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
||||||
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
||||||
|
> include behavioral data (session timelines, developer profile). How much do you
|
||||||
|
> want to sync?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
||||||
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
||||||
|
- C) Decline — keep everything local
|
||||||
|
|
||||||
|
After the user answers, run (substituting the chosen value):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chosen mode: full | artifacts-only | off
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
||||||
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
||||||
|
```
|
||||||
|
|
||||||
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
||||||
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
||||||
|
- A) Yes, run it now
|
||||||
|
- B) Show me the command, I'll run it myself
|
||||||
|
|
||||||
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
||||||
|
next skill run picks up wherever this left off.
|
||||||
|
|
||||||
|
**At skill END (before the telemetry block),** run these bash commands to
|
||||||
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
||||||
|
shims, plus drain any still-pending queue entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"$GSTACK_BIN/gstack-brain-sync" --discover-new 2>/dev/null || true
|
||||||
|
"$GSTACK_BIN/gstack-brain-sync" --once 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (claude)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the claude model family. They are
|
||||||
@@ -459,20 +687,6 @@ are shown, synthesize a one-paragraph welcome briefing before proceeding:
|
|||||||
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
"Welcome back to {branch}. Last session: /{skill} ({outcome}). [Checkpoint summary if
|
||||||
available]. [Health score if available]." Keep it to 2-3 sentences.
|
available]. [Health score if available]." Keep it to 2-3 sentences.
|
||||||
|
|
||||||
## AskUserQuestion Format
|
|
||||||
|
|
||||||
**ALWAYS follow this structure for every AskUserQuestion call. All four elements are non-skippable. If you find yourself about to skip any of them, stop and back up.**
|
|
||||||
|
|
||||||
1. **Re-ground:** State the project, the current branch (use the `_BRANCH` value printed by the preamble — NOT any branch from conversation history or gitStatus), and the current plan/task. (1-2 sentences)
|
|
||||||
2. **Simplify (ELI10, ALWAYS):** Explain what's happening in plain English a smart 16-year-old could follow. Concrete examples and analogies, not function names or internal jargon. Say what it DOES, not what it's called. State the stakes: what breaks if we pick wrong. This is NOT optional verbosity and it is NOT preamble — the user is about to make a decision and needs context. Even if you'd normally stay terse, emit the ELI10 paragraph. The user will ask for it anyway; do it the first time.
|
|
||||||
3. **Recommend (ALWAYS):** Every question ends with `RECOMMENDATION: Choose [X] because [one-line reason]` on its own line. Never omit it. Never collapse it into the options list. Required for every AskUserQuestion, regardless of whether the options are coverage-differentiated or different-in-kind.
|
|
||||||
4. **Score completeness (when meaningful):** When options differ in coverage (e.g. full test coverage vs happy path vs shortcut, complete error handling vs partial), score each with `Completeness: N/10` on its own line. Calibration: 10 = complete (all edge cases, full coverage), 7 = happy path only, 3 = shortcut. Flag any option ≤5 where a higher-completeness option exists. When options differ in kind (picking a review posture, picking an architectural approach, cherry-pick Add/Defer/Skip, choosing between two different kinds of systems), the completeness axis doesn't apply — skip `Completeness: N/10` entirely and write one line: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate filler scores.
|
|
||||||
5. **Options:** Lettered options: `A) ... B) ... C) ...` — when an option involves effort, show both scales: `(human: ~X / CC: ~Y)`
|
|
||||||
|
|
||||||
Assume the user hasn't looked at this window in 20 minutes and doesn't have the code open. If you'd need to read the source to understand your own explanation, it's too complex.
|
|
||||||
|
|
||||||
Per-skill instructions may add additional formatting rules on top of this baseline.
|
|
||||||
|
|
||||||
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
|
||||||
|
|
||||||
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
These rules apply to every AskUserQuestion, every response you write to the user, and every review finding. They compose with the AskUserQuestion Format section above: Format = *how* a question is structured; Writing Style = *the prose quality of the content inside it*.
|
||||||
|
|||||||
Vendored
+487
@@ -0,0 +1,487 @@
|
|||||||
|
/**
|
||||||
|
* Overlay-efficacy fixture registry.
|
||||||
|
*
|
||||||
|
* Each fixture defines a reproducible A/B test for one behavioral nudge
|
||||||
|
* embedded in a model-overlays/*.md file. The harness at
|
||||||
|
* test/skill-e2e-overlay-harness.test.ts iterates this registry and runs
|
||||||
|
* `fixture.trials` A/B trials per fixture, asserting `fixture.pass(arms)`.
|
||||||
|
*
|
||||||
|
* Adding a new overlay eval = one entry in this list. The harness handles
|
||||||
|
* arm wiring, concurrency, artifact storage, rate-limit retries, and the
|
||||||
|
* cross-harness diagnostic.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import {
|
||||||
|
firstTurnParallelism,
|
||||||
|
type AgentSdkResult,
|
||||||
|
} from '../helpers/agent-sdk-runner';
|
||||||
|
|
||||||
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface OverlayFixture {
|
||||||
|
/** Unique, lowercase/digits/dash only. Used in artifact paths. */
|
||||||
|
id: string;
|
||||||
|
/** Path to the overlay file, relative to repo root. */
|
||||||
|
overlayPath: string;
|
||||||
|
/** API model ID, not the overlay family name. */
|
||||||
|
model: string;
|
||||||
|
/** Integer >= 3. Trials per arm. */
|
||||||
|
trials: number;
|
||||||
|
/** Max concurrent queries for this fixture's arms. Default 3. */
|
||||||
|
concurrency?: number;
|
||||||
|
/** Populate the workspace dir before each trial. */
|
||||||
|
setupWorkspace: (dir: string) => void;
|
||||||
|
/** The prompt the model receives. Non-empty. */
|
||||||
|
userPrompt: string;
|
||||||
|
/** Per-fixture tool allowlist. Omit to use runner default [Read, Glob, Grep, Bash]. */
|
||||||
|
allowedTools?: string[];
|
||||||
|
/** Max turns per trial. Omit to use runner default (5). */
|
||||||
|
maxTurns?: number;
|
||||||
|
/**
|
||||||
|
* Direction of the expected effect. `higher_is_better` = overlay should
|
||||||
|
* increase the metric (e.g. fanout, files touched for literal scope).
|
||||||
|
* `lower_is_better` = overlay should decrease it (e.g. Bash count, turn count).
|
||||||
|
* Used only for cosmetic logging in the test output; `pass` is the actual gate.
|
||||||
|
*/
|
||||||
|
direction?: 'higher_is_better' | 'lower_is_better';
|
||||||
|
/** Compute the per-trial metric from the typed SDK result. */
|
||||||
|
metric: (r: AgentSdkResult) => number;
|
||||||
|
/** Acceptance predicate across all arms' per-trial metrics. */
|
||||||
|
pass: (arms: { overlay: number[]; off: number[] }) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Validation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function validateFixtures(fixtures: OverlayFixture[]): void {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const f of fixtures) {
|
||||||
|
if (!f.id || !/^[a-z0-9-]+$/.test(f.id)) {
|
||||||
|
throw new Error(
|
||||||
|
`fixture id must be non-empty, lowercase/digits/dash only: ${JSON.stringify(f.id)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (ids.has(f.id)) {
|
||||||
|
throw new Error(`duplicate fixture id: ${f.id}`);
|
||||||
|
}
|
||||||
|
ids.add(f.id);
|
||||||
|
|
||||||
|
if (!Number.isInteger(f.trials) || f.trials < 3) {
|
||||||
|
throw new Error(`${f.id}: trials must be an integer >= 3 (got ${f.trials})`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
f.concurrency !== undefined &&
|
||||||
|
(!Number.isInteger(f.concurrency) || f.concurrency < 1)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`${f.id}: concurrency must be an integer >= 1 (got ${f.concurrency})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!f.model) throw new Error(`${f.id}: model must be non-empty`);
|
||||||
|
if (!f.userPrompt) throw new Error(`${f.id}: userPrompt must be non-empty`);
|
||||||
|
|
||||||
|
if (path.isAbsolute(f.overlayPath) || f.overlayPath.includes('..')) {
|
||||||
|
throw new Error(
|
||||||
|
`${f.id}: overlayPath must be relative and must not contain '..' (got ${f.overlayPath})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const fullPath = path.resolve(REPO_ROOT, f.overlayPath);
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
throw new Error(`${f.id}: overlay file not found at ${f.overlayPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fn of ['setupWorkspace', 'metric', 'pass'] as const) {
|
||||||
|
if (typeof f[fn] !== 'function') {
|
||||||
|
throw new Error(`${f.id}: ${fn} must be a function`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Metric + predicate helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function mean(xs: number[]): number {
|
||||||
|
if (xs.length === 0) return 0;
|
||||||
|
return xs.reduce((a, b) => a + b, 0) / xs.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard fanout predicate: overlay mean beats off mean by at least 0.5
|
||||||
|
* parallel tool_use blocks in first turn, AND at least 3 of the overlay
|
||||||
|
* trials emit >= 2 parallel tool_use blocks.
|
||||||
|
*
|
||||||
|
* The combined rule catches both "overlay nudges every trial slightly"
|
||||||
|
* (mean) and "overlay sometimes triggers real fanout" (floor). A single
|
||||||
|
* 0.5 lift with every trial still emitting 1 call would be suspicious;
|
||||||
|
* this predicate rejects it.
|
||||||
|
*/
|
||||||
|
export function fanoutPass(arms: { overlay: number[]; off: number[] }): boolean {
|
||||||
|
const lift = mean(arms.overlay) - mean(arms.off);
|
||||||
|
const floorHits = arms.overlay.filter((n) => n >= 2).length;
|
||||||
|
return lift >= 0.5 && floorHits >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic "lower is better" pass predicate: overlay mean should drop the
|
||||||
|
* metric by at least 20% vs baseline. Used for nudges like "effort-match"
|
||||||
|
* (fewer turns) and "dedicated tools vs Bash" (fewer Bash calls).
|
||||||
|
*/
|
||||||
|
export function lowerIsBetter20Pct(arms: { overlay: number[]; off: number[] }): boolean {
|
||||||
|
const meanOff = mean(arms.off);
|
||||||
|
if (meanOff === 0) return mean(arms.overlay) <= meanOff;
|
||||||
|
return mean(arms.overlay) <= meanOff * 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic "higher is better" pass predicate: overlay mean should lift the
|
||||||
|
* metric by at least 20% vs baseline. Used for nudges like "literal
|
||||||
|
* interpretation" (more files touched when scope is ambiguous).
|
||||||
|
*/
|
||||||
|
export function higherIsBetter20Pct(arms: { overlay: number[]; off: number[] }): boolean {
|
||||||
|
const meanOff = mean(arms.off);
|
||||||
|
const meanOn = mean(arms.overlay);
|
||||||
|
if (meanOff === 0) return meanOn > 0;
|
||||||
|
return meanOn >= meanOff * 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Metrics
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the total number of Bash tool_use blocks across ALL assistant turns.
|
||||||
|
* Signal for "dedicated tools over Bash" nudge in claude.md.
|
||||||
|
*/
|
||||||
|
export function bashToolCallCount(r: AgentSdkResult): number {
|
||||||
|
return r.toolCalls.filter((c) => c.tool === 'Bash').length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total turns the session used to complete. Signal for "effort-match the
|
||||||
|
* step" nudge in opus-4-7.md — trivial prompts should complete quickly.
|
||||||
|
*/
|
||||||
|
export function turnsToCompletion(r: AgentSdkResult): number {
|
||||||
|
return r.turnsUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count of unique files the model edited or wrote. Signal for "literal
|
||||||
|
* interpretation" nudge in opus-4-7.md — "fix the tests" with multiple
|
||||||
|
* failures should touch all of them.
|
||||||
|
*/
|
||||||
|
export function uniqueFilesEdited(r: AgentSdkResult): number {
|
||||||
|
const touched = new Set<string>();
|
||||||
|
for (const call of r.toolCalls) {
|
||||||
|
if (call.tool === 'Edit' || call.tool === 'Write' || call.tool === 'MultiEdit') {
|
||||||
|
const input = call.input as { file_path?: string } | null;
|
||||||
|
if (input?.file_path) touched.add(input.file_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return touched.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Fixtures
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const OVERLAY_FIXTURES: OverlayFixture[] = [
|
||||||
|
{
|
||||||
|
id: 'opus-4-7-fanout-toy',
|
||||||
|
overlayPath: 'model-overlays/opus-4-7.md',
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
trials: 10,
|
||||||
|
concurrency: 3,
|
||||||
|
setupWorkspace: (dir) => {
|
||||||
|
fs.writeFileSync(path.join(dir, 'alpha.txt'), 'Alpha file: used in module A.\n');
|
||||||
|
fs.writeFileSync(path.join(dir, 'beta.txt'), 'Beta file: used in module B.\n');
|
||||||
|
fs.writeFileSync(path.join(dir, 'gamma.txt'), 'Gamma file: used in module C.\n');
|
||||||
|
},
|
||||||
|
userPrompt:
|
||||||
|
'Read alpha.txt, beta.txt, and gamma.txt and summarize each in one line.',
|
||||||
|
metric: (r) => firstTurnParallelism(r.assistantTurns[0]),
|
||||||
|
pass: fanoutPass,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'opus-4-7-fanout-realistic',
|
||||||
|
overlayPath: 'model-overlays/opus-4-7.md',
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
trials: 10,
|
||||||
|
concurrency: 3,
|
||||||
|
setupWorkspace: (dir) => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'app.ts'),
|
||||||
|
"import { config } from './config';\nimport { util } from './src/util';\n\nexport function main() { return config.name + ':' + util(); }\n",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'config.ts'),
|
||||||
|
"export const config = { name: 'demo', version: 1 };\n",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'README.md'),
|
||||||
|
'# demo project\n\nA small demo. Entry: `app.ts`. Config: `config.ts`.\n',
|
||||||
|
);
|
||||||
|
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'src', 'util.ts'),
|
||||||
|
"export function util() { return 'util-result'; }\n",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
userPrompt:
|
||||||
|
'Audit this project: read app.ts, config.ts, and README.md, and glob for ' +
|
||||||
|
'every .ts file under src/. Summarize what you find in 3 bullet points.',
|
||||||
|
metric: (r) => firstTurnParallelism(r.assistantTurns[0]),
|
||||||
|
pass: fanoutPass,
|
||||||
|
},
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// claude.md / "Dedicated tools over Bash"
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
{
|
||||||
|
id: 'claude-dedicated-tools-vs-bash',
|
||||||
|
overlayPath: 'model-overlays/claude.md',
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
trials: 10,
|
||||||
|
concurrency: 3,
|
||||||
|
direction: 'lower_is_better',
|
||||||
|
// 5 files + summary = needs more than default 5 turns. SDK throws
|
||||||
|
// instead of returning a result when it hits the cap.
|
||||||
|
maxTurns: 15,
|
||||||
|
setupWorkspace: (dir) => {
|
||||||
|
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(dir, 'src', 'index.ts'), "export const x = 1;\n");
|
||||||
|
fs.writeFileSync(path.join(dir, 'src', 'util.ts'), "export function util() { return 42; }\n");
|
||||||
|
fs.writeFileSync(path.join(dir, 'src', 'types.ts'), "export type Foo = { a: number };\n");
|
||||||
|
fs.writeFileSync(path.join(dir, 'src', 'config.ts'), "export const c = { n: 'demo' };\n");
|
||||||
|
fs.writeFileSync(path.join(dir, 'src', 'api.ts'), "export async function fetchFoo() { return null; }\n");
|
||||||
|
},
|
||||||
|
userPrompt:
|
||||||
|
"List every TypeScript file under src/ and tell me what each exports. " +
|
||||||
|
"You may use any tools available.",
|
||||||
|
// Metric: total Bash tool_use count across the whole session.
|
||||||
|
// The overlay says "prefer Read/Glob/Grep over cat/find/grep shell."
|
||||||
|
// A model following that should emit Glob + Read, not Bash ls/find/cat.
|
||||||
|
metric: bashToolCallCount,
|
||||||
|
pass: lowerIsBetter20Pct,
|
||||||
|
},
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// opus-4-7.md / "Effort-match the step"
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
{
|
||||||
|
id: 'opus-4-7-effort-match-trivial',
|
||||||
|
overlayPath: 'model-overlays/opus-4-7.md',
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
trials: 10,
|
||||||
|
concurrency: 3,
|
||||||
|
direction: 'lower_is_better',
|
||||||
|
maxTurns: 8,
|
||||||
|
setupWorkspace: (dir) => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'config.json'),
|
||||||
|
'{"name": "demo", "version": "1.0.0"}\n',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
userPrompt: "What's the version in config.json?",
|
||||||
|
// Metric: turns used to reach a result.
|
||||||
|
// The overlay says "simple lookups don't need deep reasoning."
|
||||||
|
// A trivial prompt like this should complete in 2 turns
|
||||||
|
// (Read, respond). Extended deliberation bumps this up.
|
||||||
|
metric: turnsToCompletion,
|
||||||
|
pass: lowerIsBetter20Pct,
|
||||||
|
},
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// opus-4-7.md / "Literal interpretation awareness"
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
{
|
||||||
|
id: 'opus-4-7-literal-interpretation',
|
||||||
|
overlayPath: 'model-overlays/opus-4-7.md',
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
trials: 10,
|
||||||
|
concurrency: 3,
|
||||||
|
direction: 'higher_is_better',
|
||||||
|
// Allow Edit and Write so the model can fix the tests.
|
||||||
|
allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'Edit', 'Write'],
|
||||||
|
maxTurns: 15,
|
||||||
|
setupWorkspace: (dir) => {
|
||||||
|
// Three failing test files. The "obvious" one is named first; the
|
||||||
|
// other two require the model to enumerate to notice them.
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'auth.test.ts'),
|
||||||
|
"import { test, expect } from 'bun:test';\n" +
|
||||||
|
"test('auth returns true', () => { expect(1).toBe(2); });\n",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'billing.test.ts'),
|
||||||
|
"import { test, expect } from 'bun:test';\n" +
|
||||||
|
"test('billing computes total', () => { expect('ten').toBe('10'); });\n",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'notifications.test.ts'),
|
||||||
|
"import { test, expect } from 'bun:test';\n" +
|
||||||
|
"test('notifications fire on save', () => { expect([1,2]).toEqual([1,2,3]); });\n",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'README.md'),
|
||||||
|
"# demo\n\nSmall project with three failing test files.\n",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
userPrompt: "Fix the failing tests.",
|
||||||
|
// Metric: number of unique test files the model edited/wrote.
|
||||||
|
// The overlay says "fix ALL failing tests, not just the obvious one."
|
||||||
|
// Overlay-ON should touch all 3 test files. Overlay-OFF might stop
|
||||||
|
// at the first one after making it pass.
|
||||||
|
metric: uniqueFilesEdited,
|
||||||
|
pass: higherIsBetter20Pct,
|
||||||
|
},
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Sonnet 4.6 variants of the Opus-4.7 fixtures.
|
||||||
|
//
|
||||||
|
// Rationale: /claude.md + /opus-4-7.md overlays measured as no-op or
|
||||||
|
// counterproductive on Opus 4.7. Before deleting the whole overlay stack,
|
||||||
|
// check whether weaker Claude models (Sonnet, Haiku) benefit from the same
|
||||||
|
// nudges. Same overlays, same prompts, same metrics, different model ID.
|
||||||
|
// Sonnet is ~4x cheaper than Opus so these 5 add ~$3 to a run.
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'opus-4-7-fanout-toy-sonnet',
|
||||||
|
overlayPath: 'model-overlays/opus-4-7.md',
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
trials: 10,
|
||||||
|
concurrency: 3,
|
||||||
|
setupWorkspace: (dir) => {
|
||||||
|
fs.writeFileSync(path.join(dir, 'alpha.txt'), 'Alpha file: used in module A.\n');
|
||||||
|
fs.writeFileSync(path.join(dir, 'beta.txt'), 'Beta file: used in module B.\n');
|
||||||
|
fs.writeFileSync(path.join(dir, 'gamma.txt'), 'Gamma file: used in module C.\n');
|
||||||
|
},
|
||||||
|
userPrompt:
|
||||||
|
'Read alpha.txt, beta.txt, and gamma.txt and summarize each in one line.',
|
||||||
|
metric: (r) => firstTurnParallelism(r.assistantTurns[0]),
|
||||||
|
pass: fanoutPass,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'opus-4-7-fanout-realistic-sonnet',
|
||||||
|
overlayPath: 'model-overlays/opus-4-7.md',
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
trials: 10,
|
||||||
|
concurrency: 3,
|
||||||
|
setupWorkspace: (dir) => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'app.ts'),
|
||||||
|
"import { config } from './config';\nimport { util } from './src/util';\n\nexport function main() { return config.name + ':' + util(); }\n",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'config.ts'),
|
||||||
|
"export const config = { name: 'demo', version: 1 };\n",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'README.md'),
|
||||||
|
'# demo project\n\nA small demo. Entry: `app.ts`. Config: `config.ts`.\n',
|
||||||
|
);
|
||||||
|
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'src', 'util.ts'),
|
||||||
|
"export function util() { return 'util-result'; }\n",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
userPrompt:
|
||||||
|
'Audit this project: read app.ts, config.ts, and README.md, and glob for ' +
|
||||||
|
'every .ts file under src/. Summarize what you find in 3 bullet points.',
|
||||||
|
metric: (r) => firstTurnParallelism(r.assistantTurns[0]),
|
||||||
|
pass: fanoutPass,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'claude-dedicated-tools-vs-bash-sonnet',
|
||||||
|
overlayPath: 'model-overlays/claude.md',
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
trials: 10,
|
||||||
|
concurrency: 3,
|
||||||
|
direction: 'lower_is_better',
|
||||||
|
maxTurns: 15,
|
||||||
|
setupWorkspace: (dir) => {
|
||||||
|
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(dir, 'src', 'index.ts'), "export const x = 1;\n");
|
||||||
|
fs.writeFileSync(path.join(dir, 'src', 'util.ts'), "export function util() { return 42; }\n");
|
||||||
|
fs.writeFileSync(path.join(dir, 'src', 'types.ts'), "export type Foo = { a: number };\n");
|
||||||
|
fs.writeFileSync(path.join(dir, 'src', 'config.ts'), "export const c = { n: 'demo' };\n");
|
||||||
|
fs.writeFileSync(path.join(dir, 'src', 'api.ts'), "export async function fetchFoo() { return null; }\n");
|
||||||
|
},
|
||||||
|
userPrompt:
|
||||||
|
"List every TypeScript file under src/ and tell me what each exports. " +
|
||||||
|
"You may use any tools available.",
|
||||||
|
metric: bashToolCallCount,
|
||||||
|
pass: lowerIsBetter20Pct,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'opus-4-7-effort-match-trivial-sonnet',
|
||||||
|
overlayPath: 'model-overlays/opus-4-7.md',
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
trials: 10,
|
||||||
|
concurrency: 3,
|
||||||
|
direction: 'lower_is_better',
|
||||||
|
maxTurns: 8,
|
||||||
|
setupWorkspace: (dir) => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'config.json'),
|
||||||
|
'{"name": "demo", "version": "1.0.0"}\n',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
userPrompt: "What's the version in config.json?",
|
||||||
|
metric: turnsToCompletion,
|
||||||
|
pass: lowerIsBetter20Pct,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'opus-4-7-literal-interpretation-sonnet',
|
||||||
|
overlayPath: 'model-overlays/opus-4-7.md',
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
trials: 10,
|
||||||
|
concurrency: 3,
|
||||||
|
direction: 'higher_is_better',
|
||||||
|
allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'Edit', 'Write'],
|
||||||
|
maxTurns: 15,
|
||||||
|
setupWorkspace: (dir) => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'auth.test.ts'),
|
||||||
|
"import { test, expect } from 'bun:test';\n" +
|
||||||
|
"test('auth returns true', () => { expect(1).toBe(2); });\n",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'billing.test.ts'),
|
||||||
|
"import { test, expect } from 'bun:test';\n" +
|
||||||
|
"test('billing computes total', () => { expect('ten').toBe('10'); });\n",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'notifications.test.ts'),
|
||||||
|
"import { test, expect } from 'bun:test';\n" +
|
||||||
|
"test('notifications fire on save', () => { expect([1,2]).toEqual([1,2,3]); });\n",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'README.md'),
|
||||||
|
"# demo\n\nSmall project with three failing test files.\n",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
userPrompt: "Fix the failing tests.",
|
||||||
|
metric: uniqueFilesEdited,
|
||||||
|
pass: higherIsBetter20Pct,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Validate at module load so a broken fixture fails fast at test startup,
|
||||||
|
// not mid-run after burning API dollars.
|
||||||
|
validateFixtures(OVERLAY_FIXTURES);
|
||||||
@@ -241,10 +241,11 @@ describe('gen-skill-docs', () => {
|
|||||||
expect(content).toContain('git branch --show-current');
|
expect(content).toContain('git branch --show-current');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tier 2+ skills contain ELI16 simplification rules (AskUserQuestion format)', () => {
|
test('tier 2+ skills contain ELI10 simplification rules (AskUserQuestion format)', () => {
|
||||||
// Root SKILL.md is tier 1 (no AskUserQuestion format). Check a tier 2+ skill instead.
|
// Root SKILL.md is tier 1 (no AskUserQuestion format). Check a tier 2+ skill instead.
|
||||||
|
// v1.7.0.0 Pros/Cons format uses "ELI10 (ALWAYS)" rather than "Simplify (ELI10".
|
||||||
const content = fs.readFileSync(path.join(ROOT, 'cso', 'SKILL.md'), 'utf-8');
|
const content = fs.readFileSync(path.join(ROOT, 'cso', 'SKILL.md'), 'utf-8');
|
||||||
expect(content).toContain('Simplify (ELI10');
|
expect(content).toContain('ELI10');
|
||||||
expect(content).toContain('plain English');
|
expect(content).toContain('plain English');
|
||||||
expect(content).toContain('not function names');
|
expect(content).toContain('not function names');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,509 @@
|
|||||||
|
/**
|
||||||
|
* Claude Agent SDK wrapper for the overlay-efficacy harness.
|
||||||
|
*
|
||||||
|
* This sits alongside session-runner.ts (which drives `claude -p` as a
|
||||||
|
* subprocess) but runs the model via the published @anthropic-ai/claude-agent-sdk
|
||||||
|
* instead. The SDK exposes the same harness primitives Claude Code itself uses,
|
||||||
|
* so overlay-driven behavior change is measured against a closer approximation
|
||||||
|
* of real Claude Code than the `claude -p` subprocess path provides.
|
||||||
|
*
|
||||||
|
* Explicit design rules (from plan review):
|
||||||
|
* - Use SDK-exported SDKMessage types. No `| unknown` union collapse.
|
||||||
|
* - Permission surface is explicit: bypassPermissions + settingSources:[] +
|
||||||
|
* disallowedTools inverse. Without these, the SDK inherits user settings,
|
||||||
|
* project .claude/, and local hooks, and arms are no longer comparable.
|
||||||
|
* - Binary pinning via pathToClaudeCodeExecutable. Resolve with `which claude`
|
||||||
|
* at setup time; the SDK would otherwise use its bundled binary.
|
||||||
|
* - 3-shape rate-limit detection: thrown error, result-message error subtype,
|
||||||
|
* mid-stream SDKRateLimitEvent. All three recover on retry.
|
||||||
|
* - On retry, caller resets workspace via a setupWorkspace callback so
|
||||||
|
* partial Bash side-effects don't contaminate the next attempt.
|
||||||
|
* - Process-level semaphore caps concurrent queries across all callers in
|
||||||
|
* the same bun-test process. Composes with bun's own --concurrent flag.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
query,
|
||||||
|
type SDKMessage,
|
||||||
|
type SDKAssistantMessage,
|
||||||
|
type SDKResultMessage,
|
||||||
|
type SDKSystemMessage,
|
||||||
|
type PermissionMode,
|
||||||
|
type SettingSource,
|
||||||
|
type Options,
|
||||||
|
} from '@anthropic-ai/claude-agent-sdk';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import type { SkillTestResult } from './session-runner';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface AgentSdkResult {
|
||||||
|
/** Full raw event stream for forensic recovery. */
|
||||||
|
events: SDKMessage[];
|
||||||
|
/** Assistant-typed subset, in order. */
|
||||||
|
assistantTurns: SDKAssistantMessage[];
|
||||||
|
/** Flat tool-call list, in order of emission. */
|
||||||
|
toolCalls: Array<{ tool: string; input: unknown; output: string }>;
|
||||||
|
/** Concatenated assistant text, newline-joined. */
|
||||||
|
output: string;
|
||||||
|
/** 'success' | 'error_during_execution' | 'error_max_turns' | ... */
|
||||||
|
exitReason: string;
|
||||||
|
turnsUsed: number;
|
||||||
|
durationMs: number;
|
||||||
|
firstResponseMs: number;
|
||||||
|
maxInterTurnMs: number;
|
||||||
|
costUsd: number;
|
||||||
|
model: string;
|
||||||
|
sdkVersion: string;
|
||||||
|
/** claude_code_version from the SDK's system/init event (authoritative). */
|
||||||
|
sdkClaudeCodeVersion: string;
|
||||||
|
/** Path to the claude binary we pinned. */
|
||||||
|
resolvedBinaryPath: string;
|
||||||
|
/** browse-error pattern scan for SkillTestResult parity. Always empty here. */
|
||||||
|
browseErrors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Signature matching `query()` from the SDK. DI hook for unit tests. */
|
||||||
|
export type QueryProvider = typeof query;
|
||||||
|
|
||||||
|
/** Subset of SDK Options['systemPrompt'] we support. */
|
||||||
|
export type SystemPromptOption =
|
||||||
|
| string
|
||||||
|
| { type: 'preset'; preset: 'claude_code'; append?: string; excludeDynamicSections?: boolean };
|
||||||
|
|
||||||
|
export interface RunAgentSdkOptions {
|
||||||
|
/**
|
||||||
|
* System prompt surface.
|
||||||
|
* - bare string "" -> omit entirely (SDK default: no system prompt)
|
||||||
|
* - bare string "...text..." -> REPLACE default with given text (use sparingly)
|
||||||
|
* - { type:'preset', preset:'claude_code' } -> use Claude Code default
|
||||||
|
* - { type:'preset', preset:'claude_code', append: "..." } -> default + append
|
||||||
|
*
|
||||||
|
* For overlay-efficacy measurement, the preset+append pattern is the right
|
||||||
|
* one: it measures "does adding overlay text to the REAL Claude Code system
|
||||||
|
* prompt change behavior" rather than "does the overlay alone (stripped of
|
||||||
|
* base scaffolding) change behavior".
|
||||||
|
*/
|
||||||
|
systemPrompt: SystemPromptOption;
|
||||||
|
userPrompt: string;
|
||||||
|
workingDirectory: string;
|
||||||
|
model?: string;
|
||||||
|
maxTurns?: number;
|
||||||
|
allowedTools?: string[];
|
||||||
|
disallowedTools?: string[];
|
||||||
|
permissionMode?: PermissionMode;
|
||||||
|
settingSources?: SettingSource[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
pathToClaudeCodeExecutable?: string;
|
||||||
|
testName?: string;
|
||||||
|
runId?: string;
|
||||||
|
fixtureId?: string;
|
||||||
|
queryProvider?: QueryProvider;
|
||||||
|
/** Max 429 retries per call. Default 3. */
|
||||||
|
maxRetries?: number;
|
||||||
|
/**
|
||||||
|
* Caller provides this when retry should reset the workspace. The harness
|
||||||
|
* invokes it with a fresh dir after a rate-limit failure. When omitted,
|
||||||
|
* retries reuse the original workingDirectory (fine for read-only tests).
|
||||||
|
*/
|
||||||
|
onRetry?: (freshDir: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RateLimitExhaustedError extends Error {
|
||||||
|
readonly attempts: number;
|
||||||
|
constructor(attempts: number, cause?: unknown) {
|
||||||
|
super(`rate limit exhausted after ${attempts} attempts`);
|
||||||
|
this.name = 'RateLimitExhaustedError';
|
||||||
|
this.attempts = attempts;
|
||||||
|
if (cause !== undefined) (this as { cause?: unknown }).cause = cause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Process-level semaphore for API concurrency
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bounded token bucket. Shared across all runAgentSdkTest calls in this
|
||||||
|
* process so that bun's --concurrent flag does not compound with in-test
|
||||||
|
* concurrency to blow past Anthropic's rate limits.
|
||||||
|
*
|
||||||
|
* Default capacity 3. Override via GSTACK_SDK_MAX_CONCURRENCY env var.
|
||||||
|
*/
|
||||||
|
class Semaphore {
|
||||||
|
private available: number;
|
||||||
|
private readonly queue: Array<() => void> = [];
|
||||||
|
constructor(capacity: number) {
|
||||||
|
this.available = capacity;
|
||||||
|
}
|
||||||
|
async acquire(): Promise<void> {
|
||||||
|
if (this.available > 0) {
|
||||||
|
this.available--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await new Promise<void>((resolve) => this.queue.push(resolve));
|
||||||
|
}
|
||||||
|
release(): void {
|
||||||
|
const next = this.queue.shift();
|
||||||
|
if (next) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
this.available++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** For tests. Returns tokens currently in-flight. */
|
||||||
|
inFlight(): number {
|
||||||
|
// Not introspectable from outside without tracking; approximate.
|
||||||
|
return this.queue.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SDK_CONCURRENCY = Number(process.env.GSTACK_SDK_MAX_CONCURRENCY ?? 3);
|
||||||
|
let _apiSemaphore: Semaphore | null = null;
|
||||||
|
function getApiSemaphore(): Semaphore {
|
||||||
|
if (!_apiSemaphore) _apiSemaphore = new Semaphore(DEFAULT_SDK_CONCURRENCY);
|
||||||
|
return _apiSemaphore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test-only. Resets the process-level semaphore. */
|
||||||
|
export function __resetSemaphoreForTests(capacity: number): void {
|
||||||
|
_apiSemaphore = new Semaphore(capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Rate-limit detection
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** True if `err` looks like a rate-limit thrown from the SDK. */
|
||||||
|
export function isRateLimitThrown(err: unknown): boolean {
|
||||||
|
if (!err || typeof err !== 'object') return false;
|
||||||
|
const msg = (err as { message?: string }).message ?? '';
|
||||||
|
const name = (err as { name?: string }).name ?? '';
|
||||||
|
const status = (err as { status?: number }).status;
|
||||||
|
return (
|
||||||
|
status === 429 ||
|
||||||
|
/rate.?limit|429|too many requests/i.test(msg) ||
|
||||||
|
/RateLimit/i.test(name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True if a SDKResultMessage is a rate-limit-shaped error. */
|
||||||
|
export function isRateLimitResult(msg: SDKMessage): boolean {
|
||||||
|
if (msg.type !== 'result') return false;
|
||||||
|
const r = msg as SDKResultMessage;
|
||||||
|
if (r.subtype === 'success') return false;
|
||||||
|
// subtype === 'error_during_execution' | 'error_max_turns' | 'error_max_budget_usd' | ...
|
||||||
|
if (r.subtype !== 'error_during_execution') return false;
|
||||||
|
const errs = (r as { errors?: string[] }).errors ?? [];
|
||||||
|
return errs.some((e) => /rate.?limit|429|too many requests/i.test(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True if mid-stream SDKRateLimitEvent indicates a blocking rate-limit. */
|
||||||
|
export function isRateLimitEvent(msg: SDKMessage): boolean {
|
||||||
|
if (msg.type !== 'rate_limit_event') return false;
|
||||||
|
const info = (msg as { rate_limit_info?: { status?: string } }).rate_limit_info;
|
||||||
|
return info?.status === 'rejected';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if `err` is the SDK's "max turns reached" throw. Some SDK versions
|
||||||
|
* raise this as an exception from the generator instead of emitting a
|
||||||
|
* result message with subtype='error_max_turns'. We treat it as terminal-
|
||||||
|
* but-recoverable: record what we collected and continue, rather than
|
||||||
|
* failing the whole run.
|
||||||
|
*/
|
||||||
|
export function isMaxTurnsError(err: unknown): boolean {
|
||||||
|
if (!err || typeof err !== 'object') return false;
|
||||||
|
const msg = (err as { message?: string }).message ?? '';
|
||||||
|
return /reached maximum number of turns|max.?turns/i.test(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Version resolution (cached)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let _sdkVersionCache: string | null = null;
|
||||||
|
function resolveSdkVersion(): string {
|
||||||
|
if (_sdkVersionCache) return _sdkVersionCache;
|
||||||
|
try {
|
||||||
|
const pkgPath = require.resolve('@anthropic-ai/claude-agent-sdk/package.json');
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version?: string };
|
||||||
|
_sdkVersionCache = pkg.version ?? 'unknown';
|
||||||
|
} catch {
|
||||||
|
_sdkVersionCache = 'unknown';
|
||||||
|
}
|
||||||
|
return _sdkVersionCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveClaudeBinary(): string | null {
|
||||||
|
try {
|
||||||
|
return execSync('which claude', { encoding: 'utf-8' }).trim() || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Main runner
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a single SDK query with retries. Returns a typed result.
|
||||||
|
*
|
||||||
|
* The retry loop treats 429 as recoverable and any other error as fatal.
|
||||||
|
* Exponential backoff: 1s, 2s, 4s. After maxRetries failures, throws
|
||||||
|
* RateLimitExhaustedError so the caller can decide what to do with the run.
|
||||||
|
*/
|
||||||
|
export async function runAgentSdkTest(
|
||||||
|
opts: RunAgentSdkOptions,
|
||||||
|
): Promise<AgentSdkResult> {
|
||||||
|
const sem = getApiSemaphore();
|
||||||
|
const maxRetries = opts.maxRetries ?? 3;
|
||||||
|
const queryImpl: QueryProvider = opts.queryProvider ?? query;
|
||||||
|
const model = opts.model ?? 'claude-opus-4-7';
|
||||||
|
|
||||||
|
let attempt = 0;
|
||||||
|
let lastErr: unknown = null;
|
||||||
|
|
||||||
|
while (attempt <= maxRetries) {
|
||||||
|
await sem.acquire();
|
||||||
|
const startMs = Date.now();
|
||||||
|
|
||||||
|
// Hoisted so the max-turns catch branch can synthesize a result from
|
||||||
|
// whatever we captured before the SDK threw.
|
||||||
|
const events: SDKMessage[] = [];
|
||||||
|
const assistantTurns: SDKAssistantMessage[] = [];
|
||||||
|
const toolCalls: Array<{ tool: string; input: unknown; output: string }> = [];
|
||||||
|
const assistantTextParts: string[] = [];
|
||||||
|
let firstResponseMs = 0;
|
||||||
|
let lastEventMs = startMs;
|
||||||
|
let maxInterTurnMs = 0;
|
||||||
|
let systemInitVersion = 'unknown';
|
||||||
|
let rateLimited: unknown = null;
|
||||||
|
let terminalResult: SDKResultMessage | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sdkOpts: Options = {
|
||||||
|
model,
|
||||||
|
cwd: opts.workingDirectory,
|
||||||
|
maxTurns: opts.maxTurns ?? 5,
|
||||||
|
tools: opts.allowedTools ?? ['Read', 'Glob', 'Grep', 'Bash'],
|
||||||
|
disallowedTools: opts.disallowedTools,
|
||||||
|
allowedTools: opts.allowedTools ?? ['Read', 'Glob', 'Grep', 'Bash'],
|
||||||
|
permissionMode: opts.permissionMode ?? 'bypassPermissions',
|
||||||
|
allowDangerouslySkipPermissions:
|
||||||
|
(opts.permissionMode ?? 'bypassPermissions') === 'bypassPermissions',
|
||||||
|
settingSources: opts.settingSources ?? [],
|
||||||
|
env: opts.env,
|
||||||
|
pathToClaudeCodeExecutable: opts.pathToClaudeCodeExecutable,
|
||||||
|
};
|
||||||
|
// Empty bare string means "omit entirely" (SDK runs with no override).
|
||||||
|
// Any object or non-empty string is passed through.
|
||||||
|
if (typeof opts.systemPrompt === 'object' || opts.systemPrompt !== '') {
|
||||||
|
sdkOpts.systemPrompt = opts.systemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = queryImpl({
|
||||||
|
prompt: opts.userPrompt,
|
||||||
|
options: sdkOpts,
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const ev of q) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (firstResponseMs === 0) firstResponseMs = now - startMs;
|
||||||
|
const interTurn = now - lastEventMs;
|
||||||
|
if (interTurn > maxInterTurnMs) maxInterTurnMs = interTurn;
|
||||||
|
lastEventMs = now;
|
||||||
|
|
||||||
|
events.push(ev);
|
||||||
|
|
||||||
|
if (ev.type === 'system' && (ev as SDKSystemMessage).subtype === 'init') {
|
||||||
|
systemInitVersion =
|
||||||
|
(ev as SDKSystemMessage).claude_code_version ?? 'unknown';
|
||||||
|
} else if (ev.type === 'assistant') {
|
||||||
|
const am = ev as SDKAssistantMessage;
|
||||||
|
assistantTurns.push(am);
|
||||||
|
const content = am.message?.content;
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
for (const block of content as Array<
|
||||||
|
| { type: 'text'; text?: string }
|
||||||
|
| { type: 'tool_use'; name?: string; input?: unknown }
|
||||||
|
| { type: string }
|
||||||
|
>) {
|
||||||
|
if (block.type === 'text') {
|
||||||
|
const t = (block as { text?: string }).text;
|
||||||
|
if (t) assistantTextParts.push(t);
|
||||||
|
} else if (block.type === 'tool_use') {
|
||||||
|
const tb = block as { name?: string; input?: unknown };
|
||||||
|
toolCalls.push({
|
||||||
|
tool: tb.name ?? 'unknown',
|
||||||
|
input: tb.input ?? {},
|
||||||
|
output: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isRateLimitEvent(ev)) {
|
||||||
|
rateLimited = new Error(
|
||||||
|
`mid-stream rate limit: ${JSON.stringify(
|
||||||
|
(ev as { rate_limit_info?: unknown }).rate_limit_info,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
} else if (ev.type === 'result') {
|
||||||
|
terminalResult = ev as SDKResultMessage;
|
||||||
|
if (isRateLimitResult(ev)) {
|
||||||
|
rateLimited = new Error(
|
||||||
|
`result-message rate limit: ${((ev as { errors?: string[] }).errors ?? []).join('; ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rateLimited) {
|
||||||
|
throw rateLimited;
|
||||||
|
}
|
||||||
|
if (!terminalResult) {
|
||||||
|
throw new Error('query stream ended without a result event');
|
||||||
|
}
|
||||||
|
|
||||||
|
const durationMs = Date.now() - startMs;
|
||||||
|
const costUsd =
|
||||||
|
(terminalResult as { total_cost_usd?: number }).total_cost_usd ?? 0;
|
||||||
|
const turnsUsed =
|
||||||
|
(terminalResult as { num_turns?: number }).num_turns ??
|
||||||
|
assistantTurns.length;
|
||||||
|
const exitReason =
|
||||||
|
(terminalResult as { subtype?: string }).subtype ?? 'unknown';
|
||||||
|
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
assistantTurns,
|
||||||
|
toolCalls,
|
||||||
|
output: assistantTextParts.join('\n'),
|
||||||
|
exitReason,
|
||||||
|
turnsUsed,
|
||||||
|
durationMs,
|
||||||
|
firstResponseMs,
|
||||||
|
maxInterTurnMs,
|
||||||
|
costUsd,
|
||||||
|
model,
|
||||||
|
sdkVersion: resolveSdkVersion(),
|
||||||
|
sdkClaudeCodeVersion: systemInitVersion,
|
||||||
|
resolvedBinaryPath: opts.pathToClaudeCodeExecutable ?? 'sdk-default',
|
||||||
|
browseErrors: [],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
lastErr = err;
|
||||||
|
|
||||||
|
// "Max turns reached" is the SDK's way of saying "this session ran
|
||||||
|
// out of turns." It's thrown from the generator instead of emitted
|
||||||
|
// as a result message. Treat as a successful-but-capped trial: the
|
||||||
|
// assistant turns we collected are real and carry a metric. Record
|
||||||
|
// them with exitReason='error_max_turns' rather than failing the
|
||||||
|
// whole run.
|
||||||
|
if (isMaxTurnsError(err)) {
|
||||||
|
const durationMs = Date.now() - startMs;
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
assistantTurns,
|
||||||
|
toolCalls,
|
||||||
|
output: assistantTextParts.join('\n'),
|
||||||
|
exitReason: 'error_max_turns',
|
||||||
|
turnsUsed: assistantTurns.length,
|
||||||
|
durationMs,
|
||||||
|
firstResponseMs,
|
||||||
|
maxInterTurnMs,
|
||||||
|
costUsd: 0, // unknown from thrown-error path
|
||||||
|
model,
|
||||||
|
sdkVersion: resolveSdkVersion(),
|
||||||
|
sdkClaudeCodeVersion: systemInitVersion,
|
||||||
|
resolvedBinaryPath: opts.pathToClaudeCodeExecutable ?? 'sdk-default',
|
||||||
|
browseErrors: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRetryable = isRateLimitThrown(err);
|
||||||
|
if (!isRetryable || attempt >= maxRetries) {
|
||||||
|
if (isRetryable) {
|
||||||
|
throw new RateLimitExhaustedError(attempt + 1, err);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
attempt++;
|
||||||
|
// backoff: 1s, 2s, 4s
|
||||||
|
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));
|
||||||
|
// Let caller reset workspace since prior attempt may have partially
|
||||||
|
// mutated files via Bash.
|
||||||
|
if (opts.onRetry) {
|
||||||
|
opts.onRetry(opts.workingDirectory);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
sem.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RateLimitExhaustedError(attempt + 1, lastErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Legacy shape mapper
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapt AgentSdkResult to the legacy SkillTestResult shape so helpers that
|
||||||
|
* expect the old `claude -p` output (extractToolSummary, etc) work unchanged.
|
||||||
|
*/
|
||||||
|
export function toSkillTestResult(r: AgentSdkResult): SkillTestResult {
|
||||||
|
// Cost estimate: use SDK's authoritative cost; back-compute chars.
|
||||||
|
// session-runner.ts:30 requires inputChars/outputChars/estimatedTokens.
|
||||||
|
// These are rough; real consumers of CostEstimate use cost + turns.
|
||||||
|
const outputChars = r.output.length;
|
||||||
|
const inputChars = 0; // unknown from SDK path; not used for pass/fail
|
||||||
|
const estimatedTokens = Math.round((inputChars + outputChars) / 4);
|
||||||
|
|
||||||
|
// Build a flat transcript list mimicking the NDJSON shape:
|
||||||
|
// parseNDJSON emits [{ type: 'assistant', message: {...} }, ...].
|
||||||
|
// Use the SDK's assistantTurns directly since their shape matches.
|
||||||
|
const transcript: unknown[] = r.events.slice();
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolCalls: r.toolCalls,
|
||||||
|
browseErrors: r.browseErrors,
|
||||||
|
exitReason: r.exitReason,
|
||||||
|
duration: r.durationMs,
|
||||||
|
output: r.output,
|
||||||
|
costEstimate: {
|
||||||
|
inputChars,
|
||||||
|
outputChars,
|
||||||
|
estimatedTokens,
|
||||||
|
estimatedCost: r.costUsd,
|
||||||
|
turnsUsed: r.turnsUsed,
|
||||||
|
},
|
||||||
|
transcript,
|
||||||
|
model: r.model,
|
||||||
|
firstResponseMs: r.firstResponseMs,
|
||||||
|
maxInterTurnMs: r.maxInterTurnMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Metric helpers (re-exported for fixtures)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count `tool_use` blocks in the first assistant turn of an SDK result.
|
||||||
|
* Returns 0 if there is no first turn or no content array.
|
||||||
|
*
|
||||||
|
* This is the core "fanout" metric. A turn with N tool_use blocks = N
|
||||||
|
* parallel tool invocations.
|
||||||
|
*/
|
||||||
|
export function firstTurnParallelism(firstTurn: SDKAssistantMessage | undefined): number {
|
||||||
|
if (!firstTurn) return 0;
|
||||||
|
const content = firstTurn.message?.content;
|
||||||
|
if (!Array.isArray(content)) return 0;
|
||||||
|
return (content as Array<{ type: string }>).filter((b) => b.type === 'tool_use').length;
|
||||||
|
}
|
||||||
@@ -84,10 +84,27 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
|||||||
|
|
||||||
// AskUserQuestion format regression (RECOMMENDATION + Completeness: N/10)
|
// AskUserQuestion format regression (RECOMMENDATION + Completeness: N/10)
|
||||||
// Fires when either template OR the two preamble resolvers change.
|
// Fires when either template OR the two preamble resolvers change.
|
||||||
'plan-ceo-review-format-mode': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts'],
|
'plan-ceo-review-format-mode': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
'plan-ceo-review-format-approach': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts'],
|
'plan-ceo-review-format-approach': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
'plan-eng-review-format-coverage': ['plan-eng-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts'],
|
'plan-eng-review-format-coverage': ['plan-eng-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
'plan-eng-review-format-kind': ['plan-eng-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts'],
|
'plan-eng-review-format-kind': ['plan-eng-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
|
|
||||||
|
// v1.7.0.0 Pros/Cons format cadence + format + negative-escape evals.
|
||||||
|
// Dependencies: same as format-mode + the 4 plan-review templates + overlay.
|
||||||
|
// All periodic-tier (non-deterministic Opus 4.7 behavior).
|
||||||
|
'plan-ceo-review-prosons-cadence': ['plan-ceo-review/**', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
|
'plan-review-prosons-format': ['plan-ceo-review/**', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
|
'plan-review-prosons-hardstop-neg': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
|
'plan-review-prosons-neutral-neg': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
|
|
||||||
|
// Expanded coverage (CT3) — 6 non-plan-review skills inherit Pros/Cons via preamble
|
||||||
|
'ship-prosons-format': ['ship/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
|
'office-hours-prosons-format': ['office-hours/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
|
'investigate-prosons-format': ['investigate/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
|
'qa-prosons-format': ['qa/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
|
'review-prosons-format': ['review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
|
'design-review-prosons-format': ['design-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
|
'document-release-prosons-format': ['document-release/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md'],
|
||||||
|
|
||||||
// /plan-tune (v1 observational)
|
// /plan-tune (v1 observational)
|
||||||
'plan-tune-inspect': ['plan-tune/**', 'scripts/question-registry.ts', 'scripts/psychographic-signals.ts', 'scripts/one-way-doors.ts', 'bin/gstack-question-log', 'bin/gstack-question-preference', 'bin/gstack-developer-profile'],
|
'plan-tune-inspect': ['plan-tune/**', 'scripts/question-registry.ts', 'scripts/psychographic-signals.ts', 'scripts/one-way-doors.ts', 'bin/gstack-question-log', 'bin/gstack-question-preference', 'bin/gstack-developer-profile'],
|
||||||
@@ -222,6 +239,24 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
|||||||
['model-overlays/claude.md', 'model-overlays/opus-4-7.md', 'scripts/models.ts', 'scripts/resolvers/model-overlay.ts'],
|
['model-overlays/claude.md', 'model-overlays/opus-4-7.md', 'scripts/models.ts', 'scripts/resolvers/model-overlay.ts'],
|
||||||
'fanout-arm-overlay-off':
|
'fanout-arm-overlay-off':
|
||||||
['model-overlays/claude.md', 'model-overlays/opus-4-7.md', 'scripts/models.ts', 'scripts/resolvers/model-overlay.ts'],
|
['model-overlays/claude.md', 'model-overlays/opus-4-7.md', 'scripts/models.ts', 'scripts/resolvers/model-overlay.ts'],
|
||||||
|
|
||||||
|
// Overlay efficacy harness (SDK) — measures whether overlay nudges change
|
||||||
|
// behavior under @anthropic-ai/claude-agent-sdk (closer to real Claude Code
|
||||||
|
// than `claude -p`). testNames in the file are template literals so the
|
||||||
|
// completeness scanner doesn't require them; these entries exist for
|
||||||
|
// diff-based selection accuracy.
|
||||||
|
'overlay-harness-opus-4-7-fanout-toy': [
|
||||||
|
'model-overlays/**',
|
||||||
|
'test/fixtures/overlay-nudges.ts',
|
||||||
|
'test/helpers/agent-sdk-runner.ts',
|
||||||
|
'scripts/resolvers/model-overlay.ts',
|
||||||
|
],
|
||||||
|
'overlay-harness-opus-4-7-fanout-realistic': [
|
||||||
|
'model-overlays/**',
|
||||||
|
'test/fixtures/overlay-nudges.ts',
|
||||||
|
'test/helpers/agent-sdk-runner.ts',
|
||||||
|
'scripts/resolvers/model-overlay.ts',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -288,6 +323,21 @@ export const E2E_TIERS: Record<string, 'gate' | 'periodic'> = {
|
|||||||
'plan-eng-review-format-coverage': 'periodic',
|
'plan-eng-review-format-coverage': 'periodic',
|
||||||
'plan-eng-review-format-kind': 'periodic',
|
'plan-eng-review-format-kind': 'periodic',
|
||||||
|
|
||||||
|
// v1.7.0.0 Pros/Cons format — cadence + negative-escape evals (all periodic)
|
||||||
|
'plan-ceo-review-prosons-cadence': 'periodic',
|
||||||
|
'plan-review-prosons-format': 'periodic',
|
||||||
|
'plan-review-prosons-hardstop-neg': 'periodic',
|
||||||
|
'plan-review-prosons-neutral-neg': 'periodic',
|
||||||
|
|
||||||
|
// CT3 expanded coverage — non-plan-review skills inheriting Pros/Cons (all periodic)
|
||||||
|
'ship-prosons-format': 'periodic',
|
||||||
|
'office-hours-prosons-format': 'periodic',
|
||||||
|
'investigate-prosons-format': 'periodic',
|
||||||
|
'qa-prosons-format': 'periodic',
|
||||||
|
'review-prosons-format': 'periodic',
|
||||||
|
'design-review-prosons-format': 'periodic',
|
||||||
|
'document-release-prosons-format': 'periodic',
|
||||||
|
|
||||||
// /plan-tune — gate (core v1 DX promise: plain-English intent routing)
|
// /plan-tune — gate (core v1 DX promise: plain-English intent routing)
|
||||||
'plan-tune-inspect': 'gate',
|
'plan-tune-inspect': 'gate',
|
||||||
|
|
||||||
@@ -398,6 +448,10 @@ export const E2E_TIERS: Record<string, 'gate' | 'periodic'> = {
|
|||||||
// Opus 4.7 overlay evals — periodic (non-deterministic LLM behavior + Opus cost)
|
// Opus 4.7 overlay evals — periodic (non-deterministic LLM behavior + Opus cost)
|
||||||
'fanout-arm-overlay-on': 'periodic',
|
'fanout-arm-overlay-on': 'periodic',
|
||||||
'fanout-arm-overlay-off': 'periodic',
|
'fanout-arm-overlay-off': 'periodic',
|
||||||
|
|
||||||
|
// Overlay efficacy harness (SDK, paid) — periodic only
|
||||||
|
'overlay-harness-opus-4-7-fanout-toy': 'periodic',
|
||||||
|
'overlay-harness-opus-4-7-fanout-realistic': 'periodic',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Opus 4.7 model overlay — gate-tier assertions on the pacing directive.
|
||||||
|
*
|
||||||
|
* v1.6.4.0 regressed plan-review cadence because the Opus 4.7 overlay
|
||||||
|
* carried a "Batch your questions" directive that physically rendered
|
||||||
|
* above the skill-level pacing rule. Opus 4.7 read top-to-bottom,
|
||||||
|
* absorbed batching as the ambient default, and stopped honoring the
|
||||||
|
* plan-review STOP directives.
|
||||||
|
*
|
||||||
|
* v1.7.0.0 replaces that block with "Pace questions to the skill" —
|
||||||
|
* one-question-at-a-time is now the default when the skill contains
|
||||||
|
* STOP directives; batching becomes the explicit exception.
|
||||||
|
*
|
||||||
|
* This test asserts:
|
||||||
|
* - The new "Pace questions" directive is present
|
||||||
|
* - The old "Batch your questions" directive is gone
|
||||||
|
* - The AUTO_DECIDE-compatible language survives (subordination, skill wins)
|
||||||
|
*/
|
||||||
|
import { describe, test, expect } from 'bun:test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import type { TemplateContext } from '../scripts/resolvers/types';
|
||||||
|
import { HOST_PATHS } from '../scripts/resolvers/types';
|
||||||
|
import { generateModelOverlay } from '../scripts/resolvers/model-overlay';
|
||||||
|
|
||||||
|
function makeCtx(model: string): TemplateContext {
|
||||||
|
return {
|
||||||
|
skillName: 'test-skill',
|
||||||
|
tmplPath: 'test.tmpl',
|
||||||
|
host: 'claude',
|
||||||
|
paths: HOST_PATHS.claude,
|
||||||
|
preambleTier: 2,
|
||||||
|
model,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
describe('Opus 4.7 overlay — pacing directive', () => {
|
||||||
|
test('raw opus-4-7.md contains "Pace questions to the skill"', () => {
|
||||||
|
const raw = fs.readFileSync(
|
||||||
|
path.join(ROOT, 'model-overlays/opus-4-7.md'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
expect(raw).toContain('Pace questions to the skill');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('raw opus-4-7.md does NOT contain "Batch your questions" directive', () => {
|
||||||
|
const raw = fs.readFileSync(
|
||||||
|
path.join(ROOT, 'model-overlays/opus-4-7.md'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
expect(raw).not.toContain('**Batch your questions.**');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolved overlay output contains "Pace questions to the skill"', () => {
|
||||||
|
const out = generateModelOverlay(makeCtx('opus-4-7'));
|
||||||
|
expect(out).toContain('Pace questions to the skill');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolved overlay inherits from claude base (INHERIT:claude)', () => {
|
||||||
|
const out = generateModelOverlay(makeCtx('opus-4-7'));
|
||||||
|
// The claude base contributes the subordination wrapper + Todo discipline
|
||||||
|
expect(out).toContain('Todo-list discipline');
|
||||||
|
expect(out).toContain('subordinate');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolved overlay says skill STOP directives trigger one-per-turn pacing', () => {
|
||||||
|
const out = generateModelOverlay(makeCtx('opus-4-7'));
|
||||||
|
expect(out).toMatch(/STOP\. AskUserQuestion/);
|
||||||
|
expect(out).toMatch(/pace one question per turn|one question per turn/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolved overlay requires AskUserQuestion as tool_use', () => {
|
||||||
|
const out = generateModelOverlay(makeCtx('opus-4-7'));
|
||||||
|
expect(out).toContain('tool_use');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolved overlay flags "obvious fix" findings still need user approval', () => {
|
||||||
|
const out = generateModelOverlay(makeCtx('opus-4-7'));
|
||||||
|
expect(out).toMatch(/obvious fix/i);
|
||||||
|
expect(out).toMatch(/user approval/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolved overlay keeps Fan out / Effort-match / Literal interpretation nudges', () => {
|
||||||
|
const out = generateModelOverlay(makeCtx('opus-4-7'));
|
||||||
|
expect(out).toContain('Fan out explicitly');
|
||||||
|
expect(out).toContain('Effort-match the step');
|
||||||
|
expect(out).toContain('Literal interpretation awareness');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('claude overlay (no INHERIT chain) does not carry the pacing directive', () => {
|
||||||
|
// Claude is the default overlay; opus-4-7 inherits FROM claude.
|
||||||
|
// The pacing directive belongs to opus-4-7 only.
|
||||||
|
const out = generateModelOverlay(makeCtx('claude'));
|
||||||
|
expect(out).not.toContain('Pace questions to the skill');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Preamble composition order — gate-tier test.
|
||||||
|
*
|
||||||
|
* Asserts that the AskUserQuestion Format section renders BEFORE the
|
||||||
|
* Model-Specific Behavioral Patch section in tier-≥2 preamble output.
|
||||||
|
* This order is load-bearing: Opus 4.7 reads top-to-bottom and absorbs
|
||||||
|
* the first pacing directive it hits. v1.6.4.0 regressed plan-review
|
||||||
|
* cadence because the overlay rendered first with "Batch your questions"
|
||||||
|
* as the ambient default.
|
||||||
|
*
|
||||||
|
* If someone later reorders `scripts/resolvers/preamble.ts` so Overlay
|
||||||
|
* comes before Format, this test catches it before the next model
|
||||||
|
* migration can silently re-break the plan-review pacing.
|
||||||
|
*/
|
||||||
|
import { describe, test, expect } from 'bun:test';
|
||||||
|
import type { TemplateContext } from '../scripts/resolvers/types';
|
||||||
|
import { HOST_PATHS } from '../scripts/resolvers/types';
|
||||||
|
import { generatePreamble } from '../scripts/resolvers/preamble';
|
||||||
|
|
||||||
|
function makeCtx(
|
||||||
|
host: 'claude' | 'codex',
|
||||||
|
tier: 1 | 2 | 3 | 4,
|
||||||
|
model?: string,
|
||||||
|
): TemplateContext {
|
||||||
|
return {
|
||||||
|
skillName: 'test-skill',
|
||||||
|
tmplPath: 'test.tmpl',
|
||||||
|
host,
|
||||||
|
paths: HOST_PATHS[host],
|
||||||
|
preambleTier: tier,
|
||||||
|
...(model ? { model } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Preamble composition order', () => {
|
||||||
|
test('AskUserQuestion Format renders before Model-Specific Behavioral Patch (tier 2, claude)', () => {
|
||||||
|
const out = generatePreamble(makeCtx('claude', 2, 'claude'));
|
||||||
|
const formatIdx = out.indexOf('## AskUserQuestion Format');
|
||||||
|
const overlayIdx = out.indexOf('## Model-Specific Behavioral Patch');
|
||||||
|
expect(formatIdx).toBeGreaterThan(-1);
|
||||||
|
expect(overlayIdx).toBeGreaterThan(-1);
|
||||||
|
expect(formatIdx).toBeLessThan(overlayIdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AskUserQuestion Format renders before Model-Specific Behavioral Patch (tier 2, opus-4-7)', () => {
|
||||||
|
const out = generatePreamble(makeCtx('claude', 2, 'opus-4-7'));
|
||||||
|
const formatIdx = out.indexOf('## AskUserQuestion Format');
|
||||||
|
const overlayIdx = out.indexOf('## Model-Specific Behavioral Patch');
|
||||||
|
expect(formatIdx).toBeGreaterThan(-1);
|
||||||
|
expect(overlayIdx).toBeGreaterThan(-1);
|
||||||
|
expect(formatIdx).toBeLessThan(overlayIdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AskUserQuestion Format renders before Model-Specific Behavioral Patch (tier 3)', () => {
|
||||||
|
const out = generatePreamble(makeCtx('claude', 3, 'opus-4-7'));
|
||||||
|
const formatIdx = out.indexOf('## AskUserQuestion Format');
|
||||||
|
const overlayIdx = out.indexOf('## Model-Specific Behavioral Patch');
|
||||||
|
expect(formatIdx).toBeLessThan(overlayIdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AskUserQuestion Format renders before Model-Specific Behavioral Patch (codex host)', () => {
|
||||||
|
const out = generatePreamble(makeCtx('codex', 2, 'opus-4-7'));
|
||||||
|
const formatIdx = out.indexOf('## AskUserQuestion Format');
|
||||||
|
const overlayIdx = out.indexOf('## Model-Specific Behavioral Patch');
|
||||||
|
expect(formatIdx).toBeLessThan(overlayIdx);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tier 1 preamble does NOT include AskUserQuestion Format (but MAY include overlay)', () => {
|
||||||
|
const out = generatePreamble(makeCtx('claude', 1));
|
||||||
|
expect(out).not.toContain('## AskUserQuestion Format');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* AskUserQuestion Format resolver — gate-tier assertions on the generated
|
||||||
|
* Pros/Cons format directive block.
|
||||||
|
*
|
||||||
|
* v1.7.0.0 introduces Pros/Cons decision-brief formatting:
|
||||||
|
* - D<N> numbered header
|
||||||
|
* - ELI10 paragraph
|
||||||
|
* - Stakes-if-we-pick-wrong line
|
||||||
|
* - Recommendation line (mandatory, even for neutral posture)
|
||||||
|
* - Pros/Cons block with ✅/❌ per option, min 2 pros + 1 con, ≥40 char bullets
|
||||||
|
* - Net: synthesis line
|
||||||
|
*
|
||||||
|
* This test pins the format contract so a future edit to the resolver
|
||||||
|
* can't silently drop a rule. If the resolver stops emitting one of
|
||||||
|
* these tokens, bun test catches it in milliseconds instead of waiting
|
||||||
|
* for the weekly periodic eval to notice.
|
||||||
|
*/
|
||||||
|
import { describe, test, expect } from 'bun:test';
|
||||||
|
import type { TemplateContext } from '../scripts/resolvers/types';
|
||||||
|
import { HOST_PATHS } from '../scripts/resolvers/types';
|
||||||
|
import { generateAskUserFormat } from '../scripts/resolvers/preamble/generate-ask-user-format';
|
||||||
|
|
||||||
|
function makeCtx(): TemplateContext {
|
||||||
|
return {
|
||||||
|
skillName: 'test-skill',
|
||||||
|
tmplPath: 'test.tmpl',
|
||||||
|
host: 'claude',
|
||||||
|
paths: HOST_PATHS.claude,
|
||||||
|
preambleTier: 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('generateAskUserFormat — v1.7.0.0 Pros/Cons format', () => {
|
||||||
|
const out = generateAskUserFormat(makeCtx());
|
||||||
|
|
||||||
|
test('includes AskUserQuestion Format header', () => {
|
||||||
|
expect(out).toContain('## AskUserQuestion Format');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('documents D-numbered header requirement', () => {
|
||||||
|
expect(out).toContain('D<N>');
|
||||||
|
expect(out).toMatch(/first question in a skill invocation is `D1`/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('documents ELI10 requirement', () => {
|
||||||
|
expect(out).toContain('ELI10');
|
||||||
|
expect(out).toMatch(/plain English.*16-year-old/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('documents Stakes-if-we-pick-wrong line', () => {
|
||||||
|
expect(out).toContain('Stakes if we pick wrong');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('documents mandatory Recommendation line', () => {
|
||||||
|
expect(out).toContain('Recommendation: <choice>');
|
||||||
|
expect(out).toMatch(/Recommendation.*ALWAYS|Recommendation \(ALWAYS\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('documents Pros / cons block header', () => {
|
||||||
|
expect(out).toContain('Pros / cons:');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('documents ✅ pro markers with min count + min length rule', () => {
|
||||||
|
expect(out).toContain('✅');
|
||||||
|
expect(out).toMatch(/[Mm]inimum 2 pros/);
|
||||||
|
expect(out).toMatch(/40 characters|≥40 chars/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('documents ❌ con markers with min count rule', () => {
|
||||||
|
expect(out).toContain('❌');
|
||||||
|
expect(out).toMatch(/1 con per option|minimum.*1 con/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('documents hard-stop escape with exact phrase', () => {
|
||||||
|
// "No cons — this is a hard-stop choice" may span a line break in the
|
||||||
|
// rendered resolver text; match across whitespace collapses.
|
||||||
|
expect(out).toMatch(/No cons\s+—\s+this is a\s+hard-stop choice/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('documents neutral-posture escape preserving (recommended) label', () => {
|
||||||
|
// CT1 resolution: (recommended) label STAYS on default option to preserve
|
||||||
|
// AUTO_DECIDE contract. Neutrality expressed in prose only.
|
||||||
|
expect(out).toMatch(/taste call/i);
|
||||||
|
// `s` flag makes . match newlines — the label + STAYS phrase spans a line break
|
||||||
|
expect(out).toMatch(/\(recommended\)[\s\S]*STAYS|STAYS[\s\S]*\(recommended\)/);
|
||||||
|
expect(out).toMatch(/AUTO_DECIDE/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('documents Net line for closing synthesis', () => {
|
||||||
|
expect(out).toMatch(/^Net:/m);
|
||||||
|
expect(out).toMatch(/synthesis|tradeoff/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('documents Completeness scoring rules (coverage vs kind)', () => {
|
||||||
|
expect(out).toContain('Completeness');
|
||||||
|
expect(out).toMatch(/10 = complete/);
|
||||||
|
expect(out).toMatch(/options differ in kind, not coverage/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('documents tool_use mandate (rule 11)', () => {
|
||||||
|
expect(out).toMatch(/tool_use/);
|
||||||
|
// "not a question" spans a newline in the rendered text
|
||||||
|
expect(out).toMatch(/not a[\s\S]*question|not[\s\S]*interactive/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('includes self-check before emitting', () => {
|
||||||
|
expect(out).toContain('Self-check before emitting');
|
||||||
|
expect(out).toMatch(/D<N> header present/);
|
||||||
|
expect(out).toMatch(/Net line closes/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('documents D-numbering as model-level not runtime state', () => {
|
||||||
|
// Codex finding #4 caveat: D-numbering is a prompt wish, not a system
|
||||||
|
// guarantee. TemplateContext has no counter. This check pins the caveat.
|
||||||
|
expect(out).toMatch(/model-level instruction|not a runtime counter|count your own/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('per-skill override guidance preserved', () => {
|
||||||
|
expect(out).toMatch(/Per-skill instructions may add/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -286,11 +286,38 @@ Log the operational learning now. Then say what you logged.`,
|
|||||||
// Add a remote so the agent can derive a project name
|
// Add a remote so the agent can derive a project name
|
||||||
run('git', ['remote', 'add', 'origin', 'https://github.com/acme/billing-app.git']);
|
run('git', ['remote', 'add', 'origin', 'https://github.com/acme/billing-app.git']);
|
||||||
|
|
||||||
// Extract AskUserQuestion format instructions from generated SKILL.md
|
// Extract AskUserQuestion format instructions from a generated SKILL.md.
|
||||||
const skillMd = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
// ROOT/SKILL.md is the browse skill (Tier 1) and does NOT contain the
|
||||||
|
// "## AskUserQuestion Format" section — that block is only emitted for
|
||||||
|
// Tier 2+ skills by scripts/resolvers/preamble.ts. Use office-hours/SKILL.md
|
||||||
|
// (Tier 3) which always has the format guidance baked in. Falls back to
|
||||||
|
// the first SKILL.md that contains the header so a future template move
|
||||||
|
// doesn't break this test again.
|
||||||
|
let skillMdPath = path.join(ROOT, 'office-hours', 'SKILL.md');
|
||||||
|
let skillMd = '';
|
||||||
|
if (fs.existsSync(skillMdPath)) {
|
||||||
|
skillMd = fs.readFileSync(skillMdPath, 'utf-8');
|
||||||
|
}
|
||||||
|
if (!skillMd.includes('## AskUserQuestion Format')) {
|
||||||
|
// Fallback: scan top-level skill dirs for the first match.
|
||||||
|
const skillDirs = fs.readdirSync(ROOT, { withFileTypes: true })
|
||||||
|
.filter(d => d.isDirectory())
|
||||||
|
.map(d => path.join(ROOT, d.name, 'SKILL.md'));
|
||||||
|
for (const candidate of skillDirs) {
|
||||||
|
if (!fs.existsSync(candidate)) continue;
|
||||||
|
const content = fs.readFileSync(candidate, 'utf-8');
|
||||||
|
if (content.includes('## AskUserQuestion Format')) {
|
||||||
|
skillMd = content;
|
||||||
|
skillMdPath = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const aqStart = skillMd.indexOf('## AskUserQuestion Format');
|
const aqStart = skillMd.indexOf('## AskUserQuestion Format');
|
||||||
const aqEnd = skillMd.indexOf('\n## ', aqStart + 1);
|
const aqEnd = skillMd.indexOf('\n## ', aqStart + 1);
|
||||||
const aqBlock = skillMd.slice(aqStart, aqEnd > 0 ? aqEnd : undefined);
|
const aqBlock = aqStart >= 0
|
||||||
|
? skillMd.slice(aqStart, aqEnd > 0 ? aqEnd : undefined)
|
||||||
|
: '';
|
||||||
|
|
||||||
const outputPath = path.join(sessionDir, 'question-output.md');
|
const outputPath = path.join(sessionDir, 'question-output.md');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
/**
|
||||||
|
* Overlay-efficacy harness (periodic tier, paid).
|
||||||
|
*
|
||||||
|
* Measures whether a model-specific overlay nudge actually changes model
|
||||||
|
* behavior when run through the real Claude Agent SDK — the harness
|
||||||
|
* Claude Code itself is built on. This complements test/skill-e2e-opus-47.test.ts
|
||||||
|
* which measures the same thing via `claude -p` subprocess (a different
|
||||||
|
* harness with different prompt composition).
|
||||||
|
*
|
||||||
|
* For each fixture in test/fixtures/overlay-nudges.ts, runs two arms at
|
||||||
|
* `fixture.trials` trials per arm with bounded concurrency:
|
||||||
|
* - overlay-on: SDK systemPrompt = resolved overlay content
|
||||||
|
* - overlay-off: SDK systemPrompt = "" (empty)
|
||||||
|
*
|
||||||
|
* Both arms have no CLAUDE.md, no skills directory, no setting-source
|
||||||
|
* inheritance (settingSources: []). This is the TRUE bare comparison —
|
||||||
|
* the only variable is the overlay text.
|
||||||
|
*
|
||||||
|
* Budget ~$20 per run at 40 trials (2 fixtures × 2 arms × 10 trials).
|
||||||
|
* Gated by EVALS=1 AND EVALS_TIER=periodic. Never runs under test:gate.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, expect, afterAll } from 'bun:test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import {
|
||||||
|
runAgentSdkTest,
|
||||||
|
resolveClaudeBinary,
|
||||||
|
type AgentSdkResult,
|
||||||
|
type SystemPromptOption,
|
||||||
|
} from './helpers/agent-sdk-runner';
|
||||||
|
import { EvalCollector, getProjectEvalDir } from './helpers/eval-store';
|
||||||
|
import {
|
||||||
|
OVERLAY_FIXTURES,
|
||||||
|
type OverlayFixture,
|
||||||
|
} from './fixtures/overlay-nudges';
|
||||||
|
import { readOverlay } from '../scripts/resolvers/model-overlay';
|
||||||
|
|
||||||
|
const evalsEnabled = !!process.env.EVALS;
|
||||||
|
const periodicTier = process.env.EVALS_TIER === 'periodic';
|
||||||
|
const shouldRun = evalsEnabled && periodicTier;
|
||||||
|
|
||||||
|
const describeE2E = shouldRun ? describe : describe.skip;
|
||||||
|
// EvalCollector's tier must be 'e2e' | 'llm-judge' per its type signature.
|
||||||
|
// The existing paid evals violate this by passing descriptive names like
|
||||||
|
// 'e2e-opus-47' — a pre-existing pattern that only works because bun-test
|
||||||
|
// runs without strict typechecking. We stay conforming here.
|
||||||
|
const evalCollector = shouldRun ? new EvalCollector('e2e') : null;
|
||||||
|
|
||||||
|
const REPO_ROOT = path.resolve(import.meta.dir, '..');
|
||||||
|
const runId = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.replace(/[:.]/g, '')
|
||||||
|
.replace('T', '-')
|
||||||
|
.slice(0, 15);
|
||||||
|
const TRANSCRIPTS_DIR = path.join(
|
||||||
|
path.dirname(getProjectEvalDir()),
|
||||||
|
'transcripts',
|
||||||
|
`overlay-harness-${runId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Per-arm helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Arm = 'overlay-on' | 'overlay-off';
|
||||||
|
|
||||||
|
function mkTrialDir(fixtureId: string, arm: Arm, n: number): string {
|
||||||
|
const dir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), `overlay-harness-${fixtureId}-${arm}-${n}-`),
|
||||||
|
);
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRawTranscript(
|
||||||
|
fixtureId: string,
|
||||||
|
arm: Arm,
|
||||||
|
n: number,
|
||||||
|
result: AgentSdkResult,
|
||||||
|
): void {
|
||||||
|
fs.mkdirSync(TRANSCRIPTS_DIR, { recursive: true });
|
||||||
|
const out = path.join(TRANSCRIPTS_DIR, `${fixtureId}-${arm}-${n}.jsonl`);
|
||||||
|
const lines = result.events.map((e) => JSON.stringify(e));
|
||||||
|
fs.writeFileSync(out, lines.join('\n') + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function overlayContentFor(fixture: OverlayFixture): string {
|
||||||
|
const family = path.basename(fixture.overlayPath, '.md');
|
||||||
|
const resolved = readOverlay(family);
|
||||||
|
if (!resolved) {
|
||||||
|
throw new Error(
|
||||||
|
`fixture ${fixture.id}: resolver returned empty content for ${family}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Per-fixture runner
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface ArmResult {
|
||||||
|
metrics: number[];
|
||||||
|
costs: number[];
|
||||||
|
durations: number[];
|
||||||
|
rateLimitExhausted: number;
|
||||||
|
sdkClaudeCodeVersions: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runArm(
|
||||||
|
fixture: OverlayFixture,
|
||||||
|
arm: Arm,
|
||||||
|
systemPrompt: SystemPromptOption,
|
||||||
|
claudeBinary: string | null,
|
||||||
|
): Promise<ArmResult> {
|
||||||
|
const result: ArmResult = {
|
||||||
|
metrics: [],
|
||||||
|
costs: [],
|
||||||
|
durations: [],
|
||||||
|
rateLimitExhausted: 0,
|
||||||
|
sdkClaudeCodeVersions: new Set(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const trials = fixture.trials;
|
||||||
|
const concurrency = fixture.concurrency ?? 3;
|
||||||
|
|
||||||
|
// Simple bounded executor: run trials in chunks of `concurrency`.
|
||||||
|
// The process-level semaphore in agent-sdk-runner.ts enforces the true cap.
|
||||||
|
let nextTrial = 0;
|
||||||
|
const workers = Array.from({ length: concurrency }, async () => {
|
||||||
|
while (true) {
|
||||||
|
const n = nextTrial++;
|
||||||
|
if (n >= trials) return;
|
||||||
|
|
||||||
|
const dir = mkTrialDir(fixture.id, arm, n);
|
||||||
|
fixture.setupWorkspace(dir);
|
||||||
|
try {
|
||||||
|
const sdkResult = await runAgentSdkTest({
|
||||||
|
systemPrompt,
|
||||||
|
userPrompt: fixture.userPrompt,
|
||||||
|
workingDirectory: dir,
|
||||||
|
model: fixture.model,
|
||||||
|
maxTurns: fixture.maxTurns ?? 5,
|
||||||
|
allowedTools: fixture.allowedTools ?? ['Read', 'Glob', 'Grep', 'Bash'],
|
||||||
|
permissionMode: 'bypassPermissions',
|
||||||
|
settingSources: [],
|
||||||
|
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? '' },
|
||||||
|
pathToClaudeCodeExecutable: claudeBinary ?? undefined,
|
||||||
|
testName: `${fixture.id}-${arm}-${n}`,
|
||||||
|
runId,
|
||||||
|
fixtureId: fixture.id,
|
||||||
|
onRetry: (_) => {
|
||||||
|
// Reset the workspace before the retry so partial Bash side effects
|
||||||
|
// from the failed attempt don't contaminate.
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
fixture.setupWorkspace(dir);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
saveRawTranscript(fixture.id, arm, n, sdkResult);
|
||||||
|
|
||||||
|
const metric = fixture.metric(sdkResult);
|
||||||
|
result.metrics.push(metric);
|
||||||
|
result.costs.push(sdkResult.costUsd);
|
||||||
|
result.durations.push(sdkResult.durationMs);
|
||||||
|
result.sdkClaudeCodeVersions.add(sdkResult.sdkClaudeCodeVersion);
|
||||||
|
|
||||||
|
evalCollector?.addTest({
|
||||||
|
name: `${fixture.id}-${arm}-${n}`,
|
||||||
|
suite: 'overlay-harness',
|
||||||
|
tier: 'e2e',
|
||||||
|
passed: true,
|
||||||
|
duration_ms: sdkResult.durationMs,
|
||||||
|
cost_usd: sdkResult.costUsd,
|
||||||
|
transcript: sdkResult.events,
|
||||||
|
prompt: fixture.userPrompt,
|
||||||
|
output: sdkResult.output,
|
||||||
|
turns_used: sdkResult.turnsUsed,
|
||||||
|
browse_errors: sdkResult.browseErrors,
|
||||||
|
exit_reason: sdkResult.exitReason,
|
||||||
|
model: sdkResult.model,
|
||||||
|
first_response_ms: sdkResult.firstResponseMs,
|
||||||
|
max_inter_turn_ms: sdkResult.maxInterTurnMs,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error && err.name === 'RateLimitExhaustedError') {
|
||||||
|
result.rateLimitExhausted++;
|
||||||
|
// Record a failed trial so the collector captures the attempt.
|
||||||
|
evalCollector?.addTest({
|
||||||
|
name: `${fixture.id}-${arm}-${n}`,
|
||||||
|
suite: 'overlay-harness',
|
||||||
|
tier: 'e2e',
|
||||||
|
passed: false,
|
||||||
|
duration_ms: 0,
|
||||||
|
cost_usd: 0,
|
||||||
|
exit_reason: 'rate_limit_exhausted',
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// best-effort cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(workers);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mean(xs: number[]): number {
|
||||||
|
if (xs.length === 0) return 0;
|
||||||
|
return xs.reduce((a, b) => a + b, 0) / xs.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sum(xs: number[]): number {
|
||||||
|
return xs.reduce((a, b) => a + b, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Test bodies
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describeE2E('overlay efficacy harness (SDK)', () => {
|
||||||
|
// Resolve binary once
|
||||||
|
const claudeBinary = resolveClaudeBinary();
|
||||||
|
|
||||||
|
if (!claudeBinary) {
|
||||||
|
test.skip(
|
||||||
|
'no local `claude` binary on PATH — cannot pin for harness parity',
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fixture of OVERLAY_FIXTURES) {
|
||||||
|
test(
|
||||||
|
`${fixture.id}: overlay-ON vs overlay-OFF, N=${fixture.trials} per arm`,
|
||||||
|
async () => {
|
||||||
|
const overlayText = overlayContentFor(fixture);
|
||||||
|
expect(overlayText.length).toBeGreaterThan(100);
|
||||||
|
|
||||||
|
// Arm composition: both arms use the real Claude Code default system
|
||||||
|
// prompt (preset). Overlay-ON APPENDS the overlay text; overlay-OFF
|
||||||
|
// uses the default alone. This measures the overlay's marginal effect
|
||||||
|
// ON TOP of Claude Code's normal behavioral scaffolding — which is
|
||||||
|
// the only measurement that matches how real Claude Code composes
|
||||||
|
// overlays into its system prompt stack.
|
||||||
|
const [onArm, offArm] = await Promise.all([
|
||||||
|
runArm(
|
||||||
|
fixture,
|
||||||
|
'overlay-on',
|
||||||
|
{ type: 'preset', preset: 'claude_code', append: overlayText },
|
||||||
|
claudeBinary,
|
||||||
|
),
|
||||||
|
runArm(
|
||||||
|
fixture,
|
||||||
|
'overlay-off',
|
||||||
|
{ type: 'preset', preset: 'claude_code' },
|
||||||
|
claudeBinary,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const arms = {
|
||||||
|
overlay: onArm.metrics,
|
||||||
|
off: offArm.metrics,
|
||||||
|
};
|
||||||
|
|
||||||
|
const meanOn = mean(arms.overlay);
|
||||||
|
const meanOff = mean(arms.off);
|
||||||
|
const lift = meanOn - meanOff;
|
||||||
|
const floorHits = arms.overlay.filter((n) => n >= 2).length;
|
||||||
|
const totalCost = sum(onArm.costs) + sum(offArm.costs);
|
||||||
|
const versionSet = new Set([
|
||||||
|
...onArm.sdkClaudeCodeVersions,
|
||||||
|
...offArm.sdkClaudeCodeVersions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Loud output for the next person reading the eval JSON:
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
`\n[${fixture.id}]\n` +
|
||||||
|
` binary: ${claudeBinary}\n` +
|
||||||
|
` claude_code_version(s): ${[...versionSet].join(', ')}\n` +
|
||||||
|
` overlay-ON metrics: [${arms.overlay.join(', ')}] mean=${meanOn.toFixed(2)}\n` +
|
||||||
|
` overlay-OFF metrics: [${arms.off.join(', ')}] mean=${meanOff.toFixed(2)}\n` +
|
||||||
|
` lift: ${lift.toFixed(2)} floor_hits(>=2): ${floorHits}/${fixture.trials}\n` +
|
||||||
|
` rate_limit_exhausted: on=${onArm.rateLimitExhausted} off=${offArm.rateLimitExhausted}\n` +
|
||||||
|
` total_cost_usd: $${totalCost.toFixed(4)}\n` +
|
||||||
|
` transcripts: ${TRANSCRIPTS_DIR}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Demand enough trials actually completed to make the assertion
|
||||||
|
// meaningful. If rate-limit exhaustion took out more than half of an
|
||||||
|
// arm, fail loudly rather than pass/fail on a fragment.
|
||||||
|
const minTrials = Math.ceil(fixture.trials / 2);
|
||||||
|
expect(arms.overlay.length).toBeGreaterThanOrEqual(minTrials);
|
||||||
|
expect(arms.off.length).toBeGreaterThanOrEqual(minTrials);
|
||||||
|
|
||||||
|
expect(fixture.pass(arms)).toBe(true);
|
||||||
|
},
|
||||||
|
30 * 60 * 1000, // 30 minute timeout per fixture
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (evalCollector) {
|
||||||
|
const filepath = await evalCollector.finalize();
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`\n[overlay-harness] eval results: ${filepath}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -35,10 +35,25 @@ const evalCollector = createEvalCollector('e2e-plan-format');
|
|||||||
// Regex predicates applied to captured AskUserQuestion content.
|
// Regex predicates applied to captured AskUserQuestion content.
|
||||||
// RECOMMENDATION regex is lenient on intervening markdown markers (e.g.
|
// RECOMMENDATION regex is lenient on intervening markdown markers (e.g.
|
||||||
// agent writes `**RECOMMENDATION:** Choose` — the `**` closers are benign).
|
// agent writes `**RECOMMENDATION:** Choose` — the `**` closers are benign).
|
||||||
const RECOMMENDATION_RE = /RECOMMENDATION:[*\s]*Choose/;
|
// Post v1.7.0.0: "Recommendation:" (mixed-case) is the canonical form per
|
||||||
|
// the Pros/Cons format; accept both cases for backward compatibility.
|
||||||
|
const RECOMMENDATION_RE = /[Rr]ecommendation:[*\s]*Choose/;
|
||||||
const COMPLETENESS_RE = /Completeness:\s*\d{1,2}\/10/;
|
const COMPLETENESS_RE = /Completeness:\s*\d{1,2}\/10/;
|
||||||
const KIND_NOTE_RE = /options differ in kind/i;
|
const KIND_NOTE_RE = /options differ in kind/i;
|
||||||
|
|
||||||
|
// v1.7.0.0 Pros/Cons format tokens. Tests are additive: existing
|
||||||
|
// RECOMMENDATION / Completeness / kind-note assertions still hold; new
|
||||||
|
// format tokens are asserted ONLY when the capture is from a v1.7+
|
||||||
|
// skill rendering. Presence is optional for backward compatibility during
|
||||||
|
// rollout; the periodic-tier cadence+format eval (see skill-e2e-plan-cadence)
|
||||||
|
// is the strict gate for the new format.
|
||||||
|
const PROS_CONS_HEADER_RE = /Pros\s*\/\s*cons:/i;
|
||||||
|
const PRO_BULLET_RE = /^\s*✅\s+\S/m;
|
||||||
|
const CON_BULLET_RE = /^\s*❌\s+\S/m;
|
||||||
|
const NET_LINE_RE = /^Net:\s+\S/m;
|
||||||
|
const D_NUMBER_RE = /^D\d+\s+—/m;
|
||||||
|
const STAKES_RE = /Stakes if we pick wrong:/i;
|
||||||
|
|
||||||
const SAMPLE_PLAN = `# Plan: Add User Dashboard
|
const SAMPLE_PLAN = `# Plan: Add User Dashboard
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|||||||
@@ -0,0 +1,352 @@
|
|||||||
|
/**
|
||||||
|
* v1.7.0.0 Pros/Cons format regression tests for plan reviews.
|
||||||
|
*
|
||||||
|
* Extends the v1.6.3.0 format harness (skill-e2e-plan-format.test.ts) with
|
||||||
|
* four new cases covering the Pros/Cons decision-brief format:
|
||||||
|
*
|
||||||
|
* 1. Format positive — every AskUserQuestion renders with D<N> / ELI10 /
|
||||||
|
* Stakes / Recommendation / Pros/cons / ✅×2+ / ❌×1+ / Net tokens.
|
||||||
|
* 2. Hard-stop positive — destructive-action question may use the single
|
||||||
|
* "No cons — this is a hard-stop choice" escape.
|
||||||
|
* 3. Hard-stop NEGATIVE (CT2) — plan with genuine tradeoff, model must NOT
|
||||||
|
* dodge to the hard-stop escape. Forces real tradeoff articulation.
|
||||||
|
* 4. Neutral-posture NEGATIVE (CT2) — plan with one clearly-dominant option,
|
||||||
|
* model must emit (recommended) label and concrete recommendation, NOT
|
||||||
|
* "no preference — taste call" dodge.
|
||||||
|
*
|
||||||
|
* Capture pattern matches existing harness: agent writes verbatim
|
||||||
|
* AskUserQuestion text to $OUT_FILE; regex predicates run on the captured
|
||||||
|
* file. Classified periodic (Opus 4.7 non-deterministic).
|
||||||
|
*
|
||||||
|
* FOLLOW-UP (not in v1.7.0.0):
|
||||||
|
* - True cadence eval (3 findings → 3 distinct asks across turns). Current
|
||||||
|
* $OUT_FILE harness captures ONE would-be question per session. Multi-turn
|
||||||
|
* cadence needs new harness support. Filed in TODOs.
|
||||||
|
* - Expanded coverage for /ship /office-hours /investigate /qa /review
|
||||||
|
* /design-review /document-release. Touchfiles entries already exist; eval
|
||||||
|
* cases will land as follow-up PRs per skill.
|
||||||
|
*/
|
||||||
|
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||||||
|
import { runSkillTest } from './helpers/session-runner';
|
||||||
|
import {
|
||||||
|
ROOT, runId,
|
||||||
|
describeIfSelected, testConcurrentIfSelected,
|
||||||
|
logCost, recordE2E,
|
||||||
|
createEvalCollector, finalizeEvalCollector,
|
||||||
|
} from './helpers/e2e-helpers';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
|
||||||
|
const evalCollector = createEvalCollector('e2e-plan-prosons');
|
||||||
|
|
||||||
|
// v1.7.0.0 format tokens
|
||||||
|
const D_NUMBER_RE = /D\d+\s+—/;
|
||||||
|
const ELI10_RE = /ELI10:/i;
|
||||||
|
const STAKES_RE = /Stakes if we pick wrong:/i;
|
||||||
|
const RECOMMENDATION_RE = /[Rr]ecommendation:/;
|
||||||
|
const PROS_CONS_HEADER_RE = /Pros\s*\/\s*cons:/i;
|
||||||
|
const NET_LINE_RE = /^Net:/m;
|
||||||
|
const HARD_STOP_ESCAPE_RE = /✅\s+No cons\s+—\s+this is a hard-stop choice/;
|
||||||
|
const NEUTRAL_POSTURE_RE = /taste call/i;
|
||||||
|
const RECOMMENDED_LABEL_RE = /\(recommended\)/;
|
||||||
|
|
||||||
|
function countChars(text: string, char: string): number {
|
||||||
|
return (text.match(new RegExp(char, 'g')) || []).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TRADEOFF_PLAN = `# Plan: Add user dashboard caching
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Dashboard renders in 3s on cold load, 800ms on warm cache. Users complain.
|
||||||
|
|
||||||
|
## Approach options
|
||||||
|
|
||||||
|
### Option A: Redis cache layer (complete)
|
||||||
|
- Add Redis with 5min TTL for dashboard aggregates.
|
||||||
|
- Cold path: compute + cache. Warm path: fetch from cache.
|
||||||
|
- Needs Redis infra, cache invalidation logic for activity updates.
|
||||||
|
- Covers all users, all flows, fails gracefully on cache miss.
|
||||||
|
|
||||||
|
### Option B: In-memory LRU cache (happy path only)
|
||||||
|
- Per-process LRU with 100-entry cap.
|
||||||
|
- No cross-process sharing; cache warms per-pod.
|
||||||
|
- Skips cache invalidation; stale reads up to 5min.
|
||||||
|
|
||||||
|
Both options have real pros and cons. This is a genuine tradeoff.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HARDSTOP_PLAN = `# Plan: Delete all user sessions
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Security incident. All active sessions need to be terminated immediately.
|
||||||
|
|
||||||
|
## Action
|
||||||
|
Run \`DELETE FROM sessions WHERE TRUE\`. No dry-run mode.
|
||||||
|
|
||||||
|
This is a one-way door. There is no "partial" version.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DOMINANT_PLAN = `# Plan: Add input validation to signup endpoint
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Signup endpoint currently accepts any email string and any password length.
|
||||||
|
Bug report: users type gibberish, signup succeeds, they can't log in.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### Option A: Full RFC 5322 email validation + min 8-char password + server-side checks
|
||||||
|
- Catches malformed emails, rejects weak passwords, validated on server.
|
||||||
|
- Prevents the reported bug and adjacent bugs.
|
||||||
|
- Standard web practice.
|
||||||
|
|
||||||
|
### Option B: Client-side type="email" only, no password validation
|
||||||
|
- Only catches some browsers' built-in validation.
|
||||||
|
- Attackers bypass by disabling JS.
|
||||||
|
- Does not fix the reported bug.
|
||||||
|
|
||||||
|
Option A clearly dominates on coverage. This is NOT a taste call.
|
||||||
|
`;
|
||||||
|
|
||||||
|
function setupPlanDir(tmpPrefix: string, planContent: string, skillName: string): string {
|
||||||
|
const planDir = fs.mkdtempSync(path.join(os.tmpdir(), tmpPrefix));
|
||||||
|
const run = (cmd: string, args: string[]) =>
|
||||||
|
spawnSync(cmd, args, { cwd: planDir, stdio: 'pipe', timeout: 5000 });
|
||||||
|
|
||||||
|
run('git', ['init', '-b', 'main']);
|
||||||
|
run('git', ['config', 'user.email', 'test@test.com']);
|
||||||
|
run('git', ['config', 'user.name', 'Test']);
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(planDir, 'plan.md'), planContent);
|
||||||
|
run('git', ['add', '.']);
|
||||||
|
run('git', ['commit', '-m', 'add plan']);
|
||||||
|
|
||||||
|
fs.mkdirSync(path.join(planDir, skillName), { recursive: true });
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join(ROOT, skillName, 'SKILL.md'),
|
||||||
|
path.join(planDir, skillName, 'SKILL.md'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return planDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureInstruction(outFile: string): string {
|
||||||
|
return `Write the verbatim text of the single AskUserQuestion you would have made to ${outFile} (full text including D<N> header, ELI10, Stakes, Recommendation, Pros/cons, and Net line — the complete rich markdown body). Do NOT call any tool to ask the user. Do NOT paraphrase. This is a format-capture test.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Case 1: Format positive — all v1.7.0.0 tokens present ---
|
||||||
|
|
||||||
|
describeIfSelected('Plan Prosons — Format Positive', ['plan-review-prosons-format'], () => {
|
||||||
|
let planDir: string;
|
||||||
|
let outFile: string;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
planDir = setupPlanDir('skill-e2e-plan-prosons-format-', TRADEOFF_PLAN, 'plan-ceo-review');
|
||||||
|
outFile = path.join(planDir, 'ask-capture.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
try { fs.rmSync(planDir, { recursive: true, force: true }); } catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
testConcurrentIfSelected('plan-review-prosons-format', async () => {
|
||||||
|
const result = await runSkillTest({
|
||||||
|
prompt: `Read plan-ceo-review/SKILL.md for the review workflow.
|
||||||
|
|
||||||
|
Read plan.md — two cache approaches with real tradeoffs. Pick the architectural approach via AskUserQuestion (Step 0C-bis / Implementation Alternatives). These options differ in coverage.
|
||||||
|
|
||||||
|
${captureInstruction(outFile)}
|
||||||
|
|
||||||
|
After writing the file, stop.`,
|
||||||
|
workingDirectory: planDir,
|
||||||
|
maxTurns: 10,
|
||||||
|
timeout: 240_000,
|
||||||
|
testName: 'plan-review-prosons-format',
|
||||||
|
runId,
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
});
|
||||||
|
|
||||||
|
logCost('/plan-review prosons format positive', result);
|
||||||
|
recordE2E(evalCollector, '/plan-review-prosons-format', 'Plan Prosons — Format Positive', result, {
|
||||||
|
passed: ['success', 'error_max_turns'].includes(result.exitReason),
|
||||||
|
});
|
||||||
|
expect(['success', 'error_max_turns']).toContain(result.exitReason);
|
||||||
|
|
||||||
|
expect(fs.existsSync(outFile)).toBe(true);
|
||||||
|
const captured = fs.readFileSync(outFile, 'utf-8');
|
||||||
|
expect(captured.length).toBeGreaterThan(200);
|
||||||
|
|
||||||
|
// Every Pros/Cons token present
|
||||||
|
expect(captured).toMatch(D_NUMBER_RE);
|
||||||
|
expect(captured).toMatch(ELI10_RE);
|
||||||
|
expect(captured).toMatch(STAKES_RE);
|
||||||
|
expect(captured).toMatch(RECOMMENDATION_RE);
|
||||||
|
expect(captured).toMatch(PROS_CONS_HEADER_RE);
|
||||||
|
expect(captured).toMatch(NET_LINE_RE);
|
||||||
|
|
||||||
|
// Pro/con bullet counts: ≥2 ✅ and ≥1 ❌ per option (total ≥4 ✅ and ≥2 ❌ for 2 options)
|
||||||
|
expect(countChars(captured, '✅')).toBeGreaterThanOrEqual(4);
|
||||||
|
expect(countChars(captured, '❌')).toBeGreaterThanOrEqual(2);
|
||||||
|
|
||||||
|
// (recommended) label on one option
|
||||||
|
expect(captured).toMatch(RECOMMENDED_LABEL_RE);
|
||||||
|
}, 300_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Case 2: Hard-stop escape NEGATIVE (CT2) ---
|
||||||
|
|
||||||
|
describeIfSelected('Plan Prosons — Hard-stop Negative', ['plan-review-prosons-hardstop-neg'], () => {
|
||||||
|
let planDir: string;
|
||||||
|
let outFile: string;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
planDir = setupPlanDir('skill-e2e-plan-prosons-hardstop-neg-', TRADEOFF_PLAN, 'plan-ceo-review');
|
||||||
|
outFile = path.join(planDir, 'ask-capture.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
try { fs.rmSync(planDir, { recursive: true, force: true }); } catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
testConcurrentIfSelected('plan-review-prosons-hardstop-neg', async () => {
|
||||||
|
const result = await runSkillTest({
|
||||||
|
prompt: `Read plan-ceo-review/SKILL.md.
|
||||||
|
|
||||||
|
Read plan.md — this has REAL tradeoffs between Redis and in-memory caching (both have pros and cons). Pick the architectural approach via AskUserQuestion.
|
||||||
|
|
||||||
|
${captureInstruction(outFile)}
|
||||||
|
|
||||||
|
After writing the file, stop.`,
|
||||||
|
workingDirectory: planDir,
|
||||||
|
maxTurns: 10,
|
||||||
|
timeout: 240_000,
|
||||||
|
testName: 'plan-review-prosons-hardstop-neg',
|
||||||
|
runId,
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
});
|
||||||
|
|
||||||
|
logCost('/plan-review prosons hard-stop negative', result);
|
||||||
|
recordE2E(evalCollector, '/plan-review-prosons-hardstop-neg', 'Plan Prosons — Hard-stop Negative', result, {
|
||||||
|
passed: ['success', 'error_max_turns'].includes(result.exitReason),
|
||||||
|
});
|
||||||
|
expect(['success', 'error_max_turns']).toContain(result.exitReason);
|
||||||
|
|
||||||
|
expect(fs.existsSync(outFile)).toBe(true);
|
||||||
|
const captured = fs.readFileSync(outFile, 'utf-8');
|
||||||
|
expect(captured.length).toBeGreaterThan(200);
|
||||||
|
|
||||||
|
// Genuine tradeoff — must NOT dodge to hard-stop escape.
|
||||||
|
expect(captured).not.toMatch(HARD_STOP_ESCAPE_RE);
|
||||||
|
// Must have real pros and cons (≥2 ✅ + ≥1 ❌ per option)
|
||||||
|
expect(countChars(captured, '✅')).toBeGreaterThanOrEqual(4);
|
||||||
|
expect(countChars(captured, '❌')).toBeGreaterThanOrEqual(2);
|
||||||
|
}, 300_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Case 3: Neutral-posture NEGATIVE (CT2) ---
|
||||||
|
|
||||||
|
describeIfSelected('Plan Prosons — Neutral-posture Negative', ['plan-review-prosons-neutral-neg'], () => {
|
||||||
|
let planDir: string;
|
||||||
|
let outFile: string;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
planDir = setupPlanDir('skill-e2e-plan-prosons-neutral-neg-', DOMINANT_PLAN, 'plan-ceo-review');
|
||||||
|
outFile = path.join(planDir, 'ask-capture.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
try { fs.rmSync(planDir, { recursive: true, force: true }); } catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
testConcurrentIfSelected('plan-review-prosons-neutral-neg', async () => {
|
||||||
|
const result = await runSkillTest({
|
||||||
|
prompt: `Read plan-ceo-review/SKILL.md.
|
||||||
|
|
||||||
|
Read plan.md — Option A dominates Option B on coverage. This is NOT a taste call. Pick the approach via AskUserQuestion (Step 0C-bis / Implementation Alternatives — coverage-differentiated, so Completeness: N/10 applies).
|
||||||
|
|
||||||
|
${captureInstruction(outFile)}
|
||||||
|
|
||||||
|
After writing the file, stop.`,
|
||||||
|
workingDirectory: planDir,
|
||||||
|
maxTurns: 10,
|
||||||
|
timeout: 240_000,
|
||||||
|
testName: 'plan-review-prosons-neutral-neg',
|
||||||
|
runId,
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
});
|
||||||
|
|
||||||
|
logCost('/plan-review prosons neutral negative', result);
|
||||||
|
recordE2E(evalCollector, '/plan-review-prosons-neutral-neg', 'Plan Prosons — Neutral Negative', result, {
|
||||||
|
passed: ['success', 'error_max_turns'].includes(result.exitReason),
|
||||||
|
});
|
||||||
|
expect(['success', 'error_max_turns']).toContain(result.exitReason);
|
||||||
|
|
||||||
|
expect(fs.existsSync(outFile)).toBe(true);
|
||||||
|
const captured = fs.readFileSync(outFile, 'utf-8');
|
||||||
|
expect(captured.length).toBeGreaterThan(200);
|
||||||
|
|
||||||
|
// One option dominates — must NOT use "taste call" neutral-posture dodge.
|
||||||
|
expect(captured).not.toMatch(NEUTRAL_POSTURE_RE);
|
||||||
|
// (recommended) label MUST be present on the dominant option.
|
||||||
|
expect(captured).toMatch(RECOMMENDED_LABEL_RE);
|
||||||
|
// Recommendation line must contain "because" (concrete reason, not "no preference")
|
||||||
|
expect(captured).toMatch(/[Rr]ecommendation:.*because/);
|
||||||
|
}, 300_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Case 4: Hard-stop POSITIVE (escape allowed when legitimately one-sided) ---
|
||||||
|
|
||||||
|
describeIfSelected('Plan Prosons — Hard-stop Positive', ['plan-ceo-review-prosons-cadence'], () => {
|
||||||
|
let planDir: string;
|
||||||
|
let outFile: string;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
planDir = setupPlanDir('skill-e2e-plan-prosons-hardstop-pos-', HARDSTOP_PLAN, 'plan-ceo-review');
|
||||||
|
outFile = path.join(planDir, 'ask-capture.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
try { fs.rmSync(planDir, { recursive: true, force: true }); } catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
testConcurrentIfSelected('plan-ceo-review-prosons-cadence', async () => {
|
||||||
|
const result = await runSkillTest({
|
||||||
|
prompt: `Read plan-ceo-review/SKILL.md.
|
||||||
|
|
||||||
|
Read plan.md — this is a destructive one-way action (terminate all sessions). Ask the user to confirm via AskUserQuestion. This is a legitimate hard-stop choice — the hard-stop escape (\`✅ No cons — this is a hard-stop choice\`) is allowed here because there is no meaningful alternative besides doing or not doing the action.
|
||||||
|
|
||||||
|
${captureInstruction(outFile)}
|
||||||
|
|
||||||
|
After writing the file, stop.`,
|
||||||
|
workingDirectory: planDir,
|
||||||
|
maxTurns: 10,
|
||||||
|
timeout: 240_000,
|
||||||
|
testName: 'plan-ceo-review-prosons-cadence',
|
||||||
|
runId,
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
});
|
||||||
|
|
||||||
|
logCost('/plan-review prosons hard-stop positive', result);
|
||||||
|
recordE2E(evalCollector, '/plan-ceo-review-prosons-cadence', 'Plan Prosons — Hard-stop Positive', result, {
|
||||||
|
passed: ['success', 'error_max_turns'].includes(result.exitReason),
|
||||||
|
});
|
||||||
|
expect(['success', 'error_max_turns']).toContain(result.exitReason);
|
||||||
|
|
||||||
|
expect(fs.existsSync(outFile)).toBe(true);
|
||||||
|
const captured = fs.readFileSync(outFile, 'utf-8');
|
||||||
|
expect(captured.length).toBeGreaterThan(100);
|
||||||
|
|
||||||
|
// Format scaffolding still required
|
||||||
|
expect(captured).toMatch(PROS_CONS_HEADER_RE);
|
||||||
|
// Hard-stop escape is ACCEPTED here (destructive one-way action)
|
||||||
|
// Either the escape is used OR real pros/cons are present — both are valid.
|
||||||
|
const hasEscape = HARD_STOP_ESCAPE_RE.test(captured);
|
||||||
|
const hasProsAndCons = countChars(captured, '✅') >= 1 && countChars(captured, '❌') >= 1;
|
||||||
|
expect(hasEscape || hasProsAndCons).toBe(true);
|
||||||
|
}, 300_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await finalizeEvalCollector(evalCollector);
|
||||||
|
});
|
||||||
@@ -566,10 +566,21 @@ describe('v0.4.1 preamble features', () => {
|
|||||||
const skillsWithPreamble = [...tier1Skills, ...tier2PlusSkills];
|
const skillsWithPreamble = [...tier1Skills, ...tier2PlusSkills];
|
||||||
|
|
||||||
for (const skill of tier2PlusSkills) {
|
for (const skill of tier2PlusSkills) {
|
||||||
test(`${skill} contains RECOMMENDATION format`, () => {
|
test(`${skill} contains AskUserQuestion Pros/Cons format`, () => {
|
||||||
const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
|
const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
|
||||||
expect(content).toContain('RECOMMENDATION: Choose');
|
// v1.7.0.0 Pros/Cons format tokens. The preamble resolver
|
||||||
|
// (generate-ask-user-format.ts) injects all of these into every
|
||||||
|
// tier-2+ skill. Drop any of them and the test catches it on the
|
||||||
|
// next `bun test` run.
|
||||||
expect(content).toContain('AskUserQuestion');
|
expect(content).toContain('AskUserQuestion');
|
||||||
|
expect(content).toContain('Pros / cons:');
|
||||||
|
expect(content).toContain('Recommendation: <choice>');
|
||||||
|
expect(content).toContain('Net:');
|
||||||
|
expect(content).toContain('ELI10');
|
||||||
|
expect(content).toContain('Stakes if we pick wrong:');
|
||||||
|
// Concrete format markers must be documented in the resolver text
|
||||||
|
expect(content).toMatch(/✅/);
|
||||||
|
expect(content).toMatch(/❌/);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user