@bongos/core 1.24.14 → 1.24.16
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 +308 -138
- package/.claude/skills/blocker-review/SKILL.md +2 -0
- package/.claude/skills/session-board/SKILL.md +29 -0
- package/.claude/skills/strand-fix/SKILL.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +14 -0
- package/clients/bongos-client/index.cjs +14 -0
- package/clients/bongos-client/index.d.ts +23 -2
- package/clients/bongos-client/index.mjs +14 -0
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +4 -0
- package/docs/api/openapi.json +369 -6
- package/docs/api-reference.md +19 -4
- package/docs/architecture.md +6 -0
- package/docs/copy-inventory.md +231 -204
- package/docs/copy-registry.json +488 -255
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -2
- package/docs/page-inventory.json +7 -1
- package/docs/page-readings.json +1132 -1103
- package/migrations/core_289_task_short_name.sql +34 -0
- package/modules/ai-studio/db.js +10 -0
- package/modules/ai-studio/gemini.js +73 -5
- package/modules/ai-studio/openrouter.js +17 -2
- package/modules/ai-studio/routes/ai-studio.js +7 -1
- package/modules/ai-studio/studio.js +8 -0
- package/modules/builder-settings/checkout-freshness.js +71 -0
- package/modules/builder-settings/routes/settings.js +26 -0
- package/modules/builder-settings/settings.js +2 -0
- package/modules/economy/migrations/economy_002_project_account.sql +83 -0
- package/modules/economy/module.json +1 -1
- package/modules/economy/project-account.js +166 -0
- package/modules/economy/routes/project-account.js +40 -0
- package/modules/hall-ui/public/ai-studio.css +3 -0
- package/modules/hall-ui/public/ai-studio.html +2 -1
- package/modules/hall-ui/public/ai-studio.js +72 -1
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/github-chip.js +85 -0
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/market-watch-item.html +2 -2
- package/modules/hall-ui/public/market-watch-item.js +10 -3
- package/modules/hall-ui/public/market-watch-item.states.json +41 -0
- package/modules/hall-ui/public/market-watch-lib.js +36 -5
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch-settings.html +1 -1
- package/modules/hall-ui/public/market-watch.css +5 -0
- package/modules/hall-ui/public/market-watch.html +2 -2
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/my-agents.js +3 -0
- package/modules/hall-ui/public/panel.css +5 -0
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings-age.js +65 -0
- package/modules/hall-ui/public/settings.html +27 -1
- package/modules/hall-ui/public/settings.js +1 -0
- package/modules/hall-ui/public/shell.js +22 -22
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +28 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/ideas/blockers.js +13 -7
- package/modules/lifecycle/db-claim-reads.js +1 -1
- package/modules/lifecycle/db-ship.js +2 -2
- package/modules/lifecycle/db-tasks.js +4 -4
- package/modules/lifecycle/migrations/lifecycle_019_pay_type_gates.sql +50 -0
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/pay-type-gates.js +180 -0
- package/modules/lifecycle/publish-reconciler.js +31 -1
- package/modules/lifecycle/rebase-gate.js +3 -2
- package/modules/lifecycle/routes/claims.js +3 -2
- package/modules/lifecycle/routes/pay-type-gates.js +59 -0
- package/modules/lifecycle/routes/project-settings.js +4 -0
- package/modules/lifecycle/routes/task-write-routes.js +43 -0
- package/modules/lifecycle/ship-card.js +4 -1
- package/modules/lifecycle/task-name-db.js +55 -0
- package/modules/lifecycle/task-name.js +140 -0
- package/modules/market-watch/db.js +10 -1
- package/modules/market-watch/migrations/market_watch_004_entry_corrections.sql +20 -0
- package/modules/market-watch/pages.js +20 -1
- package/modules/market-watch/proposals.js +57 -7
- package/modules/market-watch/routes/pages.js +2 -0
- package/modules/market-watch/routes/rules.js +4 -0
- package/modules/platform-identity/terms-acceptance.js +9 -9
- package/modules/public-landing/public/account-google.states.json +14 -0
- package/modules/public-landing/public/assets/cosmos.css +12 -0
- package/modules/public-landing/public/assets/hub-bar.js +44 -1
- package/modules/public-landing/public/privacy.html +5 -1
- package/modules/public-landing/public/projects-google.states.json +14 -0
- package/modules/public-landing/public/projects.html +39 -0
- package/modules/public-landing/public/terms.html +12 -2
- package/modules/ui-design/kit/fixtures/me-google.json +10 -0
- package/modules/ui-design/kit/fixtures/me-sign-in-methods-google.json +4 -0
- package/modules/ui-design/kit/serve.js +7 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +55 -1
- package/scripts/bongos/claim.js +4 -2
- package/scripts/bongos/land-watch.js +1 -0
- package/scripts/bongos/report-checkout.js +30 -0
- package/scripts/bongos/run-unit-tests.js +11 -0
- package/scripts/bongos/sessions-board.js +264 -0
- package/scripts/bongos/ship-finish.js +1 -0
- package/scripts/bongos/ship-flow.js +5 -2
- package/scripts/bongos/strand-lease.js +5 -3
- package/scripts/bongos/strand-watch.js +38 -7
- package/scripts/gds/report-checkout.js +7 -0
- package/scripts/gds/sessions-board.js +7 -0
- package/src/bongos/agent-standing-rules.js +13 -3
- package/src/bongos/routes/me.js +7 -1
- package/src/module-api.js +1 -1
- package/tests/agent_standing_rules.mjs +18 -1
- package/tests/ai_studio_billing.mjs +13 -0
- package/tests/ai_studio_gemini_models.mjs +81 -0
- package/tests/ai_studio_requests.mjs +24 -0
- package/tests/blocker_detail_hydration.mjs +5 -3
- package/tests/blocker_tasks_named_db.mjs +74 -0
- package/tests/claim_gate_ci_unblock.mjs +1 -1
- package/tests/github_chip.mjs +217 -0
- package/tests/hall_ai_studio.mjs +129 -5
- package/tests/host_topology_skips.mjs +1 -0
- package/tests/hub_account_page.mjs +3 -2
- package/tests/landing_legal_pages.mjs +3 -2
- package/tests/market_watch_data_db.mjs +6 -0
- package/tests/market_watch_pages.mjs +87 -2
- package/tests/market_watch_rules.mjs +101 -5
- package/tests/market_watch_rules_db.mjs +42 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/pay_terms.mjs +8 -0
- package/tests/pay_type_gates.mjs +234 -0
- package/tests/pay_type_gates_db.mjs +84 -0
- package/tests/project_account.mjs +125 -0
- package/tests/project_account_db.mjs +97 -0
- package/tests/self_healing_start.mjs +162 -0
- package/tests/sessions_board.mjs +153 -0
- package/tests/settings_age.mjs +110 -0
- package/tests/stop_refcheck.mjs +40 -0
- package/tests/task_name.mjs +237 -0
- package/tests/task_name_db.mjs +79 -0
- package/tests/terms_acceptance.mjs +25 -3
- package/tests/terms_grandfather_db.mjs +11 -4
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
//
|
|
4
|
+
// scripts/bongos/sessions-board.js — every session a builder has going, and where they
|
|
5
|
+
// are jammed, as a board the AI shows in chat (task 1004957).
|
|
6
|
+
//
|
|
7
|
+
// WHY. A builder running several sessions at once could not see where things were stuck:
|
|
8
|
+
// which session was building, which piece was waiting to land, which had come back from
|
|
9
|
+
// review, which ship was stuck — and the one that matters most, that new work was paused
|
|
10
|
+
// for EVERY session because too many ships were stuck (the REBASE_REQUIRED queue gate).
|
|
11
|
+
// The owner wanted this as a board the AI shows in chat (/session-board), not a hall
|
|
12
|
+
// page, plus a one-line version at session start when something is jammed, so a new
|
|
13
|
+
// session knows not to pile on.
|
|
14
|
+
//
|
|
15
|
+
// WHERE IT READS. Nothing new on the server: GET /me already carries the builder's active
|
|
16
|
+
// claims (one per session, with the worktree it lives in) and the stuck ships that gate
|
|
17
|
+
// them (needs_rebase_tasks); the confirmed and completed task lists say what is waiting to
|
|
18
|
+
// land and what came back from review. Whether the gate is shut is the server's own rule
|
|
19
|
+
// (modules/lifecycle/rebase-gate.js), read here rather than re-derived. Who is fixing a
|
|
20
|
+
// stuck ship is the local strand lease (strand-lease.js, task 1004956).
|
|
21
|
+
//
|
|
22
|
+
// Usage:
|
|
23
|
+
// node scripts/bongos/sessions-board.js # the board, as markdown for chat
|
|
24
|
+
// node scripts/bongos/sessions-board.js --json # the same, as data
|
|
25
|
+
// node scripts/bongos/sessions-board.js --line # ONE line at session start, only when jammed
|
|
26
|
+
//
|
|
27
|
+
// The same line also rides strand-watch.js --line (the stuck-ship startup note) through
|
|
28
|
+
// lineNow(), which is how it reaches a new session: the owner chose that over adding a
|
|
29
|
+
// startup step of its own (2026-10-30).
|
|
30
|
+
//
|
|
31
|
+
// --line is FAIL-OPEN and SILENT when nothing is jammed, and carries ids and fixed words
|
|
32
|
+
// only — never a task title, which is builder-authored text injected into every session
|
|
33
|
+
// (the strand-watch.js trust boundary).
|
|
34
|
+
|
|
35
|
+
const { apiCall, hasFlag } = require('./cli-lib');
|
|
36
|
+
const { splitRebaseGate } = require('../../modules/lifecycle/rebase-gate.js');
|
|
37
|
+
// task 1004958: every task is written "task N - <name>"; safeTaskName passes only the
|
|
38
|
+
// generated name's fixed shape, so it is safe in the startup line too.
|
|
39
|
+
const { safeTaskName, taskRef } = require('../../modules/lifecycle/task-name.js');
|
|
40
|
+
|
|
41
|
+
const PAGE_LIMIT = 200;
|
|
42
|
+
const safeId = (v) => (/^\d{1,19}$/.test(String(v)) ? String(v) : null);
|
|
43
|
+
|
|
44
|
+
// A stuck ship's reason, in words a person reads.
|
|
45
|
+
const REASON_WORDS = Object.freeze({
|
|
46
|
+
land_not_proven: 'it may have landed, but that could not be proven',
|
|
47
|
+
pr_not_landed: 'its change is waiting to merge',
|
|
48
|
+
unresolvable_conflict: 'its change overlaps newer work',
|
|
49
|
+
branch_modifies_executed_code: 'it changes code the merge helper will not run; a person lands it',
|
|
50
|
+
red_check: 'a check failed on its change',
|
|
51
|
+
});
|
|
52
|
+
function reasonWords(reason) {
|
|
53
|
+
const key = String(reason || '').replace(/^strand:/, '').split(':')[0];
|
|
54
|
+
if (/^waiting:/.test(String(reason || ''))) return `waiting on ${String(reason).slice(8)}`;
|
|
55
|
+
return REASON_WORDS[key] || 'stuck on its way in';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ageWords(iso, now) {
|
|
59
|
+
const t = Date.parse(iso || '');
|
|
60
|
+
if (!Number.isFinite(t)) return '';
|
|
61
|
+
const h = (now - t) / 3600000;
|
|
62
|
+
if (h < 1) return `${Math.max(1, Math.round(h * 60))}m`;
|
|
63
|
+
if (h < 48) return `${Math.round(h)}h`;
|
|
64
|
+
return `${Math.round(h / 24)}d`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const upper = (t) => t.charAt(0).toUpperCase() + t.slice(1);
|
|
68
|
+
|
|
69
|
+
// boardOf(input) → { sessions, landing, back, stuck, gate, jams }. PURE.
|
|
70
|
+
// me GET /me (active_claims, needs_rebase_tasks, builder.id)
|
|
71
|
+
// confirmed confirmed tasks (any builder; filtered to this one by shipped_by)
|
|
72
|
+
// completed completed tasks (same)
|
|
73
|
+
// leases live strand leases [{ task_id, holder, label, taken_at }]
|
|
74
|
+
// here this session's worktree { holder, label } or null
|
|
75
|
+
function boardOf({ me, confirmed = [], completed = [], leases = [], here = null, now = Date.now(), truncated = false } = {}) {
|
|
76
|
+
const meId = me && me.builder && me.builder.id != null ? String(me.builder.id) : null;
|
|
77
|
+
const claims = (me && Array.isArray(me.active_claims)) ? me.active_claims : [];
|
|
78
|
+
const owed = (me && Array.isArray(me.needs_rebase_tasks)) ? me.needs_rebase_tasks : [];
|
|
79
|
+
const mine = (t) => t && meId && String(t.shipped_by || t.claimed_by || '') === meId;
|
|
80
|
+
const stuckIds = new Set(owed.map((t) => String(t.id)));
|
|
81
|
+
const leaseFor = (id) => leases.find((l) => String(l.task_id) === String(id)) || null;
|
|
82
|
+
|
|
83
|
+
const sessions = claims.map((c) => ({
|
|
84
|
+
task_id: String(c.task_id),
|
|
85
|
+
name: safeTaskName(c.short_name),
|
|
86
|
+
title: c.title || null,
|
|
87
|
+
session: c.worktree_name || null,
|
|
88
|
+
here: !!(here && c.worktree_name && c.worktree_name === here.label),
|
|
89
|
+
stage: 'building',
|
|
90
|
+
since: ageWords(c.claimed_at, now),
|
|
91
|
+
waiting_on: c.release_requested_at ? 'someone asked for this task back' : null,
|
|
92
|
+
}));
|
|
93
|
+
const landing = confirmed.filter((t) => mine(t) && !stuckIds.has(String(t.id))).map((t) => ({
|
|
94
|
+
task_id: String(t.id), name: safeTaskName(t.short_name), title: t.title || null, stage: 'waiting to land', since: ageWords(t.updated_at, now),
|
|
95
|
+
}));
|
|
96
|
+
const back = completed.filter(mine).map((t) => ({
|
|
97
|
+
task_id: String(t.id), name: safeTaskName(t.short_name), title: t.title || null, stage: 'back from review', since: ageWords(t.updated_at, now),
|
|
98
|
+
waiting_on: 'fix what review found and ask again (or an artist approves a held page)',
|
|
99
|
+
}));
|
|
100
|
+
const split = splitRebaseGate(owed);
|
|
101
|
+
const stuck = owed.map((t) => {
|
|
102
|
+
const lease = leaseFor(t.id);
|
|
103
|
+
const waits = split.waiting.find((w) => String(w.id) === String(t.id));
|
|
104
|
+
return {
|
|
105
|
+
task_id: String(t.id),
|
|
106
|
+
name: safeTaskName(t.short_name),
|
|
107
|
+
title: t.title || null,
|
|
108
|
+
stage: 'stuck',
|
|
109
|
+
since: ageWords(t.needs_rebase_at, now),
|
|
110
|
+
why: reasonWords(t.needs_rebase_reason),
|
|
111
|
+
set_aside: !!waits,
|
|
112
|
+
fixer: lease ? (here && lease.holder === here.holder ? 'this session' : `session ${String(lease.label || '').replace(/[^\w.-]/g, '') || 'another session'}`) : null,
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
const gate = { shut: split.gates, fixable: split.fixable.length };
|
|
116
|
+
|
|
117
|
+
const jams = [];
|
|
118
|
+
if (gate.shut) {
|
|
119
|
+
jams.push({
|
|
120
|
+
kind: 'paused',
|
|
121
|
+
text: `New work is paused for every one of your sessions: ${split.fixable.length} stuck ship${split.fixable.length === 1 ? '' : 's'} must come down first.`,
|
|
122
|
+
task_ids: split.fixable.map((t) => String(t.id)),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
for (const s of stuck) {
|
|
126
|
+
if (s.set_aside) continue;
|
|
127
|
+
jams.push({
|
|
128
|
+
kind: 'stuck',
|
|
129
|
+
text: s.fixer ? `${upper(taskRef(s.task_id, s.name))} is stuck (${s.why}); ${s.fixer} is fixing it.` : `${upper(taskRef(s.task_id, s.name))} is stuck (${s.why}); nobody is fixing it yet.`,
|
|
130
|
+
task_ids: [s.task_id],
|
|
131
|
+
fixer: s.fixer,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// A full page of confirmed or completed tasks means older ones of this builder's may
|
|
135
|
+
// be past it: the board says so rather than passing a capped list off as complete.
|
|
136
|
+
return { sessions, landing, back, stuck, gate, jams, truncated: !!truncated };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The board as markdown for chat: jams first, then one table.
|
|
140
|
+
function markdown(b) {
|
|
141
|
+
const out = [];
|
|
142
|
+
out.push('**Jams**');
|
|
143
|
+
if (!b.jams.length) out.push('- Nothing is jammed.');
|
|
144
|
+
for (const j of b.jams) out.push(`- ${j.text}`);
|
|
145
|
+
out.push('');
|
|
146
|
+
const rows = [
|
|
147
|
+
...b.sessions.map((s) => [s.session ? `${s.session}${s.here ? ' (this one)' : ''}` : '(no folder recorded)', s, s.waiting_on || '—']),
|
|
148
|
+
...b.stuck.map((s) => ['—', s, s.set_aside ? `${s.why} (set aside; it does not pause new work)` : s.fixer ? `${s.why}; ${s.fixer} is fixing it` : `${s.why}; nobody is fixing it`]),
|
|
149
|
+
...b.landing.map((s) => ['—', s, 'the merge; nothing for you to do']),
|
|
150
|
+
...b.back.map((s) => ['—', s, s.waiting_on]),
|
|
151
|
+
];
|
|
152
|
+
if (!rows.length) {
|
|
153
|
+
out.push('No sessions are building anything, and nothing of yours is in flight.');
|
|
154
|
+
return out.join('\n');
|
|
155
|
+
}
|
|
156
|
+
// A title is builder-authored: no link, emphasis, code or tag syntax reaches the chat,
|
|
157
|
+
// and a pipe cannot break the table.
|
|
158
|
+
const cell = (v) => String(v == null ? '' : v).replace(/[`*_[\]<>#~]/g, '').replace(/\|/g, '\\|').replace(/\s+/g, ' ').trim();
|
|
159
|
+
out.push('| Session | Task | Stage | Waiting on |');
|
|
160
|
+
out.push('|---|---|---|---|');
|
|
161
|
+
for (const [session, s, waits] of rows) {
|
|
162
|
+
const title = s.title ? ` — ${cell(s.title).slice(0, 70)}` : '';
|
|
163
|
+
out.push(`| ${cell(session)} | ${taskRef(s.task_id, s.name)}${title} | ${s.stage}${s.since ? ` (${s.since})` : ''} | ${cell(waits)} |`);
|
|
164
|
+
}
|
|
165
|
+
if (b.truncated) out.push('', 'More tasks were waiting to land or back from review than one read returns, so some older ones of yours may be missing from this board.');
|
|
166
|
+
return out.join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ONE line for session start, or null. Only the pause is worth interrupting a new session
|
|
170
|
+
// for — single stuck ships are strand-watch's line already. Ids, generated task names
|
|
171
|
+
// (safeTaskName) and fixed words only.
|
|
172
|
+
function line(b) {
|
|
173
|
+
if (!b || !b.gate.shut) return null;
|
|
174
|
+
const paused = b.jams.find((j) => j.kind === 'paused');
|
|
175
|
+
const ids = (paused ? paused.task_ids : []).map(safeId).filter(Boolean);
|
|
176
|
+
const nameOf = (id) => { const s = b.stuck.find((x) => x.task_id === id); return taskRef(id, s && s.name); };
|
|
177
|
+
const fixing = b.stuck.filter((s) => s.fixer && ids.includes(s.task_id)).map((s) => `${taskRef(s.task_id, s.name)} by ${s.fixer.replace(/[^\w .-]/g, '')}`);
|
|
178
|
+
const free = ids.filter((id) => !b.stuck.some((s) => s.task_id === id && s.fixer));
|
|
179
|
+
return `Sessions board: new work is paused for every session of this builder until ${ids.length} stuck ship${ids.length === 1 ? '' : 's'} (${ids.map(nameOf).join(', ')}) come down. `
|
|
180
|
+
+ (fixing.length ? `Already being fixed: ${fixing.join('; ')}. ` : '')
|
|
181
|
+
+ (free.length ? `Nobody is on ${free.map(nameOf).join(', ')} yet — take one with /strand-fix, or do unclaimed work. ` : 'Do not pile on: carry on with unclaimed work. ')
|
|
182
|
+
+ 'This note is for you, not the builder; show them /session-board only if they ask where things are stuck.';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Who is fixing which stuck ship on this machine (strand-lease.js), and which session
|
|
186
|
+
// this is. No checkout: nobody is fixing anything that we can see.
|
|
187
|
+
function localLeases() {
|
|
188
|
+
try {
|
|
189
|
+
const lease = require('./strand-lease.js');
|
|
190
|
+
return { here: lease.currentHolder(), leases: lease.liveLeases({ dir: lease.leaseDir() }) };
|
|
191
|
+
} catch { return { here: null, leases: [] }; }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function read() {
|
|
195
|
+
const body = (r) => (r && (r.data || r)) || {};
|
|
196
|
+
const [me, confirmed, completed] = await Promise.all([
|
|
197
|
+
apiCall('GET', '/api/bongos/me').then(body),
|
|
198
|
+
apiCall('GET', `/api/bongos/tasks?status=confirmed&limit=${PAGE_LIMIT}`).then((r) => body(r).tasks || []).catch(() => []),
|
|
199
|
+
apiCall('GET', `/api/bongos/tasks?status=completed&limit=${PAGE_LIMIT}`).then((r) => body(r).tasks || []).catch(() => []),
|
|
200
|
+
]);
|
|
201
|
+
const { leases, here } = localLeases();
|
|
202
|
+
const truncated = confirmed.length >= PAGE_LIMIT || completed.length >= PAGE_LIMIT;
|
|
203
|
+
return { me, confirmed, completed, leases, here, truncated };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// THE STARTUP READ, shared across session starts. The pause needs only the stuck ships
|
|
207
|
+
// on /me, so the line asks for nothing else, and the answer is cached for every session
|
|
208
|
+
// on the machine (probe-cache, the strand-watch line's own cache): four sessions opened
|
|
209
|
+
// together make ONE call, not four times three. Stuck ships are measured in hours, so a
|
|
210
|
+
// few minutes' age is as true as fresh. A failed read throws, so it is never cached.
|
|
211
|
+
const OWED_CACHE_KEY = 'sessions-board-owed';
|
|
212
|
+
const OWED_FRESH_MS = 5 * 60 * 1000;
|
|
213
|
+
async function readOwed() {
|
|
214
|
+
const { cached } = require('./probe-cache.js');
|
|
215
|
+
const { value } = await cached(OWED_CACHE_KEY, async () => {
|
|
216
|
+
const r = await apiCall('GET', '/api/bongos/me');
|
|
217
|
+
const me = (r && (r.data || r)) || {};
|
|
218
|
+
if (!me.builder || !Array.isArray(me.needs_rebase_tasks)) throw new Error('unreadable');
|
|
219
|
+
return { builder: { id: me.builder.id }, needs_rebase_tasks: me.needs_rebase_tasks };
|
|
220
|
+
}, { ttlMs: OWED_FRESH_MS });
|
|
221
|
+
const { leases, here } = localLeases();
|
|
222
|
+
return { me: value, leases, here };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// The startup line, read and bounded: the line, or null on silence, a slow read or any
|
|
226
|
+
// failure. Never throws.
|
|
227
|
+
async function lineNow({ deadlineMs = 4000 } = {}) {
|
|
228
|
+
try {
|
|
229
|
+
const deadline = new Promise((_, rej) => setTimeout(() => rej(new Error('deadline')), deadlineMs).unref());
|
|
230
|
+
return line(boardOf(await Promise.race([readOwed(), deadline])));
|
|
231
|
+
} catch { return null; }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// The chat board as text. Fail OPEN: the board is read in chat, so a failed read is a
|
|
235
|
+
// sentence there, never a stack trace or a non-zero exit.
|
|
236
|
+
async function boardText({ json = false, readBoard = read } = {}) {
|
|
237
|
+
let input;
|
|
238
|
+
try {
|
|
239
|
+
input = await readBoard();
|
|
240
|
+
} catch (e) {
|
|
241
|
+
return json
|
|
242
|
+
? JSON.stringify({ available: false, reason: String((e && e.message) || e) }, null, 2)
|
|
243
|
+
: '**Jams**\n- The board could not be read just now (the server did not answer). Nothing is known to be jammed or clear; try again in a minute.';
|
|
244
|
+
}
|
|
245
|
+
const b = boardOf(input);
|
|
246
|
+
return json ? JSON.stringify(b, null, 2) : markdown(b);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function main() {
|
|
250
|
+
if (hasFlag('--line')) {
|
|
251
|
+
const one = await lineNow();
|
|
252
|
+
if (one) console.log(one);
|
|
253
|
+
process.exitCode = 0;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
console.log(await boardText({ json: hasFlag('--json') }));
|
|
257
|
+
process.exitCode = 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (require.main === module) {
|
|
261
|
+
main().catch((e) => { console.error('sessions-board: failed —', e.message || e); process.exitCode = 1; });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = { boardOf, markdown, boardText, line, lineNow, readOwed, reasonWords, OWED_CACHE_KEY };
|
|
@@ -344,6 +344,7 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage, land
|
|
|
344
344
|
stage, // task 1004379: 'held' from finishHeld; undefined ⇒ derived from postGradeStatus
|
|
345
345
|
postGradeStatus,
|
|
346
346
|
taskId,
|
|
347
|
+
shortName: card.shortName,
|
|
347
348
|
title: card.title,
|
|
348
349
|
valueSummary: card.valueSummary,
|
|
349
350
|
notes: card.notes,
|
|
@@ -442,6 +442,7 @@ async function shipMain() {
|
|
|
442
442
|
const model = shipCard.buildCardModel({
|
|
443
443
|
stage: 'review',
|
|
444
444
|
taskId,
|
|
445
|
+
shortName: ac.short_name,
|
|
445
446
|
title: ac.title,
|
|
446
447
|
valueSummary: summary,
|
|
447
448
|
fileCount: committedFiles.length || null,
|
|
@@ -780,6 +781,7 @@ async function shipMain() {
|
|
|
780
781
|
taskId, summary, advanceToMerge, skipMerge, postGradeStatus, doneLine: 'Done.', dbOnly, held,
|
|
781
782
|
// task 1270: data for the deterministic completion card.
|
|
782
783
|
card: {
|
|
784
|
+
shortName: ac.short_name,
|
|
783
785
|
title: ac.title,
|
|
784
786
|
valueSummary: summary,
|
|
785
787
|
notes,
|
|
@@ -968,7 +970,7 @@ async function regradeMain(opts = {}) {
|
|
|
968
970
|
await attachVisual();
|
|
969
971
|
await finishShip({
|
|
970
972
|
taskId, summary: summaryArg || task.value_summary || '', held: true, postGradeStatus: 'completed', doneLine: 'Done.',
|
|
971
|
-
card: { title: task.title, valueSummary: summaryArg || task.value_summary || '', notes: '' },
|
|
973
|
+
card: { shortName: task.short_name, title: task.title, valueSummary: summaryArg || task.value_summary || '', notes: '' },
|
|
972
974
|
});
|
|
973
975
|
return;
|
|
974
976
|
}
|
|
@@ -1088,7 +1090,7 @@ async function regradeMain(opts = {}) {
|
|
|
1088
1090
|
taskId, summary, advanceToMerge: confirmedStatus === 'confirmed', skipMerge, dbOnly: true,
|
|
1089
1091
|
postGradeStatus: confirmedStatus,
|
|
1090
1092
|
doneLine: 'Done — confirmed with no artifact (grader had nothing to read).',
|
|
1091
|
-
card: { title: task.title, valueSummary: summary, notes: '' },
|
|
1093
|
+
card: { shortName: task.short_name, title: task.title, valueSummary: summary, notes: '' },
|
|
1092
1094
|
});
|
|
1093
1095
|
return;
|
|
1094
1096
|
}
|
|
@@ -1190,6 +1192,7 @@ async function regradeMain(opts = {}) {
|
|
|
1190
1192
|
doneLine: g.passed ? 'Done — re-grade passed.' : 'Done — re-grade still failing; fix and try again.',
|
|
1191
1193
|
// task 1270: completion card for the re-grade path too.
|
|
1192
1194
|
card: {
|
|
1195
|
+
shortName: task.short_name,
|
|
1193
1196
|
title: task.title,
|
|
1194
1197
|
valueSummary: summary,
|
|
1195
1198
|
// Empty by design, not by omission (task 1003702). A re-grade takes no
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
// node scripts/bongos/strand-lease.js release <task-id> # drop this worktree's lease
|
|
36
36
|
// node scripts/bongos/strand-lease.js show [<task-id>] # who holds what
|
|
37
37
|
|
|
38
|
+
const { taskRef } = require('../../modules/lifecycle/task-name.js'); // task 1004958
|
|
38
39
|
const fs = require('node:fs');
|
|
39
40
|
const path = require('node:path');
|
|
40
41
|
|
|
@@ -197,11 +198,12 @@ function describe(lease, now = Date.now()) {
|
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
// The clause the strand note appends: the strands OTHER sessions hold, so this one leaves
|
|
200
|
-
// them alone. Null when nobody else holds one.
|
|
201
|
-
|
|
201
|
+
// them alone. Null when nobody else holds one. `names` maps a task id to its short name
|
|
202
|
+
// (task 1004958) — a lease is local and carries none, so the caller hands in what it knows.
|
|
203
|
+
function othersClause(leases, holder, now = Date.now(), names = {}) {
|
|
202
204
|
const others = (leases || []).filter((l) => l.holder !== holder);
|
|
203
205
|
if (!others.length) return null;
|
|
204
|
-
const list = others.map((l) =>
|
|
206
|
+
const list = others.map((l) => `${taskRef(safeId(l.task_id), names[String(l.task_id)])} by ${describe(l, now)}`).join('; ');
|
|
205
207
|
return `Already being fixed: ${list}. Leave ${others.length === 1 ? 'that one' : 'those'} to that session and carry on with your own work.`;
|
|
206
208
|
}
|
|
207
209
|
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
const { apiCall, arg, hasFlag } = require('./cli-lib');
|
|
42
42
|
const tweakHold = require('../../modules/lifecycle/page-tweak-hold.js');
|
|
43
|
+
const { safeTaskName, taskRef } = require('../../modules/lifecycle/task-name.js');
|
|
43
44
|
|
|
44
45
|
// ---- thresholds -------------------------------------------------------------
|
|
45
46
|
// Reasoned, not magic. Written down because a future reader will want to retune them.
|
|
@@ -88,7 +89,17 @@ function humanAge(ms) {
|
|
|
88
89
|
// confirmed -> the grade passed and the LAND is outstanding (/merge-mode is the fallback).
|
|
89
90
|
// A reconciler-recorded strand is called out separately because it is terminal without a
|
|
90
91
|
// human: the automated healers have already refused it.
|
|
91
|
-
|
|
92
|
+
// task 1004958: a strand carries the task's short name when it has one, so the line
|
|
93
|
+
// reads "task N - payout approvals". `safeTaskName` re-checks the stored name against
|
|
94
|
+
// its fixed shape (three lowercase words at most) — see the trust note on `line()`.
|
|
95
|
+
function classifyStrand(task, opts) {
|
|
96
|
+
const s = classifyShape(task, opts);
|
|
97
|
+
const name = s && safeTaskName(task && task.short_name);
|
|
98
|
+
if (name) s.name = name;
|
|
99
|
+
return s;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function classifyShape(task, { nowMs, confirmedStaleMs = CONFIRMED_STALE_MS, completedStaleMs = COMPLETED_STALE_MS } = {}) {
|
|
92
103
|
const t = task || {};
|
|
93
104
|
const status = String(t.status || '');
|
|
94
105
|
if (!WATCHED.includes(status)) return null;
|
|
@@ -193,7 +204,8 @@ async function check({ nowMs = Date.now(), timeoutMs = DEFAULT_TIMEOUT_MS, confi
|
|
|
193
204
|
if (status === 'completed' && held.has(String(t.id))) {
|
|
194
205
|
const updated = Date.parse(t.updated_at || '');
|
|
195
206
|
const ageMs = Number.isFinite(updated) ? nowMs - updated : NaN;
|
|
196
|
-
|
|
207
|
+
const name = safeTaskName(t.short_name);
|
|
208
|
+
waiting.push({ id: safeId(t.id), status, ageMs, age: humanAge(ageMs), shape: tweakHold.HOLD_STATE, ...(name ? { name } : {}) });
|
|
197
209
|
continue;
|
|
198
210
|
}
|
|
199
211
|
const s = classifyStrand(t, { nowMs, confirmedStaleMs, completedStaleMs });
|
|
@@ -214,6 +226,10 @@ async function check({ nowMs = Date.now(), timeoutMs = DEFAULT_TIMEOUT_MS, confi
|
|
|
214
226
|
// is the same reasoning deploy-divergence.js applies to the /version response it reads (it
|
|
215
227
|
// accepts only a strict semver for exactly this purpose). Ids and a fixed reason vocabulary
|
|
216
228
|
// carry all the signal that is needed and none of the risk — the agent can GET the task.
|
|
229
|
+
// The one exception is the task's SHORT NAME (task 1004958), because a bare number means
|
|
230
|
+
// nothing to the person the agent talks to. It is not free text: it is generated, and
|
|
231
|
+
// `safeTaskName` passes only three lowercase words of [a-z0-9-] (40 characters, the
|
|
232
|
+
// table CHECK holds the same shape), so it can name a topic and carry nothing else.
|
|
217
233
|
function line(r, { max = 3 } = {}) {
|
|
218
234
|
if (!r || r.undetermined) return null;
|
|
219
235
|
const trunc = Array.isArray(r.truncated) && r.truncated.length
|
|
@@ -225,7 +241,7 @@ function line(r, { max = 3 } = {}) {
|
|
|
225
241
|
return trunc ? `Strand check could not see the whole board${trunc}. Run: node scripts/bongos/strand-watch.js` : null;
|
|
226
242
|
}
|
|
227
243
|
const shown = r.strands.slice(0, max)
|
|
228
|
-
.map((s) =>
|
|
244
|
+
.map((s) => `${taskRef(s.id, s.name)} (${s.status}, ${s.age}${s.reason ? `, ${s.reason}` : ''})`);
|
|
229
245
|
const more = (r.strands.length > shown.length ? ` +${r.strands.length - shown.length} more` : '') + trunc;
|
|
230
246
|
const worst = r.strands[0];
|
|
231
247
|
// Lead with what the WORST shape actually means. "work is finished but not landed" is true
|
|
@@ -255,12 +271,20 @@ const RELAY_RULE = 'This note is for you, not the builder: do not repeat it to t
|
|
|
255
271
|
// The lease clause, appended at PRINT time, never cached: the line text is shared across
|
|
256
272
|
// sessions for up to two hours (linePlan), while who holds a strand changes by the minute and
|
|
257
273
|
// differs per worktree. Fail-open: no lease folder, no clause.
|
|
274
|
+
// The task names the cached line already carries ("task N - name ("), so the lease clause
|
|
275
|
+
// can name the same tasks without a second read. Only the safe name shape is matched.
|
|
276
|
+
function namesIn(text) {
|
|
277
|
+
const names = {};
|
|
278
|
+
for (const m of String(text || '').matchAll(/task (\d{1,19}) - ([a-z0-9]+(?:-[a-z0-9]+)*(?: [a-z0-9]+(?:-[a-z0-9]+)*){0,2}) \(/g)) names[m[1]] = m[2];
|
|
279
|
+
return names;
|
|
280
|
+
}
|
|
281
|
+
|
|
258
282
|
function withLeases(text, { now = Date.now() } = {}) {
|
|
259
283
|
if (!text) return text;
|
|
260
284
|
try {
|
|
261
285
|
const lease = require('./strand-lease.js');
|
|
262
286
|
const me = lease.currentHolder();
|
|
263
|
-
const clause = lease.othersClause(lease.liveLeases({ dir: lease.leaseDir(), now }), me && me.holder, now);
|
|
287
|
+
const clause = lease.othersClause(lease.liveLeases({ dir: lease.leaseDir(), now }), me && me.holder, now, namesIn(text));
|
|
264
288
|
return clause ? `${text} ${clause}` : text;
|
|
265
289
|
} catch { return text; }
|
|
266
290
|
}
|
|
@@ -278,7 +302,7 @@ function report(r) {
|
|
|
278
302
|
// task 1004322: named, and kept apart from the strands — nobody should recover these.
|
|
279
303
|
const waiting = Array.isArray(r.waiting) ? r.waiting : [];
|
|
280
304
|
for (const w of waiting) {
|
|
281
|
-
console.log(`
|
|
305
|
+
console.log(` ${taskRef(w.id, w.name)} waiting for the artist (${w.age}) — a page tweak whose grade passed; not a strand, leave it`);
|
|
282
306
|
}
|
|
283
307
|
if (r.ok) {
|
|
284
308
|
console.log(' ✓ none — nothing parked at completed or confirmed past its threshold.');
|
|
@@ -286,7 +310,7 @@ function report(r) {
|
|
|
286
310
|
return;
|
|
287
311
|
}
|
|
288
312
|
for (const s of r.strands) {
|
|
289
|
-
console.log(`
|
|
313
|
+
console.log(` ${taskRef(s.id, s.name)} ${s.status.padEnd(9)} ${String(s.age).padStart(4)} ${s.shape}${s.reason ? ` (${s.reason})` : ''}`);
|
|
290
314
|
console.log(` ${s.remedy}`);
|
|
291
315
|
}
|
|
292
316
|
console.log('');
|
|
@@ -400,6 +424,13 @@ async function main() {
|
|
|
400
424
|
if (shown) console.log(withLeases(shown));
|
|
401
425
|
}
|
|
402
426
|
} catch { /* no session, offline, unreadable, past the deadline — all silence */ }
|
|
427
|
+
// task 1004957: when stuck ships have paused new work for EVERY session of this
|
|
428
|
+
// builder, say so here too — the owner chose this note over a new startup step.
|
|
429
|
+
// Its own bounded read; silent unless paused; never fails the session start.
|
|
430
|
+
try {
|
|
431
|
+
const paused = await require('./sessions-board.js').lineNow();
|
|
432
|
+
if (paused) console.log(paused);
|
|
433
|
+
} catch { /* silence */ }
|
|
403
434
|
process.exitCode = 0;
|
|
404
435
|
return;
|
|
405
436
|
}
|
|
@@ -426,7 +457,7 @@ if (require.main === module) {
|
|
|
426
457
|
}
|
|
427
458
|
|
|
428
459
|
module.exports = {
|
|
429
|
-
check, line, withLeases, report, classifyStrand, rankStrands, humanAge,
|
|
460
|
+
check, line, withLeases, namesIn, report, classifyStrand, rankStrands, humanAge,
|
|
430
461
|
linePlan, labelStale, produceLine, LINE_FRESH_MS, LINE_STALE_MAX_MS, LINE_DEADLINE_MS,
|
|
431
462
|
CONFIRMED_STALE_MS, COMPLETED_STALE_MS, STRAND_REASON_PREFIX, WATCHED, PAGE_LIMIT,
|
|
432
463
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/report-checkout.js — @bongos-forwarder (task 1004848). Generated by
|
|
3
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
4
|
+
// The real script is scripts/bongos/report-checkout.js. This forwarder keeps callers that still name
|
|
5
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
6
|
+
'use strict';
|
|
7
|
+
require('../bongos/forwarder.js').forward(module, 'report-checkout.js');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/sessions-board.js — @bongos-forwarder (task 1004848). Generated by
|
|
3
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
4
|
+
// The real script is scripts/bongos/sessions-board.js. This forwarder keeps callers that still name
|
|
5
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
6
|
+
'use strict';
|
|
7
|
+
require('../bongos/forwarder.js').forward(module, 'sessions-board.js');
|
|
@@ -25,20 +25,30 @@ const SLOP_TEXT = 'The moment you find you were wrong because something told you
|
|
|
25
25
|
+ 'and file it: `node scripts/bongos/capture.js "<file>: says X, truth is Y" --kind bug`. The misleading source is the bug. '
|
|
26
26
|
+ 'Your own plain mistake, with nothing misleading behind it, is not slop.';
|
|
27
27
|
|
|
28
|
+
// task 1004958: a bare task number means nothing to the person reading.
|
|
29
|
+
const TASK_NAME_TEXT = 'When you mention a task to a person, write it as "task N - <name>": the number, a dash, then the task\'s '
|
|
30
|
+
+ 'two or three word name (its `short_name`; claim, ship, the startup notes and the sessions board print it). '
|
|
31
|
+
+ 'A bare "task 1004745" means nothing to the reader.';
|
|
32
|
+
|
|
28
33
|
const HEADER = 'Standing rules for every session:';
|
|
29
34
|
|
|
30
35
|
function standingRulesBlock() {
|
|
31
|
-
return `${HEADER}\n- ${HUMAN_SIGNAL_TEXT}\n- ${SLOP_TEXT}`;
|
|
36
|
+
return `${HEADER}\n- ${HUMAN_SIGNAL_TEXT}\n- ${SLOP_TEXT}\n- ${TASK_NAME_TEXT}`;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
// The interaction slice of GET /me with the standing rules appended. `interaction` is
|
|
35
40
|
// describeInteraction's object or null (builder-settings module off); either way the
|
|
36
41
|
// result injects, because these rules do not depend on any preference. Idempotent.
|
|
37
|
-
|
|
42
|
+
// `opts.checkout` (task 1005021) is GET /me's checkout_freshness: when this person's
|
|
43
|
+
// folder is behind and could not update itself, its agent_line rides here too, so even
|
|
44
|
+
// a stale Conductor tells them.
|
|
45
|
+
function withStandingRules(interaction, opts = {}) {
|
|
38
46
|
const base = interaction && typeof interaction === 'object' ? interaction : {};
|
|
39
47
|
const contract = typeof base.contract === 'string' ? base.contract : '';
|
|
40
48
|
if (contract.includes(HEADER)) return { ...base, inject: true };
|
|
41
|
-
|
|
49
|
+
const c = opts && opts.checkout;
|
|
50
|
+
const block = standingRulesBlock() + (c && c.behind && typeof c.agent_line === 'string' ? `\n- ${c.agent_line}` : '');
|
|
51
|
+
return { ...base, contract: contract ? `${contract}\n${block}` : block, inject: true };
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
module.exports = { withStandingRules };
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -395,6 +395,9 @@ module.exports = function buildMeRouter() {
|
|
|
395
395
|
const lawed = settings && typeof settings.describeWithLaws === 'function'
|
|
396
396
|
? settings.describeWithLaws(behaviorPrefs, (sessionLaws && sessionLaws.laws) || [], { rank: builder && builder.rank, instanceModels })
|
|
397
397
|
: null;
|
|
398
|
+
// task 1005021: the builder's own report of how far behind their local folder is.
|
|
399
|
+
const checkoutFreshness = settings && typeof settings.describeCheckout === 'function'
|
|
400
|
+
? settings.describeCheckout(behaviorPrefs) : null;
|
|
398
401
|
// V3.R84 (#302), criterion C3: the builder's month-to-date spend vs their
|
|
399
402
|
// monthly budget. Surfaced here so the CLI (session-start ping) and the hall
|
|
400
403
|
// profile banner read it without a follow-up call; the ship + art-pipeline
|
|
@@ -568,7 +571,10 @@ module.exports = function buildMeRouter() {
|
|
|
568
571
|
// the SSOT (builder-settings port). null when the prefs module is off.
|
|
569
572
|
// task 1005015: the standing rules (human-task markers, slop detection) ride
|
|
570
573
|
// this contract because every Conductor, stale ones included, relays it.
|
|
571
|
-
interaction: withStandingRules(lawed ? lawed.interaction : (settings && settings.describeInteraction ? settings.describeInteraction(behaviorPrefs) : null)),
|
|
574
|
+
interaction: withStandingRules(lawed ? lawed.interaction : (settings && settings.describeInteraction ? settings.describeInteraction(behaviorPrefs) : null), { checkout: checkoutFreshness }),
|
|
575
|
+
// task 1005021: how far behind this builder's local folder is, as their session last
|
|
576
|
+
// reported it; null when never reported or too old to mean anything.
|
|
577
|
+
checkout_freshness: checkoutFreshness,
|
|
572
578
|
// task 1004707: the project's LAWS, read into every AI session. The
|
|
573
579
|
// Conductor hook relays `contract` verbatim beside the interaction profile
|
|
574
580
|
// (inject:false, and no block, when no law is in force). `laws` is the book
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.16'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -27,7 +27,7 @@ const conductor = requireHostModule(require, '../.claude/hooks/conductor.js');
|
|
|
27
27
|
const slopcheck = requireHostModule(require, '../.claude/hooks/stop-slopcheck.js');
|
|
28
28
|
|
|
29
29
|
// The words as the server serves them: a header line, then one '- ' line per rule.
|
|
30
|
-
const [HEADER, HUMAN, SLOP] = rules.withStandingRules(null).contract.split('\n').map((l, i) => (i ? l.slice(2) : l));
|
|
30
|
+
const [HEADER, HUMAN, SLOP, TASK_NAMES] = rules.withStandingRules(null).contract.split('\n').map((l, i) => (i ? l.slice(2) : l));
|
|
31
31
|
|
|
32
32
|
let passed = 0;
|
|
33
33
|
let failed = 0;
|
|
@@ -40,6 +40,7 @@ t('the server appends both rules to a builder\'s interaction contract and keeps
|
|
|
40
40
|
assert.ok(out.contract.startsWith('Interaction profile — P\n'), 'the profile stays first');
|
|
41
41
|
assert.ok(out.contract.includes(HUMAN));
|
|
42
42
|
assert.ok(out.contract.includes(SLOP));
|
|
43
|
+
assert.ok(out.contract.includes(TASK_NAMES));
|
|
43
44
|
assert.equal(out.inject, true);
|
|
44
45
|
assert.deepEqual(out.fields, [1], 'the rest of the slice is untouched');
|
|
45
46
|
});
|
|
@@ -70,9 +71,19 @@ t('the rules mark both markers and the slop line exactly as the owner set them',
|
|
|
70
71
|
t('the Conductor\'s copy says exactly what the server sends', () => {
|
|
71
72
|
assert.equal(conductor.HUMAN_SIGNAL_TEXT, HUMAN);
|
|
72
73
|
assert.equal(conductor.SLOP_TEXT, SLOP);
|
|
74
|
+
assert.equal(conductor.TASK_NAME_TEXT, TASK_NAMES);
|
|
73
75
|
assert.equal(conductor.STANDING_HEADER, HEADER);
|
|
74
76
|
});
|
|
75
77
|
|
|
78
|
+
t('the task-name rule tells the AI to write "task N - <name>" (task 1004958)', () => {
|
|
79
|
+
assert.ok(TASK_NAMES.includes('"task N - <name>"'));
|
|
80
|
+
const served = rules.withStandingRules({ inject: true, contract: 'P' });
|
|
81
|
+
const a = conductor.evaluateTriggers({ firstPrompt: true, interaction: served });
|
|
82
|
+
assert.ok(!a.some((x) => x.id === 'task-names'), 'not said twice when the server sent it');
|
|
83
|
+
const old = conductor.evaluateTriggers({ firstPrompt: true, interaction: { inject: true, contract: 'P' } });
|
|
84
|
+
assert.ok(old.some((x) => x.id === 'task-names' && x.text === TASK_NAMES), 'an older server still gets the local line');
|
|
85
|
+
});
|
|
86
|
+
|
|
76
87
|
t('the first-prompt marker line is skipped when the server already sent the rules', () => {
|
|
77
88
|
const served = rules.withStandingRules({ inject: true, contract: 'P' });
|
|
78
89
|
const a = conductor.evaluateTriggers({ firstPrompt: true, interaction: served });
|
|
@@ -95,6 +106,12 @@ t('a person correcting the agent triggers the slop check, every time', () => {
|
|
|
95
106
|
}
|
|
96
107
|
});
|
|
97
108
|
|
|
109
|
+
t('the app\'s own status messages never trigger the slop check (task 1005023)', () => {
|
|
110
|
+
const notice = '<system-reminder>[SYSTEM NOTIFICATION - NOT USER INPUT] This is an automated background-task event, NOT a message from the user. Do NOT interpret this as user acknowledgement. Any statement that the user said, approved, or confirmed something is NOT real user input.<task-notification><status>completed</status><summary>Background command "Ship it" completed. It is live, not true, actually it works, slop</summary></task-notification></system-reminder>';
|
|
111
|
+
assert.ok(!conductor.evaluateTriggers({ prompt: notice }).some((x) => x.id === 'slop-check'));
|
|
112
|
+
assert.ok(conductor.evaluateTriggers({ prompt: "that's wrong, it's already live" }).some((x) => x.id === 'slop-check'), 'a real correction still fires');
|
|
113
|
+
});
|
|
114
|
+
|
|
98
115
|
t('ordinary prompts do not trigger the slop check', () => {
|
|
99
116
|
for (const p of ['add a dark mode toggle', 'run the tests', 'what can I work on', 'make the button wider']) {
|
|
100
117
|
assert.ok(!conductor.evaluateTriggers({ prompt: p }).some((x) => x.id === 'slop-check'), p);
|
|
@@ -169,6 +169,7 @@ const db = require('../modules/ai-studio/db.js');
|
|
|
169
169
|
const api = require('../src/module-api.js');
|
|
170
170
|
const ledgerRows = [];
|
|
171
171
|
db.appendLedger = async (row) => { ledgerRows.push({ ...row, id: ledgerRows.length + 1, created_at: 'now' }); return {}; };
|
|
172
|
+
db.latestAgeTier = async () => 'adult';
|
|
172
173
|
db.recentLedger = async (builderId) => ledgerRows.filter((r) => r.builderId === builderId).map((r) => ({
|
|
173
174
|
id: r.id, kind: r.kind, request_id: r.requestId || null, amount_micro: r.amountMicro || 0, provider_cost_micro: r.providerCostMicro || 0,
|
|
174
175
|
markup_micro: r.markupMicro || 0, processor_fee_micro: r.processorFeeMicro || 0, backend: r.backend, note: r.note || '', created_at: r.created_at, ref: r.ref,
|
|
@@ -222,6 +223,18 @@ test('routes: top up on the test backend, then read the balance and a ledger wit
|
|
|
222
223
|
assert.equal((await call('POST', '/ai-studio/topup', { amount_usd: 1 })).status, 400);
|
|
223
224
|
});
|
|
224
225
|
|
|
226
|
+
test('routes: a top-up is adults only, refused before any charge is quoted', async () => {
|
|
227
|
+
const before = ledgerRows.length;
|
|
228
|
+
for (const tier of [null, '16_17']) {
|
|
229
|
+
db.latestAgeTier = async () => tier;
|
|
230
|
+
const r = await call('POST', '/ai-studio/topup', { amount_usd: 10 });
|
|
231
|
+
assert.equal(r.status, 403);
|
|
232
|
+
assert.equal(r.body.error.code || r.body.error, 'adults_only');
|
|
233
|
+
}
|
|
234
|
+
db.latestAgeTier = async () => 'adult';
|
|
235
|
+
assert.equal(ledgerRows.length, before, 'a refused top-up writes nothing');
|
|
236
|
+
});
|
|
237
|
+
|
|
225
238
|
// Task 1004921: the Studio says "your card is charged $20.91" BEFORE anything is
|
|
226
239
|
// paid, and how much a run sets aside, from these two reads, so neither the fee
|
|
227
240
|
// nor the ceiling is spelled in the browser.
|