@ikon85/agent-workflow-kit 0.10.0 → 0.11.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/.agents/skills/kit-release/SKILL.md +26 -5
- package/.agents/skills/scale-check/SKILL.md +5 -3
- package/.agents/skills/to-issues/SKILL.md +29 -0
- package/.agents/skills/to-waves/SKILL.md +60 -21
- package/.claude/skills/kit-release/SKILL.md +26 -5
- package/.claude/skills/scale-check/SKILL.md +5 -3
- package/.claude/skills/to-issues/SKILL.md +29 -0
- package/.claude/skills/to-waves/SKILL.md +60 -21
- package/README.md +13 -0
- package/agent-workflow-kit.package.json +23 -9
- package/package.json +3 -2
- package/scripts/build-kit.test.mjs +1 -0
- package/scripts/kit-release.test.mjs +10 -0
- package/scripts/release-parity.mjs +52 -0
- package/scripts/release-parity.test.mjs +53 -0
- package/scripts/release-state.mjs +172 -0
- package/scripts/release-state.test.mjs +106 -0
- package/scripts/test_program_planning_contract.py +63 -0
- package/src/lib/bundle.mjs +5 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kit-release
|
|
3
|
-
description: "Prepare a verified release PR for agent-workflow-kit
|
|
3
|
+
description: "Prepare a verified release PR for agent-workflow-kit, delegate landing to wrapup, then monitor the trusted post-merge publishing flow through npm/GitHub parity."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Kit Release
|
|
@@ -42,10 +42,31 @@ merge, registry publishing, tags, or release creation.
|
|
|
42
42
|
`scripts/wrapup-land.py` exclusively own commit, push, PR creation, merge,
|
|
43
43
|
and cleanup. Do not reproduce those operations here.
|
|
44
44
|
|
|
45
|
-
5. After merge, the release
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
5. After merge, monitor the `release.yml` workflow and inspect its externally
|
|
46
|
+
reconstructable state:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
gh run list --workflow release.yml --limit 1
|
|
50
|
+
npm run release:status
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`awaiting-npm` means the trusted workflow has not published yet;
|
|
54
|
+
`awaiting-github` means npm readback passed and a safe rerun will skip npm
|
|
55
|
+
publish and resume at GitHub; `released` means local, npm, and the GitHub
|
|
56
|
+
release asset have identical version, manifest hash, and tarball integrity.
|
|
57
|
+
The release is complete only at `released`.
|
|
58
|
+
|
|
59
|
+
The registry identity is **`@ikon85/agent-workflow-kit`**. The unscoped
|
|
60
|
+
`agent-workflow-kit` package is owned by another publisher and must never be
|
|
61
|
+
queried, published, or treated as this repository's release.
|
|
62
|
+
|
|
63
|
+
Before the first real release, the npm package must name this repository's
|
|
64
|
+
exact `.github/workflows/release.yml` as its GitHub Trusted Publisher. If it
|
|
65
|
+
does not exist yet, stop for a separately confirmed one-time bootstrap
|
|
66
|
+
publish; npm only permits trust configuration for an existing package. That
|
|
67
|
+
bootstrap creates the scoped package but does not replace the first real
|
|
68
|
+
OIDC/provenance release. Never request, store, or substitute an npm token,
|
|
69
|
+
and never publish or create a release ad hoc beyond that explicit bootstrap.
|
|
49
70
|
|
|
50
71
|
## Guard contract
|
|
51
72
|
|
|
@@ -97,8 +97,10 @@ Tripped altitude criteria: <e.g. C1 staged delivery, C2 multiple subsystems, C4
|
|
|
97
97
|
structure-bearing decisions (escalate a single bounded choice to `decision-gate`).
|
|
98
98
|
2. `to-prd` — it auto-detects the program mode from the Wellenplan chapter and writes
|
|
99
99
|
a Program-PRD (native anchor over the waves).
|
|
100
|
-
3. `to-waves` —
|
|
101
|
-
|
|
100
|
+
3. `to-waves` — after one complete chat preview, materialize the whole Program with
|
|
101
|
+
**all waves execute-ready by default**: complete slice contracts, buckets,
|
|
102
|
+
dependencies and handoffs. Decision Gates, Verify Spikes and Design-Grill waves
|
|
103
|
+
are explicit planned exceptions; generic late-binding cleanup is not.
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
### Feature (0–1 criteria, or any doubt)
|
|
@@ -132,7 +134,7 @@ Bug: <what's broken — expected vs. actual>.
|
|
|
132
134
|
```
|
|
133
135
|
scale-check: verdict=<Program | Feature | Direct-Slice | Bug>
|
|
134
136
|
criteria tripped=<C1,C2,… | none | n/a (bug/direct)>
|
|
135
|
-
route=<program grill → to-prd → to-waves | grill-with-docs → to-prd → to-issues | tdd | diagnose>
|
|
137
|
+
route=<program grill → to-prd → to-waves (all waves execute-ready by default) | grill-with-docs → to-prd → to-issues | tdd | diagnose>
|
|
136
138
|
start-prompt: <the paste-ready block above, filled in>
|
|
137
139
|
```
|
|
138
140
|
|
|
@@ -98,6 +98,14 @@ Ask the user:
|
|
|
98
98
|
|
|
99
99
|
Iterate until the user approves the breakdown.
|
|
100
100
|
|
|
101
|
+
**Program batch handoff.** When `to-waves` invokes this skill for a Stufe-1p stub,
|
|
102
|
+
the complete Program preview has already shown and approved every wave's slice
|
|
103
|
+
contract. Reuse that approval when this pass preserves the approved cut: do not
|
|
104
|
+
pause again merely because the internal pipeline crossed a skill boundary. Run
|
|
105
|
+
the normal contract in full — maturity is not a reduced mode — and reconcile the
|
|
106
|
+
stub's existing leaves instead of duplicating them. A new structural choice or a
|
|
107
|
+
newly discovered gate invalidates the inherited approval and returns to §4.
|
|
108
|
+
|
|
101
109
|
### 5. Publish — promote-or-atomic (contract)
|
|
102
110
|
|
|
103
111
|
**Cluster/Wave discriminator (identical wording — `to-prd` §2 / `to-issues` §5):** `type:cluster` (label) always stops. A Wave number also stops, **unless** the target carries the `wave-stub` label — a Wave-stamped `wave-stub` issue is a **Stufe-1p Program-stub** (`to-waves`-published, native parent = a Program-PRD) and remains a **valid target**. A Wave-stamped issue **without** `wave-stub` (an already-assigned leaf, or a drifted item) is still a hard stop.
|
|
@@ -193,6 +201,21 @@ python3 scripts/board-sync.py dep-add --issue <blocked#> --blocked-by <blocker#>
|
|
|
193
201
|
|
|
194
202
|
The Draft-PRD stays the executable leaf. Edit its body: **remove** `<!-- prd: awaiting-decomposition -->`, keep `type:*`+`priority:*` (**no** `type:cluster`/Wave, **no** `Welle N` title prefix), stamp the leaf `plan_revision`, add the `## Handoff Start Command`. The single PR `closes #<prd#>`.
|
|
195
203
|
|
|
204
|
+
**Program-batch atomic exception.** A Stufe-1p stub already represents one named
|
|
205
|
+
wave under a Program-PRD. When its approved cut contains exactly one slice, keep
|
|
206
|
+
the stub itself as the atomic executable wave leaf: preserve its existing Wave and Phase,
|
|
207
|
+
`Welle N — …` title, native Program parent, and source marker; write the complete
|
|
208
|
+
atomic contract and bucket, then remove only `wave-stub`. Do not erase Program
|
|
209
|
+
navigation by applying the ordinary Wave-less atomic normalization.
|
|
210
|
+
|
|
211
|
+
On a delta re-run, a legacy or interrupted preliminary child may already be linked
|
|
212
|
+
under that atomic stub. Lift any still-relevant content into the stub, unlink the
|
|
213
|
+
child with `board-sync.py unlink`, and close it with the mapping comment
|
|
214
|
+
`superseded by atomic wave leaf #<stub>`. Scan both `children-of` and the Program's
|
|
215
|
+
`program-leaf-source` markers: the **matching source marker remains discoverable after unlink**.
|
|
216
|
+
Resume idempotently — unlink only when still linked; close only when still open;
|
|
217
|
+
an already completed step is a no-op. The final atomic wave has no child set.
|
|
218
|
+
|
|
196
219
|
Then set the bucket via the helper — the leaf already exists, so the workflow-label write goes through `board-sync.py add --bucket` (§5a forbids a bare `gh issue edit --add-label ready-for-agent`):
|
|
197
220
|
|
|
198
221
|
```bash
|
|
@@ -215,6 +238,12 @@ Every child **and** the atomic leaf sits in **exactly one** bucket:
|
|
|
215
238
|
|
|
216
239
|
Status alone cannot discriminate (both are `Spec`) — the **label** does. The helper's `--hitl` flag rejects a `ready-for-agent` label mechanically. Authority = `scripts/execute-ready-check.py` (`parse_bucket`) — the checker wins on a mismatch.
|
|
217
240
|
|
|
241
|
+
**A mandatory human or external setup action is never AFK.** Cut it as a HITL gate
|
|
242
|
+
slice (or an explicitly named planning wave), place it before the dependent build
|
|
243
|
+
slice, and set the native blocked-by edge. Never hide a maintainer configuration,
|
|
244
|
+
credentialless console action, approval, or design response inside a leaf carrying
|
|
245
|
+
`ready-for-agent`, even when the remaining code work could run unattended.
|
|
246
|
+
|
|
218
247
|
**Gate slices (🔬/📐) are AFK.** A `🔬` Verify-Spike or `📐` Trade-off/Research gate slice is **read-only** (a fact question, or a bounded trade-off + read-only research) — an agent can run it solo, so it carries `ready-for-agent` like any other AFK slice; the **native blocked-by edge** on its dependent build slice (set at publish, §5a step 5) is what protects the gate-before-build ordering (§3b/§4), not the HITL bucket. Only a `🧭` Design-Grill gate slice is HITL (it needs the human in the grill). A gate slice's **placement** — which altitude/layer it lands on — follows `decision-gate`'s placement rule (owned there, referenced here).
|
|
219
248
|
|
|
220
249
|
#### 5d. Issue body template (each child / atomic leaf)
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: to-waves
|
|
3
3
|
disable-model-invocation: true
|
|
4
|
-
description: "
|
|
4
|
+
description: "Turn a Program-PRD's wave plan into fully planned, execute-ready wave anchors and slice leaves after one complete chat preview. Validates the graph, publishes every wave, runs the to-prd/spec-self-critique/to-issues maturity contract for each wave, models unresolved work as explicit gates, and audits counted readiness. Idempotent and crash-recoverable. Use on a Program-PRD over several waves. NOT for a single-wave feature (to-issues) or backlog clustering (board-to-waves)."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# to-waves — Unfold a Program-PRD's wave plan onto the board
|
|
8
8
|
|
|
9
9
|
Takes a **Program-PRD** — the native Sub-Issue anchor over a multi-wave program
|
|
10
10
|
(Programm → Phase → Welle → Slice) — and turns its `## Wellenplan` chapter into
|
|
11
|
-
**
|
|
12
|
-
`scale-check → grill → to-prd (program mode) → to-waves
|
|
11
|
+
**fully planned wave anchors + slice leaves** on the board. Pipeline position:
|
|
12
|
+
`scale-check → grill → to-prd (program mode) → to-waves`, with the per-wave
|
|
13
|
+
`to-prd → spec-self-critique → to-issues` maturity pass owned inside this run.
|
|
13
14
|
The **grill and to-prd sit upstream**; to-waves runs once the Program-PRD exists.
|
|
14
15
|
It **never invents structure** — it only unfolds what the plan already decided.
|
|
15
16
|
|
|
@@ -70,28 +71,39 @@ Show:
|
|
|
70
71
|
- The **publish plan**: how many stubs + leaves will be created, which referenced
|
|
71
72
|
existing issues will be **adopted** (§4) rather than created, and which
|
|
72
73
|
Wave/Phase stamps will be applied.
|
|
74
|
+
- The complete per-wave issue contracts that will be materialized: outcome,
|
|
75
|
+
Blast-Radius, acceptance criteria, AFK/HITL bucket, native dependencies, and
|
|
76
|
+
self-contained handoff. A placeholder is not preview-complete.
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
This complete Program preview is the **single user approval** for the default
|
|
79
|
+
planning run. Only on an explicit "yes" proceed to §4/§5/§7; when a later
|
|
80
|
+
per-wave maturity pass reproduces the approved cut, **do not ask for another per-wave approval**.
|
|
81
|
+
Ask again only when new evidence changes structure or exposes
|
|
82
|
+
a genuinely undecided gate. On "no", stop — no writes happened.
|
|
76
83
|
|
|
77
84
|
## 4. Publish — the fixed order
|
|
78
85
|
|
|
79
86
|
After approval, publish in **exactly this order** (issue CREATES stay sequential so
|
|
80
87
|
the numbering is deterministic; only the field stamps are batched):
|
|
81
88
|
|
|
82
|
-
1. **Wave stubs — Stufe 1p.** One per Wellenplan row, native parent = the PRD.
|
|
89
|
+
1. **Wave stubs — transient Stufe 1p.** One per Wellenplan row, native parent = the PRD.
|
|
83
90
|
Title `Welle <N> — <Name>`; created with the `wave-stub` label and Status Spec.
|
|
84
91
|
Body = the Stufe-1p stub template (named header, the program idempotency marker,
|
|
85
|
-
the revision marker — see §6).
|
|
92
|
+
the revision marker — see §6). This is a crash-recovery checkpoint inside the
|
|
93
|
+
run, never its successful terminal state. Not yet Wave/Phase-stamped (step 4).
|
|
86
94
|
```bash
|
|
87
95
|
python3 scripts/board-sync.py create --title "Welle <N> — <Name>" \
|
|
88
96
|
--body-file <stub.md> --wave-stub --status-role spec
|
|
89
97
|
```
|
|
90
|
-
2. **
|
|
91
|
-
|
|
98
|
+
2. **Preliminary slice leaves for multi-slice waves.** Create one per slice under
|
|
99
|
+
each wave whose approved cut contains ≥2 slices, native parent = its stub. For
|
|
100
|
+
an exactly-one-slice wave, **do not create a preliminary child**: keep that
|
|
101
|
+
slice's metadata on the stub so step 6 can turn the stub itself into the atomic
|
|
102
|
+
executable wave leaf without a duplicate identity. Preliminary children use
|
|
103
|
+
the spec-role status and **no** `ready-for-agent` (a leaf is not buildable until its
|
|
92
104
|
wave is promoted — the ordering guard stays unambiguous). Body = the slice's
|
|
93
105
|
`## Slices` section carried forward per `SLICE-METADATA-FORMAT.md` (metadata block +
|
|
94
|
-
the outcome/placeholder skeleton,
|
|
106
|
+
the outcome/placeholder skeleton, finalized in step 6). **Title carries the
|
|
95
107
|
navigation prefix** `Welle <N> / Slice <local-id> — <title>` — the local-id
|
|
96
108
|
(`1a`, `1b`, …) encodes the build order, so the sub-issue LIST is navigable by title
|
|
97
109
|
alone (same convention as `to-issues`' promoted children, `Welle N / Slice X — <outcome>`).
|
|
@@ -131,10 +143,22 @@ the numbering is deterministic; only the field stamps are batched):
|
|
|
131
143
|
```bash
|
|
132
144
|
python3 scripts/board-sync.py program-sync <prd#>
|
|
133
145
|
```
|
|
134
|
-
6. **
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
146
|
+
6. **Mature every wave.** For each stub in dependency order, run the approved
|
|
147
|
+
Program batch handoff: `to-prd` Mode B writes the per-wave Draft-PRD,
|
|
148
|
+
`spec-self-critique` hardens it, and `to-issues` promotes/atomizes it while
|
|
149
|
+
reusing the preliminary leaves. The pass writes complete issue bodies, final
|
|
150
|
+
AFK/HITL buckets, handoffs, native dependencies, `plan_revision`, and removes
|
|
151
|
+
`wave-stub`. It inherits the §3 approval when the cut is unchanged.
|
|
152
|
+
7. **Audit every wave.** Run `execute-ready-check.py --mode audit` on every promoted
|
|
153
|
+
anchor or atomic wave leaf. Any incomplete placeholder, incoherent revision,
|
|
154
|
+
missing bucket, or child-set mismatch blocks successful completion. **Program completion gate:**
|
|
155
|
+
Although `to-issues` uses audit mode as an informational handoff check, **do not count that wave as matured** or report
|
|
156
|
+
Program success until its audit findings are clean.
|
|
157
|
+
8. **Counted completion report + program view link.** Report `X von Y` for each part
|
|
158
|
+
(stubs created, leaves adopted/created, field stamps set, waves matured, waves
|
|
159
|
+
execute-ready) and link the program board view. Completeness is **counted, never
|
|
160
|
+
claimed** — the numbers come from the commands, including the exact line
|
|
161
|
+
`X von Y Wellen ausführungsreif`.
|
|
138
162
|
|
|
139
163
|
## 5. Adopt path — referenced existing issues
|
|
140
164
|
|
|
@@ -176,6 +200,10 @@ A re-run of to-waves on the same program is **delta-apply** and doubles as
|
|
|
176
200
|
report as **orphaned** (do not auto-close — closing is the abort convention, §8).
|
|
177
201
|
Cross-check the native children (`children-of <prd#>` and each stub) against the
|
|
178
202
|
plan so both origins are covered.
|
|
203
|
+
- **Atomic supersession exception.** A marked preliminary leaf whose local-id maps
|
|
204
|
+
to the one slice now carried by its atomic stub is not a generic orphan. Route it
|
|
205
|
+
to `to-issues` §5b cleanup whether it is still linked or was already unlinked by
|
|
206
|
+
an interrupted run. Search-by-marker is the durable recovery identity.
|
|
179
207
|
- **Crash recovery.** Because create is search-before-create, `link` is idempotent,
|
|
180
208
|
and `stamp-batch` field-sets are idempotent, re-running the whole publish after an
|
|
181
209
|
abort resumes cleanly — already-created issues match, missing ones are created,
|
|
@@ -191,7 +219,7 @@ A re-run of to-waves on the same program is **delta-apply** and doubles as
|
|
|
191
219
|
(the same sanctioned body-write `to-prd` Mode B uses). The mechanism that renews
|
|
192
220
|
coherence is the same one the revision broke — it self-repairs.
|
|
193
221
|
|
|
194
|
-
## 7. Program
|
|
222
|
+
## 7. Program completion contract
|
|
195
223
|
|
|
196
224
|
to-waves also carries the **program-grill agenda** as a reference chapter — the
|
|
197
225
|
checklist a program grill (upstream) should cover so the wave plan does not rest on
|
|
@@ -211,12 +239,22 @@ open switches:
|
|
|
211
239
|
single bounded choice to `decision-gate` — not deferred into the plan as an open
|
|
212
240
|
task.
|
|
213
241
|
|
|
214
|
-
**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
242
|
+
**Default postcondition:** every published wave is fully planned and passes the
|
|
243
|
+
shared execute-ready audit before this run reports success. Stufe 1p is an
|
|
244
|
+
internal recovery state, not the result presented to the user. **Late Binding is not the default**
|
|
245
|
+
for unfinished issue prose or missing buckets.
|
|
246
|
+
|
|
247
|
+
Uncertainty is planned, not hidden. A bounded trade-off becomes an explicit
|
|
248
|
+
**Decision Gate**, a factual unknown becomes a **Verify Spike**, and an unresolved
|
|
249
|
+
structure seam becomes a HITL **Design-Grill** slice (or an explicitly named
|
|
250
|
+
planning wave). Place that gate before its dependent build slice and write the
|
|
251
|
+
native blocking edge. The gate issue itself still receives a complete contract and
|
|
252
|
+
handoff; the dependent work is blocked, not vaguely under-planned.
|
|
253
|
+
|
|
254
|
+
A mandatory human or external setup action follows the same rule: model it as a
|
|
255
|
+
HITL predecessor, never bury it inside a `ready-for-agent` build leaf. If later
|
|
256
|
+
drift invalidates an already planned wave, use the revision/escalation path in §8;
|
|
257
|
+
do not pre-emptively leave every future wave incomplete.
|
|
220
258
|
|
|
221
259
|
## 8. Close protection, abort convention, escalation path
|
|
222
260
|
|
|
@@ -255,6 +293,7 @@ python3 scripts/board-sync.py program-sync <prd#>
|
|
|
255
293
|
to-waves: prd=#<n> preview=<approved|rejected>
|
|
256
294
|
created=<stubs X von Y, leaves X von Y> adopted=<#a #b … | none>
|
|
257
295
|
stamped=<N von M Wave/Phase> phase=<stamped | skipped (profile lacks fields.phase)>
|
|
296
|
+
matured=<X von Y> execute-ready=<X von Y Wellen ausführungsreif>
|
|
258
297
|
revision=r<N> renewed-markers=<count | none> orphaned=<#… | none>
|
|
259
298
|
graph=<ok | blocking: …> program-view=<url>
|
|
260
299
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kit-release
|
|
3
|
-
description: "Prepare a verified release PR for agent-workflow-kit
|
|
3
|
+
description: "Prepare a verified release PR for agent-workflow-kit, delegate landing to wrapup, then monitor the trusted post-merge publishing flow through npm/GitHub parity."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Kit Release
|
|
@@ -42,10 +42,31 @@ merge, registry publishing, tags, or release creation.
|
|
|
42
42
|
`scripts/wrapup-land.py` exclusively own commit, push, PR creation, merge,
|
|
43
43
|
and cleanup. Do not reproduce those operations here.
|
|
44
44
|
|
|
45
|
-
5. After merge, the release
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
5. After merge, monitor the `release.yml` workflow and inspect its externally
|
|
46
|
+
reconstructable state:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
gh run list --workflow release.yml --limit 1
|
|
50
|
+
npm run release:status
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`awaiting-npm` means the trusted workflow has not published yet;
|
|
54
|
+
`awaiting-github` means npm readback passed and a safe rerun will skip npm
|
|
55
|
+
publish and resume at GitHub; `released` means local, npm, and the GitHub
|
|
56
|
+
release asset have identical version, manifest hash, and tarball integrity.
|
|
57
|
+
The release is complete only at `released`.
|
|
58
|
+
|
|
59
|
+
The registry identity is **`@ikon85/agent-workflow-kit`**. The unscoped
|
|
60
|
+
`agent-workflow-kit` package is owned by another publisher and must never be
|
|
61
|
+
queried, published, or treated as this repository's release.
|
|
62
|
+
|
|
63
|
+
Before the first real release, the npm package must name this repository's
|
|
64
|
+
exact `.github/workflows/release.yml` as its GitHub Trusted Publisher. If it
|
|
65
|
+
does not exist yet, stop for a separately confirmed one-time bootstrap
|
|
66
|
+
publish; npm only permits trust configuration for an existing package. That
|
|
67
|
+
bootstrap creates the scoped package but does not replace the first real
|
|
68
|
+
OIDC/provenance release. Never request, store, or substitute an npm token,
|
|
69
|
+
and never publish or create a release ad hoc beyond that explicit bootstrap.
|
|
49
70
|
|
|
50
71
|
## Guard contract
|
|
51
72
|
|
|
@@ -97,8 +97,10 @@ Tripped altitude criteria: <e.g. C1 staged delivery, C2 multiple subsystems, C4
|
|
|
97
97
|
structure-bearing decisions (escalate a single bounded choice to `decision-gate`).
|
|
98
98
|
2. `to-prd` — it auto-detects the program mode from the Wellenplan chapter and writes
|
|
99
99
|
a Program-PRD (native anchor over the waves).
|
|
100
|
-
3. `to-waves` —
|
|
101
|
-
|
|
100
|
+
3. `to-waves` — after one complete chat preview, materialize the whole Program with
|
|
101
|
+
**all waves execute-ready by default**: complete slice contracts, buckets,
|
|
102
|
+
dependencies and handoffs. Decision Gates, Verify Spikes and Design-Grill waves
|
|
103
|
+
are explicit planned exceptions; generic late-binding cleanup is not.
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
### Feature (0–1 criteria, or any doubt)
|
|
@@ -132,7 +134,7 @@ Bug: <what's broken — expected vs. actual>.
|
|
|
132
134
|
```
|
|
133
135
|
scale-check: verdict=<Program | Feature | Direct-Slice | Bug>
|
|
134
136
|
criteria tripped=<C1,C2,… | none | n/a (bug/direct)>
|
|
135
|
-
route=<program grill → to-prd → to-waves | grill-with-docs → to-prd → to-issues | tdd | diagnose>
|
|
137
|
+
route=<program grill → to-prd → to-waves (all waves execute-ready by default) | grill-with-docs → to-prd → to-issues | tdd | diagnose>
|
|
136
138
|
start-prompt: <the paste-ready block above, filled in>
|
|
137
139
|
```
|
|
138
140
|
|
|
@@ -98,6 +98,14 @@ Ask the user:
|
|
|
98
98
|
|
|
99
99
|
Iterate until the user approves the breakdown.
|
|
100
100
|
|
|
101
|
+
**Program batch handoff.** When `to-waves` invokes this skill for a Stufe-1p stub,
|
|
102
|
+
the complete Program preview has already shown and approved every wave's slice
|
|
103
|
+
contract. Reuse that approval when this pass preserves the approved cut: do not
|
|
104
|
+
pause again merely because the internal pipeline crossed a skill boundary. Run
|
|
105
|
+
the normal contract in full — maturity is not a reduced mode — and reconcile the
|
|
106
|
+
stub's existing leaves instead of duplicating them. A new structural choice or a
|
|
107
|
+
newly discovered gate invalidates the inherited approval and returns to §4.
|
|
108
|
+
|
|
101
109
|
### 5. Publish — promote-or-atomic (contract)
|
|
102
110
|
|
|
103
111
|
**Cluster/Wave discriminator (identical wording — `to-prd` §2 / `to-issues` §5):** `type:cluster` (label) always stops. A Wave number also stops, **unless** the target carries the `wave-stub` label — a Wave-stamped `wave-stub` issue is a **Stufe-1p Program-stub** (`to-waves`-published, native parent = a Program-PRD) and remains a **valid target**. A Wave-stamped issue **without** `wave-stub` (an already-assigned leaf, or a drifted item) is still a hard stop.
|
|
@@ -193,6 +201,21 @@ python3 scripts/board-sync.py dep-add --issue <blocked#> --blocked-by <blocker#>
|
|
|
193
201
|
|
|
194
202
|
The Draft-PRD stays the executable leaf. Edit its body: **remove** `<!-- prd: awaiting-decomposition -->`, keep `type:*`+`priority:*` (**no** `type:cluster`/Wave, **no** `Welle N` title prefix), stamp the leaf `plan_revision`, add the `## Handoff Start Command`. The single PR `closes #<prd#>`.
|
|
195
203
|
|
|
204
|
+
**Program-batch atomic exception.** A Stufe-1p stub already represents one named
|
|
205
|
+
wave under a Program-PRD. When its approved cut contains exactly one slice, keep
|
|
206
|
+
the stub itself as the atomic executable wave leaf: preserve its existing Wave and Phase,
|
|
207
|
+
`Welle N — …` title, native Program parent, and source marker; write the complete
|
|
208
|
+
atomic contract and bucket, then remove only `wave-stub`. Do not erase Program
|
|
209
|
+
navigation by applying the ordinary Wave-less atomic normalization.
|
|
210
|
+
|
|
211
|
+
On a delta re-run, a legacy or interrupted preliminary child may already be linked
|
|
212
|
+
under that atomic stub. Lift any still-relevant content into the stub, unlink the
|
|
213
|
+
child with `board-sync.py unlink`, and close it with the mapping comment
|
|
214
|
+
`superseded by atomic wave leaf #<stub>`. Scan both `children-of` and the Program's
|
|
215
|
+
`program-leaf-source` markers: the **matching source marker remains discoverable after unlink**.
|
|
216
|
+
Resume idempotently — unlink only when still linked; close only when still open;
|
|
217
|
+
an already completed step is a no-op. The final atomic wave has no child set.
|
|
218
|
+
|
|
196
219
|
Then set the bucket via the helper — the leaf already exists, so the workflow-label write goes through `board-sync.py add --bucket` (§5a forbids a bare `gh issue edit --add-label ready-for-agent`):
|
|
197
220
|
|
|
198
221
|
```bash
|
|
@@ -215,6 +238,12 @@ Every child **and** the atomic leaf sits in **exactly one** bucket:
|
|
|
215
238
|
|
|
216
239
|
Status alone cannot discriminate (both are `Spec`) — the **label** does. The helper's `--hitl` flag rejects a `ready-for-agent` label mechanically. Authority = `scripts/execute-ready-check.py` (`parse_bucket`) — the checker wins on a mismatch.
|
|
217
240
|
|
|
241
|
+
**A mandatory human or external setup action is never AFK.** Cut it as a HITL gate
|
|
242
|
+
slice (or an explicitly named planning wave), place it before the dependent build
|
|
243
|
+
slice, and set the native blocked-by edge. Never hide a maintainer configuration,
|
|
244
|
+
credentialless console action, approval, or design response inside a leaf carrying
|
|
245
|
+
`ready-for-agent`, even when the remaining code work could run unattended.
|
|
246
|
+
|
|
218
247
|
**Gate slices (🔬/📐) are AFK.** A `🔬` Verify-Spike or `📐` Trade-off/Research gate slice is **read-only** (a fact question, or a bounded trade-off + read-only research) — an agent can run it solo, so it carries `ready-for-agent` like any other AFK slice; the **native blocked-by edge** on its dependent build slice (set at publish, §5a step 5) is what protects the gate-before-build ordering (§3b/§4), not the HITL bucket. Only a `🧭` Design-Grill gate slice is HITL (it needs the human in the grill). A gate slice's **placement** — which altitude/layer it lands on — follows `decision-gate`'s placement rule (owned there, referenced here).
|
|
219
248
|
|
|
220
249
|
#### 5d. Issue body template (each child / atomic leaf)
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: to-waves
|
|
3
3
|
disable-model-invocation: true
|
|
4
|
-
description: "
|
|
4
|
+
description: "Turn a Program-PRD's wave plan into fully planned, execute-ready wave anchors and slice leaves after one complete chat preview. Validates the graph, publishes every wave, runs the to-prd/spec-self-critique/to-issues maturity contract for each wave, models unresolved work as explicit gates, and audits counted readiness. Idempotent and crash-recoverable. Use on a Program-PRD over several waves. NOT for a single-wave feature (to-issues) or backlog clustering (board-to-waves)."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# to-waves — Unfold a Program-PRD's wave plan onto the board
|
|
8
8
|
|
|
9
9
|
Takes a **Program-PRD** — the native Sub-Issue anchor over a multi-wave program
|
|
10
10
|
(Programm → Phase → Welle → Slice) — and turns its `## Wellenplan` chapter into
|
|
11
|
-
**
|
|
12
|
-
`scale-check → grill → to-prd (program mode) → to-waves
|
|
11
|
+
**fully planned wave anchors + slice leaves** on the board. Pipeline position:
|
|
12
|
+
`scale-check → grill → to-prd (program mode) → to-waves`, with the per-wave
|
|
13
|
+
`to-prd → spec-self-critique → to-issues` maturity pass owned inside this run.
|
|
13
14
|
The **grill and to-prd sit upstream**; to-waves runs once the Program-PRD exists.
|
|
14
15
|
It **never invents structure** — it only unfolds what the plan already decided.
|
|
15
16
|
|
|
@@ -70,28 +71,39 @@ Show:
|
|
|
70
71
|
- The **publish plan**: how many stubs + leaves will be created, which referenced
|
|
71
72
|
existing issues will be **adopted** (§4) rather than created, and which
|
|
72
73
|
Wave/Phase stamps will be applied.
|
|
74
|
+
- The complete per-wave issue contracts that will be materialized: outcome,
|
|
75
|
+
Blast-Radius, acceptance criteria, AFK/HITL bucket, native dependencies, and
|
|
76
|
+
self-contained handoff. A placeholder is not preview-complete.
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
This complete Program preview is the **single user approval** for the default
|
|
79
|
+
planning run. Only on an explicit "yes" proceed to §4/§5/§7; when a later
|
|
80
|
+
per-wave maturity pass reproduces the approved cut, **do not ask for another per-wave approval**.
|
|
81
|
+
Ask again only when new evidence changes structure or exposes
|
|
82
|
+
a genuinely undecided gate. On "no", stop — no writes happened.
|
|
76
83
|
|
|
77
84
|
## 4. Publish — the fixed order
|
|
78
85
|
|
|
79
86
|
After approval, publish in **exactly this order** (issue CREATES stay sequential so
|
|
80
87
|
the numbering is deterministic; only the field stamps are batched):
|
|
81
88
|
|
|
82
|
-
1. **Wave stubs — Stufe 1p.** One per Wellenplan row, native parent = the PRD.
|
|
89
|
+
1. **Wave stubs — transient Stufe 1p.** One per Wellenplan row, native parent = the PRD.
|
|
83
90
|
Title `Welle <N> — <Name>`; created with the `wave-stub` label and Status Spec.
|
|
84
91
|
Body = the Stufe-1p stub template (named header, the program idempotency marker,
|
|
85
|
-
the revision marker — see §6).
|
|
92
|
+
the revision marker — see §6). This is a crash-recovery checkpoint inside the
|
|
93
|
+
run, never its successful terminal state. Not yet Wave/Phase-stamped (step 4).
|
|
86
94
|
```bash
|
|
87
95
|
python3 scripts/board-sync.py create --title "Welle <N> — <Name>" \
|
|
88
96
|
--body-file <stub.md> --wave-stub --status-role spec
|
|
89
97
|
```
|
|
90
|
-
2. **
|
|
91
|
-
|
|
98
|
+
2. **Preliminary slice leaves for multi-slice waves.** Create one per slice under
|
|
99
|
+
each wave whose approved cut contains ≥2 slices, native parent = its stub. For
|
|
100
|
+
an exactly-one-slice wave, **do not create a preliminary child**: keep that
|
|
101
|
+
slice's metadata on the stub so step 6 can turn the stub itself into the atomic
|
|
102
|
+
executable wave leaf without a duplicate identity. Preliminary children use
|
|
103
|
+
the spec-role status and **no** `ready-for-agent` (a leaf is not buildable until its
|
|
92
104
|
wave is promoted — the ordering guard stays unambiguous). Body = the slice's
|
|
93
105
|
`## Slices` section carried forward per `SLICE-METADATA-FORMAT.md` (metadata block +
|
|
94
|
-
the outcome/placeholder skeleton,
|
|
106
|
+
the outcome/placeholder skeleton, finalized in step 6). **Title carries the
|
|
95
107
|
navigation prefix** `Welle <N> / Slice <local-id> — <title>` — the local-id
|
|
96
108
|
(`1a`, `1b`, …) encodes the build order, so the sub-issue LIST is navigable by title
|
|
97
109
|
alone (same convention as `to-issues`' promoted children, `Welle N / Slice X — <outcome>`).
|
|
@@ -131,10 +143,22 @@ the numbering is deterministic; only the field stamps are batched):
|
|
|
131
143
|
```bash
|
|
132
144
|
python3 scripts/board-sync.py program-sync <prd#>
|
|
133
145
|
```
|
|
134
|
-
6. **
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
146
|
+
6. **Mature every wave.** For each stub in dependency order, run the approved
|
|
147
|
+
Program batch handoff: `to-prd` Mode B writes the per-wave Draft-PRD,
|
|
148
|
+
`spec-self-critique` hardens it, and `to-issues` promotes/atomizes it while
|
|
149
|
+
reusing the preliminary leaves. The pass writes complete issue bodies, final
|
|
150
|
+
AFK/HITL buckets, handoffs, native dependencies, `plan_revision`, and removes
|
|
151
|
+
`wave-stub`. It inherits the §3 approval when the cut is unchanged.
|
|
152
|
+
7. **Audit every wave.** Run `execute-ready-check.py --mode audit` on every promoted
|
|
153
|
+
anchor or atomic wave leaf. Any incomplete placeholder, incoherent revision,
|
|
154
|
+
missing bucket, or child-set mismatch blocks successful completion. **Program completion gate:**
|
|
155
|
+
Although `to-issues` uses audit mode as an informational handoff check, **do not count that wave as matured** or report
|
|
156
|
+
Program success until its audit findings are clean.
|
|
157
|
+
8. **Counted completion report + program view link.** Report `X von Y` for each part
|
|
158
|
+
(stubs created, leaves adopted/created, field stamps set, waves matured, waves
|
|
159
|
+
execute-ready) and link the program board view. Completeness is **counted, never
|
|
160
|
+
claimed** — the numbers come from the commands, including the exact line
|
|
161
|
+
`X von Y Wellen ausführungsreif`.
|
|
138
162
|
|
|
139
163
|
## 5. Adopt path — referenced existing issues
|
|
140
164
|
|
|
@@ -176,6 +200,10 @@ A re-run of to-waves on the same program is **delta-apply** and doubles as
|
|
|
176
200
|
report as **orphaned** (do not auto-close — closing is the abort convention, §8).
|
|
177
201
|
Cross-check the native children (`children-of <prd#>` and each stub) against the
|
|
178
202
|
plan so both origins are covered.
|
|
203
|
+
- **Atomic supersession exception.** A marked preliminary leaf whose local-id maps
|
|
204
|
+
to the one slice now carried by its atomic stub is not a generic orphan. Route it
|
|
205
|
+
to `to-issues` §5b cleanup whether it is still linked or was already unlinked by
|
|
206
|
+
an interrupted run. Search-by-marker is the durable recovery identity.
|
|
179
207
|
- **Crash recovery.** Because create is search-before-create, `link` is idempotent,
|
|
180
208
|
and `stamp-batch` field-sets are idempotent, re-running the whole publish after an
|
|
181
209
|
abort resumes cleanly — already-created issues match, missing ones are created,
|
|
@@ -191,7 +219,7 @@ A re-run of to-waves on the same program is **delta-apply** and doubles as
|
|
|
191
219
|
(the same sanctioned body-write `to-prd` Mode B uses). The mechanism that renews
|
|
192
220
|
coherence is the same one the revision broke — it self-repairs.
|
|
193
221
|
|
|
194
|
-
## 7. Program
|
|
222
|
+
## 7. Program completion contract
|
|
195
223
|
|
|
196
224
|
to-waves also carries the **program-grill agenda** as a reference chapter — the
|
|
197
225
|
checklist a program grill (upstream) should cover so the wave plan does not rest on
|
|
@@ -211,12 +239,22 @@ open switches:
|
|
|
211
239
|
single bounded choice to `decision-gate` — not deferred into the plan as an open
|
|
212
240
|
task.
|
|
213
241
|
|
|
214
|
-
**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
242
|
+
**Default postcondition:** every published wave is fully planned and passes the
|
|
243
|
+
shared execute-ready audit before this run reports success. Stufe 1p is an
|
|
244
|
+
internal recovery state, not the result presented to the user. **Late Binding is not the default**
|
|
245
|
+
for unfinished issue prose or missing buckets.
|
|
246
|
+
|
|
247
|
+
Uncertainty is planned, not hidden. A bounded trade-off becomes an explicit
|
|
248
|
+
**Decision Gate**, a factual unknown becomes a **Verify Spike**, and an unresolved
|
|
249
|
+
structure seam becomes a HITL **Design-Grill** slice (or an explicitly named
|
|
250
|
+
planning wave). Place that gate before its dependent build slice and write the
|
|
251
|
+
native blocking edge. The gate issue itself still receives a complete contract and
|
|
252
|
+
handoff; the dependent work is blocked, not vaguely under-planned.
|
|
253
|
+
|
|
254
|
+
A mandatory human or external setup action follows the same rule: model it as a
|
|
255
|
+
HITL predecessor, never bury it inside a `ready-for-agent` build leaf. If later
|
|
256
|
+
drift invalidates an already planned wave, use the revision/escalation path in §8;
|
|
257
|
+
do not pre-emptively leave every future wave incomplete.
|
|
220
258
|
|
|
221
259
|
## 8. Close protection, abort convention, escalation path
|
|
222
260
|
|
|
@@ -255,6 +293,7 @@ python3 scripts/board-sync.py program-sync <prd#>
|
|
|
255
293
|
to-waves: prd=#<n> preview=<approved|rejected>
|
|
256
294
|
created=<stubs X von Y, leaves X von Y> adopted=<#a #b … | none>
|
|
257
295
|
stamped=<N von M Wave/Phase> phase=<stamped | skipped (profile lacks fields.phase)>
|
|
296
|
+
matured=<X von Y> execute-ready=<X von Y Wellen ausführungsreif>
|
|
258
297
|
revision=r<N> renewed-markers=<count | none> orphaned=<#… | none>
|
|
259
298
|
graph=<ok | blocking: …> program-view=<url>
|
|
260
299
|
```
|
package/README.md
CHANGED
|
@@ -332,6 +332,19 @@ still reference. Flags: `--force` (overwrite pre-existing files on `init`),
|
|
|
332
332
|
|
|
333
333
|
## Release notes
|
|
334
334
|
|
|
335
|
+
### 0.11.0
|
|
336
|
+
|
|
337
|
+
- added: `scripts/release-parity.mjs`
|
|
338
|
+
- added: `scripts/release-state.mjs`
|
|
339
|
+
- changed: `.agents/skills/kit-release/SKILL.md`
|
|
340
|
+
- changed: `.agents/skills/scale-check/SKILL.md`
|
|
341
|
+
- changed: `.agents/skills/to-issues/SKILL.md`
|
|
342
|
+
- changed: `.agents/skills/to-waves/SKILL.md`
|
|
343
|
+
- changed: `.claude/skills/kit-release/SKILL.md`
|
|
344
|
+
- changed: `.claude/skills/scale-check/SKILL.md`
|
|
345
|
+
- changed: `.claude/skills/to-issues/SKILL.md`
|
|
346
|
+
- changed: `.claude/skills/to-waves/SKILL.md`
|
|
347
|
+
|
|
335
348
|
### 0.10.0
|
|
336
349
|
|
|
337
350
|
- added: `.agents/skills/kit-release/SKILL.md`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kitVersion": "0.
|
|
2
|
+
"kitVersion": "0.11.0",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": ".agents/skills/ask-matt/SKILL.md",
|
|
@@ -321,7 +321,7 @@
|
|
|
321
321
|
"kind": "skill",
|
|
322
322
|
"ownerSkill": "kit-release",
|
|
323
323
|
"surface": "codex",
|
|
324
|
-
"sha256": "
|
|
324
|
+
"sha256": "d05a4059a19816ff302c119f797f1ea2b71554ecffac7a8f0b919cfe5517d940",
|
|
325
325
|
"mode": 420,
|
|
326
326
|
"origin": "kit"
|
|
327
327
|
},
|
|
@@ -438,7 +438,7 @@
|
|
|
438
438
|
"kind": "skill",
|
|
439
439
|
"ownerSkill": "scale-check",
|
|
440
440
|
"surface": "codex",
|
|
441
|
-
"sha256": "
|
|
441
|
+
"sha256": "80f8d5b4b811afd0bce4546d18170eed8a79ae2ee872b40563814374ea2ad1c2",
|
|
442
442
|
"mode": 420,
|
|
443
443
|
"origin": "kit"
|
|
444
444
|
},
|
|
@@ -654,7 +654,7 @@
|
|
|
654
654
|
"kind": "skill",
|
|
655
655
|
"ownerSkill": "to-issues",
|
|
656
656
|
"surface": "codex",
|
|
657
|
-
"sha256": "
|
|
657
|
+
"sha256": "3fdd05150f428cf8098e4dd12f67cbc7ebe17fa9ab315e456fd0df70e5c9925d",
|
|
658
658
|
"mode": 420,
|
|
659
659
|
"origin": "kit"
|
|
660
660
|
},
|
|
@@ -699,7 +699,7 @@
|
|
|
699
699
|
"kind": "skill",
|
|
700
700
|
"ownerSkill": "to-waves",
|
|
701
701
|
"surface": "codex",
|
|
702
|
-
"sha256": "
|
|
702
|
+
"sha256": "5ac4fd5d26d6ea2327fc50ad4bcc8f3d8c80186afb976410d8c3e8f3c21d896e",
|
|
703
703
|
"mode": 420,
|
|
704
704
|
"origin": "kit"
|
|
705
705
|
},
|
|
@@ -1267,7 +1267,7 @@
|
|
|
1267
1267
|
"kind": "skill",
|
|
1268
1268
|
"ownerSkill": "kit-release",
|
|
1269
1269
|
"surface": "claude",
|
|
1270
|
-
"sha256": "
|
|
1270
|
+
"sha256": "d05a4059a19816ff302c119f797f1ea2b71554ecffac7a8f0b919cfe5517d940",
|
|
1271
1271
|
"mode": 420,
|
|
1272
1272
|
"origin": "kit"
|
|
1273
1273
|
},
|
|
@@ -1384,7 +1384,7 @@
|
|
|
1384
1384
|
"kind": "skill",
|
|
1385
1385
|
"ownerSkill": "scale-check",
|
|
1386
1386
|
"surface": "claude",
|
|
1387
|
-
"sha256": "
|
|
1387
|
+
"sha256": "80f8d5b4b811afd0bce4546d18170eed8a79ae2ee872b40563814374ea2ad1c2",
|
|
1388
1388
|
"mode": 420,
|
|
1389
1389
|
"origin": "kit"
|
|
1390
1390
|
},
|
|
@@ -1618,7 +1618,7 @@
|
|
|
1618
1618
|
"kind": "skill",
|
|
1619
1619
|
"ownerSkill": "to-issues",
|
|
1620
1620
|
"surface": "claude",
|
|
1621
|
-
"sha256": "
|
|
1621
|
+
"sha256": "5874d4b2d4999bd32a0a90c955467fc4e58e230295cd6ee21936fa30456bda6e",
|
|
1622
1622
|
"mode": 420,
|
|
1623
1623
|
"origin": "kit"
|
|
1624
1624
|
},
|
|
@@ -1663,7 +1663,7 @@
|
|
|
1663
1663
|
"kind": "skill",
|
|
1664
1664
|
"ownerSkill": "to-waves",
|
|
1665
1665
|
"surface": "claude",
|
|
1666
|
-
"sha256": "
|
|
1666
|
+
"sha256": "5ac4fd5d26d6ea2327fc50ad4bcc8f3d8c80186afb976410d8c3e8f3c21d896e",
|
|
1667
1667
|
"mode": 420,
|
|
1668
1668
|
"origin": "kit"
|
|
1669
1669
|
},
|
|
@@ -1928,6 +1928,20 @@
|
|
|
1928
1928
|
"mode": 420,
|
|
1929
1929
|
"origin": "kit"
|
|
1930
1930
|
},
|
|
1931
|
+
{
|
|
1932
|
+
"path": "scripts/release-parity.mjs",
|
|
1933
|
+
"kind": "script",
|
|
1934
|
+
"sha256": "faa4a1d1116d58f69c8541a586ebd6076ebc3f7af7809e9e4a53928f49298365",
|
|
1935
|
+
"mode": 420,
|
|
1936
|
+
"origin": "kit"
|
|
1937
|
+
},
|
|
1938
|
+
{
|
|
1939
|
+
"path": "scripts/release-state.mjs",
|
|
1940
|
+
"kind": "script",
|
|
1941
|
+
"sha256": "2aae056a8e0e945dbf68298236719c78e22f13dbd8f0c3466b969355d3809058",
|
|
1942
|
+
"mode": 420,
|
|
1943
|
+
"origin": "kit"
|
|
1944
|
+
},
|
|
1931
1945
|
{
|
|
1932
1946
|
"path": "scripts/wrapup-land.py",
|
|
1933
1947
|
"kind": "script",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikon85/agent-workflow-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"kit:build": "node scripts/build-kit.mjs",
|
|
14
14
|
"kit:staleness": "node scripts/check-kit-staleness.mjs",
|
|
15
15
|
"release:prepare": "node scripts/kit-release.mjs",
|
|
16
|
-
"release:guard": "node scripts/release-delta-guard.mjs"
|
|
16
|
+
"release:guard": "node scripts/release-delta-guard.mjs",
|
|
17
|
+
"release:status": "node scripts/release-state.mjs --status"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
20
|
".claude/",
|
|
@@ -35,6 +35,7 @@ test('current build contains post-tag public files and repository metadata', asy
|
|
|
35
35
|
await readFile(join(dist, '.agents/skills/codex-adapter-sync/SKILL.md'));
|
|
36
36
|
await readFile(join(dist, 'scripts/program_graph.py'));
|
|
37
37
|
const pkg = JSON.parse(await readFile(join(dist, 'package.json'), 'utf8'));
|
|
38
|
+
assert.equal(pkg.name, '@ikon85/agent-workflow-kit');
|
|
38
39
|
assert.equal(pkg.repository.url, 'git+https://github.com/iKon85/agent-workflow-kit.git');
|
|
39
40
|
});
|
|
40
41
|
});
|
|
@@ -13,6 +13,16 @@ test('the install manifest ships both release primitives named by the skill', as
|
|
|
13
13
|
const paths = manifest.files.map((entry) => entry.path);
|
|
14
14
|
assert.ok(paths.includes('scripts/kit-release.mjs'));
|
|
15
15
|
assert.ok(paths.includes('scripts/release-delta-guard.mjs'));
|
|
16
|
+
assert.ok(paths.includes('scripts/release-parity.mjs'));
|
|
17
|
+
assert.ok(paths.includes('scripts/release-state.mjs'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('both release skill surfaces name only the owned scoped npm package', async () => {
|
|
21
|
+
const claude = await readFile(join(REPO, '.claude/skills/kit-release/SKILL.md'), 'utf8');
|
|
22
|
+
const codex = await readFile(join(REPO, '.agents/skills/kit-release/SKILL.md'), 'utf8');
|
|
23
|
+
for (const body of [claude, codex]) {
|
|
24
|
+
assert.match(body, /`@ikon85\/agent-workflow-kit`/);
|
|
25
|
+
}
|
|
16
26
|
});
|
|
17
27
|
|
|
18
28
|
test('patch, minor, and major confirmations select exactly one target version', () => {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
|
|
6
|
+
const exec = promisify(execFile);
|
|
7
|
+
const FIELDS = ['name', 'version', 'tarballIntegrity', 'manifestSha256'];
|
|
8
|
+
|
|
9
|
+
const digest = (algorithm, content, encoding = 'hex') =>
|
|
10
|
+
createHash(algorithm).update(content).digest(encoding);
|
|
11
|
+
|
|
12
|
+
async function readTarEntry(tarball, entry) {
|
|
13
|
+
const { stdout } = await exec('tar', ['-xOf', tarball, entry], { encoding: 'buffer' });
|
|
14
|
+
return stdout;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function releaseIdentityFromTarball(tarball) {
|
|
18
|
+
const bytes = await readFile(tarball);
|
|
19
|
+
const packageJson = JSON.parse(await readTarEntry(tarball, 'package/package.json'));
|
|
20
|
+
const manifest = await readTarEntry(tarball, 'package/agent-workflow-kit.package.json');
|
|
21
|
+
if (packageJson.version !== JSON.parse(manifest).kitVersion) {
|
|
22
|
+
throw new Error('tarball package and manifest versions mismatch');
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
name: packageJson.name,
|
|
26
|
+
version: packageJson.version,
|
|
27
|
+
tarballIntegrity: `sha512-${digest('sha512', bytes, 'base64')}`,
|
|
28
|
+
manifestSha256: digest('sha256', manifest),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validate(label, identity) {
|
|
33
|
+
if (!identity || typeof identity !== 'object') throw new Error(`${label} release identity missing`);
|
|
34
|
+
for (const field of FIELDS) {
|
|
35
|
+
if (typeof identity[field] !== 'string' || !identity[field]) {
|
|
36
|
+
throw new Error(`${label} ${field} missing`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function assertReleaseParity(identities) {
|
|
42
|
+
validate('local', identities.local);
|
|
43
|
+
for (const label of ['npm', 'github']) {
|
|
44
|
+
validate(label, identities[label]);
|
|
45
|
+
for (const field of FIELDS) {
|
|
46
|
+
if (identities[label][field] !== identities.local[field]) {
|
|
47
|
+
throw new Error(`${label} ${field} mismatch`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return identities.local;
|
|
52
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { assertReleaseParity, releaseIdentityFromTarball } from './release-parity.mjs';
|
|
9
|
+
|
|
10
|
+
const exec = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
const identity = {
|
|
13
|
+
name: '@ikon85/agent-workflow-kit',
|
|
14
|
+
version: '1.2.3',
|
|
15
|
+
tarballIntegrity: 'sha512-example',
|
|
16
|
+
manifestSha256: 'abc123',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
test('identical local, npm, and GitHub release identities have parity', () => {
|
|
20
|
+
assert.deepEqual(assertReleaseParity({ local: identity, npm: { ...identity }, github: { ...identity } }), identity);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('a registry or GitHub content mismatch fails closed', () => {
|
|
24
|
+
assert.throws(() => assertReleaseParity({
|
|
25
|
+
local: identity,
|
|
26
|
+
npm: { ...identity, manifestSha256: 'different' },
|
|
27
|
+
github: { ...identity },
|
|
28
|
+
}), /npm manifestSha256 mismatch/);
|
|
29
|
+
assert.throws(() => assertReleaseParity({
|
|
30
|
+
local: identity,
|
|
31
|
+
npm: { ...identity },
|
|
32
|
+
github: { ...identity, tarballIntegrity: 'different' },
|
|
33
|
+
}), /github tarballIntegrity mismatch/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('a package tarball produces a deterministic content identity', async () => {
|
|
37
|
+
const root = await mkdtemp(join(tmpdir(), 'release-parity-'));
|
|
38
|
+
const packageDir = join(root, 'package');
|
|
39
|
+
const tarball = join(root, 'kit.tgz');
|
|
40
|
+
try {
|
|
41
|
+
await mkdir(packageDir);
|
|
42
|
+
await writeFile(join(packageDir, 'package.json'), '{"name":"kit","version":"2.0.0"}\n');
|
|
43
|
+
await writeFile(join(packageDir, 'agent-workflow-kit.package.json'), '{"kitVersion":"2.0.0","files":[]}\n');
|
|
44
|
+
await exec('tar', ['-czf', tarball, '-C', root, 'package']);
|
|
45
|
+
const first = await releaseIdentityFromTarball(tarball);
|
|
46
|
+
const second = await releaseIdentityFromTarball(tarball);
|
|
47
|
+
assert.equal(first.name, 'kit');
|
|
48
|
+
assert.equal(first.version, '2.0.0');
|
|
49
|
+
assert.match(first.tarballIntegrity, /^sha512-/);
|
|
50
|
+
assert.match(first.manifestSha256, /^[a-f0-9]{64}$/);
|
|
51
|
+
assert.deepEqual(first, second);
|
|
52
|
+
} finally { await rm(root, { recursive: true, force: true }); }
|
|
53
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { assertReleaseParity, releaseIdentityFromTarball } from './release-parity.mjs';
|
|
8
|
+
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
const assertMatches = (local, remote, label) => assertReleaseParity({
|
|
12
|
+
local,
|
|
13
|
+
npm: label === 'npm' ? remote : local,
|
|
14
|
+
github: label === 'github' ? remote : local,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export async function inspectRelease(adapter) {
|
|
18
|
+
const { identity } = await adapter.local();
|
|
19
|
+
const npm = await adapter.npm(identity);
|
|
20
|
+
const github = await adapter.github(identity);
|
|
21
|
+
if (!npm) {
|
|
22
|
+
if (github) throw new Error('GitHub release exists before npm package');
|
|
23
|
+
return { status: 'awaiting-npm', identity };
|
|
24
|
+
}
|
|
25
|
+
assertMatches(identity, npm, 'npm');
|
|
26
|
+
if (!github) return { status: 'awaiting-github', identity };
|
|
27
|
+
assertReleaseParity({ local: identity, npm, github });
|
|
28
|
+
return { status: 'released', identity };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function reconcileRelease(adapter) {
|
|
32
|
+
const { identity, tarball } = await adapter.local();
|
|
33
|
+
let npm = await adapter.npm(identity);
|
|
34
|
+
let github = await adapter.github(identity);
|
|
35
|
+
|
|
36
|
+
if (!npm) {
|
|
37
|
+
if (github) throw new Error('GitHub release exists before npm package');
|
|
38
|
+
await adapter.publishNpm({ identity, tarball });
|
|
39
|
+
npm = await adapter.npm(identity);
|
|
40
|
+
if (!npm) throw new Error('npm package missing after publish');
|
|
41
|
+
}
|
|
42
|
+
assertMatches(identity, npm, 'npm');
|
|
43
|
+
|
|
44
|
+
if (!github) {
|
|
45
|
+
await adapter.createGithub({ identity, tarball });
|
|
46
|
+
github = await adapter.github(identity);
|
|
47
|
+
if (!github) throw new Error('GitHub release missing after create');
|
|
48
|
+
}
|
|
49
|
+
assertReleaseParity({ local: identity, npm, github });
|
|
50
|
+
return { status: 'released', identity };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isMissingRelease(error, service) {
|
|
54
|
+
const detail = `${error.stderr ?? ''}\n${error.message ?? ''}`;
|
|
55
|
+
return service === 'npm'
|
|
56
|
+
? /E404|404 Not Found|ETARGET|No matching version found/i.test(detail)
|
|
57
|
+
: /release not found|HTTP 404/i.test(detail);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function githubReleaseArgs({ exists, tag, tarball, target }) {
|
|
61
|
+
if (exists) return ['release', 'upload', tag, tarball, '--clobber'];
|
|
62
|
+
return [
|
|
63
|
+
'release', 'create', tag, tarball, '--target', target,
|
|
64
|
+
'--title', tag, '--generate-notes',
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function npmTarballFilename(name, version) {
|
|
69
|
+
return `${name.replace(/^@/, '').replaceAll('/', '-')}-${version}.tgz`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function packedTarball(run, spec, directory, repoRoot) {
|
|
73
|
+
const { stdout } = await run(
|
|
74
|
+
'npm', ['pack', spec, '--json', '--pack-destination', directory], { cwd: repoRoot },
|
|
75
|
+
);
|
|
76
|
+
const result = JSON.parse(stdout);
|
|
77
|
+
if (!Array.isArray(result) || result.length !== 1 || !result[0].filename) {
|
|
78
|
+
throw new Error(`unexpected npm pack result for ${spec}`);
|
|
79
|
+
}
|
|
80
|
+
return join(directory, result[0].filename);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function releaseReaders(context) {
|
|
84
|
+
const { run, repoRoot, env, scratch, state } = context;
|
|
85
|
+
async function local() {
|
|
86
|
+
const tarball = await packedTarball(run, '.', scratch, repoRoot);
|
|
87
|
+
return { tarball, identity: await releaseIdentityFromTarball(tarball) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function npm(identity) {
|
|
91
|
+
try {
|
|
92
|
+
state.npmTarball = await packedTarball(
|
|
93
|
+
run, `${identity.name}@${identity.version}`, scratch, repoRoot,
|
|
94
|
+
);
|
|
95
|
+
return await releaseIdentityFromTarball(state.npmTarball);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (isMissingRelease(error, 'npm')) return null;
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function github(identity) {
|
|
103
|
+
const tag = `v${identity.version}`;
|
|
104
|
+
try {
|
|
105
|
+
await run('gh', ['release', 'view', tag, '--json', 'tagName'], { cwd: repoRoot, env });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (isMissingRelease(error, 'github')) return null;
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const asset = npmTarballFilename(identity.name, identity.version);
|
|
112
|
+
await run('gh', ['release', 'download', tag, '--pattern', asset, '--dir', scratch, '--clobber'], {
|
|
113
|
+
cwd: repoRoot, env,
|
|
114
|
+
});
|
|
115
|
+
return await releaseIdentityFromTarball(join(scratch, asset));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (/no assets match|not found/i.test(`${error.stderr ?? ''}\n${error.message ?? ''}`)) return null;
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { local, npm, github };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function releaseWriters(context) {
|
|
126
|
+
const { run, repoRoot, env, state } = context;
|
|
127
|
+
async function publishNpm({ tarball }) {
|
|
128
|
+
await run('npm', ['publish', tarball, '--access', 'public', '--provenance'], { cwd: repoRoot, env });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function createGithub({ identity }) {
|
|
132
|
+
if (!state.npmTarball) throw new Error('verified npm tarball unavailable for GitHub release');
|
|
133
|
+
const tag = `v${identity.version}`;
|
|
134
|
+
let exists = true;
|
|
135
|
+
try {
|
|
136
|
+
await run('gh', ['release', 'view', tag, '--json', 'tagName'], { cwd: repoRoot, env });
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (!isMissingRelease(error, 'github')) throw error;
|
|
139
|
+
exists = false;
|
|
140
|
+
}
|
|
141
|
+
await run('gh', githubReleaseArgs({
|
|
142
|
+
exists, tag, tarball: state.npmTarball, target: env.GITHUB_SHA,
|
|
143
|
+
}), { cwd: repoRoot, env });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { publishNpm, createGithub };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function createCommandAdapter({ repoRoot, run = exec, env = process.env } = {}) {
|
|
150
|
+
const scratch = await mkdtemp(join(tmpdir(), 'awkit-publish-'));
|
|
151
|
+
const context = { run, repoRoot, env, scratch, state: {} };
|
|
152
|
+
return {
|
|
153
|
+
...releaseReaders(context),
|
|
154
|
+
...releaseWriters(context),
|
|
155
|
+
dispose: () => rm(scratch, { recursive: true, force: true }),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function main() {
|
|
160
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
161
|
+
const adapter = await createCommandAdapter({ repoRoot });
|
|
162
|
+
try {
|
|
163
|
+
const result = process.argv.includes('--status')
|
|
164
|
+
? await inspectRelease(adapter)
|
|
165
|
+
: await reconcileRelease(adapter);
|
|
166
|
+
console.log(`release ${result.identity.version}: ${result.status}`);
|
|
167
|
+
} finally { await adapter.dispose(); }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
171
|
+
main().catch((error) => { console.error(`release-state: ${error.message}`); process.exitCode = 1; });
|
|
172
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import {
|
|
7
|
+
createCommandAdapter, githubReleaseArgs, inspectRelease, isMissingRelease,
|
|
8
|
+
npmTarballFilename, reconcileRelease,
|
|
9
|
+
} from './release-state.mjs';
|
|
10
|
+
|
|
11
|
+
const REPO = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
|
|
13
|
+
const identity = {
|
|
14
|
+
name: '@ikon85/agent-workflow-kit', version: '1.2.3',
|
|
15
|
+
tarballIntegrity: 'sha512-example', manifestSha256: 'abc123',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function adapter({ npmPublished = false, githubReleased = false, npmIdentity = identity } = {}) {
|
|
19
|
+
const events = [];
|
|
20
|
+
let npmExists = npmPublished;
|
|
21
|
+
let githubExists = githubReleased;
|
|
22
|
+
return {
|
|
23
|
+
events,
|
|
24
|
+
local: async () => ({ identity, tarball: '/tmp/kit.tgz' }),
|
|
25
|
+
npm: async () => { events.push('read npm'); return npmExists ? npmIdentity : null; },
|
|
26
|
+
github: async () => { events.push('read github'); return githubExists ? identity : null; },
|
|
27
|
+
publishNpm: async () => { events.push('publish npm'); npmExists = true; },
|
|
28
|
+
createGithub: async () => { events.push('create github'); githubExists = true; },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('a fresh release publishes npm, verifies registry readback, then creates GitHub release', async () => {
|
|
33
|
+
const fixture = adapter();
|
|
34
|
+
assert.deepEqual(await reconcileRelease(fixture), { status: 'released', identity });
|
|
35
|
+
assert.deepEqual(fixture.events, [
|
|
36
|
+
'read npm', 'read github', 'publish npm', 'read npm', 'create github', 'read github',
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('an npm-published release resumes at GitHub without a second npm publish', async () => {
|
|
41
|
+
const fixture = adapter({ npmPublished: true });
|
|
42
|
+
await reconcileRelease(fixture);
|
|
43
|
+
assert.deepEqual(fixture.events, ['read npm', 'read github', 'create github', 'read github']);
|
|
44
|
+
assert.ok(!fixture.events.includes('publish npm'));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('post-merge status inspection is read-only and reports the reconstructable phase', async () => {
|
|
48
|
+
const fixture = adapter({ npmPublished: true });
|
|
49
|
+
assert.deepEqual(await inspectRelease(fixture), { status: 'awaiting-github', identity });
|
|
50
|
+
assert.deepEqual(fixture.events, ['read npm', 'read github']);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('an unpublished npm version is reconstructable when npm reports ETARGET', () => {
|
|
54
|
+
assert.equal(isMissingRelease({
|
|
55
|
+
stderr: 'npm error code ETARGET\nnpm error notarget No matching version found',
|
|
56
|
+
}, 'npm'), true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('a mismatching npm package blocks GitHub release creation', async () => {
|
|
60
|
+
const fixture = adapter({
|
|
61
|
+
npmPublished: true,
|
|
62
|
+
npmIdentity: { ...identity, manifestSha256: 'wrong' },
|
|
63
|
+
});
|
|
64
|
+
await assert.rejects(reconcileRelease(fixture), /npm manifestSha256 mismatch/);
|
|
65
|
+
assert.ok(!fixture.events.includes('create github'));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('release workflow uses supported OIDC trusted publishing without a long-lived npm token', async () => {
|
|
69
|
+
const workflow = await readFile(join(REPO, '.github/workflows/release.yml'), 'utf8');
|
|
70
|
+
assert.match(workflow, /node-version:\s*22\.14/);
|
|
71
|
+
assert.match(workflow, /npm@11\.5\.1/);
|
|
72
|
+
assert.match(workflow, /id-token:\s*write/);
|
|
73
|
+
assert.match(workflow, /contents:\s*write/);
|
|
74
|
+
assert.match(workflow, /registry-url:\s*['"]https:\/\/registry\.npmjs\.org['"]/);
|
|
75
|
+
assert.match(workflow, /node scripts\/release-state\.mjs/);
|
|
76
|
+
assert.doesNotMatch(workflow, /NPM_TOKEN|NODE_AUTH_TOKEN|npm_[A-Za-z0-9]/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('the production publisher explicitly requests npm provenance', async () => {
|
|
80
|
+
const commands = [];
|
|
81
|
+
const fixture = await createCommandAdapter({
|
|
82
|
+
repoRoot: REPO,
|
|
83
|
+
run: async (command, args) => { commands.push([command, ...args]); return { stdout: '' }; },
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
await fixture.publishNpm({ tarball: '/tmp/kit.tgz' });
|
|
87
|
+
assert.deepEqual(commands, [[
|
|
88
|
+
'npm', 'publish', '/tmp/kit.tgz', '--access', 'public', '--provenance',
|
|
89
|
+
]]);
|
|
90
|
+
} finally { await fixture.dispose(); }
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('an incomplete GitHub release resumes by uploading the verified registry tarball', () => {
|
|
94
|
+
assert.deepEqual(githubReleaseArgs({
|
|
95
|
+
exists: true, tag: 'v1.2.3', tarball: '/tmp/ikon85-agent-workflow-kit-1.2.3.tgz', target: 'abc123',
|
|
96
|
+
}), [
|
|
97
|
+
'release', 'upload', 'v1.2.3', '/tmp/ikon85-agent-workflow-kit-1.2.3.tgz', '--clobber',
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('a scoped npm identity maps to the registry tarball asset name', () => {
|
|
102
|
+
assert.equal(
|
|
103
|
+
npmTarballFilename('@ikon85/agent-workflow-kit', '1.2.3'),
|
|
104
|
+
'ikon85-agent-workflow-kit-1.2.3.tgz',
|
|
105
|
+
);
|
|
106
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Contract tests for the user-visible Program planning route."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProgramPlanningContractTest(unittest.TestCase):
|
|
11
|
+
def skill(self, name: str) -> str:
|
|
12
|
+
return (ROOT / ".claude" / "skills" / name / "SKILL.md").read_text()
|
|
13
|
+
|
|
14
|
+
def test_to_waves_finishes_with_every_wave_execute_ready_by_default(self):
|
|
15
|
+
text = self.skill("to-waves")
|
|
16
|
+
self.assertIn("## 7. Program completion contract", text)
|
|
17
|
+
self.assertIn("every published wave", text)
|
|
18
|
+
self.assertIn("execute-ready-check.py", text)
|
|
19
|
+
self.assertIn("X von Y Wellen ausführungsreif", text)
|
|
20
|
+
|
|
21
|
+
def test_program_preview_is_the_single_default_user_gate(self):
|
|
22
|
+
text = self.skill("to-waves")
|
|
23
|
+
self.assertIn("single user approval", text)
|
|
24
|
+
self.assertIn("do not ask for another per-wave approval", text)
|
|
25
|
+
|
|
26
|
+
def test_unresolved_work_is_modeled_as_an_explicit_gate_not_generic_late_binding(self):
|
|
27
|
+
text = self.skill("to-waves")
|
|
28
|
+
self.assertIn("Decision Gate", text)
|
|
29
|
+
self.assertIn("Verify Spike", text)
|
|
30
|
+
self.assertIn("Design-Grill", text)
|
|
31
|
+
self.assertIn("Late Binding is not the default", text)
|
|
32
|
+
|
|
33
|
+
def test_to_issues_supports_program_batch_and_keeps_human_actions_out_of_afk(self):
|
|
34
|
+
text = self.skill("to-issues")
|
|
35
|
+
self.assertIn("Program batch handoff", text)
|
|
36
|
+
self.assertIn("A mandatory human or external setup action is never AFK", text)
|
|
37
|
+
|
|
38
|
+
def test_atomic_program_wave_preserves_its_program_identity(self):
|
|
39
|
+
text = self.skill("to-issues")
|
|
40
|
+
self.assertIn("Program-batch atomic exception", text)
|
|
41
|
+
self.assertIn("preserve its existing Wave and Phase", text)
|
|
42
|
+
|
|
43
|
+
def test_atomic_program_wave_never_leaves_a_preliminary_child(self):
|
|
44
|
+
waves = self.skill("to-waves")
|
|
45
|
+
issues = self.skill("to-issues")
|
|
46
|
+
self.assertIn("do not create a preliminary child", waves)
|
|
47
|
+
self.assertIn("legacy or interrupted preliminary child", issues)
|
|
48
|
+
self.assertIn("superseded by atomic wave leaf", issues)
|
|
49
|
+
self.assertIn("Atomic supersession", waves)
|
|
50
|
+
self.assertIn("matching source marker remains discoverable after unlink", issues)
|
|
51
|
+
|
|
52
|
+
def test_program_completion_treats_audit_findings_as_blocking(self):
|
|
53
|
+
text = self.skill("to-waves")
|
|
54
|
+
self.assertIn("Program completion gate", text)
|
|
55
|
+
self.assertIn("do not count that wave as matured", text)
|
|
56
|
+
|
|
57
|
+
def test_public_program_route_promises_execute_ready_waves(self):
|
|
58
|
+
text = self.skill("scale-check")
|
|
59
|
+
self.assertIn("all waves execute-ready by default", text)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
unittest.main()
|
package/src/lib/bundle.mjs
CHANGED
|
@@ -42,6 +42,11 @@ export const HELPER_FILES = [
|
|
|
42
42
|
// Both are invoked through package scripts by /kit-release.
|
|
43
43
|
{ path: 'scripts/kit-release.mjs', kind: 'script', mode: 0o644 },
|
|
44
44
|
{ path: 'scripts/release-delta-guard.mjs', kind: 'script', mode: 0o644 },
|
|
45
|
+
// Neutral publish/readback parity and externally reconstructable release state.
|
|
46
|
+
// /kit-release uses these after merge; downstream update/consumer flows reuse
|
|
47
|
+
// the parity primitive rather than growing a second registry/GitHub comparison.
|
|
48
|
+
{ path: 'scripts/release-parity.mjs', kind: 'script', mode: 0o644 },
|
|
49
|
+
{ path: 'scripts/release-state.mjs', kind: 'script', mode: 0o644 },
|
|
45
50
|
// Shared hook utility imported by the shipped hooks (drift-guard,
|
|
46
51
|
// sync-board-status). Library (imported, not run) → 0o644. MUST ship or those
|
|
47
52
|
// hooks ImportError on arrival.
|