agentic-workflow-manager 9.11.3 → 9.11.5
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/gate.js +19 -5
- package/dist/src/commands/job/index.js +13 -7
- package/dist/src/commands/job/query.js +14 -9
- package/dist/src/commands/sensors/init.js +0 -10
- package/dist/src/commands/sensors/run.js +0 -1
- package/dist/src/commands/track/status.js +1 -1
- package/dist/src/commands/watch/supervisor.js +9 -3
- package/dist/src/core/artifact-name.js +0 -1
- package/dist/src/core/dashboard/render-html.js +0 -7
- package/dist/src/core/journal/store.js +30 -6
- package/dist/src/core/ledger/scan.js +0 -2
- package/dist/src/core/miro.js +0 -1
- package/dist/src/core/profile.js +0 -1
- package/dist/src/core/text.js +2 -2
- package/dist/src/core/tracks/teardown.js +25 -0
- package/dist/tests/commands/context.test.js +1 -1
- package/dist/tests/commands/job/absent-vs-corrupt-journal.test.js +134 -0
- package/dist/tests/commands/multi-agent-targeting.test.js +1 -1
- package/dist/tests/commands/preflight/preflight.test.js +0 -1
- package/dist/tests/commands/process.test.js +1 -1
- package/dist/tests/commands/track/status.test.js +5 -2
- package/dist/tests/commands/track/supervisor-wrapper-cli.test.js +0 -3
- package/dist/tests/commands/watch/apply.test.js +1 -1
- package/dist/tests/commands/watch/watch-init.test.js +4 -1
- package/dist/tests/core/context/provider.test.js +2 -2
- package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
- package/dist/tests/core/dashboard/render-terminal.test.js +1 -1
- package/dist/tests/core/diagnostics/provider-checks.test.js +0 -1
- package/dist/tests/core/discovery.test.js +0 -1
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/journal/process.test.js +1 -1
- package/dist/tests/core/orchestrators.test.js +4 -4
- package/dist/tests/core/secure-fs/native-bridge.test.js +0 -1
- package/dist/tests/core/text.test.js +1 -1
- package/dist/tests/structural/directory-fsync-is-single-source.test.js +1 -1
- package/dist/tests/structural/lint-covers-typescript.test.js +64 -5
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.computeGate = computeGate;
|
|
4
4
|
exports.computeTrackGate = computeTrackGate;
|
|
5
|
+
const store_1 = require("../../core/journal/store");
|
|
5
6
|
const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
|
|
6
7
|
function sameSelection(left, right) {
|
|
7
8
|
return left.selector.kind === right.selector.kind && left.selector.id === right.selector.id && left.effort.kind === right.effort.kind && left.effort.value === right.effort.value;
|
|
@@ -149,9 +150,19 @@ function evaluateEvidence(state, fingerprintNow, scope) {
|
|
|
149
150
|
}
|
|
150
151
|
return { pass: reasons.length === 0, reasons };
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
+
/**
|
|
154
|
+
* `absent` defaults to false so every existing caller keeps today's behaviour
|
|
155
|
+
* exactly: an unreadable journal fails closed either way. What changes is the
|
|
156
|
+
* REASON an absent one reports — it used to claim corruption, sending operators
|
|
157
|
+
* to look for damage in a healthy repository (#173). The exit code does not
|
|
158
|
+
* change: this interlock has no plan binding to read an execution mode from when
|
|
159
|
+
* the journal is missing, so it stays fail-closed and names the remedy instead.
|
|
160
|
+
*/
|
|
161
|
+
function computeGate(state, corrupt, fingerprintNow, absent = false) {
|
|
153
162
|
if (corrupt || state === null) {
|
|
154
|
-
return
|
|
163
|
+
return absent && !corrupt
|
|
164
|
+
? { pass: false, reasons: [{ category: 'absent', detail: store_1.ABSENT_JOURNAL_DETAIL }] }
|
|
165
|
+
: { pass: false, reasons: [{ category: 'corrupt', detail: 'state.json corrupto o ilegible: la corrupcion jamas certifica' }] };
|
|
155
166
|
}
|
|
156
167
|
return evaluateEvidence(state, fingerprintNow, { requireGlobalKinds: true });
|
|
157
168
|
}
|
|
@@ -161,9 +172,12 @@ function computeGate(state, corrupt, fingerprintNow) {
|
|
|
161
172
|
* ahi) y rechaza cualquier tarea que aparezca en el journal pero no en la
|
|
162
173
|
* asignacion del track (R2.3) — nunca certifica por una tarea ajena que se
|
|
163
174
|
* haya colado. */
|
|
164
|
-
function computeTrackGate(state, corruptState, fingerprintNow) {
|
|
165
|
-
if (corruptState || state === null)
|
|
166
|
-
return
|
|
175
|
+
function computeTrackGate(state, corruptState, fingerprintNow, absent = false) {
|
|
176
|
+
if (corruptState || state === null) {
|
|
177
|
+
return absent && !corruptState
|
|
178
|
+
? { pass: false, reasons: [{ category: 'absent-state', detail: store_1.ABSENT_JOURNAL_DETAIL }] }
|
|
179
|
+
: { pass: false, reasons: [{ category: 'corrupt-state', detail: 'journal corrupto o ilegible' }] };
|
|
180
|
+
}
|
|
167
181
|
if (state.trackContext === undefined)
|
|
168
182
|
return { pass: false, reasons: [{ category: 'wrong-context', detail: 'gate local requiere trackContext' }] };
|
|
169
183
|
const assigned = new Set(state.trackContext.taskIds);
|
|
@@ -219,8 +219,13 @@ function registerJobCommand(program) {
|
|
|
219
219
|
const branch = branchOf(repo);
|
|
220
220
|
assertAuthenticatedCwd(repo, branch);
|
|
221
221
|
const r = (0, store_1.readJournal)(repo, branch);
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
const presence = (0, store_1.journalPresence)(r);
|
|
223
|
+
if (presence !== 'present' || r.state === null) {
|
|
224
|
+
// Absence and corruption need different responses, so they are
|
|
225
|
+
// reported as different facts. Both still fail closed (#173).
|
|
226
|
+
process.stdout.write(JSON.stringify(presence === 'absent'
|
|
227
|
+
? { journal: 'absent', corruptState: false, remedy: store_1.ABSENT_JOURNAL_DETAIL }
|
|
228
|
+
: { journal: 'corrupt', corruptState: true }, null, 2) + '\n');
|
|
224
229
|
process.exit(1);
|
|
225
230
|
}
|
|
226
231
|
try {
|
|
@@ -257,7 +262,7 @@ function registerJobCommand(program) {
|
|
|
257
262
|
process.exit(1);
|
|
258
263
|
}
|
|
259
264
|
}
|
|
260
|
-
const g = (0, gate_1.computeGate)(r.state, r.corrupt, realFingerprintNow(repo));
|
|
265
|
+
const g = (0, gate_1.computeGate)(r.state, r.corrupt, realFingerprintNow(repo), r.absent);
|
|
261
266
|
process.stdout.write(JSON.stringify(g, null, 2) + '\n');
|
|
262
267
|
if (!g.pass)
|
|
263
268
|
process.exit(1); // falla cerrado (R3.2)
|
|
@@ -270,8 +275,9 @@ function registerJobCommand(program) {
|
|
|
270
275
|
const branch = branchOf(repo);
|
|
271
276
|
assertAuthenticatedCwd(repo, branch);
|
|
272
277
|
const r = (0, store_1.readJournal)(repo, branch);
|
|
273
|
-
|
|
274
|
-
|
|
278
|
+
const presence = (0, store_1.journalPresence)(r);
|
|
279
|
+
if (presence !== 'present' || r.state === null) {
|
|
280
|
+
process.stdout.write(JSON.stringify({ journal: presence, corruptState: presence === 'corrupt' }) + '\n');
|
|
275
281
|
process.exit(1);
|
|
276
282
|
return;
|
|
277
283
|
}
|
|
@@ -287,7 +293,7 @@ function registerJobCommand(program) {
|
|
|
287
293
|
assertAuthenticatedCwd(repo, branch);
|
|
288
294
|
const r = (0, store_1.readJournal)(repo, branch);
|
|
289
295
|
if (r.corrupt || r.state === null) {
|
|
290
|
-
process.stderr.write('journal corrupto o
|
|
296
|
+
process.stderr.write(`${r.absent && !r.corrupt ? store_1.ABSENT_JOURNAL_DETAIL : 'journal corrupto o ilegible'}\n`);
|
|
291
297
|
process.exit(1);
|
|
292
298
|
}
|
|
293
299
|
const plan = (0, reap_1.planReap)(r.state);
|
|
@@ -308,7 +314,7 @@ function registerJobCommand(program) {
|
|
|
308
314
|
assertAuthenticatedCwd(repo, branch);
|
|
309
315
|
const r = (0, store_1.readJournal)(repo, branch);
|
|
310
316
|
if (r.corrupt || r.state === null) {
|
|
311
|
-
process.stderr.write('journal corrupto\n
|
|
317
|
+
process.stderr.write(`${r.absent && !r.corrupt ? store_1.ABSENT_JOURNAL_DETAIL : 'journal corrupto o ilegible'}\n`);
|
|
312
318
|
process.exit(1);
|
|
313
319
|
}
|
|
314
320
|
let baseline = null;
|
|
@@ -6,23 +6,27 @@ exports.queryShow = queryShow;
|
|
|
6
6
|
const store_1 = require("../../core/journal/store");
|
|
7
7
|
const process_1 = require("../../core/journal/process");
|
|
8
8
|
/** Fuente unica de "que hay corriendo": cruza identidad completa contra
|
|
9
|
-
* procesos vivos. corrupt es VISIBLE, nunca descartado (R1.6)
|
|
9
|
+
* procesos vivos. corrupt es VISIBLE, nunca descartado (R1.6), y un journal
|
|
10
|
+
* que nunca existio se reporta como `absent`, no como corrupcion (#173). */
|
|
10
11
|
function queryPs(repoRoot, branch) {
|
|
11
12
|
const r = (0, store_1.readJournal)(repoRoot, branch);
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const journal = (0, store_1.journalPresence)(r);
|
|
14
|
+
if (journal !== 'present')
|
|
15
|
+
return { corruptState: journal === 'corrupt', journal, jobs: [] };
|
|
14
16
|
const jobs = Object.values(r.state.jobs).map((j) => ({
|
|
15
17
|
id: j.id, executionState: j.executionState, observationState: j.observationState, verdict: j.verdict,
|
|
16
18
|
alive: j.processRef ? (0, process_1.refIsAlive)(j.processRef) : 'sin-pid',
|
|
17
19
|
}));
|
|
18
|
-
return { corruptState: false, jobs };
|
|
20
|
+
return { corruptState: false, journal, jobs };
|
|
19
21
|
}
|
|
20
22
|
function queryList(repoRoot, branch) {
|
|
21
23
|
const r = (0, store_1.readJournal)(repoRoot, branch);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
const journal = (0, store_1.journalPresence)(r);
|
|
25
|
+
if (journal !== 'present')
|
|
26
|
+
return { corruptState: journal === 'corrupt', journal, cycleStatus: null, jobs: [] };
|
|
24
27
|
return {
|
|
25
28
|
corruptState: false,
|
|
29
|
+
journal,
|
|
26
30
|
cycleStatus: r.state.cycle.status,
|
|
27
31
|
jobs: Object.values(r.state.jobs).map((j) => ({
|
|
28
32
|
id: j.id, executionState: j.executionState, verdict: j.verdict, argv: j.argv, satisfies: j.satisfies,
|
|
@@ -31,7 +35,8 @@ function queryList(repoRoot, branch) {
|
|
|
31
35
|
}
|
|
32
36
|
function queryShow(repoRoot, branch, jobId) {
|
|
33
37
|
const r = (0, store_1.readJournal)(repoRoot, branch);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
const journal = (0, store_1.journalPresence)(r);
|
|
39
|
+
if (journal !== 'present')
|
|
40
|
+
return { corruptState: journal === 'corrupt', journal, job: null };
|
|
41
|
+
return { corruptState: false, journal, job: r.state.jobs[jobId] ?? null };
|
|
37
42
|
}
|
|
@@ -139,16 +139,6 @@ function buildManifest(pack, existing, registryRoot, cwd = process.cwd()) {
|
|
|
139
139
|
}
|
|
140
140
|
return { pack, sensors };
|
|
141
141
|
}
|
|
142
|
-
/** Pack names present as directories under `<registryRoot>/sensor-packs/`, sorted. */
|
|
143
|
-
function availablePacks(registryRoot) {
|
|
144
|
-
const packsDir = path_1.default.join(registryRoot, 'sensor-packs');
|
|
145
|
-
if (!fs_1.default.existsSync(packsDir) || !fs_1.default.statSync(packsDir).isDirectory())
|
|
146
|
-
return [];
|
|
147
|
-
return fs_1.default.readdirSync(packsDir, { withFileTypes: true })
|
|
148
|
-
.filter(e => e.isDirectory())
|
|
149
|
-
.map(e => e.name)
|
|
150
|
-
.sort();
|
|
151
|
-
}
|
|
152
142
|
async function initSensors(opts = {}) {
|
|
153
143
|
const cwd = opts.cwd ?? process.cwd();
|
|
154
144
|
const detectionCwd = opts.packageRoot ? path_1.default.resolve(cwd, opts.packageRoot) : cwd;
|
|
@@ -29,7 +29,6 @@ const project_1 = require("./project");
|
|
|
29
29
|
// Sensor JSON output can be several MB on large repos (e.g. `eslint --format json`
|
|
30
30
|
// with thousands of findings). A 1MB cap killed the child with SIGTERM when
|
|
31
31
|
// exceeded — which previously surfaced as a false "timeout".
|
|
32
|
-
const MAX_BUFFER = 64 * 1024 * 1024;
|
|
33
32
|
/** Hard ceiling on parallel sensors: past this, they only contend for the same cores. */
|
|
34
33
|
const MAX_CONCURRENCY = 4;
|
|
35
34
|
const READ_ONLY_MAX_ENTRIES = 250_000;
|
|
@@ -56,7 +56,7 @@ function aggregateTrackStatus(planRoot, plan) {
|
|
|
56
56
|
const observed = (0, store_1.readJournal)(ref.worktreePath, ref.branch);
|
|
57
57
|
tracks[ref.trackId] = {
|
|
58
58
|
phase: ref.phase,
|
|
59
|
-
gate: (0, gate_1.computeTrackGate)(observed.state, observed.corrupt, fingerprintFor(ref.worktreePath)),
|
|
59
|
+
gate: (0, gate_1.computeTrackGate)(observed.state, observed.corrupt, fingerprintFor(ref.worktreePath), observed.absent),
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
return { cohort: deriveCohortPhase(refs), tracks };
|
|
@@ -5,6 +5,9 @@ exports.routingFacts = routingFacts;
|
|
|
5
5
|
exports.defaultDispatchAdmission = defaultDispatchAdmission;
|
|
6
6
|
exports.admissionForConfig = admissionForConfig;
|
|
7
7
|
exports.runSupervisorLoop = runSupervisorLoop;
|
|
8
|
+
// Loop foreground (R4.4/R4.5): tick = apply -> collect/spawn -> stall -> gate.
|
|
9
|
+
// COMPLETE exige gate verde (que exige cero vivos): drenaje ANTES de declarar.
|
|
10
|
+
// Custodia BLOCKED: el loop sigue, el lock NO se libera, nada se mata.
|
|
8
11
|
const store_1 = require("../../core/journal/store");
|
|
9
12
|
const fingerprint_1 = require("../../core/journal/fingerprint");
|
|
10
13
|
const validate_1 = require("../../core/plan/validate");
|
|
@@ -464,7 +467,7 @@ class Supervisor {
|
|
|
464
467
|
if (freezing)
|
|
465
468
|
return this.attemptFreeze();
|
|
466
469
|
const r = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
467
|
-
const gate = (0, gate_1.computeGate)(r.state, r.corrupt, this.fingerprintNow);
|
|
470
|
+
const gate = (0, gate_1.computeGate)(r.state, r.corrupt, this.fingerprintNow, r.absent);
|
|
468
471
|
const liveJobs = r.state === null ? 1 : Object.values(r.state.jobs).filter((j) => LIVE.includes(j.executionState)).length;
|
|
469
472
|
// R7/C3/C4 (Task 12): un journal de PLAN con cohorte de tracks NUNCA
|
|
470
473
|
// declara `cycle.status = COMPLETE` por este camino genérico mientras
|
|
@@ -640,8 +643,11 @@ exports.Supervisor = Supervisor;
|
|
|
640
643
|
* generacion propia (cero huerfanos). */
|
|
641
644
|
async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.defaultWrapperSpawner)(), trackRuntime, dispatchAdmission) {
|
|
642
645
|
const r = (0, store_1.readJournal)(repoRoot, branch);
|
|
643
|
-
if (r.corrupt || r.state === null)
|
|
644
|
-
throw new Error(
|
|
646
|
+
if (r.corrupt || r.state === null) {
|
|
647
|
+
throw new Error(r.absent && !r.corrupt
|
|
648
|
+
? 'journal ausente: corre `awm watch --init --plan <plan>` primero'
|
|
649
|
+
: 'journal corrupto o ilegible: inspecciona .awm/journal antes de continuar');
|
|
650
|
+
}
|
|
645
651
|
if (r.state.schema !== 2 || !r.state.planBinding) {
|
|
646
652
|
throw new Error('journal legacy o sin binding compacto: `awm watch` no puede ejecutar ni despachar trabajo');
|
|
647
653
|
}
|
|
@@ -50,7 +50,6 @@ function isSafeArtifactName(name) {
|
|
|
50
50
|
if (/[. ]$/.test(value))
|
|
51
51
|
return false;
|
|
52
52
|
// Bytes de control y NUL: truncan la ruta a nivel syscall en algunos SO.
|
|
53
|
-
// eslint-disable-next-line no-control-regex
|
|
54
53
|
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
55
54
|
return false;
|
|
56
55
|
// Cualquier separador (de ambas plataformas) convierte esto en una ruta,
|
|
@@ -14,13 +14,6 @@ function sectionHtml(section, supplement = '') {
|
|
|
14
14
|
const rows = section.items.length === 0 ? '<p class="empty">No observations reported.</p>' : `<table><thead><tr><th scope="col">Observation</th><th scope="col">State</th><th scope="col">Detail</th><th scope="col">Remediation</th></tr></thead><tbody>${section.items.map((item) => `<tr><td data-label="Observation">${escapeHtml(item.label)}</td><td data-label="State"><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span></td><td data-label="Detail">${item.detail ? escapeHtml(item.detail) : '—'}</td><td data-label="Remediation">${item.remediation ? `<code>${escapeHtml(item.remediation)}</code>` : '—'}</td></tr>`).join('')}</tbody></table>`;
|
|
15
15
|
return `<section id="${section.id}" aria-label="${title}"><header><h2>${title}</h2><span class="eyebrow">${escapeHtml(section.availability.replace('_', ' '))}</span></header><div class="section-body">${availability}${supplement}${rows}</div></section>`;
|
|
16
16
|
}
|
|
17
|
-
function diagnosticCards(items, attribute) {
|
|
18
|
-
const labels = ['installation', 'sensors', 'permissions'];
|
|
19
|
-
const cards = items.slice(0, 3).map((item, index) => `<li data-diagnostic-card="${labels[index] ?? 'diagnostic'}"><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><strong>${escapeHtml(item.label)}</strong><span>${item.detail ? escapeHtml(item.detail) : 'No additional detail'}</span></li>`).join('') || '<li><span class="state not_applicable">— Not applicable</span><strong>No machine observations</strong><span>Machine diagnostics are not available.</span></li>';
|
|
20
|
-
if (attribute === 'data-machine-diagnostics')
|
|
21
|
-
return `<section data-machine-diagnostics aria-labelledby="machine-diagnostics-heading"><header><h2 id="machine-diagnostics-heading">Machine diagnostics</h2></header><div class="section-body"><ul class="diagnostic-grid">${cards}</ul></div></section>`;
|
|
22
|
-
return `<div class="machine-preparation-strip" data-machine-preparation role="group" aria-labelledby="machine-preparation-heading"><h3 id="machine-preparation-heading">Preparación de máquina</h3><ul class="diagnostic-grid">${cards}</ul></div>`;
|
|
23
|
-
}
|
|
24
17
|
function machineBento(items) {
|
|
25
18
|
const names = ['Instalación', 'Sensores globales', 'Persistencia'];
|
|
26
19
|
return `<div class="machine-bento" data-machine-bento>${names.map((name, index) => {
|
|
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ABSENT_JOURNAL_DETAIL = void 0;
|
|
7
|
+
exports.journalPresence = journalPresence;
|
|
6
8
|
exports.assertJournalInitPaths = assertJournalInitPaths;
|
|
7
9
|
exports.initJournal = initJournal;
|
|
8
10
|
exports.initBoundJournal = initBoundJournal;
|
|
@@ -18,6 +20,16 @@ const types_1 = require("./types");
|
|
|
18
20
|
const paths_1 = require("./paths");
|
|
19
21
|
const identity_1 = require("../plan/identity");
|
|
20
22
|
const validate_1 = require("../plan/validate");
|
|
23
|
+
function journalPresence(read) {
|
|
24
|
+
if (read.corrupt)
|
|
25
|
+
return 'corrupt';
|
|
26
|
+
if (read.absent || read.state === null)
|
|
27
|
+
return 'absent';
|
|
28
|
+
return 'present';
|
|
29
|
+
}
|
|
30
|
+
/** What an absent journal actually needs, naming the command rather than leaving
|
|
31
|
+
* the operator to infer it — and saying who this interlock is for. */
|
|
32
|
+
exports.ABSENT_JOURNAL_DETAIL = 'journal ausente (no corrupto): el interlock desatendido exige un journal ligado — corre `awm watch --init --plan <plan>`. Un ciclo interactivo no requiere journal.';
|
|
21
33
|
/** Schema 1 evoluciono de forma aditiva durante R1/R5. Normalizamos solamente
|
|
22
34
|
* campos que antes no existian; evidencia legacy queda deliberadamente con
|
|
23
35
|
* fingerprint vacio para que el gate la considere stale, nunca certificada.
|
|
@@ -188,28 +200,35 @@ function initBoundJournal(repoRoot, branch, binding, report) {
|
|
|
188
200
|
}
|
|
189
201
|
}
|
|
190
202
|
/** Lectura corrupt-aware (R1.6): sintaxis invalida O shape invalido => corrupt:true.
|
|
191
|
-
*
|
|
203
|
+
* Un journal que nunca se creo es `absent`, jamas `corrupt`.
|
|
204
|
+
* Los CONSUMIDORES deciden: consultas muestran el estado; gate/reconcile bloquean. */
|
|
192
205
|
function readJournal(repoRoot, branch) {
|
|
193
206
|
const sp = (0, paths_1.statePath)(repoRoot, branch);
|
|
194
207
|
let raw;
|
|
195
208
|
try {
|
|
209
|
+
// Both tree assertions RETURN on ENOENT and throw a plain Error for an
|
|
210
|
+
// unsafe or linked segment, so the only ENOENT that can reach here comes
|
|
211
|
+
// from the state file itself. An unsafe tree is therefore never reported
|
|
212
|
+
// as absence — the same rule `watchJournalStatus` already states.
|
|
196
213
|
assertJournalTree(repoRoot, branch);
|
|
197
214
|
assertJournalFileNotSymlink(sp);
|
|
198
215
|
raw = fs_1.default.readFileSync(sp, 'utf8');
|
|
199
216
|
}
|
|
200
|
-
catch {
|
|
201
|
-
return
|
|
217
|
+
catch (error) {
|
|
218
|
+
return error.code === 'ENOENT'
|
|
219
|
+
? { state: null, corrupt: false, absent: true }
|
|
220
|
+
: { state: null, corrupt: true, absent: false };
|
|
202
221
|
}
|
|
203
222
|
let parsed;
|
|
204
223
|
try {
|
|
205
224
|
parsed = normalizeSchemaOne(JSON.parse(raw));
|
|
206
225
|
}
|
|
207
226
|
catch {
|
|
208
|
-
return { state: null, corrupt: true, raw };
|
|
227
|
+
return { state: null, corrupt: true, absent: false, raw };
|
|
209
228
|
}
|
|
210
229
|
if (!(0, types_1.isWellFormedState)(parsed))
|
|
211
|
-
return { state: null, corrupt: true, raw };
|
|
212
|
-
return { state: parsed, corrupt: false, raw };
|
|
230
|
+
return { state: null, corrupt: true, absent: false, raw };
|
|
231
|
+
return { state: parsed, corrupt: false, absent: false, raw };
|
|
213
232
|
}
|
|
214
233
|
/** Escritura canonica: SOLO el supervisor la invoca (single-writer). CAS por
|
|
215
234
|
* revision monotonica: el snapshot que traes debe ser el vigente. */
|
|
@@ -219,6 +238,11 @@ function writeJournal(repoRoot, branch, state) {
|
|
|
219
238
|
const current = readJournal(repoRoot, branch);
|
|
220
239
|
if (current.corrupt)
|
|
221
240
|
throw new Error('journal corrupto: no se escribe sobre corrupcion (R1.6)');
|
|
241
|
+
// Absence used to arrive here as `corrupt`, and this guard is what kept
|
|
242
|
+
// initialization create-only. Naming it explicitly preserves that exactly:
|
|
243
|
+
// only initJournal/initBoundJournal may bring a journal into existence.
|
|
244
|
+
if (current.absent)
|
|
245
|
+
throw new Error('journal inexistente: la creacion es de initJournal/initBoundJournal, no de writeJournal');
|
|
222
246
|
if (current.state !== null && current.state.revision !== state.revision) {
|
|
223
247
|
throw new Error(`revision desactualizada: disco=${current.state.revision} propuesta=${state.revision}`);
|
|
224
248
|
}
|
|
@@ -136,7 +136,6 @@ function scanProjectLedgers(projectRoot, overrides = {}) {
|
|
|
136
136
|
throw new Error('ledger archive directory escapes project root');
|
|
137
137
|
directories.push({ directory: archive, archive: true });
|
|
138
138
|
}
|
|
139
|
-
let filesSeen = 0;
|
|
140
139
|
let linesSeen = 0;
|
|
141
140
|
let entryLimitReached = false;
|
|
142
141
|
let omittedEvidenceRefs = 0;
|
|
@@ -145,7 +144,6 @@ function scanProjectLedgers(projectRoot, overrides = {}) {
|
|
|
145
144
|
for (const { isArchive, target } of collected.candidates.slice(0, limits.maxFiles)) {
|
|
146
145
|
if (entryLimitReached)
|
|
147
146
|
break;
|
|
148
|
-
filesSeen += 1;
|
|
149
147
|
const stat = fs_1.default.lstatSync(target);
|
|
150
148
|
if (stat.isSymbolicLink()) {
|
|
151
149
|
skip('symlink-entry');
|
package/dist/src/core/miro.js
CHANGED
|
@@ -85,7 +85,6 @@ function computeLayout(storyMap) {
|
|
|
85
85
|
let groupLeftX = PADDING; // running X from frame left edge
|
|
86
86
|
for (let ai = 0; ai < activities.length; ai++) {
|
|
87
87
|
const activity = activities[ai];
|
|
88
|
-
const numTaskCols = Math.max(1, activity.tasks.length);
|
|
89
88
|
const groupWidth = activityGroupWidths[ai];
|
|
90
89
|
// Activity card — spans its task columns
|
|
91
90
|
const activityX = frameLeft + groupLeftX + groupWidth / 2;
|
package/dist/src/core/profile.js
CHANGED
|
@@ -27,7 +27,6 @@ function findProjectRoot(startDir) {
|
|
|
27
27
|
catch {
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
|
-
// eslint-disable-next-line no-constant-condition
|
|
31
30
|
while (true) {
|
|
32
31
|
if (fs_1.default.existsSync(path_1.default.join(dir, '.git')) ||
|
|
33
32
|
fs_1.default.existsSync(path_1.default.join(dir, 'package.json')) ||
|
package/dist/src/core/text.js
CHANGED
|
@@ -14,13 +14,13 @@ exports.sanitizeDeclaredField = sanitizeDeclaredField;
|
|
|
14
14
|
* `AGENTS.md` documenta bajo `defensive-guard-consistency`: endurecer una copia
|
|
15
15
|
* y dejar la otra atrás. */
|
|
16
16
|
function stripControlChars(text) {
|
|
17
|
-
//
|
|
17
|
+
// Necesitamos matchear C0 deliberadamente.
|
|
18
18
|
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
19
19
|
}
|
|
20
20
|
/** Keeps an untrusted diagnostic field on one terminal line, without changing
|
|
21
21
|
* the whitespace policy for multiline public text or composed markdown. */
|
|
22
22
|
function sanitizeDiagnosticText(text) {
|
|
23
|
-
//
|
|
23
|
+
// Remove all C0/C1 controls, including terminal escape bytes.
|
|
24
24
|
return text.replace(/\r\n|[\r\n\t\u2028\u2029]/g, ' ').replace(/[\x00-\x1f\x7f-\x9f]/g, '');
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.decideTeardown = void 0;
|
|
4
4
|
exports.worktreeOwnershipProven = worktreeOwnershipProven;
|
|
5
|
+
// Task 13 (R4.2/R4.3/R4.6/R4.10/C2/C9): `decideTeardown` YA vive en
|
|
6
|
+
// `protocol.ts` (autoridad única de decisión) — este módulo NO crea una
|
|
7
|
+
// segunda copia (mismo criterio que `join.ts` con `decideJoinReconciliation`,
|
|
8
|
+
// T11). `teardown.test.ts` prueba la matriz completa IMPORTANDO este
|
|
9
|
+
// re-export, así que cualquier fix que descubra tiene que vivir en
|
|
10
|
+
// `protocol.ts` (y volver a correr la exploración de T1 en el mismo commit)
|
|
11
|
+
// para que el gate siga significando algo.
|
|
12
|
+
//
|
|
13
|
+
// Lo que SÍ aporta este archivo es la única prueba de OWNERSHIP real que
|
|
14
|
+
// `tracks.ts` necesita antes de siquiera considerar `remove-owned-worktree`
|
|
15
|
+
// como decisión segura: cuatro condiciones simultáneas (R4.10) —
|
|
16
|
+
// 1. `teardownIntent.worktreePath` (snapshot durable tomado en
|
|
17
|
+
// `TEARDOWN_REQUESTED` -> `TEARDOWN_INTENT`) resuelve al MISMO target
|
|
18
|
+
// canónico que `ref.worktreePath` (el dato vivo del journal).
|
|
19
|
+
// 2. `teardownIntent.branch === ref.branch`.
|
|
20
|
+
// 3. `.awm/track.json` del worktree (`readDescriptor`) coincide en
|
|
21
|
+
// `planJournalId`/`trackId`/`fencingToken` — la identidad completa que
|
|
22
|
+
// el propio track escribió al armarse (Task 9), nunca reconstruida a
|
|
23
|
+
// mano.
|
|
24
|
+
// 4. `git worktree list --porcelain` asocia ESE path exactamente con la
|
|
25
|
+
// branch determinista del track (`ownedWorktreeExists`, ya construida
|
|
26
|
+
// en Task 9 sobre el mismo criterio).
|
|
27
|
+
// Cualquier chequeo indemostrable (no existe, no legible, mismatch, fallo de
|
|
28
|
+
// git) es "ownership NO probado" — fail-closed (R4.6): el caller nunca
|
|
29
|
+
// adopta ni borra lo que no pudo probar.
|
|
5
30
|
const descriptor_1 = require("./descriptor");
|
|
6
31
|
const paths_1 = require("../paths");
|
|
7
32
|
const git_1 = require("./git");
|
|
@@ -66,7 +66,7 @@ describe('awm context orchestrators', () => {
|
|
|
66
66
|
it('sanea bytes de control antes de escribir a la terminal', () => {
|
|
67
67
|
const hostil = { name: 'x\x1b[31m', appliesWhen: 'w\x07', terminatesTo: 't\x00' };
|
|
68
68
|
const r = (0, context_1.runContextOrchestrators)(collected([hostil]), { json: false });
|
|
69
|
-
//
|
|
69
|
+
// Verificamos la ausencia deliberada de C0.
|
|
70
70
|
expect(r.stdout).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
71
71
|
});
|
|
72
72
|
});
|
|
@@ -0,0 +1,134 @@
|
|
|
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 os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const gate_1 = require("../../../src/commands/job/gate");
|
|
10
|
+
const query_1 = require("../../../src/commands/job/query");
|
|
11
|
+
const store_1 = require("../../../src/core/journal/store");
|
|
12
|
+
const paths_1 = require("../../../src/core/journal/paths");
|
|
13
|
+
const types_1 = require("../../../src/core/journal/types");
|
|
14
|
+
// #173. A journal that was never created is a different FACT from a damaged
|
|
15
|
+
// one, and needs a different response: absence is resolved by
|
|
16
|
+
// `awm watch --init --plan`, corruption by inspecting the file. `readJournal`
|
|
17
|
+
// collapsed both into `corrupt: true`, so a healthy repository with no journal
|
|
18
|
+
// told the operator its state file was unreadable.
|
|
19
|
+
//
|
|
20
|
+
// These are two separate cases with two separate assertions on purpose: a single
|
|
21
|
+
// test on "not pass" would not distinguish them, which is how this survived.
|
|
22
|
+
const BRANCH = 'main';
|
|
23
|
+
describe('an absent journal is not a corrupt one', () => {
|
|
24
|
+
let repo;
|
|
25
|
+
let previous;
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-173-'));
|
|
28
|
+
previous = { home: process.env.HOME, awmHome: process.env.AWM_HOME };
|
|
29
|
+
process.env.HOME = path_1.default.join(repo, 'operator-home');
|
|
30
|
+
process.env.AWM_HOME = path_1.default.join(repo, 'awm-home');
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
for (const [key, value] of [['HOME', previous.home], ['AWM_HOME', previous.awmHome]]) {
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
delete process.env[key];
|
|
36
|
+
else
|
|
37
|
+
process.env[key] = value;
|
|
38
|
+
}
|
|
39
|
+
fs_1.default.rmSync(repo, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
function writeState(body) {
|
|
42
|
+
fs_1.default.mkdirSync((0, paths_1.journalDir)(repo, BRANCH), { recursive: true });
|
|
43
|
+
fs_1.default.writeFileSync((0, paths_1.statePath)(repo, BRANCH), body);
|
|
44
|
+
}
|
|
45
|
+
it('reports a repository that never had a journal as absent, never corrupt', () => {
|
|
46
|
+
const read = (0, store_1.readJournal)(repo, BRANCH);
|
|
47
|
+
expect(read).toEqual({ state: null, corrupt: false, absent: true });
|
|
48
|
+
expect((0, store_1.journalPresence)(read)).toBe('absent');
|
|
49
|
+
});
|
|
50
|
+
it.each([
|
|
51
|
+
['truncated JSON', '{"schema":2,"branch":"'],
|
|
52
|
+
['well-formed JSON of the wrong shape', '{"nope":1}'],
|
|
53
|
+
['an empty file', ''],
|
|
54
|
+
])('still reports %s as corrupt, not absent', (_case, body) => {
|
|
55
|
+
writeState(body);
|
|
56
|
+
const read = (0, store_1.readJournal)(repo, BRANCH);
|
|
57
|
+
expect(read.corrupt).toBe(true);
|
|
58
|
+
expect(read.absent).toBe(false);
|
|
59
|
+
expect((0, store_1.journalPresence)(read)).toBe('corrupt');
|
|
60
|
+
});
|
|
61
|
+
// The adversarial case this change had to survive: `readFileSync` on a
|
|
62
|
+
// dangling symlink raises ENOENT, so a naive ENOENT check would report an
|
|
63
|
+
// attacker-placed link as an innocent absence. The symlink assertion runs
|
|
64
|
+
// first and throws without an errno, which is what keeps that impossible.
|
|
65
|
+
it('never reports a symlinked state file as absence, even a dangling one', () => {
|
|
66
|
+
fs_1.default.mkdirSync((0, paths_1.journalDir)(repo, BRANCH), { recursive: true });
|
|
67
|
+
fs_1.default.symlinkSync(path_1.default.join(repo, 'no-such-target'), (0, paths_1.statePath)(repo, BRANCH));
|
|
68
|
+
const read = (0, store_1.readJournal)(repo, BRANCH);
|
|
69
|
+
expect(read.corrupt).toBe(true);
|
|
70
|
+
expect(read.absent).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
it('never reports a symlinked journal directory as absence', () => {
|
|
73
|
+
fs_1.default.mkdirSync(path_1.default.join(repo, 'elsewhere'), { recursive: true });
|
|
74
|
+
fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
|
|
75
|
+
fs_1.default.symlinkSync(path_1.default.join(repo, 'elsewhere'), path_1.default.join(repo, '.awm', 'journal'));
|
|
76
|
+
const read = (0, store_1.readJournal)(repo, BRANCH);
|
|
77
|
+
expect(read.corrupt).toBe(true);
|
|
78
|
+
expect(read.absent).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
// Absence used to arrive at writeJournal as `corrupt`, and that is what kept
|
|
81
|
+
// journal creation exclusive to initJournal/initBoundJournal. Naming it must
|
|
82
|
+
// not have loosened that.
|
|
83
|
+
it('still refuses to create a journal through the write path', () => {
|
|
84
|
+
expect(() => (0, store_1.writeJournal)(repo, BRANCH, (0, types_1.emptyState)(BRANCH)))
|
|
85
|
+
.toThrow(/journal inexistente: la creacion es de initJournal/);
|
|
86
|
+
expect(fs_1.default.existsSync((0, paths_1.statePath)(repo, BRANCH))).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
it('reports absence through the job queries without dereferencing a null state', () => {
|
|
89
|
+
expect((0, query_1.queryPs)(repo, BRANCH)).toEqual({ corruptState: false, journal: 'absent', jobs: [] });
|
|
90
|
+
expect((0, query_1.queryList)(repo, BRANCH)).toEqual({ corruptState: false, journal: 'absent', cycleStatus: null, jobs: [] });
|
|
91
|
+
expect((0, query_1.queryShow)(repo, BRANCH, 'j1')).toEqual({ corruptState: false, journal: 'absent', job: null });
|
|
92
|
+
});
|
|
93
|
+
it('reports corruption through the job queries with corruptState still true', () => {
|
|
94
|
+
writeState('{"schema":2,"branch":"');
|
|
95
|
+
expect((0, query_1.queryPs)(repo, BRANCH)).toEqual({ corruptState: true, journal: 'corrupt', jobs: [] });
|
|
96
|
+
expect((0, query_1.queryList)(repo, BRANCH)).toEqual({ corruptState: true, journal: 'corrupt', cycleStatus: null, jobs: [] });
|
|
97
|
+
expect((0, query_1.queryShow)(repo, BRANCH, 'j1')).toEqual({ corruptState: true, journal: 'corrupt', job: null });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe('the gates name absence and still fail closed', () => {
|
|
101
|
+
const fingerprint = () => 'fp';
|
|
102
|
+
it('reports an absent journal as absent, and names the command that creates one', () => {
|
|
103
|
+
const gate = (0, gate_1.computeGate)(null, false, fingerprint, true);
|
|
104
|
+
expect(gate.pass).toBe(false);
|
|
105
|
+
expect(gate.reasons).toEqual([{ category: 'absent', detail: store_1.ABSENT_JOURNAL_DETAIL }]);
|
|
106
|
+
expect(gate.reasons[0].detail).toContain('awm watch --init --plan');
|
|
107
|
+
// The interlock has no plan binding to read an execution mode from when
|
|
108
|
+
// the journal is missing, so it does not certify on absence either.
|
|
109
|
+
expect(gate.reasons[0].detail).toContain('interactivo');
|
|
110
|
+
});
|
|
111
|
+
it('reports a corrupt journal as corrupt even when absence is also claimed', () => {
|
|
112
|
+
expect((0, gate_1.computeGate)(null, true, fingerprint, true).reasons)
|
|
113
|
+
.toEqual([expect.objectContaining({ category: 'corrupt' })]);
|
|
114
|
+
});
|
|
115
|
+
// Proves the 60 existing call sites are unchanged: without the new argument a
|
|
116
|
+
// null state still fails closed exactly as before.
|
|
117
|
+
it('keeps every caller that omits the new argument failing closed as corrupt', () => {
|
|
118
|
+
expect((0, gate_1.computeGate)(null, false, fingerprint).pass).toBe(false);
|
|
119
|
+
expect((0, gate_1.computeGate)(null, false, fingerprint).reasons)
|
|
120
|
+
.toEqual([expect.objectContaining({ category: 'corrupt' })]);
|
|
121
|
+
});
|
|
122
|
+
it('distinguishes the same two facts for a track gate', () => {
|
|
123
|
+
expect((0, gate_1.computeTrackGate)(null, false, fingerprint, true).reasons)
|
|
124
|
+
.toEqual([{ category: 'absent-state', detail: store_1.ABSENT_JOURNAL_DETAIL }]);
|
|
125
|
+
expect((0, gate_1.computeTrackGate)(null, true, fingerprint, true).reasons)
|
|
126
|
+
.toEqual([expect.objectContaining({ category: 'corrupt-state' })]);
|
|
127
|
+
expect((0, gate_1.computeTrackGate)(null, false, fingerprint).reasons)
|
|
128
|
+
.toEqual([expect.objectContaining({ category: 'corrupt-state' })]);
|
|
129
|
+
});
|
|
130
|
+
it('does not report absence for a journal that is present', () => {
|
|
131
|
+
const gate = (0, gate_1.computeGate)((0, types_1.emptyState)('main'), false, fingerprint, false);
|
|
132
|
+
expect(gate.reasons).not.toContainEqual(expect.objectContaining({ category: 'absent' }));
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -180,7 +180,7 @@ describe('multi-agent targeting (add/remove/sync/update/doctor)', () => {
|
|
|
180
180
|
fs_1.default.writeFileSync(path_1.default.join(process.env.AWM_HOME, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.com/baseline.git' }], null, 2));
|
|
181
181
|
const { runUpdateCore } = require('../../src/commands/update');
|
|
182
182
|
const syncRegistries = jest.fn(async () => [SYNCED_REGISTRY]);
|
|
183
|
-
const planReconciliation = jest.fn((
|
|
183
|
+
const planReconciliation = jest.fn((_operations) => ({ operations: [], records: [], reports: [] }));
|
|
184
184
|
const applyInstallPlan = jest.fn(() => ({ installed: [], skipped: [], transactionId: 'tx', modifiedFiles: [] }));
|
|
185
185
|
const resyncInstalledHooks = jest.fn(() => []);
|
|
186
186
|
const outcome = await runUpdateCore({}, {
|
|
@@ -25,7 +25,6 @@ jest.mock('../../../src/commands/sensors/run', () => ({
|
|
|
25
25
|
jest.mock('../../../src/core/currentness/check', () => ({
|
|
26
26
|
checkCurrentness: jest.fn(),
|
|
27
27
|
}));
|
|
28
|
-
const mockExecSync = child_process_1.execSync;
|
|
29
28
|
const mockRunSensors = run_1.runSensors;
|
|
30
29
|
const mockCheckCurrentness = check_1.checkCurrentness;
|
|
31
30
|
/** Turn a tmpdir into a real git repo with (optionally) an `origin` remote. */
|
|
@@ -79,7 +79,7 @@ describe('awm process show (texto)', () => {
|
|
|
79
79
|
});
|
|
80
80
|
const r = (0, process_1.runProcessShow)({ models: [hostile], diagnostics: [] }, 'mi-proceso', false);
|
|
81
81
|
expect(r.code).toBe(0);
|
|
82
|
-
//
|
|
82
|
+
// Necesitamos verificar la ausencia deliberada de C0.
|
|
83
83
|
expect(r.stdout).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
84
84
|
expect(r.stdout).toContain('Objetivo[31mmalicioso[0m.');
|
|
85
85
|
expect(r.stdout).toContain('Uno]0;pwned');
|
|
@@ -83,7 +83,7 @@ describe('aggregateTrackStatus — agregado read-only (R9.5, R9.6)', () => {
|
|
|
83
83
|
expect(currentPlanState.tracks[0]).not.toHaveProperty('gate');
|
|
84
84
|
expect(fs_1.default.readFileSync((0, paths_1.statePath)(planRoot, planBranch), 'utf8')).toBe(planBefore);
|
|
85
85
|
});
|
|
86
|
-
test('journal de track
|
|
86
|
+
test('journal de track ausente nunca se descarta en silencio: gate rojo con absent-state', () => {
|
|
87
87
|
// ni siquiera se inicializa el journal del segundo track: readJournal
|
|
88
88
|
// sobre un directorio sin state.json => corrupt:true (R1.6).
|
|
89
89
|
const missingTrackRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-missing-'));
|
|
@@ -93,7 +93,10 @@ describe('aggregateTrackStatus — agregado read-only (R9.5, R9.6)', () => {
|
|
|
93
93
|
(0, store_1.writeJournal)(planRoot, planBranch, planState);
|
|
94
94
|
const out = (0, status_1.aggregateTrackStatus)(planRoot, (0, store_1.readJournal)(planRoot, planBranch).state);
|
|
95
95
|
expect(out.tracks.ghost.gate.pass).toBe(false);
|
|
96
|
-
|
|
96
|
+
// #173: the track journal here is ABSENT, not damaged. The gate is
|
|
97
|
+
// still red — it is never discarded in silence — but it now names the
|
|
98
|
+
// fact it observed instead of claiming corruption.
|
|
99
|
+
expect(out.tracks.ghost.gate.reasons).toEqual([{ category: 'absent-state', detail: expect.any(String) }]);
|
|
97
100
|
}
|
|
98
101
|
finally {
|
|
99
102
|
fs_1.default.rmSync(missingTrackRoot, { recursive: true, force: true });
|
|
@@ -25,9 +25,6 @@ class ExitSignal extends Error {
|
|
|
25
25
|
this.code = code;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
function git(repo, args) {
|
|
29
|
-
return (0, child_process_1.execFileSync)('git', args, { cwd: repo, encoding: 'utf8' }).trim();
|
|
30
|
-
}
|
|
31
28
|
function gitInit(repo, branch) {
|
|
32
29
|
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'init', '-q', '-b', branch], { cwd: repo });
|
|
33
30
|
fs_1.default.writeFileSync(path_1.default.join(repo, 'f.txt'), 'x');
|
|
@@ -126,7 +126,7 @@ describe('aplicacion transaccional de requests', () => {
|
|
|
126
126
|
expect(Object.keys((0, store_1.readJournal)(repo, 'rama').state.jobs)).toHaveLength(1); // sin duplicados
|
|
127
127
|
});
|
|
128
128
|
test('register-entity CREA task con VerificationPlan y ReviewObligations; cycle-plan y dispatch (R1.4/R1.4b)', () => {
|
|
129
|
-
|
|
129
|
+
(0, requests_1.emitRequest)(repo, 'rama', {
|
|
130
130
|
kind: 'register-entity', generationToken: 'g1', idempotencyKey: 'e1',
|
|
131
131
|
payload: {
|
|
132
132
|
entity: 'task', taskId: 'T1', title: 'implementar',
|
|
@@ -180,7 +180,10 @@ describe('watch --init: plan-vs-repo mecanico', () => {
|
|
|
180
180
|
test('watch --init --plan bloquea un plan no valido antes de crear journal', () => {
|
|
181
181
|
const invalid = { state: 'invalid', diagnostics: [{ code: 'PLAN_SHAPE', message: 'bad' }] };
|
|
182
182
|
expect(() => (0, init_1.initWatch)(repo, 'rama', { path: 'docs/plan.md', report: invalid })).toThrow(/válido/i);
|
|
183
|
-
|
|
183
|
+
// #173: this asserted `.corrupt` as a proxy for "no journal was created",
|
|
184
|
+
// which is the exact confusion that issue is about. The fact it means is
|
|
185
|
+
// absence, and that is now what it says.
|
|
186
|
+
expect((0, store_1.readJournal)(repo, 'rama')).toMatchObject({ state: null, corrupt: false, absent: true });
|
|
184
187
|
});
|
|
185
188
|
test('watch --init --plan rechaza un plan interactivo antes de crear un binding desatendido', () => {
|
|
186
189
|
const interactive = { ...validPlan('a'), executionMode: 'interactivo' };
|
|
@@ -201,7 +201,7 @@ describe('composedOrchestrators', () => {
|
|
|
201
201
|
{ name: 'a\x1bx', appliesWhen: 'w\x07', terminatesTo: 't\x00' },
|
|
202
202
|
]);
|
|
203
203
|
expect(out[0].name).toBe('ax');
|
|
204
|
-
//
|
|
204
|
+
// Verificamos la ausencia deliberada de C0.
|
|
205
205
|
const controlCharPattern = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/;
|
|
206
206
|
expect(out[0].name).not.toMatch(controlCharPattern);
|
|
207
207
|
expect(out[0].appliesWhen).not.toMatch(controlCharPattern);
|
|
@@ -216,7 +216,7 @@ describe('composedOrchestrators', () => {
|
|
|
216
216
|
{ name: 'evil\x1b[31m', appliesWhen: 'w', terminatesTo: 't' },
|
|
217
217
|
],
|
|
218
218
|
});
|
|
219
|
-
//
|
|
219
|
+
// Verificamos la ausencia deliberada de C0.
|
|
220
220
|
expect(ctx.markdown).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
221
221
|
});
|
|
222
222
|
it('lo que renderiza el payload sale de esta misma funcion', () => {
|
|
@@ -47,7 +47,7 @@ describe('ConfigInstructionsStrategy.inject', () => {
|
|
|
47
47
|
});
|
|
48
48
|
describe('ConfigInstructionsStrategy.remove', () => {
|
|
49
49
|
it('removes only the sentinel, preserving user entries', () => {
|
|
50
|
-
const { configPath,
|
|
50
|
+
const { configPath, provider, input } = setup({ instructions: ['docs/rules.md'] });
|
|
51
51
|
strat.inject(input, provider);
|
|
52
52
|
strat.remove(input, provider);
|
|
53
53
|
const cfg = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
@@ -26,7 +26,7 @@ describe('renderFullTerminal', () => {
|
|
|
26
26
|
it('renders all lifecycle sections in canonical order with status and remediation', () => {
|
|
27
27
|
const output = (0, render_terminal_1.renderFullTerminal)((0, validate_1.validateDashboardSnapshotV1)(completeSnapshot()));
|
|
28
28
|
const headings = ['Machine / install', 'Project readiness', 'Design / planning', 'Execution', 'QA', 'Docs', 'Retro', 'Final / history', 'Processes'];
|
|
29
|
-
expect(headings.map((heading) => output.indexOf(heading))).toEqual(
|
|
29
|
+
expect(headings.map((heading) => output.indexOf(heading))).toEqual(headings.map(() => expect.any(Number)));
|
|
30
30
|
for (let index = 1; index < headings.length; index++)
|
|
31
31
|
expect(output.indexOf(headings[index])).toBeGreaterThan(output.indexOf(headings[index - 1]));
|
|
32
32
|
expect(output).toContain('⚠ Sensors [project.sensors] — 2 stale');
|
|
@@ -191,7 +191,6 @@ describe('gatherProviderChecks — shared skills.global does not mask broken lin
|
|
|
191
191
|
});
|
|
192
192
|
it('still reports shared (healthy, non-degrading) when the shared dir has no broken links', () => {
|
|
193
193
|
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
194
|
-
const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
|
|
195
194
|
const scanSkills = jest.fn(() => ({ valid: ['ok-skill'], repairable: [], dead: [], usurped: [] }));
|
|
196
195
|
const ctx = gatherContext({ cwd: tmpHome, bundles: [], agents: ['opencode', 'codex'], scanSkills });
|
|
197
196
|
for (const provider of ctx.providers) {
|
|
@@ -14,7 +14,6 @@ const SKILLS_ROOT = CONTENT_ROOT;
|
|
|
14
14
|
const WORKFLOWS_ROOT = CONTENT_ROOT;
|
|
15
15
|
// For backward-compat with mock expectations that check SKILLS_DIR-style paths:
|
|
16
16
|
const SKILLS_DIR = path_1.default.join(CONTENT_ROOT, 'skills');
|
|
17
|
-
const WORKFLOWS_DIR = path_1.default.join(CONTENT_ROOT, 'workflows');
|
|
18
17
|
const directoryStat = { dev: 1n, ino: 3n, isDirectory: () => true, isSymbolicLink: () => false };
|
|
19
18
|
const skillStat = { dev: 1n, ino: 2n, size: 0n, isFile: () => true, isSymbolicLink: () => false };
|
|
20
19
|
describe('Artifact Discovery', () => {
|
|
@@ -62,7 +62,7 @@ describe('runInitSteps — orchestrator', () => {
|
|
|
62
62
|
function buildDeps(cwd) {
|
|
63
63
|
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
64
64
|
const { discoverAllBundles } = require('../../../src/core/bundles');
|
|
65
|
-
const { contentRoots
|
|
65
|
+
const { contentRoots } = require('../../../src/core/registries');
|
|
66
66
|
const { defaultActions } = require('../../../src/core/init/steps');
|
|
67
67
|
// Seed content at ~/.awm/registries/baseline (content root IS the registry root).
|
|
68
68
|
const contentRoot = path_1.default.join(process.env.AWM_HOME, 'registries', 'baseline');
|
|
@@ -222,7 +222,7 @@ describe('process identity', () => {
|
|
|
222
222
|
});
|
|
223
223
|
test('captureRefFor degrada a unknown si ps falla, nunca crashea al capturar identidad (R2.1)', () => {
|
|
224
224
|
const cp = require('child_process');
|
|
225
|
-
const spy = jest.spyOn(cp, 'execFileSync').mockImplementation((...
|
|
225
|
+
const spy = jest.spyOn(cp, 'execFileSync').mockImplementation((..._args) => {
|
|
226
226
|
const err = new Error('spawn ps ENOENT');
|
|
227
227
|
err.code = 'ENOENT';
|
|
228
228
|
throw err;
|
|
@@ -197,7 +197,7 @@ describe('readDeclaredOrchestrators', () => {
|
|
|
197
197
|
stderrBoundary(result.diagnostics);
|
|
198
198
|
expect(stderrBoundary.mock.calls[0][0].join('')).not.toMatch(/[\u2028\u0085]/);
|
|
199
199
|
expect(stderrBoundary.mock.results[0].value).not.toMatch(/[\u2028\u0085]/);
|
|
200
|
-
//
|
|
200
|
+
// Reject all terminal controls and line separators.
|
|
201
201
|
expect(result.diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
|
|
202
202
|
});
|
|
203
203
|
it.each(['inspection', 'read', 'parse', 'shape', 'fields'])('sanitizes %s diagnostics, including paths and error text', (branch) => {
|
|
@@ -224,7 +224,7 @@ describe('readDeclaredOrchestrators', () => {
|
|
|
224
224
|
expect(result.diagnostics[0]).toContain('awm-registry.json');
|
|
225
225
|
expect(result.diagnostics[0]).toContain('before');
|
|
226
226
|
expect(result.diagnostics[0]).toContain('after');
|
|
227
|
-
//
|
|
227
|
+
// Reject all terminal controls and line separators.
|
|
228
228
|
expect(result.diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
|
|
229
229
|
});
|
|
230
230
|
it('preserves valid raw fields and structural parsing without skill discovery', () => {
|
|
@@ -290,7 +290,7 @@ describe('collectDeclaredOrchestrators', () => {
|
|
|
290
290
|
writeSkill('first', 'real-skill');
|
|
291
291
|
const { diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
292
292
|
expect(diagnostics).toHaveLength(1);
|
|
293
|
-
//
|
|
293
|
+
// Diagnostic must contain no terminal controls or line separators.
|
|
294
294
|
expect(diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
|
|
295
295
|
expect(diagnostics[0]).toContain('shared');
|
|
296
296
|
expect(diagnostics[0]).toContain('forged');
|
|
@@ -304,7 +304,7 @@ describe('collectDeclaredOrchestrators', () => {
|
|
|
304
304
|
expect(diagnostics).toHaveLength(1);
|
|
305
305
|
expect(diagnostics[0]).toContain('discovery unavailable');
|
|
306
306
|
expect(diagnostics[0]).toContain('forged');
|
|
307
|
-
//
|
|
307
|
+
// Diagnostic must contain no terminal controls or line separators.
|
|
308
308
|
expect(diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
|
|
309
309
|
});
|
|
310
310
|
it('excludes a later colliding root without losing earlier or subsequent healthy roots', () => {
|
|
@@ -676,7 +676,6 @@ windowsOnly('native secure-fs Windows handle fixtures', () => {
|
|
|
676
676
|
const junction = path_1.default.join(root, 'junction');
|
|
677
677
|
fs_1.default.mkdirSync(outside);
|
|
678
678
|
fs_1.default.symlinkSync(outside, junction, 'junction');
|
|
679
|
-
const target = path_1.default.join(junction, 'sensors.json');
|
|
680
679
|
expect(() => binding.writeProjectTransaction(root, 'junction/sensors.json', Buffer.from('new bytes'), { mode: 'create', createParents: false })).toThrow(/rejected path ancestor/i);
|
|
681
680
|
expect(fs_1.default.existsSync(path_1.default.join(outside, 'sensors.json'))).toBe(false);
|
|
682
681
|
});
|
|
@@ -28,7 +28,7 @@ describe('sanitizeDeclaredField', () => {
|
|
|
28
28
|
expect((0, text_1.sanitizeDeclaredField)('a\n## Forjado *b* `c` <d>')).toBe('a Forjado b c d');
|
|
29
29
|
});
|
|
30
30
|
it('no deja ESC ni otros C0 sobrevivir aunque el saneo markdown no los toque', () => {
|
|
31
|
-
//
|
|
31
|
+
// Verificamos la ausencia deliberada de C0.
|
|
32
32
|
expect((0, text_1.sanitizeDeclaredField)('name\x1b[31mred\x07')).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
33
33
|
});
|
|
34
34
|
it('es idempotente sobre texto ya limpio', () => {
|
|
@@ -86,7 +86,7 @@ describe('directory fsync has exactly one implementation', () => {
|
|
|
86
86
|
];
|
|
87
87
|
expect(rejects(regression, 1)).toBe(true);
|
|
88
88
|
// The sanctioned replacement carries no directory open at all.
|
|
89
|
-
expect(
|
|
89
|
+
expect(rejects(['fsyncDirSync(path.dirname(file));'], 0)).toBe(false);
|
|
90
90
|
// Flushing a FILE descriptor stays legal — that is a real, portable fsync.
|
|
91
91
|
const fileFlush = ["const fd = fs.openSync(temporary, 'wx', 0o600);", 'fs.fsyncSync(fd);'];
|
|
92
92
|
expect(rejects(fileFlush, 0)).toBe(false);
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const child_process_1 = require("child_process");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
8
|
const path_1 = __importDefault(require("path"));
|
|
8
9
|
// #171: the lint gate linted 14 files — all js/mjs — and exited 0 on a
|
|
9
10
|
// TypeScript CLI. `eslint.config.awm.mjs` imports a project config that does
|
|
@@ -23,12 +24,37 @@ const CLI_ROOT = path_1.default.resolve(__dirname, '../..');
|
|
|
23
24
|
* repo already documents in tests/integration/published-doctor-evidence.e2e.test.ts,
|
|
24
25
|
* and the one this test hit on both Windows legs of its first CI run. Going
|
|
25
26
|
* through `process.execPath` needs no shell, so no argument escaping either. */
|
|
26
|
-
function lint() {
|
|
27
|
-
const
|
|
27
|
+
function lint(target = '.') {
|
|
28
|
+
const args = [
|
|
28
29
|
path_1.default.join(CLI_ROOT, 'node_modules', 'eslint', 'bin', 'eslint.js'),
|
|
29
|
-
|
|
30
|
-
]
|
|
31
|
-
|
|
30
|
+
target, '--config', 'eslint.config.awm.mjs', '--format', 'json',
|
|
31
|
+
];
|
|
32
|
+
try {
|
|
33
|
+
const raw = (0, child_process_1.execFileSync)(process.execPath, args, { cwd: CLI_ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
34
|
+
return JSON.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
// eslint exits non-zero when it reports an error, and the JSON report is
|
|
38
|
+
// still on stdout. A probe that EXPECTS a finding must read it, not die.
|
|
39
|
+
const stdout = error.stdout;
|
|
40
|
+
if (typeof stdout !== 'string' || stdout.trim() === '')
|
|
41
|
+
throw error;
|
|
42
|
+
return JSON.parse(stdout);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Lint one throwaway `.ts` file inside the repo, so the real config's `.ts`
|
|
46
|
+
* block applies to it, and report what the gate said about it. */
|
|
47
|
+
function probe(body) {
|
|
48
|
+
const directory = fs_1.default.mkdtempSync(path_1.default.join(CLI_ROOT, 'tests', '.lint-probe-'));
|
|
49
|
+
const file = path_1.default.join(directory, 'probe.ts');
|
|
50
|
+
try {
|
|
51
|
+
fs_1.default.writeFileSync(file, body);
|
|
52
|
+
const results = lint(path_1.default.relative(CLI_ROOT, file).split(path_1.default.sep).join('/'));
|
|
53
|
+
return results.flatMap(entry => entry.messages);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
fs_1.default.rmSync(directory, { recursive: true, force: true });
|
|
57
|
+
}
|
|
32
58
|
}
|
|
33
59
|
describe('the lint gate covers TypeScript (#171)', () => {
|
|
34
60
|
jest.setTimeout(10 * 60_000);
|
|
@@ -53,4 +79,37 @@ describe('the lint gate covers TypeScript (#171)', () => {
|
|
|
53
79
|
.map(m => `${entry.filePath}: ${m.message}`));
|
|
54
80
|
expect(unresolved).toEqual([]);
|
|
55
81
|
});
|
|
82
|
+
// The gate examining TypeScript is worth nothing if it cannot fail on it.
|
|
83
|
+
// #171 left the rule at 'warn' with a backlog behind it; these assert the
|
|
84
|
+
// gate now BLOCKS, and that the tree it blocks on is clean.
|
|
85
|
+
test('the tree it lints is clean, with nothing left to report', () => {
|
|
86
|
+
const findings = results.flatMap(entry => entry.messages.map(m => `${entry.filePath}: ${m.ruleId ?? 'directive'} ${m.message}`));
|
|
87
|
+
expect(findings).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
test('an unused variable in TypeScript is an error, not a warning', () => {
|
|
90
|
+
const messages = probe('const neverUsed = 1;\nexport const used = 2;\n');
|
|
91
|
+
const unused = messages.filter(m => m.ruleId === '@typescript-eslint/no-unused-vars');
|
|
92
|
+
expect(unused).toHaveLength(1);
|
|
93
|
+
expect(unused[0].severity).toBe(2);
|
|
94
|
+
expect(unused[0].message).toContain('neverUsed');
|
|
95
|
+
});
|
|
96
|
+
test('an underscore-prefixed unused binding stays deliberate and unreported', () => {
|
|
97
|
+
expect(probe('const _deliberatelyUnused = 1;\nexport const used = 2;\n')).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
// The reason the rule had to become the TypeScript-aware one rather than the
|
|
100
|
+
// base rule: eslint's own `no-unused-vars` does not understand type
|
|
101
|
+
// annotations, so it flagged every parameter NAME inside a function TYPE.
|
|
102
|
+
// That was 195 of the 259 findings #171 exposed. Renaming those to `_x` would
|
|
103
|
+
// have made the types less readable to silence noise.
|
|
104
|
+
test('parameter names inside a function type are not reported as unused', () => {
|
|
105
|
+
const messages = probe('export type Fingerprint = (argv: string[], paths: string[], cwd: string) => string | null;\n');
|
|
106
|
+
expect(messages).toEqual([]);
|
|
107
|
+
});
|
|
108
|
+
test('a caught error that is never inspected is reported too', () => {
|
|
109
|
+
const messages = probe('export function f(value: number): number {\n try { return value; } catch (error) { return 0; }\n}\n');
|
|
110
|
+
const unused = messages.filter(m => m.ruleId === '@typescript-eslint/no-unused-vars');
|
|
111
|
+
expect(unused).toHaveLength(1);
|
|
112
|
+
expect(unused[0].severity).toBe(2);
|
|
113
|
+
expect(unused[0].message).toContain('error');
|
|
114
|
+
});
|
|
56
115
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-workflow-manager",
|
|
3
|
-
"version": "9.11.
|
|
3
|
+
"version": "9.11.5",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"awm": "./dist/src/index.js"
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"@types/js-yaml": "4.0.9",
|
|
60
60
|
"@types/node": "^25.3.0",
|
|
61
61
|
"@types/semver": "^7.7.1",
|
|
62
|
+
"@typescript-eslint/eslint-plugin": "^8.70.0",
|
|
62
63
|
"@typescript-eslint/parser": "^8.70.0",
|
|
63
64
|
"dependency-cruiser": "16.10.4",
|
|
64
65
|
"eslint": "10.8.1",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|