@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.
- package/.bongos-core.json +61 -361
- package/.claude/skills/planning-session/SKILL.md +2 -2
- package/config/scheduled-routines.json +3 -8
- package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +1 -1
- package/docs/adr/0093-tranche-2-core-carve-sequence.md +1 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/file-map.md +3 -8
- package/docs/handoff-template.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/onboarding/diagrams/README.md +3 -7
- package/docs/onboarding/diagrams/assertions.json +3 -3
- package/docs/recipes/ops-gotchas.md +3 -1
- package/modules/autonomy/db.js +9 -0
- package/modules/autonomy/module.json +2 -2
- package/modules/autonomy/pollers/daily-checks.js +262 -0
- package/modules/autonomy/pollers/routine-timers.js +7 -5
- package/modules/autonomy/rank-roles.js +64 -0
- package/modules/autonomy/server-schedule.js +37 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +14 -0
- package/scripts/bongos/api-alias-caller-scan.js +2 -2
- package/scripts/bongos/audit-authorship.sh +0 -5
- package/scripts/bongos/audit-rank-roles.js +15 -56
- package/scripts/bongos/codemod-rename-scripts-gds.js +0 -1
- package/scripts/bongos/diagram-facts.js +1 -1
- package/scripts/bongos/do-api.js +1 -1
- package/scripts/bongos/fitness-checks-identity.js +4 -15
- package/scripts/bongos/gen-diagrams.js +3 -3
- package/scripts/bongos/pulse.js +5 -3
- package/scripts/bongos/routine-schedule.js +38 -3
- package/scripts/bongos/ship-regen.js +2 -3
- package/scripts/security/dep-audit.js +50 -124
- package/src/module-api.js +1 -1
- package/tests/autonomy_daily_checks.mjs +201 -0
- package/tests/claude_materialize.mjs +12 -12
- package/tests/effective_visibility_predicate.mjs +0 -18
- package/tests/routine_capture_kind.mjs +18 -7
- package/tests/routine_schedule.mjs +42 -0
- package/scripts/bongos/artifact-staleness.js +0 -267
- package/scripts/bongos/classify-discipline.js +0 -153
- package/scripts/bongos/codemod-rename-src-gds.js +0 -145
- package/scripts/bongos/coreB-carve-proof.sh +0 -725
- package/scripts/bongos/goal-names.js +0 -102
- package/scripts/bongos/rename-goals.js +0 -109
- package/scripts/bongos/seed-adopt-tasks.js +0 -75
- package/scripts/bongos/seed-api-contract-goal-tasks.js +0 -141
- package/scripts/bongos/seed-board-room-tasks.js +0 -196
- package/scripts/bongos/seed-bongos-app-tasks.js +0 -294
- package/scripts/bongos/seed-bongos-coreB-tranche1-tasks.js +0 -81
- package/scripts/bongos/seed-bongos-coreB-tranche2-tasks.js +0 -100
- package/scripts/bongos/seed-bongos-goal-hierarchy-tasks.js +0 -307
- package/scripts/bongos/seed-bongos-module-system-tasks.js +0 -346
- package/scripts/bongos/seed-bongos-tasks.js +0 -214
- package/scripts/bongos/seed-bongos-v1-seo-tasks.js +0 -104
- package/scripts/bongos/seed-bv1-community-tasks.js +0 -131
- package/scripts/bongos/seed-bv1-privacy-invites-tasks.js +0 -133
- package/scripts/bongos/seed-bv1-profile-page-tasks.js +0 -161
- package/scripts/bongos/seed-bv1-r01-delta-tasks.js +0 -154
- package/scripts/bongos/seed-bv2-tweak-mode-tasks.js +0 -277
- package/scripts/bongos/seed-gds-v4-tasks.js +0 -624
- package/scripts/bongos/seed-governance-tasks.js +0 -340
- package/scripts/bongos/seed-medusa-tasks.js +0 -440
- package/scripts/bongos/seed-module-upstreaming.js +0 -219
- package/scripts/bongos/seed-productization-tasks.js +0 -235
- package/scripts/bongos/seed-project-admin-console-tasks.js +0 -88
- package/scripts/bongos/seed-provisioning-tasks.js +0 -83
- package/scripts/bongos/seed-strict-versioning-tasks.js +0 -275
- package/scripts/bongos/seed-v2-world-tasks.js +0 -283
- package/scripts/gds/artifact-staleness.js +0 -7
- package/scripts/gds/classify-discipline.js +0 -7
- package/scripts/gds/codemod-rename-src-gds.js +0 -7
- package/scripts/gds/coreB-carve-proof.sh +0 -8
- package/scripts/gds/goal-names.js +0 -7
- package/scripts/gds/rename-goals.js +0 -7
- package/scripts/gds/seed-adopt-tasks.js +0 -7
- package/scripts/gds/seed-api-contract-goal-tasks.js +0 -7
- package/scripts/gds/seed-board-room-tasks.js +0 -6
- package/scripts/gds/seed-bongos-app-tasks.js +0 -7
- package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +0 -7
- package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +0 -7
- package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +0 -7
- package/scripts/gds/seed-bongos-module-system-tasks.js +0 -7
- package/scripts/gds/seed-bongos-tasks.js +0 -7
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +0 -6
- package/scripts/gds/seed-bv1-community-tasks.js +0 -6
- package/scripts/gds/seed-bv1-privacy-invites-tasks.js +0 -6
- package/scripts/gds/seed-bv1-profile-page-tasks.js +0 -6
- package/scripts/gds/seed-bv1-r01-delta-tasks.js +0 -6
- package/scripts/gds/seed-bv2-tweak-mode-tasks.js +0 -6
- package/scripts/gds/seed-gds-v4-tasks.js +0 -7
- package/scripts/gds/seed-governance-tasks.js +0 -7
- package/scripts/gds/seed-medusa-tasks.js +0 -7
- package/scripts/gds/seed-module-upstreaming.js +0 -7
- package/scripts/gds/seed-productization-tasks.js +0 -7
- package/scripts/gds/seed-project-admin-console-tasks.js +0 -6
- package/scripts/gds/seed-provisioning-tasks.js +0 -7
- package/scripts/gds/seed-strict-versioning-tasks.js +0 -7
- package/scripts/gds/seed-v2-world-tasks.js +0 -7
- package/scripts/migrate_cost_ledger.sh +0 -178
- package/tests/artifact_staleness.mjs +0 -207
- package/tests/goal_names.mjs +0 -120
- 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
|
|
3
|
-
//
|
|
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
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
|
16
|
-
// exit
|
|
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
|
|
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
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
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
|
|
81
|
-
// only — the JSON still contains every level
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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(
|
|
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=${
|
|
97
|
+
raw_error: `npm audit JSON parse failed: ${e.message}; stderr=${String(stderr || '').slice(0, 500)}`,
|
|
103
98
|
};
|
|
104
99
|
}
|
|
105
|
-
|
|
106
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
275
|
-
//
|
|
276
|
-
module.exports = { runNpmAudit,
|
|
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.
|
|
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
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
assert.ok(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
assert.ok(!
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
});
|