@bongos/core 1.19.380 → 1.19.382
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 +46 -31
- 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 +6 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +141 -3
- package/docs/api-reference.md +5 -3
- package/docs/copy-inventory.md +14 -9
- package/docs/copy-registry.json +87 -42
- package/migrations/core_228_idea_inbox_captured_by_idx.sql +61 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/hall-ui/public/hall-kit.js +67 -6
- package/modules/hall-ui/public/profile.html +34 -2
- package/modules/hall-ui/public/profile.js +279 -0
- package/modules/ideas/inbox.js +98 -0
- package/modules/ideas/routes/inbox.js +39 -0
- package/modules/lifecycle/activity.js +205 -0
- package/modules/lifecycle/lifecycle.js +7 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/ship-memory-sync.js +62 -5
- package/scripts/gds/ship-session-upload.js +12 -10
- package/scripts/hall-preview/server.js +13 -3
- package/src/bongos/routes/builders.js +67 -0
- package/src/module-api.js +5 -1
- package/tests/profile_activity.mjs +625 -0
- package/tests/session_records.mjs +122 -5
|
@@ -20,6 +20,7 @@ const db = require('./db');
|
|
|
20
20
|
const classifier = require('./task-classifier');
|
|
21
21
|
const helpRequests = require('./help-requests');
|
|
22
22
|
const taskRecommendations = require('./task-recommendations');
|
|
23
|
+
const activity = require('./activity');
|
|
23
24
|
|
|
24
25
|
module.exports = {
|
|
25
26
|
// --- retired doorway leaks (ADR 0093 §3) ---------------------------------
|
|
@@ -58,6 +59,12 @@ module.exports = {
|
|
|
58
59
|
recentGradesForBuilder: (...a) => db.recentGradesForBuilder(...a),
|
|
59
60
|
countShippedTasksForBuilder: (...a) => db.countShippedTasksForBuilder(...a),
|
|
60
61
|
recentShippedTasksByBuilder: (...a) => db.recentShippedTasksByBuilder(...a),
|
|
62
|
+
// builders.js (the profile's per-craft activity log, task 1003281): the paged
|
|
63
|
+
// "what did I do as an engineer / an artist" reads. Separate from the two
|
|
64
|
+
// above because those answer "how much, in total" and these answer "what,
|
|
65
|
+
// by craft" — the axis criterion legibility-activity-by-role asks for.
|
|
66
|
+
activityTasksByBuilder: (...a) => activity.activityTasksByBuilder(...a),
|
|
67
|
+
activityGoalsByBuilder: (...a) => activity.activityGoalsByBuilder(...a),
|
|
61
68
|
// ADR 0120 part 5: the confirmed tasks a builder owes a rebase on — the same
|
|
62
69
|
// read the part-3 claim gate uses, surfaced to me.js so the CLI + hall can
|
|
63
70
|
// show the owed-rebase warning that gates new claims (reward lands on ship).
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.382",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.382",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.382",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -56,21 +56,78 @@ function encodeProjectPath(absProjectPath) {
|
|
|
56
56
|
return String(absProjectPath).replace(/[^a-zA-Z0-9]/g, '-');
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// listProjectDirs — the default directory lister for the scan fallback below.
|
|
60
|
+
// Returns the immediate subdirectory NAMES of ~/.claude/projects, sorted so a
|
|
61
|
+
// scan is deterministic. Injectable (opts.readdirFn) so the fallback is unit
|
|
62
|
+
// testable without a real home dir.
|
|
63
|
+
function listProjectDirs(projectsRoot) {
|
|
64
|
+
return fs
|
|
65
|
+
.readdirSync(projectsRoot, { withFileTypes: true })
|
|
66
|
+
.filter((d) => d.isDirectory())
|
|
67
|
+
.map((d) => d.name)
|
|
68
|
+
.sort();
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
// resolveTranscriptPath — find this session's transcript .jsonl. Claude Code
|
|
60
72
|
// keys the transcript to the project dir of the CWD where the session RUNS, so
|
|
61
73
|
// a session in a git worktree stores it under the WORKTREE-encoded dir, not the
|
|
62
74
|
// main checkout. (6D.1 #540 shipped resolving only REPO_ROOT, which silently
|
|
63
75
|
// missed every worktree session — #551.) We try the session-local dir
|
|
64
|
-
// (process.cwd()) first, then REPO_ROOT
|
|
65
|
-
//
|
|
76
|
+
// (process.cwd()) first, then REPO_ROOT.
|
|
77
|
+
//
|
|
78
|
+
// task 1002505: those two fast paths NARROW the miss, they do not seal it. The
|
|
79
|
+
// session driving a ship need not be rooted in EITHER dir — a session rooted in
|
|
80
|
+
// another repo can file a task here and ship it (proven on task 1002475, whose
|
|
81
|
+
// orchestrating session lived under a different project's encoded dir, so both
|
|
82
|
+
// probed paths missed, no session_record was written, and the cost-plus reward
|
|
83
|
+
// — the only stream this instance pays, ADR 0146 — never booked). So after the
|
|
84
|
+
// fast paths we SCAN every dir under ~/.claude/projects for <sessionId>.jsonl.
|
|
85
|
+
// A session id is a UUID, so a filename hit is unambiguous and cannot
|
|
86
|
+
// mis-attribute; the scan is one stat per project dir, so it stays cheap.
|
|
87
|
+
//
|
|
88
|
+
// Pure-ish (exists + readdir + base injectable for tests). Pass opts.tried an
|
|
89
|
+
// array and every probed path is appended to it — the caller reports that in a
|
|
90
|
+
// transcript_not_found skip, so a genuine miss still names where it looked. The
|
|
91
|
+
// scan contributes ONE summary line, not one line per dir, so the reason stays
|
|
92
|
+
// readable. Returns the first existing path, or null.
|
|
66
93
|
function resolveTranscriptPath(sessionId, projectDirs, opts = {}) {
|
|
67
94
|
const exists = opts.existsFn || fs.existsSync;
|
|
95
|
+
const readdir = opts.readdirFn || listProjectDirs;
|
|
68
96
|
const base = opts.base || claudeConfigBase();
|
|
69
|
-
|
|
97
|
+
const tried = Array.isArray(opts.tried) ? opts.tried : null;
|
|
98
|
+
const projectsRoot = path.join(base, 'projects');
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
|
|
101
|
+
const probe = (p, { record = true } = {}) => {
|
|
102
|
+
if (seen.has(p)) return false;
|
|
103
|
+
seen.add(p);
|
|
104
|
+
if (tried && record) tried.push(p);
|
|
105
|
+
return exists(p);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
for (const dir of projectDirs || []) {
|
|
70
109
|
if (!dir) continue;
|
|
71
|
-
const p = path.join(
|
|
72
|
-
if (
|
|
110
|
+
const p = path.join(projectsRoot, encodeProjectPath(path.resolve(dir)), `${sessionId}.jsonl`);
|
|
111
|
+
if (probe(p)) return p;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Fallback scan. Best-effort: an unreadable/absent projects root is not an
|
|
115
|
+
// error here — it just means the scan finds nothing, same as the fast paths.
|
|
116
|
+
let names;
|
|
117
|
+
try {
|
|
118
|
+
names = readdir(projectsRoot);
|
|
119
|
+
} catch (_) {
|
|
120
|
+
names = null;
|
|
121
|
+
}
|
|
122
|
+
if (!Array.isArray(names)) {
|
|
123
|
+
if (tried) tried.push(`${path.join(projectsRoot, '*')} (could not read the projects dir)`);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
for (const name of names) {
|
|
127
|
+
const p = path.join(projectsRoot, name, `${sessionId}.jsonl`);
|
|
128
|
+
if (probe(p, { record: false })) return p;
|
|
73
129
|
}
|
|
130
|
+
if (tried) tried.push(`${path.join(projectsRoot, '*', `${sessionId}.jsonl`)} (scanned ${names.length} project dirs)`);
|
|
74
131
|
return null;
|
|
75
132
|
}
|
|
76
133
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const path = require('node:path');
|
|
13
13
|
const { cliClient, loadSession } = require('./cli-lib');
|
|
14
|
-
const {
|
|
14
|
+
const { resolveTranscriptPath } = require('./ship-memory-sync.js');
|
|
15
15
|
const { apiErrorLine } = require('./ship-io.js');
|
|
16
16
|
const { REPO_ROOT } = require('./ship-state.js');
|
|
17
17
|
|
|
@@ -117,16 +117,18 @@ async function uploadSessionOnShip(taskId) {
|
|
|
117
117
|
// hostile env value with `../` could otherwise point the read elsewhere.
|
|
118
118
|
const sessionIdSafe = !!sessionId && /^[A-Za-z0-9_-]+$/.test(sessionId);
|
|
119
119
|
// The transcript is keyed to the session's CWD (the worktree when shipping
|
|
120
|
-
// from one), so try that before REPO_ROOT (#551)
|
|
121
|
-
//
|
|
122
|
-
//
|
|
120
|
+
// from one), so try that before REPO_ROOT (#551), then let the resolver scan
|
|
121
|
+
// every ~/.claude/projects dir — a session rooted in ANOTHER repo can drive a
|
|
122
|
+
// ship here, and before task 1002505 that missed both fast paths and silently
|
|
123
|
+
// wrote no session_record at all. resolveTranscriptPath fills pathsTried with
|
|
124
|
+
// exactly what it probed, so a transcript-not-found skip names precisely where
|
|
125
|
+
// it looked (actionable, not a bare "not found") without this file having to
|
|
126
|
+
// re-derive the encoding and drift from it.
|
|
123
127
|
const dirsTried = [process.cwd(), REPO_ROOT].filter(Boolean);
|
|
124
|
-
const pathsTried =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
: [];
|
|
129
|
-
const transcriptPath = sessionIdSafe ? resolveTranscriptPath(sessionId, dirsTried) : null;
|
|
128
|
+
const pathsTried = [];
|
|
129
|
+
const transcriptPath = sessionIdSafe
|
|
130
|
+
? resolveTranscriptPath(sessionId, dirsTried, { tried: pathsTried })
|
|
131
|
+
: null;
|
|
130
132
|
|
|
131
133
|
const decision = classifyUploadSkip({ sessionId, sessionIdSafe, transcriptPath, pathsTried });
|
|
132
134
|
if (decision.skip) {
|
|
@@ -82,6 +82,11 @@ const FIXTURE_FALLBACKS = [
|
|
|
82
82
|
[/^analytics\/builder\/\d+$/, 'analytics__builder'],
|
|
83
83
|
[/^builders\/\d+\/active-claims$/, 'builders__active-claims'],
|
|
84
84
|
[/^builders\/\d+\/profile$/, 'builders__profile'],
|
|
85
|
+
// the profile's per-craft activity log (task 1003281). One fixture per SECTION
|
|
86
|
+
// rather than per id, because the section is what changes the row shape; the
|
|
87
|
+
// craft and the page window are query string, which fixture matching ignores.
|
|
88
|
+
[/^builders\/\d+\/activity$/, (q) => (q.section === 'goals' ? 'builders__activity-goals' : 'builders__activity-tasks')],
|
|
89
|
+
[/^inbox\/by-builder\/\d+$/, 'inbox__by-builder'],
|
|
85
90
|
];
|
|
86
91
|
|
|
87
92
|
function loadFixture(name) {
|
|
@@ -102,12 +107,17 @@ function emptied(v) {
|
|
|
102
107
|
return v;
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
function
|
|
110
|
+
// A fallback name may be a plain string or a function of the query — the one
|
|
111
|
+
// case that needs it (task 1003281) is the profile activity log, where the ROW
|
|
112
|
+
// SHAPE is chosen by ?section=, not by the path, so one fixture per path would
|
|
113
|
+
// render goal rows into the tasks ledger.
|
|
114
|
+
function fixtureFor(apiPath, query = {}) {
|
|
106
115
|
const exact = loadFixture(apiPath.replace(/\//g, '__'));
|
|
107
116
|
if (exact !== undefined) return exact;
|
|
108
117
|
for (const [re, name] of FIXTURE_FALLBACKS) {
|
|
109
118
|
if (re.test(apiPath)) {
|
|
110
|
-
const
|
|
119
|
+
const resolved = typeof name === 'function' ? name(query) : name;
|
|
120
|
+
const hit = resolved && loadFixture(resolved);
|
|
111
121
|
if (hit !== undefined) return hit;
|
|
112
122
|
}
|
|
113
123
|
}
|
|
@@ -141,7 +151,7 @@ app.use((req, res, next) => {
|
|
|
141
151
|
if (req.method !== 'GET') {
|
|
142
152
|
return res.status(501).json({ error: { code: 'preview_readonly', message: 'the preview harness serves fixtures; writes are not wired' } });
|
|
143
153
|
}
|
|
144
|
-
const fix = fixtureFor(apiPath);
|
|
154
|
+
const fix = fixtureFor(apiPath, req.query || {});
|
|
145
155
|
if (fix === undefined) {
|
|
146
156
|
console.log(` no fixture: ${apiPath} (add scripts/hall-preview/fixtures/${apiPath.replace(/\//g, '__')}.json)`);
|
|
147
157
|
return res.status(404).json({ error: { code: 'no_fixture', message: apiPath } });
|
|
@@ -545,6 +545,73 @@ module.exports = function buildBuildersRouter() {
|
|
|
545
545
|
}
|
|
546
546
|
});
|
|
547
547
|
|
|
548
|
+
// GET /builders/:id/activity — the profile's PER-CRAFT activity log (task
|
|
549
|
+
// 1003281, goal 1000072, criterion legibility-activity-by-role).
|
|
550
|
+
//
|
|
551
|
+
// WHAT IT ADDS OVER /profile. The profile answers "how much has this person
|
|
552
|
+
// shipped" — one count and the last five works, craft-blind. A person's artist
|
|
553
|
+
// track record is a different question from their engineer one, so this route
|
|
554
|
+
// serves ONE section (`tasks` or `goals`) for ONE craft, paged. It EXTENDS the
|
|
555
|
+
// profile surface rather than forking it: the page is the same page, the
|
|
556
|
+
// identity band and the achievement shelf are unchanged, and this fills the
|
|
557
|
+
// ledger below them. The third section of that log — the IDEA TRAIL — is
|
|
558
|
+
// served by the ideas module's GET /inbox/by-builder/:id, because an idea is
|
|
559
|
+
// its domain's row, not a lifecycle one.
|
|
560
|
+
//
|
|
561
|
+
// ONE SECTION PER CALL, not all three in one payload: each ledger pages
|
|
562
|
+
// independently, and a single response carrying three page windows would need
|
|
563
|
+
// three limit/offset pairs on one query string to say anything at all.
|
|
564
|
+
//
|
|
565
|
+
// rank: any-builder, NOT owner-only — a track record is a thing other people
|
|
566
|
+
// read; that is what makes the profile a portfolio rather than a dashboard, and
|
|
567
|
+
// it is the same posture /profile, /builders/roster and the leaderboard already
|
|
568
|
+
// take toward the same rows. The per-viewer filtering that DOES matter is the
|
|
569
|
+
// private-goal wall below, and it is applied to the DATA, not to the caller.
|
|
570
|
+
//
|
|
571
|
+
// But, unlike /profile directly above, this read IS gated to a sign-in.
|
|
572
|
+
// The goals half is viewer-dependent: a private goal's title is exactly what
|
|
573
|
+
// ADR 0112 says a non-member must not learn, so the read needs to know who is
|
|
574
|
+
// asking. Serving it publicly would mean either leaking private goals or
|
|
575
|
+
// silently omitting the viewer's own — both worse than a sign-in. The tasks
|
|
576
|
+
// half is the same already-public shipped-work data /profile returns.
|
|
577
|
+
router.get('/builders/:id/activity', auth.requireBuilder, async (req, res) => {
|
|
578
|
+
const targetId = parseId(req, res, { code: 'bad_builder_id', positiveInt: true });
|
|
579
|
+
if (targetId === null) return;
|
|
580
|
+
const section = req.query.section === 'goals' ? 'goals' : 'tasks';
|
|
581
|
+
// An unknown craft is normalized to "no filter" inside the module read
|
|
582
|
+
// rather than 400ing here — a stale bookmark should show the whole record,
|
|
583
|
+
// not an error card in the middle of a profile.
|
|
584
|
+
const role = typeof req.query.role === 'string' && req.query.role ? req.query.role : null;
|
|
585
|
+
const limit = Number(req.query.limit);
|
|
586
|
+
const offset = Number(req.query.offset);
|
|
587
|
+
try {
|
|
588
|
+
const b = await db.getBuilderById(targetId);
|
|
589
|
+
if (!b) return res.fail('builder_not_found', 404);
|
|
590
|
+
const viewer = req.builder || {};
|
|
591
|
+
const out = section === 'goals'
|
|
592
|
+
? await lc().activityGoalsByBuilder(targetId, {
|
|
593
|
+
role,
|
|
594
|
+
viewerId: viewer.id ?? null,
|
|
595
|
+
viewerIsArchon: viewer.rank === 'archon',
|
|
596
|
+
limit,
|
|
597
|
+
offset,
|
|
598
|
+
})
|
|
599
|
+
: await lc().activityTasksByBuilder(targetId, { role, limit, offset });
|
|
600
|
+
// Viewer-dependent (the goals half) and cheap to recompute — never cached.
|
|
601
|
+
res.set('Cache-Control', 'no-store');
|
|
602
|
+
res.json({
|
|
603
|
+
builder_id: targetId,
|
|
604
|
+
section,
|
|
605
|
+
role: role || null,
|
|
606
|
+
rows: out.rows,
|
|
607
|
+
total: out.total,
|
|
608
|
+
});
|
|
609
|
+
} catch (err) {
|
|
610
|
+
log.error('[gds] GET /builders/:id/activity', err);
|
|
611
|
+
res.fail('activity_read_failed', { status: 500, message: 'internal error' });
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
548
615
|
// GET /builders/me/pending-rank-change — V3.R28 / #249 ceremony signal #3.
|
|
549
616
|
//
|
|
550
617
|
// Returns the calling builder's most recent rank-change credit_log row, or
|
package/src/module-api.js
CHANGED
|
@@ -1200,8 +1200,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1200
1200
|
// landed since 1.19.378 with no explicit bump. run 33026467245. (task 1002620)
|
|
1201
1201
|
// 1.19.380 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1202
1202
|
// landed since 1.19.379 with no explicit bump. run 33085516721. (task 1002620)
|
|
1203
|
+
// 1.19.381 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1204
|
+
// landed since 1.19.380 with no explicit bump. run 33088290961. (task 1002620)
|
|
1205
|
+
// 1.19.382 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1206
|
+
// landed since 1.19.381 with no explicit bump. run 33094545251. (task 1002620)
|
|
1203
1207
|
// ---------------------------------------------------------------------------
|
|
1204
|
-
const CORE_VERSION = '1.19.
|
|
1208
|
+
const CORE_VERSION = '1.19.382'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1205
1209
|
|
|
1206
1210
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1207
1211
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|