agentic-workflow-manager 9.7.0 → 9.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +5 -1
  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/job/exec-wrapper.js +53 -3
  6. package/dist/src/commands/plan/index.js +124 -6
  7. package/dist/src/commands/registry/add.js +2 -2
  8. package/dist/src/commands/registry/index.js +7 -5
  9. package/dist/src/commands/registry/install-bundles.js +4 -4
  10. package/dist/src/commands/registry/status.js +4 -4
  11. package/dist/src/commands/sensors/bootstrap.js +36 -5
  12. package/dist/src/commands/sensors/compatibility/contract.js +19 -4
  13. package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
  14. package/dist/src/commands/sensors/compatibility/live.js +18 -1
  15. package/dist/src/commands/sensors/compatibility/probe.js +8 -2
  16. package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
  17. package/dist/src/commands/sensors/coverage/evidence.js +19 -10
  18. package/dist/src/commands/sensors/coverage/index.js +11 -2
  19. package/dist/src/commands/sensors/coverage/resolve.js +10 -0
  20. package/dist/src/commands/sensors/exec.js +16 -1
  21. package/dist/src/commands/sensors/migrate.js +62 -2
  22. package/dist/src/commands/sensors/prepare.js +2 -0
  23. package/dist/src/commands/sensors/result.js +2 -0
  24. package/dist/src/commands/sensors/run.js +64 -1
  25. package/dist/src/commands/sensors/status.js +6 -4
  26. package/dist/src/commands/sync.js +2 -1
  27. package/dist/src/commands/update.js +3 -1
  28. package/dist/src/commands/watch/archive-unused.js +138 -0
  29. package/dist/src/commands/watch/generations.js +32 -9
  30. package/dist/src/commands/watch/index.js +108 -2
  31. package/dist/src/commands/watch/init.js +149 -18
  32. package/dist/src/commands/watch/supervisor.js +167 -13
  33. package/dist/src/commands/watch/teardown-driver.js +13 -13
  34. package/dist/src/commands/watch/track-state.js +59 -0
  35. package/dist/src/commands/watch/tracks.js +38 -102
  36. package/dist/src/core/admission/index.js +181 -0
  37. package/dist/src/core/admission/registry-contracts.js +290 -0
  38. package/dist/src/core/bundle-install.js +2 -2
  39. package/dist/src/core/bundles.js +92 -19
  40. package/dist/src/core/diagnostics/context.js +1 -1
  41. package/dist/src/core/export/index.js +1 -1
  42. package/dist/src/core/export/resolve.js +2 -2
  43. package/dist/src/core/journal/fingerprint.js +35 -1
  44. package/dist/src/core/journal/paths.js +11 -0
  45. package/dist/src/core/journal/store.js +146 -4
  46. package/dist/src/core/journal/types.js +35 -1
  47. package/dist/src/core/migration/historical-provenance.js +18 -0
  48. package/dist/src/core/migration/index.js +322 -0
  49. package/dist/src/core/plan/identity.js +75 -0
  50. package/dist/src/core/plan/validate.js +178 -64
  51. package/dist/src/core/reconciliation.js +1 -1
  52. package/dist/src/index.js +46 -39
  53. package/dist/src/utils/grouping.js +1 -1
  54. package/dist/src/utils/registry-view.js +1 -1
  55. package/dist/tests/commands/bundle-diagnostics.test.js +145 -0
  56. package/dist/tests/commands/export.test.js +28 -2
  57. package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
  58. package/dist/tests/commands/job/gate-reconcile.test.js +24 -10
  59. package/dist/tests/commands/multi-agent-targeting.test.js +1 -1
  60. package/dist/tests/commands/pin.test.js +23 -23
  61. package/dist/tests/commands/plan/index.test.js +630 -14
  62. package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
  63. package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
  64. package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
  65. package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
  66. package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
  67. package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
  68. package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
  69. package/dist/tests/commands/sensors/exec.test.js +112 -0
  70. package/dist/tests/commands/sensors/run-changed.test.js +11 -0
  71. package/dist/tests/commands/sensors/run.test.js +15 -2
  72. package/dist/tests/commands/sensors/status.test.js +3 -3
  73. package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
  74. package/dist/tests/commands/watch/generations.test.js +113 -4
  75. package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
  76. package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
  77. package/dist/tests/commands/watch/track-finalize.test.js +20 -3
  78. package/dist/tests/commands/watch/track-freeze.test.js +43 -30
  79. package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
  80. package/dist/tests/commands/watch/watch-init.test.js +244 -0
  81. package/dist/tests/core/admission/index.test.js +87 -0
  82. package/dist/tests/core/bundle-install.test.js +36 -0
  83. package/dist/tests/core/bundles-multiroot.test.js +24 -2
  84. package/dist/tests/core/bundles-overrides.test.js +10 -7
  85. package/dist/tests/core/bundles.test.js +75 -6
  86. package/dist/tests/core/diagnostics/context.test.js +2 -2
  87. package/dist/tests/core/export/engine.test.js +1 -1
  88. package/dist/tests/core/export/resolve.test.js +1 -1
  89. package/dist/tests/core/init/mutation-targets.test.js +1 -1
  90. package/dist/tests/core/init/orchestrator.test.js +1 -1
  91. package/dist/tests/core/init/steps.test.js +1 -1
  92. package/dist/tests/core/journal/fingerprint.test.js +22 -0
  93. package/dist/tests/core/journal/store.test.js +53 -0
  94. package/dist/tests/core/journal/types.test.js +7 -0
  95. package/dist/tests/core/migration/facts.test.js +289 -0
  96. package/dist/tests/core/migration/historical-provenance.test.js +25 -0
  97. package/dist/tests/core/plan/validate.test.js +320 -6
  98. package/dist/tests/core/reconciliation.test.js +1 -1
  99. package/dist/tests/core/sync-profile-atomicity.test.js +1 -1
  100. package/dist/tests/integration/pack-e2e.test.js +4 -1
  101. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
  102. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
  103. package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
  104. package/dist/tests/providers/injection-config.test.js +14 -6
  105. package/dist/tests/structural/bundle-skill-reference-contract.test.js +56 -0
  106. package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
  107. package/dist/tests/structural/native-release-artifacts.test.js +16 -0
  108. package/dist/tests/utils/grouping.test.js +2 -2
  109. package/dist/tests/utils/registry-view.test.js +7 -7
  110. package/package.json +2 -2
  111. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  112. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  113. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  114. package/prebuilds/win32-x64/secure_fs.node +0 -0
package/README.md CHANGED
@@ -41,9 +41,13 @@ This generates the runnable output under `dist/index.js`.
41
41
  ### 4. Running the Tests
42
42
  Before submitting a PR, ensure that the core discovery/executor functionality passes the existing test suite:
43
43
  ```bash
44
+ npm run native:build
44
45
  npm test
45
46
  ```
46
- The goal is to maintain 100% passing tests for any new CLI parameter flags.
47
+ `native:build` creates the ignored, host-specific `secure-fs` artifact required
48
+ by the integration fixtures; it must be run again after a clean checkout or a
49
+ change under `native/`. The goal is to maintain 100% passing tests for any new
50
+ CLI parameter flags.
47
51
 
48
52
  ### 5. Local Execution (Testing changes locally)
49
53
 
@@ -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
@@ -17,19 +17,33 @@ const fs_1 = __importDefault(require("fs"));
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const child_process_1 = require("child_process");
19
19
  const process_1 = require("../../core/journal/process");
20
+ const paths_1 = require("../../core/journal/paths");
21
+ const types_1 = require("../../core/journal/types");
22
+ const native_bridge_1 = require("../../core/secure-fs/native-bridge");
20
23
  const fingerprint_1 = require("../../core/journal/fingerprint");
21
24
  const redact_1 = require("../../core/journal/redact");
22
25
  const atomic_file_1 = require("../../core/atomic-file");
26
+ function validateSidecarIdentity(logsRoot, jobId, nonce) {
27
+ if (typeof logsRoot !== 'string' || logsRoot.length === 0 || logsRoot.length > 4096 || /[\u0000-\u001F\u007F]/.test(logsRoot)
28
+ || typeof jobId !== 'string' || !/^[A-Za-z0-9_-][A-Za-z0-9_.-]{0,127}$/.test(jobId)
29
+ || typeof nonce !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(nonce)
30
+ || jobId.length + nonce.length > 230)
31
+ throw new Error('unsafe or unbounded sidecar identity');
32
+ }
23
33
  function claimPath(logsRoot, jobId, nonce) {
34
+ validateSidecarIdentity(logsRoot, jobId, nonce);
24
35
  return path_1.default.join(logsRoot, `${jobId}.${nonce}.claim`);
25
36
  }
26
37
  function identityPath(logsRoot, jobId, nonce) {
38
+ validateSidecarIdentity(logsRoot, jobId, nonce);
27
39
  return path_1.default.join(logsRoot, `${jobId}.${nonce}.identity.json`);
28
40
  }
29
41
  function resultPath(logsRoot, jobId, nonce) {
42
+ validateSidecarIdentity(logsRoot, jobId, nonce);
30
43
  return path_1.default.join(logsRoot, `${jobId}.${nonce}.result.json`);
31
44
  }
32
45
  function logPath(logsRoot, jobId, nonce) {
46
+ validateSidecarIdentity(logsRoot, jobId, nonce);
33
47
  return path_1.default.join(logsRoot, `${jobId}.${nonce}.log`);
34
48
  }
35
49
  function replayVerdict(logsRoot, jobId, nonce) {
@@ -45,11 +59,46 @@ const MAX_LOG_BYTES = 1024 * 1024; // retencion acotada (R2.5)
45
59
  // dispara, tipicamente entregados en 1 tick del event loop) sin acercarse a
46
60
  // una espera perceptible si un descendiente hereda los fds y nunca cierra.
47
61
  const STDIO_GRACE_MS = 300;
62
+ /** Only a committed generation intent may suppress controller output. A user
63
+ * job with the same name retains normal verifier logs. Never persist model
64
+ * output (which may include prompts/source bodies) in generation custody. */
65
+ function isCommittedController(logsRoot, repoRoot, jobId, nonce, argv) {
66
+ if (!/^controller-gen-[1-9][0-9]*$/.test(jobId))
67
+ return false;
68
+ try {
69
+ const bytes = native_bridge_1.secureFs.readRegularFile(path_1.default.resolve(logsRoot, '..', 'state.json'), 1024 * 1024).bytes;
70
+ const state = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
71
+ if (!(0, types_1.isWellFormedState)(state))
72
+ throw new Error('controller custody journal invalid');
73
+ const actual = fs_1.default.lstatSync(logsRoot, { bigint: true });
74
+ const expected = fs_1.default.lstatSync((0, paths_1.logsDir)(repoRoot, state.branch), { bigint: true });
75
+ if (!actual.isDirectory() || actual.isSymbolicLink() || !expected.isDirectory() || expected.isSymbolicLink()
76
+ || actual.ino === 0n || actual.dev !== expected.dev || actual.ino !== expected.ino)
77
+ throw new Error('controller custody directory identity invalid');
78
+ const committed = state.generations.some(gen => gen.controllerJobId === jobId && jobId === `controller-gen-${gen.n}`
79
+ && gen.spawnNonce === nonce && gen.resumePrompt === undefined && (0, types_1.isControllerRecoveryAction)(gen.resumeAction)
80
+ && gen.launchArgvDigest === (0, process_1.argvDigest)(argv));
81
+ if (committed)
82
+ return true;
83
+ const ordinary = state.jobs[jobId];
84
+ if (ordinary?.spawnNonce === nonce && (0, process_1.argvDigest)(ordinary.argv) === (0, process_1.argvDigest)(argv))
85
+ return false;
86
+ throw new Error('controller custody launch identity unproven');
87
+ }
88
+ catch {
89
+ throw new Error('controller custody identity unavailable or unsafe: refusing launch before claim');
90
+ }
91
+ }
48
92
  async function runExecWrapper(opts) {
49
93
  const { logsRoot, jobId, nonce, argv, cwd } = opts;
94
+ validateSidecarIdentity(logsRoot, jobId, nonce);
50
95
  const repoRoot = opts.repoRoot ?? process.cwd();
51
- if (argv.length === 0)
52
- throw new Error('argv vacio');
96
+ if (!Array.isArray(argv) || argv.length === 0 || argv.length > 256
97
+ || argv.some(argument => typeof argument !== 'string' || argument.length > 65536 || argument.includes('\0'))
98
+ || argv[0].length === 0 || argv.reduce((bytes, argument) => bytes + Buffer.byteLength(argument, 'utf8'), 0) > 1024 * 1024)
99
+ throw new Error('argv invalid or unbounded');
100
+ const safeCwd = (0, fingerprint_1.resolveWorkingDirectory)(repoRoot, cwd).absolute;
101
+ const omitControllerOutput = isCommittedController(logsRoot, repoRoot, jobId, nonce, argv);
53
102
  fs_1.default.mkdirSync(logsRoot, { recursive: true, mode: 0o700 });
54
103
  // (1) claim exclusivo DURABLE — wx + fsync de archivo y de directorio
55
104
  let fd;
@@ -77,7 +126,6 @@ async function runExecWrapper(opts) {
77
126
  // process group por job, independiente del supervisor (R4.7: shell:false,
78
127
  // argv como array, secretos solo por referencia de entorno).
79
128
  const [exe, ...args] = argv;
80
- const safeCwd = (0, fingerprint_1.resolveWorkingDirectory)(repoRoot, cwd).absolute;
81
129
  const child = (0, child_process_1.spawn)(exe, args, {
82
130
  cwd: safeCwd, shell: false, detached: true,
83
131
  env: { ...process.env, [process_1.NONCE_ENV]: nonce },
@@ -105,6 +153,8 @@ async function runExecWrapper(opts) {
105
153
  const outputChunks = [];
106
154
  const logFile = logPath(logsRoot, jobId, nonce);
107
155
  const capture = (chunk) => {
156
+ if (omitControllerOutput)
157
+ return;
108
158
  if (captured >= MAX_LOG_BYTES)
109
159
  return;
110
160
  const accepted = chunk.subarray(0, MAX_LOG_BYTES - captured);
@@ -1,13 +1,39 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.exitCodeFor = exitCodeFor;
4
7
  exports.formatReport = formatReport;
5
8
  exports.registerPlanCommand = registerPlanCommand;
6
9
  const validate_1 = require("../../core/plan/validate");
10
+ const admission_1 = require("../../core/admission");
11
+ const check_1 = require("../../core/currentness/check");
12
+ const run_1 = require("../sensors/run");
13
+ const config_1 = require("../../utils/config");
14
+ const providers_1 = require("../../providers");
15
+ const registries_1 = require("../../core/registries");
16
+ const registry_contracts_1 = require("../../core/admission/registry-contracts");
17
+ const path_1 = __importDefault(require("path"));
18
+ const child_process_1 = require("child_process");
19
+ const store_1 = require("../../core/journal/store");
20
+ const migration_1 = require("../../core/migration");
7
21
  const SUPPORTED_SCHEMA = 'compact-slices/v1';
8
22
  const MAX_PATH_LENGTH = 4096;
9
23
  const MAX_DIAGNOSTICS = 20;
10
24
  const MAX_DIAGNOSTIC_LENGTH = 4096;
25
+ function journalObservation(cwd) {
26
+ try {
27
+ const branch = (0, child_process_1.execFileSync)('git', ['branch', '--show-current'], { cwd, encoding: 'utf8', stdio: 'pipe' }).trim();
28
+ if (!branch)
29
+ return { journalState: null, journalCorrupt: true };
30
+ const journal = (0, store_1.readJournal)(cwd, branch);
31
+ return { journalState: journal.state, journalCorrupt: journal.corrupt };
32
+ }
33
+ catch {
34
+ return { journalState: null, journalCorrupt: true };
35
+ }
36
+ }
11
37
  function assertText(value, name) {
12
38
  if (typeof value !== 'string' || value.length === 0 || value.length > MAX_PATH_LENGTH || value.startsWith('--') || /[\u0000-\u001F\u007F-\u009F]/.test(value)) {
13
39
  throw new Error(`${name} must be a non-empty path without control characters`);
@@ -19,6 +45,15 @@ function terminalSafe(value) {
19
45
  function assertDependencies(deps) {
20
46
  if (!deps || typeof deps.validatePlanFile !== 'function')
21
47
  throw new Error('validatePlanFile must be a function');
48
+ if (deps.admitPlan !== undefined && typeof deps.admitPlan !== 'function')
49
+ throw new Error('admitPlan must be a function');
50
+ }
51
+ function admissionOutput(report, json) {
52
+ report = (0, admission_1.sanitizeAdmissionReport)(report);
53
+ if (json)
54
+ return `${JSON.stringify(report)}\n`;
55
+ const diagnostic = report.diagnostics[0];
56
+ return `Plan admission: ${report.state}${report.provider ? ` (${terminalSafe(report.provider)})` : ''}${diagnostic ? `\n- ${terminalSafe(diagnostic.code)}: ${terminalSafe(diagnostic.message)}` : ''}\n`;
22
57
  }
23
58
  function boundedDiagnostics(diagnostics) {
24
59
  if (!Array.isArray(diagnostics))
@@ -34,16 +69,42 @@ function boundedDiagnostics(diagnostics) {
34
69
  };
35
70
  });
36
71
  }
72
+ function assertReport(report) {
73
+ if (!report || typeof report !== 'object' || Array.isArray(report))
74
+ throw new Error('plan validator returned an invalid report');
75
+ switch (report.state) {
76
+ case 'valid':
77
+ (0, validate_1.assertVerifiedValidPlanReport)(report);
78
+ return;
79
+ case 'migration-required':
80
+ if (report.reason !== 'unmarked-plan')
81
+ throw new Error('plan validator returned an invalid migration reason');
82
+ return;
83
+ case 'invalid':
84
+ boundedDiagnostics(report.diagnostics);
85
+ return;
86
+ case 'unsupported':
87
+ if (typeof report.schema !== 'string' || report.schema.length === 0 || report.schema.length > MAX_PATH_LENGTH) {
88
+ throw new Error('plan validator returned an invalid unsupported schema');
89
+ }
90
+ boundedDiagnostics(report.diagnostics);
91
+ return;
92
+ default:
93
+ throw new Error('plan validator returned an unknown report state');
94
+ }
95
+ }
37
96
  function reportPayload(report, planPath) {
97
+ assertReport(report);
38
98
  switch (report.state) {
39
99
  case 'valid':
40
100
  return {
41
101
  state: report.state, path: planPath, schema: report.schema, planId: report.manifest.planId,
102
+ planDigest: report.planDigest,
42
103
  requirements: report.manifest.requirements.length, sources: report.manifest.sources.length,
43
104
  commands: report.manifest.commands.length, slices: report.manifest.slices.length, completeOwnership: true,
44
105
  };
45
- case 'legacy':
46
- return { state: report.state, path: planPath, message: 'existing full-quality path applies; compact optimization was not requested.' };
106
+ case 'migration-required':
107
+ return { state: report.state, path: planPath, reason: report.reason };
47
108
  case 'invalid':
48
109
  return { state: report.state, path: planPath, diagnostics: boundedDiagnostics(report.diagnostics) };
49
110
  case 'unsupported':
@@ -56,15 +117,16 @@ function reportPayload(report, planPath) {
56
117
  }
57
118
  }
58
119
  function exitCodeFor(report) {
59
- return report.state === 'valid' || report.state === 'legacy' ? 0 : 2;
120
+ assertReport(report);
121
+ return report.state === 'valid' ? 0 : 2;
60
122
  }
61
123
  function formatReport(report, planPath) {
62
124
  const payload = reportPayload(report, planPath);
63
125
  switch (report.state) {
64
126
  case 'valid':
65
127
  return `Plan validation: valid "${terminalSafe(planPath)}" (${terminalSafe(report.schema)}; ${payload.slices} slices; ${payload.requirements} requirements; complete ownership)\n`;
66
- case 'legacy':
67
- return `Plan validation: legacy "${terminalSafe(planPath)}" — existing full-quality path applies; compact optimization was not requested.\n`;
128
+ case 'migration-required':
129
+ return `Plan validation: migration-required "${terminalSafe(planPath)}" (${terminalSafe(report.reason)})\nMigrate this plan to compact-slices/v1 before execution.\n`;
68
130
  case 'invalid':
69
131
  return `Plan validation: invalid "${terminalSafe(planPath)}"\n${payload.diagnostics.map(diagnostic => `- ${terminalSafe(diagnostic.code)}: ${terminalSafe(diagnostic.message)}${diagnostic.field ? ` (${terminalSafe(diagnostic.field)})` : ''}`).join('\n')}\n`;
70
132
  case 'unsupported':
@@ -73,11 +135,25 @@ function formatReport(report, planPath) {
73
135
  throw new Error('plan validator returned an unknown report state');
74
136
  }
75
137
  }
76
- function registerPlanCommand(program, deps = { validatePlanFile: validate_1.validatePlanFile }) {
138
+ function registerPlanCommand(program, deps = { validatePlanFile: validate_1.validatePlanFile, admitPlan: admission_1.admitPlan }) {
77
139
  if (!program || typeof program.command !== 'function')
78
140
  throw new Error('program must be a Commander command');
79
141
  assertDependencies(deps);
80
142
  const plan = program.command('plan').description('inspect plan contracts');
143
+ plan.command('migration-facts <plan-path>')
144
+ .description('collect read-only durable migration facts')
145
+ .option('--cwd <path>')
146
+ .option('--historical-root <path>', 'admitted sibling #148 worktree')
147
+ .requiredOption('--issue <https-url...>', 'durable issue links, including #126')
148
+ .option('--json')
149
+ .action((planPath, options) => {
150
+ assertText(planPath, 'plan path');
151
+ const cwd = options.cwd ?? process.cwd();
152
+ assertText(cwd, '--cwd');
153
+ const report = options.historicalRoot ? (0, migration_1.collectIssue148HistoricalFacts)(options.historicalRoot, options.issue) : (deps.collectMigrationFacts ?? migration_1.collectMigrationFacts)(planPath, cwd, options.issue);
154
+ process.stdout.write(options.json ? `${JSON.stringify(report)}\n` : `Migration facts: ${report.state}\n`);
155
+ process.exitCode = report.state === 'supported-completion' ? 0 : 2;
156
+ });
81
157
  plan
82
158
  .command('validate <plan-path>')
83
159
  .description('validate a compact plan without modifying it')
@@ -95,4 +171,46 @@ function registerPlanCommand(program, deps = { validatePlanFile: validate_1.vali
95
171
  const code = exitCodeFor(report);
96
172
  process.exitCode = code;
97
173
  });
174
+ plan
175
+ .command('admit <plan-path>')
176
+ .description('read-only fail-closed compact-plan admission')
177
+ .requiredOption('--provider <target>', 'target provider')
178
+ .requiredOption('--cwd <path>', 'repository root for plan containment and source resolution')
179
+ .option('--execution-mode <mode>', 'explicit override: interactivo or desatendido')
180
+ .option('--require-current', 'require authoritative consumed-contract currentness')
181
+ .option('--verify-sensors', 'require an empirical sensor pass')
182
+ .option('--json', 'emit one stable JSON report')
183
+ .action(async (planPath, options) => {
184
+ assertText(planPath, 'plan path');
185
+ assertText(options.cwd, '--cwd');
186
+ assertText(options.provider, '--provider');
187
+ const admission = deps.admitPlan ?? admission_1.admitPlan;
188
+ const preferences = deps.readPreferences ?? config_1.readPreferences;
189
+ const currentnessCheck = deps.checkCurrentness ?? check_1.checkCurrentness;
190
+ const sensorRun = deps.runSensors ?? run_1.runSensors;
191
+ const registryInventory = deps.listRegistries ?? registries_1.listRegistries;
192
+ const planReport = deps.validatePlanFile(planPath, options.cwd);
193
+ // The mode comes from the same authenticated bytes/digest that produced
194
+ // planReport. Never reopen the path to parse a mutable header.
195
+ const executionMode = options.executionMode === undefined
196
+ ? planReport.state === 'valid' ? (planReport.executionMode ?? 'interactivo') : 'interactivo'
197
+ : options.executionMode === 'desatendido' ? 'desatendido' : options.executionMode === 'interactivo' ? 'interactivo' : options.executionMode;
198
+ const journal = executionMode === 'desatendido' ? journalObservation(options.cwd) : {};
199
+ const normalizedPlanPath = path_1.default.relative(options.cwd, path_1.default.resolve(options.cwd, planPath)).replace(/\\/g, '/');
200
+ const enabledAgents = preferences().enabledAgents;
201
+ // The documented order is plan, provider, currentness, sensors, journal,
202
+ // capabilities. Do not call the full composer early: that would resolve a
203
+ // capability before empirical evidence gates have had their ordered turn.
204
+ const earlyBoundary = planReport.state !== 'valid' || !(0, providers_1.isAgentTarget)(options.provider) || !enabledAgents.includes(options.provider);
205
+ if (earlyBoundary) {
206
+ const report = await admission({ plan: planReport, provider: options.provider, cwd: options.cwd, enabledAgents, executionMode, planPath: normalizedPlanPath, ...journal });
207
+ process.stdout.write(admissionOutput(report, options.json === true));
208
+ process.exitCode = 2;
209
+ return;
210
+ }
211
+ const report = await (0, registry_contracts_1.admitRegistryPlan)({ plan: planReport, provider: options.provider, cwd: options.cwd, enabledAgents, executionMode, planPath: normalizedPlanPath, ...journal, requireCurrent: options.requireCurrent === true, verifySensors: options.verifySensors === true }, { admitPlan: admission, listRegistries: registryInventory, checkCurrentness: currentnessCheck, runSensors: sensorRun });
212
+ process.stdout.write(admissionOutput(report, options.json === true));
213
+ if (report.state !== 'admitted')
214
+ process.exitCode = 2;
215
+ });
98
216
  }
@@ -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,7 +51,7 @@ function declaration(mode, reason) {
51
51
  return { schemaVersion: 3, mode, reason };
52
52
  }
53
53
  function digest(bytes) { return (0, crypto_1.createHash)('sha256').update(bytes).digest('hex'); }
54
- async function projectSensors(projectRoot, registryRoot, requestedPack, packageRoot) {
54
+ async function projectSensors(projectRoot, registryRoot, requestedPack, packageRoot, explicitPackSelection = requestedPack !== undefined) {
55
55
  const detectionRoot = packageRoot ? path_1.default.resolve(projectRoot, packageRoot) : projectRoot;
56
56
  const detected = (0, detection_1.detectStack)(detectionRoot);
57
57
  const pack = requestedPack ?? detected.pack;
@@ -76,7 +76,9 @@ async function projectSensors(projectRoot, registryRoot, requestedPack, packageR
76
76
  return { reason: 'source-unsupported', remedy: 'install-a-v2-sensor-pack' };
77
77
  let live;
78
78
  try {
79
- live = await (0, live_1.resolveParsedPackCompatibility)(detectionRoot, parsed.pack);
79
+ live = explicitPackSelection
80
+ ? await (0, live_1.resolveParsedPackCompatibility)(detectionRoot, parsed.pack, { packSelection: 'explicit' })
81
+ : await (0, live_1.resolveParsedPackCompatibility)(detectionRoot, parsed.pack);
80
82
  }
81
83
  catch {
82
84
  return { reason: 'compatibility-unresolvable', remedy: 'repair-project-tools-or-select-another-mode' };
@@ -84,6 +86,8 @@ async function projectSensors(projectRoot, registryRoot, requestedPack, packageR
84
86
  const sensors = {};
85
87
  for (const [name, packSensor] of Object.entries(live.pack.sensors)) {
86
88
  const evidence = live.sensors[name];
89
+ if (evidence.state === 'not-applicable')
90
+ continue;
87
91
  const variant = evidence.variantId === null ? undefined : packSensor.variants.find(item => item.id === evidence.variantId);
88
92
  if (!variant)
89
93
  return { reason: 'sensor-variant-unresolvable', remedy: 'repair-project-tools-or-select-another-mode' };
@@ -109,8 +113,32 @@ async function planSensorBootstrap(cwd = process.cwd(), input = {}) {
109
113
  }
110
114
  return { kind: 'noop', projectRoot: project.projectRoot, manifestPath: project.manifestPath, changes: [], dryRun: opts.dryRun };
111
115
  }
112
- if (project.manifest.kind === 'legacy')
113
- return blocked(project.projectRoot, project.manifestPath, opts.dryRun, 'legacy-v1-preserved', 'migrate-the-manifest-explicitly');
116
+ if (project.manifest.kind === 'legacy') {
117
+ // Legacy manifests can carry arbitrary commands and per-sensor
118
+ // semantics that a v3 pack cannot prove equivalent. Replacing
119
+ // them is therefore a deliberate opt-in, never an automatic
120
+ // side effect of inspection or an unqualified bootstrap.
121
+ if (opts.mode !== 'project-sensors') {
122
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, 'legacy-replacement-requires-project-sensors-mode', 'rerun-bootstrap-with-mode-project-sensors');
123
+ }
124
+ // The legacy pack name identifies the source to migrate, but must not
125
+ // activate v2 sensors which require an explicit project opt-in.
126
+ const planned = await projectSensors(project.projectRoot, opts.registryRoot, project.manifest.pack.pack, undefined, false);
127
+ if ('reason' in planned)
128
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, planned.reason, planned.remedy, 'candidates' in planned ? planned.candidates : undefined);
129
+ try {
130
+ const original = fs_1.default.readFileSync(project.manifestPath);
131
+ return {
132
+ kind: 'migrate', projectRoot: project.projectRoot, manifestPath: project.manifestPath,
133
+ changes: planned.changes.map((change, index) => index === 0 ? { ...change, action: 'replace' } : change),
134
+ dryRun: opts.dryRun, migration: { kind: 'legacy-v1', manifest: planned.manifest }, source: planned.source,
135
+ originalDigest: digest(original),
136
+ };
137
+ }
138
+ catch {
139
+ return blocked(project.projectRoot, project.manifestPath, opts.dryRun, 'legacy-manifest-unreadable', 'repair-the-manifest-before-bootstrap');
140
+ }
141
+ }
114
142
  let source;
115
143
  try {
116
144
  source = (0, source_1.resolveSensorSource)(project.manifest, { registries: (0, registries_1.listRegistries)() });
@@ -152,7 +180,10 @@ function applySensorBootstrap(plan) {
152
180
  const current = fs_1.default.readFileSync(plan.manifestPath);
153
181
  if (digest(current) !== plan.originalDigest)
154
182
  throw new Error('bootstrap migration plan is stale: manifest changed after planning');
155
- (0, migrate_2.replaceV2ManifestWithV3)(plan.manifestPath, plan.migration.candidate, plan.source);
183
+ if ('kind' in plan.migration)
184
+ (0, migrate_1.replaceLegacyManifestWithV3)(plan.manifestPath, plan.migration.manifest, plan.source);
185
+ else
186
+ (0, migrate_2.replaceV2ManifestWithV3)(plan.manifestPath, plan.migration.candidate, plan.source);
156
187
  return 'migrated';
157
188
  }
158
189
  if (plan.manifest.mode === 'project-sensors') {