@friedbotstudio/create-baseline 0.5.0 → 0.6.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.
@@ -261,13 +261,16 @@ def check_skill_ownership():
261
261
  if not skill_dir.is_dir():
262
262
  add(f"skill ownership: {slug}", "FAIL", "baseline skill missing")
263
263
  continue
264
- for path, expected_hash in files_map.items():
264
+ for path, entry in files_map.items():
265
265
  if not path.startswith(f".claude/skills/{slug}/"):
266
266
  continue
267
267
  disk_file = root / path
268
268
  if not disk_file.exists():
269
269
  add(f"skill ownership: {slug}", "FAIL", f"baseline skill missing: {path}")
270
270
  continue
271
+ # Manifest v3+ wraps each entry as {sha256, tier}; v2 ships bare
272
+ # sha strings. Both shapes are accepted at read time.
273
+ expected_hash = entry if isinstance(entry, str) else (entry or {}).get("sha256")
271
274
  actual = hashlib.sha256(disk_file.read_bytes()).hexdigest()
272
275
  if actual != expected_hash:
273
276
  add(f"skill ownership: {slug}", "FAIL", f"hash mismatch at {path}")
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: upgrade-project
3
+ owner: baseline
4
+ description: Reconcile baseline-versioned files that the `create-baseline upgrade` CLI staged for LLM-assisted semantic merge. Use when the CLI prints "Open Claude Code and run /upgrade-project to reconcile". Reads the stage manifest at `.claude/state/upgrade/<ts>/manifest.json`, reasons through each pending file's three-way delta in main context, writes a reconciled LOCAL, then deletes the stage when every file lands. Supports `--dry-run` (preview the reconciled diff without writing) and a structured "needs-user-input" fallback when the conflict cannot be disambiguated automatically.
5
+ ---
6
+
7
+ # /upgrade-project — semantic-merge reconciliation for baseline files
8
+
9
+ You are reconciling files that `create-baseline upgrade` decided required **semantic merge** rather than mechanical merge. The CLI has already detected per-file customization, classified each file as tier 3 (SEMANTIC) at build time, and staged the three states (BASE / INCOMING / LOCAL) for you to reason about in main context. This skill is the only sanctioned way to drive that staged state to RECONCILED.
10
+
11
+ This skill is **maintenance work**, not a workflow phase. It is invoked reactively whenever the upgrade CLI prints the "run /upgrade-project to reconcile" pointer. It does not appear in `.claude/state/workflow.json`, does not require `/triage`, and does not trigger consent gates.
12
+
13
+ ## When to use
14
+
15
+ - The user just ran `npx @friedbotstudio/create-baseline upgrade <target>` and the CLI exited 5 with a "Pending semantic-merge stage at <ts>" message.
16
+ - The user types `/upgrade-project` or asks "reconcile the staged files".
17
+ - A previous `/upgrade-project` invocation hit a `NEEDS_USER_INPUT` fallback, the user provided direction, and you re-invoke to pick up where you left off.
18
+
19
+ ## Inputs (read from disk)
20
+
21
+ For each stage directory under `.claude/state/upgrade/`:
22
+
23
+ - `manifest.json` — the **stage manifest** the CLI wrote. Schema:
24
+ ```json
25
+ {
26
+ "stage_version": 1,
27
+ "slug": "upgrade-flow-rework",
28
+ "created_at": "2026-05-20T14:49:00.000Z",
29
+ "baseline_version_from": "0.4.0",
30
+ "baseline_version_to": "0.5.0",
31
+ "files": [
32
+ {
33
+ "rel": "docs/init/seed.md",
34
+ "base_sha256": "<hex>",
35
+ "incoming_sha256": "<hex>",
36
+ "local_sha256": "<hex>",
37
+ "status": "PENDING"
38
+ }
39
+ ]
40
+ }
41
+ ```
42
+ - For each entry in `files`, three artifacts are present:
43
+ - `<rel>.baseline-base` — the **BASE** content (the file as it was when the user last installed the baseline).
44
+ - `<rel>.baseline-incoming` — the **INCOMING** content (the file as it ships in the new baseline; INCOMING and REMOTE are the same thing).
45
+ - The LOCAL file remains at its real path inside the target tree (untouched by the CLI).
46
+
47
+ ## Procedure
48
+
49
+ 1. **Discover the stage.** Read `.claude/state/upgrade/` and pick the most-recent stage directory whose manifest has at least one file with `status: PENDING` or `status: NEEDS_USER_INPUT`. If no such stage exists, tell the user "No pending stage to reconcile" and exit.
50
+ 2. **Per file**, in the order they appear in the stage manifest:
51
+ - Read BASE, INCOMING, and LOCAL.
52
+ - Reason about the three-way delta. Identify what changed between BASE → INCOMING (the upstream edit), what changed between BASE → LOCAL (the user edit), and where they conflict.
53
+ - If both edits are textually non-overlapping, the CLI would have routed the file to tier 2 (mechanical merge). The fact that the file is in tier 3 means structural reconciliation is needed — most commonly: both sides inserted content at the same structural anchor (a new section, a new numbered article, a new TOC entry).
54
+ - Apply the **zero-drift renumbering rule** below.
55
+ - Write the reconciled bytes to the LOCAL path.
56
+ - Update the stage manifest entry's `status` to `RECONCILED`.
57
+ 3. **Finalize the stage.** When every entry's status is `RECONCILED`, delete the stage directory (`rm -rf .claude/state/upgrade/<ts>/`). Report per-file status to the user.
58
+
59
+ ## The zero-drift renumbering rule (binding)
60
+
61
+ When BASE → INCOMING adds a new structural entry (a new Article, a new section, a new numbered item) at position N, and BASE → LOCAL added the user's own entry at the same position N, you SHALL renumber the user's entry to the **next available** slot (N+1) — you SHALL **never fold** the user's entry into an existing baseline section.
62
+
63
+ Concrete example (the Article-XI reproducer):
64
+ - BASE `seed.md` ends at Article X.
65
+ - LOCAL has added a project-specific `## Article XI (user content)`.
66
+ - INCOMING ships a new baseline `## Article XI (Skill provenance and the baseline manifest)`.
67
+
68
+ The reconciled `seed.md` SHALL contain:
69
+ - `## Article XI` — the baseline's content (verbatim).
70
+ - `## Article XII` — the user's prior content, renumbered.
71
+ - Every cross-reference in the document that pointed to "Article XI" SHALL be updated to point to either Article XI (when the reference was always to the new baseline content) or Article XII (when the reference was to what was previously Article XI). Surface ambiguous references as `NEEDS_USER_INPUT` per the fallback below.
72
+
73
+ The reason **shift, never fold**: the next baseline upgrade SHALL produce zero new staging entries for this file. If the user's content were folded into an existing baseline section, the next upgrade would re-detect a customization and re-stage. The renumbering preserves both bodies as independent structural units, so subsequent upgrades see exactly the baseline-owned portion (Articles I–XI) as unchanged.
74
+
75
+ The same principle applies recursively. If a later baseline ships Article XII and the user's content has been at Article XII since the prior upgrade, shift the user's content to Article XIII. Always shift to the next available slot.
76
+
77
+ ## `--dry-run` mode
78
+
79
+ When invoked with `args=dry-run` (e.g., `/upgrade-project dry-run`):
80
+
81
+ - Per file, produce the reconciled bytes in your reasoning, then emit a colorized unified diff (LOCAL vs reconciled) to the skill's terminal output rather than writing.
82
+ - DO NOT modify any LOCAL file.
83
+ - DO NOT update the stage manifest (statuses stay PENDING / NEEDS_USER_INPUT).
84
+ - DO NOT delete the stage directory.
85
+ - Tell the user: "Dry-run complete. Re-run without `dry-run` to apply."
86
+
87
+ Dry-run mode is for building trust in early use. After the first few successful reconciliations, the user typically stops dry-running.
88
+
89
+ ## Fallback — NEEDS_USER_INPUT
90
+
91
+ When you genuinely cannot disambiguate intent — the conflict has multiple plausible reconciliations and you cannot pick one without guessing the user's preference — apply the **NEEDS_USER_INPUT** fallback rather than picking arbitrarily:
92
+
93
+ 1. Update the stage manifest entry's `status` to `NEEDS_USER_INPUT`.
94
+ 2. Leave BASE, INCOMING, and LOCAL artifacts in place (do NOT delete the stage).
95
+ 3. Surface a targeted question to the user that names the file, summarizes the conflict in one sentence, and offers concrete options. Example: "Cannot disambiguate `docs/init/seed.md` Article XI: the baseline's new Article XI heading shares the user's chosen heading text. Should I (a) treat them as the same article and merge bodies, or (b) renumber the user's article to XII?"
96
+ 4. Exit clean. The user provides direction in their next prompt. A subsequent `/upgrade-project` invocation re-reads the stage manifest, finds the `NEEDS_USER_INPUT` entry, and re-attempts with the user's direction.
97
+
98
+ Use this fallback sparingly. The rework's whole point is that LLM judgment exceeds `git merge-file` for structural conflicts; if you punt to NEEDS_USER_INPUT for trivial reconciliations, you defeat the purpose.
99
+
100
+ ## Constraints
101
+
102
+ - **Validate `rel` before writing.** Before writing reconciled bytes to LOCAL, you SHALL verify that the resolved absolute path of `<target>/<rel>` is a descendant of `target`. A `rel` value that escapes the target tree (`../`, absolute path, symlink-resolved escape) SHALL be rejected as a `NEEDS_USER_INPUT` fallback with the reason `path-traversal-rejected`. The CLI's stage writer never produces escaping `rel` values, so this catches only tampered stage manifests from a local attacker with `.claude/state/` write access — defense in depth.
103
+ - **No write outside the stage directory and the LOCAL path.** You SHALL NOT touch `.claude/.baseline-prior/`, the installed `.baseline-manifest.json`, or any other CLI state.
104
+ - **No partial writes per file.** The reconciled LOCAL must be the complete final content. If you cannot produce a complete reconciliation, use the NEEDS_USER_INPUT fallback and leave LOCAL unmodified.
105
+ - **Honor Article XI of CLAUDE.md.** This skill only touches files explicitly staged by the CLI — which, by construction, are baseline-owned. User-added files at colliding paths are never staged.
106
+ - **No commits.** Reconciled files land on the working tree; the user inspects via `git diff` and commits when satisfied.
107
+ - **No re-fetching from npm.** BASE is already on disk in the stage; no network round-trip needed.
108
+
109
+ ## Output
110
+
111
+ After running, report per file:
112
+
113
+ ```
114
+ # /upgrade-project — <stage_ts>
115
+
116
+ - <rel>: RECONCILED (N lines changed)
117
+ - <rel>: NEEDS_USER_INPUT — <one-sentence question>
118
+ - <rel>: SKIPPED (dry-run)
119
+
120
+ Stage deleted: yes | no (NEEDS_USER_INPUT pending)
121
+ ```
@@ -40,7 +40,7 @@ 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, 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."
43
+ > "This repo has the Claude Code baseline installed (22 hooks, 1 subagent, 38 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."
@@ -69,7 +69,8 @@ The 11-phase workflow is the only sanctioned path from request to commit. Phase
69
69
  | 10 | Document | `/document` | docs |
70
70
  | 10.5 | Archive | `/archive` | bundle at `docs/archive/<date>/<slug>/` |
71
71
  | 10.6 | Memory flush | `/memory-flush` | curated canonical memory + reset `_pending.md` |
72
- | 11 | **Grant commit** (gate C) + commit | user runs **`/grant-commit`**, then `/commit` (skill) | commit |
72
+ | 11 | **Grant commit** (gate C) + changelog + commit | user runs **`/grant-commit`**, then `/changelog` (skill, sub-step 11.5), then `/commit` (skill) | commit |
73
+ | 11.5 | Changelog (Phase 11 sub-step) | `/changelog` (skill); harness auto-invokes between gate C and `/commit` | `CHANGELOG.md` `## [Unreleased]` section grows + `.claude/state/changelog/<slug>.json` |
73
74
 
74
75
  **Mandatory rules:**
75
76
 
@@ -292,7 +293,7 @@ Cryptographic supply-chain attestation, signed lock files, and per-skill aggrega
292
293
  |---|---|
293
294
  | `.claude/hooks/` | 22 hook scripts (17 write/run-boundary + 4 lifecycle + 1 input-boundary). Bash + python3, no jq. |
294
295
  | `.claude/agents/` | 1 baseline subagent: `swarm-worker` (rendered from `src/agents/swarm-worker.template.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/skills/` | 38 skills: artifact (4) + phases (11) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1) + maintenance (1) |
296
297
  | `.claude/commands/` | 5 consent/bootstrap gates: `approve-spec`, `approve-swarm`, `grant-commit`, `grant-push`, `init-project` |
297
298
  | `.claude/memory/` | 7 canonical knowledge files + `_pending.md` (staging) + `_resume.md` (continuity snapshot) + `README.md` |
298
299
  | `.claude/project.json` | per-project config (test/lint cmd, TDD globs, destructive patterns, swarm config, additions). Populated by `/init-project`. |
@@ -307,8 +308,8 @@ Cryptographic supply-chain attestation, signed lock files, and per-skill aggrega
307
308
  **Artifact drafting (4)** — each ships a `template.md`:
308
309
  - `intake` (Phase 1), `brd` (cross-functional pre-spec), `spec` (Phase 4, diagram-driven), `rca` (out-of-band postmortem)
309
310
 
310
- **Workflow phases (10)** — auto-invocable; orchestrator chains them:
311
- - `triage`, `scout`, `research`, `tdd`, `simplify`, `security`, `integrate`, `document`, `archive`, `commit`
311
+ **Workflow phases (11)** — auto-invocable; orchestrator chains them:
312
+ - `triage`, `scout`, `research`, `tdd`, `simplify`, `security`, `integrate`, `document`, `archive`, `changelog` (Phase 11.5), `commit`
312
313
 
313
314
  **Phase workers (5)** — execute pre-decided recipes; mandatorily invoke a sub-skill:
314
315
  - `scenario`, `implement`, `verify`, `prose`, `design-ui`
@@ -11,7 +11,7 @@
11
11
 
12
12
  **Mandatory binding language.** Each numbered section (§) below specifies a binding requirement for the baseline. Implementations SHALL conform; `CLAUDE.md` Articles SHALL reference the corresponding §; project amendments (per `CLAUDE.md` Art. X) SHALL NOT contradict any § here.
13
13
 
14
- The baseline turns soft engineering rules (no unauthorized commits, no stubs, no mocks of internal code, no self-approved specs) into structural guarantees enforced by write-boundary hooks. Eleven workflow phases plus one stripped-down chore track (skips TDD; runs verify + archive mandatorily, simplify/integrate/document conditionally), seventeen write/run-boundary guards plus four lifecycle hooks plus one input-boundary hook (twenty-two hook scripts total — twenty `.sh` + two `.mjs` after the JS-port pilot), thirty-seven skills, one subagent, and four consent gates. Decisions live in main context; the lone subagent (`swarm-worker`) executes pre-decided recipes in parallel worktrees during `/swarm-dispatch`. Every artifact is archived; every third-party API is looked up against live docs. Project memory accumulates across sessions in `.claude/memory/` — auto-extracted by a Stop hook, curated in main context via `/memory-flush`, self-healing via re-verification.
14
+ The baseline turns soft engineering rules (no unauthorized commits, no stubs, no mocks of internal code, no self-approved specs) into structural guarantees enforced by write-boundary hooks. Eleven workflow phases plus one stripped-down chore track (skips TDD; runs verify + archive mandatorily, simplify/integrate/document conditionally), seventeen write/run-boundary guards plus four lifecycle hooks plus one input-boundary hook (twenty-two hook scripts total — twenty `.sh` + two `.mjs` after the JS-port pilot), thirty-eight skills, one subagent, and four consent gates. Decisions live in main context; the lone subagent (`swarm-worker`) executes pre-decided recipes in parallel worktrees during `/swarm-dispatch`. Every artifact is archived; every third-party API is looked up against live docs. Project memory accumulates across sessions in `.claude/memory/` — auto-extracted by a Stop hook, curated in main context via `/memory-flush`, self-healing via re-verification.
15
15
 
16
16
  ---
17
17
 
@@ -110,7 +110,7 @@ Applies to every language. Mappings for TSX, Node, Python, Go, Rust ship inside
110
110
  │ │ └── lib/common.sh # shared helpers
111
111
  │ ├── agents/ # 1 subagent: swarm-worker (rendered from src/agents/swarm-worker.template.md)
112
112
  │ ├── commands/ # 5 consent/bootstrap gates (user-only — structurally)
113
- │ ├── skills/ # 37 skills: artifact (4) + phases (11) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1)
113
+ │ ├── skills/ # 38 skills: artifact (4) + phases (11) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1) + maintenance (1)
114
114
  │ ├── memory/ # project memory: 7 canonical files + _pending.md (gitignored body) + README.md
115
115
  │ └── state/ # runtime: workflow.json, approvals, swarm plans, verdicts, logs
116
116
  ├── src/ # pristine ship-time templates (overlay source for `npx @friedbotstudio/create-baseline`)
@@ -181,7 +181,7 @@ The baseline ships exactly one subagent. The architectural reason: subagents los
181
181
 
182
182
  **Automated re-rendering by `/init-project`.** Step 6.4 re-renders `swarm-worker.md` from the template, driven by the recommender's `additions.swarm_worker_skills`. The recommender does **not** propose new subagent types — only stack-skill additions for the existing worker. Specialization happens via skills loaded into the worker's context, not via parallel agent personas; new decision-making roles belong in skills, which run in main context.
183
183
 
184
- ### §4.3 Skills (37)
184
+ ### §4.3 Skills (38)
185
185
 
186
186
  Each at `.claude/skills/<name>/SKILL.md`, frontmatter `name` + `description`, plus optional `template.md` (artifact skills) or helper scripts.
187
187
 
@@ -516,7 +516,7 @@ Seed-level requirement: no stale workflow artifacts in the working tree after co
516
516
 
517
517
  **Step 4:** Write `src/agents/swarm-worker.template.md` (canonical-body store, per §4.2) — the only subagent template. Then render `.claude/agents/swarm-worker.md` from it with default tokens. The template carries four tokens — `{{NAME}}`, `{{DESCRIPTION}}`, `{{SKILLS}}`, `{{ROLE_LINE}}`. Default `SKILLS` is the YAML list block ` - scenario\n - implement` (the worker's two mandatory sub-skills). Render-parity holds at this stage. `/init-project` later re-renders the worker with stack-aware tokens when the recommender flags stack-specific skills to preload via `additions.swarm_worker_skills`.
518
518
 
519
- **Step 5:** Write `.claude/skills/` for the 37 skills (§4.3) — 29 workflow/worker/orchestration/memory/alt-track skills you author (the +1 over 28 is the `changelog` Phase 11.5 skill) plus 7 shared globals plus 1 audit skill. The breakdown: artifact drafting (4) + workflow phases (10) + phase workers (5: `scenario`, `implement`, `verify`, `prose`, `design-ui`) + spec helpers (4: `spec-lint`, `spec-render`, `spec-diagram-review`, `spec-traceability-review`) + orchestration (3: `harness`, `swarm-plan`, `swarm-dispatch`) + memory (1: `memory-flush`) + shared globals (7: `claude-automation-recommender`, `code-structure`, `humanizer`, `documentation`, `technical-tutorials`, `copywriting`, `impeccable`) + drift defender (1: `audit-baseline`) + alternate tracks (1: `chore`). The vendored `claude-automation-recommender` (Apache 2.0, from `claude-code-setup`), the writing/quality globals, and the design global ship unchanged with their licenses intact. Artifact skills (intake, brd, spec, rca) each ship a `template.md`. Helper scripts: swarm-plan gets `validate.sh`, swarm-dispatch gets `swarm_merge.sh`, spec-render gets `render.sh`, spec-lint gets `lint.sh`, archive gets `archive.sh`, audit-baseline gets `audit.sh`. All helper scripts `chmod +x`.
519
+ **Step 5:** Write `.claude/skills/` for the 38 skills (§4.3) — 29 workflow/worker/orchestration/memory/alt-track skills you author (the +1 over 28 is the `changelog` Phase 11.5 skill) plus 7 shared globals plus 1 audit skill plus 1 maintenance skill. The breakdown: artifact drafting (4) + workflow phases (10) + phase workers (5: `scenario`, `implement`, `verify`, `prose`, `design-ui`) + spec helpers (4: `spec-lint`, `spec-render`, `spec-diagram-review`, `spec-traceability-review`) + orchestration (3: `harness`, `swarm-plan`, `swarm-dispatch`) + memory (1: `memory-flush`) + shared globals (7: `claude-automation-recommender`, `code-structure`, `humanizer`, `documentation`, `technical-tutorials`, `copywriting`, `impeccable`) + drift defender (1: `audit-baseline`) + alternate tracks (1: `chore`) + maintenance (1: `upgrade-project`). The vendored `claude-automation-recommender` (Apache 2.0, from `claude-code-setup`), the writing/quality globals, and the design global ship unchanged with their licenses intact. Artifact skills (intake, brd, spec, rca) each ship a `template.md`. Helper scripts: swarm-plan gets `validate.sh`, swarm-dispatch gets `swarm_merge.sh`, spec-render gets `render.sh`, spec-lint gets `lint.sh`, archive gets `archive.sh`, audit-baseline gets `audit.sh`. All helper scripts `chmod +x`.
520
520
 
521
521
  **Step 6:** Write `.claude/commands/*.md` for the 4 gates (§4.4). All carry `disable-model-invocation: true` as belt-and-braces; structural user-only is enforced by their directory.
522
522
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@friedbotstudio/create-baseline",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Node CLI scaffolder that materializes the Claude Code baseline (hooks, skills, commands, MCP servers, governance docs) into a target project, with branded interactive install / upgrade / doctor flows. Run via `npx @friedbotstudio/create-baseline <target>`.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,7 +40,7 @@ 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, 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."
43
+ > "This repo has the Claude Code baseline installed (22 hooks, 1 subagent, 38 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."
@@ -69,7 +69,8 @@ The 11-phase workflow is the only sanctioned path from request to commit. Phase
69
69
  | 10 | Document | `/document` | docs |
70
70
  | 10.5 | Archive | `/archive` | bundle at `docs/archive/<date>/<slug>/` |
71
71
  | 10.6 | Memory flush | `/memory-flush` | curated canonical memory + reset `_pending.md` |
72
- | 11 | **Grant commit** (gate C) + commit | user runs **`/grant-commit`**, then `/commit` (skill) | commit |
72
+ | 11 | **Grant commit** (gate C) + changelog + commit | user runs **`/grant-commit`**, then `/changelog` (skill, sub-step 11.5), then `/commit` (skill) | commit |
73
+ | 11.5 | Changelog (Phase 11 sub-step) | `/changelog` (skill); harness auto-invokes between gate C and `/commit` | `CHANGELOG.md` `## [Unreleased]` section grows + `.claude/state/changelog/<slug>.json` |
73
74
 
74
75
  **Mandatory rules:**
75
76
 
@@ -292,7 +293,7 @@ Cryptographic supply-chain attestation, signed lock files, and per-skill aggrega
292
293
  |---|---|
293
294
  | `.claude/hooks/` | 22 hook scripts (17 write/run-boundary + 4 lifecycle + 1 input-boundary). Bash + python3, no jq. |
294
295
  | `.claude/agents/` | 1 baseline subagent: `swarm-worker` (rendered from `src/agents/swarm-worker.template.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/skills/` | 38 skills: artifact (4) + phases (11) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1) + maintenance (1) |
296
297
  | `.claude/commands/` | 5 consent/bootstrap gates: `approve-spec`, `approve-swarm`, `grant-commit`, `grant-push`, `init-project` |
297
298
  | `.claude/memory/` | 7 canonical knowledge files + `_pending.md` (staging) + `_resume.md` (continuity snapshot) + `README.md` |
298
299
  | `.claude/project.json` | per-project config (test/lint cmd, TDD globs, destructive patterns, swarm config, additions). Populated by `/init-project`. |
@@ -307,8 +308,8 @@ Cryptographic supply-chain attestation, signed lock files, and per-skill aggrega
307
308
  **Artifact drafting (4)** — each ships a `template.md`:
308
309
  - `intake` (Phase 1), `brd` (cross-functional pre-spec), `spec` (Phase 4, diagram-driven), `rca` (out-of-band postmortem)
309
310
 
310
- **Workflow phases (10)** — auto-invocable; orchestrator chains them:
311
- - `triage`, `scout`, `research`, `tdd`, `simplify`, `security`, `integrate`, `document`, `archive`, `commit`
311
+ **Workflow phases (11)** — auto-invocable; orchestrator chains them:
312
+ - `triage`, `scout`, `research`, `tdd`, `simplify`, `security`, `integrate`, `document`, `archive`, `changelog` (Phase 11.5), `commit`
312
313
 
313
314
  **Phase workers (5)** — execute pre-decided recipes; mandatorily invoke a sub-skill:
314
315
  - `scenario`, `implement`, `verify`, `prose`, `design-ui`
@@ -0,0 +1,54 @@
1
+ // Foundation — line-level unified-diff renderer used by the upgrade TUI's
2
+ // "Show diff" prompt. Pure function; no IO, no side effects.
3
+
4
+ const ANSI_RED = '\x1b[31m';
5
+ const ANSI_GREEN = '\x1b[32m';
6
+ const ANSI_RESET = '\x1b[0m';
7
+
8
+ export function renderUnifiedDiff(localText, incomingText, opts = {}) {
9
+ const colorize = opts.colorize === true;
10
+ const ops = diffLines(splitLines(localText), splitLines(incomingText));
11
+ return ops.map((op) => renderOp(op, colorize)).join('\n');
12
+ }
13
+
14
+ function splitLines(text) {
15
+ return String(text).split('\n');
16
+ }
17
+
18
+ function renderOp(op, colorize) {
19
+ if (op.kind === 'context') return ' ' + op.line;
20
+ const marker = op.kind === 'remove' ? '-' : '+';
21
+ if (!colorize) return marker + op.line;
22
+ const color = op.kind === 'remove' ? ANSI_RED : ANSI_GREEN;
23
+ return color + marker + op.line + ANSI_RESET;
24
+ }
25
+
26
+ function diffLines(a, b) {
27
+ const m = a.length;
28
+ const n = b.length;
29
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
30
+ for (let i = 1; i <= m; i++) {
31
+ for (let j = 1; j <= n; j++) {
32
+ if (a[i - 1] === b[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
33
+ else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
34
+ }
35
+ }
36
+ const ops = [];
37
+ let i = m;
38
+ let j = n;
39
+ while (i > 0 && j > 0) {
40
+ if (a[i - 1] === b[j - 1]) {
41
+ ops.push({ kind: 'context', line: a[i - 1] });
42
+ i--; j--;
43
+ } else if (dp[i - 1][j] >= dp[i][j - 1]) {
44
+ ops.push({ kind: 'remove', line: a[i - 1] });
45
+ i--;
46
+ } else {
47
+ ops.push({ kind: 'add', line: b[j - 1] });
48
+ j--;
49
+ }
50
+ }
51
+ while (i > 0) { ops.push({ kind: 'remove', line: a[i - 1] }); i--; }
52
+ while (j > 0) { ops.push({ kind: 'add', line: b[j - 1] }); j--; }
53
+ return ops.reverse();
54
+ }
@@ -32,14 +32,43 @@ async function listFiles(root, base = root, acc = []) {
32
32
  return acc;
33
33
  }
34
34
 
35
+ async function readPackageVersion() {
36
+ try {
37
+ const pkgPath = join(PACKAGE_ROOT, 'package.json');
38
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
39
+ return typeof pkg.version === 'string' && pkg.version.length > 0 ? pkg.version : '0.0.0';
40
+ } catch {
41
+ return '0.0.0';
42
+ }
43
+ }
44
+
35
45
  async function writeBaselineManifest(target) {
36
46
  const files = await listFiles(target);
37
- const filtered = files.filter((p) => p !== '.claude/.baseline-manifest.json');
38
- const m = await buildManifestFromDir(target, filtered);
47
+ const filtered = files.filter((p) =>
48
+ p !== '.claude/.baseline-manifest.json' && !p.startsWith('.claude/.baseline-prior/')
49
+ );
50
+ const baseline_version = await readPackageVersion();
51
+ const m = await buildManifestFromDir(target, filtered, { baseline_version });
39
52
  await mkdir(join(target, '.claude'), { recursive: true });
40
53
  await saveManifest(join(target, '.claude/.baseline-manifest.json'), m);
41
54
  }
42
55
 
56
+ async function writeBaselinePriorMirror(templateDir, target) {
57
+ const priorRoot = join(target, '.claude/.baseline-prior');
58
+ await mkdir(priorRoot, { recursive: true });
59
+ await cp(templateDir, priorRoot, {
60
+ recursive: true,
61
+ force: true,
62
+ filter: (src) => {
63
+ const rel = relative(templateDir, src).split(sep).join('/');
64
+ if (rel === '') return true;
65
+ if (COPY_EXCLUDE.includes(rel)) return false;
66
+ return true;
67
+ },
68
+ });
69
+ await writeFile(join(priorRoot, '.gitignore'), '*\n');
70
+ }
71
+
43
72
  function makeFilter(opts) {
44
73
  return (src, _dest) => {
45
74
  const rel = relative(opts.templateRoot, src).split(sep).join('/');
@@ -89,6 +118,7 @@ export async function freshInstall(templateDir, target, opts = {}) {
89
118
  await cp(templateDir, target, { recursive: true, force: false, filter });
90
119
  await applySpecialAndNeverTouch(templateDir, target);
91
120
  if (opts.withNpmrc === true) await materializeNpmrc(target);
121
+ await writeBaselinePriorMirror(templateDir, target);
92
122
  await writeBaselineManifest(target);
93
123
  }
94
124
 
@@ -97,5 +127,6 @@ export async function forceInstall(templateDir, target, opts = {}) {
97
127
  await cp(templateDir, target, { recursive: true, force: true, filter });
98
128
  await applySpecialAndNeverTouch(templateDir, target);
99
129
  if (opts.withNpmrc === true) await materializeNpmrc(target);
130
+ await writeBaselinePriorMirror(templateDir, target);
100
131
  await writeBaselineManifest(target);
101
132
  }
@@ -2,7 +2,7 @@ import { readFile, writeFile } from 'node:fs/promises';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { join } from 'node:path';
4
4
 
5
- export const MANIFEST_VERSION = 1;
5
+ export const MANIFEST_VERSION = 2;
6
6
 
7
7
  export async function hashFile(path) {
8
8
  const buf = await readFile(path);
@@ -24,15 +24,19 @@ export async function saveManifest(path, m) {
24
24
  await writeFile(path, JSON.stringify(m, null, 2) + '\n');
25
25
  }
26
26
 
27
- export async function buildManifestFromDir(rootDir, fileList) {
27
+ export async function buildManifestFromDir(rootDir, fileList, opts = {}) {
28
28
  const files = {};
29
29
  const sorted = [...fileList].sort();
30
30
  for (const rel of sorted) {
31
31
  files[rel] = await hashFile(join(rootDir, rel));
32
32
  }
33
- return {
33
+ const manifest = {
34
34
  manifest_version: MANIFEST_VERSION,
35
35
  generated_at: new Date().toISOString(),
36
36
  files,
37
37
  };
38
+ if (typeof opts.baseline_version === 'string' && opts.baseline_version.length > 0) {
39
+ manifest.baseline_version = opts.baseline_version;
40
+ }
41
+ return manifest;
38
42
  }
package/src/cli/merge.js CHANGED
@@ -4,6 +4,7 @@ import { hashFile, saveManifest } from './manifest.js';
4
4
  import { deepMergeMcpServers } from './mcp.js';
5
5
  import { NEVER_TOUCH, SPECIAL_MERGE } from './install.js';
6
6
  import { pathExists } from './util.js';
7
+ import { dispatchByTier, NoBaseError } from './upgrade-tiers.js';
7
8
 
8
9
  export const ACTION_KINDS = Object.freeze({
9
10
  ADD: 'ADD',
@@ -15,6 +16,9 @@ export const ACTION_KINDS = Object.freeze({
15
16
  NEVER_TOUCH_PRESERVE: 'NEVER_TOUCH_PRESERVE',
16
17
  NEVER_TOUCH_ADD: 'NEVER_TOUCH_ADD',
17
18
  SPECIAL_MERGE: 'SPECIAL_MERGE',
19
+ MECHANICAL_MERGE_CLEAN: 'MECHANICAL_MERGE_CLEAN',
20
+ MECHANICAL_MERGE_CONFLICTED: 'MECHANICAL_MERGE_CONFLICTED',
21
+ SEMANTIC_MERGE_STAGED: 'SEMANTIC_MERGE_STAGED',
18
22
  });
19
23
 
20
24
  async function copyFile(src, dst) {
@@ -22,13 +26,40 @@ async function copyFile(src, dst) {
22
26
  await cp(src, dst, { force: true });
23
27
  }
24
28
 
29
+ function readShaFromEntry(entry) {
30
+ if (typeof entry === 'string') return entry;
31
+ if (entry && typeof entry === 'object' && typeof entry.sha256 === 'string') return entry.sha256;
32
+ return null;
33
+ }
34
+
35
+ function readTierFromEntry(entry) {
36
+ if (entry && typeof entry === 'object' && typeof entry.tier === 'string') return entry.tier;
37
+ // Bare-sha entries (legacy shipped manifest_version: 2 OR installed-manifest
38
+ // round-trips without tier overlay) fall back to BINARY_PROMPT — the safe
39
+ // default that preserves today's two-way prompt behavior. New shipped
40
+ // manifests (v3+) carry `{sha256, tier}` per file and exercise the full
41
+ // three-tier flow.
42
+ return 'BINARY_PROMPT';
43
+ }
44
+
25
45
  export async function threeWayMerge(templateDir, target, oldManifest, newManifest, opts = {}) {
26
- const { dryRun = false, onSkipCustomized = null } = opts;
46
+ const { dryRun = false, onSkipCustomized = null, pack = null } = opts;
27
47
  const actions = [];
28
48
  const oldFiles = oldManifest?.files ?? {};
29
49
  const newFiles = newManifest?.files ?? {};
50
+ const baseline_version = oldManifest?.baseline_version;
30
51
  const allPaths = new Set([...Object.keys(oldFiles), ...Object.keys(newFiles)]);
31
52
 
53
+ const tierCtx = {
54
+ target,
55
+ templateDir,
56
+ oldManifest,
57
+ newManifest,
58
+ baseline_version,
59
+ pack,
60
+ stageRunTs: null,
61
+ };
62
+
32
63
  for (const rel of allPaths) {
33
64
  const tplPath = join(templateDir, rel);
34
65
  const tgtPath = join(target, rel);
@@ -51,8 +82,10 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
51
82
  continue;
52
83
  }
53
84
 
54
- const newHash = newFiles[rel];
55
- const oldHash = oldFiles[rel];
85
+ const newEntry = newFiles[rel];
86
+ const oldEntry = oldFiles[rel];
87
+ const newHash = readShaFromEntry(newEntry);
88
+ const oldHash = readShaFromEntry(oldEntry);
56
89
  const targetExists = await pathExists(tgtPath);
57
90
  const tgtHash = targetExists ? await hashFile(tgtPath) : null;
58
91
 
@@ -74,13 +107,10 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
74
107
  }
75
108
 
76
109
  if (newHash && tgtHash && tgtHash !== oldHash) {
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
- }
110
+ const action = await dispatchCustomized({
111
+ rel, newEntry, tierCtx, dryRun, onSkipCustomized, tplPath, tgtPath,
112
+ });
113
+ actions.push(action);
84
114
  continue;
85
115
  }
86
116
 
@@ -106,7 +136,44 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
106
136
  await saveManifest(join(target, '.claude/.baseline-manifest.json'), newManifest);
107
137
  }
108
138
 
109
- const skipKinds = [ACTION_KINDS.SKIP_CUSTOMIZED, ACTION_KINDS.PRUNE_SKIPPED_CUSTOMIZED];
110
- const exitCode = actions.some((a) => skipKinds.includes(a.kind)) ? 3 : 0;
111
- return { actions, exitCode };
139
+ return { actions, exitCode: computeExitCode(actions) };
140
+ }
141
+
142
+ async function dispatchCustomized({ rel, newEntry, tierCtx, dryRun, onSkipCustomized, tplPath, tgtPath }) {
143
+ const tier = readTierFromEntry(newEntry);
144
+ if (tier === 'MECHANICAL' || tier === 'SEMANTIC') {
145
+ if (dryRun) {
146
+ return { kind: tier === 'MECHANICAL' ? ACTION_KINDS.MECHANICAL_MERGE_CLEAN : ACTION_KINDS.SEMANTIC_MERGE_STAGED, path: rel, reason: 'dry-run: tier dispatch deferred' };
147
+ }
148
+ try {
149
+ return await dispatchByTier(rel, tier, tierCtx);
150
+ } catch (err) {
151
+ if (err instanceof NoBaseError) {
152
+ return fallbackToBinaryPrompt({ rel, onSkipCustomized, dryRun, tplPath, tgtPath, err });
153
+ }
154
+ throw err;
155
+ }
156
+ }
157
+ return fallbackToBinaryPrompt({ rel, onSkipCustomized, dryRun, tplPath, tgtPath });
158
+ }
159
+
160
+ async function fallbackToBinaryPrompt({ rel, onSkipCustomized, dryRun, tplPath, tgtPath, err = null }) {
161
+ const choice = onSkipCustomized ? await onSkipCustomized(rel) : 'keep-mine';
162
+ if (choice === 'take-theirs') {
163
+ if (!dryRun) await copyFile(tplPath, tgtPath);
164
+ return { kind: ACTION_KINDS.OVERWRITE, path: rel, reason: err ? `BASE recovery failed (${err.kind}); user chose take-theirs` : 'customized file; user chose take-theirs' };
165
+ }
166
+ return { kind: ACTION_KINDS.SKIP_CUSTOMIZED, path: rel, reason: err ? `BASE recovery failed (${err.kind}); preserved` : 'target customized since last install' };
167
+ }
168
+
169
+ function computeExitCode(actions) {
170
+ let code = 0;
171
+ for (const a of actions) {
172
+ if (a.kind === ACTION_KINDS.SEMANTIC_MERGE_STAGED) code = Math.max(code, 5);
173
+ else if (a.kind === ACTION_KINDS.MECHANICAL_MERGE_CONFLICTED) code = Math.max(code, 4);
174
+ else if (a.kind === ACTION_KINDS.SKIP_CUSTOMIZED || a.kind === ACTION_KINDS.PRUNE_SKIPPED_CUSTOMIZED) {
175
+ code = Math.max(code, 3);
176
+ }
177
+ }
178
+ return code;
112
179
  }