Scaffold a craft customization by interview and write a named manifest. Triggers — "scaffold a craft customization", "craft:init", "generate a named manifest", "create a craft config for this repo".
Scanned 9/6/2026
Install to Claude Code
npx -y skills add scolladon/craft --skill init --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Init?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/scolladon-init)More formats (shields.io, HTML) on the badges page.
---
name: init
description: Scaffold a craft customization by interview and write a named manifest. Triggers — "scaffold a craft customization", "craft:init", "generate a named manifest", "create a craft config for this repo".
argument-hint: [<name>]
---
# craft:init — named-manifest generator
Standalone session-owned skill. You (the session) probe the repo, interview the user over the full Tier-0/1 catalog, emit a manifest, lint it in a temp path, and move it into place only on a clean lint. No worker agent is spawned. Never call `worktree-setup.sh` (it installs deps). Never touch `.claude/workflow.md`.
Input: `$ARGUMENTS` (optional name; if absent, ask for one during interview).
---
## Preamble — probe + name validation (read-only)
Before asking anything, build the `CapabilityReport` from read-only probes.
### 1. Name resolution
Parse `--scope user|local` out of `$ARGUMENTS` first (default `local`) and strip it from the string; bind the result as `scope`. This is only a pre-fill — the Step 1 interview still asks the scope question and the user's answer there wins.
If the remaining `$ARGUMENTS` is non-empty, treat it as the candidate `<name>`. Otherwise hold name until the interview's first question.
When a candidate name is available, validate it immediately and bind the name and its local-scope path to shell variables:
```bash
name="<the candidate name>"
manifest_final="$(node "${CRAFT_ROOT:-${CLAUDE_PLUGIN_ROOT}}/engine/bin/init-config.js" "$name")"
```
- Exit 0: the name is valid; `$manifest_final` holds the LOCAL-scope path (e.g. `.claude/craft-<name>.md`) for the Done report and local-existence checks. It no longer supplies the land target — Step 3 passes `$name` and `$scope` to `init-land.js`, which re-derives the destination itself.
- Non-zero: STOP — surface the stderr diagnostic; do not proceed.
Defer validation to the moment a name is provided if it was not in `$ARGUMENTS`.
### 2. Ecosystem detection
```bash
bash "${CRAFT_ROOT:-${CLAUDE_PLUGIN_ROOT}}/scripts/detect-ecosystem.sh" .
```
Capture stdout as `ecosystem` (one of `npm|pnpm|yarn|bun|uv|poetry|cargo|go|bundler|composer`). If the command exits non-zero or produces empty output, set `ecosystem = null` and `lockfile = null`. The detected lockfile name is the file whose presence triggered the match (derive from ecosystem: `npm`→`package-lock.json`, `pnpm`→`pnpm-lock.yaml`, `yarn`→`yarn.lock`, `bun`→`bun.lockb`, `uv`→`uv.lock`, `poetry`→`poetry.lock`, `cargo`→`Cargo.toml`, `go`→`go.mod`, `bundler`→`Gemfile.lock`, `composer`→`composer.lock`).
### 3. Test-command discovery (gate probe, read-only)
Probe for a discoverable test command using the gate precedence: `descriptor.gate → manifest.gates[phaseId] → none`. In practice, check for common test runners in order: `package.json` `test` script, `Makefile` with a `test` target, `Cargo.toml`, `go.mod`, `pytest.ini`/`setup.cfg`/`pyproject.toml`. Set `testCmd` to the discovered command string, or `null` if none found.
### 4. Git remote presence
```bash
git remote
```
Set `hasRemote = true` if the command produces any output, `false` otherwise. A failure (git absent) degrades only the `hasRemote` dimension — never aborts the probe.
### 5. Harness technique enumeration
Enumerate candidate technique ids by reading the repo's own validation/architecture
conventions — same probe style as `testCmd`:
- Check for documented harness commands in `README.md`, `CONTRIBUTING.md`, `package.json`
scripts, `Makefile`, and `.claude/workflow.md` / `.claude/craft-*.md` (any declared
`techniquePlan` entries). Each discoverable command that validates or enforces a
quality property (lint, typecheck, format-check, boundary-check, …) produces one
candidate id derived from the command's purpose (e.g. `lint`, `typecheck`,
`format-check`, `boundary-check`).
- Set `harnessTechniques` to the deduplicated list of discovered ids (possibly empty).
### 6. Existing named configs
```bash
ls .claude/craft-*.md 2>/dev/null
```
Capture matching filenames; extract the name segment from each (strip `.claude/craft-` prefix and `.md` suffix). Store as `existingNames[]`.
**State-mutating probes** (if any are later needed) must run in a `mktemp` throwaway directory — never against the working tree.
**CapabilityReport shape (immutable once built):**
```
{
ecosystem: string | null,
lockfile: string | null,
testCmd: string | null,
hasRemote: boolean,
harnessTechniques: string[],
existingNames: string[],
}
```
---
## Procedure
### Step 1 — Interview (interactive)
Drive `AskUserQuestion` over the full Tier-0/1 catalog below, one question per point, defaults pre-filled from the CapabilityReport. A point the probe rules out is either skipped or asked with a "this will no-op in your repo" note — never silently dropped.
**Name (if not yet validated)**
If no valid name was supplied in `$ARGUMENTS`, ask:
> "What name should this customization have? (kebab-case, e.g. `ci` or `strict-review`)"
Validate immediately and bind the same shell variables the Preamble does — so `$manifest_final` is captured on this deferred path too (used for the Done report and local-existence checks):
```bash
name="<the answer>"
manifest_final="$(node "${CRAFT_ROOT:-${CLAUDE_PLUGIN_ROOT}}/engine/bin/init-config.js" "$name")"
```
On non-zero: explain the constraint and re-ask.
**Scope**
Ask, pre-filled from the `--scope` parsed in the Preamble (default `local` if none was given):
> "Where should this config live — this repo (`local`) or your user config (`~/.claude`, portable across repos)? [local]"
Bind the answer to `scope` (overrides the Preamble pre-fill on disagreement). Re-ask on any value other than `local`/`user`.
**Catalog questions (Tier-0)**
| Point | Question (default from probe) | Emits |
|---|---|---|
| skip | "Drop any phases from the default walk? List phase ids (comma-separated), or leave empty." | `pipeline.skip: […]` |
| model | "Route any agent to a different model tier? (e.g. `reviewer=opus`, `fallback=haiku`; leave empty to use defaults)" | `models.<agent>` (+ `models.fallback`) |
| gate | "Test/gate command?" (default = `testCmd` from probe, or ask explicitly if `testCmd: null` — see note below) | `gates.part` and/or `gates.phase` |
| execution | "Run any phase inline (in-session) rather than as a spawned agent? List `<phase>=inline` or `<phase>=agent`, or leave empty." | `phases.<id>.execution` |
| profile | "Whole-flow execution mode? (`full` / `lean` / `solo`, or leave empty for default)" | `pipeline.profile` |
| harness | "Declare validation/architecture techniques for this repo? (e.g. `validation.techniques: [lint, typecheck]`, or leave empty to rely on convention discovery)" — when `harnessTechniques` is empty, note: "no harness techniques discovered; phases will derive or no-op at runtime" | `phases.<phase>.harness.*` |
| backlog | "Use a tracker? (`file` with a path, `custom` with a label, or leave empty)" | `backlog: { source, ref }` |
| memory | "Enable per-repo advisory memory? (`file` for default location, `custom` with a path, or leave empty)" | `memory: { source, ref }` |
| policy | "Permission posture for outward actions? (e.g. `always: [commit, push]`, `ask: [propose]`, `never: [external-send]`)" — skip or note "no remote found; propose/integrate will no-op" if `hasRemote: false` | `policy: { always?, ask?, never? }` |
**Catalog questions (Tier-1)**
| Point | Question (default from probe) | Emits |
|---|---|---|
| context | "House-rules file to inject globally or per-phase? (e.g. `docs/rules.md` globally, or `implementation=docs/impl-rules.md`)" | `context: <path>` / `phases.<id>.context` |
| override | "Replace any phase's procedure body with your own file? (e.g. `implementation=.claude/my-impl.md`)" | `phases.<id>.override` |
| role / procedure | "Swap any phase's agent role or orchestrating skill? (e.g. `implementation.role=my-coder`)" | `phases.<id>.role` / `phases.<id>.procedure` |
| insert | "Insert a new phase? (leave empty to skip, or answer the sub-questions below)" — if the user wants to insert, drive the lettered sub-interview: | `pipeline.insert: [...]` |
- **(a) command** — "What does the phase run — a skill/command (worker step), or a shell check?" → worker emits `procedure: <skill>`; check emits `gate: <command>`.
- **(b) position** — "After which existing phase should it run?" (offer the resolved phase-id list) → emits `after: <id>`.
- **(c) does-it-block** — "Should a failure block the pipeline (hard gate) or be advisory?" → blocking shell check emits `gate`; advisory emits no gate.
Emit the **flat** shape (`after`/`id` as siblings of the phase fields — the nested `phase:{}` form is rejected at manifest-lint):
```yaml
pipeline:
insert:
- after: <id> # after/id are SIBLINGS of the phase fields, not a wrapper
id: <id>
procedure: <command> # present when a worker step
gate: <command> # present when a blocking shell check
```
No `archetype` key — narrate the inference outcome: "no archetype needed — craft will infer `harness|construction` from your gate/produces" so the collapsed descriptor stays legible.
| DoD | "Point at a Definition-of-Done artifact? (file path)" | `paths.dod` |
**No-test-command edge:** When `testCmd: null`, the gate question has no default. Surface a clear warning:
> "No test command was discovered. A manifest without a gate command will cause craft to refuse to run at the gate-floor. Please provide an explicit gate command, or leave empty and understand the run will be blocked."
Do not emit a silently-unrunnable manifest — make the consequence explicit.
After all questions are answered, present a brief summary of non-empty choices and ask:
> "Proceed to generate the manifest with these settings? (yes / edit)"
On "edit": re-ask any question the user specifies. On "yes": proceed to Step 2.
If the user aborts at any point: leave the repo unchanged (no temp file, no landed file), and stop.
---
### Step 2 — Emit
Assemble the `Answers` object from the interview responses. Write it to a temp JSON file:
```bash
answers_tmp="$(mktemp /tmp/craft-init-answers.XXXXXX)"
```
Write the collected answers as a JSON object to `$answers_tmp`.
Ensure the CHOSEN destination's `.claude/` exists, then invoke the emitter, writing to an UNPREDICTABLE temp file created with `mktemp` inside that same directory (never a guessable PID-based name):
```bash
dest_claude_dir=".claude"; [ "$scope" = "user" ] && dest_claude_dir="$HOME/.claude"
mkdir -p "$dest_claude_dir"
manifest_tmp="$(mktemp "${dest_claude_dir}/.craft-${name}.tmp.XXXXXX")"
node "${CRAFT_ROOT:-${CLAUDE_PLUGIN_ROOT}}/engine/bin/init-emit.js" "$answers_tmp" "$manifest_tmp"
```
`$name` here is the kebab name already validated in the Preamble, so the `mktemp` template is safe; `mktemp` then makes the suffix unpredictable and creates the file with `O_EXCL`, closing the TOCTOU window between lint and move (both inside Step 3) — nothing can swap the linted bytes before the move. Reuse `$manifest_tmp` verbatim in Step 3 — never re-splice `$name` into a later path.
The temp manifest is written inside the CHOSEN destination's `.claude/` — this is load-bearing: `manifest-lint`'s `fileExists` ROOT resolves to `dirname(dirname(manifestAbsPath))` = the destination root (the repo root for `local`, `$HOME` for `user`), so ref-existence checks (context files, DoD path, etc.) resolve exactly where the landed file will live. For `user` scope this means a repo-relative ref fails lint at `$HOME` by design — a user-scope config must be self-contained.
On non-zero exit from the emitter: STOP — surface stderr; remove `$answers_tmp` and `$manifest_tmp`; nothing lands.
Remove `$answers_tmp` after the emitter exits (whether success or failure). On any non-landing exit from here on, also remove `$manifest_tmp` so no stray temp is left behind.
**`.claude/` unwritable:** if the write fails at any point, STOP — report the path and reason; no partial file is left.
---
### Step 3 — Land (lint-then-move)
Run the deterministic land helper, which lints the temp file and moves it atomically only on a clean lint:
```bash
node "${CRAFT_ROOT:-${CLAUDE_PLUGIN_ROOT}}/engine/bin/init-land.js" "$manifest_tmp" "$name" --scope "$scope"
```
Pass `$name` and `$scope` — NOT `$manifest_final`; `init-land` re-derives the destination itself from the same validated kebab name. The helper lints `$manifest_tmp` first; only on exit 0 does it rename the temp into place (a POSIX atomic rename on the same filesystem). The live `.claude/workflow.md` is never touched.
When `scope` is `user` and a local `.claude/craft-<name>.md` already exists, `init-land` emits a shadow-warning on stderr — the move still proceeds (different path, no overwrite of the local file). Surface this warning to the user.
**Non-zero exit:** STOP — surface the stderr diagnostic; remove `$manifest_tmp` if it still exists; nothing lands; any prior `craft-<name>.md` at the destination scope is untouched byte-for-byte.
**Exit 0:** the config is in place at the chosen scope and lints clean. Proceed to Done.
Never swallow a lint failure; the helper ensures the move never occurs unless lint exits 0.
---
### Done
Report:
- Landed path: `.claude/craft-<name>.md` (local) or `~/.claude/craft-<name>.md` (user)
- Scope: `$scope`
- Status: lints clean
- Next step: `/craft:run --config <name> <brief>`
---
## Error semantics
| Failure | Behaviour |
|---|---|
| Invalid name (path separator, traversal, uppercase, empty) | STOP; surface the `init-config.js` diagnostic; do not proceed |
| Emitter exits non-zero | STOP; surface stderr; remove temp answers JSON; nothing lands |
| Temp manifest fails lint | STOP; surface `manifest-lint` diagnostic block; remove `$manifest_tmp`; prior same-name file untouched |
| `.claude/` unwritable | STOP; report path + reason; no partial file |
| Interview aborted | Leave repo unchanged; no temp; no landed file |
| Re-run for existing name | Direct overwrite after a clean lint; only that named file is replaced; other `.claude/craft-*.md` and `.claude/workflow.md` untouched |
| Probe error (e.g. git absent) | Degrade that dimension to a question; never abort the full probe |
| No discoverable test command | Ask for an explicit gate command; warn about the gate-floor consequence; do not emit a silently-unrunnable manifest |
| User scope + ref-bearing config (e.g. `context: <path>`) | Lint REJECTS before the move — a user-scope config must be self-contained; nothing lands at `~/.claude` |
| User scope + local same-name config present | Shadow warning surfaced on stderr; the config still lands at `~/.claude` (advisory, no overwrite of the local file) |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!