agentic-workflow-manager 3.4.0 → 3.6.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/dist/src/commands/job/exec-wrapper.js +136 -0
- package/dist/src/commands/job/export.js +94 -0
- package/dist/src/commands/job/gate.js +118 -0
- package/dist/src/commands/job/heartbeat.js +15 -0
- package/dist/src/commands/job/index.js +246 -0
- package/dist/src/commands/job/query.js +37 -0
- package/dist/src/commands/job/reap.js +24 -0
- package/dist/src/commands/job/reconcile.js +112 -0
- package/dist/src/commands/job/request.js +27 -0
- package/dist/src/commands/sensors/exec.js +121 -0
- package/dist/src/commands/sensors/index.js +4 -4
- package/dist/src/commands/sensors/run.js +124 -71
- package/dist/src/commands/watch/apply.js +352 -0
- package/dist/src/commands/watch/generations.js +249 -0
- package/dist/src/commands/watch/index.js +49 -0
- package/dist/src/commands/watch/init.js +72 -0
- package/dist/src/commands/watch/lock.js +89 -0
- package/dist/src/commands/watch/runner.js +191 -0
- package/dist/src/commands/watch/supervisor.js +266 -0
- package/dist/src/core/atomic-file.js +31 -0
- package/dist/src/core/export/pack.js +7 -1
- package/dist/src/core/journal/adapter.js +27 -0
- package/dist/src/core/journal/fingerprint.js +80 -0
- package/dist/src/core/journal/paths.js +56 -0
- package/dist/src/core/journal/process.js +284 -0
- package/dist/src/core/journal/redact.js +142 -0
- package/dist/src/core/journal/requests.js +132 -0
- package/dist/src/core/journal/store.js +107 -0
- package/dist/src/core/journal/types.js +165 -0
- package/dist/src/index.js +4 -0
- package/dist/tests/commands/job/exec-wrapper.test.js +85 -0
- package/dist/tests/commands/job/export.test.js +76 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +297 -0
- package/dist/tests/commands/job/reap-cli.test.js +101 -0
- package/dist/tests/commands/job/verbs.test.js +56 -0
- package/dist/tests/commands/job/verdict-determinism.test.js +138 -0
- package/dist/tests/commands/sensors/exec-fixtures.js +24 -0
- package/dist/tests/commands/sensors/exec.test.js +91 -0
- package/dist/tests/commands/sensors/run-inconclusive.test.js +55 -66
- package/dist/tests/commands/sensors/run-partial.test.js +225 -0
- package/dist/tests/commands/sensors/run-tool-missing.test.js +6 -6
- package/dist/tests/commands/sensors/run.test.js +64 -81
- package/dist/tests/commands/watch/apply.test.js +397 -0
- package/dist/tests/commands/watch/e2e-crash.test.js +157 -0
- package/dist/tests/commands/watch/generations.test.js +115 -0
- package/dist/tests/commands/watch/integration.test.js +124 -0
- package/dist/tests/commands/watch/lock.test.js +60 -0
- package/dist/tests/commands/watch/runner.test.js +239 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +203 -0
- package/dist/tests/commands/watch/watch-init.test.js +43 -0
- package/dist/tests/core/atomic-file-durable.test.js +42 -0
- package/dist/tests/core/journal/adapter.test.js +27 -0
- package/dist/tests/core/journal/fingerprint.test.js +164 -0
- package/dist/tests/core/journal/paths.test.js +35 -0
- package/dist/tests/core/journal/process.test.js +213 -0
- package/dist/tests/core/journal/redact.test.js +59 -0
- package/dist/tests/core/journal/requests.test.js +134 -0
- package/dist/tests/core/journal/store.test.js +88 -0
- package/dist/tests/core/journal/types.test.js +78 -0
- package/dist/tests/structural/exec-invocation-explicit-stdio.test.js +94 -0
- package/package.json +1 -1
|
@@ -0,0 +1,136 @@
|
|
|
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.claimPath = claimPath;
|
|
7
|
+
exports.identityPath = identityPath;
|
|
8
|
+
exports.resultPath = resultPath;
|
|
9
|
+
exports.logPath = logPath;
|
|
10
|
+
exports.replayVerdict = replayVerdict;
|
|
11
|
+
exports.runExecWrapper = runExecWrapper;
|
|
12
|
+
// PROCESO EXTERNO real (bloqueador 3): el supervisor lo spawnea detached y no
|
|
13
|
+
// lo espera. Hace el spawn demostrable (design R1.8): claim exclusivo por
|
|
14
|
+
// spawnNonce -> identidad real persistida -> ejecucion independiente ->
|
|
15
|
+
// resultado terminal atomico. La matriz de replay es LA UNICA (R3.3).
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
19
|
+
const process_1 = require("../../core/journal/process");
|
|
20
|
+
const fingerprint_1 = require("../../core/journal/fingerprint");
|
|
21
|
+
const redact_1 = require("../../core/journal/redact");
|
|
22
|
+
const atomic_file_1 = require("../../core/atomic-file");
|
|
23
|
+
function claimPath(logsRoot, jobId, nonce) {
|
|
24
|
+
return path_1.default.join(logsRoot, `${jobId}.${nonce}.claim`);
|
|
25
|
+
}
|
|
26
|
+
function identityPath(logsRoot, jobId, nonce) {
|
|
27
|
+
return path_1.default.join(logsRoot, `${jobId}.${nonce}.identity.json`);
|
|
28
|
+
}
|
|
29
|
+
function resultPath(logsRoot, jobId, nonce) {
|
|
30
|
+
return path_1.default.join(logsRoot, `${jobId}.${nonce}.result.json`);
|
|
31
|
+
}
|
|
32
|
+
function logPath(logsRoot, jobId, nonce) {
|
|
33
|
+
return path_1.default.join(logsRoot, `${jobId}.${nonce}.log`);
|
|
34
|
+
}
|
|
35
|
+
function replayVerdict(logsRoot, jobId, nonce) {
|
|
36
|
+
if (!fs_1.default.existsSync(claimPath(logsRoot, jobId, nonce)))
|
|
37
|
+
return 'never-started';
|
|
38
|
+
if (fs_1.default.existsSync(resultPath(logsRoot, jobId, nonce)))
|
|
39
|
+
return 'completed';
|
|
40
|
+
return 'unprovable';
|
|
41
|
+
}
|
|
42
|
+
const MAX_LOG_BYTES = 1024 * 1024; // retencion acotada (R2.5)
|
|
43
|
+
// Ventana acotada post-exit para el flush de stdio (R1.8, R2.5). 300ms es
|
|
44
|
+
// generoso frente al caso real (datos ya en vuelo en el pipe cuando 'exit'
|
|
45
|
+
// dispara, tipicamente entregados en 1 tick del event loop) sin acercarse a
|
|
46
|
+
// una espera perceptible si un descendiente hereda los fds y nunca cierra.
|
|
47
|
+
const STDIO_GRACE_MS = 300;
|
|
48
|
+
async function runExecWrapper(opts) {
|
|
49
|
+
const { logsRoot, jobId, nonce, argv, cwd } = opts;
|
|
50
|
+
const repoRoot = opts.repoRoot ?? process.cwd();
|
|
51
|
+
if (argv.length === 0)
|
|
52
|
+
throw new Error('argv vacio');
|
|
53
|
+
fs_1.default.mkdirSync(logsRoot, { recursive: true, mode: 0o700 });
|
|
54
|
+
// (1) claim exclusivo DURABLE — wx + fsync de archivo y de directorio
|
|
55
|
+
let fd;
|
|
56
|
+
try {
|
|
57
|
+
fd = fs_1.default.openSync(claimPath(logsRoot, jobId, nonce), 'wx', 0o600);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error(`claim ya existe para ${jobId}/${nonce}: spawn previo no descartable (R1.8)`);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
fs_1.default.writeFileSync(fd, JSON.stringify({ jobId, nonce, claimedAt: new Date().toISOString(), wrapperPid: process.pid }) + '\n');
|
|
64
|
+
fs_1.default.fsyncSync(fd);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
fs_1.default.closeSync(fd);
|
|
68
|
+
}
|
|
69
|
+
(0, atomic_file_1.fsyncDirSync)(logsRoot);
|
|
70
|
+
const finish = (exitCode) => {
|
|
71
|
+
const result = { exitCode, endedAt: new Date().toISOString(), resultPath: resultPath(logsRoot, jobId, nonce) };
|
|
72
|
+
// (4) resultado terminal atomico junto al claim
|
|
73
|
+
(0, atomic_file_1.writeFileAtomicDurable)(result.resultPath, JSON.stringify(result, null, 2) + '\n', 0o600);
|
|
74
|
+
return result;
|
|
75
|
+
};
|
|
76
|
+
// (2) spawn del comando EN EL GRUPO DEL WRAPPER (detached:false): un solo
|
|
77
|
+
// process group por job, independiente del supervisor (R4.7: shell:false,
|
|
78
|
+
// argv como array, secretos solo por referencia de entorno).
|
|
79
|
+
const [exe, ...args] = argv;
|
|
80
|
+
const safeCwd = (0, fingerprint_1.resolveWorkingDirectory)(repoRoot, cwd).absolute;
|
|
81
|
+
const child = (0, child_process_1.spawn)(exe, args, {
|
|
82
|
+
cwd: safeCwd, shell: false, detached: true,
|
|
83
|
+
env: { ...process.env, [process_1.NONCE_ENV]: nonce },
|
|
84
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
85
|
+
});
|
|
86
|
+
const spawnFailed = new Promise((resolve) => {
|
|
87
|
+
child.on('error', () => resolve(127));
|
|
88
|
+
child.on('spawn', () => resolve(null));
|
|
89
|
+
});
|
|
90
|
+
const failed = await spawnFailed;
|
|
91
|
+
if (failed !== null || child.pid === undefined)
|
|
92
|
+
return finish(127);
|
|
93
|
+
// (3) identidad REAL persistida ANTES de esperar el resultado: ProcessRef
|
|
94
|
+
// del wrapper Y del comando (bloqueador 3: nunca mas pid/pgid cero).
|
|
95
|
+
const identity = {
|
|
96
|
+
jobId, nonce,
|
|
97
|
+
wrapper: (0, process_1.captureSelfRef)(nonce),
|
|
98
|
+
command: (0, process_1.captureRefFor)(child.pid, nonce, argv),
|
|
99
|
+
};
|
|
100
|
+
(0, atomic_file_1.writeFileAtomicDurable)(identityPath(logsRoot, jobId, nonce), JSON.stringify(identity, null, 2) + '\n', 0o600);
|
|
101
|
+
// Salida acotada en memoria y redactada como UN flujo antes de la primera
|
|
102
|
+
// escritura durable. Redactar cada chunk aisladamente filtra asignaciones
|
|
103
|
+
// partidas por el pipe (p.ej. `API_` + `KEY=secreto`).
|
|
104
|
+
let captured = 0;
|
|
105
|
+
const outputChunks = [];
|
|
106
|
+
const logFile = logPath(logsRoot, jobId, nonce);
|
|
107
|
+
const capture = (chunk) => {
|
|
108
|
+
if (captured >= MAX_LOG_BYTES)
|
|
109
|
+
return;
|
|
110
|
+
const accepted = chunk.subarray(0, MAX_LOG_BYTES - captured);
|
|
111
|
+
outputChunks.push(accepted);
|
|
112
|
+
captured += accepted.length;
|
|
113
|
+
};
|
|
114
|
+
child.stdout?.on('data', capture);
|
|
115
|
+
child.stderr?.on('data', capture);
|
|
116
|
+
const exitCode = await new Promise((resolve) => {
|
|
117
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
118
|
+
child.on('error', () => resolve(127));
|
|
119
|
+
});
|
|
120
|
+
// Ventana acotada para dejar llegar los ultimos chunks de stdio (exit puede
|
|
121
|
+
// dispararse antes que termine el flush de 'data') SIN bloquear indefinidamente
|
|
122
|
+
// si un descendiente hereda los fds y no los cierra — el wrapper SIEMPRE debe
|
|
123
|
+
// terminar (R1.8). 'close' sin cota reintroduce el riesgo de cuelgue.
|
|
124
|
+
await new Promise((resolve) => {
|
|
125
|
+
const timer = setTimeout(() => resolve(), STDIO_GRACE_MS);
|
|
126
|
+
child.once('close', () => { clearTimeout(timer); resolve(); });
|
|
127
|
+
});
|
|
128
|
+
if (outputChunks.length > 0) {
|
|
129
|
+
fs_1.default.writeFileSync(logFile, (0, redact_1.redactText)(Buffer.concat(outputChunks).toString('utf8')), { mode: 0o600 });
|
|
130
|
+
}
|
|
131
|
+
// El resultado terminal no se publica mientras queden descendientes en el
|
|
132
|
+
// grupo propio del comando. Como el wrapper vive en otro PGID, puede drenar
|
|
133
|
+
// el grupo completo sin auto-terminarse.
|
|
134
|
+
const drained = await (0, process_1.terminatePreviouslyOwnedGroup)(identity.command, { termGraceMs: 500, killGraceMs: 500 });
|
|
135
|
+
return finish(drained ? exitCode : 125);
|
|
136
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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.buildExport = buildExport;
|
|
7
|
+
// Export sanitizado y versionado (design R3.7, RNF-T.4/T.8/T.9): reproducible
|
|
8
|
+
// desde checkout limpio; lo que el provider/baseline no reporta se declara
|
|
9
|
+
// 'unobservable' — jamas un cero inventado.
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
12
|
+
const exec_wrapper_1 = require("./exec-wrapper");
|
|
13
|
+
function wallMs(from, to) {
|
|
14
|
+
if (from === undefined || to === undefined)
|
|
15
|
+
return 'unobservable';
|
|
16
|
+
const a = Date.parse(from);
|
|
17
|
+
const b = Date.parse(to);
|
|
18
|
+
if (Number.isNaN(a) || Number.isNaN(b) || b < a)
|
|
19
|
+
return 'unobservable';
|
|
20
|
+
return b - a;
|
|
21
|
+
}
|
|
22
|
+
function compare(current, baseline) {
|
|
23
|
+
const base = baseline ?? 'unobservable';
|
|
24
|
+
if (current === 'unobservable' || base === 'unobservable')
|
|
25
|
+
return { current, baseline: base, delta: 'unobservable' };
|
|
26
|
+
return { current, baseline: base, delta: current - base };
|
|
27
|
+
}
|
|
28
|
+
/** Dedup por fingerprint+commandDigest: el primero visto es el mecanico
|
|
29
|
+
* real, cualquier repetido queda marcado (RNF-T.8/T.9). */
|
|
30
|
+
function buildJobRows(jobs) {
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
return jobs.map((j) => {
|
|
33
|
+
const k = `${j.fingerprint}:${j.commandDigest}`;
|
|
34
|
+
const deduplicated = seen.has(k);
|
|
35
|
+
seen.add(k);
|
|
36
|
+
return { id: j.id, fingerprint: j.fingerprint, executionState: j.executionState, verdict: j.verdict, phaseTimestamps: j.phaseTimestamps, deduplicated };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Hash del resultado real en disco (respaldado por reconcile.ts al adoptar,
|
|
40
|
+
* ver spawnNonce en types.ts) + comando reproducible. Sin resultado
|
|
41
|
+
* observable => 'unobservable', jamas un hash inventado (RNF-T.9). */
|
|
42
|
+
function buildEvidence(jobs, logsRoot) {
|
|
43
|
+
return jobs.map((j) => {
|
|
44
|
+
let resultHash = 'unobservable';
|
|
45
|
+
if (logsRoot !== null && j.spawnNonce !== undefined) {
|
|
46
|
+
try {
|
|
47
|
+
resultHash = crypto_1.default.createHash('sha256').update(fs_1.default.readFileSync((0, exec_wrapper_1.resultPath)(logsRoot, j.id, j.spawnNonce), 'utf8')).digest('hex');
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
resultHash = 'unobservable';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// argv ya redactado por el emisor: reproducible sin secretos (R2.3)
|
|
54
|
+
return { jobId: j.id, resultHash, reproduce: `cd ${j.cwd} && ${j.argv.join(' ')}` };
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function buildExport(state, provider, opts) {
|
|
58
|
+
const jobs = Object.values(state.jobs);
|
|
59
|
+
const jobRows = buildJobRows(jobs);
|
|
60
|
+
const evidence = buildEvidence(jobs, opts.logsRoot);
|
|
61
|
+
const cycleWall = wallMs(state.cycle.startedAt, state.cycle.completedAt);
|
|
62
|
+
const mechanicalRuns = jobRows.filter((j) => !j.deduplicated).length;
|
|
63
|
+
return {
|
|
64
|
+
schema: 2, provider, branch: state.branch, generatedBy: 'awm job export',
|
|
65
|
+
cycle: {
|
|
66
|
+
status: state.cycle.status,
|
|
67
|
+
startedAt: state.cycle.startedAt,
|
|
68
|
+
completedAt: state.cycle.completedAt ?? 'unobservable',
|
|
69
|
+
wallTimeMs: cycleWall,
|
|
70
|
+
},
|
|
71
|
+
tasks: state.tasks.map((t) => ({
|
|
72
|
+
id: t.id, status: t.status, attempts: t.attempts,
|
|
73
|
+
createdAt: t.createdAt ?? 'unobservable',
|
|
74
|
+
completedAt: t.completedAt ?? 'unobservable',
|
|
75
|
+
wallTimeMs: wallMs(t.createdAt, t.completedAt),
|
|
76
|
+
})),
|
|
77
|
+
jobs: jobRows,
|
|
78
|
+
evidence,
|
|
79
|
+
metrics: {
|
|
80
|
+
dispatches: state.dispatches.length,
|
|
81
|
+
mechanicalRunsReal: jobs.length,
|
|
82
|
+
mechanicalRunsDeduplicated: jobs.length - mechanicalRuns,
|
|
83
|
+
tokensPerRole: 'unobservable', // ningun provider lo expone mecanicamente hoy (R0)
|
|
84
|
+
},
|
|
85
|
+
baselineComparison: {
|
|
86
|
+
baselineDate: '2026-07-29',
|
|
87
|
+
source: opts.baseline?.source ?? 'unobservable',
|
|
88
|
+
wallTimeMs: compare(cycleWall, opts.baseline?.wallTimeMs),
|
|
89
|
+
dispatches: compare(state.dispatches.length, opts.baseline?.dispatches),
|
|
90
|
+
mechanicalRuns: compare(mechanicalRuns, opts.baseline?.mechanicalRuns),
|
|
91
|
+
tokensPerRole: 'unobservable',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeGate = computeGate;
|
|
4
|
+
const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
|
|
5
|
+
function computeGate(state, corrupt, fingerprintNow) {
|
|
6
|
+
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
|
+
if (state.cycle.status === 'BLOCKED') {
|
|
11
|
+
reasons.push({ category: 'cycle-blocked', detail: `ciclo BLOCKED: ${state.cycle.blockedReason ?? 'sin razon registrada'}` });
|
|
12
|
+
}
|
|
13
|
+
for (const problem of state.requestProblems) {
|
|
14
|
+
reasons.push({ category: 'request-problem', detail: `request ${problem.kind} en ${problem.file}: ${problem.detail}` });
|
|
15
|
+
}
|
|
16
|
+
for (const j of Object.values(state.jobs)) {
|
|
17
|
+
if (LIVE.includes(j.executionState) || j.executionState === 'orphaned') {
|
|
18
|
+
reasons.push({ category: 'live-job', detail: `job ${j.id} en ${j.executionState}` });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
for (const t of state.tasks) {
|
|
22
|
+
if (t.status !== 'done') {
|
|
23
|
+
reasons.push({ category: 'pending-task', detail: `task ${t.id} en ${t.status}` });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
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
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Verificadores requeridos por la config REAL del repo (watch --init):
|
|
35
|
+
// 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 presentKinds = new Set(allPlans.map((i) => i.kind));
|
|
38
|
+
for (const mechanical of ['test', 'sensors']) {
|
|
39
|
+
if (!state.requiredVerifiers.includes(mechanical)) {
|
|
40
|
+
reasons.push({ category: 'missing-verifier', detail: `el repo no tiene '${mechanical}' configurado; no se certifica por ausencia (R3.6)` });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const required of state.requiredVerifiers) {
|
|
44
|
+
if (!presentKinds.has(required)) {
|
|
45
|
+
reasons.push({ category: 'missing-verifier', detail: `el repo exige verificador '${required}' y ningun plan lo contiene` });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const item of allPlans) {
|
|
49
|
+
if (item.satisfiedBy === undefined) {
|
|
50
|
+
reasons.push({ category: 'unsatisfied-plan', detail: `item ${item.id} (${item.kind}) sin satisfacer` });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (item.kind === 'review') {
|
|
54
|
+
const v = state.verdicts.find((x) => x.id === item.satisfiedBy);
|
|
55
|
+
if (v === undefined) {
|
|
56
|
+
reasons.push({ category: 'dangling-reference', detail: `item ${item.id} cita verdict inexistente ${item.satisfiedBy}` });
|
|
57
|
+
}
|
|
58
|
+
else if (v.result !== 'pass') {
|
|
59
|
+
reasons.push({ category: 'adverse-verdict', detail: `item ${item.id} citado por verdict ${v.id} con result ${v.result}` });
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const now = fingerprintNow(v.argv, v.paths, v.cwd);
|
|
63
|
+
if (now === null || now !== v.fingerprint) {
|
|
64
|
+
reasons.push({ category: 'stale-fingerprint', detail: `verdict ${v.id} es historico y no certifica` });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const j = state.jobs[item.satisfiedBy];
|
|
70
|
+
if (j === undefined) {
|
|
71
|
+
reasons.push({ category: 'dangling-reference', detail: `item ${item.id} cita job inexistente ${item.satisfiedBy}` });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (j.verdict !== 'pass') {
|
|
75
|
+
reasons.push({ category: 'adverse-verdict', detail: `item ${item.id} citado por ${item.satisfiedBy} con verdict ${j.verdict ?? 'ausente'}` });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Vigencia (RF-2.8): recomputar con argv/paths/cwd del job y comparar.
|
|
79
|
+
const now = fingerprintNow(j.argv, j.paths, j.cwd);
|
|
80
|
+
if (now === null || now !== j.fingerprint) {
|
|
81
|
+
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
|
+
}
|
|
84
|
+
for (const t of state.tasks) {
|
|
85
|
+
for (const requiredKind of ['spec', 'quality']) {
|
|
86
|
+
if (!t.reviewObligations.some((o) => o.kind === requiredKind)) {
|
|
87
|
+
reasons.push({ category: 'open-obligation', detail: `task ${t.id} carece de ReviewObligation ${requiredKind}` });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const o of t.reviewObligations) {
|
|
91
|
+
if (o.verdictId === undefined) {
|
|
92
|
+
reasons.push({ category: 'open-obligation', detail: `obligacion ${o.id} sin verdict` });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const v = state.verdicts.find((x) => x.id === o.verdictId);
|
|
96
|
+
if (v === undefined) {
|
|
97
|
+
reasons.push({ category: 'dangling-reference', detail: `obligacion ${o.id} cita verdict inexistente ${o.verdictId}` });
|
|
98
|
+
}
|
|
99
|
+
else if (v.result !== 'pass') {
|
|
100
|
+
reasons.push({ category: 'adverse-verdict', detail: `obligacion ${o.id} citada por verdict ${v.id} con result ${v.result}` });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const now = fingerprintNow(v.argv, v.paths, v.cwd);
|
|
104
|
+
if (now === null || now !== v.fingerprint) {
|
|
105
|
+
reasons.push({ category: 'stale-fingerprint', detail: `verdict ${v.id} de obligacion ${o.id} es historico y no certifica` });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const v of state.verdicts) {
|
|
111
|
+
if (v.result !== 'pass') {
|
|
112
|
+
const fix = state.fixes.find((f) => f.verdictId === v.id);
|
|
113
|
+
if (fix === undefined || !fix.closed)
|
|
114
|
+
reasons.push({ category: 'open-fix', detail: `verdict adverso ${v.id} sin fix cerrado` });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { pass: reasons.length === 0, reasons };
|
|
118
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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.emitHeartbeat = emitHeartbeat;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const requests_1 = require("../../core/journal/requests");
|
|
9
|
+
function emitHeartbeat(repoRoot, branch, generationToken) {
|
|
10
|
+
(0, requests_1.emitRequest)(repoRoot, branch, {
|
|
11
|
+
kind: 'controller-heartbeat', generationToken,
|
|
12
|
+
idempotencyKey: crypto_1.default.randomBytes(8).toString('hex'), // cada latido es unico
|
|
13
|
+
payload: { at: new Date().toISOString() },
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
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.registerJobCommand = registerJobCommand;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
const request_1 = require("./request");
|
|
11
|
+
const heartbeat_1 = require("./heartbeat");
|
|
12
|
+
const query_1 = require("./query");
|
|
13
|
+
const gate_1 = require("./gate");
|
|
14
|
+
const reconcile_1 = require("./reconcile");
|
|
15
|
+
const reap_1 = require("./reap");
|
|
16
|
+
const export_1 = require("./export");
|
|
17
|
+
const exec_wrapper_1 = require("./exec-wrapper");
|
|
18
|
+
const requests_1 = require("../../core/journal/requests");
|
|
19
|
+
const fingerprint_1 = require("../../core/journal/fingerprint");
|
|
20
|
+
const process_1 = require("../../core/journal/process");
|
|
21
|
+
const store_1 = require("../../core/journal/store");
|
|
22
|
+
const paths_1 = require("../../core/journal/paths");
|
|
23
|
+
const lock_1 = require("../watch/lock");
|
|
24
|
+
const atomic_file_1 = require("../../core/atomic-file");
|
|
25
|
+
const fs_1 = __importDefault(require("fs"));
|
|
26
|
+
function branchOf(cwd) {
|
|
27
|
+
// stdio explicito (ver EXEC_STDIO en journal/process.ts): evita el relay
|
|
28
|
+
// default de execFileSync del stderr de git hacia el stderr del llamante,
|
|
29
|
+
// que EPIPE-crashea si ese fd es un pipe roto.
|
|
30
|
+
const b = (0, child_process_1.execFileSync)('git', ['branch', '--show-current'], { cwd, encoding: 'utf8', stdio: process_1.EXEC_STDIO }).trim();
|
|
31
|
+
if (b.length === 0)
|
|
32
|
+
throw new Error('no hay rama actual (HEAD detached): el journal es por rama');
|
|
33
|
+
return b;
|
|
34
|
+
}
|
|
35
|
+
function realFingerprintNow(repo) {
|
|
36
|
+
return (argv, paths, cwd) => {
|
|
37
|
+
try {
|
|
38
|
+
return (0, fingerprint_1.computeFingerprint)(repo, argv, paths, cwd).fingerprint;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// CONSTITUTION: commander valida los tokens de las options declaradas; los
|
|
46
|
+
// variadicos van tras `--`. Los flags numericos/JSON se validan fail-fast.
|
|
47
|
+
function registerJobCommand(program) {
|
|
48
|
+
const job = program.command('job').description('journal durable de trabajo del ciclo SDD (R1)');
|
|
49
|
+
job.command('request')
|
|
50
|
+
.description('registra la intencion de una verificacion — el supervisor la ejecuta')
|
|
51
|
+
.requiredOption('--generation <token>', 'token de la generacion vigente')
|
|
52
|
+
.option('--paths <globs...>', 'paths que el comando observa (default: arbol completo)')
|
|
53
|
+
.option('--cwd <dir>', 'cwd relativo del comando dentro del repo', '.')
|
|
54
|
+
.option('--satisfies <itemId>', 'id del item de VerificationPlan que este job satisface')
|
|
55
|
+
.argument('<cmd...>', 'comando tras --')
|
|
56
|
+
.action((cmd, opts) => {
|
|
57
|
+
const repo = process.cwd();
|
|
58
|
+
const r = (0, request_1.requestJob)(repo, branchOf(repo), opts.generation, cmd, opts.paths ?? [], opts.cwd, { satisfies: opts.satisfies });
|
|
59
|
+
process.stdout.write(JSON.stringify({ requestId: r.requestId, idempotencyKey: r.idempotencyKey }, null, 2) + '\n');
|
|
60
|
+
});
|
|
61
|
+
job.command('register')
|
|
62
|
+
.description('registra una entidad del ciclo (task | cycle-plan | dispatch | task-status | next-action | custody-decision) ANTES de actuar')
|
|
63
|
+
.requiredOption('--generation <token>')
|
|
64
|
+
.requiredOption('--entity <kind>', 'task | cycle-plan | dispatch | task-status | next-action | custody-decision')
|
|
65
|
+
.requiredOption('--json <payload>', 'payload JSON de la entidad')
|
|
66
|
+
.action((opts) => {
|
|
67
|
+
let payload;
|
|
68
|
+
try {
|
|
69
|
+
payload = JSON.parse(opts.json);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Error('--json requiere un objeto JSON valido');
|
|
73
|
+
}
|
|
74
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload))
|
|
75
|
+
throw new Error('--json requiere un objeto JSON');
|
|
76
|
+
const repo = process.cwd();
|
|
77
|
+
const r = (0, requests_1.emitRequest)(repo, branchOf(repo), {
|
|
78
|
+
kind: 'register-entity', generationToken: opts.generation,
|
|
79
|
+
idempotencyKey: crypto_1.default.createHash('sha256').update(`${opts.entity}:${opts.json}`).digest('hex'),
|
|
80
|
+
payload: { entity: opts.entity, ...payload },
|
|
81
|
+
});
|
|
82
|
+
process.stdout.write(JSON.stringify({ requestId: r.requestId }, null, 2) + '\n');
|
|
83
|
+
});
|
|
84
|
+
job.command('verdict')
|
|
85
|
+
.description('registra el veredicto de una ReviewObligation AL RECIBIRSE')
|
|
86
|
+
.requiredOption('--generation <token>')
|
|
87
|
+
.requiredOption('--obligation <id>')
|
|
88
|
+
.requiredOption('--result <r>', 'pass | fail | inconclusive')
|
|
89
|
+
.option('--detail <texto>', 'detalle del veredicto', '')
|
|
90
|
+
.action((opts) => {
|
|
91
|
+
if (!['pass', 'fail', 'inconclusive'].includes(opts.result))
|
|
92
|
+
throw new Error('--result debe ser pass | fail | inconclusive');
|
|
93
|
+
const repo = process.cwd();
|
|
94
|
+
const reviewArgv = ['awm-review', opts.obligation];
|
|
95
|
+
const reviewFingerprint = (0, fingerprint_1.computeFingerprint)(repo, reviewArgv, [], '.');
|
|
96
|
+
// Determinista a partir de los MISMOS inputs que idempotencyKey, INCLUYENDO
|
|
97
|
+
// generation en ambos (alineado — bug post-624a4c0: idempotencyKey se habia
|
|
98
|
+
// quedado sin generation mientras verdictId si la incluia, lo que hacia que
|
|
99
|
+
// un veredicto genuinamente distinto en otra generacion colisionara en
|
|
100
|
+
// idempotencyKey pero difiriera en payloadDigest, cayendo a
|
|
101
|
+
// rejected-digest-mismatch en vez de aplicarse como veredicto nuevo):
|
|
102
|
+
// un retry genuino del mismo comando (misma generation) produce un payload
|
|
103
|
+
// byte-identico, no un rejected-digest-mismatch espurio (Fix 3); una
|
|
104
|
+
// generation distinta produce una idempotencyKey ENTERAMENTE distinta, no
|
|
105
|
+
// una colision con digest distinto.
|
|
106
|
+
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), {
|
|
108
|
+
kind: 'verdict', generationToken: opts.generation,
|
|
109
|
+
idempotencyKey: crypto_1.default.createHash('sha256').update(`verdict:${opts.generation}:${opts.obligation}:${opts.result}:${opts.detail}:${reviewFingerprint.fingerprint}`).digest('hex'),
|
|
110
|
+
payload: {
|
|
111
|
+
verdictId, obligationId: opts.obligation, result: opts.result, detail: opts.detail,
|
|
112
|
+
fingerprint: reviewFingerprint.fingerprint, argv: reviewArgv, paths: [], cwd: '.',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
process.stdout.write(JSON.stringify({ verdictId }, null, 2) + '\n');
|
|
116
|
+
});
|
|
117
|
+
job.command('controller-heartbeat')
|
|
118
|
+
.requiredOption('--generation <token>')
|
|
119
|
+
.action((opts) => { (0, heartbeat_1.emitHeartbeat)(process.cwd(), branchOf(process.cwd()), opts.generation); });
|
|
120
|
+
job.command('ps').action(() => {
|
|
121
|
+
process.stdout.write(JSON.stringify((0, query_1.queryPs)(process.cwd(), branchOf(process.cwd())), null, 2) + '\n');
|
|
122
|
+
});
|
|
123
|
+
job.command('list').action(() => {
|
|
124
|
+
process.stdout.write(JSON.stringify((0, query_1.queryList)(process.cwd(), branchOf(process.cwd())), null, 2) + '\n');
|
|
125
|
+
});
|
|
126
|
+
job.command('show')
|
|
127
|
+
.argument('<jobId>')
|
|
128
|
+
.action((jobId) => {
|
|
129
|
+
const out = (0, query_1.queryShow)(process.cwd(), branchOf(process.cwd()), jobId);
|
|
130
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
131
|
+
if (out.corruptState || out.job === null)
|
|
132
|
+
process.exit(1);
|
|
133
|
+
});
|
|
134
|
+
job.command('reconcile')
|
|
135
|
+
.description('informe read-only de la matriz unica R1.8 + next_action (la mutacion es del supervisor)')
|
|
136
|
+
.action(() => {
|
|
137
|
+
const repo = process.cwd();
|
|
138
|
+
const branch = branchOf(repo);
|
|
139
|
+
const r = (0, store_1.readJournal)(repo, branch);
|
|
140
|
+
if (r.corrupt || r.state === null) {
|
|
141
|
+
process.stdout.write(JSON.stringify({ corruptState: true }, null, 2) + '\n');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
(0, lock_1.verifyBranchInvariant)(repo, r.state.branch);
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
process.stderr.write(`${e.message}\n`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
// copia en memoria: reconcileJobs muta SU copia, jamas el disco (R3.1)
|
|
152
|
+
const clone = JSON.parse(JSON.stringify(r.state));
|
|
153
|
+
const out = (0, reconcile_1.reconcileJobs)(clone, (0, paths_1.logsDir)(repo, branch));
|
|
154
|
+
const active = r.state.generations.find((g) => g.state === 'active' || g.state === 'controller-suspected-stall');
|
|
155
|
+
process.stdout.write(JSON.stringify({
|
|
156
|
+
decisions: out.decisions,
|
|
157
|
+
nextAction: r.state.cycle.nextAction ?? null,
|
|
158
|
+
cycleStatus: r.state.cycle.status,
|
|
159
|
+
generation: active === undefined ? null : { n: active.n, token: active.token },
|
|
160
|
+
}, null, 2) + '\n');
|
|
161
|
+
});
|
|
162
|
+
job.command('gate')
|
|
163
|
+
.description('interlock fail-closed: exit != 0 si CUALQUIER cosa impide certificar')
|
|
164
|
+
.action(() => {
|
|
165
|
+
const repo = process.cwd();
|
|
166
|
+
const branch = branchOf(repo);
|
|
167
|
+
const r = (0, store_1.readJournal)(repo, branch);
|
|
168
|
+
if (r.state !== null) {
|
|
169
|
+
try {
|
|
170
|
+
(0, lock_1.verifyBranchInvariant)(repo, r.state.branch);
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
process.stderr.write(`${e.message}\n`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const g = (0, gate_1.computeGate)(r.state, r.corrupt, realFingerprintNow(repo));
|
|
178
|
+
process.stdout.write(JSON.stringify(g, null, 2) + '\n');
|
|
179
|
+
if (!g.pass)
|
|
180
|
+
process.exit(1); // falla cerrado (R3.2)
|
|
181
|
+
});
|
|
182
|
+
job.command('reap')
|
|
183
|
+
.description('lista procesos de jobs (identidad completa); --execute --jobs <ids...> para terminar confirmando')
|
|
184
|
+
.option('--execute', 'ejecutar la terminacion de los jobs listados en --jobs')
|
|
185
|
+
.option('--jobs <ids...>', 'ids de jobs a terminar (obligatorio con --execute)')
|
|
186
|
+
.action(async (opts) => {
|
|
187
|
+
const repo = process.cwd();
|
|
188
|
+
const r = (0, store_1.readJournal)(repo, branchOf(repo));
|
|
189
|
+
if (r.corrupt || r.state === null) {
|
|
190
|
+
process.stderr.write('journal corrupto o ausente\n');
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
const plan = (0, reap_1.planReap)(r.state);
|
|
194
|
+
process.stdout.write(JSON.stringify(plan, null, 2) + '\n'); // R2.2: listar SIEMPRE primero
|
|
195
|
+
if (opts.execute) {
|
|
196
|
+
if (!Array.isArray(opts.jobs) || opts.jobs.length === 0)
|
|
197
|
+
throw new Error('--execute requiere --jobs <ids...>');
|
|
198
|
+
const killed = await (0, reap_1.executeReap)(r.state, opts.jobs);
|
|
199
|
+
process.stdout.write(JSON.stringify({ killed }, null, 2) + '\n');
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
job.command('export')
|
|
203
|
+
.option('--provider <p>', 'provider del ciclo', 'codex')
|
|
204
|
+
.option('--baseline <file>', 'JSON con metricas del baseline 2026-07-29 (source/wallTimeMs/dispatches/mechanicalRuns)')
|
|
205
|
+
.action((opts) => {
|
|
206
|
+
const repo = process.cwd();
|
|
207
|
+
const branch = branchOf(repo);
|
|
208
|
+
const r = (0, store_1.readJournal)(repo, branch);
|
|
209
|
+
if (r.corrupt || r.state === null) {
|
|
210
|
+
process.stderr.write('journal corrupto\n');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
let baseline = null;
|
|
214
|
+
if (opts.baseline !== undefined) {
|
|
215
|
+
let parsed;
|
|
216
|
+
try {
|
|
217
|
+
parsed = JSON.parse(fs_1.default.readFileSync(opts.baseline, 'utf8'));
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
throw new Error(`--baseline: no se pudo leer o parsear ${opts.baseline} como JSON (${e.message})`);
|
|
221
|
+
}
|
|
222
|
+
if (typeof parsed !== 'object' || parsed === null || typeof parsed.source !== 'string') {
|
|
223
|
+
throw new Error('--baseline requiere un JSON con al menos {source: string}');
|
|
224
|
+
}
|
|
225
|
+
baseline = parsed;
|
|
226
|
+
}
|
|
227
|
+
const e = (0, export_1.buildExport)(r.state, opts.provider, { logsRoot: (0, paths_1.logsDir)(repo, branch), baseline });
|
|
228
|
+
const out = path_1.default.join((0, paths_1.exportDir)(repo, branch), 'cycle-export.json');
|
|
229
|
+
(0, atomic_file_1.writeFileAtomicDurable)(out, JSON.stringify(e, null, 2) + '\n', 0o600);
|
|
230
|
+
process.stdout.write(out + '\n');
|
|
231
|
+
});
|
|
232
|
+
// Entrypoint INTERNO del wrapper externo (Task 9). Oculto del help: lo
|
|
233
|
+
// invoca el supervisor, no un humano — pero DEBE ser un comando real para
|
|
234
|
+
// que el wrapper sea un proceso independiente (bloqueador 3).
|
|
235
|
+
job.command('exec-wrapper', { hidden: true })
|
|
236
|
+
.requiredOption('--job <id>')
|
|
237
|
+
.requiredOption('--nonce <n>')
|
|
238
|
+
.requiredOption('--logs <dir>')
|
|
239
|
+
.option('--cwd <dir>', 'cwd del comando', '.')
|
|
240
|
+
.argument('<cmd...>')
|
|
241
|
+
.action(async (cmd, opts) => {
|
|
242
|
+
// El exit code del WRAPPER es 0 si registro el resultado (su exito
|
|
243
|
+
// propio); el exit code del COMANDO viaja en el result sidecar.
|
|
244
|
+
await (0, exec_wrapper_1.runExecWrapper)({ logsRoot: opts.logs, jobId: opts.job, nonce: opts.nonce, argv: cmd, cwd: opts.cwd });
|
|
245
|
+
});
|
|
246
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.queryPs = queryPs;
|
|
4
|
+
exports.queryList = queryList;
|
|
5
|
+
exports.queryShow = queryShow;
|
|
6
|
+
const store_1 = require("../../core/journal/store");
|
|
7
|
+
const process_1 = require("../../core/journal/process");
|
|
8
|
+
/** Fuente unica de "que hay corriendo": cruza identidad completa contra
|
|
9
|
+
* procesos vivos. corrupt es VISIBLE, nunca descartado (R1.6). */
|
|
10
|
+
function queryPs(repoRoot, branch) {
|
|
11
|
+
const r = (0, store_1.readJournal)(repoRoot, branch);
|
|
12
|
+
if (r.corrupt)
|
|
13
|
+
return { corruptState: true, jobs: [] };
|
|
14
|
+
const jobs = Object.values(r.state.jobs).map((j) => ({
|
|
15
|
+
id: j.id, executionState: j.executionState, observationState: j.observationState, verdict: j.verdict,
|
|
16
|
+
alive: j.processRef ? (0, process_1.refIsAlive)(j.processRef) : 'sin-pid',
|
|
17
|
+
}));
|
|
18
|
+
return { corruptState: false, jobs };
|
|
19
|
+
}
|
|
20
|
+
function queryList(repoRoot, branch) {
|
|
21
|
+
const r = (0, store_1.readJournal)(repoRoot, branch);
|
|
22
|
+
if (r.corrupt)
|
|
23
|
+
return { corruptState: true, cycleStatus: null, jobs: [] };
|
|
24
|
+
return {
|
|
25
|
+
corruptState: false,
|
|
26
|
+
cycleStatus: r.state.cycle.status,
|
|
27
|
+
jobs: Object.values(r.state.jobs).map((j) => ({
|
|
28
|
+
id: j.id, executionState: j.executionState, verdict: j.verdict, argv: j.argv, satisfies: j.satisfies,
|
|
29
|
+
})),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function queryShow(repoRoot, branch, jobId) {
|
|
33
|
+
const r = (0, store_1.readJournal)(repoRoot, branch);
|
|
34
|
+
if (r.corrupt)
|
|
35
|
+
return { corruptState: true, job: null };
|
|
36
|
+
return { corruptState: false, job: r.state.jobs[jobId] ?? null };
|
|
37
|
+
}
|