mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
9dbaf906cf
* feat(gbrain-sync): queue primitives + writer shims
Adds bin/gstack-brain-enqueue (atomic append to sync queue) and
bin/gstack-jsonl-merge (git merge driver, ts-sort with SHA-256 fallback).
Wires one backgrounded enqueue call into learnings-log, timeline-log,
review-log, and developer-profile --migrate. question-log and
question-preferences stay local per Codex v2 decision.
gstack-config gains gbrain_sync_mode (off/artifacts-only/full) and
gbrain_sync_mode_prompted keys, plus GSTACK_HOME env alignment so
tests don't leak into real ~/.gstack/config.yaml.
* feat(gbrain-sync): --once drain + secret scan + push
bin/gstack-brain-sync is the core sync binary. Subcommands: --once
(drain queue, allowlist-filter, privacy-class-filter, secret-scan
staged diff, commit with template, push with fetch+merge retry),
--status, --skip-file <path>, --drop-queue --yes, --discover-new
(cursor-based detection of artifact writes that skip the shim).
Secret regex families: AWS keys, GitHub tokens (ghp_/gho_/ghu_/ghs_/
ghr_/github_pat_), OpenAI sk-, PEM blocks, JWTs, bearer-token-in-JSON.
On hit: unstage, preserve queue, print remediation hint (--skip-file
or edit), exit clean. No daemon — invoked by preamble at skill
boundaries.
* feat(gbrain-sync): init, restore, uninstall, consumer registry
bin/gstack-brain-init: idempotent first-run. git init ~/.gstack/,
.gitignore=*, canonical .brain-allowlist + .brain-privacy-map.json,
pre-commit secret-scan hook (defense-in-depth), merge driver registration
via git config, gh repo create --private OR arbitrary --remote <url>,
initial push, ~/.gstack-brain-remote.txt for new-machine discovery,
GBrain consumer registration via HTTP POST.
bin/gstack-brain-restore: safe new-machine bootstrap. Refuses clobber
of existing allowlisted files, clones to staging, rsync-copies tracked
files, re-registers merge drivers (required — not cloned from remote),
rehydrates consumers.json, prompts for per-consumer tokens.
bin/gstack-brain-uninstall: clean off-ramp. Removes .git + .brain-*
files + consumers.json + config keys. Preserves user data (learnings,
plans, retros, profile). Optional --delete-remote for GitHub repos.
bin/gstack-brain-consumer + bin/gstack-brain-reader (symlink alias):
registry management. Internal 'consumer' term; user-facing 'reader'
per DX review decision.
* feat(gbrain-sync): preamble block — privacy gate + boundary sync
scripts/resolvers/preamble/generate-brain-sync-block.ts emits bash that
runs at every skill invocation:
- Detects ~/.gstack-brain-remote.txt on machines without local .git
and surfaces a restore-available hint (does NOT auto-run restore).
- Runs gstack-brain-sync --once at skill start to drain any pending
writes (and at skill end via prose instruction).
- Once-per-day auto-pull (cached via .brain-last-pull) for append-only
JSONL files.
- Emits BRAIN_SYNC: status line every skill run.
Also emits prose for the host LLM to fire the one-time privacy
stop-gate (full / artifacts-only / off) when gbrain is detected and
gbrain_sync_mode_prompted is false. Wired into preamble.ts composition.
* test(gbrain-sync): 27-test consolidated suite
test/brain-sync.test.ts covers:
- Config: validation, defaults, GSTACK_HOME env isolation
- Enqueue: no-op gates, skip list, concurrent atomicity, JSON escape
- JSONL merge driver: 3-way + ts-sort + SHA-256 fallback
- Init + sync: canonical file creation, merge driver registration,
push-reject + fetch+merge retry path
- Init refuses different remote (idempotency)
- Cross-machine restore round-trip (machine A write → machine B sees)
- Secret scan across all 6 regex families (AWS, GH, OpenAI, PEM, JWT,
bearer-JSON). --skip-file unblock remediation
- Uninstall removes sync config, preserves user data
- --discover-new idempotence via mtime+size cursor
Behaviors verified via integration smokes during implementation. Known
follow-up: bun-test 5s default timeout needs 30s wrapper for
spawnSync-heavy tests.
* docs(gbrain-sync): user guide + error lookup + README section
docs/gbrain-sync.md: setup walkthrough, privacy modes, cross-machine
workflow, secret protection, two-machine conflict handling, uninstall,
troubleshooting reference.
docs/gbrain-sync-errors.md: problem/cause/fix index for every
user-visible error. Patterned on Rust's error docs + Stripe's API
error reference.
README.md: 'Cross-machine memory with GBrain sync' section near the
top (discovery moment), plus docs-table entry.
* chore: bump version and changelog (v1.7.0.0)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: regenerate SKILL.md files for gbrain-sync preamble block
Re-runs bun run gen:skill-docs after adding generateBrainSyncBlock
to scripts/resolvers/preamble.ts in a2aa8a07. CI check-freshness
caught the drift. All 36 SKILL.md files regenerated with the new
skill-start bash block + privacy-gate prose + skill-end sync
instructions baked in.
* fix(test): session-awareness reads AskUserQuestion Format from a Tier 2+ SKILL.md
The test was reading ROOT/SKILL.md (browse skill, Tier 1) which never
contained '## AskUserQuestion Format' — that section is only emitted
for Tier 2+ skills by scripts/resolvers/preamble.ts. As a result the
agent was prompted with an empty format guide and only emitted
'RECOMMENDATION' intermittently, making the test flaky.
Pre-existing on main (same ROOT/SKILL.md shape there) — surfaced now
because the agent run didn't hit the RECOMMENDATION/recommend/option a
fallback strings in this particular attempt.
Fix: read from office-hours/SKILL.md (Tier 3, always has the section)
with a fallback that scans for the first top-level skill dir whose
SKILL.md contains the header. Future template moves won't break this
test again.
* chore: bump to v1.9.0.0 for gbrain-sync landing
Changes just the VERSION + package.json + CHANGELOG header (1.7.0.0 → 1.9.0.0
and date 2026-04-22 → 2026-04-23). No code changes. User call: land gbrain-sync
as a bigger-signal release above main's 1.6.4.0, skipping 1.8.0.0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
749 lines
33 KiB
Markdown
749 lines
33 KiB
Markdown
---
|
|
name: make-pdf
|
|
preamble-tier: 1
|
|
version: 1.0.0
|
|
description: |
|
|
Turn any markdown file into a publication-quality PDF. Proper 1in margins,
|
|
intelligent page breaks, page numbers, cover pages, running headers, curly
|
|
quotes and em dashes, clickable TOC, diagonal DRAFT watermark. Not a draft
|
|
artifact — a finished artifact. Use when asked to "make a PDF", "export to
|
|
PDF", "turn this markdown into a PDF", or "generate a document". (gstack)
|
|
Voice triggers (speech-to-text aliases): "make this a pdf", "make it a pdf", "export to pdf", "turn this into a pdf", "turn this markdown into a pdf", "generate a pdf", "make a pdf from", "pdf this markdown".
|
|
triggers:
|
|
- markdown to pdf
|
|
- generate pdf
|
|
- make pdf
|
|
- export pdf
|
|
allowed-tools:
|
|
- Bash
|
|
- Read
|
|
- AskUserQuestion
|
|
---
|
|
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
|
|
<!-- Regenerate: bun run gen:skill-docs -->
|
|
|
|
## Preamble (run first)
|
|
|
|
```bash
|
|
_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
|
|
[ -n "$_UPD" ] && echo "$_UPD" || true
|
|
mkdir -p ~/.gstack/sessions
|
|
touch ~/.gstack/sessions/"$PPID"
|
|
_SESSIONS=$(find ~/.gstack/sessions -mmin -120 -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
find ~/.gstack/sessions -mmin +120 -type f -exec rm {} + 2>/dev/null || true
|
|
_PROACTIVE=$(~/.claude/skills/gstack/bin/gstack-config get proactive 2>/dev/null || echo "true")
|
|
_PROACTIVE_PROMPTED=$([ -f ~/.gstack/.proactive-prompted ] && echo "yes" || echo "no")
|
|
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
|
echo "BRANCH: $_BRANCH"
|
|
_SKILL_PREFIX=$(~/.claude/skills/gstack/bin/gstack-config get skill_prefix 2>/dev/null || echo "false")
|
|
echo "PROACTIVE: $_PROACTIVE"
|
|
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
|
echo "SKILL_PREFIX: $_SKILL_PREFIX"
|
|
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
|
|
REPO_MODE=${REPO_MODE:-unknown}
|
|
echo "REPO_MODE: $REPO_MODE"
|
|
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
|
|
echo "LAKE_INTRO: $_LAKE_SEEN"
|
|
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
|
|
_TEL_PROMPTED=$([ -f ~/.gstack/.telemetry-prompted ] && echo "yes" || echo "no")
|
|
_TEL_START=$(date +%s)
|
|
_SESSION_ID="$$-$(date +%s)"
|
|
echo "TELEMETRY: ${_TEL:-off}"
|
|
echo "TEL_PROMPTED: $_TEL_PROMPTED"
|
|
# Writing style verbosity (V1: default = ELI10, terse = tighter V0 prose.
|
|
# Read on every skill run so terse mode takes effect without a restart.)
|
|
_EXPLAIN_LEVEL=$(~/.claude/skills/gstack/bin/gstack-config get explain_level 2>/dev/null || echo "default")
|
|
if [ "$_EXPLAIN_LEVEL" != "default" ] && [ "$_EXPLAIN_LEVEL" != "terse" ]; then _EXPLAIN_LEVEL="default"; fi
|
|
echo "EXPLAIN_LEVEL: $_EXPLAIN_LEVEL"
|
|
# Question tuning (see /plan-tune). Observational only in V1.
|
|
_QUESTION_TUNING=$(~/.claude/skills/gstack/bin/gstack-config get question_tuning 2>/dev/null || echo "false")
|
|
echo "QUESTION_TUNING: $_QUESTION_TUNING"
|
|
mkdir -p ~/.gstack/analytics
|
|
if [ "$_TEL" != "off" ]; then
|
|
echo '{"skill":"make-pdf","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
|
fi
|
|
# zsh-compatible: use find instead of glob to avoid NOMATCH error
|
|
for _PF in $(find ~/.gstack/analytics -maxdepth 1 -name '.pending-*' 2>/dev/null); do
|
|
if [ -f "$_PF" ]; then
|
|
if [ "$_TEL" != "off" ] && [ -x "~/.claude/skills/gstack/bin/gstack-telemetry-log" ]; then
|
|
~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true
|
|
fi
|
|
rm -f "$_PF" 2>/dev/null || true
|
|
fi
|
|
break
|
|
done
|
|
# Learnings count
|
|
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" 2>/dev/null || true
|
|
_LEARN_FILE="${GSTACK_HOME:-$HOME/.gstack}/projects/${SLUG:-unknown}/learnings.jsonl"
|
|
if [ -f "$_LEARN_FILE" ]; then
|
|
_LEARN_COUNT=$(wc -l < "$_LEARN_FILE" 2>/dev/null | tr -d ' ')
|
|
echo "LEARNINGS: $_LEARN_COUNT entries loaded"
|
|
if [ "$_LEARN_COUNT" -gt 5 ] 2>/dev/null; then
|
|
~/.claude/skills/gstack/bin/gstack-learnings-search --limit 3 2>/dev/null || true
|
|
fi
|
|
else
|
|
echo "LEARNINGS: 0"
|
|
fi
|
|
# Session timeline: record skill start (local-only, never sent anywhere)
|
|
~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"make-pdf","event":"started","branch":"'"$_BRANCH"'","session":"'"$_SESSION_ID"'"}' 2>/dev/null &
|
|
# Check if CLAUDE.md has routing rules
|
|
_HAS_ROUTING="no"
|
|
if [ -f CLAUDE.md ] && grep -q "## Skill routing" CLAUDE.md 2>/dev/null; then
|
|
_HAS_ROUTING="yes"
|
|
fi
|
|
_ROUTING_DECLINED=$(~/.claude/skills/gstack/bin/gstack-config get routing_declined 2>/dev/null || echo "false")
|
|
echo "HAS_ROUTING: $_HAS_ROUTING"
|
|
echo "ROUTING_DECLINED: $_ROUTING_DECLINED"
|
|
# Vendoring deprecation: detect if CWD has a vendored gstack copy
|
|
_VENDORED="no"
|
|
if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then
|
|
if [ -f ".claude/skills/gstack/VERSION" ] || [ -d ".claude/skills/gstack/.git" ]; then
|
|
_VENDORED="yes"
|
|
fi
|
|
fi
|
|
echo "VENDORED_GSTACK: $_VENDORED"
|
|
echo "MODEL_OVERLAY: claude"
|
|
# Checkpoint mode (explicit = no auto-commit, continuous = WIP commits as you go)
|
|
_CHECKPOINT_MODE=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit")
|
|
_CHECKPOINT_PUSH=$(~/.claude/skills/gstack/bin/gstack-config get checkpoint_push 2>/dev/null || echo "false")
|
|
echo "CHECKPOINT_MODE: $_CHECKPOINT_MODE"
|
|
echo "CHECKPOINT_PUSH: $_CHECKPOINT_PUSH"
|
|
# Detect spawned session (OpenClaw or other orchestrator)
|
|
[ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true
|
|
```
|
|
|
|
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
|
|
types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say:
|
|
"I think /skillname might help here — want me to run it?" and wait for confirmation.
|
|
The user opted out of proactive behavior.
|
|
|
|
If `SKILL_PREFIX` is `"true"`, the user has namespaced skill names. When suggesting
|
|
or invoking other gstack skills, use the `/gstack-` prefix (e.g., `/gstack-qa` instead
|
|
of `/qa`, `/gstack-ship` instead of `/ship`). Disk paths are unaffected — always use
|
|
`~/.claude/skills/gstack/[skill-name]/SKILL.md` for reading skill files.
|
|
|
|
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined).
|
|
|
|
If output shows `JUST_UPGRADED <from> <to>` AND `SPAWNED_SESSION` is NOT set: tell
|
|
the user "Running gstack v{to} (just updated!)" and then check for new features to
|
|
surface. For each per-feature marker below, if the marker file is missing AND the
|
|
feature is plausibly useful for this user, use AskUserQuestion to let them try it.
|
|
Fire once per feature per user, NOT once per upgrade.
|
|
|
|
**In spawned sessions (`SPAWNED_SESSION` = "true"): SKIP feature discovery entirely.**
|
|
Just print "Running gstack v{to}" and continue. Orchestrators do not want interactive
|
|
prompts from sub-sessions.
|
|
|
|
**Feature discovery markers and prompts** (one at a time, max one per session):
|
|
|
|
1. `~/.claude/skills/gstack/.feature-prompted-continuous-checkpoint` →
|
|
Prompt: "Continuous checkpoint auto-commits your work as you go with `WIP:` prefix
|
|
so you never lose progress to a crash. Local-only by default — doesn't push
|
|
anywhere unless you turn that on. Want to try it?"
|
|
Options: A) Enable continuous mode, B) Show me first (print the section from
|
|
the preamble Continuous Checkpoint Mode), C) Skip.
|
|
If A: run `~/.claude/skills/gstack/bin/gstack-config set checkpoint_mode continuous`.
|
|
Always: `touch ~/.claude/skills/gstack/.feature-prompted-continuous-checkpoint`
|
|
|
|
2. `~/.claude/skills/gstack/.feature-prompted-model-overlay` →
|
|
Inform only (no prompt): "Model overlays are active. `MODEL_OVERLAY: {model}`
|
|
shown in the preamble output tells you which behavioral patch is applied.
|
|
Override with `--model` when regenerating skills (e.g., `bun run gen:skill-docs
|
|
--model gpt-5.4`). Default is claude."
|
|
Always: `touch ~/.claude/skills/gstack/.feature-prompted-model-overlay`
|
|
|
|
After handling JUST_UPGRADED (prompts done or skipped), continue with the skill
|
|
workflow.
|
|
|
|
If `WRITING_STYLE_PENDING` is `yes`: You're on the first skill run after upgrading
|
|
to gstack v1. Ask the user once about the new default writing style. Use AskUserQuestion:
|
|
|
|
> v1 prompts = simpler. Technical terms get a one-sentence gloss on first use,
|
|
> questions are framed in outcome terms, sentences are shorter.
|
|
>
|
|
> Keep the new default, or prefer the older tighter prose?
|
|
|
|
Options:
|
|
- A) Keep the new default (recommended — good writing helps everyone)
|
|
- B) Restore V0 prose — set `explain_level: terse`
|
|
|
|
If A: leave `explain_level` unset (defaults to `default`).
|
|
If B: run `~/.claude/skills/gstack/bin/gstack-config set explain_level terse`.
|
|
|
|
Always run (regardless of choice):
|
|
```bash
|
|
rm -f ~/.gstack/.writing-style-prompt-pending
|
|
touch ~/.gstack/.writing-style-prompted
|
|
```
|
|
|
|
This only happens once. If `WRITING_STYLE_PENDING` is `no`, skip this entirely.
|
|
|
|
If `LAKE_INTRO` is `no`: Before continuing, introduce the Completeness Principle.
|
|
Tell the user: "gstack follows the **Boil the Lake** principle — always do the complete
|
|
thing when AI makes the marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean"
|
|
Then offer to open the essay in their default browser:
|
|
|
|
```bash
|
|
open https://garryslist.org/posts/boil-the-ocean
|
|
touch ~/.gstack/.completeness-intro-seen
|
|
```
|
|
|
|
Only run `open` if the user says yes. Always run `touch` to mark as seen. This only happens once.
|
|
|
|
If `TEL_PROMPTED` is `no` AND `LAKE_INTRO` is `yes`: After the lake intro is handled,
|
|
ask the user about telemetry. Use AskUserQuestion:
|
|
|
|
> Help gstack get better! Community mode shares usage data (which skills you use, how long
|
|
> they take, crash info) with a stable device ID so we can track trends and fix bugs faster.
|
|
> No code, file paths, or repo names are ever sent.
|
|
> Change anytime with `gstack-config set telemetry off`.
|
|
|
|
Options:
|
|
- A) Help gstack get better! (recommended)
|
|
- B) No thanks
|
|
|
|
If A: run `~/.claude/skills/gstack/bin/gstack-config set telemetry community`
|
|
|
|
If B: ask a follow-up AskUserQuestion:
|
|
|
|
> How about anonymous mode? We just learn that *someone* used gstack — no unique ID,
|
|
> no way to connect sessions. Just a counter that helps us know if anyone's out there.
|
|
|
|
Options:
|
|
- A) Sure, anonymous is fine
|
|
- B) No thanks, fully off
|
|
|
|
If B→A: run `~/.claude/skills/gstack/bin/gstack-config set telemetry anonymous`
|
|
If B→B: run `~/.claude/skills/gstack/bin/gstack-config set telemetry off`
|
|
|
|
Always run:
|
|
```bash
|
|
touch ~/.gstack/.telemetry-prompted
|
|
```
|
|
|
|
This only happens once. If `TEL_PROMPTED` is `yes`, skip this entirely.
|
|
|
|
If `PROACTIVE_PROMPTED` is `no` AND `TEL_PROMPTED` is `yes`: After telemetry is handled,
|
|
ask the user about proactive behavior. Use AskUserQuestion:
|
|
|
|
> gstack can proactively figure out when you might need a skill while you work —
|
|
> like suggesting /qa when you say "does this work?" or /investigate when you hit
|
|
> a bug. We recommend keeping this on — it speeds up every part of your workflow.
|
|
|
|
Options:
|
|
- A) Keep it on (recommended)
|
|
- B) Turn it off — I'll type /commands myself
|
|
|
|
If A: run `~/.claude/skills/gstack/bin/gstack-config set proactive true`
|
|
If B: run `~/.claude/skills/gstack/bin/gstack-config set proactive false`
|
|
|
|
Always run:
|
|
```bash
|
|
touch ~/.gstack/.proactive-prompted
|
|
```
|
|
|
|
This only happens once. If `PROACTIVE_PROMPTED` is `yes`, skip this entirely.
|
|
|
|
If `HAS_ROUTING` is `no` AND `ROUTING_DECLINED` is `false` AND `PROACTIVE_PROMPTED` is `yes`:
|
|
Check if a CLAUDE.md file exists in the project root. If it does not exist, create it.
|
|
|
|
Use AskUserQuestion:
|
|
|
|
> gstack works best when your project's CLAUDE.md includes skill routing rules.
|
|
> This tells Claude to use specialized workflows (like /ship, /investigate, /qa)
|
|
> instead of answering directly. It's a one-time addition, about 15 lines.
|
|
|
|
Options:
|
|
- A) Add routing rules to CLAUDE.md (recommended)
|
|
- B) No thanks, I'll invoke skills manually
|
|
|
|
If A: Append this section to the end of CLAUDE.md:
|
|
|
|
```markdown
|
|
|
|
## Skill routing
|
|
|
|
When the user's request matches an available skill, invoke it via the Skill tool. The
|
|
skill has multi-step workflows, checklists, and quality gates that produce better
|
|
results than an ad-hoc answer. When in doubt, invoke the skill. A false positive is
|
|
cheaper than a false negative.
|
|
|
|
Key routing rules:
|
|
- Product ideas, "is this worth building", brainstorming → invoke /office-hours
|
|
- Strategy, scope, "think bigger", "what should we build" → invoke /plan-ceo-review
|
|
- Architecture, "does this design make sense" → invoke /plan-eng-review
|
|
- Design system, brand, "how should this look" → invoke /design-consultation
|
|
- Design review of a plan → invoke /plan-design-review
|
|
- Developer experience of a plan → invoke /plan-devex-review
|
|
- "Review everything", full review pipeline → invoke /autoplan
|
|
- Bugs, errors, "why is this broken", "wtf", "this doesn't work" → invoke /investigate
|
|
- Test the site, find bugs, "does this work" → invoke /qa (or /qa-only for report only)
|
|
- Code review, check the diff, "look at my changes" → invoke /review
|
|
- Visual polish, design audit, "this looks off" → invoke /design-review
|
|
- Developer experience audit, try onboarding → invoke /devex-review
|
|
- Ship, deploy, create a PR, "send it" → invoke /ship
|
|
- Merge + deploy + verify → invoke /land-and-deploy
|
|
- Configure deployment → invoke /setup-deploy
|
|
- Post-deploy monitoring → invoke /canary
|
|
- Update docs after shipping → invoke /document-release
|
|
- Weekly retro, "how'd we do" → invoke /retro
|
|
- Second opinion, codex review → invoke /codex
|
|
- Safety mode, careful mode, lock it down → invoke /careful or /guard
|
|
- Restrict edits to a directory → invoke /freeze or /unfreeze
|
|
- Upgrade gstack → invoke /gstack-upgrade
|
|
- Save progress, "save my work" → invoke /context-save
|
|
- Resume, restore, "where was I" → invoke /context-restore
|
|
- Security audit, OWASP, "is this secure" → invoke /cso
|
|
- Make a PDF, document, publication → invoke /make-pdf
|
|
- Launch real browser for QA → invoke /open-gstack-browser
|
|
- Import cookies for authenticated testing → invoke /setup-browser-cookies
|
|
- Performance regression, page speed, benchmarks → invoke /benchmark
|
|
- Review what gstack has learned → invoke /learn
|
|
- Tune question sensitivity → invoke /plan-tune
|
|
- Code quality dashboard → invoke /health
|
|
```
|
|
|
|
Then commit the change: `git add CLAUDE.md && git commit -m "chore: add gstack skill routing rules to CLAUDE.md"`
|
|
|
|
If B: run `~/.claude/skills/gstack/bin/gstack-config set routing_declined true`
|
|
Say "No problem. You can add routing rules later by running `gstack-config set routing_declined false` and re-running any skill."
|
|
|
|
This only happens once per project. If `HAS_ROUTING` is `yes` or `ROUTING_DECLINED` is `true`, skip this entirely.
|
|
|
|
If `VENDORED_GSTACK` is `yes`: This project has a vendored copy of gstack at
|
|
`.claude/skills/gstack/`. Vendoring is deprecated. We will not keep vendored copies
|
|
up to date, so this project's gstack will fall behind.
|
|
|
|
Use AskUserQuestion (one-time per project, check for `~/.gstack/.vendoring-warned-$SLUG` marker):
|
|
|
|
> This project has gstack vendored in `.claude/skills/gstack/`. Vendoring is deprecated.
|
|
> We won't keep this copy up to date, so you'll fall behind on new features and fixes.
|
|
>
|
|
> Want to migrate to team mode? It takes about 30 seconds.
|
|
|
|
Options:
|
|
- A) Yes, migrate to team mode now
|
|
- B) No, I'll handle it myself
|
|
|
|
If A:
|
|
1. Run `git rm -r .claude/skills/gstack/`
|
|
2. Run `echo '.claude/skills/gstack/' >> .gitignore`
|
|
3. Run `~/.claude/skills/gstack/bin/gstack-team-init required` (or `optional`)
|
|
4. Run `git add .claude/ .gitignore CLAUDE.md && git commit -m "chore: migrate gstack from vendored to team mode"`
|
|
5. Tell the user: "Done. Each developer now runs: `cd ~/.claude/skills/gstack && ./setup --team`"
|
|
|
|
If B: say "OK, you're on your own to keep the vendored copy up to date."
|
|
|
|
Always run (regardless of choice):
|
|
```bash
|
|
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" 2>/dev/null || true
|
|
touch ~/.gstack/.vendoring-warned-${SLUG:-unknown}
|
|
```
|
|
|
|
This only happens once per project. If the marker file exists, skip entirely.
|
|
|
|
If `SPAWNED_SESSION` is `"true"`, you are running inside a session spawned by an
|
|
AI orchestrator (e.g., OpenClaw). In spawned sessions:
|
|
- Do NOT use AskUserQuestion for interactive prompts. Auto-choose the recommended option.
|
|
- Do NOT run upgrade checks, telemetry prompts, routing injection, or lake intro.
|
|
- Focus on completing the task and reporting results via prose output.
|
|
- End with a completion report: what shipped, decisions made, anything uncertain.
|
|
|
|
## GBrain Sync (skill start)
|
|
|
|
```bash
|
|
# gbrain-sync: drain pending writes, pull once per day. Silent no-op when
|
|
# the feature isn't initialized or gbrain_sync_mode is "off". See
|
|
# docs/gbrain-sync.md.
|
|
|
|
_GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
|
_BRAIN_SYNC_BIN="~/.claude/skills/gstack/bin/gstack-brain-sync"
|
|
_BRAIN_CONFIG_BIN="~/.claude/skills/gstack/bin/gstack-config"
|
|
|
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get gbrain_sync_mode 2>/dev/null || echo off)
|
|
|
|
# New-machine hint: URL file present, local .git missing, sync not yet enabled.
|
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
|
echo "BRAIN_SYNC: brain repo detected: $_BRAIN_NEW_URL"
|
|
echo "BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory (or 'gstack-config set gbrain_sync_mode off' to dismiss forever)"
|
|
fi
|
|
fi
|
|
|
|
# Active-sync path.
|
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
|
# Once-per-day pull.
|
|
_BRAIN_LAST_PULL_FILE="$_GSTACK_HOME/.brain-last-pull"
|
|
_BRAIN_NOW=$(date +%s)
|
|
_BRAIN_DO_PULL=1
|
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
|
fi
|
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
|
( cd "$_GSTACK_HOME" && git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
|
fi
|
|
# Drain pending queue, push.
|
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
|
fi
|
|
|
|
# Status line — always emitted, easy to grep.
|
|
if [ -d "$_GSTACK_HOME/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
|
_BRAIN_QUEUE_DEPTH=0
|
|
[ -f "$_GSTACK_HOME/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(wc -l < "$_GSTACK_HOME/.brain-queue.jsonl" | tr -d ' ')
|
|
_BRAIN_LAST_PUSH="never"
|
|
[ -f "$_GSTACK_HOME/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(cat "$_GSTACK_HOME/.brain-last-push" 2>/dev/null || echo never)
|
|
echo "BRAIN_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
|
else
|
|
echo "BRAIN_SYNC: off"
|
|
fi
|
|
```
|
|
|
|
|
|
|
|
**Privacy stop-gate (fires ONCE per machine).**
|
|
|
|
If the bash output shows `BRAIN_SYNC: off` AND the config value
|
|
`gbrain_sync_mode_prompted` is `false` AND gbrain is detected on this host
|
|
(either `gbrain doctor --fast --json` succeeds or the `gbrain` binary is in PATH),
|
|
fire a one-time privacy gate via AskUserQuestion:
|
|
|
|
> gstack can publish your session memory (learnings, plans, designs, retros) to a
|
|
> private GitHub repo that GBrain indexes across your machines. Higher tiers
|
|
> include behavioral data (session timelines, developer profile). How much do you
|
|
> want to sync?
|
|
|
|
Options:
|
|
- A) Everything allowlisted (recommended — maximum cross-machine memory)
|
|
- B) Only artifacts (plans, designs, retros, learnings) — skip timelines and profile
|
|
- C) Decline — keep everything local
|
|
|
|
After the user answers, run (substituting the chosen value):
|
|
|
|
```bash
|
|
# Chosen mode: full | artifacts-only | off
|
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode <choice>
|
|
"$_BRAIN_CONFIG_BIN" set gbrain_sync_mode_prompted true
|
|
```
|
|
|
|
If A or B was chosen AND `~/.gstack/.git` doesn't exist, ask a follow-up:
|
|
"Set up the GBrain sync repo now? (runs `gstack-brain-init`)"
|
|
- A) Yes, run it now
|
|
- B) Show me the command, I'll run it myself
|
|
|
|
Do not block the skill. Emit the question, continue the skill workflow. The
|
|
next skill run picks up wherever this left off.
|
|
|
|
**At skill END (before the telemetry block),** run these bash commands to
|
|
catch artifact writes (design docs, plans, retros) that skipped the writer
|
|
shims, plus drain any still-pending queue entries:
|
|
|
|
```bash
|
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --discover-new 2>/dev/null || true
|
|
"~/.claude/skills/gstack/bin/gstack-brain-sync" --once 2>/dev/null || true
|
|
```
|
|
|
|
|
|
## Model-Specific Behavioral Patch (claude)
|
|
|
|
The following nudges are tuned for the claude model family. They are
|
|
**subordinate** to skill workflow, STOP points, AskUserQuestion gates, plan-mode
|
|
safety, and /ship review gates. If a nudge below conflicts with skill instructions,
|
|
the skill wins. Treat these as preferences, not rules.
|
|
|
|
**Todo-list discipline.** When working through a multi-step plan, mark each task
|
|
complete individually as you finish it. Do not batch-complete at the end. If a task
|
|
turns out to be unnecessary, mark it skipped with a one-line reason.
|
|
|
|
**Think before heavy actions.** For complex operations (refactors, migrations,
|
|
non-trivial new features), briefly state your approach before executing. This lets
|
|
the user course-correct cheaply instead of mid-flight.
|
|
|
|
**Dedicated tools over Bash.** Prefer Read, Edit, Write, Glob, Grep over shell
|
|
equivalents (cat, sed, find, grep). The dedicated tools are cheaper and clearer.
|
|
|
|
## Voice
|
|
|
|
**Tone:** direct, concrete, sharp, never corporate, never academic. Sound like a builder, not a consultant. Name the file, the function, the command. No filler, no throat-clearing.
|
|
|
|
**Writing rules:** No em dashes (use commas, periods, "..."). No AI vocabulary (delve, crucial, robust, comprehensive, nuanced, etc.). Short paragraphs. End with what to do.
|
|
|
|
The user always has context you don't. Cross-model agreement is a recommendation, not a decision — the user decides.
|
|
|
|
## Completion Status Protocol
|
|
|
|
When completing a skill workflow, report status using one of:
|
|
- **DONE** — All steps completed successfully. Evidence provided for each claim.
|
|
- **DONE_WITH_CONCERNS** — Completed, but with issues the user should know about. List each concern.
|
|
- **BLOCKED** — Cannot proceed. State what is blocking and what was tried.
|
|
- **NEEDS_CONTEXT** — Missing information required to continue. State exactly what you need.
|
|
|
|
### Escalation
|
|
|
|
It is always OK to stop and say "this is too hard for me" or "I'm not confident in this result."
|
|
|
|
Bad work is worse than no work. You will not be penalized for escalating.
|
|
- If you have attempted a task 3 times without success, STOP and escalate.
|
|
- If you are uncertain about a security-sensitive change, STOP and escalate.
|
|
- If the scope of work exceeds what you can verify, STOP and escalate.
|
|
|
|
Escalation format:
|
|
```
|
|
STATUS: BLOCKED | NEEDS_CONTEXT
|
|
REASON: [1-2 sentences]
|
|
ATTEMPTED: [what you tried]
|
|
RECOMMENDATION: [what the user should do next]
|
|
```
|
|
|
|
## Operational Self-Improvement
|
|
|
|
Before completing, reflect on this session:
|
|
- Did any commands fail unexpectedly?
|
|
- Did you take a wrong approach and have to backtrack?
|
|
- Did you discover a project-specific quirk (build order, env vars, timing, auth)?
|
|
- Did something take longer than expected because of a missing flag or config?
|
|
|
|
If yes, log an operational learning for future sessions:
|
|
|
|
```bash
|
|
~/.claude/skills/gstack/bin/gstack-learnings-log '{"skill":"SKILL_NAME","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":N,"source":"observed"}'
|
|
```
|
|
|
|
Replace SKILL_NAME with the current skill name. Only log genuine operational discoveries.
|
|
Don't log obvious things or one-time transient errors (network blips, rate limits).
|
|
A good test: would knowing this save 5+ minutes in a future session? If yes, log it.
|
|
|
|
## Telemetry (run last)
|
|
|
|
After the skill workflow completes (success, error, or abort), log the telemetry event.
|
|
Determine the skill name from the `name:` field in this file's YAML frontmatter.
|
|
Determine the outcome from the workflow result (success if completed normally, error
|
|
if it failed, abort if the user interrupted).
|
|
|
|
**PLAN MODE EXCEPTION — ALWAYS RUN:** This command writes telemetry to
|
|
`~/.gstack/analytics/` (user config directory, not project files). The skill
|
|
preamble already writes to the same directory — this is the same pattern.
|
|
Skipping this command loses session duration and outcome data.
|
|
|
|
Run this bash:
|
|
|
|
```bash
|
|
_TEL_END=$(date +%s)
|
|
_TEL_DUR=$(( _TEL_END - _TEL_START ))
|
|
rm -f ~/.gstack/analytics/.pending-"$_SESSION_ID" 2>/dev/null || true
|
|
# Session timeline: record skill completion (local-only, never sent anywhere)
|
|
~/.claude/skills/gstack/bin/gstack-timeline-log '{"skill":"SKILL_NAME","event":"completed","branch":"'$(git branch --show-current 2>/dev/null || echo unknown)'","outcome":"OUTCOME","duration_s":"'"$_TEL_DUR"'","session":"'"$_SESSION_ID"'"}' 2>/dev/null || true
|
|
# Local analytics (gated on telemetry setting)
|
|
if [ "$_TEL" != "off" ]; then
|
|
echo '{"skill":"SKILL_NAME","duration_s":"'"$_TEL_DUR"'","outcome":"OUTCOME","browse":"USED_BROWSE","session":"'"$_SESSION_ID"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
|
fi
|
|
# Remote telemetry (opt-in, requires binary)
|
|
if [ "$_TEL" != "off" ] && [ -x ~/.claude/skills/gstack/bin/gstack-telemetry-log ]; then
|
|
~/.claude/skills/gstack/bin/gstack-telemetry-log \
|
|
--skill "SKILL_NAME" --duration "$_TEL_DUR" --outcome "OUTCOME" \
|
|
--used-browse "USED_BROWSE" --session-id "$_SESSION_ID" 2>/dev/null &
|
|
fi
|
|
```
|
|
|
|
Replace `SKILL_NAME` with the actual skill name from frontmatter, `OUTCOME` with
|
|
success/error/abort, and `USED_BROWSE` with true/false based on whether `$B` was used.
|
|
If you cannot determine the outcome, use "unknown". The local JSONL always logs. The
|
|
remote binary only runs if telemetry is not off and the binary exists.
|
|
|
|
## Plan Mode Safe Operations
|
|
|
|
In plan mode, these are always allowed (they inform the plan, don't modify source):
|
|
`$B` (browse), `$D` (design), `codex exec`/`codex review`, writes to `~/.gstack/`,
|
|
writes to the plan file, `open` for generated artifacts.
|
|
|
|
## Skill Invocation During Plan Mode
|
|
|
|
If the user invokes a skill in plan mode, that skill takes precedence over generic plan mode behavior. Treat it as executable instructions, not reference. Follow step
|
|
by step. AskUserQuestion calls satisfy plan mode's end-of-turn requirement. At a STOP
|
|
point, stop immediately. Do not continue the workflow past a STOP point and do not call ExitPlanMode there. Commands marked "PLAN
|
|
MODE EXCEPTION — ALWAYS RUN" execute. Other writes need to be already permitted
|
|
above or explicitly exception-marked. Call ExitPlanMode only after the skill
|
|
workflow completes — only then call ExitPlanMode (or if the user tells you to cancel the skill or leave plan mode).
|
|
|
|
## Plan Status Footer
|
|
|
|
In plan mode, before ExitPlanMode: if the plan file lacks a `## GSTACK REVIEW REPORT`
|
|
section, run `~/.claude/skills/gstack/bin/gstack-review-read` and append a report.
|
|
With JSONL entries (before `---CONFIG---`), format the standard runs/status/findings
|
|
table. With `NO_REVIEWS` or empty, append a 5-row placeholder table (CEO/Codex/Eng/
|
|
Design/DX Review) with all zeros and verdict "NO REVIEWS YET — run `/autoplan`".
|
|
If a richer review report already exists, skip — review skills wrote it.
|
|
|
|
PLAN MODE EXCEPTION — always allowed (it's the plan file).
|
|
|
|
# make-pdf: publication-quality PDFs from markdown
|
|
|
|
Turn `.md` files into PDFs that look like Faber & Faber essays: 1in margins,
|
|
left-aligned body, Helvetica throughout, curly quotes and em dashes, optional
|
|
cover page and clickable TOC, diagonal DRAFT watermark when you need it.
|
|
Copy-paste from the PDF produces clean words, never "S a i l i n g".
|
|
|
|
On Linux, install `fonts-liberation` for correct rendering — Helvetica and Arial
|
|
aren't present by default, and Liberation Sans is the standard metric-compatible
|
|
fallback. CI and Docker builds install it automatically via Dockerfile.ci.
|
|
|
|
## MAKE-PDF SETUP (run this check BEFORE any make-pdf command)
|
|
|
|
```bash
|
|
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
P=""
|
|
[ -n "$MAKE_PDF_BIN" ] && [ -x "$MAKE_PDF_BIN" ] && P="$MAKE_PDF_BIN"
|
|
[ -z "$P" ] && [ -n "$_ROOT" ] && [ -x "$_ROOT/.claude/skills/gstack/make-pdf/dist/pdf" ] && P="$_ROOT/.claude/skills/gstack/make-pdf/dist/pdf"
|
|
[ -z "$P" ] && P="$HOME/.claude/skills/gstack/make-pdf/dist/pdf"
|
|
if [ -x "$P" ]; then
|
|
echo "MAKE_PDF_READY: $P"
|
|
alias _p_="$P" # shellcheck alias helper (not exported)
|
|
export P # available as $P in subsequent blocks within the same skill invocation
|
|
else
|
|
echo "MAKE_PDF_NOT_AVAILABLE (run './setup' in the gstack repo to build it)"
|
|
fi
|
|
```
|
|
|
|
If `MAKE_PDF_NOT_AVAILABLE` is printed: tell the user the binary is not
|
|
built. Have them run `./setup` from the gstack repo, then retry.
|
|
|
|
If `MAKE_PDF_READY` is printed: `$P` is the binary path for the rest of
|
|
the skill. Use `$P` (not an explicit path) so the skill body stays portable.
|
|
|
|
Core commands:
|
|
- `$P generate <input.md> [output.pdf]` — render markdown to PDF (80% use case)
|
|
- `$P generate --cover --toc essay.md out.pdf` — full publication layout
|
|
- `$P generate --watermark DRAFT memo.md draft.pdf` — diagonal DRAFT watermark
|
|
- `$P preview <input.md>` — render HTML and open in browser (fast iteration)
|
|
- `$P setup` — verify browse + Chromium + pdftotext and run a smoke test
|
|
- `$P --help` — full flag reference
|
|
|
|
Output contract:
|
|
- `stdout`: ONLY the output path on success. One line.
|
|
- `stderr`: progress (`Rendering HTML... Generating PDF...`) unless `--quiet`.
|
|
- Exit 0 success / 1 bad args / 2 render error / 3 Paged.js timeout / 4 browse unavailable.
|
|
|
|
## Core patterns
|
|
|
|
### 80% case — memo/letter
|
|
|
|
One command, no flags. Gets a clean PDF with running header + page numbers
|
|
+ CONFIDENTIAL footer by default.
|
|
|
|
```bash
|
|
$P generate letter.md # writes /tmp/letter.pdf
|
|
$P generate letter.md letter.pdf # explicit output path
|
|
```
|
|
|
|
### Publication mode — cover + TOC + chapter breaks
|
|
|
|
```bash
|
|
$P generate --cover --toc --author "Garry Tan" --title "On Horizons" \
|
|
essay.md essay.pdf
|
|
```
|
|
|
|
Each top-level H1 in the markdown starts a new page. Disable with
|
|
`--no-chapter-breaks` for memos that happen to have multiple H1s.
|
|
|
|
### Draft-stage watermark
|
|
|
|
```bash
|
|
$P generate --watermark DRAFT memo.md draft.pdf
|
|
```
|
|
|
|
Diagonal 10% opacity DRAFT across every page. When the draft is final, drop
|
|
the flag and regenerate.
|
|
|
|
### Fast iteration via preview
|
|
|
|
```bash
|
|
$P preview essay.md
|
|
```
|
|
|
|
Renders HTML with the same print CSS and opens it in your browser. Refresh
|
|
as you edit the markdown. Skip the PDF round trip until you're ready.
|
|
|
|
### Brand-free (no CONFIDENTIAL footer)
|
|
|
|
```bash
|
|
$P generate --no-confidential memo.md memo.pdf
|
|
```
|
|
|
|
## Common flags
|
|
|
|
```
|
|
Page layout:
|
|
--margins <dim> 1in (default) | 72pt | 2.54cm | 25mm
|
|
--page-size letter|a4|legal
|
|
|
|
Structure:
|
|
--cover Cover page (title, author, date, hairline rule)
|
|
--toc Clickable TOC with page numbers
|
|
--no-chapter-breaks Don't start a new page at every H1
|
|
|
|
Branding:
|
|
--watermark <text> Diagonal watermark ("DRAFT", "CONFIDENTIAL")
|
|
--header-template <html> Custom running header
|
|
--footer-template <html> Custom footer (mutex with --page-numbers)
|
|
--no-confidential Suppress the CONFIDENTIAL right-footer
|
|
|
|
Output:
|
|
--page-numbers "N of M" footer (default on)
|
|
--tagged Accessible PDF (default on)
|
|
--outline PDF bookmarks from headings (default on)
|
|
--quiet Suppress progress on stderr
|
|
--verbose Per-stage timings
|
|
|
|
Network:
|
|
--allow-network Fetch external images. Off by default
|
|
(blocks tracking pixels).
|
|
|
|
Metadata:
|
|
--title "..." Document title (defaults to first H1)
|
|
--author "..." Author for cover + PDF metadata
|
|
--date "..." Date for cover (defaults to today)
|
|
```
|
|
|
|
## When Claude should run it
|
|
|
|
Watch for markdown-to-PDF intent. Any of these patterns → run `$P generate`:
|
|
|
|
- "Can you make this markdown a PDF"
|
|
- "Export it as a PDF"
|
|
- "Turn this letter into a PDF"
|
|
- "I need a PDF of the essay"
|
|
- "Print this as a PDF for me"
|
|
|
|
If the user has a `.md` file open and says "make it look nice", propose
|
|
`$P generate --cover --toc` and ask before running.
|
|
|
|
## Debugging
|
|
|
|
- Output looks empty / blank → check browse daemon is running: `$B status`.
|
|
- Fragmented text on copy-paste → highlight.js output (Phase 4). Retry with
|
|
`--no-syntax` once that flag exists. For now, remove fenced code blocks
|
|
and regenerate.
|
|
- Paged.js timeout → probably no headings in the markdown. Drop `--toc`.
|
|
- External image missing → add `--allow-network` (understand you're giving
|
|
the markdown file permission to fetch from its image URLs).
|
|
- Generated PDF too tall/wide → `--page-size a4` or `--margins 0.75in`.
|
|
|
|
## Output contract
|
|
|
|
```
|
|
stdout: /tmp/letter.pdf ← just the path, one line
|
|
stderr: Rendering HTML... ← progress spinner (unless --quiet)
|
|
Generating PDF...
|
|
Done in 1.5s. 43 words · 22KB · /tmp/letter.pdf
|
|
|
|
exit code: 0 success / 1 bad args / 2 render error / 3 Paged.js timeout
|
|
/ 4 browse unavailable
|
|
```
|
|
|
|
Capture the path: `PDF=$($P generate letter.md)` — then use `$PDF`.
|