@bongos/core 1.23.20 → 1.23.22
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 +137 -67
- package/.claude/skills/goal-create/SKILL.md +5 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +87 -0
- package/docs/adr/0386-the-funding-wall-is-a-per-project-rule-claim-time-hold-by-default.md +37 -0
- package/docs/adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md +39 -0
- package/docs/adr/0388-a-module-sale-is-split-by-the-project-the-buyer-pays-the-platform-fee.md +42 -0
- package/docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md +33 -0
- package/docs/adr/README.md +5 -0
- package/docs/api/openapi.json +59 -4
- package/docs/api-reference.md +7 -1
- package/docs/file-map.md +3 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +1 -1
- package/modules/ai-studio/CLAUDE.md +15 -0
- package/modules/ai-studio/module.json +26 -0
- package/modules/ai-studio/routes/ai-studio.js +22 -0
- package/modules/copy-desk/chrome-pages.js +178 -0
- package/modules/copy-desk/page-data.js +34 -2
- package/modules/copy-desk/tests/copy_chrome_pages.mjs +127 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +1 -1
- package/modules/government/db.js +74 -0
- package/modules/government/routes/government.js +28 -18
- package/modules/grading/grader-subagent.js +30 -11
- package/modules/hall-ui/public/city.html +15 -15
- package/modules/lifecycle/db-analytics.js +10 -3
- package/modules/lifecycle/db-goals.js +30 -13
- package/modules/lifecycle/db-shared.js +9 -0
- package/modules/lifecycle/db-versions.js +21 -8
- package/modules/lifecycle/ship-assurance.js +4 -1
- package/modules/lifecycle/ship-card.js +8 -2
- package/modules/onboarding/howto-moments.js +8 -1
- package/modules/provisioning/starter-bundles.js +4 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +72 -0
- package/scripts/bongos/copy-apply.js +3 -1
- package/scripts/bongos/publish-manifest.js +1 -0
- package/scripts/bongos/strand-watch.js +106 -10
- package/scripts/bongos/tweak-renders.js +9 -2
- package/src/bongos/module-scope-map.js +4 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +27 -1
- package/tests/batched_writes_parity.mjs +340 -0
- package/tests/conductor_main_e2e.mjs +4 -0
- package/tests/copy_desk_page_docx.mjs +3 -3
- package/tests/copy_desk_page_editor.mjs +6 -6
- package/tests/copy_desk_page_next.mjs +3 -2
- package/tests/copy_desk_page_reads.mjs +2 -1
- package/tests/copy_desk_page_submit.mjs +30 -2
- package/tests/fixtures/per-row-loops-legacy.cjs +245 -0
- package/tests/goal_archive_disposition.mjs +5 -5
- package/tests/government_routes.mjs +145 -22
- package/tests/grader.mjs +31 -0
- package/tests/hall_city.mjs +8 -0
- package/tests/hook_shell_tools.mjs +8 -0
- package/tests/hooks_drain.mjs +1 -1
- package/tests/host_topology_skips.mjs +2 -0
- package/tests/howto_moments.mjs +17 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/module_skills_hook.mjs +69 -0
- package/tests/profile_role_stats.mjs +2 -1
- package/tests/public_landing_projects.mjs +37 -0
- package/tests/session_start_freshness.mjs +66 -1
- package/tests/shell_hooks.mjs +209 -0
- package/tests/ship_assurance.mjs +28 -0
- package/tests/ship_card.mjs +20 -5
- package/tests/strand_watch.mjs +48 -0
- package/tests/version_close_route.mjs +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// tests/fixtures/per-row-loops-legacy.cjs — the FOUR one-query-per-row loops as
|
|
4
|
+
// they stood before task 1004929 batched them, kept verbatim (comments trimmed)
|
|
5
|
+
// so tests/batched_writes_parity.mjs and tests/government_routes.mjs can run the
|
|
6
|
+
// old and the new code over the same fake database and compare what each one
|
|
7
|
+
// returns and leaves behind. A frozen reference, not live code: nothing in src/
|
|
8
|
+
// or modules/ may require it.
|
|
9
|
+
|
|
10
|
+
const { nonTerminalSql, isTerminalStatus } = require('../../modules/lifecycle/task-status');
|
|
11
|
+
const { cancelPendingMembershipRequests } = require('../../modules/lifecycle/goal-close-sweep.js');
|
|
12
|
+
|
|
13
|
+
// modules/government/routes/government.js — POST /government/rank-sets/apply,
|
|
14
|
+
// the try-block body. `db` is the government db slice, `rankSet` the
|
|
15
|
+
// recommendedRankSet output. Returns { created, skipped } as the reply carried.
|
|
16
|
+
async function legacyApplyRankSet({ db, catalog, rankSet, builderId }) {
|
|
17
|
+
const set = rankSet;
|
|
18
|
+
const created = [];
|
|
19
|
+
const skipped = [];
|
|
20
|
+
for (const r of set.ranks) {
|
|
21
|
+
if (await db.getRankByKey(r.rank_key)) { skipped.push({ rank_key: r.rank_key, label: r.label }); continue; }
|
|
22
|
+
let rank;
|
|
23
|
+
try {
|
|
24
|
+
rank = await db.createRank({ rankKey: r.rank_key, label: r.label, description: r.description, createdBy: builderId });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (err && err.code === '23505') { skipped.push({ rank_key: r.rank_key, label: r.label }); continue; }
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
const granted = [];
|
|
30
|
+
for (const key of r.permissions) {
|
|
31
|
+
if (!catalog.byKey(key) || !catalog.isGrantableTo(key, rank)) continue;
|
|
32
|
+
if (await db.grantRankPermission({ rankKey: r.rank_key, permissionKey: key })) granted.push(key);
|
|
33
|
+
}
|
|
34
|
+
created.push({ rank_key: r.rank_key, label: r.label, permissions: granted });
|
|
35
|
+
}
|
|
36
|
+
return { created, skipped };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// modules/lifecycle/db-goals.js — archiveGoalWithDispositions, the plan loop.
|
|
40
|
+
// Runs on an open client (the caller has already locked the goal). Returns the
|
|
41
|
+
// applied list, or throws what the loop threw.
|
|
42
|
+
async function legacyArchiveLoop(client, { goalId, plan = [], reason }) {
|
|
43
|
+
const applied = [];
|
|
44
|
+
for (const step of plan) {
|
|
45
|
+
const { rows: tRows } = await client.query(
|
|
46
|
+
`SELECT id, title, status FROM tasks WHERE id = $1 AND goal_id = $2 FOR UPDATE`,
|
|
47
|
+
[step.taskId, goalId]
|
|
48
|
+
);
|
|
49
|
+
if (!tRows[0]) { applied.push({ task_id: String(step.taskId), verb: step.verb, result: 'skipped_not_in_goal' }); continue; }
|
|
50
|
+
if (isTerminalStatus(tRows[0].status)) {
|
|
51
|
+
applied.push({ task_id: String(step.taskId), verb: step.verb, result: 'skipped_already_terminal', status: tRows[0].status });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { rows: claimed } = await client.query(
|
|
56
|
+
`SELECT id FROM claims WHERE task_id = $1 AND released_at IS NULL LIMIT 1`, [step.taskId]
|
|
57
|
+
);
|
|
58
|
+
if (claimed[0]) {
|
|
59
|
+
throw Object.assign(new Error('task has an active claim'), {
|
|
60
|
+
code: 'TASK_HAS_ACTIVE_CLAIM', taskId: String(step.taskId), taskTitle: tRows[0].title,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (step.verb === 'abandon') {
|
|
65
|
+
await client.query(
|
|
66
|
+
`UPDATE tasks SET status = 'abandoned',
|
|
67
|
+
value_summary = COALESCE(NULLIF($2, ''), value_summary),
|
|
68
|
+
updated_at = now()
|
|
69
|
+
WHERE id = $1`,
|
|
70
|
+
[step.taskId, `Abandoned: ${reason}`]
|
|
71
|
+
);
|
|
72
|
+
applied.push({ task_id: String(step.taskId), verb: 'abandon', result: 'abandoned' });
|
|
73
|
+
} else {
|
|
74
|
+
const { rows: dest } = await client.query(
|
|
75
|
+
`SELECT id, status FROM goals WHERE id = $1 FOR SHARE`, [step.toGoalId]
|
|
76
|
+
);
|
|
77
|
+
if (!dest[0]) {
|
|
78
|
+
throw Object.assign(new Error('move target not found'), { code: 'MOVE_TARGET_NOT_FOUND', toGoalId: String(step.toGoalId) });
|
|
79
|
+
}
|
|
80
|
+
if (dest[0].status !== 'open') {
|
|
81
|
+
throw Object.assign(new Error('move target is not open'), {
|
|
82
|
+
code: 'MOVE_TARGET_NOT_OPEN', toGoalId: String(step.toGoalId), targetStatus: dest[0].status,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
await client.query(`UPDATE tasks SET goal_id = $2, updated_at = now() WHERE id = $1`, [step.taskId, step.toGoalId]);
|
|
86
|
+
applied.push({ task_id: String(step.taskId), verb: 'move', result: 'moved', to_goal_id: String(step.toGoalId) });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return applied;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// modules/lifecycle/db-versions.js — closeVersion, the plan loop. Same contract.
|
|
93
|
+
async function legacyCloseLoop(client, { versionId, plan = [], reason, planningVersionId = null }) {
|
|
94
|
+
const applied = [];
|
|
95
|
+
for (const step of plan) {
|
|
96
|
+
const { rows: gRows } = await client.query(
|
|
97
|
+
`SELECT id, title, status FROM goals WHERE id = $1 AND version_id = $2 FOR UPDATE`,
|
|
98
|
+
[step.goalId, versionId]
|
|
99
|
+
);
|
|
100
|
+
if (!gRows[0]) { applied.push({ goal_id: String(step.goalId), verb: step.verb, result: 'skipped_not_on_version' }); continue; }
|
|
101
|
+
if (gRows[0].status !== 'open') {
|
|
102
|
+
applied.push({ goal_id: String(step.goalId), verb: step.verb, result: 'skipped_already_closed', status: gRows[0].status });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (step.verb === 'abandon') {
|
|
107
|
+
const { rows: killed } = await client.query(
|
|
108
|
+
`UPDATE tasks SET status = 'abandoned',
|
|
109
|
+
value_summary = COALESCE(NULLIF($2, ''), value_summary),
|
|
110
|
+
updated_at = now()
|
|
111
|
+
WHERE goal_id = $1 AND ${nonTerminalSql('status')}
|
|
112
|
+
AND title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'
|
|
113
|
+
RETURNING id`,
|
|
114
|
+
[step.goalId, `Abandoned: ${reason}`]
|
|
115
|
+
);
|
|
116
|
+
await client.query(`UPDATE goals SET status = 'archived', updated_at = now() WHERE id = $1`, [step.goalId]);
|
|
117
|
+
await cancelPendingMembershipRequests(client, step.goalId);
|
|
118
|
+
applied.push({ goal_id: String(step.goalId), verb: 'abandon', result: 'archived', tasks_abandoned: killed.length });
|
|
119
|
+
} else {
|
|
120
|
+
if (!planningVersionId) {
|
|
121
|
+
throw Object.assign(new Error('roll_forward with no planning version'), {
|
|
122
|
+
code: 'NO_PLANNING_VERSION', goalId: String(step.goalId),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const { rows: made } = await client.query(
|
|
126
|
+
`WITH next_order AS (
|
|
127
|
+
SELECT COALESCE(MAX(sort_order) + 1, 0) AS n FROM goals WHERE version_id = $2
|
|
128
|
+
)
|
|
129
|
+
INSERT INTO goals (version_id, title, subtitle, description, scope_modules,
|
|
130
|
+
status, visibility, accepting_requests, created_by, sort_order, category_id, working_area)
|
|
131
|
+
SELECT $2, g.title, g.subtitle, g.description, g.scope_modules,
|
|
132
|
+
'open', g.visibility, g.accepting_requests, g.created_by, next_order.n, g.category_id, g.working_area
|
|
133
|
+
FROM goals g, next_order
|
|
134
|
+
WHERE g.id = $1
|
|
135
|
+
RETURNING id`,
|
|
136
|
+
[step.goalId, planningVersionId]
|
|
137
|
+
);
|
|
138
|
+
const successorId = made[0].id;
|
|
139
|
+
|
|
140
|
+
await client.query(
|
|
141
|
+
`INSERT INTO goal_members (goal_id, builder_id, membership_kind)
|
|
142
|
+
SELECT $2, builder_id, membership_kind FROM goal_members WHERE goal_id = $1
|
|
143
|
+
ON CONFLICT DO NOTHING`,
|
|
144
|
+
[step.goalId, successorId]
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const { rows: moved } = await client.query(
|
|
148
|
+
`UPDATE tasks SET goal_id = $2, version_id = $3, updated_at = now()
|
|
149
|
+
WHERE goal_id = $1 AND ${nonTerminalSql('status')}
|
|
150
|
+
AND title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'
|
|
151
|
+
RETURNING id`,
|
|
152
|
+
[step.goalId, successorId, planningVersionId]
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
await client.query(
|
|
156
|
+
`UPDATE goals SET succeeded_by_goal_id = $2, updated_at = now() WHERE id = $1`,
|
|
157
|
+
[step.goalId, successorId]
|
|
158
|
+
);
|
|
159
|
+
await client.query(`UPDATE goals SET status = 'archived', updated_at = now() WHERE id = $1`, [step.goalId]);
|
|
160
|
+
await cancelPendingMembershipRequests(client, step.goalId);
|
|
161
|
+
|
|
162
|
+
applied.push({
|
|
163
|
+
goal_id: String(step.goalId), verb: 'roll_forward', result: 'succeeded',
|
|
164
|
+
successor_goal_id: String(successorId), tasks_moved: moved.length,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return applied;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// modules/lifecycle/db-analytics.js — tallyPeerVotes, the whole function, with
|
|
172
|
+
// the transaction primitive passed in (it was api.withTx).
|
|
173
|
+
const PEER_VOTE_KARMA_LAG_DAYS = 7;
|
|
174
|
+
async function legacyTallyPeerVotes(withTx) {
|
|
175
|
+
return withTx(async (client) => {
|
|
176
|
+
const { rows: matured } = await client.query(
|
|
177
|
+
`SELECT pv.id, t.shipped_by AS builder_id, pv.vote
|
|
178
|
+
FROM peer_votes pv
|
|
179
|
+
JOIN tasks t ON t.id = pv.target_task_id
|
|
180
|
+
WHERE pv.tallied_at IS NULL
|
|
181
|
+
AND pv.recorded_at <= now() - ($1::int * interval '1 day')
|
|
182
|
+
AND t.shipped_by IS NOT NULL
|
|
183
|
+
FOR UPDATE OF pv`,
|
|
184
|
+
[PEER_VOTE_KARMA_LAG_DAYS]
|
|
185
|
+
);
|
|
186
|
+
const netByBuilder = new Map();
|
|
187
|
+
const voteIds = [];
|
|
188
|
+
for (const r of matured) {
|
|
189
|
+
voteIds.push(r.id);
|
|
190
|
+
netByBuilder.set(r.builder_id, (netByBuilder.get(r.builder_id) || 0) + r.vote);
|
|
191
|
+
}
|
|
192
|
+
const ideaVoteIds = [];
|
|
193
|
+
const { rows: [{ has: hasIdeaVotes }] } = await client.query(
|
|
194
|
+
`SELECT to_regclass('ideas_votes') IS NOT NULL AS has`
|
|
195
|
+
);
|
|
196
|
+
if (hasIdeaVotes) {
|
|
197
|
+
const { rows: maturedIdeas } = await client.query(
|
|
198
|
+
`SELECT iv.id, i.captured_by AS builder_id, iv.vote
|
|
199
|
+
FROM ideas_votes iv
|
|
200
|
+
JOIN idea_inbox i ON i.id = iv.target_idea_id
|
|
201
|
+
WHERE iv.tallied_at IS NULL
|
|
202
|
+
AND iv.recorded_at <= now() - ($1::int * interval '1 day')
|
|
203
|
+
AND i.captured_by IS NOT NULL
|
|
204
|
+
FOR UPDATE OF iv`,
|
|
205
|
+
[PEER_VOTE_KARMA_LAG_DAYS]
|
|
206
|
+
);
|
|
207
|
+
for (const r of maturedIdeas) {
|
|
208
|
+
ideaVoteIds.push(r.id);
|
|
209
|
+
netByBuilder.set(r.builder_id, (netByBuilder.get(r.builder_id) || 0) + r.vote);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const karmaRows = [];
|
|
213
|
+
for (const [builderId, net] of netByBuilder) {
|
|
214
|
+
if (net === 0) continue;
|
|
215
|
+
await client.query(
|
|
216
|
+
`INSERT INTO karma_log (builder_id, delta, reason, granted_by)
|
|
217
|
+
VALUES ($1, $2, 'peer_vote_weekly', NULL)`,
|
|
218
|
+
[builderId, net]
|
|
219
|
+
);
|
|
220
|
+
karmaRows.push({ builder_id: builderId, delta: net });
|
|
221
|
+
}
|
|
222
|
+
if (voteIds.length) {
|
|
223
|
+
await client.query(
|
|
224
|
+
`UPDATE peer_votes SET tallied_at = now() WHERE id = ANY($1::bigint[])`,
|
|
225
|
+
[voteIds]
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (ideaVoteIds.length) {
|
|
229
|
+
await client.query(
|
|
230
|
+
`UPDATE ideas_votes SET tallied_at = now() WHERE id = ANY($1::bigint[])`,
|
|
231
|
+
[ideaVoteIds]
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
votes_tallied: voteIds.length + ideaVoteIds.length,
|
|
237
|
+
idea_votes_tallied: ideaVoteIds.length,
|
|
238
|
+
builders_credited: karmaRows.length,
|
|
239
|
+
karma: karmaRows,
|
|
240
|
+
lag_days: PEER_VOTE_KARMA_LAG_DAYS,
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = { legacyApplyRankSet, legacyArchiveLoop, legacyCloseLoop, legacyTallyPeerVotes };
|
|
@@ -244,7 +244,7 @@ test('the whole disposition + archive runs inside ONE transaction', async () =>
|
|
|
244
244
|
const pool = txPool((sql) => {
|
|
245
245
|
const q = String(sql).replace(/\s+/g, ' ');
|
|
246
246
|
if (/^SELECT .* FROM goals WHERE id = \$1 FOR UPDATE/.test(q.trim())) return { rows: [{ id: 7, status: 'open' }] };
|
|
247
|
-
if (/FROM tasks WHERE id = \$1 AND goal_id = \$2 FOR UPDATE/.test(q)) return { rows: [{ id: 1, title: 't1', status: 'ready' }] };
|
|
247
|
+
if (/FROM tasks WHERE id = ANY\(\$1::bigint\[\]\) AND goal_id = \$2 ORDER BY id FOR UPDATE/.test(q)) return { rows: [{ id: 1, title: 't1', status: 'ready' }] };
|
|
248
248
|
if (/FROM claims WHERE task_id/.test(q)) return { rows: [] };
|
|
249
249
|
if (/count\(\*\)::int AS n FROM tasks/.test(q)) return { rows: [{ n: 0 }] };
|
|
250
250
|
if (/UPDATE goals SET status = 'archived'/.test(q)) return { rows: [{ id: 7, status: 'archived' }] };
|
|
@@ -270,7 +270,7 @@ test('the goal row is locked BEFORE the plan is applied', async () => {
|
|
|
270
270
|
const pool = txPool((sql) => {
|
|
271
271
|
const q = String(sql).replace(/\s+/g, ' ');
|
|
272
272
|
if (/FROM goals WHERE id = \$1 FOR UPDATE/.test(q)) return { rows: [{ id: 7, status: 'open' }] };
|
|
273
|
-
if (/FROM tasks WHERE id = \$1 AND goal_id = \$2 FOR UPDATE/.test(q)) return { rows: [{ id: 1, title: 't', status: 'ready' }] };
|
|
273
|
+
if (/FROM tasks WHERE id = ANY\(\$1::bigint\[\]\) AND goal_id = \$2 ORDER BY id FOR UPDATE/.test(q)) return { rows: [{ id: 1, title: 't', status: 'ready' }] };
|
|
274
274
|
if (/FROM claims/.test(q)) return { rows: [] };
|
|
275
275
|
if (/count\(\*\)::int AS n/.test(q)) return { rows: [{ n: 0 }] };
|
|
276
276
|
if (/UPDATE goals/.test(q)) return { rows: [{ id: 7, status: 'archived' }] };
|
|
@@ -287,8 +287,8 @@ test('an actively-claimed task stops the archive and rolls it back', async () =>
|
|
|
287
287
|
const pool = txPool((sql) => {
|
|
288
288
|
const q = String(sql).replace(/\s+/g, ' ');
|
|
289
289
|
if (/FROM goals WHERE id = \$1 FOR UPDATE/.test(q)) return { rows: [{ id: 7, status: 'open' }] };
|
|
290
|
-
if (/FROM tasks WHERE id = \$1 AND goal_id = \$2 FOR UPDATE/.test(q)) return { rows: [{ id: 1, title: 'busy', status: 'active' }] };
|
|
291
|
-
if (/FROM claims WHERE task_id/.test(q)) return { rows: [{
|
|
290
|
+
if (/FROM tasks WHERE id = ANY\(\$1::bigint\[\]\) AND goal_id = \$2 ORDER BY id FOR UPDATE/.test(q)) return { rows: [{ id: 1, title: 'busy', status: 'active' }] };
|
|
291
|
+
if (/FROM claims WHERE task_id/.test(q)) return { rows: [{ task_id: 1 }] }; // someone is on it
|
|
292
292
|
return { rows: [] };
|
|
293
293
|
});
|
|
294
294
|
await assert.rejects(
|
|
@@ -307,7 +307,7 @@ test('the invariant is re-checked against committed state, not against the plan'
|
|
|
307
307
|
const pool = txPool((sql) => {
|
|
308
308
|
const q = String(sql).replace(/\s+/g, ' ');
|
|
309
309
|
if (/FROM goals WHERE id = \$1 FOR UPDATE/.test(q)) return { rows: [{ id: 7, status: 'open' }] };
|
|
310
|
-
if (/FROM tasks WHERE id = \$1 AND goal_id = \$2 FOR UPDATE/.test(q)) return { rows: [{ id: 1, title: 't', status: 'ready' }] };
|
|
310
|
+
if (/FROM tasks WHERE id = ANY\(\$1::bigint\[\]\) AND goal_id = \$2 ORDER BY id FOR UPDATE/.test(q)) return { rows: [{ id: 1, title: 't', status: 'ready' }] };
|
|
311
311
|
if (/FROM claims/.test(q)) return { rows: [] };
|
|
312
312
|
if (/count\(\*\)::int AS n FROM tasks/.test(q)) return { rows: [{ n: 3 }] }; // three snuck in
|
|
313
313
|
return { rows: [] };
|
|
@@ -149,6 +149,9 @@ const stub = {
|
|
|
149
149
|
}),
|
|
150
150
|
createRank: async (a) => ({ rank_key: a.rankKey, label: a.label, description: a.description ?? null, is_system: false, seeded_rank: null, created_by: a.createdBy, created_at: 't' }),
|
|
151
151
|
grantRankPermission: async () => true,
|
|
152
|
+
// task 1004929: the rank-set apply's one-transaction write. The apply tests
|
|
153
|
+
// swap in the REAL function over a fake SQL store (withRealRankDb below).
|
|
154
|
+
applyRankSet: async () => new Map(),
|
|
152
155
|
revokeRankPermission: async () => true,
|
|
153
156
|
resetRankGrants: async ({ permissionKeys }) => permissionKeys.length,
|
|
154
157
|
// task 1004481: `deleteAnswer` scripts what the locked delete found.
|
|
@@ -265,35 +268,155 @@ test('GET /government/assignments returns builder↔role rows', async () => {
|
|
|
265
268
|
|
|
266
269
|
// --- recommended rank sets (task 1004400) -------------------------------------
|
|
267
270
|
|
|
271
|
+
// task 1004929: the apply is now ONE transaction written by db.applyRankSet. These
|
|
272
|
+
// tests run the REAL route into the REAL db functions, over a small fake of the
|
|
273
|
+
// two tables that understands exactly the statements the old per-row loop and
|
|
274
|
+
// the new batched write send. That is what lets the parity test below run the
|
|
275
|
+
// old loop (tests/fixtures/per-row-loops-legacy.cjs, verbatim) and the new route
|
|
276
|
+
// over the same starting tables and compare the replies AND what was written.
|
|
277
|
+
//
|
|
278
|
+
// `existing` are ranks already there; `race` are ranks another caller creates
|
|
279
|
+
// between this call's read and its insert (invisible to the read, a conflict on
|
|
280
|
+
// the insert) — the case the old loop caught as a 23505.
|
|
281
|
+
function rankStore({ existing = [], race = [], failOnGrant = false } = {}) {
|
|
282
|
+
let st = { ranks: existing.map((k) => ({ rank_key: k, label: 'Theirs', description: null, created_by: 7 })), grants: [] };
|
|
283
|
+
let snap = null;
|
|
284
|
+
const raced = new Set(race);
|
|
285
|
+
const queries = [];
|
|
286
|
+
const has = (k) => st.ranks.some((r) => r.rank_key === k);
|
|
287
|
+
const conflicts = (k) => {
|
|
288
|
+
if (raced.has(k)) { raced.delete(k); st.ranks.push({ rank_key: k, label: 'Raced', description: null, created_by: 8 }); }
|
|
289
|
+
return has(k);
|
|
290
|
+
};
|
|
291
|
+
const addRank = (k, label, description, createdBy) => {
|
|
292
|
+
const row = { rank_key: k, label, description: description ?? null, created_by: Number(createdBy) };
|
|
293
|
+
st.ranks.push(row);
|
|
294
|
+
return { ...row, is_system: false, seeded_rank: null, created_at: 't' };
|
|
295
|
+
};
|
|
296
|
+
const addGrant = (rk, pk) => {
|
|
297
|
+
if (failOnGrant) throw new Error('grant insert failed');
|
|
298
|
+
if (st.grants.some((g) => g.rank_key === rk && g.permission_key === pk)) return false;
|
|
299
|
+
st.grants.push({ rank_key: rk, permission_key: pk });
|
|
300
|
+
return true;
|
|
301
|
+
};
|
|
302
|
+
async function query(sql, p = []) {
|
|
303
|
+
const s = String(sql).replace(/\s+/g, ' ').trim();
|
|
304
|
+
queries.push(s);
|
|
305
|
+
if (s === 'BEGIN') { snap = JSON.parse(JSON.stringify(st)); return { rows: [] }; }
|
|
306
|
+
if (s === 'COMMIT') { snap = null; return { rows: [] }; }
|
|
307
|
+
if (s === 'ROLLBACK') { if (snap) st = snap; snap = null; return { rows: [] }; }
|
|
308
|
+
if (/^SELECT rank_key, label, description, is_system, seeded_rank, created_by, created_at FROM government_ranks WHERE rank_key = \$1$/.test(s)) {
|
|
309
|
+
return { rows: st.ranks.filter((r) => r.rank_key === p[0]).map((r) => ({ ...r, is_system: false, seeded_rank: null })) };
|
|
310
|
+
}
|
|
311
|
+
if (/^INSERT INTO government_ranks \(.*\) VALUES \(\$1, \$2, \$3, false, NULL, \$4\) RETURNING/.test(s)) {
|
|
312
|
+
if (conflicts(p[0])) throw Object.assign(new Error('duplicate key'), { code: '23505' });
|
|
313
|
+
return { rows: [addRank(p[0], p[1], p[2], p[3])] };
|
|
314
|
+
}
|
|
315
|
+
if (/^INSERT INTO government_rank_permissions \(rank_key, permission_key\) VALUES \(\$1, \$2\) ON CONFLICT/.test(s)) {
|
|
316
|
+
return { rows: [], rowCount: addGrant(p[0], p[1]) ? 1 : 0 };
|
|
317
|
+
}
|
|
318
|
+
if (/^SELECT rank_key FROM government_ranks WHERE rank_key = ANY\(\$1::text\[\]\)$/.test(s)) {
|
|
319
|
+
return { rows: st.ranks.filter((r) => p[0].includes(r.rank_key)).map((r) => ({ rank_key: r.rank_key })) };
|
|
320
|
+
}
|
|
321
|
+
if (/^INSERT INTO government_ranks .* FROM unnest\(\$1::text\[\], \$2::text\[\], \$3::text\[\]\) WITH ORDINALITY .* ORDER BY r\.ord ON CONFLICT DO NOTHING RETURNING rank_key$/.test(s)) {
|
|
322
|
+
const rows = [];
|
|
323
|
+
p[0].forEach((k, i) => { if (!conflicts(k)) { addRank(k, p[1][i], p[2][i], p[3]); rows.push({ rank_key: k }); } });
|
|
324
|
+
return { rows };
|
|
325
|
+
}
|
|
326
|
+
if (/^INSERT INTO government_rank_permissions .* FROM unnest\(\$1::text\[\], \$2::text\[\]\) WITH ORDINALITY .* ORDER BY g\.ord ON CONFLICT \(rank_key, permission_key\) DO NOTHING RETURNING rank_key, permission_key$/.test(s)) {
|
|
327
|
+
const rows = [];
|
|
328
|
+
p[0].forEach((rk, i) => { if (addGrant(rk, p[1][i])) rows.push({ rank_key: rk, permission_key: p[1][i] }); });
|
|
329
|
+
return { rows };
|
|
330
|
+
}
|
|
331
|
+
throw new Error(`rank store: unhandled statement: ${s.slice(0, 120)}`);
|
|
332
|
+
}
|
|
333
|
+
return { query, client: { query, release() {} }, queries, get state() { return JSON.parse(JSON.stringify(st)); } };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Point the REAL government db functions at a rankStore for the length of `fn`.
|
|
337
|
+
const pgPool = require(path.join(ROOT, 'src', 'bongos', 'pool.js')).pool;
|
|
338
|
+
const RANK_DB_FNS = ['getRankByKey', 'createRank', 'grantRankPermission', 'applyRankSet'];
|
|
339
|
+
async function withRealRankDb(store, fn) {
|
|
340
|
+
const saved = Object.fromEntries(RANK_DB_FNS.map((k) => [k, stub[k]]));
|
|
341
|
+
const own = { query: Object.prototype.hasOwnProperty.call(pgPool, 'query'), connect: Object.prototype.hasOwnProperty.call(pgPool, 'connect') };
|
|
342
|
+
const realQuery = pgPool.query;
|
|
343
|
+
const realConnect = pgPool.connect;
|
|
344
|
+
for (const k of RANK_DB_FNS) stub[k] = REAL[k];
|
|
345
|
+
pgPool.query = store.query;
|
|
346
|
+
pgPool.connect = async () => store.client;
|
|
347
|
+
try { return await fn(); } finally {
|
|
348
|
+
Object.assign(stub, saved);
|
|
349
|
+
if (own.query) pgPool.query = realQuery; else delete pgPool.query;
|
|
350
|
+
if (own.connect) pgPool.connect = realConnect; else delete pgPool.connect;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
268
354
|
test('POST /government/rank-sets/apply creates the missing ranks with their grants and leaves existing ones alone', async () => {
|
|
269
|
-
const realGet = stub.getRankByKey;
|
|
270
355
|
// 'design-lead' already exists: it must be skipped, never re-granted.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
356
|
+
const store = rankStore({ existing: ['design-lead'] });
|
|
357
|
+
const r = await withRealRankDb(store, () => call('POST', '/government/rank-sets/apply', { type: 'game', headcount: 20 }));
|
|
358
|
+
assert.equal(r.status, 200);
|
|
359
|
+
assert.deepEqual(r.json.skipped, [{ rank_key: 'design-lead', label: 'Design lead' }], 'a kept rank is named by its label, not its key (task 1004478)');
|
|
360
|
+
assert.deepEqual(r.json.created.map((c) => c.rank_key), ['playtester']);
|
|
361
|
+
const { ranks, grants } = store.state;
|
|
362
|
+
assert.deepEqual(ranks.map((x) => [x.rank_key, x.label, x.created_by]), [['design-lead', 'Theirs', 7], ['playtester', 'Playtester', 42]],
|
|
363
|
+
'the kept rank is untouched and created_by is the session builder');
|
|
364
|
+
assert.ok(grants.length > 0 && grants.every((g) => g.rank_key === 'playtester'), 'an existing rank is never re-granted');
|
|
365
|
+
assert.ok(grants.every((g) => catalog.isGrantableTo(g.permission_key, { is_system: false, seeded_rank: null })));
|
|
366
|
+
assert.deepEqual(r.json.created[0].permissions, grants.map((g) => g.permission_key), 'the reply names exactly what was granted');
|
|
367
|
+
assert.equal(store.queries.filter((q) => q === 'BEGIN').length, 1, 'one transaction');
|
|
368
|
+
assert.equal(store.queries.filter((q) => /^INSERT/.test(q)).length, 2, 'one rank insert and one grant insert, however many grants');
|
|
284
369
|
});
|
|
285
370
|
|
|
286
371
|
test('POST /government/rank-sets/apply needs a headcount and does nothing at founding size', async () => {
|
|
287
372
|
const bad = await call('POST', '/government/rank-sets/apply', { type: 'game' });
|
|
288
373
|
assert.equal(bad.status, 400);
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
374
|
+
const store = rankStore();
|
|
375
|
+
const solo = await withRealRankDb(store, () => call('POST', '/government/rank-sets/apply', { type: 'game', headcount: 1 }));
|
|
376
|
+
assert.equal(solo.status, 200);
|
|
377
|
+
assert.deepEqual(solo.json.created, []);
|
|
378
|
+
assert.deepEqual(store.queries, [], 'an empty set touches nothing');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test('POST /government/rank-sets/apply: a failure part-way writes NOTHING (one transaction)', async () => {
|
|
382
|
+
const store = rankStore({ failOnGrant: true });
|
|
383
|
+
const r = await withRealRankDb(store, () => call('POST', '/government/rank-sets/apply', { type: 'business', headcount: 200 }));
|
|
384
|
+
assert.equal(r.status, 500);
|
|
385
|
+
assert.deepEqual(r.json.error.details, { created: [], skipped: [] }, 'nothing was written, so nothing is reported as added');
|
|
386
|
+
assert.deepEqual(store.state, { ranks: [], grants: [] }, 'the ranks inserted before the failure are rolled back');
|
|
387
|
+
assert.ok(store.queries.includes('ROLLBACK'));
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('POST /government/rank-sets/apply: the batched write gives the SAME reply and tables as the old per-row loop', async () => {
|
|
391
|
+
const legacy = require(path.join(ROOT, 'tests', 'fixtures', 'per-row-loops-legacy.cjs'));
|
|
392
|
+
const recommendations = require(path.join(ROOT, 'modules', 'government', 'recommendations.js'));
|
|
393
|
+
const realDb = { getRankByKey: REAL.getRankByKey, createRank: REAL.createRank, grantRankPermission: REAL.grantRankPermission };
|
|
394
|
+
let compared = 0;
|
|
395
|
+
for (const { key: type } of recommendations.PROJECT_TYPES) {
|
|
396
|
+
for (const headcount of [1, 5, 20, 80, 200]) {
|
|
397
|
+
const keys = recommendations.recommendedRankSet(type, headcount).ranks.map((x) => x.rank_key);
|
|
398
|
+
const scenarios = {
|
|
399
|
+
'nothing there yet': {},
|
|
400
|
+
'the first rank exists': { existing: keys.slice(0, 1) },
|
|
401
|
+
'every rank exists': { existing: keys },
|
|
402
|
+
'the last rank is created by someone else mid-call': { race: keys.slice(-1) },
|
|
403
|
+
};
|
|
404
|
+
for (const [name, opts] of Object.entries(scenarios)) {
|
|
405
|
+
const label = `${type} × ${headcount}: ${name}`;
|
|
406
|
+
const oldStore = rankStore(opts);
|
|
407
|
+
const old = await withRealRankDb(oldStore, () => legacy.legacyApplyRankSet({
|
|
408
|
+
db: realDb, catalog, rankSet: recommendations.recommendedRankSet(type, headcount), builderId: 42,
|
|
409
|
+
}));
|
|
410
|
+
const newStore = rankStore(opts);
|
|
411
|
+
const neu = await withRealRankDb(newStore, () => call('POST', '/government/rank-sets/apply', { type, headcount }));
|
|
412
|
+
assert.equal(neu.status, 200, label);
|
|
413
|
+
assert.deepEqual({ created: neu.json.created, skipped: neu.json.skipped }, old, `${label}: the same reply`);
|
|
414
|
+
assert.deepEqual(newStore.state, oldStore.state, `${label}: the same ranks and grants written, in the same order`);
|
|
415
|
+
compared += 1;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
assert.ok(compared >= 20, `compared ${compared} cases`);
|
|
297
420
|
});
|
|
298
421
|
|
|
299
422
|
// --- create role -------------------------------------------------------------
|
package/tests/grader.mjs
CHANGED
|
@@ -789,6 +789,37 @@ test('buildContextBundle reads changed files once, skips binaries/missing, respe
|
|
|
789
789
|
assert.equal(bundle.total_chars, files['src/a.js'].length + files['docs/b.md'].length);
|
|
790
790
|
});
|
|
791
791
|
|
|
792
|
+
// task 1004932 — the bundle rides in EVERY worker prompt, so its total is capped at
|
|
793
|
+
// 60k chars (was 120k), and it is filled most-changed first so the cap drops the
|
|
794
|
+
// lightly touched files, never the heart of the change.
|
|
795
|
+
const CAP_FILE = 15_000; // under the 16k per-file cap, so nothing truncates
|
|
796
|
+
function capFiles(names) { return Object.fromEntries(names.map((n) => [n, `// ${n}\n` + 'x'.repeat(CAP_FILE)])); }
|
|
797
|
+
function diffAdding(counts) {
|
|
798
|
+
return Object.entries(counts).map(([f, n]) =>
|
|
799
|
+
`diff --git a/${f} b/${f}\n--- a/${f}\n+++ b/${f}\n@@ -1 +1,${n} @@\n` +
|
|
800
|
+
Array.from({ length: n }, (_, i) => `+const line_${i}_of_${f.replace(/\W/g, '_')} = ${i};`).join('\n')).join('\n');
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
test('buildContextBundle caps the shared bundle at 60k chars (task 1004932)', () => {
|
|
804
|
+
const names = ['a.js', 'b.js', 'c.js', 'd.js', 'e.js'];
|
|
805
|
+
const files = capFiles(names);
|
|
806
|
+
const bundle = grader.buildContextBundle({ changedFiles: names, cwd: '/r', fsImpl: fakeFsFor(files) });
|
|
807
|
+
assert.ok(bundle.total_chars <= 60_000, `bundle is ${bundle.total_chars} chars — over the 60k cap`);
|
|
808
|
+
assert.ok(bundle.total_chars > 45_000, 'the cap is not lower than 60k: three ~15k files still fit');
|
|
809
|
+
assert.deepEqual(bundle.files.map((f) => f.file), ['a.js', 'b.js', 'c.js'], 'no diff: the caller order is kept');
|
|
810
|
+
assert.deepEqual(bundle.skipped, ['d.js', 'e.js'], 'what does not fit is named in skipped, never silent');
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
test('buildContextBundle fills most-changed first, so the cap drops the lightly touched files (task 1004932)', () => {
|
|
814
|
+
const names = ['docs/index.md', 'docs/map.md', 'tests/x.mjs', 'README.md', 'src/heart.js'];
|
|
815
|
+
const files = capFiles(names);
|
|
816
|
+
const diff = diffAdding({ 'docs/index.md': 1, 'docs/map.md': 1, 'tests/x.mjs': 20, 'README.md': 2, 'src/heart.js': 60 });
|
|
817
|
+
const bundle = grader.buildContextBundle({ changedFiles: names, cwd: '/r', diff, fsImpl: fakeFsFor(files) });
|
|
818
|
+
assert.deepEqual(bundle.files.map((f) => f.file), ['src/heart.js', 'tests/x.mjs', 'README.md'],
|
|
819
|
+
'the file the diff is mostly about comes first, even when it is listed last');
|
|
820
|
+
assert.deepEqual(bundle.skipped, ['docs/index.md', 'docs/map.md'], 'ties keep their listed order, and one-line touches are what falls off');
|
|
821
|
+
});
|
|
822
|
+
|
|
792
823
|
test('buildContextBundle truncates oversized files and marks them', () => {
|
|
793
824
|
const big = 'x'.repeat(20_000);
|
|
794
825
|
const bundle = grader.buildContextBundle({
|
package/tests/hall_city.mjs
CHANGED
|
@@ -289,6 +289,14 @@ test('the city sits in the hall chrome, behind the shell\'s full-screen switch',
|
|
|
289
289
|
// gov-feeds.js, its one reader per page (shared with the shell's tally), load before it
|
|
290
290
|
// task 1004702: the card answers its first ask in place, so the deck's card and sender (and the desk rows under them) load before the page
|
|
291
291
|
assert.deepEqual(scripts, ['dom-utils', 'shell-trail', 'craft-mode', 'shell', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'hall-kit', 'street-draw', 'street-card', 'street-inside', 'street-desk', 'street-deck', 'city-decide', 'city-switchboard', 'city'], 'the shell first, as on every hall page');
|
|
292
|
+
// task 1004925: every script but dom-utils.js is deferred, so none holds up the
|
|
293
|
+
// parser; defer runs them in document order, which is why the order above still
|
|
294
|
+
// holds. dom-utils.js stays synchronous: the one-escaper guard
|
|
295
|
+
// (scripts/bongos/escape-helper-guard.js) requires window.OTB before anything runs.
|
|
296
|
+
const tags = [...html.matchAll(/<script\b[^>]*>/g)].map((m) => m[0]);
|
|
297
|
+
assert.equal(tags.length, scripts.length, 'no inline script runs ahead of the deferred ones');
|
|
298
|
+
assert.doesNotMatch(tags[0], /\b(defer|async)\b/, 'dom-utils.js loads synchronously, first');
|
|
299
|
+
for (const tag of tags.slice(1)) assert.match(tag, /\sdefer(?=[\s>])/, `${tag} is deferred`);
|
|
292
300
|
const css = read('city.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
293
301
|
assert.match(css, /\.city-main \{ position: fixed; top: var\(--topbar-h, 56px\); left: var\(--sidebar-w-rail, 64px\);[^}]*contain: layout paint;/);
|
|
294
302
|
assert.match(css, /html\[data-fullscreen="on"\] \.city-main \{ top: 0; left: 0; \}/);
|
|
@@ -57,13 +57,19 @@ function entries() {
|
|
|
57
57
|
return out;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// A hook may ALSO have an entry for the file-writing tools only (secret-gate does,
|
|
61
|
+
// since task 1004931 moved its shell half into the shell-hooks.js dispatcher). What
|
|
62
|
+
// must never exist is an entry naming SOME shell tools but not all — the bug above.
|
|
60
63
|
test('every shell-reading hook is registered, and its matcher names every shell tool', () => {
|
|
61
64
|
const all = entries();
|
|
62
65
|
for (const hook of SHELL_HOOKS) {
|
|
63
66
|
const found = all.filter((e) => e.command.includes(hook));
|
|
64
67
|
assert.ok(found.length > 0, `${hook} is not registered in settings.json at all`);
|
|
68
|
+
let coversShell = false;
|
|
65
69
|
for (const e of found) {
|
|
66
70
|
const named = e.matcher.split('|').map((s) => s.trim());
|
|
71
|
+
const shellNamed = SHELL_TOOLS.filter((tool) => named.includes(tool));
|
|
72
|
+
if (shellNamed.length === 0) continue; // a file-writing-tools entry
|
|
67
73
|
for (const tool of SHELL_TOOLS) {
|
|
68
74
|
assert.ok(
|
|
69
75
|
named.includes(tool),
|
|
@@ -71,7 +77,9 @@ test('every shell-reading hook is registered, and its matcher names every shell
|
|
|
71
77
|
'that shell tool reaches the hook not at all, silently',
|
|
72
78
|
);
|
|
73
79
|
}
|
|
80
|
+
coversShell = true;
|
|
74
81
|
}
|
|
82
|
+
assert.ok(coversShell, `${hook} has no entry whose matcher names the shell tools`);
|
|
75
83
|
}
|
|
76
84
|
});
|
|
77
85
|
|
package/tests/hooks_drain.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import path from 'node:path';
|
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
|
|
22
22
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
-
const HOOKS = ['recall-hints', 'rank-change-greeting', 'conductor', 'pull-memory', 'edit-hints', 'stuck-detector'].map((n) => path.join(ROOT, '.claude', 'hooks', `${n}.js`));
|
|
23
|
+
const HOOKS = ['recall-hints', 'module-skills', 'rank-change-greeting', 'conductor', 'pull-memory', 'edit-hints', 'stuck-detector'].map((n) => path.join(ROOT, '.claude', 'hooks', `${n}.js`));
|
|
24
24
|
// The SessionStart/SessionEnd scripts that did network work and then process.exit()ed — 59 recorded
|
|
25
25
|
// UV_HANDLE_CLOSING aborts in three days (task 1003548). Same contract: exit 0 on their own, no abort.
|
|
26
26
|
// session-digest requires `pg` at load (via db-kernel), so it can only be exercised where node_modules
|
|
@@ -40,6 +40,7 @@ const ALLOWED = {
|
|
|
40
40
|
first_run_engine_check: '.claude/hooks/session-start.js (package-excluded)',
|
|
41
41
|
hook_core_resolve: '.claude/hooks/_core-resolve.js (package-excluded)',
|
|
42
42
|
'lifecycle-card': '.claude/hooks/lifecycle-card.js (package-excluded)',
|
|
43
|
+
module_skills_hook: '.claude/hooks/module-skills.js (package-excluded)',
|
|
43
44
|
pre_compact: '.claude/hooks/pre-compact.js (package-excluded)',
|
|
44
45
|
preview_server: 'src/preview-server.js (game module, not in the core)',
|
|
45
46
|
recall_hints: '.claude/hooks/recall-hints.js (package-excluded)',
|
|
@@ -47,6 +48,7 @@ const ALLOWED = {
|
|
|
47
48
|
run_routine_two_root: '.claude/hooks/_core-resolve.js (package-excluded)',
|
|
48
49
|
'session-cards': '.claude/hooks/session-cards.js (package-excluded)',
|
|
49
50
|
session_start_freshness: '.claude/hooks/session-start.js (package-excluded)',
|
|
51
|
+
shell_hooks: '.claude/hooks/shell-hooks.js (package-excluded)',
|
|
50
52
|
stop_refcheck: '.claude/hooks/stop-refcheck.js (package-excluded; the task-1002234 precedent)',
|
|
51
53
|
stuck_detector: '.claude/hooks/stuck-strikes.js (package-excluded)',
|
|
52
54
|
worktree_guard: '.claude/hooks/worktree-guard.js (package-excluded)',
|
package/tests/howto_moments.mjs
CHANGED
|
@@ -119,6 +119,23 @@ test('ordinary prompts are not guide requests', () => {
|
|
|
119
119
|
}
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
// Task 1004744: a pasted handoff quoting "skip the tutorial" made the Conductor tell
|
|
123
|
+
// the AI to run `npx bongos howto skip` unasked. Only a prompt that IS the request counts.
|
|
124
|
+
test('a long or multi-line prompt that merely quotes a request is not one', () => {
|
|
125
|
+
const handoff = [
|
|
126
|
+
'Continue the onboarding chain from task 1004636.',
|
|
127
|
+
'Context: the owner said "skip the tutorial" is a valid phrase the guide must accept.',
|
|
128
|
+
'Next: wire the hall card.',
|
|
129
|
+
].join('\n');
|
|
130
|
+
assert.equal(m.guideRequest(handoff), null, 'multi-line handoff');
|
|
131
|
+
const oneLong = 'Reviewing the copy for the how-to card: the button reads "skip the tutorial", which matches what the guide accepts, so leave it.';
|
|
132
|
+
assert.ok(oneLong.length > 100);
|
|
133
|
+
assert.equal(m.guideRequest(oneLong), null, 'one long line');
|
|
134
|
+
assert.equal(m.guideRequest(' skip the tutorial \n'), 'skip', 'surrounding whitespace is still the request');
|
|
135
|
+
const a = howtoAdvisory({ howto: { read: [] }, prompt: handoff, firstPrompt: false, activeClaims: [] });
|
|
136
|
+
assert.ok(!a || !String(a.id).startsWith('howto-request:'), 'the Conductor gives no howto command for the handoff');
|
|
137
|
+
});
|
|
138
|
+
|
|
122
139
|
test('each request tells the AI the command that changes the stored state', () => {
|
|
123
140
|
for (const kind of ['show', 'next', 'skip', 'reset']) assert.match(m.renderRequestForAi(kind), new RegExp(`npx bongos howto ${kind}`));
|
|
124
141
|
});
|
package/tests/module_loader.mjs
CHANGED
|
@@ -278,7 +278,7 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
|
|
|
278
278
|
// That is precisely how the agents regression reached an open PR (task 1002485).
|
|
279
279
|
loader._reset();
|
|
280
280
|
const snap = loader.loaderSnapshot();
|
|
281
|
-
assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'copy-desk', 'design-styles', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'npm-release', 'onboarding', 'page-notes', 'pixel-art', 'platform-identity', 'provisioning', 'public-landing', 'raise', 'render-deploy', 'security', 'sessions', 'specialities', 'status-ui', 'ui-design']);
|
|
281
|
+
assert.deepEqual(snap.loaded, ['agents', 'ai-studio', 'autonomy', 'builder-settings', 'copy-desk', 'design-styles', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'npm-release', 'onboarding', 'page-notes', 'pixel-art', 'platform-identity', 'provisioning', 'public-landing', 'raise', 'render-deploy', 'security', 'sessions', 'specialities', 'status-ui', 'ui-design']);
|
|
282
282
|
assert.deepEqual(snap.errors, []);
|
|
283
283
|
assert.deepEqual(snap.skipped, []);
|
|
284
284
|
loader._reset();
|