@bongos/core 1.21.176 → 1.21.177

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.bongos-core.json +61 -361
  2. package/.claude/skills/planning-session/SKILL.md +2 -2
  3. package/config/scheduled-routines.json +3 -8
  4. package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +1 -1
  5. package/docs/adr/0093-tranche-2-core-carve-sequence.md +1 -1
  6. package/docs/canonical-permissions.md +1 -1
  7. package/docs/file-map.md +3 -8
  8. package/docs/handoff-template.md +1 -1
  9. package/docs/module-api-changelog.md +2 -0
  10. package/docs/onboarding/diagrams/README.md +3 -7
  11. package/docs/onboarding/diagrams/assertions.json +3 -3
  12. package/docs/recipes/ops-gotchas.md +3 -1
  13. package/modules/autonomy/db.js +9 -0
  14. package/modules/autonomy/module.json +2 -2
  15. package/modules/autonomy/pollers/daily-checks.js +262 -0
  16. package/modules/autonomy/pollers/routine-timers.js +7 -5
  17. package/modules/autonomy/rank-roles.js +64 -0
  18. package/modules/autonomy/server-schedule.js +37 -0
  19. package/package-lock.json +2 -2
  20. package/package.json +1 -1
  21. package/release-notes.json +14 -0
  22. package/scripts/bongos/api-alias-caller-scan.js +2 -2
  23. package/scripts/bongos/audit-authorship.sh +0 -5
  24. package/scripts/bongos/audit-rank-roles.js +15 -56
  25. package/scripts/bongos/codemod-rename-scripts-gds.js +0 -1
  26. package/scripts/bongos/diagram-facts.js +1 -1
  27. package/scripts/bongos/do-api.js +1 -1
  28. package/scripts/bongos/fitness-checks-identity.js +4 -15
  29. package/scripts/bongos/gen-diagrams.js +3 -3
  30. package/scripts/bongos/pulse.js +5 -3
  31. package/scripts/bongos/routine-schedule.js +38 -3
  32. package/scripts/bongos/ship-regen.js +2 -3
  33. package/scripts/security/dep-audit.js +50 -124
  34. package/src/module-api.js +1 -1
  35. package/tests/autonomy_daily_checks.mjs +201 -0
  36. package/tests/claude_materialize.mjs +12 -12
  37. package/tests/effective_visibility_predicate.mjs +0 -18
  38. package/tests/routine_capture_kind.mjs +18 -7
  39. package/tests/routine_schedule.mjs +42 -0
  40. package/scripts/bongos/artifact-staleness.js +0 -267
  41. package/scripts/bongos/classify-discipline.js +0 -153
  42. package/scripts/bongos/codemod-rename-src-gds.js +0 -145
  43. package/scripts/bongos/coreB-carve-proof.sh +0 -725
  44. package/scripts/bongos/goal-names.js +0 -102
  45. package/scripts/bongos/rename-goals.js +0 -109
  46. package/scripts/bongos/seed-adopt-tasks.js +0 -75
  47. package/scripts/bongos/seed-api-contract-goal-tasks.js +0 -141
  48. package/scripts/bongos/seed-board-room-tasks.js +0 -196
  49. package/scripts/bongos/seed-bongos-app-tasks.js +0 -294
  50. package/scripts/bongos/seed-bongos-coreB-tranche1-tasks.js +0 -81
  51. package/scripts/bongos/seed-bongos-coreB-tranche2-tasks.js +0 -100
  52. package/scripts/bongos/seed-bongos-goal-hierarchy-tasks.js +0 -307
  53. package/scripts/bongos/seed-bongos-module-system-tasks.js +0 -346
  54. package/scripts/bongos/seed-bongos-tasks.js +0 -214
  55. package/scripts/bongos/seed-bongos-v1-seo-tasks.js +0 -104
  56. package/scripts/bongos/seed-bv1-community-tasks.js +0 -131
  57. package/scripts/bongos/seed-bv1-privacy-invites-tasks.js +0 -133
  58. package/scripts/bongos/seed-bv1-profile-page-tasks.js +0 -161
  59. package/scripts/bongos/seed-bv1-r01-delta-tasks.js +0 -154
  60. package/scripts/bongos/seed-bv2-tweak-mode-tasks.js +0 -277
  61. package/scripts/bongos/seed-gds-v4-tasks.js +0 -624
  62. package/scripts/bongos/seed-governance-tasks.js +0 -340
  63. package/scripts/bongos/seed-medusa-tasks.js +0 -440
  64. package/scripts/bongos/seed-module-upstreaming.js +0 -219
  65. package/scripts/bongos/seed-productization-tasks.js +0 -235
  66. package/scripts/bongos/seed-project-admin-console-tasks.js +0 -88
  67. package/scripts/bongos/seed-provisioning-tasks.js +0 -83
  68. package/scripts/bongos/seed-strict-versioning-tasks.js +0 -275
  69. package/scripts/bongos/seed-v2-world-tasks.js +0 -283
  70. package/scripts/gds/artifact-staleness.js +0 -7
  71. package/scripts/gds/classify-discipline.js +0 -7
  72. package/scripts/gds/codemod-rename-src-gds.js +0 -7
  73. package/scripts/gds/coreB-carve-proof.sh +0 -8
  74. package/scripts/gds/goal-names.js +0 -7
  75. package/scripts/gds/rename-goals.js +0 -7
  76. package/scripts/gds/seed-adopt-tasks.js +0 -7
  77. package/scripts/gds/seed-api-contract-goal-tasks.js +0 -7
  78. package/scripts/gds/seed-board-room-tasks.js +0 -6
  79. package/scripts/gds/seed-bongos-app-tasks.js +0 -7
  80. package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +0 -7
  81. package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +0 -7
  82. package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +0 -7
  83. package/scripts/gds/seed-bongos-module-system-tasks.js +0 -7
  84. package/scripts/gds/seed-bongos-tasks.js +0 -7
  85. package/scripts/gds/seed-bongos-v1-seo-tasks.js +0 -6
  86. package/scripts/gds/seed-bv1-community-tasks.js +0 -6
  87. package/scripts/gds/seed-bv1-privacy-invites-tasks.js +0 -6
  88. package/scripts/gds/seed-bv1-profile-page-tasks.js +0 -6
  89. package/scripts/gds/seed-bv1-r01-delta-tasks.js +0 -6
  90. package/scripts/gds/seed-bv2-tweak-mode-tasks.js +0 -6
  91. package/scripts/gds/seed-gds-v4-tasks.js +0 -7
  92. package/scripts/gds/seed-governance-tasks.js +0 -7
  93. package/scripts/gds/seed-medusa-tasks.js +0 -7
  94. package/scripts/gds/seed-module-upstreaming.js +0 -7
  95. package/scripts/gds/seed-productization-tasks.js +0 -7
  96. package/scripts/gds/seed-project-admin-console-tasks.js +0 -6
  97. package/scripts/gds/seed-provisioning-tasks.js +0 -7
  98. package/scripts/gds/seed-strict-versioning-tasks.js +0 -7
  99. package/scripts/gds/seed-v2-world-tasks.js +0 -7
  100. package/scripts/migrate_cost_ledger.sh +0 -178
  101. package/tests/artifact_staleness.mjs +0 -207
  102. package/tests/goal_names.mjs +0 -120
  103. package/tests/newcomer_generator.mjs +0 -48
@@ -1,41 +1,33 @@
1
1
  #!/usr/bin/env node
2
- // Dependency vulnerability scan — single source of truth for both CI and the
3
- // droplet nightly cron (V3.R37 / #258).
2
+ // Dependency vulnerability scan — single source of truth for the CLI and the
3
+ // server's daily check (V3.R37 / #258; in-process since task 1004878).
4
4
  //
5
- // Two scans are run, in order:
6
- // 1. `npm audit --audit-level=high --json` against the root package.json.
7
- // 2. `python3 -m pip_audit -r art/pipeline/requirements.txt --format json`.
5
+ // One scan: `npm audit --audit-level=high --json` against the root package.json.
6
+ // (A second scan, pip-audit over art/pipeline/requirements.txt, went with the art
7
+ // pipeline it audited — task 1004860 deleted that pipeline, task 1004878 the scan.)
8
8
  //
9
- // The script parses both outputs and emits ONE deterministic JSON report on
10
- // stdout containing every high/critical finding. A short, builder-readable
11
- // summary goes to stderr so a CI log is skim-able.
9
+ // The script parses the output and emits ONE deterministic JSON report on stdout
10
+ // containing every high/critical finding. A short, builder-readable summary goes
11
+ // to stderr so a log is skim-able.
12
12
  //
13
13
  // Two modes:
14
14
  //
15
- // --report-mode=ci (default; used by .github/workflows/dep-audit.yml)
16
- // exit 1 if ANY finding is high or critical.
17
- // exit 0 if both scans clean.
15
+ // --report-mode=ci (default) exit 1 if ANY finding is high or critical,
16
+ // exit 0 if the scan is clean, exit 2 if npm is missing.
18
17
  //
19
- // --report-mode=nightly (used by .claude/scheduled-tasks/dep-audit-nightly)
20
- // ALWAYS exit 0 — a nightly cron must not block the
21
- // droplet's other routines. The skill consumes the
22
- // JSON, posts to the team chat-inbox, and creates a
23
- // vulnerability_report row in the Archon security
24
- // view via src/bongos/security.js.
18
+ // --report-mode=nightly ALWAYS exit 0 — the report is the product.
25
19
  //
26
- // Tool resolution:
27
- // - npm is required (CI image has it; droplet has it).
28
- // - pip-audit is resolved as `python3 -m pip_audit` (works whether installed
29
- // via pip, pipx, or pip --user — module form is the most portable).
30
- // If neither is found, the script reports `tool_missing: pip-audit` and the
31
- // mode decides whether to fail.
20
+ // WHERE IT RUNS ON A SCHEDULE. Nowhere through this CLI: the always-on server runs
21
+ // the same scan daily in-process (modules/autonomy/pollers/daily-checks.js), with an
22
+ // ASYNC spawn — spawnSync here would freeze every request for the whole scan — and
23
+ // hands the output to parseNpmAudit below, so the two cannot disagree about what a
24
+ // finding is.
32
25
  //
33
26
  // Output schema (stdout, always valid JSON, even on failure):
34
27
  // {
35
28
  // "ran_at": "<ISO timestamp>",
36
29
  // "mode": "ci" | "nightly",
37
30
  // "npm": { "ok": true|false, "tool_missing": false|true, "findings": [...] },
38
- // "pip": { "ok": true|false, "tool_missing": false|true, "findings": [...] },
39
31
  // "high_or_critical_count": <int>,
40
32
  // "exit_code": <int>
41
33
  // }
@@ -46,10 +38,8 @@
46
38
 
47
39
  const { spawnSync } = require('node:child_process');
48
40
  const path = require('node:path');
49
- const fs = require('node:fs');
50
41
 
51
42
  const REPO_ROOT = path.resolve(__dirname, '..', '..');
52
- const PIP_REQUIREMENTS = path.join(REPO_ROOT, 'art', 'pipeline', 'requirements.txt');
53
43
 
54
44
  // CLI args ----------------------------------------------------------------
55
45
 
@@ -77,33 +67,47 @@ function printHelp() {
77
67
 
78
68
  // npm audit ---------------------------------------------------------------
79
69
 
80
- // npm audit emits a JSON document. `--audit-level=high` controls the EXIT CODE
81
- // only — the JSON still contains every level. We parse the full set, then we
82
- // filter to high+critical ourselves so the threshold is consistent across both
83
- // scanners.
84
- function runNpmAudit() {
85
- const res = spawnSync('npm', ['audit', '--audit-level=high', '--json'], {
86
- cwd: REPO_ROOT,
70
+ // The npm invocation, shared with the server's async spawn. `--audit-level=high`
71
+ // controls the EXIT CODE only — the JSON still contains every level, and
72
+ // extractNpmFindings filters to high+critical itself.
73
+ const NPM_AUDIT_ARGS = ['audit', '--audit-level=high', '--json'];
74
+
75
+ function runNpmAudit({ cwd = REPO_ROOT } = {}) {
76
+ const res = spawnSync('npm', NPM_AUDIT_ARGS, {
77
+ cwd,
87
78
  encoding: 'utf8',
88
79
  maxBuffer: 32 * 1024 * 1024,
89
80
  });
90
- // npm audit exits 1 when findings >= the threshold, 0 otherwise. Both cases
91
- // still produce JSON on stdout. We don't trust the exit code; we read the
92
- // JSON and decide for ourselves.
93
81
  if (res.error) {
94
82
  return { ok: false, tool_missing: true, findings: [], raw_error: String(res.error.message || res.error) };
95
83
  }
84
+ return parseNpmAudit(res.stdout, res.stderr);
85
+ }
86
+
87
+ // parseNpmAudit — npm audit's raw output → { ok, tool_missing, findings }. Pure.
88
+ // npm audit exits 1 when findings >= the threshold, 0 otherwise, and both cases
89
+ // still produce JSON on stdout — so the exit code is never read; the JSON decides.
90
+ function parseNpmAudit(stdout, stderr = '') {
96
91
  let parsed;
97
92
  try {
98
- parsed = JSON.parse(res.stdout || '{}');
93
+ parsed = JSON.parse(stdout || '{}');
99
94
  } catch (e) {
100
95
  return {
101
96
  ok: false, tool_missing: false, findings: [],
102
- raw_error: `npm audit JSON parse failed: ${e.message}; stderr=${res.stderr.slice(0, 500)}`,
97
+ raw_error: `npm audit JSON parse failed: ${e.message}; stderr=${String(stderr || '').slice(0, 500)}`,
103
98
  };
104
99
  }
105
- const findings = extractNpmFindings(parsed);
106
- return { ok: true, tool_missing: false, findings };
100
+ // npm reports its own failures (no lockfile, registry unreachable) as JSON with
101
+ // an `error` object and no vulnerabilities. That is "could not audit", never a
102
+ // clean bill of health.
103
+ if (parsed && parsed.error && !parsed.vulnerabilities) {
104
+ const e = parsed.error;
105
+ return {
106
+ ok: false, tool_missing: false, findings: [],
107
+ raw_error: `npm audit failed: ${(e && (e.summary || e.code)) || 'unknown error'}`,
108
+ };
109
+ }
110
+ return { ok: true, tool_missing: false, findings: extractNpmFindings(parsed) };
107
111
  }
108
112
 
109
113
  // npm audit v7+ JSON shape: `vulnerabilities` is an object keyed by package
@@ -143,81 +147,6 @@ function extractNpmFindings(parsed) {
143
147
  return out;
144
148
  }
145
149
 
146
- // pip-audit ---------------------------------------------------------------
147
-
148
- function runPipAudit() {
149
- if (!fs.existsSync(PIP_REQUIREMENTS)) {
150
- return {
151
- ok: false, tool_missing: false, findings: [],
152
- raw_error: `requirements file not found: ${PIP_REQUIREMENTS}`,
153
- };
154
- }
155
- const res = spawnSync('python3', [
156
- '-m', 'pip_audit',
157
- '-r', PIP_REQUIREMENTS,
158
- '--format', 'json',
159
- '--vulnerability-service', 'pypi',
160
- ], {
161
- cwd: REPO_ROOT,
162
- encoding: 'utf8',
163
- maxBuffer: 32 * 1024 * 1024,
164
- });
165
- if (res.error) {
166
- return { ok: false, tool_missing: true, findings: [], raw_error: String(res.error.message || res.error) };
167
- }
168
- if (res.status !== 0 && res.status !== 1) {
169
- // pip-audit exits 0 on clean, 1 on findings. Anything else (missing module,
170
- // bad requirements file) is a tool problem.
171
- const stderr = (res.stderr || '').slice(0, 1000);
172
- const missing = /No module named ['"]?pip_audit/.test(stderr);
173
- return {
174
- ok: false, tool_missing: missing, findings: [],
175
- raw_error: `pip-audit exit=${res.status}; stderr=${stderr}`,
176
- };
177
- }
178
- let parsed;
179
- try {
180
- parsed = JSON.parse(res.stdout || '{}');
181
- } catch (e) {
182
- return {
183
- ok: false, tool_missing: false, findings: [],
184
- raw_error: `pip-audit JSON parse failed: ${e.message}; stderr=${res.stderr.slice(0, 500)}`,
185
- };
186
- }
187
- const findings = extractPipFindings(parsed);
188
- return { ok: true, tool_missing: false, findings };
189
- }
190
-
191
- // pip-audit JSON shape: { "dependencies": [ { "name", "version",
192
- // "vulns": [ { "id", "fix_versions": [], "description", "aliases": [] } ] } ] }
193
- // pip-audit does NOT classify severity itself — PyPI/OSV advisories carry it
194
- // in `aliases` (CVE IDs) plus a `description` field. To stay conservative, we
195
- // treat EVERY pip-audit finding as high — by the time PyPI's advisory DB has
196
- // picked it up, the project's posture is "fix or document." This is the same
197
- // stance the GitHub `pip-audit-action` takes by default.
198
- function extractPipFindings(parsed) {
199
- const out = [];
200
- const deps = (parsed && parsed.dependencies) || [];
201
- for (const dep of deps) {
202
- const vulns = Array.isArray(dep.vulns) ? dep.vulns : [];
203
- for (const v of vulns) {
204
- out.push({
205
- ecosystem: 'pypi',
206
- package: dep.name,
207
- version: dep.version,
208
- severity: 'high', // see note above
209
- id: v.id || (Array.isArray(v.aliases) && v.aliases[0]) || null,
210
- summary: (v.description || '').split('\n')[0].slice(0, 240) || `${dep.name} ${dep.version}: ${v.id}`,
211
- fix: Array.isArray(v.fix_versions) && v.fix_versions.length
212
- ? `upgrade to ${v.fix_versions.join(' / ')}`
213
- : 'no fix available',
214
- url: v.id ? `https://osv.dev/vulnerability/${v.id}` : null,
215
- });
216
- }
217
- }
218
- return out;
219
- }
220
-
221
150
  // Main --------------------------------------------------------------------
222
151
 
223
152
  function main() {
@@ -226,14 +155,13 @@ function main() {
226
155
 
227
156
  const ranAt = new Date().toISOString();
228
157
  const npm = runNpmAudit();
229
- const pip = runPipAudit();
230
158
 
231
- const allFindings = [...npm.findings, ...pip.findings];
159
+ const allFindings = npm.findings;
232
160
  const highCount = allFindings.filter(f => f.severity === 'high' || f.severity === 'critical').length;
233
161
 
234
162
  // CI mode: a tool being entirely missing in a CI environment is a build
235
163
  // problem, not a security problem — fail loudly so we don't ship blind.
236
- const toolProblem = (npm.tool_missing || pip.tool_missing) && args.mode === 'ci';
164
+ const toolProblem = npm.tool_missing && args.mode === 'ci';
237
165
 
238
166
  let exitCode = 0;
239
167
  if (toolProblem) exitCode = 2;
@@ -244,7 +172,6 @@ function main() {
244
172
  ran_at: ranAt,
245
173
  mode: args.mode,
246
174
  npm,
247
- pip,
248
175
  high_or_critical_count: highCount,
249
176
  findings: allFindings,
250
177
  exit_code: exitCode,
@@ -253,8 +180,7 @@ function main() {
253
180
  // Human summary to stderr.
254
181
  const lines = [];
255
182
  lines.push(`dep-audit [${args.mode}] @ ${ranAt}`);
256
- lines.push(` npm: ${npm.tool_missing ? 'TOOL MISSING' : (npm.ok ? `${npm.findings.length} high/critical` : `ERROR — ${npm.raw_error}`)}`);
257
- lines.push(` pip-audit: ${pip.tool_missing ? 'TOOL MISSING' : (pip.ok ? `${pip.findings.length} high/critical` : `ERROR — ${pip.raw_error}`)}`);
183
+ lines.push(` npm: ${npm.tool_missing ? 'TOOL MISSING' : (npm.ok ? `${npm.findings.length} high/critical` : `ERROR — ${npm.raw_error}`)}`);
258
184
  if (allFindings.length) {
259
185
  lines.push(` findings:`);
260
186
  for (const f of allFindings) {
@@ -271,6 +197,6 @@ function main() {
271
197
 
272
198
  if (require.main === module) main();
273
199
 
274
- // Exported for unit testing / for the nightly skill if it ever wants to drive
275
- // the scan in-process instead of via spawn.
276
- module.exports = { runNpmAudit, runPipAudit, extractNpmFindings, extractPipFindings };
200
+ // Exported for unit testing and for the server's in-process daily check, which
201
+ // spawns NPM_AUDIT_ARGS itself (asynchronously) and parses with parseNpmAudit.
202
+ module.exports = { runNpmAudit, parseNpmAudit, extractNpmFindings, NPM_AUDIT_ARGS };
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.176'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.177'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,201 @@
1
+ // tests/autonomy_daily_checks.mjs — the server's daily checks run once a day, file
2
+ // each distinct finding once, and never record "could not check" as clean
3
+ // (task 1004878).
4
+ //
5
+ // DB-free and network-free: the run ledger, the roster, npm and the idea inbox are
6
+ // all injected. The one thing these tests exist to stop is the failure every one of
7
+ // these routines already had — declared daily, run never — so the scheduling half
8
+ // (isDue against the ledger, not the boot) is pinned as hard as the filing half.
9
+ //
10
+ // Run: node --test tests/autonomy_daily_checks.mjs
11
+
12
+ import { test } from 'node:test';
13
+ import assert from 'node:assert/strict';
14
+ import { createRequire } from 'node:module';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const dc = require('../modules/autonomy/pollers/daily-checks.js');
18
+ const { SERVER_ROUTINES, DAY_MS, WEEK_MS } = require('../modules/autonomy/server-schedule.js');
19
+ const rt = require('../modules/autonomy/pollers/routine-timers.js');
20
+
21
+ const NOW = Date.parse('2026-10-26T12:00:00Z');
22
+ const hoursAgo = (h) => new Date(NOW - h * 3600_000).toISOString();
23
+ const LADDER = ['xenos', 'thetes', 'metic', 'archon'];
24
+
25
+ // A fake ledger: rows newest-first, plus a list of what got recorded.
26
+ function ledger(rows = []) {
27
+ const recorded = [];
28
+ return {
29
+ recorded,
30
+ db: {
31
+ readAutonomyRuns: async () => rows,
32
+ recordAutonomyRun: async (r) => { recorded.push(r); return {}; },
33
+ readRankRoster: async () => [
34
+ { id: 1, github_login: 'ok', rank: 'archon', status: 'active' },
35
+ { id: 2, github_login: 'old', rank: 'ephor', status: 'inactive' },
36
+ ],
37
+ },
38
+ };
39
+ }
40
+ const quiet = { log: () => {}, error: () => {} };
41
+ const capturing = () => {
42
+ const filed = [];
43
+ return { filed, captureIdea: async (i) => { filed.push(i); return { id: 42 }; } };
44
+ };
45
+
46
+ // ------------------------------------------------------------- the schedule table
47
+
48
+ test('the daily poller runs exactly the server-schedule rows that name it', () => {
49
+ assert.deepEqual(dc.ROUTINES, SERVER_ROUTINES.filter((r) => r.poller === 'daily-checks').map((r) => r.routine));
50
+ assert.deepEqual(dc.ROUTINES.sort(), ['dep-audit-nightly', 'rank-roles-audit']);
51
+ });
52
+
53
+ test('peer-vote-tally takes its period from the same table (weekly)', () => {
54
+ assert.equal(rt.PEER_VOTE_INTERVAL_MS, WEEK_MS);
55
+ });
56
+
57
+ // ------------------------------------------------------------------ isDue
58
+
59
+ test('due when it has never run', () => {
60
+ assert.equal(dc.isDue([], 'rank-roles-audit', NOW), true);
61
+ });
62
+
63
+ test('not due 20 hours after the last run, due 23.5 hours after — daily, not per boot', () => {
64
+ assert.equal(dc.isDue([{ created_at: hoursAgo(20) }], 'rank-roles-audit', NOW), false);
65
+ assert.equal(dc.isDue([{ created_at: hoursAgo(23.5) }], 'rank-roles-audit', NOW), true);
66
+ assert.equal(dc.DUE_SLACK_MS < DAY_MS / 24, true, 'slack must stay under one hourly tick or runs drift');
67
+ });
68
+
69
+ test('an ERROR row counts as a run too, so a broken check is retried daily, not hourly', async () => {
70
+ const L = ledger([{ decision: 'no-go', reason: 'error: x', created_at: hoursAgo(2) }]);
71
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, ...quiet });
72
+ assert.equal(out.skipped, 'not_due');
73
+ assert.equal(L.recorded.length, 0);
74
+ });
75
+
76
+ // --------------------------------------------------------------- filing
77
+
78
+ test('a finding is filed as one idea and the run records its fingerprint', async () => {
79
+ const L = ledger();
80
+ const C = capturing();
81
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: C.captureIdea, ...quiet });
82
+ assert.equal(out.outcome, 'filed');
83
+ assert.equal(C.filed.length, 1);
84
+ assert.match(C.filed[0].title, /^\[rank-roles\] 1 builder/);
85
+ assert.match(C.filed[0].bodyMd, /builder 2 \(old\) rank=ephor status=inactive/);
86
+ assert.equal(C.filed[0].kind, 'bug');
87
+ assert.equal(L.recorded[0].decision, 'go');
88
+ assert.match(L.recorded[0].reason, /^findings n=1 fp=[0-9a-f]{12} idea=42$/);
89
+ });
90
+
91
+ test('the SAME finding the next day is recorded unchanged and NOT filed again', async () => {
92
+ const fp = dc.fingerprint(['2:ephor']);
93
+ const L = ledger([{ decision: 'go', reason: `findings n=1 fp=${fp} idea=42`, created_at: hoursAgo(24) }]);
94
+ const C = capturing();
95
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: C.captureIdea, ...quiet });
96
+ assert.equal(out.outcome, 'unchanged');
97
+ assert.equal(C.filed.length, 0);
98
+ assert.match(L.recorded[0].reason, /unchanged$/);
99
+ });
100
+
101
+ test('an error between two identical findings does not cause a duplicate filing', async () => {
102
+ const fp = dc.fingerprint(['2:ephor']);
103
+ const L = ledger([
104
+ { decision: 'no-go', reason: 'error: db blip', created_at: hoursAgo(24) },
105
+ { decision: 'go', reason: `findings n=1 fp=${fp} idea=42`, created_at: hoursAgo(48) },
106
+ ]);
107
+ const C = capturing();
108
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: C.captureIdea, ...quiet });
109
+ assert.equal(out.outcome, 'unchanged');
110
+ assert.equal(C.filed.length, 0);
111
+ });
112
+
113
+ test('a finding that comes back after a clean day is filed again', async () => {
114
+ const L = ledger([{ decision: 'go', reason: 'clean', created_at: hoursAgo(24) }]);
115
+ const C = capturing();
116
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: C.captureIdea, ...quiet });
117
+ assert.equal(out.outcome, 'filed');
118
+ });
119
+
120
+ test('the fingerprint ignores order and changes when the set changes', () => {
121
+ assert.equal(dc.fingerprint(['a', 'b']), dc.fingerprint(['b', 'a']));
122
+ assert.notEqual(dc.fingerprint(['a']), dc.fingerprint(['a', 'b']));
123
+ });
124
+
125
+ test('a clean run files nothing and records clean', async () => {
126
+ const L = ledger();
127
+ L.db.readRankRoster = async () => [{ id: 1, rank: 'archon' }];
128
+ const C = capturing();
129
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: C.captureIdea, ...quiet });
130
+ assert.equal(out.outcome, 'clean');
131
+ assert.equal(C.filed.length, 0);
132
+ assert.deepEqual(L.recorded[0], { routine: 'rank-roles-audit', decision: 'go', reason: 'clean' });
133
+ });
134
+
135
+ test('no idea inbox (ideas module off) is recorded as undelivered, and retried next time', async () => {
136
+ const L = ledger();
137
+ const out = await dc.runCheck('rank-roles-audit', { db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: 'not a function', ...quiet });
138
+ assert.equal(out.outcome, 'undelivered');
139
+ assert.equal(L.recorded[0].decision, 'no-go', 'a no-go row is never read as delivered');
140
+ assert.equal(dc.lastFiledFingerprint(L.recorded.map((r) => ({ ...r }))), null);
141
+ });
142
+
143
+ test('a capture that throws is undelivered, never a crash', async () => {
144
+ const L = ledger();
145
+ const out = await dc.runCheck('rank-roles-audit', {
146
+ db: L.db, now: () => NOW, liveRanks: LADDER, captureIdea: async () => { throw { code: 'BAD_KIND' }; }, ...quiet,
147
+ });
148
+ assert.equal(out.outcome, 'undelivered');
149
+ assert.match(L.recorded[0].reason, /undelivered: BAD_KIND/);
150
+ });
151
+
152
+ // ------------------------------------------------------- could not check ≠ clean
153
+
154
+ test('npm missing is an ERROR row, not clean', async () => {
155
+ const L = ledger();
156
+ const out = await dc.runCheck('dep-audit-nightly', {
157
+ db: L.db, now: () => NOW, cwd: '/i', npmAudit: async () => ({ error: 'npm not found' }), ...quiet,
158
+ });
159
+ assert.equal(out.outcome, 'error');
160
+ assert.deepEqual(L.recorded[0], { routine: 'dep-audit-nightly', decision: 'no-go', reason: 'error: npm not found' });
161
+ });
162
+
163
+ test("npm's own JSON error (no lockfile) is an ERROR row, not clean", async () => {
164
+ const L = ledger();
165
+ const out = await dc.runCheck('dep-audit-nightly', {
166
+ db: L.db, now: () => NOW, cwd: '/i',
167
+ npmAudit: async () => ({ stdout: JSON.stringify({ error: { code: 'ENOLOCK', summary: 'This command requires an existing lockfile.' } }), stderr: '' }),
168
+ ...quiet,
169
+ });
170
+ assert.equal(out.outcome, 'error');
171
+ assert.match(L.recorded[0].reason, /lockfile/);
172
+ });
173
+
174
+ test('dep-audit audits the cwd it is handed and files high/critical findings only', async () => {
175
+ const L = ledger();
176
+ const C = capturing();
177
+ let auditedIn = null;
178
+ const out = await dc.runCheck('dep-audit-nightly', {
179
+ db: L.db, now: () => NOW, cwd: '/srv/instance', captureIdea: C.captureIdea, ...quiet,
180
+ npmAudit: async (cwd) => {
181
+ auditedIn = cwd;
182
+ return { stdout: JSON.stringify({ vulnerabilities: {
183
+ bad: { severity: 'critical', via: [{ severity: 'critical', title: 'RCE', url: 'https://x/1', source: 1 }], fixAvailable: true },
184
+ meh: { severity: 'moderate', via: [], fixAvailable: true },
185
+ } }), stderr: '' };
186
+ },
187
+ });
188
+ assert.equal(auditedIn, '/srv/instance');
189
+ assert.equal(out.outcome, 'filed');
190
+ assert.match(C.filed[0].title, /^\[dep-audit\] 1 high\/critical vulnerability in/);
191
+ assert.match(C.filed[0].bodyMd, /\*\*bad\*\* \(critical\) — RCE/);
192
+ assert.doesNotMatch(C.filed[0].bodyMd, /meh/);
193
+ });
194
+
195
+ // --------------------------------------------------------------- the tick
196
+
197
+ test('an unreachable database is a quiet skip for every routine (a builder laptop)', async () => {
198
+ const db = { readAutonomyRuns: async () => { throw new Error('connect ENOENT /var/run/postgresql'); } };
199
+ const out = await dc.tick({ db, ...quiet });
200
+ assert.deepEqual(out.map((r) => r.skipped), dc.ROUTINES.map(() => 'db_unreachable'));
201
+ });
@@ -177,7 +177,7 @@ t('materializeClaude: materializes scheduled-tasks — bodies→bongos, companio
177
177
  assert.ok(existsSync(rt), '.claude/scheduled-tasks created in the instance');
178
178
  // Every materialized routine's core invocation is bongosified — asserted over
179
179
  // WHATEVER routines the tree carries (the neutral core carries redteam-weekly;
180
- // the founder core also carries stale-claim-release/diagram-drift). No routine
180
+ // the founder core also carries rank-roles-audit/peer-vote-tally). No routine
181
181
  // body may keep a bare `node scripts/bongos/…` core invocation.
182
182
  let sawBongosified = false;
183
183
  for (const name of readdirSync(rt)) {
@@ -191,17 +191,17 @@ t('materializeClaude: materializes scheduled-tasks — bodies→bongos, companio
191
191
  // Founder-specific fixtures — assert their finer behavior only when present, so
192
192
  // this coverage stays live on the founder core without demanding those routines
193
193
  // exist in the neutral core (which carries a different routine set).
194
- const stalePath = join(rt, 'stale-claim-release', 'SKILL.md');
195
- if (existsSync(stalePath)) {
196
- assert.ok(readFileSync(stalePath, 'utf8').includes('bongos exec scripts/bongos/pulse.js'), 'body core-invocation rewritten to bongos exec');
197
- }
198
- const ddCheck = join(rt, 'diagram-drift', 'check.js');
199
- if (existsSync(ddCheck)) {
200
- const dd = readFileSync(join(rt, 'diagram-drift', 'SKILL.md'), 'utf8');
201
- assert.ok(dd.includes('node .claude/scheduled-tasks/diagram-drift/check.js'), 'companion-path step command preserved');
202
- const check = readFileSync(ddCheck, 'utf8');
203
- assert.ok(check.includes("require('../../hooks/_core-resolve.js')"), 'companion resolves core via _core-resolve');
204
- assert.ok(!check.includes("require('../../../scripts/bongos/"), 'no bare ../../../scripts/bongos require left in the companion');
194
+ // The companion-script case (task 1004878 moved this off the retired
195
+ // diagram-drift routine): a routine whose step runs a .js beside its SKILL.md.
196
+ const pvTally = join(rt, 'peer-vote-tally', 'tally.js');
197
+ if (existsSync(pvTally)) {
198
+ const pv = readFileSync(join(rt, 'peer-vote-tally', 'SKILL.md'), 'utf8');
199
+ assert.ok(pv.includes('node .claude/scheduled-tasks/peer-vote-tally/tally.js'), 'companion-path step command preserved');
200
+ const tally = readFileSync(pvTally, 'utf8');
201
+ assert.ok(tally.includes("require('../../hooks/_core-resolve.js')"), 'companion resolves core via _core-resolve');
202
+ // Code lines only: tally.js's own header QUOTES the bare form to explain why it is gone.
203
+ const code = tally.split('\n').filter((l) => !/^\s*\/\//.test(l)).join('\n');
204
+ assert.ok(!code.includes("require('../../../scripts/bongos/"), 'no bare ../../../scripts/bongos require left in the companion');
205
205
  }
206
206
  rmSync(dir, { recursive: true, force: true });
207
207
  });
@@ -145,24 +145,6 @@ const SPELLING_EXEMPTIONS = [
145
145
  obligation: (code) => !/\.query\s*\(/.test(code) && !/require\(\s*'pg'\s*\)/.test(code),
146
146
  demands: 'issues no database query and loads no database driver — it answers from fixtures only',
147
147
  },
148
- {
149
- file: 'scripts/bongos/seed-bv1-privacy-invites-tasks.js',
150
- allow: [
151
- { text: "profile_state='", count: 1, why: 'a one-shot historical seeder: this sits inside a task DESCRIPTION string, quoting the predicate as prose describing the work to be done. Its subject IS this predicate.' },
152
- { text: 'NOT hide_stats', count: 1, why: 'the second half of that same quoted sentence in that same task description — prose, not a query.' },
153
- ],
154
- obligation: (code) => !/\.query\s*\(/.test(code),
155
- demands: 'issues no database query at all — it posts task rows over HTTP',
156
- },
157
- {
158
- file: 'scripts/bongos/seed-bv1-r01-delta-tasks.js',
159
- allow: [
160
- { text: "profile_state='", count: 1, why: 'the ADR 0171 R01 seeder: the predicate is quoted as prose inside the task briefs this script files, the same shape as the privacy seeder above.' },
161
- { text: 'NOT hide_stats', count: 1, why: 'the second half of that same quoted sentence in that same brief — prose describing work, never executed.' },
162
- ],
163
- obligation: (code) => !/\.query\s*\(/.test(code),
164
- demands: 'issues no database query at all — it posts task rows over HTTP',
165
- },
166
148
  ];
167
149
 
168
150
  // Every SELECT against platform_identity_accounts that is deliberately NOT a
@@ -2,6 +2,7 @@
2
2
  // kind the server accepts (task 1003694).
3
3
  //
4
4
  // THE FAILURE THIS PINS. .claude/scheduled-tasks/dep-audit-nightly/SKILL.md filed
5
+ // (before task 1004878 moved its filing into the server's daily-checks poller)
5
6
  // with `--kind security`. That is not one of the kinds — captureIdea throws BAD_KIND
6
7
  // and the route 400s — so the capture step of that nightly routine had NEVER filed
7
8
  // anything.
@@ -137,11 +138,21 @@ test('a capture command wrapped over lines is still read', () => {
137
138
  'dep-audit-nightly writes exactly this shape — a line-at-a-time matcher misses it');
138
139
  });
139
140
 
140
- test('dep-audit-nightly specifically now files with an accepted kind (task 1003694)', () => {
141
- const p = path.join(REPO_ROOT, '.claude', 'scheduled-tasks', 'dep-audit-nightly', 'SKILL.md');
142
- const src = readFileSync(p, 'utf8');
143
- assert.ok(!/--kind\s+security\b/.test(src), 'security is not a kind and never was');
144
- const kinds = literalKinds(src);
145
- assert.ok(kinds.length > 0, 'the routine must still file something');
146
- for (const k of kinds) assert.ok(VALID_KINDS.includes(k), `${k} is not a valid kind`);
141
+ test('dep-audit-nightly specifically now files with an accepted kind (task 1003694)', async () => {
142
+ // Since task 1004878 the routine files from the server's daily-checks poller, not
143
+ // from a capture command in its SKILL.md — so the kind to check is the one the
144
+ // poller hands ideas.capture. Driven, not grepped: a stub npm reports one finding.
145
+ const { runCheck } = require('../modules/autonomy/pollers/daily-checks.js');
146
+ const filed = [];
147
+ const out = await runCheck('dep-audit-nightly', {
148
+ db: { readAutonomyRuns: async () => [], recordAutonomyRun: async () => ({}) },
149
+ cwd: '/instance',
150
+ npmAudit: async () => ({ stdout: JSON.stringify({ vulnerabilities: { x: { severity: 'high', via: [], fixAvailable: true } } }), stderr: '' }),
151
+ captureIdea: async (idea) => { filed.push(idea); return { id: 1 }; },
152
+ log: () => {}, error: () => {},
153
+ });
154
+ assert.equal(out.outcome, 'filed');
155
+ assert.equal(filed.length, 1);
156
+ assert.notEqual(filed[0].kind, 'security', 'security is not a kind and never was');
157
+ assert.ok(VALID_KINDS.includes(filed[0].kind), `${filed[0].kind} is not a valid kind`);
147
158
  });
@@ -234,3 +234,45 @@ test('LIVE: the patrol workflow installs gitleaks, which the history scan cannot
234
234
  assert.match(wf, /sha256sum -c -/, 'the binary must be checksum-verified (the tj-actions lesson)');
235
235
  assert.match(wf, /fetch-depth: 0/, 'a shallow clone makes the history scan a confident no-op');
236
236
  });
237
+
238
+ // ------------------------------------------------- the server scheduler (task 1004878)
239
+
240
+ const SRV = [{ routine: 'nightly-thing', cadence: 'daily' }];
241
+
242
+ test('a server row fires its routine: declared daily + server daily is green with NO workflow', () => {
243
+ const r = rs.evaluate(routines([{ name: 'nightly-thing', cadence: 'daily', steps: [] }]), [], SRV);
244
+ assert.deepEqual(r.violations, []);
245
+ });
246
+
247
+ test('without the server row the same routine is dormant — the row is what makes it green', () => {
248
+ const v = rs.evaluate(routines([{ name: 'nightly-thing', cadence: 'daily', steps: [] }]), [], []).violations;
249
+ assert.match(v[0], /NOTHING fires it/);
250
+ });
251
+
252
+ test('a server row at a different period than the routine declares is red', () => {
253
+ const v = rs.evaluate(routines([{ name: 'nightly-thing', cadence: 'weekly', steps: [] }]), [], SRV).violations;
254
+ assert.match(v[0], /declares cadence: weekly but the server fires it daily/);
255
+ });
256
+
257
+ test('the server firing a routine whose SKILL.md declares no cadence is red, not a quiet warning', () => {
258
+ const r = rs.evaluate(routines([], [{ name: 'nightly-thing' }]), [], SRV);
259
+ assert.match(r.violations[0], /fired daily by the server but its SKILL\.md declares no cadence/);
260
+ assert.equal(r.warnings.length, 0, 'the same routine must not ALSO read as manual-only');
261
+ });
262
+
263
+ test('a server row naming a routine that is gone is red (rename rot)', () => {
264
+ const v = rs.evaluate(routines(WEEKLY), [WEEKLY_WF], SRV).violations;
265
+ assert.ok(v.some((x) => /fires routine 'nightly-thing', which has no/.test(x)), v.join(' | '));
266
+ });
267
+
268
+ test('with no routine tree at all (an instance before materialize) server rows are not dangling', () => {
269
+ assert.deepEqual(rs.evaluate(routines([]), null, SRV).violations, []);
270
+ });
271
+
272
+ test('LIVE: every server-fired routine declares the cadence the server realizes', () => {
273
+ const { SERVER_ROUTINES } = require(path.join(ROOT, 'modules', 'autonomy', 'server-schedule.js'));
274
+ const declared = new Map(rs.readRoutines().withCadence.map((r) => [r.name, r.cadence]));
275
+ for (const s of SERVER_ROUTINES) {
276
+ assert.equal(declared.get(s.routine), s.cadence, `${s.routine}: SKILL.md cadence vs server-schedule.js`);
277
+ }
278
+ });