sanook-cli 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +19 -0
- package/CHANGELOG.md +173 -0
- package/README.md +153 -20
- package/README.th.md +136 -0
- package/dist/agentContext.js +4 -0
- package/dist/approval.js +6 -0
- package/dist/bin.js +405 -57
- package/dist/brain.js +92 -59
- package/dist/brand.js +47 -0
- package/dist/checkpoint.js +37 -0
- package/dist/commands.js +86 -6
- package/dist/compaction.js +76 -5
- package/dist/config.js +100 -12
- package/dist/cost.js +60 -3
- package/dist/doctor.js +92 -0
- package/dist/gateway/auth.js +2 -2
- package/dist/gateway/ledger.js +2 -2
- package/dist/gateway/scheduler.js +1 -0
- package/dist/gateway/serve.js +6 -4
- package/dist/gateway/server.js +10 -2
- package/dist/git.js +11 -2
- package/dist/hooks.js +43 -17
- package/dist/knowledge.js +48 -49
- package/dist/loop.js +182 -66
- package/dist/lsp/client.js +173 -0
- package/dist/lsp/framing.js +56 -0
- package/dist/lsp/index.js +138 -0
- package/dist/lsp/servers.js +82 -0
- package/dist/mcp-server.js +244 -0
- package/dist/mcp.js +184 -29
- package/dist/memory-store.js +559 -0
- package/dist/memory.js +143 -29
- package/dist/orchestrate.js +150 -0
- package/dist/providers/codex.js +21 -7
- package/dist/providers/keys.js +3 -2
- package/dist/providers/models.js +22 -6
- package/dist/providers/registry.js +155 -1
- package/dist/repomap.js +93 -0
- package/dist/search/chunk.js +158 -0
- package/dist/search/embed-store.js +187 -0
- package/dist/search/engine.js +203 -0
- package/dist/search/fuse.js +35 -0
- package/dist/search/index-core.js +187 -0
- package/dist/search/indexer.js +241 -0
- package/dist/search/store.js +77 -0
- package/dist/session.js +42 -8
- package/dist/skill-install.js +10 -10
- package/dist/skills.js +12 -9
- package/dist/summarize.js +31 -0
- package/dist/tools/bash.js +21 -2
- package/dist/tools/diagnostics.js +41 -0
- package/dist/tools/edit.js +29 -7
- package/dist/tools/index.js +8 -1
- package/dist/tools/list.js +7 -2
- package/dist/tools/permission.js +90 -9
- package/dist/tools/read.js +23 -4
- package/dist/tools/remember.js +1 -1
- package/dist/tools/sandbox.js +61 -0
- package/dist/tools/search.js +105 -4
- package/dist/tools/task.js +195 -29
- package/dist/tools/timeout.js +35 -0
- package/dist/tools/util.js +10 -0
- package/dist/tools/write.js +6 -4
- package/dist/trust.js +89 -0
- package/dist/ui/app.js +228 -31
- package/dist/ui/banner.js +4 -9
- package/dist/ui/brain-wizard.js +2 -2
- package/dist/ui/history.js +30 -0
- package/dist/ui/mentions.js +44 -0
- package/dist/ui/render.js +55 -15
- package/dist/ui/setup.js +97 -12
- package/dist/ui/useEditor.js +83 -0
- package/dist/update.js +114 -0
- package/dist/worktree.js +173 -0
- package/package.json +11 -5
- package/scripts/postinstall.mjs +33 -0
- package/second-brain/.agents/_Index.md +30 -0
- package/second-brain/.agents/skills/_Index.md +30 -0
- package/second-brain/.agents/workflows/_Index.md +30 -0
- package/second-brain/AGENTS.md +4 -4
- package/second-brain/Acceptance/_Index.md +30 -0
- package/second-brain/Acceptance/golden-case-template.md +39 -0
- package/second-brain/Areas/_Index.md +30 -0
- package/second-brain/Bugs/System-OS/_Index.md +30 -0
- package/second-brain/Bugs/_Index.md +30 -0
- package/second-brain/CLAUDE.md +4 -1
- package/second-brain/Checklists/_Index.md +30 -0
- package/second-brain/Checklists/preflight-postflight-template.md +29 -0
- package/second-brain/Distillations/_Index.md +30 -0
- package/second-brain/Entities/_Index.md +30 -0
- package/second-brain/Entities/entity-template.md +33 -0
- package/second-brain/Evals/_Index.md +30 -0
- package/second-brain/Evals/correction-pairs.md +24 -0
- package/second-brain/Evals/failure-taxonomy.md +24 -0
- package/second-brain/Evals/golden-set.md +25 -0
- package/second-brain/Evals/quality-ledger.md +23 -0
- package/second-brain/Evals/self-eval-rubric.md +23 -0
- package/second-brain/GEMINI.md +4 -4
- package/second-brain/Goals/_Index.md +30 -0
- package/second-brain/Handoffs/_Index.md +30 -0
- package/second-brain/Home.md +7 -0
- package/second-brain/Intake/Raw Sources/_Index.md +30 -0
- package/second-brain/Intake/_Index.md +30 -0
- package/second-brain/Intake/_Quarantine/_Index.md +30 -0
- package/second-brain/Learning/_Index.md +30 -0
- package/second-brain/Playbooks/_Index.md +30 -0
- package/second-brain/Playbooks/playbook-template.md +23 -0
- package/second-brain/Projects/_Index.md +30 -0
- package/second-brain/Prompts/_Index.md +30 -0
- package/second-brain/README.md +2 -1
- package/second-brain/Research/_Index.md +30 -0
- package/second-brain/Retrospectives/_Index.md +30 -0
- package/second-brain/Reviews/_Index.md +30 -0
- package/second-brain/Runbooks/_Index.md +30 -0
- package/second-brain/Runbooks/eval-loop.md +24 -0
- package/second-brain/Sessions/_Index.md +30 -0
- package/second-brain/Shared/AI-Context-Index.md +20 -0
- package/second-brain/Shared/AI-Threads/_Index.md +30 -0
- package/second-brain/Shared/Archive/_Index.md +30 -0
- package/second-brain/Shared/Assets/_Index.md +30 -0
- package/second-brain/Shared/Context-Packs/_Index.md +30 -0
- package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
- package/second-brain/Shared/Coordination/NOW.md +28 -0
- package/second-brain/Shared/Coordination/_Index.md +30 -0
- package/second-brain/Shared/Coordination/agent-registry.md +24 -0
- package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
- package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
- package/second-brain/Shared/Coordination/task-board.md +32 -0
- package/second-brain/Shared/Core-Facts/_Index.md +30 -0
- package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
- package/second-brain/Shared/Glossary/_Index.md +30 -0
- package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
- package/second-brain/Shared/Operating-State/_Index.md +30 -0
- package/second-brain/Shared/Prompting/_Index.md +30 -0
- package/second-brain/Shared/Provenance/_Index.md +30 -0
- package/second-brain/Shared/Rules/_Index.md +30 -0
- package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
- package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
- package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
- package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
- package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
- package/second-brain/Shared/Rules/rules-formatting.md +34 -0
- package/second-brain/Shared/Scripts/_Index.md +30 -0
- package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
- package/second-brain/Shared/User-Memory/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
- package/second-brain/Shared/Working-Memory/_Index.md +30 -0
- package/second-brain/Shared/_Index.md +30 -0
- package/second-brain/Shared/mcp-servers/_Index.md +30 -0
- package/second-brain/Skills/_Index.md +30 -0
- package/second-brain/Templates/_Index.md +30 -0
- package/second-brain/Templates/bug.md +2 -0
- package/second-brain/Templates/handoff.md +2 -0
- package/second-brain/Templates/session.md +2 -0
- package/second-brain/Tools/_Index.md +30 -0
- package/second-brain/Traces/_Index.md +30 -0
- package/second-brain/Vault Structure Map.md +33 -1
- package/second-brain/copilot/_Index.md +30 -0
- package/skills/audit-license-compliance/SKILL.md +117 -0
- package/skills/author-codemod/SKILL.md +110 -0
- package/skills/build-audit-logging/SKILL.md +112 -0
- package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
- package/skills/build-cli-tool/SKILL.md +108 -0
- package/skills/build-data-table/SKILL.md +141 -0
- package/skills/build-native-mobile-ui/SKILL.md +154 -0
- package/skills/build-offline-first-sync/SKILL.md +118 -0
- package/skills/build-realtime-channel/SKILL.md +122 -0
- package/skills/build-vector-search/SKILL.md +131 -0
- package/skills/compose-local-dev-stack/SKILL.md +149 -0
- package/skills/configure-bundler-build/SKILL.md +166 -0
- package/skills/configure-dns-tls/SKILL.md +142 -0
- package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
- package/skills/configure-security-headers-csp/SKILL.md +122 -0
- package/skills/contract-testing/SKILL.md +140 -0
- package/skills/datetime-timezone-correctness/SKILL.md +125 -0
- package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
- package/skills/debug-flaky-tests/SKILL.md +128 -0
- package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
- package/skills/deliver-webhooks/SKILL.md +116 -0
- package/skills/design-api-pagination/SKILL.md +144 -0
- package/skills/design-authorization-model/SKILL.md +119 -0
- package/skills/design-backup-dr-recovery/SKILL.md +113 -0
- package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
- package/skills/design-multi-tenancy/SKILL.md +100 -0
- package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
- package/skills/design-relational-schema/SKILL.md +129 -0
- package/skills/design-search-index-infra/SKILL.md +151 -0
- package/skills/design-state-machine/SKILL.md +108 -0
- package/skills/design-token-system/SKILL.md +109 -0
- package/skills/distributed-locks-leases/SKILL.md +120 -0
- package/skills/encrypt-sensitive-data/SKILL.md +148 -0
- package/skills/feature-flags-rollout/SKILL.md +130 -0
- package/skills/file-upload-object-storage/SKILL.md +107 -0
- package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
- package/skills/harden-llm-app-reliability/SKILL.md +126 -0
- package/skills/i18n-localization-setup/SKILL.md +113 -0
- package/skills/idempotency-keys/SKILL.md +107 -0
- package/skills/implement-push-notifications/SKILL.md +142 -0
- package/skills/ingest-webhook-secure/SKILL.md +120 -0
- package/skills/integrate-oauth-oidc/SKILL.md +126 -0
- package/skills/load-stress-test/SKILL.md +129 -0
- package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
- package/skills/model-nosql-data/SKILL.md +118 -0
- package/skills/money-decimal-arithmetic/SKILL.md +123 -0
- package/skills/monitor-ml-drift/SKILL.md +109 -0
- package/skills/numeric-precision-units/SKILL.md +144 -0
- package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
- package/skills/optimize-react-rerenders/SKILL.md +124 -0
- package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
- package/skills/payments-billing-integration/SKILL.md +114 -0
- package/skills/pin-toolchain-versions/SKILL.md +116 -0
- package/skills/plan-strangler-migration/SKILL.md +95 -0
- package/skills/property-based-testing/SKILL.md +108 -0
- package/skills/publish-package-registry/SKILL.md +130 -0
- package/skills/recover-git-state/SKILL.md +119 -0
- package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
- package/skills/resilience-timeouts-retries/SKILL.md +104 -0
- package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
- package/skills/rewrite-git-history/SKILL.md +109 -0
- package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
- package/skills/schema-evolution-compatibility/SKILL.md +121 -0
- package/skills/send-transactional-email/SKILL.md +126 -0
- package/skills/serve-deploy-ml-model/SKILL.md +107 -0
- package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
- package/skills/setup-devcontainer-env/SKILL.md +131 -0
- package/skills/setup-lint-format-precommit/SKILL.md +140 -0
- package/skills/setup-monorepo-tooling/SKILL.md +125 -0
- package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
- package/skills/structured-output-llm/SKILL.md +86 -0
- package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
- package/skills/test-data-factories/SKILL.md +158 -0
- package/skills/threat-model-stride/SKILL.md +123 -0
- package/skills/train-evaluate-ml-model/SKILL.md +109 -0
- package/skills/unicode-text-correctness/SKILL.md +109 -0
- package/skills/visual-regression-testing/SKILL.md +120 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: setup-lint-format-precommit
|
|
3
|
+
description: Stands up a lint, format, and pre-commit toolchain (Biome or ESLint flat config + Prettier, or ruff + ruff format) with .editorconfig, fast staged-only git hooks (husky+lint-staged or the pre-commit framework), and a check-only CI gate (lint --max-warnings=0 + format --check, no auto-fix), plus a one-shot reformat of a dirty repo hidden behind .git-blame-ignore-revs.
|
|
4
|
+
when_to_use: A repo has no/inconsistent linting or formatting, style churn floods diffs, or commits bypass checks and you're adding enforced gates. Distinct from type-safety-strict (type-checker strictness, not style), code-review (human correctness review), refactor-cleanup (behavior-preserving cleanups, not gates), and pin-toolchain-versions (pinning the binaries the gate runs).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
- "Set up ESLint/Biome/ruff + a formatter for this repo"
|
|
10
|
+
- "Add a pre-commit hook so unformatted/lint-failing code can't get committed"
|
|
11
|
+
- "Whitespace/quote/import-order churn is polluting every diff — kill it"
|
|
12
|
+
- "CI should fail on lint warnings and unformatted files"
|
|
13
|
+
- "We have config but it's slow, inconsistent across machines, or people `--no-verify` past it"
|
|
14
|
+
|
|
15
|
+
NOT this skill:
|
|
16
|
+
- Making the **type checker** strict (`strict: true`, removing `any`, `mypy --strict`) → type-safety-strict
|
|
17
|
+
- Judging whether the code is **correct** (logic bugs, edge cases) → code-review
|
|
18
|
+
- Behavior-preserving **cleanups/renames/dedup** of working code → refactor-cleanup
|
|
19
|
+
- Pinning Node/Python/tool **versions** so everyone runs the same binaries → pin-toolchain-versions
|
|
20
|
+
- A **monorepo's** cross-package task wiring/caching/affected-only runs → setup-monorepo-tooling
|
|
21
|
+
|
|
22
|
+
## Steps
|
|
23
|
+
|
|
24
|
+
1. **Pick the toolchain by ecosystem — never run a linter *and* a formatter that fight over the same rules.** The linter checks logic/correctness rules; the formatter owns whitespace/quotes/commas. Never leave ESLint stylistic/formatting rules on alongside Prettier.
|
|
25
|
+
|
|
26
|
+
| Stack | Default | Why |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| JS/TS, want speed + one tool | **Biome** (`biome lint` + `biome format`) | One Rust binary, no plugin graph, ~10–100x faster, lint+format+import-sort in one config |
|
|
29
|
+
| JS/TS, need plugin ecosystem (React, a11y, import, custom) | **ESLint flat config (`eslint.config.js`) + Prettier** | Plugin coverage Biome lacks; Prettier owns formatting so disable ESLint stylistic rules |
|
|
30
|
+
| Python | **ruff** (`ruff check`) **+ `ruff format`** | Replaces flake8+isort+black+pyupgrade in one tool; `ruff format` is black-compatible |
|
|
31
|
+
|
|
32
|
+
Default to **Biome** for greenfield JS/TS; switch to **ESLint flat + Prettier** only when a required plugin (e.g. `eslint-plugin-jsx-a11y`, `eslint-plugin-import`) has no Biome equivalent; **ruff + ruff format** for Python. Don't reach for ESLint legacy `.eslintrc` — flat config is the only supported format on ESLint v9+.
|
|
33
|
+
|
|
34
|
+
2. **Write minimal config that extends a shared base — don't hand-roll a 200-rule file.** Turn on the recommended preset, override only the handful you actually disagree with.
|
|
35
|
+
|
|
36
|
+
Biome (`biome.json`):
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
40
|
+
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
|
|
41
|
+
"linter": { "enabled": true, "rules": { "recommended": true } },
|
|
42
|
+
"formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 },
|
|
43
|
+
"organizeImports": { "enabled": true }
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
ESLint flat (`eslint.config.js`) — keep Prettier as the formatter, use `eslint-config-prettier` to switch off every conflicting ESLint rule:
|
|
47
|
+
```js
|
|
48
|
+
import js from "@eslint/js";
|
|
49
|
+
import prettier from "eslint-config-prettier";
|
|
50
|
+
export default [
|
|
51
|
+
js.configs.recommended,
|
|
52
|
+
prettier, // MUST be last — disables all stylistic rules so Prettier wins
|
|
53
|
+
{ ignores: ["dist/", "build/", "coverage/", "node_modules/"] },
|
|
54
|
+
];
|
|
55
|
+
```
|
|
56
|
+
ruff (`pyproject.toml`):
|
|
57
|
+
```toml
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 100
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "I", "UP", "B"] # pycodestyle, pyflakes, isort, pyupgrade, bugbear
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
3. **Add `.editorconfig` + format-on-save so the editor stops introducing churn at the source.** One `.editorconfig` at the repo root makes every editor agree on charset/EOL/indent before a linter ever runs:
|
|
65
|
+
```ini
|
|
66
|
+
root = true
|
|
67
|
+
[*]
|
|
68
|
+
charset = utf-8
|
|
69
|
+
end_of_line = lf
|
|
70
|
+
insert_final_newline = true
|
|
71
|
+
trim_trailing_whitespace = true
|
|
72
|
+
indent_style = space
|
|
73
|
+
indent_size = 2
|
|
74
|
+
```
|
|
75
|
+
Commit `.vscode/settings.json` with `"editor.formatOnSave": true` and `"editor.defaultFormatter"` set to the chosen tool (`biomejs.biome`, `esbenp.prettier-vscode`, or `charliermarsh.ruff`) so the gate rarely fires in the first place.
|
|
76
|
+
|
|
77
|
+
4. **Wire the pre-commit hook to run on *changed files only* — keep it under a few seconds or people will `--no-verify`.** For JS/TS use **husky + lint-staged** (lints/formats only staged files); for Python or polyglot repos use the **pre-commit framework**.
|
|
78
|
+
|
|
79
|
+
husky + lint-staged:
|
|
80
|
+
```bash
|
|
81
|
+
npm i -D husky lint-staged && npx husky init
|
|
82
|
+
printf 'npx lint-staged\n' > .husky/pre-commit
|
|
83
|
+
```
|
|
84
|
+
`package.json` — lint-staged runs on the staged paths and re-stages what it fixes:
|
|
85
|
+
```json
|
|
86
|
+
"lint-staged": {
|
|
87
|
+
"*.{js,ts,jsx,tsx}": ["biome check --write --no-errors-on-unmatched"],
|
|
88
|
+
"*.{json,css,md}": ["biome format --write --no-errors-on-unmatched"]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
pre-commit framework (`.pre-commit-config.yaml`) — pin hook revs, then `pre-commit install`:
|
|
92
|
+
```yaml
|
|
93
|
+
repos:
|
|
94
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
95
|
+
rev: v0.6.9
|
|
96
|
+
hooks:
|
|
97
|
+
- id: ruff # lint
|
|
98
|
+
args: [--fix]
|
|
99
|
+
- id: ruff-format
|
|
100
|
+
```
|
|
101
|
+
The hook may auto-fix locally; CI must not (step 5). Never run a full-repo lint in the hook — staged-only is what keeps it fast.
|
|
102
|
+
|
|
103
|
+
5. **Make CI the real gate: check, never fix.** The hook is bypassable (`--no-verify`, unconfigured machines, IDE off); CI is the wall. Run lint with **zero-tolerance** and format in **check** mode so CI fails on drift instead of silently rewriting it:
|
|
104
|
+
- Biome: `biome ci .` (lint + format check + import-order in one CI-tuned command)
|
|
105
|
+
- ESLint + Prettier: `eslint . --max-warnings=0` **and** `prettier --check .`
|
|
106
|
+
- ruff: `ruff check --output-format=github .` **and** `ruff format --check .`
|
|
107
|
+
|
|
108
|
+
`--max-warnings=0` makes warnings fail the build (otherwise they rot into hundreds). `--check`/`ci` exits non-zero on any unformatted file and prints the diff — **never** `--write`/`--fix` in CI, which would push autofixes nobody reviewed. Run on the same tool versions the hook uses (a lockfile + pinned hook revs), or CI and local disagree — coordinate with pin-toolchain-versions if the binaries float.
|
|
109
|
+
|
|
110
|
+
6. **Migrate a dirty repo in one isolated reformat commit, then hide it from blame.** Don't fold the format sweep into a feature PR — it buries the real change. Run the formatter across the whole tree once, commit alone, then add that commit to `.git-blame-ignore-revs` so `git blame` skips it:
|
|
111
|
+
```bash
|
|
112
|
+
biome format --write . || (prettier --write . ; ruff format .)
|
|
113
|
+
git commit -am "style: format entire repo (no behavior change)"
|
|
114
|
+
git rev-parse HEAD >> .git-blame-ignore-revs
|
|
115
|
+
git config blame.ignoreRevsFile .git-blame-ignore-revs # local; GitHub/GitLab honor the committed file
|
|
116
|
+
```
|
|
117
|
+
Sanity-check the sweep changed only formatting: `git show --stat` should be whitespace/quotes only. After this commit the CI `--check` gate passes on a clean tree, so every later PR is gated against an already-formatted baseline.
|
|
118
|
+
|
|
119
|
+
## Common Errors
|
|
120
|
+
|
|
121
|
+
- **Linter and formatter fighting.** ESLint stylistic rules (or `airbnb` quote/semi rules) vs Prettier produce an infinite "fixed by one, broken by the other" loop. Add `eslint-config-prettier` **last** in the flat config to disable every conflicting rule; let the formatter own formatting.
|
|
122
|
+
- **Hook lints the whole repo.** A pre-commit that runs `eslint .` takes 30s+ and gets `--no-verify`'d into uselessness. Use lint-staged / pre-commit's built-in file filtering so it only touches staged paths.
|
|
123
|
+
- **CI auto-fixes instead of checking.** `eslint --fix` / `prettier --write` / `ruff --fix` in CI either commits unreviewed changes or, on a read-only checkout, masks failures. CI must use `--check`/`ci`/`--max-warnings=0` and fail, not mutate.
|
|
124
|
+
- **Warnings allowed in CI.** Without `--max-warnings=0`, warnings accumulate into noise nobody reads. Treat warnings as errors in CI; downgrade a rule to `off` deliberately if you truly don't want it.
|
|
125
|
+
- **Format sweep mixed into a feature PR.** Reviewers can't see the real diff and `git blame` points everything at you. Reformat in its own commit and register it in `.git-blame-ignore-revs`.
|
|
126
|
+
- **No `.editorconfig` / format-on-save.** Editors keep reintroducing CRLF/tabs/trailing whitespace, so the hook fires on every commit. Fix it at the editor with a committed `.editorconfig` + `formatOnSave`.
|
|
127
|
+
- **Legacy `.eslintrc` with new ESLint.** ESLint v9 defaults to flat config; a leftover `.eslintrc.json` is silently ignored or errors. Migrate to `eslint.config.js`.
|
|
128
|
+
- **Unpinned hook/tool versions.** `pre-commit autoupdate` or a floating `biome`/`eslint` makes CI and local disagree and breaks reproducibly-later. Pin hook `rev`s and lock tool versions (a lockfile, or coordinate with pin-toolchain-versions).
|
|
129
|
+
- **Ignoring generated/vendored dirs not configured.** Linting `dist/`, `build/`, `coverage/`, `.next/`, migrations, or snapshots floods output and slows everything. Set ignores in config (and `useIgnoreFile`/`.eslintignore`-equivalent) so they're skipped everywhere — hook and CI alike.
|
|
130
|
+
|
|
131
|
+
## Verify
|
|
132
|
+
|
|
133
|
+
1. **Bad file is caught by the formatter check:** create a deliberately mangled file (wrong indent, double→single quotes, no final newline). `biome ci .` / `prettier --check .` / `ruff format --check .` exits non-zero and names that file.
|
|
134
|
+
2. **Bad file is caught by the linter:** add an unused import / `==` where a rule forbids it. `eslint . --max-warnings=0` / `biome lint .` / `ruff check .` exits non-zero.
|
|
135
|
+
3. **Hook blocks the commit:** `git add` the bad file and `git commit` — the commit is rejected (or the file is auto-fixed and you must re-stage), proving the hook runs on staged files.
|
|
136
|
+
4. **Hook is fast:** time a commit touching one file — pre-commit completes in a few seconds, not tens (proves it's staged-only, not whole-repo).
|
|
137
|
+
5. **CI gate fails on drift:** push the bad file (or run the CI command locally) → the lint/format job is red; fix it → green. Confirm CI uses `--check`/`--max-warnings=0`, never `--write`/`--fix`.
|
|
138
|
+
6. **Clean baseline:** on a freshly formatted tree, the full CI command exits 0 with no changes — the reformat commit landed and is in `.git-blame-ignore-revs` (`git blame` skips it).
|
|
139
|
+
|
|
140
|
+
Done = a deliberately bad file is rejected by **both** the local pre-commit hook (in seconds, staged-only) and the CI gate (lint `--max-warnings=0` + format `--check`, no auto-fix), the formatter and linter don't fight, and the existing tree is already clean behind a single blame-ignored reformat commit.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: setup-monorepo-tooling
|
|
3
|
+
description: Sets up and tunes a monorepo — pnpm-workspace layout, an acyclic internal package graph wired with the workspace: protocol, a cached task pipeline (Turborepo/Nx/Bazel) with dependsOn + inputs/outputs and remote cache, affected/changed-only CI runs, shared extended base configs, and Changesets versioning — so CI rebuilds only what changed.
|
|
4
|
+
when_to_use: Converting to or fixing a monorepo — CI rebuilds everything, circular package deps, drifting per-package tooling, duplicated shared code. NOT the CI workflow file itself (cicd-pipeline-author), one package's bundler/build config (configure-bundler-build), or the npm publish step (publish-package-registry).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reach for this skill when the problem is **repo-level orchestration and package boundaries**, not one package's build:
|
|
10
|
+
|
|
11
|
+
- "Our CI rebuilds/tests every package on every PR even when one file changed"
|
|
12
|
+
- "Convert this repo into a monorepo" / "split this app into packages"
|
|
13
|
+
- "We have a circular dependency between two internal packages"
|
|
14
|
+
- "Each package has its own drifting tsconfig/eslint — unify them"
|
|
15
|
+
- "Stop copy-pasting this util across three apps — make it a shared package"
|
|
16
|
+
- "Turbo/Nx cache never hits" or "the affected graph is wrong"
|
|
17
|
+
|
|
18
|
+
NOT this skill:
|
|
19
|
+
- Writing the GitHub Actions / GitLab CI workflow file itself → cicd-pipeline-author (this skill defines the `turbo run` command it calls, not the YAML)
|
|
20
|
+
- One package's bundler/output config (tsup/Vite/esbuild, dual ESM+CJS, externals) → configure-bundler-build
|
|
21
|
+
- Actually publishing a package to npm (auth, provenance, `npm publish`) → publish-package-registry (this skill sets up Changesets version PRs; that skill does the registry push)
|
|
22
|
+
- Repo-wide lint/format/pre-commit hook *content* → setup-lint-format-precommit
|
|
23
|
+
- Pinning the Node/pnpm/toolchain versions themselves → pin-toolchain-versions
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
1. **Pick the orchestrator by scale — default to pnpm workspaces + Turborepo.** Don't reach for Nx or Bazel unless a row below forces it.
|
|
28
|
+
|
|
29
|
+
| Tool | Use when | Cost / friction |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| **pnpm workspaces + Turborepo** | JS/TS monorepo, you want caching + affected runs with near-zero config — **the default** | Tiny `turbo.json`; no codegen, no plugins |
|
|
32
|
+
| Nx | You need code generators/scaffolding, an enforced module-boundary lint rule, or rich project-graph tooling | Heavier config, `nx.json` + project.json or inferred targets, more to learn |
|
|
33
|
+
| Bazel / Buck2 | Polyglot at scale (JS + Go + Java + protos), hermetic builds, thousands of targets | High: BUILD files everywhere, steep ramp — only worth it at large org scale |
|
|
34
|
+
| Lerna (alone) | — | Legacy; for new repos use pnpm + Turbo/Changesets instead |
|
|
35
|
+
|
|
36
|
+
Use **pnpm** as the package manager regardless (strict, fast, disk-efficient, first-class `workspace:` protocol). Commit `pnpm-lock.yaml`.
|
|
37
|
+
|
|
38
|
+
2. **Lay out the workspace and declare it.** `apps/*` = deployables (not published), `packages/*` = shared libs (publishable or internal). Root is private.
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
# pnpm-workspace.yaml
|
|
42
|
+
packages:
|
|
43
|
+
- "apps/*"
|
|
44
|
+
- "packages/*"
|
|
45
|
+
```
|
|
46
|
+
```jsonc
|
|
47
|
+
// package.json (root) — root is NOT published
|
|
48
|
+
{ "name": "@acme/root", "private": true, "packageManager": "pnpm@9.12.0" }
|
|
49
|
+
```
|
|
50
|
+
Name every internal package under one scope: `@acme/ui`, `@acme/config`, `@acme/api-client`. The scope makes ownership and the dependency graph legible at a glance.
|
|
51
|
+
|
|
52
|
+
3. **Wire internal deps with the `workspace:` protocol — never a version range.** In a consumer's `package.json`:
|
|
53
|
+
```jsonc
|
|
54
|
+
"dependencies": { "@acme/ui": "workspace:*" }
|
|
55
|
+
```
|
|
56
|
+
`workspace:*` symlinks the local source so changes are picked up instantly; at publish time Changesets/pnpm rewrites it to the real version. Run `pnpm install` from the root once — it links everything. A range like `"^1.0.0"` instead would silently pull the *registry* copy, defeating the monorepo.
|
|
57
|
+
|
|
58
|
+
4. **Keep the package graph acyclic and explicit.** Cycles break topological build order and caching. Enforce it, don't hope:
|
|
59
|
+
- **Direction:** `apps → packages → packages`. Apps depend on packages; packages never depend on apps. Leaf utils depend on nothing internal.
|
|
60
|
+
- Every cross-package import must correspond to a declared `dependency` in that package's `package.json` — no reaching into a sibling's `../other-pkg/src`. Set `eslint-plugin-import/no-relative-packages` (or Nx's `enforce-module-boundaries`) to ban it.
|
|
61
|
+
- Detect cycles in CI: `pnpm dlx madge --circular --extensions ts,tsx packages apps` must exit 0. Turbo also errors on a cyclic task graph.
|
|
62
|
+
|
|
63
|
+
5. **Define the task pipeline with dependsOn + inputs/outputs — this is what makes caching work.** A task's cache key = its declared `inputs` + its dependencies' outputs; get these wrong and you get false hits (stale) or zero hits (no speedup).
|
|
64
|
+
|
|
65
|
+
```jsonc
|
|
66
|
+
// turbo.json
|
|
67
|
+
{
|
|
68
|
+
"$schema": "https://turbo.build/schema.json",
|
|
69
|
+
"tasks": {
|
|
70
|
+
"build": {
|
|
71
|
+
"dependsOn": ["^build"], // build deps first (topo order)
|
|
72
|
+
"inputs": ["src/**", "tsconfig.json", "package.json"],
|
|
73
|
+
"outputs": ["dist/**"] // MUST list outputs or cache restores nothing
|
|
74
|
+
},
|
|
75
|
+
"test": { "dependsOn": ["build"], "outputs": ["coverage/**"] },
|
|
76
|
+
"lint": { "dependsOn": [] },
|
|
77
|
+
"dev": { "cache": false, "persistent": true } // never cache long-running dev servers
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
`^build` = "build all internal dependencies first"; `build` (no caret) = "this package's own build". Add `"globalDependencies": ["tsconfig.base.json", ".env"]` so a base-config change busts every cache. Anything generated must appear in `outputs` or the cache restore is empty and reruns do real work.
|
|
82
|
+
|
|
83
|
+
6. **Turn on remote cache so CI and teammates share results.** Local cache only helps the same machine. `npx turbo login && npx turbo link` (Vercel Remote Cache) or self-host with a `TURBO_TOKEN` + `TURBO_API` env in CI. Now a build artifact produced on one PR/runner is reused everywhere — the single biggest CI-time win.
|
|
84
|
+
|
|
85
|
+
7. **Run affected/changed-only in CI.** Replace "build everything" with a graph-filtered command so untouched packages are skipped (cache hit) or never scheduled:
|
|
86
|
+
- **Turbo:** `turbo run build test lint --filter='...[origin/main]'` — runs only packages changed since `main` plus everything that depends on them.
|
|
87
|
+
- **Nx:** `nx affected -t build test lint --base=origin/main`.
|
|
88
|
+
- Require `fetch-depth: 0` (full history) in CI checkout or the merge-base is wrong and the filter degrades to "run everything." This is the most common reason affected runs silently rebuild all.
|
|
89
|
+
|
|
90
|
+
8. **Share one base config; extend per package — don't copy.** A single source of truth in a `@acme/config` package, extended by every other package:
|
|
91
|
+
```jsonc
|
|
92
|
+
// tsconfig.base.json (root) → strict, composite for project refs
|
|
93
|
+
{ "compilerOptions": { "strict": true, "composite": true, "declaration": true } }
|
|
94
|
+
// packages/ui/tsconfig.json
|
|
95
|
+
{ "extends": "../../tsconfig.base.json", "include": ["src"], "compilerOptions": { "outDir": "dist" } }
|
|
96
|
+
```
|
|
97
|
+
Same pattern for ESLint flat config and Prettier: define once in `@acme/config`, `import`/`extends` it everywhere. Per-package files hold only the genuine deltas (paths, env). (The lint/format/hook *content* itself → setup-lint-format-precommit.)
|
|
98
|
+
|
|
99
|
+
9. **Add Changesets for versioning/release intent.** `pnpm add -Dw @changesets/cli && pnpm changeset init`. Workflow: contributor runs `pnpm changeset` (records which packages changed + semver bump + a user-facing note); CI runs `changeset version` to bump versions, rewrite `workspace:*` → real versions, and update CHANGELOGs, opening a "Version Packages" PR. Set `"linked"`/`"fixed"` groups in `.changeset/config.json` only if packages must move in lockstep. The actual `npm publish` after merge is publish-package-registry's job.
|
|
100
|
+
|
|
101
|
+
## Common Errors
|
|
102
|
+
|
|
103
|
+
- **Missing `outputs` in `turbo.json`.** Cache key matches → "cache hit" → but `dist/` is never restored, so downstream tasks see no artifacts and fail or rebuild. Every task that emits files must declare `outputs`.
|
|
104
|
+
- **Shallow CI checkout (`fetch-depth: 1`).** `--filter='...[origin/main]'` / `nx affected --base` can't compute a merge-base and falls back to running everything. Use `fetch-depth: 0`.
|
|
105
|
+
- **Version range instead of `workspace:*` for an internal dep.** Pulls the published registry copy instead of local source; edits don't propagate and you debug a stale version. Always `workspace:*` (or `workspace:^`) for internal deps.
|
|
106
|
+
- **No `dependsOn: ["^build"]`.** Packages build in arbitrary order and a consumer compiles against a missing/old `dist/` of its dependency — flaky "module not found" that vanishes on rerun (warm cache). Declare the topo dependency.
|
|
107
|
+
- **Caching `dev`/`watch`/`start`.** Persistent tasks have no terminal output to cache; mark them `"cache": false, "persistent": true` or Turbo waits forever / serves stale.
|
|
108
|
+
- **Circular package dependency.** `@acme/a` ↔ `@acme/b` makes topological ordering impossible and silently corrupts incremental builds. Break it by extracting the shared piece into a third leaf package. Gate with `madge --circular` in CI.
|
|
109
|
+
- **Reaching into a sibling via relative path** (`import x from "../../other-pkg/src/util"`). Bypasses the declared graph, so affected-detection and caching miss the edge. Import via the package name and add the `dependency`; ban relative cross-package imports with lint.
|
|
110
|
+
- **Hoisting/phantom deps.** A package uses something it never declared but that happens to be hoisted to the root `node_modules`. Works locally, breaks when published or with stricter installs. pnpm's strict layout surfaces these — fix by adding the real `dependency`; don't reach for `shamefully-hoist`.
|
|
111
|
+
- **Base config changes that don't bust the cache.** Edit `tsconfig.base.json`, rerun, get stale cached builds. List shared base files in Turbo `globalDependencies` (or each task's `inputs`).
|
|
112
|
+
- **A `changeset` not added with a PR.** The release PR then ships no version bump / empty changelog for that change. Add a CI check that PRs touching `packages/**` include a `.changeset/*.md` (or are explicitly marked no-release).
|
|
113
|
+
|
|
114
|
+
## Verify
|
|
115
|
+
|
|
116
|
+
1. **Cache hit on rerun (the core proof):** `turbo run build` once (cold), then **again with no changes** → second run reports `FULL TURBO` / every task `cache hit, replaying logs` and finishes in ~seconds. If it rebuilds, `outputs`/`inputs` are wrong.
|
|
117
|
+
2. **Affected graph is correct:** edit one file in a single leaf package, then `turbo run build --filter='...[origin/main]' --dry=json` (or `nx affected:graph`) → the task list includes that package **and only its dependents**, not the whole repo.
|
|
118
|
+
3. **Targeted invalidation:** change a file in `@acme/ui` → on next run `@acme/ui` and its consumers rebuild while unrelated packages stay cache-hit. Change `tsconfig.base.json` → **everything** rebuilds (global dep busted).
|
|
119
|
+
4. **Acyclic graph:** `pnpm dlx madge --circular --extensions ts,tsx packages apps` exits 0; `turbo run build` reports no cyclic-dependency error.
|
|
120
|
+
5. **Internal linking real:** `pnpm why @acme/ui` (from a consumer) resolves to the local workspace path, not a registry version; grep confirms `workspace:` on every internal dep.
|
|
121
|
+
6. **Boundaries enforced:** a relative cross-package import (`../other-pkg/src/...`) fails lint; an undeclared import fails install/typecheck under pnpm's strict layout.
|
|
122
|
+
7. **Remote cache shared:** run `turbo run build` on a clean checkout / second machine (or fresh CI runner) with the remote cache configured → cache hits sourced remotely, no local rebuild.
|
|
123
|
+
8. **Release intent works:** `pnpm changeset` then `pnpm changeset version` bumps only the listed packages, rewrites their `workspace:*` to concrete versions, and updates each CHANGELOG.
|
|
124
|
+
|
|
125
|
+
Done = a no-op rerun is `FULL TURBO` (cache hit), the affected filter rebuilds exactly the changed packages plus their dependents (not the repo), the package graph is acyclic with all internal deps on `workspace:` and enforced boundaries, base configs are extended (not copied), and the remote cache is shared across machines/CI.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ship-mobile-app-store-release
|
|
3
|
+
description: Prepares and ships iOS App Store and Google Play releases — code signing (certs/provisioning, upload vs app-signing keystores), marketing-version/build-number bumps, Fastlane/EAS/Gradle build lanes, TestFlight/Play-track uploads, phased rollout, store metadata, and review-rejection remediation.
|
|
4
|
+
when_to_use: Cutting a mobile release — fixing signing or keystores, automating builds with Fastlane/EAS/Gradle, uploading to TestFlight or Play tracks, staged/phased rollout, store listing/metadata, or fixing an App Store/Play review rejection. Distinct from deploy-release (server/web deploys), release-notes (changelog prose), and cicd-pipeline-author (the CI workflow that calls these lanes).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reach for this skill when the artifact is a **store binary** (`.ipa`/`.aab`) headed for Apple or Google review, not a server rollout:
|
|
10
|
+
|
|
11
|
+
- "Set up code signing / fix `No profiles for 'com.x' were found` / rotate an expired cert"
|
|
12
|
+
- "Generate or recover an Android keystore; enroll in Play App Signing; upload key vs app-signing key confusion"
|
|
13
|
+
- "Bump the version + build number and ship to TestFlight / Play internal track"
|
|
14
|
+
- "Wire up Fastlane (`gym`/`pilot`/`supply`) or EAS (`eas build`/`eas submit`) in CI"
|
|
15
|
+
- "Do a phased / staged rollout at 1% → 100%, then halt it"
|
|
16
|
+
- "Upload screenshots, description, what's-new, age rating"
|
|
17
|
+
- "Our build got rejected for Guideline 4.3 / 5.1.1 / IAP — fix and resubmit"
|
|
18
|
+
|
|
19
|
+
NOT this skill:
|
|
20
|
+
- Deploying a server, web app, or container to prod (blue-green, canary pods) → deploy-release
|
|
21
|
+
- Writing the human-facing changelog / what's-new *prose* → release-notes (this skill only *places* the text)
|
|
22
|
+
- Authoring the CI YAML that runs these lanes (job graph, caching, secrets injection) → cicd-pipeline-author
|
|
23
|
+
- Storing the keystore password / App Store Connect API key safely → secrets-management
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
1. **Set versioning first — two independent numbers, never reused.** Marketing version (user-visible) vs build number (uniqueness). Bump the build number on *every* upload even within the same marketing version; stores reject duplicates.
|
|
28
|
+
|
|
29
|
+
| Field | iOS (`Info.plist`) | Android (`build.gradle`) | Rule |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| Marketing version | `CFBundleShortVersionString` | `versionName` | SemVer `1.4.0`, free-form |
|
|
32
|
+
| Build number | `CFBundleVersion` | `versionCode` | **Must strictly increase per upload.** iOS: any string with increasing numerics. Android: a single **integer**, monotonic, max 2.1B |
|
|
33
|
+
|
|
34
|
+
Default: derive build number from CI run number or commit count (`git rev-list --count HEAD`) so it auto-increments and is reproducible. Use Fastlane `increment_build_number` / `increment_version_code` — never hand-edit and forget.
|
|
35
|
+
|
|
36
|
+
2. **iOS signing — prefer Fastlane `match`, not Xcode "Automatically manage signing", for CI.** You need a **Distribution certificate** (`.p12`) + an **App Store provisioning profile** bound to the explicit App ID and that cert.
|
|
37
|
+
|
|
38
|
+
- `match` stores certs/profiles encrypted in a private git repo (or S3/GCS), so every machine/CI agent shares one cert instead of each minting a new one (Apple caps you at 2–3 distribution certs).
|
|
39
|
+
- Authenticate CI with an **App Store Connect API key** (`.p8` + key id + issuer id) via `app_store_connect_api_key` — not your Apple ID password / 2FA, which breaks unattended.
|
|
40
|
+
- Match entitlements to enabled capabilities: Push → `aps-environment: production` in the **release** entitlements; App Groups, Sign in with Apple, Associated Domains must each be toggled on the App ID *and* present in the profile, or the build is signed but the feature silently fails.
|
|
41
|
+
```bash
|
|
42
|
+
# one-time, populates the encrypted repo
|
|
43
|
+
bundle exec fastlane match appstore
|
|
44
|
+
# CI: fetch read-only, never regenerate on agents
|
|
45
|
+
bundle exec fastlane match appstore --readonly
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
3. **Android signing — separate the upload key from the app-signing key.** Enroll in **Play App Signing**: Google holds the *app-signing key* and re-signs your `.aab`; you sign uploads with an *upload key* you control. Losing the upload key is recoverable (reset via support); losing the app-signing key without Play App Signing is fatal — you can never update the app.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
keytool -genkeypair -v -keystore upload.keystore -alias upload \
|
|
52
|
+
-keyalg RSA -keysize 2048 -validity 9125 -storetype PKCS12 # 25-yr validity
|
|
53
|
+
```
|
|
54
|
+
- Ship **`.aab`** (App Bundle), not `.apk` — Play requires it for new apps and generates per-device APKs.
|
|
55
|
+
- Keystore password, key password, alias → secrets store / env, never committed (see secrets-management). Gradle reads them from `~/.gradle/gradle.properties` or env, not `build.gradle`.
|
|
56
|
+
|
|
57
|
+
4. **Automate the build + upload with one lane per store.** Decide the toolchain up front:
|
|
58
|
+
|
|
59
|
+
| Stack | Build | Upload | Use when |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| Native iOS | `gym` (`build_app`) → `.ipa` | `pilot` (`upload_to_testflight`) / `deliver` (`upload_to_app_store`) | Bare Xcode project, full control |
|
|
62
|
+
| Native Android | `gradle bundleRelease` → `.aab` | `supply` (`upload_to_play_store`) | Bare Gradle project |
|
|
63
|
+
| Expo / RN managed | `eas build -p ios\|android` | `eas submit -p ios\|android` | Expo-managed; EAS handles signing |
|
|
64
|
+
|
|
65
|
+
Default to **Fastlane** for bare native and **EAS** for Expo-managed. Minimal Fastfile lanes:
|
|
66
|
+
```ruby
|
|
67
|
+
lane :beta do # iOS → TestFlight
|
|
68
|
+
match(type: "appstore", readonly: true)
|
|
69
|
+
increment_build_number(xcodeproj: "App.xcodeproj")
|
|
70
|
+
build_app(scheme: "App", export_method: "app-store")
|
|
71
|
+
upload_to_testflight(skip_waiting_for_build_processing: true)
|
|
72
|
+
end
|
|
73
|
+
lane :play_internal do # Android → internal track
|
|
74
|
+
gradle(task: "bundle", build_type: "Release")
|
|
75
|
+
upload_to_play_store(track: "internal", aab: "app/build/outputs/bundle/release/app-release.aab")
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
For `supply` you need a **Play Developer API service-account JSON** (`json_key`) with Release Manager permission. For EAS submit, store the same Apple/Google creds in EAS secrets.
|
|
79
|
+
|
|
80
|
+
5. **Distribute through the right track, then promote — don't ship straight to production.**
|
|
81
|
+
|
|
82
|
+
| Store | Tracks (narrow → wide) | Notes |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| TestFlight | Internal (≤100, no review) → External (review, up to 10k via groups/public link) | Internal testers see builds in minutes; external needs Beta App Review |
|
|
85
|
+
| Play | `internal` → `closed` (alpha/beta) → `open` (beta) → `production` | Promote the *same* build between tracks; don't rebuild |
|
|
86
|
+
|
|
87
|
+
Default flow: upload to TestFlight Internal / Play `internal` first, smoke-test, then promote. Gate external/production behind a manual approval.
|
|
88
|
+
|
|
89
|
+
6. **Roll out in stages with a percentage, never 100% on day one.**
|
|
90
|
+
- **Play:** set `rollout: 0.01` (1%) on the `production` track via `upload_to_play_store(rollout: "0.01")`, then bump `0.01 → 0.05 → 0.2 → 0.5 → 1.0` over days, watching crash-free rate between steps.
|
|
91
|
+
- **iOS:** App Store "Phased Release for Automatic Updates" ramps over 7 days automatically (≈1/2/5/10/20/50/100%). Enable it in App Store Connect or via `deliver`'s phased-release flag; it only covers auto-updaters, manual updaters get it immediately.
|
|
92
|
+
|
|
93
|
+
7. **Fill store metadata so review doesn't bounce on format.** Required: localized title/description, **what's-new** for this version, keywords (iOS), category, **age/content rating** questionnaire, privacy nutrition label (iOS `App Privacy`) / Play **Data safety** form, and **screenshots at every required device size** (e.g. iOS 6.7" + 6.5"; missing a required size blocks submission). Automate text/screenshot upload with `deliver`/`supply` (`metadata/`, `screenshots/` dirs) so it's version-controlled, not hand-pasted.
|
|
94
|
+
|
|
95
|
+
8. **Pre-flight against the top rejection reasons before you submit.** Address these in the binary/listing, not after a 1-week review round-trip:
|
|
96
|
+
|
|
97
|
+
| Reason | Apple guideline | Fix |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| Crash / broken on review device | 2.1 | Test on a clean device + the OS in review; provide working demo creds |
|
|
100
|
+
| Hidden/incomplete features ("placeholder") | 2.1 | No dead buttons, no "coming soon"; ship only finished flows |
|
|
101
|
+
| Spam / thin / web-wrapper | 4.3 / 4.2 | Native value beyond a website wrapper |
|
|
102
|
+
| Login wall with no demo account | 5.1.1 / 2.1 | Put **demo username+password** in Review Notes |
|
|
103
|
+
| Sign in with Apple missing | 4.8 | Required if you offer 3rd-party social login |
|
|
104
|
+
| Buying digital goods outside IAP | 3.1.1 | Digital content must use StoreKit IAP, not external payment |
|
|
105
|
+
| Privacy label mismatch | 5.1 | Declared data collection must match actual SDK behavior |
|
|
106
|
+
|
|
107
|
+
Play parallels: target the **current required API level** (`targetSdkVersion`), complete Data safety honestly, declare sensitive permissions, no deceptive metadata. Put credentials and any special-access steps in the review notes field.
|
|
108
|
+
|
|
109
|
+
9. **After release, watch crash-free rate and keep the halt path one command away.** Monitor crash-free *sessions* (target **≥ 99.5%**) and ANR rate in Crashlytics / Play vitals during ramp. If it regresses below threshold: **Play** → halt rollout in Console or `upload_to_play_store(rollout:)` won't un-ship, so use **"Halt rollout"** (stops further % but doesn't pull installed users) then ship a fixed build. **iOS** → "Pause Phased Release" in App Store Connect; to actually pull a broken build you must **expedite a new build through review** (Apple has no instant rollback). Plan a hotfix lane, not a rollback button.
|
|
110
|
+
|
|
111
|
+
## Common Errors
|
|
112
|
+
|
|
113
|
+
- **Reusing a build number.** `ERROR ITMS-90186 / "Version already exists"` (iOS) or `Version code N has already been used` (Play). Always increment per upload, even for the same marketing version.
|
|
114
|
+
- **Each CI agent minting its own distribution cert.** Hits Apple's 2–3 cert cap, then nothing can sign. Use `match --readonly` so agents fetch one shared cert.
|
|
115
|
+
- **Apple ID + password (not API key) in CI.** Breaks on 2FA prompts. Use an App Store Connect API key (`.p8`).
|
|
116
|
+
- **Committing the keystore or its passwords.** Anyone with the repo can sign as you. Keystore + passwords go to a secrets store; `.gradle` properties out of VCS.
|
|
117
|
+
- **Treating the upload key as the app-signing key.** With Play App Signing, Google re-signs; the key in your keystore is only the *upload* key. Don't panic-rotate the wrong one — resetting the upload key is a support flow, not a config change.
|
|
118
|
+
- **Shipping `.apk` to a new Play app.** Rejected — new apps require `.aab`. Use `bundleRelease`, not `assembleRelease`.
|
|
119
|
+
- **Entitlement enabled in Xcode but not on the App ID / profile.** Signs fine, feature dead at runtime (push silently drops, App Group reads empty). Toggle the capability on the App ID and regenerate the profile.
|
|
120
|
+
- **`aps-environment: development` in a store build.** Push notifications work in debug, fail in production. Release entitlements must use `production`.
|
|
121
|
+
- **Missing a required screenshot size.** Submission blocked with no obvious cause. Supply every required device dimension for the platform.
|
|
122
|
+
- **100% rollout on day one.** A crash hits every user at once with no staged escape. Start at 1% and ramp.
|
|
123
|
+
- **Expecting an instant rollback.** Neither store has one. iOS needs an expedited re-review; Play halt only stops *new* installs. Always keep a hotfix lane ready.
|
|
124
|
+
- **Login-walled app with no demo credentials.** Auto-reject under 5.1.1/2.1. Reviewer can't get past the login. Put working creds in Review Notes.
|
|
125
|
+
|
|
126
|
+
## Verify
|
|
127
|
+
|
|
128
|
+
1. **Versioning:** the build number is strictly greater than the last accepted upload (check App Store Connect / Play Console history); marketing version is correct.
|
|
129
|
+
2. **iOS signing:** `codesign -dvv <App>.app` shows the **Distribution** cert and the explicit App ID; the embedded profile is the App Store profile and not expired (`security cms -D -i embedded.mobileprovision`).
|
|
130
|
+
3. **Android signing:** `jarsigner -verify -verbose app-release.aab` (or `apksigner verify` on a built APK) passes with the **upload** key; the app is enrolled in Play App Signing.
|
|
131
|
+
4. **Entitlements:** `codesign -d --entitlements - <App>.app` lists every capability the app uses, with `aps-environment: production` for a store build.
|
|
132
|
+
5. **Upload landed:** the build appears in TestFlight Internal / the target Play track and finishes processing without an email rejection (ITMS-* / Play pre-launch report clean).
|
|
133
|
+
6. **Metadata complete:** what's-new, all required screenshot sizes, age/content rating, and the privacy/Data-safety form are filled — the submit button is enabled with no blocking warnings.
|
|
134
|
+
7. **Rollout staged:** production is at the intended start percentage (e.g. 1%), not 100%, and the phased/staged toggle is on.
|
|
135
|
+
8. **Post-release watch:** crash-free sessions and ANR are visible on a dashboard, and you've confirmed the halt/pause control works (locate it, don't trigger it).
|
|
136
|
+
|
|
137
|
+
Done = a binary with a unique, increasing build number is correctly signed (distribution cert + production entitlements / upload key + Play App Signing), uploaded to the intended track with complete metadata, rolling out at a staged percentage, and the crash-free monitor + halt path are both confirmed available.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: structured-output-llm
|
|
3
|
+
description: Gets machine-parseable JSON out of an LLM reliably — prefer provider-enforced grammar (OpenAI `response_format:{type:"json_schema",strict:true}`, Anthropic tool-calling / `tool_choice:{type:"tool"}`, Gemini `responseSchema`) over a "respond in JSON" prompt, define the contract once as a Pydantic/Zod model and validate every response (never trust the string), use constrained decoding / grammars for open weights (Outlines, llguidance, llama.cpp GBNF, vLLM `guided_json`), keep schemas SHALLOW with required fields + enums + `additionalProperties:false`, and build a repair-and-retry loop that handles refusals, `length`/`max_tokens` truncation, and streaming partial JSON (`partial-json-parser`). Schema-guided generation eliminates parse-error retries; validate-and-repair is the backstop, not the primary mechanism.
|
|
4
|
+
when_to_use: You need a model to emit JSON/objects a program consumes — extraction, classification into fixed labels, function arguments, form-filling, or feeding output into another service — and string-parsing or regex on free text keeps breaking. Distinct from prompt-engineering (crafts the instruction/few-shot prose; this skill enforces the output SHAPE via grammar+validation) and agent-tool-mcp-builder (defines the tools/MCP an agent decides to call; this skill is the one-shot "make THIS call return a typed object" half, even when implemented via a single forced tool).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reach for this skill when a program — not a human — consumes the model's output and the shape must be guaranteed:
|
|
10
|
+
|
|
11
|
+
- "Extract these 8 fields from this invoice/email as JSON I can `JSON.parse`"
|
|
12
|
+
- "Classify into one of {refund, dispute, question} — give me just the label"
|
|
13
|
+
- "The model returns ```json fences / prose / trailing commas and my parse breaks ~3% of the time"
|
|
14
|
+
- "Function arguments come back malformed or with hallucinated keys"
|
|
15
|
+
- "Fill this form schema" / "map this text to my DB row"
|
|
16
|
+
- "Open-weights model (Llama/Mistral/Qwen) won't reliably produce valid JSON"
|
|
17
|
+
- "Long extraction gets cut off mid-object" or "streaming JSON is unparseable until complete"
|
|
18
|
+
|
|
19
|
+
NOT this skill:
|
|
20
|
+
- Writing the *instruction*, few-shot examples, role/system prompt, or chain-of-thought prose → prompt-engineering (it shapes WHAT to say; this skill enforces the machine SHAPE of the reply)
|
|
21
|
+
- Defining tools/an MCP server an agent autonomously selects among across many turns → agent-tool-mcp-builder (this skill is one forced, schema-constrained call returning a typed object)
|
|
22
|
+
- Reliability of the whole LLM app — timeouts, fallbacks, circuit-breakers, idempotency around the call → harden-llm-app-reliability (this skill owns only the output-shape contract)
|
|
23
|
+
- Token/latency/cost tuning, caching, model selection → optimize-llm-cost-latency (note: strict schemas cost a cache miss on first use — that skill weighs it)
|
|
24
|
+
- Stopping a malicious payload in retrieved/user text from hijacking the model → defend-llm-prompt-injection (orthogonal; a valid-schema output can still be adversarial content)
|
|
25
|
+
- Retrieval/chunking/grounding that feeds the prompt → rag-pipeline (this skill structures rag's *output* step)
|
|
26
|
+
- Scoring whether the extracted values are *correct* across a dataset → llm-eval-harness (this skill guarantees parseable; that one measures accuracy)
|
|
27
|
+
- Validating ordinary user-submitted form data (no LLM) → build-form-validation
|
|
28
|
+
- The wire/serialization contract between your own services → rest-graphql-contract
|
|
29
|
+
- Scraping the HTML/DOM the text came from → scrape-structured-web-data
|
|
30
|
+
|
|
31
|
+
## Steps
|
|
32
|
+
|
|
33
|
+
1. **Prefer provider-enforced schema over a "respond in JSON" prompt — it's a different mechanism, not a stronger hint.** A prompt is a request the model may ignore (fences, preamble, extra keys); enforced schema masks the decoder's token logits so only schema-valid tokens are emittable — invalid JSON becomes *impossible*, not just unlikely. Use the strongest enforcement the provider offers:
|
|
34
|
+
|
|
35
|
+
| Provider | Enforcement | How |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| **OpenAI** (gpt-4o+, gpt-4.1, o-series) | Structured Outputs, 100% guaranteed | `response_format:{type:"json_schema",json_schema:{name,schema,strict:true}}` — or `strict:true` on a function tool |
|
|
38
|
+
| **Anthropic Claude** | Tool-calling (no native json_schema) | define one tool with `input_schema`, set `tool_choice:{type:"tool",name:"..."}` to force it; read `tool_use.input` |
|
|
39
|
+
| **Google Gemini** | Controlled generation | `generationConfig:{responseMimeType:"application/json",responseSchema:{...}}` (also supports `responseSchema` enums) |
|
|
40
|
+
| **Azure OpenAI** | same as OpenAI | `response_format` json_schema on `2024-08-01-preview`+ |
|
|
41
|
+
| **Open weights** (vLLM/TGI/llama.cpp/Ollama) | Constrained decoding (step 5) | `guided_json` / GBNF grammar / Outlines |
|
|
42
|
+
|
|
43
|
+
`json_object` mode (the older `{type:"json_object"}`) only guarantees *syntactically* valid JSON, NOT your schema — treat it as a weak fallback, never the goal.
|
|
44
|
+
|
|
45
|
+
2. **Define the contract ONCE as a typed model; let the SDK emit the schema and parse the result.** Don't hand-write JSON Schema and a parser separately — they drift. Use the model class as the single source of truth:
|
|
46
|
+
- **Python** → Pydantic v2. OpenAI: `client.beta.chat.completions.parse(..., response_format=MyModel)` → `.choices[0].message.parsed` is a typed instance (or `.refusal`). Or `MyModel.model_validate_json(raw)`.
|
|
47
|
+
- **TS/JS** → Zod + the OpenAI helper `zodResponseFormat(MySchema, "name")`, then `completion.choices[0].message.parsed`; or `MySchema.parse(JSON.parse(raw))`. (Instructor / `instructor-js` wrap this with retries.)
|
|
48
|
+
- **Anthropic** → derive `input_schema` from Pydantic via `Model.model_json_schema()` and pass as the tool's input_schema; validate `tool_use.input` back through the model.
|
|
49
|
+
The rule: **validate every response through the typed model even when the provider guarantees the schema** — guarantees cover JSON-schema-expressible constraints, not your business invariants (date ranges, cross-field rules, enum-of-enums), and self-hosted/fallback paths have no guarantee at all.
|
|
50
|
+
|
|
51
|
+
3. **Keep the schema SHALLOW and tight — depth and looseness are where models fail and where strict mode rejects you.** Constraints that materially improve reliability:
|
|
52
|
+
- **`required` everything + `additionalProperties:false`.** OpenAI strict mode *requires* every property be in `required` and forbids additional props — model "optional" as `required` + nullable union (`{"type":["string","null"]}`). This also blocks hallucinated keys.
|
|
53
|
+
- **Enums over free strings for categories** (`"type":"string","enum":["refund","dispute","question"]`) — turns classification into a closed set the decoder can't escape.
|
|
54
|
+
- **Flatten.** Prefer a flat object or a top-level `{"items":[...]}` array wrapper over 4-level nesting. Deep/recursive schemas raise latency, hit provider depth/property caps (OpenAI: ≤5000 props, ≤5 nesting for strict), and degrade accuracy.
|
|
55
|
+
- **Avoid where unsupported:** OpenAI strict mode disallows `minLength`/`maximum`/`pattern`/`format` and many keywords — enforce those in step 2's validator, not the wire schema. (Anthropic/Gemini tolerate more but don't *guarantee* them.)
|
|
56
|
+
- **Order fields so reasoning precedes the answer:** put a `reasoning`/`evidence` string field *before* the `answer`/`label` field — the model generates them in order, so it "thinks" before committing (cheap, schema-native CoT).
|
|
57
|
+
|
|
58
|
+
4. **Don't ask for JSON and reasoning in the same free-text turn.** If you need chain-of-thought, either (a) put it inside the schema as a leading field (step 3), or (b) do a two-call split: one reasoning call (free text), one extraction call (strict schema) over the reasoning. Mixing "think step by step, then output JSON" in one un-enforced response is the classic source of prose-before-the-brace parse failures.
|
|
59
|
+
|
|
60
|
+
5. **For open-weights / self-hosted, use constrained decoding (grammar-level), not prompting.** These enforce validity at the token sampler:
|
|
61
|
+
|
|
62
|
+
| Tool | Use | Invoke |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| **vLLM** | production serving | `guided_json=<schema>` / `guided_choice=[...]` / `guided_regex` in `extra_body` (backed by xgrammar/outlines) |
|
|
65
|
+
| **Outlines** | library, any HF model | `outlines.generate.json(model, PydanticModel)` → returns typed object |
|
|
66
|
+
| **llguidance / xgrammar** | fast grammar engines | embedded in vLLM/TGI; sub-ms per-token masking |
|
|
67
|
+
| **llama.cpp / Ollama** | local GGUF | GBNF grammar file, or `format:"json"`+schema (Ollama) |
|
|
68
|
+
| **TGI** | HF Inference | `grammar:{type:"json",value:<schema>}` |
|
|
69
|
+
|
|
70
|
+
Constrained decoding makes invalid JSON *structurally impossible*. Caveat: an over-tight grammar can force the model down an unnatural path and *lower content* quality (it'll fill a required field with junk rather than refuse) — keep the grammar permissive on values, strict on structure, and still validate semantics in step 2.
|
|
71
|
+
|
|
72
|
+
6. **Build repair-and-retry as the BACKSTOP for paths with no hard guarantee.** Even guaranteed providers can return refusals or truncation; self-hosted/`json_object` paths can emit broken JSON. Ladder:
|
|
73
|
+
1. **Salvage first (no LLM call):** strip ```` ```json ```` fences and any pre/post prose; extract the outermost balanced `{...}`/`[...]`; libraries: `json-repair` (py), `jsonrepair` (js) fix trailing commas/single quotes/unescaped newlines. Cheap, deterministic — try before re-prompting.
|
|
74
|
+
2. **Re-prompt with the error:** on `ValidationError`, send the broken output + the exact validator message back ("Your previous reply failed validation: `<pydantic error>`. Return ONLY valid JSON matching the schema.") — Instructor's `max_retries` does exactly this. Cap at **2–3 attempts**; a 4th rarely converges.
|
|
75
|
+
3. **Feed the schema again** in the repair turn; models drift from it across long contexts.
|
|
76
|
+
|
|
77
|
+
7. **Handle refusals, truncation, and length explicitly — they are NOT parse errors and must not be "repaired" into garbage.**
|
|
78
|
+
- **Refusal:** OpenAI returns `message.refusal` (non-null) instead of `parsed`; Anthropic may emit a text block, not the forced tool. Branch on it — surface to the user/safety path, don't retry-loop into the rate limit.
|
|
79
|
+
- **Truncation:** check `finish_reason == "length"` (OpenAI) / `stop_reason == "max_tokens"` (Anthropic) → the JSON is cut mid-object and is *un*-repairable by salvage. Fix the cause: raise `max_tokens`, shrink the schema/array, or paginate the extraction — don't json-repair a truncated object (it'll silently drop fields).
|
|
80
|
+
- **Empty/whitespace:** treat as failure, retry once, then fail loud.
|
|
81
|
+
|
|
82
|
+
8. **Streaming structured output — buffer or parse incrementally, never `JSON.parse` mid-stream.** A partial stream `{"name":"Al` is invalid JSON. Two correct patterns:
|
|
83
|
+
- **Simplest:** accumulate all deltas, parse once on `finish`. Use when you don't need live UI.
|
|
84
|
+
- **Incremental:** `partial-json-parser` (js) / `pydantic` partial / OpenAI's streamed-parse helpers / `jsonriver` to coerce the buffer into a valid partial object on each delta for live rendering. For tool-calls, concatenate `tool_call.function.arguments` deltas across chunks (they arrive fragmented) and parse only when the tool-call completes.
|
|
85
|
+
|
|
86
|
+
9. **Verify.** (a) Round-trip: 100+ live calls on representative inputs → 100% deserialize into the typed model with zero `JSON.parse`/`ValidationError`; assert in CI against a recorded/replayed set. (b) Schema-escape probe: feed adversarial inputs that tempt the model to add commentary or a key — confirm `additionalProperties:false` + strict mode still yields clean objects. (c) Edge branches: force a refusal (policy-tripping input) and a truncation (tiny `max_tokens`) and assert each hits its dedicated branch, not the JSON repairer. (d) Enum closure: classification only ever returns a member of the enum. (e) Open-weights: same round-trip on the self-hosted path with constrained decoding ON, then OFF — confirm the constraint is what's carrying validity. Done = the program never sees a string it can't parse into the typed model, categories are closed enums, refusals/truncation route to their own branches, repair is rare (a metric you watch, not the load-bearing path), and the typed model — not the wire schema — is the single source of the contract.
|