@bongos/core 1.18.19 → 1.19.3
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 +547 -197
- package/.claude/skills/builder-box/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +5 -5
- package/.claude/skills/scan-before-install/SKILL.md +165 -0
- package/bin/bongos.js +33 -7
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +63 -41
- package/clients/bongos-client/index.cjs +63 -41
- package/clients/bongos-client/index.d.ts +78 -44
- package/clients/bongos-client/index.mjs +63 -41
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
- package/docs/adr/0156-user-set-display-name-wins.md +62 -0
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
- package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
- package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
- package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
- package/docs/adr/0161-publish-on-merge.md +34 -0
- package/docs/adr/README.md +9 -0
- package/docs/api/openapi.json +1209 -233
- package/docs/api-reference.md +61 -46
- package/docs/architecture.md +1 -1
- package/docs/canonical-permissions.md +61 -16
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +4 -1
- package/docs/onboarding/diagrams/assertions.json +3 -3
- package/docs/recipes/core-release-pipeline.md +181 -0
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/parallel-goal-waves.md +1 -3
- package/docs/recipes/private-npm-distribution.md +6 -2
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/migrations/core_197_display_name_source.sql +56 -0
- package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
- package/migrations/core_205_versions_track_default.sql +28 -0
- package/modules/autonomy/routes/autonomy.js +1 -1
- package/modules/builder-settings/db.js +7 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
- package/modules/dev-box/routes/box.js +11 -9
- package/modules/discord/auth.js +3 -1
- package/modules/discord/routes/discord.js +8 -2
- package/modules/discord/ship-broadcast.js +48 -1
- package/modules/governance/catalog.js +70 -20
- package/modules/governance/db.js +102 -0
- package/modules/governance/migrations/governance_006_version_create.sql +38 -0
- package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
- package/modules/governance/routes/governance.js +235 -6
- package/modules/grading/grader-workers/index.js +18 -9
- package/modules/hall-ui/public/gate.js +6 -2
- package/modules/hall-ui/public/goals-page.js +6 -2
- package/modules/hall-ui/public/governance.css +100 -0
- package/modules/hall-ui/public/governance.html +77 -0
- package/modules/hall-ui/public/governance.js +353 -0
- package/modules/hall-ui/public/harbor.js +14 -8
- package/modules/hall-ui/public/instances-panel.js +3 -1
- package/modules/hall-ui/public/new-project-wizard.js +6 -0
- package/modules/hall-ui/public/people.html +51 -0
- package/modules/hall-ui/public/people.js +100 -0
- package/modules/hall-ui/public/profile.html +2 -1
- package/modules/hall-ui/public/profile.js +9 -3
- package/modules/hall-ui/public/sessions.js +4 -2
- package/modules/hall-ui/public/shell.js +28 -7
- package/modules/hall-ui/public/task-detail.js +6 -3
- package/modules/hall-ui/public/watch.js +17 -9
- package/modules/ideas/blockers.js +44 -7
- package/modules/ideas/routes/blockers.js +12 -7
- package/modules/ideas/routes/inbox.js +2 -2
- package/modules/lifecycle/db.js +84 -17
- package/modules/lifecycle/github-push.js +37 -10
- package/modules/lifecycle/publish-reconciler.js +128 -31
- package/modules/lifecycle/routes/claims.js +10 -4
- package/modules/lifecycle/routes/dependencies.js +2 -2
- package/modules/lifecycle/routes/done-when.js +4 -4
- package/modules/lifecycle/routes/gate-approvals.js +9 -3
- package/modules/lifecycle/routes/goals.js +5 -5
- package/modules/lifecycle/routes/tasks.js +99 -69
- package/modules/lifecycle/routes/versions.js +13 -2
- package/modules/lifecycle/ship-card.js +38 -7
- package/modules/memory/routes/memory.js +2 -2
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/routes/projects.js +10 -6
- package/modules/platform-identity/routes/scouting.js +2 -2
- package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
- package/modules/provisioning/provisioning.js +24 -6
- package/modules/provisioning/routes/provisioning.js +24 -6
- package/modules/security/artifact-scan/aggregate.js +133 -0
- package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
- package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
- package/modules/security/artifact-scan/detectors/credentials.js +112 -0
- package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
- package/modules/security/artifact-scan/detectors/hooks.js +116 -0
- package/modules/security/artifact-scan/detectors/injection.js +118 -0
- package/modules/security/artifact-scan/detectors/mcp.js +102 -0
- package/modules/security/artifact-scan/detectors/network.js +67 -0
- package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
- package/modules/security/artifact-scan/detectors/permissions.js +91 -0
- package/modules/security/artifact-scan/detectors/shared.js +219 -0
- package/modules/security/artifact-scan/fence-local.js +99 -0
- package/modules/security/artifact-scan/fetch.js +330 -0
- package/modules/security/artifact-scan/floor.js +319 -0
- package/modules/security/artifact-scan/limits.js +66 -0
- package/modules/security/artifact-scan/manifest.js +203 -0
- package/modules/security/artifact-scan/mirror.js +158 -0
- package/modules/security/artifact-scan/quarantine.js +149 -0
- package/modules/security/artifact-scan/rebuild-spec.js +120 -0
- package/modules/security/artifact-scan/report.js +155 -0
- package/modules/security/artifact-scan/sanitize.js +317 -0
- package/modules/security/migrations/security_001_tool_scans.sql +145 -0
- package/modules/security/module.json +2 -2
- package/modules/security/routes/reports.js +11 -7
- package/modules/security/routes/scans.js +115 -0
- package/modules/security/scan-store.js +214 -0
- package/modules/security/tests/aggregate.mjs +283 -0
- package/modules/security/tests/artifact_scan.mjs +591 -0
- package/modules/security/tests/floor.mjs +549 -0
- package/modules/security/tests/mirror.mjs +350 -0
- package/modules/security/tests/sanitize_rebuild.mjs +499 -0
- package/modules/security/tests/scan_store.mjs +250 -0
- package/modules/security/tests/tool_scans_migration.mjs +116 -0
- package/modules/sessions/routes/sessions.js +6 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +13 -3
- package/scripts/gds/abandon.js +1 -1
- package/scripts/gds/audit-authorship.sh +6 -0
- package/scripts/gds/box.js +1 -1
- package/scripts/gds/bump-version.js +76 -0
- package/scripts/gds/claim.js +15 -0
- package/scripts/gds/cli-lib.js +16 -4
- package/scripts/gds/gating.js +28 -13
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/goal.js +234 -0
- package/scripts/gds/init.js +77 -15
- package/scripts/gds/main-audit.js +80 -11
- package/scripts/gds/mirror-redact.js +6 -0
- package/scripts/gds/newcomer-floor.js +2 -2
- package/scripts/gds/provision.js +230 -94
- package/scripts/gds/run-unit-tests.js +9 -1
- package/scripts/gds/sandbox-stage.js +66 -9
- package/scripts/gds/search-ingest-md.js +1 -0
- package/scripts/gds/sequence.js +67 -8
- package/scripts/gds/ship.js +183 -50
- package/scripts/hall-preview/server.js +1 -1
- package/scripts/migrate.sh +5 -5
- package/src/bongos/auth.js +129 -27
- package/src/bongos/db-kernel.js +66 -5
- package/src/bongos/db.js +6 -7
- package/src/bongos/github-repos.js +19 -7
- package/src/bongos/goal-scope-check.js +1 -1
- package/src/bongos/module-scope-map.js +3 -1
- package/src/bongos/permission-path-check.js +1 -1
- package/src/bongos/route-rank-check.js +73 -30
- package/src/bongos/routes/audit-log.js +4 -2
- package/src/bongos/routes/auth.js +8 -4
- package/src/bongos/routes/backup.js +7 -4
- package/src/bongos/routes/builders.js +23 -11
- package/src/bongos/routes/github.js +15 -4
- package/src/bongos/routes/llm-cache.js +5 -2
- package/src/bongos/routes/modules.js +9 -5
- package/src/bongos/routes/security.js +21 -12
- package/src/bongos/serve-internal.js +22 -7
- package/src/module-api.js +96 -13
- package/src/platform-server.js +14 -0
- package/tests/api_client.mjs +18 -2
- package/tests/api_path_404.mjs +3 -0
- package/tests/art_stage.mjs +2 -1
- package/tests/atlas.mjs +32 -4
- package/tests/auth_admission_gate.mjs +17 -5
- package/tests/auth_error_brand.mjs +41 -21
- package/tests/auth_resolve_failure.mjs +155 -0
- package/tests/blocker_link.mjs +88 -0
- package/tests/bongos_cli.mjs +91 -0
- package/tests/bongos_downloads.mjs +2 -1
- package/tests/box_dns_repoint.mjs +2 -0
- package/tests/box_ship_permissions.mjs +2 -0
- package/tests/box_terminal_ssrf.mjs +23 -17
- package/tests/bump_version.mjs +71 -0
- package/tests/claim_error_surface.mjs +23 -0
- package/tests/claude_materialize.mjs +4 -0
- package/tests/cli_box_scope.mjs +40 -0
- package/tests/cloud_bongos_pack.mjs +34 -4
- package/tests/conductor.mjs +2 -1
- package/tests/criterion_authoring_gate.mjs +28 -5
- package/tests/currency_label.mjs +10 -1
- package/tests/deploy_instance.mjs +2 -0
- package/tests/devbox_downloads.mjs +2 -1
- package/tests/display_name.mjs +95 -6
- package/tests/eviction_policy.mjs +2 -1
- package/tests/file_map.mjs +10 -0
- package/tests/first_admin_bootstrap.mjs +10 -2
- package/tests/first_run_engine_check.mjs +2 -1
- package/tests/fitness.mjs +26 -6
- package/tests/gate_approvals.mjs +15 -2
- package/tests/github_push_land_proof.mjs +50 -4
- package/tests/github_repos_helpers.mjs +30 -5
- package/tests/github_token_store.mjs +23 -0
- package/tests/goal_abuse_matrix.mjs +13 -3
- package/tests/goal_cli.mjs +89 -0
- package/tests/governance_abuse_matrix.mjs +386 -0
- package/tests/governance_fold.mjs +156 -0
- package/tests/governance_fold_bench.mjs +50 -0
- package/tests/governance_live_proof.mjs +201 -0
- package/tests/governance_page_gates.mjs +21 -9
- package/tests/governance_parity.mjs +11 -2
- package/tests/governance_require_permission.mjs +125 -1
- package/tests/governance_routes.mjs +334 -0
- package/tests/grader.mjs +7 -3
- package/tests/hall_nav.mjs +14 -4
- package/tests/helpers.mjs +73 -0
- package/tests/hook_core_resolve.mjs +2 -1
- package/tests/host_topology_skips.mjs +97 -0
- package/tests/infra_exec_paths.mjs +14 -4
- package/tests/init.mjs +147 -0
- package/tests/known_failing_ratchet.mjs +89 -0
- package/tests/lifecycle-card.mjs +2 -1
- package/tests/main_audit.mjs +72 -1
- package/tests/memory_isolation.mjs +56 -9
- package/tests/migrate_root_resolution.mjs +1 -1
- package/tests/mirror_redact.mjs +19 -0
- package/tests/module_api.mjs +3 -1
- package/tests/module_loader.mjs +4 -0
- package/tests/modules.mjs +35 -7
- package/tests/modules_route.mjs +29 -5
- package/tests/onboarding_observability.mjs +49 -11
- package/tests/onboarding_route_signals.mjs +1 -0
- package/tests/one_active_claim_per_session.mjs +80 -0
- package/tests/patch_gate_ordering.mjs +102 -0
- package/tests/pre_compact.mjs +2 -1
- package/tests/preview_server.mjs +2 -1
- package/tests/profile_route.mjs +1 -0
- package/tests/provision.mjs +170 -0
- package/tests/provisioning_onboard_mode.mjs +180 -0
- package/tests/publish_manifest.mjs +16 -3
- package/tests/publish_reconciler.mjs +181 -10
- package/tests/rank_tier_single_source.mjs +205 -0
- package/tests/recall_hints.mjs +2 -1
- package/tests/repo_map.mjs +4 -0
- package/tests/route_permission_check.mjs +14 -5
- package/tests/rubric_fixtures.mjs +3 -0
- package/tests/run_routine_two_root.mjs +2 -0
- package/tests/sandbox_stage.mjs +69 -7
- package/tests/search_ingest.mjs +7 -0
- package/tests/security_docs.mjs +28 -6
- package/tests/sequence.mjs +64 -0
- package/tests/session-cards.mjs +2 -1
- package/tests/session_start_freshness.mjs +2 -1
- package/tests/ship_broadcast_server.mjs +63 -1
- package/tests/ship_card.mjs +66 -0
- package/tests/ship_ci_deploy.mjs +66 -1
- package/tests/ship_smoke_gate.mjs +76 -0
- package/tests/versions_route.mjs +15 -0
- package/tests/workflow_exec_paths.mjs +16 -4
- package/tests/worktree_guard.mjs +2 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// people.js — the People page: the /scouting cross-project builder directory
|
|
2
|
+
// graduated into the hall (task 1002577; ADR 0141 §4 is the data's design of
|
|
3
|
+
// record). Same data endpoint as scouting.js — /api/gds/scouting, Archon-gated
|
|
4
|
+
// server-side ('project.curate'); the page itself is gated in serve-internal.js
|
|
5
|
+
// on that exact permission, so a visitor without it never sees this shell. The
|
|
6
|
+
// hall chrome + kit primitives are what "graduate" means: searchable, paged,
|
|
7
|
+
// shell-navigable — spec R14 (goal 1000045) later widens WHO sees it, not this.
|
|
8
|
+
// All network values render through OTBKit.esc — never raw innerHTML.
|
|
9
|
+
(function () {
|
|
10
|
+
'use strict';
|
|
11
|
+
var kit = window.OTBKit;
|
|
12
|
+
var esc = kit.esc;
|
|
13
|
+
var body = document.getElementById('people-body');
|
|
14
|
+
|
|
15
|
+
function fmtRecency(iso) {
|
|
16
|
+
if (!iso) return '—';
|
|
17
|
+
var t = Date.parse(iso);
|
|
18
|
+
if (isNaN(t)) return '—';
|
|
19
|
+
var days = Math.floor((Date.now() - t) / 86400000);
|
|
20
|
+
if (days <= 0) return 'today';
|
|
21
|
+
if (days === 1) return '1 day ago';
|
|
22
|
+
if (days < 30) return days + ' days ago';
|
|
23
|
+
return Math.floor(days / 30) + ' mo ago';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function builderName(b) {
|
|
27
|
+
return b.github_login || b.display_name || ('#' + b.github_id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var all = [];
|
|
31
|
+
var query = '';
|
|
32
|
+
|
|
33
|
+
function matches(b) {
|
|
34
|
+
if (!query) return true;
|
|
35
|
+
var hay = (builderName(b) + ' ' + (b.display_name || '') + ' ' +
|
|
36
|
+
(Array.isArray(b.disciplines) ? b.disciplines.join(' ') : '')).toLowerCase();
|
|
37
|
+
return hay.indexOf(query) !== -1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
var pager = kit.makePager('people', 'hall-people-per-page');
|
|
41
|
+
var ledger = null;
|
|
42
|
+
|
|
43
|
+
function rowHtml(b) {
|
|
44
|
+
return '<tr>' +
|
|
45
|
+
'<td><span class="who">' + esc(builderName(b)) + '</span></td>' +
|
|
46
|
+
'<td class="num">' + esc(b.project_count != null ? b.project_count : '—') + '</td>' +
|
|
47
|
+
'<td class="num">' + esc(b.total_credits != null ? b.total_credits : 0) + '</td>' +
|
|
48
|
+
'<td class="num">' + esc(b.total_tasks_shipped != null ? b.total_tasks_shipped : 0) + '</td>' +
|
|
49
|
+
'<td class="num">' + esc(b.total_karma != null ? b.total_karma : 0) + '</td>' +
|
|
50
|
+
'<td>' + esc(Array.isArray(b.disciplines) ? b.disciplines.join(', ') : '—') + '</td>' +
|
|
51
|
+
'<td>' + esc(fmtRecency(b.last_active)) + '</td>' +
|
|
52
|
+
'</tr>';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function rerender() {
|
|
56
|
+
if (ledger) ledger.render(all.filter(matches));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mountDirectory(builders) {
|
|
60
|
+
all = Array.isArray(builders) ? builders : [];
|
|
61
|
+
kit.makeFilterBar({
|
|
62
|
+
mount: document.getElementById('people-filters'),
|
|
63
|
+
search: { id: 'people-q', label: 'Search', placeholder: 'name or discipline' },
|
|
64
|
+
onChange: function (vals) {
|
|
65
|
+
query = String(vals['people-q'] || '').trim().toLowerCase();
|
|
66
|
+
pager.page = 1;
|
|
67
|
+
rerender();
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
body.textContent = '';
|
|
71
|
+
ledger = kit.makeLedger({
|
|
72
|
+
mount: body,
|
|
73
|
+
columns: ['Builder', { label: 'Projects', class: 'num' }, { label: 'Credits', class: 'num' },
|
|
74
|
+
{ label: 'Ships', class: 'num' }, { label: 'Karma', class: 'num' }, 'Disciplines', 'Last active'],
|
|
75
|
+
rowHtml: rowHtml,
|
|
76
|
+
emptyMsg: 'No cross-project activity reported yet.',
|
|
77
|
+
pager: pager,
|
|
78
|
+
});
|
|
79
|
+
rerender();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function notice(msg) {
|
|
83
|
+
body.innerHTML = '';
|
|
84
|
+
body.className = 'note';
|
|
85
|
+
body.textContent = msg;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fetch('/api/gds/scouting?limit=500', { headers: { Accept: 'application/json' } })
|
|
89
|
+
.then(function (r) {
|
|
90
|
+
if (r.status === 403 || r.status === 401) { throw { gated: true }; }
|
|
91
|
+
if (!r.ok) throw new Error('http ' + r.status);
|
|
92
|
+
return r.json();
|
|
93
|
+
})
|
|
94
|
+
.then(function (data) { mountDirectory(data && data.builders); })
|
|
95
|
+
.catch(function (e) {
|
|
96
|
+
notice(e && e.gated
|
|
97
|
+
? 'The builder directory is Archon-only for now.'
|
|
98
|
+
: 'Couldn’t load the directory right now.');
|
|
99
|
+
});
|
|
100
|
+
}());
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
<script src="/builders/palette.js" defer></script>
|
|
83
83
|
<!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
|
|
84
84
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
85
|
-
<script src="/builders/
|
|
85
|
+
<script src="/builders/brand-apply.js?v=2026-06-27-r75"></script>
|
|
86
|
+
<script src="/builders/profile.js?v=2026-08-04-reward1"></script>
|
|
86
87
|
</body>
|
|
87
88
|
</html>
|
|
@@ -131,6 +131,14 @@
|
|
|
131
131
|
const tbody = $('#works-table tbody');
|
|
132
132
|
const lede = $('#works-lede');
|
|
133
133
|
if (!tbody) return;
|
|
134
|
+
// Every row here is a shipped task, so one header serves them all — but on a
|
|
135
|
+
// cost-plus-only instance the figure is still an unpaid estimate, so the
|
|
136
|
+
// label must keep its "Est." qualifier (ADR 0146).
|
|
137
|
+
const rewardTh = $('#works-table thead .ledger__drachmae');
|
|
138
|
+
if (rewardTh) {
|
|
139
|
+
rewardTh.textContent = window.OTBBranding?.taskCreditLabel?.({ status: 'shipped' }, { caps: true })
|
|
140
|
+
?? (curLabel(true) + ' Est.');
|
|
141
|
+
}
|
|
134
142
|
const list = Array.isArray(ships) ? ships : [];
|
|
135
143
|
if (!list.length) {
|
|
136
144
|
tbody.innerHTML = `<tr><td colspan="4" class="ledger__empty">Nothing shipped yet.</td></tr>`;
|
|
@@ -138,9 +146,7 @@
|
|
|
138
146
|
return;
|
|
139
147
|
}
|
|
140
148
|
tbody.innerHTML = list.map((s) => {
|
|
141
|
-
|
|
142
|
-
// to the creation-time estimate only if the awarded figure is absent.
|
|
143
|
-
const reward = s.credits_awarded != null ? s.credits_awarded : s.credits_reward;
|
|
149
|
+
const reward = window.OTBBranding?.taskCreditValue?.(s) ?? s.credits_reward;
|
|
144
150
|
const summary = s.value_summary
|
|
145
151
|
? `<span style="display:block;margin-top:2px;font-size:12px;color:var(--ink-faint)">${escapeHtml(s.value_summary)}</span>`
|
|
146
152
|
: '';
|
|
@@ -208,8 +208,10 @@
|
|
|
208
208
|
return;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
// ADR 0157: `session.search` moved archon→metic, so the session log is a
|
|
212
|
+
// Metic+ surface now. Cosmetic — the route gate is the wall.
|
|
213
|
+
if (!builder || (builder.rank !== 'metic' && builder.rank !== 'archon')) {
|
|
214
|
+
showSealed('The session log is available from the Metic rank up. Your current rank does not have access.');
|
|
213
215
|
return;
|
|
214
216
|
}
|
|
215
217
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// top bar (drawer toggle, page title, greeting slot) into the static mounts
|
|
13
13
|
// (#app-sidebar / #app-topbar / #app-foot) every hall page carries;
|
|
14
14
|
// - rank-gate nav items COSMETICALLY (server-side page gates remain the real
|
|
15
|
-
// access control — serve-internal.js
|
|
15
|
+
// access control — serve-internal.js requireGovernancePage etc.);
|
|
16
16
|
// - on the index (body[data-page="home"]) route the three views (#home,
|
|
17
17
|
// #standing, #leaderboard) plus the #achievements alias into Standing. The
|
|
18
18
|
// `#/task|idea|blocker/N` deep-link hashes belong to builders.js and are
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
goals: '<circle cx="9" cy="9" r="6.5"/><circle cx="9" cy="9" r="3.5"/><circle cx="9" cy="9" r="0.8" fill="currentColor"/>',
|
|
79
79
|
roadmap: '<path d="M3 15.5V3.5l4 2 4-2 4 2v10l-4-2-4 2-4-2Z"/><path d="M7 3.5v10M11 5.5v10"/>',
|
|
80
80
|
leaderboard: '<path d="M3.5 15.5v-6M9 15.5V3.5m5.5 12v-9"/>',
|
|
81
|
+
people: '<circle cx="6.5" cy="6.5" r="2.5"/><path d="M2.5 15c.5-2.4 2-3.7 4-3.7s3.5 1.3 4 3.7"/><circle cx="12.5" cy="7" r="2"/><path d="M12 11.5c1.8.1 3 1.3 3.5 3.3"/>',
|
|
81
82
|
docs: '<path d="M4 3.5h7l3 3v10H4v-13Z"/><path d="M11 3.5v3h3"/>',
|
|
82
83
|
settings: '<circle cx="9" cy="9" r="2.5"/><path d="M9 2.5v2m0 9v2M2.5 9h2m9 0h2M4.4 4.4l1.4 1.4m6.4 6.4 1.4 1.4m0-9.2-1.4 1.4M5.8 12.2l-1.4 1.4"/>',
|
|
83
84
|
admin: '<path d="M9 2.5 15 5v4c0 3.5-2.4 6-6 7-3.6-1-6-3.5-6-7V5l6-2.5Z"/>',
|
|
@@ -96,10 +97,13 @@
|
|
|
96
97
|
// work — signed, but for newcomers only once the "ship three" stage is
|
|
97
98
|
// current (mirrors the old #hall-nav-work rule, task #833)
|
|
98
99
|
// archon — rank === 'archon'
|
|
100
|
+
// governance — rank is metic or archon (ADR 0157): the monitoring/ops pages.
|
|
101
|
+
// Mirrors the server's `page.view.governance` floor, which that ADR
|
|
102
|
+
// lowered archon→metic. Cosmetic only — requireGovernancePage is the wall.
|
|
99
103
|
// `view` marks an index view (hash-routed); `module` gates on window.__MODULES__.
|
|
100
104
|
// task 2175: grouped by AUDIENCE of the data, so the sidebar reads in coherent
|
|
101
105
|
// runs instead of one long list — the project (unlabeled), You, Community,
|
|
102
|
-
// Docs,
|
|
106
|
+
// Docs, Governance. A group's `gate` must match its most permissive item's, or the
|
|
103
107
|
// head renders over nothing.
|
|
104
108
|
var NAV = [
|
|
105
109
|
{ group: '', items: [
|
|
@@ -131,6 +135,12 @@
|
|
|
131
135
|
] },
|
|
132
136
|
{ group: 'Community', gate: 'member', items: [
|
|
133
137
|
{ id: 'leaderboard', label: 'Leaderboard', icon: 'leaderboard', href: '/#leaderboard', view: 'leaderboard', gate: 'member' },
|
|
138
|
+
// task 1002577: the cross-project builder directory (/scouting graduated
|
|
139
|
+
// into the hall). Archon-gated to MATCH its data — /api/gds/scouting is
|
|
140
|
+
// deliberately "NOT shown to builders" (ADR 0141 §4) until spec R14 widens
|
|
141
|
+
// it; this gate is cosmetic, requireProjectCuratePage is the wall. The
|
|
142
|
+
// module gate hides the tab on instances without the platform-identity hub.
|
|
143
|
+
{ id: 'people', label: 'People', icon: 'people', href: '/people', gate: 'archon', module: 'platform-identity' },
|
|
134
144
|
] },
|
|
135
145
|
{ group: 'Docs', gate: 'signed', items: [
|
|
136
146
|
{ id: 'primer', label: 'Primer', icon: 'docs', href: '/primer', gate: 'member' },
|
|
@@ -150,11 +160,18 @@
|
|
|
150
160
|
// subdomain — external so the rewrite shim doesn't turn it into /builders/docs.
|
|
151
161
|
{ id: 'apidocs', label: 'API docs', icon: 'docs', href: '/docs', gate: 'signed', external: true },
|
|
152
162
|
] },
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
{ id: '
|
|
163
|
+
// Relabeled 'Archon' → 'Governance' in ADR 0157: these pages are no longer
|
|
164
|
+
// Archon-only, so a group header reading "Archon" over items a Metic can open
|
|
165
|
+
// would misdescribe the ladder to the builders most likely to be learning it.
|
|
166
|
+
{ group: 'Governance', gate: 'governance', items: [
|
|
167
|
+
{ id: 'watch', label: 'Watch', icon: 'admin', href: '/watch', gate: 'governance' },
|
|
168
|
+
{ id: 'harbor', label: 'Harbor', icon: 'admin', href: '/harbor', gate: 'governance', module: 'dev-box' },
|
|
169
|
+
{ id: 'gate', label: 'Gate', icon: 'admin', href: '/gate', gate: 'governance' },
|
|
170
|
+
{ id: 'sessions', label: 'Sessions', icon: 'admin', href: '/sessions', gate: 'governance' },
|
|
171
|
+
// BV1.R113: the roles-and-permissions tab (ADR 0151). The one genuinely
|
|
172
|
+
// Archon-only item the 'governance' gate comment anticipated — cosmetic
|
|
173
|
+
// only; requireGovernanceManagePage (the governance.manage atom) is the wall.
|
|
174
|
+
{ id: 'governance', label: 'Roles', icon: 'admin', href: '/governance', gate: 'archon' },
|
|
158
175
|
] },
|
|
159
176
|
];
|
|
160
177
|
|
|
@@ -365,6 +382,10 @@
|
|
|
365
382
|
case 'member': return member;
|
|
366
383
|
case 'work': return authed && (member || access.showWork);
|
|
367
384
|
case 'archon': return authed && access.rank === 'archon';
|
|
385
|
+
// ADR 0157: the monitoring/ops pages sit at the `page.view.governance` floor,
|
|
386
|
+
// which is metic. Kept as its own key rather than widening 'archon', so a
|
|
387
|
+
// genuinely Archon-only nav item can still be added later.
|
|
388
|
+
case 'governance': return authed && (access.rank === 'metic' || access.rank === 'archon');
|
|
368
389
|
default: return false;
|
|
369
390
|
}
|
|
370
391
|
}
|
|
@@ -199,9 +199,12 @@
|
|
|
199
199
|
function triageError(err, fallback) {
|
|
200
200
|
if (err && err.message === 'redirecting to auth') return null;
|
|
201
201
|
var code = errCode(err);
|
|
202
|
-
// '
|
|
203
|
-
//
|
|
204
|
-
|
|
202
|
+
// 'permission_forbidden' is what requirePermission emits (PATCH /inbox/:id →
|
|
203
|
+
// idea.triage, POST /tasks → task.create, both migrated in BV1.R105);
|
|
204
|
+
// 'rank_forbidden' is requireRank's older code, still emitted by the in-handler
|
|
205
|
+
// walls on these paths (e.g. PATCH /tasks/:id's security_sensitive check). Both
|
|
206
|
+
// mean the same thing to the reader, and the 403 arm backstops either.
|
|
207
|
+
if (code === 'permission_forbidden' || code === 'rank_forbidden' || (err && err.status === 403)) return 'Only Metic+ can triage ideas.';
|
|
205
208
|
if (code === 'MERGE_SELF') return 'An idea can’t be merged into itself.';
|
|
206
209
|
if (code === 'MERGE_TARGET_NOT_FOUND') return 'No idea with that number.';
|
|
207
210
|
if (code === 'MERGE_TARGET_MERGED') return 'That idea was itself merged — merge into the one it folded into.';
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
// watch.js — Watch (/builders/watch).
|
|
2
2
|
//
|
|
3
|
-
// Archon-only
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// and a tally of recorded sessions.
|
|
3
|
+
// Metic+ monitoring view (Archon-only until ADR 0157 delegated the operational
|
|
4
|
+
// permissions to the working rank). One self-contained page that stitches together
|
|
5
|
+
// the oversight surfaces: pending access requests, security (red-team reports +
|
|
6
|
+
// override queue + audit log), the builder roster, per-builder + project-wide
|
|
7
|
+
// review grades, estimation drift, repo health, and a tally of recorded sessions.
|
|
8
|
+
//
|
|
9
|
+
// Note the page floor and the panel floors are no longer identical: the page and
|
|
10
|
+
// most panels sit at metic, but ACCESS REQUESTS (`access_request.review`) stayed
|
|
11
|
+
// archon — admitting a builder is identity. So a Metic sees this page with that one
|
|
12
|
+
// section sealed, which is the 403-degrades-per-section path below working as designed.
|
|
8
13
|
//
|
|
9
14
|
// This page reads /api/bongos/me to learn the viewer's rank and shows a
|
|
10
|
-
// restricted notice
|
|
11
|
-
// endpoint below is ALSO gated
|
|
15
|
+
// restricted notice below the Metic floor — but that is DEFENCE IN DEPTH only. Every
|
|
16
|
+
// endpoint below is ALSO gated server-side (ADR 0016: the DB
|
|
12
17
|
// enforces, the markdown/UI only describes). On a 401 it redirects into the
|
|
13
18
|
// web sign-in flow; on a 403 the affected section degrades to an inline
|
|
14
19
|
// "unavailable" message rather than blanking the whole page. Each section
|
|
@@ -870,8 +875,11 @@
|
|
|
870
875
|
return;
|
|
871
876
|
}
|
|
872
877
|
|
|
873
|
-
|
|
874
|
-
|
|
878
|
+
// ADR 0157: Watch sits at the `page.view.governance` floor (metic), and every panel
|
|
879
|
+
// it reads (roster, security reports, audit log, sessions, box costs) delegated to
|
|
880
|
+
// metic with it. Cosmetic — requireGovernancePage + the route gates are the wall.
|
|
881
|
+
if (!builder || (builder.rank !== 'metic' && builder.rank !== 'archon')) {
|
|
882
|
+
showSealed('This page is available from the Metic rank up. Your current rank does not grant access.');
|
|
875
883
|
return;
|
|
876
884
|
}
|
|
877
885
|
|
|
@@ -81,14 +81,49 @@ async function resolveBlocker({ id, builderId, resolutionNote }) {
|
|
|
81
81
|
return { blocker: rows[0], promoted_tasks: promoted };
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
//
|
|
84
|
+
// Statuses that linking a blocker moves to 'blocked' (task 1002559).
|
|
85
|
+
//
|
|
86
|
+
// 'ready' only, deliberately. The resolve trigger (migration 009) promotes
|
|
87
|
+
// 'blocked' → 'ready', so anything we flip INTO blocked must be something
|
|
88
|
+
// 'ready' is a correct destination for. 'backlog' is not: it is pre-workable
|
|
89
|
+
// and already unclaimable, and migration 176 keeps a dep-ship from promoting it
|
|
90
|
+
// past an open blocker — so blocking it would buy nothing and would let a
|
|
91
|
+
// blocker resolution launder a task past the metic+archon promote gate.
|
|
92
|
+
const LINK_BLOCKABLE_STATUSES = ['ready'];
|
|
93
|
+
|
|
94
|
+
function blocksOnLink(status) {
|
|
95
|
+
return LINK_BLOCKABLE_STATUSES.includes(status);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Link a blocker to a task (lives in task_blockers via the new blocker_id col)
|
|
99
|
+
// and park the task in 'blocked' so the resolve-side trigger has something to
|
|
100
|
+
// promote. Both writes share one transaction: a link that recorded the edge but
|
|
101
|
+
// left the task claimable is the exact defect this replaces.
|
|
85
102
|
async function linkBlockerToTask({ blockerId, taskId }) {
|
|
86
|
-
await pool.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
const client = await pool.connect();
|
|
104
|
+
try {
|
|
105
|
+
await client.query('BEGIN');
|
|
106
|
+
await client.query(
|
|
107
|
+
`INSERT INTO task_blockers (task_id, blocker_id, blocker_ref)
|
|
108
|
+
VALUES ($1, $2, $3)
|
|
109
|
+
ON CONFLICT DO NOTHING`,
|
|
110
|
+
[taskId, blockerId, `blocker:${blockerId}`]
|
|
111
|
+
);
|
|
112
|
+
const { rows } = await client.query(
|
|
113
|
+
`UPDATE tasks
|
|
114
|
+
SET status = 'blocked', updated_at = now()
|
|
115
|
+
WHERE id = $1 AND status = ANY($2)
|
|
116
|
+
RETURNING id`,
|
|
117
|
+
[taskId, LINK_BLOCKABLE_STATUSES]
|
|
118
|
+
);
|
|
119
|
+
await client.query('COMMIT');
|
|
120
|
+
return { blocked: rows.length > 0 };
|
|
121
|
+
} catch (err) {
|
|
122
|
+
await client.query('ROLLBACK');
|
|
123
|
+
throw err;
|
|
124
|
+
} finally {
|
|
125
|
+
client.release();
|
|
126
|
+
}
|
|
92
127
|
}
|
|
93
128
|
|
|
94
129
|
module.exports = {
|
|
@@ -97,4 +132,6 @@ module.exports = {
|
|
|
97
132
|
createBlocker,
|
|
98
133
|
resolveBlocker,
|
|
99
134
|
linkBlockerToTask,
|
|
135
|
+
blocksOnLink,
|
|
136
|
+
LINK_BLOCKABLE_STATUSES,
|
|
100
137
|
};
|
|
@@ -73,7 +73,7 @@ module.exports = function buildBlockersRouter() {
|
|
|
73
73
|
// /blockers above); a Xenos cannot resolve or link them.
|
|
74
74
|
// rank: metic+archon — blocker review (auto-promotes linked blocked tasks);
|
|
75
75
|
// criterion C2 / #360 / ADR 0018.
|
|
76
|
-
router.post('/blockers/:id/resolve', api.requireBuilder, api.
|
|
76
|
+
router.post('/blockers/:id/resolve', api.requireBuilder, api.requirePermission('blocker.review'), async (req, res) => {
|
|
77
77
|
if (validateOrRespond(req, res, {
|
|
78
78
|
resolution_note: { type: 'string', maxLength: LIMITS.NOTES },
|
|
79
79
|
})) return;
|
|
@@ -95,11 +95,13 @@ module.exports = function buildBlockersRouter() {
|
|
|
95
95
|
}
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
// Link an existing blocker to a task
|
|
99
|
-
//
|
|
100
|
-
//
|
|
98
|
+
// Link an existing blocker to a task. A task that is currently 'ready' is
|
|
99
|
+
// parked in 'blocked' so it stops advertising as claimable and the
|
|
100
|
+
// resolve-side trigger can release it; every other status is left alone
|
|
101
|
+
// (task 1002559 — see LINK_BLOCKABLE_STATUSES for why 'backlog' is excluded).
|
|
102
|
+
// Metic+ (#360 / ADR 0018) — same blocker-review grant as resolve above.
|
|
101
103
|
// rank: metic+archon — link a blocker to a task; same blocker-review grant as resolve.
|
|
102
|
-
router.post('/blockers/:id/link', api.requireBuilder, api.
|
|
104
|
+
router.post('/blockers/:id/link', api.requireBuilder, api.requirePermission('blocker.review'), async (req, res) => {
|
|
103
105
|
if (validateOrRespond(req, res, {
|
|
104
106
|
task_id: { required: true }, // accept number or stringified number; coerced below
|
|
105
107
|
})) return;
|
|
@@ -108,8 +110,11 @@ module.exports = function buildBlockersRouter() {
|
|
|
108
110
|
const taskId = Number(req.body?.task_id);
|
|
109
111
|
if (!Number.isFinite(taskId)) return res.fail('task_id_required', 400);
|
|
110
112
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
// `blocked` says whether the task actually moved. The old handler always
|
|
114
|
+
// answered {ok:true} even when nothing changed, which is how a dead link
|
|
115
|
+
// loop went unnoticed for six weeks (task 1002559).
|
|
116
|
+
const { blocked } = await blockers.linkBlockerToTask({ blockerId: id, taskId });
|
|
117
|
+
res.json({ ok: true, blocked });
|
|
113
118
|
} catch (err) {
|
|
114
119
|
console.error('[gds] POST /blockers/:id/link', err);
|
|
115
120
|
res.fail('link_failed', { status: 500, message: 'internal error' });
|
|
@@ -129,7 +129,7 @@ module.exports = function buildInboxRouter() {
|
|
|
129
129
|
// archon-only to metic+archon when the three-rank model went live. Any
|
|
130
130
|
// builder can still FILE an idea (POST /inbox above); a Xenos cannot triage.
|
|
131
131
|
// rank: metic+archon — idea triage shapes the backlog; criterion C2 / #360 / ADR 0018.
|
|
132
|
-
router.patch('/inbox/:id', api.requireBuilder, api.
|
|
132
|
+
router.patch('/inbox/:id', api.requireBuilder, api.requirePermission('idea.triage'), async (req, res) => {
|
|
133
133
|
if (validateOrRespond(req, res, {
|
|
134
134
|
// Enum kept light — inbox.updateIdea returns a structured BAD_STATUS
|
|
135
135
|
// error if the value falls outside the accepted set, and the supervisor
|
|
@@ -211,7 +211,7 @@ module.exports = function buildInboxRouter() {
|
|
|
211
211
|
router.post(
|
|
212
212
|
'/inbox/:id/ratify',
|
|
213
213
|
api.requireBuilder,
|
|
214
|
-
api.
|
|
214
|
+
api.requirePermission('idea.ratify'),
|
|
215
215
|
async (req, res) => {
|
|
216
216
|
// notes_md is OPTIONAL and free text. The schema keeps the accepted shape
|
|
217
217
|
// exactly as it was (absent → ''), and adds what the typeof guard could not:
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
'use strict';
|
|
30
30
|
|
|
31
31
|
const api = require('../../src/module-api');
|
|
32
|
-
const { pool, withTx, insertAuditLog, LIVE_RANK_LADDER } = api;
|
|
32
|
+
const { pool, withTx, insertAuditLog, LIVE_RANK_LADDER, RANK_TIER } = api;
|
|
33
33
|
// The seam registry (resolveOptional/emit/emitAsync) — the doorway re-exports it,
|
|
34
34
|
// so the module reaches the `reward`/`onboarding` ports + emits kernel events
|
|
35
35
|
// (task.shipped, builder.box-reconcile-needed) without importing core.
|
|
@@ -149,10 +149,10 @@ function qualifiesForThetesGraduation(rank, shippedCount) {
|
|
|
149
149
|
|
|
150
150
|
// GENERAL_QUEUE_RANKS — the live ranks allowed to claim any ready task (the
|
|
151
151
|
// whole general queue). Everything NOT in this set is sandboxed (see
|
|
152
|
-
// xenosClaimAllowed).
|
|
153
|
-
//
|
|
152
|
+
// xenosClaimAllowed). LIVE_RANK_LADDER minus the sandboxed entry rank 'xenos':
|
|
153
|
+
// thetes (graduated newcomer, #692 / ADR 0034) and up — derived (BV1.R108).
|
|
154
154
|
|
|
155
|
-
const GENERAL_QUEUE_RANKS = new Set(
|
|
155
|
+
const GENERAL_QUEUE_RANKS = new Set(LIVE_RANK_LADDER.filter((r) => r !== 'xenos'));
|
|
156
156
|
|
|
157
157
|
// xenosClaimAllowed — the row-level half of the three-rank model (#360 / ADR
|
|
158
158
|
// 0018; renamed in V3.R15 #239 to newcomer_friendly). The sandboxed entry rank
|
|
@@ -231,7 +231,10 @@ async function claimPermissionsFor(builderId) {
|
|
|
231
231
|
// claimTask + claimTasksBatch both call it after xenosClaimAllowed; the two
|
|
232
232
|
// gates compose without contradiction (see migration 044 for the rationale).
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
// BV1.R108: no longer a hand-copied literal — the canonical map is DERIVED from
|
|
235
|
+
// LIVE_RANK_LADDER in db-kernel and reaches this module through the doorway.
|
|
236
|
+
// The local name survives because ~10 helpers below and the module export read it.
|
|
237
|
+
const REQUIRES_RANK_TIER = RANK_TIER;
|
|
235
238
|
|
|
236
239
|
function rankAllowsTask(builderRank, requiredRank) {
|
|
237
240
|
// Tasks default to requires_rank='xenos' (the NOT NULL DEFAULT on the
|
|
@@ -575,13 +578,16 @@ async function versionProgress() {
|
|
|
575
578
|
// commit together, so a bad criterion leaves nothing half-created. Criteria are
|
|
576
579
|
// idempotent on (version_id, criterion_id) so a re-run can't double-insert.
|
|
577
580
|
|
|
578
|
-
async function createVersion({ id, name, status = 'planning', scopeDocPath = null, doneWhen = null, criteria = [] }) {
|
|
581
|
+
async function createVersion({ id, name, status = 'planning', track = 'product', scopeDocPath = null, doneWhen = null, criteria = [] }) {
|
|
582
|
+
// versions.track is NOT NULL with no DB default (migration 003) — omitting it
|
|
583
|
+
// here made every create 500. An instance's first version is its product
|
|
584
|
+
// track, so that's the default; 'internal' is the platform's own track.
|
|
579
585
|
return withTx(async (client) => {
|
|
580
586
|
const { rows } = await client.query(
|
|
581
|
-
`INSERT INTO versions (id, name, status, scope_doc_path, done_when)
|
|
582
|
-
VALUES ($1, $2, $3, $4, $5)
|
|
583
|
-
RETURNING id, name, status, scope_doc_path, started_at, shipped_at, done_when`,
|
|
584
|
-
[id, name, status, scopeDocPath, doneWhen]
|
|
587
|
+
`INSERT INTO versions (id, name, status, track, scope_doc_path, done_when)
|
|
588
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
589
|
+
RETURNING id, name, status, track, scope_doc_path, started_at, shipped_at, done_when`,
|
|
590
|
+
[id, name, status, track, scopeDocPath, doneWhen]
|
|
585
591
|
);
|
|
586
592
|
const version = rows[0];
|
|
587
593
|
const createdCriteria = [];
|
|
@@ -2479,6 +2485,35 @@ async function unsatisfiedDeps(taskId, client = pool) {
|
|
|
2479
2485
|
// overlapping touches[] (caught BEFORE the unique-violation, so we
|
|
2480
2486
|
// give a useful error rather than a generic constraint failure).
|
|
2481
2487
|
|
|
2488
|
+
// task 1002589 (idea 1000638) — a claims INSERT can trip TWO partial unique
|
|
2489
|
+
// indexes, and each means something different to the caller:
|
|
2490
|
+
// uniq_active_claim_per_task — race lost: another session claimed the task.
|
|
2491
|
+
// the per-session backstop — this session already holds a bound active claim
|
|
2492
|
+
// in the same working tree. Collapsing this case onto ALREADY_CLAIMED sent
|
|
2493
|
+
// builders hunting a phantom rival claimant while the target task sat
|
|
2494
|
+
// 'ready'.
|
|
2495
|
+
// Both generations of the backstop index map to the honest code: a deploy
|
|
2496
|
+
// applies code and migration at different instants, and an instance may still
|
|
2497
|
+
// carry the old index.
|
|
2498
|
+
const SESSION_CLAIM_INDEXES = new Set([
|
|
2499
|
+
'uniq_active_claim_per_session', // migration 180 — session-only (pre-re-key)
|
|
2500
|
+
'uniq_active_claim_per_session_worktree', // core_204 (task 1002570) — per (session, worktree)
|
|
2501
|
+
]);
|
|
2502
|
+
// Branch on err.constraint; an UNKNOWN constraint fails CLOSED on the generic
|
|
2503
|
+
// ALREADY_CLAIMED (never widen a claim refusal). Pure + exported for DB-free
|
|
2504
|
+
// tests; claimTask enriches the session case with the in-flight claim's id.
|
|
2505
|
+
function claimUniqueViolationError(err) {
|
|
2506
|
+
if (err && SESSION_CLAIM_INDEXES.has(err.constraint)) {
|
|
2507
|
+
return {
|
|
2508
|
+
code: 'session_already_has_bound_claim',
|
|
2509
|
+
what_to_do: 'Ship or release the in-flight claim first (/builder-ship or /builder-release), '
|
|
2510
|
+
+ 'or claim this task from its own separate worktree (since core_204 the index allows '
|
|
2511
|
+
+ 'one bound claim per worktree). {"bind_session":false} (an unbound claim) always coexists.',
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
return { code: 'ALREADY_CLAIMED' };
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2482
2517
|
async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSessionId, enforceOverlap = false }) {
|
|
2483
2518
|
const heldPermissions = await claimPermissionsFor(builderId); // BV1.R106 — before the tx opens
|
|
2484
2519
|
const client = await pool.connect();
|
|
@@ -2716,8 +2751,31 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
|
|
|
2716
2751
|
} catch (err) {
|
|
2717
2752
|
await client.query('ROLLBACK').catch(() => {});
|
|
2718
2753
|
if (err && err.code === '23505') {
|
|
2719
|
-
//
|
|
2720
|
-
|
|
2754
|
+
// task 1002589: branch on WHICH unique index tripped — see
|
|
2755
|
+
// claimUniqueViolationError above. The session case is enriched with the
|
|
2756
|
+
// in-flight bound claim (post-ROLLBACK, same client) so the refusal
|
|
2757
|
+
// points at the real cause; best-effort — the honest code must not
|
|
2758
|
+
// depend on this read succeeding.
|
|
2759
|
+
let mapped = claimUniqueViolationError(err);
|
|
2760
|
+
if (mapped.code === 'session_already_has_bound_claim' && creatorSessionId) {
|
|
2761
|
+
try {
|
|
2762
|
+
// Same-worktree row first: under the core_204 index that is the
|
|
2763
|
+
// exact colliding claim; under the old session-only index any bound
|
|
2764
|
+
// row of the session is the culprit, so fall back to the earliest.
|
|
2765
|
+
const { rows } = await client.query(
|
|
2766
|
+
`SELECT c.task_id, t.title
|
|
2767
|
+
FROM claims c JOIN tasks t ON t.id = c.task_id
|
|
2768
|
+
WHERE c.creator_session_id = $1 AND c.released_at IS NULL
|
|
2769
|
+
ORDER BY (COALESCE(c.worktree_name, '') = COALESCE($2, '')) DESC, c.claimed_at, c.id
|
|
2770
|
+
LIMIT 1`,
|
|
2771
|
+
[creatorSessionId, worktreeName ?? null]
|
|
2772
|
+
);
|
|
2773
|
+
if (rows[0]) {
|
|
2774
|
+
mapped = { ...mapped, active_task_id: Number(rows[0].task_id), active_task_title: rows[0].title };
|
|
2775
|
+
}
|
|
2776
|
+
} catch (_) { /* enrichment only — the mapped code still throws */ }
|
|
2777
|
+
}
|
|
2778
|
+
throw mapped;
|
|
2721
2779
|
}
|
|
2722
2780
|
throw err;
|
|
2723
2781
|
} finally {
|
|
@@ -2941,11 +2999,14 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
|
|
|
2941
2999
|
// UPDATE and INSERT (it can't, given the row lock, but defense in depth),
|
|
2942
3000
|
// we surface that as ALREADY_CLAIMED for the specific task.
|
|
2943
3001
|
//
|
|
2944
|
-
// task 1802 — uniq_active_claim_per_session (migration 180)
|
|
2945
|
-
// creator_session_id alone, so it
|
|
3002
|
+
// task 1802 — uniq_active_claim_per_session (migration 180) was keyed on
|
|
3003
|
+
// creator_session_id alone, so it couldn't tell "N rows from one batch call"
|
|
2946
3004
|
// from "N rows from N separate /claims calls": every row here used to carry
|
|
2947
3005
|
// the SAME non-null creatorSessionId for a real (bound) CLI session, so the
|
|
2948
|
-
// 2nd INSERT always hit the partial unique index.
|
|
3006
|
+
// 2nd INSERT always hit the partial unique index. (core_204 / task
|
|
3007
|
+
// 1002570 re-keyed it per (session, worktree) — but a prefix-less batch
|
|
3008
|
+
// stores NULL worktree_name on every row, which the index folds into ONE
|
|
3009
|
+
// group, so first-row-only binding stays load-bearing.) Each batch-claimed task
|
|
2949
3010
|
// also gets its OWN worktree name (worktreePrefix-taskId, right below) — a
|
|
2950
3011
|
// batch is claiming N tasks for N future (possibly parallel) working
|
|
2951
3012
|
// sessions, not one shared worktree — so there is no correctness reason to
|
|
@@ -2982,7 +3043,11 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
|
|
|
2982
3043
|
inserted.push(insertClaim.rows[0]);
|
|
2983
3044
|
} catch (err) {
|
|
2984
3045
|
if (err && err.code === '23505') {
|
|
2985
|
-
|
|
3046
|
+
// task 1002589: same constraint branch as claimTask — the per-session
|
|
3047
|
+
// index can trip here on the FIRST (bound) row when the calling
|
|
3048
|
+
// session already holds a bound claim; every other 23505 stays the
|
|
3049
|
+
// per-task race answer.
|
|
3050
|
+
throw { code: 'BATCH_FAILED', failures: [{ task_id: t.id, reason: claimUniqueViolationError(err).code }] };
|
|
2986
3051
|
}
|
|
2987
3052
|
throw err;
|
|
2988
3053
|
}
|
|
@@ -3663,7 +3728,8 @@ async function listConfirmedTasksForReconcile({ limit = 100 } = {}) {
|
|
|
3663
3728
|
// confirm time. The reconciler ages out silent strands on it (task 1546 / blocker 47:
|
|
3664
3729
|
// 15 tasks sat unprovable for up to 66h because every sweep miss was a silent continue).
|
|
3665
3730
|
const { rows } = await pool.query(
|
|
3666
|
-
`SELECT DISTINCT ON (t.id) t.id AS task_id,
|
|
3731
|
+
`SELECT DISTINCT ON (t.id) t.id AS task_id, t.title, t.security_sensitive,
|
|
3732
|
+
c.builder_id, c.worktree_name, c.head_sha,
|
|
3667
3733
|
c.published_head_sha, t.updated_at AS confirmed_at
|
|
3668
3734
|
FROM tasks t
|
|
3669
3735
|
JOIN claims c ON c.task_id = t.id
|
|
@@ -5344,6 +5410,7 @@ module.exports = {
|
|
|
5344
5410
|
removeTaskCriterion,
|
|
5345
5411
|
linkTaskCriteria,
|
|
5346
5412
|
claimTask,
|
|
5413
|
+
claimUniqueViolationError,
|
|
5347
5414
|
claimTasksBatch,
|
|
5348
5415
|
releaseClaimsBatch,
|
|
5349
5416
|
validateClaimBatch,
|