@bongos/core 1.19.887 → 1.19.888

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/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.887",
6
- "core_contract": "1.19.887",
7
- "source_commit": "b874dffd8907b22a633526eddcb279c009c89b40",
5
+ "core_version": "1.19.888",
6
+ "core_contract": "1.19.888",
7
+ "source_commit": "7ccf0b2f4cc7f7bafb93762cbd5cfb9ed37cf546",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-19T20:48:51.300Z",
9
+ "built_at": "2026-09-19T20:57:53.725Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 497,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2264,
14
+ "functional_verbatim": 2266,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2787,
20
- "tree_sha256": "920b5751e940860113c8a2a84ede022e547e1901077f62330213144c8612091b",
19
+ "file_count": 2789,
20
+ "tree_sha256": "42c331016e53b14ca5416a325790e2846ea0ee2c6a24177222b823a1d2977b9c",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2607,7 +2607,7 @@
2607
2607
  {
2608
2608
  "path": "docs/module-api-changelog.md",
2609
2609
  "mode": "0000644",
2610
- "sha256": "31fdd25f81e095205720c92ce31100187f7d1c1c89eecc8b9c84f00936350352"
2610
+ "sha256": "7f60dfa2c479fecbd5a81de040689b9ff6f9d2aa5a907f9a7fcf86b77b476fea"
2611
2611
  },
2612
2612
  {
2613
2613
  "path": "docs/modules-contract.md",
@@ -3732,7 +3732,7 @@
3732
3732
  {
3733
3733
  "path": "modules/agents/lib/validate.js",
3734
3734
  "mode": "0000644",
3735
- "sha256": "3ff56e388e9ae4cccb09fb072793b5544f771aeae51e8477f8a221f0c78b9515"
3735
+ "sha256": "e9b3c905539a323d74a55f98455204bd46daca2daf49c476324e85684ba7971f"
3736
3736
  },
3737
3737
  {
3738
3738
  "path": "modules/agents/migrations/agents_001_definitions.sql",
@@ -3754,6 +3754,11 @@
3754
3754
  "mode": "0000644",
3755
3755
  "sha256": "31a5ac35536cecd264bc989907f4cf094e050db0b8de85e7e3e7831cf7c25a9d"
3756
3756
  },
3757
+ {
3758
+ "path": "modules/agents/migrations/agents_005_cron_trigger.sql",
3759
+ "mode": "0000644",
3760
+ "sha256": "5d422998b1d53ee015f4a494ccede1adcb1eb90bd501bd28c1d88f9840b87367"
3761
+ },
3757
3762
  {
3758
3763
  "path": "modules/agents/module.json",
3759
3764
  "mode": "0000644",
@@ -7692,12 +7697,12 @@
7692
7697
  {
7693
7698
  "path": "package-lock.json",
7694
7699
  "mode": "0000644",
7695
- "sha256": "356149fe3c3c69d00f16886662e5fee0ab51c4be066af64156a2b3bb7e14c69c"
7700
+ "sha256": "d3b1b9f84e1713ff7815c19a9bd02b6c4de4dd3b44a5ede68ff56fbd5ad0cfcd"
7696
7701
  },
7697
7702
  {
7698
7703
  "path": "package.json",
7699
7704
  "mode": "0000644",
7700
- "sha256": "9e2d057866feafc0e9606c36335013a47ad55190c03e7beed63d1e592f2fd23a"
7705
+ "sha256": "4cd9682e13a9a2e1e358c7bd500286378e7b5b8ceb1bb284a925b91ab8de2a08"
7701
7706
  },
7702
7707
  {
7703
7708
  "path": "public-docs/index.html",
@@ -9702,7 +9707,7 @@
9702
9707
  {
9703
9708
  "path": "src/module-api.js",
9704
9709
  "mode": "0000644",
9705
- "sha256": "309fd0ddeb0185da023052aaab8967162dbdb833ade7226f49e1636440146e83"
9710
+ "sha256": "ae3867dc81f944026ce67bae25ca927232b84accc4ed03d4ef45798526c72c46"
9706
9711
  },
9707
9712
  {
9708
9713
  "path": "src/module-loader/catalog.js",
@@ -9844,6 +9849,11 @@
9844
9849
  "mode": "0000644",
9845
9850
  "sha256": "eea8f9e0bdf2c84fabc81e16c30e4026a7a336c1e705d614e68b805957b9b74a"
9846
9851
  },
9852
+ {
9853
+ "path": "tests/agents_cron_trigger.mjs",
9854
+ "mode": "0000644",
9855
+ "sha256": "be78fa37657214c39a4880d2e1b44a1647c8b509d63262ca9a4a334431d7bdde"
9856
+ },
9847
9857
  {
9848
9858
  "path": "tests/agents_event_router.mjs",
9849
9859
  "mode": "0000644",
@@ -9877,7 +9887,7 @@
9877
9887
  {
9878
9888
  "path": "tests/agents_validate.mjs",
9879
9889
  "mode": "0000644",
9880
- "sha256": "9376185bc6e11571b050dcf61a993faf780cea66aa4c0844d114b1cb738043e2"
9890
+ "sha256": "164059d69fabbd5dd70a29b29c524b5359f0346b71091e73a5e4fafa5dbc0804"
9881
9891
  },
9882
9892
  {
9883
9893
  "path": "tests/agents_write_routes.mjs",
@@ -2248,5 +2248,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2248
2248
  landed since 1.19.885 with no explicit bump. run 35467975408. (task 1002620)
2249
2249
  1.19.887 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2250
2250
  landed since 1.19.886 with no explicit bump. run 35468511941. (task 1002620)
2251
+ 1.19.888 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2252
+ landed since 1.19.887 with no explicit bump. run 35468948342. (task 1002620)
2251
2253
  ---------------------------------------------------------------------------
2252
2254
  ```
@@ -48,8 +48,31 @@
48
48
  const EVENT_CATALOG = Object.freeze(['task.shipped', 'idea.graded.full']);
49
49
 
50
50
  // Mirrors agents_definitions_trigger_type_chk. 'event' consumes the catalog above;
51
- // 'on-demand' is the historian shape (a builder asks, the agent answers).
52
- const TRIGGER_TYPES = Object.freeze(['event', 'on-demand']);
51
+ // 'on-demand' is the historian shape (a builder asks, the agent answers); 'cron'
52
+ // fires on a cadence and consumes CADENCES below (task 1004008, agents_005).
53
+ const TRIGGER_TYPES = Object.freeze(['event', 'on-demand', 'cron']);
54
+
55
+ // The cadence vocabulary a 'cron' trigger may declare, in trigger_spec.cadence.
56
+ //
57
+ // DUPLICATED FROM scripts/gds/routine-schedule.js ON PURPOSE, for the reason
58
+ // RANK_ORDER below is duplicated: a module may require only src/module-api.js
59
+ // (ADR 0083), so it cannot reach into scripts/. The two copies are kept honest by
60
+ // tests/agents_cron_trigger.mjs, which reads routine-schedule.js's own CADENCES +
61
+ // isUniformDivisor and asserts this file agrees — a drifting copy fails there
62
+ // rather than at some future tick.
63
+ //
64
+ // N MUST DIVIDE 60 for the sub-hourly form. A cron step restarts each hour, so
65
+ // only a divisor yields a uniform period: `*/15` fires :00 :15 :30 :45 always,
66
+ // but `*/7` fires :00…:56 then waits 4 — a "7min" cadence that is not one.
67
+ const CADENCES = Object.freeze(['hourly', 'daily', 'weekly', 'monthly']);
68
+ const SUBHOURLY_RE = /^([1-9]\d*)min$/;
69
+ function isCadence(name) {
70
+ if (CADENCES.includes(name)) return true;
71
+ const m = SUBHOURLY_RE.exec(String(name == null ? '' : name));
72
+ if (!m) return false;
73
+ const n = Number(m[1]);
74
+ return Number.isInteger(n) && n > 0 && n < 60 && 60 % n === 0;
75
+ }
53
76
 
54
77
  // Mirrors agents_definitions_model_tier_chk. No 'main' tier by design — an agent
55
78
  // is a subagent, never the main session.
@@ -222,6 +245,29 @@ function validateAgentDefinition(obj, opts = {}) {
222
245
  }
223
246
  }
224
247
 
248
+ // --- the cadence, fail-closed, the same shape as the event catalog above ----
249
+ // agents_005's CHECK pins that a cron definition HAS a non-empty cadence; the
250
+ // vocabulary is pinned here, because the sub-hourly rule is a predicate and a
251
+ // CHECK spelling it out would be a second copy free to drift.
252
+ //
253
+ // The reach is deliberately the EVENT BLOCK'S reach, no more: an UNWIRED
254
+ // cadence is refused wherever it appears, including on a non-cron definition
255
+ // where it is dead config. A VALID cadence on a non-cron definition is ACCEPTED,
256
+ // exactly as a wired `event:` on an on-demand definition is — harmless, unread,
257
+ // and not this validator's call to police. Saying otherwise in a comment would
258
+ // describe a guard that is not here.
259
+ if (spec) {
260
+ const cadence = safeGet(spec, 'cadence');
261
+ const hasCadenceKey = cadence !== undefined && cadence !== null;
262
+ if (triggerType === 'cron' && !isNonEmptyString(cadence)) {
263
+ E('trigger_cadence_missing', 'trigger_spec.cadence',
264
+ `trigger_spec.cadence: required for a 'cron' trigger — a definition with no cadence can never fire. Named cadences: ${CADENCES.join(', ')}; or a sub-hourly '<N>min' where N divides 60.`);
265
+ } else if (hasCadenceKey && !isCadence(cadence)) {
266
+ E('trigger_cadence_unwired', 'trigger_spec.cadence',
267
+ `trigger_spec.cadence: ${JSON.stringify(cadence)} is not a cadence. Named cadences: ${CADENCES.join(', ')}; or a sub-hourly '<N>min' where N divides 60 (so the declared period and the realized one are the same claim).`);
268
+ }
269
+ }
270
+
225
271
  // --- model tier -----------------------------------------------------------
226
272
  const rawTier = safeGet(obj, 'model_tier');
227
273
  const modelTier = rawTier === undefined || rawTier === null ? 'default' : rawTier;
@@ -355,6 +401,7 @@ function validateAgentDefinition(obj, opts = {}) {
355
401
  module.exports = {
356
402
  validateAgentDefinition,
357
403
  EVENT_CATALOG,
404
+ CADENCES, isCadence,
358
405
  TRIGGER_TYPES,
359
406
  MODEL_TIERS,
360
407
  RANK_ORDER,
@@ -0,0 +1,78 @@
1
+ -- agents_005_cron_trigger.sql — make 'cron' a trigger type that can exist (task 1004008).
2
+ --
3
+ -- Tasks 1002498 (the cron-router poller) and 1002501 (the cron roster) both ask
4
+ -- for a timer whose each tick SELECTS DUE CRON AGENTS. There are none and there
5
+ -- can be none: agents_001 pinned trigger_type to ('event','on-demand') on BOTH
6
+ -- agents_definitions and agents_runs, and lib/validate.js mirrors it. Build the
7
+ -- poller against that and you get a timer that is correct, tested, and selects
8
+ -- from an empty set by construction — green tests over an unreachable branch,
9
+ -- which is a false claim of coverage rather than coverage. This migration is the
10
+ -- enabling half; the poller is 1002498's and is deliberately NOT built here.
11
+ --
12
+ -- WIDENING A FAIL-CLOSED ENUM IS THE POINT, NOT A WORKAROUND. agents_001's header
13
+ -- calls these enums fail-closed on purpose: "an unwired trigger name is refused,
14
+ -- never silently stored". That rule is kept — 'cron' stops being unwired in the
15
+ -- same change that makes it storable, because lib/validate.js is widened and
16
+ -- constrained alongside. What is still true after this: a trigger name outside
17
+ -- the three is refused at both the validator and the constraint.
18
+ --
19
+ -- BOTH TABLES, AND WHY THE LEDGER MATTERS AS MUCH AS THE DEFINITION. agents_001
20
+ -- constrains trigger_type on agents_runs too. Widen only the definition and a
21
+ -- cron agent could be stored and enabled, and then fail at the moment it fires,
22
+ -- when the ledger insert violates its own CHECK — the worst shape available: a
23
+ -- definition that looks armed and cannot record a single run.
24
+ --
25
+ -- A CRON DEFINITION MUST SAY WHEN. agents_001 already refuses an event definition
26
+ -- with no event name ("a definition that can never fire"). A cron definition with
27
+ -- no cadence is the same defect wearing a different key, so it gets the same
28
+ -- treatment rather than being left for the poller to interpret at tick time.
29
+ -- The cadence VOCABULARY is validated in lib/validate.js, not here: the named set
30
+ -- ('hourly','daily','weekly','monthly') plus the open-ended sub-hourly form
31
+ -- ('15min') is a predicate, and a CHECK that tried to spell the sub-hourly rule —
32
+ -- N must divide 60 — would be a second, drifting copy of it. The constraint
33
+ -- pins the SHAPE (a non-empty cadence key is present); the validator pins the
34
+ -- VALUE. That is the same division agents_001 draws for `event`.
35
+ --
36
+ -- Module migrations are ADDITIVE and NAMESPACED (ADR 0083 §Decision #5): every
37
+ -- object is agents_-prefixed, there is no down-migration, and the filename is
38
+ -- agents_-prefixed (ADR 0108 §5). Idempotent: safe to re-run. No existing row
39
+ -- changes — widening a CHECK can only make rows MORE legal, so nothing that
40
+ -- validates today stops validating.
41
+
42
+ BEGIN;
43
+
44
+ DO $$
45
+ BEGIN
46
+ -- agents_definitions: the three WIRED trigger types.
47
+ IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'agents_definitions_trigger_type_chk') THEN
48
+ ALTER TABLE agents_definitions DROP CONSTRAINT agents_definitions_trigger_type_chk;
49
+ END IF;
50
+ ALTER TABLE agents_definitions ADD CONSTRAINT agents_definitions_trigger_type_chk
51
+ CHECK (trigger_type IN ('event', 'on-demand', 'cron'));
52
+
53
+ -- agents_runs: the same three, so a cron fire can be RECORDED and not only run.
54
+ IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'agents_runs_trigger_type_chk') THEN
55
+ ALTER TABLE agents_runs DROP CONSTRAINT agents_runs_trigger_type_chk;
56
+ END IF;
57
+ ALTER TABLE agents_runs ADD CONSTRAINT agents_runs_trigger_type_chk
58
+ CHECK (trigger_type IN ('event', 'on-demand', 'cron'));
59
+
60
+ -- The twin of agents_definitions_trigger_spec_event_chk: a cron definition
61
+ -- carries a non-empty cadence, or it is a definition that can never fire.
62
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'agents_definitions_trigger_spec_cadence_chk') THEN
63
+ ALTER TABLE agents_definitions ADD CONSTRAINT agents_definitions_trigger_spec_cadence_chk
64
+ CHECK (trigger_type <> 'cron'
65
+ OR (trigger_spec ? 'cadence' AND length(trigger_spec ->> 'cadence') > 0));
66
+ END IF;
67
+ END $$;
68
+
69
+ -- agents_001 indexes (trigger_type, trigger_spec->>'event') for the event
70
+ -- router's "who wants this event" read. The cron router's read is a different
71
+ -- question — "which cron agents are due" — and it is answered by scanning the
72
+ -- cron rows, which are few. Adding a cadence index now would be indexing for a
73
+ -- query no committed code makes; 1002498 adds one if its poller needs it.
74
+
75
+ COMMENT ON CONSTRAINT agents_definitions_trigger_type_chk ON agents_definitions IS
76
+ 'The WIRED trigger vocabulary (task 1004008 added cron). Mirrored by TRIGGER_TYPES in modules/agents/lib/validate.js — widen both together or the two disagree about what can exist.';
77
+
78
+ COMMIT;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.887",
3
+ "version": "1.19.888",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.887",
9
+ "version": "1.19.888",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.887",
3
+ "version": "1.19.888",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.887'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.888'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,133 @@
1
+ // tests/agents_cron_trigger.mjs — 'cron' is a trigger type that can EXIST
2
+ // (task 1004008, goal 1000095).
3
+ //
4
+ // Tasks 1002498 (cron-router poller) and 1002501 (cron roster) both select "due
5
+ // cron agents" from a set that was empty by construction: agents_001 pinned
6
+ // trigger_type to ('event','on-demand') on BOTH agents_definitions and
7
+ // agents_runs, and lib/validate.js mirrored it. A poller built against that is
8
+ // correct, tested, and unreachable — green tests over a branch nothing can enter.
9
+ //
10
+ // What this locks:
11
+ // * the THREE places that must agree (validator, definitions CHECK, runs CHECK)
12
+ // — the ledger constraint is the one that is easy to forget, and forgetting it
13
+ // yields the worst shape available: an agent that looks armed, fires, and
14
+ // then violates a CHECK when it tries to record the run.
15
+ // * the cadence VOCABULARY, including the sub-hourly divisor rule.
16
+ // * that validate.js's duplicated CADENCES really match scripts/gds/
17
+ // routine-schedule.js's, which is the copy this one was made from.
18
+ // * that widening the enum did not loosen it — an unwired name is still refused.
19
+
20
+ import { test } from 'node:test';
21
+ import assert from 'node:assert/strict';
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { createRequire } from 'node:module';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const require = createRequire(import.meta.url);
28
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
29
+ const v = require('../modules/agents/lib/validate.js');
30
+ const schedule = require('../scripts/gds/routine-schedule.js');
31
+
32
+ const sql = (f) => fs.readFileSync(path.join(ROOT, 'modules', 'agents', 'migrations', f), 'utf8');
33
+ const mig001 = sql('agents_001_definitions.sql');
34
+ const mig005 = sql('agents_005_cron_trigger.sql');
35
+
36
+ const def = (over = {}) => ({ name: 'ship-digest', persona: 'x'.repeat(40), ...over });
37
+ const cron = (cadence) => def({ trigger_type: 'cron', trigger_spec: { cadence } });
38
+
39
+ // ---- 1. the validator ------------------------------------------------------
40
+
41
+ test("'cron' is a wired trigger type", () => {
42
+ assert.ok(v.TRIGGER_TYPES.includes('cron'));
43
+ // Widening is not loosening: the enum is still closed.
44
+ assert.equal(v.validateAgentDefinition(def({ trigger_type: 'webhook', trigger_spec: {} })).ok, false);
45
+ assert.deepEqual([...v.TRIGGER_TYPES].sort(), ['cron', 'event', 'on-demand']);
46
+ });
47
+
48
+ test('a cron definition must say WHEN, or it can never fire', () => {
49
+ const r = v.validateAgentDefinition(def({ trigger_type: 'cron', trigger_spec: {} }));
50
+ assert.equal(r.ok, false);
51
+ assert.ok(r.errors.some((e) => e.code === 'trigger_cadence_missing'),
52
+ 'the missing cadence is named as its own error, not folded into a generic spec failure');
53
+ });
54
+
55
+ test('the cadence vocabulary: named, and sub-hourly where N divides 60', () => {
56
+ for (const good of ['hourly', 'daily', 'weekly', 'monthly', '15min', '5min', '30min', '1min']) {
57
+ assert.equal(v.validateAgentDefinition(cron(good)).ok, true, `${good} is a cadence`);
58
+ }
59
+ // 7 and 45 do not divide 60: `*/7` fires :00…:56 then waits 4, so the declared
60
+ // period and the realized period would be different claims.
61
+ for (const bad of ['7min', '45min', '0min', '60min', 'fortnightly', 'often', '']) {
62
+ assert.equal(v.validateAgentDefinition(cron(bad)).ok, false, `${bad} is not a cadence`);
63
+ }
64
+ });
65
+
66
+ test('an accepted cron definition keeps its cadence through normalization', () => {
67
+ // The bug class task 1004007 documents one axis over: validate accepted a field
68
+ // and then DROPPED it, so a definition looked configured and was not.
69
+ const r = v.validateAgentDefinition(cron('daily'));
70
+ assert.equal(r.ok, true);
71
+ assert.equal(r.value.trigger_spec.cadence, 'daily', 'the cadence survives into the normalized value');
72
+ assert.equal(r.value.trigger_type, 'cron');
73
+ });
74
+
75
+ test('the event path is untouched', () => {
76
+ assert.equal(v.validateAgentDefinition(def({ trigger_type: 'event', trigger_spec: { event: 'task.shipped' } })).ok, true);
77
+ assert.equal(v.validateAgentDefinition(def({ trigger_type: 'event', trigger_spec: {} })).ok, false);
78
+ assert.equal(v.validateAgentDefinition(def({ trigger_type: 'event', trigger_spec: { event: 'nope' } })).ok, false);
79
+ });
80
+
81
+ test('a VALID cadence on a non-cron definition is accepted, as a wired event is', () => {
82
+ // Pinned because the reach is a judgement call and the comment beside the code
83
+ // claims exactly this. An UNWIRED cadence is still refused wherever it appears.
84
+ assert.equal(v.validateAgentDefinition(def({ trigger_type: 'on-demand', trigger_spec: { cadence: 'daily' } })).ok, true);
85
+ assert.equal(v.validateAgentDefinition(def({ trigger_type: 'on-demand', trigger_spec: { cadence: '7min' } })).ok, false);
86
+ });
87
+
88
+ // ---- 2. the copy that is free to drift -------------------------------------
89
+
90
+ test('validate.js CADENCES agrees with routine-schedule.js, the copy it came from', () => {
91
+ // A module may require only src/module-api.js (ADR 0083), so validate.js cannot
92
+ // import this list — it holds a duplicate. This test is the thing that keeps the
93
+ // duplicate honest; without it the two are one edit away from disagreeing about
94
+ // what a cadence is, and nothing would say so until a tick.
95
+ assert.deepEqual([...v.CADENCES].sort(), [...schedule.CADENCES].sort());
96
+ for (const n of [1, 5, 7, 15, 30, 45, 60, 0]) {
97
+ assert.equal(v.isCadence(`${n}min`), schedule.isCadence(`${n}min`),
98
+ `the two copies agree on ${n}min`);
99
+ }
100
+ });
101
+
102
+ // ---- 3. both CHECK constraints ---------------------------------------------
103
+
104
+ test('BOTH tables widen — the ledger as well as the definition', () => {
105
+ // agents_001 constrains trigger_type on agents_runs too. Widen only the
106
+ // definition and a cron agent stores, enables, fires, and THEN fails when the
107
+ // run insert violates its own CHECK.
108
+ for (const c of ['agents_definitions_trigger_type_chk', 'agents_runs_trigger_type_chk']) {
109
+ assert.ok(mig001.includes(c), `${c} exists in agents_001 — this test is about widening it`);
110
+ assert.match(mig005, new RegExp(`ADD CONSTRAINT ${c}[\\s\\S]{0,200}?'cron'`),
111
+ `${c} is re-added including 'cron'`);
112
+ assert.match(mig005, new RegExp(`DROP CONSTRAINT ${c}`),
113
+ `${c} is dropped before being re-added — ALTER cannot widen a CHECK in place`);
114
+ }
115
+ });
116
+
117
+ test('the migration pins the SHAPE of a cron spec, leaving the vocabulary to the validator', () => {
118
+ assert.match(mig005, /agents_definitions_trigger_spec_cadence_chk/);
119
+ assert.match(mig005, /trigger_type <> 'cron'/, 'the guard only binds cron rows');
120
+ assert.match(mig005, /trigger_spec \? 'cadence'/, 'and requires the key');
121
+ // The sub-hourly divisor rule must NOT be spelled in SQL — that would be a
122
+ // second copy of isCadence(), free to drift from the one the validator uses.
123
+ assert.ok(!/% *60|divid/i.test(mig005.replace(/^--.*$/gm, '')),
124
+ 'no arithmetic cadence rule in the executable SQL — the predicate lives in one place');
125
+ });
126
+
127
+ test('the migration is additive, namespaced and idempotent (ADR 0083 §5)', () => {
128
+ assert.match(mig005, /IF (NOT )?EXISTS/, 'guarded so a re-run is safe');
129
+ assert.ok(!/\bDROP TABLE\b|\bDROP COLUMN\b/i.test(mig005), 'nothing is destroyed');
130
+ for (const stmt of mig005.match(/(CREATE|ALTER)\s+TABLE\s+(\w+)/gi) || []) {
131
+ assert.match(stmt, /\s(agents_\w+)$/, `${stmt} touches only agents_-namespaced tables`);
132
+ }
133
+ });
@@ -164,12 +164,20 @@ await test('an on-demand definition carrying a WIRED event is accepted', () => {
164
164
 
165
165
  // --- THE TRIGGER TYPE -------------------------------------------------------
166
166
 
167
- await test('TRIGGER_TYPES is the v1 pair the DB CHECK also pins', () => {
168
- assert.deepEqual(TRIGGER_TYPES, ['event', 'on-demand']);
167
+ await test('TRIGGER_TYPES is the wired set the DB CHECK also pins', () => {
168
+ // Was the v1 PAIR; task 1004008 added 'cron' so tasks 1002498/1002501 select
169
+ // from a set that can be non-empty. Kept as an exact list on purpose: widening
170
+ // this vocabulary must be a deliberate edit here AND in agents_005's CHECK on
171
+ // both agents_definitions and agents_runs (tests/agents_cron_trigger.mjs pins
172
+ // that the three agree).
173
+ assert.deepEqual(TRIGGER_TYPES, ['event', 'on-demand', 'cron']);
169
174
  });
170
175
 
171
176
  await test('an unknown trigger type is refused', () => {
172
- for (const bad of ['cron', 'webhook', 'Event', 'on demand', '', null, undefined, 7, {}]) {
177
+ // 'cron' was this list's first entry until task 1004008 wired it. 'webhook' and
178
+ // 'schedule' are the plausible-but-unwired names now — the case exists to prove
179
+ // an unwired name is refused, so it needs names that are actually unwired.
180
+ for (const bad of ['webhook', 'schedule', 'Event', 'on demand', '', null, undefined, 7, {}]) {
173
181
  const r = validateAgentDefinition({ ...OK_DEF, trigger_type: bad }, OPTS);
174
182
  assert.equal(r.ok, false, `${JSON.stringify(bad)} must be refused`);
175
183
  assert.ok(hasCode(r, 'trigger_type_unknown'), `${JSON.stringify(bad)} → ${JSON.stringify(codes(r))}`);
@@ -471,7 +479,7 @@ await test('a hostile getter cannot escape the validator', () => {
471
479
 
472
480
  await test('EVERY error carries a code, a field and a human message', () => {
473
481
  const r = validateAgentDefinition(
474
- { name: 'BAD NAME', trigger_type: 'cron', trigger_spec: 'nope', scope_modules: ['government'] },
482
+ { name: 'BAD NAME', trigger_type: 'webhook', trigger_spec: 'nope', scope_modules: ['government'] },
475
483
  { authorRank: 'xenos', allowedModules: ['agents'] },
476
484
  );
477
485
  assert.equal(r.ok, false);
@@ -485,7 +493,7 @@ await test('EVERY error carries a code, a field and a human message', () => {
485
493
 
486
494
  await test('validation reports ALL faults at once, not just the first', () => {
487
495
  // An author fixing a definition file should not have to re-run four times.
488
- const r = validateAgentDefinition({ name: 'X', persona: '', trigger_type: 'cron' }, OPTS);
496
+ const r = validateAgentDefinition({ name: 'X', persona: '', trigger_type: 'webhook' }, OPTS);
489
497
  for (const code of ['name_invalid', 'persona_missing', 'trigger_type_unknown']) {
490
498
  assert.ok(hasCode(r, code), `${code} missing from ${JSON.stringify(codes(r))}`);
491
499
  }