@bongos/core 1.19.803 → 1.19.805
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 +261 -236
- package/.claude/skills/ask-for-help/SKILL.md +6 -6
- package/.claude/skills/backlog-review/SKILL.md +1 -1
- package/.claude/skills/blocker-review/SKILL.md +7 -7
- package/.claude/skills/blocker-solve/SKILL.md +5 -5
- package/.claude/skills/bug-triage/SKILL.md +7 -7
- package/.claude/skills/builder-claim/SKILL.md +3 -3
- package/.claude/skills/builder-cost/SKILL.md +2 -2
- package/.claude/skills/builder-end/SKILL.md +2 -2
- package/.claude/skills/builder-exit/SKILL.md +7 -7
- package/.claude/skills/builder-key/SKILL.md +2 -2
- package/.claude/skills/builder-reauth/SKILL.md +2 -2
- package/.claude/skills/builder-redteam/SKILL.md +7 -7
- package/.claude/skills/builder-release/SKILL.md +2 -2
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-setup/SKILL.md +3 -3
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/builder-stage/SKILL.md +1 -1
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/fix-task/SKILL.md +4 -4
- package/.claude/skills/goal-create/SKILL.md +18 -18
- package/.claude/skills/goal-review/SKILL.md +7 -7
- package/.claude/skills/grade-audit/SKILL.md +5 -5
- package/.claude/skills/grade-recover/SKILL.md +4 -4
- package/.claude/skills/grader-health/SKILL.md +6 -6
- package/.claude/skills/idea-triage/SKILL.md +14 -14
- package/.claude/skills/merge-mode/SKILL.md +3 -3
- package/.claude/skills/planning-session/SKILL.md +20 -20
- package/.claude/skills/priority-session/SKILL.md +10 -10
- package/.claude/skills/recall/SKILL.md +1 -1
- package/.claude/skills/scan-before-install/SKILL.md +3 -3
- package/.claude/skills/status/SKILL.md +2 -2
- package/.claude/skills/strand-fix/SKILL.md +2 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +45 -11
- package/docs/api-reference.md +11 -10
- package/docs/module-api-changelog.md +4 -0
- package/modules/builder-settings/profile-nudge-prefs.js +1 -1
- package/modules/builder-settings/render-prefs.js +1 -1
- package/modules/dev-box/app/README.md +1 -1
- package/modules/dev-box/app/src/gds-client.js +11 -9
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/dev-box/app/test/unit.mjs +13 -13
- package/modules/dev-box/box-access.js +3 -3
- package/modules/dev-box/box-onboard.js +7 -7
- package/modules/discord/bug-attachments.js +1 -1
- package/modules/economy/cost-classify.js +6 -6
- package/modules/grading/grader-rubric.json +1 -1
- package/modules/grading/grader-score.js +1 -1
- package/modules/hall-ui/module.json +1 -1
- package/modules/hall-ui/public/drachmae.js +3 -3
- package/modules/hall-ui/public/idea.js +1 -1
- package/modules/hall-ui/public/md-reader.js +1 -1
- package/modules/hall-ui/public/people.html +5 -1
- package/modules/hall-ui/public/people.js +44 -5
- package/modules/hall-ui/public/primer.js +4 -4
- package/modules/hall-ui/public/roster.js +1 -1
- package/modules/hall-ui/public/shell.js +1 -1
- package/modules/hall-ui/public/task.js +1 -1
- package/modules/ideas/inbox.js +2 -2
- package/modules/lifecycle/db-analytics.js +1 -1
- package/modules/lifecycle/done-when.js +4 -4
- package/modules/lifecycle/version-close.js +1 -1
- package/modules/memory/learnings.js +2 -2
- package/modules/memory/memory.js +1 -1
- package/modules/onboarding/onboarding.js +1 -1
- package/modules/platform-identity/routes/my-projects.js +1 -1
- package/modules/platform-identity/routes/sso.js +2 -2
- package/modules/public-landing/public/index.html +2 -2
- package/modules/public-landing/public/projects-floor.states.json +1 -1
- package/modules/public-landing/public/projects.html +1 -1
- package/modules/public-landing/public/projects.states.json +1 -1
- package/modules/sessions/db.js +120 -0
- package/modules/sessions/module.json +2 -2
- package/modules/sessions/pollers/session-upload-health.js +111 -0
- package/modules/sessions/pulse.js +2 -2
- package/modules/sessions/routes/sessions.js +46 -0
- package/modules/sessions/upload-health.js +197 -0
- package/modules/status-ui/module.json +1 -1
- package/modules/status-ui/public/explore.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/agent-invoke.js +3 -3
- package/scripts/gds/api.js +4 -4
- package/scripts/gds/api.sh +1 -1
- package/scripts/gds/architect-audit.js +1 -1
- package/scripts/gds/artifact-audit.js +2 -2
- package/scripts/gds/audit-ships.js +2 -2
- package/scripts/gds/backfill-cost.js +1 -1
- package/scripts/gds/backfill-sessions.js +1 -1
- package/scripts/gds/backfill-subagent-rewards.js +1 -1
- package/scripts/gds/backfill-task-rewards.js +2 -2
- package/scripts/gds/box-auth-check.js +1 -1
- package/scripts/gds/box-sync.js +3 -3
- package/scripts/gds/box.js +10 -10
- package/scripts/gds/build-cli-package.js +3 -3
- package/scripts/gds/capture.js +2 -2
- package/scripts/gds/ci-grade.js +1 -1
- package/scripts/gds/claim-recall.js +2 -2
- package/scripts/gds/cli-lib.js +3 -3
- package/scripts/gds/context-pack.js +1 -1
- package/scripts/gds/copy-apply.js +2 -2
- package/scripts/gds/dev-lib.js +2 -2
- package/scripts/gds/fetch-art-key.js +2 -2
- package/scripts/gds/fetch-sound-prefs.js +3 -3
- package/scripts/gds/grade-smoke.js +1 -1
- package/scripts/gds/hooks/pre-push.js +1 -1
- package/scripts/gds/init.js +9 -9
- package/scripts/gds/key-card.js +1 -1
- package/scripts/gds/learning-capture.js +2 -2
- package/scripts/gds/linkify-refs.js +2 -2
- package/scripts/gds/local-preview-lib.js +1 -1
- package/scripts/gds/login.js +11 -10
- package/scripts/gds/main-audit.js +1 -1
- package/scripts/gds/newcomer-floor.config.json +1 -1
- package/scripts/gds/newcomer-floor.js +2 -2
- package/scripts/gds/newcomer-floor.templates.json +2 -2
- package/scripts/gds/onboarding-ship.js +1 -1
- package/scripts/gds/paste-token.js +3 -3
- package/scripts/gds/portability-smoke-memory.sh +5 -5
- package/scripts/gds/prove-strict-versioning.js +2 -2
- package/scripts/gds/provision-net.js +2 -2
- package/scripts/gds/provision-units.js +3 -3
- package/scripts/gds/pulse.js +2 -2
- package/scripts/gds/push-path-brief.js +2 -2
- package/scripts/gds/reauth.js +1 -1
- package/scripts/gds/recall.js +1 -1
- package/scripts/gds/redteam-patrol.js +3 -3
- package/scripts/gds/render-ideas.js +5 -5
- package/scripts/gds/repo-metrics.js +1 -1
- package/scripts/gds/sandbox-stage.js +3 -3
- package/scripts/gds/seed-adopt-tasks.js +3 -3
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/seed-bongos-app-tasks.js +2 -2
- package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +3 -3
- package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +4 -4
- package/scripts/gds/seed-bongos-tasks.js +5 -5
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +1 -1
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +1 -1
- package/scripts/gds/seed-bv1-community-tasks.js +1 -1
- package/scripts/gds/seed-bv1-privacy-invites-tasks.js +1 -1
- package/scripts/gds/seed-bv1-profile-page-tasks.js +1 -1
- package/scripts/gds/seed-bv1-profiles-tasks.js +1 -1
- package/scripts/gds/seed-bv1-r01-delta-tasks.js +1 -1
- package/scripts/gds/seed-gds-v4-tasks.js +3 -3
- package/scripts/gds/seed-medusa-tasks.js +1 -1
- package/scripts/gds/seed-productization-tasks.js +1 -1
- package/scripts/gds/seed-project-admin-console-tasks.js +1 -1
- package/scripts/gds/seed-provisioning-tasks.js +2 -2
- package/scripts/gds/seed-strict-versioning-tasks.js +3 -3
- package/scripts/gds/seed-v2-world-tasks.js +2 -2
- package/scripts/gds/seed-verify-criteria.js +1 -1
- package/scripts/gds/session-digest.js +1 -1
- package/scripts/gds/session.js +2 -2
- package/scripts/gds/setup.js +7 -7
- package/scripts/gds/ship-flow.js +3 -3
- package/scripts/gds/ship-grade-steps.js +3 -3
- package/scripts/gds/ship-memory-sync.js +1 -1
- package/scripts/gds/ship-session-upload.js +3 -3
- package/scripts/gds/smoke-batch-claim.sh +10 -10
- package/scripts/gds/smoke-batch-release.sh +14 -14
- package/scripts/gds/smoke-claim-consistency.sh +19 -19
- package/scripts/gds/smoke-dependencies.sh +9 -9
- package/scripts/gds/smoke-write.sh +11 -11
- package/scripts/gds/start.js +1 -1
- package/scripts/gds/status.js +2 -2
- package/scripts/gds/strand-watch.js +1 -1
- package/scripts/gds/triage.js +4 -4
- package/scripts/gds/version-close.js +2 -2
- package/scripts/hall-preview/README.md +1 -1
- package/scripts/hall-preview/server.js +1 -1
- package/src/bongos/auth-admission.js +3 -3
- package/src/bongos/auth-idp.js +2 -2
- package/src/bongos/auth.js +3 -3
- package/src/bongos/db.js +2 -2
- package/src/bongos/llm-cache.js +2 -2
- package/src/bongos/metrics.js +1 -1
- package/src/bongos/middleware/audit.js +3 -3
- package/src/bongos/middleware/rate-limit.js +1 -1
- package/src/bongos/platform-visibility-gate.js +2 -2
- package/src/bongos/routes/_helpers.js +1 -1
- package/src/bongos/routes/audit-log.js +1 -1
- package/src/bongos/routes/auth.js +5 -5
- package/src/bongos/routes/backup.js +1 -1
- package/src/bongos/routes/builders.js +1 -1
- package/src/bongos/routes/instance.js +1 -1
- package/src/bongos/routes/live.js +1 -1
- package/src/bongos/routes/me.js +2 -2
- package/src/bongos/routes/modules.js +2 -2
- package/src/bongos/routes/public.js +1 -1
- package/src/bongos/routes/security.js +4 -4
- package/src/bongos/routes.js +1 -1
- package/src/bongos/serve-internal.js +6 -6
- package/src/branding.js +1 -1
- package/src/module-api.js +1 -1
- package/src/module-seams.js +2 -2
- package/src/platform-server.js +2 -2
- package/tests/api_alias_exceptions.mjs +67 -0
- package/tests/application_echo.mjs +12 -12
- package/tests/artifact_audit.mjs +1 -1
- package/tests/auth_page_gate.mjs +4 -4
- package/tests/auth_web_recovery.mjs +11 -11
- package/tests/bongos_login.mjs +7 -4
- package/tests/box_env.mjs +6 -6
- package/tests/bug_attachments.mjs +1 -1
- package/tests/cli_sessions.mjs +1 -1
- package/tests/dev_lib.mjs +1 -1
- package/tests/discord_bugs.mjs +2 -2
- package/tests/discord_inbound.mjs +2 -2
- package/tests/explore_visible_states.mjs +1 -1
- package/tests/government_page_gates.mjs +1 -1
- package/tests/hall_reference_world.mjs +1 -1
- package/tests/idea_ideate_full.mjs +1 -1
- package/tests/idp_federation.mjs +2 -2
- package/tests/llm-cache.mjs +1 -1
- package/tests/manage_manifest_shared_read.mjs +3 -3
- package/tests/people_directory_clip.mjs +219 -0
- package/tests/planning_session_skill.mjs +1 -1
- package/tests/platform_visibility_gate.mjs +15 -15
- package/tests/profile_route.mjs +37 -26
- package/tests/projects_hub.mjs +2 -2
- package/tests/provision.mjs +2 -2
- package/tests/provision_settings_apply.mjs +4 -4
- package/tests/sandbox_stage.mjs +8 -8
- package/tests/scouting_owner_access_and_reach.mjs +3 -3
- package/tests/session_upload_health.mjs +329 -0
- package/tests/setup_device_flow_fallback.mjs +1 -1
- package/tests/skill_ask_for_help.mjs +3 -3
- package/tests/skill_grade_audit.mjs +2 -2
- package/tests/skill_grader_health.mjs +3 -3
- package/tests/smoke_write_contract.mjs +1 -1
package/modules/sessions/db.js
CHANGED
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
|
|
25
25
|
const api = require('../../src/module-api');
|
|
26
26
|
const { pool } = api;
|
|
27
|
+
// The PURE verdict + every clamp and threshold (task 1003865). The module's own
|
|
28
|
+
// file, not the doorway: it touches no kernel surface, and keeping the numbers
|
|
29
|
+
// in one place is what stops the route and the poller disagreeing.
|
|
30
|
+
const uploadHealth = require('./upload-health');
|
|
27
31
|
// `seams` is the doorway's kernel-mediated seam surface (resolveOptional/resolve).
|
|
28
32
|
// upsertSessionRecordWithReward resolves the economy `reward` port through it —
|
|
29
33
|
// the module↔module seam, never a sibling-module require (BV1.R77 / ADR 0093 §2).
|
|
@@ -656,6 +660,118 @@ async function filterTaskIdsClaimedBy(builderId, taskIds) {
|
|
|
656
660
|
return rows.map((r) => Number(r.task_id));
|
|
657
661
|
}
|
|
658
662
|
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
// -------------------------------------------------------------------------
|
|
666
|
+
// Session-upload health (task 1003865) — is the corpus still being written?
|
|
667
|
+
// -------------------------------------------------------------------------
|
|
668
|
+
//
|
|
669
|
+
// THE SIGNAL. The server already knows both halves independently: a task reached
|
|
670
|
+
// status='shipped', and whether that task's builder uploaded any session around
|
|
671
|
+
// then. A ship with no session record for its builder inside a window is the unit
|
|
672
|
+
// of evidence. Before this, the ONLY trace of a skipped upload was a jsonl file on
|
|
673
|
+
// the builder's own laptop, so a weeks-long outage that stopped every cost-plus
|
|
674
|
+
// reward from booking was invisible from the server.
|
|
675
|
+
//
|
|
676
|
+
// GROUPED BY BUILDER, which is the entire design and is measured, not assumed —
|
|
677
|
+
// see ../upload-health for the numbers. The corpus-wide share cannot see the
|
|
678
|
+
// outage (3.4% in its worst month, against 1.7-2.0% in healthy ones); per builder
|
|
679
|
+
// the same data fires on 13 days, all inside the outage window, all one builder.
|
|
680
|
+
//
|
|
681
|
+
// KEYED ON builder_id + TIME, NEVER ON task_ids. session_records.task_ids is
|
|
682
|
+
// EMPTY on 54% of rows (1,771 of 3,873 when task 1002448 measured it), so the
|
|
683
|
+
// obvious predicate — "no session record NAMES this task" — reports the linkage
|
|
684
|
+
// gap as an outage. Neither query below mentions task_ids, and a test asserts
|
|
685
|
+
// that about the SQL these functions actually emit.
|
|
686
|
+
//
|
|
687
|
+
// Every threshold and clamp lives in ../upload-health (PURE, unit-tested), so the
|
|
688
|
+
// route and the poller cannot answer this question two different ways.
|
|
689
|
+
|
|
690
|
+
// sessionUploadCoverage — per-BUILDER ship counts over the lookback, and how many
|
|
691
|
+
// of those ships had NO session record for that builder inside the window. One
|
|
692
|
+
// query, server-side, deliberately: /sessions/search is rate-limited per builder
|
|
693
|
+
// and clamps limit to 100, so paging the corpus from a client to compute this is
|
|
694
|
+
// neither cheap nor reliable (measured while scoping this task — 45 pages, with
|
|
695
|
+
// 429s).
|
|
696
|
+
async function sessionUploadCoverage({ days, windowHours } = {}) {
|
|
697
|
+
const d = uploadHealth.clampLookbackDays(days);
|
|
698
|
+
const w = uploadHealth.clampWindowHours(windowHours);
|
|
699
|
+
const { rows } = await pool.query(
|
|
700
|
+
`SELECT t.shipped_by AS builder_id,
|
|
701
|
+
b.github_login,
|
|
702
|
+
COUNT(*)::int AS ships,
|
|
703
|
+
COUNT(*) FILTER (
|
|
704
|
+
WHERE NOT EXISTS (
|
|
705
|
+
SELECT 1
|
|
706
|
+
FROM session_records sr
|
|
707
|
+
WHERE sr.builder_id = t.shipped_by
|
|
708
|
+
AND sr.uploaded_at >= t.shipped_at - ($2 * interval '1 hour')
|
|
709
|
+
AND sr.uploaded_at <= t.shipped_at + ($2 * interval '1 hour')
|
|
710
|
+
)
|
|
711
|
+
)::int AS uncovered
|
|
712
|
+
FROM tasks t
|
|
713
|
+
JOIN builders b ON b.id = t.shipped_by
|
|
714
|
+
WHERE t.status = 'shipped'
|
|
715
|
+
AND t.shipped_by IS NOT NULL
|
|
716
|
+
AND t.shipped_at IS NOT NULL
|
|
717
|
+
AND t.shipped_at >= now() - ($1 * interval '1 day')
|
|
718
|
+
GROUP BY t.shipped_by, b.github_login
|
|
719
|
+
ORDER BY 4 DESC`,
|
|
720
|
+
[d, w]
|
|
721
|
+
);
|
|
722
|
+
return { days: d, windowHours: w, rows };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// shipsMissingSessionRecord — the offending ships themselves, newest first. The
|
|
726
|
+
// verdict says how bad and whose; this says WHICH, because recovering the unpaid
|
|
727
|
+
// work needs the task ids and a percentage does not carry them. Bounded by
|
|
728
|
+
// clampSampleLimit: evidence, not a report.
|
|
729
|
+
async function shipsMissingSessionRecord({ days, windowHours, limit, builderId } = {}) {
|
|
730
|
+
const d = uploadHealth.clampLookbackDays(days);
|
|
731
|
+
const w = uploadHealth.clampWindowHours(windowHours);
|
|
732
|
+
const n = uploadHealth.clampSampleLimit(limit);
|
|
733
|
+
const b = Number.isFinite(Number(builderId)) ? Number(builderId) : null;
|
|
734
|
+
const { rows } = await pool.query(
|
|
735
|
+
`SELECT t.id, t.title, t.shipped_by, b.github_login, t.shipped_at
|
|
736
|
+
FROM tasks t
|
|
737
|
+
JOIN builders b ON b.id = t.shipped_by
|
|
738
|
+
WHERE t.status = 'shipped'
|
|
739
|
+
AND t.shipped_by IS NOT NULL
|
|
740
|
+
AND t.shipped_at IS NOT NULL
|
|
741
|
+
AND t.shipped_at >= now() - ($1 * interval '1 day')
|
|
742
|
+
AND ($3::bigint IS NULL OR t.shipped_by = $3)
|
|
743
|
+
AND NOT EXISTS (
|
|
744
|
+
SELECT 1
|
|
745
|
+
FROM session_records sr
|
|
746
|
+
WHERE sr.builder_id = t.shipped_by
|
|
747
|
+
AND sr.uploaded_at >= t.shipped_at - ($2 * interval '1 hour')
|
|
748
|
+
AND sr.uploaded_at <= t.shipped_at + ($2 * interval '1 hour')
|
|
749
|
+
)
|
|
750
|
+
ORDER BY t.shipped_at DESC
|
|
751
|
+
LIMIT ${n}`,
|
|
752
|
+
[d, w, b]
|
|
753
|
+
);
|
|
754
|
+
return rows;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// sessionUploadHealth — the whole answer in one call: the verdict, the per-builder
|
|
758
|
+
// counts behind it, and a sample of the ships it is counting. The route serves
|
|
759
|
+
// this and the poller logs from it, so neither owns the arithmetic. The sample is
|
|
760
|
+
// skipped entirely when nothing is uncovered — the healthy path costs one query.
|
|
761
|
+
async function sessionUploadHealth({ days, windowHours, sampleLimit } = {}) {
|
|
762
|
+
const coverage = await sessionUploadCoverage({ days, windowHours });
|
|
763
|
+
const verdict = uploadHealth.uploadHealthVerdict(coverage.rows);
|
|
764
|
+
const sample = verdict.uncovered > 0
|
|
765
|
+
? await shipsMissingSessionRecord({ days, windowHours, limit: sampleLimit })
|
|
766
|
+
: [];
|
|
767
|
+
return {
|
|
768
|
+
days: coverage.days,
|
|
769
|
+
window_hours: coverage.windowHours,
|
|
770
|
+
verdict,
|
|
771
|
+
sample,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
659
775
|
module.exports = {
|
|
660
776
|
// write side (6D.1)
|
|
661
777
|
upsertSessionRecord,
|
|
@@ -667,6 +783,10 @@ module.exports = {
|
|
|
667
783
|
// search + fetch (6D.2)
|
|
668
784
|
searchSessions,
|
|
669
785
|
summarizeSessions,
|
|
786
|
+
// upload health (task 1003865) — is the corpus still being written at all?
|
|
787
|
+
sessionUploadCoverage,
|
|
788
|
+
shipsMissingSessionRecord,
|
|
789
|
+
sessionUploadHealth,
|
|
670
790
|
getSessionById,
|
|
671
791
|
listSessionsByBuilder,
|
|
672
792
|
filterTaskIdsClaimedBy,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"key": "sessions",
|
|
3
3
|
"title": "Sessions & BFG (session corpus · efficiency eval)",
|
|
4
|
-
"description": "The session-records corpus + its data plane (POST /sessions upload, the Archon-gated cross-builder /sessions/search + /sessions/:id deep-dive), the BFG 'read-the-room' cross-builder scorer (ADR 0026 §6B), and the session-pulse / stale-claim heartbeat helpers. A core-domain module carved out of the GDS core in BV1.R82 (ADR 0093 §1); default-on because every instance records its builders' sessions for the BFG. Like the memory module, sessions registers no port — core never calls back into it: the ship-time digest write is an HTTP POST to the module's own /sessions route, and the only outward dependency is the economy `reward` kernel PORT it resolves (session token reward + sumCreditsForTasks) at upload time.",
|
|
4
|
+
"description": "The session-records corpus + its data plane (POST /sessions upload, the Archon-gated cross-builder /sessions/search + /sessions/:id deep-dive + /sessions/upload-health, the server-side signal that a builder's session uploads have silently stopped), the BFG 'read-the-room' cross-builder scorer (ADR 0026 §6B), and the session-pulse / stale-claim heartbeat helpers. A core-domain module carved out of the GDS core in BV1.R82 (ADR 0093 §1); default-on because every instance records its builders' sessions for the BFG. Like the memory module, sessions registers no port — core never calls back into it: the ship-time digest write is an HTTP POST to the module's own /sessions route, and the only outward dependency is the economy `reward` kernel PORT it resolves (session token reward + sumCreditsForTasks) at upload time.",
|
|
5
5
|
"coreVersion": "^1.10.0",
|
|
6
6
|
"default": true,
|
|
7
7
|
"maintenance": { "status": "core-maintained" },
|
|
8
|
-
"contributes": { "routes": ["sessions"], "pollers": ["session-reward-reconcile"] },
|
|
8
|
+
"contributes": { "routes": ["sessions"], "pollers": ["session-reward-reconcile", "session-upload-health"] },
|
|
9
9
|
"provides": [],
|
|
10
10
|
"consumes": []
|
|
11
11
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/sessions/pollers/session-upload-health.js — the half of task 1003865
|
|
4
|
+
// that makes the signal NOTICE rather than merely be answerable.
|
|
5
|
+
//
|
|
6
|
+
// GET /sessions/upload-health makes the outage visible to anyone who asks. Nobody
|
|
7
|
+
// asked for weeks, which is the whole incident: the failure mode is silence, and a
|
|
8
|
+
// number you have to go and look at is silent too. So this sweep asks on a
|
|
9
|
+
// schedule and writes the answer where the server's own operator surface already
|
|
10
|
+
// is — the process log, beside the reward reconciler's lines, which is what a
|
|
11
|
+
// journalctl over this unit is read for.
|
|
12
|
+
//
|
|
13
|
+
// SEPARATE FROM session-reward-reconcile ON PURPOSE. That poller MOVES MONEY; this
|
|
14
|
+
// one only reports. Folding telemetry into the payout sweep would let a bad read
|
|
15
|
+
// here delay or crash a booking there, and the two want different cadences —
|
|
16
|
+
// reward recovery wants minutes, a 14-day coverage ratio does not change in ten.
|
|
17
|
+
//
|
|
18
|
+
// It writes nothing and books nothing. A check that could itself change state
|
|
19
|
+
// would need its own safety argument; this one needs none.
|
|
20
|
+
|
|
21
|
+
const db = require('../db');
|
|
22
|
+
const uploadHealth = require('../upload-health');
|
|
23
|
+
// The doorway's namespaced structured logger (task 1003130), not console: this
|
|
24
|
+
// module reaches the kernel only through module-api, and console.* in a server
|
|
25
|
+
// path is the count the fitness ratchet exists to shrink, not to grow.
|
|
26
|
+
const api = require('../../../src/module-api');
|
|
27
|
+
const log = api.logger('sessions');
|
|
28
|
+
|
|
29
|
+
// Hourly. The measurement behind the thresholds used a 14-day lookback stepped
|
|
30
|
+
// daily, so anything under a day is already far finer than the signal; hourly is
|
|
31
|
+
// chosen so a fresh outage surfaces within the same working session rather than
|
|
32
|
+
// the next morning.
|
|
33
|
+
const INTERVAL_MS = Number(process.env.SESSION_UPLOAD_HEALTH_MS) || 60 * 60_000;
|
|
34
|
+
const FIRST_SWEEP_DELAY_MS = Number(process.env.SESSION_UPLOAD_HEALTH_FIRST_MS) || 120_000;
|
|
35
|
+
|
|
36
|
+
let intervalTimer = null;
|
|
37
|
+
let firstTimer = null;
|
|
38
|
+
|
|
39
|
+
// logLinesFor — PURE, so the wording is pinned by a DB-free test. A log line that
|
|
40
|
+
// says "unhealthy" and no more is the same silence with a timestamp on it: the
|
|
41
|
+
// firing line has to name the builder, the arithmetic, and where to look next,
|
|
42
|
+
// because the person reading it at 2am did not scope this task.
|
|
43
|
+
function logLinesFor(health) {
|
|
44
|
+
const v = (health && health.verdict) || {};
|
|
45
|
+
const where = `GET /api/gds/sessions/upload-health?days=${health && health.days}`;
|
|
46
|
+
if (!v.firing) {
|
|
47
|
+
return [`[session-upload-health] ${v.status || 'unknown'} — ${v.reason || 'no reason given'}`];
|
|
48
|
+
}
|
|
49
|
+
const lines = [
|
|
50
|
+
`[session-upload-health] ALERT — ${v.reason}`,
|
|
51
|
+
`[session-upload-health] a missing session record means no session.token_reward booked (ADR 0146: cost-plus is the only reward stream), so this is unpaid shipped work, not just missing telemetry.`,
|
|
52
|
+
`[session-upload-health] look: ${where} · likely cause: the upload skipping client-side (session-upload-skips.jsonl on that builder's machine).`,
|
|
53
|
+
];
|
|
54
|
+
const sample = Array.isArray(health.sample) ? health.sample.slice(0, 5) : [];
|
|
55
|
+
if (sample.length > 0) {
|
|
56
|
+
lines.push(`[session-upload-health] uncovered ships, newest first: ${sample.map((s) => `#${s.id}`).join(' ')}`);
|
|
57
|
+
}
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function runSweep() {
|
|
62
|
+
const health = await db.sessionUploadHealth({
|
|
63
|
+
days: uploadHealth.DEFAULT_LOOKBACK_DAYS,
|
|
64
|
+
windowHours: uploadHealth.DEFAULT_WINDOW_HOURS,
|
|
65
|
+
});
|
|
66
|
+
const lines = logLinesFor(health);
|
|
67
|
+
// A firing sweep goes out at ERROR so it survives a level filter set to hide
|
|
68
|
+
// the hourly all-clear — the whole failure mode here is a signal nobody saw.
|
|
69
|
+
const write = health.verdict && health.verdict.firing ? log.error : log.info;
|
|
70
|
+
for (const line of lines) write(line);
|
|
71
|
+
return health;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Start the periodic sweep. Idempotent (a second call is a no-op). Kill-switched
|
|
75
|
+
// by SESSION_UPLOAD_HEALTH_DISABLED=1. Timers are unref'd so they never hold the
|
|
76
|
+
// process open on a systemd restart (the session-reward-reconcile precedent).
|
|
77
|
+
function start() {
|
|
78
|
+
if (process.env.SESSION_UPLOAD_HEALTH_DISABLED === '1') {
|
|
79
|
+
log.info('[session-upload-health] disabled (SESSION_UPLOAD_HEALTH_DISABLED=1)');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (intervalTimer) return; // already started
|
|
83
|
+
|
|
84
|
+
const tick = () => {
|
|
85
|
+
runSweep().catch((e) =>
|
|
86
|
+
log.error(e, '[session-upload-health] sweep crashed')
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
firstTimer = setTimeout(tick, FIRST_SWEEP_DELAY_MS);
|
|
91
|
+
if (firstTimer.unref) firstTimer.unref();
|
|
92
|
+
intervalTimer = setInterval(tick, INTERVAL_MS);
|
|
93
|
+
if (intervalTimer.unref) intervalTimer.unref();
|
|
94
|
+
|
|
95
|
+
log.info(
|
|
96
|
+
`[session-upload-health] started — checking session-upload coverage every ${Math.round(INTERVAL_MS / 60_000)}m`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function stop() {
|
|
101
|
+
if (firstTimer) {
|
|
102
|
+
clearTimeout(firstTimer);
|
|
103
|
+
firstTimer = null;
|
|
104
|
+
}
|
|
105
|
+
if (intervalTimer) {
|
|
106
|
+
clearInterval(intervalTimer);
|
|
107
|
+
intervalTimer = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { start, stop, runSweep, logLinesFor, INTERVAL_MS };
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// rows as data; a follow-up task is expected to add the marker
|
|
22
22
|
// column + UPDATE write + builder surface in /builder-start.
|
|
23
23
|
//
|
|
24
|
-
// 2. POST /api/
|
|
24
|
+
// 2. POST /api/bongos/pulse route is NOT wired here — routes.js is out
|
|
25
25
|
// of touches[]. recordPulse() is exported for whichever future
|
|
26
26
|
// request handler is created. The CLI shell at scripts/gds/pulse.js
|
|
27
27
|
// posts to the intended endpoint and will start working once the
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
//
|
|
30
30
|
// Follow-up work (file new tasks when picked up):
|
|
31
31
|
// a) New migration adding claims.pulse_lost_at timestamptz.
|
|
32
|
-
// b) Route: POST /api/
|
|
32
|
+
// b) Route: POST /api/bongos/pulse → recordPulse() (auth + claim
|
|
33
33
|
// ownership check).
|
|
34
34
|
// c) sweepStaleClaims() side-effect: UPDATE claims
|
|
35
35
|
// SET pulse_lost_at = now() WHERE id = ANY(...).
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
// POST /sessions — upload your OWN session digest (6D.1). Owner is
|
|
7
7
|
// forced to the caller; body builder_id is ignored.
|
|
8
8
|
// GET /sessions/mine — your own sessions (self-serve summary list).
|
|
9
|
+
// GET /sessions/upload-health
|
|
10
|
+
// — is the corpus still being written? Ships whose
|
|
11
|
+
// builder uploaded no session around them, per
|
|
12
|
+
// builder (task 1003865). Same cross-builder gate +
|
|
13
|
+
// audit as /search, because it reads the same corpus.
|
|
9
14
|
// GET /sessions/search — cross-builder search over session + builder facets
|
|
10
15
|
// (rank/karma/drachmae/tenure). Archon-gated +
|
|
11
16
|
// audited (the BFG principal posture until 6C.1).
|
|
@@ -36,6 +41,11 @@ const db = require('../db');
|
|
|
36
41
|
// the interaction profile as data precisely so it does not have to require
|
|
37
42
|
// builder-settings, which ADR 0083 forbids.
|
|
38
43
|
const { scoreAccessibility } = require('../accessibility');
|
|
44
|
+
// The PURE clamps behind /sessions/upload-health. The route audits the values it
|
|
45
|
+
// will actually query with, not the raw query string — an audit row naming
|
|
46
|
+
// `days=99999` for a read that ran at 365 is a record of something that did not
|
|
47
|
+
// happen (task 1003865).
|
|
48
|
+
const uploadHealth = require('../upload-health');
|
|
39
49
|
const seams = api; // resolveOptional('reward' | 'builder-settings') — kernel-mediated ports
|
|
40
50
|
const { validateOrRespond, parseId } = api;
|
|
41
51
|
|
|
@@ -358,6 +368,42 @@ module.exports = function buildSessionsRouter() {
|
|
|
358
368
|
}
|
|
359
369
|
});
|
|
360
370
|
|
|
371
|
+
// GET /sessions/upload-health — the answer to "has anyone's session upload
|
|
372
|
+
// silently stopped?", which before task 1003865 existed ONLY as a jsonl file on
|
|
373
|
+
// the builder's own laptop. A skipped upload means no session_records row, which
|
|
374
|
+
// means the ADR 0146 cost-plus reward never books — a builder earning nothing for
|
|
375
|
+
// work that shipped, invisible from here.
|
|
376
|
+
//
|
|
377
|
+
// Read-only and side-effect-free, so it is DELIBERATELY not a new permission:
|
|
378
|
+
// it reads the same corpus /sessions/search reads, for the same cross-builder
|
|
379
|
+
// audience, so it rides the same `session.search` gate and writes the same
|
|
380
|
+
// fail-closed audit row. A new permission would be a new thing to grant before
|
|
381
|
+
// anyone could look at a number that already exists.
|
|
382
|
+
router.get('/sessions/upload-health', auth.requireBuilder, auth.requirePermission('session.search'), readRateLimit, async (req, res) => {
|
|
383
|
+
const q = req.query || {};
|
|
384
|
+
try {
|
|
385
|
+
// Audit BEFORE the query, like /search: no cross-builder read without a
|
|
386
|
+
// durable record of who asked.
|
|
387
|
+
await recordSessionRead(req, {
|
|
388
|
+
route: '/api/bongos/sessions/upload-health',
|
|
389
|
+
targetBuilderId: null,
|
|
390
|
+
target: `upload-health days=${uploadHealth.clampLookbackDays(q.days)} window_hours=${uploadHealth.clampWindowHours(q.window_hours)}`,
|
|
391
|
+
});
|
|
392
|
+
const health = await db.sessionUploadHealth({
|
|
393
|
+
days: q.days,
|
|
394
|
+
windowHours: q.window_hours,
|
|
395
|
+
sampleLimit: q.sample_limit,
|
|
396
|
+
});
|
|
397
|
+
res.json(health);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
// The namespaced structured logger, not console — see the poller's note.
|
|
400
|
+
// Its siblings above still use console because they predate task 1003130;
|
|
401
|
+
// new call sites do not add to the count the ratchet exists to shrink.
|
|
402
|
+
api.logger('sessions').error(err, 'GET /sessions/upload-health failed');
|
|
403
|
+
res.fail('upload_health_failed', 500);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
361
407
|
// rank: any-builder — your OWN sessions (self-serve; no audit needed).
|
|
362
408
|
router.get('/sessions/mine', auth.requireBuilder, readRateLimit, async (req, res) => {
|
|
363
409
|
try {
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/sessions/upload-health.js — the PURE verdict over session-upload
|
|
4
|
+
// coverage (task 1003865, goal 1000118).
|
|
5
|
+
//
|
|
6
|
+
// THE OUTAGE THIS EXISTS FOR. A Windows path-encoding bug made every transcript
|
|
7
|
+
// lookup miss, so each ship skipped its session upload with `transcript_not_found`.
|
|
8
|
+
// No session_records row was written, so no `session.token_reward` credit booked —
|
|
9
|
+
// and since ADR 0146 made cost-plus the SOLE reward stream, the affected builder
|
|
10
|
+
// earned nothing for real shipped work. It ran for weeks before a human noticed.
|
|
11
|
+
//
|
|
12
|
+
// WHY NOTHING NOTICED. The only trace was `session-upload-skips.jsonl`, written by
|
|
13
|
+
// recordUploadSkip on the BUILDER'S OWN MACHINE. It never reaches the server, so
|
|
14
|
+
// from the server's side a skipped upload and a builder who simply did not work
|
|
15
|
+
// look identical: the absence of a row, in both cases.
|
|
16
|
+
//
|
|
17
|
+
// So the signal is built from two things the server already knows INDEPENDENTLY:
|
|
18
|
+
// a task reached `shipped`, and whether that task's builder uploaded any session
|
|
19
|
+
// around then. A ship with no session record for its builder inside a window is
|
|
20
|
+
// the unit of evidence.
|
|
21
|
+
//
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// PER BUILDER, AND THAT IS THE WHOLE DESIGN — measured, not assumed.
|
|
24
|
+
//
|
|
25
|
+
// The obvious version of this check — the uncovered share across the CORPUS —
|
|
26
|
+
// cannot see the outage at all. Measured against the real corpora on 2026-09-16
|
|
27
|
+
// (4,495 session records and 2,640 ships carrying shipped_at + shipped_by, paged
|
|
28
|
+
// out of /sessions/search and /tasks):
|
|
29
|
+
//
|
|
30
|
+
// corpus-wide uncovered share, ±24h: 2026-06 1.7% · 2026-07 3.4% ·
|
|
31
|
+
// 2026-08 2.0% · 2026-09 0.2%
|
|
32
|
+
//
|
|
33
|
+
// July and August — the outage months — are indistinguishable from the rest. Of
|
|
34
|
+
// course they are: the failure was one builder on one machine, about 19 ships out
|
|
35
|
+
// of the ~1,164 that landed across those two months. A corpus-wide ratio puts the
|
|
36
|
+
// signal two orders of magnitude beneath its own noise floor.
|
|
37
|
+
//
|
|
38
|
+
// Keyed on the BUILDER, the same data separates cleanly. Re-running the check as
|
|
39
|
+
// it would have run — a rolling 14-day lookback stepped one day at a time over
|
|
40
|
+
// the ~92 evaluable days — at ±24h, min 8 ships, firing above 25%:
|
|
41
|
+
//
|
|
42
|
+
// fires on 13 days, ALL of them 2026-07-24 … 2026-08-05, ALL one builder
|
|
43
|
+
// peak 2026-07-25: 19 of 19 ships with no session record — a total outage
|
|
44
|
+
// silent on the other ~79 days and on every other builder
|
|
45
|
+
//
|
|
46
|
+
// That is the stated outage window (2026-07 into 2026-08) and about the stated
|
|
47
|
+
// ship count ("at least 16 ships"), found without being told where to look.
|
|
48
|
+
//
|
|
49
|
+
// The window width is measured too, not chosen: at ±12h the same sweep fires on
|
|
50
|
+
// 27 days across THREE builders — long sessions that upload once at the end drift
|
|
51
|
+
// past a narrow window and read as outages. ±24h keeps them in.
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
//
|
|
54
|
+
// KEYED ON BUILDER + TIME, NEVER ON task_ids. `session_records.task_ids` is EMPTY
|
|
55
|
+
// on 54% of rows (1,771 of 3,873 when task 1002448 measured it in 2026-09), so
|
|
56
|
+
// "no session record NAMES this task" is not a usable predicate — it reports the
|
|
57
|
+
// linkage gap as an outage. This file never sees task_ids at all, and the SQL in
|
|
58
|
+
// db.js does not mention it; a test asserts that about the SQL actually emitted.
|
|
59
|
+
//
|
|
60
|
+
// PURE so the firing condition is provable without a database, and so the route
|
|
61
|
+
// (on demand) and the poller (periodically) cannot answer the question two
|
|
62
|
+
// different ways.
|
|
63
|
+
|
|
64
|
+
// ---- the knobs, every one of them measured above ---------------------------
|
|
65
|
+
|
|
66
|
+
// The window around a ship in which its builder must have uploaded SOMETHING.
|
|
67
|
+
// The digest uploads at ship time (uploadSessionOnShip, inside finishShip), so a
|
|
68
|
+
// healthy ship's nearest upload is minutes away — p50 is 0.21h across the whole
|
|
69
|
+
// corpus. The width is for what is awkward but not an outage: a long session that
|
|
70
|
+
// uploads once after several ships, a land that polled for hours, a clock that
|
|
71
|
+
// disagrees. ±12h costs three false builders; ±24h costs nothing measurable.
|
|
72
|
+
const DEFAULT_WINDOW_HOURS = 24;
|
|
73
|
+
|
|
74
|
+
// How far back the check looks. Long enough that one quiet week cannot empty it,
|
|
75
|
+
// short enough that a resolved outage stops firing instead of haunting the number
|
|
76
|
+
// for a quarter. 14 days is what the sweep above was tuned on.
|
|
77
|
+
const DEFAULT_LOOKBACK_DAYS = 14;
|
|
78
|
+
|
|
79
|
+
// Below this many ships IN THE LOOKBACK, a builder is not judged at all. Three
|
|
80
|
+
// uncovered out of four is 75% and means nothing; the same share over twenty
|
|
81
|
+
// ships is the outage. A check that fires on someone's quiet fortnight teaches
|
|
82
|
+
// people to mute it, which is the state this task exists to fix.
|
|
83
|
+
const DEFAULT_MIN_SHIPS = 8;
|
|
84
|
+
|
|
85
|
+
// The uncovered share that counts as broken, per builder. The real outage ran at
|
|
86
|
+
// 86-100%; every healthy builder-fortnight in the same sweep stayed under this.
|
|
87
|
+
// Set here rather than at 50% so a PARTIAL outage — some ships uploading, some
|
|
88
|
+
// not — still fires, which is the shape a flaky path bug actually has.
|
|
89
|
+
const DEFAULT_MAX_UNCOVERED_SHARE = 0.25;
|
|
90
|
+
|
|
91
|
+
// ---- the clamps ------------------------------------------------------------
|
|
92
|
+
//
|
|
93
|
+
// Both reach an SQL interval, and both are reachable from a URL. They are clamped
|
|
94
|
+
// HERE rather than at the route because the route is not the only caller — the
|
|
95
|
+
// poller is the other — and a bound that lives at one of two call sites is not a
|
|
96
|
+
// bound.
|
|
97
|
+
function clampLookbackDays(n) {
|
|
98
|
+
const x = Number(n);
|
|
99
|
+
if (!Number.isFinite(x)) return DEFAULT_LOOKBACK_DAYS;
|
|
100
|
+
return Math.max(1, Math.min(365, Math.trunc(x)));
|
|
101
|
+
}
|
|
102
|
+
function clampWindowHours(n) {
|
|
103
|
+
const x = Number(n);
|
|
104
|
+
if (!Number.isFinite(x)) return DEFAULT_WINDOW_HOURS;
|
|
105
|
+
return Math.max(1, Math.min(720, Math.trunc(x)));
|
|
106
|
+
}
|
|
107
|
+
// The offender sample is evidence, not a report: enough rows to recognise the
|
|
108
|
+
// shape (one machine? one week? everyone?), never the whole corpus.
|
|
109
|
+
function clampSampleLimit(n) {
|
|
110
|
+
if (n == null) return 20;
|
|
111
|
+
const x = Number(n);
|
|
112
|
+
if (!Number.isFinite(x)) return 20;
|
|
113
|
+
return Math.max(1, Math.min(100, Math.trunc(x)));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- the verdict -----------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
function toCount(v) {
|
|
119
|
+
const n = Number(v);
|
|
120
|
+
return Number.isFinite(n) && n > 0 ? Math.trunc(n) : 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// normalizeBuilderRows — take what the SQL returns (per-builder counts, with
|
|
124
|
+
// Postgres' bigint-as-string) and hand back clean numbers. A row without a
|
|
125
|
+
// builder is dropped rather than trusted, and an uncovered count above the ship
|
|
126
|
+
// count is a query bug, not an outage several times over: clamp, don't propagate.
|
|
127
|
+
function normalizeBuilderRows(rows) {
|
|
128
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
129
|
+
return list
|
|
130
|
+
.filter((r) => r && r.builder_id != null)
|
|
131
|
+
.map((r) => {
|
|
132
|
+
const ships = toCount(r.ships);
|
|
133
|
+
const uncovered = Math.min(ships, toCount(r.uncovered));
|
|
134
|
+
return {
|
|
135
|
+
builder_id: String(r.builder_id),
|
|
136
|
+
github_login: r.github_login == null ? null : String(r.github_login),
|
|
137
|
+
ships,
|
|
138
|
+
uncovered,
|
|
139
|
+
uncovered_share: ships > 0 ? Number((uncovered / ships).toFixed(4)) : 0,
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// uploadHealthVerdict — is any builder's session upload silently failing?
|
|
145
|
+
//
|
|
146
|
+
// The verdict always carries its own arithmetic and names the builder, because
|
|
147
|
+
// the first question a firing check has to answer is "on what evidence", and the
|
|
148
|
+
// answer to "whose machine" is what makes it actionable. An alert that cannot
|
|
149
|
+
// answer either is one more thing to mute.
|
|
150
|
+
function uploadHealthVerdict(rows, opts = {}) {
|
|
151
|
+
const minShips = Number.isFinite(opts.minShips) ? opts.minShips : DEFAULT_MIN_SHIPS;
|
|
152
|
+
const maxShare = Number.isFinite(opts.maxUncoveredShare) ? opts.maxUncoveredShare : DEFAULT_MAX_UNCOVERED_SHARE;
|
|
153
|
+
const builders = normalizeBuilderRows(rows).sort((a, b) => b.uncovered_share - a.uncovered_share);
|
|
154
|
+
const judged = builders.filter((b) => b.ships >= minShips);
|
|
155
|
+
const offenders = judged.filter((b) => b.uncovered_share > maxShare);
|
|
156
|
+
const ships = builders.reduce((a, b) => a + b.ships, 0);
|
|
157
|
+
const uncovered = builders.reduce((a, b) => a + b.uncovered, 0);
|
|
158
|
+
const base = { ships, uncovered, builders, judged: judged.length, offenders, minShips, maxUncoveredShare: maxShare };
|
|
159
|
+
|
|
160
|
+
if (judged.length === 0) {
|
|
161
|
+
return {
|
|
162
|
+
...base,
|
|
163
|
+
firing: false,
|
|
164
|
+
status: 'insufficient-data',
|
|
165
|
+
reason: `no builder shipped ${minShips}+ tasks in the window (${builders.length} builder(s), ${ships} ship(s) total) — not enough to judge`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (offenders.length > 0) {
|
|
169
|
+
const worst = offenders[0];
|
|
170
|
+
const who = worst.github_login || `builder #${worst.builder_id}`;
|
|
171
|
+
const rest = offenders.length > 1 ? ` (and ${offenders.length - 1} other builder(s) over the line)` : '';
|
|
172
|
+
return {
|
|
173
|
+
...base,
|
|
174
|
+
firing: true,
|
|
175
|
+
status: 'uploads-missing',
|
|
176
|
+
reason: `${who} has ${worst.uncovered} of ${worst.ships} ships (${(worst.uncovered_share * 100).toFixed(1)}%) with no session record in the window${rest}. Their session uploads are probably failing, which means the cost-plus reward is not booking for work that shipped.`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
...base,
|
|
181
|
+
firing: false,
|
|
182
|
+
status: 'healthy',
|
|
183
|
+
reason: `${judged.length} builder(s) judged, worst ${(judged[0].uncovered_share * 100).toFixed(1)}% uncovered — under the ${(maxShare * 100).toFixed(0)}% threshold`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
DEFAULT_WINDOW_HOURS,
|
|
189
|
+
DEFAULT_LOOKBACK_DAYS,
|
|
190
|
+
DEFAULT_MIN_SHIPS,
|
|
191
|
+
DEFAULT_MAX_UNCOVERED_SHARE,
|
|
192
|
+
clampLookbackDays,
|
|
193
|
+
clampWindowHours,
|
|
194
|
+
clampSampleLimit,
|
|
195
|
+
normalizeBuilderRows,
|
|
196
|
+
uploadHealthVerdict,
|
|
197
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"key": "status-ui",
|
|
3
3
|
"title": "Status dashboard (\"The Watch\")",
|
|
4
|
-
"description": "The public build-status dashboard served at status.<apex> (status.example.com) — the live up/down banner, version-progress rollups, cost summary, recent ships, leaderboard, and the ecosystem fingerprint, all polling the read-only /api/
|
|
4
|
+
"description": "The public build-status dashboard served at status.<apex> (status.example.com) — the live up/down banner, version-progress rollups, cost summary, recent ships, leaderboard, and the ecosystem fingerprint, all polling the read-only /api/bongos/public/* surface every 60s. A WEB-SURFACE core-domain module carved out of the GDS core in BV1.R84 (ADR 0093 §1; 02-module-map §6 — a web surface is first-class). default-on: a Cloud Bongos instance ships its status page like it ships its hall. Registers no port and contributes no routes — it declares a webSurface (host prefix + static dir) the loader hands serve-internal, which serves the module's public/ dir on the matching host exactly as it served public-status/ before. The public read endpoints it renders stay SHARED core in routes/public.js (the hall + the off-box mirror read them too).",
|
|
5
5
|
"coreVersion": "^1.11.0",
|
|
6
6
|
"default": true,
|
|
7
7
|
"maintenance": { "status": "core-maintained" },
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
return el;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
fetch('/api/
|
|
116
|
+
fetch('/api/bongos/projects/featured', { headers: { Accept: 'application/json' } })
|
|
117
117
|
.then(function (r) { if (!r.ok) throw new Error('http ' + r.status); return r.json(); })
|
|
118
118
|
.then(function (data) {
|
|
119
119
|
var projects = (data && Array.isArray(data.projects)) ? data.projects : [];
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.805",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.805",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.805",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -610,7 +610,7 @@ module.exports = {
|
|
|
610
610
|
|
|
611
611
|
function usage() {
|
|
612
612
|
console.error('Usage: node scripts/gds/agent-invoke.js <agent> "<question>" [--out <path>] [--json]');
|
|
613
|
-
console.error(' <agent> the agent name as it appears in the registry (GET /api/
|
|
613
|
+
console.error(' <agent> the agent name as it appears in the registry (GET /api/bongos/agents)');
|
|
614
614
|
console.error(' --out <path> write the answer to a file and record it as the run\'s output_ref');
|
|
615
615
|
console.error(' --json print the whole result as JSON instead of prose');
|
|
616
616
|
}
|
|
@@ -624,7 +624,7 @@ async function main() {
|
|
|
624
624
|
// Rule 5. requireSession exits with its own guidance when there is no session.
|
|
625
625
|
const cli = require('./cli-lib');
|
|
626
626
|
await cli.requireSession();
|
|
627
|
-
const me = await cli.apiCall('GET', '/api/
|
|
627
|
+
const me = await cli.apiCall('GET', '/api/bongos/me');
|
|
628
628
|
const builder = (me && me.data && me.data.builder) || null;
|
|
629
629
|
if (!builder || !builder.id) {
|
|
630
630
|
console.error('agent-invoke: could not resolve who you are from /me — a fire has to be attributable.');
|
|
@@ -659,7 +659,7 @@ async function main() {
|
|
|
659
659
|
console.error(`\n ${name}: ${result.code}`);
|
|
660
660
|
if (result.reason) console.error(` ${result.reason}`);
|
|
661
661
|
if (result.code === 'agent_not_found') {
|
|
662
|
-
console.error(' Nothing by that name is in the registry. `node scripts/gds/api.js GET /api/
|
|
662
|
+
console.error(' Nothing by that name is in the registry. `node scripts/gds/api.js GET /api/bongos/agents` lists it.');
|
|
663
663
|
}
|
|
664
664
|
if (result.code === 'agent_refused' && /not enabled/.test(result.reason || '')) {
|
|
665
665
|
console.error(' Definitions land in the registry DISABLED by design: agents-sync reconciles');
|
package/scripts/gds/api.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
// session token + API base, so there is no manual token extraction here.
|
|
9
9
|
//
|
|
10
10
|
// Usage:
|
|
11
|
-
// node scripts/gds/api.js GET /api/
|
|
12
|
-
// node scripts/gds/api.js POST /api/
|
|
13
|
-
// node scripts/gds/api.js POST /api/
|
|
14
|
-
// echo '{"verdict":"promote"}' | node scripts/gds/api.js PATCH /api/
|
|
11
|
+
// node scripts/gds/api.js GET /api/bongos/inbox
|
|
12
|
+
// node scripts/gds/api.js POST /api/bongos/blockers/5/resolve --body '{"resolution_note":"fixed in #690"}'
|
|
13
|
+
// node scripts/gds/api.js POST /api/bongos/security/reports --body-file report.json
|
|
14
|
+
// echo '{"verdict":"promote"}' | node scripts/gds/api.js PATCH /api/bongos/inbox/12 --body-file -
|
|
15
15
|
//
|
|
16
16
|
// The HTTP status goes to STDERR ("POST /path → 201"); the pretty-printed JSON
|
|
17
17
|
// body goes to STDOUT, so `... | node -e ...` style piping stays clean. Exit 0
|
package/scripts/gds/api.sh
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# scripts/gds/api.sh — Git Bash/Linux passthrough wrapper around api.js.
|
|
3
|
-
# MSYS_NO_PATHCONV avoids Git Bash mangling API paths (e.g. /api/
|
|
3
|
+
# MSYS_NO_PATHCONV avoids Git Bash mangling API paths (e.g. /api/bongos/inbox) into
|
|
4
4
|
# Windows paths before node ever sees them.
|
|
5
5
|
export MSYS_NO_PATHCONV=1
|
|
6
6
|
exec node "$(dirname "$0")/api.js" "$@"
|