mirror of https://github.com/garrytan/gstack.git
Merge remote-tracking branch 'origin/main' into garrytan/create-gbrain-skill
This commit is contained in:
commit
05a0d0c049
|
|
@ -0,0 +1,64 @@
|
||||||
|
name: PR Title Sync
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, edited]
|
||||||
|
paths:
|
||||||
|
- 'VERSION'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pr-title-sync-${{ github.event.pull_request.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
name: Sync PR title to VERSION
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
if: github.actor != 'github-actions[bot]'
|
||||||
|
steps:
|
||||||
|
- name: Checkout PR head
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- name: Read VERSION + current title
|
||||||
|
id: inspect
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION=$(cat VERSION | tr -d '[:space:]')
|
||||||
|
TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
# Only rewrite titles that ALREADY follow the v<X.Y.Z.W> prefix pattern.
|
||||||
|
# Custom titles (no prefix) are left alone — user kept them intentionally.
|
||||||
|
if printf '%s' "$TITLE" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ '; then
|
||||||
|
PREFIX=$(printf '%s' "$TITLE" | awk '{print $1}')
|
||||||
|
REST=$(printf '%s' "$TITLE" | sed 's/^v[0-9][0-9.]* //')
|
||||||
|
{
|
||||||
|
echo "prefix=$PREFIX"
|
||||||
|
echo "rest=$REST"
|
||||||
|
echo "eligible=true"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "eligible=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Rewrite title if version changed
|
||||||
|
if: steps.inspect.outputs.eligible == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_NUM: ${{ github.event.pull_request.number }}
|
||||||
|
NEW_V: ${{ steps.inspect.outputs.version }}
|
||||||
|
OLD_PREFIX: ${{ steps.inspect.outputs.prefix }}
|
||||||
|
REST: ${{ steps.inspect.outputs.rest }}
|
||||||
|
run: |
|
||||||
|
if [ "v$NEW_V" = "$OLD_PREFIX" ]; then
|
||||||
|
echo "Title already matches v$NEW_V; no change."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
NEW_TITLE="v$NEW_V $REST"
|
||||||
|
echo "Rewriting: $OLD_PREFIX ... → v$NEW_V ..."
|
||||||
|
gh pr edit "$PR_NUM" --title "$NEW_TITLE"
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
name: Version Gate
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'VERSION'
|
||||||
|
- 'CHANGELOG.md'
|
||||||
|
- 'package.json'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: version-gate-${{ github.event.pull_request.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Check VERSION is not stale vs queue
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout PR head
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
|
- name: Read versions
|
||||||
|
id: versions
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
PR_VERSION=$(cat VERSION | tr -d '[:space:]')
|
||||||
|
BASE_REF="${{ github.event.pull_request.base.ref }}"
|
||||||
|
git fetch origin "$BASE_REF" --depth=1 --quiet || true
|
||||||
|
BASE_VERSION=$(git show "origin/$BASE_REF:VERSION" 2>/dev/null | tr -d '[:space:]' || echo "0.0.0.0")
|
||||||
|
{
|
||||||
|
echo "pr_version=$PR_VERSION"
|
||||||
|
echo "base_version=$BASE_VERSION"
|
||||||
|
echo "base_ref=$BASE_REF"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Detect bump level
|
||||||
|
id: bump
|
||||||
|
run: |
|
||||||
|
LEVEL=$(bun run scripts/detect-bump.ts "${{ steps.versions.outputs.base_version }}" "${{ steps.versions.outputs.pr_version }}")
|
||||||
|
echo "level=$LEVEL" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Query queue (util) — fail-open on error
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
bun run bin/gstack-next-version \
|
||||||
|
--base "${{ steps.versions.outputs.base_ref }}" \
|
||||||
|
--bump "${{ steps.bump.outputs.level }}" \
|
||||||
|
--current-version "${{ steps.versions.outputs.base_version }}" \
|
||||||
|
--workspace-root null \
|
||||||
|
--exclude-pr "${{ github.event.pull_request.number }}" \
|
||||||
|
> next.json 2> next.err
|
||||||
|
RC=$?
|
||||||
|
if [ "$RC" != "0" ] || [ ! -s next.json ]; then
|
||||||
|
echo '{"offline":true}' > next.json
|
||||||
|
echo "::warning::util exit=$RC — failing open. stderr:"
|
||||||
|
cat next.err || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Compare PR VERSION to next free slot
|
||||||
|
env:
|
||||||
|
PR_VERSION: ${{ steps.versions.outputs.pr_version }}
|
||||||
|
run: |
|
||||||
|
bun run scripts/compare-pr-version.ts next.json "${{ github.event.pull_request.number }}"
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
# GitLab CI parity for workspace-aware ship.
|
||||||
|
# Mirrors .github/workflows/version-gate.yml and pr-title-sync.yml.
|
||||||
|
# Projects that mirror to GitLab get the same protection as GitHub.
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- check
|
||||||
|
|
||||||
|
variables:
|
||||||
|
BUN_VERSION: "1.3.10"
|
||||||
|
|
||||||
|
.setup-bun: &setup-bun
|
||||||
|
- apt-get update -qq && apt-get install -qq -y curl jq git
|
||||||
|
- curl -fsSL https://bun.sh/install | bash -s "bun-v$BUN_VERSION"
|
||||||
|
- export PATH="$HOME/.bun/bin:$PATH"
|
||||||
|
|
||||||
|
version-gate:
|
||||||
|
stage: check
|
||||||
|
image: debian:stable-slim
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
changes:
|
||||||
|
- VERSION
|
||||||
|
- CHANGELOG.md
|
||||||
|
- package.json
|
||||||
|
script:
|
||||||
|
- *setup-bun
|
||||||
|
- PR_VERSION=$(cat VERSION | tr -d '[:space:]')
|
||||||
|
- BASE_VERSION=$(git show "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME:VERSION" 2>/dev/null | tr -d '[:space:]' || echo "0.0.0.0")
|
||||||
|
- LEVEL=$(bun run scripts/detect-bump.ts "$BASE_VERSION" "$PR_VERSION")
|
||||||
|
# Util fail-open: on non-zero exit, emit offline marker
|
||||||
|
- |
|
||||||
|
set +e
|
||||||
|
bun run bin/gstack-next-version \
|
||||||
|
--base "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
|
||||||
|
--bump "$LEVEL" \
|
||||||
|
--current-version "$BASE_VERSION" \
|
||||||
|
--workspace-root null \
|
||||||
|
--exclude-pr "$CI_MERGE_REQUEST_IID" \
|
||||||
|
> next.json
|
||||||
|
RC=$?
|
||||||
|
if [ "$RC" != "0" ] || [ ! -s next.json ]; then
|
||||||
|
echo '{"offline":true}' > next.json
|
||||||
|
echo "WARNING: util exit=$RC — failing open"
|
||||||
|
fi
|
||||||
|
set -e
|
||||||
|
- PR_VERSION="$PR_VERSION" bun run scripts/compare-pr-version.ts next.json "$CI_MERGE_REQUEST_IID"
|
||||||
|
|
||||||
|
pr-title-sync:
|
||||||
|
stage: check
|
||||||
|
image: debian:stable-slim
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
changes:
|
||||||
|
- VERSION
|
||||||
|
script:
|
||||||
|
- apt-get update -qq && apt-get install -qq -y curl jq git
|
||||||
|
- curl -fsSL https://gitlab.com/gitlab-org/cli/-/releases/permalink/latest/downloads/glab_linux_amd64.deb -o glab.deb && dpkg -i glab.deb
|
||||||
|
- VERSION=$(cat VERSION | tr -d '[:space:]')
|
||||||
|
- TITLE="$CI_MERGE_REQUEST_TITLE"
|
||||||
|
- |
|
||||||
|
if printf '%s' "$TITLE" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ '; then
|
||||||
|
PREFIX=$(printf '%s' "$TITLE" | awk '{print $1}')
|
||||||
|
REST=$(printf '%s' "$TITLE" | sed 's/^v[0-9][0-9.]* //')
|
||||||
|
if [ "v$VERSION" != "$PREFIX" ]; then
|
||||||
|
echo "Rewriting: $PREFIX ... → v$VERSION ..."
|
||||||
|
glab mr update "$CI_MERGE_REQUEST_IID" -t "v$VERSION $REST"
|
||||||
|
else
|
||||||
|
echo "Title already matches v$VERSION; no change."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Title does not use v<X.Y.Z.W> prefix — leaving alone."
|
||||||
|
fi
|
||||||
308
CHANGELOG.md
308
CHANGELOG.md
|
|
@ -1,5 +1,312 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.11.1.0] - 2026-04-23
|
||||||
|
|
||||||
|
## **Plan mode stopped silently rubber-stamping your reviews. The forcing questions actually fire now.**
|
||||||
|
|
||||||
|
If you ran `/plan-ceo-review` or any interactive review skill while in plan mode, the skill used to read your diff, skip every STOP gate, write a plan file, and exit. Zero AskUserQuestion calls. Zero mode selection. Zero per-section decisions. The skill's interactive contract got outranked by plan mode's system-reminder, which tells the model to run its own workflow and ignore everything else. This release adds a preamble-level STOP gate that fires before any analysis, so you always get the interactive review the skill was designed to run.
|
||||||
|
|
||||||
|
### What shipped
|
||||||
|
|
||||||
|
Four interactive review skills (plan-ceo-review, plan-eng-review, plan-design-review, plan-devex-review) now emit a two-option AskUserQuestion the moment plan mode is detected: exit-and-rerun interactively, or cancel. No silent bypass. The gate is classified one-way-door in the question registry so `/plan-tune` preferences can't auto-decide past it. Outcome gets logged to `~/.gstack/analytics/skill-usage.jsonl` synchronously when the handshake fires, so A-exit and C-cancel are captured even though they terminate the skill before the end-of-run telemetry block.
|
||||||
|
|
||||||
|
The test harness got a canUseTool extension built on Anthropic's Agent SDK (already installed at v0.2.117). When a test supplies a canUseTool callback, `test/helpers/agent-sdk-runner.ts` flips `permissionMode` from `bypassPermissions` to `default` so the callback actually fires. This is the foundation for asserting AskUserQuestion content end-to-end, which gstack's E2E tests previously couldn't do at all. They had to instruct the model to skip AskUserQuestion entirely. Every future interactive-skill test builds on this.
|
||||||
|
|
||||||
|
### The numbers that matter
|
||||||
|
|
||||||
|
Source: new unit tests in `test/gen-skill-docs.test.ts` (8 tests covering handshake presence, absence, composition ordering, 0C-bis STOP block) and `test/agent-sdk-runner.test.ts` (6 tests covering canUseTool + permission-mode + passThrough helper). All 14 pass locally in <250ms, free tier.
|
||||||
|
|
||||||
|
| Surface | Before | After |
|
||||||
|
|---|---|---|
|
||||||
|
| Claude skills rendering the handshake | 0 | 4 (plan-ceo, plan-eng, plan-design, plan-devex) |
|
||||||
|
| Non-Claude host outputs with handshake text | N/A | 0 (host-scoped via `ctx.host === 'claude'` check) |
|
||||||
|
| E2E tests that can assert AskUserQuestion content | 0 | 1 harness primitive, ready for every interactive skill |
|
||||||
|
| Plan-mode entry to any of 4 review skills | Silent bypass | Two-option STOP gate |
|
||||||
|
| Step 0C-bis in plan-ceo-review | No STOP block, could drift to 0F | Explicit `**STOP.**` block matching 0F pattern |
|
||||||
|
| Post-handshake telemetry outcomes captured | Neither A-exit nor C-cancel | Both (synchronous write before ExitPlanMode) |
|
||||||
|
|
||||||
|
### What this means for builders
|
||||||
|
|
||||||
|
If you're running gstack in plan mode on a PR review, you'll see one question before the skill does anything: "Exit plan mode and run interactively, or cancel?" Pick A, press esc-esc, rerun the skill in normal mode, get the full interactive review you expected. Pick C to bail cleanly. No more silent rubber-stamp.
|
||||||
|
|
||||||
|
If you're building new interactive skills (yours or contributing to gstack), you can now write real E2E tests that assert on AskUserQuestion shape and routing via the canUseTool harness. See `test/agent-sdk-runner.test.ts` for the pattern and `test/helpers/agent-sdk-runner.ts` for the API.
|
||||||
|
|
||||||
|
### Itemized changes
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- Plan mode no longer silently skips AskUserQuestion gates in `/plan-ceo-review`, `/plan-eng-review`, `/plan-design-review`, or `/plan-devex-review`. A preamble-level handshake fires as the first thing the skill does when the plan-mode system-reminder is present, forcing a user choice before any analysis or plan-file writes.
|
||||||
|
- `/plan-ceo-review` Step 0C-bis now has an explicit STOP block matching the pattern used at Step 0F, so the approach-selection question can't be silently skipped when the skill continues to mode selection.
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- New resolver `scripts/resolvers/preamble/generate-plan-mode-handshake.ts` emits the handshake prose and telemetry bash. Host-scoped to Claude only via `ctx.host === 'claude'` check. Opt-in per skill via `interactive: true` in frontmatter.
|
||||||
|
- New frontmatter field `interactive: boolean` on skill templates. Generator-only input parsed by `scripts/gen-skill-docs.ts`, never written to generated SKILL.md output (follows the `preamble-tier` precedent).
|
||||||
|
- New question registry entries `plan-{ceo,eng,design,devex}-review-plan-mode-handshake` with `door_type: 'one-way'` in `scripts/question-registry.ts`. Question-tuning `never-ask` preferences cannot suppress this gate.
|
||||||
|
- New telemetry field `plan_mode_handshake` in `~/.gstack/analytics/skill-usage.jsonl` with outcomes `fired`, `A-exit`, `C-cancel` written synchronously as the handshake fires. Captures outcomes that would otherwise terminate the skill before end-of-run telemetry runs.
|
||||||
|
- `test/helpers/agent-sdk-runner.ts` extended with optional `canUseTool` callback parameter. When supplied, flips `permissionMode` to `default`, auto-adds `AskUserQuestion` to `allowedTools`, and passes the callback to the SDK. Exports `passThroughNonAskUserQuestion` helper for tests that only want to assert on AskUserQuestion but auto-allow other tools.
|
||||||
|
|
||||||
|
#### For contributors
|
||||||
|
|
||||||
|
- Added 5 unit tests in `test/gen-skill-docs.test.ts` verifying handshake presence in 4 interactive skills, absence in non-interactive skills, absence in non-Claude host outputs, composition ordering (handshake precedes upgrade-check), and 0C-bis STOP block wiring.
|
||||||
|
- Added 6 unit tests in `test/agent-sdk-runner.test.ts` verifying permission-mode flip, allowedTools auto-injection, canUseTool callback propagation, and pass-through helper behavior.
|
||||||
|
- Added 6 gate-tier entries to `test/helpers/touchfiles.ts` covering the new E2E test surface. Dependency glob fires any of the new tests when: the relevant skill template, the handshake resolver, preamble composition, the question registry, the one-way-door classifier, or the agent-sdk-runner changes.
|
||||||
|
- Filed 2 P1/P2 follow-ups in `TODOS.md`: structural STOP-Ask forcing function across all skills (broader class of bug beyond plan-mode entry), and extending `interactive: true` audit to non-review interactive skills like `/office-hours`, `/codex`, `/investigate`, `/qa`.
|
||||||
|
|
||||||
|
## [1.11.0.0] - 2026-04-23
|
||||||
|
|
||||||
|
## **Workspace-aware ship. Two open PRs can't both claim the same VERSION anymore.**
|
||||||
|
|
||||||
|
If you run gstack in multiple Conductor windows at once, you've probably seen this: two branches bump to the same version, whoever merges second silently overwrites the first one's CHANGELOG entry or lands with a duplicate header, and nobody notices until a `grep "^## \["` later. This release makes that collision impossible by construction. `/ship` now queries the open PR queue, sees what versions are already claimed, and picks the next free slot at your chosen bump level. If a collision is detected between ship and land, the land step aborts and tells you to rerun `/ship` rather than silently overwriting. A new `/landing-report` command shows the whole queue on demand.
|
||||||
|
|
||||||
|
### 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 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)
|
||||||
|
|
||||||
|
- `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.
|
||||||
|
- 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.
|
||||||
|
- `workspace_root` config key, default `$HOME/conductor/workspaces`, null disables sibling scan for non-Conductor users.
|
||||||
|
|
||||||
|
### What this means for teams running parallel workspaces
|
||||||
|
|
||||||
|
If you're routinely running 3-10 Conductor windows against the same repo, this is the capability that lets the model scale. Before: you mostly got away with it because you noticed collisions by eye. After: the queue is an observable surface, and the system refuses to ship a stale version. `/landing-report` is the new "where am I in line" check when you're about to open PR #6 for the day. Run it before `/ship` if you want to see what's coming without shipping.
|
||||||
|
|
||||||
|
### Itemized changes
|
||||||
|
|
||||||
|
#### 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. 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.
|
||||||
|
- `.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).
|
||||||
|
- `.gitlab-ci.yml`. GitLab CI parity. Both jobs mirrored with the same fail-open semantics.
|
||||||
|
- `landing-report/SKILL.md.tmpl`. New `/landing-report` or `/gstack-landing-report` skill. Read-only dashboard.
|
||||||
|
- `bin/gstack-config`. New `workspace_root` key. Default `$HOME/conductor/workspaces`, `null` disables sibling scan.
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
- `ship/SKILL.md.tmpl` Step 12. Queue-aware VERSION pick in FRESH path, drift detection in ALREADY_BUMPED path. On detected drift the user is prompted to rebump, which runs the full metadata path (VERSION + package.json + CHANGELOG header + PR title) atomically so nothing goes stale.
|
||||||
|
- `ship/SKILL.md.tmpl` Step 19. PR title format is now `v<X.Y.Z.W> <type>: <summary>`, version ALWAYS first. Rerun path updates the title (not just the body) when VERSION changed. Both GitHub and GitLab paths.
|
||||||
|
- `land-and-deploy/SKILL.md.tmpl`. New Step 3.4 pre-merge drift detection. Aborts with a clear rerun-/ship instruction rather than auto-mutating files. Rerunning `/ship` is the clean path because ship owns the full metadata flow.
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
## **Plan mode stopped silently rubber-stamping your reviews. The forcing questions actually fire now.**
|
||||||
|
|
||||||
|
If you ran `/plan-ceo-review` or any interactive review skill while in plan mode, the skill used to read your diff, skip every STOP gate, write a plan file, and exit. Zero AskUserQuestion calls. Zero mode selection. Zero per-section decisions. The skill's interactive contract got outranked by plan mode's system-reminder, which tells the model to run its own workflow and ignore everything else. This release adds a preamble-level STOP gate that fires before any analysis, so you always get the interactive review the skill was designed to run.
|
||||||
|
|
||||||
|
### What shipped
|
||||||
|
|
||||||
|
Four interactive review skills (plan-ceo-review, plan-eng-review, plan-design-review, plan-devex-review) now emit a two-option AskUserQuestion the moment plan mode is detected: exit-and-rerun interactively, or cancel. No silent bypass. The gate is classified one-way-door in the question registry so `/plan-tune` preferences can't auto-decide past it. Outcome gets logged to `~/.gstack/analytics/skill-usage.jsonl` synchronously when the handshake fires, so A-exit and C-cancel are captured even though they terminate the skill before the end-of-run telemetry block.
|
||||||
|
|
||||||
|
The test harness got a canUseTool extension built on Anthropic's Agent SDK (already installed at v0.2.117). When a test supplies a canUseTool callback, `test/helpers/agent-sdk-runner.ts` flips `permissionMode` from `bypassPermissions` to `default` so the callback actually fires. This is the foundation for asserting AskUserQuestion content end-to-end, which gstack's E2E tests previously couldn't do at all. They had to instruct the model to skip AskUserQuestion entirely. Every future interactive-skill test builds on this.
|
||||||
|
|
||||||
|
### The numbers that matter
|
||||||
|
|
||||||
|
Source: new unit tests in `test/gen-skill-docs.test.ts` (8 tests covering handshake presence, absence, composition ordering, 0C-bis STOP block) and `test/agent-sdk-runner.test.ts` (6 tests covering canUseTool + permission-mode + passThrough helper). All 14 pass locally in <250ms, free tier.
|
||||||
|
|
||||||
|
| Surface | Before | After |
|
||||||
|
|---|---|---|
|
||||||
|
| Claude skills rendering the handshake | 0 | 4 (plan-ceo, plan-eng, plan-design, plan-devex) |
|
||||||
|
| Non-Claude host outputs with handshake text | N/A | 0 (host-scoped via `ctx.host === 'claude'` check) |
|
||||||
|
| E2E tests that can assert AskUserQuestion content | 0 | 1 harness primitive, ready for every interactive skill |
|
||||||
|
| Plan-mode entry to any of 4 review skills | Silent bypass | Two-option STOP gate |
|
||||||
|
| Step 0C-bis in plan-ceo-review | No STOP block, could drift to 0F | Explicit `**STOP.**` block matching 0F pattern |
|
||||||
|
| Post-handshake telemetry outcomes captured | Neither A-exit nor C-cancel | Both (synchronous write before ExitPlanMode) |
|
||||||
|
|
||||||
|
### What this means for builders
|
||||||
|
|
||||||
|
If you're running gstack in plan mode on a PR review, you'll see one question before the skill does anything: "Exit plan mode and run interactively, or cancel?" Pick A, press esc-esc, rerun the skill in normal mode, get the full interactive review you expected. Pick C to bail cleanly. No more silent rubber-stamp.
|
||||||
|
|
||||||
|
If you're building new interactive skills (yours or contributing to gstack), you can now write real E2E tests that assert on AskUserQuestion shape and routing via the canUseTool harness. See `test/agent-sdk-runner.test.ts` for the pattern and `test/helpers/agent-sdk-runner.ts` for the API.
|
||||||
|
|
||||||
|
### Itemized changes
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- Plan mode no longer silently skips AskUserQuestion gates in `/plan-ceo-review`, `/plan-eng-review`, `/plan-design-review`, or `/plan-devex-review`. A preamble-level handshake fires as the first thing the skill does when the plan-mode system-reminder is present, forcing a user choice before any analysis or plan-file writes.
|
||||||
|
- `/plan-ceo-review` Step 0C-bis now has an explicit STOP block matching the pattern used at Step 0F, so the approach-selection question can't be silently skipped when the skill continues to mode selection.
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- New resolver `scripts/resolvers/preamble/generate-plan-mode-handshake.ts` emits the handshake prose and telemetry bash. Host-scoped to Claude only via `ctx.host === 'claude'` check. Opt-in per skill via `interactive: true` in frontmatter.
|
||||||
|
- New frontmatter field `interactive: boolean` on skill templates. Generator-only input parsed by `scripts/gen-skill-docs.ts`, never written to generated SKILL.md output (follows the `preamble-tier` precedent).
|
||||||
|
- New question registry entry `plan-mode-handshake` with `door_type: 'one-way'` in `scripts/question-registry.ts`. Question-tuning `never-ask` preferences cannot suppress this gate.
|
||||||
|
- New telemetry field `plan_mode_handshake` in `~/.gstack/analytics/skill-usage.jsonl` with outcomes `fired`, `A-exit`, `C-cancel` written synchronously as the handshake fires. Captures outcomes that would otherwise terminate the skill before end-of-run telemetry runs.
|
||||||
|
- `test/helpers/agent-sdk-runner.ts` extended with optional `canUseTool` callback parameter. When supplied, flips `permissionMode` to `default`, auto-adds `AskUserQuestion` to `allowedTools`, and passes the callback to the SDK. Exports `passThroughNonAskUserQuestion` helper for tests that only want to assert on AskUserQuestion but auto-allow other tools.
|
||||||
|
|
||||||
|
#### For contributors
|
||||||
|
|
||||||
|
- Added 8 unit tests in `test/gen-skill-docs.test.ts` verifying handshake presence in 4 interactive skills, absence in non-interactive skills, absence in non-Claude host outputs, composition ordering (handshake precedes upgrade-check), and 0C-bis STOP block wiring.
|
||||||
|
- Added 6 unit tests in `test/agent-sdk-runner.test.ts` verifying permission-mode flip, allowedTools auto-injection, canUseTool callback propagation, and pass-through helper behavior.
|
||||||
|
- Added 6 gate-tier entries to `test/helpers/touchfiles.ts` covering the new E2E test surface. Dependency glob fires any of the new tests when: the relevant skill template, the handshake resolver, preamble composition, the question registry, the one-way-door classifier, or the agent-sdk-runner changes.
|
||||||
|
- Filed 2 P1/P2 follow-ups in `TODOS.md`: structural STOP-Ask forcing function across all skills (broader class of bug beyond plan-mode entry), and extending `interactive: true` audit to non-review interactive skills like `/office-hours`, `/codex`, `/investigate`, `/qa`.
|
||||||
|
|
||||||
|
## [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
|
## [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.**
|
## **Your gstack memory now travels with you. Cross-machine brain via a private git repo + optional GBrain indexing, no daemon, no credential leaks.**
|
||||||
|
|
@ -75,6 +382,7 @@ Work on the laptop Monday. Switch to the desktop Tuesday. Skill preamble sees th
|
||||||
- `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.
|
- `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.
|
- 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.
|
- 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.**
|
||||||
|
|
|
||||||
10
CLAUDE.md
10
CLAUDE.md
|
|
@ -407,6 +407,16 @@ No auto-merging. No "I'll just clean this up."
|
||||||
|
|
||||||
## CHANGELOG + VERSION style
|
## CHANGELOG + VERSION style
|
||||||
|
|
||||||
|
**Versioning invariant (workspace-aware ship).** VERSION is a monotonic ordered
|
||||||
|
release identifier, not a strict semver commitment. The bump level
|
||||||
|
(major/minor/patch/micro) expresses intent at ship time. Queue-advancing past a
|
||||||
|
claimed version within the same bump level is explicitly permitted — if branch A
|
||||||
|
claims v1.7.0.0 as a MINOR and branch B is also a MINOR, B lands at v1.8.0.0
|
||||||
|
(still a MINOR relative to main). Downstream consumers must NOT rely on
|
||||||
|
"MINOR = feature-only, PATCH = fix-only" as a strict contract. This is why
|
||||||
|
`bin/gstack-next-version` advances within the chosen bump level rather than
|
||||||
|
repicking the level when collisions happen.
|
||||||
|
|
||||||
**VERSION and CHANGELOG are branch-scoped.** Every feature branch that ships gets its
|
**VERSION and CHANGELOG are branch-scoped.** Every feature branch that ships gets its
|
||||||
own version bump and CHANGELOG entry. The entry describes what THIS branch adds —
|
own version bump and CHANGELOG entry. The entry describes what THIS branch adds —
|
||||||
not what was already on main.
|
not what was already on main.
|
||||||
|
|
|
||||||
77
TODOS.md
77
TODOS.md
|
|
@ -1,5 +1,57 @@
|
||||||
# TODOS
|
# TODOS
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### `security-bench-haiku-responses.json` is 27MB, violates the 2MB tracked-file gate
|
||||||
|
|
||||||
|
**What:** `browse/test/fixtures/security-bench-haiku-responses.json` landed on main at v1.6.4.0 (PR #1135) at 27MB. The `no compiled binaries in git > git tracks no files larger than 2MB` gate in `test/skill-validation.test.ts:1623` fails on main and on every feature branch that merges main afterward.
|
||||||
|
|
||||||
|
**Why:** The fixture is a legitimate CI replay corpus (real Haiku responses from the 500-case BrowseSafe-Bench) used to verify the ensemble classifier deterministically. But 13x over the 2MB limit means it will keep failing the validation test for every future ship.
|
||||||
|
|
||||||
|
**Pros:** Removes a pre-existing failure that wastes a triage slot in every /ship run.
|
||||||
|
|
||||||
|
**Cons:** Moving to git-lfs adds a dependency. Splitting into chunks risks breaking the bench test. External hosting adds a CI fetch step.
|
||||||
|
|
||||||
|
**Context:** Noticed during workspace-aware-ship /ship on 2026-04-23 when the post-merge test suite flagged this single failure. Introduced on main in PR #1135 (`v1.6.4.0: cut Haiku classifier FP from 44% to 23%`), commit d75402bb. Two reasonable paths: (a) split into multiple ≤2MB chunks and load them in the bench test, (b) move to git-lfs.
|
||||||
|
|
||||||
|
**Effort:** M (human: ~2-3h / CC: ~20 min)
|
||||||
|
**Priority:** P1 (not blocking ship, but every future /ship triages the same failure)
|
||||||
|
**Depends on:** nothing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1: Structural STOP-Ask forcing function across all skills (v1.11.1.0 follow-up)
|
||||||
|
|
||||||
|
**What:** Design and implement a structural forcing function that catches when a skill mandates per-issue AskUserQuestion but the model silently substitutes batch-synthesis. Candidate mechanisms: question-count assertion (skill declares expected question count in frontmatter; post-run audit logs if model fired <N), typed question templates (skill hands the model pre-built AskUserQuestion payloads rather than prose instructions), or a canUseTool-based post-run audit that compares declared-gates-fired vs expected.
|
||||||
|
|
||||||
|
**Why:** v1.11.1.0 shipped a plan-mode handshake that forces AskUserQuestion when plan mode is active. It fixes the reported instance of the bug (plan-mode entry) but NOT the broader class. The injected commentary from a separate Claude session documented the same failure mode in auto mode — model silently substitutes batch-synthesis for STOP-Ask loops whenever the skill's interactive contract collides with any other rule surface (auto mode, plan mode, tool-count anxiety, cognitive load). Without structural enforcement, every skill with STOP-per-issue contracts remains vulnerable.
|
||||||
|
|
||||||
|
**Pros:** Catches a class-of-bug, not an instance. Applies to every skill that declares STOP gates. Builds on `canUseTool` primitive shipped in v1.11.1.0's agent-sdk-runner extension.
|
||||||
|
|
||||||
|
**Cons:** Real design work. How does a skill declare expected question count — static value in frontmatter, or dynamic based on number of review sections that surface findings? Is the audit inline (blocking, same-turn) or post-hoc (after skill completion)? Calibration of expected-vs-actual thresholds depends on real V0 question-log data across skills.
|
||||||
|
|
||||||
|
**Context:** Relevant files — `scripts/question-registry.ts` (typed question catalog), `scripts/resolvers/question-tuning.ts` (preference classification), `bin/gstack-question-log` (event log), `bin/gstack-question-preference` (read/write preferences), `test/helpers/agent-sdk-runner.ts` (canUseTool harness added in v1.11.1.0). Existing question-log already captures fire events; the gap is declaring expected counts and auditing against them.
|
||||||
|
|
||||||
|
**Effort:** L (human: ~1-2 weeks / CC+gstack: ~2-3 hours for design doc + first-pass implementation).
|
||||||
|
**Priority:** P1 if interactive-skill volume is growing; P2 otherwise.
|
||||||
|
**Depends on / blocked by:** v1.11.1.0 handshake landing (provides concrete forcing-function pattern to generalize from). Also needs a design doc — likely its own `docs/designs/STOP_ASK_ENFORCEMENT_V0.md`.
|
||||||
|
|
||||||
|
## P2: Apply preamble handshake to non-review interactive skills
|
||||||
|
|
||||||
|
**What:** Survey gstack skills beyond the 4 review skills to identify ones with interactive STOP-Ask contracts. For each, audit whether `interactive: true` in the frontmatter is appropriate (fires the preamble handshake in plan mode). Candidate skills to audit: `/office-hours`, `/codex` (consult mode), `/investigate`, `/qa`, `/retro`, `/cso`, `/brainstorm`. Non-candidates: workflow-executors like `/ship`, `/land-and-deploy`, `/context-save` that benefit from plan-mode batch execution.
|
||||||
|
|
||||||
|
**Why:** v1.11.1.0 opted 4 review skills into the plan-mode handshake (plan-ceo-review, plan-eng-review, plan-design-review, plan-devex-review). Codex's outside-voice review (Finding 6) noted nested composition — `/plan-ceo-review` can invoke `/office-hours` inline, so `/office-hours` in plan mode standalone would still silently skip its forcing questions. Extending handshake coverage closes that gap for the next tier of interactive skills.
|
||||||
|
|
||||||
|
**Pros:** Consistent plan-mode behavior across interactive skills. Low per-skill cost under the preamble-pivot architecture — one frontmatter line (`interactive: true`) + SKILL.md regeneration.
|
||||||
|
|
||||||
|
**Cons:** Requires per-skill classification review. Some skills that look interactive (e.g., `/qa`) actually run long non-interactive tool loops punctuated by occasional AskUserQuestion — the handshake gate might over-fire. Audit needs a judgment call per skill.
|
||||||
|
|
||||||
|
**Context:** Files to consult per-skill — the SKILL.md.tmpl frontmatter, the skill's STOP-point count, and any existing touchfiles.ts entries for E2E coverage. The handshake resolver is at `scripts/resolvers/preamble/generate-plan-mode-handshake.ts`. Pattern to follow: set `interactive: true` in the skill's frontmatter, add a gate-tier E2E test to touchfiles.ts using the extended `test/helpers/agent-sdk-runner.ts`.
|
||||||
|
|
||||||
|
**Effort:** M (human: ~3-5 days / CC+gstack: ~1-2 hours — the thinking cost is per-skill classification, not code).
|
||||||
|
**Priority:** P2 — plan-mode handshake on the 4 review skills catches the reported bug; this TODO catches the cousins.
|
||||||
|
**Depends on / blocked by:** v1.11.1.0 landing (provides the preamble-pivot architecture the cousins inherit).
|
||||||
|
|
||||||
## Context skills
|
## Context skills
|
||||||
|
|
||||||
### `/context-save --lane` + `/context-restore --lane` for parallel workstreams
|
### `/context-save --lane` + `/context-restore --lane` for parallel workstreams
|
||||||
|
|
@ -18,22 +70,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.
|
||||||
|
|
@ -1268,6 +1304,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())
|
||||||
|
|
|
||||||
|
|
@ -358,6 +358,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -570,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*.
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,13 @@ CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on ne
|
||||||
# gstack_contributor: false # true = file field reports when gstack misbehaves
|
# gstack_contributor: false # true = file field reports when gstack misbehaves
|
||||||
# skip_eng_review: false # true = skip eng review gate in /ship (not recommended)
|
# skip_eng_review: false # true = skip eng review gate in /ship (not recommended)
|
||||||
#
|
#
|
||||||
|
# ─── Workspace-aware ship ────────────────────────────────────────────
|
||||||
|
# workspace_root: $HOME/conductor/workspaces # Where /ship looks for sibling
|
||||||
|
# # Conductor worktrees when picking a VERSION slot.
|
||||||
|
# # Set to "null" to disable sibling scanning entirely.
|
||||||
|
# # Non-Conductor users can point this at any directory
|
||||||
|
# # that holds parallel worktrees of the same repo.
|
||||||
|
#
|
||||||
'
|
'
|
||||||
|
|
||||||
# DEFAULTS table — canonical default values for known keys.
|
# DEFAULTS table — canonical default values for known keys.
|
||||||
|
|
@ -96,6 +103,7 @@ lookup_default() {
|
||||||
codex_reviews) echo "enabled" ;;
|
codex_reviews) echo "enabled" ;;
|
||||||
gstack_contributor) echo "false" ;;
|
gstack_contributor) echo "false" ;;
|
||||||
skip_eng_review) echo "false" ;;
|
skip_eng_review) echo "false" ;;
|
||||||
|
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) echo "off" ;;
|
||||||
gbrain_sync_mode_prompted) echo "false" ;;
|
gbrain_sync_mode_prompted) echo "false" ;;
|
||||||
|
|
@ -162,8 +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 gbrain_sync_mode \
|
gstack_contributor skip_eng_review workspace_root \
|
||||||
gbrain_sync_mode_prompted; do
|
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
|
||||||
|
|
@ -178,8 +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 gbrain_sync_mode \
|
gstack_contributor skip_eng_review workspace_root \
|
||||||
gbrain_sync_mode_prompted; do
|
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
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,477 @@
|
||||||
|
#!/usr/bin/env bun
|
||||||
|
// gstack-next-version — host-aware VERSION allocator for /ship.
|
||||||
|
//
|
||||||
|
// Queries the PR queue (GitHub or GitLab), fetches each open PR's VERSION,
|
||||||
|
// scans configurable Conductor sibling worktrees, picks the next free version
|
||||||
|
// slot at the requested bump level, and emits the whole picture as JSON.
|
||||||
|
//
|
||||||
|
// Contract: util NEVER writes files or mutates state. Pure reader + reporter.
|
||||||
|
// /ship consumes the JSON and decides what to do.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// gstack-next-version --base <branch> --bump <major|minor|patch|micro> \
|
||||||
|
// --current-version <X.Y.Z.W> [--workspace-root <path>|null] [--json]
|
||||||
|
//
|
||||||
|
// Exit codes:
|
||||||
|
// 0 — emitted JSON successfully (may include "offline":true or "host":"unknown")
|
||||||
|
// 2 — invalid arguments
|
||||||
|
// 3 — util bug (unexpected exception)
|
||||||
|
|
||||||
|
import { execFileSync, spawnSync } from "node:child_process";
|
||||||
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { join, resolve } from "node:path";
|
||||||
|
|
||||||
|
type Bump = "major" | "minor" | "patch" | "micro";
|
||||||
|
type Version = [number, number, number, number];
|
||||||
|
|
||||||
|
type ClaimedPR = {
|
||||||
|
pr: number;
|
||||||
|
branch: string;
|
||||||
|
version: string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Sibling = {
|
||||||
|
path: string;
|
||||||
|
branch: string;
|
||||||
|
version: string;
|
||||||
|
last_commit_ts: number;
|
||||||
|
has_open_pr: boolean;
|
||||||
|
is_active: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Output = {
|
||||||
|
version: string;
|
||||||
|
current_version: string;
|
||||||
|
base_version: string;
|
||||||
|
bump: Bump;
|
||||||
|
host: "github" | "gitlab" | "unknown";
|
||||||
|
offline: boolean;
|
||||||
|
claimed: ClaimedPR[];
|
||||||
|
siblings: Sibling[];
|
||||||
|
active_siblings: Sibling[];
|
||||||
|
reason: string;
|
||||||
|
warnings: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const ACTIVE_SIBLING_MAX_AGE_S = 24 * 60 * 60;
|
||||||
|
const GH_API_CONCURRENCY = 10;
|
||||||
|
|
||||||
|
function parseVersion(s: string): Version | null {
|
||||||
|
const m = s.trim().match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
||||||
|
if (!m) return null;
|
||||||
|
return [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtVersion(v: Version): string {
|
||||||
|
return v.join(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
function bumpVersion(v: Version, level: Bump): Version {
|
||||||
|
switch (level) {
|
||||||
|
case "major":
|
||||||
|
return [v[0] + 1, 0, 0, 0];
|
||||||
|
case "minor":
|
||||||
|
return [v[0], v[1] + 1, 0, 0];
|
||||||
|
case "patch":
|
||||||
|
return [v[0], v[1], v[2] + 1, 0];
|
||||||
|
case "micro":
|
||||||
|
return [v[0], v[1], v[2], v[3] + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmpVersion(a: Version, b: Version): number {
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
if (a[i] !== b[i]) return a[i] - b[i];
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collision resolution: bump past the highest claimed within the same level.
|
||||||
|
// Semantics: if my bump is MINOR and the queue claims 1.7.0.0, I advance to
|
||||||
|
// 1.8.0.0 (still a MINOR relative to main). Preserves ship-time intent.
|
||||||
|
function pickNextSlot(base: Version, claimed: Version[], level: Bump): { version: Version; reason: string } {
|
||||||
|
let candidate = bumpVersion(base, level);
|
||||||
|
const sortedClaimed = [...claimed].sort(cmpVersion);
|
||||||
|
const highest = sortedClaimed[sortedClaimed.length - 1];
|
||||||
|
if (highest && cmpVersion(highest, base) > 0) {
|
||||||
|
// Queue already advanced past base; bump past the highest claim.
|
||||||
|
const bumpedPastHighest = bumpVersion(highest, level);
|
||||||
|
if (cmpVersion(bumpedPastHighest, candidate) > 0) {
|
||||||
|
return { version: bumpedPastHighest, reason: `bumped past claimed ${fmtVersion(highest)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { version: candidate, reason: "no collision; clean bump from base" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCommand(cmd: string, args: string[], timeoutMs = 15000): { ok: boolean; stdout: string; stderr: string } {
|
||||||
|
const r = spawnSync(cmd, args, { encoding: "utf8", timeout: timeoutMs });
|
||||||
|
return {
|
||||||
|
ok: r.status === 0 && !r.error,
|
||||||
|
stdout: r.stdout ?? "",
|
||||||
|
stderr: r.stderr ?? (r.error ? String(r.error) : ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectHost(): "github" | "gitlab" | "unknown" {
|
||||||
|
const remote = runCommand("git", ["remote", "get-url", "origin"]);
|
||||||
|
if (remote.ok) {
|
||||||
|
const url = remote.stdout.trim();
|
||||||
|
if (url.includes("github.com")) return "github";
|
||||||
|
if (url.includes("gitlab")) return "gitlab";
|
||||||
|
}
|
||||||
|
const gh = runCommand("gh", ["auth", "status"]);
|
||||||
|
if (gh.ok) return "github";
|
||||||
|
const glab = runCommand("glab", ["auth", "status"]);
|
||||||
|
if (glab.ok) return "gitlab";
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBaseVersion(base: string, warnings: string[]): string {
|
||||||
|
// git fetch is best-effort; we tolerate failure and fall back to whatever
|
||||||
|
// origin/<base> currently points at.
|
||||||
|
runCommand("git", ["fetch", "origin", base, "--quiet"], 10000);
|
||||||
|
const r = runCommand("git", ["show", `origin/${base}:VERSION`]);
|
||||||
|
if (!r.ok) {
|
||||||
|
warnings.push(`could not read VERSION at origin/${base}; assuming 0.0.0.0`);
|
||||||
|
return "0.0.0.0";
|
||||||
|
}
|
||||||
|
return r.stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchGithubClaimed(base: string, excludePR: number | null, warnings: string[]): Promise<{ claimed: ClaimedPR[]; offline: boolean }> {
|
||||||
|
const list = runCommand("gh", [
|
||||||
|
"pr",
|
||||||
|
"list",
|
||||||
|
"--state",
|
||||||
|
"open",
|
||||||
|
"--base",
|
||||||
|
base,
|
||||||
|
"--limit",
|
||||||
|
"200",
|
||||||
|
"--json",
|
||||||
|
"number,headRefName,headRepositoryOwner,url,isDraft",
|
||||||
|
]);
|
||||||
|
if (!list.ok) {
|
||||||
|
warnings.push(`gh pr list failed: ${list.stderr.trim().slice(0, 200)}`);
|
||||||
|
return { claimed: [], offline: true };
|
||||||
|
}
|
||||||
|
let prs: {
|
||||||
|
number: number;
|
||||||
|
headRefName: string;
|
||||||
|
headRepositoryOwner?: { login: string };
|
||||||
|
url: string;
|
||||||
|
isDraft: boolean;
|
||||||
|
}[];
|
||||||
|
try {
|
||||||
|
prs = JSON.parse(list.stdout);
|
||||||
|
} catch (e) {
|
||||||
|
warnings.push(`gh pr list returned invalid JSON`);
|
||||||
|
return { claimed: [], offline: true };
|
||||||
|
}
|
||||||
|
// Determine our repo owner to filter out fork PRs. `gh api contents?ref=<branch>`
|
||||||
|
// resolves to OUR repo regardless of where the PR originated, so fork PRs would
|
||||||
|
// otherwise return our main's VERSION as a phantom claim.
|
||||||
|
const viewer = runCommand("gh", ["repo", "view", "--json", "owner", "-q", ".owner.login"]);
|
||||||
|
const myOwner = viewer.ok ? viewer.stdout.trim() : "";
|
||||||
|
const sameRepoPRs = (myOwner
|
||||||
|
? prs.filter((p) => (p.headRepositoryOwner?.login ?? "") === myOwner)
|
||||||
|
: prs
|
||||||
|
).filter((p) => excludePR === null || p.number !== excludePR);
|
||||||
|
// Fetch each PR's VERSION at its head in parallel (bounded concurrency).
|
||||||
|
const results: ClaimedPR[] = [];
|
||||||
|
const queue = [...sameRepoPRs];
|
||||||
|
const workers = Array.from({ length: Math.min(GH_API_CONCURRENCY, sameRepoPRs.length) }, async () => {
|
||||||
|
while (queue.length) {
|
||||||
|
const pr = queue.shift();
|
||||||
|
if (!pr) return;
|
||||||
|
// gh passes branch name via argv, not shell — safe.
|
||||||
|
const content = runCommand("gh", [
|
||||||
|
"api",
|
||||||
|
`repos/{owner}/{repo}/contents/VERSION?ref=${encodeURIComponent(pr.headRefName)}`,
|
||||||
|
"-q",
|
||||||
|
".content",
|
||||||
|
]);
|
||||||
|
if (!content.ok) {
|
||||||
|
warnings.push(`PR #${pr.number}: could not fetch VERSION (fork or private)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let versionStr: string;
|
||||||
|
try {
|
||||||
|
versionStr = Buffer.from(content.stdout.trim(), "base64").toString("utf8").trim();
|
||||||
|
} catch {
|
||||||
|
warnings.push(`PR #${pr.number}: VERSION is not valid base64`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!parseVersion(versionStr)) {
|
||||||
|
warnings.push(`PR #${pr.number}: VERSION is malformed (${versionStr})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
results.push({ pr: pr.number, branch: pr.headRefName, version: versionStr, url: pr.url });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(workers);
|
||||||
|
return { claimed: results, offline: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchGitlabClaimed(base: string, excludePR: number | null, warnings: string[]): Promise<{ claimed: ClaimedPR[]; offline: boolean }> {
|
||||||
|
const list = runCommand("glab", [
|
||||||
|
"mr",
|
||||||
|
"list",
|
||||||
|
"--opened",
|
||||||
|
"--target-branch",
|
||||||
|
base,
|
||||||
|
"--output",
|
||||||
|
"json",
|
||||||
|
"--per-page",
|
||||||
|
"200",
|
||||||
|
]);
|
||||||
|
if (!list.ok) {
|
||||||
|
warnings.push(`glab mr list failed: ${list.stderr.trim().slice(0, 200)}`);
|
||||||
|
return { claimed: [], offline: true };
|
||||||
|
}
|
||||||
|
let mrs: { iid: number; source_branch: string; web_url: string }[];
|
||||||
|
try {
|
||||||
|
mrs = JSON.parse(list.stdout);
|
||||||
|
} catch {
|
||||||
|
warnings.push(`glab mr list returned invalid JSON`);
|
||||||
|
return { claimed: [], offline: true };
|
||||||
|
}
|
||||||
|
if (excludePR !== null) {
|
||||||
|
mrs = mrs.filter((mr) => mr.iid !== excludePR);
|
||||||
|
}
|
||||||
|
const results: ClaimedPR[] = [];
|
||||||
|
for (const mr of mrs) {
|
||||||
|
const content = runCommand("glab", [
|
||||||
|
"api",
|
||||||
|
`projects/:id/repository/files/VERSION?ref=${encodeURIComponent(mr.source_branch)}`,
|
||||||
|
]);
|
||||||
|
if (!content.ok) {
|
||||||
|
warnings.push(`MR !${mr.iid}: could not fetch VERSION`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const j = JSON.parse(content.stdout);
|
||||||
|
const versionStr = Buffer.from(j.content, "base64").toString("utf8").trim();
|
||||||
|
if (!parseVersion(versionStr)) {
|
||||||
|
warnings.push(`MR !${mr.iid}: VERSION malformed (${versionStr})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
results.push({ pr: mr.iid, branch: mr.source_branch, version: versionStr, url: mr.web_url });
|
||||||
|
} catch {
|
||||||
|
warnings.push(`MR !${mr.iid}: unexpected glab api response`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { claimed: results, offline: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveWorkspaceRoot(override?: string): string | null {
|
||||||
|
if (override === "null") return null;
|
||||||
|
if (override) return override;
|
||||||
|
const r = runCommand(join(__dirname, "gstack-config"), ["get", "workspace_root"]);
|
||||||
|
const configured = r.ok ? r.stdout.trim() : "";
|
||||||
|
if (configured === "null") return null;
|
||||||
|
if (configured) return configured;
|
||||||
|
// Default: $HOME/conductor/workspaces/
|
||||||
|
return join(homedir(), "conductor", "workspaces");
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentRepoSlug(): string {
|
||||||
|
const r = runCommand("git", ["remote", "get-url", "origin"]);
|
||||||
|
if (!r.ok) return "";
|
||||||
|
// Extract "owner/repo" from URL like git@github.com:owner/repo.git
|
||||||
|
const m = r.stdout.trim().match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
||||||
|
return m ? m[1] : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanSiblings(root: string | null, claimed: ClaimedPR[], warnings: string[]): Sibling[] {
|
||||||
|
if (!root || !existsSync(root)) return [];
|
||||||
|
const mySlug = currentRepoSlug();
|
||||||
|
if (!mySlug) {
|
||||||
|
warnings.push("could not determine current repo slug; skipping sibling scan");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const repoName = mySlug.split("/").pop() ?? "";
|
||||||
|
// Conductor layout: <root>/<repo>/<workspace>/
|
||||||
|
const repoDir = join(root, repoName);
|
||||||
|
if (!existsSync(repoDir)) return [];
|
||||||
|
const myAbsPath = resolve(process.cwd());
|
||||||
|
const results: Sibling[] = [];
|
||||||
|
for (const name of readdirSync(repoDir)) {
|
||||||
|
const p = join(repoDir, name);
|
||||||
|
if (resolve(p) === myAbsPath) continue;
|
||||||
|
try {
|
||||||
|
const s = statSync(p);
|
||||||
|
if (!s.isDirectory()) continue;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!existsSync(join(p, ".git")) && !existsSync(join(p, ".git/HEAD"))) continue;
|
||||||
|
const versionFile = join(p, "VERSION");
|
||||||
|
if (!existsSync(versionFile)) continue;
|
||||||
|
let version: string;
|
||||||
|
try {
|
||||||
|
version = readFileSync(versionFile, "utf8").trim();
|
||||||
|
if (!parseVersion(version)) continue;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const branchR = runCommand("git", ["-C", p, "rev-parse", "--abbrev-ref", "HEAD"]);
|
||||||
|
if (!branchR.ok) continue;
|
||||||
|
const branch = branchR.stdout.trim();
|
||||||
|
const commitTsR = runCommand("git", ["-C", p, "log", "-1", "--format=%ct"]);
|
||||||
|
const last_commit_ts = commitTsR.ok ? Number(commitTsR.stdout.trim()) : 0;
|
||||||
|
const has_open_pr = claimed.some((c) => c.branch === branch);
|
||||||
|
results.push({
|
||||||
|
path: p,
|
||||||
|
branch,
|
||||||
|
version,
|
||||||
|
last_commit_ts,
|
||||||
|
has_open_pr,
|
||||||
|
is_active: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markActiveSiblings(siblings: Sibling[], baseVersion: Version): Sibling[] {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
return siblings.map((s) => {
|
||||||
|
const v = parseVersion(s.version);
|
||||||
|
const isAhead = v ? cmpVersion(v, baseVersion) > 0 : false;
|
||||||
|
const isFresh = s.last_commit_ts > 0 && now - s.last_commit_ts < ACTIVE_SIBLING_MAX_AGE_S;
|
||||||
|
const is_active = isAhead && isFresh && !s.has_open_pr;
|
||||||
|
return { ...s, is_active };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv: string[]): { base: string; bump: Bump; current: string; workspaceRoot?: string; excludePR: number | null; help: boolean } {
|
||||||
|
let base = "";
|
||||||
|
let bump: Bump | "" = "";
|
||||||
|
let current = "";
|
||||||
|
let workspaceRoot: string | undefined;
|
||||||
|
let excludePR: number | null = null;
|
||||||
|
let help = false;
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const a = argv[i];
|
||||||
|
if (a === "--base") base = argv[++i] ?? "";
|
||||||
|
else if (a === "--bump") bump = (argv[++i] ?? "") as Bump;
|
||||||
|
else if (a === "--current-version") current = argv[++i] ?? "";
|
||||||
|
else if (a === "--workspace-root") workspaceRoot = argv[++i];
|
||||||
|
else if (a === "--exclude-pr") {
|
||||||
|
const n = Number(argv[++i]);
|
||||||
|
excludePR = Number.isFinite(n) && n > 0 ? n : null;
|
||||||
|
}
|
||||||
|
else if (a === "-h" || a === "--help") help = true;
|
||||||
|
}
|
||||||
|
if (help) return { base: "", bump: "micro", current: "", excludePR: null, help: true };
|
||||||
|
if (!base) base = "main";
|
||||||
|
if (!bump) {
|
||||||
|
console.error("Error: --bump is required (major|minor|patch|micro)");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
if (!["major", "minor", "patch", "micro"].includes(bump)) {
|
||||||
|
console.error(`Error: --bump must be major|minor|patch|micro (got ${bump})`);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
return { base, bump: bump as Bump, current, workspaceRoot, excludePR, help: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect: if --exclude-pr wasn't passed, check whether the current branch
|
||||||
|
// already has an open PR and exclude it by default. This prevents the self-
|
||||||
|
// reference bug where /ship's own PR inflates the queue on rerun.
|
||||||
|
function autoDetectExcludePR(): number | null {
|
||||||
|
const r = runCommand("gh", ["pr", "view", "--json", "number", "-q", ".number"]);
|
||||||
|
if (!r.ok) return null;
|
||||||
|
const n = Number(r.stdout.trim());
|
||||||
|
return Number.isFinite(n) && n > 0 ? n : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
if (args.help) {
|
||||||
|
console.log(
|
||||||
|
"Usage: gstack-next-version --base <branch> --bump <level> --current-version <X.Y.Z.W> [--workspace-root <path|null>]",
|
||||||
|
);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
const warnings: string[] = [];
|
||||||
|
const host = detectHost();
|
||||||
|
const baseVersion = args.current || readBaseVersion(args.base, warnings);
|
||||||
|
const baseParsed = parseVersion(baseVersion);
|
||||||
|
if (!baseParsed) {
|
||||||
|
console.error(`Error: could not parse base version '${baseVersion}'`);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const excludePR = args.excludePR ?? autoDetectExcludePR();
|
||||||
|
if (excludePR !== null && args.excludePR === null) {
|
||||||
|
warnings.push(`auto-excluded PR #${excludePR} (current branch's own PR)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let claimed: ClaimedPR[] = [];
|
||||||
|
let offline = false;
|
||||||
|
if (host === "github") {
|
||||||
|
({ claimed, offline } = await fetchGithubClaimed(args.base, excludePR, warnings));
|
||||||
|
} else if (host === "gitlab") {
|
||||||
|
({ claimed, offline } = await fetchGitlabClaimed(args.base, excludePR, warnings));
|
||||||
|
} else {
|
||||||
|
warnings.push("host unknown; queue-awareness unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only count PRs that actually bumped VERSION past base as real "claims".
|
||||||
|
// A PR whose VERSION equals base's VERSION hasn't claimed anything.
|
||||||
|
const realClaims = claimed.filter((c) => {
|
||||||
|
const v = parseVersion(c.version);
|
||||||
|
return v !== null && cmpVersion(v, baseParsed) > 0;
|
||||||
|
});
|
||||||
|
const claimedVersions = realClaims
|
||||||
|
.map((c) => parseVersion(c.version))
|
||||||
|
.filter((v): v is Version => v !== null);
|
||||||
|
|
||||||
|
const { version: picked, reason } = pickNextSlot(baseParsed, claimedVersions, args.bump);
|
||||||
|
|
||||||
|
const workspaceRoot = resolveWorkspaceRoot(args.workspaceRoot);
|
||||||
|
const siblings = markActiveSiblings(scanSiblings(workspaceRoot, claimed, warnings), baseParsed);
|
||||||
|
const activeSiblings = siblings.filter((s) => s.is_active);
|
||||||
|
|
||||||
|
// If an active sibling outranks our pick, bump past it (same bump level).
|
||||||
|
let finalVersion = picked;
|
||||||
|
let finalReason = reason;
|
||||||
|
const activeAhead = activeSiblings
|
||||||
|
.map((s) => parseVersion(s.version))
|
||||||
|
.filter((v): v is Version => v !== null)
|
||||||
|
.filter((v) => cmpVersion(v, finalVersion) >= 0);
|
||||||
|
if (activeAhead.length) {
|
||||||
|
const highest = activeAhead.sort(cmpVersion)[activeAhead.length - 1];
|
||||||
|
finalVersion = bumpVersion(highest, args.bump);
|
||||||
|
finalReason = `bumped past active sibling ${fmtVersion(highest)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const out: Output = {
|
||||||
|
version: fmtVersion(finalVersion),
|
||||||
|
current_version: args.current || baseVersion,
|
||||||
|
base_version: baseVersion,
|
||||||
|
bump: args.bump,
|
||||||
|
host,
|
||||||
|
offline,
|
||||||
|
claimed: realClaims,
|
||||||
|
siblings,
|
||||||
|
active_siblings: activeSiblings,
|
||||||
|
reason: finalReason,
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
|
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pure-function exports for testing
|
||||||
|
export { parseVersion, fmtVersion, bumpVersion, cmpVersion, pickNextSlot, markActiveSiblings };
|
||||||
|
|
||||||
|
// Only run main() when invoked as a script, not when imported by tests.
|
||||||
|
if (import.meta.main) {
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error("Unexpected error:", e?.stack ?? e);
|
||||||
|
process.exit(3);
|
||||||
|
});
|
||||||
|
}
|
||||||
185
bun.lock
185
bun.lock
|
|
@ -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=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
143
canary/SKILL.md
143
canary/SKILL.md
|
|
@ -350,6 +350,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -562,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*.
|
||||||
|
|
|
||||||
143
codex/SKILL.md
143
codex/SKILL.md
|
|
@ -352,6 +352,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -564,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*.
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -566,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*.
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -566,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*.
|
||||||
|
|
|
||||||
143
cso/SKILL.md
143
cso/SKILL.md
|
|
@ -355,6 +355,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -567,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*.
|
||||||
|
|
|
||||||
|
|
@ -355,6 +355,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -567,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*.
|
||||||
|
|
|
||||||
|
|
@ -357,6 +357,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -569,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*.
|
||||||
|
|
|
||||||
|
|
@ -355,6 +355,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -567,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*.
|
||||||
|
|
|
||||||
|
|
@ -352,6 +352,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -564,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*.
|
||||||
|
|
|
||||||
|
|
@ -355,6 +355,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -567,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*.
|
||||||
|
|
|
||||||
|
|
@ -352,6 +352,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -564,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*.
|
||||||
|
|
|
||||||
143
health/SKILL.md
143
health/SKILL.md
|
|
@ -352,6 +352,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -564,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*.
|
||||||
|
|
|
||||||
|
|
@ -369,6 +369,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -581,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*.
|
||||||
|
|
|
||||||
|
|
@ -349,6 +349,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -561,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*.
|
||||||
|
|
@ -1332,6 +1447,49 @@ If timeout (15 min): **STOP.** "CI has been running for over 15 minutes — that
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Step 3.4: VERSION drift detection (workspace-aware ship)
|
||||||
|
|
||||||
|
Before gathering readiness evidence, verify that the VERSION this PR claims is still the next free slot. A sibling workspace may have shipped and landed since `/ship` ran, leaving this PR's VERSION stale.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BRANCH_VERSION=$(git show HEAD:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "")
|
||||||
|
BASE_BRANCH=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo main)
|
||||||
|
BASE_VERSION=$(git show origin/$BASE_BRANCH:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "")
|
||||||
|
|
||||||
|
# Imply bump level by comparing branch VERSION to base (crude but good enough for drift detection)
|
||||||
|
# We don't need the exact original level — we just need "a level" that passes to the util.
|
||||||
|
# If the minor digit advanced, call it minor; patch digit, patch; etc. If base > branch, skip (not ours to land).
|
||||||
|
# For simplicity: use "patch" as a conservative default; util handles collision-past regardless of input level.
|
||||||
|
QUEUE_JSON=$(bun run bin/gstack-next-version \
|
||||||
|
--base "$BASE_BRANCH" \
|
||||||
|
--bump patch \
|
||||||
|
--current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}')
|
||||||
|
NEXT_SLOT=$(echo "$QUEUE_JSON" | jq -r '.version // empty')
|
||||||
|
OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false')
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
|
||||||
|
1. If `OFFLINE=true` or the util fails: print `⚠ VERSION drift check unavailable (util offline) — proceeding with PR version v<BRANCH_VERSION>`. Continue to Step 3.5. CI's version-gate job is the backstop.
|
||||||
|
|
||||||
|
2. If `BRANCH_VERSION` is already `>=` than `NEXT_SLOT`: no drift (or our PR is ahead of the queue). Continue.
|
||||||
|
|
||||||
|
3. If drift is detected (a PR landed ahead of us and `BRANCH_VERSION < NEXT_SLOT`): **STOP** and print exactly:
|
||||||
|
```
|
||||||
|
⚠ VERSION drift detected.
|
||||||
|
This PR claims: v<BRANCH_VERSION>
|
||||||
|
Next free slot: v<NEXT_SLOT> (queue moved since last /ship)
|
||||||
|
|
||||||
|
Rerun /ship from the feature branch to reconcile. /ship's ALREADY_BUMPED
|
||||||
|
branch will detect the drift and rewrite VERSION + CHANGELOG header + PR title
|
||||||
|
atomically. Do NOT merge from here — the landed PR would overwrite the other
|
||||||
|
branch's CHANGELOG entry or land with a duplicate version header.
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit non-zero. Do NOT auto-bump from `/land-and-deploy` — rerunning `/ship` is the clean path (it already handles VERSION + package.json + CHANGELOG header + PR title atomically via Step 12 ALREADY_BUMPED detection).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Step 3.5: Pre-merge readiness gate
|
## Step 3.5: Pre-merge readiness gate
|
||||||
|
|
||||||
**This is the critical safety check before an irreversible merge.** The merge cannot
|
**This is the critical safety check before an irreversible merge.** The merge cannot
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,49 @@ If timeout (15 min): **STOP.** "CI has been running for over 15 minutes — that
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Step 3.4: VERSION drift detection (workspace-aware ship)
|
||||||
|
|
||||||
|
Before gathering readiness evidence, verify that the VERSION this PR claims is still the next free slot. A sibling workspace may have shipped and landed since `/ship` ran, leaving this PR's VERSION stale.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BRANCH_VERSION=$(git show HEAD:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "")
|
||||||
|
BASE_BRANCH=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo main)
|
||||||
|
BASE_VERSION=$(git show origin/$BASE_BRANCH:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "")
|
||||||
|
|
||||||
|
# Imply bump level by comparing branch VERSION to base (crude but good enough for drift detection)
|
||||||
|
# We don't need the exact original level — we just need "a level" that passes to the util.
|
||||||
|
# If the minor digit advanced, call it minor; patch digit, patch; etc. If base > branch, skip (not ours to land).
|
||||||
|
# For simplicity: use "patch" as a conservative default; util handles collision-past regardless of input level.
|
||||||
|
QUEUE_JSON=$(bun run bin/gstack-next-version \
|
||||||
|
--base "$BASE_BRANCH" \
|
||||||
|
--bump patch \
|
||||||
|
--current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}')
|
||||||
|
NEXT_SLOT=$(echo "$QUEUE_JSON" | jq -r '.version // empty')
|
||||||
|
OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false')
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
|
||||||
|
1. If `OFFLINE=true` or the util fails: print `⚠ VERSION drift check unavailable (util offline) — proceeding with PR version v<BRANCH_VERSION>`. Continue to Step 3.5. CI's version-gate job is the backstop.
|
||||||
|
|
||||||
|
2. If `BRANCH_VERSION` is already `>=` than `NEXT_SLOT`: no drift (or our PR is ahead of the queue). Continue.
|
||||||
|
|
||||||
|
3. If drift is detected (a PR landed ahead of us and `BRANCH_VERSION < NEXT_SLOT`): **STOP** and print exactly:
|
||||||
|
```
|
||||||
|
⚠ VERSION drift detected.
|
||||||
|
This PR claims: v<BRANCH_VERSION>
|
||||||
|
Next free slot: v<NEXT_SLOT> (queue moved since last /ship)
|
||||||
|
|
||||||
|
Rerun /ship from the feature branch to reconcile. /ship's ALREADY_BUMPED
|
||||||
|
branch will detect the drift and rewrite VERSION + CHANGELOG header + PR title
|
||||||
|
atomically. Do NOT merge from here — the landed PR would overwrite the other
|
||||||
|
branch's CHANGELOG entry or land with a duplicate version header.
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit non-zero. Do NOT auto-bump from `/land-and-deploy` — rerunning `/ship` is the clean path (it already handles VERSION + package.json + CHANGELOG header + PR title atomically via Step 12 ALREADY_BUMPED detection).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Step 3.5: Pre-merge readiness gate
|
## Step 3.5: Pre-merge readiness gate
|
||||||
|
|
||||||
**This is the critical safety check before an irreversible merge.** The merge cannot
|
**This is the critical safety check before an irreversible merge.** The merge cannot
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,163 @@
|
||||||
|
---
|
||||||
|
name: landing-report
|
||||||
|
version: 0.1.0
|
||||||
|
description: |
|
||||||
|
Read-only queue dashboard for workspace-aware ship. Shows which VERSION slots
|
||||||
|
are currently claimed by open PRs, which sibling Conductor workspaces have
|
||||||
|
WIP work likely to ship soon, and what slot /ship would pick next. No
|
||||||
|
mutations — just a snapshot. Use when asked to "landing report", "what's in
|
||||||
|
the queue", "show me open PRs", or "which version do I claim next". (gstack)
|
||||||
|
triggers:
|
||||||
|
- landing report
|
||||||
|
- version queue
|
||||||
|
- ship queue
|
||||||
|
- what version comes next
|
||||||
|
- show open PR versions
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
sensitive: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# /landing-report — Version Queue Dashboard
|
||||||
|
|
||||||
|
{{PREAMBLE}}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why this skill exists
|
||||||
|
|
||||||
|
When you're running 5-10 parallel Conductor workspaces, it helps to see — at a
|
||||||
|
glance — which version numbers are claimed, by whom, and what slot your next
|
||||||
|
`/ship` would land in. This skill is a read-only call into the same
|
||||||
|
`bin/gstack-next-version` utility `/ship` uses, but with nothing mutating.
|
||||||
|
Think of it as `gh pr list` for VERSION numbers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Detect platform and base branch
|
||||||
|
|
||||||
|
Same detection as other gstack skills.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BASE_BRANCH=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || \
|
||||||
|
gh repo view --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null || \
|
||||||
|
echo main)
|
||||||
|
echo "Base branch: $BASE_BRANCH"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Read current state
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CURRENT_VERSION=$(cat VERSION 2>/dev/null | tr -d '[:space:]' || echo "0.0.0.0")
|
||||||
|
git fetch origin "$BASE_BRANCH" --quiet 2>/dev/null || true
|
||||||
|
BASE_VERSION=$(git show "origin/$BASE_BRANCH:VERSION" 2>/dev/null | tr -d '[:space:]' || echo "$CURRENT_VERSION")
|
||||||
|
echo "origin/$BASE_BRANCH VERSION: $BASE_VERSION"
|
||||||
|
echo "branch HEAD VERSION: $CURRENT_VERSION"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Query the queue
|
||||||
|
|
||||||
|
Call the util three times — once for each bump level — so the user sees what
|
||||||
|
they'd claim for micro/patch/minor/major. Cheap (same gh call cached by bun).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for LEVEL in micro patch minor major; do
|
||||||
|
bun run bin/gstack-next-version \
|
||||||
|
--base "$BASE_BRANCH" \
|
||||||
|
--bump "$LEVEL" \
|
||||||
|
--current-version "$BASE_VERSION" \
|
||||||
|
> "/tmp/landing-$LEVEL.json" 2>/dev/null || echo '{"offline":true}' > "/tmp/landing-$LEVEL.json"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Render the dashboard
|
||||||
|
|
||||||
|
Build a single table output. Use the `patch`-level JSON as canonical for
|
||||||
|
queue + siblings (they're identical across bump levels; only `.version`
|
||||||
|
differs).
|
||||||
|
|
||||||
|
Use `jq` to extract:
|
||||||
|
- `.host` — github | gitlab | unknown
|
||||||
|
- `.offline` — did the query fail?
|
||||||
|
- `.claimed` — array of {pr, branch, version, url}
|
||||||
|
- `.siblings` — all sibling worktrees found
|
||||||
|
- `.active_siblings` — subset that's likely about to ship
|
||||||
|
|
||||||
|
Render in this exact format:
|
||||||
|
|
||||||
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
║ GSTACK LANDING REPORT ║
|
||||||
|
╠══════════════════════════════════════════════════════════════════╣
|
||||||
|
║ Repo: <owner/repo> ║
|
||||||
|
║ Base: <base> @ v<base-version> ║
|
||||||
|
║ Host: <github|gitlab|unknown> ║
|
||||||
|
║ Status: <ONLINE|OFFLINE: queue-awareness unavailable> ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
Open PRs claiming versions on <base>:
|
||||||
|
#1152 alpha-branch → v1.7.0.0
|
||||||
|
#1153 beta-branch → v1.7.0.0 ⚠ collision with #1152
|
||||||
|
#1151 gamma-branch → v1.6.5.0
|
||||||
|
|
||||||
|
Sibling Conductor worktrees (<workspace_root>):
|
||||||
|
path branch VERSION last commit PR
|
||||||
|
──────────────────────────────────────────────────────────────────────────────────
|
||||||
|
../tokyo-v2 feat/dashboard v1.7.1.0 3h ago none ★ active
|
||||||
|
../melbourne feat/review v1.6.0.0 12d ago none
|
||||||
|
../osaka feat/payments v1.8.0.0 5h ago #1155
|
||||||
|
|
||||||
|
★ active = has VERSION ahead of base AND last commit < 24h AND no open PR.
|
||||||
|
These are the ones likely to ship soon.
|
||||||
|
|
||||||
|
If you ran /ship right now, you'd claim:
|
||||||
|
micro bump: v1.6.3.1 (queue-advance: none)
|
||||||
|
patch bump: v1.7.1.0 (bumped past claimed 1.7.0.0)
|
||||||
|
minor bump: v1.8.0.0 (bumped past claimed 1.7.0.0)
|
||||||
|
major bump: v2.0.0.0 (no major collisions)
|
||||||
|
```
|
||||||
|
|
||||||
|
For offline / unknown-host output, print a shorter block:
|
||||||
|
|
||||||
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
║ GSTACK LANDING REPORT ║
|
||||||
|
╠══════════════════════════════════════════════════════════════════╣
|
||||||
|
║ Status: OFFLINE — queue-awareness unavailable ║
|
||||||
|
║ Reason: <offline reason from warnings> ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
Fallback: local VERSION bumps still work, but collisions cannot be detected.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Suggest next action
|
||||||
|
|
||||||
|
After rendering the table, suggest ONE of:
|
||||||
|
|
||||||
|
1. **If there are collisions in the queue** (two open PRs claim the same version):
|
||||||
|
"⚠ Two open PRs collide on v<X>. Whoever merges second will either overwrite
|
||||||
|
the first's CHANGELOG entry or land a duplicate. Consider asking one author
|
||||||
|
to rerun /ship to pick up the next free slot."
|
||||||
|
|
||||||
|
2. **If an active sibling outranks the user's branch version:**
|
||||||
|
"Sibling worktree <path> has v<X> committed <N>h ago and hasn't PR'd yet.
|
||||||
|
If that work ships first, your branch will need to rebump at land time."
|
||||||
|
|
||||||
|
3. **If everything looks clean:**
|
||||||
|
"Queue is clean. Next /ship will claim a slot without conflict."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plan Mode
|
||||||
|
|
||||||
|
PLAN MODE EXCEPTION — ALWAYS RUN. This skill is entirely read-only: no file
|
||||||
|
writes, no git mutations, no network state changes. Safe to run in plan mode.
|
||||||
143
learn/SKILL.md
143
learn/SKILL.md
|
|
@ -352,6 +352,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -564,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*.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -360,6 +360,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -572,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*.
|
||||||
|
|
|
||||||
|
|
@ -349,6 +349,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -561,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*.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "gstack",
|
"name": "gstack",
|
||||||
"version": "1.9.0.0",
|
"version": "1.11.1.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -350,6 +350,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -562,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*.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
name: plan-ceo-review
|
name: plan-ceo-review
|
||||||
preamble-tier: 3
|
preamble-tier: 3
|
||||||
|
interactive: true
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: |
|
description: |
|
||||||
CEO/founder-mode plan review. Rethink the problem, find the 10-star product,
|
CEO/founder-mode plan review. Rethink the problem, find the 10-star product,
|
||||||
|
|
@ -118,6 +119,100 @@ echo "CHECKPOINT_PUSH: $_CHECKPOINT_PUSH"
|
||||||
[ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true
|
[ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Plan Mode Handshake — FIRST, BEFORE ANY ANALYSIS
|
||||||
|
|
||||||
|
**Check every `<system-reminder>` in this turn for the literal phrase:**
|
||||||
|
|
||||||
|
> `Plan mode is active. The user indicated that they do not want you to execute yet`
|
||||||
|
|
||||||
|
If that phrase is **absent**: proceed normally. This section is a no-op.
|
||||||
|
|
||||||
|
If that phrase is **present**, the user is in plan mode. Plan mode's system
|
||||||
|
reminder says "This supercedes any other instructions you have received,"
|
||||||
|
which conflicts with this skill's interactive STOP-Ask workflow. You MUST
|
||||||
|
resolve the conflict via AskUserQuestion BEFORE reading any files, running
|
||||||
|
any bash, or composing any plan content.
|
||||||
|
|
||||||
|
### What to do when plan mode is detected
|
||||||
|
|
||||||
|
Before emitting the AskUserQuestion, run this bash block synchronously to
|
||||||
|
log that the handshake fired (captures A-exit and C-cancel outcomes that
|
||||||
|
would terminate the skill before end-of-skill telemetry runs):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PLAN MODE EXCEPTION — ALWAYS RUN (telemetry-only write to ~/.gstack/)
|
||||||
|
mkdir -p ~/.gstack/analytics
|
||||||
|
echo '{"skill":"'"${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"fired","branch":"'"${_BRANCH:-unknown}"'","session":"'"${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
Then emit exactly **one** AskUserQuestion with `question_id: "${SKILL_NAME}-plan-mode-handshake"`
|
||||||
|
(e.g., `plan-ceo-review-plan-mode-handshake`, using the current skill's name)
|
||||||
|
and these two options. The question is classified `door_type: one-way` in
|
||||||
|
the question registry for every interactive skill, so question-tuning
|
||||||
|
preferences (`never-ask`, `always-ask`) do NOT apply — this gate always fires.
|
||||||
|
|
||||||
|
**Question body (follow the AskUserQuestion Format section below):**
|
||||||
|
|
||||||
|
> This skill runs an interactive review that stops at every finding to ask
|
||||||
|
> you a question. Plan mode's default workflow is "read files, write plan,
|
||||||
|
> exit" — that silently bypasses every STOP gate in this skill. How do you
|
||||||
|
> want to proceed?
|
||||||
|
>
|
||||||
|
> **Recommendation: A** because this skill was designed for back-and-forth.
|
||||||
|
> Each scope call and each per-section finding needs your decision before it
|
||||||
|
> lands in the plan. Exiting plan mode and running the skill normally is the
|
||||||
|
> only path that preserves the interactive contract.
|
||||||
|
>
|
||||||
|
> *Note: options differ in kind (workflow shape), not coverage — no
|
||||||
|
> completeness score.*
|
||||||
|
>
|
||||||
|
> **A) Exit plan mode and run interactively (recommended)**
|
||||||
|
> ✅ Every STOP gate in this skill fires as designed — you approve each
|
||||||
|
> scope call, each per-section finding, each cross-model tension before any
|
||||||
|
> decision lands in the plan. No silent bypass.
|
||||||
|
> ✅ Matches the skill's documented workflow. Each AskUserQuestion has a
|
||||||
|
> clear recommendation, pros/cons, and net line you can skim in ~5 seconds.
|
||||||
|
> ❌ Two-step: press esc-esc to exit plan mode, then rerun
|
||||||
|
> `/plan-{skill-name}`. Slight context-switch friction, but the alternative
|
||||||
|
> is shipping a rubber-stamp review.
|
||||||
|
>
|
||||||
|
> **C) Cancel — I meant to run something else**
|
||||||
|
> ✅ Clean exit, no partial state, no plan file written, no findings
|
||||||
|
> recorded. Use this if you invoked the skill by mistake.
|
||||||
|
> ❌ No output at all — no review, no plan file. Fine if that's what you
|
||||||
|
> want; otherwise pick A.
|
||||||
|
>
|
||||||
|
> **Net.** Plan mode is incompatible with this skill's per-finding STOP
|
||||||
|
> gates. A is the right choice for any real review; C is the bail-out.
|
||||||
|
|
||||||
|
### Routing the user's answer
|
||||||
|
|
||||||
|
**If the user picks A (exit and rerun):**
|
||||||
|
|
||||||
|
1. Append the outcome to the telemetry log (synchronous, before ExitPlanMode):
|
||||||
|
```bash
|
||||||
|
echo '{"skill":"'"${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"A-exit","branch":"'"${_BRANCH:-unknown}"'","session":"'"${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
```
|
||||||
|
2. Respond to the user: "Press **esc-esc** to exit plan mode, then rerun
|
||||||
|
`/{skill-name}`. The skill will run interactively with every STOP gate
|
||||||
|
firing as designed."
|
||||||
|
3. Call `ExitPlanMode` with an empty plan body (plan mode requires
|
||||||
|
turn-end via AskUserQuestion or ExitPlanMode; there is no plan to
|
||||||
|
approve, so ExitPlanMode with an empty message is the correct exit).
|
||||||
|
|
||||||
|
**If the user picks C (cancel):**
|
||||||
|
|
||||||
|
1. Append the outcome:
|
||||||
|
```bash
|
||||||
|
echo '{"skill":"'"${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"C-cancel","branch":"'"${_BRANCH:-unknown}"'","session":"'"${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
```
|
||||||
|
2. Tell the user: "Cancelled. No plan written."
|
||||||
|
3. Call `ExitPlanMode` with an empty message noting the user cancelled.
|
||||||
|
|
||||||
|
**After the handshake completes (either A or C),** do NOT continue with the
|
||||||
|
rest of this skill's workflow. The handshake is terminal for this turn.
|
||||||
|
|
||||||
|
|
||||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||||
|
|
@ -356,6 +451,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -568,20 +792,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*.
|
||||||
|
|
@ -1295,6 +1505,9 @@ Rules:
|
||||||
|
|
||||||
Present these approach options via AskUserQuestion using the preamble's AskUserQuestion Format section: include RECOMMENDATION and `Completeness: N/10` on every option. These approaches differ in coverage (minimal viable vs ideal architecture), so completeness scoring applies directly.
|
Present these approach options via AskUserQuestion using the preamble's AskUserQuestion Format section: include RECOMMENDATION and `Completeness: N/10` on every option. These approaches differ in coverage (minimal viable vs ideal architecture), so completeness scoring applies directly.
|
||||||
|
|
||||||
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. Do NOT proceed to Step 0D or 0F until the user responds to 0C-bis. A "clearly winning approach" is still an approach decision and still needs explicit user approval before it lands in the plan.
|
||||||
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### 0D-prelude. Expansion Framing (shared by EXPANSION and SELECTIVE EXPANSION)
|
### 0D-prelude. Expansion Framing (shared by EXPANSION and SELECTIVE EXPANSION)
|
||||||
|
|
||||||
Every expansion proposal you generate in SCOPE EXPANSION or SELECTIVE EXPANSION mode follows this framing pattern:
|
Every expansion proposal you generate in SCOPE EXPANSION or SELECTIVE EXPANSION mode follows this framing pattern:
|
||||||
|
|
@ -1483,7 +1696,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)
|
||||||
|
|
@ -1513,7 +1726,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
|
||||||
|
|
@ -1543,7 +1756,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
|
||||||
|
|
@ -1559,7 +1772,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
|
||||||
|
|
@ -1596,7 +1809,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
|
||||||
|
|
@ -1609,7 +1822,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
|
||||||
|
|
@ -1650,7 +1863,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
|
||||||
|
|
@ -1662,7 +1875,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
|
||||||
|
|
@ -1679,7 +1892,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
|
||||||
|
|
@ -1695,7 +1908,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
|
||||||
|
|
@ -1711,7 +1924,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)
|
||||||
|
|
@ -1734,7 +1947,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)
|
||||||
|
|
@ -1892,7 +2105,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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
name: plan-ceo-review
|
name: plan-ceo-review
|
||||||
preamble-tier: 3
|
preamble-tier: 3
|
||||||
|
interactive: true
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: |
|
description: |
|
||||||
CEO/founder-mode plan review. Rethink the problem, find the 10-star product,
|
CEO/founder-mode plan review. Rethink the problem, find the 10-star product,
|
||||||
|
|
@ -248,6 +249,9 @@ Rules:
|
||||||
|
|
||||||
Present these approach options via AskUserQuestion using the preamble's AskUserQuestion Format section: include RECOMMENDATION and `Completeness: N/10` on every option. These approaches differ in coverage (minimal viable vs ideal architecture), so completeness scoring applies directly.
|
Present these approach options via AskUserQuestion using the preamble's AskUserQuestion Format section: include RECOMMENDATION and `Completeness: N/10` on every option. These approaches differ in coverage (minimal viable vs ideal architecture), so completeness scoring applies directly.
|
||||||
|
|
||||||
|
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. Do NOT proceed to Step 0D or 0F until the user responds to 0C-bis. A "clearly winning approach" is still an approach decision and still needs explicit user approval before it lands in the plan.
|
||||||
|
**Reminder: Do NOT make any code changes. Review only.**
|
||||||
|
|
||||||
### 0D-prelude. Expansion Framing (shared by EXPANSION and SELECTIVE EXPANSION)
|
### 0D-prelude. Expansion Framing (shared by EXPANSION and SELECTIVE EXPANSION)
|
||||||
|
|
||||||
Every expansion proposal you generate in SCOPE EXPANSION or SELECTIVE EXPANSION mode follows this framing pattern:
|
Every expansion proposal you generate in SCOPE EXPANSION or SELECTIVE EXPANSION mode follows this framing pattern:
|
||||||
|
|
@ -376,7 +380,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 +410,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 +440,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 +456,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 +493,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 +506,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 +547,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 +559,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 +576,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 +592,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 +608,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 +631,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 +655,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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
name: plan-design-review
|
name: plan-design-review
|
||||||
preamble-tier: 3
|
preamble-tier: 3
|
||||||
|
interactive: true
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
description: |
|
description: |
|
||||||
Designer's eye plan review — interactive, like CEO and Eng review.
|
Designer's eye plan review — interactive, like CEO and Eng review.
|
||||||
|
|
@ -115,6 +116,100 @@ echo "CHECKPOINT_PUSH: $_CHECKPOINT_PUSH"
|
||||||
[ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true
|
[ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Plan Mode Handshake — FIRST, BEFORE ANY ANALYSIS
|
||||||
|
|
||||||
|
**Check every `<system-reminder>` in this turn for the literal phrase:**
|
||||||
|
|
||||||
|
> `Plan mode is active. The user indicated that they do not want you to execute yet`
|
||||||
|
|
||||||
|
If that phrase is **absent**: proceed normally. This section is a no-op.
|
||||||
|
|
||||||
|
If that phrase is **present**, the user is in plan mode. Plan mode's system
|
||||||
|
reminder says "This supercedes any other instructions you have received,"
|
||||||
|
which conflicts with this skill's interactive STOP-Ask workflow. You MUST
|
||||||
|
resolve the conflict via AskUserQuestion BEFORE reading any files, running
|
||||||
|
any bash, or composing any plan content.
|
||||||
|
|
||||||
|
### What to do when plan mode is detected
|
||||||
|
|
||||||
|
Before emitting the AskUserQuestion, run this bash block synchronously to
|
||||||
|
log that the handshake fired (captures A-exit and C-cancel outcomes that
|
||||||
|
would terminate the skill before end-of-skill telemetry runs):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PLAN MODE EXCEPTION — ALWAYS RUN (telemetry-only write to ~/.gstack/)
|
||||||
|
mkdir -p ~/.gstack/analytics
|
||||||
|
echo '{"skill":"'"${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"fired","branch":"'"${_BRANCH:-unknown}"'","session":"'"${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
Then emit exactly **one** AskUserQuestion with `question_id: "${SKILL_NAME}-plan-mode-handshake"`
|
||||||
|
(e.g., `plan-ceo-review-plan-mode-handshake`, using the current skill's name)
|
||||||
|
and these two options. The question is classified `door_type: one-way` in
|
||||||
|
the question registry for every interactive skill, so question-tuning
|
||||||
|
preferences (`never-ask`, `always-ask`) do NOT apply — this gate always fires.
|
||||||
|
|
||||||
|
**Question body (follow the AskUserQuestion Format section below):**
|
||||||
|
|
||||||
|
> This skill runs an interactive review that stops at every finding to ask
|
||||||
|
> you a question. Plan mode's default workflow is "read files, write plan,
|
||||||
|
> exit" — that silently bypasses every STOP gate in this skill. How do you
|
||||||
|
> want to proceed?
|
||||||
|
>
|
||||||
|
> **Recommendation: A** because this skill was designed for back-and-forth.
|
||||||
|
> Each scope call and each per-section finding needs your decision before it
|
||||||
|
> lands in the plan. Exiting plan mode and running the skill normally is the
|
||||||
|
> only path that preserves the interactive contract.
|
||||||
|
>
|
||||||
|
> *Note: options differ in kind (workflow shape), not coverage — no
|
||||||
|
> completeness score.*
|
||||||
|
>
|
||||||
|
> **A) Exit plan mode and run interactively (recommended)**
|
||||||
|
> ✅ Every STOP gate in this skill fires as designed — you approve each
|
||||||
|
> scope call, each per-section finding, each cross-model tension before any
|
||||||
|
> decision lands in the plan. No silent bypass.
|
||||||
|
> ✅ Matches the skill's documented workflow. Each AskUserQuestion has a
|
||||||
|
> clear recommendation, pros/cons, and net line you can skim in ~5 seconds.
|
||||||
|
> ❌ Two-step: press esc-esc to exit plan mode, then rerun
|
||||||
|
> `/plan-{skill-name}`. Slight context-switch friction, but the alternative
|
||||||
|
> is shipping a rubber-stamp review.
|
||||||
|
>
|
||||||
|
> **C) Cancel — I meant to run something else**
|
||||||
|
> ✅ Clean exit, no partial state, no plan file written, no findings
|
||||||
|
> recorded. Use this if you invoked the skill by mistake.
|
||||||
|
> ❌ No output at all — no review, no plan file. Fine if that's what you
|
||||||
|
> want; otherwise pick A.
|
||||||
|
>
|
||||||
|
> **Net.** Plan mode is incompatible with this skill's per-finding STOP
|
||||||
|
> gates. A is the right choice for any real review; C is the bail-out.
|
||||||
|
|
||||||
|
### Routing the user's answer
|
||||||
|
|
||||||
|
**If the user picks A (exit and rerun):**
|
||||||
|
|
||||||
|
1. Append the outcome to the telemetry log (synchronous, before ExitPlanMode):
|
||||||
|
```bash
|
||||||
|
echo '{"skill":"'"${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"A-exit","branch":"'"${_BRANCH:-unknown}"'","session":"'"${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
```
|
||||||
|
2. Respond to the user: "Press **esc-esc** to exit plan mode, then rerun
|
||||||
|
`/{skill-name}`. The skill will run interactively with every STOP gate
|
||||||
|
firing as designed."
|
||||||
|
3. Call `ExitPlanMode` with an empty plan body (plan mode requires
|
||||||
|
turn-end via AskUserQuestion or ExitPlanMode; there is no plan to
|
||||||
|
approve, so ExitPlanMode with an empty message is the correct exit).
|
||||||
|
|
||||||
|
**If the user picks C (cancel):**
|
||||||
|
|
||||||
|
1. Append the outcome:
|
||||||
|
```bash
|
||||||
|
echo '{"skill":"'"${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"C-cancel","branch":"'"${_BRANCH:-unknown}"'","session":"'"${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
```
|
||||||
|
2. Tell the user: "Cancelled. No plan written."
|
||||||
|
3. Call `ExitPlanMode` with an empty message noting the user cancelled.
|
||||||
|
|
||||||
|
**After the handshake completes (either A or C),** do NOT continue with the
|
||||||
|
rest of this skill's workflow. The handshake is terminal for this turn.
|
||||||
|
|
||||||
|
|
||||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||||
|
|
@ -353,6 +448,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -565,20 +789,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*.
|
||||||
|
|
@ -1721,7 +1931,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
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
name: plan-design-review
|
name: plan-design-review
|
||||||
preamble-tier: 3
|
preamble-tier: 3
|
||||||
|
interactive: true
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
description: |
|
description: |
|
||||||
Designer's eye plan review — interactive, like CEO and Eng review.
|
Designer's eye plan review — interactive, like CEO and Eng review.
|
||||||
|
|
@ -345,7 +346,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
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
name: plan-devex-review
|
name: plan-devex-review
|
||||||
preamble-tier: 3
|
preamble-tier: 3
|
||||||
|
interactive: true
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
description: |
|
description: |
|
||||||
Interactive developer experience plan review. Explores developer personas,
|
Interactive developer experience plan review. Explores developer personas,
|
||||||
|
|
@ -119,6 +120,100 @@ echo "CHECKPOINT_PUSH: $_CHECKPOINT_PUSH"
|
||||||
[ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true
|
[ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Plan Mode Handshake — FIRST, BEFORE ANY ANALYSIS
|
||||||
|
|
||||||
|
**Check every `<system-reminder>` in this turn for the literal phrase:**
|
||||||
|
|
||||||
|
> `Plan mode is active. The user indicated that they do not want you to execute yet`
|
||||||
|
|
||||||
|
If that phrase is **absent**: proceed normally. This section is a no-op.
|
||||||
|
|
||||||
|
If that phrase is **present**, the user is in plan mode. Plan mode's system
|
||||||
|
reminder says "This supercedes any other instructions you have received,"
|
||||||
|
which conflicts with this skill's interactive STOP-Ask workflow. You MUST
|
||||||
|
resolve the conflict via AskUserQuestion BEFORE reading any files, running
|
||||||
|
any bash, or composing any plan content.
|
||||||
|
|
||||||
|
### What to do when plan mode is detected
|
||||||
|
|
||||||
|
Before emitting the AskUserQuestion, run this bash block synchronously to
|
||||||
|
log that the handshake fired (captures A-exit and C-cancel outcomes that
|
||||||
|
would terminate the skill before end-of-skill telemetry runs):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PLAN MODE EXCEPTION — ALWAYS RUN (telemetry-only write to ~/.gstack/)
|
||||||
|
mkdir -p ~/.gstack/analytics
|
||||||
|
echo '{"skill":"'"${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"fired","branch":"'"${_BRANCH:-unknown}"'","session":"'"${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
Then emit exactly **one** AskUserQuestion with `question_id: "${SKILL_NAME}-plan-mode-handshake"`
|
||||||
|
(e.g., `plan-ceo-review-plan-mode-handshake`, using the current skill's name)
|
||||||
|
and these two options. The question is classified `door_type: one-way` in
|
||||||
|
the question registry for every interactive skill, so question-tuning
|
||||||
|
preferences (`never-ask`, `always-ask`) do NOT apply — this gate always fires.
|
||||||
|
|
||||||
|
**Question body (follow the AskUserQuestion Format section below):**
|
||||||
|
|
||||||
|
> This skill runs an interactive review that stops at every finding to ask
|
||||||
|
> you a question. Plan mode's default workflow is "read files, write plan,
|
||||||
|
> exit" — that silently bypasses every STOP gate in this skill. How do you
|
||||||
|
> want to proceed?
|
||||||
|
>
|
||||||
|
> **Recommendation: A** because this skill was designed for back-and-forth.
|
||||||
|
> Each scope call and each per-section finding needs your decision before it
|
||||||
|
> lands in the plan. Exiting plan mode and running the skill normally is the
|
||||||
|
> only path that preserves the interactive contract.
|
||||||
|
>
|
||||||
|
> *Note: options differ in kind (workflow shape), not coverage — no
|
||||||
|
> completeness score.*
|
||||||
|
>
|
||||||
|
> **A) Exit plan mode and run interactively (recommended)**
|
||||||
|
> ✅ Every STOP gate in this skill fires as designed — you approve each
|
||||||
|
> scope call, each per-section finding, each cross-model tension before any
|
||||||
|
> decision lands in the plan. No silent bypass.
|
||||||
|
> ✅ Matches the skill's documented workflow. Each AskUserQuestion has a
|
||||||
|
> clear recommendation, pros/cons, and net line you can skim in ~5 seconds.
|
||||||
|
> ❌ Two-step: press esc-esc to exit plan mode, then rerun
|
||||||
|
> `/plan-{skill-name}`. Slight context-switch friction, but the alternative
|
||||||
|
> is shipping a rubber-stamp review.
|
||||||
|
>
|
||||||
|
> **C) Cancel — I meant to run something else**
|
||||||
|
> ✅ Clean exit, no partial state, no plan file written, no findings
|
||||||
|
> recorded. Use this if you invoked the skill by mistake.
|
||||||
|
> ❌ No output at all — no review, no plan file. Fine if that's what you
|
||||||
|
> want; otherwise pick A.
|
||||||
|
>
|
||||||
|
> **Net.** Plan mode is incompatible with this skill's per-finding STOP
|
||||||
|
> gates. A is the right choice for any real review; C is the bail-out.
|
||||||
|
|
||||||
|
### Routing the user's answer
|
||||||
|
|
||||||
|
**If the user picks A (exit and rerun):**
|
||||||
|
|
||||||
|
1. Append the outcome to the telemetry log (synchronous, before ExitPlanMode):
|
||||||
|
```bash
|
||||||
|
echo '{"skill":"'"${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"A-exit","branch":"'"${_BRANCH:-unknown}"'","session":"'"${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
```
|
||||||
|
2. Respond to the user: "Press **esc-esc** to exit plan mode, then rerun
|
||||||
|
`/{skill-name}`. The skill will run interactively with every STOP gate
|
||||||
|
firing as designed."
|
||||||
|
3. Call `ExitPlanMode` with an empty plan body (plan mode requires
|
||||||
|
turn-end via AskUserQuestion or ExitPlanMode; there is no plan to
|
||||||
|
approve, so ExitPlanMode with an empty message is the correct exit).
|
||||||
|
|
||||||
|
**If the user picks C (cancel):**
|
||||||
|
|
||||||
|
1. Append the outcome:
|
||||||
|
```bash
|
||||||
|
echo '{"skill":"'"${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"C-cancel","branch":"'"${_BRANCH:-unknown}"'","session":"'"${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
```
|
||||||
|
2. Tell the user: "Cancelled. No plan written."
|
||||||
|
3. Call `ExitPlanMode` with an empty message noting the user cancelled.
|
||||||
|
|
||||||
|
**After the handshake completes (either A or C),** do NOT continue with the
|
||||||
|
rest of this skill's workflow. The handshake is terminal for this turn.
|
||||||
|
|
||||||
|
|
||||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||||
|
|
@ -357,6 +452,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -569,20 +793,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*.
|
||||||
|
|
@ -1891,8 +2101,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
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
name: plan-devex-review
|
name: plan-devex-review
|
||||||
preamble-tier: 3
|
preamble-tier: 3
|
||||||
|
interactive: true
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
description: |
|
description: |
|
||||||
Interactive developer experience plan review. Explores developer personas,
|
Interactive developer experience plan review. Explores developer personas,
|
||||||
|
|
@ -666,8 +667,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
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
name: plan-eng-review
|
name: plan-eng-review
|
||||||
preamble-tier: 3
|
preamble-tier: 3
|
||||||
|
interactive: true
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: |
|
description: |
|
||||||
Eng manager-mode plan review. Lock in the execution plan — architecture,
|
Eng manager-mode plan review. Lock in the execution plan — architecture,
|
||||||
|
|
@ -117,6 +118,100 @@ echo "CHECKPOINT_PUSH: $_CHECKPOINT_PUSH"
|
||||||
[ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true
|
[ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Plan Mode Handshake — FIRST, BEFORE ANY ANALYSIS
|
||||||
|
|
||||||
|
**Check every `<system-reminder>` in this turn for the literal phrase:**
|
||||||
|
|
||||||
|
> `Plan mode is active. The user indicated that they do not want you to execute yet`
|
||||||
|
|
||||||
|
If that phrase is **absent**: proceed normally. This section is a no-op.
|
||||||
|
|
||||||
|
If that phrase is **present**, the user is in plan mode. Plan mode's system
|
||||||
|
reminder says "This supercedes any other instructions you have received,"
|
||||||
|
which conflicts with this skill's interactive STOP-Ask workflow. You MUST
|
||||||
|
resolve the conflict via AskUserQuestion BEFORE reading any files, running
|
||||||
|
any bash, or composing any plan content.
|
||||||
|
|
||||||
|
### What to do when plan mode is detected
|
||||||
|
|
||||||
|
Before emitting the AskUserQuestion, run this bash block synchronously to
|
||||||
|
log that the handshake fired (captures A-exit and C-cancel outcomes that
|
||||||
|
would terminate the skill before end-of-skill telemetry runs):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PLAN MODE EXCEPTION — ALWAYS RUN (telemetry-only write to ~/.gstack/)
|
||||||
|
mkdir -p ~/.gstack/analytics
|
||||||
|
echo '{"skill":"'"${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"fired","branch":"'"${_BRANCH:-unknown}"'","session":"'"${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
Then emit exactly **one** AskUserQuestion with `question_id: "${SKILL_NAME}-plan-mode-handshake"`
|
||||||
|
(e.g., `plan-ceo-review-plan-mode-handshake`, using the current skill's name)
|
||||||
|
and these two options. The question is classified `door_type: one-way` in
|
||||||
|
the question registry for every interactive skill, so question-tuning
|
||||||
|
preferences (`never-ask`, `always-ask`) do NOT apply — this gate always fires.
|
||||||
|
|
||||||
|
**Question body (follow the AskUserQuestion Format section below):**
|
||||||
|
|
||||||
|
> This skill runs an interactive review that stops at every finding to ask
|
||||||
|
> you a question. Plan mode's default workflow is "read files, write plan,
|
||||||
|
> exit" — that silently bypasses every STOP gate in this skill. How do you
|
||||||
|
> want to proceed?
|
||||||
|
>
|
||||||
|
> **Recommendation: A** because this skill was designed for back-and-forth.
|
||||||
|
> Each scope call and each per-section finding needs your decision before it
|
||||||
|
> lands in the plan. Exiting plan mode and running the skill normally is the
|
||||||
|
> only path that preserves the interactive contract.
|
||||||
|
>
|
||||||
|
> *Note: options differ in kind (workflow shape), not coverage — no
|
||||||
|
> completeness score.*
|
||||||
|
>
|
||||||
|
> **A) Exit plan mode and run interactively (recommended)**
|
||||||
|
> ✅ Every STOP gate in this skill fires as designed — you approve each
|
||||||
|
> scope call, each per-section finding, each cross-model tension before any
|
||||||
|
> decision lands in the plan. No silent bypass.
|
||||||
|
> ✅ Matches the skill's documented workflow. Each AskUserQuestion has a
|
||||||
|
> clear recommendation, pros/cons, and net line you can skim in ~5 seconds.
|
||||||
|
> ❌ Two-step: press esc-esc to exit plan mode, then rerun
|
||||||
|
> `/plan-{skill-name}`. Slight context-switch friction, but the alternative
|
||||||
|
> is shipping a rubber-stamp review.
|
||||||
|
>
|
||||||
|
> **C) Cancel — I meant to run something else**
|
||||||
|
> ✅ Clean exit, no partial state, no plan file written, no findings
|
||||||
|
> recorded. Use this if you invoked the skill by mistake.
|
||||||
|
> ❌ No output at all — no review, no plan file. Fine if that's what you
|
||||||
|
> want; otherwise pick A.
|
||||||
|
>
|
||||||
|
> **Net.** Plan mode is incompatible with this skill's per-finding STOP
|
||||||
|
> gates. A is the right choice for any real review; C is the bail-out.
|
||||||
|
|
||||||
|
### Routing the user's answer
|
||||||
|
|
||||||
|
**If the user picks A (exit and rerun):**
|
||||||
|
|
||||||
|
1. Append the outcome to the telemetry log (synchronous, before ExitPlanMode):
|
||||||
|
```bash
|
||||||
|
echo '{"skill":"'"${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"A-exit","branch":"'"${_BRANCH:-unknown}"'","session":"'"${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
```
|
||||||
|
2. Respond to the user: "Press **esc-esc** to exit plan mode, then rerun
|
||||||
|
`/{skill-name}`. The skill will run interactively with every STOP gate
|
||||||
|
firing as designed."
|
||||||
|
3. Call `ExitPlanMode` with an empty plan body (plan mode requires
|
||||||
|
turn-end via AskUserQuestion or ExitPlanMode; there is no plan to
|
||||||
|
approve, so ExitPlanMode with an empty message is the correct exit).
|
||||||
|
|
||||||
|
**If the user picks C (cancel):**
|
||||||
|
|
||||||
|
1. Append the outcome:
|
||||||
|
```bash
|
||||||
|
echo '{"skill":"'"${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"C-cancel","branch":"'"${_BRANCH:-unknown}"'","session":"'"${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
```
|
||||||
|
2. Tell the user: "Cancelled. No plan written."
|
||||||
|
3. Call `ExitPlanMode` with an empty message noting the user cancelled.
|
||||||
|
|
||||||
|
**After the handshake completes (either A or C),** do NOT continue with the
|
||||||
|
rest of this skill's workflow. The handshake is terminal for this turn.
|
||||||
|
|
||||||
|
|
||||||
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not
|
||||||
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
auto-invoke skills based on conversation context. Only run skills the user explicitly
|
||||||
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
||||||
|
|
@ -355,6 +450,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -567,20 +791,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*.
|
||||||
|
|
@ -1506,7 +1716,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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
name: plan-eng-review
|
name: plan-eng-review
|
||||||
preamble-tier: 3
|
preamble-tier: 3
|
||||||
|
interactive: true
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: |
|
description: |
|
||||||
Eng manager-mode plan review. Lock in the execution plan — architecture,
|
Eng manager-mode plan review. Lock in the execution plan — architecture,
|
||||||
|
|
@ -186,7 +187,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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -575,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*.
|
||||||
|
|
|
||||||
143
qa-only/SKILL.md
143
qa-only/SKILL.md
|
|
@ -351,6 +351,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -563,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*.
|
||||||
|
|
|
||||||
143
qa/SKILL.md
143
qa/SKILL.md
|
|
@ -357,6 +357,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -569,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*.
|
||||||
|
|
|
||||||
143
retro/SKILL.md
143
retro/SKILL.md
|
|
@ -350,6 +350,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -562,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*.
|
||||||
|
|
|
||||||
165
review/SKILL.md
165
review/SKILL.md
|
|
@ -354,6 +354,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -566,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*.
|
||||||
|
|
@ -1216,6 +1331,28 @@ git fetch origin <base> --quiet
|
||||||
|
|
||||||
Run `git diff origin/<base>` to get the full diff. This includes both committed and uncommitted changes against the latest base branch.
|
Run `git diff origin/<base>` to get the full diff. This includes both committed and uncommitted changes against the latest base branch.
|
||||||
|
|
||||||
|
## Step 3.4: Workspace-aware queue status (advisory)
|
||||||
|
|
||||||
|
Check whether this PR's claimed VERSION still points at a free slot in the queue. Advisory only — never blocks review; just informs the reviewer about landing-order risk.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BRANCH_VERSION=$(git show HEAD:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "")
|
||||||
|
BASE_BRANCH=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo main)
|
||||||
|
BASE_VERSION=$(git show origin/$BASE_BRANCH:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "")
|
||||||
|
QUEUE_JSON=$(bun run bin/gstack-next-version \
|
||||||
|
--base "$BASE_BRANCH" \
|
||||||
|
--bump patch \
|
||||||
|
--current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}')
|
||||||
|
NEXT_SLOT=$(echo "$QUEUE_JSON" | jq -r '.version // empty')
|
||||||
|
CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length // 0')
|
||||||
|
OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false')
|
||||||
|
```
|
||||||
|
|
||||||
|
- If `OFFLINE=true`: skip this section (no signal to report).
|
||||||
|
- Otherwise, include ONE line in the review output: `Version claimed: v<BRANCH_VERSION>. Queue: <CLAIMED_COUNT> PR(s) ahead. <VERDICT>` where VERDICT is either `Slot free` (if `BRANCH_VERSION >= NEXT_SLOT`) or `⚠ queue moved — rerun /ship to reconcile v<BRANCH_VERSION> → v<NEXT_SLOT>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Step 3.5: Slop scan (advisory)
|
## Step 3.5: Slop scan (advisory)
|
||||||
|
|
||||||
Run a slop scan on changed files to catch AI code quality issues (empty catches,
|
Run a slop scan on changed files to catch AI code quality issues (empty catches,
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,28 @@ git fetch origin <base> --quiet
|
||||||
|
|
||||||
Run `git diff origin/<base>` to get the full diff. This includes both committed and uncommitted changes against the latest base branch.
|
Run `git diff origin/<base>` to get the full diff. This includes both committed and uncommitted changes against the latest base branch.
|
||||||
|
|
||||||
|
## Step 3.4: Workspace-aware queue status (advisory)
|
||||||
|
|
||||||
|
Check whether this PR's claimed VERSION still points at a free slot in the queue. Advisory only — never blocks review; just informs the reviewer about landing-order risk.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BRANCH_VERSION=$(git show HEAD:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "")
|
||||||
|
BASE_BRANCH=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo main)
|
||||||
|
BASE_VERSION=$(git show origin/$BASE_BRANCH:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "")
|
||||||
|
QUEUE_JSON=$(bun run bin/gstack-next-version \
|
||||||
|
--base "$BASE_BRANCH" \
|
||||||
|
--bump patch \
|
||||||
|
--current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}')
|
||||||
|
NEXT_SLOT=$(echo "$QUEUE_JSON" | jq -r '.version // empty')
|
||||||
|
CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length // 0')
|
||||||
|
OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false')
|
||||||
|
```
|
||||||
|
|
||||||
|
- If `OFFLINE=true`: skip this section (no signal to report).
|
||||||
|
- Otherwise, include ONE line in the review output: `Version claimed: v<BRANCH_VERSION>. Queue: <CLAIMED_COUNT> PR(s) ahead. <VERDICT>` where VERDICT is either `Slot free` (if `BRANCH_VERSION >= NEXT_SLOT`) or `⚠ queue moved — rerun /ship to reconcile v<BRANCH_VERSION> → v<NEXT_SLOT>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Step 3.5: Slop scan (advisory)
|
## Step 3.5: Slop scan (advisory)
|
||||||
|
|
||||||
Run a slop scan on changed files to catch AI code quality issues (empty catches,
|
Run a slop scan on changed files to catch AI code quality issues (empty catches,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
#!/usr/bin/env bun
|
||||||
|
// compare-pr-version — CI gate helper. Compares the util's next-slot output
|
||||||
|
// against the PR's branch VERSION. Exits 0 (pass), 1 (confirmed collision),
|
||||||
|
// or 2 (util was offline — fail-open per user decision, exit 0 with warning).
|
||||||
|
//
|
||||||
|
// Input:
|
||||||
|
// argv[2] — path to next.json (the util's JSON output)
|
||||||
|
// argv[3] — optional PR number for log lines
|
||||||
|
//
|
||||||
|
// Design note: fail-open on util error. A gstack bug must never freeze the
|
||||||
|
// merge queue. Confirmed collisions (util OK, PR version < next slot) DO block.
|
||||||
|
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const [, , jsonPath, prNumber] = process.argv;
|
||||||
|
if (!jsonPath) {
|
||||||
|
console.error("Usage: compare-pr-version <next.json> [pr-number]");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: any;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(readFileSync(jsonPath, "utf8"));
|
||||||
|
} catch (e) {
|
||||||
|
console.log("::warning::could not parse util output; failing open");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.offline === true) {
|
||||||
|
console.log("::warning::workspace-aware-ship util offline; failing open (no collision check performed)");
|
||||||
|
console.log(`::notice::If you merge this PR and a queued PR landed ahead, CHANGELOG may need manual reconciliation.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PR_VERSION is supplied via env (set by the workflow from `cat VERSION`).
|
||||||
|
const prVersion = (process.env.PR_VERSION ?? "").trim();
|
||||||
|
const nextSlot = parsed.version;
|
||||||
|
|
||||||
|
if (!prVersion) {
|
||||||
|
console.log("::warning::PR_VERSION not set; failing open");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse versions for comparison.
|
||||||
|
function parseV(s: string): number[] | null {
|
||||||
|
const m = s.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
||||||
|
return m ? [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])] : null;
|
||||||
|
}
|
||||||
|
function cmp(a: number[], b: number[]): number {
|
||||||
|
for (let i = 0; i < 4; i++) if (a[i] !== b[i]) return a[i] - b[i];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const pPR = parseV(prVersion);
|
||||||
|
const pNext = parseV(nextSlot);
|
||||||
|
if (!pPR || !pNext) {
|
||||||
|
console.log(`::warning::malformed version string (PR=${prVersion}, next=${nextSlot}); failing open`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = prNumber ? `PR #${prNumber}` : "this PR";
|
||||||
|
|
||||||
|
// Emit a GitHub step summary (always helpful, even on pass).
|
||||||
|
const claimedList = (parsed.claimed ?? [])
|
||||||
|
.map((c: any) => ` #${c.pr} ${c.branch} → v${c.version}`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
console.log(`::group::Version gate (${tag})`);
|
||||||
|
console.log(` PR VERSION: v${prVersion}`);
|
||||||
|
console.log(` Next slot: v${nextSlot}`);
|
||||||
|
console.log(` Queue (${(parsed.claimed ?? []).length} open PRs claiming versions):`);
|
||||||
|
if (claimedList) console.log(claimedList);
|
||||||
|
console.log("::endgroup::");
|
||||||
|
|
||||||
|
if (cmp(pPR, pNext) >= 0) {
|
||||||
|
console.log(`✓ ${tag} claims v${prVersion} — slot is free (next would be v${nextSlot}).`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmed collision: PR version is stale.
|
||||||
|
console.log(`::error::VERSION drift: ${tag} claims v${prVersion} but the queue has moved — next free slot is v${nextSlot}.`);
|
||||||
|
console.log(`::error::Rerun /ship from the feature branch to reconcile. /ship's ALREADY_BUMPED branch handles this atomically (VERSION, package.json, CHANGELOG, PR title).`);
|
||||||
|
process.exit(1);
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
#!/usr/bin/env bun
|
||||||
|
// detect-bump — crude heuristic for picking a bump level from a VERSION pair.
|
||||||
|
// Used by CI's version-gate job to re-run the util with the "same" level that
|
||||||
|
// /ship used, without needing persisted bump-intent.
|
||||||
|
//
|
||||||
|
// Input: two VERSION strings via argv: current (base) and target (branch).
|
||||||
|
// Output: a single word: major|minor|patch|micro
|
||||||
|
//
|
||||||
|
// Heuristic: compare slot-by-slot. The first slot that differs IS the level.
|
||||||
|
// If nothing differs (shouldn't happen when called by CI gate — the whole point
|
||||||
|
// is the branch bumped VERSION), default to "patch".
|
||||||
|
|
||||||
|
function detect(a: string, b: string): string {
|
||||||
|
const pa = a.trim().match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
||||||
|
const pb = b.trim().match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
||||||
|
if (!pa || !pb) return "patch";
|
||||||
|
const [, a1, a2, a3, a4] = pa;
|
||||||
|
const [, b1, b2, b3, b4] = pb;
|
||||||
|
if (a1 !== b1) return "major";
|
||||||
|
if (a2 !== b2) return "minor";
|
||||||
|
if (a3 !== b3) return "patch";
|
||||||
|
if (a4 !== b4) return "micro";
|
||||||
|
return "patch";
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, , base, target] = process.argv;
|
||||||
|
if (!base || !target) {
|
||||||
|
console.error("Usage: detect-bump <base-version> <branch-version>");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
console.log(detect(base, target));
|
||||||
|
|
@ -425,7 +425,11 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
|
||||||
const tierMatch = tmplContent.match(/^preamble-tier:\s*(\d+)$/m);
|
const tierMatch = tmplContent.match(/^preamble-tier:\s*(\d+)$/m);
|
||||||
const preambleTier = tierMatch ? parseInt(tierMatch[1], 10) : undefined;
|
const preambleTier = tierMatch ? parseInt(tierMatch[1], 10) : undefined;
|
||||||
|
|
||||||
const ctx: TemplateContext = { skillName, tmplPath, benefitsFrom, host, paths: HOST_PATHS[host], preambleTier, model: MODEL_ARG_VAL };
|
// Extract interactive flag from frontmatter (generator-only; controls plan-mode handshake inclusion)
|
||||||
|
const interactiveMatch = tmplContent.match(/^interactive:\s*(true|false)\s*$/m);
|
||||||
|
const interactive = interactiveMatch ? interactiveMatch[1] === 'true' : undefined;
|
||||||
|
|
||||||
|
const ctx: TemplateContext = { skillName, tmplPath, benefitsFrom, host, paths: HOST_PATHS[host], preambleTier, model: MODEL_ARG_VAL, interactive };
|
||||||
|
|
||||||
// Replace placeholders (supports parameterized: {{NAME:arg1:arg2}})
|
// Replace placeholders (supports parameterized: {{NAME:arg1:arg2}})
|
||||||
// Config-driven: suppressedResolvers return empty string for this host
|
// Config-driven: suppressedResolvers return empty string for this host
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
@ -261,6 +261,45 @@ export const QUESTIONS = {
|
||||||
description: "Approve the design doc, revise sections, or start over?",
|
description: "Approve the design doc, revise sections, or start over?",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Plan-mode handshake — fires at the top of any interactive review skill
|
||||||
|
// when the user is in plan mode. Safety-critical, always asked regardless
|
||||||
|
// of user's tuning preferences. See scripts/resolvers/preamble/generate-
|
||||||
|
// plan-mode-handshake.ts.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
'plan-ceo-review-plan-mode-handshake': {
|
||||||
|
id: 'plan-ceo-review-plan-mode-handshake',
|
||||||
|
skill: 'plan-ceo-review',
|
||||||
|
category: 'routing',
|
||||||
|
door_type: 'one-way',
|
||||||
|
options: ['exit-and-rerun', 'cancel'],
|
||||||
|
description: "Plan mode detected — exit and rerun interactively, or cancel?",
|
||||||
|
},
|
||||||
|
'plan-eng-review-plan-mode-handshake': {
|
||||||
|
id: 'plan-eng-review-plan-mode-handshake',
|
||||||
|
skill: 'plan-eng-review',
|
||||||
|
category: 'routing',
|
||||||
|
door_type: 'one-way',
|
||||||
|
options: ['exit-and-rerun', 'cancel'],
|
||||||
|
description: "Plan mode detected — exit and rerun interactively, or cancel?",
|
||||||
|
},
|
||||||
|
'plan-design-review-plan-mode-handshake': {
|
||||||
|
id: 'plan-design-review-plan-mode-handshake',
|
||||||
|
skill: 'plan-design-review',
|
||||||
|
category: 'routing',
|
||||||
|
door_type: 'one-way',
|
||||||
|
options: ['exit-and-rerun', 'cancel'],
|
||||||
|
description: "Plan mode detected — exit and rerun interactively, or cancel?",
|
||||||
|
},
|
||||||
|
'plan-devex-review-plan-mode-handshake': {
|
||||||
|
id: 'plan-devex-review-plan-mode-handshake',
|
||||||
|
skill: 'plan-devex-review',
|
||||||
|
category: 'routing',
|
||||||
|
door_type: 'one-way',
|
||||||
|
options: ['exit-and-rerun', 'cancel'],
|
||||||
|
description: "Plan mode detected — exit and rerun interactively, or cancel?",
|
||||||
|
},
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// /plan-ceo-review — scope & strategy
|
// /plan-ceo-review — scope & strategy
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { generateQuestionTuning } from './question-tuning';
|
||||||
|
|
||||||
// Core bootstrap
|
// Core bootstrap
|
||||||
import { generatePreambleBash } from './preamble/generate-preamble-bash';
|
import { generatePreambleBash } from './preamble/generate-preamble-bash';
|
||||||
|
import { generatePlanModeHandshake } from './preamble/generate-plan-mode-handshake';
|
||||||
import { generateUpgradeCheck } from './preamble/generate-upgrade-check';
|
import { generateUpgradeCheck } from './preamble/generate-upgrade-check';
|
||||||
import { generateCompletionStatus } from './preamble/generate-completion-status';
|
import { generateCompletionStatus } from './preamble/generate-completion-status';
|
||||||
|
|
||||||
|
|
@ -78,6 +79,13 @@ export function generatePreamble(ctx: TemplateContext): string {
|
||||||
}
|
}
|
||||||
const sections = [
|
const sections = [
|
||||||
generatePreambleBash(ctx),
|
generatePreambleBash(ctx),
|
||||||
|
// Plan-mode handshake at position 1: after bash (so _SESSION_ID / _BRANCH /
|
||||||
|
// _TEL env vars are live for the synchronous telemetry write) and before
|
||||||
|
// all onboarding AskUserQuestion gates (so fresh-install users in plan mode
|
||||||
|
// see the handshake first, not drowned in telemetry / proactive / routing
|
||||||
|
// prompts). Host-scoped to Claude + interactive-frontmatter-scoped inside
|
||||||
|
// the resolver — no-op for every other skill/host combination.
|
||||||
|
generatePlanModeHandshake(ctx),
|
||||||
generateUpgradeCheck(ctx),
|
generateUpgradeCheck(ctx),
|
||||||
generateWritingStyleMigration(ctx),
|
generateWritingStyleMigration(ctx),
|
||||||
generateLakeIntro(),
|
generateLakeIntro(),
|
||||||
|
|
@ -87,12 +95,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),
|
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,141 @@
|
||||||
|
/**
|
||||||
|
* Plan-mode handshake resolver.
|
||||||
|
*
|
||||||
|
* Emits a STOP-Ask gate at the very top of the preamble that fires when a user
|
||||||
|
* invokes an interactive review skill while their Claude Code session is in
|
||||||
|
* plan mode. Without this gate, plan mode's "This supercedes any other
|
||||||
|
* instructions you have received" system-reminder wins against the skill's
|
||||||
|
* interactive STOP-Ask workflow and the skill silently writes a plan file
|
||||||
|
* instead of running the per-finding AskUserQuestion loop (v1.10.2.0 bug fix).
|
||||||
|
*
|
||||||
|
* Host scope
|
||||||
|
* ----------
|
||||||
|
* Only renders for Claude host (ctx.host === 'claude'). Other hosts use
|
||||||
|
* different plan-mode semantics (Codex, OpenClaw, etc.) and should not see
|
||||||
|
* Claude-specific ExitPlanMode / esc-esc prose.
|
||||||
|
*
|
||||||
|
* Opt-in
|
||||||
|
* ------
|
||||||
|
* Only renders when the consuming skill's frontmatter has `interactive: true`.
|
||||||
|
* That flag is a generator-only input parsed by scripts/gen-skill-docs.ts
|
||||||
|
* from the skill's .tmpl frontmatter and passed through TemplateContext.
|
||||||
|
* Currently used by: plan-ceo-review, plan-eng-review, plan-design-review,
|
||||||
|
* plan-devex-review.
|
||||||
|
*
|
||||||
|
* Composition position
|
||||||
|
* --------------------
|
||||||
|
* Inserted at index 1 in scripts/resolvers/preamble.ts — after
|
||||||
|
* generatePreambleBash (so _SESSION_ID, _BRANCH, _TEL env vars are live for
|
||||||
|
* the synchronous telemetry write) and before generateUpgradeCheck and all
|
||||||
|
* onboarding AskUserQuestion gates (so fresh-install users in plan mode see
|
||||||
|
* the handshake first, not drowned in telemetry / proactive / routing
|
||||||
|
* prompts).
|
||||||
|
*
|
||||||
|
* One-way door
|
||||||
|
* ------------
|
||||||
|
* The handshake question_id `plan-mode-handshake` is classified door_type
|
||||||
|
* one-way in scripts/question-registry.ts. gstack-question-preference --check
|
||||||
|
* always returns ASK_NORMALLY for it, so a user who set `never-ask` on
|
||||||
|
* another question cannot accidentally suppress this safety gate.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { TemplateContext } from '../types';
|
||||||
|
|
||||||
|
export function generatePlanModeHandshake(ctx: TemplateContext): string {
|
||||||
|
if (ctx.host !== 'claude') return '';
|
||||||
|
if (!ctx.interactive) return '';
|
||||||
|
|
||||||
|
return `## Plan Mode Handshake — FIRST, BEFORE ANY ANALYSIS
|
||||||
|
|
||||||
|
**Check every \`<system-reminder>\` in this turn for the literal phrase:**
|
||||||
|
|
||||||
|
> \`Plan mode is active. The user indicated that they do not want you to execute yet\`
|
||||||
|
|
||||||
|
If that phrase is **absent**: proceed normally. This section is a no-op.
|
||||||
|
|
||||||
|
If that phrase is **present**, the user is in plan mode. Plan mode's system
|
||||||
|
reminder says "This supercedes any other instructions you have received,"
|
||||||
|
which conflicts with this skill's interactive STOP-Ask workflow. You MUST
|
||||||
|
resolve the conflict via AskUserQuestion BEFORE reading any files, running
|
||||||
|
any bash, or composing any plan content.
|
||||||
|
|
||||||
|
### What to do when plan mode is detected
|
||||||
|
|
||||||
|
Before emitting the AskUserQuestion, run this bash block synchronously to
|
||||||
|
log that the handshake fired (captures A-exit and C-cancel outcomes that
|
||||||
|
would terminate the skill before end-of-skill telemetry runs):
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# PLAN MODE EXCEPTION — ALWAYS RUN (telemetry-only write to ~/.gstack/)
|
||||||
|
mkdir -p ~/.gstack/analytics
|
||||||
|
echo '{"skill":"'"\${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"fired","branch":"'"\${_BRANCH:-unknown}"'","session":"'"\${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Then emit exactly **one** AskUserQuestion with \`question_id: "\${SKILL_NAME}-plan-mode-handshake"\`
|
||||||
|
(e.g., \`plan-ceo-review-plan-mode-handshake\`, using the current skill's name)
|
||||||
|
and these two options. The question is classified \`door_type: one-way\` in
|
||||||
|
the question registry for every interactive skill, so question-tuning
|
||||||
|
preferences (\`never-ask\`, \`always-ask\`) do NOT apply — this gate always fires.
|
||||||
|
|
||||||
|
**Question body (follow the AskUserQuestion Format section below):**
|
||||||
|
|
||||||
|
> This skill runs an interactive review that stops at every finding to ask
|
||||||
|
> you a question. Plan mode's default workflow is "read files, write plan,
|
||||||
|
> exit" — that silently bypasses every STOP gate in this skill. How do you
|
||||||
|
> want to proceed?
|
||||||
|
>
|
||||||
|
> **Recommendation: A** because this skill was designed for back-and-forth.
|
||||||
|
> Each scope call and each per-section finding needs your decision before it
|
||||||
|
> lands in the plan. Exiting plan mode and running the skill normally is the
|
||||||
|
> only path that preserves the interactive contract.
|
||||||
|
>
|
||||||
|
> *Note: options differ in kind (workflow shape), not coverage — no
|
||||||
|
> completeness score.*
|
||||||
|
>
|
||||||
|
> **A) Exit plan mode and run interactively (recommended)**
|
||||||
|
> ✅ Every STOP gate in this skill fires as designed — you approve each
|
||||||
|
> scope call, each per-section finding, each cross-model tension before any
|
||||||
|
> decision lands in the plan. No silent bypass.
|
||||||
|
> ✅ Matches the skill's documented workflow. Each AskUserQuestion has a
|
||||||
|
> clear recommendation, pros/cons, and net line you can skim in ~5 seconds.
|
||||||
|
> ❌ Two-step: press esc-esc to exit plan mode, then rerun
|
||||||
|
> \`/plan-{skill-name}\`. Slight context-switch friction, but the alternative
|
||||||
|
> is shipping a rubber-stamp review.
|
||||||
|
>
|
||||||
|
> **C) Cancel — I meant to run something else**
|
||||||
|
> ✅ Clean exit, no partial state, no plan file written, no findings
|
||||||
|
> recorded. Use this if you invoked the skill by mistake.
|
||||||
|
> ❌ No output at all — no review, no plan file. Fine if that's what you
|
||||||
|
> want; otherwise pick A.
|
||||||
|
>
|
||||||
|
> **Net.** Plan mode is incompatible with this skill's per-finding STOP
|
||||||
|
> gates. A is the right choice for any real review; C is the bail-out.
|
||||||
|
|
||||||
|
### Routing the user's answer
|
||||||
|
|
||||||
|
**If the user picks A (exit and rerun):**
|
||||||
|
|
||||||
|
1. Append the outcome to the telemetry log (synchronous, before ExitPlanMode):
|
||||||
|
\`\`\`bash
|
||||||
|
echo '{"skill":"'"\${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"A-exit","branch":"'"\${_BRANCH:-unknown}"'","session":"'"\${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
\`\`\`
|
||||||
|
2. Respond to the user: "Press **esc-esc** to exit plan mode, then rerun
|
||||||
|
\`/{skill-name}\`. The skill will run interactively with every STOP gate
|
||||||
|
firing as designed."
|
||||||
|
3. Call \`ExitPlanMode\` with an empty plan body (plan mode requires
|
||||||
|
turn-end via AskUserQuestion or ExitPlanMode; there is no plan to
|
||||||
|
approve, so ExitPlanMode with an empty message is the correct exit).
|
||||||
|
|
||||||
|
**If the user picks C (cancel):**
|
||||||
|
|
||||||
|
1. Append the outcome:
|
||||||
|
\`\`\`bash
|
||||||
|
echo '{"skill":"'"\${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"C-cancel","branch":"'"\${_BRANCH:-unknown}"'","session":"'"\${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
||||||
|
\`\`\`
|
||||||
|
2. Tell the user: "Cancelled. No plan written."
|
||||||
|
3. Call \`ExitPlanMode\` with an empty message noting the user cancelled.
|
||||||
|
|
||||||
|
**After the handshake completes (either A or C),** do NOT continue with the
|
||||||
|
rest of this skill's workflow. The handshake is terminal for this turn.
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
@ -61,6 +61,7 @@ export interface TemplateContext {
|
||||||
paths: HostPaths;
|
paths: HostPaths;
|
||||||
preambleTier?: number; // 1-4, controls which preamble sections are included
|
preambleTier?: number; // 1-4, controls which preamble sections are included
|
||||||
model?: Model; // model family for behavioral overlay. Omitted/undefined → no overlay.
|
model?: Model; // model family for behavioral overlay. Omitted/undefined → no overlay.
|
||||||
|
interactive?: boolean; // true → emit plan-mode handshake in preamble. Generator-only, not written to SKILL.md.
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolver function signature. args is populated for parameterized placeholders like {{INVOKE_SKILL:name}}. */
|
/** Resolver function signature. args is populated for parameterized placeholders like {{INVOKE_SKILL:name}}. */
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -565,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*.
|
||||||
|
|
|
||||||
187
ship/SKILL.md
187
ship/SKILL.md
|
|
@ -355,6 +355,135 @@ 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)
|
## GBrain Sync (skill start)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -567,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*.
|
||||||
|
|
@ -2621,8 +2736,8 @@ fi
|
||||||
Read the `STATE:` line and dispatch:
|
Read the `STATE:` line and dispatch:
|
||||||
|
|
||||||
- **FRESH** → proceed with the bump action below (steps 1–4).
|
- **FRESH** → proceed with the bump action below (steps 1–4).
|
||||||
- **ALREADY_BUMPED** → skip the bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. Continue to the next step.
|
- **ALREADY_BUMPED** → skip the bump by default, BUT check for queue drift first: call `bin/gstack-next-version` with the implied bump level (derived from `CURRENT_VERSION` vs `BASE_VERSION`), compare its `.version` against `CURRENT_VERSION`. If they differ (queue moved since last ship), use **AskUserQuestion**: "VERSION drift detected: you claim v<CURRENT> but next available is v<NEW> (queue moved). A) Rebump to v<NEW> and rewrite CHANGELOG header + PR title (recommended), B) Keep v<CURRENT> — will be rejected by CI version-gate until resolved." If A, treat this as FRESH with `NEW_VERSION=<new>` and run steps 1-4 (which will also trigger Step 13 CHANGELOG header rewrite and Step 19 PR title rewrite). If B, reuse `CURRENT_VERSION` and warn that CI will likely reject. If util is offline, warn and reuse `CURRENT_VERSION`.
|
||||||
- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body.
|
- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. (Queue check still runs in ALREADY_BUMPED terms after repair.)
|
||||||
- **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative.
|
- **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative.
|
||||||
|
|
||||||
1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`)
|
1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`)
|
||||||
|
|
@ -2635,9 +2750,33 @@ Read the `STATE:` line and dispatch:
|
||||||
- **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added
|
- **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added
|
||||||
- **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes
|
- **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes
|
||||||
|
|
||||||
3. Compute the new version:
|
Save the chosen level as `BUMP_LEVEL` (one of `major`, `minor`, `patch`, `micro`). This is the user-intended level. The next step decides *placement* — the level stays the same even if queue-aware allocation has to advance past a claimed slot.
|
||||||
- Bumping a digit resets all digits to its right to 0
|
|
||||||
- Example: `0.19.1.0` + PATCH → `0.19.2.0`
|
3. **Queue-aware version pick (workspace-aware ship, v1.6.4.0+).** Call `bin/gstack-next-version` to see what's already claimed by open PRs + active sibling Conductor worktrees, then render the queue state to the user:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
QUEUE_JSON=$(bun run bin/gstack-next-version \
|
||||||
|
--base <base> \
|
||||||
|
--bump "$BUMP_LEVEL" \
|
||||||
|
--current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}')
|
||||||
|
NEW_VERSION=$(echo "$QUEUE_JSON" | jq -r '.version // empty')
|
||||||
|
CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length')
|
||||||
|
ACTIVE_SIBLING_COUNT=$(echo "$QUEUE_JSON" | jq -r '.active_siblings | length')
|
||||||
|
OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false')
|
||||||
|
REASON=$(echo "$QUEUE_JSON" | jq -r '.reason // ""')
|
||||||
|
```
|
||||||
|
|
||||||
|
- If `OFFLINE=true` or the util fails (auth expired, no `gh`/`glab`, network): fall back to local `BUMP_LEVEL` arithmetic (bump `BASE_VERSION` at the chosen level). Print `⚠ workspace-aware ship offline — using local bump only`. Continue.
|
||||||
|
- If `CLAIMED_COUNT > 0`: render the queue table to the user so they can see landing order at a glance:
|
||||||
|
```
|
||||||
|
Queue on <base> (vBASE_VERSION):
|
||||||
|
#<pr> <branch> → v<version> [⚠ collision with #<other>]
|
||||||
|
Active sibling workspaces (WIP, not yet PR'd):
|
||||||
|
<path> → v<version> (committed Nh ago)
|
||||||
|
Your branch will claim: vNEW_VERSION (<reason>)
|
||||||
|
```
|
||||||
|
- If `ACTIVE_SIBLING_COUNT > 0` and any active sibling's VERSION is `>= NEW_VERSION`, use **AskUserQuestion**: "Sibling workspace <path> has v<X> committed <N>h ago but hasn't PR'd yet. Wait for them to ship first, or advance past? A) Advance past (recommended for unrelated work), B) Abort /ship and sync up with sibling first."
|
||||||
|
- Validate `NEW_VERSION` matches `MAJOR.MINOR.PATCH.MICRO`. If util returns an empty or malformed version, fall back to local bump.
|
||||||
|
|
||||||
4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`.
|
4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`.
|
||||||
|
|
||||||
|
|
@ -2978,7 +3117,11 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number):
|
||||||
glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR"
|
glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR"
|
||||||
```
|
```
|
||||||
|
|
||||||
If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. Print the existing URL and continue to Step 20.
|
If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run.
|
||||||
|
|
||||||
|
**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v<NEW_VERSION> <type>: <summary>` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION <type>: <summary>"` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v<X.Y.Z.W>` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format.
|
||||||
|
|
||||||
|
Print the existing URL and continue to Step 20.
|
||||||
|
|
||||||
If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0.
|
If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0.
|
||||||
|
|
||||||
|
|
@ -3046,7 +3189,7 @@ you missed it.>
|
||||||
**If GitHub:**
|
**If GitHub:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh pr create --base <base> --title "<type>: <summary>" --body "$(cat <<'EOF'
|
gh pr create --base <base> --title "v$NEW_VERSION <type>: <summary>" --body "$(cat <<'EOF'
|
||||||
<PR body from above>
|
<PR body from above>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
@ -3055,7 +3198,7 @@ EOF
|
||||||
**If GitLab:**
|
**If GitLab:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
glab mr create -b <base> -t "<type>: <summary>" -d "$(cat <<'EOF'
|
glab mr create -b <base> -t "v$NEW_VERSION <type>: <summary>" -d "$(cat <<'EOF'
|
||||||
<MR body from above>
|
<MR body from above>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
|
||||||
|
|
@ -451,8 +451,8 @@ fi
|
||||||
Read the `STATE:` line and dispatch:
|
Read the `STATE:` line and dispatch:
|
||||||
|
|
||||||
- **FRESH** → proceed with the bump action below (steps 1–4).
|
- **FRESH** → proceed with the bump action below (steps 1–4).
|
||||||
- **ALREADY_BUMPED** → skip the bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. Continue to the next step.
|
- **ALREADY_BUMPED** → skip the bump by default, BUT check for queue drift first: call `bin/gstack-next-version` with the implied bump level (derived from `CURRENT_VERSION` vs `BASE_VERSION`), compare its `.version` against `CURRENT_VERSION`. If they differ (queue moved since last ship), use **AskUserQuestion**: "VERSION drift detected: you claim v<CURRENT> but next available is v<NEW> (queue moved). A) Rebump to v<NEW> and rewrite CHANGELOG header + PR title (recommended), B) Keep v<CURRENT> — will be rejected by CI version-gate until resolved." If A, treat this as FRESH with `NEW_VERSION=<new>` and run steps 1-4 (which will also trigger Step 13 CHANGELOG header rewrite and Step 19 PR title rewrite). If B, reuse `CURRENT_VERSION` and warn that CI will likely reject. If util is offline, warn and reuse `CURRENT_VERSION`.
|
||||||
- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body.
|
- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. (Queue check still runs in ALREADY_BUMPED terms after repair.)
|
||||||
- **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative.
|
- **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative.
|
||||||
|
|
||||||
1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`)
|
1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`)
|
||||||
|
|
@ -465,9 +465,33 @@ Read the `STATE:` line and dispatch:
|
||||||
- **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added
|
- **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added
|
||||||
- **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes
|
- **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes
|
||||||
|
|
||||||
3. Compute the new version:
|
Save the chosen level as `BUMP_LEVEL` (one of `major`, `minor`, `patch`, `micro`). This is the user-intended level. The next step decides *placement* — the level stays the same even if queue-aware allocation has to advance past a claimed slot.
|
||||||
- Bumping a digit resets all digits to its right to 0
|
|
||||||
- Example: `0.19.1.0` + PATCH → `0.19.2.0`
|
3. **Queue-aware version pick (workspace-aware ship, v1.6.4.0+).** Call `bin/gstack-next-version` to see what's already claimed by open PRs + active sibling Conductor worktrees, then render the queue state to the user:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
QUEUE_JSON=$(bun run bin/gstack-next-version \
|
||||||
|
--base <base> \
|
||||||
|
--bump "$BUMP_LEVEL" \
|
||||||
|
--current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}')
|
||||||
|
NEW_VERSION=$(echo "$QUEUE_JSON" | jq -r '.version // empty')
|
||||||
|
CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length')
|
||||||
|
ACTIVE_SIBLING_COUNT=$(echo "$QUEUE_JSON" | jq -r '.active_siblings | length')
|
||||||
|
OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false')
|
||||||
|
REASON=$(echo "$QUEUE_JSON" | jq -r '.reason // ""')
|
||||||
|
```
|
||||||
|
|
||||||
|
- If `OFFLINE=true` or the util fails (auth expired, no `gh`/`glab`, network): fall back to local `BUMP_LEVEL` arithmetic (bump `BASE_VERSION` at the chosen level). Print `⚠ workspace-aware ship offline — using local bump only`. Continue.
|
||||||
|
- If `CLAIMED_COUNT > 0`: render the queue table to the user so they can see landing order at a glance:
|
||||||
|
```
|
||||||
|
Queue on <base> (vBASE_VERSION):
|
||||||
|
#<pr> <branch> → v<version> [⚠ collision with #<other>]
|
||||||
|
Active sibling workspaces (WIP, not yet PR'd):
|
||||||
|
<path> → v<version> (committed Nh ago)
|
||||||
|
Your branch will claim: vNEW_VERSION (<reason>)
|
||||||
|
```
|
||||||
|
- If `ACTIVE_SIBLING_COUNT > 0` and any active sibling's VERSION is `>= NEW_VERSION`, use **AskUserQuestion**: "Sibling workspace <path> has v<X> committed <N>h ago but hasn't PR'd yet. Wait for them to ship first, or advance past? A) Advance past (recommended for unrelated work), B) Abort /ship and sync up with sibling first."
|
||||||
|
- Validate `NEW_VERSION` matches `MAJOR.MINOR.PATCH.MICRO`. If util returns an empty or malformed version, fall back to local bump.
|
||||||
|
|
||||||
4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`.
|
4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`.
|
||||||
|
|
||||||
|
|
@ -768,7 +792,11 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number):
|
||||||
glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR"
|
glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR"
|
||||||
```
|
```
|
||||||
|
|
||||||
If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. Print the existing URL and continue to Step 20.
|
If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run.
|
||||||
|
|
||||||
|
**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v<NEW_VERSION> <type>: <summary>` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION <type>: <summary>"` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v<X.Y.Z.W>` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format.
|
||||||
|
|
||||||
|
Print the existing URL and continue to Step 20.
|
||||||
|
|
||||||
If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0.
|
If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0.
|
||||||
|
|
||||||
|
|
@ -836,7 +864,7 @@ you missed it.>
|
||||||
**If GitHub:**
|
**If GitHub:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh pr create --base <base> --title "<type>: <summary>" --body "$(cat <<'EOF'
|
gh pr create --base <base> --title "v$NEW_VERSION <type>: <summary>" --body "$(cat <<'EOF'
|
||||||
<PR body from above>
|
<PR body from above>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
@ -845,7 +873,7 @@ EOF
|
||||||
**If GitLab:**
|
**If GitLab:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
glab mr create -b <base> -t "<type>: <summary>" -d "$(cat <<'EOF'
|
glab mr create -b <base> -t "v$NEW_VERSION <type>: <summary>" -d "$(cat <<'EOF'
|
||||||
<MR body from above>
|
<MR body from above>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,820 @@
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// canUseTool extension (D10 CEO / D4 eng)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('runAgentSdkTest — canUseTool extension', () => {
|
||||||
|
test('permissionMode flips to "default" when canUseTool is supplied', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [[systemInit(), assistantTurn([{ type: 'text', text: 'ok' }]), resultSuccess()]],
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
canUseTool: async (_toolName, input) => ({ behavior: 'allow', updatedInput: input }),
|
||||||
|
});
|
||||||
|
const opts = stub.calls[0]!.options!;
|
||||||
|
expect(opts.permissionMode).toBe('default');
|
||||||
|
expect(opts.allowDangerouslySkipPermissions).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('permissionMode stays "bypassPermissions" when canUseTool is NOT supplied', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [[systemInit(), assistantTurn([{ type: 'text', text: 'ok' }]), resultSuccess()]],
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
});
|
||||||
|
const opts = stub.calls[0]!.options!;
|
||||||
|
expect(opts.permissionMode).toBe('bypassPermissions');
|
||||||
|
expect(opts.allowDangerouslySkipPermissions).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('canUseTool callback reaches the SDK options', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [[systemInit(), assistantTurn([{ type: 'text', text: 'ok' }]), resultSuccess()]],
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
const cb = async (_toolName: string, input: Record<string, unknown>) => ({
|
||||||
|
behavior: 'allow' as const,
|
||||||
|
updatedInput: input,
|
||||||
|
});
|
||||||
|
await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
canUseTool: cb,
|
||||||
|
});
|
||||||
|
const opts = stub.calls[0]!.options! as Options & { canUseTool?: unknown };
|
||||||
|
expect(typeof opts.canUseTool).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AskUserQuestion is auto-added to allowedTools when canUseTool is supplied', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [[systemInit(), assistantTurn([{ type: 'text', text: 'ok' }]), resultSuccess()]],
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
allowedTools: ['Read', 'Grep'], // explicitly omits AskUserQuestion
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
canUseTool: async (_toolName, input) => ({ behavior: 'allow', updatedInput: input }),
|
||||||
|
});
|
||||||
|
const opts = stub.calls[0]!.options!;
|
||||||
|
expect(opts.allowedTools).toContain('AskUserQuestion');
|
||||||
|
expect(opts.tools).toContain('AskUserQuestion');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AskUserQuestion is NOT auto-added when canUseTool is absent', async () => {
|
||||||
|
freshSem();
|
||||||
|
const stub: StubConfig = {
|
||||||
|
streams: [[systemInit(), assistantTurn([{ type: 'text', text: 'ok' }]), resultSuccess()]],
|
||||||
|
calls: [],
|
||||||
|
};
|
||||||
|
await runAgentSdkTest({
|
||||||
|
...BASE_OPTS,
|
||||||
|
allowedTools: ['Read', 'Grep'],
|
||||||
|
queryProvider: makeStubProvider(stub),
|
||||||
|
});
|
||||||
|
const opts = stub.calls[0]!.options!;
|
||||||
|
expect(opts.allowedTools).not.toContain('AskUserQuestion');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passThroughNonAskUserQuestion helper returns allow+updatedInput', async () => {
|
||||||
|
const { passThroughNonAskUserQuestion } = await import('../test/helpers/agent-sdk-runner');
|
||||||
|
const result = passThroughNonAskUserQuestion('Read', { file_path: '/tmp/x' });
|
||||||
|
expect(result.behavior).toBe('allow');
|
||||||
|
expect(result.updatedInput).toEqual({ file_path: '/tmp/x' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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,113 @@
|
||||||
|
/**
|
||||||
|
* E2E harness audit — every skill with `interactive: true` in its frontmatter
|
||||||
|
* must have at least one test file that uses `canUseTool` via the extended
|
||||||
|
* agent-sdk-runner. This prevents future drift where a skill opts into the
|
||||||
|
* handshake without adding real coverage.
|
||||||
|
*
|
||||||
|
* Runs as a free unit test (no API calls). Pure filesystem scan.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, expect } from 'bun:test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const ROOT = path.resolve(import.meta.dir, '..');
|
||||||
|
const SKILL_GLOBS = [
|
||||||
|
'plan-ceo-review',
|
||||||
|
'plan-eng-review',
|
||||||
|
'plan-design-review',
|
||||||
|
'plan-devex-review',
|
||||||
|
'office-hours',
|
||||||
|
'codex',
|
||||||
|
'investigate',
|
||||||
|
'qa',
|
||||||
|
'retro',
|
||||||
|
'cso',
|
||||||
|
'review',
|
||||||
|
'ship',
|
||||||
|
'design-review',
|
||||||
|
'devex-review',
|
||||||
|
'qa-only',
|
||||||
|
'design-consultation',
|
||||||
|
'design-shotgun',
|
||||||
|
'autoplan',
|
||||||
|
'land-and-deploy',
|
||||||
|
'plan-tune',
|
||||||
|
'document-release',
|
||||||
|
'context-save',
|
||||||
|
'context-restore',
|
||||||
|
'health',
|
||||||
|
'setup-deploy',
|
||||||
|
'setup-browser-cookies',
|
||||||
|
'canary',
|
||||||
|
'learn',
|
||||||
|
'benchmark',
|
||||||
|
'benchmark-models',
|
||||||
|
'make-pdf',
|
||||||
|
'open-gstack-browser',
|
||||||
|
'gstack-upgrade',
|
||||||
|
'pair-agent',
|
||||||
|
'design-html',
|
||||||
|
'freeze',
|
||||||
|
'unfreeze',
|
||||||
|
'careful',
|
||||||
|
'guard',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load .tmpl files for each skill and return the names of those that have
|
||||||
|
* `interactive: true` in frontmatter.
|
||||||
|
*/
|
||||||
|
function findInteractiveSkills(): string[] {
|
||||||
|
const interactive: string[] = [];
|
||||||
|
for (const skill of SKILL_GLOBS) {
|
||||||
|
const tmplPath = path.join(ROOT, skill, 'SKILL.md.tmpl');
|
||||||
|
if (!fs.existsSync(tmplPath)) continue;
|
||||||
|
const content = fs.readFileSync(tmplPath, 'utf-8');
|
||||||
|
// Frontmatter lives between the first '---' and the next '---'.
|
||||||
|
const fmEnd = content.indexOf('\n---', 4);
|
||||||
|
if (fmEnd < 0) continue;
|
||||||
|
const frontmatter = content.slice(0, fmEnd);
|
||||||
|
if (/^interactive:\s*true\s*$/m.test(frontmatter)) {
|
||||||
|
interactive.push(skill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return interactive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan a test file's contents for the canUseTool-via-harness pattern.
|
||||||
|
* Either: direct canUseTool usage in runAgentSdkTest, or usage of the
|
||||||
|
* shared plan-mode-handshake-helpers that wrap it.
|
||||||
|
*/
|
||||||
|
function hasCanUseToolCoverage(testFile: string): boolean {
|
||||||
|
const content = fs.readFileSync(testFile, 'utf-8');
|
||||||
|
if (content.includes('canUseTool')) return true;
|
||||||
|
if (content.includes('runPlanModeHandshakeTest')) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('E2E harness audit — interactive skills must have canUseTool coverage', () => {
|
||||||
|
test('every interactive: true skill has at least one canUseTool test', () => {
|
||||||
|
const interactive = findInteractiveSkills();
|
||||||
|
expect(interactive.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const testFiles = fs
|
||||||
|
.readdirSync(path.join(ROOT, 'test'))
|
||||||
|
.filter((f) => f.startsWith('skill-e2e-') && f.endsWith('.test.ts'))
|
||||||
|
.map((f) => path.join(ROOT, 'test', f));
|
||||||
|
|
||||||
|
const filesWithCoverage = testFiles.filter(hasCanUseToolCoverage);
|
||||||
|
|
||||||
|
for (const skill of interactive) {
|
||||||
|
// Match the skill name in any test file that uses canUseTool. File
|
||||||
|
// naming convention is `skill-e2e-<skill>-*.test.ts` — either the full
|
||||||
|
// name (plan-ceo-review) or a subset token.
|
||||||
|
const hasDedicatedTest = filesWithCoverage.some((f) => {
|
||||||
|
const base = path.basename(f, '.test.ts');
|
||||||
|
return base.includes(skill) || base.includes(skill.replace(/-review$/, ''));
|
||||||
|
});
|
||||||
|
expect(hasDedicatedTest, `skill "${skill}" has interactive:true but no canUseTool-based E2E test`).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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*.
|
||||||
|
|
@ -2522,8 +2736,8 @@ fi
|
||||||
Read the `STATE:` line and dispatch:
|
Read the `STATE:` line and dispatch:
|
||||||
|
|
||||||
- **FRESH** → proceed with the bump action below (steps 1–4).
|
- **FRESH** → proceed with the bump action below (steps 1–4).
|
||||||
- **ALREADY_BUMPED** → skip the bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. Continue to the next step.
|
- **ALREADY_BUMPED** → skip the bump by default, BUT check for queue drift first: call `bin/gstack-next-version` with the implied bump level (derived from `CURRENT_VERSION` vs `BASE_VERSION`), compare its `.version` against `CURRENT_VERSION`. If they differ (queue moved since last ship), use **AskUserQuestion**: "VERSION drift detected: you claim v<CURRENT> but next available is v<NEW> (queue moved). A) Rebump to v<NEW> and rewrite CHANGELOG header + PR title (recommended), B) Keep v<CURRENT> — will be rejected by CI version-gate until resolved." If A, treat this as FRESH with `NEW_VERSION=<new>` and run steps 1-4 (which will also trigger Step 13 CHANGELOG header rewrite and Step 19 PR title rewrite). If B, reuse `CURRENT_VERSION` and warn that CI will likely reject. If util is offline, warn and reuse `CURRENT_VERSION`.
|
||||||
- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body.
|
- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. (Queue check still runs in ALREADY_BUMPED terms after repair.)
|
||||||
- **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative.
|
- **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative.
|
||||||
|
|
||||||
1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`)
|
1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`)
|
||||||
|
|
@ -2536,9 +2750,33 @@ Read the `STATE:` line and dispatch:
|
||||||
- **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added
|
- **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added
|
||||||
- **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes
|
- **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes
|
||||||
|
|
||||||
3. Compute the new version:
|
Save the chosen level as `BUMP_LEVEL` (one of `major`, `minor`, `patch`, `micro`). This is the user-intended level. The next step decides *placement* — the level stays the same even if queue-aware allocation has to advance past a claimed slot.
|
||||||
- Bumping a digit resets all digits to its right to 0
|
|
||||||
- Example: `0.19.1.0` + PATCH → `0.19.2.0`
|
3. **Queue-aware version pick (workspace-aware ship, v1.6.4.0+).** Call `bin/gstack-next-version` to see what's already claimed by open PRs + active sibling Conductor worktrees, then render the queue state to the user:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
QUEUE_JSON=$(bun run bin/gstack-next-version \
|
||||||
|
--base <base> \
|
||||||
|
--bump "$BUMP_LEVEL" \
|
||||||
|
--current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}')
|
||||||
|
NEW_VERSION=$(echo "$QUEUE_JSON" | jq -r '.version // empty')
|
||||||
|
CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length')
|
||||||
|
ACTIVE_SIBLING_COUNT=$(echo "$QUEUE_JSON" | jq -r '.active_siblings | length')
|
||||||
|
OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false')
|
||||||
|
REASON=$(echo "$QUEUE_JSON" | jq -r '.reason // ""')
|
||||||
|
```
|
||||||
|
|
||||||
|
- If `OFFLINE=true` or the util fails (auth expired, no `gh`/`glab`, network): fall back to local `BUMP_LEVEL` arithmetic (bump `BASE_VERSION` at the chosen level). Print `⚠ workspace-aware ship offline — using local bump only`. Continue.
|
||||||
|
- If `CLAIMED_COUNT > 0`: render the queue table to the user so they can see landing order at a glance:
|
||||||
|
```
|
||||||
|
Queue on <base> (vBASE_VERSION):
|
||||||
|
#<pr> <branch> → v<version> [⚠ collision with #<other>]
|
||||||
|
Active sibling workspaces (WIP, not yet PR'd):
|
||||||
|
<path> → v<version> (committed Nh ago)
|
||||||
|
Your branch will claim: vNEW_VERSION (<reason>)
|
||||||
|
```
|
||||||
|
- If `ACTIVE_SIBLING_COUNT > 0` and any active sibling's VERSION is `>= NEW_VERSION`, use **AskUserQuestion**: "Sibling workspace <path> has v<X> committed <N>h ago but hasn't PR'd yet. Wait for them to ship first, or advance past? A) Advance past (recommended for unrelated work), B) Abort /ship and sync up with sibling first."
|
||||||
|
- Validate `NEW_VERSION` matches `MAJOR.MINOR.PATCH.MICRO`. If util returns an empty or malformed version, fall back to local bump.
|
||||||
|
|
||||||
4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`.
|
4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`.
|
||||||
|
|
||||||
|
|
@ -2879,7 +3117,11 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number):
|
||||||
glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR"
|
glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR"
|
||||||
```
|
```
|
||||||
|
|
||||||
If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. Print the existing URL and continue to Step 20.
|
If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run.
|
||||||
|
|
||||||
|
**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v<NEW_VERSION> <type>: <summary>` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION <type>: <summary>"` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v<X.Y.Z.W>` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format.
|
||||||
|
|
||||||
|
Print the existing URL and continue to Step 20.
|
||||||
|
|
||||||
If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0.
|
If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0.
|
||||||
|
|
||||||
|
|
@ -2947,7 +3189,7 @@ you missed it.>
|
||||||
**If GitHub:**
|
**If GitHub:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh pr create --base <base> --title "<type>: <summary>" --body "$(cat <<'EOF'
|
gh pr create --base <base> --title "v$NEW_VERSION <type>: <summary>" --body "$(cat <<'EOF'
|
||||||
<PR body from above>
|
<PR body from above>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
@ -2956,7 +3198,7 @@ EOF
|
||||||
**If GitLab:**
|
**If GitLab:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
glab mr create -b <base> -t "<type>: <summary>" -d "$(cat <<'EOF'
|
glab mr create -b <base> -t "v$NEW_VERSION <type>: <summary>" -d "$(cat <<'EOF'
|
||||||
<MR body from above>
|
<MR body from above>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
|
||||||
|
|
@ -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*.
|
||||||
|
|
@ -2137,8 +2351,8 @@ fi
|
||||||
Read the `STATE:` line and dispatch:
|
Read the `STATE:` line and dispatch:
|
||||||
|
|
||||||
- **FRESH** → proceed with the bump action below (steps 1–4).
|
- **FRESH** → proceed with the bump action below (steps 1–4).
|
||||||
- **ALREADY_BUMPED** → skip the bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. Continue to the next step.
|
- **ALREADY_BUMPED** → skip the bump by default, BUT check for queue drift first: call `bin/gstack-next-version` with the implied bump level (derived from `CURRENT_VERSION` vs `BASE_VERSION`), compare its `.version` against `CURRENT_VERSION`. If they differ (queue moved since last ship), use **AskUserQuestion**: "VERSION drift detected: you claim v<CURRENT> but next available is v<NEW> (queue moved). A) Rebump to v<NEW> and rewrite CHANGELOG header + PR title (recommended), B) Keep v<CURRENT> — will be rejected by CI version-gate until resolved." If A, treat this as FRESH with `NEW_VERSION=<new>` and run steps 1-4 (which will also trigger Step 13 CHANGELOG header rewrite and Step 19 PR title rewrite). If B, reuse `CURRENT_VERSION` and warn that CI will likely reject. If util is offline, warn and reuse `CURRENT_VERSION`.
|
||||||
- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body.
|
- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. (Queue check still runs in ALREADY_BUMPED terms after repair.)
|
||||||
- **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative.
|
- **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative.
|
||||||
|
|
||||||
1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`)
|
1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`)
|
||||||
|
|
@ -2151,9 +2365,33 @@ Read the `STATE:` line and dispatch:
|
||||||
- **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added
|
- **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added
|
||||||
- **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes
|
- **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes
|
||||||
|
|
||||||
3. Compute the new version:
|
Save the chosen level as `BUMP_LEVEL` (one of `major`, `minor`, `patch`, `micro`). This is the user-intended level. The next step decides *placement* — the level stays the same even if queue-aware allocation has to advance past a claimed slot.
|
||||||
- Bumping a digit resets all digits to its right to 0
|
|
||||||
- Example: `0.19.1.0` + PATCH → `0.19.2.0`
|
3. **Queue-aware version pick (workspace-aware ship, v1.6.4.0+).** Call `bin/gstack-next-version` to see what's already claimed by open PRs + active sibling Conductor worktrees, then render the queue state to the user:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
QUEUE_JSON=$(bun run bin/gstack-next-version \
|
||||||
|
--base <base> \
|
||||||
|
--bump "$BUMP_LEVEL" \
|
||||||
|
--current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}')
|
||||||
|
NEW_VERSION=$(echo "$QUEUE_JSON" | jq -r '.version // empty')
|
||||||
|
CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length')
|
||||||
|
ACTIVE_SIBLING_COUNT=$(echo "$QUEUE_JSON" | jq -r '.active_siblings | length')
|
||||||
|
OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false')
|
||||||
|
REASON=$(echo "$QUEUE_JSON" | jq -r '.reason // ""')
|
||||||
|
```
|
||||||
|
|
||||||
|
- If `OFFLINE=true` or the util fails (auth expired, no `gh`/`glab`, network): fall back to local `BUMP_LEVEL` arithmetic (bump `BASE_VERSION` at the chosen level). Print `⚠ workspace-aware ship offline — using local bump only`. Continue.
|
||||||
|
- If `CLAIMED_COUNT > 0`: render the queue table to the user so they can see landing order at a glance:
|
||||||
|
```
|
||||||
|
Queue on <base> (vBASE_VERSION):
|
||||||
|
#<pr> <branch> → v<version> [⚠ collision with #<other>]
|
||||||
|
Active sibling workspaces (WIP, not yet PR'd):
|
||||||
|
<path> → v<version> (committed Nh ago)
|
||||||
|
Your branch will claim: vNEW_VERSION (<reason>)
|
||||||
|
```
|
||||||
|
- If `ACTIVE_SIBLING_COUNT > 0` and any active sibling's VERSION is `>= NEW_VERSION`, use **AskUserQuestion**: "Sibling workspace <path> has v<X> committed <N>h ago but hasn't PR'd yet. Wait for them to ship first, or advance past? A) Advance past (recommended for unrelated work), B) Abort /ship and sync up with sibling first."
|
||||||
|
- Validate `NEW_VERSION` matches `MAJOR.MINOR.PATCH.MICRO`. If util returns an empty or malformed version, fall back to local bump.
|
||||||
|
|
||||||
4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`.
|
4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`.
|
||||||
|
|
||||||
|
|
@ -2494,7 +2732,11 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number):
|
||||||
glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR"
|
glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR"
|
||||||
```
|
```
|
||||||
|
|
||||||
If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. Print the existing URL and continue to Step 20.
|
If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run.
|
||||||
|
|
||||||
|
**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v<NEW_VERSION> <type>: <summary>` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION <type>: <summary>"` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v<X.Y.Z.W>` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format.
|
||||||
|
|
||||||
|
Print the existing URL and continue to Step 20.
|
||||||
|
|
||||||
If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0.
|
If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0.
|
||||||
|
|
||||||
|
|
@ -2562,7 +2804,7 @@ you missed it.>
|
||||||
**If GitHub:**
|
**If GitHub:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh pr create --base <base> --title "<type>: <summary>" --body "$(cat <<'EOF'
|
gh pr create --base <base> --title "v$NEW_VERSION <type>: <summary>" --body "$(cat <<'EOF'
|
||||||
<PR body from above>
|
<PR body from above>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
@ -2571,7 +2813,7 @@ EOF
|
||||||
**If GitLab:**
|
**If GitLab:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
glab mr create -b <base> -t "<type>: <summary>" -d "$(cat <<'EOF'
|
glab mr create -b <base> -t "v$NEW_VERSION <type>: <summary>" -d "$(cat <<'EOF'
|
||||||
<MR body from above>
|
<MR body from above>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
|
||||||
|
|
@ -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*.
|
||||||
|
|
@ -2513,8 +2727,8 @@ fi
|
||||||
Read the `STATE:` line and dispatch:
|
Read the `STATE:` line and dispatch:
|
||||||
|
|
||||||
- **FRESH** → proceed with the bump action below (steps 1–4).
|
- **FRESH** → proceed with the bump action below (steps 1–4).
|
||||||
- **ALREADY_BUMPED** → skip the bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. Continue to the next step.
|
- **ALREADY_BUMPED** → skip the bump by default, BUT check for queue drift first: call `bin/gstack-next-version` with the implied bump level (derived from `CURRENT_VERSION` vs `BASE_VERSION`), compare its `.version` against `CURRENT_VERSION`. If they differ (queue moved since last ship), use **AskUserQuestion**: "VERSION drift detected: you claim v<CURRENT> but next available is v<NEW> (queue moved). A) Rebump to v<NEW> and rewrite CHANGELOG header + PR title (recommended), B) Keep v<CURRENT> — will be rejected by CI version-gate until resolved." If A, treat this as FRESH with `NEW_VERSION=<new>` and run steps 1-4 (which will also trigger Step 13 CHANGELOG header rewrite and Step 19 PR title rewrite). If B, reuse `CURRENT_VERSION` and warn that CI will likely reject. If util is offline, warn and reuse `CURRENT_VERSION`.
|
||||||
- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body.
|
- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. (Queue check still runs in ALREADY_BUMPED terms after repair.)
|
||||||
- **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative.
|
- **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative.
|
||||||
|
|
||||||
1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`)
|
1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`)
|
||||||
|
|
@ -2527,9 +2741,33 @@ Read the `STATE:` line and dispatch:
|
||||||
- **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added
|
- **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added
|
||||||
- **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes
|
- **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes
|
||||||
|
|
||||||
3. Compute the new version:
|
Save the chosen level as `BUMP_LEVEL` (one of `major`, `minor`, `patch`, `micro`). This is the user-intended level. The next step decides *placement* — the level stays the same even if queue-aware allocation has to advance past a claimed slot.
|
||||||
- Bumping a digit resets all digits to its right to 0
|
|
||||||
- Example: `0.19.1.0` + PATCH → `0.19.2.0`
|
3. **Queue-aware version pick (workspace-aware ship, v1.6.4.0+).** Call `bin/gstack-next-version` to see what's already claimed by open PRs + active sibling Conductor worktrees, then render the queue state to the user:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
QUEUE_JSON=$(bun run bin/gstack-next-version \
|
||||||
|
--base <base> \
|
||||||
|
--bump "$BUMP_LEVEL" \
|
||||||
|
--current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}')
|
||||||
|
NEW_VERSION=$(echo "$QUEUE_JSON" | jq -r '.version // empty')
|
||||||
|
CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length')
|
||||||
|
ACTIVE_SIBLING_COUNT=$(echo "$QUEUE_JSON" | jq -r '.active_siblings | length')
|
||||||
|
OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false')
|
||||||
|
REASON=$(echo "$QUEUE_JSON" | jq -r '.reason // ""')
|
||||||
|
```
|
||||||
|
|
||||||
|
- If `OFFLINE=true` or the util fails (auth expired, no `gh`/`glab`, network): fall back to local `BUMP_LEVEL` arithmetic (bump `BASE_VERSION` at the chosen level). Print `⚠ workspace-aware ship offline — using local bump only`. Continue.
|
||||||
|
- If `CLAIMED_COUNT > 0`: render the queue table to the user so they can see landing order at a glance:
|
||||||
|
```
|
||||||
|
Queue on <base> (vBASE_VERSION):
|
||||||
|
#<pr> <branch> → v<version> [⚠ collision with #<other>]
|
||||||
|
Active sibling workspaces (WIP, not yet PR'd):
|
||||||
|
<path> → v<version> (committed Nh ago)
|
||||||
|
Your branch will claim: vNEW_VERSION (<reason>)
|
||||||
|
```
|
||||||
|
- If `ACTIVE_SIBLING_COUNT > 0` and any active sibling's VERSION is `>= NEW_VERSION`, use **AskUserQuestion**: "Sibling workspace <path> has v<X> committed <N>h ago but hasn't PR'd yet. Wait for them to ship first, or advance past? A) Advance past (recommended for unrelated work), B) Abort /ship and sync up with sibling first."
|
||||||
|
- Validate `NEW_VERSION` matches `MAJOR.MINOR.PATCH.MICRO`. If util returns an empty or malformed version, fall back to local bump.
|
||||||
|
|
||||||
4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`.
|
4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`.
|
||||||
|
|
||||||
|
|
@ -2870,7 +3108,11 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number):
|
||||||
glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR"
|
glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR"
|
||||||
```
|
```
|
||||||
|
|
||||||
If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. Print the existing URL and continue to Step 20.
|
If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run.
|
||||||
|
|
||||||
|
**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v<NEW_VERSION> <type>: <summary>` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION <type>: <summary>"` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v<X.Y.Z.W>` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format.
|
||||||
|
|
||||||
|
Print the existing URL and continue to Step 20.
|
||||||
|
|
||||||
If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0.
|
If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0.
|
||||||
|
|
||||||
|
|
@ -2938,7 +3180,7 @@ you missed it.>
|
||||||
**If GitHub:**
|
**If GitHub:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh pr create --base <base> --title "<type>: <summary>" --body "$(cat <<'EOF'
|
gh pr create --base <base> --title "v$NEW_VERSION <type>: <summary>" --body "$(cat <<'EOF'
|
||||||
<PR body from above>
|
<PR body from above>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
@ -2947,7 +3189,7 @@ EOF
|
||||||
**If GitLab:**
|
**If GitLab:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
glab mr create -b <base> -t "<type>: <summary>" -d "$(cat <<'EOF'
|
glab mr create -b <base> -t "v$NEW_VERSION <type>: <summary>" -d "$(cat <<'EOF'
|
||||||
<MR body from above>
|
<MR body from above>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
@ -2773,3 +2774,93 @@ describe('voice-triggers processing', () => {
|
||||||
expect(frontmatter).not.toContain('voice-triggers:');
|
expect(frontmatter).not.toContain('voice-triggers:');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('plan-mode handshake (interactive: true) resolver', () => {
|
||||||
|
const INTERACTIVE_SKILLS = [
|
||||||
|
'plan-ceo-review',
|
||||||
|
'plan-eng-review',
|
||||||
|
'plan-design-review',
|
||||||
|
'plan-devex-review',
|
||||||
|
];
|
||||||
|
|
||||||
|
const HANDSHAKE_MARKER = '## Plan Mode Handshake';
|
||||||
|
|
||||||
|
test.each(INTERACTIVE_SKILLS)(
|
||||||
|
'%s (Claude host) SKILL.md contains the handshake section',
|
||||||
|
(skill) => {
|
||||||
|
const content = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8');
|
||||||
|
expect(content).toContain(HANDSHAKE_MARKER);
|
||||||
|
expect(content).toContain(
|
||||||
|
'Plan mode is active. The user indicated that they do not want you to execute yet',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('handshake is absent from non-interactive Claude skills', () => {
|
||||||
|
const nonInteractive = ['ship', 'review', 'qa', 'office-hours', 'codex', 'retro', 'cso'];
|
||||||
|
for (const skill of nonInteractive) {
|
||||||
|
const content = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8');
|
||||||
|
expect(content).not.toContain(HANDSHAKE_MARKER);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handshake is absent from non-Claude host outputs when present on disk', () => {
|
||||||
|
// Non-Claude hosts render to hostSubdirs (.agents/, .openclaw/, etc). The
|
||||||
|
// handshake resolver returns '' when ctx.host !== 'claude', so those
|
||||||
|
// outputs must not contain the marker. The current gen-skill-docs layout
|
||||||
|
// prefixes skill names as `gstack-<skill>` under the hostSubdir; older
|
||||||
|
// layouts used `gstack/<skill>` (no prefix). Only stable-present paths
|
||||||
|
// are asserted — older ones may or may not exist per install history.
|
||||||
|
const candidateOutputs = [
|
||||||
|
// Current prefixed layout
|
||||||
|
path.join(ROOT, '.agents', 'skills', 'gstack-plan-ceo-review', 'SKILL.md'),
|
||||||
|
path.join(ROOT, '.openclaw', 'skills', 'gstack-plan-ceo-review', 'SKILL.md'),
|
||||||
|
path.join(ROOT, '.opencode', 'skills', 'gstack-plan-ceo-review', 'SKILL.md'),
|
||||||
|
path.join(ROOT, '.factory', 'skills', 'gstack-plan-ceo-review', 'SKILL.md'),
|
||||||
|
path.join(ROOT, '.hermes', 'skills', 'gstack-plan-ceo-review', 'SKILL.md'),
|
||||||
|
];
|
||||||
|
let checked = 0;
|
||||||
|
for (const out of candidateOutputs) {
|
||||||
|
if (fs.existsSync(out)) {
|
||||||
|
const content = fs.readFileSync(out, 'utf-8');
|
||||||
|
expect(content).not.toContain(HANDSHAKE_MARKER);
|
||||||
|
checked++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// At least one non-Claude host's output should exist after a full gen
|
||||||
|
// run; this test is meaningful only if we checked something. If no
|
||||||
|
// non-Claude outputs exist locally, the cross-host guarantee is still
|
||||||
|
// enforced by the resolver's ctx.host check; this test is belt-and-
|
||||||
|
// suspenders and becomes a no-op rather than a false positive.
|
||||||
|
if (checked === 0) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn(
|
||||||
|
'plan-mode handshake: no non-Claude host outputs found for cross-host absence check — ' +
|
||||||
|
'run `bun run gen:skill-docs --host all` to populate',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('0C-bis STOP block present in plan-ceo-review/SKILL.md', () => {
|
||||||
|
const content = fs.readFileSync(path.join(ROOT, 'plan-ceo-review', 'SKILL.md'), 'utf-8');
|
||||||
|
const presentIdx = content.indexOf('Present these approach options via AskUserQuestion');
|
||||||
|
const preludeIdx = content.indexOf('### 0D-prelude');
|
||||||
|
expect(presentIdx).toBeGreaterThan(0);
|
||||||
|
expect(preludeIdx).toBeGreaterThan(presentIdx);
|
||||||
|
const between = content.slice(presentIdx, preludeIdx);
|
||||||
|
expect(between).toContain('**STOP.**');
|
||||||
|
expect(between).toContain('Do NOT proceed to Step 0D or 0F until the user responds to 0C-bis');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handshake resolver is wired BEFORE generateUpgradeCheck in preamble', () => {
|
||||||
|
const content = fs.readFileSync(
|
||||||
|
path.join(ROOT, 'plan-ceo-review', 'SKILL.md'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
const handshakeIdx = content.indexOf(HANDSHAKE_MARKER);
|
||||||
|
const upgradeIdx = content.indexOf('UPGRADE_AVAILABLE');
|
||||||
|
expect(handshakeIdx).toBeGreaterThan(0);
|
||||||
|
expect(upgradeIdx).toBeGreaterThan(0);
|
||||||
|
expect(handshakeIdx).toBeLessThan(upgradeIdx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
// Pure-function tests for bin/gstack-next-version.
|
||||||
|
// Covers the version arithmetic and slot-picking logic. Subprocess paths
|
||||||
|
// (gh/glab/git) are covered by the integration test at the bottom (skipped
|
||||||
|
// when the relevant CLI isn't available).
|
||||||
|
|
||||||
|
import { test, expect, describe } from "bun:test";
|
||||||
|
import {
|
||||||
|
parseVersion,
|
||||||
|
fmtVersion,
|
||||||
|
bumpVersion,
|
||||||
|
cmpVersion,
|
||||||
|
pickNextSlot,
|
||||||
|
markActiveSiblings,
|
||||||
|
} from "../bin/gstack-next-version";
|
||||||
|
|
||||||
|
describe("parseVersion", () => {
|
||||||
|
test("accepts 4-digit semver", () => {
|
||||||
|
expect(parseVersion("1.6.3.0")).toEqual([1, 6, 3, 0]);
|
||||||
|
expect(parseVersion("0.0.0.0")).toEqual([0, 0, 0, 0]);
|
||||||
|
expect(parseVersion("99.99.99.99")).toEqual([99, 99, 99, 99]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("trims whitespace", () => {
|
||||||
|
expect(parseVersion(" 1.2.3.4 \n")).toEqual([1, 2, 3, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects malformed", () => {
|
||||||
|
expect(parseVersion("1.2.3")).toBeNull();
|
||||||
|
expect(parseVersion("1.2.3.4.5")).toBeNull();
|
||||||
|
expect(parseVersion("v1.2.3.4")).toBeNull();
|
||||||
|
expect(parseVersion("")).toBeNull();
|
||||||
|
expect(parseVersion("not-a-version")).toBeNull();
|
||||||
|
expect(parseVersion("1.2.3.x")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bumpVersion", () => {
|
||||||
|
test("major zeros everything right", () => {
|
||||||
|
expect(bumpVersion([1, 6, 3, 0], "major")).toEqual([2, 0, 0, 0]);
|
||||||
|
expect(bumpVersion([1, 6, 3, 7], "major")).toEqual([2, 0, 0, 0]);
|
||||||
|
});
|
||||||
|
test("minor zeros patch+micro", () => {
|
||||||
|
expect(bumpVersion([1, 6, 3, 0], "minor")).toEqual([1, 7, 0, 0]);
|
||||||
|
expect(bumpVersion([1, 6, 3, 7], "minor")).toEqual([1, 7, 0, 0]);
|
||||||
|
});
|
||||||
|
test("patch zeros micro", () => {
|
||||||
|
expect(bumpVersion([1, 6, 3, 0], "patch")).toEqual([1, 6, 4, 0]);
|
||||||
|
expect(bumpVersion([1, 6, 3, 7], "patch")).toEqual([1, 6, 4, 0]);
|
||||||
|
});
|
||||||
|
test("micro increments slot 4", () => {
|
||||||
|
expect(bumpVersion([1, 6, 3, 0], "micro")).toEqual([1, 6, 3, 1]);
|
||||||
|
expect(bumpVersion([1, 6, 3, 7], "micro")).toEqual([1, 6, 3, 8]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmpVersion", () => {
|
||||||
|
test("detects order", () => {
|
||||||
|
expect(cmpVersion([1, 6, 3, 0], [1, 6, 3, 0])).toBe(0);
|
||||||
|
expect(cmpVersion([1, 6, 4, 0], [1, 6, 3, 0])).toBeGreaterThan(0);
|
||||||
|
expect(cmpVersion([1, 6, 3, 0], [1, 6, 4, 0])).toBeLessThan(0);
|
||||||
|
expect(cmpVersion([2, 0, 0, 0], [1, 99, 99, 99])).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("pickNextSlot (the heart of queue-aware allocation)", () => {
|
||||||
|
const base: [number, number, number, number] = [1, 6, 3, 0];
|
||||||
|
|
||||||
|
test("happy path — no claims, clean bump", () => {
|
||||||
|
const r = pickNextSlot(base, [], "minor");
|
||||||
|
expect(fmtVersion(r.version)).toBe("1.7.0.0");
|
||||||
|
expect(r.reason).toMatch(/no collision/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("collision — one PR claims the next slot, bump past", () => {
|
||||||
|
const r = pickNextSlot(base, [[1, 7, 0, 0]], "minor");
|
||||||
|
expect(fmtVersion(r.version)).toBe("1.8.0.0");
|
||||||
|
expect(r.reason).toMatch(/bumped past/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("multi-collision — two PRs claim sequential slots", () => {
|
||||||
|
const r = pickNextSlot(base, [[1, 7, 0, 0], [1, 8, 0, 0]], "minor");
|
||||||
|
expect(fmtVersion(r.version)).toBe("1.9.0.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("collision cross-level — queued MINOR bumps past my PATCH", () => {
|
||||||
|
// Queue has 1.7.0.0 (minor), my bump is patch. I should land at 1.7.1.0
|
||||||
|
// (patch relative to the highest claim).
|
||||||
|
const r = pickNextSlot(base, [[1, 7, 0, 0]], "patch");
|
||||||
|
expect(fmtVersion(r.version)).toBe("1.7.1.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("claims below base are ignored", () => {
|
||||||
|
const r = pickNextSlot(base, [[1, 5, 0, 0], [1, 6, 2, 0]], "patch");
|
||||||
|
expect(fmtVersion(r.version)).toBe("1.6.4.0");
|
||||||
|
expect(r.reason).toMatch(/no collision/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("claims equal to base are treated as no-claim", () => {
|
||||||
|
// The caller is expected to pre-filter base-equal claims out, but even if
|
||||||
|
// one slipped through, we don't want to inflate past it.
|
||||||
|
const r = pickNextSlot(base, [], "micro");
|
||||||
|
expect(fmtVersion(r.version)).toBe("1.6.3.1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("major collision — competing majors", () => {
|
||||||
|
const r = pickNextSlot(base, [[2, 0, 0, 0]], "major");
|
||||||
|
expect(fmtVersion(r.version)).toBe("3.0.0.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unsorted claims still resolve correctly", () => {
|
||||||
|
const r = pickNextSlot(base, [[1, 9, 0, 0], [1, 7, 0, 0], [1, 8, 0, 0]], "minor");
|
||||||
|
expect(fmtVersion(r.version)).toBe("1.10.0.0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("markActiveSiblings", () => {
|
||||||
|
const base: [number, number, number, number] = [1, 6, 3, 0];
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
test("flags siblings that are ahead of base AND recent AND have no PR", () => {
|
||||||
|
const siblings = [
|
||||||
|
{ path: "/a", branch: "feat/alpha", version: "1.7.0.0", last_commit_ts: now - 60, has_open_pr: false, is_active: false },
|
||||||
|
];
|
||||||
|
const r = markActiveSiblings(siblings, base);
|
||||||
|
expect(r[0].is_active).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not flag siblings with open PRs (already in the queue)", () => {
|
||||||
|
const siblings = [
|
||||||
|
{ path: "/a", branch: "feat/alpha", version: "1.7.0.0", last_commit_ts: now - 60, has_open_pr: true, is_active: false },
|
||||||
|
];
|
||||||
|
expect(markActiveSiblings(siblings, base)[0].is_active).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not flag stale siblings (commit > 24h old)", () => {
|
||||||
|
const siblings = [
|
||||||
|
{ path: "/a", branch: "feat/alpha", version: "1.7.0.0", last_commit_ts: now - 25 * 3600, has_open_pr: false, is_active: false },
|
||||||
|
];
|
||||||
|
expect(markActiveSiblings(siblings, base)[0].is_active).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not flag siblings at or below base", () => {
|
||||||
|
const siblings = [
|
||||||
|
{ path: "/a", branch: "feat/alpha", version: "1.6.3.0", last_commit_ts: now - 60, has_open_pr: false, is_active: false },
|
||||||
|
{ path: "/b", branch: "feat/beta", version: "1.5.0.0", last_commit_ts: now - 60, has_open_pr: false, is_active: false },
|
||||||
|
];
|
||||||
|
const r = markActiveSiblings(siblings, base);
|
||||||
|
expect(r[0].is_active).toBe(false);
|
||||||
|
expect(r[1].is_active).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Integration smoke — only runs if gh is available and authenticated. Confirms
|
||||||
|
// the CLI executes end-to-end against real APIs without crashing.
|
||||||
|
describe("integration (smoke)", () => {
|
||||||
|
test("CLI runs against real repo and emits parseable JSON", async () => {
|
||||||
|
const proc = Bun.spawnSync([
|
||||||
|
"bun",
|
||||||
|
"run",
|
||||||
|
"./bin/gstack-next-version",
|
||||||
|
"--base",
|
||||||
|
"main",
|
||||||
|
"--bump",
|
||||||
|
"patch",
|
||||||
|
"--current-version",
|
||||||
|
"1.6.3.0",
|
||||||
|
"--workspace-root",
|
||||||
|
"null", // skip sibling scan in CI
|
||||||
|
]);
|
||||||
|
const out = new TextDecoder().decode(proc.stdout);
|
||||||
|
const parsed = JSON.parse(out);
|
||||||
|
expect(parsed).toHaveProperty("version");
|
||||||
|
expect(parseVersion(parsed.version)).not.toBeNull();
|
||||||
|
expect(parsed).toHaveProperty("bump", "patch");
|
||||||
|
expect(parsed).toHaveProperty("host");
|
||||||
|
expect(["github", "gitlab", "unknown"]).toContain(parsed.host);
|
||||||
|
expect(parsed).toHaveProperty("claimed");
|
||||||
|
expect(Array.isArray(parsed.claimed)).toBe(true);
|
||||||
|
expect(parsed).toHaveProperty("siblings");
|
||||||
|
expect(parsed.siblings).toEqual([]); // --workspace-root null disabled scanning
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,565 @@
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
type CanUseTool,
|
||||||
|
} 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;
|
||||||
|
/**
|
||||||
|
* Optional canUseTool callback. When supplied, the harness flips
|
||||||
|
* permissionMode from 'bypassPermissions' to 'default' so the SDK actually
|
||||||
|
* routes tool-use approval decisions through the callback. Without this
|
||||||
|
* flip, bypassPermissions short-circuits the callback and tests that want
|
||||||
|
* to assert on AskUserQuestion content silently pass without asserting.
|
||||||
|
*
|
||||||
|
* Callback contract matches the SDK: fires on every tool-use approval
|
||||||
|
* request and on AskUserQuestion invocations. For non-AskUserQuestion
|
||||||
|
* tools that tests don't care about, use `passThroughNonAskUserQuestion`
|
||||||
|
* to auto-allow them.
|
||||||
|
*/
|
||||||
|
canUseTool?: CanUseTool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass-through helper: auto-allows any tool_use that isn't AskUserQuestion.
|
||||||
|
* Most plan-mode handshake tests only care about the handshake AskUserQuestion;
|
||||||
|
* every other tool (Read, Grep, Bash, Write, Edit, ExitPlanMode) should just
|
||||||
|
* run. Compose with a test-specific AskUserQuestion handler:
|
||||||
|
*
|
||||||
|
* canUseTool: async (toolName, input, options) => {
|
||||||
|
* if (toolName === 'AskUserQuestion') {
|
||||||
|
* // custom assertions + canned answer
|
||||||
|
* return { behavior: 'allow', updatedInput: { questions: input.questions, answers: {...} } };
|
||||||
|
* }
|
||||||
|
* return passThroughNonAskUserQuestion(toolName, input);
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function passThroughNonAskUserQuestion(
|
||||||
|
toolName: string,
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
): { behavior: 'allow'; updatedInput: Record<string, unknown> } {
|
||||||
|
// SDK requires an allow response to include updatedInput — pass the original
|
||||||
|
// input through unchanged so the tool runs as the model intended.
|
||||||
|
void toolName;
|
||||||
|
return { behavior: 'allow', updatedInput: input };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// When canUseTool is supplied, the SDK must route tool-use approval
|
||||||
|
// decisions through the callback. bypassPermissions short-circuits
|
||||||
|
// that. Flip to 'default' mode so canUseTool actually fires. Tests
|
||||||
|
// that want AskUserQuestion interception without this flip would
|
||||||
|
// silently auto-pass — the exact testability gap D14/D4-eng fix.
|
||||||
|
const hasCanUseTool = typeof opts.canUseTool === 'function';
|
||||||
|
const resolvedPermissionMode: PermissionMode =
|
||||||
|
opts.permissionMode ?? (hasCanUseTool ? 'default' : 'bypassPermissions');
|
||||||
|
|
||||||
|
// When canUseTool is supplied, ensure AskUserQuestion is in the allowed
|
||||||
|
// tools list. Without it, Claude can't invoke AskUserQuestion at all
|
||||||
|
// and the callback never has a chance to fire on it.
|
||||||
|
const baseTools = opts.allowedTools ?? ['Read', 'Glob', 'Grep', 'Bash'];
|
||||||
|
const resolvedTools =
|
||||||
|
hasCanUseTool && !baseTools.includes('AskUserQuestion')
|
||||||
|
? [...baseTools, 'AskUserQuestion']
|
||||||
|
: baseTools;
|
||||||
|
|
||||||
|
const sdkOpts: Options = {
|
||||||
|
model,
|
||||||
|
cwd: opts.workingDirectory,
|
||||||
|
maxTurns: opts.maxTurns ?? 5,
|
||||||
|
tools: resolvedTools,
|
||||||
|
disallowedTools: opts.disallowedTools,
|
||||||
|
allowedTools: resolvedTools,
|
||||||
|
permissionMode: resolvedPermissionMode,
|
||||||
|
allowDangerouslySkipPermissions: resolvedPermissionMode === 'bypassPermissions',
|
||||||
|
settingSources: opts.settingSources ?? [],
|
||||||
|
env: opts.env,
|
||||||
|
pathToClaudeCodeExecutable: opts.pathToClaudeCodeExecutable,
|
||||||
|
...(hasCanUseTool ? { canUseTool: opts.canUseTool } : {}),
|
||||||
|
};
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
/**
|
||||||
|
* Shared helpers for plan-mode handshake E2E tests.
|
||||||
|
*
|
||||||
|
* Four sibling test files (plan-ceo, plan-eng, plan-design, plan-devex) exercise
|
||||||
|
* the identical handshake contract against different skills. This helper
|
||||||
|
* centralizes the canUseTool interceptor and the assertion shape so the four
|
||||||
|
* test files are thin wiring (~40 LOC each) and can't drift out of sync.
|
||||||
|
*
|
||||||
|
* See scripts/resolvers/preamble/generate-plan-mode-handshake.ts for the
|
||||||
|
* handshake prose that the tests below assert against.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect } from 'bun:test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import {
|
||||||
|
runAgentSdkTest,
|
||||||
|
passThroughNonAskUserQuestion,
|
||||||
|
resolveClaudeBinary,
|
||||||
|
type AgentSdkResult,
|
||||||
|
} from './agent-sdk-runner';
|
||||||
|
|
||||||
|
/** Distinctive phrase matching what Claude Code's harness actually injects. */
|
||||||
|
export const PLAN_MODE_REMINDER =
|
||||||
|
'Plan mode is active. The user indicated that they do not want you to execute yet';
|
||||||
|
|
||||||
|
export interface HandshakeCaptureResult {
|
||||||
|
sdkResult: AgentSdkResult;
|
||||||
|
/** Each AskUserQuestion that fired, with its input payload. */
|
||||||
|
askUserQuestions: Array<{ input: Record<string, unknown>; orderIndex: number }>;
|
||||||
|
/** Tool-use events in the order they fired (names only). */
|
||||||
|
toolOrder: string[];
|
||||||
|
/** Whether any Write or Edit tool fired BEFORE the first AskUserQuestion. */
|
||||||
|
writeOrEditBeforeAsk: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a skill via the Agent SDK with canUseTool intercepting every tool use.
|
||||||
|
* Inject the plan-mode distinctive phrase into the system prompt and auto-
|
||||||
|
* answer the handshake with the given answerLabel ("Exit" or "Cancel"). Return
|
||||||
|
* the captured events for assertion.
|
||||||
|
*/
|
||||||
|
export async function runPlanModeHandshakeTest(opts: {
|
||||||
|
/** Skill name, e.g. 'plan-ceo-review'. */
|
||||||
|
skillName: string;
|
||||||
|
/** "Exit" to pick option A (exit-and-rerun) or "Cancel" for option C. */
|
||||||
|
answerLabel: 'Exit' | 'Cancel';
|
||||||
|
/** If true, DO NOT inject the reminder — used by the no-op regression test. */
|
||||||
|
omitPlanModeReminder?: boolean;
|
||||||
|
/** Max turns for the SDK call (default 4 — handshake + exit should fit easily). */
|
||||||
|
maxTurns?: number;
|
||||||
|
}): Promise<HandshakeCaptureResult> {
|
||||||
|
const { skillName, answerLabel, omitPlanModeReminder, maxTurns } = opts;
|
||||||
|
|
||||||
|
const askUserQuestions: HandshakeCaptureResult['askUserQuestions'] = [];
|
||||||
|
const toolOrder: string[] = [];
|
||||||
|
let toolIndex = 0;
|
||||||
|
let firstAskIndex = -1;
|
||||||
|
|
||||||
|
const workingDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), `plan-mode-handshake-${skillName}-`),
|
||||||
|
);
|
||||||
|
|
||||||
|
// The SDK requires AskUserQuestion to be in the allowed tools list. The
|
||||||
|
// harness auto-adds it when canUseTool is supplied, but we also want Read
|
||||||
|
// so the skill can load its own file if it tries to.
|
||||||
|
const binary = resolveClaudeBinary();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Inject the distinctive phrase into the system prompt by appending it to
|
||||||
|
// the default Claude Code preset. Claude Code's real plan mode uses an
|
||||||
|
// injected system-reminder; in SDK tests we use systemPrompt.append which
|
||||||
|
// the model treats as equally authoritative.
|
||||||
|
const reminderAppend = omitPlanModeReminder
|
||||||
|
? ''
|
||||||
|
: `\n\n<system-reminder>\n${PLAN_MODE_REMINDER}. This supercedes any other instructions you have received.\n</system-reminder>\n`;
|
||||||
|
|
||||||
|
const sdkResult = await runAgentSdkTest({
|
||||||
|
systemPrompt: {
|
||||||
|
type: 'preset',
|
||||||
|
preset: 'claude_code',
|
||||||
|
append: reminderAppend,
|
||||||
|
},
|
||||||
|
userPrompt: `Read the skill file at ${path.resolve(
|
||||||
|
import.meta.dir,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
skillName,
|
||||||
|
'SKILL.md',
|
||||||
|
)} and follow its instructions. There is no real plan to review — just start the skill and respond to any AskUserQuestion that fires.`,
|
||||||
|
workingDirectory: workingDir,
|
||||||
|
maxTurns: maxTurns ?? 4,
|
||||||
|
allowedTools: ['Read', 'Grep', 'Glob', 'Bash'],
|
||||||
|
...(binary ? { pathToClaudeCodeExecutable: binary } : {}),
|
||||||
|
canUseTool: async (toolName, input) => {
|
||||||
|
toolOrder.push(toolName);
|
||||||
|
if (toolName === 'AskUserQuestion') {
|
||||||
|
if (firstAskIndex === -1) firstAskIndex = toolIndex;
|
||||||
|
askUserQuestions.push({ input, orderIndex: toolIndex });
|
||||||
|
toolIndex++;
|
||||||
|
// Auto-answer with the label the test specified.
|
||||||
|
const q = (input.questions as Array<{ question: string; options: Array<{ label: string }> }>)[0];
|
||||||
|
const matched = q.options.find((o) => o.label.includes(answerLabel));
|
||||||
|
const answer = matched ? matched.label : q.options[0]!.label;
|
||||||
|
return {
|
||||||
|
behavior: 'allow',
|
||||||
|
updatedInput: {
|
||||||
|
questions: input.questions,
|
||||||
|
answers: { [q.question]: answer },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
toolIndex++;
|
||||||
|
return passThroughNonAskUserQuestion(toolName, input);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const writeOrEditBeforeAsk =
|
||||||
|
firstAskIndex > 0 &&
|
||||||
|
toolOrder.slice(0, firstAskIndex).some((t) => t === 'Write' || t === 'Edit');
|
||||||
|
|
||||||
|
return { sdkResult, askUserQuestions, toolOrder, writeOrEditBeforeAsk };
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
fs.rmSync(workingDir, { recursive: true, force: true });
|
||||||
|
} catch { /* ignore cleanup errors */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Assert the shape of a fired handshake AskUserQuestion. */
|
||||||
|
export function assertHandshakeShape(
|
||||||
|
aq: { input: Record<string, unknown> },
|
||||||
|
): void {
|
||||||
|
const questions = aq.input.questions as Array<{
|
||||||
|
question: string;
|
||||||
|
options: Array<{ label: string }>;
|
||||||
|
}>;
|
||||||
|
expect(questions).toBeDefined();
|
||||||
|
expect(questions.length).toBe(1);
|
||||||
|
const q = questions[0]!;
|
||||||
|
// D8 dropped Option B; handshake has exactly 2 options.
|
||||||
|
expect(q.options.length).toBe(2);
|
||||||
|
const labels = q.options.map((o) => o.label);
|
||||||
|
expect(labels.some((l) => l.includes('Exit'))).toBe(true);
|
||||||
|
expect(labels.some((l) => l.includes('Cancel'))).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read the skill-usage.jsonl log and return handshake entries. */
|
||||||
|
export function readHandshakeLog(): Array<Record<string, unknown>> {
|
||||||
|
const logPath = path.join(os.homedir(), '.gstack', 'analytics', 'skill-usage.jsonl');
|
||||||
|
if (!fs.existsSync(logPath)) return [];
|
||||||
|
const lines = fs.readFileSync(logPath, 'utf-8').split('\n').filter(Boolean);
|
||||||
|
return lines
|
||||||
|
.map((line) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(line);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((x): x is Record<string, unknown> => x !== null && x.event === 'plan_mode_handshake');
|
||||||
|
}
|
||||||
|
|
||||||
|
export { execSync };
|
||||||
|
|
@ -82,12 +82,40 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||||
'plan-eng-review-artifact': ['plan-eng-review/**'],
|
'plan-eng-review-artifact': ['plan-eng-review/**'],
|
||||||
'plan-review-report': ['plan-eng-review/**', 'scripts/gen-skill-docs.ts'],
|
'plan-review-report': ['plan-eng-review/**', 'scripts/gen-skill-docs.ts'],
|
||||||
|
|
||||||
|
// Plan-mode handshake (v1.10.2.0) — gate-tier safety regression tests.
|
||||||
|
// Each fires when any of: the interactive skill's template, the resolver,
|
||||||
|
// preamble composition, the Agent SDK harness, the question registry, or
|
||||||
|
// the one-way-door classifier changes.
|
||||||
|
'plan-ceo-review-plan-mode': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-plan-mode-handshake.ts', 'scripts/resolvers/preamble.ts', 'scripts/question-registry.ts', 'scripts/one-way-doors.ts', 'test/helpers/agent-sdk-runner.ts'],
|
||||||
|
'plan-eng-review-plan-mode': ['plan-eng-review/**', 'scripts/resolvers/preamble/generate-plan-mode-handshake.ts', 'scripts/resolvers/preamble.ts', 'scripts/question-registry.ts', 'scripts/one-way-doors.ts', 'test/helpers/agent-sdk-runner.ts'],
|
||||||
|
'plan-design-review-plan-mode-handshake': ['plan-design-review/**', 'scripts/resolvers/preamble/generate-plan-mode-handshake.ts', 'scripts/resolvers/preamble.ts', 'scripts/question-registry.ts', 'scripts/one-way-doors.ts', 'test/helpers/agent-sdk-runner.ts'],
|
||||||
|
'plan-devex-review-plan-mode': ['plan-devex-review/**', 'scripts/resolvers/preamble/generate-plan-mode-handshake.ts', 'scripts/resolvers/preamble.ts', 'scripts/question-registry.ts', 'scripts/one-way-doors.ts', 'test/helpers/agent-sdk-runner.ts'],
|
||||||
|
'plan-mode-no-op': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-plan-mode-handshake.ts', 'scripts/resolvers/preamble.ts', 'test/helpers/agent-sdk-runner.ts'],
|
||||||
|
'e2e-harness-audit': ['plan-ceo-review/**', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'scripts/resolvers/preamble/generate-plan-mode-handshake.ts', 'test/helpers/agent-sdk-runner.ts'],
|
||||||
|
|
||||||
// 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 +250,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',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -282,12 +328,35 @@ export const E2E_TIERS: Record<string, 'gate' | 'periodic'> = {
|
||||||
'plan-eng-coverage-audit': 'gate',
|
'plan-eng-coverage-audit': 'gate',
|
||||||
'plan-review-report': 'gate',
|
'plan-review-report': 'gate',
|
||||||
|
|
||||||
|
// Plan-mode handshake — deterministic safety regression, gate-tier
|
||||||
|
'plan-ceo-review-plan-mode': 'gate',
|
||||||
|
'plan-eng-review-plan-mode': 'gate',
|
||||||
|
'plan-design-review-plan-mode-handshake': 'gate',
|
||||||
|
'plan-devex-review-plan-mode': 'gate',
|
||||||
|
'plan-mode-no-op': 'gate',
|
||||||
|
'e2e-harness-audit': 'gate',
|
||||||
|
|
||||||
// AskUserQuestion format regression — periodic (Opus 4.7 non-deterministic benchmark)
|
// AskUserQuestion format regression — periodic (Opus 4.7 non-deterministic benchmark)
|
||||||
'plan-ceo-review-format-mode': 'periodic',
|
'plan-ceo-review-format-mode': 'periodic',
|
||||||
'plan-ceo-review-format-approach': 'periodic',
|
'plan-ceo-review-format-approach': '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 +467,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/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* plan-ceo-review plan-mode handshake E2E (gate tier, paid).
|
||||||
|
*
|
||||||
|
* Asserts: when /plan-ceo-review is invoked with the plan-mode distinctive
|
||||||
|
* phrase in the system reminder, the skill fires AskUserQuestion FIRST
|
||||||
|
* (before any Write or Edit), the question has exactly 2 options (A exit,
|
||||||
|
* C cancel), picking "Exit" leads to an orderly exit with no plan file
|
||||||
|
* written.
|
||||||
|
*
|
||||||
|
* Cost: ~$0.50–$1.00 per run. Gated: EVALS=1 EVALS_TIER=gate.
|
||||||
|
* Depends on: scripts/resolvers/preamble/generate-plan-mode-handshake.ts,
|
||||||
|
* test/helpers/agent-sdk-runner.ts (canUseTool extension).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, expect } from 'bun:test';
|
||||||
|
import {
|
||||||
|
runPlanModeHandshakeTest,
|
||||||
|
assertHandshakeShape,
|
||||||
|
} from './helpers/plan-mode-handshake-helpers';
|
||||||
|
|
||||||
|
const shouldRun = !!process.env.EVALS && process.env.EVALS_TIER === 'gate';
|
||||||
|
const describeE2E = shouldRun ? describe : describe.skip;
|
||||||
|
|
||||||
|
describeE2E('plan-ceo-review plan-mode handshake (gate)', () => {
|
||||||
|
test('handshake fires before any Write/Edit when plan mode is detected', async () => {
|
||||||
|
const result = await runPlanModeHandshakeTest({
|
||||||
|
skillName: 'plan-ceo-review',
|
||||||
|
answerLabel: 'Exit',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handshake must have fired at least once.
|
||||||
|
expect(result.askUserQuestions.length).toBeGreaterThanOrEqual(1);
|
||||||
|
// Critically: no Write or Edit fired before the first AskUserQuestion.
|
||||||
|
// This is the bug v1.10.2.0 fixes — plan mode used to allow silent
|
||||||
|
// plan-file writes without any interactive gate.
|
||||||
|
expect(result.writeOrEditBeforeAsk).toBe(false);
|
||||||
|
// Handshake shape: 2 options (Exit/Cancel), Option B dropped per D8.
|
||||||
|
assertHandshakeShape(result.askUserQuestions[0]!);
|
||||||
|
}, 120_000);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* plan-design-review plan-mode handshake E2E (gate tier, paid).
|
||||||
|
*
|
||||||
|
* See test/skill-e2e-plan-ceo-plan-mode.test.ts for the shared assertion
|
||||||
|
* contract. This file exercises the same handshake against /plan-design-review.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, expect } from 'bun:test';
|
||||||
|
import {
|
||||||
|
runPlanModeHandshakeTest,
|
||||||
|
assertHandshakeShape,
|
||||||
|
} from './helpers/plan-mode-handshake-helpers';
|
||||||
|
|
||||||
|
const shouldRun = !!process.env.EVALS && process.env.EVALS_TIER === 'gate';
|
||||||
|
const describeE2E = shouldRun ? describe : describe.skip;
|
||||||
|
|
||||||
|
describeE2E('plan-design-review plan-mode handshake (gate)', () => {
|
||||||
|
test('handshake fires before any Write/Edit when plan mode is detected', async () => {
|
||||||
|
const result = await runPlanModeHandshakeTest({
|
||||||
|
skillName: 'plan-design-review',
|
||||||
|
answerLabel: 'Cancel', // exercise the C-cancel branch instead of A-exit
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.askUserQuestions.length).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(result.writeOrEditBeforeAsk).toBe(false);
|
||||||
|
assertHandshakeShape(result.askUserQuestions[0]!);
|
||||||
|
}, 120_000);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* plan-devex-review plan-mode handshake E2E (gate tier, paid).
|
||||||
|
*
|
||||||
|
* See test/skill-e2e-plan-ceo-plan-mode.test.ts for the shared assertion
|
||||||
|
* contract. This file exercises the same handshake against /plan-devex-review.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, expect } from 'bun:test';
|
||||||
|
import {
|
||||||
|
runPlanModeHandshakeTest,
|
||||||
|
assertHandshakeShape,
|
||||||
|
} from './helpers/plan-mode-handshake-helpers';
|
||||||
|
|
||||||
|
const shouldRun = !!process.env.EVALS && process.env.EVALS_TIER === 'gate';
|
||||||
|
const describeE2E = shouldRun ? describe : describe.skip;
|
||||||
|
|
||||||
|
describeE2E('plan-devex-review plan-mode handshake (gate)', () => {
|
||||||
|
test('handshake fires before any Write/Edit when plan mode is detected', async () => {
|
||||||
|
const result = await runPlanModeHandshakeTest({
|
||||||
|
skillName: 'plan-devex-review',
|
||||||
|
answerLabel: 'Exit',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.askUserQuestions.length).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(result.writeOrEditBeforeAsk).toBe(false);
|
||||||
|
assertHandshakeShape(result.askUserQuestions[0]!);
|
||||||
|
}, 120_000);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* plan-eng-review plan-mode handshake E2E (gate tier, paid).
|
||||||
|
*
|
||||||
|
* See test/skill-e2e-plan-ceo-plan-mode.test.ts for the shared assertion
|
||||||
|
* contract. This file exercises the same handshake against /plan-eng-review.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, expect } from 'bun:test';
|
||||||
|
import {
|
||||||
|
runPlanModeHandshakeTest,
|
||||||
|
assertHandshakeShape,
|
||||||
|
} from './helpers/plan-mode-handshake-helpers';
|
||||||
|
|
||||||
|
const shouldRun = !!process.env.EVALS && process.env.EVALS_TIER === 'gate';
|
||||||
|
const describeE2E = shouldRun ? describe : describe.skip;
|
||||||
|
|
||||||
|
describeE2E('plan-eng-review plan-mode handshake (gate)', () => {
|
||||||
|
test('handshake fires before any Write/Edit when plan mode is detected', async () => {
|
||||||
|
const result = await runPlanModeHandshakeTest({
|
||||||
|
skillName: 'plan-eng-review',
|
||||||
|
answerLabel: 'Exit',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.askUserQuestions.length).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(result.writeOrEditBeforeAsk).toBe(false);
|
||||||
|
assertHandshakeShape(result.askUserQuestions[0]!);
|
||||||
|
}, 120_000);
|
||||||
|
});
|
||||||
|
|
@ -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,43 @@
|
||||||
|
/**
|
||||||
|
* Plan-mode handshake negative regression (gate tier, paid).
|
||||||
|
*
|
||||||
|
* Asserts: when /plan-ceo-review is invoked WITHOUT the plan-mode distinctive
|
||||||
|
* phrase in the system reminder, the handshake does NOT fire. The skill
|
||||||
|
* should proceed to its normal Step 0 flow. This is the REGRESSION RULE
|
||||||
|
* guardrail — the handshake must be a no-op outside plan mode or it breaks
|
||||||
|
* every existing interactive-review session.
|
||||||
|
*
|
||||||
|
* Cost: ~$0.50 per run. Gated: EVALS=1 EVALS_TIER=gate.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, expect } from 'bun:test';
|
||||||
|
import {
|
||||||
|
runPlanModeHandshakeTest,
|
||||||
|
PLAN_MODE_REMINDER,
|
||||||
|
} from './helpers/plan-mode-handshake-helpers';
|
||||||
|
|
||||||
|
const shouldRun = !!process.env.EVALS && process.env.EVALS_TIER === 'gate';
|
||||||
|
const describeE2E = shouldRun ? describe : describe.skip;
|
||||||
|
|
||||||
|
describeE2E('plan-mode handshake no-op outside plan mode (gate regression)', () => {
|
||||||
|
test('handshake does NOT fire when distinctive phrase is absent', async () => {
|
||||||
|
const result = await runPlanModeHandshakeTest({
|
||||||
|
skillName: 'plan-ceo-review',
|
||||||
|
answerLabel: 'Exit', // ignored — handshake should never fire
|
||||||
|
omitPlanModeReminder: true,
|
||||||
|
maxTurns: 3, // enough to see Step 0 start, but bounded
|
||||||
|
});
|
||||||
|
|
||||||
|
// The handshake AskUserQuestion should NOT have fired during Step 0 entry.
|
||||||
|
// Other AskUserQuestions may fire later in the skill (e.g., Step 0C-bis),
|
||||||
|
// but they will NOT have the handshake's question text.
|
||||||
|
for (const aq of result.askUserQuestions) {
|
||||||
|
const questions = aq.input.questions as Array<{ question: string }>;
|
||||||
|
for (const q of questions) {
|
||||||
|
// The handshake's question mentions the distinctive phrase in its
|
||||||
|
// prose; a non-handshake AskUserQuestion won't.
|
||||||
|
expect(q.question).not.toContain(PLAN_MODE_REMINDER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 120_000);
|
||||||
|
});
|
||||||
|
|
@ -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(/❌/);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,16 @@ describe('selectTests', () => {
|
||||||
expect(result.selected).toContain('codex-offered-ceo-review');
|
expect(result.selected).toContain('codex-offered-ceo-review');
|
||||||
expect(result.selected).toContain('plan-ceo-review-format-mode');
|
expect(result.selected).toContain('plan-ceo-review-format-mode');
|
||||||
expect(result.selected).toContain('plan-ceo-review-format-approach');
|
expect(result.selected).toContain('plan-ceo-review-format-approach');
|
||||||
expect(result.selected.length).toBe(8);
|
// v1.10.2.0 plan-mode handshake entries also depend on plan-ceo-review/**
|
||||||
expect(result.skipped.length).toBe(Object.keys(E2E_TOUCHFILES).length - 8);
|
expect(result.selected).toContain('plan-ceo-review-plan-mode');
|
||||||
|
expect(result.selected).toContain('plan-mode-no-op');
|
||||||
|
expect(result.selected).toContain('e2e-harness-audit');
|
||||||
|
expect(result.selected).toContain('plan-ceo-review-prosons-cadence');
|
||||||
|
expect(result.selected).toContain('plan-review-prosons-format');
|
||||||
|
expect(result.selected).toContain('plan-review-prosons-hardstop-neg');
|
||||||
|
expect(result.selected).toContain('plan-review-prosons-neutral-neg');
|
||||||
|
expect(result.selected.length).toBe(15);
|
||||||
|
expect(result.skipped.length).toBe(Object.keys(E2E_TOUCHFILES).length - 15);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('global touchfile triggers ALL tests', () => {
|
test('global touchfile triggers ALL tests', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue