@friedbotstudio/create-baseline 0.1.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/LICENSE +202 -0
- package/README.md +222 -0
- package/bin/cli.js +247 -0
- package/obj/template/.claude/agents/swarm-worker.md +52 -0
- package/obj/template/.claude/bin/LICENSE +201 -0
- package/obj/template/.claude/bin/NOTICE +48 -0
- package/obj/template/.claude/commands/approve-spec.md +29 -0
- package/obj/template/.claude/commands/approve-swarm.md +27 -0
- package/obj/template/.claude/commands/grant-commit.md +19 -0
- package/obj/template/.claude/commands/init-project.md +191 -0
- package/obj/template/.claude/hooks/artifact_template_guard.sh +141 -0
- package/obj/template/.claude/hooks/consent_gate_grant.sh +89 -0
- package/obj/template/.claude/hooks/destructive_cmd_guard.sh +42 -0
- package/obj/template/.claude/hooks/env_guard.sh +36 -0
- package/obj/template/.claude/hooks/git_commit_guard.sh +93 -0
- package/obj/template/.claude/hooks/harness_continuation.sh +121 -0
- package/obj/template/.claude/hooks/lib/__pycache__/resume_writer.cpython-314.pyc +0 -0
- package/obj/template/.claude/hooks/lib/common.sh +328 -0
- package/obj/template/.claude/hooks/lib/resume_writer.py +341 -0
- package/obj/template/.claude/hooks/lint_runner.sh +55 -0
- package/obj/template/.claude/hooks/memory_pre_compact.sh +36 -0
- package/obj/template/.claude/hooks/memory_session_start.sh +244 -0
- package/obj/template/.claude/hooks/memory_stop.sh +173 -0
- package/obj/template/.claude/hooks/plantuml_syntax_guard.sh +161 -0
- package/obj/template/.claude/hooks/process_lifecycle_guard.sh +89 -0
- package/obj/template/.claude/hooks/setup_guard.sh +50 -0
- package/obj/template/.claude/hooks/spec_approval_guard.sh +81 -0
- package/obj/template/.claude/hooks/spec_design_calls_guard.sh +183 -0
- package/obj/template/.claude/hooks/spec_diagram_presence_guard.sh +141 -0
- package/obj/template/.claude/hooks/swarm_approval_guard.sh +39 -0
- package/obj/template/.claude/hooks/swarm_boundary_guard.sh +136 -0
- package/obj/template/.claude/hooks/tdd_order_guard.sh +176 -0
- package/obj/template/.claude/hooks/test_runner.sh +75 -0
- package/obj/template/.claude/hooks/tests/fixtures/ac008_byte_equal_reference.txt +12 -0
- package/obj/template/.claude/hooks/tests/memory_session_start_test.sh +285 -0
- package/obj/template/.claude/hooks/track_guard.sh +127 -0
- package/obj/template/.claude/hooks/verify_pass_guard.sh +88 -0
- package/obj/template/.claude/memory/README.md +108 -0
- package/obj/template/.claude/memory/_pending.md +15 -0
- package/obj/template/.claude/memory/_resume.md +12 -0
- package/obj/template/.claude/memory/conventions.md +26 -0
- package/obj/template/.claude/memory/decisions.md +29 -0
- package/obj/template/.claude/memory/landmarks.md +26 -0
- package/obj/template/.claude/memory/landmines.md +27 -0
- package/obj/template/.claude/memory/libraries.md +27 -0
- package/obj/template/.claude/memory/pending-questions.md +28 -0
- package/obj/template/.claude/project.json +221 -0
- package/obj/template/.claude/settings.json +110 -0
- package/obj/template/.claude/skills/archive/SKILL.md +48 -0
- package/obj/template/.claude/skills/archive/archive.sh +145 -0
- package/obj/template/.claude/skills/audit-baseline/SKILL.md +80 -0
- package/obj/template/.claude/skills/audit-baseline/audit.sh +919 -0
- package/obj/template/.claude/skills/brd/SKILL.md +44 -0
- package/obj/template/.claude/skills/brd/template.md +83 -0
- package/obj/template/.claude/skills/chore/SKILL.md +99 -0
- package/obj/template/.claude/skills/claude-automation-recommender/LICENSE +202 -0
- package/obj/template/.claude/skills/claude-automation-recommender/NOTICE +69 -0
- package/obj/template/.claude/skills/claude-automation-recommender/SKILL.md +358 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/hooks-patterns.md +226 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/mcp-servers.md +263 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/plugins-reference.md +98 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/skills-reference.md +408 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/subagent-templates.md +181 -0
- package/obj/template/.claude/skills/code-structure/SKILL.md +204 -0
- package/obj/template/.claude/skills/commit/SKILL.md +21 -0
- package/obj/template/.claude/skills/copywriting/SKILL.md +252 -0
- package/obj/template/.claude/skills/copywriting/evals/evals.json +111 -0
- package/obj/template/.claude/skills/copywriting/references/ai-writing-detection.md +200 -0
- package/obj/template/.claude/skills/copywriting/references/copy-frameworks.md +344 -0
- package/obj/template/.claude/skills/copywriting/references/natural-transitions.md +272 -0
- package/obj/template/.claude/skills/design-ui/SKILL.md +175 -0
- package/obj/template/.claude/skills/design-ui/references/design-vs-development.md +89 -0
- package/obj/template/.claude/skills/design-ui/references/intent-table.md +64 -0
- package/obj/template/.claude/skills/design-ui/references/orchestration.md +121 -0
- package/obj/template/.claude/skills/design-ui/references/state-machine.md +125 -0
- package/obj/template/.claude/skills/document/SKILL.md +66 -0
- package/obj/template/.claude/skills/documentation/SKILL.md +50 -0
- package/obj/template/.claude/skills/harness/SKILL.md +169 -0
- package/obj/template/.claude/skills/humanizer/SKILL.md +489 -0
- package/obj/template/.claude/skills/humanizer/references/ai-writing-detection.md +208 -0
- package/obj/template/.claude/skills/impeccable/PROJECT_NOTES.md +22 -0
- package/obj/template/.claude/skills/impeccable/SKILL.md +153 -0
- package/obj/template/.claude/skills/impeccable/agents/openai.yaml +4 -0
- package/obj/template/.claude/skills/impeccable/reference/adapt.md +190 -0
- package/obj/template/.claude/skills/impeccable/reference/animate.md +173 -0
- package/obj/template/.claude/skills/impeccable/reference/audit.md +134 -0
- package/obj/template/.claude/skills/impeccable/reference/bolder.md +113 -0
- package/obj/template/.claude/skills/impeccable/reference/brand.md +104 -0
- package/obj/template/.claude/skills/impeccable/reference/clarify.md +174 -0
- package/obj/template/.claude/skills/impeccable/reference/cognitive-load.md +106 -0
- package/obj/template/.claude/skills/impeccable/reference/color-and-contrast.md +105 -0
- package/obj/template/.claude/skills/impeccable/reference/colorize.md +154 -0
- package/obj/template/.claude/skills/impeccable/reference/craft.md +138 -0
- package/obj/template/.claude/skills/impeccable/reference/critique.md +213 -0
- package/obj/template/.claude/skills/impeccable/reference/delight.md +302 -0
- package/obj/template/.claude/skills/impeccable/reference/distill.md +111 -0
- package/obj/template/.claude/skills/impeccable/reference/document.md +427 -0
- package/obj/template/.claude/skills/impeccable/reference/extract.md +70 -0
- package/obj/template/.claude/skills/impeccable/reference/harden.md +347 -0
- package/obj/template/.claude/skills/impeccable/reference/heuristics-scoring.md +234 -0
- package/obj/template/.claude/skills/impeccable/reference/interaction-design.md +195 -0
- package/obj/template/.claude/skills/impeccable/reference/layout.md +141 -0
- package/obj/template/.claude/skills/impeccable/reference/live.md +513 -0
- package/obj/template/.claude/skills/impeccable/reference/motion-design.md +99 -0
- package/obj/template/.claude/skills/impeccable/reference/onboard.md +234 -0
- package/obj/template/.claude/skills/impeccable/reference/optimize.md +258 -0
- package/obj/template/.claude/skills/impeccable/reference/overdrive.md +130 -0
- package/obj/template/.claude/skills/impeccable/reference/personas.md +178 -0
- package/obj/template/.claude/skills/impeccable/reference/polish.md +232 -0
- package/obj/template/.claude/skills/impeccable/reference/product.md +62 -0
- package/obj/template/.claude/skills/impeccable/reference/quieter.md +99 -0
- package/obj/template/.claude/skills/impeccable/reference/responsive-design.md +114 -0
- package/obj/template/.claude/skills/impeccable/reference/shape.md +136 -0
- package/obj/template/.claude/skills/impeccable/reference/spatial-design.md +100 -0
- package/obj/template/.claude/skills/impeccable/reference/teach.md +137 -0
- package/obj/template/.claude/skills/impeccable/reference/typeset.md +124 -0
- package/obj/template/.claude/skills/impeccable/reference/typography.md +159 -0
- package/obj/template/.claude/skills/impeccable/reference/ux-writing.md +107 -0
- package/obj/template/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs +284 -0
- package/obj/template/.claude/skills/impeccable/scripts/command-metadata.json +94 -0
- package/obj/template/.claude/skills/impeccable/scripts/design-parser.mjs +820 -0
- package/obj/template/.claude/skills/impeccable/scripts/detect-csp.mjs +198 -0
- package/obj/template/.claude/skills/impeccable/scripts/is-generated.mjs +69 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-accept.mjs +465 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-browser.js +4684 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-inject.mjs +436 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-poll.mjs +187 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-server.mjs +679 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-wrap.mjs +395 -0
- package/obj/template/.claude/skills/impeccable/scripts/live.mjs +247 -0
- package/obj/template/.claude/skills/impeccable/scripts/load-context.mjs +93 -0
- package/obj/template/.claude/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/obj/template/.claude/skills/impeccable/scripts/pin.mjs +214 -0
- package/obj/template/.claude/skills/implement/SKILL.md +83 -0
- package/obj/template/.claude/skills/intake/SKILL.md +46 -0
- package/obj/template/.claude/skills/intake/template.md +61 -0
- package/obj/template/.claude/skills/integrate/SKILL.md +62 -0
- package/obj/template/.claude/skills/memory-flush/SKILL.md +172 -0
- package/obj/template/.claude/skills/memory-flush/sweep.py +286 -0
- package/obj/template/.claude/skills/memory-flush/tests/run.sh +327 -0
- package/obj/template/.claude/skills/prose/SKILL.md +119 -0
- package/obj/template/.claude/skills/rca/SKILL.md +42 -0
- package/obj/template/.claude/skills/rca/template.md +83 -0
- package/obj/template/.claude/skills/research/SKILL.md +75 -0
- package/obj/template/.claude/skills/scenario/SKILL.md +64 -0
- package/obj/template/.claude/skills/scout/SKILL.md +72 -0
- package/obj/template/.claude/skills/security/SKILL.md +75 -0
- package/obj/template/.claude/skills/simplify/SKILL.md +67 -0
- package/obj/template/.claude/skills/spec/SKILL.md +69 -0
- package/obj/template/.claude/skills/spec/template.md +274 -0
- package/obj/template/.claude/skills/spec-diagram-review/SKILL.md +81 -0
- package/obj/template/.claude/skills/spec-lint/SKILL.md +55 -0
- package/obj/template/.claude/skills/spec-lint/lint.sh +218 -0
- package/obj/template/.claude/skills/spec-render/SKILL.md +45 -0
- package/obj/template/.claude/skills/spec-render/render.sh +109 -0
- package/obj/template/.claude/skills/spec-traceability-review/SKILL.md +72 -0
- package/obj/template/.claude/skills/swarm-dispatch/SKILL.md +212 -0
- package/obj/template/.claude/skills/swarm-dispatch/swarm_merge.sh +154 -0
- package/obj/template/.claude/skills/swarm-plan/SKILL.md +90 -0
- package/obj/template/.claude/skills/swarm-plan/validate.sh +181 -0
- package/obj/template/.claude/skills/tdd/SKILL.md +100 -0
- package/obj/template/.claude/skills/technical-tutorials/SKILL.md +569 -0
- package/obj/template/.claude/skills/technical-tutorials/references/audience-context-README.md +53 -0
- package/obj/template/.claude/skills/technical-tutorials/references/audience-context.md +246 -0
- package/obj/template/.claude/skills/technical-tutorials/references/audience-example.md +175 -0
- package/obj/template/.claude/skills/technical-tutorials/references/audience-template.md +152 -0
- package/obj/template/.claude/skills/triage/SKILL.md +55 -0
- package/obj/template/.claude/skills/verify/SKILL.md +74 -0
- package/obj/template/.mcp.json +24 -0
- package/obj/template/CLAUDE.md +327 -0
- package/obj/template/docs/init/seed.md +585 -0
- package/obj/template/manifest.json +214 -0
- package/package.json +48 -0
- package/src/.mcp.template.json +24 -0
- package/src/.npmrc.template +2 -0
- package/src/CLAUDE.template.md +327 -0
- package/src/agents/swarm-worker.template.md +51 -0
- package/src/cli/conflict.js +31 -0
- package/src/cli/doctor.js +152 -0
- package/src/cli/install.js +93 -0
- package/src/cli/io.js +27 -0
- package/src/cli/manifest.js +38 -0
- package/src/cli/mcp.js +54 -0
- package/src/cli/merge.js +107 -0
- package/src/cli/plantuml.js +121 -0
- package/src/cli/util.js +10 -0
- package/src/memory/_pending.template.md +15 -0
- package/src/memory/_resume.template.md +12 -0
- package/src/memory/conventions.template.md +26 -0
- package/src/memory/decisions.template.md +29 -0
- package/src/memory/landmarks.template.md +26 -0
- package/src/memory/landmines.template.md +27 -0
- package/src/memory/libraries.template.md +27 -0
- package/src/memory/pending-questions.template.md +28 -0
- package/src/project.template.json +221 -0
- package/src/seed.template.md +585 -0
- package/src/settings.template.json +110 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: swarm-dispatch
|
|
3
|
+
owner: baseline
|
|
4
|
+
description: Execute a swarm plan wave by wave with filesystem isolation via git worktrees. For each wave, main context decides the scenario recipe + implementation contract for every task, then spawns one swarm-worker per task in parallel. Each worker executes its recipe and reports JSON status. Worktree merge-audit verifies write-set discipline before changes land on main. Aborts remaining waves on any audit or task failure.
|
|
5
|
+
argument-hint: "<slug — matches .claude/state/swarm/<slug>.json>"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# swarm-dispatch — wave runner with worktree isolation
|
|
9
|
+
|
|
10
|
+
Invoked after `/swarm-plan` + `/approve-swarm`. The architecture is the user's principle made concrete:
|
|
11
|
+
|
|
12
|
+
> **Main context decides. Workers execute.**
|
|
13
|
+
|
|
14
|
+
Per task, before dispatch, you (main context) produce two recipes:
|
|
15
|
+
1. The **scenario recipe** — exactly which failing tests the worker should write.
|
|
16
|
+
2. The **implementation contract** — exactly which source files the worker may touch and what behavior they must implement.
|
|
17
|
+
|
|
18
|
+
The worker's prompt contains both recipes verbatim. The worker invokes `Skill(scenario)` then `Skill(implement)` and reports JSON. It makes no design decisions.
|
|
19
|
+
|
|
20
|
+
## Isolation modes
|
|
21
|
+
|
|
22
|
+
Read `project.json → swarm.isolation` (default `"auto"`):
|
|
23
|
+
|
|
24
|
+
- `"auto"` → choose `worktree` if the project root is inside a git repo (`git rev-parse --is-inside-work-tree` succeeds), else `shared`.
|
|
25
|
+
- `"worktree"` → require a git repo; bail if absent.
|
|
26
|
+
- `"shared"` → never use worktrees; rely on `swarm_boundary_guard` for runtime enforcement.
|
|
27
|
+
|
|
28
|
+
**Default path is `worktree`.** The rest of this document describes that mode. The `shared` fallback is at the end.
|
|
29
|
+
|
|
30
|
+
## Prereqs (worktree mode)
|
|
31
|
+
|
|
32
|
+
Verify in order, abort on any failure:
|
|
33
|
+
|
|
34
|
+
1. `.claude/state/swarm/<slug>.json` exists, has `status: "planned"`, and a non-null `waves` array.
|
|
35
|
+
2. `.claude/state/swarm_approvals/<slug>.approval` exists and begins with `APPROVED`.
|
|
36
|
+
3. `.claude/state/swarm/active_wave.json` does **not** already exist (stale/racing dispatch — ask before clobbering).
|
|
37
|
+
4. `git rev-parse --is-inside-work-tree` succeeds at the project root.
|
|
38
|
+
5. Working tree is clean (`git status --porcelain` empty) **if** `project.json → swarm.refuse_dirty_tree` is true (default).
|
|
39
|
+
|
|
40
|
+
Record the baseline: `git rev-parse HEAD` → this SHA is the reference every worktree will be compared against at merge time.
|
|
41
|
+
|
|
42
|
+
## Per-wave loop
|
|
43
|
+
|
|
44
|
+
For each wave in `plan.waves`, in order:
|
|
45
|
+
|
|
46
|
+
### 1. Decide the recipes (main context)
|
|
47
|
+
|
|
48
|
+
For every task in the wave, produce:
|
|
49
|
+
|
|
50
|
+
- **Scenario recipe** — list of failing tests to write. Each: `name`, `covers`, `assertion`, `fixtures`. Plus `out-of-scope` list and `test target paths`.
|
|
51
|
+
- **Implementation contract** — `failing_tests` (the paths the scenario step will produce), `write_set` (from the plan), behavior contract (the spec's §Behavior excerpts for the task's ACs, plus §Design data model + contracts), project conventions (from `project.json`).
|
|
52
|
+
- **Style anchors** — 1–2 existing test files and 1–2 existing source files in the touched modules so the worker matches the project's idioms.
|
|
53
|
+
|
|
54
|
+
This is where the heavy thinking lives. Do it before dispatch — once a worker is running, the recipe cannot be changed.
|
|
55
|
+
|
|
56
|
+
### 2. Raise the barrier
|
|
57
|
+
|
|
58
|
+
Write `.claude/state/swarm/active_wave.json`:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"slug": "<slug>",
|
|
63
|
+
"wave": <n>,
|
|
64
|
+
"isolation": "worktree",
|
|
65
|
+
"baseline_ref": "<HEAD SHA>",
|
|
66
|
+
"started_at": <epoch>,
|
|
67
|
+
"write_sets": [
|
|
68
|
+
{"task_id": "T-001", "files": [...]},
|
|
69
|
+
{"task_id": "T-003", "files": [...]}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
In worktree mode this file is consumed by `swarm_merge.sh` (which reads `baseline_ref`). `swarm_boundary_guard` is dormant — writes happen inside worktrees that don't contain `active_wave.json`.
|
|
75
|
+
|
|
76
|
+
### 3. Update plan status
|
|
77
|
+
|
|
78
|
+
Set each wave task's `status` to `"running"` inside `.claude/state/swarm/<slug>.json`.
|
|
79
|
+
|
|
80
|
+
### 4. Dispatch the wave
|
|
81
|
+
|
|
82
|
+
One message, N parallel `Agent` calls — one per task. Each uses:
|
|
83
|
+
|
|
84
|
+
- `subagent_type: "swarm-worker"`
|
|
85
|
+
- `isolation: "worktree"`
|
|
86
|
+
- `run_in_background: true`
|
|
87
|
+
|
|
88
|
+
Worker prompt template (self-contained — the worker has no memory of this conversation):
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
You are executing swarm task <T-XXX> from plan <slug>, in your own isolated
|
|
92
|
+
git worktree. Your write_set is the ONLY set of files you may modify.
|
|
93
|
+
|
|
94
|
+
# Task metadata
|
|
95
|
+
- task_id: <T-XXX>
|
|
96
|
+
- slug: <slug>
|
|
97
|
+
- ACs covered: <AC list>
|
|
98
|
+
- Component: <component id>
|
|
99
|
+
|
|
100
|
+
# Spec excerpt (behavior contract)
|
|
101
|
+
<paste §Behavior sequences for this task's ACs + §Design data-model/contract
|
|
102
|
+
rows the task touches. Keep under ~200 lines.>
|
|
103
|
+
|
|
104
|
+
# Scenario recipe — what tests to write
|
|
105
|
+
out-of-scope: [<scenarios explicitly NOT to write>]
|
|
106
|
+
test target paths: <test file paths>
|
|
107
|
+
style anchors: <1-2 existing test files>
|
|
108
|
+
|
|
109
|
+
scenarios:
|
|
110
|
+
- name: test_when_X_then_Y
|
|
111
|
+
covers: AC-001
|
|
112
|
+
assertion: "<one plain sentence>"
|
|
113
|
+
fixtures: [<paths/factories>]
|
|
114
|
+
- name: test_when_A_then_B
|
|
115
|
+
covers: AC-002
|
|
116
|
+
assertion: "..."
|
|
117
|
+
fixtures: [...]
|
|
118
|
+
- ...
|
|
119
|
+
|
|
120
|
+
# Implementation contract
|
|
121
|
+
write_set (STRICT — anywhere else fails the merge audit):
|
|
122
|
+
- <file 1>
|
|
123
|
+
- <file 2>
|
|
124
|
+
- ...
|
|
125
|
+
|
|
126
|
+
read_set (advisory):
|
|
127
|
+
- <file 1>
|
|
128
|
+
- ...
|
|
129
|
+
|
|
130
|
+
style anchors: <1-2 existing source files>
|
|
131
|
+
project conventions:
|
|
132
|
+
test.cmd: <...>
|
|
133
|
+
lint.cmd: <...>
|
|
134
|
+
tdd.test_globs: <...>
|
|
135
|
+
|
|
136
|
+
# Your job
|
|
137
|
+
1. Invoke Skill(scenario) with the scenario recipe + test target paths.
|
|
138
|
+
2. If all expected tests are RED, invoke Skill(implement) with the failing test
|
|
139
|
+
paths, the write_set, the behavior contract above, and the project
|
|
140
|
+
conventions.
|
|
141
|
+
3. Report JSON on your final line per the swarm protocol:
|
|
142
|
+
{"task_id": "<T-XXX>", "status": "done" | "failed",
|
|
143
|
+
"files_touched": [...], "note": "<one short line>"}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The `swarm-worker` agent's body already knows the protocol. The prompt contains the recipes; the worker executes them.
|
|
147
|
+
|
|
148
|
+
### 5. Wait
|
|
149
|
+
|
|
150
|
+
Do not respond to the user until every task in the wave has completed. Each `Agent` return gives you the worktree path (if the worker made changes) and the JSON summary line.
|
|
151
|
+
|
|
152
|
+
### 6. Per-task merge-audit
|
|
153
|
+
|
|
154
|
+
For each completed task:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
.claude/skills/swarm-dispatch/swarm_merge.sh \
|
|
158
|
+
.claude/state/swarm/<slug>.json \
|
|
159
|
+
<task-id> \
|
|
160
|
+
<worktree-path>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Outcomes:
|
|
164
|
+
- **Exit 0**: audit passed, patch applied to main, worktree removed. Update task `status: "done"`.
|
|
165
|
+
- **Exit 1**: audit failed OR `git apply` failed. Worktree preserved for inspection. Update task `status: "failed"` with a `note` naming the offending file(s).
|
|
166
|
+
- **No worktree path returned** (worker made no changes): the harness auto-cleans the empty worktree. Mark task per the worker's self-reported JSON.
|
|
167
|
+
|
|
168
|
+
### 7. Clear the barrier
|
|
169
|
+
|
|
170
|
+
Delete `.claude/state/swarm/active_wave.json`.
|
|
171
|
+
|
|
172
|
+
### 8. Decide the wave's fate
|
|
173
|
+
|
|
174
|
+
- Every task `done` → advance to the next wave.
|
|
175
|
+
- Any task `failed` (worker-reported OR audit failure) → set plan `status: "failed"`, stop, surface the failed task(s) with their `note` and (for audit failures) the preserved worktree path.
|
|
176
|
+
|
|
177
|
+
## After the last wave
|
|
178
|
+
|
|
179
|
+
1. Set plan `status: "complete"`.
|
|
180
|
+
2. Run `/integrate` on the full codebase — per-wave success is necessary but not sufficient; cross-component integration must be re-verified.
|
|
181
|
+
3. If `/integrate` passes: tell the user "Swarm `<slug>` complete. `<N>` tasks across `<M>` waves. Next: `/document`."
|
|
182
|
+
|
|
183
|
+
## Shared-mode fallback
|
|
184
|
+
|
|
185
|
+
When isolation is `"shared"`:
|
|
186
|
+
|
|
187
|
+
- No worktrees. Each `Agent` call uses `isolation` omitted or `"none"`.
|
|
188
|
+
- `active_wave.json` carries `isolation: "shared"` and the union of write_sets (no `baseline_ref`).
|
|
189
|
+
- `swarm_boundary_guard` is the runtime enforcer: writes in enforced paths must be in the union of active write_sets, else denied.
|
|
190
|
+
- **No per-task merge-audit** (no worktrees to diff). The guard catches drift out of the wave; cross-task bleed within the wave is a known limitation.
|
|
191
|
+
- After each wave: clear `active_wave.json`, update per-task status from the worker's self-reported JSON.
|
|
192
|
+
|
|
193
|
+
Use shared mode deliberately — it trades real safety (physical isolation) for runtime permissiveness. Worktree mode is preferred whenever git is available.
|
|
194
|
+
|
|
195
|
+
## Failure recovery
|
|
196
|
+
|
|
197
|
+
- Plan stays in `"failed"` state for user inspection.
|
|
198
|
+
- In worktree mode, failed tasks' worktrees are preserved. The user can `cd` in, read the worker's changes, and either:
|
|
199
|
+
- Manually finish + commit to main, then mark the task done in the plan.
|
|
200
|
+
- Drop the worktree (`git worktree remove --force <path>`) and re-plan.
|
|
201
|
+
- In shared mode, partial writes may have landed on main. `git status` shows them; revert or keep as appropriate.
|
|
202
|
+
- **Never auto-retry a failed task.** Failures warrant human attention.
|
|
203
|
+
|
|
204
|
+
## Constraints
|
|
205
|
+
|
|
206
|
+
- **Recipes are decided before dispatch.** Once a worker is running, you cannot change its recipe. Plan with that in mind.
|
|
207
|
+
- **`run_in_background: true` is mandatory** on every `Agent` call inside a wave. Foreground calls would serialize the wave.
|
|
208
|
+
- **`isolation: "worktree"` is mandatory** in worktree mode. Without it, the merge-audit guarantee collapses.
|
|
209
|
+
- **One message, N parallel `Agent` calls.** Sequential issuance defeats parallelism.
|
|
210
|
+
- **`subagent_type` is always `swarm-worker`.** No per-stack variants — stack-specific skill loading is handled by the worker template's `{{SKILLS}}` token at `/init-project` time, not by spawning different agents.
|
|
211
|
+
- **Never touch source files from this skill.** This orchestrator only reads and updates `.claude/state/`. File edits happen inside workers; merges happen via `swarm_merge.sh`.
|
|
212
|
+
- **`active_wave.json` lingering** after abnormal termination is recoverable: delete it, inspect per-task status, re-dispatch the first incomplete wave.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# swarm_merge.sh — post-task merge + audit tool for worktree-isolated swarm tasks.
|
|
3
|
+
#
|
|
4
|
+
# Usage: swarm_merge.sh <plan-path> <task-id> <worktree-path>
|
|
5
|
+
#
|
|
6
|
+
# Inputs:
|
|
7
|
+
# <plan-path> .claude/state/swarm/<slug>.json
|
|
8
|
+
# <task-id> e.g. "T-001" — must exist in plan.tasks[].id
|
|
9
|
+
# <worktree-path> absolute path to the git worktree created by Agent(isolation="worktree")
|
|
10
|
+
#
|
|
11
|
+
# Preconditions:
|
|
12
|
+
# .claude/state/swarm/active_wave.json exists and contains `baseline_ref`
|
|
13
|
+
# (the commit SHA recorded when the wave started). The audit diffs the
|
|
14
|
+
# worktree against this baseline.
|
|
15
|
+
#
|
|
16
|
+
# Behaviour:
|
|
17
|
+
# 1. Loads the task's write_set from the plan.
|
|
18
|
+
# 2. Computes changed files: `git -C <worktree> diff <baseline> --name-only`.
|
|
19
|
+
# 3. AUDIT: every changed file must be in write_set. Any violation → fail loud,
|
|
20
|
+
# preserve the worktree, exit 1.
|
|
21
|
+
# 4. If clean: `git -C <worktree> diff <baseline>` | `git -C <main> apply` to
|
|
22
|
+
# land the changes on main.
|
|
23
|
+
# 5. Removes the worktree on success (`git worktree remove`).
|
|
24
|
+
#
|
|
25
|
+
# Exit codes:
|
|
26
|
+
# 0 merge applied successfully (or task made no changes)
|
|
27
|
+
# 1 audit failed, apply failed, or worktree could not be read
|
|
28
|
+
# 2 bad invocation / missing inputs
|
|
29
|
+
|
|
30
|
+
set -u
|
|
31
|
+
|
|
32
|
+
if [ "${1:-}" = "" ] || [ "${2:-}" = "" ] || [ "${3:-}" = "" ]; then
|
|
33
|
+
echo "usage: swarm_merge.sh <plan-path> <task-id> <worktree-path>" >&2
|
|
34
|
+
exit 2
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
PLAN="$1"
|
|
38
|
+
TASK_ID="$2"
|
|
39
|
+
WT="$3"
|
|
40
|
+
|
|
41
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
42
|
+
|
|
43
|
+
if [ ! -f "$PLAN" ]; then
|
|
44
|
+
echo "swarm_merge: plan not found at $PLAN" >&2
|
|
45
|
+
exit 2
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
if [ ! -d "$WT" ]; then
|
|
49
|
+
echo "swarm_merge: worktree not found at $WT" >&2
|
|
50
|
+
exit 2
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
PLAN="$PLAN" TASK_ID="$TASK_ID" WT="$WT" ROOT="$ROOT" python3 <<'PY'
|
|
54
|
+
import json, os, subprocess, sys
|
|
55
|
+
|
|
56
|
+
plan_path = os.environ['PLAN']
|
|
57
|
+
task_id = os.environ['TASK_ID']
|
|
58
|
+
wt = os.environ['WT']
|
|
59
|
+
root = os.environ['ROOT']
|
|
60
|
+
|
|
61
|
+
plan = json.load(open(plan_path))
|
|
62
|
+
|
|
63
|
+
task = next((t for t in plan.get('tasks', []) if t.get('id') == task_id), None)
|
|
64
|
+
if task is None:
|
|
65
|
+
print(f"swarm_merge: task {task_id} not found in plan", file=sys.stderr)
|
|
66
|
+
sys.exit(2)
|
|
67
|
+
|
|
68
|
+
write_set = set(task.get('write_set') or [])
|
|
69
|
+
if not write_set:
|
|
70
|
+
print(f"swarm_merge: task {task_id} has empty write_set — refusing to merge", file=sys.stderr)
|
|
71
|
+
sys.exit(2)
|
|
72
|
+
|
|
73
|
+
# Read baseline from active_wave.json
|
|
74
|
+
active_path = os.path.join(root, '.claude/state/swarm/active_wave.json')
|
|
75
|
+
try:
|
|
76
|
+
active = json.load(open(active_path))
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"swarm_merge: active_wave.json unreadable: {e}", file=sys.stderr)
|
|
79
|
+
sys.exit(2)
|
|
80
|
+
|
|
81
|
+
baseline = active.get('baseline_ref')
|
|
82
|
+
if not baseline:
|
|
83
|
+
print("swarm_merge: active_wave.json missing baseline_ref", file=sys.stderr)
|
|
84
|
+
sys.exit(2)
|
|
85
|
+
|
|
86
|
+
# Change detection: diff worktree against baseline.
|
|
87
|
+
r = subprocess.run(
|
|
88
|
+
['git', '-C', wt, 'diff', baseline, '--name-only'],
|
|
89
|
+
capture_output=True
|
|
90
|
+
)
|
|
91
|
+
if r.returncode != 0:
|
|
92
|
+
print(f"swarm_merge: `git diff` in worktree failed: {r.stderr.decode(errors='replace')}",
|
|
93
|
+
file=sys.stderr)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
changed = [f for f in r.stdout.decode().splitlines() if f.strip()]
|
|
97
|
+
|
|
98
|
+
# If task made no changes, nothing to merge — clean up and exit OK.
|
|
99
|
+
if not changed:
|
|
100
|
+
rm = subprocess.run(['git', '-C', root, 'worktree', 'remove', wt], capture_output=True)
|
|
101
|
+
if rm.returncode != 0:
|
|
102
|
+
print(f"swarm_merge: worktree removal warned: {rm.stderr.decode(errors='replace').strip()}",
|
|
103
|
+
file=sys.stderr)
|
|
104
|
+
print(f"swarm_merge: OK — task {task_id} made no changes; worktree cleaned up")
|
|
105
|
+
sys.exit(0)
|
|
106
|
+
|
|
107
|
+
# AUDIT: every changed file must be in the declared write_set.
|
|
108
|
+
violations = [f for f in changed if f not in write_set]
|
|
109
|
+
if violations:
|
|
110
|
+
print(f"swarm_merge: AUDIT FAIL — task {task_id} modified files outside its declared write_set:")
|
|
111
|
+
for v in sorted(violations):
|
|
112
|
+
print(f" + {v}")
|
|
113
|
+
print(f"Declared write_set ({len(write_set)} file(s)):")
|
|
114
|
+
for f in sorted(write_set):
|
|
115
|
+
print(f" - {f}")
|
|
116
|
+
print(f"Worktree preserved for inspection at: {wt}")
|
|
117
|
+
print(f"Branch: swarm/{task_id} (inspect with `git log swarm/{task_id}` or `git diff {baseline}..swarm/{task_id}`)")
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
# Audit passed. Extract full patch and apply to main.
|
|
121
|
+
r = subprocess.run(['git', '-C', wt, 'diff', baseline], capture_output=True)
|
|
122
|
+
if r.returncode != 0:
|
|
123
|
+
print(f"swarm_merge: `git diff` (full patch) failed: {r.stderr.decode(errors='replace')}",
|
|
124
|
+
file=sys.stderr)
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
|
|
127
|
+
patch = r.stdout
|
|
128
|
+
if not patch.strip():
|
|
129
|
+
# Diff --name-only found files but full diff is empty — bizarre, but bail safely.
|
|
130
|
+
print(f"swarm_merge: diff was empty despite changed files. Worktree preserved at {wt}",
|
|
131
|
+
file=sys.stderr)
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
apply_r = subprocess.run(
|
|
135
|
+
['git', '-C', root, 'apply', '--whitespace=nowarn', '-'],
|
|
136
|
+
input=patch, capture_output=True
|
|
137
|
+
)
|
|
138
|
+
if apply_r.returncode != 0:
|
|
139
|
+
print(f"swarm_merge: APPLY FAIL — patch from {wt} did not apply cleanly to main:")
|
|
140
|
+
print(apply_r.stderr.decode(errors='replace').strip())
|
|
141
|
+
print(f"Worktree preserved for inspection at: {wt}")
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
# Remove the worktree.
|
|
145
|
+
rm = subprocess.run(['git', '-C', root, 'worktree', 'remove', wt], capture_output=True)
|
|
146
|
+
if rm.returncode != 0:
|
|
147
|
+
# Not fatal — warn but consider the merge successful.
|
|
148
|
+
print(f"swarm_merge: WARNING — could not remove worktree at {wt}: "
|
|
149
|
+
f"{rm.stderr.decode(errors='replace').strip()}", file=sys.stderr)
|
|
150
|
+
|
|
151
|
+
print(f"swarm_merge: OK — task {task_id} merged ({len(changed)} file(s))")
|
|
152
|
+
for f in sorted(changed):
|
|
153
|
+
print(f" + {f}")
|
|
154
|
+
PY
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: swarm-plan
|
|
3
|
+
owner: baseline
|
|
4
|
+
description: Decompose an approved spec into a dependency-ordered swarm plan — one task per component, each with an explicit write_set. Produces `.claude/state/swarm/<slug>.json` with tasks + waves. The wave scheduler guarantees pairwise-disjoint write_sets within each wave so parallel dispatch is provably conflict-free.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# swarm-plan — Phase 5.5: decompose for parallel execution
|
|
8
|
+
|
|
9
|
+
Invoked after `/approve-spec` and before `/tdd` on any spec that has ≥3 independent components worth parallelizing (per `project.json → swarm.min_tasks_worth_swarming`). For smaller specs, skip swarm and go straight to `/tdd` solo.
|
|
10
|
+
|
|
11
|
+
## Prereqs
|
|
12
|
+
|
|
13
|
+
1. `.claude/state/spec_approvals/<slug>.approval` exists (spec is human-approved).
|
|
14
|
+
2. `docs/specs/<slug>.md` exists and passes `/spec-lint`.
|
|
15
|
+
3. `docs/scout/<slug>.md` exists (component → file mapping is its job).
|
|
16
|
+
|
|
17
|
+
If any prereq is missing, stop and surface what's needed.
|
|
18
|
+
|
|
19
|
+
## Output contract
|
|
20
|
+
|
|
21
|
+
`.claude/state/swarm/<slug>.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"slug": "<slug>",
|
|
26
|
+
"spec": "docs/specs/<slug>.md",
|
|
27
|
+
"created_at": <epoch>,
|
|
28
|
+
"status": "planned",
|
|
29
|
+
"tasks": [
|
|
30
|
+
{
|
|
31
|
+
"id": "T-001",
|
|
32
|
+
"title": "<what the task does — one line>",
|
|
33
|
+
"component": "<component id from C4 Component diagram>",
|
|
34
|
+
"acs": ["AC-001", "AC-002"],
|
|
35
|
+
"write_set": ["src/foo/bar.py", "tests/foo/test_bar.py"],
|
|
36
|
+
"read_set": ["src/common/http.py"],
|
|
37
|
+
"depends_on": []
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"waves": null
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You produce `tasks[]`. The validator (`validate.sh`) computes `waves[]` deterministically via Kahn-with-disjointness.
|
|
45
|
+
|
|
46
|
+
## Steps
|
|
47
|
+
|
|
48
|
+
1. **Read upstream**: the spec, the scout report, the approval token. If an older `.claude/state/swarm/<slug>.json` exists, confirm with the user whether to overwrite (replan) or abort.
|
|
49
|
+
2. **Extract inputs** from the spec:
|
|
50
|
+
- **Components**: every `Component(id, …)` in the C4 Component diagram (there may be multiple C4 Component diagrams, one per container).
|
|
51
|
+
- **ACs**: every row in the Acceptance criteria table.
|
|
52
|
+
- **Dependency edges**: every `A --> B` in the spec's dependency-graph fence.
|
|
53
|
+
- **Behavior ↔ component mapping**: for each AC, the sequence diagram it references names participants; treat each non-actor, non-external participant as a component this AC touches.
|
|
54
|
+
3. **Get file mapping** from the scout report: for each component id, the files that back it. If the spec introduces greenfield components not in the scout, propose new file paths and flag them under a "new_paths" note in your plan summary — they will be accepted by the boundary guard when declared in `write_set`.
|
|
55
|
+
4. **Construct tasks** — one per component (per `swarm.granularity: component`):
|
|
56
|
+
- `id`: T-001, T-002, … in stable order.
|
|
57
|
+
- `title`: one-line imperative description.
|
|
58
|
+
- `component`: the C4 component id.
|
|
59
|
+
- `acs`: every AC whose sequence names this component.
|
|
60
|
+
- `write_set`: union of (component files) + (test files covering those ACs). Every file must be explicit; no globs.
|
|
61
|
+
- `read_set`: files this task will consult but not modify. Advisory; not enforced.
|
|
62
|
+
- `depends_on`: for each component B such that this component depends on B (per the dependency graph), include the task id of the task owning B.
|
|
63
|
+
5. **Merge overlapping tasks where forced**:
|
|
64
|
+
- If two tasks share any file AND have no `depends_on` relationship, either introduce a `depends_on` edge (making them sequential across waves) or merge them into one task. Merging is preferred when they're on the same component.
|
|
65
|
+
6. **Validate the plan**:
|
|
66
|
+
```
|
|
67
|
+
.claude/skills/swarm-plan/validate.sh docs/specs/<slug>.md .claude/state/swarm/<slug>.json
|
|
68
|
+
```
|
|
69
|
+
The validator checks: required fields, depends_on references resolve, DAG is acyclic, and assigns `waves[]`. If validation fails, it prints a precise error — fix the plan and re-run.
|
|
70
|
+
7. **Surface the plan** to the user as a table:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
Swarm plan for <slug> — <N> tasks across <M> waves
|
|
74
|
+
|
|
75
|
+
wave 1:
|
|
76
|
+
T-001 webhook-worker [AC-001, AC-002] 3 files
|
|
77
|
+
T-003 backoff-policy [AC-004] 2 files
|
|
78
|
+
wave 2:
|
|
79
|
+
T-002 webhook-retry [AC-003] 2 files (needs T-001)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
8. Tell the user: "Swarm planned at `.claude/state/swarm/<slug>.json`. Review it, then run `/approve-swarm <slug>`. After approval, run `/swarm-dispatch <slug>`."
|
|
83
|
+
|
|
84
|
+
## Constraints
|
|
85
|
+
|
|
86
|
+
- **Never dispatch from this skill.** Planning and execution are separated by a human consent gate (`/approve-swarm`).
|
|
87
|
+
- **Every file in a `write_set` must be a concrete path**, not a glob. The boundary guard does string-level membership checks.
|
|
88
|
+
- **The `validate.sh` script is the source of truth for wave assignment.** Do not hand-write `waves[]`.
|
|
89
|
+
- **Greenfield files are allowed** in `write_set` even if they don't exist yet — the guard checks declared ownership, not disk presence.
|
|
90
|
+
- **If validation keeps failing**, the problem is usually that two tasks share a file with no dependency. Either merge or introduce a dependency edge.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# swarm-plan validator — verifies a draft plan and assigns waves deterministically.
|
|
3
|
+
#
|
|
4
|
+
# Usage: validate.sh <spec-path> <plan-path>
|
|
5
|
+
#
|
|
6
|
+
# Reads plan-path (JSON), performs:
|
|
7
|
+
# - schema check: required fields on every task
|
|
8
|
+
# - reference check: depends_on ids all resolve to tasks in the plan
|
|
9
|
+
# - acyclicity: DAG has no cycles (Kahn's algorithm)
|
|
10
|
+
# - wave assignment: topological sort with pairwise-disjoint write_set constraint
|
|
11
|
+
#
|
|
12
|
+
# On success, rewrites plan-path with `waves` populated. Exit 0.
|
|
13
|
+
# On failure, prints the precise violation to stderr and exits non-zero.
|
|
14
|
+
|
|
15
|
+
set -u
|
|
16
|
+
|
|
17
|
+
if [ "${1:-}" = "" ] || [ "${2:-}" = "" ]; then
|
|
18
|
+
echo "usage: validate.sh <spec-path> <plan-path>" >&2
|
|
19
|
+
exit 2
|
|
20
|
+
fi
|
|
21
|
+
SPEC="$1"
|
|
22
|
+
PLAN="$2"
|
|
23
|
+
|
|
24
|
+
if [ ! -f "$PLAN" ]; then
|
|
25
|
+
echo "validate: plan not found at $PLAN" >&2
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
SPEC="$SPEC" PLAN="$PLAN" python3 <<'PY'
|
|
30
|
+
import json, os, sys, time
|
|
31
|
+
|
|
32
|
+
plan_path = os.environ["PLAN"]
|
|
33
|
+
try:
|
|
34
|
+
plan = json.load(open(plan_path))
|
|
35
|
+
except Exception as e:
|
|
36
|
+
print(f"validate: plan is not valid JSON: {e}", file=sys.stderr)
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
errs = []
|
|
40
|
+
|
|
41
|
+
# Top-level fields.
|
|
42
|
+
for k in ("slug", "spec", "tasks"):
|
|
43
|
+
if k not in plan:
|
|
44
|
+
errs.append(f"missing top-level field: {k}")
|
|
45
|
+
|
|
46
|
+
tasks = plan.get("tasks") or []
|
|
47
|
+
if not isinstance(tasks, list) or not tasks:
|
|
48
|
+
errs.append("tasks[] must be a non-empty array")
|
|
49
|
+
|
|
50
|
+
# Per-task schema.
|
|
51
|
+
REQ = {"id", "title", "component", "acs", "write_set", "depends_on"}
|
|
52
|
+
ids = set()
|
|
53
|
+
for i, t in enumerate(tasks):
|
|
54
|
+
if not isinstance(t, dict):
|
|
55
|
+
errs.append(f"task[{i}] is not an object"); continue
|
|
56
|
+
missing = REQ - set(t.keys())
|
|
57
|
+
if missing:
|
|
58
|
+
errs.append(f"task[{i}] missing fields: {sorted(missing)}")
|
|
59
|
+
continue
|
|
60
|
+
if not isinstance(t["id"], str) or not t["id"]:
|
|
61
|
+
errs.append(f"task[{i}].id must be a non-empty string")
|
|
62
|
+
if t["id"] in ids:
|
|
63
|
+
errs.append(f"duplicate task id: {t['id']}")
|
|
64
|
+
ids.add(t["id"])
|
|
65
|
+
for listfield in ("acs", "write_set", "depends_on"):
|
|
66
|
+
v = t.get(listfield)
|
|
67
|
+
if not isinstance(v, list) or not all(isinstance(x, str) for x in v):
|
|
68
|
+
errs.append(f"task {t.get('id', '?')}.{listfield} must be a list of strings")
|
|
69
|
+
if not t["write_set"]:
|
|
70
|
+
errs.append(f"task {t['id']}.write_set is empty — every task must declare at least one file")
|
|
71
|
+
|
|
72
|
+
if errs:
|
|
73
|
+
for e in errs: print(f"validate: {e}", file=sys.stderr)
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
|
|
76
|
+
# depends_on references resolve.
|
|
77
|
+
for t in tasks:
|
|
78
|
+
for d in t["depends_on"]:
|
|
79
|
+
if d not in ids:
|
|
80
|
+
errs.append(f"task {t['id']}.depends_on references unknown id: {d}")
|
|
81
|
+
if d == t["id"]:
|
|
82
|
+
errs.append(f"task {t['id']} depends on itself")
|
|
83
|
+
|
|
84
|
+
if errs:
|
|
85
|
+
for e in errs: print(f"validate: {e}", file=sys.stderr)
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
# Cycle detection (Kahn's).
|
|
89
|
+
indeg = {t["id"]: 0 for t in tasks}
|
|
90
|
+
outedges = {t["id"]: [] for t in tasks}
|
|
91
|
+
for t in tasks:
|
|
92
|
+
for d in t["depends_on"]:
|
|
93
|
+
# edge: d -> t (t depends on d; d must finish first).
|
|
94
|
+
outedges[d].append(t["id"])
|
|
95
|
+
indeg[t["id"]] += 1
|
|
96
|
+
|
|
97
|
+
by_id = {t["id"]: t for t in tasks}
|
|
98
|
+
ready = sorted([tid for tid, deg in indeg.items() if deg == 0])
|
|
99
|
+
visited = 0
|
|
100
|
+
topo_order = []
|
|
101
|
+
indeg_work = dict(indeg)
|
|
102
|
+
ready_work = list(ready)
|
|
103
|
+
while ready_work:
|
|
104
|
+
nxt = ready_work.pop(0)
|
|
105
|
+
topo_order.append(nxt)
|
|
106
|
+
visited += 1
|
|
107
|
+
for n in sorted(outedges[nxt]):
|
|
108
|
+
indeg_work[n] -= 1
|
|
109
|
+
if indeg_work[n] == 0:
|
|
110
|
+
ready_work.append(n)
|
|
111
|
+
ready_work.sort()
|
|
112
|
+
|
|
113
|
+
if visited != len(tasks):
|
|
114
|
+
unvisited = [tid for tid in indeg if indeg_work[tid] > 0]
|
|
115
|
+
print(f"validate: dependency graph has a cycle among: {unvisited}", file=sys.stderr)
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
|
|
118
|
+
# Wave assignment: greedy topological layering with pairwise-disjoint write_set.
|
|
119
|
+
# Within a wave, all tasks share no files. Tasks whose deps are done AND whose
|
|
120
|
+
# write_set doesn't clash with already-chosen wave members go in; others wait.
|
|
121
|
+
indeg2 = dict(indeg)
|
|
122
|
+
placed = set()
|
|
123
|
+
waves = []
|
|
124
|
+
remaining = set(ids)
|
|
125
|
+
while remaining:
|
|
126
|
+
# Candidates: remaining tasks with indeg2 == 0.
|
|
127
|
+
candidates = sorted([tid for tid in remaining if indeg2[tid] == 0])
|
|
128
|
+
if not candidates:
|
|
129
|
+
# Shouldn't happen given acyclicity, but defensive.
|
|
130
|
+
print(f"validate: internal error — no candidates but tasks remain: {remaining}", file=sys.stderr)
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
|
|
133
|
+
wave = []
|
|
134
|
+
wave_files = set()
|
|
135
|
+
# Greedy: heaviest task first (most files), so overflow tasks get pushed with
|
|
136
|
+
# smaller write_sets and can pack into later waves.
|
|
137
|
+
candidates.sort(key=lambda tid: (-len(by_id[tid]["write_set"]), tid))
|
|
138
|
+
overflow = []
|
|
139
|
+
for tid in candidates:
|
|
140
|
+
files = set(by_id[tid]["write_set"])
|
|
141
|
+
if files & wave_files:
|
|
142
|
+
overflow.append(tid)
|
|
143
|
+
else:
|
|
144
|
+
wave.append(tid)
|
|
145
|
+
wave_files |= files
|
|
146
|
+
if not wave:
|
|
147
|
+
# Every candidate conflicts with every other — impossible unless a single
|
|
148
|
+
# candidate had an internal duplicate; but then it's still placeable alone.
|
|
149
|
+
wave = [candidates[0]]
|
|
150
|
+
overflow = candidates[1:]
|
|
151
|
+
wave_files = set(by_id[candidates[0]]["write_set"])
|
|
152
|
+
|
|
153
|
+
wave.sort()
|
|
154
|
+
waves.append(wave)
|
|
155
|
+
for tid in wave:
|
|
156
|
+
remaining.discard(tid)
|
|
157
|
+
# Decrement indeg of descendants AFTER the whole wave is placed so the
|
|
158
|
+
# remaining wave members don't become each other's dependents mid-wave.
|
|
159
|
+
for tid in wave:
|
|
160
|
+
for n in outedges[tid]:
|
|
161
|
+
indeg2[n] -= 1
|
|
162
|
+
|
|
163
|
+
plan["waves"] = waves
|
|
164
|
+
plan["status"] = "planned"
|
|
165
|
+
plan["validated_at"] = int(time.time())
|
|
166
|
+
|
|
167
|
+
# Write back.
|
|
168
|
+
with open(plan_path, "w") as f:
|
|
169
|
+
json.dump(plan, f, indent=2)
|
|
170
|
+
|
|
171
|
+
# Human summary to stdout.
|
|
172
|
+
print(f"validate: OK — {len(tasks)} task(s) in {len(waves)} wave(s).")
|
|
173
|
+
for i, wave in enumerate(waves, start=1):
|
|
174
|
+
print(f" wave {i}:")
|
|
175
|
+
for tid in wave:
|
|
176
|
+
t = by_id[tid]
|
|
177
|
+
nfiles = len(t["write_set"])
|
|
178
|
+
acs = ",".join(t["acs"]) if t["acs"] else "-"
|
|
179
|
+
deps = ",".join(t["depends_on"]) if t["depends_on"] else "-"
|
|
180
|
+
print(f" {tid} {t['component']:<24} [{acs}] {nfiles} file(s) deps={deps}")
|
|
181
|
+
PY
|