@ikon85/agent-workflow-kit 0.31.0 → 0.32.1
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/audit-skills/SKILL.md +34 -5
- package/.agents/skills/census-update/SKILL.md +34 -3
- package/.agents/skills/git-worktree-recover/SKILL.md +31 -10
- package/.agents/skills/kit-update/SKILL.md +18 -4
- package/.agents/skills/orchestrate-wave/SKILL.md +43 -14
- package/.agents/skills/setup-workflow/census.md +14 -7
- package/.claude/skills/audit-skills/SKILL.md +34 -5
- package/.claude/skills/census-update/SKILL.md +34 -3
- package/.claude/skills/git-worktree-recover/SKILL.md +31 -10
- package/.claude/skills/kit-update/SKILL.md +18 -4
- package/.claude/skills/orchestrate-wave/SKILL.md +43 -14
- package/.claude/skills/setup-workflow/census.md +14 -7
- package/README.md +23 -1
- package/agent-workflow-kit.package.json +14 -14
- package/package.json +1 -1
- package/scripts/readiness.mjs +48 -14
- package/scripts/test_census_update_contract.test.mjs +14 -0
- package/scripts/test_skill_readiness_contract.py +127 -7
- package/src/cli.mjs +2 -2
- package/src/commands/update.mjs +21 -3
- package/src/lib/updateCandidate.mjs +78 -4
|
@@ -66,13 +66,20 @@ delegates this route to the shipped `census-update` contract and its focused
|
|
|
66
66
|
`scripts/test_census_update_contract.test.mjs` proof; it does not reproduce
|
|
67
67
|
activation, snapshots, or enforcement. Setup itself never calls `activateCensus`.
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
Activation owns durable enforcement: every focused test declared by the active
|
|
70
|
+
profile must remain transitively reachable through one shared project-local
|
|
71
|
+
census check entry point from both local CI and pre-push. `census-update`
|
|
72
|
+
reconciles that narrow kit-owned census wiring idempotently and requires an
|
|
73
|
+
executable wiring proof before it can report `current`; setup does not pre-wire
|
|
74
|
+
the optional bootstrap state.
|
|
75
|
+
|
|
76
|
+
Disable follows the ordered contract: remove only the kit-owned census wiring
|
|
77
|
+
(including the kit-owned hook and gate block), then atomically replace only the
|
|
78
|
+
profile's `enabled` value with `false`, preserving unknown keys, and verify
|
|
79
|
+
`disabled` through `resolveCensusState`. Enforcement removal must finish before
|
|
80
|
+
any profile mutation. Treat the choice document, local scanners, their tests,
|
|
81
|
+
the profile, and the active snapshot as consumer-owned evidence. List those
|
|
82
|
+
files and ask for separate deletion approval; without that approval, retain them.
|
|
76
83
|
Setup never deletes consumer-owned files as part of disable.
|
|
77
84
|
|
|
78
85
|
## Setup report
|
package/README.md
CHANGED
|
@@ -305,7 +305,10 @@ any language works — map each role once, rename an option with one profile edi
|
|
|
305
305
|
`init` records a sha256 of every file it installs. That's the line between the
|
|
306
306
|
two: **edit any skill or script freely** — `update` detects your edits and backs
|
|
307
307
|
them up rather than clobbering them. Your **project layer** (`docs/agents/*`, the
|
|
308
|
-
board profile, `CLAUDE.md`)
|
|
308
|
+
board profile, `CLAUDE.md`, `AGENTS.md`) remains consumer-owned. `update` may
|
|
309
|
+
only apply a previewed, schema-driven, idempotent compatibility migration that
|
|
310
|
+
fills missing evidence without rewriting an existing value; it verifies and
|
|
311
|
+
rolls that migration back with the rest of the candidate.
|
|
309
312
|
|
|
310
313
|
## Updating & removing
|
|
311
314
|
|
|
@@ -348,6 +351,25 @@ concurrency-safe. Do not run manifest-mutating commands concurrently. Flags:
|
|
|
348
351
|
|
|
349
352
|
## Release notes
|
|
350
353
|
|
|
354
|
+
### 0.32.1
|
|
355
|
+
|
|
356
|
+
- changed: `.agents/skills/census-update/SKILL.md`
|
|
357
|
+
- changed: `.agents/skills/kit-update/SKILL.md`
|
|
358
|
+
- changed: `.agents/skills/setup-workflow/census.md`
|
|
359
|
+
- changed: `.claude/skills/census-update/SKILL.md`
|
|
360
|
+
- changed: `.claude/skills/kit-update/SKILL.md`
|
|
361
|
+
- changed: `.claude/skills/setup-workflow/census.md`
|
|
362
|
+
- changed: `scripts/readiness.mjs`
|
|
363
|
+
|
|
364
|
+
### 0.32.0
|
|
365
|
+
|
|
366
|
+
- changed: `.agents/skills/audit-skills/SKILL.md`
|
|
367
|
+
- changed: `.agents/skills/git-worktree-recover/SKILL.md`
|
|
368
|
+
- changed: `.agents/skills/orchestrate-wave/SKILL.md`
|
|
369
|
+
- changed: `.claude/skills/audit-skills/SKILL.md`
|
|
370
|
+
- changed: `.claude/skills/git-worktree-recover/SKILL.md`
|
|
371
|
+
- changed: `.claude/skills/orchestrate-wave/SKILL.md`
|
|
372
|
+
|
|
351
373
|
### 0.31.0
|
|
352
374
|
|
|
353
375
|
- changed: `.agents/skills/board-to-waves/SKILL.md`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kitVersion": "0.
|
|
2
|
+
"kitVersion": "0.32.1",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": ".agents/skills/ask-matt/SKILL.md",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"ownerSkill": "audit-skills",
|
|
28
28
|
"surface": "codex",
|
|
29
29
|
"installRole": "consumer",
|
|
30
|
-
"sha256": "
|
|
30
|
+
"sha256": "662c4dec0c03716c468f202f73f52140ceba84e092873e6619692dde6d7ed0f2",
|
|
31
31
|
"mode": 420,
|
|
32
32
|
"origin": "kit"
|
|
33
33
|
},
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"ownerSkill": "census-update",
|
|
48
48
|
"surface": "codex",
|
|
49
49
|
"installRole": "consumer",
|
|
50
|
-
"sha256": "
|
|
50
|
+
"sha256": "b122fe2eb46c070696e38660b021411458b455b7a4e977174f943f2ce4c00819",
|
|
51
51
|
"mode": 420,
|
|
52
52
|
"origin": "kit"
|
|
53
53
|
},
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
"ownerSkill": "git-worktree-recover",
|
|
198
198
|
"surface": "codex",
|
|
199
199
|
"installRole": "consumer",
|
|
200
|
-
"sha256": "
|
|
200
|
+
"sha256": "1628f47a5b5f71b7596ec070c8f038a3ce98abe007fce1b7f5c21ea40858124d",
|
|
201
201
|
"mode": 420,
|
|
202
202
|
"origin": "kit"
|
|
203
203
|
},
|
|
@@ -377,7 +377,7 @@
|
|
|
377
377
|
"ownerSkill": "kit-update",
|
|
378
378
|
"surface": "codex",
|
|
379
379
|
"installRole": "consumer",
|
|
380
|
-
"sha256": "
|
|
380
|
+
"sha256": "249a45761bcb95148b1567d1bc4d1b622b9b8c41519de4e67b1ef4e33c9ee9fd",
|
|
381
381
|
"mode": 420,
|
|
382
382
|
"origin": "kit"
|
|
383
383
|
},
|
|
@@ -417,7 +417,7 @@
|
|
|
417
417
|
"ownerSkill": "orchestrate-wave",
|
|
418
418
|
"surface": "codex",
|
|
419
419
|
"installRole": "consumer",
|
|
420
|
-
"sha256": "
|
|
420
|
+
"sha256": "7a94293b7db49f42497d34e6ae0fe9992ab1c770ae0766557618818d6d671c50",
|
|
421
421
|
"mode": 420,
|
|
422
422
|
"origin": "kit"
|
|
423
423
|
},
|
|
@@ -567,7 +567,7 @@
|
|
|
567
567
|
"ownerSkill": "setup-workflow",
|
|
568
568
|
"surface": "codex",
|
|
569
569
|
"installRole": "consumer",
|
|
570
|
-
"sha256": "
|
|
570
|
+
"sha256": "d495758ae52bebd11c81bbe28fc5a1f9b614f972afdba55d1b728630d12cfee8",
|
|
571
571
|
"mode": 420,
|
|
572
572
|
"origin": "kit"
|
|
573
573
|
},
|
|
@@ -1189,7 +1189,7 @@
|
|
|
1189
1189
|
"ownerSkill": "audit-skills",
|
|
1190
1190
|
"surface": "claude",
|
|
1191
1191
|
"installRole": "consumer",
|
|
1192
|
-
"sha256": "
|
|
1192
|
+
"sha256": "dce7c13e9b9c2eb6fe9028f69fe2fe2d48ad50546327c9d9c1232191425f1b7c",
|
|
1193
1193
|
"mode": 420,
|
|
1194
1194
|
"origin": "kit"
|
|
1195
1195
|
},
|
|
@@ -1209,7 +1209,7 @@
|
|
|
1209
1209
|
"ownerSkill": "census-update",
|
|
1210
1210
|
"surface": "claude",
|
|
1211
1211
|
"installRole": "consumer",
|
|
1212
|
-
"sha256": "
|
|
1212
|
+
"sha256": "b122fe2eb46c070696e38660b021411458b455b7a4e977174f943f2ce4c00819",
|
|
1213
1213
|
"mode": 420,
|
|
1214
1214
|
"origin": "kit"
|
|
1215
1215
|
},
|
|
@@ -1419,7 +1419,7 @@
|
|
|
1419
1419
|
"ownerSkill": "git-worktree-recover",
|
|
1420
1420
|
"surface": "claude",
|
|
1421
1421
|
"installRole": "consumer",
|
|
1422
|
-
"sha256": "
|
|
1422
|
+
"sha256": "3251c8db671286330e48748719e05ee3fdc0555d93d61d7fee3fc0e0d2868e92",
|
|
1423
1423
|
"mode": 420,
|
|
1424
1424
|
"origin": "kit"
|
|
1425
1425
|
},
|
|
@@ -1659,7 +1659,7 @@
|
|
|
1659
1659
|
"ownerSkill": "kit-update",
|
|
1660
1660
|
"surface": "claude",
|
|
1661
1661
|
"installRole": "consumer",
|
|
1662
|
-
"sha256": "
|
|
1662
|
+
"sha256": "249a45761bcb95148b1567d1bc4d1b622b9b8c41519de4e67b1ef4e33c9ee9fd",
|
|
1663
1663
|
"mode": 420,
|
|
1664
1664
|
"origin": "kit"
|
|
1665
1665
|
},
|
|
@@ -1699,7 +1699,7 @@
|
|
|
1699
1699
|
"ownerSkill": "orchestrate-wave",
|
|
1700
1700
|
"surface": "claude",
|
|
1701
1701
|
"installRole": "consumer",
|
|
1702
|
-
"sha256": "
|
|
1702
|
+
"sha256": "9690ec772a99131c225037dd4bca884e899c133a8e460c36b26a25d6c6c797e1",
|
|
1703
1703
|
"mode": 420,
|
|
1704
1704
|
"origin": "kit"
|
|
1705
1705
|
},
|
|
@@ -1869,7 +1869,7 @@
|
|
|
1869
1869
|
"ownerSkill": "setup-workflow",
|
|
1870
1870
|
"surface": "claude",
|
|
1871
1871
|
"installRole": "consumer",
|
|
1872
|
-
"sha256": "
|
|
1872
|
+
"sha256": "d495758ae52bebd11c81bbe28fc5a1f9b614f972afdba55d1b728630d12cfee8",
|
|
1873
1873
|
"mode": 420,
|
|
1874
1874
|
"origin": "kit"
|
|
1875
1875
|
},
|
|
@@ -2587,7 +2587,7 @@
|
|
|
2587
2587
|
"path": "scripts/readiness.mjs",
|
|
2588
2588
|
"kind": "script",
|
|
2589
2589
|
"installRole": "consumer",
|
|
2590
|
-
"sha256": "
|
|
2590
|
+
"sha256": "9e7a9c6882e372f38383fe92d98ff2f818bae0be61a62301b7e9a6a2842dd21b",
|
|
2591
2591
|
"mode": 493,
|
|
2592
2592
|
"origin": "kit"
|
|
2593
2593
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikon85/agent-workflow-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.1",
|
|
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": {
|
package/scripts/readiness.mjs
CHANGED
|
@@ -99,22 +99,53 @@ function section(text, heading) {
|
|
|
99
99
|
return body.join('\n').trim();
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
async function
|
|
103
|
-
const
|
|
102
|
+
export async function inspectProdSections(root, paths) {
|
|
103
|
+
const sections = [];
|
|
104
104
|
for (const path of paths) {
|
|
105
105
|
const text = await readText(root, path);
|
|
106
|
-
if (text === null) continue;
|
|
107
106
|
const body = section(text, '## Prod');
|
|
108
|
-
|
|
107
|
+
const problem = text === null ? 'missing-file'
|
|
108
|
+
: (body === null ? 'missing-section' : (body ? null : 'empty-section'));
|
|
109
|
+
sections.push({
|
|
110
|
+
path,
|
|
111
|
+
state: body === null ? 'missing' : (body ? 'valid' : 'invalid'),
|
|
112
|
+
body,
|
|
113
|
+
problem,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return sections;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function prodEvidence(root, paths) {
|
|
120
|
+
const sections = await inspectProdSections(root, paths);
|
|
121
|
+
const present = sections.filter(({ problem }) => problem !== 'missing-file');
|
|
122
|
+
const valid = present.filter(({ state }) => state === 'valid');
|
|
123
|
+
const empty = present.filter(({ problem }) => problem === 'empty-section');
|
|
124
|
+
if (!valid.length && !empty.length) {
|
|
125
|
+
return {
|
|
126
|
+
verdict: 'absent',
|
|
127
|
+
diagnostics: sections.map(({ path, problem }) => ({ path, problem })),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const malformed = present.filter(({ state }) => state !== 'valid');
|
|
131
|
+
if (malformed.length) {
|
|
132
|
+
return {
|
|
133
|
+
verdict: 'invalid',
|
|
134
|
+
diagnostics: malformed.map(({ path, problem }) => ({ path, problem })),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const bodies = [...new Set(present.map(({ body }) => body))];
|
|
138
|
+
if (bodies.length > 1) {
|
|
139
|
+
return {
|
|
140
|
+
verdict: 'invalid',
|
|
141
|
+
diagnostics: present.map(({ path }) => ({ path, problem: 'divergent-section' })),
|
|
142
|
+
};
|
|
109
143
|
}
|
|
110
|
-
|
|
111
|
-
if (bodies.every((body) => body === null)) return 'absent';
|
|
112
|
-
if (bodies.some((body) => !body)) return 'invalid';
|
|
113
|
-
return bodies.every((body) => body === bodies[0]) ? 'valid' : 'invalid';
|
|
144
|
+
return { verdict: 'valid', diagnostics: [] };
|
|
114
145
|
}
|
|
115
146
|
|
|
116
147
|
async function evidenceVerdict(root, evidence) {
|
|
117
|
-
if (evidence.type === 'prod-section') return
|
|
148
|
+
if (evidence.type === 'prod-section') return (await prodEvidence(root, evidence.paths)).verdict;
|
|
118
149
|
if (evidence.type === 'runbook-reference') return runbookVerdict(root, evidence);
|
|
119
150
|
const verdicts = await Promise.all((evidence.paths ?? []).map(async (path) => {
|
|
120
151
|
const text = await readText(root, path);
|
|
@@ -129,14 +160,17 @@ async function evidenceVerdict(root, evidence) {
|
|
|
129
160
|
}
|
|
130
161
|
|
|
131
162
|
export async function evaluateCapability({ root, capability, decision }) {
|
|
132
|
-
const
|
|
133
|
-
|
|
163
|
+
const prod = capability.evidence.type === 'prod-section'
|
|
164
|
+
? await prodEvidence(root, capability.evidence.paths) : null;
|
|
165
|
+
const evidence = prod?.verdict ?? await evidenceVerdict(root, capability.evidence);
|
|
166
|
+
const diagnostics = prod?.diagnostics?.length ? { diagnostics: prod.diagnostics } : {};
|
|
167
|
+
if (evidence === 'invalid') return { state: 'invalid', clearDecision: false, ...diagnostics };
|
|
134
168
|
if (evidence === 'valid') return { state: 'ready', clearDecision: Boolean(decision) };
|
|
135
|
-
if (decision === 'pending') return { state: 'pending', clearDecision: false };
|
|
169
|
+
if (decision === 'pending') return { state: 'pending', clearDecision: false, ...diagnostics };
|
|
136
170
|
if (decision === 'not-applicable' && capability.allowNotApplicable) {
|
|
137
|
-
return { state: 'not-applicable', clearDecision: false };
|
|
171
|
+
return { state: 'not-applicable', clearDecision: false, ...diagnostics };
|
|
138
172
|
}
|
|
139
|
-
return { state: 'missing', clearDecision: false };
|
|
173
|
+
return { state: 'missing', clearDecision: false, ...diagnostics };
|
|
140
174
|
}
|
|
141
175
|
|
|
142
176
|
async function loadManifest(root) {
|
|
@@ -169,6 +169,20 @@ test('census-update coordinates the stable foundation API without a second engin
|
|
|
169
169
|
assert.match(skill, /\.census\/profile\.json/);
|
|
170
170
|
assert.match(skill, /\.census\/active\.json/);
|
|
171
171
|
assert.match(skill, /schemaVersion/);
|
|
172
|
+
assert.match(skill, /every `localScanners\[\]\.test`/);
|
|
173
|
+
assert.match(skill, /shared project-local census check entry point/i);
|
|
174
|
+
assert.match(
|
|
175
|
+
skill,
|
|
176
|
+
/It must execute every declared focused test,\s+and both local CI and pre-push must transitively reach that same entry point\./i,
|
|
177
|
+
);
|
|
178
|
+
assert.match(skill, /executable wiring test/i);
|
|
179
|
+
assert.match(skill, /missing or partial[\s\S]*failed/i);
|
|
180
|
+
assert.match(skill, /previous active\s+census bytes are unchanged/i);
|
|
181
|
+
assert.match(skill, /idempotent/i);
|
|
182
|
+
|
|
183
|
+
const setupCensus = await text('.claude/skills/setup-workflow/census.md');
|
|
184
|
+
assert.match(setupCensus, /activation[\s\S]*durable[\s\S]*local CI[\s\S]*pre-push/i);
|
|
185
|
+
assert.match(setupCensus, /remove only[\s\S]*kit-owned census wiring/i);
|
|
172
186
|
});
|
|
173
187
|
|
|
174
188
|
test('brownfield bootstrap activates real surface coverage with a separate behavior overview', async () => {
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import re
|
|
7
|
+
import subprocess
|
|
7
8
|
import unittest
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
@@ -12,7 +13,8 @@ MANIFEST_PATH = ROOT / ".claude/skills/skill-manifest.json"
|
|
|
12
13
|
START = re.compile(r"^<!-- readiness:block ([a-z][A-Za-z0-9]*) -->$")
|
|
13
14
|
END = "<!-- readiness:end -->"
|
|
14
15
|
SURFACE = {"claude": ".claude/skills", "codex": ".agents/skills"}
|
|
15
|
-
|
|
16
|
+
FIXTURES = ROOT / "test/fixtures/readiness"
|
|
17
|
+
REQUIRED_V1 = {
|
|
16
18
|
"to-prd": ["issueTracker", "managedBoard"],
|
|
17
19
|
"to-issues": ["issueTracker", "managedBoard", "specCompleteness"],
|
|
18
20
|
"to-waves": ["issueTracker", "managedBoard", "specCompleteness"],
|
|
@@ -23,7 +25,7 @@ REQUIRED = {
|
|
|
23
25
|
"project-release": ["projectReleaseProfile"],
|
|
24
26
|
"security-audit": ["securityAuditRunbook"],
|
|
25
27
|
}
|
|
26
|
-
|
|
28
|
+
OPTIONAL_V1 = {
|
|
27
29
|
"wrapup": {"deployReport": "prodTarget"},
|
|
28
30
|
"orchestrate-wave": {"projectRecipe": "orchestrateWaveRecipe"},
|
|
29
31
|
"spec-self-critique": {"projectEnrichment": "specCritiqueLayer"},
|
|
@@ -32,6 +34,74 @@ OPTIONAL = {
|
|
|
32
34
|
"audit-skills": {"projectChecks": "auditSkillsLayer"},
|
|
33
35
|
"git-worktree-recover": {"projectRecovery": "worktreeRecoveryLayer"},
|
|
34
36
|
}
|
|
37
|
+
COMPAT_UPDATE_SCRIPT = r"""
|
|
38
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
39
|
+
import { join } from 'node:path';
|
|
40
|
+
import { pathToFileURL } from 'node:url';
|
|
41
|
+
const [repo, fixturePath] = process.argv.slice(-2);
|
|
42
|
+
const load = async (path) => import(pathToFileURL(join(repo, path)));
|
|
43
|
+
const { init } = await load('src/commands/init.mjs');
|
|
44
|
+
const { update } = await load('src/commands/update.mjs');
|
|
45
|
+
const { checkSkill } = await load('scripts/readiness.mjs');
|
|
46
|
+
const { makeKit, makeEmptyDir, cleanup } = await load('test/helpers.mjs');
|
|
47
|
+
const { PACKAGE_MANIFEST_NAME, readManifest, writeManifest } = await load('src/lib/manifest.mjs');
|
|
48
|
+
const { sha256 } = await load('src/lib/hash.mjs');
|
|
49
|
+
const readinessPath = '.claude/skills/skill-manifest.json';
|
|
50
|
+
const fixture = JSON.parse(await readFile(fixturePath, 'utf8'));
|
|
51
|
+
const current = JSON.parse(await readFile(join(repo, readinessPath), 'utf8'));
|
|
52
|
+
const previous = structuredClone(current);
|
|
53
|
+
for (const skill of Object.keys(fixture.readinessFixture.skills)) {
|
|
54
|
+
previous.skills[skill].readiness = {};
|
|
55
|
+
}
|
|
56
|
+
const kit = await makeKit({ '.claude/skills/to-prd/SKILL.md': 'fixture\n' });
|
|
57
|
+
const consumer = await makeEmptyDir();
|
|
58
|
+
async function setKitReadiness(manifest) {
|
|
59
|
+
const content = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
60
|
+
const path = join(kit, readinessPath);
|
|
61
|
+
await mkdir(join(kit, '.claude/skills'), { recursive: true });
|
|
62
|
+
await writeFile(path, content);
|
|
63
|
+
const pkg = await readManifest(join(kit, PACKAGE_MANIFEST_NAME));
|
|
64
|
+
const entry = pkg.files.find(({ path: candidate }) => candidate === readinessPath);
|
|
65
|
+
if (entry) entry.sha256 = sha256(content);
|
|
66
|
+
else pkg.files.push({ path: readinessPath, kind: 'doc', sha256: sha256(content), mode: 0o644, origin: 'kit' });
|
|
67
|
+
await writeManifest(join(kit, PACKAGE_MANIFEST_NAME), pkg);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
await setKitReadiness(previous);
|
|
71
|
+
await init({ kitRoot: kit, consumerRoot: consumer });
|
|
72
|
+
for (const [relative, body] of Object.entries(fixture.readinessFixture.evidence)) {
|
|
73
|
+
const path = join(consumer, relative);
|
|
74
|
+
await mkdir(join(path, '..'), { recursive: true });
|
|
75
|
+
await writeFile(path, body);
|
|
76
|
+
}
|
|
77
|
+
const manifestPath = join(consumer, 'agent-workflow-kit.json');
|
|
78
|
+
const installed = await readManifest(manifestPath);
|
|
79
|
+
await writeManifest(manifestPath, {
|
|
80
|
+
...installed,
|
|
81
|
+
readinessDecisions: fixture.readinessDecisions ?? {},
|
|
82
|
+
readinessFixture: fixture.readinessFixture,
|
|
83
|
+
});
|
|
84
|
+
const before = {};
|
|
85
|
+
for (const skill of Object.keys(fixture.readinessFixture.skills)) {
|
|
86
|
+
before[skill] = await checkSkill({ root: consumer, skill, manifest: previous });
|
|
87
|
+
}
|
|
88
|
+
await setKitReadiness(current);
|
|
89
|
+
const identity = { name: '@ikon85/agent-workflow-kit', version: '0.1.0', tarballIntegrity: 'sha512-fixture', manifestSha256: 'fixture-manifest' };
|
|
90
|
+
const result = await update({
|
|
91
|
+
kitRoot: kit, consumerRoot: consumer,
|
|
92
|
+
releaseIdentities: { installed: { name: identity.name, version: identity.version, manifestSha256: identity.manifestSha256 }, npm: identity, github: identity },
|
|
93
|
+
verify: async () => {},
|
|
94
|
+
});
|
|
95
|
+
const afterManifest = await readManifest(manifestPath);
|
|
96
|
+
const after = {};
|
|
97
|
+
for (const skill of Object.keys(fixture.readinessFixture.skills)) {
|
|
98
|
+
after[skill] = await checkSkill({ root: consumer, skill, manifest: current });
|
|
99
|
+
}
|
|
100
|
+
console.log(JSON.stringify({ before, result, afterManifest, after }));
|
|
101
|
+
} finally {
|
|
102
|
+
await cleanup(kit, consumer);
|
|
103
|
+
}
|
|
104
|
+
"""
|
|
35
105
|
|
|
36
106
|
|
|
37
107
|
def load_manifest() -> dict:
|
|
@@ -100,11 +170,32 @@ class ReadinessContractTests(unittest.TestCase):
|
|
|
100
170
|
blocks = list(declaration.get("optionalBlocks", {}))
|
|
101
171
|
self.assertEqual(len(blocks), len(set(blocks)), skill)
|
|
102
172
|
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
173
|
+
def test_locked_v1_policy_cannot_disappear_from_the_manifest(self) -> None:
|
|
174
|
+
actual_required = {
|
|
175
|
+
skill: entry["readiness"]["required"]
|
|
176
|
+
for skill, entry in self.published.items()
|
|
177
|
+
if entry.get("readiness", {}).get("required")
|
|
178
|
+
}
|
|
179
|
+
actual_optional = {
|
|
180
|
+
skill: entry["readiness"]["optionalBlocks"]
|
|
181
|
+
for skill, entry in self.published.items()
|
|
182
|
+
if entry.get("readiness", {}).get("optionalBlocks")
|
|
183
|
+
}
|
|
184
|
+
self.assertEqual(actual_required, REQUIRED_V1)
|
|
185
|
+
self.assertEqual(actual_optional, OPTIONAL_V1)
|
|
186
|
+
|
|
187
|
+
def test_every_declared_consumer_has_the_standard_preflight(self) -> None:
|
|
188
|
+
consumers = {
|
|
189
|
+
skill: entry for skill, entry in self.published.items()
|
|
190
|
+
if entry.get("readiness")
|
|
191
|
+
}
|
|
192
|
+
self.assertGreater(len(consumers), 0)
|
|
193
|
+
for skill, entry in consumers.items():
|
|
194
|
+
expected = f"node scripts/readiness.mjs check --skill {skill} --json"
|
|
195
|
+
for surface in entry["surfaces"]:
|
|
196
|
+
path = ROOT / SURFACE[surface] / skill / "SKILL.md"
|
|
197
|
+
body = path.read_text(encoding="utf-8")
|
|
198
|
+
self.assertEqual(body.count(expected), 1, f"{surface}:{skill}")
|
|
108
199
|
|
|
109
200
|
def test_markers_are_balanced_declared_and_surface_equal_when_activated(self) -> None:
|
|
110
201
|
for skill, entry in self.published.items():
|
|
@@ -114,6 +205,7 @@ class ReadinessContractTests(unittest.TestCase):
|
|
|
114
205
|
path = ROOT / SURFACE[surface] / skill / "SKILL.md"
|
|
115
206
|
self.assertTrue(path.is_file(), f"missing published surface: {path}")
|
|
116
207
|
sequence = marker_sequence(path.read_text(encoding="utf-8"), declared)
|
|
208
|
+
self.assertEqual(set(sequence), declared, f"undeclared marker coverage: {surface}:{skill}")
|
|
117
209
|
sequences.append((surface, sequence))
|
|
118
210
|
if len(sequences) > 1:
|
|
119
211
|
validate_surface_sequences([sequence for _, sequence in sequences])
|
|
@@ -134,6 +226,34 @@ class ReadinessContractTests(unittest.TestCase):
|
|
|
134
226
|
with self.assertRaises(AssertionError):
|
|
135
227
|
validate_surface_sequences([["projectEnrichment"], []])
|
|
136
228
|
|
|
229
|
+
def test_legacy_and_current_consumers_keep_previously_ready_skill_cores(self) -> None:
|
|
230
|
+
fixtures = sorted(FIXTURES.glob("*/agent-workflow-kit.json"))
|
|
231
|
+
self.assertEqual([path.parent.name for path in fixtures], ["current", "legacy"])
|
|
232
|
+
for fixture_path in fixtures:
|
|
233
|
+
fixture = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
234
|
+
scenario = fixture["readinessFixture"]
|
|
235
|
+
with self.subTest(fixture=fixture_path.parent.name):
|
|
236
|
+
run = subprocess.run(
|
|
237
|
+
["node", "--input-type=module", "-e", COMPAT_UPDATE_SCRIPT,
|
|
238
|
+
str(ROOT), str(fixture_path)],
|
|
239
|
+
check=True, capture_output=True, text=True,
|
|
240
|
+
)
|
|
241
|
+
proof = json.loads(run.stdout)
|
|
242
|
+
self.assertEqual(proof["result"]["state"], "applied")
|
|
243
|
+
self.assertEqual(proof["result"]["availability"]["newlyBlocked"], [])
|
|
244
|
+
for skill, expected in scenario["skills"].items():
|
|
245
|
+
self.assertEqual(proof["before"][skill]["verdict"], "ready", skill)
|
|
246
|
+
verdict = proof["after"][skill]
|
|
247
|
+
self.assertNotEqual(verdict["verdict"], "blocked", skill)
|
|
248
|
+
self.assertEqual(verdict["verdict"], expected["verdict"], skill)
|
|
249
|
+
self.assertEqual(verdict["inactiveBlocks"], expected["inactiveBlocks"], skill)
|
|
250
|
+
self.assertEqual(
|
|
251
|
+
proof["afterManifest"].get("readinessDecisions", {}),
|
|
252
|
+
scenario["expectedDecisions"],
|
|
253
|
+
"unavailable behavior must not manufacture a decision",
|
|
254
|
+
)
|
|
255
|
+
self.assertEqual(proof["afterManifest"]["readinessFixture"], scenario)
|
|
256
|
+
|
|
137
257
|
|
|
138
258
|
if __name__ == "__main__":
|
|
139
259
|
unittest.main()
|
package/src/cli.mjs
CHANGED
|
@@ -57,7 +57,7 @@ try {
|
|
|
57
57
|
} else if (r.status === 'current') {
|
|
58
58
|
p.outro(`aktuell · unchanged ${r.unchanged.length} · local modifications ${r.userModified.length}`);
|
|
59
59
|
} else {
|
|
60
|
-
p.outro(`updated ${r.updated.length} · added ${r.added.length} · deleted ${r.deleted.length}`);
|
|
60
|
+
p.outro(`updated ${r.updated.length} · added ${r.added.length} · migrated ${r.migrated?.length ?? 0} · deleted ${r.deleted.length}`);
|
|
61
61
|
}
|
|
62
62
|
} else if (cmd === 'uninstall') {
|
|
63
63
|
const ok = yes || (await p.confirm({ message: 'Remove kit-installed files?' })) === true;
|
|
@@ -82,7 +82,7 @@ function printPlan(r) {
|
|
|
82
82
|
const lines = [];
|
|
83
83
|
for (const k of [
|
|
84
84
|
'added', 'updated', 'userModified', 'consumerOwned', 'unchanged',
|
|
85
|
-
'deleted', 'keptDeleted', 'collisions',
|
|
85
|
+
'deleted', 'keptDeleted', 'collisions', 'migrated',
|
|
86
86
|
])
|
|
87
87
|
if (r[k]?.length) lines.push(`${k}: ${r[k].length}`);
|
|
88
88
|
if (r.conflicts?.length) lines.push(`conflicts: ${r.conflicts.length}`);
|
package/src/commands/update.mjs
CHANGED
|
@@ -55,6 +55,11 @@ export async function update(options) {
|
|
|
55
55
|
Object.assign(preview, await previewReadinessAdoption({
|
|
56
56
|
kitRoot, consumerRoot, priorReadinessManifest, nextReadinessManifest,
|
|
57
57
|
}));
|
|
58
|
+
preview.conflicts.push(...(preview.migrationConflicts ?? []).map((path) => ({
|
|
59
|
+
path,
|
|
60
|
+
kind: 'prod-section',
|
|
61
|
+
diff: 'Prod section differs from the other instruction surface or is malformed.',
|
|
62
|
+
})));
|
|
58
63
|
} catch (error) {
|
|
59
64
|
previewFailure = error;
|
|
60
65
|
}
|
|
@@ -66,6 +71,9 @@ export async function update(options) {
|
|
|
66
71
|
};
|
|
67
72
|
}
|
|
68
73
|
if (dryRun) return { ...preview, state: 'preview', history };
|
|
74
|
+
if (preview.migrationConflicts?.length) {
|
|
75
|
+
return terminal(preview, 'conflicted', history, transition);
|
|
76
|
+
}
|
|
69
77
|
if (preview.conflicts.length) return terminal(preview, 'conflicted', history, transition);
|
|
70
78
|
const resolvedPreview = await resolvePreview({
|
|
71
79
|
kitRoot, consumerRoot, preview, decisions, decide, transition,
|
|
@@ -138,7 +146,13 @@ async function applyTransaction(context) {
|
|
|
138
146
|
nextManifest: nextReadinessManifest,
|
|
139
147
|
});
|
|
140
148
|
preview.generated = readiness.generated;
|
|
149
|
+
preview.migrations = readiness.migrations;
|
|
150
|
+
preview.migrated = readiness.migrated;
|
|
151
|
+
preview.migrationConflicts = readiness.migrationConflicts;
|
|
141
152
|
preview.availability = readiness.availability;
|
|
153
|
+
if (readiness.migrationConflicts.length) {
|
|
154
|
+
throw new Error(`Prod section migration conflict: ${readiness.migrationConflicts.join(', ')}`);
|
|
155
|
+
}
|
|
142
156
|
if (readiness.incompatible.length) {
|
|
143
157
|
throw new Error(`monotonic compatibility would block existing skill core: ${readiness.incompatible.join(', ')}`);
|
|
144
158
|
}
|
|
@@ -180,6 +194,7 @@ function verifyRelease(identities, kitVersion) {
|
|
|
180
194
|
|
|
181
195
|
function hasUpstreamDelta(result) {
|
|
182
196
|
return result.manifestChanged ||
|
|
197
|
+
(result.migrations?.length ?? 0) > 0 ||
|
|
183
198
|
result.added.length + result.updated.length + result.deleted.length > 0;
|
|
184
199
|
}
|
|
185
200
|
|
|
@@ -205,9 +220,12 @@ async function terminal(result, state, history, transition) {
|
|
|
205
220
|
conflicts: result.conflicts.map(({ path }) => path),
|
|
206
221
|
keptDeleted: result.keptDeleted,
|
|
207
222
|
},
|
|
208
|
-
recommendation: result.
|
|
209
|
-
?
|
|
210
|
-
|
|
223
|
+
recommendation: result.migrationConflicts?.length
|
|
224
|
+
? `Prod sections differ or are malformed in: ${result.migrationConflicts.join(', ')}. ` +
|
|
225
|
+
'Resolve them manually; no consumer file was changed.'
|
|
226
|
+
: (result.conflicts.length
|
|
227
|
+
? 'Review each named conflict; keep the local file or apply the incoming diff manually.'
|
|
228
|
+
: null),
|
|
211
229
|
},
|
|
212
230
|
};
|
|
213
231
|
}
|