@bongos/core 1.20.80 → 1.20.82
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 +98 -53
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +12 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/api/openapi.json +245 -4
- package/docs/api-reference.md +7 -3
- package/docs/copy-inventory.md +84 -71
- package/docs/copy-registry.json +271 -145
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +2 -1
- package/docs/page-readings.json +139 -119
- package/modules/autonomy/cadence.js +3 -0
- package/modules/autonomy/db.js +136 -0
- package/modules/autonomy/fence.js +88 -3
- package/modules/autonomy/migrations/autonomy_005_builder_scope.sql +55 -0
- package/modules/autonomy/routes/autonomy.js +123 -3
- package/modules/economy/earnings.js +117 -0
- package/modules/economy/module.json +1 -1
- package/modules/economy/routes/earnings.js +47 -0
- package/modules/government/catalog.js +9 -1
- package/modules/government/migrations/government_022_autonomy_run.sql +38 -0
- package/modules/hall-ui/public/gate.css +49 -0
- package/modules/hall-ui/public/gate.html +8 -7
- package/modules/hall-ui/public/gate.js +272 -115
- package/modules/hall-ui/public/profile-charts.js +206 -104
- package/modules/hall-ui/public/profile.css +23 -0
- package/modules/hall-ui/public/profile.js +3 -3
- package/modules/hall-ui/public/profile.states.json +8 -4
- package/modules/hall-ui/public/settings-autobongos.js +156 -0
- package/modules/hall-ui/public/settings.html +19 -0
- package/modules/hall-ui/public/settings.js +1 -0
- package/modules/hall-ui/records/profile-roles.md +21 -0
- package/modules/lifecycle/db-goals.js +6 -0
- package/modules/lifecycle/db-versions.js +3 -0
- package/modules/lifecycle/done-when.js +4 -0
- package/modules/lifecycle/goal-close-sweep.js +34 -0
- package/modules/provisioning/app-status.js +17 -4
- package/modules/public-landing/public/projects.html +7 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +28 -0
- package/scripts/hall-preview/server.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/auto_satisfy_criteria.mjs +8 -6
- package/tests/autonomy_builder_scope.mjs +474 -0
- package/tests/autonomy_fence_priority.mjs +7 -3
- package/tests/autonomy_runner_identity.mjs +1 -1
- package/tests/criterion_goal_attach.mjs +5 -1
- package/tests/goal_close_cancels_requests.mjs +82 -0
- package/tests/goal_closure_invariants.mjs +5 -2
- package/tests/hall_profile_world.mjs +57 -58
- package/tests/profile_earnings.mjs +237 -0
- package/tests/projects_hub_app_status.mjs +17 -0
- package/tests/provisioning_app_status.mjs +25 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// modules/hall-ui/public/settings-autobongos.js — the "Your Autobongos runner"
|
|
2
|
+
// panel on /builders/settings (task 1004501).
|
|
3
|
+
//
|
|
4
|
+
// WHY THE PANEL EXISTS. The fence used to be one switch and one goal list for
|
|
5
|
+
// every runner on the instance. Owner ruling, 2026-10-01: each builder says which
|
|
6
|
+
// work THEIR runner takes, and a builder who has said nothing gets no autonomous
|
|
7
|
+
// work. This is where they say it.
|
|
8
|
+
//
|
|
9
|
+
// WHAT IT READS. GET /autonomy/fence answers with the caller's EFFECTIVE fence: the
|
|
10
|
+
// project allowlist narrowed to their picks, plus `scope` (what they picked, whether
|
|
11
|
+
// they may run a runner, whether the owner paused them) and `project` (the
|
|
12
|
+
// unnarrowed allowlist they choose from). One read, so the panel cannot disagree
|
|
13
|
+
// with what the builder's runner is told. The server decides every one of those
|
|
14
|
+
// facts; this page only draws them.
|
|
15
|
+
//
|
|
16
|
+
// WHAT IT WRITES. PUT /autonomy/me/goals with the whole list, on Save. The route
|
|
17
|
+
// refuses a goal that is not allowlisted (409), and it is behind `autonomy.run`, so
|
|
18
|
+
// a builder who may not run a runner is told so here instead of shown a form that
|
|
19
|
+
// would fail.
|
|
20
|
+
//
|
|
21
|
+
// Its own file on the settings-interaction.js precedent: settings.js sits against
|
|
22
|
+
// the 1500-line ratchet, and this panel shares nothing with the rest of the page.
|
|
23
|
+
|
|
24
|
+
(() => {
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const API = '/api/bongos';
|
|
28
|
+
const { escapeHtml, $ } = window.OTB;
|
|
29
|
+
const { emptyStateHtml } = window.OTBKit;
|
|
30
|
+
const toast = (msg) => window.OTB.toast(msg);
|
|
31
|
+
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
32
|
+
|
|
33
|
+
const toAuth = () => window.location.assign(`${API}/auth/web/start`);
|
|
34
|
+
|
|
35
|
+
// The lines above the form: each one says why the runner might be idle, in the
|
|
36
|
+
// order the server checks them, so the first line a builder reads is the reason.
|
|
37
|
+
function noticesHtml(fence) {
|
|
38
|
+
const project = fence.project || {};
|
|
39
|
+
const scope = fence.scope || {};
|
|
40
|
+
const out = [];
|
|
41
|
+
if (project.enabled !== true) {
|
|
42
|
+
out.push(`The project's runner switch is off right now${project.paused_reason ? `: ${escapeHtml(project.paused_reason)}` : ''}. Your choices are kept and apply when it is back on.`);
|
|
43
|
+
}
|
|
44
|
+
if (scope.owner_paused) {
|
|
45
|
+
out.push(`The owner paused your runner${scope.owner_paused_reason ? `: ${escapeHtml(scope.owner_paused_reason)}` : ''}. Only the owner can resume it.`);
|
|
46
|
+
}
|
|
47
|
+
return out.map((t) => `<p class="prow__hint">${t}</p>`).join('');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Goal names, by id. The fence carries only ids and the owner's note, so the
|
|
51
|
+
// names come from GET /goals — the list the Goals page draws for this same
|
|
52
|
+
// builder, so it names only goals they can already see. Best-effort: a failed
|
|
53
|
+
// read leaves the map empty and a row falls back to its number.
|
|
54
|
+
let goalNames = new Map();
|
|
55
|
+
async function loadGoalNames() {
|
|
56
|
+
try {
|
|
57
|
+
const res = await api.request('GET', `${API}/goals`);
|
|
58
|
+
if (res.ok && res.data && Array.isArray(res.data.goals)) {
|
|
59
|
+
goalNames = new Map(res.data.goals.map((g) => [Number(g.id), g.title]));
|
|
60
|
+
}
|
|
61
|
+
} catch (_) { /* names are a nicety; the picker works without them */ }
|
|
62
|
+
}
|
|
63
|
+
const goalLabel = (id) => goalNames.get(Number(id)) || `Goal ${id}`;
|
|
64
|
+
|
|
65
|
+
function render(fence) {
|
|
66
|
+
const body = $('#autobongos-body');
|
|
67
|
+
if (!body) return;
|
|
68
|
+
const scope = fence.scope || {};
|
|
69
|
+
if (scope.may_run !== true) {
|
|
70
|
+
body.innerHTML = emptyStateHtml('Your rank cannot run an Autobongos runner on this project. Ask the owner if you need one.');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const goals = (fence.project && Array.isArray(fence.project.goals)) ? fence.project.goals : [];
|
|
74
|
+
if (!goals.length) {
|
|
75
|
+
body.innerHTML = noticesHtml(fence) + emptyStateHtml('The project has not allowed any goals for runners yet, so there is nothing to choose from.');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const picked = new Set((scope.goals || []).map(Number));
|
|
79
|
+
body.innerHTML = `${noticesHtml(fence)}
|
|
80
|
+
<form id="autobongos-form">
|
|
81
|
+
<div class="prows">
|
|
82
|
+
${goals.map((g) => {
|
|
83
|
+
const id = `autobongos-goal-${escapeHtml(String(g.goal_id))}`;
|
|
84
|
+
return `<label class="prow prow--pick" for="${id}">
|
|
85
|
+
<input type="checkbox" id="${id}" name="autobongos-goal" value="${escapeHtml(String(g.goal_id))}"${picked.has(Number(g.goal_id)) ? ' checked' : ''}>
|
|
86
|
+
<span class="prow__main">
|
|
87
|
+
<span class="prow__label">${escapeHtml(goalLabel(g.goal_id))}</span>
|
|
88
|
+
<span class="prow__desc">${escapeHtml([goalNames.has(Number(g.goal_id)) ? `goal ${g.goal_id}` : '', g.note || ''].filter(Boolean).join(' · '))}</span>
|
|
89
|
+
</span>
|
|
90
|
+
</label>`;
|
|
91
|
+
}).join('')}
|
|
92
|
+
</div>
|
|
93
|
+
<div class="settings-card">
|
|
94
|
+
<button type="submit" class="settings-btn" id="autobongos-save">Save</button>
|
|
95
|
+
<span class="settings-card__hint">${picked.size ? `Your runner works ${picked.size} goal${picked.size === 1 ? '' : 's'}.` : 'Your runner takes no work until you choose at least one goal.'}</span>
|
|
96
|
+
</div>
|
|
97
|
+
</form>`;
|
|
98
|
+
$('#autobongos-form').addEventListener('submit', (e) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
const ids = [...e.target.querySelectorAll('input[name="autobongos-goal"]:checked')].map((i) => Number(i.value));
|
|
101
|
+
save(ids);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function load() {
|
|
106
|
+
const res = await api.request('GET', `${API}/autonomy/fence`);
|
|
107
|
+
if (res.status === 401) { toAuth(); return null; }
|
|
108
|
+
if (!res.ok) throw new Error(`fence GET → ${res.status}`);
|
|
109
|
+
return res.data;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function save(goalIds) {
|
|
113
|
+
const status = $('#autobongos-status');
|
|
114
|
+
const btn = $('#autobongos-save');
|
|
115
|
+
if (status) status.textContent = 'Saving…';
|
|
116
|
+
if (btn) btn.disabled = true;
|
|
117
|
+
try {
|
|
118
|
+
const res = await api.request('PUT', `${API}/autonomy/me/goals`, { body: { goal_ids: goalIds } });
|
|
119
|
+
if (res.status === 401) { toAuth(); return; }
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
const env = res.data && res.data.error;
|
|
122
|
+
throw new Error((env && env.message) || `could not save (${res.status})`);
|
|
123
|
+
}
|
|
124
|
+
const msg = goalIds.length ? `Saved — your runner works ${goalIds.length} goal${goalIds.length === 1 ? '' : 's'}.` : 'Saved — your runner takes no work.';
|
|
125
|
+
if (status) status.textContent = msg;
|
|
126
|
+
toast(msg);
|
|
127
|
+
const fence = await load();
|
|
128
|
+
if (fence) render(fence);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error('[settings] save autobongos goals failed', err);
|
|
131
|
+
if (status) status.textContent = `Could not save: ${err.message}`;
|
|
132
|
+
toast('Could not save your runner’s goals. Try again.');
|
|
133
|
+
if (btn) btn.disabled = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function setup() {
|
|
138
|
+
const section = $('#autobongos-scroll');
|
|
139
|
+
if (!section) return;
|
|
140
|
+
try {
|
|
141
|
+
const [fence] = await Promise.all([load(), loadGoalNames()]);
|
|
142
|
+
if (!fence) return;
|
|
143
|
+
render(fence);
|
|
144
|
+
section.hidden = false;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
// Its own try/catch, like every other panel: a failing read shows this
|
|
147
|
+
// panel's own error rather than taking the settings page down with it.
|
|
148
|
+
console.error('[settings] failed to load autobongos scope', err);
|
|
149
|
+
section.hidden = false;
|
|
150
|
+
$('#autobongos-body').innerHTML =
|
|
151
|
+
'<p class="voices-empty">Your runner settings could not be loaded. Reload the page to try again.</p>';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setup();
|
|
156
|
+
})();
|
|
@@ -163,6 +163,23 @@
|
|
|
163
163
|
<p class="section-note">Ending a session takes effect immediately; the token stops working on its very next request.</p>
|
|
164
164
|
</section>
|
|
165
165
|
|
|
166
|
+
<!-- YOUR AUTOBONGOS RUNNER — which goals your own unattended runner
|
|
167
|
+
works (task 1004501). Rendered by settings-autobongos.js against
|
|
168
|
+
GET /autonomy/fence (your effective fence) and PUT
|
|
169
|
+
/autonomy/me/goals. Picking nothing means your runner takes no
|
|
170
|
+
work. The project allowlist and the master switch stay the
|
|
171
|
+
owner's, on the Gate page. -->
|
|
172
|
+
<section class="scroll" id="autobongos-scroll" aria-labelledby="autobongos-h" data-group="access" hidden>
|
|
173
|
+
<h2 id="autobongos-h" class="scroll__h">Your Autobongos runner</h2>
|
|
174
|
+
<p class="scroll__lede">
|
|
175
|
+
Choose which goals your own runner works while you are away. You can
|
|
176
|
+
only choose from the goals the project allows. If you choose none, your
|
|
177
|
+
runner takes no work.
|
|
178
|
+
</p>
|
|
179
|
+
<div id="autobongos-body" aria-live="polite"></div>
|
|
180
|
+
<p class="settings-status" id="autobongos-status" role="status" aria-live="polite"></p>
|
|
181
|
+
</section>
|
|
182
|
+
|
|
166
183
|
<!-- IMAGE KEY — per-builder Gemini key (task 1013 / ADR 0073).
|
|
167
184
|
GET/PUT/DELETE /api/bongos/me/art-key/own. Write-only (set/replace/
|
|
168
185
|
remove); the full key is never shown again after save (masked,
|
|
@@ -425,5 +442,7 @@
|
|
|
425
442
|
<script src="/builders/settings-render.js?v=2026-09-16-render-levels"></script>
|
|
426
443
|
<!-- task 1004296: Settings → Software update. Own file on the same precedent. -->
|
|
427
444
|
<script src="/builders/settings-software-update.js?v=2026-09-26-update"></script>
|
|
445
|
+
<!-- task 1004501: Settings → Your Autobongos runner. Own file on the same precedent. -->
|
|
446
|
+
<script src="/builders/settings-autobongos.js?v=2026-10-01-autobongos"></script>
|
|
428
447
|
</body>
|
|
429
448
|
</html>
|
|
@@ -1066,6 +1066,7 @@
|
|
|
1066
1066
|
'privacy': 'account-hub-scroll',
|
|
1067
1067
|
'disciplines': 'craft-scroll',
|
|
1068
1068
|
'cli': 'cli-scroll',
|
|
1069
|
+
'autobongos': 'autobongos-scroll',
|
|
1069
1070
|
'art-key': 'art-key',
|
|
1070
1071
|
'interaction': 'interaction-scroll',
|
|
1071
1072
|
'wandering': 'wandering-scroll',
|
|
@@ -48,3 +48,24 @@ Signed out, the section is one sign-in line (owner decision (a)).
|
|
|
48
48
|
- **The whole after line is highlighted**, where the mock highlights only the changed words: the batch stores whole lines, not the changed span.
|
|
49
49
|
- **The artist tab's lede does not say what each page paid**: the pay per page is not in the read.
|
|
50
50
|
- **The trail's window** is the span of the person's own thoughts (at least a month), with a line at each month, rather than the mock's fixed three months.
|
|
51
|
+
|
|
52
|
+
## Earnings for everyone, labelled by kind of credit (task 1004436, WA6.RP06)
|
|
53
|
+
|
|
54
|
+
The Overview's charts used to stop at "No works shipped yet" before they read a credit, so an artist or an ideator with a full credit history got no chart at all. Now the **earnings figure** leads the "Over time" grid for anyone who has earned, shipped task or not: the mock's "Earnings over time", a sentence about the person, the kit's cumulative step line, and beside it "where it came from", one bar per kind of credit in the mock's words, largest first.
|
|
55
|
+
|
|
56
|
+
- **The read is `GET /builders/:builderId/earnings`** (economy, `modules/economy/earnings.js` + `routes/earnings.js`): the ledger summed per UTC day and per kind, for any signed-in viewer and any builder, own profile included. It carries sums only, so the itemised ledger (`/builders/:builderId/credits`) stays behind `builder.roster.read`; the chart no longer reads it, and the estimate a refused viewer used to get (summed from shipped tasks, which is nothing for these two crafts) is gone.
|
|
57
|
+
- **The kinds are economy's** (`KINDS`), the words the page's (`KIND_WORDS` in `profile-charts.js`). A descent payment shares stream 1's reason key, so it is told apart by the descent writer's own description ("N hops below idea #"); `tests/profile_earnings.mjs` holds that phrase against the writer.
|
|
58
|
+
- **The kinds follow the lookback window**, as every other figure does; the line still enters the window at its running total and ends at the balance.
|
|
59
|
+
- **The works charts still need a shipped task**, and without one they are a single line under the figure ("No works shipped yet ..."), not two empty cards. Someone with neither credits nor works gets one line for the whole section.
|
|
60
|
+
- **On a phone** the figure stacks: the line, then "where it came from". The plot's viewBox is half again a card's on a wide screen so its type matches the cards', and the kit's own on a phone (read at draw time, at the same 720px the grid stacks).
|
|
61
|
+
|
|
62
|
+
Harness: `builders__earnings.json` serves any id; the artist (2001) and the ideator (2002) carry their own earnings beside an empty `builders__<id>__activity.json`, and the `artist`/`ideator` states expect `#chart-drachmae` and `.pearn__kinds`.
|
|
63
|
+
|
|
64
|
+
### Deliberate differences from the mock (RP06)
|
|
65
|
+
|
|
66
|
+
- **No "Spark splits" row.** A spark author's 30% share is counted under the stream that paid it (shipped work, a Full Idea pass, the board), and the task's own list of kinds does not name it; a separate row would count the same credits under two words.
|
|
67
|
+
- **No "historical estimate" foot.** The figure is only what the ledger paid. An idea's historical estimate is part of its SIZE (By the numbers says so with its ~ mark), never money booked, so the earnings line has none to include.
|
|
68
|
+
- **Two kinds the mock does not draw:** "Simplification bonuses" (the net-negative ship bonus, which the task names) and "Other credits" (achievements, a hand grant), so the bars always add up to the balance.
|
|
69
|
+
- **The live chart chrome stays:** the crosshair readout line above the plot, the "as a table" twin and the kit's end ticks (the first day and "today", no "0" label), rather than the mock's "8 weeks ago / this week". No number is reachable only through the picture.
|
|
70
|
+
- **It sits in "Over time" under the lookback control**, so the kinds follow the window rather than the mock's fixed eight weeks.
|
|
71
|
+
- **The sentence uses the person's full display name**, not a first name the hall does not have, and for someone who never shipped a task it names where their credits came from ("approved pages and ideas"), where the mock's artist sample had the general sentence.
|
|
@@ -18,6 +18,8 @@ const { nonTerminalSql, isTerminalStatus } = require('./task-status');
|
|
|
18
18
|
const { maintenanceGoalTitle } = require('./goal-advisory.js');
|
|
19
19
|
const { SMOKE_NOT_LIKE } = require('./db-shared.js');
|
|
20
20
|
const { executorFor, inTransaction } = require('./db-executor.js');
|
|
21
|
+
// Task 1003660 — closing a goal cancels its pending invites + join requests.
|
|
22
|
+
const { cancelPendingMembershipRequests } = require('./goal-close-sweep.js');
|
|
21
23
|
// BV1.R16 (task 1003603) — auto-close, shared with the done-when cascade so the
|
|
22
24
|
// two achievement writers cannot drift apart (ADR 0263 §4).
|
|
23
25
|
const { autoCloseVersionsForGoals } = require('./version-autoclose.js');
|
|
@@ -282,6 +284,7 @@ async function carryBugsForward({ fromVersionId, toVersionId }, deps = {}) {
|
|
|
282
284
|
// never "done", it stopped being the live queue. Leaving it open on a shipped
|
|
283
285
|
// version is the mirror of the defect this whole goal exists to remove.
|
|
284
286
|
await exec.query(`UPDATE goals SET status = 'archived', updated_at = now() WHERE id = $1`, [from[0].id]);
|
|
287
|
+
await cancelPendingMembershipRequests(exec, from[0].id);
|
|
285
288
|
return { fromGoalId: String(from[0].id), toGoalId: String(to.id), moved: moved.length, reason: null };
|
|
286
289
|
}
|
|
287
290
|
|
|
@@ -493,6 +496,8 @@ async function setGoalStatus({ goalId, status }, deps = {}) {
|
|
|
493
496
|
RETURNING ${GOAL_COLS}`,
|
|
494
497
|
[goalId, status]
|
|
495
498
|
);
|
|
499
|
+
// A goal leaving `open` takes no new members, so its pending asks are dead.
|
|
500
|
+
if (rows[0] && status !== 'open') await cancelPendingMembershipRequests(activePool, goalId);
|
|
496
501
|
return rows[0] ?? null;
|
|
497
502
|
}
|
|
498
503
|
|
|
@@ -797,6 +802,7 @@ async function archiveGoalWithDispositions({ goalId, plan = [], reason }, deps =
|
|
|
797
802
|
`UPDATE goals SET status = 'archived', updated_at = now() WHERE id = $1 RETURNING ${GOAL_COLS}`,
|
|
798
803
|
[goalId]
|
|
799
804
|
);
|
|
805
|
+
await cancelPendingMembershipRequests(client, goalId);
|
|
800
806
|
await client.query('COMMIT');
|
|
801
807
|
return { goal: archived[0], applied, alreadyArchived: false };
|
|
802
808
|
} catch (err) {
|
|
@@ -20,6 +20,7 @@ const { executorFor, inTransaction } = require('./db-executor.js');
|
|
|
20
20
|
// (fitness pins the count). createVersion needs it to seed a version's FIRST goal
|
|
21
21
|
// in the same transaction — task 1003701.
|
|
22
22
|
const { createGoal } = require('./db-goals.js');
|
|
23
|
+
const { cancelPendingMembershipRequests } = require('./goal-close-sweep.js');
|
|
23
24
|
|
|
24
25
|
async function listVersions() {
|
|
25
26
|
const { rows } = await pool.query(
|
|
@@ -405,6 +406,7 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
|
|
|
405
406
|
[step.goalId, `Abandoned: ${reason}`]
|
|
406
407
|
);
|
|
407
408
|
await client.query(`UPDATE goals SET status = 'archived', updated_at = now() WHERE id = $1`, [step.goalId]);
|
|
409
|
+
await cancelPendingMembershipRequests(client, step.goalId);
|
|
408
410
|
applied.push({ goal_id: String(step.goalId), verb: 'abandon', result: 'archived', tasks_abandoned: killed.length });
|
|
409
411
|
} else {
|
|
410
412
|
// roll_forward (BV1.R17, task 1003604, ADR 0263 §6): the goal survives the
|
|
@@ -496,6 +498,7 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
|
|
|
496
498
|
// on the way out rather than being special-cased around it (the brief's
|
|
497
499
|
// explicit ask).
|
|
498
500
|
await client.query(`UPDATE goals SET status = 'archived', updated_at = now() WHERE id = $1`, [step.goalId]);
|
|
501
|
+
await cancelPendingMembershipRequests(client, step.goalId);
|
|
499
502
|
|
|
500
503
|
applied.push({
|
|
501
504
|
goal_id: String(step.goalId), verb: 'roll_forward', result: 'succeeded',
|
|
@@ -27,6 +27,7 @@ const { pool } = require('../../src/module-api');
|
|
|
27
27
|
const { nonTerminalSql } = require('./task-status');
|
|
28
28
|
const { SMOKE_NOT_LIKE } = require('./db-shared.js');
|
|
29
29
|
const { executorFor } = require('./db-executor.js');
|
|
30
|
+
const { cancelPendingMembershipRequests } = require('./goal-close-sweep.js');
|
|
30
31
|
// BV1.R16 (task 1003603) — auto-close. Dependency-free module, so requiring it
|
|
31
32
|
// here creates no cycle (db-versions.js requires THIS file, which is why the
|
|
32
33
|
// rule could not live beside closeVersion).
|
|
@@ -565,6 +566,9 @@ async function autoSatisfyShippedCriteria(exec, { taskId = null, criterionId = n
|
|
|
565
566
|
RETURNING g.id, g.title, g.version_id, g.status`,
|
|
566
567
|
[goalIds]
|
|
567
568
|
);
|
|
569
|
+
// Task 1003660: an achieved goal takes no new members — close its pending asks
|
|
570
|
+
// on the same executor, so the cancel rides the ship transaction.
|
|
571
|
+
await cancelPendingMembershipRequests(exec, goals.map((g) => g.id));
|
|
568
572
|
// BV1.R16 (task 1003603, ADR 0263 §§3–4): a version closes ITSELF once its last
|
|
569
573
|
// non-maintenance goal achieves. On the caller's `exec`, so it sees the flips
|
|
570
574
|
// above that have not committed yet — a check on the pool would never close the
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// goal-close-sweep.js — closing a goal cancels its outstanding invites + join
|
|
3
|
+
// requests (task 1003660, from idea 1000787).
|
|
4
|
+
//
|
|
5
|
+
// Task 1002887 added the accept-time `goal_not_open` re-check, which refuses a
|
|
6
|
+
// stale invite when someone tries to accept it — but until then the row kept
|
|
7
|
+
// listing in the invitee's inbox as something they could act on. A goal that has
|
|
8
|
+
// left `open` (achieved or archived) can take no new members, so its pending rows
|
|
9
|
+
// are dead; this closes them at the moment the goal closes instead.
|
|
10
|
+
//
|
|
11
|
+
// BOTH kinds ('invite' and 'request') are cancelled, with the same 'cancelled'
|
|
12
|
+
// terminal state addGoalMember uses (core_188: "withdrawn/rescinded"). 'rejected'
|
|
13
|
+
// would stamp a decision on someone nobody made one about, and 'accepted' stays
|
|
14
|
+
// reserved for a flow that actually ran. resolved_by is left NULL: no person
|
|
15
|
+
// resolved it, the goal closing did.
|
|
16
|
+
//
|
|
17
|
+
// Takes the CALLER'S executor, never the module pool, and requires nothing — so
|
|
18
|
+
// the ship-time cascade in done-when.js (which cannot require db-goals.js without
|
|
19
|
+
// a cycle) and every archive path share one statement, and the cancel commits or
|
|
20
|
+
// rolls back with the status flip it belongs to.
|
|
21
|
+
|
|
22
|
+
async function cancelPendingMembershipRequests(exec, goalIds) {
|
|
23
|
+
const ids = (Array.isArray(goalIds) ? goalIds : [goalIds]).filter((g) => g != null).map(Number);
|
|
24
|
+
if (ids.length === 0) return 0;
|
|
25
|
+
const { rowCount } = await exec.query(
|
|
26
|
+
`UPDATE goal_membership_requests
|
|
27
|
+
SET status = 'cancelled', resolved_at = now(), updated_at = now()
|
|
28
|
+
WHERE goal_id = ANY($1::bigint[]) AND status = 'pending'`,
|
|
29
|
+
[ids]
|
|
30
|
+
);
|
|
31
|
+
return rowCount ?? 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { cancelPendingMembershipRequests };
|
|
@@ -23,27 +23,40 @@ const RENDER_URL_RE = /^https:\/\/[a-z0-9-]{1,63}\.onrender\.com\/?$/;
|
|
|
23
23
|
|
|
24
24
|
// The stages the owner sees, in order. `failed` is not a stage: it is the setup stopping.
|
|
25
25
|
const STAGES = ['creating', 'building', 'live'];
|
|
26
|
+
|
|
27
|
+
// How long a build may go unanswered before the owner stops being told it is building
|
|
28
|
+
// (task 1004502). Render can give up on a deploy — project 43's timed out in 15 minutes —
|
|
29
|
+
// and with the key gone (ADR 0327) the platform cannot read that verdict. So past this the
|
|
30
|
+
// stage is `quiet`: not a failure (we cannot see one), but no longer a promise, with the
|
|
31
|
+
// way to Render's own deploy history. The poller keeps knocking, so a late answer still
|
|
32
|
+
// turns it `live`. Measured from the runner's last write (`updatedAt`, provision-render.js),
|
|
33
|
+
// which is when `deploying` or `linked` began — the poller's `live` write never touches it.
|
|
34
|
+
const QUIET_AFTER_MS = 20 * 60_000;
|
|
26
35
|
const STAGE_OF = { requested: 'creating', creating: 'creating', deploying: 'building', linked: 'building', live: 'live' };
|
|
27
36
|
|
|
28
37
|
function serviceLinks(serviceId) {
|
|
29
38
|
if (!SERVICE_ID_RE.test(String(serviceId || ''))) return null;
|
|
30
39
|
const base = `${DASHBOARD}/web/${serviceId}`;
|
|
31
|
-
return { service: base, env: `${base}/env` };
|
|
40
|
+
return { service: base, env: `${base}/env`, deploys: `${base}/events` };
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
function appUrl(url) { return RENDER_URL_RE.test(String(url || '')) ? String(url).replace(/\/+$/, '') : null; }
|
|
35
44
|
|
|
36
45
|
// The owner's view of app_host: the stage, the address, the links, the failure sentence,
|
|
37
46
|
// and the answers a retry re-sends (never the key — there is none to send).
|
|
38
|
-
function appStatus(appHost, { keyWaiting = false } = {}) {
|
|
47
|
+
function appStatus(appHost, { keyWaiting = false, now = Date.now() } = {}) {
|
|
39
48
|
const h = appHost || null;
|
|
40
49
|
if (!h || h.platform !== 'render') return null;
|
|
41
50
|
if (h.state === 'disconnected') return { platform: 'render', state: 'disconnected', stage: null };
|
|
42
51
|
const failed = h.state === 'failed';
|
|
52
|
+
const stage = failed ? null : (STAGE_OF[h.state] || 'creating');
|
|
53
|
+
const since = Date.parse(h.updatedAt || '');
|
|
54
|
+
const quiet = stage === 'building' && Number.isFinite(since) && now - since > QUIET_AFTER_MS;
|
|
43
55
|
return {
|
|
44
56
|
platform: 'render',
|
|
45
57
|
state: h.state || 'requested',
|
|
46
|
-
stage:
|
|
58
|
+
stage: quiet ? 'quiet' : stage,
|
|
59
|
+
quiet,
|
|
47
60
|
// an intent that still holds the lent key has not run yet, so the owner waits rather
|
|
48
61
|
// than retries
|
|
49
62
|
waiting: !!keyWaiting,
|
|
@@ -80,4 +93,4 @@ function retryAnswers(h) {
|
|
|
80
93
|
return r;
|
|
81
94
|
}
|
|
82
95
|
|
|
83
|
-
module.exports = { appStatus, appUrl, serviceLinks, oneSentence, retryAnswers, STAGES, RENDER_URL_RE, SERVICE_ID_RE };
|
|
96
|
+
module.exports = { appStatus, appUrl, serviceLinks, oneSentence, retryAnswers, STAGES, QUIET_AFTER_MS, RENDER_URL_RE, SERVICE_ID_RE };
|
|
@@ -2333,6 +2333,13 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2333
2333
|
v.hint.setAttribute('role', 'status');
|
|
2334
2334
|
v.retryForm.appendChild(v.hint);
|
|
2335
2335
|
v.root.appendChild(v.retryForm);
|
|
2336
|
+
} else if (app.stage === 'quiet') {
|
|
2337
|
+
/* Past the time box (task 1004502): Render may have given up, and with the key gone we
|
|
2338
|
+
cannot see its verdict — so no stage is claimed, the owner is sent to Render's own
|
|
2339
|
+
deploy history, and the watch keeps asking in case the app still comes up. */
|
|
2340
|
+
v.root.appendChild(appPara(['Your app hasn’t answered for over 20 minutes. Render may have stopped the deploy — its deploy history says why.']));
|
|
2341
|
+
if (app.links && app.links.deploys) v.root.appendChild(appPara([appLink(app.links.deploys, 'See its deploys on Render')]));
|
|
2342
|
+
v.root.appendChild(appPara(['If it starts answering, this changes to Live by itself.']));
|
|
2336
2343
|
} else {
|
|
2337
2344
|
var at = APP_STAGES.map(function (s) { return s[0]; }).indexOf(app.stage);
|
|
2338
2345
|
var ol = appEl('ol', 'appStages');
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.82",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.82",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.82",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8625,5 +8625,33 @@
|
|
|
8625
8625
|
"id": "1003907",
|
|
8626
8626
|
"text": "Autobongos has now been proven against the live project: it shipped work by itself with nobody watching, refused every kind of work it is not allowed to touch, and recovered from restarts and network failures on its own. The o"
|
|
8627
8627
|
}
|
|
8628
|
+
],
|
|
8629
|
+
"1.20.81": [
|
|
8630
|
+
{
|
|
8631
|
+
"id": "1004501",
|
|
8632
|
+
"text": "Each builder now chooses which goals their own Autobongos runner works, in Settings, from the goals the project allows. A builder who chooses nothing gets no unattended work. Who may run a runner at all is now a project permis"
|
|
8633
|
+
}
|
|
8634
|
+
],
|
|
8635
|
+
"1.20.82": [
|
|
8636
|
+
{
|
|
8637
|
+
"id": "1004502",
|
|
8638
|
+
"text": "When an app on Render never comes up, its project card no longer says 'building' forever: after 20 minutes it says the app hasn't answered and links straight to the app's deploy history on Render."
|
|
8639
|
+
},
|
|
8640
|
+
{
|
|
8641
|
+
"id": "1004452",
|
|
8642
|
+
"text": "We tested the deploy self-repair on a real throwaway project on the server. When a project has leftover unsaved changes, the platform now fixes them itself and the update goes through. When it hits something it cannot fix, it"
|
|
8643
|
+
},
|
|
8644
|
+
{
|
|
8645
|
+
"id": "1004436",
|
|
8646
|
+
"text": "Every profile now shows an earnings chart for anyone who has earned credits, even artists and ideators who have never shipped a task (before, they got no chart at all). Beside the line, a \"where it came from\" list shows each k"
|
|
8647
|
+
},
|
|
8648
|
+
{
|
|
8649
|
+
"id": "1003660",
|
|
8650
|
+
"text": "Closing a goal now clears its pending invites and join requests, so a stale invite disappears from the invitee's inbox instead of sitting there until someone tries to accept it."
|
|
8651
|
+
},
|
|
8652
|
+
{
|
|
8653
|
+
"id": "1004509",
|
|
8654
|
+
"text": "The Autobongos runner panel on the Gate page is now much lighter to read. While anything is missing it shows a short checklist saying what to do next; once everything is ready it shrinks to one status line with an on/off switc"
|
|
8655
|
+
}
|
|
8628
8656
|
]
|
|
8629
8657
|
}
|
|
@@ -118,6 +118,10 @@ const FIXTURE_FALLBACKS = [
|
|
|
118
118
|
// the profile's drachmae chart reads the credits ledger (task 1003491): one
|
|
119
119
|
// ledger fixture serves your own (/me/credits, exact match) and any other id.
|
|
120
120
|
[/^builders\/\d+\/credits$/, 'me__credits'],
|
|
121
|
+
// the profile's earnings figure reads earnings by day and kind (task 1004436):
|
|
122
|
+
// one engineer's fixture serves any id; the artist and the ideator who never
|
|
123
|
+
// shipped a task (2001, 2002) carry their own, beside an empty activity record.
|
|
124
|
+
[/^builders\/\d+\/earnings$/, 'builders__earnings'],
|
|
121
125
|
// the profile's per-craft activity log (task 1003281). One fixture per SECTION
|
|
122
126
|
// rather than per id, because the section is what changes the row shape; the
|
|
123
127
|
// craft and the page window are query string, which fixture matching ignores.
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.20.
|
|
78
|
+
const CORE_VERSION = '1.20.82'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -107,12 +107,14 @@ await test('ASYMMETRY (rollup): the /status rollup is untouched by this task', (
|
|
|
107
107
|
await test('EXEC: every write goes through the caller-supplied exec (so a ship txn is atomic)', async () => {
|
|
108
108
|
const exec = fakeExec({ rows: [CRIT] }, { rows: [{ id: 77, title: 'g', version_id: 'BONGOS-V1', status: 'achieved' }] });
|
|
109
109
|
const out = await autoSatisfyShippedCriteria(exec, { taskId: 5 });
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// that
|
|
114
|
-
|
|
115
|
-
assert.
|
|
110
|
+
// Four: the criterion update, the goal cascade, the pending-request sweep for the
|
|
111
|
+
// goals it just achieved (task 1003660), and the version auto-close (BV1.R16,
|
|
112
|
+
// task 1003603) — every one must ride the SAME exec or a rolled-back ship could
|
|
113
|
+
// leave a half-applied close (or a version that never closes on the ship that
|
|
114
|
+
// finished it).
|
|
115
|
+
assert.equal(exec.calls.length, 4, 'criterion update + goal cascade + request sweep + version auto-close');
|
|
116
|
+
assert.match(exec.calls[2].sql, /UPDATE goal_membership_requests/, 'the sweep rides exec');
|
|
117
|
+
assert.match(exec.calls[3].sql, /UPDATE versions v/, 'and the last is the auto-close, on exec');
|
|
116
118
|
assert.deepEqual(out.criteria, [CRIT]);
|
|
117
119
|
assert.deepEqual(out.goals.map((g) => g.id), [77]);
|
|
118
120
|
const src = readFileSync(new URL('../modules/lifecycle/done-when.js', import.meta.url), 'utf8');
|