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.
Files changed (55) hide show
  1. package/README.md +2 -0
  2. package/dist/src/commands/doctor.js +4 -2
  3. package/dist/src/commands/export.js +3 -0
  4. package/dist/src/commands/init.js +10 -2
  5. package/dist/src/commands/registry/add.js +2 -2
  6. package/dist/src/commands/registry/index.js +7 -5
  7. package/dist/src/commands/registry/install-bundles.js +4 -4
  8. package/dist/src/commands/registry/status.js +4 -4
  9. package/dist/src/commands/sync.js +2 -1
  10. package/dist/src/commands/track/index.js +3 -7
  11. package/dist/src/commands/update.js +3 -1
  12. package/dist/src/commands/watch/apply.js +15 -0
  13. package/dist/src/commands/watch/tracks.js +56 -7
  14. package/dist/src/core/bundle-install.js +2 -2
  15. package/dist/src/core/bundles.js +92 -19
  16. package/dist/src/core/diagnostics/context.js +1 -1
  17. package/dist/src/core/export/index.js +1 -1
  18. package/dist/src/core/export/resolve.js +2 -2
  19. package/dist/src/core/journal/types.js +1 -0
  20. package/dist/src/core/reconciliation.js +1 -1
  21. package/dist/src/core/tracks/git.js +15 -3
  22. package/dist/src/core/tracks/protocol.js +26 -6
  23. package/dist/src/index.js +46 -39
  24. package/dist/src/utils/grouping.js +1 -1
  25. package/dist/src/utils/registry-view.js +1 -1
  26. package/dist/tests/commands/bundle-diagnostics.test.js +145 -0
  27. package/dist/tests/commands/export.test.js +28 -2
  28. package/dist/tests/commands/multi-agent-targeting.test.js +1 -1
  29. package/dist/tests/commands/track/verbs.test.js +6 -0
  30. package/dist/tests/commands/watch/apply.test.js +46 -9
  31. package/dist/tests/commands/watch/track-remove-request.test.js +165 -0
  32. package/dist/tests/commands/watch/track-teardown-crash.test.js +27 -0
  33. package/dist/tests/core/bundle-install.test.js +36 -0
  34. package/dist/tests/core/bundles-multiroot.test.js +24 -2
  35. package/dist/tests/core/bundles-overrides.test.js +10 -7
  36. package/dist/tests/core/bundles.test.js +75 -6
  37. package/dist/tests/core/diagnostics/context.test.js +2 -2
  38. package/dist/tests/core/export/engine.test.js +1 -1
  39. package/dist/tests/core/export/resolve.test.js +1 -1
  40. package/dist/tests/core/init/mutation-targets.test.js +1 -1
  41. package/dist/tests/core/init/orchestrator.test.js +1 -1
  42. package/dist/tests/core/init/steps.test.js +1 -1
  43. package/dist/tests/core/journal/types.test.js +11 -0
  44. package/dist/tests/core/reconciliation.test.js +1 -1
  45. package/dist/tests/core/sync-profile-atomicity.test.js +1 -1
  46. package/dist/tests/core/tracks/git.test.js +7 -0
  47. package/dist/tests/core/tracks/protocol.test.js +103 -0
  48. package/dist/tests/structural/bundle-skill-reference-contract.test.js +56 -0
  49. package/dist/tests/utils/grouping.test.js +2 -2
  50. package/dist/tests/utils/registry-view.test.js +7 -7
  51. package/package.json +1 -1
  52. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  53. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  54. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  55. 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
- // NO IMPLEMENTADO del lado del supervisor: no hay handler para `track-teardown-request`,
108
- // así que la request se emite y el supervisor la RECHAZA (`request-rejected-invalid`).
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('NO IMPLEMENTADO: emite track-teardown-request, que el supervisor todavía no sabe aplicar y rechaza')
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): mismo criterio que `cohortPlanHeadSha` arriba — valores
139
- // que solo avanzan (nunca retroceden a `undefined` una vez fijados por
140
- // `reconcileProtocol`).
141
- if (protocol.globalQaHeadSha !== undefined)
142
- next.globalQaHeadSha = protocol.globalQaHeadSha;
143
- if (protocol.finalIntegrationJobId !== undefined)
144
- next.finalIntegrationJobId = protocol.finalIntegrationJobId;
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.name, 'skill');
32
- refs.push({ name: s.name, type: 'skill', installName: s.name, sourcePath: path_1.default.join(contentDir, 'skills', s.name) });
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');
@@ -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
- /** Tolera `skills` ausente o mal formado: lo que no sea un array se trata como
89
- * vacio en vez de tirar `(raw ?? []).map is not a function`, y cada entrada se
90
- * valida individualmente. */
91
- function normalizeSkillRefs(raw) {
92
- if (!Array.isArray(raw))
93
- return [];
94
- return raw.flatMap((s) => {
95
- if (typeof s === 'string')
96
- return [{ name: s, onSignal: false }];
97
- if (s !== null && typeof s === 'object' && typeof s.name === 'string') {
98
- return [{ name: s.name, onSignal: s.onSignal === true }];
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
- return [];
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 discoverBundles(contentDir) {
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: normalizeSkillRefs(m.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.name);
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 discoverAllBundles(roots = (0, registries_1.contentRoots)()) {
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
- for (const b of discoverBundles(root)) {
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();