---
name: ArcKit Build
slug: arckit-build
category: AI Engineering
description: "ArcKit Build orchestrates parallel ArcKit artefact generation using subagent isolation, computing dependency DAGs and committing waves. Use it to bulk-build or resume ArcKit projects instead of running individual /arckit:* commands."
github: "https://github.com/tractorjuice/arc-kit/tree/main/plugins/arckit-claude/skills/arckit-build"
language: JavaScript
stars: 2171
forks: 274
install: "npx degit https://github.com/tractorjuice/arc-kit/tree/main/plugins/arckit-claude/skills/arckit-build ~/.claude/skills/arckit-build"
installs_to: ~/.claude/skills/arckit-build
source_path: plugins/arckit-claude/skills/arckit-build/SKILL.md
collection_size: 15
category_size: 2451
collection_url: "https://dirskills.com/collections/tractorjuice/arc-kit"
added: 2026-08-18T06:58:26.962Z
last_synced: 2026-08-18T06:58:26.962Z
canonical_url: "https://dirskills.com/skills/arckit-build"
---

# ArcKit Build

ArcKit Build orchestrates parallel ArcKit artefact generation using subagent isolation, computing dependency DAGs and committing waves. Use it to bulk-build or resume ArcKit projects instead of running individual /arckit:* commands.

**Install:**

```bash
npx degit https://github.com/tractorjuice/arc-kit/tree/main/plugins/arckit-claude/skills/arckit-build ~/.claude/skills/arckit-build
```

## README

# ArcKit Build Harness (v0.4)

You are running the ArcKit build harness. Your job is **orchestration only** — never read or write artefact content yourself. Spawn subagents for that.

## Operating principles

- **Never read or write artefact content in main context.** That's what subagents are for.
- **One commit per wave**, not per artefact (atomic units of progress, clean history).
- **Halt-on-fail** by default — if any agent in a wave reports failure, stop and surface to user.
- **State is sacred** — update `projects/{P}-{NAME}/.arckit/state.json` after every wave, before moving on.
- **Single message, multiple Agent calls** for parallelism within a wave. Never loop sequential Agent calls.
- **Idempotency**: if state says `complete`, the file exists at the recorded path, AND every input file's SHA-256 matches the hash recorded at build time, skip. Otherwise the target is **stale** — rebuild it (and propagate staleness through the DAG). See § "Input-hash change detection".
- **Trust the path-allocation hook.** ArcKit's `validate-arc-filename.mjs` PreToolUse hook is the authoritative path normalizer — it allocates sequence numbers, applies subfolders, pads project IDs at write time. The orchestrator and workers never construct paths by string substitution or call `generate-document-id.mjs` directly. Read the corrected path back from the Write tool result.

## Args

| Arg | Effect |
|-----|--------|
| `<project>` | Project directory name or numeric ID (e.g. `001` or `001-arckit-saas`). If absent, prompt user. |
| `--plan` | Dry run. Print the wave plan, do not dispatch any Agents, exit. |
| `--resume` | Read state.json; continue from last incomplete wave. |
| `--target NAME` | Build only NAME and its missing dependencies. |
| `--refresh NAME` | Force-rebuild NAME and everything downstream. |
| `--no-commit` | Skip the per-wave git commit. |
| `--recipe NAME` | Recipe name (default `uk-saas`). Resolved against the precedence list below. |
| `--enable ID` | Enable an optional target (e.g. `--enable AIP`). |
| `--exclude ID` | Exclude a default-on optional target (e.g. `--exclude SVCASS`). |
| `--skip-hash-check` | Treat any `complete` target with its output file present as up-to-date; skip SHA-256 staleness detection. Fast resume; risks missing edits to inputs. |

## Recipe loading

Recipes are external YAML files. Lookup precedence for `--recipe NAME` (first hit wins):

1. **Project override**: `.arckit/recipes/{NAME}.yaml` — user customizations preserved across plugin updates.
2. **Core plugin**: `${CLAUDE_PLUGIN_ROOT}/skills/arckit-build/recipes/{NAME}.yaml` — recipes shipped with the `arckit` core plugin (`uk-saas`, `uk-mod-sovereign`).
3. **Sibling community plugins**: `${CLAUDE_PLUGIN_ROOT}/../arckit-*/recipes/{NAME}.yaml` — recipes shipped with installed community plugins (e.g. `arckit-uae/recipes/uae-federal-ai.yaml`, `arckit-ca/recipes/ca-federal-fitaa.yaml`).

Resolution: glob the parent directory of `${CLAUDE_PLUGIN_ROOT}` for `arckit-*/recipes/{NAME}.yaml` and take the first match. The glob works in both layouts — marketplace-installed plugins land as siblings under the same marketplace-source cache directory, and the dev-mode same-repo layout has them as sibling directories in the repo root.

Default recipe is `uk-saas`. To customize, copy the core default to `.arckit/recipes/uk-saas.yaml` and edit there:

```bash
mkdir -p .arckit/recipes
cp "${CLAUDE_PLUGIN_ROOT}/skills/arckit-build/recipes/uk-saas.yaml" .arckit/recipes/uk-saas.yaml
```

**Built-in recipes**:

| Recipe | Plugin | Use case |
|--------|--------|----------|
| `uk-saas` | `arckit` (core) | UK Government managed multi-tenant SaaS — civilian departments |
| `uk-mod-sovereign` | `arckit` (core) | UK MOD / sovereign / air-gapped — `mod-secure` + `jsp-936`, no SVCASS, sealed-media distribution |
| `uae-federal-ai` | `arckit-uae` | UAE Federal AI — full Cabinet agentic AI decree compliance with all 12 UAE community commands, integrated research wave (general AI + AWS / Azure UAE region availability), plus core ArcKit governance |
| `uae-agentic-transformation` | `arckit-uae` | UAE Federal Agentic AI Transformation — focused 24-month playbook for the 23 April 2026 Cabinet framework's 50%-of-services-by-April-2028 target; ADRs reshaped around agentic architecture (orchestration, human-in-the-loop, observability, kill-switch); PLAN + ROADMAP timeboxed to the 24-month window |
| `ca-federal-fitaa` | `arckit-ca` | Canadian Federal — FITAA, ITSG-33, GC Digital Standards |
| `au-federal` | `arckit-au` | Australian Federal / DISP-supplier — ASD Essential Eight, ISM, DTA DSS, Privacy Act 1988, OAIC NDB, PSPF, AI Assurance, DISP attestation (35 targets, 9 waves) |
| `au-energy` | `arckit-au-energy` (composes `arckit-au` baseline) | Australian Energy Sector — AESCSF maturity, AER ring-fencing, AEMC NER/NGR, AEMO interfaces, DERMS/DOE, CSIP-AUS layered on the AU federal baseline (Essential Eight, ISM, OT security, SOCI/CIRMP, Privacy Act/NDB). Optional default-off `SERVICE_INVENTORY` (`servicenow`). 22 targets. First **Australian sector** overlay. |
| `uk-nhs-clinical-safety` | `arckit` (core, references `arckit-uk-nhs` commands) | UK NHS Clinical Safety + UK/EU MDR — NHS DCB0129 (manufacturer) + DCB0160 (deployer) clinical safety case (Marcus Baw SAFETY.md 3-file spec), NHS DTAC v3, UK MDR 2002 + EU MDR 2017/745 SaMD/AIaMD classification. Composes with UK SaaS baseline (no swaps; adds clinical safety + medical-device regulation on top). 44 targets across 8 waves. First **sector** overlay. |

### Recipe schema (v1)

See `${CLAUDE_PLUGIN_ROOT}/skills/arckit-build/recipes/uk-saas.yaml` for an annotated reference. Top-level keys:

- `recipe` — recipe name (string, must match filename stem)
- `schema_version` — recipe schema version (currently `1`)
- `description` — free text
- `defaults.version` — default version stamp for outputs (e.g. `"1.0"`)
- `optional_targets` — map of target ID → `{description, default}`
- `post_build_hooks` — list of `{skill, args}` to run after final wave (parallel)
- `targets` — list of target entries

Each `targets[]` entry:

| Field | Required | Notes |
|-------|----------|-------|
| `id` | yes | Unique target ID (e.g. `PRIN`, `ADR-001`, `DIAG-C4`) |
| `skill` | yes | ArcKit skill name (e.g. `arckit:requirements`) |
| `args` | yes | Args string passed to the skill, after substitution |
| `output.project` | yes | `"000-global"` or `"{P}-{NAME}"` |
| `output.type` | yes | ArcKit doc-type code (`PRIN`, `REQ`, `ADR`, `DIAG`, …) — used for state.json keys, NOT for path construction |
| `output.subfolder` | no | Orientation hint shown in `--plan` and worker prompts; the actual subfolder is enforced by `validate-arc-filename.mjs`'s `SUBDIR_MAP` at write time |
| `output.multi_instance` | no | Orientation hint; the hook's `MULTI_INSTANCE_TYPES` list is authoritative |
| `topic` | no | Used in commit messages and `{TOPIC}` substitution |
| `deps` | yes | List of target IDs, may include glob `"ADR-*"` |

The `output.subfolder` / `output.multi_instance` fields document recipe author intent for human readers and `--plan` output, but the path-allocation hook is the source of truth at write time. If a recipe says `subfolder: decisions` for a single-instance type the hook doesn't recognise, the file lands wherever the hook decides — debug via the hook, not the recipe.

### Variable substitution

The orchestrator substitutes these placeholders in `args` and `output.project` before dispatching to workers (workers never see placeholders):

| Placeholder | Source |
|-------------|--------|
| `{P}` | Project ID, zero-padded (e.g. `"001"`) |
| `{NAME}` | Project slug (e.g. `"arckit-saas"`) |
| `{V}` | `defaults.version` from the recipe |
| `{TOPIC}` | `target.topic` (multi-instance ADR/DIAG topics) |

### Dep resolution

`deps: ["ADR-*"]` matches all targets whose ID begins with `ADR-`. Exact IDs take precedence; globs expand at wave-computation time against the resolved target list (after optional-target filtering).

## Input-hash change detection

The orchestrator records the SHA-256 of every input artefact at build time and compares against the live filesystem on the next run. This catches the "user edited REQ after the build completed but never re-ran" case that pure `test -f` idempotency misses.

### What counts as an input

A target's inputs are the **resolved output paths of its `deps`** (after glob expansion). Externally-supplied inputs (e.g. `external/policies/*.md`) are not hashed by v0.4 — only artefacts the harness itself produced or recorded as `source: "pre-existing"`. Recipes that surface external inputs should hash them in v0.5.

### What's recorded

Each completed target's state entry gains an `input_hashes` map of dep-ID → SHA-256:

```json
"RISK": {
  "status": "complete",
  "path": "projects/001-arckit-saas/ARC-001-RISK-v1.0.md",
  "input_hashes": {
    "REQ":  "9f2a...c41e",
    "STKE": "1b88...07ad",
    "PRIN": "44e7...bc92"
  },
  ...
}
```

The map is populated by the orchestrator (Bash `sha256sum`) immediately after a target validates `complete` in step 5 — workers don't compute hashes.

### Staleness rules (skipped if `--skip-hash-check`)

At work-list computation (step 7 of the run order):

1. Start with `done = { t ∈ state.targets : t.status == "complete" AND test -f t.path }`.
2. **Direct staleness**: for each `t ∈ done`, recompute SHA-256 for every entry in `t.input_hashes`. If any current hash differs from the recorded value (or the input file is now missing), `t` is **stale** — move it from `done` back into pending and clear it from `state.targets[t].status` (set `"stale"`).
3. **Propagated staleness**: walk the DAG; any target whose deps (transitively) include a stale target is itself stale. Apply in topological order so a single REQ edit cascades to RISK, HLD, SOBC, PLAN, TRACE, …
4. Targets explicitly named in `--refresh NAME` are marked stale unconditionally (existing behaviour — overrides hash check).
5. With `--skip-hash-check`, skip steps 2–3 entirely. `done` is whatever survives the `test -f` filter.

Print the staleness reason in the `--plan` output so users can see why a target rebuilt:

```text
Wave 1: REQ
  (stale) REQ ← file edited since last build (sha mismatch)
Wave 3: RISK, HLD, STRATEGY, ...
  (stale-cascade) RISK ← REQ changed since 2026-05-12T10:14:03Z
```

### Performance note

SHA-256 of typical ArcKit artefacts (≤500 KB markdown) is sub-millisecond per file. A 50-target project incurs <100 ms of hashing on `--resume`. Workers do no hashing; only the orchestrator does (single shell loop in step 5/step 7).

## Wave plan algorithm

Standard topological sort with parallelism:

1. `pending = recipe.targets ∩ enabled - done` — where `enabled` reflects `--enable`/`--exclude` flags and `optional_targets[id].default`, and `done` is computed by the staleness rules in § "Input-hash change detection" (i.e. `complete` AND `test -f` AND every input hash matches, unless `--skip-hash-check` is set). Stale targets stay in `pending`.
2. `done` from staleness computation above.
3. While pending non-empty:
   - `wave = { t ∈ pending : deps(t) ⊆ done }` (after expanding globs)
   - If wave empty → cycle / unresolvable. Halt with error, list involved targets.
   - **If `|wave| > 20`, split it**: dispatch the first 20 (stable sort by target ID), and leave the remainder in `pending` for the next iteration. They have no unmet deps, so they form the next wave immediately.
   - Emit wave; remove its members from pending; (after dispatch + validate) add to done.

**Why the 20 cap is load-bearing.** Claude Code caps concurrently-running subagents at 20 by default (`CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS`, v2.1.217+) and **denies** over-cap spawns rather than queueing them — the 21st `Agent` call in a single message fails with *"Concurrent subagent limit reached … Do not retry"*. Because this harness is halt-on-fail, an over-wide wave does not run slower, it fails. No bundled recipe exceeds 16 (CI enforces this via `scripts/check_recipes.py`), but a user-authored recipe under `.arckit/recipes/` never passes through CI, so split at dispatch time rather than assuming the recipe is well-formed.

A split wave is still one commit — commit after the whole logical wave completes, not after each chunk of 20.

**Worked example** — for project 001 (UK-SaaS recipe) **starting from empty state**, the algorithm produces something like:

- W0: PRIN
- W1: GLOSSARY, REQ, STKE
- W2: ADR-001..ADR-008 (parallel)
- W3: STRATEGY, WARDLEY, RISK, HLD, DEVOPS, FINOPS
- W4: SOBC, TCOP, SBD, DPIA, DIAG-C4, DIAG-SEQ
- W5: DIAG-DEP, PLAN, OPS, AIP (if enabled)
- W6: ROADMAP
- W7: SVCASS
- W8: TRACE
- W9 (post-build): health, pages

Treat this as illustrative only; the harness recomputes waves at runtime from the recipe DAG.

## Per-wave execution

For each wave, in order:

### 1. Plan dispatch

Print:

```
Wave {N}/{total}: {targets joined}
  - estimated agents: {len(wave)}
  - estimated duration: ~2-5 min wall-clock per agent (parallel)
```

### 2. Single message, multiple Agent calls — all in parallel

Use the **Agent tool** with `subagent_type: "general-purpose"`. One Agent call per target. **All in the same assistant message** so they run in parallel.

Per-agent prompt template (substitute `{...}` placeholders from the resolved target):

```
You are an ArcKit artefact worker subagent (orchestrated by arckit-build, wave {WAVE_N}).

Project: {PROJECT_ID} ({PROJECT_NAME})
Target: {TARGET_ID}
Skill to invoke: {SKILL}
Skill args: {ARGS_RESOLVED}
Expected directory: {EXPECTED_DIR}     # for orientation only — actual filename is hook-allocated

Inputs you may read (only these):
{INPUT_PATHS_BULLETED}

Steps:
1. Use the Skill tool to invoke `{SKILL}` with the args above verbatim.

   **Interactive Q&A handling (CRITICAL — subagents have no user available):**
   If the skill calls `AskUserQuestion`, you MUST select the option marked `(Recommended)`
   without asking. If no option is marked Recommended, use these defaults:

   | Question header | Default |
   |-----------------|---------|
   | Scope | `Full system` |
   | Consultation | `Surveys` |
   | Phase | value from skill args, else `alpha` |
   | AI mode / scope | derive from REQ FRs (AI-in-scope iff any FR mentions AI/ML/LLM) |
   | Risk appetite | `Medium` |
   | Anything else | first option in the list |

   Document the choice you made in your final report so the orchestrator can record it.
   Never block waiting for an answer.

2. Capture the actual file path the skill wrote to. Inside the Write tool call,
   the ArcKit `validate-arc-filename.mjs` PreToolUse hook normalizes the path
   (allocates the next sequence number for multi-instance types like ADR/DIAG,
   moves into the correct subfolder, pads project IDs). The hook returns the
   corrected path as `updatedInput.file_path` and the actual write proceeds
   there. You will see this corrected path in the Skill tool's result.

   Read `ACTUAL_PATH` from that result. Do NOT call `generate-document-id.mjs`
   yourself or construct paths by string substitution — the hook is the
   authoritative path allocator.

3. Sanity check the corrected path via Bash:
   - `test -f "$ACTUAL_PATH"` returns success
   - `[ "$(wc -l < "$ACTUAL_PATH")" -gt 100 ]`
   - `grep -c '^## Document Control\|^| Document ID' "$ACTUAL_PATH"` returns ≥ 1

4. Do NOT git commit. Do NOT modify other files. The orchestrator handles version control.

Report back ≤ 200 words:
- Actual file path written + exact line count
- Top 3 findings, scores, or RAG ratings (whatever the skill produces as headline result)
- Validation result: PASS or FAIL (with reason)
- Any failures, partial completions, or warnings
- AskUserQuestion choices made (if any)

Do NOT include the document content in your report. Just the summary.
```

### 3. Wait for all agents

When all return, collect summaries.

### 4. Validate

For each target in wave:

- File exists at expected path (`test -f`).
- Line count > 100 (`wc -l`).
- Document control header present (`grep -c '^## Document Control'` ≥ 1).

### 5. Update state.json

Read `projects/{P}-{NAME}/.arckit/state.json`, update each target with:

```json
{
  "<TARGET_ID>": {
    "status": "complete",
    "path": "{ACTUAL_PATH}",
    "built_at": "{ISO_TIMESTAMP}",
    "wave": {WAVE_N},
    "line_count": {LC},
    "skill": "{SKILL}",
    "topic": "{TOPIC_OR_NULL}",
    "agent_summary": "{≤200-word agent report}",
    "input_hashes": {
      "<DEP_ID>": "{SHA256_OF_state.targets[DEP_ID].path}"
    }
  }
}
```

Compute `input_hashes` via Bash `sha256sum` on each resolved dep's `state.targets[DEP_ID].path` immediately before persisting state. Skip deps whose target has no recorded path (e.g. external inputs). Targets with no deps get `input_hashes: {}`.

For failures: `status: "failed"`, `error: "..."`, `wave: {WAVE_N}` — do not record `input_hashes` for failed targets.

### 6. Git commit

If `--no-commit` not set:

```bash
git add {OUTPUT_PATHS} projects/{P}-{NAME}/.arckit/state.json
git commit -m "$(cat <<'EOF'
Build wave {N}: {targets joined} via arckit-build

{One-line per target with line count and headline result}

Co-Authored-By: Claude Code <noreply@anthropic.com>
EOF
)"
```

### 7. Halt-on-fail

If any agent in the wave reported `FAIL` or validation failed:

- **DO write state.json** — record `status: "failed"`, `error: "..."`, `wave: {WAVE_N}` for failed targets, and `status: "complete"` for the targets in the wave that *did* succeed. State is needed for `--resume`.
- **Do NOT git commit** (don't half-commit a wave). Successfully-written artefacts are left in the working tree; they get bundled into the resume commit.
- Surface to user: per-target outcome, error summary, suggested remediation.
- Suggest `--resume` once fixed.
- Stop the build.

### 8. Move to next wave

Otherwise, proceed.

## State file shape

`projects/{P}-{NAME}/.arckit/state.json`:

```json
{
  "state_format_version": "0.4",
  "project_id": "001",
  "project_name": "001-arckit-saas",
  "recipe": "uk-saas",
  "recipe_path": ".claude/skills/arckit-build/recipes/uk-saas.yaml",
  "started_at": "2026-05-03T16:00:00Z",
  "last_wave_completed": 5,
  "current_wave": 6,
  "targets": {
    "PRIN":   {"status": "complete", "path": "projects/000-global/ARC-000-PRIN-v1.0.md", "wave": 0, "source": "pre-existing", "input_hashes": {}},
    "REQ":    {"status": "complete", "path": "...", "wave": 1, "source": "pre-existing", "input_hashes": {"PRIN": "44e7...bc92"}},
    "RISK":   {"status": "complete", "path": "...", "wave": 3, "skill": "arckit:risk", "input_hashes": {"REQ": "9f2a...c41e", "STKE": "1b88...07ad", "PRIN": "44e7...bc92"}},
    "SVCASS": {"status": "pending"}
  },
  "waves": [
    {"n": 0, "targets": ["PRIN"], "status": "complete", "completed_at": "..."},
    {"n": 3, "targets": ["RISK", "STRATEGY"], "status": "complete", "completed_at": "..."}
  ]
}
```

State written by older versions (`state_format_version: "0.3"`) is read-compatible: targets without `input_hashes` are treated as if all hashes match (no spurious rebuild on upgrade). The next successful build of any such target records its hashes and migrates the entry to 0.4 in place. The orchestrator rewrites `state_format_version` to `"0.4"` on first write.

## When invoked, perform these steps in order

1. **Parse arguments** from skill input (project, --plan, --resume, --recipe, --enable, --exclude, etc.). If project not specified, ask user.
2. **Detect project**: resolve `<project>` arg → `projects/{P}-{slug}/`. Confirm directory exists.
3. **Load recipe**: resolve `--recipe NAME` (default `uk-saas`) against the precedence list. Read the YAML with the Read tool. Validate top-level shape (`recipe`, `schema_version`, `targets`, `defaults.version`). Halt with a clear error if the recipe file is missing or malformed.
4. **Resolve enabled targets**: drop `optional_targets` whose `default: false` unless `--enable ID` was passed; drop `option
