@friedbotstudio/create-baseline 0.2.1 → 0.4.0

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.
Files changed (87) hide show
  1. package/README.md +17 -7
  2. package/bin/cli.js +197 -119
  3. package/obj/template/.claude/commands/grant-push.md +19 -0
  4. package/obj/template/.claude/commands/init-project.md +26 -4
  5. package/obj/template/.claude/hooks/consent_gate_grant.mjs +107 -0
  6. package/obj/template/.claude/hooks/git_commit_guard.mjs +224 -0
  7. package/obj/template/.claude/hooks/harness_continuation.sh +101 -34
  8. package/obj/template/.claude/hooks/lib/common.mjs +283 -0
  9. package/obj/template/.claude/hooks/lib/common.sh +1 -1
  10. package/obj/template/.claude/hooks/memory_session_start.sh +20 -6
  11. package/obj/template/.claude/hooks/memory_stop.sh +161 -2
  12. package/obj/template/.claude/hooks/spec_approval_guard.sh +1 -1
  13. package/obj/template/.claude/hooks/swarm_approval_guard.sh +1 -1
  14. package/obj/template/.claude/hooks/tests/fixtures/ac008_byte_equal_reference.txt +7 -7
  15. package/obj/template/.claude/hooks/tests/fixtures/memory_stop_landmark_baseline.txt +21 -0
  16. package/obj/template/.claude/hooks/tests/fixtures/regenerate-ac008.sh +47 -0
  17. package/obj/template/.claude/hooks/tests/memory_session_start_test.sh +7 -3
  18. package/obj/template/.claude/hooks/tests/memory_stop_intent_test.sh +329 -0
  19. package/obj/template/.claude/hooks/tests/regenerate_ac008_test.sh +99 -0
  20. package/obj/template/.claude/memory/README.md +8 -3
  21. package/obj/template/.claude/memory/backlog.md +12 -0
  22. package/obj/template/.claude/project.json +6 -1
  23. package/obj/template/.claude/settings.json +3 -4
  24. package/obj/template/.claude/skills/audit-baseline/audit.sh +28 -16
  25. package/obj/template/.claude/skills/audit-baseline/tests/fixtures/_pending_opener_only.md +3 -0
  26. package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_full_empty_body.md +4 -0
  27. package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_full_with_entries.md +9 -0
  28. package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_no_opener.md +3 -0
  29. package/obj/template/.claude/skills/audit-baseline/tests/fixtures/preamble_opener_only.md +3 -0
  30. package/obj/template/.claude/skills/audit-baseline/tests/preamble_check_test.sh +147 -0
  31. package/obj/template/.claude/skills/changelog/SKILL.md +69 -0
  32. package/obj/template/.claude/skills/changelog/changelog.mjs +163 -0
  33. package/obj/template/.claude/skills/changelog/classifier.mjs +49 -0
  34. package/obj/template/.claude/skills/changelog/state-writer.mjs +19 -0
  35. package/obj/template/.claude/skills/changelog/tests/consent-expired_test.sh +126 -0
  36. package/obj/template/.claude/skills/changelog/tests/golden-path_test.sh +191 -0
  37. package/obj/template/.claude/skills/changelog/tests/idempotent-reentry_test.sh +121 -0
  38. package/obj/template/.claude/skills/changelog/tests/keepachangelog-unreleased-preserved_test.mjs +149 -0
  39. package/obj/template/.claude/skills/changelog/tests/non-git-shortcircuit_test.sh +98 -0
  40. package/obj/template/.claude/skills/changelog/tests/preview-only_test.sh +96 -0
  41. package/obj/template/.claude/skills/changelog/tests/run.sh +28 -0
  42. package/obj/template/.claude/skills/changelog/unreleased-writer.mjs +155 -0
  43. package/obj/template/.claude/skills/changelog/version-preview.mjs +124 -0
  44. package/obj/template/.claude/skills/chore/SKILL.md +5 -3
  45. package/obj/template/.claude/skills/commit/SKILL.md +5 -4
  46. package/obj/template/.claude/skills/copywriting/LICENSE +21 -0
  47. package/obj/template/.claude/skills/copywriting/NOTICE +23 -0
  48. package/obj/template/.claude/skills/copywriting/SKILL.md +1 -1
  49. package/obj/template/.claude/skills/design-ui/SKILL.md +23 -5
  50. package/obj/template/.claude/skills/design-ui/references/design-vs-development.md +26 -5
  51. package/obj/template/.claude/skills/design-ui/references/orchestration.md +1 -0
  52. package/obj/template/.claude/skills/design-ui/references/state-machine.md +5 -3
  53. package/obj/template/.claude/skills/documentation/LICENSE +202 -0
  54. package/obj/template/.claude/skills/documentation/NOTICE +22 -0
  55. package/obj/template/.claude/skills/harness/SKILL.md +5 -1
  56. package/obj/template/.claude/skills/humanizer/LICENSE +21 -0
  57. package/obj/template/.claude/skills/humanizer/NOTICE +21 -0
  58. package/obj/template/.claude/skills/impeccable/LICENSE +202 -0
  59. package/obj/template/.claude/skills/impeccable/NOTICE +24 -0
  60. package/obj/template/.claude/skills/memory-flush/SKILL.md +20 -4
  61. package/obj/template/.claude/skills/memory-flush/sweep.py +74 -6
  62. package/obj/template/.claude/skills/memory-flush/tests/run.sh +300 -1
  63. package/obj/template/.claude/skills/tdd/SKILL.md +2 -1
  64. package/obj/template/.claude/skills/tdd/drift_check.py +180 -0
  65. package/obj/template/.claude/skills/tdd/tests/drift_check_test.sh +190 -0
  66. package/obj/template/.claude/skills/tdd/tests/run.sh +21 -0
  67. package/obj/template/.claude/skills/technical-tutorials/LICENSE +21 -0
  68. package/obj/template/.claude/skills/technical-tutorials/NOTICE +23 -0
  69. package/obj/template/.claude/skills/technical-tutorials/SKILL.md +1 -1
  70. package/obj/template/.claude/skills/triage/SKILL.md +11 -5
  71. package/obj/template/CLAUDE.md +36 -25
  72. package/obj/template/docs/init/seed.md +39 -24
  73. package/obj/template/manifest.json +73 -33
  74. package/package.json +5 -2
  75. package/src/CLAUDE.template.md +36 -25
  76. package/src/cli/merge.js +15 -10
  77. package/src/cli/tui/doctor.js +56 -0
  78. package/src/cli/tui/install.js +79 -0
  79. package/src/cli/tui/meta.js +30 -0
  80. package/src/cli/tui/tokens.js +38 -0
  81. package/src/cli/tui/upgrade.js +100 -0
  82. package/src/memory/backlog.template.md +12 -0
  83. package/src/project.template.json +6 -1
  84. package/src/seed.template.md +39 -24
  85. package/src/settings.template.json +3 -4
  86. package/obj/template/.claude/hooks/consent_gate_grant.sh +0 -89
  87. package/obj/template/.claude/hooks/git_commit_guard.sh +0 -93
@@ -40,11 +40,11 @@ On every new session, before any work, you SHALL:
40
40
 
41
41
  1. **Read** `.claude/project.json` and check the `configured` field.
42
42
  2. **If `configured: false`** — `/init-project` has not run. The repository is in a sanctioned operating state called **project-agnostic mode**: hooks are active but `test_runner` and `lint_runner` run in guide mode and nothing is tailored to the user's stack. You SHALL greet the user with this exact framing:
43
- > "This repo has the Claude Code baseline installed (22 hooks, 1 subagent, 36 skills). It's in **project-agnostic mode** — `test_runner` and `lint_runner` are in guide mode and nothing is tailored to your stack. Run **`/init-project`** to scout the codebase, run the recommender, and generate a config. Skip it if you want baseline-only behavior, but you'll miss stack-specific tailoring."
43
+ > "This repo has the Claude Code baseline installed (22 hooks, 1 subagent, 37 skills). It's in **project-agnostic mode** — `test_runner` and `lint_runner` are in guide mode and nothing is tailored to your stack. Run **`/init-project`** to scout the codebase, run the recommender, and generate a config. Skip it if you want baseline-only behavior, but you'll miss stack-specific tailoring."
44
44
  You SHALL then proceed with whatever the user asks. Project-agnostic mode is **allowed** — the user is not required to run `/init-project` to use the baseline. The `setup_guard` hook surfaces a one-shot reminder on Write/Edit/MultiEdit (rate-limited to 10 minutes); it does **not** block writes. Other guards (commit, env, spec-approval, verify-pass, track, swarm-boundary) remain hard regardless of `configured` state.
45
45
  3. **If `configured: true`** — read `docs/init/seed.md` §16 if present so you know what was added. Tell the user:
46
46
  > "Configured for `<stack>`. Run `/triage \"<request>\"` to start a workflow, or `/harness` for the full pipeline."
47
- 4. **Memory check.** The `memory_session_start` hook injects a memory index into your additional context. If it reports `K candidates pending in _pending.md` with `K > 0`, you SHALL invoke `/memory-flush` before any workflow phase work keeps canonical memory fresh for downstream skills.
47
+ 4. **Memory check.** The `memory_session_start` hook injects a memory index into your additional context. The hook emits a **debt-mode nag** only when `_pending.md` has unflushed candidates AND no active workflow on disk (i.e., `.claude/state/workflow.json` is absent) those candidates are debt from a prior workflow that didn't end-flush. During an active workflow, **Phase 10.6** (memory-flush, between archive and grant-commit) handles flushing automatically; the session-start nag stays silent. You SHALL run `/memory-flush` when the debt-mode nag fires, before starting new work.
48
48
  5. **Git-repo check.** Run `git rev-parse --is-inside-work-tree 2>/dev/null` at the project root. If non-zero (not a git repo), surface this once per session and tell the user that gate C and the `commit` phase will be auto-excepted; the workflow ends after `/archive`. This is a sanctioned operating mode — Article IV phase 11 and Article VII are git-conditional.
49
49
  6. Once per session is sufficient. You SHALL NOT repeat the greeting on every prompt.
50
50
 
@@ -68,6 +68,7 @@ The 11-phase workflow is the only sanctioned path from request to commit. Phase
68
68
  | 9 | Integrate | `/integrate` | binding verify verdict |
69
69
  | 10 | Document | `/document` | docs |
70
70
  | 10.5 | Archive | `/archive` | bundle at `docs/archive/<date>/<slug>/` |
71
+ | 10.6 | Memory flush | `/memory-flush` | curated canonical memory + reset `_pending.md` |
71
72
  | 11 | **Grant commit** (gate C) + commit | user runs **`/grant-commit`**, then `/commit` (skill) | commit |
72
73
 
73
74
  **Mandatory rules:**
@@ -77,7 +78,7 @@ The 11-phase workflow is the only sanctioned path from request to commit. Phase
77
78
  - The only mechanism to bypass a phase is the `exceptions` array in `.claude/state/workflow.json`, written by `/triage`.
78
79
  - **Phase 6c and Phase 11 are git-conditional.** On a project where `git rev-parse --is-inside-work-tree` exits non-zero (no `.git/`, not inside a work tree), `/triage` SHALL auto-add `swarm-plan`, `approve-swarm`, `swarm-dispatch`, `grant-commit`, and `commit` to `exceptions`. Phase 6 routes to solo `/tdd` unconditionally; the workflow ends after `/archive`. Worktree isolation (the swarm contract's physical safety mechanism) requires git; `swarm.isolation: "shared"` is a sanctioned configuration knob for git projects that opt out of worktrees but does NOT restore the cross-task write isolation the swarm-worker assumes — it is unsafe as a non-git fallback, especially when `swarm.exempt_path_prefixes` covers baseline-internal paths (e.g. `.claude/`). Persistence outside git is the user's responsibility. See Article VII for the matching rule on git operations.
79
80
  - The three consent gates (A, B, C) are **commands**, not skills. They are structurally un-invokable by Claude. You SHALL NOT self-approve.
80
- - **How the gates are structurally enforced.** Each consent command (`/approve-spec`, `/approve-swarm`, `/grant-commit`) is a slash command typed by the user. The `consent_gate_grant` UserPromptSubmit hook (Art. VIII) parses the user's prompt **before Claude is invoked** and writes a short-lived consent marker at `.claude/state/.<gate>_grant`. The corresponding PreToolUse approval guard (`spec_approval_guard`, `swarm_approval_guard`, `git_commit_guard`) then allows Claude's slash-command-body write of the approval token only when the marker is present, fresh (≤ `consent.gate_marker_ttl_seconds`, default 120), and slug-matched; the marker is single-use and deleted on the allowed write. Slug derivation is centralized in `lib/common.sh → canonical_slug` (strip directory prefix + trailing `.md`) so the marker and the expected slug always agree, whether the user typed a bare slug, a filename, or a full path. The same guards block Claude from writing the marker file itself via Write/Edit/MultiEdit. Claude cannot reach the UserPromptSubmit code path, so it cannot forge consent.
81
+ - **How the gates are structurally enforced.** Each consent command (`/approve-spec`, `/approve-swarm`, `/grant-commit`, `/grant-push`) is a slash command typed by the user. The `consent_gate_grant` UserPromptSubmit hook (Art. VIII) parses the user's prompt **before Claude is invoked** and writes a short-lived consent marker at `.claude/state/.<gate>_grant`. The corresponding PreToolUse approval guard (`spec_approval_guard`, `swarm_approval_guard`, `git_commit_guard`) then allows Claude's slash-command-body write of the approval token only when the marker is present, fresh (≤ `consent.gate_marker_ttl_seconds`, default 120), and slug-matched; the marker is single-use and deleted on the allowed write. `/grant-push` is **not** a workflow-phase gate — it is a Bash-time consent for push to a protected branch (see Article VII). Slug derivation is centralized in `lib/common.sh → canonical_slug` (strip directory prefix + trailing `.md`) so the marker and the expected slug always agree, whether the user typed a bare slug, a filename, or a full path. The same guards block Claude from writing the marker file itself via Write/Edit/MultiEdit. Claude cannot reach the UserPromptSubmit code path, so it cannot forge consent.
81
82
  - **Out-of-band**: `/rca` produces an incident postmortem at `docs/rca/<slug>.md`. It is not a workflow phase and often precedes a bugfix intake.
82
83
 
83
84
  **Entry points** (`/triage` writes `workflow.json` with `entry_phase` and `exceptions`):
@@ -105,7 +106,7 @@ When `/harness` is invoked, you SHALL:
105
106
 
106
107
  **The safety net.** The `harness_continuation` Stop hook (Article VIII) re-fires `Skill(harness)` only when the loop exited mid-flow — i.e., on-disk state is `{state: "continue"}` with the marker present, and `stop_hook_active` is absent on the Stop payload. In normal operation (loop runs to gate/failure/done), the hook sees `state != continue` or marker absent and stays silent. The hook is a defense-in-depth signal, not the primary driver: a healthy `Skill(harness)` invocation never depends on it. The hook is also bounded to one block per turn by Claude Code's `stop_hook_active` semantics, so it cannot itself drive multi-phase chaining.
107
108
 
108
- **Resume after yield.** The user runs the consent command (which writes the gate marker via `consent_gate_grant`), then `/harness` again. The next invocation re-enters preflight, finds the gate token on disk, marks the gate task `completed`, and re-enters the loop.
109
+ **Resume after yield (auto).** After yielding at a consent gate, the harness skill writes `state: yielded` and removes `.harness_active`. The user runs the consent slash command in their next prompt; `consent_gate_grant` writes the gate marker (outside Claude's tool boundary), the command body writes the consent token, and the `harness_continuation` Stop hook detects fresh consent (rung 4: `workflow.json` present + `state=yielded` + a consent-token mtime newer than `harness_state`). The hook emits `{decision:"block"}`, and `Skill(harness)` is re-invoked in the same turn. The next invocation re-enters preflight, finds the gate token on disk, marks the gate task `completed`, and re-enters the loop. The user does not type `/harness` to resume.
109
110
 
110
111
  **Task discipline (autonomous progression).** `/triage` seeds a `TaskCreate`-backed checklist covering every non-excepted phase plus consent-gate placeholders (the placeholders carry `metadata.needs_user: true`). Inside the loop you SHALL: (a) call `TaskList` first; if empty, re-seed from `workflow.json → completed + exceptions + entry_phase` using the canonical templates in `triage`'s SKILL.md; (b) `TaskUpdate` the next pending non-blocked task to `in_progress` before invoking its phase skill; (c) `TaskUpdate` to `completed` only after the phase skill returns success; (d) when the next pending task carries `needs_user: true`, EXIT LOOP with YIELD — never invoke a skill for that task. The TaskList is session-bound; `workflow.json → completed` is the durable truth, and re-seeding always reconciles to it.
111
112
 
@@ -157,21 +158,25 @@ The following bind every code change.
157
158
 
158
159
  **Applicability.** Article VII applies only when the project is a git repository (`git rev-parse --is-inside-work-tree` exits 0 at the project root). On a non-git project, this Article is vacuously satisfied: you SHALL NOT attempt any git operation, gate C and the `commit` phase are auto-excepted at triage time (Art. IV), and the workflow ends after `/archive`. The rules below bind only inside the git-repository case.
159
160
 
160
- You SHALL run `git add <named paths>` and `git commit` only when **both** hold:
161
+ **Branch-aware consent policy.** Consent enforcement for `git commit` and `git push` is driven by two `project.json` knobs:
161
162
 
162
- 1. The user has explicitly asked for a commit in their current request; **and**
163
- 2. A fresh `commit_consent` token exists at `.claude/state/commit_consent` (5-minute TTL, written by `/grant-commit`).
163
+ - `git.protected_branches` glob list. `null` (default) means every branch is protected. Set e.g. `["main", "release/*"]` to limit consent enforcement to those branches.
164
+ - `git.branch_pattern` regex. `null` (default) means no naming check. Set e.g. `"^(feat|fix|chore|docs)/[a-z0-9-]+$"` to require conformant branch names on commit.
164
165
 
165
- `git_commit_guard` (Art. VIII) enforces both conditions.
166
+ On a **protected branch**, commits require a fresh `commit_consent` token (written by `/grant-commit`, 5-min TTL) and pushes require a fresh `push_consent` token (written by `/grant-push`, 5-min TTL) — both gated by the user having explicitly asked for the operation in their current request. On a non-protected branch, commits and pushes proceed without consent. `git_commit_guard` (Art. VIII) is the enforcer.
166
167
 
167
- You SHALL NEVER, unless the user names the exact operation in their current request:
168
+ **Detached HEAD.** When the current branch resolves to the literal string `HEAD` (detached state), the guard denies both commit and push with an explicit message. Check out a named branch before attempting either — branch-aware policy needs a named branch to evaluate `git.protected_branches` and `git.branch_pattern`.
168
169
 
169
- - `git push`, `git push --force`, `--force-with-lease`
170
- - `git commit --amend` — always create a new commit
171
- - `--no-verify`, `--no-gpg-sign`, or any flag that skips hooks/signing
172
- - `git reset --hard`, `git clean -f`, `git checkout --`, `git branch -D`
173
- - `git config`, `git rebase -i`, `git add -i`
174
- - `git add -A`, `git add .` — name the paths
170
+ **Hard-blocks regardless of consent, branch, or user request.** These operations rewrite history, skip safety, or sweep paths; `git_commit_guard`'s `FORBIDDEN_RE` blocks them flat-out:
171
+
172
+ - `git commit --amend` always create a new commit.
173
+ - `--no-verify`, `--no-gpg-sign`, or any flag that skips hooks/signing.
174
+ - `git reset --hard`, `git clean -f`, `git checkout --`, `git branch -D`.
175
+ - `git config` changes.
176
+ - `git rebase -i`, `git add -i` (interactive).
177
+ - `git add -A`, `git add .` — name the paths.
178
+
179
+ `git push` is no longer in this set — it is governed by the branch-aware policy above. `git push --force` and `--force-with-lease` are still forbidden unless the user names the exact operation in their current request, AND additionally subject to the branch-aware policy (force-push to a protected branch requires fresh `push_consent` plus the user-named carve-out).
175
180
 
176
181
  ## Article VIII — Hooks (the enforcement layer)
177
182
 
@@ -181,7 +186,7 @@ The 22 hooks in `.claude/hooks/` are the structural enforcement of this constitu
181
186
  |---|---|---|---|
182
187
  | `setup_guard` | PreToolUse / Edit\|Write\|MultiEdit | Art. III | Advisory reminder when `configured: false` (rate-limited 10 min). Does **not** block. |
183
188
  | `destructive_cmd_guard` | PreToolUse / Bash | Art. VII | Hard-block catastrophic commands; ask risky |
184
- | `git_commit_guard` | PreToolUse / Bash + Edit\|Write\|MultiEdit | Art. IV gate C, Art. VII | Bash: require fresh consent for `git commit`; hard-block forbidden flags. Write: gate writes to `.claude/state/commit_consent` and the `.commit_consent_grant` marker |
189
+ | `git_commit_guard` | PreToolUse / Bash + Edit\|Write\|MultiEdit | Art. IV gate C, Art. VII | Bash: enforce branch-aware policy `git commit` on a protected branch requires fresh `commit_consent`; `git push` on a protected branch requires fresh `push_consent`; both proceed without consent on non-protected branches; off-`branch_pattern` branches deny commits; detached HEAD denies both. Hard-block remaining forbidden flags (--amend, --no-verify, reset --hard, etc.). Write: gate writes to `.claude/state/{commit,push}_consent` and the matching `.{commit,push}_consent_grant` markers. |
185
190
  | `env_guard` | PreToolUse / Edit\|Write\|MultiEdit\|NotebookEdit | Art. VII | Block writes to `.env*` (allows `.env.example`) |
186
191
  | `spec_approval_guard` | PreToolUse / Edit\|Write\|MultiEdit | Art. IV gate A | Validate fresh `.spec_approval_grant` marker before allowing approval-token writes; block self-approval inside spec markdown; block direct writes to the marker |
187
192
  | `swarm_approval_guard` | PreToolUse / Edit\|Write\|MultiEdit | Art. IV gate B | Validate fresh `.swarm_approval_grant` marker before allowing swarm-approval writes; block direct writes to the marker |
@@ -200,13 +205,13 @@ The 22 hooks in `.claude/hooks/` are the structural enforcement of this constitu
200
205
  | `memory_stop` | Stop | Art. IX | Auto-extract memory candidates each turn-end |
201
206
  | `harness_continuation` | Stop | Art. V | Three-rung gate: (1) `stop_hook_active` absent on payload; (2) `.claude/state/.harness_active` exists (session-scoped marker created by the harness skill on `continue`, deleted on `yielded`/`done`, cleaned by `memory_session_start.sh` on session boundary); (3) `harness_state.state == "continue"`. When all three pass, emits `{"decision":"block","reason":"…invoke Skill(harness)…"}`. Sanity rail: marker-slug-vs-`workflow.json`-slug mismatch logs WARN to `harness_continuation.log` without changing the decision. Silent on any rung fail. Never writes consent markers. |
202
207
  | `memory_pre_compact` | PreCompact | Art. IX | Capture resume snapshot before context compaction |
203
- | `consent_gate_grant` | UserPromptSubmit | Art. IV gates A/B/C | Detect `/approve-spec`/`/approve-swarm`/`/grant-commit` in user input and write the gate-specific consent marker — runs OUTSIDE Claude's tool boundary so Claude cannot forge it |
208
+ | `consent_gate_grant` | UserPromptSubmit | Art. IV gates A/B/C, Art. VII | Detect `/approve-spec`/`/approve-swarm`/`/grant-commit`/`/grant-push` in user input and write the gate-specific consent marker — runs OUTSIDE Claude's tool boundary so Claude cannot forge it |
204
209
 
205
210
  ## Article IX — Project memory
206
211
 
207
212
  The memory system at `.claude/memory/` accumulates project facts across sessions. You SHALL:
208
213
 
209
- 1. Treat the six canonical files (`landmarks.md`, `libraries.md`, `decisions.md`, `landmines.md`, `conventions.md`, `pending-questions.md`) as long-term project memory. Each entry has a stable key per the schema in `.claude/memory/README.md`.
214
+ 1. Treat the seven canonical files (`landmarks.md`, `libraries.md`, `decisions.md`, `landmines.md`, `conventions.md`, `pending-questions.md`, `backlog.md`) as long-term project memory. Each entry has a stable key per the schema in `.claude/memory/README.md`.
210
215
  2. **Re-verify before citing.** Every skill that cites a memory entry SHALL re-verify it (file exists, symbol still at named line, library version still pinned). Failed verification → you SHALL correct or delete the entry in the same run before proceeding.
211
216
  3. Treat `_pending.md` as the auto-extraction inbox (written by `memory_stop`). Promote candidates to canonical files only via `/memory-flush`. You SHALL NOT write directly into canonical memory files outside the natural byproduct of phase skills.
212
217
  4. Treat `_resume.md` as the cross-session continuity snapshot (refreshed every turn-end and before compaction). It is **session memory**, not project memory.
@@ -257,7 +262,7 @@ Every UI design task that originates inside a workflow phase SHALL route through
257
262
  | A spec whose `write_set` intersects `project.json → tdd.ui_globs` SHALL declare a populated `## Design calls` section, one row per design surface. | `spec_design_calls_guard` (Art. VIII) at the Write boundary; `/spec-lint` at preflight. |
258
263
  | `/tdd` Step 6 SHALL invoke `Skill(design-ui, task_brief)` once per `## Design calls` row before Step 7 (verify). | `tdd` skill SOP. |
259
264
  | `design-ui` SHALL NOT write product code. Its only writes are the state file at `.claude/state/design/<slug>.json`, snapshots under `docs/design/<slug>.*.md`, and memory candidates. The product-code writes happen inside `impeccable` invocations. | `design-ui` SKILL.md. |
260
- | `design-ui` SHALL classify incoming intents at Stage 0 (design / development / copy). A misrouted intent returns `final_state: "not_a_design_task"` with a pointer to the correct lane and writes no code. | `design-ui` Stage 0 + `references/design-vs-development.md`. |
265
+ | `design-ui` SHALL classify incoming intents at Stage 0 (design / development / copy). A misrouted intent returns one of two terminal states: `final_state: "not_a_design_task"` (single-lane misroute) with `correct_lane`, OR `final_state: "mixed_brief"` (multi-lane misroute) with a structured `lane_split` array. Neither writes code. | `design-ui` Stage 0 + `references/design-vs-development.md`. |
261
266
  | Iteration cap: `audit → polish` loops SHALL terminate after 3 iterations with `final_state: "needs_human"` if P0 ≥ 1 or P1 > 0 persist. P0 issues block (do not loop). | `design-ui` SKILL.md + `references/orchestration.md`. |
262
267
  | Multi-step impeccable recipes SHALL ask the user before proceeding. Single-step recipes SHALL auto-execute. | `references/intent-table.md` `mode` column. |
263
268
 
@@ -287,12 +292,12 @@ Cryptographic supply-chain attestation, signed lock files, and per-skill aggrega
287
292
  |---|---|
288
293
  | `.claude/hooks/` | 22 hook scripts (17 write/run-boundary + 4 lifecycle + 1 input-boundary). Bash + python3, no jq. |
289
294
  | `.claude/agents/` | 1 baseline subagent: `swarm-worker` (rendered from `src/agents/swarm-worker.template.md`) |
290
- | `.claude/skills/` | 36 skills: artifact (4) + phases (10) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1) |
291
- | `.claude/commands/` | 4 consent/bootstrap gates: `approve-spec`, `approve-swarm`, `grant-commit`, `init-project` |
292
- | `.claude/memory/` | 6 canonical knowledge files + `_pending.md` (staging) + `_resume.md` (continuity snapshot) + `README.md` |
295
+ | `.claude/skills/` | 37 skills: artifact (4) + phases (11) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1) |
296
+ | `.claude/commands/` | 5 consent/bootstrap gates: `approve-spec`, `approve-swarm`, `grant-commit`, `grant-push`, `init-project` |
297
+ | `.claude/memory/` | 7 canonical knowledge files + `_pending.md` (staging) + `_resume.md` (continuity snapshot) + `README.md` |
293
298
  | `.claude/project.json` | per-project config (test/lint cmd, TDD globs, destructive patterns, swarm config, additions). Populated by `/init-project`. |
294
299
  | `.claude/settings.json` | hook wiring + permissions |
295
- | `.claude/state/` | runtime: `workflow.json`, `commit_consent`, `spec_approvals/`, `swarm_approvals/`, `swarm/`, `harness/<slug>.log`, `last_test_result` |
300
+ | `.claude/state/` | runtime: `workflow.json`, `commit_consent`, `push_consent`, `spec_approvals/`, `swarm_approvals/`, `swarm/`, `harness/<slug>.log`, `last_test_result` |
296
301
  | `.mcp.json` | three baseline MCP servers: `context7`, `plantuml`, `playwright` |
297
302
  | `src/` | pristine ship-time templates for every file `/init-project` modifies (overlay source for `npx @friedbotstudio/create-baseline`) |
298
303
  | `docs/init/seed.md` | genesis prompt — governing specification of the baseline |
@@ -317,8 +322,14 @@ Cryptographic supply-chain attestation, signed lock files, and per-skill aggrega
317
322
  **Memory (1)**:
318
323
  - `memory-flush`
319
324
 
320
- **Shared globals (7)** — vendored / globally available:
321
- - `claude-automation-recommender` (Apache 2.0, vendored), `code-structure` (mandatory on all code), `humanizer`, `documentation`, `technical-tutorials`, `copywriting`, `impeccable`
325
+ **Shared globals (7)** — one written for this baseline, six vendored from external sources with their upstream licenses preserved in `LICENSE` + `NOTICE` alongside each skill:
326
+ - `claude-automation-recommender` vendored from Anthropic's `claude-code-setup` plugin, Apache 2.0.
327
+ - `code-structure` — written for this baseline (Friedbot Studio). Mandatory on every code-generation step.
328
+ - `humanizer` — vendored from [`blader/humanizer`](https://github.com/blader/humanizer), MIT.
329
+ - `documentation` — vendored from Anthropic's `claude-code-setup` plugin, Apache 2.0.
330
+ - `technical-tutorials` — vendored from [`jonathimer/devmarketing-skills`](https://github.com/jonathimer/devmarketing-skills), MIT.
331
+ - `copywriting` — vendored from [`coreyhaines31/marketingskills`](https://github.com/coreyhaines31/marketingskills), MIT.
332
+ - `impeccable` — vendored from [`pbakaus/impeccable`](https://github.com/pbakaus/impeccable), Apache 2.0.
322
333
 
323
334
  **Audit (1)**:
324
335
  - `audit-baseline` — drift check between this constitution + seed.md and the implementation
package/src/cli/merge.js CHANGED
@@ -22,7 +22,8 @@ async function copyFile(src, dst) {
22
22
  await cp(src, dst, { force: true });
23
23
  }
24
24
 
25
- export async function threeWayMerge(templateDir, target, oldManifest, newManifest) {
25
+ export async function threeWayMerge(templateDir, target, oldManifest, newManifest, opts = {}) {
26
+ const { dryRun = false, onSkipCustomized = null } = opts;
26
27
  const actions = [];
27
28
  const oldFiles = oldManifest?.files ?? {};
28
29
  const newFiles = newManifest?.files ?? {};
@@ -36,7 +37,7 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
36
37
  if (await pathExists(tgtPath)) {
37
38
  actions.push({ kind: ACTION_KINDS.NEVER_TOUCH_PRESERVE, path: rel, reason: 'NEVER_TOUCH path present in target' });
38
39
  } else if (rel in newFiles) {
39
- await copyFile(tplPath, tgtPath);
40
+ if (!dryRun) await copyFile(tplPath, tgtPath);
40
41
  actions.push({ kind: ACTION_KINDS.NEVER_TOUCH_ADD, path: rel, reason: 'NEVER_TOUCH path absent; written from template' });
41
42
  }
42
43
  continue;
@@ -44,7 +45,7 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
44
45
 
45
46
  if (SPECIAL_MERGE.includes(rel)) {
46
47
  if (rel in newFiles && await pathExists(tplPath)) {
47
- await deepMergeMcpServers(tplPath, tgtPath);
48
+ if (!dryRun) await deepMergeMcpServers(tplPath, tgtPath);
48
49
  actions.push({ kind: ACTION_KINDS.SPECIAL_MERGE, path: rel, reason: 'additive deep-merge applied' });
49
50
  }
50
51
  continue;
@@ -56,7 +57,7 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
56
57
  const tgtHash = targetExists ? await hashFile(tgtPath) : null;
57
58
 
58
59
  if (!targetExists && newHash) {
59
- await copyFile(tplPath, tgtPath);
60
+ if (!dryRun) await copyFile(tplPath, tgtPath);
60
61
  actions.push({ kind: ACTION_KINDS.ADD, path: rel, reason: 'new in template; not present in target' });
61
62
  continue;
62
63
  }
@@ -67,13 +68,19 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
67
68
  }
68
69
 
69
70
  if (newHash && oldHash && tgtHash === oldHash) {
70
- await copyFile(tplPath, tgtPath);
71
+ if (!dryRun) await copyFile(tplPath, tgtPath);
71
72
  actions.push({ kind: ACTION_KINDS.OVERWRITE, path: rel, reason: 'target untouched since last install; updated' });
72
73
  continue;
73
74
  }
74
75
 
75
76
  if (newHash && tgtHash && tgtHash !== oldHash) {
76
- actions.push({ kind: ACTION_KINDS.SKIP_CUSTOMIZED, path: rel, reason: 'target customized since last install' });
77
+ const choice = onSkipCustomized ? await onSkipCustomized(rel) : 'keep-mine';
78
+ if (choice === 'take-theirs') {
79
+ if (!dryRun) await copyFile(tplPath, tgtPath);
80
+ actions.push({ kind: ACTION_KINDS.OVERWRITE, path: rel, reason: 'customized file; user chose take-theirs' });
81
+ } else {
82
+ actions.push({ kind: ACTION_KINDS.SKIP_CUSTOMIZED, path: rel, reason: 'target customized since last install' });
83
+ }
77
84
  continue;
78
85
  }
79
86
 
@@ -84,10 +91,8 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
84
91
  // prune. Otherwise the user accumulates stale baseline files forever.
85
92
  // - target customized (tgtHash != oldHash) → preserve to avoid
86
93
  // destroying user work; report drift via exit 3.
87
- // Pruning only runs when --merge already applies; there is no separate
88
- // flag (decision recorded in README).
89
94
  if (targetExists && tgtHash === oldHash) {
90
- await unlink(tgtPath);
95
+ if (!dryRun) await unlink(tgtPath);
91
96
  actions.push({ kind: ACTION_KINDS.PRUNE, path: rel, reason: 'removed from new template; target was untouched — deleted' });
92
97
  } else if (targetExists) {
93
98
  actions.push({ kind: ACTION_KINDS.PRUNE_SKIPPED_CUSTOMIZED, path: rel, reason: 'removed from new template; target customized — preserved' });
@@ -96,7 +101,7 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
96
101
  }
97
102
  }
98
103
 
99
- if (newManifest) {
104
+ if (newManifest && !dryRun) {
100
105
  await mkdir(join(target, '.claude'), { recursive: true });
101
106
  await saveManifest(join(target, '.claude/.baseline-manifest.json'), newManifest);
102
107
  }
@@ -0,0 +1,56 @@
1
+ // Domain — branded sectioned doctor report. Consumes the structured
2
+ // DoctorReport from src/cli/doctor.js (unchanged) and writes a colorized,
3
+ // sectioned rendering to stdout. The non-TTY plain path stays on doctor.js's
4
+ // formatReport — this renderer is only invoked when stdout is a TTY.
5
+
6
+ import { accent, muted, success, warn, error, accentLight } from './tokens.js';
7
+
8
+ function brandHeader(target, manifestInfo) {
9
+ const lines = [accent('Baseline doctor')];
10
+ if (target) lines.push(muted(`target: ${target}`));
11
+ if (manifestInfo) lines.push(muted(`manifest: ${manifestInfo}`));
12
+ return lines;
13
+ }
14
+
15
+ export function render(report) {
16
+ if (report.error) {
17
+ const headerLines = brandHeader(report.target);
18
+ process.stdout.write(headerLines.join('\n') + '\n\n');
19
+ process.stdout.write(`${error('doctor:')} ${report.error}\n`);
20
+ return;
21
+ }
22
+ const lines = brandHeader(report.target, `version ${report.manifestVersion}, installed ${report.generatedAt}`);
23
+ lines.push('');
24
+ lines.push(` ${success('matched')}: ${report.matched.length}`);
25
+ lines.push(` ${accentLight('customized')}: ${report.customized.length}`);
26
+ lines.push(` ${error('missing')}: ${report.missing.length}`);
27
+ lines.push(` ${warn('added')}: ${report.added.length}`);
28
+
29
+ if (report.missing.length > 0) {
30
+ lines.push('');
31
+ lines.push(error('Missing (deleted from disk; exit 1):'));
32
+ for (const p of report.missing) lines.push(` - ${p}`);
33
+ }
34
+ if (report.customized.length > 0) {
35
+ lines.push('');
36
+ const header = report.strict
37
+ ? accentLight('Customized (strict mode → exit 1):')
38
+ : accentLight('Customized (informational):');
39
+ lines.push(header);
40
+ if (Array.isArray(report.tampered) && report.tampered.length > 0) {
41
+ for (const entry of report.tampered) {
42
+ lines.push(` ${warn('TAMPERED')}: ${entry.path}`);
43
+ lines.push(` shipped=${muted(entry.shipped)} observed=${muted(entry.observed)}`);
44
+ }
45
+ } else {
46
+ for (const p of report.customized) lines.push(` - ${p}`);
47
+ }
48
+ }
49
+ if (report.added.length > 0) {
50
+ lines.push('');
51
+ lines.push(warn('Added under .claude/ since install (likely /init-project; informational):'));
52
+ for (const p of report.added) lines.push(` - ${p}`);
53
+ }
54
+
55
+ process.stdout.write(lines.join('\n') + '\n');
56
+ }
@@ -0,0 +1,79 @@
1
+ // Domain — branded install flow. Composes the pure-data install + plantuml
2
+ // foundations behind a clack-style presentation seam. The `prompts` parameter
3
+ // defaults to @clack/prompts but is injected in tests.
4
+
5
+ import * as clackModule from '@clack/prompts';
6
+ import { readFile } from 'node:fs/promises';
7
+ import { freshInstall, forceInstall } from '../install.js';
8
+ import { fetchPlantumlIfMissing, FETCH_OUTCOMES } from '../plantuml.js';
9
+
10
+ const SUCCESS = 0;
11
+ const ERR_INSTALL_FAILED = 1;
12
+ const ERR_PLANTUML_REQUIRED = 4;
13
+
14
+ export async function run({ target, opts = {}, prompts = clackModule } = {}) {
15
+ if (!target || typeof target !== 'string') {
16
+ throw new Error('tui.install.run requires a non-empty string target');
17
+ }
18
+ if (!opts.templateDir) {
19
+ throw new Error('tui.install.run requires opts.templateDir');
20
+ }
21
+
22
+ const version = await readPackageVersion();
23
+ prompts.intro(`create-baseline v${version}`);
24
+
25
+ const spinner = prompts.spinner();
26
+ spinner.start('Copying baseline files');
27
+
28
+ try {
29
+ await copyTemplate(target, opts);
30
+ } catch (err) {
31
+ spinner.error('Install failed');
32
+ prompts.outro(err.message);
33
+ return ERR_INSTALL_FAILED;
34
+ }
35
+
36
+ const plantumlExit = await fetchPlantumlBranded(target, opts, prompts, spinner);
37
+ if (plantumlExit !== SUCCESS) return plantumlExit;
38
+
39
+ spinner.stop('Baseline installed');
40
+ prompts.outro(`Installed at ${target}`);
41
+ return SUCCESS;
42
+ }
43
+
44
+ async function copyTemplate(target, opts) {
45
+ const installOpts = { withNpmrc: !!opts.withNpmrc };
46
+ if (opts.force) await forceInstall(opts.templateDir, target, installOpts);
47
+ else await freshInstall(opts.templateDir, target, installOpts);
48
+ }
49
+
50
+ async function fetchPlantumlBranded(target, opts, prompts, spinner) {
51
+ if (opts.noPlantuml) return SUCCESS;
52
+ spinner.message('Fetching PlantUML jar');
53
+ const result = await fetchPlantumlIfMissing(target, {
54
+ noPlantuml: opts.noPlantuml,
55
+ requirePlantuml: opts.requirePlantuml,
56
+ });
57
+ if (result.outcome === FETCH_OUTCOMES.ERRORED_REQUIRE_PLANTUML) {
58
+ spinner.error('PlantUML required but unavailable');
59
+ prompts.outro(result.reason);
60
+ return ERR_PLANTUML_REQUIRED;
61
+ }
62
+ if (
63
+ result.outcome === FETCH_OUTCOMES.WARNED_NETWORK_FAILURE ||
64
+ result.outcome === FETCH_OUTCOMES.WARNED_HASH_MISMATCH
65
+ ) {
66
+ prompts.log.warn(`PlantUML jar: ${result.reason} — install continued`);
67
+ }
68
+ return SUCCESS;
69
+ }
70
+
71
+ async function readPackageVersion() {
72
+ try {
73
+ const url = new URL('../../../package.json', import.meta.url);
74
+ const pkg = JSON.parse(await readFile(url, 'utf8'));
75
+ return pkg.version || '0.0.0';
76
+ } catch {
77
+ return '0.0.0';
78
+ }
79
+ }
@@ -0,0 +1,30 @@
1
+ // Domain — branded renderers for the meta commands (--help, --version).
2
+ // In a TTY, a brand banner frames the canonical body; in non-TTY the body is
3
+ // emitted unchanged so that piped consumers (`$(cli --version)`, `cli --help |
4
+ // grep ...`) keep working byte-clean.
5
+
6
+ import { accent, muted, rule } from './tokens.js';
7
+
8
+ export function renderHelp(helpText, version) {
9
+ if (!process.stdout.isTTY) {
10
+ process.stdout.write(helpText.endsWith('\n') ? helpText : helpText + '\n');
11
+ return;
12
+ }
13
+ const banner = [
14
+ '',
15
+ ` ${accent('Baseline CLI')} ${muted(`v${version}`)}`,
16
+ ` ${muted('@friedbotstudio/create-baseline')}`,
17
+ ` ${rule('─'.repeat(48))}`,
18
+ '',
19
+ ].join('\n');
20
+ process.stdout.write(banner + '\n');
21
+ process.stdout.write(helpText.endsWith('\n') ? helpText : helpText + '\n');
22
+ }
23
+
24
+ export function renderVersion(version) {
25
+ if (!process.stdout.isTTY) {
26
+ process.stdout.write(`${version}\n`);
27
+ return;
28
+ }
29
+ process.stdout.write(`${accent('baseline')} ${muted('v')}${version}\n`);
30
+ }
@@ -0,0 +1,38 @@
1
+ // Foundation — ANSI brand-color helpers for the TUI presentation layer.
2
+ // Translates Friedbot Studio's oklch tokens (site-src/assets/site.css :root) to
3
+ // 24-bit truecolor escape sequences. Respects NO_COLOR (https://no-color.org)
4
+ // and skips emission when stdout is not a TTY.
5
+
6
+ const NO_COLOR = process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '';
7
+
8
+ // oklch -> approximate sRGB hex used in the rendered docs site:
9
+ // --accent oklch(55.8% 0.187 41.5) ~ #c2410c (orange-700)
10
+ // --accent-light oklch(70.3% 0.187 41.5) ~ #ea6a25 (orange-500)
11
+ // --muted oklch(45% 0.026 257) ~ #6b7280
12
+ // --cli-success oklch(70% 0.15 145) ~ #4ade80
13
+ // --warn oklch(58% 0.13 60) ~ #d97706
14
+ // --mac-red oklch(70% 0.21 24) ~ #ef4444
15
+ // --rule oklch(89% 0.013 257) ~ #d1d5db
16
+ const RGB = {
17
+ accent: [194, 65, 12],
18
+ accentLight: [234, 106, 37],
19
+ muted: [107, 114, 128],
20
+ success: [74, 222, 128],
21
+ warn: [217, 119, 6],
22
+ error: [239, 68, 68],
23
+ rule: [209, 213, 219],
24
+ };
25
+
26
+ function paint(rgb, text) {
27
+ if (NO_COLOR || !process.stdout.isTTY) return text;
28
+ const [r, g, b] = rgb;
29
+ return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
30
+ }
31
+
32
+ export const accent = (text) => paint(RGB.accent, text);
33
+ export const accentLight = (text) => paint(RGB.accentLight, text);
34
+ export const muted = (text) => paint(RGB.muted, text);
35
+ export const success = (text) => paint(RGB.success, text);
36
+ export const warn = (text) => paint(RGB.warn, text);
37
+ export const error = (text) => paint(RGB.error, text);
38
+ export const rule = (text) => paint(RGB.rule, text);
@@ -0,0 +1,100 @@
1
+ // Domain — branded upgrade flow with interactive per-file conflict resolution.
2
+ // Plan/apply split:
3
+ // 1. dry-run threeWayMerge → enumerate SKIP_CUSTOMIZED conflicts
4
+ // 2. prompt the user once per conflict
5
+ // 3. on cancel/abort: bail before any write
6
+ // 4. on resolve: real threeWayMerge with onSkipCustomized backed by the Map
7
+
8
+ import * as clackModule from '@clack/prompts';
9
+ import { existsSync } from 'node:fs';
10
+ import { readdir } from 'node:fs/promises';
11
+ import { join, relative, sep } from 'node:path';
12
+ import { threeWayMerge, ACTION_KINDS } from '../merge.js';
13
+ import { loadManifest, buildManifestFromDir } from '../manifest.js';
14
+
15
+ const SUCCESS = 0;
16
+ const ERR_ABORT = 1;
17
+ const ERR_NO_MANIFEST = 2;
18
+ const ERR_DIVERGENCE = 3;
19
+
20
+ const CHOICE_OPTIONS = [
21
+ { value: 'keep-mine', label: 'Keep mine', hint: 'preserve target file as-is' },
22
+ { value: 'take-theirs', label: 'Take theirs', hint: 'overwrite with new baseline' },
23
+ { value: 'abort', label: 'Abort', hint: 'exit without changes' },
24
+ ];
25
+
26
+ export async function run({ target, opts = {}, prompts = clackModule } = {}) {
27
+ if (!target || typeof target !== 'string') {
28
+ throw new Error('tui.upgrade.run requires a non-empty string target');
29
+ }
30
+ if (!opts.templateDir) {
31
+ throw new Error('tui.upgrade.run requires opts.templateDir');
32
+ }
33
+
34
+ const manifestPath = join(target, '.claude/.baseline-manifest.json');
35
+ if (!existsSync(manifestPath)) {
36
+ prompts.log.error(`No baseline manifest at ${manifestPath}. Run a fresh install first.`);
37
+ return ERR_NO_MANIFEST;
38
+ }
39
+
40
+ prompts.intro('create-baseline upgrade');
41
+
42
+ const { oldManifest, newManifest } = await loadManifests(opts.templateDir, manifestPath);
43
+ const dryReport = await threeWayMerge(opts.templateDir, target, oldManifest, newManifest, { dryRun: true });
44
+ const conflicts = dryReport.actions.filter((a) => a.kind === ACTION_KINDS.SKIP_CUSTOMIZED);
45
+
46
+ const choices = new Map();
47
+ for (const conflict of conflicts) {
48
+ const choice = await prompts.select({
49
+ message: `${conflict.path} has been customized — choose:`,
50
+ options: CHOICE_OPTIONS,
51
+ });
52
+ if (prompts.isCancel(choice) || choice === 'abort') {
53
+ prompts.cancel('Upgrade aborted; tree unchanged.');
54
+ return ERR_ABORT;
55
+ }
56
+ choices.set(conflict.path, choice);
57
+ }
58
+
59
+ if (opts.dryRun) {
60
+ for (const action of dryReport.actions) {
61
+ prompts.log.info(`${action.kind.padEnd(24)} ${action.path}`);
62
+ }
63
+ prompts.outro('Dry run complete; no changes written.');
64
+ return SUCCESS;
65
+ }
66
+
67
+ const onSkipCustomized = (rel) => choices.get(rel) ?? 'keep-mine';
68
+ const finalReport = await threeWayMerge(opts.templateDir, target, oldManifest, newManifest, { onSkipCustomized });
69
+
70
+ const applied = finalReport.actions.filter((a) => isApplied(a.kind)).length;
71
+ const skipped = finalReport.actions.filter((a) => a.kind === ACTION_KINDS.SKIP_CUSTOMIZED).length;
72
+ prompts.outro(`Applied ${applied}; ${skipped} skipped.`);
73
+ return finalReport.exitCode === 3 ? ERR_DIVERGENCE : SUCCESS;
74
+ }
75
+
76
+ function isApplied(kind) {
77
+ return (
78
+ kind === ACTION_KINDS.ADD ||
79
+ kind === ACTION_KINDS.OVERWRITE ||
80
+ kind === ACTION_KINDS.PRUNE ||
81
+ kind === ACTION_KINDS.SPECIAL_MERGE ||
82
+ kind === ACTION_KINDS.NEVER_TOUCH_ADD
83
+ );
84
+ }
85
+
86
+ async function loadManifests(templateDir, manifestPath) {
87
+ const oldManifest = await loadManifest(manifestPath);
88
+ const tplFiles = await listShippedFiles(templateDir);
89
+ const newManifest = await buildManifestFromDir(templateDir, tplFiles);
90
+ return { oldManifest, newManifest };
91
+ }
92
+
93
+ async function listShippedFiles(root, base = root, acc = []) {
94
+ for (const entry of await readdir(root, { withFileTypes: true })) {
95
+ const full = join(root, entry.name);
96
+ if (entry.isDirectory()) await listShippedFiles(full, base, acc);
97
+ else if (entry.isFile()) acc.push(relative(base, full).split(sep).join('/'));
98
+ }
99
+ return acc;
100
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ owners: [/memory-flush]
3
+ category: future-work intent
4
+ size-cap: 500
5
+ key: <slug>-<4char-hash>
6
+ verifies-against: none
7
+ stale-exempt: true
8
+ ---
9
+
10
+ # Backlog
11
+
12
+ (populated by /memory-flush from auto-extracted candidates)
@@ -185,7 +185,12 @@
185
185
  },
186
186
  "consent": {
187
187
  "commit_ttl_seconds": 300,
188
- "gate_marker_ttl_seconds": 120
188
+ "gate_marker_ttl_seconds": 120,
189
+ "push_ttl_seconds": 300
190
+ },
191
+ "git": {
192
+ "protected_branches": null,
193
+ "branch_pattern": null
189
194
  },
190
195
  "swarm": {
191
196
  "max_parallel": 4,