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
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.watchJournalStatus = watchJournalStatus;
7
+ exports.archiveUnusedWatch = archiveUnusedWatch;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const crypto_1 = __importDefault(require("crypto"));
11
+ const types_1 = require("../../core/journal/types");
12
+ const store_1 = require("../../core/journal/store");
13
+ const paths_1 = require("../../core/journal/paths");
14
+ const atomic_file_1 = require("../../core/atomic-file");
15
+ const lock_1 = require("./lock");
16
+ function controlledDirectory(directory) {
17
+ try {
18
+ const stat = fs_1.default.lstatSync(directory);
19
+ if (!stat.isDirectory() || stat.isSymbolicLink())
20
+ throw new Error(`unused bootstrap archive rejects unsafe directory: ${directory}`);
21
+ }
22
+ catch (error) {
23
+ if (error.code !== 'ENOENT')
24
+ throw error;
25
+ }
26
+ }
27
+ /** Read-only status of this branch, not directory-wide journal-first inference.
28
+ * A corrupt or linked tree is never reported as absence. No plan body, prompt,
29
+ * command argv or task/verdict content crosses this public observation. */
30
+ function watchJournalStatus(repoRoot, branch) {
31
+ const file = (0, paths_1.statePath)(repoRoot, branch);
32
+ try {
33
+ for (const directory of [path_1.default.join(repoRoot, '.awm'), path_1.default.join(repoRoot, '.awm/journal'), (0, paths_1.journalDir)(repoRoot, branch)])
34
+ controlledDirectory(directory);
35
+ const stat = fs_1.default.lstatSync(file);
36
+ if (!stat.isFile() || stat.isSymbolicLink())
37
+ return { state: 'corrupt', bootstrapUnused: false };
38
+ }
39
+ catch (error) {
40
+ return { state: error.code === 'ENOENT' ? 'missing' : 'corrupt', bootstrapUnused: false };
41
+ }
42
+ const current = (0, store_1.readJournal)(repoRoot, branch);
43
+ if (current.corrupt || !current.state || current.state.branch !== branch)
44
+ return { state: 'corrupt', bootstrapUnused: false };
45
+ let bootstrapUnused = false;
46
+ try {
47
+ assertUnused(current.state);
48
+ assertUnusedTree(repoRoot, branch);
49
+ bootstrapUnused = true;
50
+ }
51
+ catch { /* recognized present journal, but never eligible by absence alone */ }
52
+ const binding = current.state.planBinding;
53
+ return {
54
+ state: 'present', cycleState: current.state.cycle.status, bootstrapUnused,
55
+ ...(binding ? { binding: { path: binding.path, digest: binding.digest, ...(binding.executionDigest ? { executionDigest: binding.executionDigest, executionIdentitySchema: binding.executionIdentitySchema } : {}), schema: binding.schema, executionMode: binding.executionMode, boundAt: binding.boundAt } } : {}),
56
+ };
57
+ }
58
+ /** Default-deny: only the exact unexecuted bootstrap shape is archivable.
59
+ * Future runtime fields automatically make this route ineligible. */
60
+ function assertUnused(state) {
61
+ const initial = (0, types_1.emptyState)(state.branch);
62
+ const allowed = new Set([...Object.keys(initial), 'planBinding', 'planBindingHistory']);
63
+ const bootstrap = initial.cycle.nextAction;
64
+ if (state.schema !== 2 || !state.planBinding || state.cycle.status !== 'IN_PROGRESS'
65
+ || Object.keys(state).some(key => !allowed.has(key))
66
+ || Object.keys(state.cycle).some(key => !['status', 'startedAt', 'nextAction'].includes(key))
67
+ || JSON.stringify(state.cycle.nextAction) !== JSON.stringify(bootstrap)
68
+ || state.tasks.length !== 0 || state.cycleVerificationPlan.length !== 0
69
+ || state.generations.length !== 0 || state.dispatches.length !== 0
70
+ || Object.keys(state.jobs).length !== 0 || state.verdicts.length !== 0 || state.fixes.length !== 0
71
+ || Object.keys(state.appliedRequests).length !== 0 || state.requestProblems.length !== 0
72
+ || (state.custodyDecisions?.length ?? 0) !== 0) {
73
+ throw new Error('unused bootstrap requerido: el journal tiene ejecución, evidencia o estado no reconocido; no se archiva');
74
+ }
75
+ }
76
+ function assertUnusedTree(repoRoot, branch) {
77
+ const root = (0, paths_1.journalDir)(repoRoot, branch);
78
+ const emptyDirectories = new Set(['requests', 'acks', 'logs', 'export']);
79
+ for (const entry of fs_1.default.readdirSync(root, { withFileTypes: true })) {
80
+ const file = path_1.default.join(root, entry.name);
81
+ if (entry.isSymbolicLink())
82
+ throw new Error('unused bootstrap rejects symlink artifact');
83
+ if (entry.name === 'state.json' && entry.isFile())
84
+ continue;
85
+ if (emptyDirectories.has(entry.name) && entry.isDirectory() && fs_1.default.readdirSync(file).length === 0)
86
+ continue;
87
+ throw new Error(`unused bootstrap has runtime or unrecognized artifact: ${entry.name}`);
88
+ }
89
+ }
90
+ /** Remove ONLY an unused admission bootstrap from the active namespace by one
91
+ * recoverable rename. The archived state remains IN_PROGRESS, not COMPLETE;
92
+ * no task, verdict, cycle evidence, or claimed native execution is generated. */
93
+ function archiveUnusedWatch(repoRoot, branch, planPath) {
94
+ const canonicalPlan = (0, paths_1.bindingPlanPath)(planPath);
95
+ const before = (0, store_1.readJournal)(repoRoot, branch);
96
+ if (before.corrupt || !before.state)
97
+ throw new Error('unused bootstrap journal inexistente, corrupto o inseguro');
98
+ const lockPath = (0, paths_1.supervisorLockPath)(repoRoot);
99
+ try {
100
+ if (fs_1.default.lstatSync(lockPath).isSymbolicLink())
101
+ throw new Error('unused bootstrap rejects symlink lock');
102
+ }
103
+ catch (error) {
104
+ if (error.code !== 'ENOENT')
105
+ throw error;
106
+ }
107
+ const archiveRoot = path_1.default.join(repoRoot, '.awm', 'journal-unused');
108
+ controlledDirectory(archiveRoot);
109
+ const lock = (0, lock_1.acquireLock)(repoRoot);
110
+ try {
111
+ (0, lock_1.verifyBranchInvariant)(repoRoot, branch);
112
+ const current = (0, store_1.readJournal)(repoRoot, branch);
113
+ if (current.corrupt || !current.state || current.state.branch !== branch)
114
+ throw new Error('unused bootstrap journal corrupto o de otra rama');
115
+ if (current.state.planBinding?.path !== canonicalPlan)
116
+ throw new Error('unused bootstrap requiere la misma ruta canónica del plan');
117
+ assertUnused(current.state);
118
+ assertUnusedTree(repoRoot, branch);
119
+ controlledDirectory(archiveRoot);
120
+ if (!fs_1.default.existsSync(archiveRoot))
121
+ fs_1.default.mkdirSync(archiveRoot, { mode: 0o700 });
122
+ controlledDirectory(archiveRoot);
123
+ (0, atomic_file_1.fsyncDirSync)(path_1.default.dirname(archiveRoot));
124
+ const source = (0, paths_1.journalDir)(repoRoot, branch);
125
+ const destination = path_1.default.join(archiveRoot, `${(0, paths_1.branchSlug)(branch)}.${crypto_1.default.randomUUID()}`);
126
+ if (fs_1.default.existsSync(destination))
127
+ throw new Error('unused bootstrap archive destination already exists');
128
+ // A single directory rename is the commit point; before it the source
129
+ // exists, after it the original byte-for-byte state is recoverable.
130
+ fs_1.default.renameSync(source, destination);
131
+ (0, atomic_file_1.fsyncDirSync)(archiveRoot);
132
+ (0, atomic_file_1.fsyncDirSync)(path_1.default.dirname(source));
133
+ return { archived: path_1.default.relative(repoRoot, destination).split(path_1.default.sep).join('/'), kind: 'unused-bootstrap', journalId: current.state.journalId };
134
+ }
135
+ finally {
136
+ (0, lock_1.releaseLock)(repoRoot, lock);
137
+ }
138
+ }
@@ -88,22 +88,42 @@ function beginGeneration(repoRoot, branch) {
88
88
  return gen;
89
89
  }
90
90
  function promptForGeneration(gen) {
91
- return `${gen.resumePrompt}\nGeneracion activa: ${gen.token}. Incluye --generation ${gen.token} en cada comando awm job.`;
91
+ if (!(0, types_1.isControllerRecoveryAction)(gen.resumeAction) || !/^[a-f0-9]{16}$/.test(gen.token))
92
+ throw new Error('recovery action invalida: no se persisten prompts libres');
93
+ const instructions = {
94
+ 'resume-cycle': 'Retoma el plan del ciclo desde el journal.',
95
+ 'resume-next-action': 'Retoma el next_action vigente del journal.',
96
+ 'reconcile-active-jobs': 'Reconcilia los jobs activos existentes del journal sin duplicarlos.',
97
+ };
98
+ return `${instructions[gen.resumeAction.kind]}\nGeneracion activa: ${gen.token}. Incluye --generation ${gen.token} en cada comando awm job.`;
99
+ }
100
+ function recoveryAction(action) {
101
+ if (!(0, types_1.isControllerRecoveryAction)(action))
102
+ throw new Error('recovery action invalida: no se persisten prompts libres');
103
+ return { schema: action.schema, kind: action.kind };
92
104
  }
93
105
  /** Persiste el intent completo ANTES de delegarlo al wrapper. El wrapper usa
94
106
  * claim exclusivo por (controllerJobId, spawnNonce), por lo que reemitir este
95
107
  * mismo intent tras un crash nunca lanza dos controllers. */
96
- function launchControllerGeneration(repoRoot, branch, provider, resumePrompt, spawner = (0, runner_1.defaultWrapperSpawner)()) {
108
+ function launchControllerGeneration(repoRoot, branch, provider, action, spawner = (0, runner_1.defaultWrapperSpawner)()) {
109
+ const requestedAction = recoveryAction(action);
97
110
  const s = requireState(repoRoot, branch);
98
111
  const gen = activeGeneration(s);
99
112
  if (gen === undefined)
100
113
  throw new Error('no hay generacion activa para lanzar');
114
+ if (gen.resumePrompt !== undefined)
115
+ throw new Error('generacion legacy con prompt: decision explicita requerida; no se reemite');
116
+ const adapter = (0, adapter_1.adapterFor)(gen.provider ?? provider);
101
117
  gen.controllerJobId = gen.controllerJobId ?? `controller-gen-${gen.n}`;
102
118
  gen.spawnNonce = gen.spawnNonce ?? crypto_1.default.randomBytes(8).toString('hex');
103
119
  gen.provider = gen.provider ?? provider;
104
- gen.resumePrompt = gen.resumePrompt ?? resumePrompt;
120
+ gen.resumeAction = gen.resumeAction ?? requestedAction;
121
+ const argv = adapter.launchArgv(promptForGeneration(gen));
122
+ const digest = (0, process_1.argvDigest)(argv);
123
+ if (gen.launchArgvDigest !== undefined && gen.launchArgvDigest !== digest)
124
+ throw new Error('controller argv cambio: no se reemite el mismo nonce con otro comando');
125
+ gen.launchArgvDigest = digest;
105
126
  (0, store_1.writeJournal)(repoRoot, branch, s);
106
- const argv = (0, adapter_1.adapterFor)(gen.provider).launchArgv(promptForGeneration(gen));
107
127
  const job = {
108
128
  id: gen.controllerJobId, fingerprint: `generation:${gen.token}`, commandDigest: `generation:${gen.token}`,
109
129
  argv, cwd: '.', paths: [], expandedPaths: [], executionState: 'spawn-intent',
@@ -140,8 +160,8 @@ function collectControllerGeneration(repoRoot, branch) {
140
160
  if (identity.jobId !== gen.controllerJobId || identity.nonce !== gen.spawnNonce
141
161
  || !(0, types_1.isWellFormedProcessRef)(identity.wrapper) || !(0, types_1.isWellFormedProcessRef)(identity.command)
142
162
  || identity.wrapper.spawnNonce !== gen.spawnNonce || identity.command.spawnNonce !== gen.spawnNonce
143
- || gen.provider === undefined || gen.resumePrompt === undefined
144
- || identity.command.argvDigest !== (0, process_1.argvDigest)((0, adapter_1.adapterFor)(gen.provider).launchArgv(promptForGeneration(gen))))
163
+ || gen.provider === undefined || gen.resumePrompt !== undefined || !(0, types_1.isControllerRecoveryAction)(gen.resumeAction)
164
+ || gen.launchArgvDigest === undefined || identity.command.argvDigest !== gen.launchArgvDigest)
145
165
  return false;
146
166
  const changed = gen.wrapperRef?.pid !== identity.wrapper.pid || gen.processRef?.pid !== identity.command.pid;
147
167
  gen.wrapperRef = identity.wrapper;
@@ -162,16 +182,19 @@ function controllerGenerationHasUnresolvedClaim(repoRoot, branch, gen) {
162
182
  /** Recupera tanto crash-before-spawn como crash-after-spawn-before-journal.
163
183
  * Claim sin identidad queda ambiguo y se conserva bajo custodia tras la gracia;
164
184
  * nunca se resuelve lanzando otro token/nonce a ciegas. */
165
- function ensureControllerGeneration(repoRoot, branch, provider, resumePrompt, spawner, ambiguityGraceMs) {
185
+ function ensureControllerGeneration(repoRoot, branch, provider, action, spawner, ambiguityGraceMs) {
186
+ const requestedAction = recoveryAction(action);
166
187
  if (collectControllerGeneration(repoRoot, branch))
167
188
  return;
168
189
  let gen = activeGeneration(requireState(repoRoot, branch));
169
190
  if (gen === undefined)
170
191
  return;
192
+ if (gen.resumePrompt !== undefined)
193
+ throw new Error('generacion legacy con prompt: decision explicita requerida; no se reemite');
171
194
  if (gen.processRef !== undefined || gen.wrapperRef !== undefined)
172
195
  return;
173
196
  if (gen.controllerJobId === undefined || gen.spawnNonce === undefined) {
174
- launchControllerGeneration(repoRoot, branch, provider, resumePrompt, spawner);
197
+ launchControllerGeneration(repoRoot, branch, provider, requestedAction, spawner);
175
198
  return;
176
199
  }
177
200
  const logs = (0, paths_1.logsDir)(repoRoot, branch);
@@ -194,7 +217,7 @@ function ensureControllerGeneration(repoRoot, branch, provider, resumePrompt, sp
194
217
  // El intent ya estaba durable pero el spawn no ocurrio: es seguro reemitir
195
218
  // exactamente el mismo nonce. Si el wrapper original solo estaba demorado,
196
219
  // su claim wx arbitra cual de ambos ejecuta.
197
- launchControllerGeneration(repoRoot, branch, gen.provider ?? provider, gen.resumePrompt ?? resumePrompt, spawner);
220
+ launchControllerGeneration(repoRoot, branch, gen.provider ?? provider, gen.resumeAction ?? requestedAction, spawner);
198
221
  }
199
222
  /** Custodia (R4.5): ciclo BLOCKED con razon auditada. QUIEN NO HACE NADA:
200
223
  * no mata, no relanza, no libera lock — el loop sigue vivo auditando. */
@@ -1,4 +1,7 @@
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.registerWatchCommand = registerWatchCommand;
4
7
  const child_process_1 = require("child_process");
@@ -8,6 +11,11 @@ const process_1 = require("../../core/journal/process");
8
11
  const adapter_1 = require("../../core/journal/adapter");
9
12
  const context_1 = require("../../core/tracks/context");
10
13
  const concurrency_1 = require("../../core/tracks/concurrency");
14
+ const validate_1 = require("../../core/plan/validate");
15
+ const path_1 = __importDefault(require("path"));
16
+ const archive_unused_1 = require("./archive-unused");
17
+ const store_1 = require("../../core/journal/store");
18
+ const fingerprint_1 = require("../../core/journal/fingerprint");
11
19
  function currentBranch(cwd) {
12
20
  // stdio explicito (ver EXEC_STDIO en journal/process.ts): evita el relay
13
21
  // default de execFileSync del stderr de git hacia el stderr del llamante,
@@ -23,11 +31,19 @@ function minutes(flag, raw) {
23
31
  throw new Error(`${flag} requiere un numero de minutos > 0`);
24
32
  return n * 60000;
25
33
  }
34
+ function validPlanPath(value) {
35
+ return typeof value === 'string' && value.length > 0 && value.length <= 4096 && !/[\u0000-\u001F\u007F-\u009F]/.test(value);
36
+ }
37
+ function planForBinding(repo, rawPath) {
38
+ const report = (0, validate_1.validatePlanFile)(rawPath, repo);
39
+ return { path: path_1.default.relative(repo, path_1.default.resolve(repo, rawPath)).replace(/\\/g, '/'), report };
40
+ }
26
41
  function registerWatchCommand(program) {
27
- program
42
+ const watch = program
28
43
  .command('watch')
29
44
  .description('supervisor durable: ejecuta jobs, releva controladores caidos, nunca mata trabajo vivo')
30
45
  .option('--init', 'bootstrap: crea el journal de la rama actual, detecta verificadores y sale')
46
+ .option('--plan <path>', 'plan compacto desatendido que se vincula al inicializar')
31
47
  .option('--provider <p>', adapter_1.WATCH_PROVIDERS.join(' | '), 'codex')
32
48
  .option('--heartbeat-timeout <min>', 'minutos de silencio de heartbeat', '5')
33
49
  .option('--activity-window <min>', 'minutos extra sin actividad de proceso', '10')
@@ -45,8 +61,24 @@ function registerWatchCommand(program) {
45
61
  process.stderr.write(`${e.message}\n`);
46
62
  process.exit(1);
47
63
  }
64
+ if (opts.plan !== undefined && !opts.init) {
65
+ process.stderr.write('--plan requiere --init\n');
66
+ process.exitCode = 1;
67
+ return;
68
+ }
69
+ if (opts.init && opts.plan === undefined) {
70
+ process.stderr.write('watch --init requiere --plan con un plan compacto válido\n');
71
+ process.exitCode = 1;
72
+ return;
73
+ }
74
+ if (opts.plan !== undefined && !validPlanPath(opts.plan)) {
75
+ process.stderr.write('--plan requiere un path sin caracteres de control\n');
76
+ process.exitCode = 1;
77
+ return;
78
+ }
48
79
  if (opts.init) {
49
- const out = (0, init_1.initWatch)(repo, branch);
80
+ const plan = opts.plan === undefined ? undefined : planForBinding(repo, opts.plan);
81
+ const out = (0, init_1.initWatch)(repo, branch, plan);
50
82
  process.stdout.write(`journal inicializado para ${branch}; verificadores requeridos: ${JSON.stringify(out.requiredVerifiers)}\n`);
51
83
  return;
52
84
  }
@@ -70,4 +102,78 @@ function registerWatchCommand(program) {
70
102
  await (0, supervisor_1.runSupervisorLoop)(repo, branch, cfg);
71
103
  process.stdout.write('gate verde: ciclo COMPLETE — drenado, lock liberado, apagando\n');
72
104
  });
105
+ watch
106
+ .command('rebind')
107
+ .description('reconcilia intencionalmente el binding desatendido tras un cambio válido del ciclo del plan')
108
+ // `watch` conserva su --plan histórico para `watch --init --plan`.
109
+ // Commander lo asocia al padre incluso después del subcomando, por lo
110
+ // que este option es documental y la acción toma el valor del padre
111
+ // cuando corresponde; marcarlo required acá rechazaría una invocación
112
+ // válida antes de llegar a esa reconciliación explícita.
113
+ .option('--plan <path>', 'mismo plan compacto previamente vinculado; valida y conserva la historia antes de actualizar su digest')
114
+ .action((opts, command) => {
115
+ const repo = process.cwd();
116
+ const branch = currentBranch(repo);
117
+ try {
118
+ (0, context_1.resolveCommandContext)(repo, branch);
119
+ }
120
+ catch (e) {
121
+ process.stderr.write(`${e.message}\n`);
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+ const plan = opts.plan ?? command.parent?.opts().plan;
126
+ if (!validPlanPath(plan)) {
127
+ process.stderr.write('--plan requiere un path sin caracteres de control\n');
128
+ process.exitCode = 1;
129
+ return;
130
+ }
131
+ try {
132
+ const binding = (0, init_1.rebindWatchPlan)(repo, branch, planForBinding(repo, plan));
133
+ process.stdout.write(`binding reconciliado para ${binding.path}; digest ${binding.digest}\n`);
134
+ const state = (0, store_1.readJournal)(repo, branch).state;
135
+ const staleJobs = Object.values(state.jobs).filter(job => job.verdict === 'pass' && (() => {
136
+ try {
137
+ return (0, fingerprint_1.computeFingerprint)(repo, job.argv, job.paths, job.cwd).fingerprint !== job.fingerprint;
138
+ }
139
+ catch {
140
+ return true;
141
+ }
142
+ })()).map(job => job.id);
143
+ if (staleJobs.length > 0) {
144
+ process.stdout.write(`stale-fingerprint: ${staleJobs.join(', ')}; el rebind no conserva PASS con fingerprint cambiado. Re-ejecutar las verificaciones afectadas mediante awm job request con generation, paths y satisfies originales; no repetir watch/rebind ni re-fingerprinting de evidencia anterior.\n`);
145
+ }
146
+ }
147
+ catch (e) {
148
+ process.stderr.write(`${e.message}\n`);
149
+ process.exitCode = 1;
150
+ }
151
+ });
152
+ watch.command('archive-unused')
153
+ .description('archiva recuperablemente solo un bootstrap no utilizado; nunca declara COMPLETE ni crea evidencia de ejecución')
154
+ .option('--plan <path>', 'mismo plan vinculado al bootstrap; no adopta evidencia de trabajo manual')
155
+ .action((opts, command) => {
156
+ const repo = process.cwd();
157
+ const branch = currentBranch(repo);
158
+ try {
159
+ (0, context_1.resolveCommandContext)(repo, branch);
160
+ const plan = opts.plan ?? command.parent?.opts().plan;
161
+ if (!validPlanPath(plan))
162
+ throw new Error('--plan requiere un path sin caracteres de control');
163
+ const relative = path_1.default.relative(repo, path_1.default.resolve(repo, plan)).replace(/\\/g, '/');
164
+ process.stdout.write(`${JSON.stringify((0, archive_unused_1.archiveUnusedWatch)(repo, branch, relative))}\n`);
165
+ }
166
+ catch (error) {
167
+ process.stderr.write(`${error.message}\n`);
168
+ process.exitCode = 1;
169
+ }
170
+ });
171
+ watch.command('journal-status')
172
+ .description('consulta read-only el journal de la rama actual: missing | corrupt | present; no exporta contenido')
173
+ .option('--json', 'estado estructural y metadata de binding, sin cuerpos ni evidencia inferida')
174
+ .action(() => {
175
+ const repo = process.cwd();
176
+ const branch = currentBranch(repo);
177
+ process.stdout.write(`${JSON.stringify((0, archive_unused_1.watchJournalStatus)(repo, branch))}\n`);
178
+ });
73
179
  }
@@ -6,40 +6,78 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.detectRequiredVerifiers = detectRequiredVerifiers;
7
7
  exports.ensureJournalGitignored = ensureJournalGitignored;
8
8
  exports.initWatch = initWatch;
9
+ exports.rebindWatchPlan = rebindWatchPlan;
9
10
  // Bootstrap unico writer (R4.1) + validacion MECANICA plan-vs-repo (R1.4b,
10
11
  // bloqueador 5): la config real del repo determina los verificadores exigidos.
11
12
  const fs_1 = __importDefault(require("fs"));
12
13
  const path_1 = __importDefault(require("path"));
13
14
  const store_1 = require("../../core/journal/store");
15
+ const paths_1 = require("../../core/journal/paths");
16
+ const lock_1 = require("./lock");
17
+ const validate_1 = require("../../core/plan/validate");
18
+ const identity_1 = require("../../core/plan/identity");
19
+ const native_bridge_1 = require("../../core/secure-fs/native-bridge");
20
+ const MAX_VERIFIER_SCAN_DEPTH = 64;
21
+ const MAX_VERIFIER_SCAN_ENTRIES = 10000;
22
+ const MAX_PACKAGE_JSON_BYTES = 256 * 1024;
23
+ const MAX_SENSORS_JSON_BYTES = 256 * 1024;
14
24
  function detectRequiredVerifiers(repoRoot) {
15
25
  const kinds = new Set();
16
- const visit = (dir) => {
26
+ let visitedEntries = 0;
27
+ const visit = (dir, depth) => {
28
+ if (depth > MAX_VERIFIER_SCAN_DEPTH)
29
+ throw new Error('verifier scan depth limit exceeded');
17
30
  const sensors = path_1.default.join(dir, '.awm', 'sensors.json');
18
- if (fs_1.default.existsSync(sensors))
19
- kinds.add('sensors');
31
+ let sensorsPresent = false;
32
+ try {
33
+ fs_1.default.lstatSync(sensors);
34
+ sensorsPresent = true;
35
+ }
36
+ catch (error) {
37
+ if (error.code !== 'ENOENT')
38
+ throw new Error('verifier scan rejected sensors.json: unreadable path');
39
+ }
40
+ if (sensorsPresent) {
41
+ try {
42
+ const bytes = native_bridge_1.secureFs.readRegularFile(path_1.default.resolve(sensors), MAX_SENSORS_JSON_BYTES).bytes;
43
+ const config = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
44
+ if (!config || typeof config !== 'object' || Array.isArray(config))
45
+ throw new Error('sensors config must be a JSON object');
46
+ kinds.add('sensors');
47
+ }
48
+ catch (error) {
49
+ throw new Error(`verifier scan rejected sensors.json: ${error.message}`);
50
+ }
51
+ }
20
52
  let entries;
21
53
  try {
22
54
  entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
23
55
  }
24
- catch {
25
- return;
56
+ catch (error) {
57
+ throw new Error(`verifier scan cannot read ${dir}: ${error.message}`);
26
58
  }
27
59
  for (const entry of entries) {
28
- if (entry.isSymbolicLink())
29
- continue;
30
- if (entry.isFile() && entry.name === 'package.json') {
60
+ if (++visitedEntries > MAX_VERIFIER_SCAN_ENTRIES)
61
+ throw new Error('verifier scan entry limit exceeded');
62
+ if (entry.name === 'package.json') {
31
63
  try {
32
- const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, entry.name), 'utf8'));
64
+ const packagePath = path_1.default.join(dir, entry.name);
65
+ const bytes = native_bridge_1.secureFs.readRegularFile(path_1.default.resolve(packagePath), MAX_PACKAGE_JSON_BYTES).bytes;
66
+ const pkg = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
33
67
  if (typeof pkg === 'object' && pkg !== null && typeof pkg.scripts === 'object' && pkg.scripts !== null && typeof pkg.scripts.test === 'string')
34
68
  kinds.add('test');
35
69
  }
36
- catch { /* package ilegible: no prueba disponibilidad */ }
70
+ catch (error) {
71
+ throw new Error(`verifier scan rejected package.json: ${error.message}`);
72
+ }
37
73
  }
74
+ if (entry.isSymbolicLink())
75
+ continue;
38
76
  if (entry.isDirectory() && !['node_modules', '.git', '.awm'].includes(entry.name))
39
- visit(path_1.default.join(dir, entry.name));
77
+ visit(path_1.default.join(dir, entry.name), depth + 1);
40
78
  }
41
79
  };
42
- visit(repoRoot);
80
+ visit(repoRoot, 0);
43
81
  return ['test', 'sensors'].filter((kind) => kinds.has(kind));
44
82
  }
45
83
  /** El journal es gitignoreado (R1.1): sus escrituras jamas alteran fingerprints. */
@@ -47,19 +85,65 @@ function ensureJournalGitignored(repoRoot) {
47
85
  const gi = path_1.default.join(repoRoot, '.gitignore');
48
86
  let current = '';
49
87
  try {
88
+ if (fs_1.default.lstatSync(gi).isSymbolicLink())
89
+ throw new Error('.gitignore must not be a symlink');
50
90
  current = fs_1.default.readFileSync(gi, 'utf8');
51
91
  }
52
- catch {
53
- current = '';
92
+ catch (error) {
93
+ if (error.code !== 'ENOENT')
94
+ throw error;
54
95
  }
55
96
  if (!current.split('\n').some((l) => l.trim() === '.awm/' || l.trim() === '.awm')) {
56
97
  fs_1.default.writeFileSync(gi, current.length > 0 && !current.endsWith('\n') ? `${current}\n.awm/\n` : `${current}.awm/\n`);
57
98
  }
58
99
  }
59
- function initWatch(repoRoot, branch) {
60
- ensureJournalGitignored(repoRoot);
61
- (0, store_1.initJournal)(repoRoot, branch);
100
+ const TERMINAL_JOB_STATES = new Set(['exited', 'cancelled']);
101
+ function rebindBlocker(state) {
102
+ if (state === null)
103
+ return 'journal inexistente o corrupto';
104
+ const nonterminal = Object.values(state.jobs).filter(job => !TERMINAL_JOB_STATES.has(job.executionState));
105
+ if (nonterminal.length > 0)
106
+ return `hay jobs no terminales: ${nonterminal.map(job => job.id).join(', ')}`;
107
+ if (state.requestProblems.length > 0)
108
+ return 'hay conflictos durables de requests';
109
+ if (state.verdicts.some(verdict => verdict.result !== 'pass'))
110
+ return 'hay evidencia adversa durable';
111
+ if (state.fixes.some(fix => !fix.closed))
112
+ return 'hay fixes abiertos';
113
+ if (state.cycle.status === 'BLOCKED')
114
+ return 'el ciclo está bloqueado';
115
+ if (state.tracks?.some(track => track.phase !== 'REMOVED'))
116
+ return 'hay tracks no terminales';
117
+ return undefined;
118
+ }
119
+ function initWatch(repoRoot, branch, plan) {
120
+ if (plan) {
121
+ if (plan.report.state !== 'valid')
122
+ throw new Error('watch --init --plan requiere un plan compacto válido');
123
+ if (plan.report.executionMode !== 'desatendido')
124
+ throw new Error('watch --init --plan requiere un plan compacto desatendido');
125
+ if (plan.report.executionDigest)
126
+ (0, validate_1.verifiedPlanSnapshot)(plan.report);
127
+ }
128
+ if (plan && fs_1.default.existsSync((0, paths_1.statePath)(repoRoot, branch))) {
129
+ throw new Error('refusing to overwrite existing journal');
130
+ }
131
+ let planBinding;
132
+ if (plan) {
133
+ const report = plan.report;
134
+ planBinding = {
135
+ path: (0, paths_1.bindingPlanPath)(plan.path), digest: report.planDigest, schema: report.schema,
136
+ ...(report.executionDigest ? { executionDigest: report.executionDigest, executionIdentitySchema: identity_1.EXECUTION_IDENTITY_SCHEMA } : {}),
137
+ executionMode: 'desatendido', boundAt: new Date().toISOString(),
138
+ };
139
+ }
62
140
  const required = detectRequiredVerifiers(repoRoot);
141
+ (0, store_1.assertJournalInitPaths)(repoRoot, branch);
142
+ ensureJournalGitignored(repoRoot);
143
+ if (planBinding)
144
+ (0, store_1.initBoundJournal)(repoRoot, branch, planBinding, plan.report);
145
+ else
146
+ (0, store_1.initJournal)(repoRoot, branch);
63
147
  const r = (0, store_1.readJournal)(repoRoot, branch);
64
148
  if (r.corrupt || r.state === null)
65
149
  throw new Error('journal corrupto tras init: no se continua (R1.6)');
@@ -68,5 +152,52 @@ function initWatch(repoRoot, branch) {
68
152
  s.requiredVerifiers = required;
69
153
  (0, store_1.writeJournal)(repoRoot, branch, s);
70
154
  }
71
- return { requiredVerifiers: required };
155
+ return { requiredVerifiers: required, ...(planBinding ? { planBinding } : {}) };
156
+ }
157
+ /**
158
+ * Intentional recovery route for a valid compact plan whose lifecycle update
159
+ * changed its digest. It cannot initialize or overwrite a journal: only an
160
+ * existing schema-2 unattended binding for the exact canonical plan path is
161
+ * eligible, and all outstanding work/evidence conflicts fail closed.
162
+ */
163
+ function rebindWatchPlan(repoRoot, branch, plan) {
164
+ if (plan.report.state !== 'valid')
165
+ throw new Error('watch rebind --plan requiere un plan compacto válido');
166
+ // The same exclusive lock used by the supervisor closes the interval
167
+ // between observing quiescence and publishing the recovered binding.
168
+ const lock = (0, lock_1.acquireLock)(repoRoot);
169
+ try {
170
+ const current = (0, store_1.readJournal)(repoRoot, branch);
171
+ if (current.corrupt || current.state === null || current.state.schema !== 2 || !current.state.planBinding) {
172
+ throw new Error('no existe un journal schema-2 desatendido para reconciliar');
173
+ }
174
+ const existing = current.state.planBinding;
175
+ const canonicalPath = (0, paths_1.bindingPlanPath)(plan.path);
176
+ if (existing.executionMode !== 'desatendido')
177
+ throw new Error('solo se reconcilia un binding desatendido');
178
+ if (existing.path !== canonicalPath)
179
+ throw new Error('rebind requiere la misma ruta canónica del plan');
180
+ if (existing.digest === plan.report.planDigest && existing.schema === plan.report.schema)
181
+ throw new Error('el binding ya está vigente');
182
+ const blocker = rebindBlocker(current.state);
183
+ if (blocker)
184
+ throw new Error(`rebind rechazado: ${blocker}`);
185
+ const report = plan.report;
186
+ if (!existing.executionDigest || existing.executionIdentitySchema !== identity_1.EXECUTION_IDENTITY_SCHEMA || !report.executionDigest)
187
+ throw new Error('rebind rechazado: ciclo en curso o histórico sin prueba de identidad de ejecución');
188
+ (0, validate_1.verifiedPlanSnapshot)(report);
189
+ if (report.executionDigest !== existing.executionDigest || report.executionMode !== existing.executionMode || report.schema !== existing.schema)
190
+ throw new Error('rebind rechazado: cambiaron requisitos o contenido de ejecución; la evidencia anterior no se conserva');
191
+ const binding = {
192
+ path: canonicalPath, digest: report.planDigest, schema: report.schema,
193
+ executionDigest: report.executionDigest,
194
+ executionIdentitySchema: identity_1.EXECUTION_IDENTITY_SCHEMA,
195
+ executionMode: 'desatendido', boundAt: new Date().toISOString(),
196
+ };
197
+ (0, store_1.rebindJournalPlan)(repoRoot, branch, binding, report);
198
+ return binding;
199
+ }
200
+ finally {
201
+ (0, lock_1.releaseLock)(repoRoot, lock);
202
+ }
72
203
  }