Merge origin/main (v1.65.0.0 fork port wave 2) into test-evals-ci-speedup

Second overlapping-wave merge; resolutions compose intent:

- TEST_ROOTS: ours is the superset (main also wired ios-qa/daemon/test;
  ours additionally has ios-qa/scripts + browser-skills). package.json
  'test' keeps routing through the canonical strict runner.
- gbrainAvailable: main fixed the same load-flake with a strictly better
  mechanism (memoized stat-based PATH scan, no subprocess at all) —
  theirs supersedes this branch's memoized-exec probe. Main also made
  the query timeout env-overridable (GSTACK_BRAIN_TIMEOUT_MS).
- Model defaults: adopted main's lib/eval-model.ts abstraction (one
  resolution point, env-overridable per kind) and applied decision D1a
  inside it: capture defaults to Sonnet (Opus opt-in via explicit arg or
  GSTACK_EVAL_MODEL_CAPTURE); test pins updated to follow.
- Parent watchdog: main's rewrite (named parameterized tick, driven
  deterministically by its test via __testInternals__, plus handoff
  suppression semantics from session persistence) supersedes this
  branch's env-tunable interval; adopted their server + test wholesale.
- windows-free-tests: ours (curated bun run test:windows) — main's
  hand-list grew by one more file, which the curated runner subsumes
  automatically; that drift is the reason for D11.
- context-skills 0-for-26 fix: both waves made the IDENTICAL fix; kept
  this branch's comment (carries the receipts).
- .gitignore: main's superset (also ignores Package.resolved — their
  never-commit call; untracked the copy this branch had committed).

Verified: 239-test merge battery green, watchdog 8/8, eval-model 5/5,
actionlint clean, eval:select works.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-15 11:56:34 -07:00
co-authored by Claude Fable 5
200 changed files with 11004 additions and 1207 deletions
+49
View File
@@ -0,0 +1,49 @@
/**
* {{DESIGN_DOC_DISCOVERY}} — the canonical design-doc discovery block (#703).
*
* Finds the design doc a plan review should read: newest branch-scoped doc
* under ~/.gstack/projects/<slug>/, falling back to newest project-scoped
* doc, then lets a repo-local doc (DESIGN.md or docs/designs/*.md) win when
* it is at least as fresh. office-hours dual-writes docs/designs/ alongside
* ~/.gstack, and the committed copy is what teammates see — but a stale old
* repo doc must never shadow a newer private session.
*
* Single source of truth for the freshness-preference logic that previously
* lived verbatim in plan-ceo-review, plan-eng-review, plan-devex-review, and
* the prerequisite-skill re-check in review.ts (GStack 2 fork-port wave,
* time-attack/gstack). Drift between copies meant plan reviews could
* disagree about which design doc wins.
*
* The fragment carries no code fences — the {{DESIGN_DOC_DISCOVERY}} token
* sits inside each caller's ```bash block. Callers must set $SLUG and
* $BRANCH first (and `setopt +o nomatch` for zsh); the block sets $DESIGN
* and prints "Design doc found: ..." or "No design doc found".
*/
import type { TemplateContext } from './types';
/**
* Raw canonical fragment, exported so TS resolvers (review.ts's prerequisite
* re-check) can interpolate it into their own template strings instead of
* embedding a drifting copy.
*/
export const DESIGN_DOC_DISCOVERY_BLOCK = `_LOCALDOC=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$_LOCALDOC" ] && _LOCALDOC=$(ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1)
# Repo-local docs win when at least as fresh (#703): office-hours dual-writes
# docs/designs/ alongside ~/.gstack, and the committed copy is what teammates
# see. A stale old repo doc never shadows a newer private session.
_REPOTOP=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
_REPODOC=""
if [ -n "$_REPOTOP" ]; then
[ -f "$_REPOTOP/DESIGN.md" ] && _REPODOC="$_REPOTOP/DESIGN.md"
[ -z "$_REPODOC" ] && _REPODOC=$(ls -t "$_REPOTOP"/docs/designs/*.md 2>/dev/null | head -1)
fi
DESIGN="$_LOCALDOC"
if [ -n "$_REPODOC" ] && { [ -z "$_LOCALDOC" ] || [ "$_REPODOC" -nt "$_LOCALDOC" ]; }; then
DESIGN="$_REPODOC"
fi
[ -n "$DESIGN" ] && echo "Design doc found: $DESIGN" || echo "No design doc found"`;
export function generateDesignDocDiscovery(_ctx: TemplateContext): string {
return DESIGN_DOC_DISCOVERY_BLOCK;
}
+4
View File
@@ -33,11 +33,15 @@ import { generateGBrainContextLoad, generateGBrainSaveResults, generateBrainPref
import { generateTasksSectionEmit, generateTasksSectionAggregate } from './tasks-section';
import { SECTION, SECTION_INDEX } from './sections';
import { generateRedactInvocationBlock } from './redact-doc';
import { generateThirdPartyActions } from './third-party-actions';
import { generateDesignDocDiscovery } from './design-doc-discovery';
export const RESOLVERS: Record<string, ResolverFn> = {
SLUG_EVAL: generateSlugEval,
SLUG_SETUP: generateSlugSetup,
REDACT_INVOCATION_BLOCK: generateRedactInvocationBlock,
THIRD_PARTY_ACTIONS: generateThirdPartyActions,
DESIGN_DOC_DISCOVERY: generateDesignDocDiscovery,
COMMAND_REFERENCE: generateCommandReference,
SNAPSHOT_FLAGS: generateSnapshotFlags,
PREAMBLE: generatePreamble,
+2
View File
@@ -53,6 +53,7 @@ import { generateAskUserFormat } from './preamble/generate-ask-user-format';
import { generateWritingStyle } from './preamble/generate-writing-style';
import { generateCompletenessSection } from './preamble/generate-completeness-section';
import { generateConfusionProtocol } from './preamble/generate-confusion-protocol';
import { generateEvidenceDirective } from './preamble/generate-evidence-directive';
import { generateContinuousCheckpoint } from './preamble/generate-continuous-checkpoint';
import { generateContextHealth } from './preamble/generate-context-health';
@@ -117,6 +118,7 @@ export function generatePreamble(ctx: TemplateContext): string {
generateWritingStyle(ctx),
generateCompletenessSection(ctx),
generateConfusionProtocol(ctx),
generateEvidenceDirective(ctx),
generateContinuousCheckpoint(),
generateContextHealth(ctx),
generateQuestionTuning(ctx),
@@ -0,0 +1,18 @@
import type { TemplateContext } from '../types';
/**
* Evidence-before-claimed-limitations (fork port wave 2, D1).
*
* The single highest-leverage judgment clause from the fork's live App Store
* releases: nine release failures in two days shared one root — the agent
* asserting folklore as fact ("the API can't do this", "X requires a
* password") instead of running the ten-second check that would have
* disproven it. Adapted as ONE directive into the shared preamble (the
* fork's full SHARED-JUDGMENT contract is deliberately not imported).
*/
export function generateEvidenceDirective(ctx?: TemplateContext): string {
if (ctx?.explainLevel === 'terse') return '';
return `## Claimed Limitations Need Evidence
A claimed limitation or requirement ("the API can't do this", "X requires a credential", "that's impossible on this platform") is a material claim. State one only with the verbatim error, the documented statement, or a live probe in hand — pattern-matching a failure to a familiar story is not evidence. When a cheap probe settles the question, run it BEFORE asking the user anything or declaring a step blocked.`;
}
+15 -9
View File
@@ -15,6 +15,7 @@
import type { TemplateContext } from './types';
import { generateInvokeSkill } from './composition';
import { codexPreflight, codexErrorHandling } from './constants';
import { DESIGN_DOC_DISCOVERY_BLOCK } from './design-doc-discovery';
import { getHostConfig } from '../../hosts/index';
const CODEX_BOUNDARY = 'IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.\\n\\n';
@@ -310,9 +311,7 @@ After /${first} completes, re-run the design doc check:
setopt +o nomatch 2>/dev/null || true # zsh compat
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$DESIGN" ] && DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1)
[ -n "$DESIGN" ] && echo "Design doc found: $DESIGN" || echo "No design doc found"
${DESIGN_DOC_DISCOVERY_BLOCK}
\`\`\`
If a design doc is now found, read it and continue the review.
@@ -1145,16 +1144,23 @@ Using the plan file already discovered in Step 8, look for a verification sectio
### 2. Check for running dev server
Before invoking browse-based verification, check if a dev server is reachable:
Before invoking browse-based verification, find the dev-server URL the way the
project declares it — never trust a hardcoded port list alone:
1. **CLAUDE.md first:** look for a documented dev URL or dev command (a
\`## Development\`/\`## Testing\` section naming a port or URL). Use it.
2. **The plan file:** if the plan's verification section names a URL, use it.
3. **Fallback probe** (common ports, only when 1-2 found nothing):
\`\`\`bash
curl -s -o /dev/null -w '%{http_code}' http://localhost:3000 2>/dev/null || \\
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080 2>/dev/null || \\
curl -s -o /dev/null -w '%{http_code}' http://localhost:5173 2>/dev/null || \\
curl -s -o /dev/null -w '%{http_code}' http://localhost:4000 2>/dev/null || echo "NO_SERVER"
for _p in 3000 8080 5173 4000 4321 8000; do
_code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$_p" 2>/dev/null)
[ -n "$_code" ] && [ "$_code" != "000" ] && { echo "DEV_SERVER: http://localhost:$_p ($_code)"; break; }
done
[ -z "\${_code:-}" ] || [ "\${_code:-000}" = "000" ] && echo "NO_SERVER"
\`\`\`
**If NO_SERVER:** Skip with "No dev server detected — skipping plan verification. Run /qa separately after deploying."
**If NO_SERVER:** Skip with "No dev server detected (checked CLAUDE.md, the plan, and common ports) — skipping plan verification. Run /qa separately after deploying, or document the dev URL in CLAUDE.md so this step finds it next time."
### 3. Invoke /qa-only inline
+5 -5
View File
@@ -120,12 +120,12 @@ if command -v jq >/dev/null 2>&1; then
# Filter to current branch + recent commits, then keep records for the
# latest run_id only. (Single phase may have multiple files if the user
# re-ran the review; aggregator takes the newest.)
# NOTE: bind .commit BEFORE the split pipe. Inside ($commits | split(...))
# the "." context is the resulting ARRAY, so a bare .commit there raises
# "Cannot index array with string" on every record — and the 2>/dev/null
# below swallows it, so the whole aggregation silently yields zero tasks.
# .commit must be bound BEFORE piping to the split commit array: a
# pipe rebinds jq's context, so a bare .commit after it indexes the
# ARRAY with a string, every line errors into 2>/dev/null, and the
# aggregate is empty forever — the #2018 zero-tasks bug.
jq -c --arg branch "$BRANCH" --arg commits "$COMMITS_RECENT" \\
'select(.branch == $branch and ((.commit) as $c | ($commits | split("|") | index($c)) != null))' \\
'.commit as $c | select(.branch == $branch and ($commits | split("|") | index($c) != null))' \\
"$f" 2>/dev/null >> "$ALL_JSONL" || true
done < <(find "$TASKS_DIR" -maxdepth 1 -name "tasks-$phase-*.jsonl" 2>/dev/null | sort)
# Reduce to latest run_id per phase
+53 -17
View File
@@ -3,41 +3,70 @@ import type { TemplateContext } from './types';
export function generateTestBootstrap(_ctx: TemplateContext): string {
return `## Test Framework Bootstrap
**Detect existing test framework and project runtime:**
**Read the project's CLAUDE.md (and TESTING.md if present) FIRST.** If it documents a test command, the project already told you: no detection, no bootstrap. Skip the rest of bootstrap and use that command in Step 5.
**Otherwise gather markers. Every marker below is EVIDENCE for the question you ask — never a command to run blind.** A marker tells you which ecosystem you're in and which command to OFFER. It does not tell you the command works. Do not execute a candidate test command to "check" it: a probe on a project that never had that runner fails loudly and teaches you nothing, and installing a second framework over a working one is worse.
\`\`\`bash
setopt +o nomatch 2>/dev/null || true # zsh compat
# Detect project runtime
[ -f Gemfile ] && echo "RUNTIME:ruby"
# Definitive ecosystem markers (presence = ecosystem, NOT a command to run)
[ -f manage.py ] && echo "RUNTIME:python FRAMEWORK:django MARKER:manage.py"
{ [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f tox.ini ] || [ -f setup.cfg ] || [ -f requirements.txt ]; } && echo "RUNTIME:python"
[ -f Gemfile ] || [ -f Rakefile ] || [ -f .rspec ] && echo "RUNTIME:ruby"
[ -f package.json ] && echo "RUNTIME:node"
[ -f requirements.txt ] || [ -f pyproject.toml ] && echo "RUNTIME:python"
[ -f go.mod ] && echo "RUNTIME:go"
[ -f Cargo.toml ] && echo "RUNTIME:rust"
[ -f composer.json ] && echo "RUNTIME:php"
[ -f mix.exs ] && echo "RUNTIME:elixir"
[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven"
{ [ -f build.gradle ] || [ -f build.gradle.kts ]; } && echo "RUNTIME:jvm BUILD:gradle"
# Detect sub-frameworks
[ -f Gemfile ] && grep -q "rails" Gemfile 2>/dev/null && echo "FRAMEWORK:rails"
[ -f package.json ] && grep -q '"next"' package.json 2>/dev/null && echo "FRAMEWORK:nextjs"
# Check for existing test infrastructure
ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini pyproject.toml phpunit.xml 2>/dev/null
ls -d test/ tests/ spec/ __tests__/ cypress/ e2e/ 2>/dev/null
# Existing test path — config files, declared scripts, AND test FILES.
# A project with real tests and no config file is the common miss.
ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini tox.ini phpunit.xml* 2>/dev/null
[ -f package.json ] && grep -q '"test"[[:space:]]*:' package.json && echo "SCRIPT:package.json test"
[ -f Makefile ] && grep -qE '^(test|check):' Makefile && echo "TARGET:make test"
[ -f pyproject.toml ] && grep -q "pytest" pyproject.toml && echo "CONFIG:pyproject pytest"
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\\.py$|(^|/)test_[^/]+\\.py$|_test\\.(go|py|rb|ts|js|exs)$|\\.(test|spec)\\.[jt]sx?$|_spec\\.rb$|Test\\.(java|kt)$' | sed 's/^/TESTFILES:/'
# Rust keeps unit tests inside src/, so file names alone miss them
[ -f Cargo.toml ] && git grep -lF '#[test]' -- 'src' >/dev/null 2>&1 && echo "TESTS:rust in-source"
# Check opt-out marker
[ -f .gstack/no-test-bootstrap ] && echo "BOOTSTRAP_DECLINED"
\`\`\`
**If test framework detected** (config files or test directories found):
Print "Test framework detected: {name} ({N} existing tests). Skipping bootstrap."
Map the markers to the command you will OFFER — never to one you run on a guess:
| Marker | Ecosystem | Candidate command to offer |
|--------|-----------|----------------------------|
| \`manage.py\` | Django | \`python manage.py test\` (or \`pytest\` when pytest-django is in the deps) |
| \`pytest.ini\` / \`tox.ini\` / pytest in \`pyproject.toml\` / \`test_*.py\` | Python | \`pytest\` |
| \`go.mod\` (+ any \`*_test.go\`) | Go | \`go test ./...\` |
| \`Cargo.toml\` | Rust | \`cargo test\` |
| \`pom.xml\` | JVM (Maven) | \`mvn test\` |
| \`build.gradle\` / \`build.gradle.kts\` | JVM (Gradle) | \`./gradlew test\` |
| \`Gemfile\` / \`Rakefile\` / \`.rspec\` | Ruby | \`bundle exec rspec\`, \`bin/rails test\`, or \`rake test\` |
| \`mix.exs\` | Elixir | \`mix test\` |
| \`composer.json\` | PHP | \`composer test\` or \`./vendor/bin/phpunit\` |
| \`package.json\` with a \`test\` script | Node | that script, run with the package manager the lockfile names |
| \`Makefile\` with a \`test:\` target | any | \`make test\` |
**If ANY existing-test evidence appears** (a config file, a declared test script or make target, a nonzero \`TESTFILES:\` count, or \`TESTS:rust in-source\`): the project has tests. **Do NOT bootstrap.** Print "Existing tests detected: {the evidence}." Then get the command the same way Step 5 does — CLAUDE.md/TESTING.md if documented, otherwise AskUserQuestion offering the candidates from the table above plus "Other", and persist the answer to CLAUDE.md's \`## Testing\` section so it is never asked again. When the ecosystem ships a runner (Django, Go, Rust, Elixir, Maven/Gradle), that runner is the candidate — never install a second framework beside a working one.
Read 2-3 existing test files to learn conventions (naming, imports, assertion style, setup patterns).
Store conventions as prose context for use in Phase 8e.5 or Step 7. **Skip the rest of bootstrap.**
Absent config files and absent \`tests/\` directories are NOT evidence of "no tests": Django keeps tests in \`<app>/tests.py\`, Go in \`*_test.go\` beside the source, Rust in \`#[test]\` blocks inside \`src/\`. A green \`python manage.py test\` with no \`pytest.ini\` is a tested project, not a bootstrap candidate.
**If BOOTSTRAP_DECLINED** appears: Print "Test bootstrap previously declined — skipping." **Skip the rest of bootstrap.**
**If NO runtime detected** (no config files found): Use AskUserQuestion:
**If NO ecosystem marker matched:** Use AskUserQuestion:
"I couldn't detect your project's language. What runtime are you using?"
Options: A) Node.js/TypeScript B) Ruby/Rails C) Python D) Go E) Rust F) PHP G) Elixir H) This project doesn't need tests.
If the runtime you need isn't listed, offer "Other" and take the runtime plus the test command as free text.
If user picks H → write \`.gstack/no-test-bootstrap\` and continue without tests.
**If runtime detected but no test framework — bootstrap:**
**If an ecosystem matched but there is no existing-test evidence at all — bootstrap:**
### B2. Research best practices
@@ -53,7 +82,9 @@ If WebSearch is unavailable, use this built-in knowledge table:
| Node.js | vitest + @testing-library | jest + @testing-library |
| Next.js | vitest + @testing-library/react + playwright | jest + cypress |
| Python | pytest + pytest-cov | unittest |
| Django | pytest + pytest-django | Django's built-in \`manage.py test\` (unittest) |
| Go | stdlib testing + testify | stdlib only |
| JVM (Maven/Gradle) | JUnit 5 + AssertJ | JUnit 5 only |
| Rust | cargo test (built-in) + mockall | — |
| PHP | phpunit + mockery | pest |
| Elixir | ExUnit (built-in) + ex_machina | — |
@@ -202,15 +233,20 @@ Before analyzing coverage, detect the project's test framework:
\`\`\`bash
setopt +o nomatch 2>/dev/null || true # zsh compat
# Detect project runtime
[ -f Gemfile ] && echo "RUNTIME:ruby"
# Detect project runtime (markers are evidence, not commands to run blind)
[ -f manage.py ] && echo "RUNTIME:python FRAMEWORK:django"
{ [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f tox.ini ] || [ -f setup.cfg ] || [ -f requirements.txt ]; } && echo "RUNTIME:python"
[ -f Gemfile ] || [ -f Rakefile ] || [ -f .rspec ] && echo "RUNTIME:ruby"
[ -f package.json ] && echo "RUNTIME:node"
[ -f requirements.txt ] || [ -f pyproject.toml ] && echo "RUNTIME:python"
[ -f go.mod ] && echo "RUNTIME:go"
[ -f Cargo.toml ] && echo "RUNTIME:rust"
# Check for existing test infrastructure
ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini phpunit.xml 2>/dev/null
ls -d test/ tests/ spec/ __tests__/ cypress/ e2e/ 2>/dev/null
[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven"
{ [ -f build.gradle ] || [ -f build.gradle.kts ]; } && echo "RUNTIME:jvm BUILD:gradle"
# Check for existing test infrastructure — config files, scripts, AND test files
ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini tox.ini phpunit.xml 2>/dev/null
[ -f package.json ] && grep -q '"test"[[:space:]]*:' package.json && echo "SCRIPT:package.json test"
[ -f Makefile ] && grep -qE '^(test|check):' Makefile && echo "TARGET:make test"
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\\.py$|(^|/)test_[^/]+\\.py$|_test\\.(go|py|rb|ts|js|exs)$|\\.(test|spec)\\.[jt]sx?$|_spec\\.rb$|Test\\.(java|kt)$' | sed 's/^/TESTFILES:/'
\`\`\`
3. **If no framework detected:**${mode === 'ship' ? ' falls through to the Test Framework Bootstrap step (Step 4) which handles full setup.' : ' still produce the coverage diagram, but skip test generation.'}`);
+33
View File
@@ -0,0 +1,33 @@
/**
* {{THIRD_PARTY_ACTIONS}} — the third-party web actions contract.
*
* Governs the moment a workflow needs something done on an external website
* the user controls: registering an API key, creating a vendor account,
* configuring a dashboard, webhook, OAuth app, billing plan, or domain
* verification. Instead of dumping a manual step list, the skill offers to
* drive the browser (consent-gated, secrets never in chat) and verifies the
* captured credential before claiming success.
*
* Adapted from time-attack/gstack's THIRD-PARTY-ACTIONS.md (GStack 2, MIT):
* the fork detected the Aside AI browser; we drive our own stack — browse
* headed mode + handoff/resume ($B), GStack Browser, and pair-agent.
* Portions copyright (c) 2026 Sina Matian, time-attack/gstack, MIT.
*/
import type { TemplateContext } from './types';
export function generateThirdPartyActions(_ctx: TemplateContext): string {
return `## Third-Party Web Actions
A step sometimes requires action on an external website the user controls: registering an API key, creating a vendor or developer account, configuring a dashboard, webhook, OAuth app, billing plan, or domain verification. This contract governs that moment. It grants no new browsing authority — the AskUserQuestion format and one-way-door rules remain binding, including approval before anything that spends money.
1. **Never hand the user a manual step list for a third-party site without first offering to drive it.** The driver is gstack's own browser stack: \`$B\` headed mode with handoff/resume for the human-only moments (see the /browse skill), or GStack Browser when installed. Never install new tooling to close the gap, and never treat tooling presence as consent to browse.
2. **One explicit question before any browsing.** STOP and name the exact site and the exact actions (for example "create a test-mode API token in the Duffel dashboard"), then offer: A) I drive it now in a visible browser — you take over for sign-in and approvals, B) manual instructions, C) defer. The selection is per-task consent; never persist it as standing permission and never infer it from an earlier task.
3. **When driving, touch only the named site and actions.** Password entry, new-account credential choice, payment, CAPTCHA, and identity verification are user-performed: hand off (\`$B handoff\`) and wait instead of acting. Prefer credential flows that never expose the secret to the agent, such as password-manager autofill or the dashboard's own copy button used by the human.
4. **A captured secret never appears in chat output, logs, or shell history.** Write it to a user-approved local file with owner-only permissions (0600) or the user's secret store, and keep generated destinations out of version control. Dashboard fields are often masked placeholders — verify the captured credential with ONE non-mutating API call before claiming success; a 401 here has caught a placeholder masquerading as a key.
5. **If the user declines or defers, or no browser is usable,** provide the manual steps and mark the step blocked on the user. Do not recommend or install new products to close the gap.`;
}