@lesliechan721/aw 0.1.0-beta.14 → 0.1.0-beta.16

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 (49) hide show
  1. package/dist/src/commands/aw-capabilities.d.ts +1 -1
  2. package/dist/src/commands/aw-capabilities.js +1 -0
  3. package/dist/src/commands/aw-capabilities.js.map +1 -1
  4. package/dist/src/commands/review-inspection.d.ts +169 -0
  5. package/dist/src/commands/review-inspection.js +916 -0
  6. package/dist/src/commands/review-inspection.js.map +1 -0
  7. package/dist/src/commands/skill-eval.d.ts +5 -0
  8. package/dist/src/commands/skill-eval.js +86 -63
  9. package/dist/src/commands/skill-eval.js.map +1 -1
  10. package/dist/src/commands/status-v6.d.ts +2 -1
  11. package/dist/src/commands/status-v6.js +7 -1
  12. package/dist/src/commands/status-v6.js.map +1 -1
  13. package/dist/src/eval/manifest-v1.js +0 -5
  14. package/dist/src/eval/manifest-v1.js.map +1 -1
  15. package/dist/src/index.js +39 -6
  16. package/dist/src/index.js.map +1 -1
  17. package/dist/src/portable/commands.d.ts +1 -0
  18. package/dist/src/portable/commands.js +43 -1
  19. package/dist/src/portable/commands.js.map +1 -1
  20. package/dist/src/portable/materializer.d.ts +12 -0
  21. package/dist/src/portable/materializer.js +67 -41
  22. package/dist/src/portable/materializer.js.map +1 -1
  23. package/dist/src/review/eval-source-input.d.ts +43 -0
  24. package/dist/src/review/eval-source-input.js +207 -0
  25. package/dist/src/review/eval-source-input.js.map +1 -0
  26. package/dist/src/review/source-binding.d.ts +170 -0
  27. package/dist/src/review/source-binding.js +622 -0
  28. package/dist/src/review/source-binding.js.map +1 -0
  29. package/dist/src/tui/console-app-v4.js +523 -133
  30. package/dist/src/tui/console-app-v4.js.map +1 -1
  31. package/dist/src/tui/controller-runtime.js +59 -37
  32. package/dist/src/tui/controller-runtime.js.map +1 -1
  33. package/dist/src/tui/controller.d.ts +15 -1
  34. package/dist/src/tui/controller.js +86 -3
  35. package/dist/src/tui/controller.js.map +1 -1
  36. package/dist/src/tui/model.js +2 -3
  37. package/dist/src/tui/model.js.map +1 -1
  38. package/dist/src/tui/operation-review-planner.js +22 -1
  39. package/dist/src/tui/operation-review-planner.js.map +1 -1
  40. package/dist/src/tui/plugin-write-task.d.ts +2 -1
  41. package/dist/src/tui/plugin-write-task.js +17 -0
  42. package/dist/src/tui/plugin-write-task.js.map +1 -1
  43. package/dist/src/tui/router.d.ts +1 -0
  44. package/dist/src/tui/router.js +13 -5
  45. package/dist/src/tui/router.js.map +1 -1
  46. package/dist/src/tui/screen-registry.d.ts +21 -1
  47. package/dist/src/tui/screen-registry.js +5 -1
  48. package/dist/src/tui/screen-registry.js.map +1 -1
  49. package/package.json +3 -3
@@ -0,0 +1,916 @@
1
+ import path from 'node:path';
2
+ import { computeCaseCorpusDigest, computeSkillDigest, } from '@lesliechan721/agent-skill-eval';
3
+ import { calculateCatalogWorkspaceSourceIdentity, DEFAULT_CATALOG_OUTPUT_PATH, GENERATED_OUTPUT_MARKER_FILENAME, readCatalogWorkspace, resolveSupportedPlatforms, validateCatalogSnapshotComplete, } from '@lesliechan721/aw-plugin-core';
4
+ import fs from 'fs-extra';
5
+ import { readAwSkillEvalManifest, } from '../eval/manifest-v1.js';
6
+ import { CLI_RESERVED_IMPORTS } from '../utils/manifest.js';
7
+ import { resolveCatalogWorkspaceRoot, } from './catalog-workspace-resolution.js';
8
+ import { computeReviewComponentDigest } from './review-publication.js';
9
+ import { computeAwSourceBindingSelectedCasesDigest, computeAwSourceBindingSuiteDefaultsDigest, readAwSourceBinding, resolveEvalProfileBindingInput, selectAwSourceBindingEvalCases, validateAwSourceBindingForWorkspace, } from '../review/source-binding.js';
10
+ const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
11
+ function stripDotSlash(value) {
12
+ return value.startsWith('./') ? value.slice(2) : value;
13
+ }
14
+ function relativeWorkspacePath(workspaceRoot, filePath) {
15
+ return path.relative(workspaceRoot, filePath).split(path.sep).join('/') || '.';
16
+ }
17
+ function sourceKey(plugin, skill, kind) {
18
+ return `source:${plugin}:${skill}:${kind}`;
19
+ }
20
+ function requiredFinalKey(seed) {
21
+ return [
22
+ 'final',
23
+ seed.plugin.manifest.name,
24
+ seed.skill,
25
+ seed.profile,
26
+ seed.targetId ?? 'static',
27
+ ].join(':');
28
+ }
29
+ function materializationIdentityKey(options) {
30
+ return [
31
+ options.plugin,
32
+ options.skill,
33
+ options.profile,
34
+ options.materializationProfile.kind,
35
+ options.materializationProfile.id,
36
+ options.materializationProfile.inputDigest,
37
+ ].join(':');
38
+ }
39
+ function scopeFromProfile(profile) {
40
+ return profile.endsWith('-project') ? 'project' : 'user';
41
+ }
42
+ async function directSkillDirectories(root) {
43
+ const entries = await fs.readdir(root, { withFileTypes: true });
44
+ const result = [];
45
+ for (const entry of entries.sort((left, right) => compareText(left.name, right.name))) {
46
+ const entryPath = path.join(root, entry.name);
47
+ const stat = await fs.lstat(entryPath);
48
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
49
+ throw new Error(`Skill collection direct child must be a regular directory: ${entryPath}`);
50
+ }
51
+ result.push({ name: entry.name, root: entryPath });
52
+ }
53
+ return result;
54
+ }
55
+ function pluginImports(plugin) {
56
+ return [
57
+ ...(plugin.manifest.buildConfig?.buildImports ?? []).map((entry) => ({
58
+ purpose: 'build',
59
+ provider: entry.plugin,
60
+ export: entry.export,
61
+ consumer: entry.env,
62
+ })),
63
+ ...(plugin.manifest.buildConfig?.skillRuntimeFiles ?? []).flatMap((entry) => entry.import ? [{
64
+ purpose: 'skill-runtime-file',
65
+ provider: entry.import.plugin,
66
+ export: entry.import.export,
67
+ consumer: `${entry.skill}/${entry.target}`,
68
+ }] : []),
69
+ ...(plugin.manifest.artifacts?.targets ?? []).flatMap((target) => (target.runtimeImports ?? []).map((entry) => ({
70
+ purpose: 'artifact-runtime',
71
+ provider: entry.plugin,
72
+ export: entry.export,
73
+ consumer: `${target.id}/${entry.name}`,
74
+ }))),
75
+ ].sort((left, right) => compareText(`${left.purpose}\0${left.consumer}\0${left.provider}\0${left.export}`, `${right.purpose}\0${right.consumer}\0${right.provider}\0${right.export}`));
76
+ }
77
+ async function inspectSnapshot(workspace, workspaceSourceIdentity) {
78
+ const snapshotPath = path.join(workspace.workspaceRoot, DEFAULT_CATALOG_OUTPUT_PATH);
79
+ if (!await fs.pathExists(snapshotPath)) {
80
+ return {
81
+ receipt: {
82
+ path: snapshotPath,
83
+ state: 'absent',
84
+ catalogVersion: null,
85
+ snapshotDigest: null,
86
+ workspaceIdentity: null,
87
+ sourceIdentity: null,
88
+ },
89
+ entries: new Map(),
90
+ root: null,
91
+ };
92
+ }
93
+ try {
94
+ const [validation, marker] = await Promise.all([
95
+ validateCatalogSnapshotComplete(snapshotPath),
96
+ fs.readJson(path.join(snapshotPath, GENERATED_OUTPUT_MARKER_FILENAME)),
97
+ ]);
98
+ const current = marker.workspaceIdentity === workspace.workspaceIdentity
99
+ && marker.sourceIdentity === workspaceSourceIdentity;
100
+ return {
101
+ receipt: {
102
+ path: snapshotPath,
103
+ state: current ? 'current' : 'stale',
104
+ catalogVersion: validation.manifest.catalogVersion,
105
+ snapshotDigest: validation.snapshotDigest,
106
+ workspaceIdentity: marker.workspaceIdentity,
107
+ sourceIdentity: marker.sourceIdentity,
108
+ },
109
+ entries: new Map(validation.manifest.entries.map((entry) => [entry.name, entry])),
110
+ root: current ? validation.rootDir : null,
111
+ };
112
+ }
113
+ catch {
114
+ return {
115
+ receipt: {
116
+ path: snapshotPath,
117
+ state: 'invalid',
118
+ catalogVersion: null,
119
+ snapshotDigest: null,
120
+ workspaceIdentity: null,
121
+ sourceIdentity: null,
122
+ },
123
+ entries: new Map(),
124
+ root: null,
125
+ };
126
+ }
127
+ }
128
+ async function addSourceSubject(options) {
129
+ const key = sourceKey(options.plugin.manifest.name, options.skill, options.kind);
130
+ if (options.subjects.some((subject) => subject.key === key))
131
+ return key;
132
+ const digest = options.kind === 'artifact-source'
133
+ ? (await computeReviewComponentDigest({
134
+ repoRoot: options.workspace.workspaceRoot,
135
+ path: options.root,
136
+ })).digest
137
+ : await computeSkillDigest(options.root);
138
+ options.subjects.push({
139
+ key,
140
+ kind: options.kind,
141
+ plugin: options.plugin.manifest.name,
142
+ skill: options.skill,
143
+ path: options.root,
144
+ profile: null,
145
+ digest,
146
+ origin: 'workspace',
147
+ });
148
+ return key;
149
+ }
150
+ async function inventoryPluginSource(options) {
151
+ const { workspace, plugin, subjects } = options;
152
+ const nativeSkills = plugin.manifest.skills
153
+ ? await directSkillDirectories(path.join(plugin.rootDir, stripDotSlash(plugin.manifest.skills)))
154
+ : [];
155
+ const portableSkills = plugin.manifest.distributions?.agentSkills?.skills
156
+ ? await directSkillDirectories(path.join(plugin.rootDir, stripDotSlash(plugin.manifest.distributions.agentSkills.skills)))
157
+ : [];
158
+ const nativeByName = new Map(nativeSkills.map((entry) => [entry.name, entry]));
159
+ const portableByName = new Map(portableSkills.map((entry) => [entry.name, entry]));
160
+ const nativeDynamicByPlatform = new Map([
161
+ ['claude', new Set()],
162
+ ['codex', new Set()],
163
+ ]);
164
+ const portableDynamic = new Set();
165
+ const sourceSubjectKeys = [];
166
+ const seeds = [];
167
+ for (const target of plugin.manifest.artifacts?.targets ?? []) {
168
+ if (!['codex.skill', 'claude.skill', 'agent-skills.skill'].includes(target.target))
169
+ continue;
170
+ const skill = target.skillKey;
171
+ if (target.target === 'agent-skills.skill')
172
+ portableDynamic.add(skill);
173
+ else
174
+ nativeDynamicByPlatform.get(target.target === 'codex.skill' ? 'codex' : 'claude').add(skill);
175
+ }
176
+ for (const skill of nativeSkills) {
177
+ sourceSubjectKeys.push(await addSourceSubject({
178
+ workspace,
179
+ subjects,
180
+ plugin,
181
+ skill: skill.name,
182
+ root: skill.root,
183
+ kind: 'source-skill',
184
+ }));
185
+ }
186
+ for (const skill of portableSkills) {
187
+ sourceSubjectKeys.push(await addSourceSubject({
188
+ workspace,
189
+ subjects,
190
+ plugin,
191
+ skill: skill.name,
192
+ root: skill.root,
193
+ kind: 'portable-source-skill',
194
+ }));
195
+ }
196
+ for (const platform of [...resolveSupportedPlatforms(plugin.manifest)].sort(compareText)) {
197
+ for (const skill of nativeSkills) {
198
+ if (nativeDynamicByPlatform.get(platform).has(skill.name))
199
+ continue;
200
+ for (const scope of ['project', 'user']) {
201
+ seeds.push({
202
+ plugin,
203
+ skill: skill.name,
204
+ profile: `${platform}-${scope}`,
205
+ targetId: null,
206
+ sourceSubjectKey: sourceKey(plugin.manifest.name, skill.name, 'source-skill'),
207
+ dynamic: false,
208
+ });
209
+ }
210
+ }
211
+ }
212
+ if (plugin.manifest.distributions?.agentSkills) {
213
+ for (const skill of portableSkills) {
214
+ if (portableDynamic.has(skill.name))
215
+ continue;
216
+ for (const scope of ['project', 'user']) {
217
+ seeds.push({
218
+ plugin,
219
+ skill: skill.name,
220
+ profile: `agent-skills-${scope}`,
221
+ targetId: null,
222
+ sourceSubjectKey: sourceKey(plugin.manifest.name, skill.name, 'portable-source-skill'),
223
+ dynamic: false,
224
+ });
225
+ }
226
+ }
227
+ }
228
+ for (const target of plugin.manifest.artifacts?.targets ?? []) {
229
+ if (!['codex.skill', 'claude.skill', 'agent-skills.skill'].includes(target.target))
230
+ continue;
231
+ const skill = target.skillKey;
232
+ const platform = target.target === 'agent-skills.skill'
233
+ ? 'agent-skills'
234
+ : target.target === 'codex.skill' ? 'codex' : 'claude';
235
+ const sourceEntry = target.target === 'agent-skills.skill'
236
+ ? portableByName.get(skill) ?? nativeByName.get(skill)
237
+ : nativeByName.get(skill);
238
+ let sourceSubjectKey = target.target === 'agent-skills.skill' && portableByName.has(skill)
239
+ ? sourceKey(plugin.manifest.name, skill, 'portable-source-skill')
240
+ : sourceKey(plugin.manifest.name, skill, 'source-skill');
241
+ if (!sourceEntry) {
242
+ const declaredSource = target.sourceDirs?.[0];
243
+ const sourceRoot = declaredSource
244
+ ? path.join(plugin.rootDir, stripDotSlash(declaredSource))
245
+ : path.join(plugin.rootDir, stripDotSlash(plugin.manifest.scripts?.artifacts?.render?.path ?? '.agents-plugin'));
246
+ sourceSubjectKey = await addSourceSubject({
247
+ workspace,
248
+ subjects,
249
+ plugin,
250
+ skill,
251
+ root: sourceRoot,
252
+ kind: 'artifact-source',
253
+ });
254
+ sourceSubjectKeys.push(sourceSubjectKey);
255
+ }
256
+ for (const scope of target.scopes) {
257
+ seeds.push({
258
+ plugin,
259
+ skill,
260
+ profile: `${platform}-${scope}`,
261
+ targetId: target.id,
262
+ sourceSubjectKey,
263
+ dynamic: true,
264
+ });
265
+ }
266
+ }
267
+ return {
268
+ seeds: seeds.sort((left, right) => compareText(requiredFinalKey(left), requiredFinalKey(right))),
269
+ sourceSubjectKeys: [...new Set(sourceSubjectKeys)].sort(compareText),
270
+ };
271
+ }
272
+ function releaseCases(suite, document, runs) {
273
+ const splits = new Set(suite.releaseSplits ?? []);
274
+ return document.cases
275
+ .filter((entry) => splits.size === 0 || splits.has('all') || splits.has(entry.split))
276
+ .map((entry) => ({ id: entry.id, split: entry.split, minimumRuns: runs }))
277
+ .sort((left, right) => compareText(left.id, right.id));
278
+ }
279
+ async function inventoryRequiredEvals(options) {
280
+ const required = [];
281
+ const caseDigests = new Map();
282
+ const bySkill = new Map();
283
+ for (const seed of options.seeds) {
284
+ const key = `${seed.plugin.manifest.name}\0${seed.skill}`;
285
+ bySkill.set(key, [...(bySkill.get(key) ?? []), seed]);
286
+ }
287
+ for (const seeds of bySkill.values()) {
288
+ const first = seeds[0];
289
+ const manifestPath = path.join(first.plugin.rootDir, 'skills', first.skill, 'evals', 'manifest.json');
290
+ if (!await fs.pathExists(manifestPath))
291
+ continue;
292
+ let loaded;
293
+ try {
294
+ loaded = await readAwSkillEvalManifest({ plugin: first.plugin, skill: first.skill });
295
+ }
296
+ catch (error) {
297
+ options.issues.push({
298
+ category: 'invalid',
299
+ code: 'evalManifestInvalid',
300
+ message: error instanceof Error ? error.message : String(error),
301
+ blocking: true,
302
+ path: relativeWorkspacePath(options.workspace.workspaceRoot, manifestPath),
303
+ });
304
+ continue;
305
+ }
306
+ const requiredSuites = loaded.manifest.suites.filter((suite) => suite.requiredForRelease === true);
307
+ const profileBindings = new Map();
308
+ const invalidProfileIds = new Set();
309
+ const codexSeeds = seeds.filter((seed) => seed.profile.startsWith('codex-'));
310
+ for (const suite of requiredSuites) {
311
+ const accepted = suite.profiles ? new Set(suite.profiles) : null;
312
+ const suiteProfiles = loaded.manifest.profiles.filter((profile) => !accepted || accepted.has(profile.id));
313
+ for (const profile of suiteProfiles) {
314
+ if (!profileBindings.has(profile.id) && !invalidProfileIds.has(profile.id)) {
315
+ try {
316
+ profileBindings.set(profile.id, resolveEvalProfileBindingInput(profile, options.workspace.plugins));
317
+ }
318
+ catch (error) {
319
+ invalidProfileIds.add(profile.id);
320
+ options.issues.push({
321
+ category: 'invalid',
322
+ code: 'evalProfileInvalid',
323
+ message: error instanceof Error ? error.message : String(error),
324
+ blocking: true,
325
+ path: relativeWorkspacePath(options.workspace.workspaceRoot, manifestPath),
326
+ });
327
+ }
328
+ }
329
+ const profileBinding = profileBindings.get(profile.id);
330
+ if (!profileBinding)
331
+ continue;
332
+ const matchingSeeds = codexSeeds.filter((seed) => scopeFromProfile(seed.profile) === profile.scope);
333
+ if (matchingSeeds.length === 0) {
334
+ options.issues.push({
335
+ category: 'unsupported',
336
+ code: 'requiredEvalTargetUnsupported',
337
+ message: `Required Eval ${first.plugin.manifest.name}/${first.skill}/${suite.id}/${profile.id} has no declared Codex ${profile.scope} final.`,
338
+ blocking: true,
339
+ path: relativeWorkspacePath(options.workspace.workspaceRoot, manifestPath),
340
+ });
341
+ continue;
342
+ }
343
+ const document = loaded.casesBySuite.get(suite.id);
344
+ const casesPath = path.join(loaded.evalRoot, suite.cases);
345
+ const runs = suite.runs ?? loaded.manifest.defaults.runs ?? 1;
346
+ const timeoutMs = suite.timeoutMs ?? loaded.manifest.defaults.timeoutMs ?? null;
347
+ let casesDigest = caseDigests.get(casesPath);
348
+ if (!casesDigest) {
349
+ casesDigest = await computeCaseCorpusDigest([casesPath]);
350
+ caseDigests.set(casesPath, casesDigest);
351
+ }
352
+ const minimumPassRate = typeof document.suite.runtime?.minimumPassRate === 'number'
353
+ ? document.suite.runtime.minimumPassRate
354
+ : 1;
355
+ const selected = releaseCases(suite, document, runs);
356
+ const selectedDocument = selected.length === 0
357
+ ? { ...document, cases: [] }
358
+ : selectAwSourceBindingEvalCases(document, {
359
+ caseIds: selected.map((entry) => entry.id),
360
+ });
361
+ for (const seed of matchingSeeds) {
362
+ const finalKey = requiredFinalKey(seed);
363
+ required.push({
364
+ key: `eval:${finalKey}:${profile.id}:${suite.id}`,
365
+ plugin: first.plugin.manifest.name,
366
+ skill: first.skill,
367
+ finalProfile: seed.profile,
368
+ requiredFinalKey: finalKey,
369
+ evalProfile: {
370
+ id: profile.id,
371
+ scope: profile.scope,
372
+ inputDigest: profileBinding.profile.inputDigest,
373
+ plugins: profileBinding.materializationProfile.plugins
374
+ .map((entry) => entry.name)
375
+ .sort(compareText),
376
+ },
377
+ suite: {
378
+ id: suite.id,
379
+ kind: suite.kind,
380
+ provider: suite.provider ?? null,
381
+ suiteDefaultsDigest: computeAwSourceBindingSuiteDefaultsDigest(suite, loaded.manifest.defaults),
382
+ },
383
+ cases: {
384
+ sourcePath: relativeWorkspacePath(options.workspace.workspaceRoot, casesPath),
385
+ sourceCorpusDigest: casesDigest,
386
+ selectedCasesDigest: computeAwSourceBindingSelectedCasesDigest(selectedDocument),
387
+ selected,
388
+ },
389
+ runs,
390
+ timeoutMs,
391
+ minimumPassRate,
392
+ processManagement: suite.releaseProcessManagement ?? 'managed-only',
393
+ materializedFinalKeys: [],
394
+ verifiedBindingPaths: [],
395
+ });
396
+ }
397
+ }
398
+ }
399
+ }
400
+ return required.sort((left, right) => compareText(left.key, right.key));
401
+ }
402
+ async function resolveTarget(options) {
403
+ if (options.target === 'catalog') {
404
+ return {
405
+ kind: 'catalog',
406
+ id: options.workspace.workspaceIdentity,
407
+ path: options.workspace.workspaceRoot,
408
+ plugins: options.workspace.plugins,
409
+ };
410
+ }
411
+ const named = options.workspace.plugins.find((plugin) => plugin.manifest.name === options.target);
412
+ if (named) {
413
+ return { kind: 'plugin', id: named.manifest.name, path: named.rootDir, plugins: [named] };
414
+ }
415
+ const candidate = path.resolve(options.cwd, options.target);
416
+ const stat = await fs.lstat(candidate).catch(() => null);
417
+ if (!stat?.isDirectory() || stat.isSymbolicLink()) {
418
+ throw new Error('Review inspection target must be a canonical Plugin or Catalog Workspace directory.');
419
+ }
420
+ const real = await fs.realpath(candidate);
421
+ if (real === options.workspace.workspaceRoot) {
422
+ return {
423
+ kind: 'catalog',
424
+ id: options.workspace.workspaceIdentity,
425
+ path: real,
426
+ plugins: options.workspace.plugins,
427
+ };
428
+ }
429
+ const plugin = options.workspace.plugins.find((entry) => entry.rootDir === real);
430
+ if (!plugin) {
431
+ throw new Error('Review inspection target is not a canonical Plugin in the selected Catalog Workspace.');
432
+ }
433
+ return { kind: 'plugin', id: plugin.manifest.name, path: real, plugins: [plugin] };
434
+ }
435
+ async function inspectMaterializations(options) {
436
+ const materializations = [];
437
+ const finals = new Map();
438
+ const targetPluginNames = new Set(options.targetPlugins.map((plugin) => plugin.manifest.name));
439
+ const context = {
440
+ workspace: options.workspace,
441
+ source: {
442
+ kind: 'workspace',
443
+ workspaceIdentity: options.workspace.workspaceIdentity,
444
+ sourceIdentity: options.workspaceSourceIdentity,
445
+ },
446
+ };
447
+ for (const bindingPath of options.paths) {
448
+ let parsed = null;
449
+ try {
450
+ parsed = await readAwSourceBinding(bindingPath);
451
+ }
452
+ catch {
453
+ // The validator returns the public typed state and safe issue below.
454
+ }
455
+ const expectedSubject = parsed?.subject ?? {
456
+ plugin: options.targetPlugins[0]?.manifest.name ?? 'unknown',
457
+ };
458
+ const validation = await validateAwSourceBindingForWorkspace({
459
+ bindingPath,
460
+ context,
461
+ expectedSubject,
462
+ cwd: options.workspace.workspaceRoot,
463
+ env: options.env,
464
+ });
465
+ if (validation.state !== 'verified') {
466
+ materializations.push({
467
+ path: bindingPath,
468
+ state: validation.state,
469
+ plugin: validation.binding?.subject.plugin ?? null,
470
+ skill: validation.binding?.subject.skill ?? null,
471
+ profile: validation.binding
472
+ ? validation.binding.profile.kind === 'eval'
473
+ ? `codex-${validation.binding.profile.scope}`
474
+ : `agent-skills-${validation.binding.profile.scope}`
475
+ : null,
476
+ finalKeys: [],
477
+ verifiedBinding: null,
478
+ issues: validation.issues.map((issue) => issue.message),
479
+ });
480
+ continue;
481
+ }
482
+ const profile = validation.binding.profile.kind === 'eval'
483
+ ? `codex-${validation.binding.profile.scope}`
484
+ : `agent-skills-${validation.binding.profile.scope}`;
485
+ const finalKeys = [];
486
+ for (const output of validation.outputs) {
487
+ if (output.kind !== 'eval-target' && output.kind !== 'portable-skill')
488
+ continue;
489
+ const outputPlugin = output.kind === 'portable-skill'
490
+ ? output.plugin
491
+ : validation.binding.subject.plugin;
492
+ if (!targetPluginNames.has(outputPlugin))
493
+ continue;
494
+ const belongsToSelectedPlugin = options.requiredFinals.some((required) => required.plugin === outputPlugin
495
+ && required.skill === output.skill
496
+ && required.profile === profile);
497
+ if (!belongsToSelectedPlugin) {
498
+ options.issues.push({
499
+ category: 'unknown',
500
+ code: 'materializedOutputOwnerUnknown',
501
+ message: `The verified source binding does not contain a selected leaf for ${outputPlugin}/${output.skill}/${profile}.`,
502
+ blocking: false,
503
+ path: output.path,
504
+ });
505
+ continue;
506
+ }
507
+ const identityKey = materializationIdentityKey({
508
+ plugin: outputPlugin,
509
+ skill: output.skill,
510
+ profile,
511
+ materializationProfile: validation.binding.profile,
512
+ });
513
+ const key = [
514
+ 'materialized',
515
+ identityKey,
516
+ output.digest,
517
+ ].join(':');
518
+ const existingSubject = options.subjects.find((subject) => subject.key === key);
519
+ if (existingSubject) {
520
+ if (compareText(output.path, existingSubject.path) < 0)
521
+ existingSubject.path = output.path;
522
+ }
523
+ else {
524
+ options.subjects.push({
525
+ key,
526
+ kind: 'materialized-skill',
527
+ plugin: outputPlugin,
528
+ skill: output.skill,
529
+ path: output.path,
530
+ profile,
531
+ digest: output.digest,
532
+ origin: 'source-binding',
533
+ materializationProfile: validation.binding.profile,
534
+ });
535
+ }
536
+ finalKeys.push(key);
537
+ const existingFinal = finals.get(key);
538
+ if (existingFinal) {
539
+ if (compareText(output.path, existingFinal.path) < 0)
540
+ existingFinal.path = output.path;
541
+ }
542
+ else {
543
+ finals.set(key, {
544
+ key,
545
+ plugin: outputPlugin,
546
+ skill: output.skill,
547
+ profile,
548
+ materializationProfile: validation.binding.profile,
549
+ identityKey,
550
+ path: output.path,
551
+ digest: output.digest,
552
+ });
553
+ }
554
+ }
555
+ const materializationIssues = [];
556
+ if (finalKeys.length === 0) {
557
+ const message = 'The verified source binding does not contribute a final to the selected inspection target.';
558
+ materializationIssues.push(message);
559
+ options.issues.push({
560
+ category: 'unknown',
561
+ code: 'sourceBindingUnused',
562
+ message,
563
+ blocking: false,
564
+ path: bindingPath,
565
+ });
566
+ }
567
+ materializations.push({
568
+ path: bindingPath,
569
+ state: 'verified',
570
+ plugin: validation.binding.subject.plugin,
571
+ skill: validation.binding.subject.skill ?? null,
572
+ profile,
573
+ finalKeys: [...new Set(finalKeys)].sort(compareText),
574
+ verifiedBinding: {
575
+ source: validation.binding.source,
576
+ subject: validation.binding.subject,
577
+ profile: validation.binding.profile,
578
+ evalInput: validation.binding.evalInput ?? null,
579
+ finalDigest: validation.binding.finalDigest,
580
+ artifacts: [...validation.outputs].sort((left, right) => compareText(`${left.kind}\0${'plugin' in left ? left.plugin : ''}\0${'skill' in left ? left.skill : ''}\0${left.path}`, `${right.kind}\0${'plugin' in right ? right.plugin : ''}\0${'skill' in right ? right.skill : ''}\0${right.path}`)),
581
+ },
582
+ issues: materializationIssues,
583
+ });
584
+ }
585
+ return {
586
+ materializations,
587
+ finals: [...finals.values()].sort((left, right) => compareText(left.key, right.key)),
588
+ };
589
+ }
590
+ export async function inspectReviewTarget(options) {
591
+ const targetCandidate = options.target === 'catalog' ? null : path.resolve(options.cwd, options.target);
592
+ const targetCandidateStat = targetCandidate
593
+ ? await fs.lstat(targetCandidate).catch(() => null)
594
+ : null;
595
+ const resolution = await resolveCatalogWorkspaceRoot({
596
+ cwd: options.workspaceRoot === undefined && targetCandidateStat?.isDirectory()
597
+ ? targetCandidate
598
+ : options.cwd,
599
+ ...(options.workspaceRoot === undefined ? {} : { explicitRoot: options.workspaceRoot }),
600
+ mode: 'existing',
601
+ });
602
+ const workspace = await readCatalogWorkspace(resolution.workspaceRoot, {
603
+ reservedImports: CLI_RESERVED_IMPORTS,
604
+ });
605
+ const workspaceSourceIdentity = await calculateCatalogWorkspaceSourceIdentity(workspace);
606
+ const [target, snapshot] = await Promise.all([
607
+ resolveTarget({ cwd: options.cwd, target: options.target, workspace }),
608
+ inspectSnapshot(workspace, workspaceSourceIdentity),
609
+ ]);
610
+ const issues = [];
611
+ const subjects = [{
612
+ key: 'catalog:source',
613
+ kind: 'catalog-source',
614
+ plugin: null,
615
+ skill: null,
616
+ path: workspace.workspaceRoot,
617
+ profile: null,
618
+ digest: workspaceSourceIdentity,
619
+ origin: 'workspace',
620
+ }];
621
+ if (snapshot.receipt.state === 'current') {
622
+ subjects.push({
623
+ key: 'catalog:snapshot',
624
+ kind: 'catalog-snapshot',
625
+ plugin: null,
626
+ skill: null,
627
+ path: snapshot.receipt.path,
628
+ profile: null,
629
+ digest: snapshot.receipt.snapshotDigest,
630
+ origin: 'catalog-snapshot',
631
+ });
632
+ }
633
+ else if (snapshot.receipt.state !== 'absent') {
634
+ issues.push({
635
+ category: snapshot.receipt.state === 'invalid' ? 'invalid' : 'unknown',
636
+ code: snapshot.receipt.state === 'invalid' ? 'snapshotInvalid' : 'snapshotStale',
637
+ message: snapshot.receipt.state === 'invalid'
638
+ ? 'The existing Catalog snapshot is invalid.'
639
+ : 'The existing Catalog snapshot is not bound to the current Workspace source.',
640
+ blocking: false,
641
+ path: relativeWorkspacePath(workspace.workspaceRoot, snapshot.receipt.path),
642
+ });
643
+ }
644
+ const seeds = [];
645
+ const pluginInputs = [];
646
+ for (const plugin of target.plugins) {
647
+ const sourceContainerKey = `plugin:${plugin.manifest.name}:source`;
648
+ const component = await computeReviewComponentDigest({
649
+ repoRoot: workspace.workspaceRoot,
650
+ path: plugin.rootDir,
651
+ });
652
+ subjects.push({
653
+ key: sourceContainerKey,
654
+ kind: 'aw-plugin-source',
655
+ plugin: plugin.manifest.name,
656
+ skill: null,
657
+ path: plugin.rootDir,
658
+ profile: null,
659
+ digest: component.digest,
660
+ origin: 'workspace',
661
+ });
662
+ const source = await inventoryPluginSource({ workspace, plugin, subjects });
663
+ seeds.push(...source.seeds);
664
+ pluginInputs.push({ plugin, sourceContainerKey, sourceSubjectKeys: source.sourceSubjectKeys });
665
+ }
666
+ const requiredFinals = seeds.map((seed) => ({
667
+ key: requiredFinalKey(seed),
668
+ kind: 'materialized-skill',
669
+ plugin: seed.plugin.manifest.name,
670
+ skill: seed.skill,
671
+ path: null,
672
+ profile: seed.profile,
673
+ digest: null,
674
+ targetId: seed.targetId,
675
+ sourceSubjectKey: seed.sourceSubjectKey,
676
+ status: 'missing',
677
+ matchedFinalKeys: [],
678
+ }));
679
+ const requiredByKey = new Map(requiredFinals.map((entry) => [entry.key, entry]));
680
+ if (snapshot.root) {
681
+ for (const seed of seeds.filter((entry) => !entry.dynamic && !entry.profile.startsWith('agent-skills-'))) {
682
+ const platform = seed.profile.startsWith('codex-') ? 'codex' : 'claude';
683
+ const finalPath = path.join(snapshot.root, 'plugins', seed.plugin.manifest.name, platform, 'skills', seed.skill);
684
+ try {
685
+ const digest = await computeSkillDigest(finalPath);
686
+ const key = `${requiredFinalKey(seed)}:snapshot`;
687
+ subjects.push({
688
+ key,
689
+ kind: 'materialized-skill',
690
+ plugin: seed.plugin.manifest.name,
691
+ skill: seed.skill,
692
+ path: finalPath,
693
+ profile: seed.profile,
694
+ digest,
695
+ origin: 'catalog-snapshot',
696
+ });
697
+ const required = requiredByKey.get(requiredFinalKey(seed));
698
+ required.path = finalPath;
699
+ required.digest = digest;
700
+ required.status = 'resolved';
701
+ required.matchedFinalKeys.push(key);
702
+ }
703
+ catch (error) {
704
+ const required = requiredByKey.get(requiredFinalKey(seed));
705
+ required.status = 'invalid';
706
+ issues.push({
707
+ category: 'invalid',
708
+ code: 'snapshotFinalInvalid',
709
+ message: error instanceof Error ? error.message : String(error),
710
+ blocking: true,
711
+ subjectKey: required.key,
712
+ path: relativeWorkspacePath(workspace.workspaceRoot, finalPath),
713
+ });
714
+ }
715
+ }
716
+ }
717
+ const binding = await inspectMaterializations({
718
+ paths: options.materializations ?? [],
719
+ workspace,
720
+ workspaceSourceIdentity,
721
+ targetPlugins: target.plugins,
722
+ requiredFinals,
723
+ subjects,
724
+ issues,
725
+ env: options.env,
726
+ });
727
+ const bindingFinalsByRequiredKey = new Map();
728
+ for (const final of binding.finals) {
729
+ const candidates = requiredFinals.filter((required) => required.plugin === final.plugin
730
+ && required.skill === final.skill
731
+ && required.profile === final.profile);
732
+ if (candidates.length === 1) {
733
+ const required = candidates[0];
734
+ bindingFinalsByRequiredKey.set(required.key, [...(bindingFinalsByRequiredKey.get(required.key) ?? []), final]);
735
+ }
736
+ else if (candidates.length > 1) {
737
+ issues.push({
738
+ category: 'unknown',
739
+ code: 'materializedFinalAmbiguous',
740
+ message: `Materialized final ${final.plugin}/${final.skill}/${final.profile} matches multiple declarations.`,
741
+ blocking: true,
742
+ subjectKey: final.key,
743
+ });
744
+ }
745
+ }
746
+ for (const required of requiredFinals) {
747
+ const finals = bindingFinalsByRequiredKey.get(required.key) ?? [];
748
+ const byIdentity = new Map();
749
+ for (const final of finals) {
750
+ byIdentity.set(final.identityKey, [...(byIdentity.get(final.identityKey) ?? []), final]);
751
+ }
752
+ const ambiguous = [...byIdentity.entries()]
753
+ .filter(([, entries]) => new Set(entries.map((entry) => entry.digest)).size > 1)
754
+ .sort(([left], [right]) => compareText(left, right));
755
+ required.matchedFinalKeys = [...new Set([
756
+ ...required.matchedFinalKeys,
757
+ ...finals.map((final) => final.key),
758
+ ])].sort(compareText);
759
+ if (ambiguous.length > 0) {
760
+ required.status = 'unknown';
761
+ required.path = null;
762
+ required.digest = null;
763
+ for (const [identity] of ambiguous) {
764
+ issues.push({
765
+ category: 'unknown',
766
+ code: 'materializedFinalDigestAmbiguous',
767
+ message: `One materialization identity produced different target digests for ${required.plugin}/${required.skill}/${required.profile}: ${identity}.`,
768
+ blocking: true,
769
+ subjectKey: required.key,
770
+ });
771
+ }
772
+ continue;
773
+ }
774
+ if (finals.length > 0)
775
+ required.status = 'resolved';
776
+ if (required.matchedFinalKeys.length === 1) {
777
+ const subject = subjects.find((entry) => entry.key === required.matchedFinalKeys[0]);
778
+ required.path = subject.path;
779
+ required.digest = subject.digest;
780
+ }
781
+ else if (required.matchedFinalKeys.length > 1) {
782
+ required.path = null;
783
+ required.digest = null;
784
+ }
785
+ }
786
+ for (const required of requiredFinals.filter((entry) => entry.status === 'missing')) {
787
+ issues.push({
788
+ category: 'missing',
789
+ code: 'requiredFinalMissing',
790
+ message: `No current final is available for ${required.plugin}/${required.skill}/${required.profile}.`,
791
+ blocking: true,
792
+ subjectKey: required.key,
793
+ });
794
+ }
795
+ const requiredEvals = await inventoryRequiredEvals({ workspace, seeds, issues });
796
+ for (const required of requiredEvals) {
797
+ required.materializedFinalKeys = binding.finals
798
+ .filter((final) => final.plugin === required.plugin
799
+ && final.skill === required.skill
800
+ && final.profile === required.finalProfile
801
+ && final.materializationProfile.kind === 'eval'
802
+ && final.materializationProfile.id === required.evalProfile.id
803
+ && final.materializationProfile.inputDigest === required.evalProfile.inputDigest)
804
+ .map((final) => final.key)
805
+ .sort(compareText);
806
+ required.verifiedBindingPaths = [...new Set(binding.materializations
807
+ .filter((materialization) => materialization.state === 'verified'
808
+ && materialization.verifiedBinding?.subject.plugin === required.plugin
809
+ && materialization.verifiedBinding.subject.skill === required.skill
810
+ && materialization.verifiedBinding.subject.suite === required.suite.id
811
+ && materialization.verifiedBinding.profile.kind === 'eval'
812
+ && materialization.verifiedBinding.profile.id === required.evalProfile.id
813
+ && materialization.verifiedBinding.profile.inputDigest === required.evalProfile.inputDigest)
814
+ .map((materialization) => materialization.path))]
815
+ .sort(compareText);
816
+ if (required.materializedFinalKeys.length === 0) {
817
+ const snapshotFinal = requiredByKey.get(required.requiredFinalKey)?.matchedFinalKeys ?? [];
818
+ required.materializedFinalKeys = [...snapshotFinal];
819
+ }
820
+ }
821
+ const plugins = pluginInputs.map(({ plugin, sourceContainerKey, sourceSubjectKeys }) => {
822
+ const entry = snapshot.receipt.state === 'current' ? snapshot.entries.get(plugin.manifest.name) : undefined;
823
+ let finalContainerKey = null;
824
+ if (entry && snapshot.root) {
825
+ finalContainerKey = `plugin:${plugin.manifest.name}:snapshot`;
826
+ subjects.push({
827
+ key: finalContainerKey,
828
+ kind: 'aw-plugin',
829
+ plugin: plugin.manifest.name,
830
+ skill: null,
831
+ path: path.join(snapshot.root, 'plugins', plugin.manifest.name),
832
+ profile: null,
833
+ digest: entry.artifactDigest,
834
+ origin: 'catalog-snapshot',
835
+ });
836
+ }
837
+ const pluginFinals = requiredFinals.filter((required) => required.plugin === plugin.manifest.name);
838
+ const pluginEvals = requiredEvals.filter((required) => required.plugin === plugin.manifest.name);
839
+ return {
840
+ id: plugin.manifest.name,
841
+ name: plugin.manifest.name,
842
+ version: plugin.manifest.version,
843
+ path: plugin.rootDir,
844
+ sourceContainerKey,
845
+ finalContainerKey,
846
+ dependencies: [...(plugin.manifest.dependencies ?? [])].sort(compareText),
847
+ supportedPlatforms: [...resolveSupportedPlatforms(plugin.manifest)].sort(compareText),
848
+ finalProfiles: [...new Set(pluginFinals.map((entry) => entry.profile))].sort(compareText),
849
+ imports: pluginImports(plugin),
850
+ snapshotEntry: entry ? {
851
+ artifactDigest: entry.artifactDigest,
852
+ manifestDigest: entry.manifestDigest,
853
+ contentDigest: entry.contentDigest,
854
+ } : null,
855
+ sourceSubjectKeys,
856
+ requiredFinalKeys: pluginFinals.map((entry) => entry.key).sort(compareText),
857
+ requiredEvalKeys: pluginEvals.map((entry) => entry.key).sort(compareText),
858
+ };
859
+ });
860
+ issues.push(...binding.materializations
861
+ .filter((entry) => entry.state !== 'verified')
862
+ .map((entry) => ({
863
+ category: entry.state === 'incomplete'
864
+ ? 'missing'
865
+ : entry.state === 'unsupported' ? 'unsupported' : entry.state === 'drifted' ? 'unknown' : 'invalid',
866
+ code: `sourceBinding${entry.state[0].toUpperCase()}${entry.state.slice(1)}`,
867
+ message: entry.issues.join('; ') || `Source binding is ${entry.state}.`,
868
+ blocking: true,
869
+ path: entry.path,
870
+ })));
871
+ issues.sort((left, right) => compareText(`${left.code}\0${left.subjectKey ?? ''}\0${left.path ?? ''}`, `${right.code}\0${right.subjectKey ?? ''}\0${right.path ?? ''}`));
872
+ subjects.sort((left, right) => compareText(left.key, right.key));
873
+ const ok = !issues.some((issue) => issue.blocking);
874
+ return {
875
+ schemaVersion: 1,
876
+ command: 'review:inspect',
877
+ ok,
878
+ completeness: ok ? 'complete' : 'incomplete',
879
+ target: { kind: target.kind, id: target.id, path: target.path },
880
+ workspace: {
881
+ root: workspace.workspaceRoot,
882
+ resolvedFrom: resolution.workspaceRootResolvedFrom,
883
+ identity: workspace.workspaceIdentity,
884
+ sourceIdentity: workspaceSourceIdentity,
885
+ catalogVersion: workspace.metadata.catalog.version,
886
+ },
887
+ snapshot: snapshot.receipt,
888
+ plugins,
889
+ subjects,
890
+ requiredFinals,
891
+ finalMappings: requiredFinals.map((required) => ({
892
+ requiredFinalKey: required.key,
893
+ sourceSubjectKey: required.sourceSubjectKey,
894
+ finalSubjectKeys: [...required.matchedFinalKeys].sort(compareText),
895
+ status: required.status,
896
+ })),
897
+ requiredEvals,
898
+ materializations: binding.materializations,
899
+ issues,
900
+ };
901
+ }
902
+ export function createReviewInspectionFailure(error, code = 'reviewInspectionFailed') {
903
+ return {
904
+ schemaVersion: 1,
905
+ command: 'review:inspect',
906
+ ok: false,
907
+ completeness: 'incomplete',
908
+ issues: [{
909
+ category: 'invalid',
910
+ code,
911
+ message: error instanceof Error ? error.message : String(error),
912
+ blocking: true,
913
+ }],
914
+ };
915
+ }
916
+ //# sourceMappingURL=review-inspection.js.map