intent(ignore-control): keep `--ignore "**/.gitignore"` from leaking files when `.gitignore` is a directory name, not a file
learned(deferred-filter): globby normalized `**/.gitignore` to `**/.gitignore/**`, which excluded both the file and any descendants; the post-filter only did a direct match, so contents of a directory literally named `.gitignore` leaked into output (narrow regression vs the pre-defer behavior)
decision(deferred-filter): match `${pattern}/**` in addition to the direct match to restore the old descendant-excluding semantics; the basename guard means this can only ever affect paths under a real `.gitignore` directory, so no over-filtering
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
intent(ignore-control): `--ignore "**/.gitignore/"` should behave like the slash-less form — exclude the file while keeping its rules
learned(deferred-filter): detection stripped the trailing slash but the post-filter matched the raw pattern, so the file was deferred out of globby's ignore yet never removed by the filter, leaking it into output
decision(deferred-filter): share one canonical normalization (toPosixIgnorePattern) between detection and filtering so the two can never diverge again
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
intent(multiRootSpec): address a CodeRabbit nitpick that the JSON test only checked markers appeared somewhere in the values, not that each disambiguated key maps to its own root's content
decision(multiRootSpec): assert app/* keys hold rootA content and app-2/* keys hold rootB content, proving the disambiguation maps correctly
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
intent(rootDisplayPath): collisions across roots are rare, so a numeric suffix fallback is acceptable instead of disambiguating via parent directories
decision(rootDisplayPath): rely solely on uniquifyLabelsWithSuffixes, which leaves unique labels untouched and only appends a suffix to collisions
learned(rootDisplayPath): the removed depth loop never improved on the suffix fallback in any reachable case (cwd-relative labels are full unique paths; outside-cwd labels have no segments), so output is byte-for-byte unchanged
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
intent(packager): keep packager.ts lean by moving the multi-root label/display-path helpers into their own module
decision(rootDisplayPath): expose only buildRootLabels and joinDisplayPath; keep toDisplayPath and the label-dedup helpers private
decision(tests): add direct unit tests for the extracted helpers, which were previously only exercised via the multiRootSpec integration test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
intent(token-budget): CI pipelines and agent harnesses need to guarantee packed output fits a target model's context window (#1616)
decision(token-budget): name it --token-budget / output.tokenBudget, not --max-tokens, to avoid collision with the LLM API max_tokens (generation length) and match the existing --token-count-* flags
decision(token-budget): enforce as a post-pack guard (clear error + non-zero exit), not an in-pack fail-fast
decision(token-budget): enforce in runDefaultAction (after reportResults) so direct runDefaultAction callers and MCP (which routes through runCli to runDefaultAction) are covered, while pack() stays a pure result-producer
decision(token-budget): remote runs defer the check (deferTokenBudgetCheck) and run it after copying output out of the temp dir, so over-budget remote runs still deliver output before failing, consistent with local
decision(token-budget): keep the schema strict (optional positive integer, no null) like other optional numeric fields; document the option as a commented-out example rather than null, since configs parse via JSON5 and null would fail validation
constraint(token-budget): token counts are computed after output generation (parallel with produceOutput in packager.ts), so the output is already written by the time the budget is checked; this is a guard, not a transform
constraint(token-budget): default is undefined (unlimited) to stay backward compatible
decision(schema-update): make the concurrency group static (schema-update) instead of schema-update-${{ github.ref }}. The workflow always pushes to the fixed chore/schema-update branch, so a workflow_dispatch from a different ref could otherwise run a separate concurrency group and race on that shared branch. A single static group serializes every run, matching the group's stated intent.
intent(schema-update): the same-repo PR auto-commit left the PR head on a GITHUB_TOKEN-authored commit, which does not trigger CI, so required checks were missing on the head and PRs got stuck in BLOCKED (seen on #1621)
decision(schema-update): run only on push to main (and workflow_dispatch); regenerate the schema and deliver it as the chore/schema-update PR via COMMITTER_TOKEN so that PR itself triggers CI and is mergeable
decision(schema-update): pull requests no longer run this workflow at all. Schema is a main-side concern, regenerated after merge; contributors edit only configSchema.ts, never the generated JSON
rejected(schema-update): keeping a PR-side drift check (fail or comment). Failing would block any PR that changes configSchema.ts (chicken-and-egg, and contradicts not hand-editing the generated schema); main-side regeneration already covers it
decision(schema-update): fork PRs now check out the merge ref and fail on schema drift instead of silently checking out main — surfaces fork-authored schema changes in CI (CodeRabbit)
decision(schema-update): add a concurrency group so two near-simultaneous main pushes don't race on the reused chore/schema-update branch
decision(schema-token): drop the unused `pull-requests: write` permission — create-pull-request uses COMMITTER_TOKEN (PAT), not GITHUB_TOKEN, and auto-commit needs only contents:write
decision(commit-message): capitalize the bot commit message to 'Auto-generate schema' per the project's Conventional Commits casing rule
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
decision(schema-update): check out the PR head branch only for same-repo PRs so git-auto-commit can push the regenerated schema; push/dispatch/fork all fall back to main
decision(schema-update): gate the PR-branch auto-commit to same-repo PRs — fork PRs get a read-only GITHUB_TOKEN and would fail (or push to main)
decision(schema-update): pin create-pull-request to `base: main` so a manual workflow_dispatch from another ref still targets main
learned(gha-expressions): accessing `github.event.pull_request.head.repo.full_name` on push/dispatch (where pull_request is null) yields empty, not an error, so the `&& ... || 'main'` fallback is safe
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
intent(schema-update): the Update Schema workflow's direct push to main fails at every version bump — the branch ruleset rejects it (GH013, pull_request required) because github-actions[bot] is not in the bypass list (only the admin role is)
decision(schema-update): on main (push / workflow_dispatch) open a PR via peter-evans/create-pull-request instead of pushing; keep the existing git-auto-commit-into-PR-branch behavior for pull_request events so schema changes still ride along with the PR that caused them
decision(schema-token): use the existing COMMITTER_TOKEN PAT so the generated PR triggers CI and is mergeable; the create-pull-request step is gated to non-pull_request events, so the PAT is never exposed to fork-triggered runs
constraint(branch-ruleset): main requires PR + 1 approving + code-owner review with no required status checks, so the schema PR is merged by the admin (bypass) — typically one click per release
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
intent(renovate): defer @clack/prompts v1 — the root major deps update (PR #1607) is fully red because v1 changed the text() validate callback arg to `string | undefined`, breaking the tsc build at src/cli/actions/initAction.ts:97 (TS18048); since build runs via the prepare hook, every npm ci fails and all CI jobs cascade-fail
decision(renovate): ignore only @clack/prompts, leaving typescript 6.0 and @secretlint/* 13 in the major group so CI can still evaluate them once clack is out of the batch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
intent(website): re-enable auto-pack on `?repo=` URLs now that the Cloudflare bot defense (BFM + invisible Turnstile) reliably blocks crawler-driven mass requests
constraint(auto-pack): keep the `!isBot()` guard as defense-in-depth — auto-execution was originally disabled because crawlers (e.g. Applebot) executing JS on permalink URLs caused mass pack requests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>