@ikon85/agent-workflow-kit 0.38.0 → 0.40.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.
Files changed (44) hide show
  1. package/.agents/skills/grill-me/SKILL.md +1 -1
  2. package/.agents/skills/grill-with-docs/SKILL.md +1 -1
  3. package/.agents/skills/kit-update/SKILL.md +33 -1
  4. package/.agents/skills/orchestrate-wave/SKILL.md +4 -4
  5. package/.agents/skills/setup-workflow/SKILL.md +84 -3
  6. package/.agents/skills/setup-workflow/board-sync.md +6 -2
  7. package/.agents/skills/setup-workflow/workflow-advisories.md +34 -0
  8. package/.agents/skills/setup-workflow/worktree-lifecycle.md +54 -3
  9. package/.agents/skills/wrapup/SKILL.md +24 -0
  10. package/.claude/hooks/drift-guard.py +212 -21
  11. package/.claude/skills/grill-me/SKILL.md +1 -1
  12. package/.claude/skills/grill-with-docs/SKILL.md +1 -1
  13. package/.claude/skills/kit-update/SKILL.md +33 -1
  14. package/.claude/skills/orchestrate-wave/SKILL.md +4 -4
  15. package/.claude/skills/setup-workflow/SKILL.md +84 -3
  16. package/.claude/skills/setup-workflow/board-sync.md +6 -2
  17. package/.claude/skills/setup-workflow/workflow-advisories.md +34 -0
  18. package/.claude/skills/setup-workflow/worktree-lifecycle.md +54 -3
  19. package/.claude/skills/wrapup/SKILL.md +24 -0
  20. package/README.md +62 -0
  21. package/agent-workflow-kit.package.json +57 -25
  22. package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +84 -0
  23. package/package.json +1 -1
  24. package/scripts/board_bootstrap.py +367 -0
  25. package/scripts/kit-update-pr.mjs +20 -7
  26. package/scripts/kit-update-pr.test.mjs +29 -0
  27. package/scripts/profile_globs.py +347 -0
  28. package/scripts/test_board_bootstrap.py +348 -0
  29. package/scripts/test_drift_guard_diagnostics.py +295 -0
  30. package/scripts/test_profile_globs.py +280 -0
  31. package/scripts/test_skill_setup_workflow_seeds.py +87 -0
  32. package/scripts/test_worktree_ignore_seed.py +320 -0
  33. package/scripts/test_worktree_wrapup_contract.py +588 -0
  34. package/scripts/workflow-advisories/core.py +29 -4
  35. package/scripts/worktree-lifecycle/README.md +53 -4
  36. package/scripts/worktree-lifecycle/core.py +211 -60
  37. package/scripts/worktree-lifecycle/ignore_seed.py +226 -0
  38. package/scripts/worktree-lifecycle/plan-artifacts.json +37 -0
  39. package/scripts/wrapup-land.py +179 -34
  40. package/src/cli.mjs +32 -1
  41. package/src/commands/update.mjs +30 -21
  42. package/src/consumer-migrations.json +19 -0
  43. package/src/lib/bundle.mjs +16 -0
  44. package/src/lib/consumerMigrations.mjs +161 -0
@@ -14,6 +14,7 @@ import {
14
14
  import {
15
15
  inspectRoutingProfile, reconcileRoutingProfile,
16
16
  } from '../lib/routingProfile.mjs';
17
+ import { evaluateConsumerMigrations } from '../lib/consumerMigrations.mjs';
17
18
 
18
19
  const RELEASE_NAME = '@ikon85/agent-workflow-kit';
19
20
 
@@ -75,7 +76,14 @@ async function updatePackage(options) {
75
76
  if (!decisions.has(key)) decisions.set(key, await decide(action, path));
76
77
  return decisions.get(key);
77
78
  };
79
+ // Required consumer migrations are decisions the consumer still owes; `update`
80
+ // never writes the consumer's project layer, so the pre-apply reading is also
81
+ // the post-apply state. Detected once, rendered by every surface.
82
+ const requiredMigrations = await evaluateConsumerMigrations({
83
+ consumerRoot, kitVersion: pkg.kitVersion,
84
+ });
78
85
  const preview = await reconcile({ kitRoot, consumerRoot, decide: choosePreview, dryRun: true });
86
+ preview.requiredMigrations = requiredMigrations;
79
87
  let previewFailure;
80
88
  try {
81
89
  Object.assign(preview, await previewReadinessAdoption({
@@ -92,7 +100,7 @@ async function updatePackage(options) {
92
100
  failure: { phase: 'staging', consumerState: 'unchanged' },
93
101
  };
94
102
  }
95
- if (dryRun) return { ...preview, state: 'preview', history };
103
+ if (dryRun) return { ...preview, state: 'preview', history, report: reportOf(preview) };
96
104
  if (preview.migrationConflicts?.length) {
97
105
  return terminal(preview, 'conflicted', history, transition);
98
106
  }
@@ -100,6 +108,7 @@ async function updatePackage(options) {
100
108
  const resolvedPreview = await resolvePreview({
101
109
  kitRoot, consumerRoot, preview, decisions, decide, transition,
102
110
  });
111
+ resolvedPreview.requiredMigrations = requiredMigrations;
103
112
  if (resolvedPreview.collisions.length) {
104
113
  return terminal(resolvedPreview, 'conflicted', history, transition);
105
114
  }
@@ -266,28 +275,28 @@ function hasUpstreamDelta(result) {
266
275
 
267
276
  async function terminal(result, state, history, transition) {
268
277
  await transition(state);
278
+ return { ...result, state, history, report: reportOf(result) };
279
+ }
280
+
281
+ function reportOf(result) {
269
282
  return {
270
- ...result,
271
- state,
272
- history,
273
- report: {
274
- unchanged: result.unchanged.length,
275
- added: result.added.length,
276
- updated: result.updated.length,
277
- deleted: result.deleted.length,
278
- localModified: result.userModified.length,
279
- conflicts: result.conflicts.length,
280
- keptDeleted: result.keptDeleted.length,
281
- paths: {
282
- added: result.added,
283
- updated: result.updated,
284
- deleted: result.deleted,
285
- localModified: result.userModified,
286
- conflicts: result.conflicts.map(({ path }) => path),
287
- keptDeleted: result.keptDeleted,
288
- },
289
- recommendation: updateRecommendation(result),
283
+ unchanged: result.unchanged.length,
284
+ added: result.added.length,
285
+ updated: result.updated.length,
286
+ deleted: result.deleted.length,
287
+ localModified: result.userModified.length,
288
+ conflicts: result.conflicts.length,
289
+ keptDeleted: result.keptDeleted.length,
290
+ paths: {
291
+ added: result.added,
292
+ updated: result.updated,
293
+ deleted: result.deleted,
294
+ localModified: result.userModified,
295
+ conflicts: result.conflicts.map(({ path }) => path),
296
+ keptDeleted: result.keptDeleted,
290
297
  },
298
+ recommendation: updateRecommendation(result),
299
+ requiredMigrations: result.requiredMigrations ?? [],
291
300
  };
292
301
  }
293
302
 
@@ -0,0 +1,19 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "migrations": [
4
+ {
5
+ "id": "wrapup-landing-artifact-policy",
6
+ "requiredFrom": "0.38.0",
7
+ "title": "Explicit landing-artifact policy",
8
+ "workflow": "setup-workflow",
9
+ "decision": "wrapup.landingGeneratedArtifactPatterns",
10
+ "detect": {
11
+ "type": "json-key",
12
+ "path": "docs/agents/workflow-capabilities.json",
13
+ "key": ["wrapup", "landingGeneratedArtifactPatterns"]
14
+ },
15
+ "consequence": "wrapup-land fails closed before landing until the decision is committed.",
16
+ "remediation": "Run setup-workflow, review the wrapup.landingGeneratedArtifactPatterns decision it writes into docs/agents/workflow-capabilities.json, and commit that profile change. kit-update never chooses cleanup patterns for you."
17
+ }
18
+ ]
19
+ }
@@ -16,6 +16,10 @@ export const HELPER_FILES = [
16
16
  // every board-specific value from docs/agents/board-sync.md through it, so it
17
17
  // MUST ship or they are broken-on-arrival. Library (imported, not run) → 0o644.
18
18
  { path: 'scripts/board_config.py', kind: 'script', mode: 0o644 },
19
+ // Offered board provisioning for /setup-workflow Section D: creates the
20
+ // project + Status/workflow fields, reads the opaque ids back, and writes the
21
+ // filled profile. Imports board_config (above). Invokable CLI → 0o755.
22
+ { path: 'scripts/board_bootstrap.py', kind: 'script', mode: 0o755 },
19
23
  // Pure Slices-table logic imported by board-sync.py for `anchor-sync` —
20
24
  // library (imported, not run) → 0o644. MUST ship or board-sync.py ImportErrors.
21
25
  { path: 'scripts/anchor_table.py', kind: 'script', mode: 0o644 },
@@ -127,6 +131,12 @@ export const HELPER_FILES = [
127
131
  { path: 'scripts/memory-lifecycle/setup.mjs', kind: 'script', mode: 0o644 },
128
132
  { path: 'assets/memory-templates/meta_decision_layer_choice.md', kind: 'template', mode: 0o644 },
129
133
  { path: 'assets/memory-templates/meta_memory_lifecycle.md', kind: 'template', mode: 0o644 },
134
+ // The one repository-relative glob dialect. Both capability cores below load
135
+ // it, so a consumer profile pattern selects the same paths for an advisory
136
+ // and for a cleanup decision. Also the reviewable migration check consumers
137
+ // run after setup or update, so a narrowed or widened pattern is named
138
+ // instead of silently changing deletion authority. Library + CLI → 0o755.
139
+ { path: 'scripts/profile_globs.py', kind: 'script', mode: 0o755 },
130
140
  // Profile-driven Worktree Lifecycle foundation. The setup adapter imports
131
141
  // core.py, while capabilities.json keeps the historical 8/8 denominator
132
142
  // explicit until the remaining hook and cleanup adapters are activated.
@@ -137,6 +147,12 @@ export const HELPER_FILES = [
137
147
  { path: 'scripts/worktree-lifecycle/session.py', kind: 'script', mode: 0o755 },
138
148
  { path: 'scripts/worktree-lifecycle/capabilities.json', kind: 'doc', mode: 0o644 },
139
149
  { path: 'scripts/worktree-lifecycle/README.md', kind: 'doc', mode: 0o644 },
150
+ // Offered planning-artifact ignore rules for /setup-workflow Section A11.
151
+ // `.gitignore` is consumer-owned, so only an approved setup step may append
152
+ // the block — init/update reconciliation never reach this. plan-artifacts.json
153
+ // is the kit-owned declaration it reads. Invokable CLI → 0o755.
154
+ { path: 'scripts/worktree-lifecycle/plan-artifacts.json', kind: 'doc', mode: 0o644 },
155
+ { path: 'scripts/worktree-lifecycle/ignore_seed.py', kind: 'script', mode: 0o755 },
140
156
  // Shared hook utility imported by the shipped hooks (drift-guard,
141
157
  // sync-board-status). Library (imported, not run) → 0o644. MUST ship or those
142
158
  // hooks ImportError on arrival.
@@ -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
+ }