@ikon85/agent-workflow-kit 0.38.0 → 0.39.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-update/SKILL.md +33 -1
- package/.agents/skills/setup-workflow/SKILL.md +34 -3
- package/.agents/skills/setup-workflow/board-sync.md +6 -2
- package/.agents/skills/setup-workflow/workflow-advisories.md +34 -0
- package/.agents/skills/setup-workflow/worktree-lifecycle.md +31 -2
- package/.agents/skills/wrapup/SKILL.md +24 -0
- package/.claude/hooks/drift-guard.py +212 -21
- package/.claude/skills/kit-update/SKILL.md +33 -1
- package/.claude/skills/setup-workflow/SKILL.md +34 -3
- package/.claude/skills/setup-workflow/board-sync.md +6 -2
- package/.claude/skills/setup-workflow/workflow-advisories.md +34 -0
- package/.claude/skills/setup-workflow/worktree-lifecycle.md +31 -2
- package/.claude/skills/wrapup/SKILL.md +24 -0
- package/README.md +37 -0
- package/agent-workflow-kit.package.json +35 -19
- package/package.json +1 -1
- package/scripts/board_bootstrap.py +367 -0
- package/scripts/kit-update-pr.mjs +20 -7
- package/scripts/kit-update-pr.test.mjs +29 -0
- package/scripts/profile_globs.py +347 -0
- package/scripts/test_board_bootstrap.py +348 -0
- package/scripts/test_drift_guard_diagnostics.py +295 -0
- package/scripts/test_profile_globs.py +280 -0
- package/scripts/test_skill_setup_workflow_seeds.py +87 -0
- package/scripts/test_worktree_wrapup_contract.py +588 -0
- package/scripts/workflow-advisories/core.py +29 -4
- package/scripts/worktree-lifecycle/README.md +35 -4
- package/scripts/worktree-lifecycle/core.py +211 -60
- package/scripts/wrapup-land.py +179 -34
- package/src/cli.mjs +32 -1
- package/src/commands/update.mjs +30 -21
- package/src/consumer-migrations.json +19 -0
- package/src/lib/bundle.mjs +10 -0
- package/src/lib/consumerMigrations.mjs +161 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { validateConsumerFile } from './consumerPath.mjs';
|
|
3
|
+
import { compareSemver, parseSemver } from './semver.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Required consumer migrations are declarative, versioned data — never prose.
|
|
7
|
+
* A release that forces the consumer to commit a decision registers it here;
|
|
8
|
+
* `update` detects and reports the outstanding action but never performs it.
|
|
9
|
+
*/
|
|
10
|
+
export const CONSUMER_MIGRATION_SCHEMA_VERSION = 1;
|
|
11
|
+
const REGISTRY_URL = new URL('../consumer-migrations.json', import.meta.url);
|
|
12
|
+
const DETECTORS = new Set(['json-key']);
|
|
13
|
+
const TEXT_FIELDS = ['id', 'title', 'workflow', 'decision', 'consequence', 'remediation'];
|
|
14
|
+
|
|
15
|
+
let shipped;
|
|
16
|
+
|
|
17
|
+
function text(value, field, id) {
|
|
18
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
19
|
+
throw new Error(`consumer migration registry: ${field} must be a non-empty string (${id})`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function assertSafeRelativePath(path, id) {
|
|
25
|
+
if (typeof path !== 'string' || !path || path.startsWith('/') || path.includes('\\')
|
|
26
|
+
|| path.split('/').some((segment) => !segment || segment === '.' || segment === '..')) {
|
|
27
|
+
throw new Error(`consumer migration registry: unsafe consumer path ${path} (${id})`);
|
|
28
|
+
}
|
|
29
|
+
return path;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validateDetector(detect, id) {
|
|
33
|
+
if (!detect || typeof detect !== 'object' || Array.isArray(detect)) {
|
|
34
|
+
throw new Error(`consumer migration registry: detect is required (${id})`);
|
|
35
|
+
}
|
|
36
|
+
if (!DETECTORS.has(detect.type)) {
|
|
37
|
+
throw new Error(`consumer migration registry: unsupported detector ${detect.type} (${id})`);
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(detect.key) || !detect.key.length
|
|
40
|
+
|| detect.key.some((segment) => typeof segment !== 'string' || !segment)) {
|
|
41
|
+
throw new Error(`consumer migration registry: detect.key must name the decision (${id})`);
|
|
42
|
+
}
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
type: detect.type,
|
|
45
|
+
path: assertSafeRelativePath(detect.path, id),
|
|
46
|
+
key: Object.freeze([...detect.key]),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Reject a registry that could not be evaluated deterministically or safely. */
|
|
51
|
+
export function validateConsumerMigrationRegistry(registry) {
|
|
52
|
+
if (!registry || typeof registry !== 'object' || Array.isArray(registry)) {
|
|
53
|
+
throw new Error('consumer migration registry: document must be an object');
|
|
54
|
+
}
|
|
55
|
+
if (registry.schemaVersion !== CONSUMER_MIGRATION_SCHEMA_VERSION) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`consumer migration registry: unsupported schemaVersion ${registry.schemaVersion}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (!Array.isArray(registry.migrations)) {
|
|
61
|
+
throw new Error('consumer migration registry: migrations must be an array');
|
|
62
|
+
}
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const migrations = registry.migrations.map((entry) => {
|
|
65
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
66
|
+
throw new Error('consumer migration registry: each migration must be an object');
|
|
67
|
+
}
|
|
68
|
+
const id = text(entry.id, 'id', entry.id);
|
|
69
|
+
if (seen.has(id)) throw new Error(`consumer migration registry: duplicate migration id ${id}`);
|
|
70
|
+
seen.add(id);
|
|
71
|
+
parseSemver(entry.requiredFrom);
|
|
72
|
+
for (const field of TEXT_FIELDS) text(entry[field], field, id);
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
id,
|
|
75
|
+
requiredFrom: entry.requiredFrom,
|
|
76
|
+
title: entry.title,
|
|
77
|
+
workflow: entry.workflow,
|
|
78
|
+
decision: entry.decision,
|
|
79
|
+
consequence: entry.consequence,
|
|
80
|
+
remediation: entry.remediation,
|
|
81
|
+
detect: validateDetector(entry.detect, id),
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
return Object.freeze({
|
|
85
|
+
schemaVersion: registry.schemaVersion,
|
|
86
|
+
migrations: Object.freeze(migrations),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The registry travels with the kit code that evaluates it, not with the consumer. */
|
|
91
|
+
export async function readShippedConsumerMigrationRegistry() {
|
|
92
|
+
shipped ??= validateConsumerMigrationRegistry(JSON.parse(await readFile(REGISTRY_URL, 'utf8')));
|
|
93
|
+
return shipped;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function readConsumerJson(consumerRoot, path) {
|
|
97
|
+
let resolved;
|
|
98
|
+
try {
|
|
99
|
+
resolved = await validateConsumerFile(consumerRoot, path);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error.message.startsWith('unsafe consumer path (not a regular file)')) {
|
|
102
|
+
return { reason: 'missing-file' };
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
return { document: JSON.parse(await readFile(resolved, 'utf8')) };
|
|
108
|
+
} catch {
|
|
109
|
+
return { reason: 'unreadable-file' };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hasDecision(document, key) {
|
|
114
|
+
let node = document;
|
|
115
|
+
for (const segment of key) {
|
|
116
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)
|
|
117
|
+
|| !Object.prototype.hasOwnProperty.call(node, segment)) return false;
|
|
118
|
+
node = node[segment];
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function detect(consumerRoot, migration) {
|
|
124
|
+
const { document, reason } = await readConsumerJson(consumerRoot, migration.detect.path);
|
|
125
|
+
if (reason) return reason;
|
|
126
|
+
return hasDecision(document, migration.detect.key) ? null : 'missing-decision';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Report every registered migration the consumer still owes for `kitVersion`.
|
|
131
|
+
* Read-only by construction: an outstanding decision is named, never written —
|
|
132
|
+
* inventing a cleanup pattern would hand the tool deletion authority.
|
|
133
|
+
*/
|
|
134
|
+
export async function evaluateConsumerMigrations({ consumerRoot, kitVersion, registry }) {
|
|
135
|
+
const source = registry ? validateConsumerMigrationRegistry(registry)
|
|
136
|
+
: await readShippedConsumerMigrationRegistry();
|
|
137
|
+
const pending = [];
|
|
138
|
+
for (const migration of source.migrations) {
|
|
139
|
+
if (compareSemver(kitVersion, migration.requiredFrom) < 0) continue;
|
|
140
|
+
const reason = await detect(consumerRoot, migration);
|
|
141
|
+
if (!reason) continue;
|
|
142
|
+
pending.push({
|
|
143
|
+
id: migration.id,
|
|
144
|
+
state: 'pending',
|
|
145
|
+
reason,
|
|
146
|
+
requiredFrom: migration.requiredFrom,
|
|
147
|
+
title: migration.title,
|
|
148
|
+
workflow: migration.workflow,
|
|
149
|
+
path: migration.detect.path,
|
|
150
|
+
decision: migration.decision,
|
|
151
|
+
consequence: migration.consequence,
|
|
152
|
+
remediation: migration.remediation,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return pending;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** One rendering of the shared record, used by every human-facing update surface. */
|
|
159
|
+
export function renderRequiredMigration({ id, workflow, path, decision }) {
|
|
160
|
+
return `required migration: ${id} · ${workflow} · ${path} · ${decision}`;
|
|
161
|
+
}
|