@bongos/core 1.19.876 → 1.19.877

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 (42) hide show
  1. package/.bongos-core.json +93 -38
  2. package/.gitattributes +6 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +4 -0
  5. package/clients/bongos-client/index.cjs +4 -0
  6. package/clients/bongos-client/index.d.ts +7 -0
  7. package/clients/bongos-client/index.mjs +4 -0
  8. package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +12 -1
  9. package/docs/api/openapi.json +149 -3
  10. package/docs/api-reference.md +4 -2
  11. package/docs/copy-inventory.md +7 -6
  12. package/docs/copy-registry.json +16 -7
  13. package/docs/module-api-changelog.md +2 -0
  14. package/docs/recipes/autobongos-windows-host.md +138 -0
  15. package/docs/recipes/instance-deploy.md +29 -3
  16. package/modules/autonomy/cadence.js +276 -0
  17. package/modules/autonomy/db.js +47 -0
  18. package/modules/autonomy/migrations/autonomy_002_runner_heartbeat.sql +93 -0
  19. package/modules/autonomy/routes/autonomy.js +69 -0
  20. package/modules/autonomy/runner-health.js +97 -0
  21. package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
  22. package/modules/hall-ui/public/gate.html +4 -1
  23. package/modules/hall-ui/public/gate.js +51 -2
  24. package/package-lock.json +2 -2
  25. package/package.json +1 -1
  26. package/scripts/gds/autobongos-run.js +368 -11
  27. package/scripts/gds/autobongos-service.cmd +38 -0
  28. package/scripts/gds/autobongos-service.ps1 +129 -0
  29. package/scripts/gds/deploy-main-audit-gate.sh +90 -0
  30. package/scripts/gds/main-audit.js +26 -4
  31. package/scripts/gds/run-unit-tests.js +16 -1
  32. package/src/module-api.js +1 -1
  33. package/tests/autobongos_cadence.mjs +602 -0
  34. package/tests/autonomy_runner_health.mjs +135 -0
  35. package/tests/bump_version.mjs +12 -1
  36. package/tests/cli_package.mjs +11 -1
  37. package/tests/deploy_audit_gate.mjs +154 -0
  38. package/tests/deploy_divergence_line.mjs +47 -1
  39. package/tests/helpers.mjs +61 -0
  40. package/tests/main_audit_db_roster.mjs +97 -0
  41. package/tests/publish_watch.mjs +9 -0
  42. package/tests/setup_device_flow_fallback.mjs +2 -1
@@ -0,0 +1,129 @@
1
+ # scripts/gds/autobongos-service.ps1 -- install the Autobongos runner so it
2
+ # outlives the reboot (task 1003905, ADR 0307 section 5).
3
+ #
4
+ # powershell -ExecutionPolicy Bypass -File scripts\gds\autobongos-service.ps1 install
5
+ # powershell -ExecutionPolicy Bypass -File scripts\gds\autobongos-service.ps1 status
6
+ # powershell -ExecutionPolicy Bypass -File scripts\gds\autobongos-service.ps1 start|stop|uninstall
7
+ #
8
+ # WHY TASK SCHEDULER AND NOT A REAL SERVICE. Windows services speak a protocol
9
+ # (SCM handshakes, stop signals) that node does not, so every "node as a service"
10
+ # route needs a third-party wrapper: nssm, winsw, node-windows. Each is a binary
11
+ # to download, verify, update and explain -- a permanent dependency bought for one
12
+ # machine. Task Scheduler is already on the box, runs at boot with no session,
13
+ # survives sign-out, and restarts a crashed task natively. The one thing it does
14
+ # not give is the Services panel, which nobody here needs.
15
+ #
16
+ # WHAT ACTUALLY KEEPS IT ALIVE IS THE INNER LOOP, and that is the important part
17
+ # to understand before reading the RestartCount settings below. `--forever` never
18
+ # returns: it backs off and probes instead of exiting (task 1003904), so the
19
+ # scheduler's restart is the OUTER ring, for the cases the loop cannot survive at
20
+ # all -- a reboot, a power cut, a node crash, an OOM kill. If this ring is doing
21
+ # work regularly, something is wrong that the backoff should have absorbed, and
22
+ # the run log will say so.
23
+ #
24
+ # UNPRIVILEGED ON PURPOSE. It runs as the logged-in user, not SYSTEM: the runner
25
+ # needs that user's git credential, npm config and Claude session, and SYSTEM has
26
+ # none of them. It is also the smaller blast radius for a process that spawns
27
+ # workers with blanket bypassPermissions.
28
+
29
+ [CmdletBinding()]
30
+ param(
31
+ [Parameter(Position = 0)]
32
+ [ValidateSet('install', 'uninstall', 'start', 'stop', 'status')]
33
+ [string]$Action = 'status',
34
+
35
+ # The checkout to run from. Defaults to the repo this script lives in, so the
36
+ # common case needs no argument and cannot point at the wrong tree by typo.
37
+ [string]$RepoRoot,
38
+
39
+ [string]$TaskName = 'Autobongos'
40
+ )
41
+
42
+ $ErrorActionPreference = 'Stop'
43
+
44
+ if (-not $RepoRoot) {
45
+ $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
46
+ }
47
+ $LogDir = Join-Path $env:LOCALAPPDATA 'cloudbongos'
48
+ $LogFile = Join-Path $LogDir 'autobongos.log'
49
+ $Wrapper = Join-Path $RepoRoot 'scripts\gds\autobongos-service.cmd'
50
+
51
+ function Assert-Prereqs {
52
+ if (-not (Test-Path (Join-Path $RepoRoot 'scripts\gds\autobongos-run.js'))) {
53
+ throw "No autobongos-run.js under $RepoRoot -- pass -RepoRoot <path to the checkout>."
54
+ }
55
+ foreach ($exe in @('node', 'git', 'claude')) {
56
+ if (-not (Get-Command $exe -ErrorAction SilentlyContinue)) {
57
+ # Named, not guessed at: a runner that starts without `claude` on PATH will
58
+ # spawn-fail every worker and look like a mysterious outage in the log.
59
+ throw "'$exe' is not on PATH for this user. The runner needs node, git and claude."
60
+ }
61
+ }
62
+ }
63
+
64
+ function Show-Status {
65
+ $t = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
66
+ if (-not $t) { Write-Host "not installed (task '$TaskName' does not exist)"; return }
67
+ $info = Get-ScheduledTaskInfo -TaskName $TaskName
68
+ Write-Host "task : $TaskName"
69
+ Write-Host "state : $($t.State)"
70
+ Write-Host "last run : $($info.LastRunTime) (result $($info.LastTaskResult))"
71
+ Write-Host "next run : $($info.NextRunTime)"
72
+ Write-Host "log : $LogFile"
73
+ if (Test-Path $LogFile) {
74
+ Write-Host "log size : $([math]::Round((Get-Item $LogFile).Length / 1KB)) KB"
75
+ Write-Host ''
76
+ Write-Host '--- last 15 lines ---'
77
+ Get-Content $LogFile -Tail 15
78
+ } else {
79
+ Write-Host 'log : (not written yet)'
80
+ }
81
+ Write-Host ''
82
+ Write-Host 'The scheduler only says whether the PROCESS is up. Whether the runner'
83
+ Write-Host 'is actually working is the hall: it reads the heartbeat this posts.'
84
+ }
85
+
86
+ switch ($Action) {
87
+ 'install' {
88
+ Assert-Prereqs
89
+ New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
90
+
91
+ $action = New-ScheduledTaskAction -Execute $Wrapper -WorkingDirectory $RepoRoot
92
+ # AtStartup covers the cold reboot the done-when actually names. AtLogOn is
93
+ # NOT used: it would wait for someone to sign in, which is the one thing that
94
+ # must not be required.
95
+ $trigger = New-ScheduledTaskTrigger -AtStartup
96
+ $principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType S4U -RunLevel Limited
97
+ $settings = New-ScheduledTaskSettingsSet `
98
+ -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
99
+ -StartWhenAvailable `
100
+ -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) `
101
+ -ExecutionTimeLimit ([TimeSpan]::Zero) `
102
+ -MultipleInstances IgnoreNew
103
+ # ExecutionTimeLimit ZERO = never kill it. The default is three days, and a
104
+ # daemon silently executed after three days is exactly the invisible death
105
+ # this whole task exists to prevent.
106
+ # MultipleInstances IgnoreNew = the scheduler will not start a second copy
107
+ # over a live one; the heartbeat file is the second guard, inside the runner.
108
+
109
+ Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
110
+ -Principal $principal -Settings $settings -Force | Out-Null
111
+
112
+ Write-Host "installed '$TaskName' -- starts at boot, restarts on crash, logs to $LogFile"
113
+ Write-Host ''
114
+ Write-Host 'Start it now with: .\scripts\gds\autobongos-service.ps1 start'
115
+ Write-Host 'Then confirm from the hall (/builders/gate) that it is checking in.'
116
+ }
117
+ 'uninstall' {
118
+ Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
119
+ Write-Host "removed '$TaskName'. The log at $LogFile is left in place."
120
+ }
121
+ 'start' { Start-ScheduledTask -TaskName $TaskName; Write-Host "started '$TaskName'" }
122
+ 'stop' {
123
+ Stop-ScheduledTask -TaskName $TaskName
124
+ Write-Host "stopped '$TaskName' -- but note this only stops the PROCESS on this machine."
125
+ Write-Host 'The durable way to stop the runner is the kill switch in the hall, which'
126
+ Write-Host 'survives a reboot and cannot be undone by anything running here.'
127
+ }
128
+ default { Show-Status }
129
+ }
@@ -0,0 +1,90 @@
1
+ #!/bin/sh
2
+ # scripts/gds/deploy-main-audit-gate.sh — the SEC #863 / ADR 0043 deploy-side gate.
3
+ #
4
+ # Refuses a deploy when a builder BELOW Metic modified a permission-sensitive path in the
5
+ # delta this deploy introduces. Run from the instance checkout root, AFTER the working tree
6
+ # has been moved to the new commit and BEFORE anything is migrated or restarted.
7
+ #
8
+ # OTB_DEPLOY_PREV_SHA="$(git rev-parse HEAD)"; export OTB_DEPLOY_PREV_SHA # BEFORE the pull
9
+ # git reset --hard origin/main
10
+ # sh node_modules/@bongos/core/scripts/gds/deploy-main-audit-gate.sh || exit 1
11
+ # ...migrate, restart...
12
+ #
13
+ # WHY THIS LIVES IN THE CORE (task 1001426). It used to live in the control-plane repo's
14
+ # infra/, where a standalone instance checkout cannot reach it — and on cloudbongos.com it
15
+ # was never wired into ~/deploy.sh at all, so the "poison never reaches prod" check had
16
+ # never once run. Shipping it beside main-audit.js means every instance inherits the gate
17
+ # with its pinned core, and there is no per-instance copy to drift.
18
+ #
19
+ # THE MARKER IS THE CHECKED-OUT HEAD, AND IT ADVANCES ONLY ON SUCCESS.
20
+ # That is the substance of this task and the reason the restore below is in the GATE rather
21
+ # than in each caller's deploy script. The gate can only audit what was pulled, so it runs
22
+ # after the tree has already moved. Exiting non-zero and leaving it moved would mean the
23
+ # next deploy computes OTB_DEPLOY_PREV_SHA from the POISONED commit, never re-audits that
24
+ # delta, and finds the poison already checked out — the marker-bypass this gate exists to
25
+ # close, reintroduced one layer up. So a refusal puts the tree back where it started.
26
+ # Set OTB_GATE_NO_RESTORE=1 only when the caller restores it itself.
27
+ #
28
+ # STRICT IS OFF BY DEFAULT, AND THAT IS NOT LAZINESS. Rank resolution keys on github_login
29
+ # only (#863-followup finding #6: display_name is builder-controlled, so keying it let one
30
+ # builder alias another's rank). A deploy lane whose commits are authored under a human
31
+ # display name ("Firstname Lastname") rather than the builder's github_login therefore
32
+ # resolves to nothing, and OTB_AUDIT_STRICT=1 would block every deploy forever.
33
+ # Non-strict still hard-blocks the incident class this was built for: a RESOLVED below-Metic
34
+ # author touching a protected path. Turn strict on once the lane commits under logins.
35
+ #
36
+ # KNOWN LIMIT, stated because a gate nobody distrusts is worse than no gate: if the roster
37
+ # cannot be read at all, every author is unresolved and non-strict passes. main-audit.js
38
+ # announces that loudly on stderr (task 1001426 gave it a fallback and a voice — it used to
39
+ # fail silently and return an empty map, so the gate reported a clean audit that never ran).
40
+ # Availability is chosen over a hard stop deliberately: a database blip must not freeze
41
+ # deploys. Read the stderr.
42
+
43
+ set -eu
44
+
45
+ if [ -n "${OTB_DEPLOY_PREV_SHA:-}" ]; then
46
+ OTB_AUDIT_RANGE="${OTB_DEPLOY_PREV_SHA}..HEAD"; export OTB_AUDIT_RANGE
47
+ echo "main audit: range ${OTB_AUDIT_RANGE} (exact deploy delta)"
48
+ else
49
+ echo "main audit: OTB_DEPLOY_PREV_SHA not set — falling back to the wide window."
50
+ echo " (Recommended: capture OTB_DEPLOY_PREV_SHA=\$(git rev-parse HEAD) BEFORE the pull;"
51
+ echo " without it a poisoned commit buried under the window can escape, and a refusal"
52
+ echo " cannot put the tree back.)"
53
+ fi
54
+
55
+ STRICT=""
56
+ [ "${OTB_AUDIT_STRICT:-0}" = "1" ] && STRICT="--strict"
57
+
58
+ # Prefer the installed core; fall back to an in-tree checkout (the core repo itself).
59
+ MAIN_AUDIT="node_modules/@bongos/core/scripts/gds/main-audit.js"
60
+ [ -f "$MAIN_AUDIT" ] || MAIN_AUDIT="scripts/gds/main-audit.js"
61
+ if [ ! -f "$MAIN_AUDIT" ]; then
62
+ echo "X DEPLOY ABORTED: main-audit.js not found (looked in node_modules/@bongos/core and in-tree)."
63
+ echo " A gate that cannot find its auditor must fail closed, not wave the deploy through."
64
+ exit 1
65
+ fi
66
+
67
+ if node "$MAIN_AUDIT" --verify-head --db ${STRICT}; then
68
+ echo "OK main audit clean${STRICT:+ (strict)} — proceeding with deploy."
69
+ exit 0
70
+ fi
71
+
72
+ echo ""
73
+ echo "X DEPLOY ABORTED (SEC #863): a below-Metic${STRICT:+ or unresolved} author modified a"
74
+ echo " permission-sensitive path in this deploy. Investigate and revert before deploying:"
75
+ echo " node $MAIN_AUDIT --range \"\${OTB_AUDIT_RANGE:-HEAD~200..HEAD}\" --revert"
76
+
77
+ if [ -n "${OTB_DEPLOY_PREV_SHA:-}" ] && [ "${OTB_GATE_NO_RESTORE:-0}" != "1" ]; then
78
+ if git rev-parse --verify --quiet "${OTB_DEPLOY_PREV_SHA}^{commit}" >/dev/null; then
79
+ echo ""
80
+ echo " restoring the checkout to ${OTB_DEPLOY_PREV_SHA} — the deployed marker advances only on success,"
81
+ echo " so the NEXT deploy re-audits this same delta instead of treating it as already-reviewed."
82
+ git reset --hard "${OTB_DEPLOY_PREV_SHA}" >/dev/null
83
+ else
84
+ echo ""
85
+ echo " ::WARNING:: cannot restore — ${OTB_DEPLOY_PREV_SHA} is not a commit in this checkout."
86
+ echo " The tree is left at the REFUSED commit; the next deploy will not re-audit this delta."
87
+ fi
88
+ fi
89
+
90
+ exit 1
@@ -251,6 +251,13 @@ function rowsToMap(rows) {
251
251
  return map;
252
252
  }
253
253
 
254
+ // Where getBuildersRoster lives. It was carved out of src/bongos/db.js into the
255
+ // lifecycle module (goal 1000010's per-domain db.js split) and this file was not
256
+ // updated with it, so `--db` threw `db.getBuildersRoster is not a function` on every
257
+ // run — for however long that carve has been landed. Kept as a named constant so the
258
+ // next carve breaks a test (tests/main_audit_db_roster.mjs) rather than a deploy gate.
259
+ const ROSTER_DB_MODULE = '../../modules/lifecycle/db.js';
260
+
254
261
  // API mode (default): for an Archon session / operator laptop. DB mode (--db):
255
262
  // for the droplet deploy gate, which has DB access but no GDS session — it reads
256
263
  // the roster straight from Postgres. db.js is lazy-required so importing this
@@ -258,13 +265,24 @@ function rowsToMap(rows) {
258
265
  async function fetchRankMap({ useDb = false } = {}) {
259
266
  if (useDb) {
260
267
  try {
261
- const db = require('../../src/bongos/db.js');
268
+ const db = require(ROSTER_DB_MODULE);
269
+ if (typeof db.getBuildersRoster !== 'function') {
270
+ // The exact failure this file already shipped once, named rather than caught as a
271
+ // generic TypeError: a moved export reads identically to "no database" otherwise.
272
+ throw new Error(`${ROSTER_DB_MODULE} exports no getBuildersRoster — it has moved again`);
273
+ }
262
274
  const roster = await db.getBuildersRoster({ staleHours: 24 });
263
- return rowsToMap(roster.builders || roster);
275
+ const map = rowsToMap(roster.builders || roster);
276
+ if (Object.keys(map).length) return map;
277
+ console.error('main-audit: --db roster read returned NO builders — falling through to the API/public roster.');
264
278
  } catch (e) {
265
- console.error(`main-audit: --db roster read failed (${e.message})`);
266
- return {};
279
+ console.error(`main-audit: --db roster read failed (${e.message}) — falling through to the API/public roster.`);
267
280
  }
281
+ // FALL THROUGH, do not return {}. Returning an empty map here is the vacuous green
282
+ // this file's own comment warns about twelve lines down: with no ranks, every author
283
+ // resolves to null, no blocker can ever fire, and the gate reports success for the
284
+ // reason it was built to prevent. A `--db` gate that cannot reach Postgres is better
285
+ // served by the public leaderboard than by silence.
268
286
  }
269
287
  try {
270
288
  const api = await cliClient({ allowUnauthenticated: true });
@@ -394,4 +412,8 @@ if (require.main === module) {
394
412
  module.exports = {
395
413
  analyzeCommits, resolveRank, parseCommits, rowsToMap,
396
414
  clampWindow, defaultRange, firstParentDepth, DEFAULT_WINDOW,
415
+ // Exported so a test can assert the address is LIVE without a database. The --db
416
+ // path was dead for as long as the db.js carve has been landed precisely because
417
+ // nothing checked that this require target still exports what it is called for.
418
+ ROSTER_DB_MODULE, fetchRankMap,
397
419
  };
@@ -464,7 +464,12 @@ function main() {
464
464
  if (n) skipped.push({ name: displayName, n });
465
465
  console.log(` ok ${displayName}${n ? ` (${n} SKIPPED — not run on this platform)` : ''}`);
466
466
  } catch (err) {
467
- const why = err.killed ? 'TIMEOUT' : `exit ${err.status}`;
467
+ // `exit null` on its own is unreadable, and it is what a HANG looks like here: the
468
+ // signal that ended the process is the whole diagnosis, so say it (task 1004052 —
469
+ // a test that hung for the full budget reported `exit null` and nothing else).
470
+ const why = err.killed
471
+ ? `TIMEOUT after ${perTestTimeout / 1000}s${err.signal ? `, ${err.signal}` : ''}`
472
+ : `exit ${err.status}${err.signal ? ` (${err.signal})` : ''}`;
468
473
  const stdoutText = (err.stdout ? err.stdout.toString() : '').trim();
469
474
  if (!err.killed && isCleanTeardownAbort(stdoutText, err.stderr ? err.stderr.toString() : '')) {
470
475
  console.log(` ok ${displayName} (clean verdict, then libuv teardown abort, ${why})`);
@@ -474,6 +479,16 @@ function main() {
474
479
  console.log(` FAIL ${displayName} (${why})`);
475
480
  // The actual failures first: the file's own FAIL lines, then its stdout
476
481
  // tail (the last failure's assertion detail + summary), both bounded.
482
+ // EMPTY STDOUT IS ITSELF THE CLUE, and it used to print as a blank gap. node:test
483
+ // buffers a file's whole report until the file finishes, so a suite that hangs
484
+ // mid-run emits NOTHING — which reads as "no detail available" when it actually
485
+ // means "it never got to the end". Name that, and name how to find the case.
486
+ if (!stdoutText) {
487
+ console.log(' (no output at all — node:test buffers a file\'s report until it finishes,');
488
+ console.log(' so this suite did not reach the end. To find which case: run it on the same');
489
+ console.log(' platform with each case writing its name to STDERR before it runs; stderr is');
490
+ console.log(' unbuffered, so the last name with no completion is the one that hung.)');
491
+ }
477
492
  const failLines = stdoutText ? stdoutText.split('\n').filter((l) => /\bFAIL\b/.test(l)).slice(0, 5) : [];
478
493
  for (const l of failLines) console.log(` ${l.trim()}`);
479
494
  if (stdoutText) {
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.876'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.877'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');