agentic-workflow-manager 9.6.1 → 9.7.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.
- package/README.md +2 -0
- package/dist/src/commands/doctor.js +4 -2
- package/dist/src/commands/export.js +3 -0
- package/dist/src/commands/init.js +10 -2
- package/dist/src/commands/registry/add.js +2 -2
- package/dist/src/commands/registry/index.js +7 -5
- package/dist/src/commands/registry/install-bundles.js +4 -4
- package/dist/src/commands/registry/status.js +4 -4
- package/dist/src/commands/sync.js +2 -1
- package/dist/src/commands/track/index.js +3 -7
- package/dist/src/commands/update.js +3 -1
- package/dist/src/commands/watch/apply.js +15 -0
- package/dist/src/commands/watch/tracks.js +56 -7
- package/dist/src/core/bundle-install.js +2 -2
- package/dist/src/core/bundles.js +92 -19
- package/dist/src/core/diagnostics/context.js +1 -1
- package/dist/src/core/export/index.js +1 -1
- package/dist/src/core/export/resolve.js +2 -2
- package/dist/src/core/journal/types.js +1 -0
- package/dist/src/core/reconciliation.js +1 -1
- package/dist/src/core/tracks/git.js +15 -3
- package/dist/src/core/tracks/protocol.js +26 -6
- package/dist/src/index.js +46 -39
- package/dist/src/utils/grouping.js +1 -1
- package/dist/src/utils/registry-view.js +1 -1
- package/dist/tests/commands/bundle-diagnostics.test.js +145 -0
- package/dist/tests/commands/export.test.js +28 -2
- package/dist/tests/commands/multi-agent-targeting.test.js +1 -1
- package/dist/tests/commands/track/verbs.test.js +6 -0
- package/dist/tests/commands/watch/apply.test.js +46 -9
- package/dist/tests/commands/watch/track-remove-request.test.js +165 -0
- package/dist/tests/commands/watch/track-teardown-crash.test.js +27 -0
- package/dist/tests/core/bundle-install.test.js +36 -0
- package/dist/tests/core/bundles-multiroot.test.js +24 -2
- package/dist/tests/core/bundles-overrides.test.js +10 -7
- package/dist/tests/core/bundles.test.js +75 -6
- package/dist/tests/core/diagnostics/context.test.js +2 -2
- package/dist/tests/core/export/engine.test.js +1 -1
- package/dist/tests/core/export/resolve.test.js +1 -1
- package/dist/tests/core/init/mutation-targets.test.js +1 -1
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/steps.test.js +1 -1
- package/dist/tests/core/journal/types.test.js +11 -0
- package/dist/tests/core/reconciliation.test.js +1 -1
- package/dist/tests/core/sync-profile-atomicity.test.js +1 -1
- package/dist/tests/core/tracks/git.test.js +7 -0
- package/dist/tests/core/tracks/protocol.test.js +103 -0
- package/dist/tests/structural/bundle-skill-reference-contract.test.js +56 -0
- package/dist/tests/utils/grouping.test.js +2 -2
- package/dist/tests/utils/registry-view.test.js +7 -7
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
package/README.md
CHANGED
|
@@ -69,6 +69,8 @@ and one JSON argv integration command. Start or resume with `awm watch`; inspect
|
|
|
69
69
|
`awm track status`. Track workers commit only their assigned files and request
|
|
70
70
|
`awm track join`. The plan supervisor freezes and merges tracks, runs global QA once on the
|
|
71
71
|
final HEAD, executes the canonical integration job once, then applies the interlock.
|
|
72
|
+
`awm track remove` requests a safe cancellation of the complete cohort; only demonstrably
|
|
73
|
+
owned resources are removed before returning to serial execution.
|
|
72
74
|
|
|
73
75
|
Any missing declaration, overlap, global file class, unavailable worktree, or failed
|
|
74
76
|
preparation degrades to serial with an event naming the cause. `BLOCKED` means ownership or
|
|
@@ -19,6 +19,7 @@ const collect_1 = require("../core/dashboard/collect");
|
|
|
19
19
|
const render_html_1 = require("../core/dashboard/render-html");
|
|
20
20
|
const render_terminal_1 = require("../core/dashboard/render-terminal");
|
|
21
21
|
const write_html_1 = require("../core/dashboard/write-html");
|
|
22
|
+
const bundles_1 = require("../core/bundles");
|
|
22
23
|
function glyph(status) {
|
|
23
24
|
if (status === 'ok')
|
|
24
25
|
return picocolors_1.default.green('✔');
|
|
@@ -127,6 +128,7 @@ function renderProviderReport(report) {
|
|
|
127
128
|
}
|
|
128
129
|
function runDoctor(opts = {}) {
|
|
129
130
|
const resolveTargets = opts.resolveTargets ?? agent_targets_1.resolveAgentTargets;
|
|
131
|
+
const reporter = opts.reporter ?? (0, bundles_1.createBundleDiagnosticReporter)((message) => process.stderr.write(`${message}\n`));
|
|
130
132
|
const invalid = (message) => { process.stderr.write(`awm doctor: ${message}\n`); return 2; };
|
|
131
133
|
const htmlRequested = opts.html !== undefined;
|
|
132
134
|
if (opts.json && opts.full)
|
|
@@ -143,7 +145,7 @@ function runDoctor(opts = {}) {
|
|
|
143
145
|
const target = htmlRequested ? (0, write_html_1.resolveHtmlTarget)({ cwd, target: opts.html, force: opts.force }) : undefined;
|
|
144
146
|
const targets = resolveTargets({ prefs: (0, config_1.readPreferences)(), explicit: opts.agent });
|
|
145
147
|
const collectSnapshot = opts.collectSnapshot ?? collect_1.collectDashboardSnapshot;
|
|
146
|
-
const context = (0, context_1.gatherContext)({ cwd, agents: targets });
|
|
148
|
+
const context = (0, context_1.gatherContext)({ cwd, agents: targets, reporter });
|
|
147
149
|
const snapshot = collectSnapshot({ cwd, now: new Date().toISOString(), adapters: {
|
|
148
150
|
...(0, collect_1.productionDashboardAdapters)(context),
|
|
149
151
|
} });
|
|
@@ -175,7 +177,7 @@ function runDoctor(opts = {}) {
|
|
|
175
177
|
}
|
|
176
178
|
let report;
|
|
177
179
|
try {
|
|
178
|
-
const ctx = (0, context_1.gatherContext)({ cwd: opts.cwd, agents: targets });
|
|
180
|
+
const ctx = (0, context_1.gatherContext)({ cwd: opts.cwd, agents: targets, reporter });
|
|
179
181
|
const providers = ctx.providers ?? [];
|
|
180
182
|
report = { providers, overall: (0, checks_1.computeProviderOverall)(providers) };
|
|
181
183
|
}
|
|
@@ -8,14 +8,17 @@ exports.registerExportCommand = registerExportCommand;
|
|
|
8
8
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const export_1 = require("../core/export");
|
|
11
|
+
const bundles_1 = require("../core/bundles");
|
|
11
12
|
function runExportCommand(name, flags, deps = {}) {
|
|
12
13
|
const log = deps.log ?? console.log;
|
|
14
|
+
const reporter = deps.reporter ?? (0, bundles_1.createBundleDiagnosticReporter)((message) => process.stderr.write(`${message}\n`));
|
|
13
15
|
const opts = {
|
|
14
16
|
name,
|
|
15
17
|
target: flags.target,
|
|
16
18
|
out: flags.out,
|
|
17
19
|
roots: deps.roots,
|
|
18
20
|
zip: deps.zip,
|
|
21
|
+
reporter,
|
|
19
22
|
};
|
|
20
23
|
const summary = (0, export_1.runExport)(opts);
|
|
21
24
|
log(picocolors_1.default.dim(`Exported ${summary.kind} "${name}"`));
|
|
@@ -138,6 +138,14 @@ async function runInit(opts = {}) {
|
|
|
138
138
|
(0, paths_1.noteWindowsCaveat)((m) => console.log(picocolors_1.default.dim(`ℹ ${m}`)));
|
|
139
139
|
}
|
|
140
140
|
const cwd = opts.cwd ?? process.cwd();
|
|
141
|
+
const report = opts.reporter ?? (0, bundles_1.createBundleDiagnosticReporter)((message) => process.stderr.write(`${message}\n`));
|
|
142
|
+
const reportedDiagnostics = new Set();
|
|
143
|
+
const reporter = (diagnostic) => {
|
|
144
|
+
if (reportedDiagnostics.has(diagnostic))
|
|
145
|
+
return;
|
|
146
|
+
reportedDiagnostics.add(diagnostic);
|
|
147
|
+
report(diagnostic);
|
|
148
|
+
};
|
|
141
149
|
const agent = opts.agent === undefined ? 'claude-code' : (0, providers_1.requireAgentTarget)(opts.agent);
|
|
142
150
|
// R2: gate BEFORE anything is read or written — an unsupported provider
|
|
143
151
|
// version must never touch preferences.json or any provider file.
|
|
@@ -190,7 +198,7 @@ async function runInit(opts = {}) {
|
|
|
190
198
|
// that exact run won't roll those specific artifacts back — every
|
|
191
199
|
// other target (preferences, hook, injection, previously-installed
|
|
192
200
|
// bundle content) is covered.
|
|
193
|
-
const preSyncBundles = (0, bundles_1.discoverAllBundles)();
|
|
201
|
+
const preSyncBundles = (0, bundles_1.discoverAllBundles)(undefined, reporter);
|
|
194
202
|
const mutationTargets = (0, mutation_targets_1.planInitMutationTargets)({
|
|
195
203
|
cwd,
|
|
196
204
|
agent,
|
|
@@ -207,7 +215,7 @@ async function runInit(opts = {}) {
|
|
|
207
215
|
// step's failure instead of being reported as its own cause.
|
|
208
216
|
(0, registries_1.assertSyncedRegistriesUsable)((await mergedActions.syncCache()) ?? []);
|
|
209
217
|
}
|
|
210
|
-
const bundles = (0, bundles_1.discoverAllBundles)();
|
|
218
|
+
const bundles = (0, bundles_1.discoverAllBundles)(undefined, reporter);
|
|
211
219
|
const ctx = (0, context_1.gatherContext)({ cwd, bundles, agent });
|
|
212
220
|
// In machineOnly mode, null out the project context so project steps are skipped
|
|
213
221
|
const effectiveCtx = opts.machineOnly
|
|
@@ -26,7 +26,7 @@ function deriveRegistryName(remote) {
|
|
|
26
26
|
const base = remote.replace(/[/\\]+$/, '').split(/[/\\:]/).pop() ?? '';
|
|
27
27
|
return base.replace(/\.git$/, '');
|
|
28
28
|
}
|
|
29
|
-
async function addRegistry(remote, nameOverride) {
|
|
29
|
+
async function addRegistry(remote, nameOverride, reporter) {
|
|
30
30
|
const name = nameOverride ?? deriveRegistryName(remote);
|
|
31
31
|
if (!name || name === '.' || name.includes('..') || /[/\\]/.test(name)) {
|
|
32
32
|
return { ok: false, error: `Invalid registry name "${name}" — use --name <simple-dir-name>` };
|
|
@@ -61,7 +61,7 @@ async function addRegistry(remote, nameOverride) {
|
|
|
61
61
|
(0, discovery_1.discoverSkills)(roots);
|
|
62
62
|
(0, discovery_1.discoverWorkflows)(roots);
|
|
63
63
|
(0, discovery_1.discoverAgents)(roots);
|
|
64
|
-
(0, bundles_1.discoverAllBundles)(roots);
|
|
64
|
+
(0, bundles_1.discoverAllBundles)(roots, reporter);
|
|
65
65
|
}
|
|
66
66
|
catch (e) {
|
|
67
67
|
fs_1.default.rmSync(dest, { recursive: true, force: true });
|
|
@@ -28,10 +28,11 @@ function registerRegistryCommand(program) {
|
|
|
28
28
|
.option('--install-all', 'install every bundle from the new registry for the default agent')
|
|
29
29
|
.option('--no-install', 'skip the bundle install offer')
|
|
30
30
|
.action(async (remote, options) => {
|
|
31
|
+
const reporter = (0, bundles_1.createBundleDiagnosticReporter)((message) => process.stderr.write(`${message}\n`));
|
|
31
32
|
(0, prompts_1.intro)(picocolors_1.default.bgCyan(picocolors_1.default.black(' AWM - Add Registry ')));
|
|
32
33
|
const s = (0, prompts_1.spinner)();
|
|
33
34
|
s.start('Cloning and validating registry...');
|
|
34
|
-
const result = await (0, add_1.addRegistry)(remote, options.name);
|
|
35
|
+
const result = await (0, add_1.addRegistry)(remote, options.name, reporter);
|
|
35
36
|
if (!result.ok) {
|
|
36
37
|
s.stop('Failed.');
|
|
37
38
|
console.error(picocolors_1.default.red(result.error));
|
|
@@ -49,7 +50,7 @@ function registerRegistryCommand(program) {
|
|
|
49
50
|
}
|
|
50
51
|
// Bundle install offer — failure NEVER reverts the add.
|
|
51
52
|
try {
|
|
52
|
-
const available = (0, install_bundles_1.bundlesInRegistry)(result.contentRoot);
|
|
53
|
+
const available = (0, install_bundles_1.bundlesInRegistry)(result.contentRoot, reporter);
|
|
53
54
|
if (available.length > 0) {
|
|
54
55
|
const prefs = (0, config_1.getPreferences)();
|
|
55
56
|
const projectRoot = (0, profile_1.findProjectRoot)(process.cwd()) ?? process.cwd();
|
|
@@ -82,7 +83,7 @@ function registerRegistryCommand(program) {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
if (selection) {
|
|
85
|
-
for (const r of (0, install_bundles_1.installBundlesFromRegistry)(result.contentRoot, selection, agents, projectRoot)) {
|
|
86
|
+
for (const r of (0, install_bundles_1.installBundlesFromRegistry)(result.contentRoot, selection, agents, projectRoot, reporter)) {
|
|
86
87
|
for (const line of r.installed)
|
|
87
88
|
console.log(picocolors_1.default.green(` ✓ ${line}`));
|
|
88
89
|
for (const sk of r.skipped)
|
|
@@ -103,6 +104,7 @@ function registerRegistryCommand(program) {
|
|
|
103
104
|
reg.command('list')
|
|
104
105
|
.description('list configured additional registries')
|
|
105
106
|
.action(() => {
|
|
107
|
+
const reporter = (0, bundles_1.createBundleDiagnosticReporter)((message) => process.stderr.write(`${message}\n`));
|
|
106
108
|
const regs = (0, registries_1.listRegistries)();
|
|
107
109
|
if (regs.length === 0) {
|
|
108
110
|
console.log(picocolors_1.default.dim('No additional registries. Add one with `awm registry add <git-url>`.'));
|
|
@@ -125,12 +127,12 @@ function registerRegistryCommand(program) {
|
|
|
125
127
|
try {
|
|
126
128
|
const counts = [
|
|
127
129
|
`${(0, discovery_1.discoverSkills)([r.contentRoot]).length} skills`,
|
|
128
|
-
`${(0, bundles_1.discoverAllBundles)([r.contentRoot]).length} bundles`,
|
|
130
|
+
`${(0, bundles_1.discoverAllBundles)([r.contentRoot], reporter).length} bundles`,
|
|
129
131
|
`${(0, discovery_1.discoverWorkflows)([r.contentRoot]).length} workflows`,
|
|
130
132
|
`${(0, discovery_1.discoverAgents)([r.contentRoot]).length} agents`,
|
|
131
133
|
].join(', ');
|
|
132
134
|
console.log(`${picocolors_1.default.cyan(r.name)} ${r.remote} ${picocolors_1.default.dim(counts)}`);
|
|
133
|
-
for (const o of (0, status_1.overrideStatus)(r.contentRoot, earlier)) {
|
|
135
|
+
for (const o of (0, status_1.overrideStatus)(r.contentRoot, earlier, reporter)) {
|
|
134
136
|
console.log(o.active
|
|
135
137
|
? picocolors_1.default.yellow(` ↑ override active: ${o.name}`)
|
|
136
138
|
: picocolors_1.default.dim(` ∅ override with no effect: ${o.name}`));
|
|
@@ -9,16 +9,16 @@ const bundles_1 = require("../../core/bundles");
|
|
|
9
9
|
const bundle_install_1 = require("../../core/bundle-install");
|
|
10
10
|
/** Bundles disponibles en un content root concreto (candidatos a instalar tras el add).
|
|
11
11
|
* Uses single-root discovery to avoid surfacing cross-registry collision errors here. */
|
|
12
|
-
function bundlesInRegistry(contentRoot) {
|
|
13
|
-
return (0, bundles_1.discoverBundles)(contentRoot).map((b) => b.name);
|
|
12
|
+
function bundlesInRegistry(contentRoot, reporter) {
|
|
13
|
+
return (0, bundles_1.discoverBundles)(contentRoot, reporter).map((b) => b.name);
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Instala bundles del registry `contentRoot` para los agentes dados.
|
|
17
17
|
* `selection` = 'all' instala todos los del registry; una lista instala solo esos.
|
|
18
18
|
* Las dependencias se resuelven contra TODOS los roots (pueden vivir en el base).
|
|
19
19
|
*/
|
|
20
|
-
function installBundlesFromRegistry(contentRoot, selection, agents, projectRoot) {
|
|
21
|
-
const allBundles = (0, bundles_1.discoverAllBundles)();
|
|
20
|
+
function installBundlesFromRegistry(contentRoot, selection, agents, projectRoot, reporter) {
|
|
21
|
+
const allBundles = (0, bundles_1.discoverAllBundles)(undefined, reporter);
|
|
22
22
|
const candidates = allBundles.filter((b) => b.contentRoot === contentRoot);
|
|
23
23
|
const wanted = selection === 'all' ? candidates : candidates.filter((b) => selection.includes(b.name));
|
|
24
24
|
const results = [];
|
|
@@ -13,7 +13,7 @@ const bundles_1 = require("../../core/bundles");
|
|
|
13
13
|
function canDiscoverRegistryContent(contentRoot, safeRoots) {
|
|
14
14
|
return safeRoots.has(contentRoot);
|
|
15
15
|
}
|
|
16
|
-
function artifactNamesInRoot(root) {
|
|
16
|
+
function artifactNamesInRoot(root, reporter) {
|
|
17
17
|
const names = new Set();
|
|
18
18
|
for (const s of (0, discovery_1.discoverSkills)([root]))
|
|
19
19
|
names.add(s.name);
|
|
@@ -21,17 +21,17 @@ function artifactNamesInRoot(root) {
|
|
|
21
21
|
names.add(w.name);
|
|
22
22
|
for (const a of (0, discovery_1.discoverAgents)([root]))
|
|
23
23
|
names.add(a.name);
|
|
24
|
-
for (const b of (0, bundles_1.discoverAllBundles)([root]))
|
|
24
|
+
for (const b of (0, bundles_1.discoverAllBundles)([root], reporter))
|
|
25
25
|
names.add(b.name);
|
|
26
26
|
return names;
|
|
27
27
|
}
|
|
28
|
-
function overrideStatus(contentRoot, earlierRoots) {
|
|
28
|
+
function overrideStatus(contentRoot, earlierRoots, reporter) {
|
|
29
29
|
const declared = Array.from((0, registries_1.readRegistryManifest)(contentRoot).overrides);
|
|
30
30
|
if (declared.length === 0)
|
|
31
31
|
return [];
|
|
32
32
|
const earlier = new Set();
|
|
33
33
|
for (const root of earlierRoots) {
|
|
34
|
-
for (const n of artifactNamesInRoot(root))
|
|
34
|
+
for (const n of artifactNamesInRoot(root, reporter))
|
|
35
35
|
earlier.add(n);
|
|
36
36
|
}
|
|
37
37
|
return declared.map((name) => ({ name, active: earlier.has(name) }));
|
|
@@ -51,6 +51,7 @@ async function runSyncCore(options, deps = {}) {
|
|
|
51
51
|
// emission point for this command (mirrors `runInit`/`runUpdateCore`).
|
|
52
52
|
(0, paths_1.noteWindowsCaveat)((m) => console.log(picocolors_1.default.dim(`ℹ ${m}`)));
|
|
53
53
|
const d = { ...defaultDeps, ...deps };
|
|
54
|
+
const reporter = options.reporter ?? (0, bundles_1.createBundleDiagnosticReporter)((message) => process.stderr.write(`${message}\n`));
|
|
54
55
|
const cwd = options.cwd ?? process.cwd();
|
|
55
56
|
const projectRoot = (0, profile_1.findProjectRoot)(cwd);
|
|
56
57
|
if (!projectRoot) {
|
|
@@ -130,7 +131,7 @@ async function runSyncCore(options, deps = {}) {
|
|
|
130
131
|
const method = options.method === 'copy' ? 'copy' : 'symlink';
|
|
131
132
|
let result;
|
|
132
133
|
try {
|
|
133
|
-
result = d.syncProfile({ projectRoot, bundles: (0, bundles_1.discoverAllBundles)(), agents: selectedAgents, method });
|
|
134
|
+
result = d.syncProfile({ projectRoot, bundles: (0, bundles_1.discoverAllBundles)(undefined, reporter), agents: selectedAgents, method });
|
|
134
135
|
}
|
|
135
136
|
catch (e) {
|
|
136
137
|
console.error(picocolors_1.default.red(e.message));
|
|
@@ -104,14 +104,10 @@ function registerTrackCommand(program) {
|
|
|
104
104
|
}
|
|
105
105
|
process.stdout.write(JSON.stringify({ requestId: r.requestId, idempotencyKey: r.idempotencyKey, qaHeadSha: r.qaHeadSha }, null, 2) + '\n');
|
|
106
106
|
});
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// La descripción lo dice en vez de prometer un desmantelamiento que no ocurre — hasta
|
|
110
|
-
// este release afirmaba que el supervisor desmantelaba worktree/rama, y no lo hacía.
|
|
111
|
-
// El teardown que sí funciona es el automático de la degradación a serial
|
|
112
|
-
// (`begin-teardown` desde `FALLBACK_PENDING`), que no pasa por este verbo.
|
|
107
|
+
// Request-only: el supervisor persiste la intención, cancela la cohorte completa y
|
|
108
|
+
// reutiliza el teardown fail-closed antes de volver a ejecución serial.
|
|
113
109
|
track.command('remove')
|
|
114
|
-
.description('
|
|
110
|
+
.description('emite track-teardown-request — cancela la cohorte y solicita teardown seguro antes del fallback serial')
|
|
115
111
|
.requiredOption('--generation <token>', 'token de la generacion vigente')
|
|
116
112
|
.argument('<trackId>')
|
|
117
113
|
.action((trackId, opts) => {
|
|
@@ -29,6 +29,7 @@ const registries_1 = require("../core/registries");
|
|
|
29
29
|
const paths_2 = require("../core/paths");
|
|
30
30
|
const regenerate_1 = require("../core/context/regenerate");
|
|
31
31
|
const reconciliation_1 = require("../core/reconciliation");
|
|
32
|
+
const bundles_1 = require("../core/bundles");
|
|
32
33
|
const install_transaction_1 = require("../core/install-transaction");
|
|
33
34
|
const resync_1 = require("../commands/hooks/resync");
|
|
34
35
|
const defaultDeps = {
|
|
@@ -79,6 +80,7 @@ async function runUpdateCore(options = {}, deps = {}) {
|
|
|
79
80
|
// single emission point for this command (mirrors `runInit`/`runSyncCore`).
|
|
80
81
|
(0, paths_1.noteWindowsCaveat)((m) => console.log(picocolors_1.default.dim(`ℹ ${m}`)));
|
|
81
82
|
const d = { ...defaultDeps, ...deps };
|
|
83
|
+
const reporter = options.reporter ?? (0, bundles_1.createBundleDiagnosticReporter)((message) => process.stderr.write(`${message}\n`));
|
|
82
84
|
const prefs = (0, config_1.getPreferences)();
|
|
83
85
|
const resolved = (0, agent_targets_1.resolveAgentTargetsOrError)({ prefs, explicit: options.agent });
|
|
84
86
|
if (!resolved.ok) {
|
|
@@ -154,7 +156,7 @@ async function runUpdateCore(options = {}, deps = {}) {
|
|
|
154
156
|
console.log(picocolors_1.default.green(` ✓ Regenerated AWM context for: ${refreshed.join(', ')}`));
|
|
155
157
|
let artifactResult;
|
|
156
158
|
try {
|
|
157
|
-
const artifactPlan = d.planReconciliation({ targets: selectedAgents, roots: (0, registries_1.contentRoots)() });
|
|
159
|
+
const artifactPlan = d.planReconciliation({ targets: selectedAgents, roots: (0, registries_1.contentRoots)(), reporter });
|
|
158
160
|
artifactResult = d.applyInstallPlan(artifactPlan);
|
|
159
161
|
}
|
|
160
162
|
catch (e) {
|
|
@@ -250,6 +250,21 @@ function applyRequestToState(s, env, digest, repoRoot) {
|
|
|
250
250
|
(0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: trackId });
|
|
251
251
|
return;
|
|
252
252
|
}
|
|
253
|
+
if (env.kind === 'track-teardown-request') {
|
|
254
|
+
const p = env.payload;
|
|
255
|
+
if (typeof p.trackId !== 'string' || p.trackId.length === 0)
|
|
256
|
+
throw new Error('track-teardown-request requiere trackId');
|
|
257
|
+
const trackId = p.trackId;
|
|
258
|
+
const ref = s.tracks?.find((t) => t.trackId === trackId);
|
|
259
|
+
if (ref === undefined)
|
|
260
|
+
throw new Error(`track-teardown-request: track desconocido: ${trackId}`);
|
|
261
|
+
if (s.tracks === undefined || s.tracks.length < 2 || s.cohortPhase === undefined) {
|
|
262
|
+
throw new Error('track-teardown-request requiere una cohorte válida de al menos dos tracks');
|
|
263
|
+
}
|
|
264
|
+
ref.teardownRequested = true;
|
|
265
|
+
(0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: trackId });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
253
268
|
if (env.kind === 'track-freeze-request') {
|
|
254
269
|
// R5.2/R6.3 (Task 10): request administrativa CROSS-JOURNAL — el
|
|
255
270
|
// supervisor del PLAN la emite directamente al `requestsDir` de ESTE
|
|
@@ -135,13 +135,22 @@ function applyProtocolToState(state, protocol) {
|
|
|
135
135
|
// `expectedPlanHeadSha` incorrecto.
|
|
136
136
|
if (protocol.planHeadSha !== undefined)
|
|
137
137
|
next.cohortPlanHeadSha = protocol.planHeadSha;
|
|
138
|
-
// R7/C3 (Task 12):
|
|
139
|
-
// que
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (protocol.
|
|
144
|
-
next.
|
|
138
|
+
// R7/C3 (Task 12): los valores finales normalmente solo avanzan, salvo
|
|
139
|
+
// que fallback abandone explícitamente esa ruta. En ese caso el reducer
|
|
140
|
+
// ya invalidó su evidencia y el mirror durable DEBE borrarla también;
|
|
141
|
+
// conservarla permitiría que un restart reconstruyera un protocolo que
|
|
142
|
+
// contradice los tracks en teardown.
|
|
143
|
+
if (protocol.cohortPhase === 'FALLBACK_PENDING') {
|
|
144
|
+
next.globalQaHeadSha = undefined;
|
|
145
|
+
next.finalIntegrationJobId = undefined;
|
|
146
|
+
next.qaFinalizeRequested = undefined;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
if (protocol.globalQaHeadSha !== undefined)
|
|
150
|
+
next.globalQaHeadSha = protocol.globalQaHeadSha;
|
|
151
|
+
if (protocol.finalIntegrationJobId !== undefined)
|
|
152
|
+
next.finalIntegrationJobId = protocol.finalIntegrationJobId;
|
|
153
|
+
}
|
|
145
154
|
if (protocol.fallbackReason !== undefined)
|
|
146
155
|
next.cohortFallbackReason = protocol.fallbackReason;
|
|
147
156
|
next.tracks = (next.tracks ?? []).map((ref) => {
|
|
@@ -818,6 +827,46 @@ async function reconcileTracks(planRoot, branch, state, runtime, maxParallel) {
|
|
|
818
827
|
if (s.tracks === undefined || s.tracks.length < 2 || s.cohortPhase === undefined) {
|
|
819
828
|
return { state: s, effectExecuted: null };
|
|
820
829
|
}
|
|
830
|
+
// La request de teardown se consume en dos pasos durables, igual que
|
|
831
|
+
// join: `apply.ts` solo deja el marcador y este reconciler traduce la
|
|
832
|
+
// intención al reducer. BLOCKED, SERIAL y COMPLETE no admiten nueva
|
|
833
|
+
// transición: limpiar únicamente el marcador preserva el estado seguro.
|
|
834
|
+
const pendingTeardown = (s.tracks ?? []).find((track) => track.teardownRequested === true);
|
|
835
|
+
if (pendingTeardown !== undefined) {
|
|
836
|
+
const moot = ['BLOCKED', 'SERIAL', 'COMPLETE'].includes(s.cohortPhase);
|
|
837
|
+
if (moot) {
|
|
838
|
+
const next = structuredClone(s);
|
|
839
|
+
for (const track of next.tracks ?? [])
|
|
840
|
+
if (track.trackId === pendingTeardown.trackId)
|
|
841
|
+
track.teardownRequested = undefined;
|
|
842
|
+
s = persist(planRoot, branch, next);
|
|
843
|
+
(0, store_1.appendEvent)(planRoot, branch, {
|
|
844
|
+
kind: 'track-teardown-moot', trackId: pendingTeardown.trackId, phase: s.cohortPhase,
|
|
845
|
+
});
|
|
846
|
+
return { state: s, effectExecuted: null };
|
|
847
|
+
}
|
|
848
|
+
const protocolBefore = toProtocol(s, maxParallel);
|
|
849
|
+
const observed = (0, protocol_1.reconcileProtocol)(protocolBefore, { kind: 'teardown-requested', trackId: pendingTeardown.trackId });
|
|
850
|
+
const next = applyProtocolToState(s, observed);
|
|
851
|
+
for (const track of next.tracks ?? [])
|
|
852
|
+
if (track.trackId === pendingTeardown.trackId)
|
|
853
|
+
track.teardownRequested = undefined;
|
|
854
|
+
s = persist(planRoot, branch, next);
|
|
855
|
+
// El lock pertenece al proceso vivo que alcanzó la integración.
|
|
856
|
+
// Al abandonar FINAL_INTERLOCK debe soltarse en este mismo proceso;
|
|
857
|
+
// tras un crash el lease muerto se reclama por identidad al iniciar.
|
|
858
|
+
if (protocolBefore.cohortPhase === 'FINAL_INTERLOCK' && s.cohortPhase === 'FALLBACK_PENDING') {
|
|
859
|
+
runtime.releaseIntegrationLockIfHeld();
|
|
860
|
+
}
|
|
861
|
+
(0, store_1.appendEvent)(planRoot, branch, {
|
|
862
|
+
kind: 'track-teardown-observed', trackId: pendingTeardown.trackId,
|
|
863
|
+
phase: s.tracks?.find((t) => t.trackId === pendingTeardown.trackId)?.phase,
|
|
864
|
+
});
|
|
865
|
+
// El marcador es una frontera durable por sí mismo. No se consume
|
|
866
|
+
// otro marcador ni se entra a begin-teardown hasta la siguiente
|
|
867
|
+
// llamada, para que un crash conserve el resto de las requests.
|
|
868
|
+
return { state: s, effectExecuted: null };
|
|
869
|
+
}
|
|
821
870
|
// El pedido de join del controller (`joinRequested`, marcado declarativamente al
|
|
822
871
|
// consumir `track-join-request`) se convierte ACÁ en la observación que el reducer
|
|
823
872
|
// entiende. La regla de si eso mueve la fase vive en `reconcileProtocol` (solo
|
|
@@ -28,8 +28,8 @@ function bundleArtifacts(b, contentDir) {
|
|
|
28
28
|
// estructural sobre la ruta ya resuelta.
|
|
29
29
|
const refs = [];
|
|
30
30
|
for (const s of b.skills) {
|
|
31
|
-
(0, artifact_name_1.assertSafeArtifactName)(s
|
|
32
|
-
refs.push({ name: s
|
|
31
|
+
(0, artifact_name_1.assertSafeArtifactName)(s, 'skill');
|
|
32
|
+
refs.push({ name: s, type: 'skill', installName: s, sourcePath: path_1.default.join(contentDir, 'skills', s) });
|
|
33
33
|
}
|
|
34
34
|
for (const w of b.workflows) {
|
|
35
35
|
(0, artifact_name_1.assertSafeArtifactName)(w, 'workflow');
|
package/dist/src/core/bundles.js
CHANGED
|
@@ -4,15 +4,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.readCatalog = readCatalog;
|
|
7
|
+
exports.inspectBundles = inspectBundles;
|
|
8
|
+
exports.createBundleDiagnosticReporter = createBundleDiagnosticReporter;
|
|
7
9
|
exports.discoverBundles = discoverBundles;
|
|
8
10
|
exports.resolveBundleSkills = resolveBundleSkills;
|
|
9
11
|
exports.resolveBundleAgents = resolveBundleAgents;
|
|
10
12
|
exports.defaultScopeForBundle = defaultScopeForBundle;
|
|
13
|
+
exports.inspectAllBundles = inspectAllBundles;
|
|
11
14
|
exports.discoverAllBundles = discoverAllBundles;
|
|
12
15
|
exports.resolveBundleClosure = resolveBundleClosure;
|
|
13
16
|
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
14
18
|
const path_1 = __importDefault(require("path"));
|
|
15
19
|
const registries_1 = require("./registries");
|
|
20
|
+
const text_1 = require("./text");
|
|
16
21
|
function catalogPath(contentDir) {
|
|
17
22
|
return path_1.default.join(contentDir, 'catalog.json');
|
|
18
23
|
}
|
|
@@ -85,29 +90,63 @@ function readCatalog(contentDir) {
|
|
|
85
90
|
// TypeError opaco mas adelante; se descarta aca, nombrando el archivo.
|
|
86
91
|
return bundles.filter((entry) => entry !== null && typeof entry === 'object' && typeof entry.source === 'string');
|
|
87
92
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
function safeManifestIdentity(manifestPath) {
|
|
94
|
+
const sanitized = (0, text_1.sanitizeDiagnosticText)(manifestPath);
|
|
95
|
+
if (sanitized.length <= 240)
|
|
96
|
+
return sanitized;
|
|
97
|
+
const suffix = crypto_1.default.createHash('sha256').update(sanitized).digest('hex').slice(0, 16);
|
|
98
|
+
return `${sanitized.slice(0, 220)}…#${suffix}`;
|
|
99
|
+
}
|
|
100
|
+
function boundedDiagnostic(diagnostic) {
|
|
101
|
+
return (0, text_1.sanitizeDiagnosticText)(diagnostic).slice(0, 512);
|
|
102
|
+
}
|
|
103
|
+
/** Normaliza la única forma de compatibilidad CLI 9 antes de que el manifest
|
|
104
|
+
* alcance consumidores operativos. Los objetos nunca salen de este límite. */
|
|
105
|
+
function normalizeSkillRefs(raw, manifestPath) {
|
|
106
|
+
const identity = safeManifestIdentity(manifestPath);
|
|
107
|
+
if (!Array.isArray(raw)) {
|
|
108
|
+
throw new Error(`${identity}: "skills" must be an array.`);
|
|
109
|
+
}
|
|
110
|
+
const skills = [];
|
|
111
|
+
let legacyCount = 0;
|
|
112
|
+
raw.forEach((ref, index) => {
|
|
113
|
+
const location = `${identity}: skills[${index}]`;
|
|
114
|
+
if (typeof ref === 'string') {
|
|
115
|
+
if (ref === '')
|
|
116
|
+
throw new Error(`${location} must be a non-empty string.`);
|
|
117
|
+
skills.push(ref);
|
|
118
|
+
return;
|
|
99
119
|
}
|
|
100
|
-
|
|
120
|
+
if (ref === null || typeof ref !== 'object' || Array.isArray(ref)) {
|
|
121
|
+
throw new Error(`${location} must be a non-empty string or a supported legacy object.`);
|
|
122
|
+
}
|
|
123
|
+
const legacy = ref;
|
|
124
|
+
const keys = Object.keys(legacy);
|
|
125
|
+
if (!keys.every((key) => key === 'name' || key === 'onSignal')) {
|
|
126
|
+
throw new Error(`${location} legacy object may contain only "name" and optional "onSignal".`);
|
|
127
|
+
}
|
|
128
|
+
if (typeof legacy.name !== 'string' || legacy.name === '') {
|
|
129
|
+
throw new Error(`${location} legacy object "name" must be a non-empty string.`);
|
|
130
|
+
}
|
|
131
|
+
if (legacy.onSignal !== undefined && typeof legacy.onSignal !== 'boolean') {
|
|
132
|
+
throw new Error(`${location} legacy object "onSignal" must be a boolean.`);
|
|
133
|
+
}
|
|
134
|
+
skills.push(legacy.name);
|
|
135
|
+
legacyCount++;
|
|
101
136
|
});
|
|
137
|
+
const diagnostics = legacyCount === 0 ? [] : [boundedDiagnostic(`${identity}: ${legacyCount} legacy object skill reference(s) accepted for CLI 9; ` +
|
|
138
|
+
'replace with canonical skills: ["skill-name"]. Legacy object support is removed in v10.')];
|
|
139
|
+
return { skills, diagnostics };
|
|
102
140
|
}
|
|
103
141
|
/** Array de strings, o vacio. Cualquier otra forma en el contenido del registry
|
|
104
142
|
* se ignora en vez de propagarse hasta un crash aguas abajo. */
|
|
105
143
|
function stringArray(raw) {
|
|
106
144
|
return Array.isArray(raw) ? raw.filter((v) => typeof v === 'string') : [];
|
|
107
145
|
}
|
|
108
|
-
function
|
|
146
|
+
function inspectBundles(contentDir) {
|
|
109
147
|
const entries = readCatalog(contentDir);
|
|
110
148
|
const bundles = [];
|
|
149
|
+
const diagnostics = [];
|
|
111
150
|
for (const entry of entries) {
|
|
112
151
|
const manifestPath = bundleManifestPath(contentDir, entry.source);
|
|
113
152
|
if (!(0, registries_1.assertRegularRegistryFile)(manifestPath))
|
|
@@ -128,6 +167,8 @@ function discoverBundles(contentDir) {
|
|
|
128
167
|
if (typeof m.name !== 'string' || m.name === '') {
|
|
129
168
|
throw new Error(`${manifestPath}: "name" must be a non-empty string.`);
|
|
130
169
|
}
|
|
170
|
+
const normalizedSkills = normalizeSkillRefs(m.skills, manifestPath);
|
|
171
|
+
diagnostics.push(...normalizedSkills.diagnostics);
|
|
131
172
|
bundles.push({
|
|
132
173
|
name: m.name,
|
|
133
174
|
description: typeof m.description === 'string' ? m.description : '',
|
|
@@ -135,13 +176,36 @@ function discoverBundles(contentDir) {
|
|
|
135
176
|
scope: m.scope ?? 'project',
|
|
136
177
|
visibility: m.visibility ?? 'public',
|
|
137
178
|
dependsOn: stringArray(m.dependsOn),
|
|
138
|
-
skills:
|
|
179
|
+
skills: normalizedSkills.skills,
|
|
139
180
|
workflows: stringArray(m.workflows),
|
|
140
181
|
agents: stringArray(m.agents),
|
|
141
182
|
contentRoot: contentDir,
|
|
142
183
|
});
|
|
143
184
|
}
|
|
144
|
-
return bundles;
|
|
185
|
+
return { bundles, diagnostics };
|
|
186
|
+
}
|
|
187
|
+
/** Creates the command-scoped CLI reporter used by discovery facades. */
|
|
188
|
+
function createBundleDiagnosticReporter(write = (message) => console.error(message)) {
|
|
189
|
+
const emitted = new Set();
|
|
190
|
+
return (diagnostic) => {
|
|
191
|
+
const safeDiagnostic = boundedDiagnostic(diagnostic);
|
|
192
|
+
if (emitted.has(safeDiagnostic))
|
|
193
|
+
return;
|
|
194
|
+
emitted.add(safeDiagnostic);
|
|
195
|
+
write(`warning: ${safeDiagnostic}`);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const defaultBundleDiagnosticReporter = createBundleDiagnosticReporter();
|
|
199
|
+
function reportDiagnostics(diagnostics, reporter) {
|
|
200
|
+
const emit = reporter ?? defaultBundleDiagnosticReporter;
|
|
201
|
+
for (const diagnostic of new Set(diagnostics))
|
|
202
|
+
emit(diagnostic);
|
|
203
|
+
}
|
|
204
|
+
/** Compatibility facade for callers that only need bundle definitions. */
|
|
205
|
+
function discoverBundles(contentDir, reporter) {
|
|
206
|
+
const result = inspectBundles(contentDir);
|
|
207
|
+
reportDiagnostics(result.diagnostics, reporter);
|
|
208
|
+
return result.bundles;
|
|
145
209
|
}
|
|
146
210
|
function resolveBundleSkills(bundleName, bundles) {
|
|
147
211
|
const byName = new Map(bundles.map((b) => [b.name, b]));
|
|
@@ -157,7 +221,7 @@ function resolveBundleSkills(bundleName, bundles) {
|
|
|
157
221
|
for (const dep of b.dependsOn)
|
|
158
222
|
visit(dep);
|
|
159
223
|
for (const s of b.skills)
|
|
160
|
-
skills.add(s
|
|
224
|
+
skills.add(s);
|
|
161
225
|
};
|
|
162
226
|
visit(bundleName);
|
|
163
227
|
return Array.from(skills);
|
|
@@ -192,11 +256,14 @@ function defaultScopeForBundle(scope) {
|
|
|
192
256
|
/** Descubre bundles de TODOS los roots (base + registries adicionales).
|
|
193
257
|
* Colisión de nombre entre roots: override declarado en awm-registry.json
|
|
194
258
|
* del root posterior → reemplaza; no declarado → error nombrando ambas fuentes. */
|
|
195
|
-
function
|
|
259
|
+
function inspectAllBundles(roots = (0, registries_1.contentRoots)()) {
|
|
196
260
|
const byName = new Map();
|
|
261
|
+
const diagnostics = [];
|
|
197
262
|
for (const root of roots) {
|
|
198
263
|
const overrides = (0, registries_1.readRegistryManifest)(root).overrides;
|
|
199
|
-
|
|
264
|
+
const result = inspectBundles(root);
|
|
265
|
+
diagnostics.push(...result.diagnostics);
|
|
266
|
+
for (const b of result.bundles) {
|
|
200
267
|
const prev = byName.get(b.name);
|
|
201
268
|
if (!prev) {
|
|
202
269
|
byName.set(b.name, b);
|
|
@@ -210,7 +277,13 @@ function discoverAllBundles(roots = (0, registries_1.contentRoots)()) {
|
|
|
210
277
|
`Remove or rename one of them, or declare "${b.name}" in "overrides" of the later registry's awm-registry.json.`);
|
|
211
278
|
}
|
|
212
279
|
}
|
|
213
|
-
return Array.from(byName.values());
|
|
280
|
+
return { bundles: Array.from(byName.values()), diagnostics: Array.from(new Set(diagnostics)) };
|
|
281
|
+
}
|
|
282
|
+
/** Compatibility facade for callers that only need bundle definitions. */
|
|
283
|
+
function discoverAllBundles(roots = (0, registries_1.contentRoots)(), reporter) {
|
|
284
|
+
const result = inspectAllBundles(roots);
|
|
285
|
+
reportDiagnostics(result.diagnostics, reporter);
|
|
286
|
+
return result.bundles;
|
|
214
287
|
}
|
|
215
288
|
/**
|
|
216
289
|
* Resolves the dependency closure of a bundle in deps-first order, deduped.
|
|
@@ -307,7 +307,7 @@ function gatherProject(root, bundles, agent = 'claude-code') {
|
|
|
307
307
|
}
|
|
308
308
|
function gatherContext(opts = {}) {
|
|
309
309
|
const cwd = opts.cwd ?? process.cwd();
|
|
310
|
-
const bundles = opts.bundles ?? (0, bundles_1.discoverAllBundles)();
|
|
310
|
+
const bundles = opts.bundles ?? (0, bundles_1.discoverAllBundles)(undefined, opts.reporter);
|
|
311
311
|
const agent = opts.agent ?? 'claude-code';
|
|
312
312
|
const root = (0, profile_1.findProjectRoot)(cwd);
|
|
313
313
|
const agents = opts.agents ?? [agent];
|
|
@@ -23,7 +23,7 @@ function runExport(opts) {
|
|
|
23
23
|
}
|
|
24
24
|
const roots = opts.roots ?? (0, registries_1.contentRoots)();
|
|
25
25
|
const outDir = path_1.default.join(opts.out ?? path_1.default.join(process.cwd(), 'awm-export'), target);
|
|
26
|
-
const resolution = (0, resolve_1.resolveExport)(opts.name, roots);
|
|
26
|
+
const resolution = (0, resolve_1.resolveExport)(opts.name, roots, opts.reporter);
|
|
27
27
|
fs_1.default.mkdirSync(outDir, { recursive: true });
|
|
28
28
|
const exported = [];
|
|
29
29
|
let zipAvailable = true;
|
|
@@ -44,8 +44,8 @@ function assertOverrideConsistency(s) {
|
|
|
44
44
|
throw new Error(`Inconsistent metadata for skill "${s.name}": ${OVERRIDE_FILE} exists but SKILL.md does not declare portable: true.`);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
function resolveExport(requested, roots) {
|
|
48
|
-
const bundles = (0, bundles_1.discoverAllBundles)(roots);
|
|
47
|
+
function resolveExport(requested, roots, reporter) {
|
|
48
|
+
const bundles = (0, bundles_1.discoverAllBundles)(roots, reporter);
|
|
49
49
|
if (bundles.some((b) => b.name === requested)) {
|
|
50
50
|
const skills = [];
|
|
51
51
|
const skipped = [];
|
|
@@ -246,6 +246,7 @@ function isWellFormedTrackRef(x) {
|
|
|
246
246
|
&& (x.supervisorProcessRef === undefined || isWellFormedProcessRef(x.supervisorProcessRef))
|
|
247
247
|
&& (x.joinIntent === undefined || isWellFormedJoinIntent(x.joinIntent))
|
|
248
248
|
&& (x.teardownIntent === undefined || isWellFormedTeardownIntent(x.teardownIntent))
|
|
249
|
+
&& (x.teardownRequested === undefined || typeof x.teardownRequested === 'boolean')
|
|
249
250
|
&& (x.joinedCommitSha === undefined || typeof x.joinedCommitSha === 'string')
|
|
250
251
|
&& (x.blockedReason === undefined || typeof x.blockedReason === 'string');
|
|
251
252
|
}
|
|
@@ -16,7 +16,7 @@ function intentKey(intent) {
|
|
|
16
16
|
*/
|
|
17
17
|
function planReconciliation(params) {
|
|
18
18
|
const { targets, roots } = params;
|
|
19
|
-
const bundles = params.bundles ?? (0, bundles_1.discoverAllBundles)(roots);
|
|
19
|
+
const bundles = params.bundles ?? (0, bundles_1.discoverAllBundles)(roots, params.reporter);
|
|
20
20
|
const contentDir = roots[0] ?? '';
|
|
21
21
|
const machineBundles = bundles.filter((b) => b.scope === 'baseline' || b.scope === 'ambient');
|
|
22
22
|
const seen = new Set();
|