@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
@@ -303,7 +303,7 @@ Once the spec is finalized (Phase 4), **read `docs/specs/<ver>-<goal-slug>.md` a
303
303
  - **For a brand-new version scaffolded from scratch (DR / a fresh instance):** put the `INSERT INTO done_when_criteria` (with `goal_id`, `ON CONFLICT ... DO UPDATE`) in **`migrations/instance/<NN>_<ver_slug>_planning_seed.sql`** — and recreate the goal idempotently in the same file (the goal is a runtime row a replay won't have). Applies only under `GDS_APPLY_INSTANCE_SEEDS=1`.
304
304
  - **Migration** at `migrations/<NN>_<ver_slug>_planning_seed.sql` (CORE — platform-universal, non-scope-truth changes only): `UPDATE versions SET done_when = ...` and `UPDATE tasks` for existing-task priority + V##.R## title prefix (guarded by `WHERE title NOT LIKE 'V<ver>.R%'`). **Do NOT put versions/goals/`done_when_criteria` INSERTs here** — that's scope-truth and fails the `selfhost-boot` gate (ADR 0105).
305
305
 
306
- 3. **Write a seed script** at `scripts/bongos/seed-<ver_slug>-tasks.js` modeled on `scripts/bongos/seed-v3-tasks.js` containing:
306
+ 3. **Write a seed script** at `scripts/bongos/seed-<ver_slug>-tasks.js` modeled on `scripts/bongos/<redacted>.js` (or `<redacted>.js`) containing:
307
307
  - The TASKS array — one entry per net-new task with full description, kind, discipline, priority, est_minutes, manual_degree, credits_reward, **optional** `touches` (see below), optional `promoted_idea_id`, `depends_on_refs: ['R##', ...]` (the dep edges captured during brainstorm), AND **`criterion_ids: ['Cn', ...]`** — the done-when criterion(s) this task gates. The brainstorm in Phase 3 is already grouped by criterion, so this is just transcribing that grouping. **Use the criterion_id SLUG, not a positional `"Cn"` token.** `Cn` ranks over the whole VERSION's criteria, so on a version holding more than one goal it resolves to some other goal's criterion — which is how all 15 tasks of goal 1000071 were mislinked while the seed printed success (learning 1000188). Since task 1003108 the server **refuses** a positional ref on a multi-goal version (`400 ambiguous_criterion_ref`) rather than guessing, so a slug is the only ref that works there. The server resolves it against the task's version at create (ADR 0025 / task 438). **This replaces the old `**Criterion:** Cn` prose** — don't write that line anymore; pass `criterion_ids` instead so the task is linked in `task_criteria` at creation and `/status` counts it with no backfill.
308
308
  - **`touches` is now OPTIONAL (task 882 / [ADR 0049](../../../docs/adr/0049-split-parallel-safety-contract.md)).** Omit it (or leave it blank) unless you already know a tight file set and want the advisory overlap signal during the brainstorm. At first `/builder-ship` the server backfills `touches[]` from the real committed diff — the declared set becomes what git actually reported. Don't burn planning effort predicting file lists; a rough or absent prediction is fine and no longer ship-blocking.
309
309
  - **`requires_rank` AUTO-DERIVES — you don't hand-set it per task (task [#1498](https://example.com/builders#/task/1498) / [ADR 0084](../../../docs/adr/0084-auto-derived-per-task-rank-floor.md)).** On create the server floors a task at `metic` when it is `security_sensitive` or its `touches` reach the permission / authz / ship-grade / deploy / migration core (the same protected paths a sub-Metic builder can't push to); everything else stays the open `xenos` default. To deliberately gate a task **higher** than the auto-floor (e.g. owner-only work), pass `requires_rank: 'metic' | 'archon'` on the task — it can only RAISE the floor, never drop below it.
@@ -395,7 +395,7 @@ node scripts/bongos/api.js GET /api/bongos/inbox
395
395
  node scripts/bongos/api.js GET /api/bongos/versions/<id>/done-when
396
396
  ```
397
397
 
398
- Phase 5 writes use the same `POST /tasks`, `PATCH /inbox/:id` shape as `idea-triage` and `seed-v3-tasks.js`. Mirror those patterns rather than inventing new ones.
398
+ Phase 5 writes use the same `POST /tasks`, `PATCH /inbox/:id` shape as `idea-triage` and `scripts/bongos/<redacted>.js`. Mirror those patterns rather than inventing new ones.
399
399
 
400
400
  ## Tone for the user
401
401
 
@@ -3,25 +3,20 @@
3
3
  "defaults": { "scope": "instance-wide", "enabled": true, "requiresAutonomy": false },
4
4
  "routines": {
5
5
  "artifact-audit": { "scope": "instance-wide", "enabled": true, "cadence": "weekly", "requiresAutonomy": false, "note": "deterministic; task 1003200. Reads main's own history + tree, so it runs in CI (a vendored core is not a git repo) — .github/workflows/artifact-audit.yml." },
6
- "artifact-staleness": { "scope": "instance-wide", "enabled": true, "cadence": "daily", "requiresAutonomy": false, "note": "deterministic; task 1001930 / ADR 0110 pt.5. INSTANCE-SIDE ONLY, and the opposite of artifact-audit above: it reads the gitignored deploy-generated files on the serving machine's disk, which a CI checkout legitimately does not have — so it declares no cadence in its SKILL.md and no workflow fires it. This cadence is the hint the operator's own scheduler reads." },
7
6
  "bfg": { "scope": "instance-wide", "builderAware": true, "enabled": true, "cadence": "nightly", "requiresAutonomy": false, "note": "Parts A+B read-only + always-on; Part C (cross-builder write) is separately gated by BFG_WRITE_ENABLED + a provisioned principal token." },
8
7
  "bfg-session-eval": { "scope": "instance-wide", "builderAware": true, "enabled": true, "cadence": "nightly", "requiresAutonomy": false },
9
8
  "core-update-subscription": { "scope": "instance-wide", "enabled": true, "cadence": "15min", "requiresAutonomy": true, "note": "Patch-only auto-upgrade of subscribed consumer instances (ADR 0136 / task 2150). Default OFF + opt-in: the roster (config/update-subscriptions.json) ships empty, so this is a no-op until an operator arms autonomy AND lists an instance. Every upgrade is health-gated with auto-rollback (task 2149). CADENCE (task 1003674): this is the knob that decides how long a published release waits before a subscribed instance takes it — at 'daily' a builder's ship reached the live site up to 24h late, because everything upstream is already fast (CI publishes to npm within ~2 min of merge) and this sweep was the only slow step. 15min is the worst-case lag, not a re-check cost: the sweep exits immediately when the registry has nothing newer. FLOOR ~5min: an upgrading run takes ~2 min, and runs must not overlap. Installing the period is the operator's half — see docs/recipes/upgrading-the-core.md." },
10
- "dep-audit-nightly": { "scope": "instance-wide", "enabled": true, "cadence": "nightly", "requiresAutonomy": false },
11
- "diagram-drift": { "scope": "instance-wide", "enabled": true, "cadence": "daily", "requiresAutonomy": false },
9
+ "dep-audit-nightly": { "scope": "instance-wide", "enabled": true, "cadence": "daily", "requiresAutonomy": false, "note": "deterministic; task 1004878. Fired by the in-process daily-checks poller (modules/autonomy/pollers/daily-checks.js): npm audit of the deployed instance, high/critical findings filed as ONE idea, re-filed only when the set changes." },
12
10
  "idea-triage-nightly": { "scope": "instance-wide", "enabled": false, "cadence": "nightly", "requiresAutonomy": true },
13
11
  "main-audit": { "scope": "instance-wide", "enabled": true, "cadence": "on-merge", "requiresAutonomy": false, "note": "Trust-boundary backstop — audits commits landed on main." },
14
- "newcomer-floor": { "scope": "instance-wide", "enabled": true, "cadence": "daily", "requiresAutonomy": false },
15
- "newcomer-generator": { "scope": "instance-wide", "enabled": true, "cadence": "daily", "requiresAutonomy": false },
16
12
  "overnight-builder": { "scope": "instance-wide", "enabled": false, "cadence": "nightly", "requiresAutonomy": true, "note": "Autonomous claim+ship; OFF until the instance arms autonomy." },
17
13
  "overnight-code-review": { "scope": "instance-wide", "enabled": false, "cadence": "nightly", "requiresAutonomy": true },
18
- "peer-vote-tally": { "scope": "instance-wide", "enabled": true, "cadence": "daily", "requiresAutonomy": false },
19
- "rank-roles-audit": { "scope": "instance-wide", "enabled": true, "cadence": "nightly", "requiresAutonomy": false, "note": "deterministic; read-only; never fatal at boot (idea 1000622) — task 1002873" },
14
+ "peer-vote-tally": { "scope": "instance-wide", "enabled": true, "cadence": "weekly", "requiresAutonomy": false, "note": "deterministic; fired weekly by the in-process routine-timers poller (modules/autonomy/pollers/routine-timers.js)." },
15
+ "rank-roles-audit": { "scope": "instance-wide", "enabled": true, "cadence": "daily", "requiresAutonomy": false, "note": "deterministic; read-only; never fatal at boot (idea 1000622) — task 1002873. Fired by the in-process daily-checks poller (task 1004878)." },
20
16
  "redteam-weekly": { "scope": "instance-wide", "enabled": true, "cadence": "weekly", "requiresAutonomy": false, "note": "deterministic; task 1003128" },
21
17
  "repo-simplify-sweep": { "scope": "instance-wide", "enabled": true, "cadence": "weekly", "requiresAutonomy": false },
22
18
  "security-review-weekly": { "scope": "instance-wide", "enabled": true, "cadence": "weekly", "requiresAutonomy": false },
23
19
  "skill-usage-review": { "scope": "instance-wide", "enabled": true, "cadence": "nightly", "requiresAutonomy": false },
24
- "stale-claim-release": { "scope": "instance-wide", "enabled": true, "cadence": "hourly", "requiresAutonomy": false },
25
20
  "stranded-rows": { "scope": "instance-wide", "enabled": true, "requiresAutonomy": false, "note": "no cadence: nothing schedules it yet, so it is run by hand — declaring one with no cron firing it fails fitness Check 25 (task 1003775)" }
26
21
  }
27
22
  }
@@ -84,7 +84,7 @@ This ADR's deliverable is the decision above **plus** the carve broken into clai
84
84
 
85
85
  **Proof of equivalence at every step** (ADR 0083 / 0062 discipline): after each carve, OTB behaves **byte-for-byte the same**. That is the per-task done-when.
86
86
 
87
- **Tranche-1 status: PROVEN** (R74, [#1578](https://example.com/builders#/task/1578)). The five claims above are demonstrated, in one re-runnable command, by [`scripts/gds/coreB-carve-proof.sh`](../../scripts/gds/coreB-carve-proof.sh): memory + grading are loader-discovered modules; `db-kernel.js` + the memory query slice left the `db.js` monolith; `KERNEL_FILES` is the bounded 17-file roster with zero fitness hard-failures (incl. the three ENFORCING C7 boundary checks); grading resolves through the `grade` kernel port (`seams.resolveOptional('grade')`) with no core→grader import; and the 27 DB-free tests exercising the carved surfaces pass unchanged (byte-identical). The ongoing guard is CI — `fitness.js` (the kernel-imports-no-domain ratchet flips red at zero leaks) + `run-unit-tests.js` on every PR; the script is the consolidated demonstration, not a new gate. Tranche 2 (the remaining ~8 core domains) stays deferred per owner until greenlit.
87
+ **Tranche-1 status: PROVEN** (R74, [#1578](https://example.com/builders#/task/1578)). The five claims above are demonstrated, in one re-runnable command, by `scripts/gds/coreB-carve-proof.sh` (deleted once the carve was done, task 1004856; in history at dc7d88169): memory + grading are loader-discovered modules; `db-kernel.js` + the memory query slice left the `db.js` monolith; `KERNEL_FILES` is the bounded 17-file roster with zero fitness hard-failures (incl. the three ENFORCING C7 boundary checks); grading resolves through the `grade` kernel port (`seams.resolveOptional('grade')`) with no core→grader import; and the 27 DB-free tests exercising the carved surfaces pass unchanged (byte-identical). The ongoing guard is CI — `fitness.js` (the kernel-imports-no-domain ratchet flips red at zero leaks) + `run-unit-tests.js` on every PR; the script is the consolidated demonstration, not a new gate. Tranche 2 (the remaining ~8 core domains) stays deferred per owner until greenlit.
88
88
 
89
89
  ---
90
90
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  ## Context
12
12
 
13
- Tranche 1 ([ADR 0091](0091-bounding-the-kernel-and-db-carve.md)) did two things: it **proved the core-carve pattern** on the two cleanest-slicing domains (memory, grading — demonstrated by [`scripts/gds/coreB-carve-proof.sh`](../../scripts/gds/coreB-carve-proof.sh), R74), and it **shipped the infrastructure** every later carve reuses — the bounded kernel roster + `KERNEL_FILES` fitness ratchet (R70), the doorway with `withTx` (R68), the per-domain `db.js` slice pattern + `db-kernel.js` (R69), and the `grade` kernel **port** (R73). That infrastructure is generic: each remaining core domain is now a *pure carve* copying R72/R73, with **no new scaffolding**.
13
+ Tranche 1 ([ADR 0091](0091-bounding-the-kernel-and-db-carve.md)) did two things: it **proved the core-carve pattern** on the two cleanest-slicing domains (memory, grading — demonstrated by `scripts/gds/coreB-carve-proof.sh` (deleted once the carve was done, task 1004856; in history at dc7d88169), R74), and it **shipped the infrastructure** every later carve reuses — the bounded kernel roster + `KERNEL_FILES` fitness ratchet (R70), the doorway with `withTx` (R68), the per-domain `db.js` slice pattern + `db-kernel.js` (R69), and the `grade` kernel **port** (R73). That infrastructure is generic: each remaining core domain is now a *pure carve* copying R72/R73, with **no new scaffolding**.
14
14
 
15
15
  What remains is the bulk of the core: ~8 cohesive domains + the two web UIs + the lifecycle state machine ([02 §6](../design/modular-architecture/02-module-map.md) roster). The import-graph scan ([02 §2](../design/modular-architecture/02-module-map.md)) already told us which slice cleanly (economy, ideas, security alongside the proven memory/grading) and which is the coupled cluster (the lifecycle — `tasks → grader/github-push/ship-broadcast`). This ADR turns that evidence into a sequence.
16
16
 
@@ -94,7 +94,7 @@ forming the ordered claim-gate tier **`xenos` (1) < `thetes` (2) < `metic` (3) <
94
94
  deliberate Archon promotion.
95
95
  - **`xenos`** — the **sandboxed entry rank** every new GitHub-authed builder defaults to (migration
96
96
  029 flipped the column default here). Read-only on public surfaces; may claim + ship **only** tasks
97
- an Archon (or the newcomer-generator) has flagged `newcomer_friendly = true` (enforced in
97
+ an Archon has flagged `newcomer_friendly = true` (enforced in
98
98
  `db.claimTask` / `claimTasksBatch`, returns `rank_too_low_for_task`). Cannot triage, resolve
99
99
  blockers, or run planning/priority sessions. This is also the tier a deactivated/offboarded builder
100
100
  is dropped to by `/builder-exit` (migration `025`); for them `status='inactive'` additionally blocks
package/docs/file-map.md CHANGED
@@ -431,26 +431,21 @@ tests/
431
431
  ```
432
432
  .claude/scheduled-tasks/
433
433
  ├── artifact-audit/SKILL.md ← weekly artifact audit (task 1003200): every task at shipped/confirmed/completed that DECLARES a code artifact is checked against main's commit messages AND the content of its tracked files — both, because a fix can land under another branch and be cited only in a comment (task 1003212). Anything unproven is filed as a blocker, deduped on the finding set. Scoped to ships on/after the ship-assurance gate (2026-08-19, ADR 0184), before which a DB-only ship recorded no declaration and silence proves nothing. FIRED BY .github/workflows/artifact-audit.yml, Mondays 06:23 UTC — `cadence: weekly` here and that cron must agree or fitness Check 25 (scripts/bongos/routine-schedule.js) fails the gate (task 1003209, ADR 0188)
434
- ├── artifact-staleness/SKILL.md ← freshness backstop for the gitignored deploy-generated navigation docs (task 1001930, ADR 0110 pt.5): docs/repo-map.md and docs/session-log-index.md are untracked, so no PR gate can see them and a broken deploy regen step leaves a stale or missing map with only a journald line as the alarm. Asks each artifact's own generator (--check-whole) whether it still matches the served commit, reports absent and stale separately, and posts findings to Discord #ship-news itself. INSTANCE-SIDE ONLY — a fresh CI clone legitimately lacks these files, so it declares no cadence and no workflow fires it; config/scheduled-routines.json records the intended daily cadence.
435
434
  ├── bfg/SKILL.md ← nightly BFG pass (MAS Phase 6, ADR 0026) — doc/memory hygiene + DB counter-vs-ledger drift (Part A), per-builder interaction scoring into transferable-practice suggestions (Part B), and the audited cross-builder memory write (Part C, OFF unless BFG_WRITE_ENABLED) (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
436
435
  ├── bfg-session-eval/SKILL.md ← nightly session-inefficiency evaluator (MAS Phase 6D, ADR 0027) — mines the session corpus for wasteful sessions and files the fixes as idea_inbox rows plus a docs/audits report (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
437
436
  ├── core-update-subscription/SKILL.md ← daily patch-only auto-upgrade of subscribed consumer instances (ADR 0136) — deterministic `subscribe.js` step, health-gated with auto-rollback; opt-in and autonomy-gated, so a no-op until an operator arms it (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
438
- ├── dep-audit-nightly/SKILL.md ← nightly dependency-vulnerability scan (`scripts/security/dep-audit.js`) across npm and the droplet's system packages; findings become idea_inbox rows plus one vulnerability_report per high/critical (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
439
- ├── diagram-drift/SKILL.md ← daily deterministic backstop that re-derives the onboarding diagrams from migrations plus the live version API and files an idea when the committed ones have drifted (`check.js --file-idea`) (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
437
+ ├── dep-audit-nightly/SKILL.md ← daily npm audit of the deployed instance (`scripts/security/dep-audit.js`); high/critical findings become ONE idea_inbox row, re-filed only when the set changes — fired in-process by modules/autonomy/pollers/daily-checks.js (task 1004878)
440
438
  ├── idea-triage-nightly/SKILL.md ← nightly autonomous idea_inbox walk — promotes high-confidence ideas to tasks, discards obvious noise, defers anything ambiguous; autonomy-gated and OFF by default (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
441
439
  ├── main-audit/SKILL.md ← post-merge trust-boundary backstop (ADR 0043 / SEC 863) — audits commits landed on main for sub-Metic edits to permission-sensitive surfaces (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
442
- ├── newcomer-floor/SKILL.md ← daily check that the three evergreen newcomer chores (engineer / artist / ideator) stay stocked; the server restocks them via modules/onboarding/newcomer-restock.js (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
443
- ├── newcomer-generator/SKILL.md ← daily deterministic generator (`generate.js`) that tops the newcomer_friendly task queue back up to its floor (criterion C1 — newcomer queue always populated) (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
444
440
  ├── overnight-builder/SKILL.md ← autonomous overnight session-runner — claims the highest-priority safe task, works it end-to-end and ships it; autonomy-gated and OFF by default (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
445
441
  ├── overnight-code-review/SKILL.md ← nightly autonomous code-quality review; each finding lands as its own idea_inbox row so it survives triage; autonomy-gated and OFF by default (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
446
- ├── peer-vote-tally/SKILL.md ← deterministic tally (`tally.js`) folding builder-on-builder peer votes at least 7 days old into karma_log rows (criterion C2) (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
442
+ ├── peer-vote-tally/SKILL.md ← deterministic tally (`tally.js`) folding builder-on-builder peer votes at least 7 days old into karma_log rows (criterion C2) — fired weekly in-process by modules/autonomy/pollers/routine-timers.js
447
443
  ├── publish-watch/SKILL.md ← daily publish-lane check (task 1003916): compares every version the release lane ANNOUNCED in its own release commits against what the registry actually holds, and posts anything announced-but-never-delivered to Discord #ship-news. Exists because the 2026-09-13 outage (8 failed runs, 3 hours) was found by a person reading the npm version list — a failed run is one red row nobody watches, and the next merge erases the evidence. Silent on a healthy day by design; exit 2 (could not read) is distinct from exit 1 (found one).
448
- ├── rank-roles-audit/SKILL.md ← nightly deterministic check that every builder holds one of the four live ranks, so none silently resolves to zero permissions under the governance_002 backfill's INNER JOIN; detect-only, files one idea_inbox row and never repairs, and never fatal at boot (task 1002873; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
444
+ ├── rank-roles-audit/SKILL.md ← daily deterministic check that every builder holds one of the four live ranks, so none silently resolves to zero permissions under the governance_002 backfill's INNER JOIN; detect-only, files one idea_inbox row and never repairs (task 1002873) — fired in-process by modules/autonomy/pollers/daily-checks.js (task 1004878)
449
445
  ├── redteam-weekly/SKILL.md ← weekly deterministic red-team patrol (task 1003128): full-history gitleaks scan + dependency drift review (audit delta, cooldown, young pkgs, stale overrides) + action-pin check → files security reports (advisory, ADR 0158 §2). FIRED BY .github/workflows/redteam-patrol.yml, Mondays 06:17 UTC — `cadence: weekly` here and that cron must agree or fitness Check 25 (scripts/bongos/routine-schedule.js) fails the gate (task 1003209, ADR 0188)
450
446
  ├── repo-simplify-sweep/SKILL.md ← weekly repo-cleanliness janitor — runs repo-metrics, files dead-code findings to idea_inbox, and auto-fixes only the narrow safe class (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
451
447
  ├── security-review-weekly/SKILL.md ← weekly security-state review — stale vulnerability_reports, ageing security-tagged tasks, recent high/critical dependency alerts; one Archon broadcast when anything is stale, silent otherwise (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
452
448
  ├── skill-usage-review/SKILL.md ← nightly skill-usage feedback loop — aggregates 24h of skill invocations into friction findings filed as `[skill-friction]` idea_inbox rows (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
453
- ├── stale-claim-release/SKILL.md ← hourly sweep releasing claims whose session_pulses heartbeat has been silent for GDS_STALE_CLAIM_HOURS, returning the task to ready and pinging the builder (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
454
449
  └── stranded-rows/SKILL.md ← flags an open task whose work ALREADY LANDED on main (task 1003775): a row still in ready/backlog while a commit claims it with a line ending in `(task N)`. Narrower than "mentions N" on purpose — idea 1000807 measured that a mention-scan returns ~44 open rows that merely cite an id. Measured clean at 878 claims vs 434 open rows, and still catches the historical case (task 1002200, whose claim lives in the MERGE commit body). NO cadence declared: nothing schedules it yet, so it is run by hand — wiring a scheduler needs the control plane, and declaring a cadence with no cron firing it fails fitness Check 25 (task 1003209, ADR 0188). Prints ids + fixed vocabulary only, never builder-authored text.
455
450
  ```
456
451
  <!-- END GENERATED FILE-MAP .claude/scheduled-tasks/ -->
@@ -70,7 +70,7 @@ If your change touched anything the onboarding diagrams describe — the **rank
70
70
  - **All five diagrams are auto-generated** by `scripts/bongos/gen-diagrams.js` — run it (`node scripts/bongos/gen-diagrams.js`) and confirm the result looks right; **do not hand-edit any `.mmd`**. The mechanical three (`01-task-lifecycle`, `02-rank-ladder`, `05-versioning`) regenerate from migrations + the public version API. The conceptual two (`03-example-karma`, `04-architecture`) regenerate from the **authenticated** `/tasks/:id` status of their backing tasks (and 03's seeded-achievement count); if no session is present the generator skips them cleanly. Rendering produces both `.svg` (the pannable viewer asset) and `.png` (fallback).
71
71
  - To change a diagram's **layout or wording**, edit the matching template function in `gen-diagrams.js` — not the `.mmd`. `assertions.json` is rebuilt by the generator in lockstep, so you don't hand-maintain it.
72
72
 
73
- The `diagram-drift` routine is a belt-and-braces backstop (it should find nothing once the generator has run), but the point is not to rely on it — running `gen-diagrams.js` is part of the change.
73
+ There is no drift backstop (the `diagram-drift` routine was retired in task 1004878) — running `gen-diagrams.js` is part of the change.
74
74
 
75
75
  ---
76
76
 
@@ -3189,5 +3189,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3189
3189
  landed since 1.21.174 with no explicit bump. run 40234506483. (task 1002620)
3190
3190
  1.21.176 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3191
3191
  landed since 1.21.175 with no explicit bump. run 40235567383. (task 1002620)
3192
+ 1.21.177 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3193
+ landed since 1.21.176 with no explicit bump. run 40241825861. (task 1002620)
3192
3194
  ---------------------------------------------------------------------------
3193
3195
  ```
@@ -69,16 +69,12 @@ The diagrams are surfaced inside the authenticated **builders' hall** at
69
69
 
70
70
  ## Staying current (auto-update)
71
71
 
72
- Two mechanisms keep these honest as the system changes (added under [#356](https://example.com/builders#/task/356)):
72
+ One mechanism keeps these honest as the system changes (added under [#356](https://example.com/builders#/task/356)):
73
73
 
74
74
  1. **Render-on-deploy** — all five diagrams auto-regenerate on deploy: `scripts/bongos/gen-diagrams.js` re-renders every `.mmd` to its `.svg` + `.png` and rebuilds `assertions.json` in lockstep, so the committed images never drift from their source. (Do **not** hand-edit the `.mmd`/`.svg`/`.png`/`assertions.json` — edit the template in `gen-diagrams.js` and re-run it.)
75
- 2. **Drift detection** — the `diagram-drift` scheduled task (`.claude/scheduled-tasks/diagram-drift/`) runs a deterministic checker that compares `assertions.json` (below) against the live DB/code. Since all five diagrams auto-regenerate on deploy, this is a **belt-and-braces backstop** (it should find nothing in normal operation); on divergence it files an `idea_inbox` entry. Run it manually any time:
76
- ```bash
77
- node .claude/scheduled-tasks/diagram-drift/check.js # report only
78
- node .claude/scheduled-tasks/diagram-drift/check.js --file-idea
79
- ```
75
+ (A second, the `diagram-drift` scheduled check, was retired in task 1004878: it was never scheduled, could not run on the packaged server, and duplicated the regeneration above.)
80
76
 
81
- `assertions.json` is the machine-readable record of what these diagrams claim about the live system (rank enum + wired ranks, task-status set, live versions, and the conceptual `architecture` + `example_karma` piece→task slices). It is **rebuilt by `gen-diagrams.js` in lockstep with the diagrams**, so you no longer hand-maintain it; it stays as the drift checker's reference.
77
+ `assertions.json` is the machine-readable record of what these diagrams claim about the live system (rank enum + wired ranks, task-status set, live versions, and the conceptual `architecture` + `example_karma` piece→task slices). It is **rebuilt by `gen-diagrams.js` in lockstep with the diagrams**, so you no longer hand-maintain it.
82
78
 
83
79
  ## Regenerating the diagrams by hand
84
80
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "Machine-readable facts the onboarding diagrams assert about the LIVE system. The diagram-drift scheduled task (.claude/scheduled-tasks/diagram-drift/) compares these against the live DB/code and files an idea_inbox entry when they diverge, so the diagrams stay an honest snapshot. When a diagram is intentionally updated, update the matching values here in the same commit. Baselined 2026-05-26 under task #356.",
2
+ "_comment": "Machine-readable facts the onboarding diagrams assert about the LIVE system. gen-diagrams.js rebuilds them from the live DB/code in lockstep with the diagrams. When a diagram is intentionally updated, update the matching values here in the same commit. Baselined 2026-05-26 under task #356.",
3
3
  "rank_ladder": {
4
4
  "diagram": "02-rank-ladder",
5
5
  "rank_enum": [
@@ -82,7 +82,7 @@
82
82
  },
83
83
  "drachmae_karma": {
84
84
  "diagram": "03-drachmae-karma",
85
- "_comment": "Pieces of 03-drachmae-karma that are gated on a backing Bongos task. A piece is LIVE iff its task is 'shipped'. live_tasks must ALL be shipped live; pending_tasks must NOT yet be shipped. Rebuilt automatically by scripts/bongos/gen-diagrams.js from the authenticated /tasks/:id status; the diagram is regenerated in lockstep, so this slice is a redundant backstop the drift checker verifies. D_SHIP (credit_log trigger) and D_ACH (achievement bonuses) have no task gate and are always LIVE. achievements_seeded mirrors 03's 'N seeded' label, counted from the achievements seed INSERTs in migrations/.",
85
+ "_comment": "Pieces of 03-drachmae-karma that are gated on a backing Bongos task. A piece is LIVE iff its task is 'shipped'. live_tasks must ALL be shipped live; pending_tasks must NOT yet be shipped. Rebuilt automatically by scripts/bongos/gen-diagrams.js from the authenticated /tasks/:id status; the diagram is regenerated in lockstep. D_SHIP (credit_log trigger) and D_ACH (achievement bonuses) have no task gate and are always LIVE. achievements_seeded mirrors 03's 'N seeded' label, counted from the achievements seed INSERTs in migrations/.",
86
86
  "live_tasks": [
87
87
  {
88
88
  "id": 284,
@@ -115,7 +115,7 @@
115
115
  },
116
116
  "architecture": {
117
117
  "diagram": "04-architecture",
118
- "_comment": "Pieces of 04-architecture gated on a backing Bongos task. A piece is LIVE iff its task is 'shipped'; live_tasks must ALL be shipped, pending_tasks must NOT yet be. Rebuilt by scripts/bongos/gen-diagrams.js in lockstep with the diagram, so this is the drift checker's backstop. The machine/server/GitHub topology is structural (not task-gated); these are the LIVE/PLANNED markers. MEMORY layer (ADR 0024): 273 schema / 274 sync-on-ship / 275 pull-on-start / 276 isolation. Ship flow: 1025 (server-mediated publish, for a checkout with no push credential), 1028 (GitHub App push credential). The deploy leg is the standalone AUTO-PULL (ADR 0108/0129), not a task-gated GitHub-Actions CI job, so 859 is intentionally absent.",
118
+ "_comment": "Pieces of 04-architecture gated on a backing Bongos task. A piece is LIVE iff its task is 'shipped'; live_tasks must ALL be shipped, pending_tasks must NOT yet be. Rebuilt by scripts/bongos/gen-diagrams.js in lockstep with the diagram. The machine/server/GitHub topology is structural (not task-gated); these are the LIVE/PLANNED markers. MEMORY layer (ADR 0024): 273 schema / 274 sync-on-ship / 275 pull-on-start / 276 isolation. Ship flow: 1025 (server-mediated publish, for a checkout with no push credential), 1028 (GitHub App push credential). The deploy leg is the standalone AUTO-PULL (ADR 0108/0129), not a task-gated GitHub-Actions CI job, so 859 is intentionally absent.",
119
119
  "live_tasks": [
120
120
  {
121
121
  "id": 273,
@@ -216,11 +216,13 @@ Inside the namespace the file lists as `---------- 1 root root 0`. The runner un
216
216
 
217
217
  ## Bongos build pipeline
218
218
 
219
- ### Two deterministic routines run as in-process server timers, not external cron
219
+ ### The deterministic routines run as in-process server timers, not external cron
220
220
 
221
221
  - **What:** `peer-vote-tally` (weekly) runs inside the always-on Node process via the autonomy module's poller `modules/autonomy/pollers/routine-timers.js` (ADR 0078 amendment, task 1382; carved into `modules/autonomy/` at BV1.R83) — no `claude -p`, no external cron, no token. The loader's `startModulePollers` starts it from `src/platform-server.js` at boot (task 1002419 — before that fix the call site had been lost with the retired `server.js` and no module poller started at all); it reaches `tallyPeerVotes` through the doorway. It's a no-op anywhere the DB is unreachable (builder machines), so it only does work on the prod droplet.
222
222
  - **Disable / move it out-of-process:** set `ROUTINE_TIMERS_DISABLED=1` in `/etc/example/web-source.env` and restart. Also set it on any **second** always-on instance that shares the `example` DB so the tally can't double-fold (the `FOR UPDATE OF pv` row lock is the hard guard if you forget).
223
223
  - **Run it manually now:** `node scripts/bongos/run-routine.js peer-vote-tally` (the dispatcher front door still works). The cadence is boot-anchored ("~90s after boot, then weekly"), not a wall-clock Monday job — frequent deploys re-anchor it, which is harmless (the tally is idempotent).
224
+ - **The daily checks** (task 1004878): `rank-roles-audit` and `dep-audit-nightly` run from the sibling poller `modules/autonomy/pollers/daily-checks.js`. Unlike peer-vote it is **not** boot-anchored — deploys restart the server many times a day — so it ticks hourly and runs a check only when that check's last `autonomy_runs` row is ~a day old. Findings go to the idea inbox, once per distinct set (the row stores a fingerprint; an identical result is recorded `unchanged`). See what ran without SSH: `GET /autonomy/runs?routine=dep-audit-nightly`. Same kill switch (`ROUTINE_TIMERS_DISABLED=1`). Which routines the server fires, and how often, is ONE table — `modules/autonomy/server-schedule.js` — read by both the pollers and the merge-gate check `scripts/bongos/routine-schedule.js`.
225
+ - **`dep-audit-nightly` audits the INSTANCE root** (`resolveInstanceRoot()`, the service's working directory), so it needs `npm` on the service's PATH and a `package-lock.json` there. Without either it records a `no-go` row with the reason — never a clean pass.
224
226
 
225
227
  ### A ship was graded FAIL but the work is genuinely good (grader false-negative)
226
228
 
@@ -62,6 +62,14 @@ async function readAutonomyRuns({ routine = null, limit = 20 } = {}) {
62
62
  return rows.map((r) => ({ ...r, id: String(r.id) }));
63
63
  }
64
64
 
65
+ // readRankRoster — every builder's id, login, rank and status, for the daily
66
+ // rank-roles check (task 1004878). No WHERE on purpose: an INACTIVE builder on a
67
+ // legacy rank is exactly the row a future CHECK-narrowing migration would fail on.
68
+ async function readRankRoster() {
69
+ const { rows } = await pool.query('SELECT id, github_login, rank, status FROM builders ORDER BY id');
70
+ return rows;
71
+ }
72
+
65
73
  // ── the Autobongos fence (task 1003908, ADR 0307 §6) ─────────────────────────
66
74
  //
67
75
  // These back the one control surface the runner host cannot write to. Everything
@@ -350,6 +358,7 @@ module.exports = {
350
358
  recordAutonomyRun,
351
359
  lastAutonomyHead,
352
360
  readAutonomyRuns,
361
+ readRankRoster,
353
362
  recordRunnerHeartbeat,
354
363
  readRunnerHeartbeats,
355
364
  readFence,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "key": "autonomy",
3
3
  "title": "Autonomy & MAS (conductor · architect · gate · routine timers)",
4
- "description": "The Multi-Agent System's autonomy surfaces (ADR 0024): the $0 deterministic autonomy gate (System 3 — a precheck route + the gate CLI, inert until OTB_AUTONOMY_ENABLED=1), the conductor mid-flight dispatch CLI (Phase 4), the Architect bounded first-principles --scope audit CLI (Phase 5), and the in-process routine timers (System 4 / ADR 0078 amendment — the weekly peer-vote-tally poller). A core-domain module carved out of the GDS core in BV1.R83 (ADR 0093 §1); default-on because the autonomy machinery + the durable routine timer ship to every instance (both are inert/no-op until armed or the pool is reachable). Cross-cutting: it OBSERVES lifecycle events. The routine timer reaches the lifecycle `tallyPeerVotes` capability through the published doorway (a tracked lifecycle leak until R86), never a core require; the precheck route + the gate/conductor/architect CLIs travel with this domain (the CLIs stay under scripts/, outside the kernel boundary, so they may still read the ideas/economy/lifecycle signals + the grader directly). Registers no port — core never calls back in.",
4
+ "description": "The Multi-Agent System's autonomy surfaces (ADR 0024): the $0 deterministic autonomy gate (System 3 — a precheck route + the gate CLI, inert until OTB_AUTONOMY_ENABLED=1), the conductor mid-flight dispatch CLI (Phase 4), the Architect bounded first-principles --scope audit CLI (Phase 5), and the in-process routine timers (System 4 / ADR 0078 amendment — the weekly peer-vote-tally poller, and since task 1004878 the daily rank-roles and dependency-audit checks). A core-domain module carved out of the GDS core in BV1.R83 (ADR 0093 §1); default-on because the autonomy machinery + the durable routine timer ship to every instance (both are inert/no-op until armed or the pool is reachable). Cross-cutting: it OBSERVES lifecycle events. The routine timer reaches the lifecycle `tallyPeerVotes` capability through the published doorway (a tracked lifecycle leak until R86), never a core require; the precheck route + the gate/conductor/architect CLIs travel with this domain (the CLIs stay under scripts/, outside the kernel boundary, so they may still read the ideas/economy/lifecycle signals + the grader directly). Registers no port — core never calls back in.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.11.0",
7
7
  "category": { "parent": "work" },
@@ -9,7 +9,7 @@
9
9
  "maintenance": { "status": "core-maintained" },
10
10
  "contributes": {
11
11
  "routes": ["autonomy"],
12
- "pollers": ["routine-timers"]
12
+ "pollers": ["routine-timers", "daily-checks"]
13
13
  },
14
14
  "provides": [],
15
15
  "consumes": []
@@ -0,0 +1,262 @@
1
+ 'use strict';
2
+
3
+ // modules/autonomy/pollers/daily-checks.js — the deterministic daily checks, fired
4
+ // by the always-on server itself (task 1004878).
5
+ //
6
+ // WHY IN-PROCESS. Every one of these routines declared a cadence and nothing fired
7
+ // it: the repo's only scheduler was .github/workflows/, whose minutes are billable
8
+ // and already oversubscribed, and the operator-side cron the SKILL.md files kept
9
+ // asking for was never written. The routine-timers poller beside this one solved
10
+ // the same problem for peer-vote-tally (ADR 0078 amendment) — an in-process timer
11
+ // costs nothing and cannot be forgotten by an operator. This is that pattern for
12
+ // the daily checks.
13
+ //
14
+ // THE CHECKS (the rows of ../server-schedule.js with poller 'daily-checks'):
15
+ // - rank-roles-audit — every builder on one of the live ranks (task 1002873).
16
+ // Reads the roster straight from this instance's DB, so it
17
+ // needs no CLI session — the reason the script never ran
18
+ // unattended.
19
+ // - dep-audit-nightly — `npm audit` of the DEPLOYED instance (its own root and
20
+ // lockfile, not a repo checkout), high/critical only.
21
+ //
22
+ // DAILY BY THE CLOCK, NOT BY THE BOOT. The server restarts on every deploy, and a
23
+ // boot-anchored timer would re-run (and re-file) on each one. So the timer only
24
+ // ticks hourly and asks the ledger: a check runs when its last `autonomy_runs` row
25
+ // is (nearly) a day old. The same rows are what GET /autonomy/runs shows, so
26
+ // "did the daily check run, and what did it find" is answerable from a laptop.
27
+ //
28
+ // ONE IDEA PER DISTINCT FINDING SET. A finding is filed into the idea inbox once;
29
+ // the run row stores a fingerprint of what was found, and the next day's identical
30
+ // result is recorded as `unchanged` instead of filed again. A changed set (a new
31
+ // vulnerability, a second bad rank) files again. A clean run clears it, so a
32
+ // regression after a fix is a fresh idea.
33
+ //
34
+ // Mirrors routine-timers.js (but logs through api.logger, not console): kill switch ROUTINE_TIMERS_DISABLED, unref'd timers,
35
+ // best-effort (a throw logs and waits for the next tick — never crashes the
36
+ // server), pure w.r.t. injected deps so it unit-tests without a database, and a
37
+ // quiet no-op anywhere the database is unreachable (builder laptops).
38
+
39
+ const { execFile } = require('node:child_process');
40
+ const crypto = require('node:crypto');
41
+ const api = require('../../../src/module-api');
42
+ const autonomyDb = require('../db');
43
+ const rankRoles = require('../rank-roles');
44
+ const depAudit = require('../../../scripts/security/dep-audit');
45
+ const { SERVER_ROUTINES, periodMs } = require('../server-schedule');
46
+
47
+ const log = api.logger('autonomy');
48
+
49
+ const ROUTINES = SERVER_ROUTINES.filter((r) => r.poller === 'daily-checks').map((r) => r.routine);
50
+
51
+ const TICK_MS = Number(process.env.ROUTINE_DAILY_CHECKS_TICK_MS) || 60 * 60_000;
52
+ // Later than peer-vote's 90s so the two first ticks never land together.
53
+ const FIRST_MS = Number(process.env.ROUTINE_DAILY_CHECKS_FIRST_MS) || 3 * 60_000;
54
+ const JITTER_MS = 60_000;
55
+ // A check is due this long BEFORE a full period has passed. Hourly ticks land up to
56
+ // an hour late; without the slack each day's run would slide an hour later than the
57
+ // last. With it, runs stay between 23.5 and 24.5 hours apart.
58
+ const DUE_SLACK_MS = 30 * 60_000;
59
+ // npm audit is a network call to the registry; one stuck call must not hold the
60
+ // next day's check hostage.
61
+ const NPM_AUDIT_TIMEOUT_MS = 5 * 60_000;
62
+
63
+ // ── the checks ─────────────────────────────────────────────────────────────────
64
+ // Each returns { keys, idea } — `keys` identify the findings (empty = clean) and
65
+ // feed the fingerprint; `idea` is what gets filed. A check that cannot run THROWS:
66
+ // "could not check" must never be recorded as clean.
67
+
68
+ async function checkRankRoles(deps) {
69
+ const rows = await deps.db.readRankRoster();
70
+ const offenders = rankRoles.findRanksWithoutRoles(rows, deps.liveRanks || api.LIVE_RANK_LADDER);
71
+ return {
72
+ keys: offenders.map((o) => `${o.id}:${o.rank}`),
73
+ idea: { ...rankRoles.ideaFor(offenders, { filedBy: "the server's daily rank-roles check" }), kind: 'bug' },
74
+ };
75
+ }
76
+
77
+ // The async spawn. execFile reports npm's own exit 1 (findings present) as an error
78
+ // too, so only a missing binary or a timeout counts as "could not run".
79
+ function npmAudit(cwd) {
80
+ return new Promise((resolve) => {
81
+ execFile('npm', depAudit.NPM_AUDIT_ARGS, { cwd, maxBuffer: 32 * 1024 * 1024, timeout: NPM_AUDIT_TIMEOUT_MS },
82
+ (err, stdout, stderr) => {
83
+ const failed = err && (err.code === 'ENOENT' || err.killed);
84
+ resolve({ stdout, stderr, error: failed ? (err.code === 'ENOENT' ? 'npm not found' : 'npm audit timed out') : null });
85
+ });
86
+ });
87
+ }
88
+
89
+ async function checkDepAudit(deps) {
90
+ const cwd = deps.cwd || api.resolveInstanceRoot();
91
+ const out = await (deps.npmAudit || npmAudit)(cwd);
92
+ if (out.error) throw new Error(out.error);
93
+ const res = depAudit.parseNpmAudit(out.stdout, out.stderr);
94
+ if (!res.ok) throw new Error(res.raw_error);
95
+ const f = res.findings;
96
+ return {
97
+ keys: f.map((x) => `${x.package}:${x.id || x.summary}`),
98
+ idea: {
99
+ kind: 'infra',
100
+ title: `[dep-audit] ${f.length} high/critical ${f.length === 1 ? 'vulnerability' : 'vulnerabilities'} in this instance's dependencies`,
101
+ bodyMd: [
102
+ `The server's daily \`npm audit\` of this instance (${cwd}) found ${f.length} high/critical advisory(ies):`,
103
+ '',
104
+ ...f.map((x) => `- **${x.package}** (${x.severity}) — ${x.summary}${x.url ? ` — ${x.url}` : ''}. Fix: ${x.fix}.`),
105
+ '',
106
+ 'Most of these arrive through the core package, so the fix is usually a core release',
107
+ 'that bumps the dependency, then moving this instance onto it.',
108
+ '',
109
+ 'Filed by modules/autonomy/pollers/daily-checks.js (task 1004878). The same set is not',
110
+ 're-filed tomorrow; a changed set is.',
111
+ ].join('\n'),
112
+ },
113
+ };
114
+ }
115
+
116
+ const CHECKS = {
117
+ 'rank-roles-audit': checkRankRoles,
118
+ 'dep-audit-nightly': checkDepAudit,
119
+ };
120
+
121
+ // ── the ledger half ────────────────────────────────────────────────────────────
122
+
123
+ const fingerprint = (keys) =>
124
+ crypto.createHash('sha1').update([...keys].sort().join('\n')).digest('hex').slice(0, 12);
125
+
126
+ // The fingerprint the last DELIVERED finding carried, or null. Only a `go` row
127
+ // counts: an error or an undelivered filing in between must not make tomorrow's
128
+ // identical result look new, and must not make it look delivered either.
129
+ function lastFiledFingerprint(rows) {
130
+ const go = (rows || []).find((r) => r.decision === 'go');
131
+ const m = go && /\bfp=([0-9a-f]+)/.exec(go.reason || '');
132
+ return m ? m[1] : null;
133
+ }
134
+
135
+ function isDue(rows, routine, now) {
136
+ const last = (rows || [])[0];
137
+ if (!last) return true;
138
+ const at = new Date(last.created_at).getTime();
139
+ if (!Number.isFinite(at)) return true;
140
+ return now - at >= periodMs(routine) - DUE_SLACK_MS;
141
+ }
142
+
143
+ // One routine, once: run it if it is due, file what it found if that is new, and
144
+ // write the row that says so. NEVER throws.
145
+ async function runCheck(routine, deps = {}) {
146
+ const db = deps.db || autonomyDb;
147
+ const logFn = deps.log || ((m) => log.info(m));
148
+ const errFn = deps.error || ((m) => log.error(m));
149
+ const now = deps.now ? deps.now() : Date.now();
150
+ const record = (decision, reason) =>
151
+ db.recordAutonomyRun({ routine, decision, reason }).catch((e) =>
152
+ errFn(`[daily-checks] ${routine}: could not record the run (${e && e.message})`));
153
+
154
+ let rows;
155
+ try {
156
+ rows = await db.readAutonomyRuns({ routine, limit: 10 });
157
+ } catch (e) {
158
+ return { skipped: 'db_unreachable', error: e && e.message };
159
+ }
160
+ if (!isDue(rows, routine, now)) return { skipped: 'not_due' };
161
+
162
+ let result;
163
+ try {
164
+ result = await CHECKS[routine]({ ...deps, db });
165
+ } catch (e) {
166
+ const msg = (e && e.message) || String(e);
167
+ errFn(`[daily-checks] ${routine}: could not run (${msg})`);
168
+ await record('no-go', `error: ${msg}`.slice(0, 500));
169
+ return { outcome: 'error', error: msg };
170
+ }
171
+
172
+ if (!result.keys.length) {
173
+ await record('go', 'clean');
174
+ return { outcome: 'clean' };
175
+ }
176
+ const fp = fingerprint(result.keys);
177
+ const n = result.keys.length;
178
+ if (fp === lastFiledFingerprint(rows)) {
179
+ await record('go', `findings n=${n} fp=${fp} unchanged`);
180
+ return { outcome: 'unchanged', fingerprint: fp };
181
+ }
182
+ const capture = deps.captureIdea || api.resolveOptional('ideas.capture');
183
+ if (typeof capture !== 'function') {
184
+ errFn(`[daily-checks] ${routine}: ${n} finding(s) and no idea inbox to file them in (ideas module off)`);
185
+ await record('no-go', `findings n=${n} fp=${fp} undelivered: no ideas.capture`);
186
+ return { outcome: 'undelivered', fingerprint: fp };
187
+ }
188
+ try {
189
+ const idea = await capture({ ...result.idea, filedVia: 'api' });
190
+ logFn(`[daily-checks] ${routine}: ${n} finding(s) filed as idea ${idea && idea.id}`);
191
+ await record('go', `findings n=${n} fp=${fp} idea=${idea && idea.id}`);
192
+ return { outcome: 'filed', fingerprint: fp, ideaId: idea && idea.id };
193
+ } catch (e) {
194
+ const msg = (e && (e.message || e.code)) || String(e);
195
+ errFn(`[daily-checks] ${routine}: ${n} finding(s), filing failed (${msg})`);
196
+ await record('no-go', `findings n=${n} fp=${fp} undelivered: ${msg}`.slice(0, 500));
197
+ return { outcome: 'undelivered', fingerprint: fp, error: msg };
198
+ }
199
+ }
200
+
201
+ // One tick: every daily routine in turn. Sequential on purpose — npm audit is the
202
+ // heavy one and nothing here is urgent enough to run two at once.
203
+ let running = false;
204
+ let warnedUnreachable = false;
205
+ async function tick(deps = {}) {
206
+ if (running) return [];
207
+ running = true;
208
+ try {
209
+ const out = [];
210
+ for (const routine of ROUTINES) {
211
+ const r = await runCheck(routine, deps);
212
+ if (r.skipped === 'db_unreachable' && !warnedUnreachable) {
213
+ warnedUnreachable = true;
214
+ log.info(`[daily-checks] skipped — database unreachable (${r.error}); a builder machine has nothing to check`);
215
+ }
216
+ out.push({ routine, ...r });
217
+ }
218
+ return out;
219
+ } finally {
220
+ running = false;
221
+ }
222
+ }
223
+
224
+ let firstTimer = null;
225
+ let interval = null;
226
+
227
+ function start(deps = {}) {
228
+ if (process.env.ROUTINE_TIMERS_DISABLED === '1') {
229
+ log.info('[daily-checks] disabled (ROUTINE_TIMERS_DISABLED=1)');
230
+ return;
231
+ }
232
+ if (firstTimer || interval) return; // already started
233
+ const fire = () => tick(deps).catch((e) => log.error(`[daily-checks] tick crashed: ${e && e.message}`));
234
+ firstTimer = setTimeout(() => {
235
+ fire();
236
+ interval = setInterval(fire, TICK_MS);
237
+ if (interval.unref) interval.unref();
238
+ }, FIRST_MS + Math.floor(Math.random() * JITTER_MS));
239
+ if (firstTimer.unref) firstTimer.unref();
240
+ log.info(`[daily-checks] started — ${ROUTINES.join(', ')} once a day (checked hourly against the run ledger)`);
241
+ }
242
+
243
+ function stop() {
244
+ if (firstTimer) clearTimeout(firstTimer);
245
+ if (interval) clearInterval(interval);
246
+ firstTimer = interval = null;
247
+ }
248
+
249
+ module.exports = {
250
+ start,
251
+ stop,
252
+ tick,
253
+ runCheck,
254
+ isDue,
255
+ fingerprint,
256
+ lastFiledFingerprint,
257
+ checkRankRoles,
258
+ checkDepAudit,
259
+ ROUTINES,
260
+ TICK_MS,
261
+ DUE_SLACK_MS,
262
+ };
@@ -38,9 +38,9 @@
38
38
  // so over-/under-firing is harmless and a second instance can't double-fold.
39
39
  //
40
40
  // (diagram-drift was intentionally NOT made a timer: all five onboarding diagrams
41
- // already auto-regenerate from live facts on every deploy, so a daily drift check
42
- // is near-redundant; the manual `node scripts/bongos/run-routine.js diagram-drift`
43
- // remains for on-demand use. See the ADR 0078 amendment.)
41
+ // already auto-regenerate from live facts on every deploy. Task 1004878 retired
42
+ // the routine outright. The daily checks that ARE timed live beside this file, in
43
+ // daily-checks.js.)
44
44
  //
45
45
  // Mirrors the publish-reconciler contract: env-overridable interval, an UNREF'd
46
46
  // timer (never holds the process open across a systemd restart), best-effort (a
@@ -57,9 +57,11 @@
57
57
  // core load at module-discovery time, and so tests that pass deps.db bypass it.
58
58
  const api = require('../../../src/module-api');
59
59
 
60
- const WEEK_MS = 7 * 24 * 60 * 60_000;
60
+ // The period comes from ../server-schedule.js, the table the merge-gate cadence check
61
+ // (scripts/bongos/routine-schedule.js) also reads — so the two cannot disagree.
62
+ const { periodMs } = require('../server-schedule');
61
63
 
62
- const PEER_VOTE_INTERVAL_MS = Number(process.env.ROUTINE_PEER_VOTE_MS) || WEEK_MS;
64
+ const PEER_VOTE_INTERVAL_MS = Number(process.env.ROUTINE_PEER_VOTE_MS) || periodMs('peer-vote-tally');
63
65
  // First-fire delay: let the server finish binding + the other in-process pollers
64
66
  // settle before the first tally. Jitter spreads first-fire across a minute so a
65
67
  // crash-loop / deploy storm doesn't synchronize every restart's first run.