Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable changes to this project. Format: [Keep a Changelog](https://keepacha

## Unreleased

- **feat: generate `templates/INDEX.md`, scaffold `.claude/workflows/`, and document teams / channels / routines.** Three gaps the currency audit left open. **(1)** `templates/INDEX.md` was hand-maintained and had gone stale enough to mislead — it still referenced a `configurator.html` that no longer exists and was missing half the modules. It is now **generated** from `MODULES` by `python3 configure.py --write-index`, and `--check` fails when the committed copy and the generator disagree, so it cannot drift again. **(2)** Dynamic workflows have been a first-class Claude Code surface since 2.1.154 and the configurator scaffolded nothing for them. The `multi-agent` module now ships `.claude/workflows/spec-fanout.js` (runs as `/spec-fanout`), which generates N variants of one spec into disjoint slots and then **screens each variant against the spec** before reporting. It is the workflow-native successor to the `/infinite` skill in the same module — same job, but the runtime holds the loop and the intermediate results, the run is resumable, and the screening pass is a real gate rather than a suggestion. Project workflows under `.claude/workflows/` are shared with everyone who clones the repo. `--check` gained a rule validating that every shipped workflow declares a usable `meta` block and uses no `import()` (the runtime rejects both). `workflowSizeGuideline` is stubbed in `settings.local.json.example`. **(3)** `docs/04` gains a table comparing the five ways to run work in parallel (subagent / skill / agent team / workflow / worktree session) by *who holds the plan*, and states plainly why the configurator ships no templates for agent teams, channels or routines: teams are spawned in conversation and live for a session (only `teammateMode` is worth setting, and it's a per-machine terminal preference — stubbed in settings.local); the channel gate keys `channelsEnabled` and `allowedChannelPlugins` are **managed-settings only**, so a project cannot enable them; and routines are scheduled cloud agents that run against a repo rather than from your checkout, where a `Stop` or `SessionStart` hook is the project-scoped equivalent.

- **fix(commands): rename `/review` → `/review-branch` so it stops shadowing the bundled `/code-review`.** CC 2.1.223 made `/review` the alias of the bundled `/code-review` — Claude Code's multi-agent reviewer, including the cloud `ultra` mode. A project skill of that name wins it (verified headlessly on 2.1.241: a project skill named `review` ran for `/review`, and the same held for `plan` against the built-in `/plan`), so **every scaffolded project was silently hiding the better built-in behind this simpler single-pass skill** — overlap that turned into a real capability loss the day the alias shipped. The skill moves to `templates/commands/review-branch/` with `name: review-branch`, and its description now positions it honestly ("a quick single-pass review; Claude Code's bundled `/code-review` is the deeper multi-agent one"). Both are reachable again. Updated across `config_schema.py`, `configure.py`'s pattern-integration map, `templates/INDEX.md`, the `/investigate` and `/plan-eng-review` cross-references, docs 02/03/05/09/10/11, README, and the example project. **Migration:** the configurator has no mechanism to delete a file it previously wrote, so an upgraded project keeps the old `.claude/skills/review/` alongside the new one — and the stale copy still shadows the alias. New `/verify-setup` **check 13** detects exactly that pair and tells the user to `rm -rf .claude/skills/review`. `/plan` is left alone deliberately: it shadows a built-in *command* rather than a bundled skill, and plan mode stays reachable via Shift+Tab, so it's a name clash rather than a lost capability — README now says so and points at the rename if you'd rather keep the shortcut.

- **docs: re-baseline the MCP context claims against tool search, and scope `/infinite` against dynamic workflows.** Two of the project's headline claims had been overtaken by Claude Code and were overstating what the modules buy. **MCP.** README claimed per-task profiles "drop a bloated 4-MCP baseline from ~49% context to under 5%", and `docs/04` asserted "every MCP tool is a chunk of JSON schema loaded at session start". Tool search defers MCP schemas by default (`alwaysLoad: true` is the opt-*out*), so the premise no longer holds. Measured rather than re-guessed — four local stdio servers advertising twelve tools each (48 total) against an otherwise identical one-turn session on CC 2.1.245: **26,665 tokens with no MCP servers, 27,361 deferred (+696, ~14/tool), 40,993 with `alwaysLoad: true` (+14,328, ~298/tool)** — deferral removes ~95% of the schema cost, and the numbers reproduced exactly across runs. The claim was also embedded in four *shipped* templates, which is worse than in the docs because it lands in every user's project: `check-context/SKILL.md` (its budget guardrails and the "MCP > 10%" flag), `claude-ctx.sh`'s rationale comment, `servers-cookbook.md` (which already explained deferral correctly a few sections earlier, so it contradicted itself), and the `mcp.minimal.json` profile comment. All corrected. Profiles are now documented for what they still genuinely buy — which servers *connect*: startup time, auth prompts, cold start, and the blast radius `--strict-mcp-config` enforces — and `docs/06` picks up the same correction. The dated `experiments-memory` example keeps its original result with a superseding **addendum** rather than a rewrite, because an experiment log records what was true when it ran. **`/infinite`.** Dynamic workflows now do staged, resumable, budgeted fan-out with structured output between stages; hand-rolled wave batching is the weaker instrument for that job. The skill opens with a decision table sending staged / merge-heavy / resumable work to a workflow, and keeps the one case it is genuinely good at — N variants of a single spec into disjoint slots with no cross-iteration coordination. README's module row and a new `docs/04` section say the same.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ All five preflight checks are silent on a clean default scaffold; informational
--force Kill-switch: skip the deep-merge AND the collision
strategy. Every existing file is overwritten with .bak-<ts>
(the pre-Tier-2 behavior). Implies --on-collision=overwrite.
--write-index Regenerate templates/INDEX.md from MODULES (maintainer
tool; --check fails when the committed index is stale).
--save-config FILE Save answers to FILE (plus scaffolding)
--save-config-only FILE Save answers only, no scaffolding
--check Static validation of templates + MODULES (CI gate);
Expand Down
6 changes: 4 additions & 2 deletions config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
"paths": [
"multi-agent/dot-claude/rules/multi-agent-guardrails.md",
"multi-agent/dot-claude/agents/parallel-generator.md",
"multi-agent/dot-claude/workflows/spec-fanout.js",
"commands/merge-worktrees/SKILL.md",
"commands/infinite/SKILL.md",
],
Expand Down Expand Up @@ -1100,10 +1101,11 @@ def target_path_for(template_rel: str):
if rest_path.startswith("agents/"):
agent_name = rest_path[len("agents/"):]
return f".claude/agents/{agent_name}"
# microbit-enforcer.sh routes to .claude/hooks/, not .claude/skills/.
# microbit-enforcer.sh / .ps1 route to .claude/hooks/, not .claude/skills/.
# The directory's settings-patch.json is consumed via extraSettingsPatch
# and never copied as a file (so no special-case skip needed).
if rest_path.startswith("microbit-enforcer/") and rest_path.endswith(".sh"):
if (rest_path.startswith("microbit-enforcer/")
and rest_path.endswith((".sh", ".ps1"))):
fname = rest_path[len("microbit-enforcer/"):]
return f".claude/hooks/{fname}"
return f".claude/skills/{rest_path}"
Expand Down
173 changes: 173 additions & 0 deletions configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ def compute_merged_settings(form_values: dict, selected: set, module_flags: dict
# Final pass: strip any `//`-prefixed keys at any nesting depth. Catches
# nested doc labels / stubs that escape the shallow per-merge filters.
settings = _strip_doc_labels(settings)

return settings


Expand Down Expand Up @@ -680,6 +681,139 @@ def _frontmatter_block(text: str) -> str:
return ""


INDEX_PATH = TEMPLATE_DIR / "INDEX.md"

INDEX_FOOTER = """## How settings merge works

Several modules contribute `hooks` entries, and they all land in one
`.claude/settings.json`. The CLI merges them (see `deep_merge_settings` and
`_merge_hook_groups` in `configure.py`): groups are keyed by `matcher`, inner
hooks are unioned by `command`, and a user's own entries are never rewritten.
If you're hand-copying instead, the shape is:

```json
{
"hooks": {
"PreToolUse": [ ...all matchers from all modules... ],
"PostToolUse": [ ... ],
"Stop": [ ... ]
}
}
```

Within one event, hooks from different modules concatenate; Claude Code runs
every matching entry.

## Path rewrites

- `*/dot-claude/*` -> `.claude/*` and `*/dot-github/*` -> `.github/*`. The
template tree avoids real dotfolders so it browses and syncs cleanly on
tools that special-case them.
- `mcp/mcp.json` -> `.mcp.json`; `mcp/profiles/mcp.<name>.json` ->
`.mcp.<name>.json` at the repo root; `mcp/servers-cookbook.md` ->
`docs/mcp-servers.md`; `mcp/claude-ctx.sh` -> `claude-ctx` (executable).
- Hook scripts are written executable, and with LF endings on every platform.
"""


def render_template_index() -> str:
"""Render templates/INDEX.md from MODULES — the same source the CLI
scaffolds from, so the index cannot drift from what actually ships.

The hand-maintained version went stale (it still referenced a
`configurator.html` that no longer exists and was missing half the
modules); `--check` now fails when this output and the file disagree.
Regenerate with `python3 configure.py --write-index`.
"""
lines = [
"# Template library index",
"",
"**Generated — do not edit by hand.** Produced from `MODULES` in",
"`config_schema.py` by `python3 configure.py --write-index`, and verified",
"by `python3 configure.py --check`.",
"",
"Every module contributes drop-in files for a target project. `cc-configure`",
"composes the selected modules; you can also copy any file directly.",
"",
]
for m in MODULES:
flags = m.get("flags") or {}
lines.append(f"## `{m['id']}`" + (" *(required)*" if m.get("required") else ""))
lines.append("")
desc = (m.get("description") or "").strip()
if desc:
lines.append(desc.split(". ")[0].rstrip(".") + ".")
lines.append("")
paths = m.get("paths") or []
if paths:
lines.append("| Template | Installs to |")
lines.append("|---|---|")
for p in paths:
try:
target = target_path_for(p)
except Exception:
target = "(computed at scaffold time)"
lines.append(f"| `{p}` | `{target}` |")
lines.append("")
extras = []
if m.get("settingsPatch"):
extras.append(f"`{m['settingsPatch']}` merges into `.claude/settings.json`")
if m.get("extraSettingsHook"):
extras.append("registers hook entries in `.claude/settings.json`")
if m.get("gitignoreSource"):
extras.append(f"`{m['gitignoreSource']}` appends to `.gitignore`")
if m.get("gitattributesSource"):
extras.append(f"`{m['gitattributesSource']}` appends to `.gitattributes`")
if m.get("dependsOn"):
extras.append("depends on " + ", ".join(f"`{d}`" for d in m["dependsOn"]))
for name, spec in flags.items():
opts = spec.get("options")
extras.append(
f"flag `{name}`" + (f" ({' | '.join(map(str, opts))}" +
f", default `{spec.get('default')}`)" if opts else "")
)
for e in extras:
lines.append(f"- {e}")
if extras:
lines.append("")
lines.append(INDEX_FOOTER.rstrip("\n"))
lines.append("")
return "\n".join(lines)


def _js_meta_block(text: str) -> str:
"""Return the `export const meta = { ... }` object literal, brace-matched.

Deliberately not `text.split("}", 1)[0]`: a `meta` block legitimately nests
objects — `phases: [{ title, detail }]` — so splitting on the first `}`
truncates it and would miss a `name:`/`description:` written after `phases:`.
String literals are skipped so a brace inside a description can't unbalance
the scan. Returns "" when there is no terminated literal.
"""
i = text.find("export const meta")
if i == -1:
return ""
start = text.find("{", i)
if start == -1:
return ""
depth, j, n = 0, start, len(text)
while j < n:
c = text[j]
if c in "'\"`":
quote = c
j += 1
while j < n and text[j] != quote:
j += 2 if text[j] == "\\" else 1
elif c == "{":
depth += 1
elif c == "}":
depth -= 1
if depth == 0:
return text[start:j + 1]
j += 1
return ""


def _find_bash():
"""Return a bash that can actually execute, or None.

Expand Down Expand Up @@ -915,6 +1049,38 @@ def warn(source, msg):
if f"include {pat}" not in text:
err(f"templates/{skill_rel}", f"missing required pattern include: {pat}")

# --- 4b. Dynamic-workflow scripts declare a usable meta block ---
# The runtime rejects a script whose `meta` isn't a pure literal with name +
# description, and a broken workflow fails at invocation time rather than at
# scaffold time — so catch it here instead of in the user's project.
for f in sorted(TEMPLATE_DIR.rglob("workflows/*.js")):
rel = f.relative_to(TEMPLATE_DIR)
text = f.read_text(encoding="utf-8")
if "export const meta" not in text:
err(f"templates/{rel}", "workflow script has no `export const meta` block")
continue
meta_block = _js_meta_block(text)
if not meta_block:
err(f"templates/{rel}",
"workflow `meta` is not a terminated object literal")
continue
for field in ("name:", "description:"):
if field not in meta_block:
err(f"templates/{rel}", f"workflow `meta` is missing `{field}`")
# Static `import` on ANY line, not only the first: the runtime rejects
# a mid-file `import x from "y"` just as hard as one at the top.
if re.search(r"(?m)^\s*import\s", text) or re.search(r"(?<![\w$])import\s*\(", text):
err(f"templates/{rel}",
"workflow scripts cannot use import()/import — the runtime rejects them")

# --- 5. Generated index is current ---
if INDEX_PATH.exists():
if INDEX_PATH.read_text(encoding="utf-8").replace("\r\n", "\n") != render_template_index():
err("templates/INDEX.md",
"out of date — regenerate with `python3 configure.py --write-index`")
else:
err("templates/INDEX.md", "missing — generate with `python3 configure.py --write-index`")

# --- Report ---
errors = [i for i in issues if i[0] == "ERR"]
for _sev, _src, _msg in issues:
Expand Down Expand Up @@ -2677,6 +2843,9 @@ def parse_args():
help="Static validation of templates + MODULES registry (CI-friendly). "
"Exits 0 on clean, 1 with a per-issue summary otherwise. "
"Skips all other processing — no scaffolding, no prompts.")
p.add_argument("--write-index", action="store_true",
help="Regenerate templates/INDEX.md from MODULES (maintainer tool). "
"--check fails when the committed index and this output disagree.")
p.add_argument("--whats-new", action="store_true",
help="Read-only: compare this project's .cc-manifest.json "
"version/SHA against the current configurator build and "
Expand Down Expand Up @@ -2781,6 +2950,10 @@ def cmd_whats_new(target_dir):
def main():
args = parse_args()
# --check short-circuits everything else: no scaffolding, no target dir creation.
if args.write_index:
write_text_lf(INDEX_PATH, render_template_index())
print(green(f"wrote {INDEX_PATH.relative_to(REPO_ROOT)}"))
return
if args.check:
sys.exit(run_check())
# --whats-new is read-only: report the configurator-vs-manifest delta and
Expand Down
Loading