@bongos/core 1.19.785 → 1.19.787
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 +18 -18
- package/.claude/skills/planning-session/SKILL.md +3 -3
- package/docs/module-api-changelog.md +4 -0
- package/modules/lifecycle/db-deps-criteria.js +35 -8
- package/modules/lifecycle/routes/task-write-routes.js +7 -3
- package/modules/lifecycle/routes/tasks.js +4 -1
- package/modules/security/secret-scrub.js +23 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +7 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +9 -0
- package/src/module-api.js +1 -1
- package/tests/criterion_ref_parse.mjs +83 -4
- package/tests/scrubber_corpus.mjs +59 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.787",
|
|
6
|
+
"core_contract": "1.19.787",
|
|
7
|
+
"source_commit": "b32b78533de72aa607032c7eeea369d98103ef3b",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-16T22:
|
|
9
|
+
"built_at": "2026-09-16T22:32:51.862Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 483,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2668,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "6633332cd057cd360e74849250fa2f7ab2458f7f683c8c6315c4820d19d06ac7",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
{
|
|
208
208
|
"path": ".claude/skills/planning-session/SKILL.md",
|
|
209
209
|
"mode": "0000644",
|
|
210
|
-
"sha256": "
|
|
210
|
+
"sha256": "ede2aa15718bff19d38ffa7f42d64bae7490a9025f7e85d43c43306da3f611b6"
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
213
|
"path": ".claude/skills/priority-session/SKILL.md",
|
|
@@ -2512,7 +2512,7 @@
|
|
|
2512
2512
|
{
|
|
2513
2513
|
"path": "docs/module-api-changelog.md",
|
|
2514
2514
|
"mode": "0000644",
|
|
2515
|
-
"sha256": "
|
|
2515
|
+
"sha256": "3c0fb2e250b9dca8299cae204683c9fb815421e52206b44472b94952d7e61ede"
|
|
2516
2516
|
},
|
|
2517
2517
|
{
|
|
2518
2518
|
"path": "docs/modules-contract.md",
|
|
@@ -5447,7 +5447,7 @@
|
|
|
5447
5447
|
{
|
|
5448
5448
|
"path": "modules/lifecycle/db-deps-criteria.js",
|
|
5449
5449
|
"mode": "0000644",
|
|
5450
|
-
"sha256": "
|
|
5450
|
+
"sha256": "2fefa2efa05b2a63ee510345180c04d211d5068c8bc82c050392dac3d02cfac2"
|
|
5451
5451
|
},
|
|
5452
5452
|
{
|
|
5453
5453
|
"path": "modules/lifecycle/db-goals.js",
|
|
@@ -5737,12 +5737,12 @@
|
|
|
5737
5737
|
{
|
|
5738
5738
|
"path": "modules/lifecycle/routes/task-write-routes.js",
|
|
5739
5739
|
"mode": "0000644",
|
|
5740
|
-
"sha256": "
|
|
5740
|
+
"sha256": "9cff3bcd27467dc413aa975cd20262b47e8595a50a845d93b557a24828719bbf"
|
|
5741
5741
|
},
|
|
5742
5742
|
{
|
|
5743
5743
|
"path": "modules/lifecycle/routes/tasks.js",
|
|
5744
5744
|
"mode": "0000644",
|
|
5745
|
-
"sha256": "
|
|
5745
|
+
"sha256": "0497ab99a5c13399617b43f70d775e77773f1eed259548433f19c20c708ebab9"
|
|
5746
5746
|
},
|
|
5747
5747
|
{
|
|
5748
5748
|
"path": "modules/lifecycle/routes/version-route-authz.js",
|
|
@@ -7062,7 +7062,7 @@
|
|
|
7062
7062
|
{
|
|
7063
7063
|
"path": "modules/security/secret-scrub.js",
|
|
7064
7064
|
"mode": "0000644",
|
|
7065
|
-
"sha256": "
|
|
7065
|
+
"sha256": "c28b9a47e3d01d206cb2a13e71844865a95cc2a9dc5e0237e1153ccf8f292286"
|
|
7066
7066
|
},
|
|
7067
7067
|
{
|
|
7068
7068
|
"path": "modules/security/security.js",
|
|
@@ -7622,12 +7622,12 @@
|
|
|
7622
7622
|
{
|
|
7623
7623
|
"path": "package-lock.json",
|
|
7624
7624
|
"mode": "0000644",
|
|
7625
|
-
"sha256": "
|
|
7625
|
+
"sha256": "c99209b9bfd8f2191eb0e622e45d4875760a65c7ae4f8653758667e11bd0bb66"
|
|
7626
7626
|
},
|
|
7627
7627
|
{
|
|
7628
7628
|
"path": "package.json",
|
|
7629
7629
|
"mode": "0000644",
|
|
7630
|
-
"sha256": "
|
|
7630
|
+
"sha256": "fae2586d07f9b98f2631be427db59eb76c7149b49a01c404af51a40edde911a3"
|
|
7631
7631
|
},
|
|
7632
7632
|
{
|
|
7633
7633
|
"path": "public-docs/index.html",
|
|
@@ -8702,7 +8702,7 @@
|
|
|
8702
8702
|
{
|
|
8703
8703
|
"path": "scripts/gds/seed-bongos-v1-idea-routing-tasks.js",
|
|
8704
8704
|
"mode": "0000644",
|
|
8705
|
-
"sha256": "
|
|
8705
|
+
"sha256": "fc30acb5a295289c446b5fd7dd79bcd4e2ba8a3b90bbe1c426fecc087bbefadd"
|
|
8706
8706
|
},
|
|
8707
8707
|
{
|
|
8708
8708
|
"path": "scripts/gds/seed-bongos-v1-seo-tasks.js",
|
|
@@ -8727,7 +8727,7 @@
|
|
|
8727
8727
|
{
|
|
8728
8728
|
"path": "scripts/gds/seed-bv1-profiles-tasks.js",
|
|
8729
8729
|
"mode": "0000644",
|
|
8730
|
-
"sha256": "
|
|
8730
|
+
"sha256": "53353029323de1e200b11bceccf822f524a19ff857cf17f6fa4b1ada73408c40"
|
|
8731
8731
|
},
|
|
8732
8732
|
{
|
|
8733
8733
|
"path": "scripts/gds/seed-bv1-r01-delta-tasks.js",
|
|
@@ -9492,7 +9492,7 @@
|
|
|
9492
9492
|
{
|
|
9493
9493
|
"path": "src/module-api.js",
|
|
9494
9494
|
"mode": "0000644",
|
|
9495
|
-
"sha256": "
|
|
9495
|
+
"sha256": "37250a4442445fd5b73d199d17c2646944e633f7816a366ef7f6477168c6c2bd"
|
|
9496
9496
|
},
|
|
9497
9497
|
{
|
|
9498
9498
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10342,7 +10342,7 @@
|
|
|
10342
10342
|
{
|
|
10343
10343
|
"path": "tests/criterion_ref_parse.mjs",
|
|
10344
10344
|
"mode": "0000644",
|
|
10345
|
-
"sha256": "
|
|
10345
|
+
"sha256": "80ce711abf62a78dc4f36b513b852a5396173a2ff50e5deab2460531d17911e4"
|
|
10346
10346
|
},
|
|
10347
10347
|
{
|
|
10348
10348
|
"path": "tests/criterion_suggest.mjs",
|
|
@@ -12557,7 +12557,7 @@
|
|
|
12557
12557
|
{
|
|
12558
12558
|
"path": "tests/scrubber_corpus.mjs",
|
|
12559
12559
|
"mode": "0000644",
|
|
12560
|
-
"sha256": "
|
|
12560
|
+
"sha256": "d1f80894bd96f866359d3d1997e36e8843fe3dba141ac621874cdc1d530b9282"
|
|
12561
12561
|
},
|
|
12562
12562
|
{
|
|
12563
12563
|
"path": "tests/seam_wiring_guard.mjs",
|
|
@@ -172,7 +172,7 @@ Don't tunnel-vision. Surface lateral options the user might not have thought of:
|
|
|
172
172
|
- **Criteria (done-when)** — the goal's criteria verbatim by slug (C1, C2, …); every task links to one.
|
|
173
173
|
- **Design decisions & assumptions** — what we're committing to and why; note grounding from the codebase (existing modules/tables/routes to reuse). Starts from your best defaults; the Phase-4 interview fills and revises this.
|
|
174
174
|
- **Open questions (for the interview)** — the non-obvious decisions still unresolved, each with why-it-matters + the options you see. This is the raw material for Phase 4; empty it as answers land.
|
|
175
|
-
- **Task outline** — grouped by criterion (and phase, if phased), one row per task: `R## · kind · title · one-line done-when · deps:[R##] · criterion_ids:[
|
|
175
|
+
- **Task outline** — grouped by criterion (and phase, if phased), one row per task: `R## · kind · title · one-line done-when · deps:[R##] · criterion_ids:[<criterion-slug>]`, tagged `[E]`/`[I]`/`[N]`. Mark **spikes** (needs-exploration) and **proofs** (the live demo per criterion).
|
|
176
176
|
- **Dependency graph & ranking** — filled in Phase 4 (topo order + priority tiers).
|
|
177
177
|
- **Out of scope** — what this goal explicitly does NOT do, so the interview and tasks stay bounded.
|
|
178
178
|
|
|
@@ -269,10 +269,10 @@ Once the spec is finalized (Phase 4), **read `docs/specs/<ver>-<goal-slug>.md` a
|
|
|
269
269
|
- **Migration** at `migrations/<NN>_<ver_slug>_planning_seed.sql` (CORE — platform-universal, non-scope-truth changes only): `UPDATE versions SET done_when = ...` and `UPDATE tasks` for existing-task priority + V##.R## title prefix (guarded by `WHERE title NOT LIKE 'V<ver>.R%'`). **Do NOT put versions/goals/`done_when_criteria` INSERTs here** — that's scope-truth and fails the `selfhost-boot` gate (ADR 0105).
|
|
270
270
|
|
|
271
271
|
3. **Write a seed script** at `scripts/gds/seed-<ver_slug>-tasks.js` modeled on `scripts/gds/seed-v3-tasks.js` containing:
|
|
272
|
-
- The TASKS array — one entry per net-new task with full description, kind, discipline, priority, est_minutes, manual_degree, credits_reward, **optional** `touches` (see below), optional `promoted_idea_id`, `depends_on_refs: ['R##', ...]` (the dep edges captured during brainstorm), AND **`criterion_ids: ['Cn', ...]`** — the done-when criterion(s) this task gates. The brainstorm in Phase 3 is already grouped by criterion, so this is just transcribing that grouping. Use the positional `"Cn"` token
|
|
272
|
+
- The TASKS array — one entry per net-new task with full description, kind, discipline, priority, est_minutes, manual_degree, credits_reward, **optional** `touches` (see below), optional `promoted_idea_id`, `depends_on_refs: ['R##', ...]` (the dep edges captured during brainstorm), AND **`criterion_ids: ['Cn', ...]`** — the done-when criterion(s) this task gates. The brainstorm in Phase 3 is already grouped by criterion, so this is just transcribing that grouping. **Use the criterion_id SLUG, not a positional `"Cn"` token.** `Cn` ranks over the whole VERSION's criteria, so on a version holding more than one goal it resolves to some other goal's criterion — which is how all 15 tasks of goal 1000071 were mislinked while the seed printed success (learning 1000188). Since task 1003108 the server **refuses** a positional ref on a multi-goal version (`400 ambiguous_criterion_ref`) rather than guessing, so a slug is the only ref that works there. The server resolves it against the task's version at create (ADR 0025 / task 438). **This replaces the old `**Criterion:** Cn` prose** — don't write that line anymore; pass `criterion_ids` instead so the task is linked in `task_criteria` at creation and `/status` counts it with no backfill.
|
|
273
273
|
- **`touches` is now OPTIONAL (task 882 / [ADR 0049](../../../docs/adr/0049-split-parallel-safety-contract.md)).** Omit it (or leave it blank) unless you already know a tight file set and want the advisory overlap signal during the brainstorm. At first `/builder-ship` the server backfills `touches[]` from the real committed diff — the declared set becomes what git actually reported. Don't burn planning effort predicting file lists; a rough or absent prediction is fine and no longer ship-blocking.
|
|
274
274
|
- **`requires_rank` AUTO-DERIVES — you don't hand-set it per task (task [#1498](https://example.com/builders#/task/1498) / [ADR 0084](../../../docs/adr/0084-auto-derived-per-task-rank-floor.md)).** On create the server floors a task at `metic` when it is `security_sensitive` or its `touches` reach the permission / authz / ship-grade / deploy / migration core (the same protected paths a sub-Metic builder can't push to); everything else stays the open `xenos` default. To deliberately gate a task **higher** than the auto-floor (e.g. owner-only work), pass `requires_rank: 'metic' | 'archon'` on the task — it can only RAISE the floor, never drop below it.
|
|
275
|
-
- **Pass `goal_id: <gid>` on each task** so it lands under its goal (not the version's default catch-all). `POST /tasks` now honors `goal_id` (validated to belong to the version); to move an already-created task, `PATCH /tasks/:id` with `{goal_id}` (task 1763). `criterion_ids` accepts a **slug** (the `criterion_id` you set above), a `"Cn"` position
|
|
275
|
+
- **Pass `goal_id: <gid>` on each task** so it lands under its goal (not the version's default catch-all). `POST /tasks` now honors `goal_id` (validated to belong to the version); to move an already-created task, `PATCH /tasks/:id` with `{goal_id}` (task 1763). `criterion_ids` accepts a **slug** (the `criterion_id` you set above), a numeric id, or a `"Cn"` position — **but `Cn` is rejected outright once the version holds a second goal**, and a version almost always gains one. Treat the slug as the only form you write.
|
|
276
276
|
- A main() that POSTs each task **with its `criterion_ids`** (idempotent on `source_ref`; the criterion links are inserted at create, idempotent on `(task_id, criterion_id)`), PATCHes `idea_inbox` for promotions, and after all tasks exist, POSTs `task_dependencies` via `POST /tasks/:id/dependencies` (resolving each R## to the just-created task ID). Idempotent on `(task_id, depends_on_task_id)`. (Need to re-attribute an already-seeded task? `POST /tasks/:id/criteria` with `{criterion_ids:['Cn',...]}`; `DELETE /tasks/:id/criteria/:criterionId` to detach — both Metic+ ([ADR 0090](../../../docs/adr/0090-metic-task-authoring.md)), mirroring the dependency endpoints.)
|
|
277
277
|
|
|
278
278
|
> **Seeding contract — validated on goal 1000045 (2026-07-16):** proof-tasks use `kind='verify'` (a deliverable that IS a verification; can't be prematurely confirmed), spikes `kind='spike'`; `POST /tasks` accepts `criterion_ids` + `goal_id` inline and disciplines include `ui`; there is **no `promoted_idea_id`** field on `POST /tasks` — mark a promoted idea via `PATCH /inbox/:id`; **descoping a criterion has no API** (done-when is create/satisfy/unsatisfy only) — remove the row via a direct DB delete (`ssh … "sudo -u postgres psql -d <db> -c \"DELETE FROM done_when_criteria WHERE id=…\""`) under the meta-task claim, capturing the SQL in its notes; and a seed script **must throttle + honor `429` `retry_after`** (per-IP rate limit) — add a base delay between calls and back off on 429, or a large seed dies halfway. A completed reference seed: `scripts/gds/<redacted>.js`.
|
|
@@ -2044,5 +2044,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2044
2044
|
landed since 1.19.783 with no explicit bump. run 35155326585. (task 1002620)
|
|
2045
2045
|
1.19.785 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2046
2046
|
landed since 1.19.784 with no explicit bump. run 35156349018. (task 1002620)
|
|
2047
|
+
1.19.786 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2048
|
+
landed since 1.19.785 with no explicit bump. run 35157132070. (task 1002620)
|
|
2049
|
+
1.19.787 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2050
|
+
landed since 1.19.786 with no explicit bump. run 35158163962. (task 1002620)
|
|
2047
2051
|
---------------------------------------------------------------------------
|
|
2048
2052
|
```
|
|
@@ -286,13 +286,26 @@ function parseCriterionRef(raw) {
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
// Resolve an array of refs to done_when_criteria.id values within a version.
|
|
289
|
-
// Returns { ids: [unique numeric ids, first-appearance order], unresolved: [refs]
|
|
290
|
-
|
|
291
|
-
|
|
289
|
+
// Returns { ids: [unique numeric ids, first-appearance order], unresolved: [refs],
|
|
290
|
+
// ambiguous: [positional refs refused because the version holds >1 goal] }.
|
|
291
|
+
//
|
|
292
|
+
// WHY ambiguous exists (task 1003108). A positional "Cn" ranks over the WHOLE
|
|
293
|
+
// version's criteria. That is unambiguous only while the version holds ONE goal:
|
|
294
|
+
// add a second and the "C1" a planner reads off a per-goal spec silently resolves
|
|
295
|
+
// to some other goal's criterion — a wrong link, not an error. It mislinked all 15
|
|
296
|
+
// tasks of goal 1000071 (2026-08-17, learning 1000188) and the seed still printed
|
|
297
|
+
// success. Task 1003076 added the after-the-fact tally that catches the symptom;
|
|
298
|
+
// this refuses the ref at the write, so the mislink cannot be created. Slugs are
|
|
299
|
+
// position-independent and stay legal on every version, which is why the skill now
|
|
300
|
+
// requires them. `executor` is the same testability seam countTasksWithoutCriterionLink
|
|
301
|
+
// uses — a fake SQL-recording client needs no live Postgres.
|
|
302
|
+
|
|
303
|
+
async function resolveCriterionIds(versionId, refs, executor = pool) {
|
|
292
304
|
const ids = [];
|
|
293
305
|
const unresolved = [];
|
|
294
|
-
|
|
295
|
-
|
|
306
|
+
const ambiguous = [];
|
|
307
|
+
if (!Array.isArray(refs) || refs.length === 0) return { ids, unresolved, ambiguous };
|
|
308
|
+
const { rows } = await executor.query(
|
|
296
309
|
`SELECT id, criterion_id,
|
|
297
310
|
ROW_NUMBER() OVER (ORDER BY sort_order, criterion_id) AS cnum
|
|
298
311
|
FROM done_when_criteria
|
|
@@ -302,17 +315,31 @@ async function resolveCriterionIds(versionId, refs) {
|
|
|
302
315
|
const byId = new Set(rows.map((r) => Number(r.id)));
|
|
303
316
|
const byCnum = new Map(rows.map((r) => [Number(r.cnum), Number(r.id)]));
|
|
304
317
|
const bySlug = new Map(rows.map((r) => [String(r.criterion_id), Number(r.id)]));
|
|
318
|
+
// Only pay for the goal count when a positional ref is actually present — the
|
|
319
|
+
// slug path (the one the skill now requires) costs no extra query.
|
|
320
|
+
let multiGoal = false;
|
|
321
|
+
if (refs.some((r) => parseCriterionRef(r).kind === 'cnum')) {
|
|
322
|
+
const { rows: g } = await executor.query(
|
|
323
|
+
'SELECT COUNT(*)::int AS n FROM goals WHERE version_id = $1',
|
|
324
|
+
[versionId]
|
|
325
|
+
);
|
|
326
|
+
multiGoal = Number(g[0] ? g[0].n : 0) > 1;
|
|
327
|
+
}
|
|
305
328
|
const seen = new Set();
|
|
306
329
|
for (const raw of refs) {
|
|
307
330
|
const p = parseCriterionRef(raw);
|
|
308
331
|
let id = null;
|
|
309
|
-
if (p.kind === 'cnum')
|
|
310
|
-
|
|
332
|
+
if (p.kind === 'cnum') {
|
|
333
|
+
// Refuse rather than guess: resolving it would produce a plausible-looking
|
|
334
|
+
// wrong link that no later check attributes to the ref.
|
|
335
|
+
if (multiGoal) { ambiguous.push(String(raw).trim()); continue; }
|
|
336
|
+
id = byCnum.get(p.n) ?? null;
|
|
337
|
+
} else if (p.kind === 'id') id = byId.has(p.id) ? p.id : null;
|
|
311
338
|
else id = bySlug.get(p.slug) ?? null;
|
|
312
339
|
if (id == null) { unresolved.push(String(raw).trim()); continue; }
|
|
313
340
|
if (!seen.has(id)) { seen.add(id); ids.push(id); }
|
|
314
341
|
}
|
|
315
|
-
return { ids, unresolved };
|
|
342
|
+
return { ids, unresolved, ambiguous };
|
|
316
343
|
}
|
|
317
344
|
|
|
318
345
|
// countTasksWithoutCriterionLink — task 1003157's DONE WHEN item 2: the count of
|
|
@@ -62,8 +62,9 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
62
62
|
requires_rank: { type: 'string', maxLength: 16 }, // task 1498 / ADR 0084 — per-task rank floor (auto-derived; explicit value can only raise it)
|
|
63
63
|
auto_detect_touches: { type: 'boolean' },
|
|
64
64
|
// task 438: link the new task to done-when criteria at creation time. Each
|
|
65
|
-
// element is a
|
|
66
|
-
//
|
|
65
|
+
// element is a criterion_id slug (required on a multi-goal version — task
|
|
66
|
+
// 1003108), a numeric done_when_criteria.id, or a positional "Cn" token —
|
|
67
|
+
// resolved against this task's version below (ADR 0025).
|
|
67
68
|
criterion_ids: { type: 'array', maxItems: 100 },
|
|
68
69
|
// BV1.R53 (task 1422): module affinity label. Drives module-disjointness at
|
|
69
70
|
// claim time and diff⊆module drift detection at ship time.
|
|
@@ -119,7 +120,10 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
119
120
|
// is unambiguous and a numeric id from another version doesn't resolve.
|
|
120
121
|
let criterionIdsResolved = [];
|
|
121
122
|
if (Array.isArray(body.criterion_ids) && body.criterion_ids.length > 0) {
|
|
122
|
-
const { ids, unresolved } = await db.resolveCriterionIds(body.version_id, body.criterion_ids);
|
|
123
|
+
const { ids, unresolved, ambiguous } = await db.resolveCriterionIds(body.version_id, body.criterion_ids);
|
|
124
|
+
if (ambiguous.length > 0) {
|
|
125
|
+
return res.fail('ambiguous_criterion_ref', { status: 400, message: `Version '${body.version_id}' holds more than one goal, so a positional "Cn" ref ranks over the WHOLE version's criteria — not this goal's — and would link to the wrong criterion silently (task 1003108). Re-send these as criterion_id slugs: ${ambiguous.join(', ')}.`, details: { ambiguous } });
|
|
126
|
+
}
|
|
123
127
|
if (unresolved.length > 0) {
|
|
124
128
|
return res.fail('bad_criterion_ref', { status: 400, message: `These criterion refs don't match any criterion of version '${body.version_id}': ` +
|
|
125
129
|
`${unresolved.join(', ')}. Use a numeric done_when_criteria.id, a "Cn" position, or a criterion_id slug.`, details: { unresolved } });
|
|
@@ -452,7 +452,10 @@ module.exports = function buildTasksRouter() {
|
|
|
452
452
|
|
|
453
453
|
const task = await db.getTask(id);
|
|
454
454
|
if (!task) return res.fail('task_not_found', 404);
|
|
455
|
-
const { ids, unresolved } = await db.resolveCriterionIds(task.version_id, refs);
|
|
455
|
+
const { ids, unresolved, ambiguous } = await db.resolveCriterionIds(task.version_id, refs);
|
|
456
|
+
if (ambiguous.length > 0) {
|
|
457
|
+
return res.fail('ambiguous_criterion_ref', { status: 400, message: `Version '${task.version_id}' holds more than one goal, so a positional "Cn" ref ranks over the WHOLE version's criteria — not this goal's — and would link to the wrong criterion silently (task 1003108). Re-send these as criterion_id slugs: ${ambiguous.join(', ')}.`, details: { ambiguous } });
|
|
458
|
+
}
|
|
456
459
|
if (unresolved.length > 0) {
|
|
457
460
|
return res.fail('bad_criterion_ref', { status: 400, message: `These criterion refs don't match any criterion of version '${task.version_id}': ${unresolved.join(', ')}.`, details: { unresolved } });
|
|
458
461
|
}
|
|
@@ -190,12 +190,34 @@ const ENV_SECRET_PREFIX_RE = /^(?:gds_|pms_|mac_|apple|anthropic|stripe|cf_|clou
|
|
|
190
190
|
// last `cd` (task 2130). Exact-match only, so real password vars (MYSQL_PWD, DB_PWD, …)
|
|
191
191
|
// still match below.
|
|
192
192
|
const ENV_BENIGN_KEY_RE = /^(?:PWD|OLDPWD)$/;
|
|
193
|
+
// CONFIG-IDENTIFIER NAMES under a provider prefix (task 1003512). ENV_SECRET_PREFIX_RE
|
|
194
|
+
// matches ^anthropic, so the Claude Code harness's own ANTHROPIC_DEFAULT_OPUS_MODEL —
|
|
195
|
+
// whose value is a MODEL ID like claude-opus-5, not a credential — was collected as a
|
|
196
|
+
// live secret. package-core's fail-closed leak gate then flagged every functional file
|
|
197
|
+
// containing that string verbatim and refused to build, on ANY machine where Claude
|
|
198
|
+
// Code had set its model vars, i.e. every builder machine. Hit live packaging
|
|
199
|
+
// 1.19.209; the workaround was `env -u ANTHROPIC_DEFAULT_OPUS_MODEL`.
|
|
200
|
+
//
|
|
201
|
+
// AN ALLOWLIST ON THE NAME, NOT A SHAPE TEST ON THE VALUE. The entropy/prefix check on
|
|
202
|
+
// the VALUE is the other obvious fix and this file already records why that direction
|
|
203
|
+
// loses: see the doc-path exemption above, where three shape rules were tried and each
|
|
204
|
+
// one leaked, because a predicate built on what a secret LOOKS like also admits a
|
|
205
|
+
// secret dressed the other way. These literals feed the PUBLIC mirror and session
|
|
206
|
+
// uploads, so a shape hole here is a smuggling channel. That a _MODEL or _BASE_URL var
|
|
207
|
+
// holds a configuration identifier is a fact about the NAME, which cannot be dressed.
|
|
208
|
+
//
|
|
209
|
+
// NARROW BY CONSTRUCTION: this exempts only names matched by the provider PREFIX. A
|
|
210
|
+
// name carrying an actual secret word (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN) matches
|
|
211
|
+
// ENV_SECRET_KEY_RE first and is still collected, whatever it ends with.
|
|
212
|
+
const ENV_CONFIG_NAME_RE = /(?:_MODEL|_BASE_URL)$/i;
|
|
193
213
|
function collectEnvSecrets(env = process.env) {
|
|
194
214
|
const out = [];
|
|
195
215
|
for (const [k, v] of Object.entries(env || {})) {
|
|
196
216
|
if (typeof v !== 'string' || v.length < 8) continue;
|
|
197
217
|
if (ENV_BENIGN_KEY_RE.test(k)) continue;
|
|
198
|
-
|
|
218
|
+
// A secret WORD in the name always wins — it is never exempted below.
|
|
219
|
+
if (ENV_SECRET_KEY_RE.test(k)) { out.push(v); continue; }
|
|
220
|
+
if (ENV_SECRET_PREFIX_RE.test(k) && !ENV_CONFIG_NAME_RE.test(k)) out.push(v);
|
|
199
221
|
}
|
|
200
222
|
return out;
|
|
201
223
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.787",
|
|
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.787",
|
|
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.787",
|
|
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",
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// DO NOT COPY THE `crit:` TOKENS FROM THIS FILE (task 1003108) — copy its
|
|
2
|
+
// verifySeedCriteriaLinks tail instead, which is what this script is cited for.
|
|
3
|
+
// Its positional "Cn" refs rank over the whole VERSION's criteria, so on a
|
|
4
|
+
// multi-goal version they resolve to another goal's criterion silently (learning
|
|
5
|
+
// 1000188). POST /tasks now refuses a positional ref there (400
|
|
6
|
+
// ambiguous_criterion_ref). A new seed uses criterion_id SLUGS.
|
|
7
|
+
//
|
|
1
8
|
// Seed the tasks for goal 1000071 "Idea routing — work that has a home lands in it" (BONGOS-V1).
|
|
2
9
|
// Source of truth: docs/specs/bongos-v1-idea-routing.md (status: final).
|
|
3
10
|
// Idempotent on (source, source_ref). Re-running resolves to the existing rows.
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
// DO NOT COPY THE `crit:` TOKENS FROM THIS FILE (task 1003108). Its positional
|
|
2
|
+
// "Cn" refs were written when BONGOS-V1 held one goal, where they were unambiguous.
|
|
3
|
+
// Cn ranks over the whole VERSION's criteria, so on a multi-goal version it resolves
|
|
4
|
+
// to another goal's criterion silently — that is how all 15 tasks of goal 1000071 were
|
|
5
|
+
// mislinked (learning 1000188). POST /tasks now refuses a positional ref on a
|
|
6
|
+
// multi-goal version (400 ambiguous_criterion_ref). A new seed uses criterion_id
|
|
7
|
+
// SLUGS. This file is kept as-is because it has already run and is idempotent on
|
|
8
|
+
// source_ref — a re-run returns the existing rows before criterion resolution.
|
|
9
|
+
//
|
|
1
10
|
// Seed the tasks for goal 1000045 "Public builder profiles" (BONGOS-V1, internal).
|
|
2
11
|
// Source of truth: docs/specs/bongos-v1-public-builder-profiles.md (status: final).
|
|
3
12
|
// Idempotent on (source, source_ref). Re-running resolves to the existing rows.
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.787'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
// - a numeric done_when id → { kind: 'id', id }
|
|
7
7
|
// - a criterion_id slug → { kind: 'slug', slug }
|
|
8
8
|
// resolveCriterionIds itself is DB-backed (it ranks a version's criteria), so the
|
|
9
|
-
// parsing rule — the part that decides what a token MEANS — is
|
|
10
|
-
//
|
|
9
|
+
// parsing rule — the part that decides what a token MEANS — is unit-tested here
|
|
10
|
+
// without a database. Task 1003108 added a second block covering resolveCriterionIds
|
|
11
|
+
// itself through its executor seam: a positional "Cn" must be REFUSED once the
|
|
12
|
+
// version holds more than one goal. Run: node tests/criterion_ref_parse.mjs
|
|
11
13
|
|
|
12
14
|
import { strict as assert } from 'node:assert';
|
|
13
15
|
import { createRequire } from 'node:module';
|
|
@@ -49,5 +51,82 @@ test('a "Cn" embedded in a longer string does NOT match (anchored)', () => {
|
|
|
49
51
|
assert.equal(db.parseCriterionRef('see C8 above').kind, 'slug');
|
|
50
52
|
});
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
// --- resolveCriterionIds: the multi-goal refusal (task 1003108) -------------
|
|
55
|
+
// resolveCriterionIds is DB-backed, but it takes an `executor` seam (the one
|
|
56
|
+
// countTasksWithoutCriterionLink uses), so a fake SQL-recording client covers the
|
|
57
|
+
// rule without Postgres. The rule: a positional "Cn" ranks over the WHOLE
|
|
58
|
+
// version's criteria, so once a version holds a second goal it silently resolves
|
|
59
|
+
// to another goal's criterion — the shape that mislinked all 15 tasks of goal
|
|
60
|
+
// 1000071. It must come back as `ambiguous`, never as an id.
|
|
61
|
+
|
|
62
|
+
function fakeExecutor(goalCount) {
|
|
63
|
+
const seen = [];
|
|
64
|
+
return {
|
|
65
|
+
seen,
|
|
66
|
+
async query(sql, params) {
|
|
67
|
+
seen.push(sql);
|
|
68
|
+
if (/FROM goals/.test(sql)) return { rows: [{ n: goalCount }] };
|
|
69
|
+
return { rows: [
|
|
70
|
+
{ id: 501, criterion_id: 'first-slug', cnum: 1 },
|
|
71
|
+
{ id: 502, criterion_id: 'second-slug', cnum: 2 },
|
|
72
|
+
] };
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function atest(name, fn) {
|
|
78
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
79
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
await atest('a positional "Cn" resolves on a SINGLE-goal version', async () => {
|
|
84
|
+
const r = await db.resolveCriterionIds('V9', ['C2'], fakeExecutor(1));
|
|
85
|
+
assert.deepEqual(r.ids, [502]);
|
|
86
|
+
assert.deepEqual(r.ambiguous, []);
|
|
87
|
+
assert.deepEqual(r.unresolved, []);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await atest('a positional "Cn" is REFUSED on a multi-goal version', async () => {
|
|
91
|
+
const r = await db.resolveCriterionIds('V9', ['C2'], fakeExecutor(2));
|
|
92
|
+
assert.deepEqual(r.ids, [], 'must not resolve to an id — refusing is the whole point');
|
|
93
|
+
assert.deepEqual(r.ambiguous, ['C2']);
|
|
94
|
+
assert.deepEqual(r.unresolved, [], 'ambiguous is not the same failure as unknown');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await atest('slugs still resolve on a multi-goal version', async () => {
|
|
98
|
+
const r = await db.resolveCriterionIds('V9', ['second-slug'], fakeExecutor(2));
|
|
99
|
+
assert.deepEqual(r.ids, [502]);
|
|
100
|
+
assert.deepEqual(r.ambiguous, []);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await atest('a mixed list refuses only the positional ref', async () => {
|
|
104
|
+
const r = await db.resolveCriterionIds('V9', ['first-slug', 'C2', '502'], fakeExecutor(2));
|
|
105
|
+
assert.deepEqual(r.ids, [501, 502], 'the slug and the numeric id survive');
|
|
106
|
+
assert.deepEqual(r.ambiguous, ['C2']);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await atest('an unknown slug is unresolved, not ambiguous', async () => {
|
|
110
|
+
const r = await db.resolveCriterionIds('V9', ['no-such-slug'], fakeExecutor(2));
|
|
111
|
+
assert.deepEqual(r.unresolved, ['no-such-slug']);
|
|
112
|
+
assert.deepEqual(r.ambiguous, []);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await atest('the goal count is NOT queried when no positional ref is present', async () => {
|
|
116
|
+
// The slug path is the one the skill now requires — it must not pay for an
|
|
117
|
+
// extra round trip. A green test here is what keeps the fast path fast.
|
|
118
|
+
const ex = fakeExecutor(2);
|
|
119
|
+
await db.resolveCriterionIds('V9', ['first-slug'], ex);
|
|
120
|
+
assert.equal(ex.seen.filter((s) => /FROM goals/.test(s)).length, 0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await atest('an empty ref list returns all three arrays', async () => {
|
|
124
|
+
const r = await db.resolveCriterionIds('V9', [], fakeExecutor(2));
|
|
125
|
+
assert.deepEqual(r, { ids: [], unresolved: [], ambiguous: [] });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
console.log(`\ncriterion ref parse (task 438) + multi-goal refusal (task 1003108) tests: ${passed} passed, ${failed} failed`);
|
|
129
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main();
|
|
@@ -327,6 +327,65 @@ test('collectEnvSecrets: benign shell PWD/OLDPWD are skipped, real *_PWD passwor
|
|
|
327
327
|
assert.ok(got.includes(env.MYSQL_PWD), 'MYSQL_PWD (a real password var) must still be collected');
|
|
328
328
|
});
|
|
329
329
|
|
|
330
|
+
test('collectEnvSecrets: Claude Code harness config vars are skipped, real anthropic credentials still collected (task 1003512)', () => {
|
|
331
|
+
// Regression: ENV_SECRET_PREFIX_RE matches ^anthropic, so the harness's own
|
|
332
|
+
// ANTHROPIC_DEFAULT_OPUS_MODEL — value = a MODEL ID, not a credential — was collected
|
|
333
|
+
// as a live secret. package-core's fail-closed leak gate then flagged every functional
|
|
334
|
+
// file containing that string verbatim (the grader, llm-pricing and their tests) and
|
|
335
|
+
// refused to build on ANY machine where Claude Code had set its model vars. Hit live
|
|
336
|
+
// packaging 1.19.209; the workaround was `env -u ANTHROPIC_DEFAULT_OPUS_MODEL`.
|
|
337
|
+
const env = {
|
|
338
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-5',
|
|
339
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-5',
|
|
340
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001',
|
|
341
|
+
ANTHROPIC_MODEL: 'claude-opus-5-20260101',
|
|
342
|
+
ANTHROPIC_SMALL_FAST_MODEL: 'claude-haiku-4-5-20251001',
|
|
343
|
+
ANTHROPIC_BASE_URL: 'https://api.anthropic.com',
|
|
344
|
+
// ...and the ones that really are credentials, under the SAME provider prefix.
|
|
345
|
+
ANTHROPIC_API_KEY: 'sk-' + 'ant-' + randStr(31, 30, B64URL),
|
|
346
|
+
ANTHROPIC_AUTH_TOKEN: randStr(32, 40, B64URL),
|
|
347
|
+
};
|
|
348
|
+
const got = collectEnvSecrets(env);
|
|
349
|
+
|
|
350
|
+
for (const k of ['ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
351
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_MODEL', 'ANTHROPIC_SMALL_FAST_MODEL',
|
|
352
|
+
'ANTHROPIC_BASE_URL']) {
|
|
353
|
+
assert.ok(!got.includes(env[k]), `${k} is a config identifier and must not be collected`);
|
|
354
|
+
}
|
|
355
|
+
// The exemption is on the NAME and must never reach a name carrying a secret word.
|
|
356
|
+
assert.ok(got.includes(env.ANTHROPIC_API_KEY), 'ANTHROPIC_API_KEY must still be collected');
|
|
357
|
+
assert.ok(got.includes(env.ANTHROPIC_AUTH_TOKEN), 'ANTHROPIC_AUTH_TOKEN must still be collected');
|
|
358
|
+
assert.equal(got.length, 2, 'exactly the two real credentials, nothing else');
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('the *_MODEL exemption cannot be used to smuggle a credential (task 1003512)', () => {
|
|
362
|
+
// A name that carries a secret WORD is collected whatever it ends with — the
|
|
363
|
+
// exemption applies only to names matched by the provider PREFIX alone. Without this,
|
|
364
|
+
// the fix would be a hole: these literals feed the PUBLIC mirror and session uploads.
|
|
365
|
+
const env = {
|
|
366
|
+
ANTHROPIC_SECRET_MODEL: randStr(33, 40, B64URL),
|
|
367
|
+
ANTHROPIC_API_KEY_MODEL: randStr(34, 40, B64URL),
|
|
368
|
+
OPENAI_TOKEN_BASE_URL: randStr(35, 40, B64URL),
|
|
369
|
+
};
|
|
370
|
+
const got = collectEnvSecrets(env);
|
|
371
|
+
for (const k of Object.keys(env)) {
|
|
372
|
+
assert.ok(got.includes(env[k]), `${k} carries a secret word and must still be collected`);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('a model id survives a transcript scrub while a real key does not (task 1003512)', () => {
|
|
377
|
+
// The end-to-end shape of the bug: the model id is what package-core found verbatim
|
|
378
|
+
// in modules/grading and failed the build on. RECALL IS UNCHANGED for the credential.
|
|
379
|
+
const env = {
|
|
380
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-5',
|
|
381
|
+
ANTHROPIC_API_KEY: 'sk-' + 'ant-' + randStr(36, 30, B64URL),
|
|
382
|
+
};
|
|
383
|
+
const text = `the grader ran on ${env.ANTHROPIC_DEFAULT_OPUS_MODEL} using ${env.ANTHROPIC_API_KEY}`;
|
|
384
|
+
const out = scrubSecrets(text, collectEnvSecrets(env));
|
|
385
|
+
assert.ok(out.includes('claude-opus-5'), 'a model id is not a secret and must survive');
|
|
386
|
+
assert.ok(!out.includes(env.ANTHROPIC_API_KEY), 'the key must still be scrubbed');
|
|
387
|
+
});
|
|
388
|
+
|
|
330
389
|
test('self-reference `key = key` (SQL SET col = col) survives — zero recall loss', () => {
|
|
331
390
|
// task 1688: the core packager redacts live SQL, so a `_token` COLUMN self-increment
|
|
332
391
|
// must NOT be corrupted — while a real `key = <secret>` assignment is STILL redacted.
|