@bongos/core 1.19.379 → 1.19.381

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.
Files changed (34) hide show
  1. package/.bongos-core.json +149 -14
  2. package/docs/design/apex-pass-2-direction.md +104 -0
  3. package/docs/design/mocks/world/blue-hud/_tokens.css +246 -0
  4. package/docs/design/mocks/world/blue-hud/hall.html +595 -0
  5. package/docs/design/mocks/world/blue-hud/landing.html +424 -0
  6. package/docs/design/mocks/world/blue-hud/status.html +410 -0
  7. package/docs/design/mocks/world/chrome-blobs/_tokens.css +479 -0
  8. package/docs/design/mocks/world/chrome-blobs/hall.html +553 -0
  9. package/docs/design/mocks/world/chrome-blobs/landing-b.html +581 -0
  10. package/docs/design/mocks/world/chrome-blobs/landing.html +328 -0
  11. package/docs/design/mocks/world/chrome-blobs/status.html +416 -0
  12. package/docs/design/mocks/world/natgeo-editorial/_tokens.css +377 -0
  13. package/docs/design/mocks/world/natgeo-editorial/hall.html +649 -0
  14. package/docs/design/mocks/world/natgeo-editorial/landing-b.html +743 -0
  15. package/docs/design/mocks/world/natgeo-editorial/landing.html +475 -0
  16. package/docs/design/mocks/world/natgeo-editorial/status.html +437 -0
  17. package/docs/design/mocks/world/organic-cards/_tokens.css +462 -0
  18. package/docs/design/mocks/world/organic-cards/hall.html +625 -0
  19. package/docs/design/mocks/world/organic-cards/landing-b.html +736 -0
  20. package/docs/design/mocks/world/organic-cards/landing.html +420 -0
  21. package/docs/design/mocks/world/organic-cards/status.html +458 -0
  22. package/docs/design/mocks/world/world-sheet.html +156 -0
  23. package/docs/design/references/2026-08-26/hall-1-light-mono-label-dashboard.webp +0 -0
  24. package/docs/design/references/2026-08-26/hall-2-dark-glass-dashboard-forest.webp +0 -0
  25. package/docs/design/references/2026-08-26/site-1-natgeo-dark-editorial-planets-expedition-map.jpg +0 -0
  26. package/docs/design/references/2026-08-26/site-2-iridescent-chrome-blobs-warm-cta.jpg +0 -0
  27. package/docs/design/references/2026-08-26/site-3-blue-accent-hud-portfolio.jpg +0 -0
  28. package/docs/design/references/2026-08-26/site-4-green-brown-organic-card-system.jpg +0 -0
  29. package/package-lock.json +2 -2
  30. package/package.json +1 -1
  31. package/scripts/gds/ship-memory-sync.js +62 -5
  32. package/scripts/gds/ship-session-upload.js +12 -10
  33. package/src/module-api.js +5 -1
  34. package/tests/session_records.mjs +122 -5
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.379",
3
+ "version": "1.19.381",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.379",
9
+ "version": "1.19.381",
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.379",
3
+ "version": "1.19.381",
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 as a fallback. Pure (exists + base
65
- // injectable for tests). Returns the first existing path, or null.
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
- for (const dir of projectDirs) {
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(base, 'projects', encodeProjectPath(path.resolve(dir)), `${sessionId}.jsonl`);
72
- if (exists(p)) return p;
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 { claudeConfigBase, encodeProjectPath, resolveTranscriptPath } = require('./ship-memory-sync.js');
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). Reconstruct the exact
121
- // paths resolveTranscriptPath checks so a transcript-not-found skip can name
122
- // precisely where it looked (actionable, not a bare "not found").
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 = sessionIdSafe
125
- ? dirsTried.map((d) =>
126
- path.join(claudeConfigBase(), 'projects', encodeProjectPath(path.resolve(d)), `${sessionId}.jsonl`)
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) {
package/src/module-api.js CHANGED
@@ -1198,8 +1198,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1198
1198
  // landed since 1.19.377 with no explicit bump. run 33025512093. (task 1002620)
1199
1199
  // 1.19.379 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1200
1200
  // landed since 1.19.378 with no explicit bump. run 33026467245. (task 1002620)
1201
+ // 1.19.380 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
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)
1201
1205
  // ---------------------------------------------------------------------------
1202
- const CORE_VERSION = '1.19.379'; // CI auto-patch carrier (ADR 0161); see changelog above
1206
+ const CORE_VERSION = '1.19.381'; // CI auto-patch carrier (ADR 0161); see changelog above
1203
1207
 
1204
1208
  // A namespaced logger so a module's log lines are attributable + consistent.
1205
1209
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -112,27 +112,144 @@ test('snippetOf flattens whitespace, caps length, and scrubs', () => {
112
112
  });
113
113
 
114
114
  // ---------- resolveTranscriptPath (#551: worktree sessions) ----------
115
+ //
116
+ // FIXTURE NOTE: the expected transcript path is DERIVED via encodeProjectPath(
117
+ // path.resolve(dir)) rather than hard-coded as its POSIX encoding. path.resolve
118
+ // is platform-dependent — on win32 '/proj' resolves to 'C:\\\\proj', which encodes
119
+ // to 'C--proj', not '-proj' — so hard-coded encodings made these two tests fail
120
+ // on Windows while passing in Linux CI. Deriving them asserts the same behaviour
121
+ // on both. (POSIX encoding itself is pinned separately below.)
122
+ const sidPath = (base, dir, sid) =>
123
+ path.join(base, 'projects', encodeProjectPath(path.resolve(dir)), `${sid}.jsonl`);
115
124
 
116
125
  test('resolveTranscriptPath prefers the session CWD (worktree) over REPO_ROOT', () => {
117
126
  const sid = 'abc-123';
118
127
  const base = '/fake/.claude';
119
128
  const worktree = '/proj/.claude/worktrees/wt1';
120
129
  const mainRepo = '/proj';
121
- const wtPath = path.join(base, 'projects', '-proj--claude-worktrees-wt1', `${sid}.jsonl`);
130
+ const wtPath = sidPath(base, worktree, sid);
122
131
  // Only the worktree-encoded path exists — the bug looked at REPO_ROOT first.
123
- const got = resolveTranscriptPath(sid, [worktree, mainRepo], { base, existsFn: (p) => p === wtPath });
132
+ const got = resolveTranscriptPath(sid, [worktree, mainRepo], {
133
+ base,
134
+ existsFn: (p) => p === wtPath,
135
+ readdirFn: () => [],
136
+ });
124
137
  assert.equal(got, wtPath);
125
138
  });
126
139
 
127
140
  test('resolveTranscriptPath falls back to REPO_ROOT, else null', () => {
128
141
  const sid = 'abc-123';
129
142
  const base = '/fake/.claude';
130
- const mainPath = path.join(base, 'projects', '-proj', `${sid}.jsonl`);
143
+ const mainPath = sidPath(base, '/proj', sid);
131
144
  assert.equal(
132
- resolveTranscriptPath(sid, ['/proj/.claude/worktrees/wt1', '/proj'], { base, existsFn: (p) => p === mainPath }),
145
+ resolveTranscriptPath(sid, ['/proj/.claude/worktrees/wt1', '/proj'], {
146
+ base,
147
+ existsFn: (p) => p === mainPath,
148
+ readdirFn: () => [],
149
+ }),
133
150
  mainPath
134
151
  );
135
- assert.equal(resolveTranscriptPath(sid, ['/a', '/b'], { base, existsFn: () => false }), null);
152
+ assert.equal(
153
+ resolveTranscriptPath(sid, ['/a', '/b'], { base, existsFn: () => false, readdirFn: () => [] }),
154
+ null
155
+ );
156
+ });
157
+
158
+ // ---------- resolveTranscriptPath scan fallback (task 1002505) ----------
159
+ //
160
+ // The two fast paths above NARROW the miss, they do not seal it: the session
161
+ // driving a ship need not be rooted in process.cwd() OR REPO_ROOT. A session
162
+ // rooted in ANOTHER repo can file a task here and ship it (proven on task
163
+ // 1002475), and before this fallback both probes missed, classifyUploadSkip
164
+ // returned transcript_not_found, no session_record was written, and the
165
+ // cost-plus reward — the only stream this instance pays (ADR 0146) — never
166
+ // booked. The fallback scans every dir under <base>/projects for
167
+ // <sessionId>.jsonl; a session id is a UUID, so a filename hit is unambiguous.
168
+
169
+ test('resolveTranscriptPath finds a transcript rooted in a DIFFERENT project dir (the 1002475 shape)', () => {
170
+ const sid = 'e53f445b-308a-454e-9ac1-9c22d0188a37';
171
+ const base = '/fake/.claude';
172
+ // The orchestrating session lived under an unrelated repo's encoded dir, so
173
+ // neither the claim worktree nor REPO_ROOT could ever match it.
174
+ const foreign = path.join(base, 'projects', 'C--Users-x-Git-repos-other-project', `${sid}.jsonl`);
175
+ const got = resolveTranscriptPath(sid, ['/proj/.claude/worktrees/wt1', '/proj'], {
176
+ base,
177
+ existsFn: (p) => p === foreign,
178
+ readdirFn: () => ['C--Users-x-Git-repos-other-project', '-proj', '-proj--claude-worktrees-wt1'],
179
+ });
180
+ assert.equal(got, foreign);
181
+ });
182
+
183
+ test('resolveTranscriptPath scan only matches the exact session id — no mis-attribution', () => {
184
+ const sid = 'abc-123';
185
+ const base = '/fake/.claude';
186
+ const other = path.join(base, 'projects', '-somewhere-else', 'def-456.jsonl');
187
+ assert.equal(
188
+ resolveTranscriptPath(sid, ['/proj'], {
189
+ base,
190
+ existsFn: (p) => p === other,
191
+ readdirFn: () => ['-somewhere-else'],
192
+ }),
193
+ null
194
+ );
195
+ });
196
+
197
+ test('resolveTranscriptPath does not scan when a fast path hits (precedence unchanged)', () => {
198
+ const sid = 'abc-123';
199
+ const base = '/fake/.claude';
200
+ const wtPath = sidPath(base, '/proj/.claude/worktrees/wt1', sid);
201
+ let scanned = 0;
202
+ const got = resolveTranscriptPath(sid, ['/proj/.claude/worktrees/wt1', '/proj'], {
203
+ base,
204
+ existsFn: (p) => p === wtPath,
205
+ readdirFn: () => { scanned++; return []; },
206
+ });
207
+ assert.equal(got, wtPath);
208
+ assert.equal(scanned, 0, 'the scan must not run once a fast path has matched');
209
+ });
210
+
211
+ test('resolveTranscriptPath fills opts.tried: the fast paths, plus ONE scan summary', () => {
212
+ const sid = 'abc-123';
213
+ const base = '/fake/.claude';
214
+ const tried = [];
215
+ const got = resolveTranscriptPath(sid, ['/proj/.claude/worktrees/wt1', '/proj'], {
216
+ base,
217
+ existsFn: () => false,
218
+ readdirFn: () => ['-a', '-b', '-c'],
219
+ tried,
220
+ });
221
+ assert.equal(got, null);
222
+ // Two exact fast paths...
223
+ assert.equal(tried[0], sidPath(base, '/proj/.claude/worktrees/wt1', sid));
224
+ assert.equal(tried[1], sidPath(base, '/proj', sid));
225
+ // ...then a single bounded summary, not one line per scanned dir, so the
226
+ // transcript_not_found reason stays readable however many projects exist.
227
+ assert.equal(tried.length, 3);
228
+ assert.ok(tried[2].includes('scanned 3 project dirs'), tried[2]);
229
+ });
230
+
231
+ test('resolveTranscriptPath survives an unreadable projects dir and says so', () => {
232
+ const sid = 'abc-123';
233
+ const base = '/fake/.claude';
234
+ const tried = [];
235
+ const got = resolveTranscriptPath(sid, ['/proj'], {
236
+ base,
237
+ existsFn: () => false,
238
+ readdirFn: () => { throw new Error('ENOENT'); },
239
+ tried,
240
+ });
241
+ assert.equal(got, null, 'a missing/unreadable projects dir must not throw into the ship path');
242
+ assert.ok(tried[tried.length - 1].includes('could not read the projects dir'), tried.join(' ; '));
243
+ });
244
+
245
+ test('resolveTranscriptPath tolerates a null/absent projectDirs list', () => {
246
+ const sid = 'abc-123';
247
+ const base = '/fake/.claude';
248
+ const hit = path.join(base, 'projects', '-only-dir', `${sid}.jsonl`);
249
+ assert.equal(
250
+ resolveTranscriptPath(sid, null, { base, existsFn: (p) => p === hit, readdirFn: () => ['-only-dir'] }),
251
+ hit
252
+ );
136
253
  });
137
254
 
138
255
  // ---------- encodeProjectPath (task 1002471: win32 separators) ----------