@bongos/core 1.19.50 → 1.19.52

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.50",
6
- "core_contract": "1.19.50",
7
- "source_commit": "1a7b11b4377e9fefb36560c0cda9d4b1030a05c9",
5
+ "core_version": "1.19.52",
6
+ "core_contract": "1.19.52",
7
+ "source_commit": "ce8e0d15694aed30b1640891c4256c96322a2a6f",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-11T18:44:57.114Z",
9
+ "built_at": "2026-08-11T19:06:37.776Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 291,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1217,
14
+ "functional_verbatim": 1218,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1529,
20
- "tree_sha256": "e96a0d29e2d6adc516aaa0d095dbb1c4d543dbec0508228b2cbcf16a1b400dbd",
19
+ "file_count": 1530,
20
+ "tree_sha256": "72953759b072de03029b81979095699aeb2d45b2363ba7047d8503457761956f",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -367,7 +367,7 @@
367
367
  {
368
368
  "path": "clients/bongos-client/index.d.ts",
369
369
  "mode": "0000644",
370
- "sha256": "649065862779b6a9b871607debfee5ca18bde8cccbde4d00ea42b0d175938c9d"
370
+ "sha256": "a5b9642b383c8c415158e091785ff40f28490d8341b4010f4c051b1a68942daf"
371
371
  },
372
372
  {
373
373
  "path": "clients/bongos-client/index.mjs",
@@ -1307,12 +1307,12 @@
1307
1307
  {
1308
1308
  "path": "docs/api-reference.md",
1309
1309
  "mode": "0000644",
1310
- "sha256": "8f83ea78edb348688196414bf2fb7352df3de0ded6abbbe44f2371c099431016"
1310
+ "sha256": "27e9c0ab46db5386833b9e03bcff0dae31f4606882e845e7e830f9ef6f2550b5"
1311
1311
  },
1312
1312
  {
1313
1313
  "path": "docs/api/openapi.json",
1314
1314
  "mode": "0000644",
1315
- "sha256": "27cf9c2b8548168f13dd2add1633bc9b3ca57f2fd5aa3b42ac8dbfb5cf60c41f"
1315
+ "sha256": "612c7a81756f61f2c18d4b35540379cf63f32a4caa9e8d84a64b247d497ad7a3"
1316
1316
  },
1317
1317
  {
1318
1318
  "path": "docs/architecture.md",
@@ -2502,7 +2502,7 @@
2502
2502
  {
2503
2503
  "path": "modules/dev-box/app/src/gds-client.js",
2504
2504
  "mode": "0000644",
2505
- "sha256": "845f51622803363dbc1229c325c4d6148087c756812e7e8e049ced7aa479f168"
2505
+ "sha256": "323f5e71b7836dd1fb6c7710e1b2159dec6382df89a7c5d001b7e7cccd5aa46d"
2506
2506
  },
2507
2507
  {
2508
2508
  "path": "modules/dev-box/app/src/main.js",
@@ -3387,7 +3387,7 @@
3387
3387
  {
3388
3388
  "path": "modules/lifecycle/db.js",
3389
3389
  "mode": "0000644",
3390
- "sha256": "57c88852ff0aa9f0b3673fa62b35f352d92cc9070e7872137003e395c42eb026"
3390
+ "sha256": "8e67cd1325da44cf95d5d8c34427222e555e0458bb17f23f33c1055b1ae7e211"
3391
3391
  },
3392
3392
  {
3393
3393
  "path": "modules/lifecycle/done-when.js",
@@ -3492,7 +3492,7 @@
3492
3492
  {
3493
3493
  "path": "modules/lifecycle/routes/tasks.js",
3494
3494
  "mode": "0000644",
3495
- "sha256": "8a6ecd6c7ecadcbec79483a589fef11957d13e2c94b569a9ff9da97f2908b713"
3495
+ "sha256": "68b4f47e3a5016645893c60eca2fe11a242012abf6625c99d6a774356d82d1ce"
3496
3496
  },
3497
3497
  {
3498
3498
  "path": "modules/lifecycle/routes/versions.js",
@@ -4037,12 +4037,12 @@
4037
4037
  {
4038
4038
  "path": "package-lock.json",
4039
4039
  "mode": "0000644",
4040
- "sha256": "ffd1f91d40f2098324c3054cd3accdfdf210a668972dcb8702104cd11ed6c4dc"
4040
+ "sha256": "37bb9183f916ffafd19c5c88408f5f776bdb091afc46b1bfbad64669d642102f"
4041
4041
  },
4042
4042
  {
4043
4043
  "path": "package.json",
4044
4044
  "mode": "0000644",
4045
- "sha256": "cd444deb3940b96b8afedb89f5ece513d842d4a9148b3a50304d8d77866dd904"
4045
+ "sha256": "80465cb4506affe5fc47c835df7a0f4f1634a0743c2de6d14f843f342d487c45"
4046
4046
  },
4047
4047
  {
4048
4048
  "path": "public-docs/index.html",
@@ -5632,7 +5632,7 @@
5632
5632
  {
5633
5633
  "path": "src/bongos/serve-internal.js",
5634
5634
  "mode": "0000644",
5635
- "sha256": "25c52c06c03af400c94f04f2cda1aaab0c5e5486a084005b9a4e224f345a4871"
5635
+ "sha256": "2f88796346b1c3a6dc49e25766b223fbf2d99e34b38b9a0f1e64a6fb9a59856d"
5636
5636
  },
5637
5637
  {
5638
5638
  "path": "src/bongos/uptime-poller.js",
@@ -5662,7 +5662,7 @@
5662
5662
  {
5663
5663
  "path": "src/module-api.js",
5664
5664
  "mode": "0000644",
5665
- "sha256": "2d8b78d34893b1e4c25d081c343ad1b0d1deb2a675cae9a3c4e96500c6cea821"
5665
+ "sha256": "fc779475caf64dd09a7a3f952b4dadfcf2f123ca1a3d0ea9af20762b3de5e6c8"
5666
5666
  },
5667
5667
  {
5668
5668
  "path": "src/module-loader/loader.js",
@@ -6247,7 +6247,7 @@
6247
6247
  {
6248
6248
  "path": "tests/devbox_downloads.mjs",
6249
6249
  "mode": "0000644",
6250
- "sha256": "91950e7477f9fff30e361bc32def211e1cb3792fa098e9c839d5db1e1ba21d6a"
6250
+ "sha256": "897fe6b114c612e132e6fa0b3c68f43b865c684c7653e73cf42db894a7e4dc94"
6251
6251
  },
6252
6252
  {
6253
6253
  "path": "tests/devbox_icons.mjs",
@@ -6264,6 +6264,11 @@
6264
6264
  "mode": "0000644",
6265
6265
  "sha256": "ac0a024d96ef40af52af8b94570447ad5f1556467b68380c1b45fd3419139ad6"
6266
6266
  },
6267
+ {
6268
+ "path": "tests/devbox_update_path.mjs",
6269
+ "mode": "0000644",
6270
+ "sha256": "b58e87eeca17053709a8e2bdcefd032060358cc85314d28bf816a92d465fdae8"
6271
+ },
6267
6272
  {
6268
6273
  "path": "tests/device_flow_start.mjs",
6269
6274
  "mode": "0000644",
@@ -6882,7 +6887,7 @@
6882
6887
  {
6883
6888
  "path": "tests/migration_allocation.mjs",
6884
6889
  "mode": "0000644",
6885
- "sha256": "08db201ebec44ea5a711e3c86e36dda63ec22881a400e910dd7c64961c80f6e9"
6890
+ "sha256": "6755c9568cd6cdb7b8600ae2c6651856d63c053684dcb8b765f7329590a9fc08"
6886
6891
  },
6887
6892
  {
6888
6893
  "path": "tests/migration_namespace.mjs",
@@ -131,7 +131,7 @@ export interface PatchMeSkillPrefsResponse { prefs: unknown; effective: unknown;
131
131
  export interface PatchMeSoundPrefsResponse { prefs: unknown; effective: unknown; effective_map: unknown; volume: unknown; defaults: unknown; known_sounds: unknown }
132
132
  export interface PatchProjectsIdFeaturedRequest { featured: boolean }
133
133
  export interface PatchProjectsIdFeaturedResponse { project: unknown }
134
- export interface PatchTasksIdRequest { kind?: string; discipline?: string; requires_rank?: string; module_key?: string; goal_id?: number; parent_task_id?: StringifiableId; parallel_safe?: unknown; newcomer_friendly?: boolean; xenos_claimable?: boolean; security_sensitive?: boolean; automation_tag?: string; needs_migration?: boolean; status?: unknown }
134
+ export interface PatchTasksIdRequest { kind?: string; discipline?: string; requires_rank?: string; module_key?: string; goal_id?: number; parent_task_id?: StringifiableId; parallel_safe?: unknown; newcomer_friendly?: boolean; xenos_claimable?: boolean; security_sensitive?: boolean; automation_tag?: string; needs_migration?: boolean; description?: string; status?: unknown }
135
135
  export interface PostAccessRequestsRequest { github_login: string; display_name?: string; note?: string }
136
136
  export interface PostAccessRequestsResponse { ok: boolean; request?: unknown; already_pending?: boolean }
137
137
  export interface PostAuthAppPairApproveRequest { pair_code: string }
@@ -13792,6 +13792,11 @@
13792
13792
  "needs_migration": {
13793
13793
  "type": "boolean"
13794
13794
  },
13795
+ "description": {
13796
+ "type": "string",
13797
+ "minLength": 1,
13798
+ "maxLength": 50000
13799
+ },
13795
13800
  "status": {}
13796
13801
  },
13797
13802
  "additionalProperties": false
@@ -463,7 +463,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
463
463
  | GET | `/api/bongos/tasks` | `any-builder` | — | reads (filtered task list); preserved per CRITICAL §4. |
464
464
  | POST | `/api/bongos/tasks` | `metic+archon` | `version_id`, `title`, `description`, `status`, `touches`, `est_minutes`, `est_cost_usd`, `manual_degree`, `priority`, `automation_tag`, `credits_reward`, `source`, `source_ref`, `value_summary`, `kind`, `discipline`, `parallel_safe`, `newcomer_friendly`, `security_sensitive`, `requires_rank`, `auto_detect_touches`, `criterion_ids`, `module_key`, `goal_id` | Task create/promote/edit (plan/scope mutation) is Metic+ as of ADR 0090 (owner decision 2026-06-26): a Metic who can run idea-triage may … |
465
465
  | GET | `/api/bongos/tasks/:id` | `any-builder` | — | single-task read; preserved per CRITICAL §4 (supervisor hot path). |
466
- | PATCH | `/api/bongos/tasks/:id` | `metic+archon` | `kind`, `discipline`, `requires_rank`, `module_key`, `goal_id`, `parent_task_id`, `parallel_safe`, `newcomer_friendly`, `xenos_claimable`, `security_sensitive`, `automation_tag`, `needs_migration`, `status` | PATCH /tasks/:id — supports body.parent_task_id (V2 C2), body.kind (V2 B1), body.discipline (mig 014), body.parallel_safe (mig 028), and … |
466
+ | PATCH | `/api/bongos/tasks/:id` | `metic+archon` | `kind`, `discipline`, `requires_rank`, `module_key`, `goal_id`, `parent_task_id`, `parallel_safe`, `newcomer_friendly`, `xenos_claimable`, `security_sensitive`, `automation_tag`, `needs_migration`, `description`, `status` | PATCH /tasks/:id — supports body.parent_task_id (V2 C2), body.kind (V2 B1), body.discipline (mig 014), body.parallel_safe (mig 028), and … |
467
467
  | POST | `/api/bongos/tasks/:id/abandon` | `metic+archon` | `reason` | POST /tasks/:id/abandon — terminal "retire this task" transition (task 987). |
468
468
  | POST | `/api/bongos/tasks/:id/attest-gate` | `any-builder` | `head_sha` | POST /tasks/:id/attest-gate — body: { head_sha }. |
469
469
  | POST | `/api/bongos/tasks/:id/confirm` | `any-builder` | `verified` | |
@@ -82,8 +82,12 @@ function createClient({ getToken, getApiBase, fetchImpl } = {}) {
82
82
  claimableTasks: () => unwrap(gen('').request('GET', '/api/gds/tasks/claimable')),
83
83
  claimTask: (taskId) => unwrap(gen('').request('POST', '/api/gds/claims', { body: { task_id: taskId } })),
84
84
  boxScope: () => unwrap(gen('').request('GET', '/api/gds/box/scope')),
85
- // update check (public static asset at the origin root, not under /api/gds)
86
- latestRelease: () => unwrap(gen('').request('GET', '/downloads/devbox/latest.json')),
85
+ // update check (public route at the origin root, not under /api/gds).
86
+ // task 1002649: polls the EXTENSIONLESS /version twin, NOT latest.json — a
87
+ // front proxy with a file-matcher can eat *.json paths before Node (the
88
+ // live instance's pre-ADR-0152 vhost 404'd latest.json empty while this
89
+ // endpoint worked), and the updater only needs { version, assets } anyway.
90
+ latestRelease: () => unwrap(gen('').request('GET', '/downloads/devbox/version')),
87
91
  };
88
92
  }
89
93
 
@@ -1477,7 +1477,7 @@ async function findTaskBySourceRef({ source, sourceRef } = {}, deps = {}) {
1477
1477
 
1478
1478
  const TASK_SETTER_COLUMNS = new Set([
1479
1479
  'kind', 'discipline', 'newcomer_friendly', 'security_sensitive', 'parallel_safe', 'automation_tag',
1480
- 'needs_migration', 'module_key',
1480
+ 'needs_migration', 'module_key', 'description',
1481
1481
  ]);
1482
1482
 
1483
1483
  async function updateTaskColumn(id, column, value) {
@@ -1532,6 +1532,12 @@ async function updateTaskGoal(id, goalId) {
1532
1532
 
1533
1533
  async function updateTaskKind(id, kind) { return updateTaskColumn(id, 'kind', kind); }
1534
1534
 
1535
+ // task 1002647 (choke-point map s4.3): the task body is a load-bearing prompt —
1536
+ // it was settable only at create, un-editable at every rank. Editable now under
1537
+ // the same task.edit permission as the other planning fields; the global
1538
+ // write-audit middleware captures each edit's new body in audit_log.
1539
+ async function updateTaskDescription(id, description) { return updateTaskColumn(id, 'description', description); }
1540
+
1535
1541
 
1536
1542
  async function updateTaskDiscipline(id, discipline) { return updateTaskColumn(id, 'discipline', discipline); }
1537
1543
 
@@ -5457,6 +5463,7 @@ module.exports = {
5457
5463
  updateTaskGoal,
5458
5464
  createCriterion,
5459
5465
  updateTaskKind,
5466
+ updateTaskDescription,
5460
5467
  updateTaskDiscipline,
5461
5468
  updateTaskNewcomerFriendly,
5462
5469
  updateTaskRequiresRank,
@@ -793,6 +793,12 @@ module.exports = function buildTasksRouter() {
793
793
  security_sensitive: { type: 'boolean' }, // task 990 — Archon-only ship broadcast
794
794
  automation_tag: { type: 'string', maxLength: 64 },
795
795
  needs_migration: { type: 'boolean' }, // task 1002648 — correct the create-time heuristic's flag (the claim-time allocator keys off it)
796
+ // task 1002647 (map s4.3): the task body is a load-bearing builder prompt
797
+ // and was settable only at create — no route edited it at any rank. Now
798
+ // editable under this route's task.edit permission. minLength 1: a body
799
+ // can be rewritten, never blanked (and null is refused by the type check).
800
+ // Each edit's new body lands in audit_log via the write-audit middleware.
801
+ description: { type: 'string', minLength: 1, maxLength: LIMITS.DESCRIPTION },
796
802
  // task 1002614 (map D-18): DECLARED so strict-mode admits the request far
797
803
  // enough to reach the deliberate status_not_patchable hint below (#514) —
798
804
  // the hint had been unreachable dead code behind unknown_field since the
@@ -818,6 +824,7 @@ module.exports = function buildTasksRouter() {
818
824
  const hasRequiresRank = Object.prototype.hasOwnProperty.call(body, 'requires_rank');
819
825
  const hasModuleKey = Object.prototype.hasOwnProperty.call(body, 'module_key');
820
826
  const hasGoalId = Object.prototype.hasOwnProperty.call(body, 'goal_id');
827
+ const hasDescription = Object.prototype.hasOwnProperty.call(body, 'description');
821
828
  // #514: status is a lifecycle field, not a PATCH-able column — it moves only
822
829
  // through the dedicated transition routes. The #506 session tried
823
830
  // `PATCH status=ready`, got the generic no_supported_fields error, and had
@@ -829,8 +836,8 @@ module.exports = function buildTasksRouter() {
829
836
  if (Object.prototype.hasOwnProperty.call(body, 'status')) {
830
837
  return res.fail('status_not_patchable', { status: 400, message: STATUS_HINT });
831
838
  }
832
- if (!hasParent && !hasKind && !hasDiscipline && !hasNewcomerOrXenos && !hasParallelSafe && !hasSecuritySensitive && !hasAutomationTag && !hasNeedsMigration && !hasRequiresRank && !hasModuleKey && !hasGoalId) {
833
- return res.fail('no_supported_fields', { status: 400, message: 'PATCH /tasks/:id supports parent_task_id, kind, discipline, parallel_safe, security_sensitive, automation_tag, needs_migration, requires_rank, module_key, goal_id, and newcomer_friendly (alias: xenos_claimable). ' + STATUS_HINT });
839
+ if (!hasParent && !hasKind && !hasDiscipline && !hasNewcomerOrXenos && !hasParallelSafe && !hasSecuritySensitive && !hasAutomationTag && !hasNeedsMigration && !hasRequiresRank && !hasModuleKey && !hasGoalId && !hasDescription) {
840
+ return res.fail('no_supported_fields', { status: 400, message: 'PATCH /tasks/:id supports parent_task_id, kind, discipline, description, parallel_safe, security_sensitive, automation_tag, needs_migration, requires_rank, module_key, goal_id, and newcomer_friendly (alias: xenos_claimable). ' + STATUS_HINT });
834
841
  }
835
842
  // ---- task 1002609 (G-22): settle EVERY gate and validation before the ----
836
843
  // ---- FIRST write. The previous shape validated-and-wrote per field in ----
@@ -976,6 +983,15 @@ module.exports = function buildTasksRouter() {
976
983
  return res.fail('patch_failed', { status: 500, message: 'internal error' });
977
984
  }
978
985
  }
986
+ if (hasDescription) {
987
+ try {
988
+ updated = await db.updateTaskDescription(id, body.description);
989
+ if (!updated) return res.fail('task_not_found', 404);
990
+ } catch (err) {
991
+ console.error('[gds] PATCH /tasks/:id description', err);
992
+ return res.fail('patch_failed', { status: 500, message: 'internal error' });
993
+ }
994
+ }
979
995
  if (hasGoalId) {
980
996
  try {
981
997
  updated = await db.updateTaskGoal(id, body.goal_id);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.50",
3
+ "version": "1.19.52",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.50",
9
+ "version": "1.19.52",
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.50",
3
+ "version": "1.19.52",
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",
@@ -359,6 +359,38 @@ function isBuildersHost(req) {
359
359
  // Call this ONCE, right after app setup (trust proxy etc.) and BEFORE any
360
360
  // entrypoint-specific tail (the game static + themed 404 in server.js; the plain
361
361
  // 404 in platform-server.js).
362
+ // The Dev Box download/manifest mini-router (#904, ADR 0045), extracted so the
363
+ // ACTUAL route handlers are hermetically testable (tests/devbox_update_path.mjs
364
+ // mounts them on a throwaway app — the docs_openapi_route pattern) instead of
365
+ // only their pure builder. task 1002649: the manifest is served under BOTH
366
+ // /latest.json and an extensionless /latest twin — a front proxy with a
367
+ // file-matcher can eat *.json paths before Node (the live pre-ADR-0152 vhost
368
+ // 404'd latest.json empty while extensionless siblings worked), so every
369
+ // manifest consumer has a path that survives that class. Registration order
370
+ // matters: the literal routes must precede the `:target` catch-all.
371
+ function mountDevboxDownloads(app, devboxDownloads) {
372
+ const manifest = (_req, res) => {
373
+ // Short TTL: a release bump (config change + deploy) is visible within a
374
+ // minute, same posture as the hall's HTML.
375
+ res.set('Cache-Control', 'public, max-age=60');
376
+ res.json(devboxDownloads.latestJson());
377
+ };
378
+ app.get('/downloads/devbox/latest.json', manifest);
379
+ app.get('/downloads/devbox/latest', manifest);
380
+ // #924: the lightweight update-check endpoint the Dev Box app polls + the hall
381
+ // /settings courtesy line reads.
382
+ app.get('/downloads/devbox/version', (_req, res) => {
383
+ res.set('Cache-Control', 'public, max-age=60');
384
+ res.json(devboxDownloads.versionJson());
385
+ });
386
+ app.get('/downloads/devbox/:target', (req, res) => {
387
+ const url = devboxDownloads.redirectFor(req.params.target);
388
+ if (!url) return res.fail('unknown_target', 404, { targets: Object.keys(devboxDownloads.latestJson().assets) });
389
+ res.set('Cache-Control', 'public, max-age=60');
390
+ res.redirect(302, url);
391
+ });
392
+ }
393
+
362
394
  function mountInternalSurfaces(app) {
363
395
  // Prometheus instrumentation (task #1680, V4.R28a). The counting middleware
364
396
  // mounts FIRST so it wraps every surface registered below (and, since
@@ -477,26 +509,7 @@ function mountInternalSurfaces(app) {
477
509
  // /builders/downloads/* and 404).
478
510
  const devboxCfg = tryLoadInstanceConfig('devbox-app.json');
479
511
  if (devboxCfg) {
480
- const devboxDownloads = buildDevboxDownloads(devboxCfg);
481
- app.get('/downloads/devbox/latest.json', (_req, res) => {
482
- // Short TTL: a release bump (config change + deploy) is visible within a
483
- // minute, same posture as the hall's HTML.
484
- res.set('Cache-Control', 'public, max-age=60');
485
- res.json(devboxDownloads.latestJson());
486
- });
487
- // #924: the lightweight update-check endpoint the Dev Box app polls + the hall
488
- // /settings courtesy line reads. MUST be registered before the `:target` route
489
- // below, or Express would treat "version" as a download target and 404 it.
490
- app.get('/downloads/devbox/version', (_req, res) => {
491
- res.set('Cache-Control', 'public, max-age=60');
492
- res.json(devboxDownloads.versionJson());
493
- });
494
- app.get('/downloads/devbox/:target', (req, res) => {
495
- const url = devboxDownloads.redirectFor(req.params.target);
496
- if (!url) return res.fail('unknown_target', 404, { targets: Object.keys(devboxDownloads.latestJson().assets) });
497
- res.set('Cache-Control', 'public, max-age=60');
498
- res.redirect(302, url);
499
- });
512
+ mountDevboxDownloads(app, buildDevboxDownloads(devboxCfg));
500
513
  }
501
514
 
502
515
  // Standalone `bongos` CLI downloads + the one-line installer (task 1248,
@@ -979,4 +992,4 @@ function mountInternalSurfaces(app) {
979
992
  return app;
980
993
  }
981
994
 
982
- module.exports = { mountInternalSurfaces, isStatusHost, isBuildersHost, injectBrandingGlobal, injectBrandingInto, injectModuleHallWidgets, themeMarkup, applyBrandTokens, resolveOpenapiJson, openapiJson, tryLoadInstanceConfig };
995
+ module.exports = { mountInternalSurfaces, mountDevboxDownloads, isStatusHost, isBuildersHost, injectBrandingGlobal, injectBrandingInto, injectModuleHallWidgets, themeMarkup, applyBrandTokens, resolveOpenapiJson, openapiJson, tryLoadInstanceConfig };
package/src/module-api.js CHANGED
@@ -527,8 +527,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
527
527
  // landed since 1.19.48 with no explicit bump. run 31523559509. (task 1002620)
528
528
  // 1.19.50 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
529
529
  // landed since 1.19.49 with no explicit bump. run 31524280584. (task 1002620)
530
+ // 1.19.51 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
531
+ // landed since 1.19.50 with no explicit bump. run 31525032273. (task 1002620)
532
+ // 1.19.52 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
533
+ // landed since 1.19.51 with no explicit bump. run 31526131183. (task 1002620)
530
534
  // ---------------------------------------------------------------------------
531
- const CORE_VERSION = '1.19.50'; // CI auto-patch carrier (ADR 0161); see changelog above
535
+ const CORE_VERSION = '1.19.52'; // CI auto-patch carrier (ADR 0161); see changelog above
532
536
 
533
537
  // A namespaced logger so a module's log lines are attributable + consistent.
534
538
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -119,5 +119,22 @@ t('config/devbox-app.json parses, resolves every target, and aliases point at re
119
119
  }
120
120
  });
121
121
 
122
+ console.log('update manifest (task 1002649 — the payload the devbox-app updater reads):');
123
+
124
+ t('committed config yields a NON-EMPTY manifest — version + at least one asset in both shapes', () => {
125
+ // The live symptom behind task 1002649 was latest.json arriving as an empty
126
+ // body. The route is a pure function of this committed config, so pin here
127
+ // that the manifest can never be empty at the source.
128
+ const d = buildDevboxDownloads(committedConfig);
129
+ for (const shape of [d.latestJson(), d.versionJson()]) {
130
+ assert.match(shape.version, /^\d+\.\d+\.\d+$/, 'manifest carries a semver version');
131
+ assert.ok(Object.keys(shape.assets).length >= 1, 'manifest carries at least one asset');
132
+ }
133
+ });
134
+
135
+ // The client-path pin (extensionless /version, never .json) lives in
136
+ // tests/devbox_update_path.mjs — NOT here — so it still runs on the neutral
137
+ // core checkout where this file topology-skips (task 1002649).
138
+
122
139
  console.log(`\n${passed} passed, ${failed} failed`);
123
140
  process.exit(failed === 0 ? 0 : 1);
@@ -0,0 +1,84 @@
1
+ // tests/devbox_update_path.mjs — the devbox-app update-check path pin (task 1002649).
2
+ //
3
+ // Deliberately SEPARATE from tests/devbox_downloads.mjs: that file topology-
4
+ // skips wherever config/devbox-app.json is absent (this neutral core checkout),
5
+ // but this pin reads only the app client's SOURCE, so it must run everywhere —
6
+ // including this repo's `unit` lane.
7
+ //
8
+ // Why the pin exists: a front proxy with a file-matcher can eat *.json paths
9
+ // before they reach Node — the live pre-ADR-0152 vhost 404'd
10
+ // /downloads/devbox/latest.json with an empty body while the extensionless
11
+ // /downloads/devbox/version twin worked, silently breaking the app's update
12
+ // check. The updater only needs { version, assets }, which /version carries.
13
+ //
14
+ // Run: node --test tests/devbox_update_path.mjs
15
+
16
+ import { strict as assert } from 'node:assert';
17
+ import { test } from 'node:test';
18
+ import { readFileSync } from 'node:fs';
19
+ import { createRequire } from 'node:module';
20
+
21
+ const require = createRequire(import.meta.url);
22
+ const src = readFileSync(new URL('../modules/dev-box/app/src/gds-client.js', import.meta.url), 'utf8');
23
+
24
+ test('latestRelease polls the extensionless /downloads/devbox/version, never a .json path', () => {
25
+ const line = src.split('\n').find((l) => l.includes('latestRelease:'));
26
+ assert.ok(line, 'gds-client exposes latestRelease');
27
+ assert.ok(line.includes("'/downloads/devbox/version'"), 'latestRelease polls /downloads/devbox/version');
28
+ assert.ok(!line.includes('latest.json'), 'latestRelease must not poll a .json-suffixed path');
29
+ });
30
+
31
+ // ---- The ACTUAL route handlers, over loopback (the docs_openapi_route ----
32
+ // pattern). This is the layer the repo owns: the Express routes must serve a
33
+ // NON-EMPTY manifest on all three paths and redirect targets. (The infra layer
34
+ // above Node — the host's front proxy — cannot be pinned from a unit test;
35
+ // that is blocker 1000105. These pins guarantee that when a request reaches
36
+ // Node, the manifest is never empty.)
37
+ const CFG = {
38
+ releases_repo: 'owner/devbox-releases',
39
+ version: '9.9.9',
40
+ assets: { 'win-x64': 'Setup-9.9.9-x64.exe', 'mac-arm64': 'App-9.9.9-arm64.dmg' },
41
+ aliases: { windows: 'win-x64', mac: 'mac-arm64' },
42
+ };
43
+
44
+ async function withDevboxServer(fn) {
45
+ const express = require('express');
46
+ const { mountDevboxDownloads } = require('../src/bongos/serve-internal.js');
47
+ const { buildDevboxDownloads } = require('../src/devbox-downloads.js');
48
+ const app = express();
49
+ mountDevboxDownloads(app, buildDevboxDownloads(CFG));
50
+ const server = await new Promise((resolve) => { const s = app.listen(0, () => resolve(s)); });
51
+ try {
52
+ await fn(`http://127.0.0.1:${server.address().port}`);
53
+ } finally {
54
+ await new Promise((r) => server.close(r));
55
+ }
56
+ }
57
+
58
+ test('latest.json and the extensionless /latest twin both serve the full non-empty manifest', async () => {
59
+ await withDevboxServer(async (base) => {
60
+ for (const p of ['/downloads/devbox/latest.json', '/downloads/devbox/latest']) {
61
+ const r = await fetch(base + p);
62
+ assert.equal(r.status, 200, `${p} must 200`);
63
+ assert.match(r.headers.get('content-type') || '', /application\/json/, `${p} content-type`);
64
+ const j = await r.json();
65
+ assert.equal(j.version, '9.9.9');
66
+ assert.ok(Object.keys(j.assets).length >= 1, `${p} manifest must carry assets — the task-1002649 empty-body class`);
67
+ assert.equal(j.assets['win-x64'].file, 'Setup-9.9.9-x64.exe');
68
+ assert.match(j.assets['win-x64'].url, /\/releases\/download\/v9\.9\.9\//);
69
+ }
70
+ });
71
+ });
72
+
73
+ test('/version serves the flat update-check payload; a target redirects 302', async () => {
74
+ await withDevboxServer(async (base) => {
75
+ const v = await fetch(base + '/downloads/devbox/version');
76
+ assert.equal(v.status, 200);
77
+ const j = await v.json();
78
+ assert.equal(j.version, '9.9.9');
79
+ assert.equal(typeof j.assets['win-x64'], 'string', 'flat {target: url} shape');
80
+ const r = await fetch(base + '/downloads/devbox/windows', { redirect: 'manual' });
81
+ assert.equal(r.status, 302);
82
+ assert.match(r.headers.get('location') || '', /Setup-9\.9\.9-x64\.exe$/);
83
+ });
84
+ });
@@ -197,6 +197,7 @@ function shouldReserveMigrationNumber(task) {
197
197
  [() => db.updateTaskAutomationTag(1, 'live-only'), 'automation_tag'],
198
198
  [() => db.updateTaskNeedsMigration(1, false), 'needs_migration'],
199
199
  [() => db.updateTaskModuleKey(1, null), 'module_key'],
200
+ [() => db.updateTaskDescription(1, 'edited body'), 'description'], // task 1002647
200
201
  ];
201
202
  for (const [call, column] of setters) {
202
203
  const before = seen.length;