@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.
- package/README.md +1 -1
- package/bin/cli.js +33 -6
- package/obj/template/.claude/manifest.json +916 -230
- package/obj/template/.claude/skills/audit-baseline/audit.sh +4 -1
- package/obj/template/.claude/skills/upgrade-project/SKILL.md +121 -0
- package/obj/template/CLAUDE.md +6 -5
- package/obj/template/docs/init/seed.md +4 -4
- package/package.json +1 -1
- package/src/CLAUDE.template.md +6 -5
- package/src/cli/diff-render.js +54 -0
- package/src/cli/install.js +33 -2
- package/src/cli/manifest.js +7 -3
- package/src/cli/merge.js +80 -13
- package/src/cli/tui/upgrade.js +122 -25
- package/src/cli/upgrade-tiers.js +234 -0
- package/src/seed.template.md +4 -4
|
@@ -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,
|
|
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
|
+
```
|
package/obj/template/CLAUDE.md
CHANGED
|
@@ -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,
|
|
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/` |
|
|
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 (
|
|
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-
|
|
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/ #
|
|
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 (
|
|
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
|
|
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.
|
|
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": {
|
package/src/CLAUDE.template.md
CHANGED
|
@@ -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,
|
|
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/` |
|
|
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 (
|
|
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
|
+
}
|
package/src/cli/install.js
CHANGED
|
@@ -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) =>
|
|
38
|
-
|
|
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
|
}
|
package/src/cli/manifest.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
55
|
-
const
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
}
|