@bongos/core 1.17.1 → 1.17.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 +521 -281
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +2 -0
- package/.claude/skills/builder-exit/SKILL.md +2 -2
- package/.claude/skills/builder-redteam/SKILL.md +3 -3
- package/.claude/skills/builder-setup/SKILL.md +2 -2
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/dev/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +2 -2
- package/.claude/skills/ideate/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/new-project/SKILL.md +3 -0
- package/.claude/skills/paint/SKILL.md +1 -1
- package/GOVERNANCE.md +1 -1
- package/README.md +1 -1
- 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 +4 -2
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0001-stack-choice-single-vps.md +1 -1
- package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
- package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
- package/docs/adr/0030-builders-hall-live-updates.md +1 -1
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
- package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
- package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
- package/docs/adr/0057-container-cost-ledger.md +3 -3
- package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
- package/docs/adr/0096-require-reward-before-workable.md +9 -9
- package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
- package/docs/adr/0100-two-repo-instance-model.md +1 -1
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
- package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
- package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
- package/docs/adr/README.md +9 -6
- package/docs/api/openapi.json +65 -26
- package/docs/api-reference.md +3 -2
- package/docs/architecture.md +2 -2
- package/docs/branding-contract.md +5 -4
- package/docs/canonical-permissions.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +18 -17
- package/docs/handoff-template.md +2 -2
- package/docs/modules-contract.md +6 -1
- package/docs/onboarding/diagrams/README.md +7 -7
- package/docs/onboarding/drachmae-and-karma.md +18 -18
- package/docs/onboarding/primer.md +9 -9
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/builder-box-lifecycle.md +2 -2
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/ops-gotchas.md +1 -1
- package/docs/recipes/private-npm-distribution.md +17 -0
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/upgrading-the-core.md +55 -0
- package/docs/research/cursor-competitive-analysis.md +1 -1
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
- package/modules/discord/module.json +2 -1
- package/modules/economy/achievements.js +9 -1
- package/modules/economy/credits.js +10 -1
- package/modules/hall-ui/public/builders.js +137 -620
- package/modules/hall-ui/public/goal-inbox.js +232 -0
- package/modules/hall-ui/public/goals-page.js +4 -1
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/hall-kit.js +474 -0
- package/modules/hall-ui/public/hall-widgets.js +14 -5
- package/modules/hall-ui/public/index.html +3 -0
- package/modules/hall-ui/public/instances-panel.js +24 -3
- package/modules/hall-ui/public/new-project-wizard.js +7 -5
- package/modules/hall-ui/public/style.css +168 -0
- package/modules/hall-ui/public/work.css +194 -0
- package/modules/hall-ui/public/work.html +46 -374
- package/modules/hall-ui/public/work.js +137 -102
- package/modules/lifecycle/criterion-suggest.js +80 -0
- package/modules/lifecycle/db.js +42 -5
- package/modules/lifecycle/routes/goals.js +26 -0
- package/modules/lifecycle/routes/tasks.js +21 -0
- package/modules/provisioning/CLAUDE.md +15 -0
- package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
- package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
- package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
- package/modules/provisioning/module.json +17 -0
- package/modules/provisioning/onboard-plan.js +37 -0
- package/modules/provisioning/provisioning.js +539 -0
- package/modules/provisioning/rate-limit.js +35 -0
- package/modules/provisioning/routes/provisioning.js +607 -0
- package/modules/provisioning/tests/provisioning.mjs +232 -0
- package/modules/provisioning/ui/.gitkeep +0 -0
- package/modules/security/secret-scrub.js +10 -0
- package/package-lock.json +2 -2
- package/package.json +7 -2
- package/scripts/discord/README.md +1 -1
- package/scripts/gds/claim.js +98 -48
- package/scripts/gds/claude-materialize.js +30 -9
- package/scripts/gds/cutover-codemod.js +241 -0
- package/scripts/gds/fitness.js +54 -0
- package/scripts/gds/init.js +59 -3
- package/scripts/gds/lint-infra-exec.js +70 -0
- package/scripts/gds/lint-workflow-exec.js +63 -0
- package/scripts/gds/mirror-lineage-check.js +14 -5
- package/scripts/gds/mirror-redact.js +8 -0
- package/scripts/gds/onboard.js +4 -0
- package/scripts/gds/package-core.js +18 -3
- package/scripts/gds/publish-manifest.js +60 -2
- package/scripts/gds/run-routine.js +51 -13
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/update-channel.js +186 -0
- package/scripts/gds/upgrade.js +116 -9
- package/scripts/hall-preview/README.md +103 -0
- package/scripts/hall-preview/fixtures/access-requests.json +1 -0
- package/scripts/hall-preview/fixtures/achievements.json +76 -0
- package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
- package/scripts/hall-preview/fixtures/audit-log.json +1 -0
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
- package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
- package/scripts/hall-preview/fixtures/builders__roster.json +1 -0
- package/scripts/hall-preview/fixtures/goals.json +502 -0
- package/scripts/hall-preview/fixtures/goals__25.json +96 -0
- package/scripts/hall-preview/fixtures/goals__26.json +67 -0
- package/scripts/hall-preview/fixtures/goals__3.json +103 -0
- package/scripts/hall-preview/fixtures/goals__35.json +49 -0
- package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
- package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
- package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
- package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
- package/scripts/hall-preview/fixtures/grades__by-builder.json +1 -0
- package/scripts/hall-preview/fixtures/inbox.json +330 -0
- package/scripts/hall-preview/fixtures/instance.json +124 -0
- package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
- package/scripts/hall-preview/fixtures/me.json +476 -0
- package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
- package/scripts/hall-preview/fixtures/override-requests.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
- package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
- package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
- package/scripts/hall-preview/fixtures/public__grades.json +50 -0
- package/scripts/hall-preview/fixtures/public__progress.json +124 -0
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
- package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
- package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
- package/scripts/hall-preview/fixtures/security__docs.json +1 -0
- package/scripts/hall-preview/fixtures/security__reports.json +1 -0
- package/scripts/hall-preview/fixtures/sessions__search.json +1 -0
- package/scripts/hall-preview/fixtures/tasks.json +2420 -0
- package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
- package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
- package/scripts/hall-preview/fixtures/versions.json +89 -0
- package/scripts/hall-preview/refresh-fixtures.js +77 -0
- package/scripts/hall-preview/server.js +178 -0
- package/scripts/migrate.sh +29 -4
- package/src/bongos/auth.js +14 -1
- package/src/bongos/optimizer.js +37 -2
- package/src/bongos/serve-internal.js +10 -1
- package/src/branding.js +1 -0
- package/src/module-api.js +12 -1
- package/src/module-loader/loader.js +36 -4
- package/src/module-loader/manifest-schema.js +16 -1
- package/tests/branding.mjs +11 -0
- package/tests/claim_error_surface.mjs +107 -0
- package/tests/claude_materialize.mjs +20 -0
- package/tests/consumer_layout_boot.mjs +302 -0
- package/tests/criterion_suggest.mjs +106 -0
- package/tests/cutover_codemod.mjs +184 -0
- package/tests/device_flow_start.mjs +25 -2
- package/tests/hook_core_resolve.mjs +125 -0
- package/tests/infra_exec_paths.mjs +104 -0
- package/tests/init.mjs +47 -0
- package/tests/migrate_root_resolution.mjs +97 -0
- package/tests/mirror_lineage_proof.mjs +14 -0
- package/tests/module_loader.mjs +42 -0
- package/tests/module_manifest.mjs +19 -0
- package/tests/onboard.mjs +13 -0
- package/tests/package_core.mjs +8 -0
- package/tests/provisioning_github_app_routes.mjs +15 -0
- package/tests/publish_manifest.mjs +52 -1
- package/tests/run_routine.mjs +26 -1
- package/tests/run_routine_two_root.mjs +77 -0
- package/tests/scrubber_corpus.mjs +17 -0
- package/tests/update_channel.mjs +186 -0
- package/tests/upgrade.mjs +131 -0
- package/tests/workflow_exec_paths.mjs +98 -0
- package/docs/design/container-exterior-tiles.md +0 -49
- package/docs/design/container-exterior.md +0 -81
- package/docs/design/container-interior.md +0 -77
- package/docs/design/world-map-scroll.md +0 -41
- package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
- package/docs/recipes/cloudbongos-staging-gate.md +0 -139
- package/docs/recipes/cloudbongos-standup.md +0 -111
- package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
- package/docs/recipes/parallel-load-test.md +0 -32
- package/docs/recipes/pixel-art-pipeline.md +0 -411
- package/docs/recipes/staging-data.md +0 -50
- package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
- package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
- package/migrations/instance/110_medusa_planning_seed.sql +0 -70
- package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
- package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
- package/scripts/gds/art-stage.js +0 -152
- package/server.js +0 -212
- package/tests/art_key.mjs +0 -121
- package/tests/day_cycle.mjs +0 -197
- package/tests/snapshot_picker.mjs +0 -233
- package/tests/sprite_facing.mjs +0 -35
- package/tests/terrain_picker.mjs +0 -298
- package/tests/turn_in_place.mjs +0 -280
- package/tests/walkability.mjs +0 -188
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
// file's exact variants (date-only fmtDate; a 2200ms clearing toast).
|
|
37
37
|
// fmtAgo / toAuth / getJSON / postJSON stay local below.
|
|
38
38
|
const { escapeHtml, $, fmtNum } = window.OTB;
|
|
39
|
+
// Shared component kit (hall-kit.js, task 2166): tabs, pager, filter bar.
|
|
40
|
+
const { makePager, paginate, wirePager, pagerBarHtml, makeTabs, makeFilterBar } = window.OTBKit;
|
|
39
41
|
const fmtDate = (iso) => window.OTB.fmtDate(iso);
|
|
40
42
|
const toast = (msg) => window.OTB.toast(msg, { duration: 2200, clear: true });
|
|
41
43
|
const curLabel = (caps) => { const l = (window.__BRANDING__ && window.__BRANDING__.currency && window.__BRANDING__.currency.label) || 'credits'; return caps ? l.charAt(0).toUpperCase() + l.slice(1) : l; };
|
|
@@ -102,25 +104,11 @@
|
|
|
102
104
|
return data;
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
// ---- paginated lists
|
|
106
|
-
// All four Work-page lists
|
|
107
|
-
//
|
|
108
|
-
// per-page
|
|
109
|
-
//
|
|
110
|
-
// Page/per-page live in module scope so a background poll / action refresh
|
|
111
|
-
// re-renders on the SAME page — only an explicit filter/per-page change resets.
|
|
112
|
-
const PER_PAGE_OPTIONS = [10, 25, 50, 100];
|
|
113
|
-
const MIN_PER_PAGE = PER_PAGE_OPTIONS[0]; // ≤ this fits one page — hide the bar
|
|
114
|
-
const DEFAULT_PER_PAGE = 25;
|
|
115
|
-
function loadPerPage(key, fallback) {
|
|
116
|
-
try {
|
|
117
|
-
const v = Number(window.localStorage.getItem(key));
|
|
118
|
-
return PER_PAGE_OPTIONS.includes(v) ? v : fallback;
|
|
119
|
-
} catch (_) { return fallback; }
|
|
120
|
-
}
|
|
121
|
-
function makePager(prefix, key) {
|
|
122
|
-
return { prefix, key, page: 1, perPage: loadPerPage(key, DEFAULT_PER_PAGE) };
|
|
123
|
-
}
|
|
107
|
+
// ---- paginated lists (task 1931/1955 → kit, task 2170) --------------------
|
|
108
|
+
// All four Work-page lists paginate through the shared OTBKit pager (the
|
|
109
|
+
// canonical copy of the helper that used to live here). localStorage keys are
|
|
110
|
+
// unchanged so builders' per-page prefs survive. The bar markup is stamped
|
|
111
|
+
// into the #<prefix>-pager-mount divs at boot (wireEvents) via pagerBarHtml.
|
|
124
112
|
const pagers = {
|
|
125
113
|
wip: makePager('wip', 'otb.work.wipPerPage'),
|
|
126
114
|
ready: makePager('ready', 'otb.work.readyPerPage'),
|
|
@@ -128,6 +116,11 @@
|
|
|
128
116
|
backlog: makePager('backlog', 'otb.work.backlogPerPage'),
|
|
129
117
|
};
|
|
130
118
|
|
|
119
|
+
// task 2170: the four sections are hash-synced tabs (Ready default). Built in
|
|
120
|
+
// wireEvents; renders feed the tab count pills via setTabCount.
|
|
121
|
+
let workTabs = null;
|
|
122
|
+
function setTabCount(id, n) { if (workTabs) workTabs.setCount(id, n); }
|
|
123
|
+
|
|
131
124
|
// A section-header count pill: reveal it and set its number once data lands.
|
|
132
125
|
function setCountPill(sel, n) {
|
|
133
126
|
const el = $(sel);
|
|
@@ -136,53 +129,6 @@
|
|
|
136
129
|
el.textContent = String(n);
|
|
137
130
|
}
|
|
138
131
|
|
|
139
|
-
// Slice `list` to the pager's current page (clamped — the filtered list may
|
|
140
|
-
// have shrunk under a poll), paint the bar, and return the slice to render.
|
|
141
|
-
// The bar hides when the whole list fits one minimum page.
|
|
142
|
-
function paginate(list, p) {
|
|
143
|
-
const total = list.length;
|
|
144
|
-
const totalPages = Math.max(1, Math.ceil(total / p.perPage));
|
|
145
|
-
if (p.page > totalPages) p.page = totalPages;
|
|
146
|
-
if (p.page < 1) p.page = 1;
|
|
147
|
-
const start = (p.page - 1) * p.perPage;
|
|
148
|
-
const slice = list.slice(start, start + p.perPage);
|
|
149
|
-
renderPagerBar(p, total, totalPages, start, slice.length);
|
|
150
|
-
return slice;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function renderPagerBar(p, total, totalPages, start, shownCount) {
|
|
154
|
-
const pager = $(`#${p.prefix}-pager`);
|
|
155
|
-
if (!pager) return;
|
|
156
|
-
if (total <= MIN_PER_PAGE) { pager.hidden = true; return; }
|
|
157
|
-
pager.hidden = false;
|
|
158
|
-
const range = $(`#${p.prefix}-range`);
|
|
159
|
-
if (range) range.textContent = total
|
|
160
|
-
? `Showing ${start + 1}–${start + shownCount} of ${total}`
|
|
161
|
-
: 'Showing 0 of 0';
|
|
162
|
-
const ind = $(`#${p.prefix}-page`);
|
|
163
|
-
if (ind) ind.textContent = `Page ${p.page} of ${totalPages}`;
|
|
164
|
-
const prev = $(`#${p.prefix}-prev`);
|
|
165
|
-
if (prev) prev.disabled = p.page <= 1;
|
|
166
|
-
const next = $(`#${p.prefix}-next`);
|
|
167
|
-
if (next) next.disabled = p.page >= totalPages;
|
|
168
|
-
const sel = $(`#${p.prefix}-per-page`);
|
|
169
|
-
if (sel && sel.value !== String(p.perPage)) sel.value = String(p.perPage);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Wire a pager's controls once (per-page dropdown + Prev/Next). `rerender`
|
|
173
|
-
// re-renders that list; a per-page change resets to page 1 and persists.
|
|
174
|
-
function wirePager(p, rerender) {
|
|
175
|
-
$(`#${p.prefix}-per-page`)?.addEventListener('change', (e) => {
|
|
176
|
-
const v = Number(e.target.value);
|
|
177
|
-
if (!PER_PAGE_OPTIONS.includes(v)) return;
|
|
178
|
-
p.perPage = v; p.page = 1;
|
|
179
|
-
try { window.localStorage.setItem(p.key, String(v)); } catch (_) { /* private mode → in-memory only */ }
|
|
180
|
-
rerender();
|
|
181
|
-
});
|
|
182
|
-
$(`#${p.prefix}-prev`)?.addEventListener('click', () => { if (p.page > 1) { p.page -= 1; rerender(); } });
|
|
183
|
-
$(`#${p.prefix}-next`)?.addEventListener('click', () => { p.page += 1; rerender(); });
|
|
184
|
-
}
|
|
185
|
-
|
|
186
132
|
// ---- 1a. live in-flight list (active claims) -----------------------------
|
|
187
133
|
|
|
188
134
|
function renderInFlight(active) {
|
|
@@ -196,6 +142,7 @@
|
|
|
196
142
|
const isMine = (c) => viewer.id != null && Number(c.builder_id) === Number(viewer.id);
|
|
197
143
|
const rows = all.slice().sort((a, b) => (isMine(b) ? 1 : 0) - (isMine(a) ? 1 : 0));
|
|
198
144
|
setCountPill('#wip-count', all.length);
|
|
145
|
+
setTabCount('in-progress', all.length);
|
|
199
146
|
if (!rows.length) {
|
|
200
147
|
tbody.innerHTML = `<tr><td colspan="6" class="ledger__empty">No tasks are in progress right now.</td></tr>`;
|
|
201
148
|
pagers.wip.page = 1;
|
|
@@ -378,8 +325,10 @@
|
|
|
378
325
|
|
|
379
326
|
const count = $('#claim-count');
|
|
380
327
|
if (count) count.textContent = `${list.length} ${list.length === 1 ? 'task' : 'tasks'} waiting`;
|
|
381
|
-
// task 1899: header count pill reflects the filtered-list length
|
|
328
|
+
// task 1899: header count pill reflects the filtered-list length; the tab
|
|
329
|
+
// pill (task 2170) carries the FULL ready count, filters notwithstanding.
|
|
382
330
|
setCountPill('#ready-count', list.length);
|
|
331
|
+
setTabCount('ready', taskState.all.length);
|
|
383
332
|
|
|
384
333
|
if (!list.length) {
|
|
385
334
|
root.innerHTML = `<div class="profile__placeholder">No tasks match these filters. Try widening them.</div>`;
|
|
@@ -405,9 +354,11 @@
|
|
|
405
354
|
? '<span class="task__tag task__tag--newcomer">newcomer</span>'
|
|
406
355
|
: '';
|
|
407
356
|
const rankTag = rankTagHtml(t.requires_rank);
|
|
357
|
+
// task 2170 density pass: the multi-line path dump becomes a compact
|
|
358
|
+
// "✎ N files" chip in the meta row; the full list rides the tooltip.
|
|
408
359
|
const touches = Array.isArray(t.touches) ? t.touches : [];
|
|
409
|
-
const
|
|
410
|
-
? `<
|
|
360
|
+
const touchesChip = touches.length
|
|
361
|
+
? `<span class="task__touches" title="${escapeHtml(touches.join('\n'))}">✎ ${touches.length} file${touches.length === 1 ? '' : 's'}</span>`
|
|
411
362
|
: '';
|
|
412
363
|
const selected = selection.claim.has(String(t.id));
|
|
413
364
|
return `
|
|
@@ -428,8 +379,8 @@
|
|
|
428
379
|
${t.priority ? `<span class="task__tag">${escapeHtml(t.priority)}</span>` : ''}
|
|
429
380
|
<span>${minutesText}</span>
|
|
430
381
|
<span>${cpmText}</span>
|
|
382
|
+
${touchesChip}
|
|
431
383
|
</div>
|
|
432
|
-
${touchesLine}
|
|
433
384
|
</div>
|
|
434
385
|
<div class="task__credits">
|
|
435
386
|
<span class="task__credits-num">${fmtNum(window.OTBBranding?.taskCreditValue?.(t) ?? t.credits_reward)}</span>
|
|
@@ -437,13 +388,26 @@
|
|
|
437
388
|
</div>
|
|
438
389
|
<div class="task__actions">
|
|
439
390
|
<button class="task__claim" type="button" data-task-id="${t.id}" aria-label="Claim task ${t.id}">Claim</button>
|
|
440
|
-
<button class="task__copy" type="button" data-task-id="${t.id}" title="Copy the task prompt to your clipboard" aria-label="Copy prompt for task ${t.id}"><span class="task__copy-icon" aria-hidden="true">📋</span
|
|
391
|
+
<button class="task__copy" type="button" data-task-id="${t.id}" title="Copy the task prompt to your clipboard" aria-label="Copy prompt for task ${t.id}"><span class="task__copy-icon" aria-hidden="true">📋</span></button>
|
|
441
392
|
</div>
|
|
442
393
|
</div>
|
|
443
394
|
`;
|
|
444
395
|
}).join('');
|
|
445
396
|
}
|
|
446
397
|
|
|
398
|
+
// task 2170: the filter bar restores URL-query state at build time, but the
|
|
399
|
+
// Version/Goal selects fill their options LATER (from /versions and the
|
|
400
|
+
// claimable feed) — so a shared URL's value has nothing to select against
|
|
401
|
+
// until now. Returns true when it changed the control.
|
|
402
|
+
function restoreUrlFilter(id) {
|
|
403
|
+
const want = new URLSearchParams(window.location.search).get(id);
|
|
404
|
+
const sel = $(`#${id}`);
|
|
405
|
+
if (!want || !sel || sel.value === want) return false;
|
|
406
|
+
if (![...sel.options].some((o) => o.value === want)) return false;
|
|
407
|
+
sel.value = want;
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
447
411
|
function populateVersionFilter(versions) {
|
|
448
412
|
const sel = $('#f-version');
|
|
449
413
|
if (!sel) return;
|
|
@@ -990,11 +954,11 @@
|
|
|
990
954
|
if (Array.isArray(tasks)) historyRows = tasks;
|
|
991
955
|
const root = $('#history-list');
|
|
992
956
|
if (!root) return;
|
|
993
|
-
// task
|
|
994
|
-
|
|
995
|
-
if (scroll) scroll.hidden = !historyRows.length;
|
|
957
|
+
// task 2170: section visibility is tab-owned now — the render only fills
|
|
958
|
+
// the panel and feeds the counts.
|
|
996
959
|
const rows = historyRows;
|
|
997
960
|
setCountPill('#done-count', rows.length);
|
|
961
|
+
setTabCount('completed', rows.length);
|
|
998
962
|
if (!rows.length) {
|
|
999
963
|
root.innerHTML = `<div class="profile__placeholder">Nothing shipped yet.</div>`;
|
|
1000
964
|
pagers.done.page = 1;
|
|
@@ -1029,16 +993,17 @@
|
|
|
1029
993
|
|
|
1030
994
|
function renderBacklog(tasks) {
|
|
1031
995
|
if (Array.isArray(tasks)) backlogRows = tasks;
|
|
1032
|
-
const scroll = $('#backlog-scroll');
|
|
1033
996
|
const root = $('#backlog-list');
|
|
1034
997
|
if (!root) return;
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
-
|
|
1038
|
-
if (!backlogRows.length) {
|
|
998
|
+
// task 2170: section visibility is tab-owned now. The tab pill carries the
|
|
999
|
+
// full backlog count; the heading count reflects the search-filtered list.
|
|
1000
|
+
setTabCount('backlog', backlogRows.length);
|
|
1001
|
+
if (!backlogRows.length) {
|
|
1002
|
+
root.innerHTML = '<div class="profile__placeholder">Nothing in the backlog right now.</div>';
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1039
1005
|
const q = searchQuery();
|
|
1040
1006
|
const list = q ? backlogRows.filter((t) => matchesSearch(t, q)) : backlogRows;
|
|
1041
|
-
if (scroll) scroll.hidden = false;
|
|
1042
1007
|
const countEl = $('#backlog-count');
|
|
1043
1008
|
if (countEl) countEl.textContent = `${list.length} ${list.length === 1 ? 'task' : 'tasks'}`;
|
|
1044
1009
|
const shown = paginate(list, pagers.backlog);
|
|
@@ -1094,10 +1059,17 @@
|
|
|
1094
1059
|
// ---- gate notice (signed in, primer not yet read) ------------------------
|
|
1095
1060
|
|
|
1096
1061
|
function showClaimGate() {
|
|
1062
|
+
// task 2170: the primer gate replaces the whole tabbed board (the claimable
|
|
1063
|
+
// feed and active-claims read are both primer-gated server-side, so every
|
|
1064
|
+
// tab would be empty-but-misleading). Gate notice on, tabs + panels off.
|
|
1097
1065
|
const gate = $('#gate-scroll');
|
|
1098
|
-
const claim = $('#claim-scroll');
|
|
1099
1066
|
if (gate) gate.hidden = false;
|
|
1100
|
-
|
|
1067
|
+
const tabsMount = $('#work-tabs');
|
|
1068
|
+
if (tabsMount) tabsMount.hidden = true;
|
|
1069
|
+
for (const id of ['claim-scroll', 'wip-scroll', 'backlog-scroll', 'history-scroll']) {
|
|
1070
|
+
const el = document.getElementById(id);
|
|
1071
|
+
if (el) el.hidden = true;
|
|
1072
|
+
}
|
|
1101
1073
|
}
|
|
1102
1074
|
|
|
1103
1075
|
// ---- bootstrap -----------------------------------------------------------
|
|
@@ -1163,10 +1135,14 @@
|
|
|
1163
1135
|
]);
|
|
1164
1136
|
taskState.versions = versionsResp.versions || [];
|
|
1165
1137
|
populateVersionFilter(taskState.versions);
|
|
1138
|
+
restoreUrlFilter('f-version');
|
|
1166
1139
|
renderInFlight(claimResp.active_claims || []);
|
|
1167
1140
|
taskState.all = claimResp.claimable || [];
|
|
1168
1141
|
renderClaimable();
|
|
1169
1142
|
populateGoalFilter(); // task 1755
|
|
1143
|
+
// task 2170: a shared URL's goal filter can only apply once the options
|
|
1144
|
+
// exist — restore it now and re-render if it changed anything.
|
|
1145
|
+
if (restoreUrlFilter('f-goal')) renderClaimable();
|
|
1170
1146
|
pruneSelection();
|
|
1171
1147
|
renderBulkToolbar();
|
|
1172
1148
|
} catch (err) {
|
|
@@ -1182,31 +1158,90 @@
|
|
|
1182
1158
|
}
|
|
1183
1159
|
|
|
1184
1160
|
function wireEvents() {
|
|
1185
|
-
// task
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
$('#f-newcomer')?.addEventListener('change', onReadyFilterChange);
|
|
1192
|
-
$('#f-fit')?.addEventListener('change', onReadyFilterChange);
|
|
1193
|
-
$('#f-sort')?.addEventListener('change', onReadyFilterChange);
|
|
1194
|
-
// task 1829: free-text search — 'input' fires on every keystroke and on the
|
|
1195
|
-
// type=search clear (✕). Re-render BOTH the claimable and backlog lists,
|
|
1196
|
-
// resetting each to page 1 so matches aren't hidden behind pagination.
|
|
1197
|
-
$('#f-search')?.addEventListener('input', () => {
|
|
1198
|
-
pagers.ready.page = 1;
|
|
1199
|
-
pagers.backlog.page = 1;
|
|
1200
|
-
renderClaimable();
|
|
1201
|
-
renderBacklog();
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
// task 1931/1955: pagination controls for all four Work-page lists.
|
|
1161
|
+
// task 2170: stamp the standard pager bars into their mounts, then wire —
|
|
1162
|
+
// the markup that used to be hand-copied four times in work.html.
|
|
1163
|
+
for (const [prefix, label] of [['ready', 'Ready tasks'], ['wip', 'In-progress tasks'], ['done', 'Completed tasks'], ['backlog', 'Backlog tasks']]) {
|
|
1164
|
+
const m = $(`#${prefix}-pager-mount`);
|
|
1165
|
+
if (m) m.innerHTML = pagerBarHtml(prefix, { label });
|
|
1166
|
+
}
|
|
1205
1167
|
wirePager(pagers.ready, renderClaimable);
|
|
1206
1168
|
wirePager(pagers.wip, renderInFlight);
|
|
1207
1169
|
wirePager(pagers.done, renderHistory);
|
|
1208
1170
|
wirePager(pagers.backlog, renderBacklog);
|
|
1209
1171
|
|
|
1172
|
+
// task 2170: the four sections become hash-synced tabs, Ready first. The
|
|
1173
|
+
// kit never claims "#/…" hashes, so the task/idea/blocker overlay deep
|
|
1174
|
+
// links keep working. Selection is scoped per tab — switching clears it.
|
|
1175
|
+
workTabs = makeTabs({
|
|
1176
|
+
mount: $('#work-tabs'),
|
|
1177
|
+
hash: true,
|
|
1178
|
+
initial: 'ready',
|
|
1179
|
+
tabs: [
|
|
1180
|
+
{ id: 'ready', label: 'Ready', panel: 'claim-scroll' },
|
|
1181
|
+
{ id: 'in-progress', label: 'In Progress', panel: 'wip-scroll' },
|
|
1182
|
+
{ id: 'backlog', label: 'Backlog', panel: 'backlog-scroll' },
|
|
1183
|
+
{ id: 'completed', label: 'Completed', panel: 'history-scroll' },
|
|
1184
|
+
],
|
|
1185
|
+
onChange: () => { if (selectionTotal()) clearSelection(); },
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
// task 2170: filter diet — Search/Sort/Version primary, the rest behind
|
|
1189
|
+
// the kit's "Filters" disclosure; state rides the URL query (shareable).
|
|
1190
|
+
// Control ids are unchanged so renderClaimable's reads + the dynamic
|
|
1191
|
+
// version/goal option fills keep working untouched.
|
|
1192
|
+
makeFilterBar({
|
|
1193
|
+
mount: $('#work-filters'),
|
|
1194
|
+
search: { id: 'f-search', label: 'Search', placeholder: 'task #, title, or goal…' },
|
|
1195
|
+
primary: [
|
|
1196
|
+
{
|
|
1197
|
+
id: 'f-sort',
|
|
1198
|
+
label: 'Sort',
|
|
1199
|
+
options: [
|
|
1200
|
+
{ value: 'cpm', label: `${curLabel()} / minute (high → low)` },
|
|
1201
|
+
{ value: 'credits', label: `${curLabel()} (high → low)` },
|
|
1202
|
+
{ value: 'minutes', label: 'minutes (low → high)' },
|
|
1203
|
+
{ value: 'priority', label: 'priority (P0 first)' },
|
|
1204
|
+
{ value: 'id', label: 'task number (low → high)' },
|
|
1205
|
+
],
|
|
1206
|
+
},
|
|
1207
|
+
{ id: 'f-version', label: 'Version', options: [{ value: '', label: '— any —' }] },
|
|
1208
|
+
],
|
|
1209
|
+
overflow: [
|
|
1210
|
+
{ id: 'f-goal', label: 'Goal', options: [{ value: '', label: '— all goals —' }] },
|
|
1211
|
+
{
|
|
1212
|
+
id: 'f-kind',
|
|
1213
|
+
label: 'Kind',
|
|
1214
|
+
options: [{ value: '', label: '— any —' },
|
|
1215
|
+
'feature', 'bug', 'infra', 'refactor', 'spike', 'learning-capture',
|
|
1216
|
+
'cleanup', 'decision', 'blocker-resolution', 'unclassified'],
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
id: 'f-discipline',
|
|
1220
|
+
label: 'Discipline',
|
|
1221
|
+
options: [{ value: '', label: '— any —' }, 'engineer', 'artist', 'ideator', 'ui', 'unclassified'],
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
id: 'f-newcomer',
|
|
1225
|
+
label: 'Newcomer',
|
|
1226
|
+
options: [{ value: '', label: '— any —' }, { value: 'yes', label: 'newcomer-friendly only' }],
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
id: 'f-fit',
|
|
1230
|
+
label: 'Run mode',
|
|
1231
|
+
options: [{ value: '', label: '— any —' },
|
|
1232
|
+
{ value: 'parallel', label: 'parallel-safe only' },
|
|
1233
|
+
{ value: 'conflict', label: 'has file conflicts' }],
|
|
1234
|
+
},
|
|
1235
|
+
],
|
|
1236
|
+
url: true,
|
|
1237
|
+
onChange: () => {
|
|
1238
|
+
pagers.ready.page = 1;
|
|
1239
|
+
pagers.backlog.page = 1;
|
|
1240
|
+
renderClaimable();
|
|
1241
|
+
renderBacklog();
|
|
1242
|
+
},
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1210
1245
|
$('#task-list')?.addEventListener('click', (e) => {
|
|
1211
1246
|
// task 1759: the multi-select checkbox. Toggle the Set + the card's
|
|
1212
1247
|
// highlight class directly (cheaper than a full renderClaimable) and
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// criterion-suggest.js — task 2165. A pure, deterministic RECOMMENDER: given a
|
|
3
|
+
// goal's criteria and a candidate task's title + description, rank the criteria
|
|
4
|
+
// by lexical overlap so task-create can SUGGEST the most likely done-when
|
|
5
|
+
// criterion to link. The builder always confirms or overrides — nothing is
|
|
6
|
+
// auto-linked (the "suggest, human picks" contract). No DB, no network, so it's
|
|
7
|
+
// unit-testable in the DB-free suite (tests live in modules/lifecycle/tests/).
|
|
8
|
+
//
|
|
9
|
+
// WHY lexical (not embeddings): the criterion set per goal is tiny (a handful),
|
|
10
|
+
// the signal is strong (criteria prose shares vocabulary with the tasks that
|
|
11
|
+
// fulfil it), and a deterministic ranker needs no model call, no key, and no
|
|
12
|
+
// flakiness — it slots straight into the create path. The scoring is a weighted
|
|
13
|
+
// term overlap: a term from the task TITLE counts double a term from the body,
|
|
14
|
+
// because the title is the denser signal of what the task is about.
|
|
15
|
+
|
|
16
|
+
// Common words that carry no discriminating signal — dropping them keeps a
|
|
17
|
+
// criterion from scoring on filler like "the work is done when…".
|
|
18
|
+
const STOPWORDS = new Set([
|
|
19
|
+
'the', 'a', 'an', 'and', 'or', 'but', 'to', 'of', 'in', 'on', 'for', 'with',
|
|
20
|
+
'without', 'is', 'are', 'be', 'was', 'were', 'it', 'its', 'this', 'that',
|
|
21
|
+
'these', 'those', 'as', 'at', 'by', 'from', 'into', 'out', 'over', 'so',
|
|
22
|
+
'then', 'than', 'not', 'no', 'can', 'do', 'does', 'done', 'when', 'which',
|
|
23
|
+
'who', 'what', 'how', 'their', 'them', 'they', 'you', 'your', 'our', 'via',
|
|
24
|
+
'per', 'each', 'one', 'all', 'any', 'new', 'use', 'used', 'using', 'make',
|
|
25
|
+
'made', 'get', 'got', 'set', 'add', 'added', 'work', 'works', 'working',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
// Lowercase, strip punctuation, drop stopwords + tokens shorter than 3 chars,
|
|
29
|
+
// and crudely singularize (trailing 's' on longer words) so "criteria"/"tasks"
|
|
30
|
+
// match "criterion"/"task"-ish stems. Deterministic and allocation-cheap.
|
|
31
|
+
function tokenize(text) {
|
|
32
|
+
return String(text || '')
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/[^a-z0-9\s]+/g, ' ')
|
|
35
|
+
.split(/\s+/)
|
|
36
|
+
.filter((w) => w.length >= 3 && !STOPWORDS.has(w))
|
|
37
|
+
.map((w) => (w.length > 4 && w.endsWith('s') ? w.slice(0, -1) : w));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Rank a goal's UNSATISFIED criteria against a candidate task. Title terms weigh
|
|
41
|
+
// 2, description-only terms weigh 1. Returns a new array sorted best-first, each
|
|
42
|
+
// { id, criterion_id, criterion_md, score, matched_terms }. Ties break by
|
|
43
|
+
// ascending numeric id so the output is stable. Options:
|
|
44
|
+
// limit — cap the number of suggestions returned (default 3)
|
|
45
|
+
// includeZero — keep criteria with no term overlap (default false: drop them,
|
|
46
|
+
// so "no confident suggestion" returns [] rather than noise)
|
|
47
|
+
function suggestCriteria(criteria, candidate = {}, opts = {}) {
|
|
48
|
+
const { title = '', description = '' } = candidate;
|
|
49
|
+
const { limit = 3, includeZero = false } = opts;
|
|
50
|
+
|
|
51
|
+
// Build the weighted term map: description terms at 1, title terms override to
|
|
52
|
+
// 2 (a term in both is a title term — the stronger signal wins).
|
|
53
|
+
const weights = new Map();
|
|
54
|
+
for (const t of tokenize(description)) weights.set(t, 1);
|
|
55
|
+
for (const t of tokenize(title)) weights.set(t, 2);
|
|
56
|
+
|
|
57
|
+
const ranked = (Array.isArray(criteria) ? criteria : [])
|
|
58
|
+
.filter((c) => c && !c.satisfied)
|
|
59
|
+
.map((c) => {
|
|
60
|
+
const critTerms = new Set(tokenize(c.criterion_md));
|
|
61
|
+
let score = 0;
|
|
62
|
+
const matched = [];
|
|
63
|
+
for (const [term, w] of weights) {
|
|
64
|
+
if (critTerms.has(term)) { score += w; matched.push(term); }
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
id: c.id,
|
|
68
|
+
criterion_id: c.criterion_id,
|
|
69
|
+
criterion_md: c.criterion_md,
|
|
70
|
+
score,
|
|
71
|
+
matched_terms: matched.sort(),
|
|
72
|
+
};
|
|
73
|
+
})
|
|
74
|
+
.sort((a, b) => (b.score - a.score) || (Number(a.id) - Number(b.id)));
|
|
75
|
+
|
|
76
|
+
const kept = includeZero ? ranked : ranked.filter((r) => r.score > 0);
|
|
77
|
+
return kept.slice(0, Math.max(0, limit));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { suggestCriteria, tokenize };
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -1665,6 +1665,43 @@ async function listTasks({ versionId, status, kind, discipline, goalId } = {}) {
|
|
|
1665
1665
|
// never can. This is a HARD control (a JS post-filter on the fit query), not a
|
|
1666
1666
|
// soft prompt instruction. Backstops the SKILL.md fencing (layer 1).
|
|
1667
1667
|
|
|
1668
|
+
// task 2181 (incident, blocker 74): these are the EXACT bodies of the
|
|
1669
|
+
// claimable_tasks and tasks_session_fit views (migration
|
|
1670
|
+
// core_190_rebuild_claimable_task_views.sql), inlined as subqueries so the
|
|
1671
|
+
// claimable feed reads base tables (ACCESS SHARE only) instead of the views.
|
|
1672
|
+
// A detached backend queued an ACCESS EXCLUSIVE DDL on the views, which parks
|
|
1673
|
+
// every view SELECT behind it in the lock queue — the hall's primary feed hung
|
|
1674
|
+
// indefinitely while raw-table reads stayed fast. Postgres inlines simple
|
|
1675
|
+
// views into the same plan anyway, so this is semantics- and plan-identical;
|
|
1676
|
+
// it just stops the query's LOCKING from referencing the view relations.
|
|
1677
|
+
// If the view bodies change in a future migration, change these in lockstep
|
|
1678
|
+
// (or fold back to the views once per-view DDL contention is a solved problem).
|
|
1679
|
+
const CLAIMABLE_SQL = `
|
|
1680
|
+
SELECT t.*, pms_calibrated_minutes(t.est_minutes, t.kind) AS est_minutes_calibrated
|
|
1681
|
+
FROM tasks t
|
|
1682
|
+
WHERE t.status = 'ready'
|
|
1683
|
+
AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'
|
|
1684
|
+
AND NOT EXISTS (
|
|
1685
|
+
SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id
|
|
1686
|
+
)
|
|
1687
|
+
AND NOT EXISTS (
|
|
1688
|
+
SELECT 1 FROM claims c
|
|
1689
|
+
JOIN tasks t2 ON t2.id = c.task_id
|
|
1690
|
+
WHERE c.released_at IS NULL
|
|
1691
|
+
AND t2.touches && t.touches
|
|
1692
|
+
)`;
|
|
1693
|
+
const SESSION_FIT_SQL = `
|
|
1694
|
+
SELECT t.id,
|
|
1695
|
+
CASE
|
|
1696
|
+
WHEN t.automation_tag = 'overnight-safe' AND COALESCE(t.manual_degree, 5) <= 2
|
|
1697
|
+
AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
|
|
1698
|
+
THEN ARRAY['autonomous','check-in','active']
|
|
1699
|
+
WHEN t.automation_tag = 'live-only' OR COALESCE(t.manual_degree, 5) >= 4
|
|
1700
|
+
THEN ARRAY['active']
|
|
1701
|
+
ELSE ARRAY['check-in','active']
|
|
1702
|
+
END AS fits_session_modes
|
|
1703
|
+
FROM tasks t`;
|
|
1704
|
+
|
|
1668
1705
|
async function listClaimableTasks({ versionId = null, builderId = null, discipline = null, fit = null } = {}) {
|
|
1669
1706
|
const params = [];
|
|
1670
1707
|
const where = [];
|
|
@@ -1717,12 +1754,12 @@ async function listClaimableTasks({ versionId = null, builderId = null, discipli
|
|
|
1717
1754
|
END
|
|
1718
1755
|
)`);
|
|
1719
1756
|
}
|
|
1720
|
-
// Autonomy / parallel-eligibility filter. Joining
|
|
1721
|
-
// SELECT t.*
|
|
1722
|
-
// so the ORDER BY below qualifies them with v. unconditionally.
|
|
1757
|
+
// Autonomy / parallel-eligibility filter. Joining the session-fit relation
|
|
1758
|
+
// (also a SELECT t.* shape) makes bare column names like priority/created_at
|
|
1759
|
+
// ambiguous, so the ORDER BY below qualifies them with v. unconditionally.
|
|
1723
1760
|
let fitJoin = '';
|
|
1724
1761
|
if (fit === 'autonomous' || fit === 'parallel') {
|
|
1725
|
-
fitJoin = ` JOIN
|
|
1762
|
+
fitJoin = ` JOIN (${SESSION_FIT_SQL}) sf ON sf.id = v.id`;
|
|
1726
1763
|
where.push(`'autonomous' = ANY(sf.fits_session_modes)`);
|
|
1727
1764
|
if (fit === 'parallel') {
|
|
1728
1765
|
where.push(`v.parallel_safe = true`);
|
|
@@ -1733,7 +1770,7 @@ async function listClaimableTasks({ versionId = null, builderId = null, discipli
|
|
|
1733
1770
|
// recent actuals; ordering on the raw estimate puts oversized tasks at the
|
|
1734
1771
|
// bottom even when their actual time-to-ship is short. See migration 015.
|
|
1735
1772
|
const sql =
|
|
1736
|
-
`SELECT v.* FROM
|
|
1773
|
+
`SELECT v.* FROM (${CLAIMABLE_SQL}) v` + fitJoin +
|
|
1737
1774
|
(where.length ? ` WHERE ${where.join(' AND ')}` : '') +
|
|
1738
1775
|
` ORDER BY v.priority NULLS LAST,
|
|
1739
1776
|
COALESCE(v.est_minutes_calibrated, v.est_minutes) NULLS LAST,
|
|
@@ -26,6 +26,7 @@ const api = require('../../../src/module-api');
|
|
|
26
26
|
const auth = api;
|
|
27
27
|
const db = require('../db');
|
|
28
28
|
const hierarchy = require('../hierarchy');
|
|
29
|
+
const { suggestCriteria } = require('../criterion-suggest');
|
|
29
30
|
const { classifyTaskKind, VALID_KINDS, VALID_DISCIPLINES, TASK_FIELD_BOUNDS } = require('../task-classifier');
|
|
30
31
|
const { validateOrRespond, parseId, LIMITS } = api;
|
|
31
32
|
const { isModuleKey, scopeIncludesProtected, moduleKeys } = api.moduleScopeMap;
|
|
@@ -548,6 +549,31 @@ module.exports = function buildGoalsRouter() {
|
|
|
548
549
|
}
|
|
549
550
|
});
|
|
550
551
|
|
|
552
|
+
// GET /goals/:id/suggest-criteria?title=&description=&limit= — task 2165.
|
|
553
|
+
// Rank THIS goal's unsatisfied criteria by lexical overlap with a candidate
|
|
554
|
+
// task, so a create flow can SUGGEST which done-when criterion to link (the
|
|
555
|
+
// author confirms/overrides — nothing is auto-linked). Read-only + advisory;
|
|
556
|
+
// rank: any-builder (same as GET /goals/:id).
|
|
557
|
+
router.get('/goals/:id/suggest-criteria', auth.requireBuilder, async (req, res) => {
|
|
558
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
559
|
+
if (id === null) return;
|
|
560
|
+
try {
|
|
561
|
+
const goal = await db.getGoal(id);
|
|
562
|
+
if (!goal) return res.fail('goal_not_found', 404);
|
|
563
|
+
const criteria = await db.listCriteriaForGoal(id);
|
|
564
|
+
const limit = Math.min(10, Math.max(1, Number(req.query.limit) || 3));
|
|
565
|
+
const suggestions = suggestCriteria(
|
|
566
|
+
criteria,
|
|
567
|
+
{ title: req.query.title, description: req.query.description },
|
|
568
|
+
{ limit }
|
|
569
|
+
);
|
|
570
|
+
return res.json({ goal_id: goal.id, suggestions });
|
|
571
|
+
} catch (err) {
|
|
572
|
+
console.error('[gds] GET /goals/:id/suggest-criteria', err);
|
|
573
|
+
return res.fail('fetch_failed', { status: 500, message: 'internal error' });
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
551
577
|
// POST /goals/:id/join — join an open goal.
|
|
552
578
|
// rank: any-builder — open join for a NORMAL goal. A PROTECTED-scope goal is
|
|
553
579
|
// Archon-mediated: a non-Archon cannot self-join (an Archon admits them via
|
|
@@ -7,6 +7,7 @@ const api = require('../../../src/module-api');
|
|
|
7
7
|
const auth = api;
|
|
8
8
|
const db = require('../db');
|
|
9
9
|
const hierarchy = require('../hierarchy');
|
|
10
|
+
const { suggestCriteria } = require('../criterion-suggest');
|
|
10
11
|
// grading carved to modules/grading/ (BV1.R73 / ADR 0091 §4): the lifecycle no
|
|
11
12
|
// longer imports the grader — it resolves the `grade` kernel PORT at grade time
|
|
12
13
|
// (seams.resolveOptional('grade'), in POST /tasks/:id/grade below).
|
|
@@ -689,11 +690,31 @@ module.exports = function buildTasksRouter() {
|
|
|
689
690
|
// task 1957: credits_reward is an ESTIMATE, not a guaranteed payout. The hall
|
|
690
691
|
// renders it as "<Currency> Est." until the task settles (confirmed/shipped),
|
|
691
692
|
// then switches to the real ledger figure (credits_awarded from the task read).
|
|
693
|
+
// task 2165 — the task landed under a goal but the caller linked no
|
|
694
|
+
// criterion → SUGGEST the most likely done-when criterion(s) for the author
|
|
695
|
+
// to confirm (they link via POST /tasks/:id/criteria). Advisory only, never
|
|
696
|
+
// auto-linked ("suggest, human picks"). Best-effort: a suggestion hiccup
|
|
697
|
+
// must not fail an otherwise-successful create. The general catch-all goal
|
|
698
|
+
// usually has no criteria, so this is naturally empty there.
|
|
699
|
+
let suggested_criteria = [];
|
|
700
|
+
if (criteria.length === 0 && task.goal_id != null) {
|
|
701
|
+
try {
|
|
702
|
+
const goalCriteria = await db.listCriteriaForGoal(task.goal_id);
|
|
703
|
+
suggested_criteria = suggestCriteria(
|
|
704
|
+
goalCriteria,
|
|
705
|
+
{ title: body.title, description: body.description },
|
|
706
|
+
{ limit: 3 }
|
|
707
|
+
);
|
|
708
|
+
} catch (e) {
|
|
709
|
+
console.error('[gds] POST /tasks suggest-criteria (non-blocking)', e && e.message);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
692
712
|
res.status(201).json({
|
|
693
713
|
task,
|
|
694
714
|
credits_reward: task.credits_reward,
|
|
695
715
|
needs_migration: needsMigration,
|
|
696
716
|
criteria,
|
|
717
|
+
suggested_criteria,
|
|
697
718
|
});
|
|
698
719
|
} catch (err) {
|
|
699
720
|
console.error('[gds] POST /tasks', err);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @bongos/core — installed platform dependency (not your project's CLAUDE.md)
|
|
2
|
+
|
|
3
|
+
This file is a placeholder shipped inside the **@bongos/core** package. The core's own
|
|
4
|
+
internal agent notes are intentionally omitted from the published artifact so they cannot
|
|
5
|
+
load into your instance's context.
|
|
6
|
+
|
|
7
|
+
**It does not govern this repository.** Your own root `CLAUDE.md`, your `.claude/`
|
|
8
|
+
directory, and the live Bongos database are the source of truth for how work happens here
|
|
9
|
+
and who may do what.
|
|
10
|
+
|
|
11
|
+
You are seeing this only because an agent opened a file under `node_modules/@bongos/core/`.
|
|
12
|
+
Nothing in here needs to be read to use the platform — run `bongos help` for commands.
|
|
13
|
+
|
|
14
|
+
_To work ON the platform core itself, use the standalone @bongos/core source repository,
|
|
15
|
+
not this vendored copy._
|