@bongos/core 1.19.806 → 1.19.807

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,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.806",
6
- "core_contract": "1.19.806",
7
- "source_commit": "f8c520693845c7f9adf348df09423e304f95209a",
5
+ "core_version": "1.19.807",
6
+ "core_contract": "1.19.807",
7
+ "source_commit": "6154a1dcba2e8a7f84903bbf6a300e969382af47",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-17T04:40:33.822Z",
9
+ "built_at": "2026-09-17T04:54:31.340Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 487,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2708,
20
- "tree_sha256": "9bbb51184882b2c5393431e5c4ef4cd40107a22ea904758de3f6fa51f794e9b2",
20
+ "tree_sha256": "4d685c0339573af46854e0aac2b8a2b3981f2a289478dadf3d99f83b066b9cdc",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2532,7 +2532,7 @@
2532
2532
  {
2533
2533
  "path": "docs/module-api-changelog.md",
2534
2534
  "mode": "0000644",
2535
- "sha256": "42401492ce439f5d5e9bd8acbb2dd594be86a2a85490c7be364639c74975ff6b"
2535
+ "sha256": "bf7add64727463c6823ec5b4eb2031ba38eca1adbcacbc29cfa0c22799459865"
2536
2536
  },
2537
2537
  {
2538
2538
  "path": "docs/modules-contract.md",
@@ -2792,7 +2792,7 @@
2792
2792
  {
2793
2793
  "path": "docs/recipes/upgrading-the-core.md",
2794
2794
  "mode": "0000644",
2795
- "sha256": "041955ed109343181ea563e47e905c59e3bbd052e4dc3988b5ea34fbe5029f16"
2795
+ "sha256": "ffe0add6718e0620144e8b786f237153aca9888a9f9052adb41948fcbc17ddec"
2796
2796
  },
2797
2797
  {
2798
2798
  "path": "docs/recipes/windows-builders.md",
@@ -7722,12 +7722,12 @@
7722
7722
  {
7723
7723
  "path": "package-lock.json",
7724
7724
  "mode": "0000644",
7725
- "sha256": "8d7000897d233110534c4beb2ecd9e8d2688239abadf5b7a37f48f8a90c7fcad"
7725
+ "sha256": "784deb39aa5635d1c72a322fc760668827b10bd9be69a6350c3cdfae04d48ed5"
7726
7726
  },
7727
7727
  {
7728
7728
  "path": "package.json",
7729
7729
  "mode": "0000644",
7730
- "sha256": "a9a60765fc2fd7cd71a31ba524f0be16d9c7ca77282c1baa2dfd6b88ac6bdc06"
7730
+ "sha256": "9c405eb98a8f839686c4b573afd3c9af4efda58095e2b6a6bd6fd9c335f919b4"
7731
7731
  },
7732
7732
  {
7733
7733
  "path": "public-docs/index.html",
@@ -8742,7 +8742,7 @@
8742
8742
  {
8743
8743
  "path": "scripts/gds/routine-schedule.js",
8744
8744
  "mode": "0000644",
8745
- "sha256": "0d75c6b9be5a67738c91e55cda9084e10d7c64aa49c80ce9c0fa4219afcbd14c"
8745
+ "sha256": "9ef89c7bc7e2784abcbaecbdddbb03fe089019597a68323faa45bd1c887f199f"
8746
8746
  },
8747
8747
  {
8748
8748
  "path": "scripts/gds/run-routine.js",
@@ -9617,7 +9617,7 @@
9617
9617
  {
9618
9618
  "path": "src/module-api.js",
9619
9619
  "mode": "0000644",
9620
- "sha256": "a7b864edf20ff522c36758ae16b9050cdc2621595cd6883ae9484547126155d1"
9620
+ "sha256": "305f25717a1dbfdf109ba439f0160a047b374a80d20ce0111aad0f2a8e70ccde"
9621
9621
  },
9622
9622
  {
9623
9623
  "path": "src/module-loader/catalog.js",
@@ -12697,7 +12697,7 @@
12697
12697
  {
12698
12698
  "path": "tests/routine_schedule.mjs",
12699
12699
  "mode": "0000644",
12700
- "sha256": "ba7f5690b9a3649a4c4cd2571d316aa837ffc01874d57aaa418dc72683c6a1f1"
12700
+ "sha256": "ba6bc3abb46cdaf2100efb843d25526212d747feb71f51b13214ef3ffc92d624"
12701
12701
  },
12702
12702
  {
12703
12703
  "path": "tests/routine_timers.mjs",
@@ -2086,5 +2086,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2086
2086
  landed since 1.19.804 with no explicit bump. run 35180781365. (task 1002620)
2087
2087
  1.19.806 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2088
2088
  landed since 1.19.805 with no explicit bump. run 35182765995. (task 1002620)
2089
+ 1.19.807 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2090
+ landed since 1.19.806 with no explicit bump. run 35183654397. (task 1002620)
2089
2091
  ---------------------------------------------------------------------------
2090
2092
  ```
@@ -114,6 +114,45 @@ Each subscribed instance: read its installed core → ask the registry (`npm vie
114
114
 
115
115
  **Triple-gated, so nothing moves by accident:** the `core-update-subscription` routine is `requiresAutonomy: true` + **default OFF** in [`config/scheduled-routines.json`](../../config/scheduled-routines.json), the roster ships empty, and every upgrade is health-gated. Arm it by enabling the routine's timer on the control plane once autonomy is on.
116
116
 
117
+ #### How often it sweeps — the lag knob (task 1003674)
118
+
119
+ **This timer's period IS the delay between a builder's merge and the live site
120
+ serving it.** Everything upstream is already fast: CI publishes to npm within
121
+ ~2 minutes of a merge. The pull-deploy timer is fast too, but it watches the
122
+ *instance repo*, not npm — it logs `already at origin/main — nothing to deploy`
123
+ and only acts once a pin-bump commit exists. **This sweep is what creates that
124
+ commit**, so nothing reaches the site between its runs. Left at `OnCalendar=daily`
125
+ the worst case is ~24 hours, which is what made shipped work look unshipped and
126
+ pushed the owner into manual go-lives.
127
+
128
+ Install it at the declared cadence (`15min` in `config/scheduled-routines.json`):
129
+
130
+ ```ini
131
+ # /etc/systemd/system/<instance>-core-update-subscription.timer
132
+ [Timer]
133
+ OnCalendar=*:0/15
134
+ RandomizedDelaySec=120
135
+ Persistent=true
136
+ ```
137
+
138
+ ```bash
139
+ sudo systemctl daemon-reload && sudo systemctl restart <instance>-core-update-subscription.timer
140
+ systemctl list-timers '*core-update-subscription*' --no-pager # confirm the next elapse is minutes away
141
+ ```
142
+
143
+ **Do not go below ~5 minutes.** An upgrading run takes ~2 minutes; at a tighter
144
+ spacing runs overlap. A run with nothing to take costs one `npm view` and exits,
145
+ so 15 minutes is cheap — the cost scales with releases, not with checks.
146
+
147
+ This does **not** weaken the pinned-core model ([ADR 0108](../adr/0108-instance-composition-configurable-root.md)):
148
+ the pin still exists, a bad release can still be frozen or rolled back, and every
149
+ upgrade stays health-gated with auto-rollback (task 2149). A faster sweep only
150
+ removes the wait.
151
+
152
+ **Verify:** merge something, wait for the next elapse, and confirm
153
+ `curl -s https://<instance>/version` reports the new `coreVersion` with nobody
154
+ running a go-live.
155
+
117
156
  **Not covered:** multi-host / SSH orchestration across a real fleet (the deferred fleet-control-plane epic, task [#1948](https://example.com/builders#/task/1948)); and *publishing* new patches, which stays a gated owner step (ADR 0134) — a follow-up task decides whether merges auto-publish.
118
157
 
119
158
  ## Gotchas
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.806",
3
+ "version": "1.19.807",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.806",
9
+ "version": "1.19.807",
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.806",
3
+ "version": "1.19.807",
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",
@@ -52,11 +52,49 @@ const WORKFLOWS_DIR = path.join(ROOT, '.github', 'workflows');
52
52
 
53
53
  const { parseRoutineFrontmatter } = require('./run-routine.js');
54
54
 
55
- // The cadences a routine may declare. The value is the period a cron expression
56
- // must resolve to for the two to agree. Add a name here and cronCadence() below
57
- // must be able to produce it, or the pair can never match.
55
+ // The NAMED cadences a routine may declare. The value is the period a cron
56
+ // expression must resolve to for the two to agree. Add a name here and
57
+ // cronCadence() below must be able to produce it, or the pair can never match.
58
58
  const CADENCES = new Set(['hourly', 'daily', 'weekly', 'monthly']);
59
59
 
60
+ // Sub-hourly cadences are open-ended rather than enumerated: '15min', '5min',
61
+ // '30min'. Task 1003674 found the vocabulary topped out at `hourly`, so a
62
+ // routine an operator wants to fire every 15 minutes could not SAY so — the
63
+ // step cron that realizes it classified as unclassifiable and hard-failed the
64
+ // check, which left "declare nothing and stay dormant" as the only green
65
+ // option. That is the exact failure mode this file exists to refuse.
66
+ //
67
+ // N MUST DIVIDE 60. A cron step restarts each hour, so only a divisor of 60
68
+ // yields a genuinely uniform period: `*/15` fires at :00 :15 :30 :45 (every 15
69
+ // minutes, always), but `*/7` fires at :00 …:56 and then waits 4 — a "7min"
70
+ // cadence that is not one. Refusing the non-divisors keeps the declared period
71
+ // and the realized period the same claim.
72
+ const SUBHOURLY_RE = /^([1-9]\d*)min$/;
73
+
74
+ // The one definition of "is N a real sub-hourly period" — both the cadence NAME
75
+ // ('15min') and the cron STEP (*/15) are validated through here, so the two can
76
+ // never drift apart and disagree about the same number.
77
+ function isUniformDivisor(n) {
78
+ return Number.isInteger(n) && n > 0 && n < 60 && 60 % n === 0;
79
+ }
80
+
81
+ function subhourlyMinutes(name) {
82
+ const m = SUBHOURLY_RE.exec(String(name == null ? '' : name));
83
+ if (!m) return null;
84
+ const n = Number(m[1]);
85
+ return isUniformDivisor(n) ? n : null;
86
+ }
87
+
88
+ // The validity predicate the check asks. Kept separate from CADENCES so the
89
+ // named set stays a plain enumerable list for the error message.
90
+ function isCadence(name) {
91
+ return CADENCES.has(name) || subhourlyMinutes(name) !== null;
92
+ }
93
+
94
+ // The minute-field step spelling, hoisted so it is compiled once rather than per
95
+ // classified cron: `*/15` and the equivalent `0/15`.
96
+ const STEP_RE = /^(?:\*|0)\/([1-9]\d*)$/;
97
+
60
98
  // Pure: classify a 5-field cron expression into the cadence it actually realizes.
61
99
  // Returns null for anything this vocabulary cannot describe (step values, lists,
62
100
  // ranges) — a cadence can then never "match" it, which is the safe direction: an
@@ -67,12 +105,25 @@ const CADENCES = new Set(['hourly', 'daily', 'weekly', 'monthly']);
67
105
  // 17 6 * * * → daily (a specific hour, every day)
68
106
  // 17 6 3 * * → monthly (a specific day-of-month)
69
107
  // 17 * * * * → hourly (a specific minute, every hour)
108
+ // */15 * * * * → 15min (a uniform sub-hourly step)
70
109
  function cronCadence(expr) {
71
110
  const f = String(expr == null ? '' : expr).trim().split(/\s+/);
72
111
  if (f.length !== 5) return null;
73
112
  const [min, hour, dom, mon, dow] = f;
74
113
  const fixed = (v) => /^\d+$/.test(v);
75
114
  const any = (v) => v === '*';
115
+
116
+ // Sub-hourly: a step in the minute field with every coarser field wide open.
117
+ // (STEP_RE is module-scope — see above.)
118
+ // Anything narrower (a step confined to one hour, or to weekdays) is NOT the
119
+ // period it looks like, so it stays unclassifiable.
120
+ const step = STEP_RE.exec(min);
121
+ if (step) {
122
+ if (!(any(hour) && any(dom) && any(mon) && any(dow))) return null;
123
+ const n = Number(step[1]);
124
+ return isUniformDivisor(n) ? `${n}min` : null;
125
+ }
126
+
76
127
  if (!fixed(min)) return null; // every cadence below pins the minute
77
128
  if (!any(mon)) return null; // a specific month is a yearly shape
78
129
  if (any(hour)) return any(dom) && any(dow) ? 'hourly' : null;
@@ -145,8 +196,8 @@ function evaluate({ withCadence, withoutCadence }, workflows) {
145
196
  const wf = workflows || [];
146
197
 
147
198
  for (const r of withCadence) {
148
- if (!CADENCES.has(r.cadence)) {
149
- violations.push(`routine '${r.name}': unknown cadence "${r.cadence}" (want ${[...CADENCES].join('|')})`);
199
+ if (!isCadence(r.cadence)) {
200
+ violations.push(`routine '${r.name}': unknown cadence "${r.cadence}" (want ${[...CADENCES].join('|')}, or <N>min where N divides 60)`);
150
201
  continue;
151
202
  }
152
203
  const firing = wf.filter((w) => invokesRoutine(w.text, r.name, r.steps));
@@ -228,4 +279,5 @@ if (require.main === module) main();
228
279
  module.exports = {
229
280
  checkRoutineCadencesScheduled, evaluate, cronCadence, workflowCrons,
230
281
  invokesRoutine, readRoutines, readWorkflows, CADENCES,
282
+ isCadence, subhourlyMinutes, isUniformDivisor,
231
283
  };
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.806'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.807'; // 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');
@@ -50,13 +50,73 @@ test('cronCadence refuses what its vocabulary cannot describe, rather than guess
50
50
  // Unclassifiable must mean "never matches a declared cadence" — the safe
51
51
  // direction. A clever cron that silently counted as `weekly` would reopen
52
52
  // exactly the drift this check exists to catch.
53
- for (const expr of ['*/5 * * * *', '0 6 * * 1-5', '0 6 * * 1,4', '0 0 1 1 *', '17 6 * *', '', 'nonsense']) {
53
+ for (const expr of ['0 6 * * 1-5', '0 6 * * 1,4', '0 0 1 1 *', '17 6 * *', '', 'nonsense']) {
54
54
  assert.equal(rs.cronCadence(expr), null, `${expr} should not classify`);
55
55
  }
56
56
  // A day-of-month AND day-of-week together is an OR in cron — neither period.
57
57
  assert.equal(rs.cronCadence('17 6 3 * 1'), null);
58
58
  });
59
59
 
60
+ test('cronCadence classifies a uniform sub-hourly step (task 1003674)', () => {
61
+ // The vocabulary used to stop at `hourly`, so a routine meant to fire every
62
+ // 15 minutes had no way to declare it: the cron that realizes it read as
63
+ // unclassifiable and hard-failed, leaving "declare nothing, stay dormant" as
64
+ // the only green option — the state this check exists to refuse.
65
+ assert.equal(rs.cronCadence('*/15 * * * *'), '15min');
66
+ assert.equal(rs.cronCadence('*/5 * * * *'), '5min');
67
+ assert.equal(rs.cronCadence('*/30 * * * *'), '30min');
68
+ assert.equal(rs.cronCadence('0/15 * * * *'), '15min', 'the 0/N spelling is the same schedule');
69
+ });
70
+
71
+ test('cronCadence refuses a step that is not a true period (task 1003674)', () => {
72
+ // A cron step restarts every hour, so only a divisor of 60 is uniform:
73
+ // */7 fires at :00…:56 then waits 4. Calling that "7min" would be the same
74
+ // silent drift between declared and realized period the check is here for.
75
+ for (const expr of ['*/7 * * * *', '*/8 * * * *', '*/45 * * * *', '*/60 * * * *', '*/0 * * * *']) {
76
+ assert.equal(rs.cronCadence(expr), null, `${expr} is not a uniform period`);
77
+ }
78
+ // A step narrowed to one hour or one weekday is not the sub-hourly period it
79
+ // resembles — it only fires inside that window.
80
+ assert.equal(rs.cronCadence('*/15 6 * * *'), null);
81
+ assert.equal(rs.cronCadence('*/15 * * * 1'), null);
82
+ });
83
+
84
+ test('isCadence accepts the named periods and the divisor-of-60 Nmin form (task 1003674)', () => {
85
+ for (const ok of ['hourly', 'daily', 'weekly', 'monthly', '15min', '5min', '30min', '1min']) {
86
+ assert.equal(rs.isCadence(ok), true, `${ok} should be declarable`);
87
+ }
88
+ for (const no of ['7min', '60min', '90min', 'min', '0min', 'fortnightly', '', null, undefined]) {
89
+ assert.equal(rs.isCadence(no), false, `${no} should not be declarable`);
90
+ }
91
+ });
92
+
93
+ test('a routine declaring a sub-hourly cadence matches a step cron that fires it (task 1003674)', () => {
94
+ // End-to-end through evaluate(): declaring 15min is only green when a
95
+ // committed scheduler actually fires it at 15 minutes.
96
+ const wf = (cron) => [{
97
+ file: 'sweep.yml',
98
+ text: `on:\n schedule:\n - cron: '${cron}'\njobs:\n go:\n steps:\n - run: node scripts/gds/run-routine.js sweepy\n`,
99
+ }];
100
+ const declared = routines([{ name: 'sweepy', cadence: '15min', steps: [] }]);
101
+ assert.deepEqual(rs.evaluate(declared, wf('*/15 * * * *')).violations, []);
102
+
103
+ // Wrong period, and the check names both sides rather than shrugging.
104
+ const drifted = rs.evaluate(declared, wf('17 6 * * *')).violations;
105
+ assert.equal(drifted.length, 1);
106
+ assert.match(drifted[0], /declares cadence: 15min/);
107
+ assert.match(drifted[0], /-> daily|→ daily/);
108
+
109
+ // A non-divisor step cannot satisfy it either — it classifies as nothing.
110
+ assert.match(rs.evaluate(declared, wf('*/7 * * * *')).violations[0], /unclassifiable/);
111
+ });
112
+
113
+ test('an undeclarable cadence is refused by name (task 1003674)', () => {
114
+ const v = rs.evaluate(routines([{ name: 'sweepy', cadence: '7min', steps: [] }]), []).violations;
115
+ assert.equal(v.length, 1);
116
+ assert.match(v[0], /unknown cadence "7min"/);
117
+ assert.match(v[0], /N divides 60/);
118
+ });
119
+
60
120
  test('workflowCrons reads every cron a workflow declares, quoted or bare, ignoring trailing comments', () => {
61
121
  const y = [" schedule:", " - cron: '17 6 * * 1'", ' - cron: "0 3 * * *"', ' - cron: 5 * * * * # bare + comment'].join('\n');
62
122
  assert.deepEqual(rs.workflowCrons(y), ['17 6 * * 1', '0 3 * * *', '5 * * * *']);