agentic-workflow-manager 6.2.0 → 6.3.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 (75) hide show
  1. package/README.md +17 -0
  2. package/dist/src/commands/job/gate.js +37 -13
  3. package/dist/src/commands/job/index.js +48 -8
  4. package/dist/src/commands/job/request.js +44 -6
  5. package/dist/src/commands/track/emit.js +26 -0
  6. package/dist/src/commands/track/index.js +194 -0
  7. package/dist/src/commands/track/status.js +63 -0
  8. package/dist/src/commands/track/supervisor-wrapper.js +89 -0
  9. package/dist/src/commands/watch/apply.js +201 -13
  10. package/dist/src/commands/watch/index.js +14 -0
  11. package/dist/src/commands/watch/runner.js +20 -1
  12. package/dist/src/commands/watch/supervisor.js +251 -27
  13. package/dist/src/commands/watch/teardown-driver.js +189 -0
  14. package/dist/src/commands/watch/tracks.js +1100 -0
  15. package/dist/src/core/diagnostics/provider-checks.js +12 -9
  16. package/dist/src/core/journal/adapter.js +46 -6
  17. package/dist/src/core/journal/paths.js +9 -0
  18. package/dist/src/core/journal/process.js +101 -1
  19. package/dist/src/core/journal/requests.js +5 -1
  20. package/dist/src/core/journal/store.js +34 -2
  21. package/dist/src/core/journal/types.js +93 -4
  22. package/dist/src/core/paths.js +51 -0
  23. package/dist/src/core/profile.js +15 -1
  24. package/dist/src/core/tracks/concurrency.js +85 -0
  25. package/dist/src/core/tracks/context.js +89 -0
  26. package/dist/src/core/tracks/descriptor.js +40 -0
  27. package/dist/src/core/tracks/git.js +318 -0
  28. package/dist/src/core/tracks/join.js +185 -0
  29. package/dist/src/core/tracks/ownership.js +100 -0
  30. package/dist/src/core/tracks/plan-parser.js +108 -0
  31. package/dist/src/core/tracks/protocol.js +466 -0
  32. package/dist/src/core/tracks/teardown.js +34 -0
  33. package/dist/src/core/tracks/types.js +14 -0
  34. package/dist/src/core/update-check.js +5 -1
  35. package/dist/src/index.js +2 -0
  36. package/dist/tests/commands/job/gate-reconcile.test.js +267 -0
  37. package/dist/tests/commands/track/fixtures.js +13 -0
  38. package/dist/tests/commands/track/status.test.js +157 -0
  39. package/dist/tests/commands/track/supervisor-wrapper-cli.test.js +129 -0
  40. package/dist/tests/commands/track/supervisor-wrapper.test.js +131 -0
  41. package/dist/tests/commands/track/verbs.test.js +326 -0
  42. package/dist/tests/commands/watch/apply.test.js +116 -4
  43. package/dist/tests/commands/watch/runner.test.js +22 -0
  44. package/dist/tests/commands/watch/supervisor-loop.test.js +150 -0
  45. package/dist/tests/commands/watch/track-bootstrap-crash.test.js +342 -0
  46. package/dist/tests/commands/watch/track-bootstrap.test.js +350 -0
  47. package/dist/tests/commands/watch/track-finalize.test.js +643 -0
  48. package/dist/tests/commands/watch/track-freeze.test.js +591 -0
  49. package/dist/tests/commands/watch/track-join-crash.test.js +439 -0
  50. package/dist/tests/commands/watch/track-runtime-git.test.js +119 -0
  51. package/dist/tests/commands/watch/track-teardown-crash.test.js +426 -0
  52. package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
  53. package/dist/tests/core/journal/adapter-override.test.js +51 -0
  54. package/dist/tests/core/journal/process.test.js +37 -0
  55. package/dist/tests/core/journal/requests.test.js +21 -0
  56. package/dist/tests/core/journal/store.test.js +28 -0
  57. package/dist/tests/core/journal/types.test.js +89 -0
  58. package/dist/tests/core/profile.test.js +9 -2
  59. package/dist/tests/core/same-existing-path.test.js +48 -0
  60. package/dist/tests/core/tracks/concurrency.test.js +134 -0
  61. package/dist/tests/core/tracks/context.test.js +177 -0
  62. package/dist/tests/core/tracks/descriptor.test.js +76 -0
  63. package/dist/tests/core/tracks/git.test.js +132 -0
  64. package/dist/tests/core/tracks/join-reconcile.test.js +53 -0
  65. package/dist/tests/core/tracks/join.test.js +197 -0
  66. package/dist/tests/core/tracks/ownership.test.js +96 -0
  67. package/dist/tests/core/tracks/plan-parser.test.js +94 -0
  68. package/dist/tests/core/tracks/protocol.test.js +416 -0
  69. package/dist/tests/core/tracks/teardown.test.js +62 -0
  70. package/dist/tests/core/update-check.test.js +34 -3
  71. package/dist/tests/helpers/git-fixture.js +35 -0
  72. package/dist/tests/integration/parallel-tracks.e2e.test.js +343 -0
  73. package/dist/tests/integration/r5-provider-evidence.test.js +67 -0
  74. package/dist/tests/structural/path-identity-not-string-compare.test.js +51 -0
  75. package/package.json +1 -1
package/README.md CHANGED
@@ -60,3 +60,20 @@ npm run build
60
60
  npm link
61
61
  ```
62
62
  Now simply type `awm` from any path on your machine.
63
+
64
+ ### Parallel tracks
65
+
66
+ Plans without `## Tracks` run serially exactly as before — parallelism is opt-in and there is
67
+ nothing to migrate. A parallel plan must declare task membership, dependency/resource rows,
68
+ and one JSON argv integration command. Start or resume with `awm watch`; inspect with
69
+ `awm track status`. Track workers commit only their assigned files and request
70
+ `awm track join`. The plan supervisor freezes and merges tracks, runs global QA once on the
71
+ final HEAD, executes the canonical integration job once, then applies the interlock.
72
+
73
+ Any missing declaration, overlap, global file class, unavailable worktree, or failed
74
+ preparation degrades to serial with an event naming the cause. `BLOCKED` means ownership or
75
+ identity was **not provable** and needs operator evidence; never delete a worktree, branch,
76
+ lock, or process merely to make it proceed — that destroys the evidence of why it blocked.
77
+
78
+ Full operating guide, including the fallback table and the `BLOCKED` diagnostic path:
79
+ [`docs/guides/parallel-tracks.md`](../docs/guides/parallel-tracks.md).
@@ -1,12 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.computeGate = computeGate;
4
+ exports.computeTrackGate = computeTrackGate;
4
5
  const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
5
- function computeGate(state, corrupt, fingerprintNow) {
6
+ function evaluateEvidence(state, fingerprintNow, scope) {
6
7
  const reasons = [];
7
- if (corrupt || state === null) {
8
- return { pass: false, reasons: [{ category: 'corrupt', detail: 'state.json corrupto o ilegible: la corrupcion jamas certifica' }] };
9
- }
10
8
  if (state.cycle.status === 'BLOCKED') {
11
9
  reasons.push({ category: 'cycle-blocked', detail: `ciclo BLOCKED: ${state.cycle.blockedReason ?? 'sin razon registrada'}` });
12
10
  }
@@ -18,22 +16,25 @@ function computeGate(state, corrupt, fingerprintNow) {
18
16
  reasons.push({ category: 'live-job', detail: `job ${j.id} en ${j.executionState}` });
19
17
  }
20
18
  }
21
- for (const t of state.tasks) {
19
+ const tasksInScope = scope.taskIds === undefined ? state.tasks : state.tasks.filter((t) => scope.taskIds.has(t.id));
20
+ for (const t of tasksInScope) {
22
21
  if (t.status !== 'done') {
23
22
  reasons.push({ category: 'pending-task', detail: `task ${t.id} en ${t.status}` });
24
23
  }
25
24
  }
26
- if (state.cycleVerificationPlan.length === 0) {
27
- reasons.push({ category: 'empty-cycle-plan', detail: 'CycleVerificationPlan vacio: un ciclo sin plan de cierre jamas certifica (R1.4b)' });
28
- }
29
- for (const required of ['qa', 'interlock']) {
30
- if (!state.cycleVerificationPlan.some((item) => item.kind === required)) {
31
- reasons.push({ category: 'missing-verifier', detail: `CycleVerificationPlan requiere '${required}'` });
25
+ if (scope.requireGlobalKinds) {
26
+ if (state.cycleVerificationPlan.length === 0) {
27
+ reasons.push({ category: 'empty-cycle-plan', detail: 'CycleVerificationPlan vacio: un ciclo sin plan de cierre jamas certifica (R1.4b)' });
28
+ }
29
+ for (const required of ['qa', 'interlock']) {
30
+ if (!state.cycleVerificationPlan.some((item) => item.kind === required)) {
31
+ reasons.push({ category: 'missing-verifier', detail: `CycleVerificationPlan requiere '${required}'` });
32
+ }
32
33
  }
33
34
  }
34
35
  // Verificadores requeridos por la config REAL del repo (watch --init):
35
36
  // cada kind requerido debe existir en algun plan (R1.4b, R3.6).
36
- const allPlans = [...state.tasks.flatMap((t) => t.verificationPlan), ...state.cycleVerificationPlan];
37
+ const allPlans = [...tasksInScope.flatMap((t) => t.verificationPlan), ...state.cycleVerificationPlan];
37
38
  const presentKinds = new Set(allPlans.map((i) => i.kind));
38
39
  for (const mechanical of ['test', 'sensors']) {
39
40
  if (!state.requiredVerifiers.includes(mechanical)) {
@@ -81,7 +82,7 @@ function computeGate(state, corrupt, fingerprintNow) {
81
82
  reasons.push({ category: 'stale-fingerprint', detail: `item ${item.id}: la evidencia de ${j.id} es historica (fingerprint ${now === null ? 'no recomputable' : 'cambiado'}) — no certifica` });
82
83
  }
83
84
  }
84
- for (const t of state.tasks) {
85
+ for (const t of tasksInScope) {
85
86
  for (const requiredKind of ['spec', 'quality']) {
86
87
  if (!t.reviewObligations.some((o) => o.kind === requiredKind)) {
87
88
  reasons.push({ category: 'open-obligation', detail: `task ${t.id} carece de ReviewObligation ${requiredKind}` });
@@ -116,3 +117,26 @@ function computeGate(state, corrupt, fingerprintNow) {
116
117
  }
117
118
  return { pass: reasons.length === 0, reasons };
118
119
  }
120
+ function computeGate(state, corrupt, fingerprintNow) {
121
+ if (corrupt || state === null) {
122
+ return { pass: false, reasons: [{ category: 'corrupt', detail: 'state.json corrupto o ilegible: la corrupcion jamas certifica' }] };
123
+ }
124
+ return evaluateEvidence(state, fingerprintNow, { requireGlobalKinds: true });
125
+ }
126
+ /** Gate local de un track (C6, R3.5): NUNCA exige QA/interlock de ambito de
127
+ * plan — solo la evidencia que el propio track declaro. Antes de evaluar,
128
+ * exige `trackContext` (R3.2: la lista de tareas viene EXCLUSIVAMENTE de
129
+ * ahi) y rechaza cualquier tarea que aparezca en el journal pero no en la
130
+ * asignacion del track (R2.3) — nunca certifica por una tarea ajena que se
131
+ * haya colado. */
132
+ function computeTrackGate(state, corruptState, fingerprintNow) {
133
+ if (corruptState || state === null)
134
+ return { pass: false, reasons: [{ category: 'corrupt-state', detail: 'journal ausente o corrupto' }] };
135
+ if (state.trackContext === undefined)
136
+ return { pass: false, reasons: [{ category: 'wrong-context', detail: 'gate local requiere trackContext' }] };
137
+ const assigned = new Set(state.trackContext.taskIds);
138
+ const foreign = state.tasks.filter((task) => !assigned.has(task.id));
139
+ if (foreign.length > 0)
140
+ return { pass: false, reasons: foreign.map((task) => ({ category: 'foreign-task', detail: `task ${task.id} fuera del track` })) };
141
+ return evaluateEvidence(state, fingerprintNow, { requireGlobalKinds: false, taskIds: assigned });
142
+ }
@@ -22,6 +22,7 @@ const store_1 = require("../../core/journal/store");
22
22
  const paths_1 = require("../../core/journal/paths");
23
23
  const lock_1 = require("../watch/lock");
24
24
  const atomic_file_1 = require("../../core/atomic-file");
25
+ const context_1 = require("../../core/tracks/context");
25
26
  const fs_1 = __importDefault(require("fs"));
26
27
  function branchOf(cwd) {
27
28
  // stdio explicito (ver EXEC_STDIO en journal/process.ts): evita el relay
@@ -42,6 +43,20 @@ function realFingerprintNow(repo) {
42
43
  }
43
44
  };
44
45
  }
46
+ /** Guard de entrada (R9.4): sin descriptor de track, es un no-op — el caso
47
+ * comun de siempre no paga costo ni riesgo nuevo. Con descriptor presente
48
+ * que no autentica (fencing/realpath/journalId no coinciden), rechaza ANTES
49
+ * de que el verbo emita o consulte nada, mismo patron de salida que
50
+ * verifyBranchInvariant mas abajo (stderr + exit 1). */
51
+ function assertAuthenticatedCwd(repo, branch) {
52
+ try {
53
+ (0, context_1.resolveCommandContext)(repo, branch);
54
+ }
55
+ catch (e) {
56
+ process.stderr.write(`${e.message}\n`);
57
+ process.exit(1);
58
+ }
59
+ }
45
60
  // CONSTITUTION: commander valida los tokens de las options declaradas; los
46
61
  // variadicos van tras `--`. Los flags numericos/JSON se validan fail-fast.
47
62
  function registerJobCommand(program) {
@@ -55,7 +70,9 @@ function registerJobCommand(program) {
55
70
  .argument('<cmd...>', 'comando tras --')
56
71
  .action((cmd, opts) => {
57
72
  const repo = process.cwd();
58
- const r = (0, request_1.requestJob)(repo, branchOf(repo), opts.generation, cmd, opts.paths ?? [], opts.cwd, { satisfies: opts.satisfies });
73
+ const branch = branchOf(repo);
74
+ assertAuthenticatedCwd(repo, branch);
75
+ const r = (0, request_1.requestJob)(repo, branch, opts.generation, cmd, opts.paths ?? [], opts.cwd, { satisfies: opts.satisfies });
59
76
  process.stdout.write(JSON.stringify({ requestId: r.requestId, idempotencyKey: r.idempotencyKey }, null, 2) + '\n');
60
77
  });
61
78
  job.command('register')
@@ -74,7 +91,9 @@ function registerJobCommand(program) {
74
91
  if (typeof payload !== 'object' || payload === null || Array.isArray(payload))
75
92
  throw new Error('--json requiere un objeto JSON');
76
93
  const repo = process.cwd();
77
- const r = (0, requests_1.emitRequest)(repo, branchOf(repo), {
94
+ const branch = branchOf(repo);
95
+ assertAuthenticatedCwd(repo, branch);
96
+ const r = (0, requests_1.emitRequest)(repo, branch, {
78
97
  kind: 'register-entity', generationToken: opts.generation,
79
98
  idempotencyKey: crypto_1.default.createHash('sha256').update(`${opts.entity}:${opts.json}`).digest('hex'),
80
99
  payload: { entity: opts.entity, ...payload },
@@ -91,6 +110,8 @@ function registerJobCommand(program) {
91
110
  if (!['pass', 'fail', 'inconclusive'].includes(opts.result))
92
111
  throw new Error('--result debe ser pass | fail | inconclusive');
93
112
  const repo = process.cwd();
113
+ const branch = branchOf(repo);
114
+ assertAuthenticatedCwd(repo, branch);
94
115
  const reviewArgv = ['awm-review', opts.obligation];
95
116
  const reviewFingerprint = (0, fingerprint_1.computeFingerprint)(repo, reviewArgv, [], '.');
96
117
  // Determinista a partir de los MISMOS inputs que idempotencyKey, INCLUYENDO
@@ -104,7 +125,7 @@ function registerJobCommand(program) {
104
125
  // generation distinta produce una idempotencyKey ENTERAMENTE distinta, no
105
126
  // una colision con digest distinto.
106
127
  const verdictId = `verd-${crypto_1.default.createHash('sha256').update(`${opts.generation}:${opts.obligation}:${opts.result}:${opts.detail}:${reviewFingerprint.fingerprint}`).digest('hex').slice(0, 16)}`;
107
- (0, requests_1.emitRequest)(repo, branchOf(repo), {
128
+ (0, requests_1.emitRequest)(repo, branch, {
108
129
  kind: 'verdict', generationToken: opts.generation,
109
130
  idempotencyKey: crypto_1.default.createHash('sha256').update(`verdict:${opts.generation}:${opts.obligation}:${opts.result}:${opts.detail}:${reviewFingerprint.fingerprint}`).digest('hex'),
110
131
  payload: {
@@ -116,17 +137,31 @@ function registerJobCommand(program) {
116
137
  });
117
138
  job.command('controller-heartbeat')
118
139
  .requiredOption('--generation <token>')
119
- .action((opts) => { (0, heartbeat_1.emitHeartbeat)(process.cwd(), branchOf(process.cwd()), opts.generation); });
140
+ .action((opts) => {
141
+ const repo = process.cwd();
142
+ const branch = branchOf(repo);
143
+ assertAuthenticatedCwd(repo, branch);
144
+ (0, heartbeat_1.emitHeartbeat)(repo, branch, opts.generation);
145
+ });
120
146
  job.command('ps').action(() => {
121
- process.stdout.write(JSON.stringify((0, query_1.queryPs)(process.cwd(), branchOf(process.cwd())), null, 2) + '\n');
147
+ const repo = process.cwd();
148
+ const branch = branchOf(repo);
149
+ assertAuthenticatedCwd(repo, branch);
150
+ process.stdout.write(JSON.stringify((0, query_1.queryPs)(repo, branch), null, 2) + '\n');
122
151
  });
123
152
  job.command('list').action(() => {
124
- process.stdout.write(JSON.stringify((0, query_1.queryList)(process.cwd(), branchOf(process.cwd())), null, 2) + '\n');
153
+ const repo = process.cwd();
154
+ const branch = branchOf(repo);
155
+ assertAuthenticatedCwd(repo, branch);
156
+ process.stdout.write(JSON.stringify((0, query_1.queryList)(repo, branch), null, 2) + '\n');
125
157
  });
126
158
  job.command('show')
127
159
  .argument('<jobId>')
128
160
  .action((jobId) => {
129
- const out = (0, query_1.queryShow)(process.cwd(), branchOf(process.cwd()), jobId);
161
+ const repo = process.cwd();
162
+ const branch = branchOf(repo);
163
+ assertAuthenticatedCwd(repo, branch);
164
+ const out = (0, query_1.queryShow)(repo, branch, jobId);
130
165
  process.stdout.write(JSON.stringify(out, null, 2) + '\n');
131
166
  if (out.corruptState || out.job === null)
132
167
  process.exit(1);
@@ -136,6 +171,7 @@ function registerJobCommand(program) {
136
171
  .action(() => {
137
172
  const repo = process.cwd();
138
173
  const branch = branchOf(repo);
174
+ assertAuthenticatedCwd(repo, branch);
139
175
  const r = (0, store_1.readJournal)(repo, branch);
140
176
  if (r.corrupt || r.state === null) {
141
177
  process.stdout.write(JSON.stringify({ corruptState: true }, null, 2) + '\n');
@@ -164,6 +200,7 @@ function registerJobCommand(program) {
164
200
  .action(() => {
165
201
  const repo = process.cwd();
166
202
  const branch = branchOf(repo);
203
+ assertAuthenticatedCwd(repo, branch);
167
204
  const r = (0, store_1.readJournal)(repo, branch);
168
205
  if (r.state !== null) {
169
206
  try {
@@ -185,7 +222,9 @@ function registerJobCommand(program) {
185
222
  .option('--jobs <ids...>', 'ids de jobs a terminar (obligatorio con --execute)')
186
223
  .action(async (opts) => {
187
224
  const repo = process.cwd();
188
- const r = (0, store_1.readJournal)(repo, branchOf(repo));
225
+ const branch = branchOf(repo);
226
+ assertAuthenticatedCwd(repo, branch);
227
+ const r = (0, store_1.readJournal)(repo, branch);
189
228
  if (r.corrupt || r.state === null) {
190
229
  process.stderr.write('journal corrupto o ausente\n');
191
230
  process.exit(1);
@@ -205,6 +244,7 @@ function registerJobCommand(program) {
205
244
  .action((opts) => {
206
245
  const repo = process.cwd();
207
246
  const branch = branchOf(repo);
247
+ assertAuthenticatedCwd(repo, branch);
208
248
  const r = (0, store_1.readJournal)(repo, branch);
209
249
  if (r.corrupt || r.state === null) {
210
250
  process.stderr.write('journal corrupto\n');
@@ -7,21 +7,59 @@ exports.requestJob = requestJob;
7
7
  const crypto_1 = __importDefault(require("crypto"));
8
8
  const fingerprint_1 = require("../../core/journal/fingerprint");
9
9
  const requests_1 = require("../../core/journal/requests");
10
+ const store_1 = require("../../core/journal/store");
11
+ const git_1 = require("../../core/tracks/git");
12
+ function argvDigest(argv) {
13
+ return crypto_1.default.createHash('sha256').update(JSON.stringify(argv)).digest('hex');
14
+ }
10
15
  /** El agente NO ejecuta: registra la intencion (design R3.1). La idempotencyKey
11
- * es hash(fingerprint + commandDigest) => get-or-create atomico (RNF-T.7).
12
- * El cwd relativo REAL es parte del fingerprint (R3.4). `satisfies` enlaza el
13
- * job con el item de VerificationPlan que pretende satisfacer (R1.4c). */
16
+ * es hash(fingerprint + commandDigest + satisfies) => get-or-create atomico
17
+ * (RNF-T.7). El cwd relativo REAL es parte del fingerprint (R3.4). `satisfies`
18
+ * enlaza el job con el/los item(s) de VerificationPlan que pretende satisfacer
19
+ * (R1.4c). R7 Task 12: `satisfies` migra a `string | string[]` — un caller
20
+ * singular (todo el codigo pre-Task-12) produce la MISMA idempotencyKey que
21
+ * antes (`[opts.satisfies].join('\0') === opts.satisfies`, y
22
+ * `[].join('\0') === ''` para `undefined`): la identidad mecanica de jobs
23
+ * existentes no cambia. El finalizer es el ÚNICO caller que pasa un array con
24
+ * MÁS de un elemento — siempre el conjunto COMPLETO y ordenado de
25
+ * `track-integration:*` de la cohorte (nunca un subconjunto, ver Step 5). */
14
26
  function requestJob(repoRoot, branch, generationToken, argv, paths, cwdRel, opts = {}) {
15
27
  const fp = (0, fingerprint_1.computeFingerprint)(repoRoot, argv, paths, cwdRel);
16
28
  // La obligacion es parte de la identidad de la REQUEST, no de la ejecucion:
17
- // apply.ts reutiliza el job mecanicamente equivalente y enlaza el nuevo item.
18
- const idempotencyKey = crypto_1.default.createHash('sha256').update(`${fp.fingerprint}:${fp.commandDigest}:${opts.satisfies ?? ''}`).digest('hex');
29
+ // apply.ts reutiliza el job mecanicamente equivalente y enlaza los items
30
+ // nuevos. El set se deduplica y ordena SIEMPRE — dos pedidos del mismo
31
+ // comando con el mismo conjunto de satisfiers (en cualquier orden) deben
32
+ // colapsar a la MISMA idempotencyKey.
33
+ const satisfies = opts.satisfies === undefined ? []
34
+ : Array.isArray(opts.satisfies) ? [...new Set(opts.satisfies)].sort() : [opts.satisfies];
35
+ const idempotencyKey = crypto_1.default.createHash('sha256')
36
+ .update(`${fp.fingerprint}:${fp.commandDigest}:${satisfies.join('\0')}`).digest('hex');
37
+ if (opts.verificationKind === 'track-integration') {
38
+ // Step 6 (R7.1/C3/C4): el job canónico de integración final SOLO se
39
+ // pide con todos los merges aplicados, árbol limpio, y exactamente el
40
+ // argv registrado como contrato de la cohorte — cualquier desviación
41
+ // se rechaza ANTES de tocar el requestsDir (fail-closed, nunca se
42
+ // emite una request que `apply.ts` tendría que rechazar después).
43
+ const r = (0, store_1.readJournal)(repoRoot, branch);
44
+ if (r.corrupt || r.state === null)
45
+ throw new Error('track-integration requiere journal legible (R1.6)');
46
+ const s = r.state;
47
+ if (s.tracks?.some((t) => t.phase !== 'MERGED_UNVERIFIED')) {
48
+ throw new Error('track-integration requiere todos los merges aplicados');
49
+ }
50
+ if ((0, git_1.dirtyPaths)(repoRoot).length > 0)
51
+ throw new Error('track-integration requiere árbol limpio');
52
+ if (s.trackIntegration === undefined)
53
+ throw new Error('track-integration requiere un contrato canónico registrado');
54
+ if (argvDigest(argv) !== argvDigest(s.trackIntegration.argv))
55
+ throw new Error('argv de integración no canónico');
56
+ }
19
57
  return (0, requests_1.emitRequest)(repoRoot, branch, {
20
58
  kind: 'job-request', generationToken, idempotencyKey,
21
59
  payload: {
22
60
  argv, paths, cwd: cwdRel,
23
61
  fingerprint: fp.fingerprint, commandDigest: fp.commandDigest, expandedPaths: fp.expandedPaths,
24
- ...(opts.satisfies !== undefined ? { satisfies: opts.satisfies } : {}),
62
+ ...(satisfies.length > 0 ? { satisfies } : {}),
25
63
  },
26
64
  });
27
65
  }
@@ -0,0 +1,26 @@
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.emitTrackRequest = emitTrackRequest;
7
+ // Emisores de requests de track (R6.1): `add`/`join`/`remove` jamas mutan
8
+ // Git ni el journal directamente — el unico efecto observable es publicar
9
+ // una request inmutable que el supervisor del plan consume despues (mismo
10
+ // modelo single-writer de R1). La idempotencyKey se liga al journal
11
+ // (branch), al track y al tipo de intent, para que un retry genuino del
12
+ // mismo comando produzca la MISMA key (colapso seguro) mientras que dos
13
+ // intents distintos sobre el mismo track jamas colisionan.
14
+ const crypto_1 = __importDefault(require("crypto"));
15
+ const requests_1 = require("../../core/journal/requests");
16
+ function emitTrackRequest(repoRoot, branch, generationToken, kind, trackId) {
17
+ if (trackId.length === 0)
18
+ throw new Error('trackId obligatorio');
19
+ const payload = { trackId };
20
+ return (0, requests_1.emitRequest)(repoRoot, branch, {
21
+ kind, generationToken,
22
+ idempotencyKey: crypto_1.default.createHash('sha256')
23
+ .update(`${kind}\0${branch}\0${trackId}`).digest('hex'),
24
+ payload,
25
+ });
26
+ }
@@ -0,0 +1,194 @@
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.registerTrackCommand = registerTrackCommand;
7
+ const child_process_1 = require("child_process");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const emit_1 = require("./emit");
10
+ const status_1 = require("./status");
11
+ const store_1 = require("../../core/journal/store");
12
+ const process_1 = require("../../core/journal/process");
13
+ const context_1 = require("../../core/tracks/context");
14
+ const plan_parser_1 = require("../../core/tracks/plan-parser");
15
+ const ownership_1 = require("../../core/tracks/ownership");
16
+ const git_1 = require("../../core/tracks/git");
17
+ const descriptor_1 = require("../../core/tracks/descriptor");
18
+ const supervisor_wrapper_1 = require("./supervisor-wrapper");
19
+ // Mismo patron que cli/src/commands/job/index.ts (branchOf/assertAuthenticatedCwd):
20
+ // duplicado deliberadamente en vez de exportado desde job/index.ts, que no
21
+ // expone esos helpers y no forma parte del alcance de esta task.
22
+ function branchOf(cwd) {
23
+ const b = (0, child_process_1.execFileSync)('git', ['branch', '--show-current'], { cwd, encoding: 'utf8', stdio: process_1.EXEC_STDIO }).trim();
24
+ if (b.length === 0)
25
+ throw new Error('no hay rama actual (HEAD detached): el journal es por rama');
26
+ return b;
27
+ }
28
+ /** Unico punto que escribe a stderr y sale != 0 por un fallo de guard —
29
+ * ambos guards de abajo comparten esta forma (R9.4). */
30
+ function failGuard(message) {
31
+ process.stderr.write(`${message}\n`);
32
+ process.exit(1);
33
+ }
34
+ /** Autentica el cwd (R9.4) y devuelve el contexto resuelto: sin descriptor
35
+ * de track es un no-op que resuelve a modo plan (caso comun); con
36
+ * descriptor presente que no autentica, rechaza ANTES de que el verbo
37
+ * emita o consulte nada. */
38
+ function resolveGuardedContext(repo, branch) {
39
+ try {
40
+ return (0, context_1.resolveCommandContext)(repo, branch);
41
+ }
42
+ catch (e) {
43
+ failGuard(e.message);
44
+ }
45
+ }
46
+ /** Guard de entrada identico al de `awm job`/`awm watch`: solo autentica. */
47
+ function assertAuthenticatedCwd(repo, branch) {
48
+ resolveGuardedContext(repo, branch);
49
+ }
50
+ /** Guard mas estricto para `list`/`status`: ambos verbos agregan el journal
51
+ * del PLAN (R9.5/R9.6 — `tracks` solo existe ahi, nunca en el journal de un
52
+ * track individual). Sin esto, correrlos desde el worktree de un track
53
+ * produce silenciosamente "sin tracks declarados" — indistinguible de un
54
+ * plan serial/vacio genuino. */
55
+ function assertPlanCwd(repo, branch) {
56
+ const ctx = resolveGuardedContext(repo, branch);
57
+ if (ctx.mode === 'track') {
58
+ failGuard(`este cwd es el worktree del track '${ctx.context.trackContext.trackId}', no la raiz del plan: `
59
+ + `'awm track list'/'status' agregan el journal del PLAN — corre el comando desde ahi`);
60
+ }
61
+ }
62
+ function registerTrackCommand(program) {
63
+ const track = program.command('track').description('tracks paralelos sobre worktrees (R5): superficie request-only + status agregado read-only');
64
+ // --- Verbos mutantes: SOLO emiten una request (R6.1). Jamas tocan Git ni
65
+ // el journal directamente — el supervisor del plan la consume despues.
66
+ track.command('add')
67
+ .description('emite track-prepare-request — el supervisor del plan la consume (R6.1)')
68
+ .requiredOption('--generation <token>', 'token de la generacion vigente')
69
+ .argument('<trackId>')
70
+ .action((trackId, opts) => {
71
+ const repo = process.cwd();
72
+ const branch = branchOf(repo);
73
+ assertAuthenticatedCwd(repo, branch);
74
+ const r = (0, emit_1.emitTrackRequest)(repo, branch, opts.generation, 'track-prepare-request', trackId);
75
+ process.stdout.write(JSON.stringify({ requestId: r.requestId, idempotencyKey: r.idempotencyKey }, null, 2) + '\n');
76
+ });
77
+ track.command('join')
78
+ .description('emite track-join-request — integracion es propiedad exclusiva del supervisor del plan (R6.1)')
79
+ .requiredOption('--generation <token>', 'token de la generacion vigente')
80
+ .argument('<trackId>')
81
+ .action((trackId, opts) => {
82
+ const repo = process.cwd();
83
+ const branch = branchOf(repo);
84
+ assertAuthenticatedCwd(repo, branch);
85
+ const r = (0, emit_1.emitTrackRequest)(repo, branch, opts.generation, 'track-join-request', trackId);
86
+ process.stdout.write(JSON.stringify({ requestId: r.requestId, idempotencyKey: r.idempotencyKey }, null, 2) + '\n');
87
+ });
88
+ track.command('remove')
89
+ .description('emite track-teardown-request — el supervisor del plan desmantela worktree/rama (R6.1)')
90
+ .requiredOption('--generation <token>', 'token de la generacion vigente')
91
+ .argument('<trackId>')
92
+ .action((trackId, opts) => {
93
+ const repo = process.cwd();
94
+ const branch = branchOf(repo);
95
+ assertAuthenticatedCwd(repo, branch);
96
+ const r = (0, emit_1.emitTrackRequest)(repo, branch, opts.generation, 'track-teardown-request', trackId);
97
+ process.stdout.write(JSON.stringify({ requestId: r.requestId, idempotencyKey: r.idempotencyKey }, null, 2) + '\n');
98
+ });
99
+ // --- Verbos read-only: nunca aceptan --generation ni emiten requests.
100
+ track.command('list')
101
+ .description('lista los TrackRef declarados en el journal del plan (read-only)')
102
+ .action(() => {
103
+ const repo = process.cwd();
104
+ const branch = branchOf(repo);
105
+ assertPlanCwd(repo, branch);
106
+ const r = (0, store_1.readJournal)(repo, branch);
107
+ if (r.corrupt || r.state === null) {
108
+ process.stdout.write(JSON.stringify({ corruptState: true }, null, 2) + '\n');
109
+ process.exit(1);
110
+ }
111
+ process.stdout.write(JSON.stringify({ tracks: r.state.tracks ?? [] }, null, 2) + '\n');
112
+ });
113
+ track.command('status')
114
+ .description('agregado read-only: compone el gate de cada journal de track + fase de la cohorte (R9.5, R9.6)')
115
+ .action(() => {
116
+ const repo = process.cwd();
117
+ const branch = branchOf(repo);
118
+ assertPlanCwd(repo, branch);
119
+ const r = (0, store_1.readJournal)(repo, branch);
120
+ if (r.corrupt || r.state === null) {
121
+ process.stdout.write(JSON.stringify({ corruptState: true }, null, 2) + '\n');
122
+ process.exit(1);
123
+ }
124
+ const out = (0, status_1.aggregateTrackStatus)(repo, r.state);
125
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
126
+ // Fail-closed (R3.2 aplicado por composicion, R9.6): CUALQUIER
127
+ // track con gate rojo — corrupto, bloqueado o con evidencia
128
+ // pendiente — hace que `status` salga != 0, aunque el agregado
129
+ // completo ya se haya impreso arriba.
130
+ if (Object.values(out.tracks).some((t) => !t.gate.pass))
131
+ process.exit(1);
132
+ });
133
+ track.command('verify-independence')
134
+ .description('R5.10: verifica independencia declarada de un plan de tracks; invocable por argv, sale != 0 ante cualquier violacion')
135
+ .requiredOption('--plan <file>', 'ruta al plan markdown con membresia de Track y tabla ## Tracks')
136
+ .action((opts) => {
137
+ // R5.10: sale != 0 ante CUALQUIER violacion, incluyendo que la
138
+ // tuberia leer-plan -> parsear -> evaluar independencia lance
139
+ // sincronicamente (id de track peligroso, `Integration argv`
140
+ // no-JSON, `Shared resources` sin forma `<clase>:<valor>`, etc.)
141
+ // — nunca un stack trace crudo. El try envuelve la tuberia
142
+ // ENTERA a proposito (no solo el parseo) y el `process.exit` de
143
+ // abajo vive AFUERA de el: los tests mockean `process.exit` para
144
+ // que *lance*, y si ese exit ocurriera dentro del try se
145
+ // re-atraparia como un fallo de dominio mas.
146
+ let result;
147
+ try {
148
+ const source = fs_1.default.readFileSync(opts.plan, 'utf8');
149
+ const parsed = (0, plan_parser_1.parseTrackPlan)(source, git_1.gitCheckTrackId);
150
+ result = parsed.mode === 'serial'
151
+ // Un plan que ni siquiera califica como candidato paralelo
152
+ // (recursos compartidos sin declarar, dependencias entre
153
+ // tracks, etc.) es, por definicion, una violacion de R5.10.
154
+ ? { parallel: false, reasons: [parsed.reason] }
155
+ : (0, ownership_1.assessDeclaredIndependence)(Object.values(parsed.tracks));
156
+ }
157
+ catch (e) {
158
+ result = { parallel: false, reasons: [`parse-error:${e.message}`] };
159
+ }
160
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
161
+ if (!result.parallel)
162
+ process.exit(1);
163
+ });
164
+ // --- supervisor-wrapper: PROCESO EXTERNO detached (Step 5, R4.7-R4.10,
165
+ // C11). El supervisor del plan lo spawnea con el cwd fijado en el
166
+ // worktree del track — jamás se invoca a mano. Autentica el descriptor
167
+ // local (mismo patrón que R9.4) antes de reclamar nada: un cwd sin
168
+ // descriptor o con un descriptor que no coincide con el argv recibido
169
+ // jamás llega a tocar el claim.
170
+ track.command('supervisor-wrapper')
171
+ .description('PROCESO INTERNO: lanzado detached por el supervisor del plan (R4.7) — no invocar a mano')
172
+ .requiredOption('--track <id>')
173
+ .requiredOption('--readiness <nonce>')
174
+ .requiredOption('--fence <token>')
175
+ // R4.7/C11: el nonce del `supervisorIntent` persistido por el plan
176
+ // (tracks.ts) ANTES de spawnear este wrapper — NUNCA generado acá.
177
+ // Sin este flag, `observeSupervisorFromDisk` no tiene con qué
178
+ // comparar la identidad y todo wrapper legítimo quedaría 'foreign'.
179
+ .requiredOption('--nonce <n>')
180
+ .action(async (opts) => {
181
+ const worktreePath = process.cwd();
182
+ const descriptor = (0, descriptor_1.readDescriptor)(worktreePath);
183
+ if (descriptor === null)
184
+ failGuard('supervisor-wrapper: sin descriptor de track en este cwd — no autenticado');
185
+ if (descriptor.trackId !== opts.track || descriptor.fencingToken !== opts.fence) {
186
+ failGuard('supervisor-wrapper: descriptor no coincide con los argumentos recibidos — abortando');
187
+ }
188
+ await (0, supervisor_wrapper_1.runSupervisorWrapper)({
189
+ worktreePath, trackId: opts.track, nonce: opts.nonce, readinessNonce: opts.readiness, fencingToken: opts.fence,
190
+ planRoot: descriptor.planRoot, planBranch: descriptor.planBranch,
191
+ });
192
+ process.exit(0); // C11: tanto "ya reclamado" como "arrancado" salen 0 — jamás un segundo supervisor
193
+ });
194
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fingerprintFor = fingerprintFor;
4
+ exports.deriveCohortPhase = deriveCohortPhase;
5
+ exports.aggregateTrackStatus = aggregateTrackStatus;
6
+ // Agregado read-only de status por track (R9.5, R9.6): el journal del PLAN
7
+ // jamas espeja el estado de los journals de track — cada consulta lee el
8
+ // journal observado del track AL MOMENTO de necesitarlo y compone su gate,
9
+ // sin escribir nada de vuelta al plan journal.
10
+ const fingerprint_1 = require("../../core/journal/fingerprint");
11
+ const store_1 = require("../../core/journal/store");
12
+ const types_1 = require("../../core/tracks/types");
13
+ const gate_1 = require("../job/gate");
14
+ /** Espejo de `realFingerprintNow` (cli/src/commands/job/index.ts) pero
15
+ * cerrado sobre el worktree OBSERVADO del track, no sobre el repo desde el
16
+ * que corre el comando `awm track status` (que es el del plan). */
17
+ function fingerprintFor(worktreePath) {
18
+ return (argv, paths, cwd) => {
19
+ try {
20
+ return (0, fingerprint_1.computeFingerprint)(worktreePath, argv, paths, cwd).fingerprint;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ };
26
+ }
27
+ /** String de DISPLAY para un humano corriendo `awm track status` — no es
28
+ * input de ninguna decision de protocolo (esa vive en tracks/protocol.ts).
29
+ * Reporta el cuello de botella: cualquier track BLOCKED domina el mensaje;
30
+ * si no hay ninguno, se muestra la fase MENOS avanzada segun el orden
31
+ * canonico de TRACK_PHASES (la que atrasa a la cohorte) y cuantos tracks
32
+ * comparten esa fase. */
33
+ function deriveCohortPhase(tracks) {
34
+ if (tracks.length === 0)
35
+ return 'sin tracks declarados';
36
+ const blocked = tracks.filter((t) => t.phase === 'BLOCKED');
37
+ if (blocked.length > 0) {
38
+ return `BLOCKED: ${blocked.map((t) => t.trackId).sort().join(', ')}`;
39
+ }
40
+ const rank = (phase) => types_1.TRACK_PHASES.indexOf(phase);
41
+ const bottleneck = tracks.reduce((min, t) => (rank(t.phase) < rank(min.phase) ? t : min));
42
+ const atBottleneck = tracks.filter((t) => t.phase === bottleneck.phase).length;
43
+ return `${bottleneck.phase} (${atBottleneck}/${tracks.length})`;
44
+ }
45
+ /** `planRoot` no se usa para leer el journal del PLAN (ese ya se leyo antes
46
+ * de llamar aca) — se recibe por simetria con la firma del plan y para que
47
+ * el llamador no tenga que reconstruirla; cada TrackRef ya trae su propio
48
+ * `worktreePath`/`branch` absolutos (R9.2), que es lo que efectivamente se
49
+ * lee. Un journal de track que no puede leerse (corrupto o ausente) nunca
50
+ * se descarta en silencio: su gate sale rojo con categoria `corrupt-state`,
51
+ * igual que cualquier otra corrupcion en este codebase (R1.6). */
52
+ function aggregateTrackStatus(planRoot, plan) {
53
+ const refs = plan.tracks ?? [];
54
+ const tracks = {};
55
+ for (const ref of refs) {
56
+ const observed = (0, store_1.readJournal)(ref.worktreePath, ref.branch);
57
+ tracks[ref.trackId] = {
58
+ phase: ref.phase,
59
+ gate: (0, gate_1.computeTrackGate)(observed.state, observed.corrupt, fingerprintFor(ref.worktreePath)),
60
+ };
61
+ }
62
+ return { cohort: deriveCohortPhase(refs), tracks };
63
+ }