@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.
@@ -1,5 +1,5 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { access, cp, mkdtemp, readFile, rm, stat, symlink } from 'node:fs/promises';
2
+ import { access, cp, lstat, mkdtemp, readFile, rm, stat, symlink } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join, relative } from 'node:path';
5
5
  import { promisify } from 'node:util';
@@ -12,10 +12,15 @@ import {
12
12
  CONSUMER_MANIFEST_NAME, CONSUMER_ORIGIN, READINESS_MANIFEST_PATH,
13
13
  indexByPath, readManifest, writeManifest,
14
14
  } from './manifest.mjs';
15
- import { checkSkill, evaluateCapability } from '../../scripts/readiness.mjs';
15
+ import { checkSkill, evaluateCapability, inspectProdSections } from '../../scripts/readiness.mjs';
16
16
 
17
17
  const run = promisify(execFile);
18
18
  const exists = (path) => access(path).then(() => true, () => false);
19
+ const pathEntryExists = (path) => lstat(path).then(() => true, (error) => {
20
+ if (error.code === 'ENOENT') return false;
21
+ throw error;
22
+ });
23
+ const MIGRATABLE_INSTRUCTION_PATHS = new Set(['CLAUDE.md', 'AGENTS.md']);
19
24
 
20
25
  /** Copy a verification candidate without duplicating git metadata or dependencies. */
21
26
  export async function stageConsumer(consumerRoot) {
@@ -40,7 +45,11 @@ export async function activateCandidate({
40
45
  }) {
41
46
  const changed = [...preview.added, ...preview.updated];
42
47
  const generated = preview.generated ?? [];
43
- const touched = [...changed, ...generated, ...preview.deleted, CONSUMER_MANIFEST_NAME];
48
+ const migrations = preview.migrations ?? [];
49
+ const touched = [
50
+ ...changed, ...generated, ...migrations.map(({ path }) => path),
51
+ ...preview.deleted, CONSUMER_MANIFEST_NAME,
52
+ ];
44
53
  const currentManifest = await readFile(join(consumerRoot, CONSUMER_MANIFEST_NAME));
45
54
  if (!currentManifest.equals(consumerManifestBefore)) {
46
55
  throw new Error('consumer manifest changed during verification');
@@ -58,6 +67,11 @@ export async function activateCandidate({
58
67
  throw new Error(`generated candidate hash mismatch: ${path}`);
59
68
  }
60
69
  }
70
+ for (const migration of migrations) {
71
+ if (await sha256File(join(candidateRoot, migration.path)) !== migration.afterSha256) {
72
+ throw new Error(`migrated candidate hash mismatch: ${migration.path}`);
73
+ }
74
+ }
61
75
  await assertConsumerStillMatchesPreview(consumerRoot, preview);
62
76
  const rollback = new Map();
63
77
  for (const path of touched) rollback.set(path, await snapshot(join(consumerRoot, path)));
@@ -68,6 +82,9 @@ export async function activateCandidate({
68
82
  for (const path of generated) {
69
83
  await writeAtomic(join(consumerRoot, path), await readFile(join(candidateRoot, path)));
70
84
  }
85
+ for (const { path } of migrations) {
86
+ await writeAtomic(join(consumerRoot, path), await readFile(join(candidateRoot, path)));
87
+ }
71
88
  await afterGenerated();
72
89
  for (const path of preview.deleted) await rm(join(consumerRoot, path), { force: true });
73
90
  await writeAtomic(
@@ -105,6 +122,17 @@ async function assertConsumerStillMatchesPreview(consumerRoot, preview) {
105
122
  throw new Error(`consumer changed during verification: ${path}`);
106
123
  }
107
124
  }
125
+ for (const migration of preview.migrations ?? []) {
126
+ const present = await pathEntryExists(join(consumerRoot, migration.path));
127
+ if (present) await validateConsumerFile(consumerRoot, migration.path);
128
+ else if (!MIGRATABLE_INSTRUCTION_PATHS.has(migration.path)) {
129
+ throw new Error(`unsafe consumer path: ${migration.path}`);
130
+ }
131
+ const current = present ? await sha256File(join(consumerRoot, migration.path)) : null;
132
+ if (current !== migration.beforeSha256) {
133
+ throw new Error(`consumer changed during verification: ${migration.path}`);
134
+ }
135
+ }
108
136
  for (const path of [...preview.updated, ...preview.deleted]) {
109
137
  const prior = installed.get(path);
110
138
  const current = await exists(join(consumerRoot, path))
@@ -144,6 +172,9 @@ export async function adoptReadinessCandidate({ candidateRoot, consumerRoot, pri
144
172
  }
145
173
  await writeManifest(manifestPath, { ...manifest, installed });
146
174
  }
175
+ const { migrations, migrationConflicts } = await migrateProdSections({
176
+ candidateRoot, consumerRoot, nextManifest,
177
+ });
147
178
  const before = await readinessSnapshot(consumerRoot, priorManifest);
148
179
  const after = await readinessSnapshot(candidateRoot, nextManifest);
149
180
  const incompatible = Object.entries(after.skills)
@@ -151,7 +182,50 @@ export async function adoptReadinessCandidate({ candidateRoot, consumerRoot, pri
151
182
  && before.skills[skill] && current.verdict === 'blocked')
152
183
  .map(([skill]) => skill)
153
184
  .sort();
154
- return { generated, availability: readinessDiff(before, after), incompatible };
185
+ return {
186
+ generated,
187
+ migrations,
188
+ migrated: migrations.map(({ path }) => path),
189
+ migrationConflicts,
190
+ availability: readinessDiff(before, after),
191
+ incompatible,
192
+ };
193
+ }
194
+
195
+ async function migrateProdSections({ candidateRoot, consumerRoot, nextManifest }) {
196
+ const paths = [...new Set(Object.values(nextManifest?.readiness?.capabilities ?? {})
197
+ .flatMap(({ evidence }) => evidence?.type === 'prod-section' ? evidence.paths ?? [] : []))];
198
+ if (paths.length < 2) return { migrations: [], migrationConflicts: [] };
199
+ if (paths.some((path) => !MIGRATABLE_INSTRUCTION_PATHS.has(path))) {
200
+ return { migrations: [], migrationConflicts: paths };
201
+ }
202
+ const sections = await inspectProdSections(candidateRoot, paths);
203
+ const invalid = sections.filter(({ state }) => state === 'invalid');
204
+ const validBodies = [...new Set(
205
+ sections.filter(({ state }) => state === 'valid').map(({ body }) => body),
206
+ )];
207
+ if (invalid.length || validBodies.length > 1) {
208
+ return { migrations: [], migrationConflicts: paths };
209
+ }
210
+ if (validBodies.length !== 1) return { migrations: [], migrationConflicts: [] };
211
+
212
+ const body = validBodies[0];
213
+ const migrations = [];
214
+ for (const entry of sections.filter(({ state }) => state === 'missing')) {
215
+ const candidatePath = join(candidateRoot, entry.path);
216
+ const present = await pathEntryExists(candidatePath);
217
+ if (present) await validateConsumerFile(candidateRoot, entry.path);
218
+ const before = present ? await readFile(candidatePath, 'utf8') : '';
219
+ const separator = before && !before.endsWith('\n') ? '\n\n' : (before ? '\n' : '');
220
+ await writeAtomic(candidatePath, `${before}${separator}## Prod\n\n${body}\n`);
221
+ migrations.push({
222
+ path: entry.path,
223
+ beforeSha256: await exists(join(consumerRoot, entry.path))
224
+ ? await sha256File(join(consumerRoot, entry.path)) : null,
225
+ afterSha256: await sha256File(candidatePath),
226
+ });
227
+ }
228
+ return { migrations, migrationConflicts: [] };
155
229
  }
156
230
 
157
231
  function readinessStubPaths(manifest) {