@ikon85/agent-workflow-kit 0.30.0 → 0.31.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/board-to-waves/SKILL.md +14 -0
- package/.agents/skills/code-review/SKILL.md +22 -3
- package/.agents/skills/kit-update/SKILL.md +21 -1
- package/.agents/skills/local-ci/SKILL.md +16 -0
- package/.agents/skills/project-release/SKILL.md +18 -0
- package/.agents/skills/security-audit/SKILL.md +18 -0
- package/.agents/skills/setup-workflow/SKILL.md +39 -2
- package/.agents/skills/spec-self-critique/SKILL.md +16 -7
- package/.agents/skills/to-issues/SKILL.md +13 -1
- package/.agents/skills/to-prd/SKILL.md +14 -0
- package/.agents/skills/to-waves/SKILL.md +16 -2
- package/.agents/skills/triage/SKILL.md +24 -0
- package/.agents/skills/verify-spike/SKILL.md +20 -1
- package/.agents/skills/wrapup/SKILL.md +26 -4
- package/.claude/skills/board-to-waves/SKILL.md +14 -0
- package/.claude/skills/code-review/SKILL.md +22 -3
- package/.claude/skills/kit-update/SKILL.md +21 -1
- package/.claude/skills/local-ci/SKILL.md +16 -0
- package/.claude/skills/project-release/SKILL.md +18 -0
- package/.claude/skills/security-audit/SKILL.md +18 -0
- package/.claude/skills/setup-workflow/SKILL.md +39 -2
- package/.claude/skills/spec-self-critique/SKILL.md +16 -7
- package/.claude/skills/to-issues/SKILL.md +13 -1
- package/.claude/skills/to-prd/SKILL.md +14 -0
- package/.claude/skills/to-waves/SKILL.md +16 -2
- package/.claude/skills/triage/SKILL.md +24 -0
- package/.claude/skills/verify-spike/SKILL.md +20 -1
- package/.claude/skills/wrapup/SKILL.md +26 -4
- package/README.md +32 -0
- package/agent-workflow-kit.package.json +30 -30
- package/package.json +1 -1
- package/scripts/kit-update-pr.mjs +14 -1
- package/scripts/kit-update-pr.test.mjs +21 -0
- package/scripts/test_retro_wrapup_contract.py +17 -0
- package/scripts/test_skill_optional_readiness.py +171 -0
- package/scripts/test_skill_readiness_preflight.py +180 -0
- package/scripts/test_skill_required_readiness.py +233 -0
- package/scripts/test_skill_setup_workflow_seeds.py +22 -0
- package/src/cli.mjs +10 -2
- package/src/commands/update.mjs +65 -8
- package/src/lib/updateCandidate.mjs +116 -2
|
@@ -6,7 +6,13 @@ import { promisify } from 'node:util';
|
|
|
6
6
|
import { writeAtomic } from './atomicWrite.mjs';
|
|
7
7
|
import { validateConsumerFile } from './consumerPath.mjs';
|
|
8
8
|
import { sha256File } from './hash.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { stubSentinel } from './sentinel.mjs';
|
|
10
|
+
import { STUB_TARGETS } from './bundle.mjs';
|
|
11
|
+
import {
|
|
12
|
+
CONSUMER_MANIFEST_NAME, CONSUMER_ORIGIN, READINESS_MANIFEST_PATH,
|
|
13
|
+
indexByPath, readManifest, writeManifest,
|
|
14
|
+
} from './manifest.mjs';
|
|
15
|
+
import { checkSkill, evaluateCapability } from '../../scripts/readiness.mjs';
|
|
10
16
|
|
|
11
17
|
const run = promisify(execFile);
|
|
12
18
|
const exists = (path) => access(path).then(() => true, () => false);
|
|
@@ -30,9 +36,11 @@ export async function stageConsumer(consumerRoot) {
|
|
|
30
36
|
/** Activate only verified kit-owned deltas, rolling every touched path back on failure. */
|
|
31
37
|
export async function activateCandidate({
|
|
32
38
|
candidateRoot, consumerRoot, pkg, preview, consumerManifestBefore,
|
|
39
|
+
afterGenerated = async () => {},
|
|
33
40
|
}) {
|
|
34
41
|
const changed = [...preview.added, ...preview.updated];
|
|
35
|
-
const
|
|
42
|
+
const generated = preview.generated ?? [];
|
|
43
|
+
const touched = [...changed, ...generated, ...preview.deleted, CONSUMER_MANIFEST_NAME];
|
|
36
44
|
const currentManifest = await readFile(join(consumerRoot, CONSUMER_MANIFEST_NAME));
|
|
37
45
|
if (!currentManifest.equals(consumerManifestBefore)) {
|
|
38
46
|
throw new Error('consumer manifest changed during verification');
|
|
@@ -43,6 +51,13 @@ export async function activateCandidate({
|
|
|
43
51
|
throw new Error(`candidate hash mismatch: ${path}`);
|
|
44
52
|
}
|
|
45
53
|
}
|
|
54
|
+
const candidateManifest = await readManifest(join(candidateRoot, CONSUMER_MANIFEST_NAME));
|
|
55
|
+
const candidateInstalled = indexByPath(candidateManifest, 'installed');
|
|
56
|
+
for (const path of generated) {
|
|
57
|
+
if (await sha256File(join(candidateRoot, path)) !== candidateInstalled.get(path)?.installedSha256) {
|
|
58
|
+
throw new Error(`generated candidate hash mismatch: ${path}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
46
61
|
await assertConsumerStillMatchesPreview(consumerRoot, preview);
|
|
47
62
|
const rollback = new Map();
|
|
48
63
|
for (const path of touched) rollback.set(path, await snapshot(join(consumerRoot, path)));
|
|
@@ -50,6 +65,10 @@ export async function activateCandidate({
|
|
|
50
65
|
for (const path of changed) {
|
|
51
66
|
await writeAtomic(join(consumerRoot, path), await readFile(join(candidateRoot, path)), pkgIdx.get(path)?.mode);
|
|
52
67
|
}
|
|
68
|
+
for (const path of generated) {
|
|
69
|
+
await writeAtomic(join(consumerRoot, path), await readFile(join(candidateRoot, path)));
|
|
70
|
+
}
|
|
71
|
+
await afterGenerated();
|
|
53
72
|
for (const path of preview.deleted) await rm(join(consumerRoot, path), { force: true });
|
|
54
73
|
await writeAtomic(
|
|
55
74
|
join(consumerRoot, CONSUMER_MANIFEST_NAME),
|
|
@@ -57,6 +76,7 @@ export async function activateCandidate({
|
|
|
57
76
|
);
|
|
58
77
|
} catch (error) {
|
|
59
78
|
for (const path of touched.reverse()) await restore(join(consumerRoot, path), rollback.get(path));
|
|
79
|
+
error.consumerState = 'rolled-back';
|
|
60
80
|
throw error;
|
|
61
81
|
}
|
|
62
82
|
}
|
|
@@ -80,6 +100,11 @@ async function assertConsumerStillMatchesPreview(consumerRoot, preview) {
|
|
|
80
100
|
if (replacements.has(path)) continue;
|
|
81
101
|
if (await exists(join(consumerRoot, path))) throw new Error(`consumer changed during verification: ${path}`);
|
|
82
102
|
}
|
|
103
|
+
for (const path of preview.generated ?? []) {
|
|
104
|
+
if (await exists(join(consumerRoot, path))) {
|
|
105
|
+
throw new Error(`consumer changed during verification: ${path}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
83
108
|
for (const path of [...preview.updated, ...preview.deleted]) {
|
|
84
109
|
const prior = installed.get(path);
|
|
85
110
|
const current = await exists(join(consumerRoot, path))
|
|
@@ -90,6 +115,95 @@ async function assertConsumerStillMatchesPreview(consumerRoot, preview) {
|
|
|
90
115
|
}
|
|
91
116
|
}
|
|
92
117
|
|
|
118
|
+
/** Seed only newly declared, decision-free project-layer stubs in a staged candidate. */
|
|
119
|
+
export async function adoptReadinessCandidate({ candidateRoot, consumerRoot, priorManifest, nextManifest }) {
|
|
120
|
+
const priorPaths = readinessStubPaths(priorManifest);
|
|
121
|
+
const manifestPath = join(candidateRoot, CONSUMER_MANIFEST_NAME);
|
|
122
|
+
const manifest = await readManifest(manifestPath);
|
|
123
|
+
const candidateInstalled = indexByPath(manifest, 'installed');
|
|
124
|
+
const generated = [];
|
|
125
|
+
for (const path of readinessStubPaths(nextManifest)) {
|
|
126
|
+
if (priorPaths.has(path)) continue;
|
|
127
|
+
if (await exists(join(candidateRoot, path))) {
|
|
128
|
+
if (candidateInstalled.get(path)?.origin === CONSUMER_ORIGIN
|
|
129
|
+
&& !await exists(join(consumerRoot, path))) generated.push(path);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (await exists(join(consumerRoot, path))) continue;
|
|
133
|
+
await writeAtomic(join(candidateRoot, path), `${stubSentinel()}\n`);
|
|
134
|
+
generated.push(path);
|
|
135
|
+
}
|
|
136
|
+
if (generated.length) {
|
|
137
|
+
const installed = [...manifest.installed];
|
|
138
|
+
for (const generatedPath of generated) {
|
|
139
|
+
if (candidateInstalled.has(generatedPath)) continue;
|
|
140
|
+
installed.push({
|
|
141
|
+
path: generatedPath, kind: 'doc', installedSha256: await sha256File(join(candidateRoot, generatedPath)),
|
|
142
|
+
origin: CONSUMER_ORIGIN, installRole: 'consumer',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
await writeManifest(manifestPath, { ...manifest, installed });
|
|
146
|
+
}
|
|
147
|
+
const before = await readinessSnapshot(consumerRoot, priorManifest);
|
|
148
|
+
const after = await readinessSnapshot(candidateRoot, nextManifest);
|
|
149
|
+
const incompatible = Object.entries(after.skills)
|
|
150
|
+
.filter(([skill, current]) => before.skills[skill]?.verdict !== 'blocked'
|
|
151
|
+
&& before.skills[skill] && current.verdict === 'blocked')
|
|
152
|
+
.map(([skill]) => skill)
|
|
153
|
+
.sort();
|
|
154
|
+
return { generated, availability: readinessDiff(before, after), incompatible };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readinessStubPaths(manifest) {
|
|
158
|
+
const safe = new Set(STUB_TARGETS);
|
|
159
|
+
return new Set(Object.values(manifest?.readiness?.capabilities ?? {}).flatMap((capability) => {
|
|
160
|
+
if (capability.evidence?.type !== 'sentinel') return [];
|
|
161
|
+
return (capability.evidence.paths ?? []).filter((path) => safe.has(path));
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function readinessSnapshot(root, manifest) {
|
|
166
|
+
const skills = {};
|
|
167
|
+
for (const [name, declaration] of Object.entries(manifest?.skills ?? {})) {
|
|
168
|
+
if (!declaration.readiness) continue;
|
|
169
|
+
skills[name] = await checkSkill({ root, skill: name, manifest });
|
|
170
|
+
}
|
|
171
|
+
const consumer = await readManifest(join(root, CONSUMER_MANIFEST_NAME));
|
|
172
|
+
const capabilities = {};
|
|
173
|
+
for (const [name, capability] of Object.entries(manifest?.readiness?.capabilities ?? {})) {
|
|
174
|
+
capabilities[name] = await evaluateCapability({
|
|
175
|
+
root, capability, decision: consumer?.readinessDecisions?.[name],
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return { skills, capabilities };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readinessDiff(before, after) {
|
|
182
|
+
const newlyAvailable = [];
|
|
183
|
+
const newlyDegraded = [];
|
|
184
|
+
const newlyBlocked = [];
|
|
185
|
+
const unresolved = new Set();
|
|
186
|
+
for (const [skill, current] of Object.entries(after.skills)) {
|
|
187
|
+
const prior = before.skills[skill];
|
|
188
|
+
if (current.verdict === 'blocked' && prior?.verdict !== 'blocked') newlyBlocked.push(skill);
|
|
189
|
+
if (current.verdict !== 'blocked' && (!prior || prior.verdict === 'blocked')) newlyAvailable.push(skill);
|
|
190
|
+
for (const block of current.inactiveBlocks) {
|
|
191
|
+
if (!prior || !prior.inactiveBlocks.includes(block)) newlyDegraded.push(`${skill}.${block}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const [capability, result] of Object.entries(after.capabilities)) {
|
|
195
|
+
if (result.state !== 'ready') unresolved.add(`${capability}:${result.state}`);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
newlyAvailable: newlyAvailable.sort(), newlyDegraded: newlyDegraded.sort(),
|
|
199
|
+
newlyBlocked: newlyBlocked.sort(), stillUnresolved: [...unresolved].sort(),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function readReadinessManifest(root) {
|
|
204
|
+
return readManifest(join(root, READINESS_MANIFEST_PATH));
|
|
205
|
+
}
|
|
206
|
+
|
|
93
207
|
async function snapshot(path) {
|
|
94
208
|
if (!await exists(path)) return null;
|
|
95
209
|
const info = await stat(path);
|