agentic-workflow-manager 6.2.1 → 6.4.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.
- package/README.md +17 -0
- package/dist/src/commands/job/gate.js +37 -13
- package/dist/src/commands/job/index.js +48 -8
- package/dist/src/commands/job/request.js +44 -6
- package/dist/src/commands/track/emit.js +26 -0
- package/dist/src/commands/track/index.js +194 -0
- package/dist/src/commands/track/status.js +63 -0
- package/dist/src/commands/track/supervisor-wrapper.js +89 -0
- package/dist/src/commands/watch/apply.js +201 -13
- package/dist/src/commands/watch/index.js +14 -0
- package/dist/src/commands/watch/runner.js +20 -1
- package/dist/src/commands/watch/supervisor.js +251 -27
- package/dist/src/commands/watch/teardown-driver.js +189 -0
- package/dist/src/commands/watch/tracks.js +1100 -0
- package/dist/src/core/journal/adapter.js +46 -6
- package/dist/src/core/journal/paths.js +9 -0
- package/dist/src/core/journal/process.js +101 -1
- package/dist/src/core/journal/requests.js +5 -1
- package/dist/src/core/journal/store.js +34 -2
- package/dist/src/core/journal/types.js +93 -4
- package/dist/src/core/paths.js +51 -0
- package/dist/src/core/tracks/concurrency.js +85 -0
- package/dist/src/core/tracks/context.js +89 -0
- package/dist/src/core/tracks/descriptor.js +40 -0
- package/dist/src/core/tracks/git.js +318 -0
- package/dist/src/core/tracks/join.js +185 -0
- package/dist/src/core/tracks/ownership.js +100 -0
- package/dist/src/core/tracks/plan-parser.js +108 -0
- package/dist/src/core/tracks/protocol.js +466 -0
- package/dist/src/core/tracks/teardown.js +34 -0
- package/dist/src/core/tracks/types.js +14 -0
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +267 -0
- package/dist/tests/commands/track/fixtures.js +13 -0
- package/dist/tests/commands/track/status.test.js +157 -0
- package/dist/tests/commands/track/supervisor-wrapper-cli.test.js +129 -0
- package/dist/tests/commands/track/supervisor-wrapper.test.js +131 -0
- package/dist/tests/commands/track/verbs.test.js +326 -0
- package/dist/tests/commands/watch/apply.test.js +116 -4
- package/dist/tests/commands/watch/runner.test.js +22 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +150 -0
- package/dist/tests/commands/watch/track-bootstrap-crash.test.js +342 -0
- package/dist/tests/commands/watch/track-bootstrap.test.js +350 -0
- package/dist/tests/commands/watch/track-finalize.test.js +643 -0
- package/dist/tests/commands/watch/track-freeze.test.js +591 -0
- package/dist/tests/commands/watch/track-join-crash.test.js +439 -0
- package/dist/tests/commands/watch/track-runtime-git.test.js +119 -0
- package/dist/tests/commands/watch/track-teardown-crash.test.js +426 -0
- package/dist/tests/core/journal/adapter-override.test.js +51 -0
- package/dist/tests/core/journal/process.test.js +37 -0
- package/dist/tests/core/journal/requests.test.js +21 -0
- package/dist/tests/core/journal/store.test.js +28 -0
- package/dist/tests/core/journal/types.test.js +89 -0
- package/dist/tests/core/same-existing-path.test.js +48 -0
- package/dist/tests/core/tracks/concurrency.test.js +134 -0
- package/dist/tests/core/tracks/context.test.js +177 -0
- package/dist/tests/core/tracks/descriptor.test.js +76 -0
- package/dist/tests/core/tracks/git.test.js +132 -0
- package/dist/tests/core/tracks/join-reconcile.test.js +53 -0
- package/dist/tests/core/tracks/join.test.js +197 -0
- package/dist/tests/core/tracks/ownership.test.js +96 -0
- package/dist/tests/core/tracks/plan-parser.test.js +94 -0
- package/dist/tests/core/tracks/protocol.test.js +416 -0
- package/dist/tests/core/tracks/teardown.test.js +62 -0
- package/dist/tests/helpers/git-fixture.js +35 -0
- package/dist/tests/integration/parallel-tracks.e2e.test.js +343 -0
- package/dist/tests/integration/r5-provider-evidence.test.js +67 -0
- package/dist/tests/structural/path-identity-not-string-compare.test.js +51 -0
- package/dist/tests/structural/sensor-configs-are-present.test.js +49 -0
- 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
|
|
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
|
-
|
|
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 (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 = [...
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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) => {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
12
|
-
* El cwd relativo REAL es parte del fingerprint (R3.4). `satisfies`
|
|
13
|
-
* job con el item de VerificationPlan que pretende satisfacer
|
|
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
|
|
18
|
-
|
|
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
|
-
...(
|
|
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
|
+
}
|