agentic-workflow-manager 9.13.1 → 9.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/watch/supervisor.js +25 -11
- package/dist/src/core/admission/index.js +8 -2
- package/dist/tests/commands/plan/index.test.js +6 -2
- package/dist/tests/commands/watch/supervisor-loop.test.js +82 -0
- package/dist/tests/core/admission/index.test.js +34 -6
- package/package.json +1 -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
|
@@ -245,14 +245,18 @@ function recoveryWhitelistBlocker(state) {
|
|
|
245
245
|
// Pending work is the normal reason to launch (or retry launching) the
|
|
246
246
|
// controller. It is not recovery evidence and therefore cannot turn a
|
|
247
247
|
// transient launch failure into permanent custody. Conversely, once a
|
|
248
|
-
// non-empty task set claims completion,
|
|
249
|
-
//
|
|
248
|
+
// non-empty task set claims completion, a required verifier that no plan
|
|
249
|
+
// contains, or a task-level item left unsatisfied, is an unsafe recovery
|
|
250
|
+
// fact and must remain fail-closed. A cycle-level item that was planned
|
|
251
|
+
// but never requested is not: it is the pending closure a relaunched
|
|
252
|
+
// controller exists to perform (#196, v1 smoke), and the gate still
|
|
253
|
+
// demands its evidence before COMPLETE.
|
|
250
254
|
if (state.tasks.length === 0 || state.tasks.some(task => task.status !== 'done'))
|
|
251
255
|
return undefined;
|
|
252
256
|
const verificationItems = [...state.cycleVerificationPlan, ...state.tasks.flatMap(task => task.verificationPlan)];
|
|
253
257
|
for (const required of state.requiredVerifiers) {
|
|
254
258
|
const requiredItems = verificationItems.filter(item => item.kind === required);
|
|
255
|
-
if (requiredItems.length === 0 || requiredItems.some(item => item.satisfiedBy === undefined))
|
|
259
|
+
if (requiredItems.length === 0 || requiredItems.some(item => item.satisfiedBy === undefined && !state.cycleVerificationPlan.includes(item)))
|
|
256
260
|
return `falta evidencia del verificador requerido: ${required}`;
|
|
257
261
|
}
|
|
258
262
|
return undefined;
|
|
@@ -483,18 +487,23 @@ class Supervisor {
|
|
|
483
487
|
const job = before0.state.jobs[id];
|
|
484
488
|
return job !== undefined && job.verdict === 'pass' && !staleJob;
|
|
485
489
|
};
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
|
|
490
|
+
const cycleItems = before0.state.cycleVerificationPlan;
|
|
491
|
+
const verificationItems = [...cycleItems, ...before0.state.tasks.flatMap(task => task.verificationPlan)];
|
|
492
|
+
// Planned cycle items never requested are closure work, not
|
|
493
|
+
// evidence; every requested or task-level item is still judged.
|
|
494
|
+
const judged = (items) => items.filter(item => item.satisfiedBy !== undefined || !cycleItems.includes(item));
|
|
495
|
+
const tests = judged(verificationItems.filter(item => item.kind === 'test'));
|
|
496
|
+
const sensorItems = judged(verificationItems.filter(item => item.kind === 'sensors'));
|
|
489
497
|
// `computeGate` is the single authority for every verification
|
|
490
498
|
// kind (review, QA and interlock included). Reuse its evidence
|
|
491
499
|
// semantics instead of maintaining a weaker recovery subset.
|
|
492
500
|
const evidenceGate = (0, gate_1.computeGate)(before0.state, false, this.fingerprintNow);
|
|
493
501
|
const terminalTaskClaims = before0.state.tasks.length > 0 && before0.state.tasks.every(task => task.status === 'done');
|
|
494
|
-
const
|
|
495
|
-
|
|
502
|
+
const closurePending = terminalTaskClaims && cycleItems.some(item => item.satisfiedBy === undefined);
|
|
503
|
+
const unresolvedVerification = terminalTaskClaims && (evidenceGate.reasons.some(reason => [
|
|
504
|
+
'dangling-reference', 'adverse-verdict',
|
|
496
505
|
'stale-fingerprint', 'open-obligation', 'open-fix',
|
|
497
|
-
].includes(reason.category));
|
|
506
|
+
].includes(reason.category)) || verificationItems.some(item => item.satisfiedBy === undefined && !cycleItems.includes(item)));
|
|
498
507
|
const hasStaleReview = before0.state.verdicts.some(verdict => verdict.fingerprint === '' || (verdict.argv.length > 0 && (() => {
|
|
499
508
|
try {
|
|
500
509
|
return (0, fingerprint_1.computeFingerprint)(this.repoRoot, verdict.argv, verdict.paths, verdict.cwd).fingerprint !== verdict.fingerprint;
|
|
@@ -524,7 +533,7 @@ class Supervisor {
|
|
|
524
533
|
sensors: !terminalTaskClaims || sensorItems.length === 0 || sensorItems.every(item => passed(item.satisfiedBy)) ? 'pass' : 'missing',
|
|
525
534
|
verdicts: hasStaleReview ? 'stale' : (openReviewOrFix || unresolvedVerification) ? 'missing' : 'current',
|
|
526
535
|
});
|
|
527
|
-
(0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'unattended-recovery', nextAction: recovery.nextAction, activeJobIds: recovery.activeJobIds, diagnostics: recovery.diagnostics });
|
|
536
|
+
(0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'unattended-recovery', nextAction: recovery.nextAction, activeJobIds: recovery.activeJobIds, diagnostics: recovery.diagnostics, closurePending });
|
|
528
537
|
if (recovery.state !== 'ready') {
|
|
529
538
|
const staleReasons = terminalTaskClaims ? evidenceGate.reasons.filter(reason => reason.category === 'stale-fingerprint') : [];
|
|
530
539
|
const remedy = staleReasons.length > 0
|
|
@@ -533,7 +542,7 @@ class Supervisor {
|
|
|
533
542
|
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `recovery no autorizado: ${recovery.diagnostics.join(', ')}${remedy}`);
|
|
534
543
|
return 'custody';
|
|
535
544
|
}
|
|
536
|
-
recoveryResumeAction = { schema: 'controller-recovery/v1', kind: recovery.nextAction === 'reconcile-active-jobs' ? 'reconcile-active-jobs' : 'resume-next-action' };
|
|
545
|
+
recoveryResumeAction = { schema: 'controller-recovery/v1', kind: recovery.nextAction === 'reconcile-active-jobs' ? 'reconcile-active-jobs' : closurePending ? 'resume-cycle' : 'resume-next-action' };
|
|
537
546
|
}
|
|
538
547
|
// R6.2/R6.8/C7 (Task 11): reconciliar un `MERGE_HEAD` abierto por un
|
|
539
548
|
// crash a mitad de un merge ANTES de cualquier guard general — hoy
|
|
@@ -792,6 +801,11 @@ class Supervisor {
|
|
|
792
801
|
const decision = snap === null && safeToReplace === 'safe' ? 'resolve-generation' : (0, generations_1.decideStall)({ heartbeatAgeMs, activityFrozenMs, safeToReplace }, { heartbeatTimeoutMs: this.cfg.heartbeatTimeoutMs, activityWindowMs: this.cfg.activityWindowMs });
|
|
793
802
|
if (decision === 'healthy') {
|
|
794
803
|
this.backoff.reset();
|
|
804
|
+
if (gen.state === 'controller-suspected-stall') {
|
|
805
|
+
gen.state = 'active'; // heartbeat reanudado: la sospecha se levanta
|
|
806
|
+
(0, store_1.writeJournal)(this.repoRoot, this.branch, s);
|
|
807
|
+
(0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'controller-stall-cleared', n: gen.n });
|
|
808
|
+
}
|
|
795
809
|
return 'ok';
|
|
796
810
|
}
|
|
797
811
|
if (decision === 'suspected-stall-observe') {
|
|
@@ -233,8 +233,14 @@ async function admitPlan(input) {
|
|
|
233
233
|
return blocked(input, [diagnostic('ADMISSION_SENSORS_BLOCKED', `Sensor verdict is ${sensors}.`)], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors });
|
|
234
234
|
}
|
|
235
235
|
if (mode === 'desatendido') {
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
// The durable controller (journal + awm watch + jobs) is opt-in, as it
|
|
237
|
+
// was before 9.8.0: native v1 dispatch with no journal on its branch
|
|
238
|
+
// runs as one provider session. Creating a journal is the opt-in, and
|
|
239
|
+
// from then on it must be current. v2 routing custody lives in the
|
|
240
|
+
// journal, so awm-routed work still requires it.
|
|
241
|
+
const observed = journalStatus(input, plan);
|
|
242
|
+
const journal = observed === 'missing' && plan.schema === 'compact-slices/v1' ? 'not-required' : observed;
|
|
243
|
+
if (journal !== 'current' && journal !== 'not-required')
|
|
238
244
|
return blocked(input, [diagnostic('ADMISSION_JOURNAL_BINDING_REQUIRED', 'Unattended execution requires a healthy schema-2 journal binding for this exact plan; run watch --init --plan.')], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors: input.verifySensors ? 'pass' : 'not-required' });
|
|
239
245
|
const capabilities = unattendedCapabilities(provider, input.controllerAutonomy);
|
|
240
246
|
const resolution = { outcome: capabilities.unattendedController === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
|
|
@@ -309,7 +309,7 @@ describe('plan admit Commander wiring', () => {
|
|
|
309
309
|
process.exitCode = undefined;
|
|
310
310
|
}
|
|
311
311
|
});
|
|
312
|
-
it('derives desatendido from the canonical plan header
|
|
312
|
+
it('derives desatendido from the canonical plan header; a native v1 plan needs no journal (opt-in durable custody)', async () => {
|
|
313
313
|
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-header-'));
|
|
314
314
|
const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
315
315
|
try {
|
|
@@ -327,7 +327,11 @@ describe('plan admit Commander wiring', () => {
|
|
|
327
327
|
readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }),
|
|
328
328
|
});
|
|
329
329
|
await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', root, '--json']);
|
|
330
|
-
|
|
330
|
+
const withoutPosture = JSON.parse(String(output.mock.calls[0][0]));
|
|
331
|
+
expect(withoutPosture).toMatchObject({ state: 'blocked', executionMode: 'desatendido', journal: 'not-required' });
|
|
332
|
+
expect(withoutPosture.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
|
|
333
|
+
await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', root, '--controller-autonomy', 'approval-free', '--json']);
|
|
334
|
+
expect(JSON.parse(String(output.mock.calls[1][0]))).toMatchObject({ state: 'admitted', executionMode: 'desatendido', journal: 'not-required' });
|
|
331
335
|
}
|
|
332
336
|
finally {
|
|
333
337
|
output.mockRestore();
|
|
@@ -621,6 +621,88 @@ describe('supervisor loop', () => {
|
|
|
621
621
|
expect(spawns).toBe(0);
|
|
622
622
|
expect((0, store_1.readJournal)(repo, 'main').state.cycle.status).toBe('BLOCKED');
|
|
623
623
|
});
|
|
624
|
+
// v1 smoke 2026-09-26 (#196): the Codex controller exited after the last
|
|
625
|
+
// review, before requesting the cycle-level verifications it had planned.
|
|
626
|
+
// Tasks done + planned-but-never-requested cycle items is pending closure
|
|
627
|
+
// work, not an unsafe recovery claim: relaunch the controller to close.
|
|
628
|
+
function closurePendingState(repoRoot) {
|
|
629
|
+
initUnattendedFixture(repoRoot);
|
|
630
|
+
const state = (0, store_1.readJournal)(repoRoot, 'main').state;
|
|
631
|
+
const verdict = (id, obligationId) => {
|
|
632
|
+
const argv = ['awm-review', obligationId];
|
|
633
|
+
return { id, obligationId, result: 'pass', detail: 'ok', receivedAt: new Date().toISOString(),
|
|
634
|
+
fingerprint: (0, fingerprint_1.computeFingerprint)(repoRoot, argv, [], '.').fingerprint, argv, paths: [], cwd: '.' };
|
|
635
|
+
};
|
|
636
|
+
state.requiredVerifiers = ['test', 'sensors'];
|
|
637
|
+
state.tasks.push({ id: 'S1', title: 'terminada', status: 'done', attempts: 1, verificationPlan: [], reviewObligations: [
|
|
638
|
+
{ id: 'o-spec', taskId: 'S1', kind: 'spec', verdictId: 'v-spec' },
|
|
639
|
+
{ id: 'o-quality', taskId: 'S1', kind: 'quality', verdictId: 'v-quality' },
|
|
640
|
+
] });
|
|
641
|
+
state.verdicts.push(verdict('v-spec', 'o-spec'), verdict('v-quality', 'o-quality'));
|
|
642
|
+
state.cycleVerificationPlan.push({ id: 'cycle-test', kind: 'test' }, { id: 'cycle-sensors', kind: 'sensors' }, { id: 'cycle-qa', kind: 'qa' }, { id: 'cycle-interlock', kind: 'interlock' });
|
|
643
|
+
return state;
|
|
644
|
+
}
|
|
645
|
+
test('cierre pendiente: tareas terminadas + verificaciones de ciclo nunca pedidas => relanza el controller para cerrar, sin custodia', async () => {
|
|
646
|
+
const state = closurePendingState(repo);
|
|
647
|
+
(0, store_1.writeJournal)(repo, 'main', state);
|
|
648
|
+
(0, generations_1.beginGeneration)(repo, 'main'); // el reemplazo que el loop abre tras la salida del anterior
|
|
649
|
+
let spawns = 0;
|
|
650
|
+
const outcome = await new supervisor_1.Supervisor(repo, 'main', supervisor_1.DEFAULT_SUPERVISOR_CONFIG, () => { spawns++; }, undefined, admitted).tick();
|
|
651
|
+
expect(outcome).not.toBe('custody');
|
|
652
|
+
const after = (0, store_1.readJournal)(repo, 'main').state;
|
|
653
|
+
expect(after.cycle.status).toBe('IN_PROGRESS');
|
|
654
|
+
expect(spawns).toBe(1);
|
|
655
|
+
expect((0, generations_1.activeGeneration)(after).resumeAction?.kind).toBe('resume-cycle');
|
|
656
|
+
expect(after.tasks.map(task => [task.id, task.status, task.attempts])).toEqual([['S1', 'done', 1]]); // nada se redespacha
|
|
657
|
+
});
|
|
658
|
+
test.each([
|
|
659
|
+
['un item de tarea sin satisfacer tras declararla terminada', (s) => {
|
|
660
|
+
s.tasks[0].verificationPlan.push({ id: 'task-test', kind: 'test' });
|
|
661
|
+
}],
|
|
662
|
+
['un item de ciclo que cita un job inexistente', (s) => {
|
|
663
|
+
s.cycleVerificationPlan[0].satisfiedBy = 'job-fantasma';
|
|
664
|
+
}],
|
|
665
|
+
['una revision obligatoria sin verdict', (s) => {
|
|
666
|
+
s.tasks[0].reviewObligations[1].verdictId = undefined;
|
|
667
|
+
}],
|
|
668
|
+
['un verdict de revision adverso sin fix cerrado', (s) => {
|
|
669
|
+
s.verdicts[1].result = 'fail';
|
|
670
|
+
}],
|
|
671
|
+
['un verificador requerido que ningun plan contiene', (s) => {
|
|
672
|
+
s.cycleVerificationPlan.splice(0, 1);
|
|
673
|
+
}],
|
|
674
|
+
])('cierre pendiente sigue fail-closed con %s: custodia sin controller', async (_name, mutate) => {
|
|
675
|
+
const state = closurePendingState(repo);
|
|
676
|
+
mutate(state);
|
|
677
|
+
(0, store_1.writeJournal)(repo, 'main', state);
|
|
678
|
+
(0, generations_1.beginGeneration)(repo, 'main'); // mismo reemplazo: solo cambia la evidencia
|
|
679
|
+
let spawns = 0;
|
|
680
|
+
const outcome = await new supervisor_1.Supervisor(repo, 'main', supervisor_1.DEFAULT_SUPERVISOR_CONFIG, () => { spawns++; }, undefined, admitted).tick();
|
|
681
|
+
expect(outcome).toBe('custody');
|
|
682
|
+
expect(spawns).toBe(0);
|
|
683
|
+
expect((0, store_1.readJournal)(repo, 'main').state.cycle.status).toBe('BLOCKED');
|
|
684
|
+
});
|
|
685
|
+
// A controller that resumes heartbeating is healthy again; leaving the
|
|
686
|
+
// generation marked as a suspected stall misreports the cycle.
|
|
687
|
+
test('una generacion en sospecha de stall vuelve a active cuando el heartbeat se reanuda', async () => {
|
|
688
|
+
initUnattendedFixture(repo);
|
|
689
|
+
(0, generations_1.beginGeneration)(repo, 'main');
|
|
690
|
+
const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', 'setTimeout(()=>{}, 20000)'], process.cwd(), 'nBack');
|
|
691
|
+
try {
|
|
692
|
+
const s = (0, store_1.readJournal)(repo, 'main').state;
|
|
693
|
+
const gen = (0, generations_1.activeGeneration)(s);
|
|
694
|
+
gen.processRef = ref;
|
|
695
|
+
gen.state = 'controller-suspected-stall';
|
|
696
|
+
s.controllerHeartbeatAt = new Date().toISOString();
|
|
697
|
+
(0, store_1.writeJournal)(repo, 'main', s);
|
|
698
|
+
const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, provider: 'codex', heartbeatTimeoutMs: 60000, activityWindowMs: 60000, tickMs: 20 };
|
|
699
|
+
expect(await new supervisor_1.Supervisor(repo, 'main', cfg, fakeSpawner, undefined, admitted).tick()).not.toBe('custody');
|
|
700
|
+
expect((0, generations_1.activeGeneration)((0, store_1.readJournal)(repo, 'main').state).state).toBe('active');
|
|
701
|
+
}
|
|
702
|
+
finally {
|
|
703
|
+
child.kill('SIGKILL');
|
|
704
|
+
}
|
|
705
|
+
});
|
|
624
706
|
test('un journal schema-1 legacy no despacha controller ni jobs aunque contenga trabajo pendiente', async () => {
|
|
625
707
|
(0, store_1.initJournal)(repo, 'main');
|
|
626
708
|
const legacy = (0, store_1.readJournal)(repo, 'main').state;
|
|
@@ -143,12 +143,40 @@ describe('admitPlan', () => {
|
|
|
143
143
|
expect(report).toMatchObject({ state: 'blocked', executionMode: 'interactivo', provider: 'cursor', journal: 'not-required' });
|
|
144
144
|
expect(report.diagnostics[0].code).toBe('ADMISSION_CAPABILITY_UNVERIFIED');
|
|
145
145
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
146
|
+
// The durable controller (journal + awm watch + jobs) is opt-in again for
|
|
147
|
+
// native v1 dispatch, as it was before 9.8.0: a v1 unattended plan with no
|
|
148
|
+
// journal on its branch runs as one native provider session. Creating a
|
|
149
|
+
// journal (awm watch --init) is the opt-in, after which it must be current.
|
|
150
|
+
// v2 routing custody lives in the journal, so awm-routed still requires it.
|
|
151
|
+
describe('the durable journal is opt-in for native v1 unattended work', () => {
|
|
152
|
+
const unattended = () => ({ ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } });
|
|
153
|
+
const base = () => ({ plan: unattended(), provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], planPath: 'docs/plan.md', controllerAutonomy: 'approval-free' });
|
|
154
|
+
it('admits a v1 unattended plan with no journal as journal not-required', async () => {
|
|
155
|
+
const report = await (0, admission_1.admitPlan)(base());
|
|
156
|
+
expect(report).toMatchObject({ state: 'admitted', executionMode: 'desatendido', journal: 'not-required' });
|
|
157
|
+
expect(report.diagnostics.map(d => d.code)).not.toContain('ADMISSION_JOURNAL_BINDING_REQUIRED');
|
|
158
|
+
});
|
|
159
|
+
it('keeps a journal that exists (the opt-in) fail-closed when it is corrupt or stale', async () => {
|
|
160
|
+
const corrupt = await (0, admission_1.admitPlan)({ ...base(), journalCorrupt: true });
|
|
161
|
+
expect(corrupt).toMatchObject({ state: 'blocked', journal: 'corrupt' });
|
|
162
|
+
expect(corrupt.diagnostics[0]).toMatchObject({ code: 'ADMISSION_JOURNAL_BINDING_REQUIRED' });
|
|
163
|
+
const stale = await (0, admission_1.admitPlan)({ ...base(), journalState: { ...(0, types_1.emptyState)('main'), schema: 2, planBinding: { path: 'docs/plan.md', digest: 'b'.repeat(64), schema: 'compact-slices/v1', executionMode: 'desatendido', boundAt: '2026-09-15T00:00:00.000Z' } } });
|
|
164
|
+
expect(stale).toMatchObject({ state: 'blocked', journal: 'stale' });
|
|
165
|
+
const legacy = await (0, admission_1.admitPlan)({ ...base(), journalState: (0, types_1.emptyState)('main') });
|
|
166
|
+
expect(legacy).toMatchObject({ state: 'blocked', journal: 'stale' });
|
|
167
|
+
});
|
|
168
|
+
it('still requires the journal for awm-routed v2 unattended work', async () => {
|
|
169
|
+
const v2Unattended = { ...valid, schema: 'compact-slices/v2', manifest: { ...valid.manifest, executionMode: 'desatendido', schema: 'compact-slices/v2', slices: valid.manifest.slices.map(slice => ({ ...slice, implementerProfile: 'mechanical' })) } };
|
|
170
|
+
const report = await (0, admission_1.admitPlan)({ ...base(), plan: v2Unattended });
|
|
171
|
+
expect(report).toMatchObject({ state: 'blocked', executionMode: 'desatendido', journal: 'missing' });
|
|
172
|
+
expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_JOURNAL_BINDING_REQUIRED' });
|
|
173
|
+
expect(report.forecast).toBeUndefined();
|
|
174
|
+
});
|
|
175
|
+
it('keeps the controller posture gate for journal-less unattended work', async () => {
|
|
176
|
+
const report = await (0, admission_1.admitPlan)({ ...base(), controllerAutonomy: undefined });
|
|
177
|
+
expect(report.state).toBe('blocked');
|
|
178
|
+
expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
|
|
179
|
+
});
|
|
152
180
|
});
|
|
153
181
|
it('admits unattended work only with an exact schema-2 plan binding', async () => {
|
|
154
182
|
const unattended = { ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } };
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|