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.
Files changed (61) hide show
  1. package/dist/src/commands/job/exec-wrapper.js +136 -0
  2. package/dist/src/commands/job/export.js +94 -0
  3. package/dist/src/commands/job/gate.js +118 -0
  4. package/dist/src/commands/job/heartbeat.js +15 -0
  5. package/dist/src/commands/job/index.js +246 -0
  6. package/dist/src/commands/job/query.js +37 -0
  7. package/dist/src/commands/job/reap.js +24 -0
  8. package/dist/src/commands/job/reconcile.js +112 -0
  9. package/dist/src/commands/job/request.js +27 -0
  10. package/dist/src/commands/sensors/exec.js +121 -0
  11. package/dist/src/commands/sensors/index.js +4 -4
  12. package/dist/src/commands/sensors/run.js +124 -71
  13. package/dist/src/commands/watch/apply.js +352 -0
  14. package/dist/src/commands/watch/generations.js +249 -0
  15. package/dist/src/commands/watch/index.js +49 -0
  16. package/dist/src/commands/watch/init.js +72 -0
  17. package/dist/src/commands/watch/lock.js +89 -0
  18. package/dist/src/commands/watch/runner.js +191 -0
  19. package/dist/src/commands/watch/supervisor.js +266 -0
  20. package/dist/src/core/atomic-file.js +31 -0
  21. package/dist/src/core/export/pack.js +7 -1
  22. package/dist/src/core/journal/adapter.js +27 -0
  23. package/dist/src/core/journal/fingerprint.js +80 -0
  24. package/dist/src/core/journal/paths.js +56 -0
  25. package/dist/src/core/journal/process.js +284 -0
  26. package/dist/src/core/journal/redact.js +142 -0
  27. package/dist/src/core/journal/requests.js +132 -0
  28. package/dist/src/core/journal/store.js +107 -0
  29. package/dist/src/core/journal/types.js +165 -0
  30. package/dist/src/index.js +4 -0
  31. package/dist/tests/commands/job/exec-wrapper.test.js +85 -0
  32. package/dist/tests/commands/job/export.test.js +76 -0
  33. package/dist/tests/commands/job/gate-reconcile.test.js +297 -0
  34. package/dist/tests/commands/job/reap-cli.test.js +101 -0
  35. package/dist/tests/commands/job/verbs.test.js +56 -0
  36. package/dist/tests/commands/job/verdict-determinism.test.js +138 -0
  37. package/dist/tests/commands/sensors/exec-fixtures.js +24 -0
  38. package/dist/tests/commands/sensors/exec.test.js +91 -0
  39. package/dist/tests/commands/sensors/run-inconclusive.test.js +55 -66
  40. package/dist/tests/commands/sensors/run-partial.test.js +225 -0
  41. package/dist/tests/commands/sensors/run-tool-missing.test.js +6 -6
  42. package/dist/tests/commands/sensors/run.test.js +64 -81
  43. package/dist/tests/commands/watch/apply.test.js +397 -0
  44. package/dist/tests/commands/watch/e2e-crash.test.js +157 -0
  45. package/dist/tests/commands/watch/generations.test.js +115 -0
  46. package/dist/tests/commands/watch/integration.test.js +124 -0
  47. package/dist/tests/commands/watch/lock.test.js +60 -0
  48. package/dist/tests/commands/watch/runner.test.js +239 -0
  49. package/dist/tests/commands/watch/supervisor-loop.test.js +203 -0
  50. package/dist/tests/commands/watch/watch-init.test.js +43 -0
  51. package/dist/tests/core/atomic-file-durable.test.js +42 -0
  52. package/dist/tests/core/journal/adapter.test.js +27 -0
  53. package/dist/tests/core/journal/fingerprint.test.js +164 -0
  54. package/dist/tests/core/journal/paths.test.js +35 -0
  55. package/dist/tests/core/journal/process.test.js +213 -0
  56. package/dist/tests/core/journal/redact.test.js +59 -0
  57. package/dist/tests/core/journal/requests.test.js +134 -0
  58. package/dist/tests/core/journal/store.test.js +88 -0
  59. package/dist/tests/core/journal/types.test.js +78 -0
  60. package/dist/tests/structural/exec-invocation-explicit-stdio.test.js +94 -0
  61. package/package.json +1 -1
@@ -0,0 +1,132 @@
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.digestOf = digestOf;
7
+ exports.emitRequest = emitRequest;
8
+ exports.listPendingRequests = listPendingRequests;
9
+ exports.applyOutcome = applyOutcome;
10
+ exports.ackFor = ackFor;
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const crypto_1 = __importDefault(require("crypto"));
14
+ const paths_1 = require("./paths");
15
+ const redact_1 = require("./redact");
16
+ const atomic_file_1 = require("../atomic-file");
17
+ function digestOf(payload) {
18
+ return crypto_1.default.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
19
+ }
20
+ function countPendingRequests(dir) {
21
+ try {
22
+ return fs_1.default.readdirSync(dir).filter((f) => f.endsWith('.json')).length;
23
+ }
24
+ catch {
25
+ return 0;
26
+ }
27
+ }
28
+ /** Publicacion atomica Y durable (R1.3, bloqueador 4): tmp + fsync + close +
29
+ * rename + fsync del DIRECTORIO. Redaccion EN EL EMISOR; secreto literal en
30
+ * flag sensible => rechazo sin persistir (R2.3). El segmento `seq` es un
31
+ * conteo del directorio de requests AL MOMENTO DE EMITIR (no un contador
32
+ * en memoria del proceso): las emisiones causalmente dependientes son, por
33
+ * definicion, secuenciales desde la perspectiva del emisor (el agente
34
+ * orquestador espera a que el proceso A termine — incluyendo su
35
+ * fsyncDirSync — antes de lanzar el proceso B), asi que el archivo de A ya
36
+ * esta durablemente presente cuando B escanea el directorio. Esto preserva
37
+ * el orden causal AUN ENTRE PROCESOS SEPARADOS, a diferencia de un
38
+ * contador en memoria por proceso (que reinicia en 0 en cada invocacion
39
+ * nueva de la CLI). Emisores genuinamente independientes/concurrentes
40
+ * pueden empatar en el conteo y caer al sufijo hex aleatorio — aceptable,
41
+ * porque requests independientes no requieren orden relativo. */
42
+ function emitRequest(repoRoot, branch, env) {
43
+ const payload = { ...env.payload };
44
+ if (Array.isArray(payload.argv)) {
45
+ const secretFlag = (0, redact_1.findLiteralSecretFlag)(payload.argv);
46
+ if (secretFlag !== null)
47
+ throw new Error(`secreto literal en ${secretFlag}: pasalo por referencia (-env), no por valor`);
48
+ payload.argv = (0, redact_1.redactArgv)(payload.argv);
49
+ }
50
+ const dir = (0, paths_1.requestsDir)(repoRoot, branch);
51
+ const seq = countPendingRequests(dir).toString().padStart(10, '0');
52
+ const requestId = `req-${Date.now()}-${seq}-${crypto_1.default.randomBytes(4).toString('hex')}`;
53
+ const body = JSON.stringify({ requestId, ...env, payload }, null, 2) + '\n';
54
+ const tmp = path_1.default.join(dir, `.${requestId}.tmp`);
55
+ const final = path_1.default.join(dir, `${requestId}.json`);
56
+ const fd = fs_1.default.openSync(tmp, 'wx', 0o600);
57
+ try {
58
+ fs_1.default.writeFileSync(fd, body);
59
+ fs_1.default.fsyncSync(fd);
60
+ }
61
+ finally {
62
+ fs_1.default.closeSync(fd);
63
+ }
64
+ fs_1.default.renameSync(tmp, final);
65
+ (0, atomic_file_1.fsyncDirSync)(dir); // la ENTRADA renombrada tambien debe sobrevivir un crash (R1.3)
66
+ return { requestId, idempotencyKey: env.idempotencyKey, payloadDigest: digestOf(payload), file: final };
67
+ }
68
+ const KNOWN_KINDS = ['job-request', 'register-entity', 'controller-heartbeat', 'verdict'];
69
+ function isRecord(x) {
70
+ return typeof x === 'object' && x !== null && !Array.isArray(x);
71
+ }
72
+ function isWellFormedEnvelope(x) {
73
+ if (!isRecord(x))
74
+ return false;
75
+ return typeof x.requestId === 'string' && x.requestId.length > 0
76
+ && KNOWN_KINDS.includes(x.kind)
77
+ && typeof x.generationToken === 'string' && x.generationToken.length > 0
78
+ && typeof x.idempotencyKey === 'string' && x.idempotencyKey.length > 0
79
+ && isRecord(x.payload);
80
+ }
81
+ function listPendingRequests(repoRoot, branch) {
82
+ const dir = (0, paths_1.requestsDir)(repoRoot, branch);
83
+ return fs_1.default.readdirSync(dir).filter((f) => f.endsWith('.json')).sort().map((f) => {
84
+ const file = path_1.default.join(dir, f);
85
+ try {
86
+ const parsed = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
87
+ // Shape completa antes de cualquier uso downstream. JSON valido no
88
+ // implica envelope valido: payload/generation/idempotency ausentes
89
+ // tambien son corrupcion visible, no una excepcion del supervisor.
90
+ if (!isWellFormedEnvelope(parsed)) {
91
+ return { requestId: f, envelope: null, file, corrupt: true };
92
+ }
93
+ if (f !== `${parsed.requestId}.json`) {
94
+ return { requestId: f, envelope: null, file, corrupt: true };
95
+ }
96
+ return { requestId: parsed.requestId, envelope: parsed, file, corrupt: false };
97
+ }
98
+ catch {
99
+ return { requestId: f, envelope: null, file, corrupt: true };
100
+ }
101
+ });
102
+ }
103
+ /** Registro del resultado en el state (el ack es derivable — R1.3).
104
+ * - mismo requestId ya registrado => no-op (replay seguro);
105
+ * - misma idempotencyKey con digest DISTINTO => error explicito;
106
+ * - misma idempotencyKey con mismo digest => ALIAS: el requestId nuevo
107
+ * registra SU PROPIA entrada con el mismo outcome/resultRef, para poder
108
+ * regenerar su ack (bloqueador 4 de la review).
109
+ * Devuelve el state mutado (el caller es el supervisor, que luego hace
110
+ * writeJournal — orden estado -> journal -> borrado de archivos). */
111
+ function applyOutcome(state, applied) {
112
+ if (state.appliedRequests[applied.requestId] !== undefined)
113
+ return state;
114
+ const prior = Object.values(state.appliedRequests).find((a) => a.idempotencyKey === applied.idempotencyKey);
115
+ if (prior !== undefined && prior.payloadDigest !== applied.payloadDigest) {
116
+ if (applied.outcome === 'applied') {
117
+ throw new Error(`idempotencyKey ${applied.idempotencyKey} reutilizada con payload digest distinto`);
118
+ }
119
+ // outcome de rechazo: ninguna mutacion ocurrio, seguro registrar aun con digest distinto
120
+ state.appliedRequests[applied.requestId] = applied;
121
+ return state;
122
+ }
123
+ if (prior !== undefined) {
124
+ state.appliedRequests[applied.requestId] = { ...applied, outcome: prior.outcome, resultRef: prior.resultRef };
125
+ return state;
126
+ }
127
+ state.appliedRequests[applied.requestId] = applied;
128
+ return state;
129
+ }
130
+ function ackFor(state, requestId) {
131
+ return state.appliedRequests[requestId] ?? null;
132
+ }
@@ -0,0 +1,107 @@
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.initJournal = initJournal;
7
+ exports.readJournal = readJournal;
8
+ exports.writeJournal = writeJournal;
9
+ exports.appendEvent = appendEvent;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const atomic_file_1 = require("../atomic-file");
12
+ const types_1 = require("./types");
13
+ const paths_1 = require("./paths");
14
+ /** Schema 1 evoluciono de forma aditiva durante R1. Normalizamos solamente
15
+ * campos que antes no existian; evidencia legacy queda deliberadamente con
16
+ * fingerprint vacio para que el gate la considere stale, nunca certificada. */
17
+ function normalizeSchemaOne(value) {
18
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
19
+ return value;
20
+ const parsed = structuredClone(value);
21
+ if (parsed.schema !== 1)
22
+ return parsed;
23
+ if (parsed.requestProblems === undefined)
24
+ parsed.requestProblems = [];
25
+ if (parsed.custodyDecisions === undefined)
26
+ parsed.custodyDecisions = [];
27
+ if (typeof parsed.cycle === 'object' && parsed.cycle !== null && !Array.isArray(parsed.cycle)) {
28
+ const cycle = parsed.cycle;
29
+ if (cycle.status === 'IN_PROGRESS' && cycle.nextAction === undefined) {
30
+ cycle.nextAction = { actionId: 'bootstrap-cycle', type: 'plan-cycle', target: 'cycle', preconditions: [], attempt: 0, state: 'pending' };
31
+ }
32
+ }
33
+ if (Array.isArray(parsed.verdicts)) {
34
+ for (const item of parsed.verdicts) {
35
+ if (typeof item !== 'object' || item === null || Array.isArray(item))
36
+ continue;
37
+ const verdict = item;
38
+ if (verdict.fingerprint === undefined)
39
+ verdict.fingerprint = '';
40
+ if (verdict.argv === undefined)
41
+ verdict.argv = [];
42
+ if (verdict.paths === undefined)
43
+ verdict.paths = [];
44
+ if (verdict.cwd === undefined)
45
+ verdict.cwd = '.';
46
+ }
47
+ }
48
+ return parsed;
49
+ }
50
+ function initJournal(repoRoot, branch) {
51
+ for (const d of [(0, paths_1.journalDir)(repoRoot, branch), (0, paths_1.requestsDir)(repoRoot, branch), (0, paths_1.acksDir)(repoRoot, branch), (0, paths_1.logsDir)(repoRoot, branch), (0, paths_1.exportDir)(repoRoot, branch)]) {
52
+ fs_1.default.mkdirSync(d, { recursive: true, mode: 0o700 });
53
+ fs_1.default.chmodSync(d, 0o700); // mkdirSync mode es umask-dependiente: fijar explicito (R1.2)
54
+ }
55
+ const sp = (0, paths_1.statePath)(repoRoot, branch);
56
+ if (!fs_1.default.existsSync(sp)) {
57
+ (0, atomic_file_1.writeFileAtomicDurable)(sp, JSON.stringify((0, types_1.emptyState)(branch), null, 2) + '\n', 0o600);
58
+ }
59
+ }
60
+ /** Lectura corrupt-aware (R1.6): sintaxis invalida O shape invalido => corrupt:true.
61
+ * Los CONSUMIDORES deciden: consultas muestran 'corrupt'; gate/reconcile bloquean. */
62
+ function readJournal(repoRoot, branch) {
63
+ const sp = (0, paths_1.statePath)(repoRoot, branch);
64
+ let raw;
65
+ try {
66
+ raw = fs_1.default.readFileSync(sp, 'utf8');
67
+ }
68
+ catch {
69
+ return { state: null, corrupt: true };
70
+ }
71
+ let parsed;
72
+ try {
73
+ parsed = normalizeSchemaOne(JSON.parse(raw));
74
+ }
75
+ catch {
76
+ return { state: null, corrupt: true, raw };
77
+ }
78
+ if (!(0, types_1.isWellFormedState)(parsed))
79
+ return { state: null, corrupt: true, raw };
80
+ return { state: parsed, corrupt: false, raw };
81
+ }
82
+ /** Escritura canonica: SOLO el supervisor la invoca (single-writer). CAS por
83
+ * revision monotonica: el snapshot que traes debe ser el vigente. */
84
+ function writeJournal(repoRoot, branch, state) {
85
+ if (state.branch !== branch)
86
+ throw new Error(`writeJournal: branch del estado (${state.branch}) no coincide con branch destino (${branch})`);
87
+ const current = readJournal(repoRoot, branch);
88
+ if (current.corrupt)
89
+ throw new Error('journal corrupto: no se escribe sobre corrupcion (R1.6)');
90
+ if (current.state !== null && current.state.revision !== state.revision) {
91
+ throw new Error(`revision desactualizada: disco=${current.state.revision} propuesta=${state.revision}`);
92
+ }
93
+ const next = { ...state, revision: state.revision + 1 };
94
+ if (!(0, types_1.isWellFormedState)(next))
95
+ throw new Error('writeJournal: estado propuesto con forma invalida, no se persiste (R1.6)');
96
+ (0, atomic_file_1.writeFileAtomicDurable)((0, paths_1.statePath)(repoRoot, branch), JSON.stringify(next, null, 2) + '\n', 0o600);
97
+ }
98
+ /** Auditoria derivada best-effort (R4.6): la escribe SOLO el supervisor, un
99
+ * fallo aqui jamas invalida el estado — state.json es la unica autoridad. */
100
+ function appendEvent(repoRoot, branch, event) {
101
+ try {
102
+ fs_1.default.appendFileSync((0, paths_1.eventsPath)(repoRoot, branch), JSON.stringify({ at: new Date().toISOString(), ...event }) + '\n', { mode: 0o600 });
103
+ }
104
+ catch {
105
+ // best-effort: un evento perdido no se reconstruye ni bloquea (R4.6)
106
+ }
107
+ }
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ // Única fuente de tipos del journal. CONSTITUTION: estados separados, nunca
3
+ // sobrecargados; shape validation antes de usar campos deserializados.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.GENERATION_STATES = exports.EXECUTION_STATES = void 0;
6
+ exports.emptyState = emptyState;
7
+ exports.isWellFormedState = isWellFormedState;
8
+ exports.isWellFormedProcessRef = isWellFormedProcessRef;
9
+ exports.isWellFormedJob = isWellFormedJob;
10
+ exports.EXECUTION_STATES = [
11
+ 'received', 'spawn-intent', 'claimed', 'running',
12
+ 'exited', 'cancel-requested', 'cancelled', 'orphaned',
13
+ ];
14
+ exports.GENERATION_STATES = [
15
+ 'active', 'controller-suspected-stall', 'terminated', 'superseded',
16
+ ];
17
+ function emptyState(branch) {
18
+ return {
19
+ schema: 1, revision: 0, branch,
20
+ cycle: {
21
+ status: 'IN_PROGRESS', startedAt: new Date().toISOString(),
22
+ nextAction: { actionId: 'bootstrap-cycle', type: 'plan-cycle', target: 'cycle', preconditions: [], attempt: 0, state: 'pending' },
23
+ },
24
+ cycleVerificationPlan: [], requiredVerifiers: [], generations: [], tasks: [],
25
+ dispatches: [], jobs: {}, verdicts: [], fixes: [], appliedRequests: {}, requestProblems: [], custodyDecisions: [],
26
+ };
27
+ }
28
+ function isObj(x) {
29
+ return typeof x === 'object' && x !== null && !Array.isArray(x);
30
+ }
31
+ function isWellFormedState(x) {
32
+ if (!isObj(x))
33
+ return false;
34
+ if (x.schema !== 1)
35
+ return false;
36
+ if (typeof x.revision !== 'number')
37
+ return false;
38
+ if (typeof x.branch !== 'string')
39
+ return false;
40
+ if (!isObj(x.cycle) || !['IN_PROGRESS', 'COMPLETE', 'BLOCKED'].includes(String(x.cycle.status))
41
+ || typeof x.cycle.startedAt !== 'string'
42
+ || (x.cycle.completedAt !== undefined && typeof x.cycle.completedAt !== 'string')
43
+ || (x.cycle.blockedReason !== undefined && typeof x.cycle.blockedReason !== 'string')
44
+ || (x.cycle.nextAction !== undefined && !isWellFormedNextAction(x.cycle.nextAction))
45
+ || (x.cycle.status === 'IN_PROGRESS' && x.cycle.nextAction === undefined))
46
+ return false;
47
+ if (!Array.isArray(x.generations) || !Array.isArray(x.tasks))
48
+ return false;
49
+ if (!Array.isArray(x.cycleVerificationPlan) || !Array.isArray(x.verdicts) || !Array.isArray(x.fixes))
50
+ return false;
51
+ if (!Array.isArray(x.requiredVerifiers) || !x.requiredVerifiers.every((kind) => ['test', 'lint', 'sensors', 'review', 'qa', 'interlock'].includes(String(kind)))
52
+ || !Array.isArray(x.dispatches) || !x.dispatches.every(isWellFormedDispatch))
53
+ return false;
54
+ if (!isObj(x.jobs) || !Object.values(x.jobs).every(isWellFormedJob))
55
+ return false;
56
+ if (!isObj(x.appliedRequests) || !Object.values(x.appliedRequests).every(isWellFormedAppliedRequest))
57
+ return false;
58
+ if (!Array.isArray(x.requestProblems) || !x.requestProblems.every(isWellFormedRequestProblem))
59
+ return false;
60
+ if (x.custodyDecisions !== undefined && (!Array.isArray(x.custodyDecisions) || !x.custodyDecisions.every(isWellFormedCustodyDecision)))
61
+ return false;
62
+ if (!x.generations.every(isWellFormedGeneration) || !x.tasks.every(isWellFormedTask))
63
+ return false;
64
+ if (!x.cycleVerificationPlan.every(isWellFormedVerificationItem))
65
+ return false;
66
+ if (!x.verdicts.every(isWellFormedVerdict) || !x.fixes.every(isWellFormedFix))
67
+ return false;
68
+ return true;
69
+ }
70
+ function isWellFormedNextAction(x) {
71
+ return isObj(x) && typeof x.actionId === 'string' && typeof x.type === 'string' && typeof x.target === 'string'
72
+ && strings(x.preconditions) && typeof x.attempt === 'number'
73
+ && (x.state === 'pending' || x.state === 'in-progress');
74
+ }
75
+ function isWellFormedDispatch(x) {
76
+ return isObj(x) && typeof x.id === 'string' && typeof x.taskId === 'string' && typeof x.at === 'string';
77
+ }
78
+ function strings(x) {
79
+ return Array.isArray(x) && x.every((item) => typeof item === 'string');
80
+ }
81
+ function isWellFormedVerificationItem(x) {
82
+ return isObj(x) && typeof x.id === 'string'
83
+ && ['test', 'lint', 'sensors', 'review', 'qa', 'interlock'].includes(String(x.kind))
84
+ && (x.satisfiedBy === undefined || typeof x.satisfiedBy === 'string');
85
+ }
86
+ function isWellFormedReviewObligation(x) {
87
+ return isObj(x) && typeof x.id === 'string' && typeof x.taskId === 'string'
88
+ && (x.kind === 'spec' || x.kind === 'quality')
89
+ && (x.verdictId === undefined || typeof x.verdictId === 'string');
90
+ }
91
+ function isWellFormedTask(x) {
92
+ return isObj(x) && typeof x.id === 'string' && typeof x.title === 'string'
93
+ && ['pending', 'in-progress', 'done'].includes(String(x.status))
94
+ && typeof x.attempts === 'number'
95
+ && Array.isArray(x.verificationPlan) && x.verificationPlan.every(isWellFormedVerificationItem)
96
+ && Array.isArray(x.reviewObligations) && x.reviewObligations.every(isWellFormedReviewObligation);
97
+ }
98
+ function isWellFormedGeneration(x) {
99
+ return isObj(x) && typeof x.n === 'number' && typeof x.token === 'string'
100
+ && exports.GENERATION_STATES.includes(String(x.state))
101
+ && typeof x.launchedAt === 'string'
102
+ && (x.controllerJobId === undefined || typeof x.controllerJobId === 'string')
103
+ && (x.spawnNonce === undefined || typeof x.spawnNonce === 'string')
104
+ && (x.provider === undefined || typeof x.provider === 'string')
105
+ && (x.resumePrompt === undefined || typeof x.resumePrompt === 'string')
106
+ && (x.processRef === undefined || isWellFormedProcessRef(x.processRef))
107
+ && (x.wrapperRef === undefined || isWellFormedProcessRef(x.wrapperRef));
108
+ }
109
+ function isWellFormedVerdict(x) {
110
+ return isObj(x) && typeof x.id === 'string' && typeof x.obligationId === 'string'
111
+ && ['pass', 'fail', 'inconclusive'].includes(String(x.result))
112
+ && typeof x.detail === 'string' && typeof x.receivedAt === 'string'
113
+ && typeof x.fingerprint === 'string' && strings(x.argv) && strings(x.paths) && typeof x.cwd === 'string';
114
+ }
115
+ function isWellFormedFix(x) {
116
+ return isObj(x) && typeof x.id === 'string' && typeof x.verdictId === 'string' && typeof x.closed === 'boolean';
117
+ }
118
+ function isWellFormedAppliedRequest(x) {
119
+ return isObj(x) && typeof x.requestId === 'string' && typeof x.idempotencyKey === 'string'
120
+ && typeof x.payloadDigest === 'string'
121
+ && ['applied', 'rejected-stale-generation', 'rejected-digest-mismatch', 'rejected-secret'].includes(String(x.outcome))
122
+ && (x.resultRef === undefined || typeof x.resultRef === 'string');
123
+ }
124
+ function isWellFormedRequestProblem(x) {
125
+ return isObj(x) && typeof x.file === 'string' && (x.kind === 'corrupt' || x.kind === 'rejected')
126
+ && typeof x.detail === 'string' && typeof x.at === 'string';
127
+ }
128
+ function isWellFormedCustodyDecision(x) {
129
+ return isObj(x) && typeof x.at === 'string' && x.decision === 'resume'
130
+ && typeof x.reason === 'string' && typeof x.generationToken === 'string';
131
+ }
132
+ function isWellFormedProcessRef(x) {
133
+ if (!isObj(x))
134
+ return false;
135
+ return typeof x.pid === 'number' && Number.isInteger(x.pid) && x.pid > 0
136
+ && typeof x.startTime === 'string'
137
+ && typeof x.spawnNonce === 'string'
138
+ && typeof x.argvDigest === 'string'
139
+ && typeof x.processGroup === 'number' && Number.isInteger(x.processGroup) && x.processGroup > 0
140
+ && typeof x.psArgsDigest === 'string';
141
+ }
142
+ function isWellFormedJob(x) {
143
+ if (!isObj(x))
144
+ return false;
145
+ return typeof x.id === 'string'
146
+ && typeof x.fingerprint === 'string'
147
+ && typeof x.commandDigest === 'string'
148
+ && strings(x.argv)
149
+ && typeof x.cwd === 'string'
150
+ && strings(x.paths)
151
+ && strings(x.expandedPaths)
152
+ && (x.observationState === 'progressing' || x.observationState === 'suspected-stall')
153
+ && isObj(x.phaseTimestamps) && Object.entries(x.phaseTimestamps).every(([state, at]) => exports.EXECUTION_STATES.includes(state) && typeof at === 'string')
154
+ && (x.verdict === undefined || ['pass', 'fail', 'inconclusive'].includes(String(x.verdict)))
155
+ && (x.spawnNonce === undefined || typeof x.spawnNonce === 'string')
156
+ && (x.processRef === undefined || isWellFormedProcessRef(x.processRef))
157
+ && (x.wrapperRef === undefined || isWellFormedProcessRef(x.wrapperRef))
158
+ && (x.lastProgressAt === undefined || typeof x.lastProgressAt === 'string')
159
+ && (x.logPath === undefined || typeof x.logPath === 'string')
160
+ && (x.result === undefined || (isObj(x.result) && typeof x.result.exitCode === 'number'
161
+ && typeof x.result.endedAt === 'string' && typeof x.result.resultPath === 'string'))
162
+ && (x.satisfies === undefined || typeof x.satisfies === 'string')
163
+ && (x.attemptOf === undefined || typeof x.attemptOf === 'string')
164
+ && exports.EXECUTION_STATES.includes(x.executionState);
165
+ }
package/dist/src/index.js CHANGED
@@ -33,6 +33,8 @@ const registry_1 = require("./commands/registry");
33
33
  const pin_1 = require("./commands/pin");
34
34
  const export_1 = require("./commands/export");
35
35
  const agent_1 = require("./commands/agent");
36
+ const job_1 = require("./commands/job");
37
+ const watch_1 = require("./commands/watch");
36
38
  const add_1 = require("./commands/add");
37
39
  const sync_1 = require("./commands/sync");
38
40
  const update_1 = require("./commands/update");
@@ -611,4 +613,6 @@ miroCmd.command('sync <storyMapPath>')
611
613
  (0, pin_1.registerPinCommands)(program);
612
614
  (0, export_1.registerExportCommand)(program);
613
615
  (0, agent_1.registerAgentCommand)(program);
616
+ (0, job_1.registerJobCommand)(program);
617
+ (0, watch_1.registerWatchCommand)(program);
614
618
  program.parse();
@@ -0,0 +1,85 @@
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
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const exec_wrapper_1 = require("../../../src/commands/job/exec-wrapper");
10
+ const process_1 = require("../../../src/core/journal/process");
11
+ describe('exec-wrapper', () => {
12
+ let dir;
13
+ beforeEach(() => { dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-wrap-')); });
14
+ afterEach(() => { fs_1.default.rmSync(dir, { recursive: true, force: true }); });
15
+ test('claim + identity sidecar + resultado terminal atomico (R1.8)', async () => {
16
+ const out = await (0, exec_wrapper_1.runExecWrapper)({ logsRoot: dir, jobId: 'job1', nonce: 'nonceA', argv: ['node', '-e', 'setTimeout(()=>process.exit(0), 300)'], cwd: '.' });
17
+ expect(out.exitCode).toBe(0);
18
+ expect(fs_1.default.existsSync((0, exec_wrapper_1.claimPath)(dir, 'job1', 'nonceA'))).toBe(true);
19
+ const identity = JSON.parse(fs_1.default.readFileSync((0, exec_wrapper_1.identityPath)(dir, 'job1', 'nonceA'), 'utf8'));
20
+ expect(identity.wrapper.pid).toBe(process.pid); // ProcessRef REAL del wrapper
21
+ expect(identity.command.pid).toBeGreaterThan(0); // ProcessRef REAL del comando
22
+ expect(identity.command.psArgsDigest).toMatch(/^[0-9a-f]{16}$/);
23
+ expect(identity.command.processGroup).not.toBe(identity.wrapper.processGroup); // el wrapper puede limpiar el grupo sin matarse
24
+ const result = JSON.parse(fs_1.default.readFileSync((0, exec_wrapper_1.resultPath)(dir, 'job1', 'nonceA'), 'utf8'));
25
+ expect(result.exitCode).toBe(0);
26
+ });
27
+ test('segundo claim con el mismo nonce falla: exactly-once (R1.8)', async () => {
28
+ await (0, exec_wrapper_1.runExecWrapper)({ logsRoot: dir, jobId: 'job2', nonce: 'nonceB', argv: ['node', '-e', 'process.exit(0)'], cwd: '.' });
29
+ await expect((0, exec_wrapper_1.runExecWrapper)({ logsRoot: dir, jobId: 'job2', nonce: 'nonceB', argv: ['node', '-e', 'process.exit(0)'], cwd: '.' }))
30
+ .rejects.toThrow(/claim/);
31
+ });
32
+ test('comando inexistente produce resultado 127, no crash (R1.8)', async () => {
33
+ const out = await (0, exec_wrapper_1.runExecWrapper)({ logsRoot: dir, jobId: 'job3', nonce: 'nonceC', argv: ['binario-inexistente-xyz'], cwd: '.' });
34
+ expect(out.exitCode).toBe(127);
35
+ expect((0, exec_wrapper_1.replayVerdict)(dir, 'job3', 'nonceC')).toBe('completed');
36
+ });
37
+ test('matriz de replay: sin claim / claim+resultado / claim sin resultado (R1.8)', async () => {
38
+ expect((0, exec_wrapper_1.replayVerdict)(dir, 'jobX', 'n1')).toBe('never-started'); // sin claim => re-spawn seguro
39
+ await (0, exec_wrapper_1.runExecWrapper)({ logsRoot: dir, jobId: 'jobY', nonce: 'n2', argv: ['node', '-e', 'process.exit(3)'], cwd: '.' });
40
+ expect((0, exec_wrapper_1.replayVerdict)(dir, 'jobY', 'n2')).toBe('completed'); // adoptar resultado
41
+ fs_1.default.writeFileSync((0, exec_wrapper_1.claimPath)(dir, 'jobZ', 'n3'), '{"claimed":true}'); // claim sin resultado
42
+ expect((0, exec_wrapper_1.replayVerdict)(dir, 'jobZ', 'n3')).toBe('unprovable'); // orphaned, jamas relanzar solo
43
+ });
44
+ test('el log captura la salida completa incluso si exit llega antes que el flush de stdio (R2.5)', async () => {
45
+ await (0, exec_wrapper_1.runExecWrapper)({ logsRoot: dir, jobId: 'job4', nonce: 'nonceD', argv: ['node', '-e', "process.stdout.write('linea-final-no-se-debe-perder'); process.exit(0)"], cwd: '.' });
46
+ const log = fs_1.default.readFileSync((0, exec_wrapper_1.logPath)(dir, 'job4', 'nonceD'), 'utf8');
47
+ expect(log).toContain('linea-final-no-se-debe-perder');
48
+ });
49
+ test('redacta secretos aunque la asignacion llegue dividida entre chunks de stdout (R2.3)', async () => {
50
+ const script = "process.stdout.write('API_'); setTimeout(()=>{process.stdout.write('KEY=hunter2\\n'); process.exit(0)}, 80)";
51
+ await (0, exec_wrapper_1.runExecWrapper)({ logsRoot: dir, jobId: 'job-split', nonce: 'nonce-split', argv: ['node', '-e', script], cwd: '.' });
52
+ const log = fs_1.default.readFileSync((0, exec_wrapper_1.logPath)(dir, 'job-split', 'nonce-split'), 'utf8');
53
+ expect(log).toContain('API_KEY=[REDACTED]');
54
+ expect(log).not.toContain('hunter2');
55
+ });
56
+ test('el log se acota aprox. en MAX_LOG_BYTES cuando la salida supera el limite, no crece sin cota (R2.5)', async () => {
57
+ const MAX_LOG_BYTES = 1024 * 1024;
58
+ const bytesToWrite = 2 * MAX_LOG_BYTES; // 2MB, muy por encima del cap de 1MB
59
+ const out = await (0, exec_wrapper_1.runExecWrapper)({
60
+ logsRoot: dir, jobId: 'job6', nonce: 'nonceF',
61
+ argv: ['node', '-e', `process.stdout.write('x'.repeat(${bytesToWrite}))`],
62
+ cwd: '.',
63
+ });
64
+ expect(out.exitCode).toBe(0);
65
+ const size = fs_1.default.statSync((0, exec_wrapper_1.logPath)(dir, 'job6', 'nonceF')).size;
66
+ // el append corta apenas se cruza el cap (chequeo ANTES de cada chunk),
67
+ // asi que el tamano final ronda MAX_LOG_BYTES +/- un ultimo chunk de
68
+ // pipe, jamas los 2MB reales escritos por el comando (R2.5).
69
+ expect(size).toBeGreaterThanOrEqual(MAX_LOG_BYTES);
70
+ expect(size).toBeLessThan(MAX_LOG_BYTES * 1.5);
71
+ expect(size).toBeLessThan(bytesToWrite);
72
+ }, 10000);
73
+ test('no se cuelga si un descendiente hereda stdio y no lo cierra (R1.8)', async () => {
74
+ const script = "const {spawn}=require('child_process'); const gc=spawn('sleep',['3'],{stdio:'inherit',detached:true}); gc.unref(); process.exit(0);";
75
+ const out = await (0, exec_wrapper_1.runExecWrapper)({ logsRoot: dir, jobId: 'job5', nonce: 'nonceE', argv: ['node', '-e', script], cwd: '.' });
76
+ expect(out.exitCode).toBe(0);
77
+ }, 10000);
78
+ test('no publica pass hasta drenar descendientes que quedan en el process group del comando', async () => {
79
+ const script = "require('child_process').spawn('sleep',['30'],{stdio:'ignore'}); process.exit(0);";
80
+ const out = await (0, exec_wrapper_1.runExecWrapper)({ logsRoot: dir, jobId: 'job7', nonce: 'nonceG', argv: ['node', '-e', script], cwd: '.' });
81
+ const identity = JSON.parse(fs_1.default.readFileSync((0, exec_wrapper_1.identityPath)(dir, 'job7', 'nonceG'), 'utf8'));
82
+ expect(out.exitCode).toBe(0);
83
+ expect((0, process_1.groupIsGone)(identity.command.processGroup)).toBe(true);
84
+ }, 10000);
85
+ });
@@ -0,0 +1,76 @@
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
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ const export_1 = require("../../../src/commands/job/export");
11
+ const types_1 = require("../../../src/core/journal/types");
12
+ function job(partial) {
13
+ return {
14
+ id: 'j1', fingerprint: 'fp', commandDigest: 'cd', argv: ['npm', 'test'], cwd: '.',
15
+ paths: [], expandedPaths: [], executionState: 'exited', observationState: 'progressing',
16
+ phaseTimestamps: {}, ...partial,
17
+ };
18
+ }
19
+ describe('export', () => {
20
+ let logs;
21
+ beforeEach(() => { logs = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-exp-')); });
22
+ afterEach(() => { fs_1.default.rmSync(logs, { recursive: true, force: true }); });
23
+ test('schema, timestamps por fase, wall time por task y ciclo (R3.7 / RNF-T.4)', () => {
24
+ const s = (0, types_1.emptyState)('r');
25
+ s.cycle.startedAt = '2026-08-01T10:00:00.000Z';
26
+ s.cycle.completedAt = '2026-08-01T10:30:00.000Z';
27
+ s.tasks.push({ id: 'T1', title: 't', status: 'done', attempts: 2, verificationPlan: [], reviewObligations: [], createdAt: '2026-08-01T10:00:00.000Z', completedAt: '2026-08-01T10:10:00.000Z' });
28
+ s.tasks.push({ id: 'T2', title: 'sin-timestamps', status: 'done', attempts: 1, verificationPlan: [], reviewObligations: [] });
29
+ s.jobs['j1'] = job({ phaseTimestamps: { received: 'a', running: 'b', exited: 'c' } });
30
+ const e = (0, export_1.buildExport)(s, 'codex', { logsRoot: null, baseline: null });
31
+ expect(e.schema).toBe(2);
32
+ expect(e.cycle.wallTimeMs).toBe(30 * 60000);
33
+ expect(e.tasks[0].wallTimeMs).toBe(10 * 60000);
34
+ expect(e.tasks[1].wallTimeMs).toBe('unobservable'); // sin timestamps => declarado, no cero
35
+ expect(e.jobs[0].phaseTimestamps.running).toBe('b');
36
+ });
37
+ test('despachos REALES, dedup real, evidencia con hash + comando reproducible (RNF-T.8/T.9)', () => {
38
+ const s = (0, types_1.emptyState)('r');
39
+ s.dispatches.push({ id: 'd1', taskId: 'T1', at: 'x' }, { id: 'd2', taskId: 'T1', at: 'y' });
40
+ s.jobs['j1'] = job({ id: 'j1', spawnNonce: 'n1' });
41
+ s.jobs['j2'] = job({ id: 'j2', spawnNonce: 'n2' }); // mismo fingerprint+cmd => dedup
42
+ const resultBody = JSON.stringify({ exitCode: 0, endedAt: 'x', resultPath: 'p' });
43
+ fs_1.default.writeFileSync(path_1.default.join(logs, 'j1.n1.result.json'), resultBody);
44
+ const e = (0, export_1.buildExport)(s, 'codex', { logsRoot: logs, baseline: null });
45
+ expect(e.metrics.dispatches).toBe(2); // reales, no attempts-proxy
46
+ expect(e.metrics.mechanicalRunsReal).toBe(2);
47
+ expect(e.metrics.mechanicalRunsDeduplicated).toBe(1);
48
+ expect(e.jobs.find((j) => j.id === 'j2').deduplicated).toBe(true);
49
+ const ev1 = e.evidence.find((x) => x.jobId === 'j1');
50
+ expect(ev1.resultHash).toBe(crypto_1.default.createHash('sha256').update(resultBody).digest('hex'));
51
+ expect(ev1.reproduce).toContain('npm test');
52
+ expect(e.evidence.find((x) => x.jobId === 'j2').resultHash).toBe('unobservable'); // sin result file
53
+ });
54
+ test('baselineComparison: con baseline compara, sin baseline declara unobservable (R3.7)', () => {
55
+ const s = (0, types_1.emptyState)('r');
56
+ s.cycle.startedAt = '2026-08-01T10:00:00.000Z';
57
+ s.cycle.completedAt = '2026-08-01T10:20:00.000Z';
58
+ s.dispatches.push({ id: 'd1', taskId: 'T1', at: 'x' });
59
+ const withBase = (0, export_1.buildExport)(s, 'codex', { logsRoot: null, baseline: { source: 'docs/baseline-2026-07-29.json', wallTimeMs: 40 * 60000, dispatches: 3 } });
60
+ expect(withBase.baselineComparison.baselineDate).toBe('2026-07-29');
61
+ expect(withBase.baselineComparison.wallTimeMs).toEqual({ current: 20 * 60000, baseline: 40 * 60000, delta: -20 * 60000 });
62
+ expect(withBase.baselineComparison.dispatches).toEqual({ current: 1, baseline: 3, delta: -2 });
63
+ expect(withBase.baselineComparison.tokensPerRole).toBe('unobservable'); // ningun provider lo reporta (R0)
64
+ const noBase = (0, export_1.buildExport)(s, 'codex', { logsRoot: null, baseline: null });
65
+ expect(noBase.baselineComparison.wallTimeMs.baseline).toBe('unobservable');
66
+ expect(noBase.baselineComparison.wallTimeMs.delta).toBe('unobservable');
67
+ expect(noBase.metrics.tokensPerRole).toBe('unobservable');
68
+ });
69
+ test('wallMs declara unobservable ante timestamps invertidos, nunca un numero negativo (R3.7)', () => {
70
+ const s = (0, types_1.emptyState)('r');
71
+ s.cycle.startedAt = '2026-08-01T10:30:00.000Z';
72
+ s.cycle.completedAt = '2026-08-01T10:00:00.000Z'; // invertido: dato corrupto/anomalo
73
+ const e = (0, export_1.buildExport)(s, 'codex', { logsRoot: null, baseline: null });
74
+ expect(e.cycle.wallTimeMs).toBe('unobservable');
75
+ });
76
+ });