@bongos/core 1.19.841 → 1.19.843
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 +49 -24
- package/docs/module-api-changelog.md +4 -0
- package/migrations/core_247_discipline_required.sql +58 -0
- package/modules/copy-desk/routes/copy-desk.js +6 -0
- package/modules/ideas/inbox.js +4 -2
- package/modules/lifecycle/db-tasks.js +28 -2
- package/modules/lifecycle/lifecycle.js +6 -0
- package/modules/lifecycle/routes/goals.js +17 -4
- package/modules/lifecycle/routes/task-write-routes.js +25 -6
- package/modules/lifecycle/task-classifier.js +39 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/backfill-disciplines.js +144 -0
- package/scripts/gds/fitness-ratchets.js +59 -2
- package/scripts/gds/init.js +6 -1
- package/scripts/gds/triage.js +7 -0
- package/src/module-api.js +1 -1
- package/tests/agents_roster.mjs +132 -0
- package/tests/knip_ratchet_ambient.mjs +101 -0
- package/tests/rank_floor_wiring.mjs +4 -0
- package/tests/reward_gate.mjs +4 -4
- package/tests/task_discipline_required.mjs +163 -0
- package/tests/task_goal_required.mjs +4 -4
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.843",
|
|
6
|
+
"core_contract": "1.19.843",
|
|
7
|
+
"source_commit": "7c57c44f66a63fda88681f46ac67857f8c8ed1e7",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-17T18:
|
|
9
|
+
"built_at": "2026-09-17T18:30:58.977Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 495,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2270,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2791,
|
|
20
|
+
"tree_sha256": "9f6bfba9532caa1edab6600efaf86e006add511e45378f5cf2ccf8ad81d81231",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2577,7 +2577,7 @@
|
|
|
2577
2577
|
{
|
|
2578
2578
|
"path": "docs/module-api-changelog.md",
|
|
2579
2579
|
"mode": "0000644",
|
|
2580
|
-
"sha256": "
|
|
2580
|
+
"sha256": "e2083366af3229b576d6d034f1d2e1c6e7e4fa61fba384ba0d0fca9b7911087f"
|
|
2581
2581
|
},
|
|
2582
2582
|
{
|
|
2583
2583
|
"path": "docs/modules-contract.md",
|
|
@@ -3679,6 +3679,11 @@
|
|
|
3679
3679
|
"mode": "0000644",
|
|
3680
3680
|
"sha256": "0481e0b8205c707ed002f404c801d3f2059b2b0e0fcfa9bc436968f6665a296f"
|
|
3681
3681
|
},
|
|
3682
|
+
{
|
|
3683
|
+
"path": "migrations/core_247_discipline_required.sql",
|
|
3684
|
+
"mode": "0000644",
|
|
3685
|
+
"sha256": "e8d3e3f764b61ea559df7f997758e639f8778ee24da4293908e9d45ca30a425b"
|
|
3686
|
+
},
|
|
3682
3687
|
{
|
|
3683
3688
|
"path": "modules/agents/CLAUDE.md",
|
|
3684
3689
|
"mode": "0000644",
|
|
@@ -3882,7 +3887,7 @@
|
|
|
3882
3887
|
{
|
|
3883
3888
|
"path": "modules/copy-desk/routes/copy-desk.js",
|
|
3884
3889
|
"mode": "0000644",
|
|
3885
|
-
"sha256": "
|
|
3890
|
+
"sha256": "4839f077aee504947a3bada77fd9c3df882f9005f571ed2e2367b615c111b76b"
|
|
3886
3891
|
},
|
|
3887
3892
|
{
|
|
3888
3893
|
"path": "modules/copy-desk/tests/copy_flags.mjs",
|
|
@@ -5477,7 +5482,7 @@
|
|
|
5477
5482
|
{
|
|
5478
5483
|
"path": "modules/ideas/inbox.js",
|
|
5479
5484
|
"mode": "0000644",
|
|
5480
|
-
"sha256": "
|
|
5485
|
+
"sha256": "ff6c0346215d1b88309c4c9360cd60c59299b21b45e27cce09c4b748bf3411e1"
|
|
5481
5486
|
},
|
|
5482
5487
|
{
|
|
5483
5488
|
"path": "modules/ideas/migrations/ideas_001_grades.sql",
|
|
@@ -5657,7 +5662,7 @@
|
|
|
5657
5662
|
{
|
|
5658
5663
|
"path": "modules/lifecycle/db-tasks.js",
|
|
5659
5664
|
"mode": "0000644",
|
|
5660
|
-
"sha256": "
|
|
5665
|
+
"sha256": "5d17541d837a06903be832814a74218c9fee8b9119dd870c88273743be5a5efa"
|
|
5661
5666
|
},
|
|
5662
5667
|
{
|
|
5663
5668
|
"path": "modules/lifecycle/db-versions.js",
|
|
@@ -5752,7 +5757,7 @@
|
|
|
5752
5757
|
{
|
|
5753
5758
|
"path": "modules/lifecycle/lifecycle.js",
|
|
5754
5759
|
"mode": "0000644",
|
|
5755
|
-
"sha256": "
|
|
5760
|
+
"sha256": "41ef55efff7f334ee8b19490f03944ca25f22454c718dfb81445539bc6a77aa4"
|
|
5756
5761
|
},
|
|
5757
5762
|
{
|
|
5758
5763
|
"path": "modules/lifecycle/merge-lock.js",
|
|
@@ -5877,7 +5882,7 @@
|
|
|
5877
5882
|
{
|
|
5878
5883
|
"path": "modules/lifecycle/routes/goals.js",
|
|
5879
5884
|
"mode": "0000644",
|
|
5880
|
-
"sha256": "
|
|
5885
|
+
"sha256": "17f7c0ccfc87eec24c662bf8d5599223562e773430802cd054b27a3ddadab46e"
|
|
5881
5886
|
},
|
|
5882
5887
|
{
|
|
5883
5888
|
"path": "modules/lifecycle/routes/help-requests.js",
|
|
@@ -5922,7 +5927,7 @@
|
|
|
5922
5927
|
{
|
|
5923
5928
|
"path": "modules/lifecycle/routes/task-write-routes.js",
|
|
5924
5929
|
"mode": "0000644",
|
|
5925
|
-
"sha256": "
|
|
5930
|
+
"sha256": "bb172485d8d5f84d96cdb611b0d57cd5f69ee768bdceddeb1eea42dc3f5676c1"
|
|
5926
5931
|
},
|
|
5927
5932
|
{
|
|
5928
5933
|
"path": "modules/lifecycle/routes/tasks.js",
|
|
@@ -5962,7 +5967,7 @@
|
|
|
5962
5967
|
{
|
|
5963
5968
|
"path": "modules/lifecycle/task-classifier.js",
|
|
5964
5969
|
"mode": "0000644",
|
|
5965
|
-
"sha256": "
|
|
5970
|
+
"sha256": "39047afd6440d589cee978887ce4b5fa285b00d55ed2b023546fce41e59bdc9c"
|
|
5966
5971
|
},
|
|
5967
5972
|
{
|
|
5968
5973
|
"path": "modules/lifecycle/task-list-limits.js",
|
|
@@ -7892,12 +7897,12 @@
|
|
|
7892
7897
|
{
|
|
7893
7898
|
"path": "package-lock.json",
|
|
7894
7899
|
"mode": "0000644",
|
|
7895
|
-
"sha256": "
|
|
7900
|
+
"sha256": "090497aae4ab83f6ab3f821417ece5fcc50b86a09e026b3e4644aa660333d191"
|
|
7896
7901
|
},
|
|
7897
7902
|
{
|
|
7898
7903
|
"path": "package.json",
|
|
7899
7904
|
"mode": "0000644",
|
|
7900
|
-
"sha256": "
|
|
7905
|
+
"sha256": "e7d47a1c39751137250a2cc087fb288d3c349b44d8faebc38f70e841f6475560"
|
|
7901
7906
|
},
|
|
7902
7907
|
{
|
|
7903
7908
|
"path": "public-docs/index.html",
|
|
@@ -8064,6 +8069,11 @@
|
|
|
8064
8069
|
"mode": "0000644",
|
|
8065
8070
|
"sha256": "9c03bf914c4a1a6f5d9936bb5542a4e3952873de65b0518254808bfbd1901b40"
|
|
8066
8071
|
},
|
|
8072
|
+
{
|
|
8073
|
+
"path": "scripts/gds/backfill-disciplines.js",
|
|
8074
|
+
"mode": "0000644",
|
|
8075
|
+
"sha256": "cc9bd2d437e6a7d6eb2b48504cc7d7c44a674403a0d00625a0526719825f88d2"
|
|
8076
|
+
},
|
|
8067
8077
|
{
|
|
8068
8078
|
"path": "scripts/gds/backfill-session-earnings.js",
|
|
8069
8079
|
"mode": "0000644",
|
|
@@ -8432,7 +8442,7 @@
|
|
|
8432
8442
|
{
|
|
8433
8443
|
"path": "scripts/gds/fitness-ratchets.js",
|
|
8434
8444
|
"mode": "0000644",
|
|
8435
|
-
"sha256": "
|
|
8445
|
+
"sha256": "178d2cfb91c97c107d60cc3895ba1b3eff74c45d383646e7a64621d018c21b47"
|
|
8436
8446
|
},
|
|
8437
8447
|
{
|
|
8438
8448
|
"path": "scripts/gds/fitness.js",
|
|
@@ -8577,7 +8587,7 @@
|
|
|
8577
8587
|
{
|
|
8578
8588
|
"path": "scripts/gds/init.js",
|
|
8579
8589
|
"mode": "0000644",
|
|
8580
|
-
"sha256": "
|
|
8590
|
+
"sha256": "e1fbe45a7619cd5000b5bb34c80bbd63c2591dcc78a64188870847e5da049448"
|
|
8581
8591
|
},
|
|
8582
8592
|
{
|
|
8583
8593
|
"path": "scripts/gds/install-git-hooks.js",
|
|
@@ -9367,7 +9377,7 @@
|
|
|
9367
9377
|
{
|
|
9368
9378
|
"path": "scripts/gds/triage.js",
|
|
9369
9379
|
"mode": "0000644",
|
|
9370
|
-
"sha256": "
|
|
9380
|
+
"sha256": "a4af12591b829f9b89a5768858e8a2ba02a44d7e7ebd848e945e77f95fc55da7"
|
|
9371
9381
|
},
|
|
9372
9382
|
{
|
|
9373
9383
|
"path": "scripts/gds/turn-end-sound.js",
|
|
@@ -9827,7 +9837,7 @@
|
|
|
9827
9837
|
{
|
|
9828
9838
|
"path": "src/module-api.js",
|
|
9829
9839
|
"mode": "0000644",
|
|
9830
|
-
"sha256": "
|
|
9840
|
+
"sha256": "a24a8e5c67584fff7c871ad60afaa3bc9f81a2c77189629597dd4fd63cd43c31"
|
|
9831
9841
|
},
|
|
9832
9842
|
{
|
|
9833
9843
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9969,6 +9979,11 @@
|
|
|
9969
9979
|
"mode": "0000644",
|
|
9970
9980
|
"sha256": "919ca0ea3fe5f78a194781f6b634b9da23212073cf0c7cbe8ae36a65cc3fecdb"
|
|
9971
9981
|
},
|
|
9982
|
+
{
|
|
9983
|
+
"path": "tests/agents_roster.mjs",
|
|
9984
|
+
"mode": "0000644",
|
|
9985
|
+
"sha256": "acc25f4d72cb22719d6977f939b41f9f17e0d102880976699b6ebcda1c64c523"
|
|
9986
|
+
},
|
|
9972
9987
|
{
|
|
9973
9988
|
"path": "tests/agents_routes.mjs",
|
|
9974
9989
|
"mode": "0000644",
|
|
@@ -11984,6 +11999,11 @@
|
|
|
11984
11999
|
"mode": "0000644",
|
|
11985
12000
|
"sha256": "416d413757a13fbddb5841ea8a2c503077d7e6067bc1c58ce1f4d416b8d8487f"
|
|
11986
12001
|
},
|
|
12002
|
+
{
|
|
12003
|
+
"path": "tests/knip_ratchet_ambient.mjs",
|
|
12004
|
+
"mode": "0000644",
|
|
12005
|
+
"sha256": "b48d7f46c4518ef7da8e252d8009edd25ec5771f7be320f889f437762af72b54"
|
|
12006
|
+
},
|
|
11987
12007
|
{
|
|
11988
12008
|
"path": "tests/known_failing_ratchet.mjs",
|
|
11989
12009
|
"mode": "0000644",
|
|
@@ -12857,7 +12877,7 @@
|
|
|
12857
12877
|
{
|
|
12858
12878
|
"path": "tests/rank_floor_wiring.mjs",
|
|
12859
12879
|
"mode": "0000644",
|
|
12860
|
-
"sha256": "
|
|
12880
|
+
"sha256": "973fc0768bd52f3e25c98306c21cc6a9c6268daae033e1329c68620a847bc0a9"
|
|
12861
12881
|
},
|
|
12862
12882
|
{
|
|
12863
12883
|
"path": "tests/rank_gate.mjs",
|
|
@@ -12967,7 +12987,7 @@
|
|
|
12967
12987
|
{
|
|
12968
12988
|
"path": "tests/reward_gate.mjs",
|
|
12969
12989
|
"mode": "0000644",
|
|
12970
|
-
"sha256": "
|
|
12990
|
+
"sha256": "0cd9f335da162c3562c099a2dbe4375b009f5a8a8321f4ef84b7a577be816bdc"
|
|
12971
12991
|
},
|
|
12972
12992
|
{
|
|
12973
12993
|
"path": "tests/reward_mode_cost_plus_only.mjs",
|
|
@@ -13614,6 +13634,11 @@
|
|
|
13614
13634
|
"mode": "0000644",
|
|
13615
13635
|
"sha256": "0bc0d93696b5ce80fb4307b7991bff640316e46fd6fcdebf0ba5a62397ffbe8f"
|
|
13616
13636
|
},
|
|
13637
|
+
{
|
|
13638
|
+
"path": "tests/task_discipline_required.mjs",
|
|
13639
|
+
"mode": "0000644",
|
|
13640
|
+
"sha256": "2fda6816614259699227a21f97b81d816f20fdd654c861e24a282c1d178ddff4"
|
|
13641
|
+
},
|
|
13617
13642
|
{
|
|
13618
13643
|
"path": "tests/task_field_bounds_match_db.mjs",
|
|
13619
13644
|
"mode": "0000644",
|
|
@@ -13632,7 +13657,7 @@
|
|
|
13632
13657
|
{
|
|
13633
13658
|
"path": "tests/task_goal_required.mjs",
|
|
13634
13659
|
"mode": "0000644",
|
|
13635
|
-
"sha256": "
|
|
13660
|
+
"sha256": "073581d9a97095666ee2ed7ebb0a6b280f3e8ea1efc53015c2f77eb5d08c4975"
|
|
13636
13661
|
},
|
|
13637
13662
|
{
|
|
13638
13663
|
"path": "tests/task_lib.mjs",
|
|
@@ -2156,5 +2156,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2156
2156
|
landed since 1.19.839 with no explicit bump. run 35254616836. (task 1002620)
|
|
2157
2157
|
1.19.841 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2158
2158
|
landed since 1.19.840 with no explicit bump. run 35257419225. (task 1002620)
|
|
2159
|
+
1.19.842 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2160
|
+
landed since 1.19.841 with no explicit bump. run 35258288930. (task 1002620)
|
|
2161
|
+
1.19.843 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2162
|
+
landed since 1.19.842 with no explicit bump. run 35259228465. (task 1002620)
|
|
2159
2163
|
---------------------------------------------------------------------------
|
|
2160
2164
|
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- core_247_discipline_required.sql — task 1002988: every task row carries a craft.
|
|
2
|
+
--
|
|
3
|
+
-- `unclassified` is a triage state, not a craft. It was the DEFAULT on every create
|
|
4
|
+
-- vector, so it accumulated: 1,189 rows on this instance as of 2026-09-17 (1,025
|
|
5
|
+
-- real + 164 __smoke__ test rows), of which only 208 were live work. A row nobody's
|
|
6
|
+
-- craft owns is a row nobody picks up.
|
|
7
|
+
--
|
|
8
|
+
-- THE ACCEPTED SET IS FOUR, NOT THREE. The task was written on 2026-08-16 naming
|
|
9
|
+
-- {engineer, artist, ideator}. ADR 0272 (2026-09-09) postdates it and KEEPS `ui` as
|
|
10
|
+
-- a module-contributed discipline, and the owner reconfirmed that on 2026-09-17 when
|
|
11
|
+
-- abandoning the sibling task 1002987 ("Delete the ui role across the platform") for
|
|
12
|
+
-- exactly this reason. Narrowing to three would break 50+ live `ui` rows and
|
|
13
|
+
-- contradict a live ADR, so migration 176's `ui` entry survives untouched here.
|
|
14
|
+
--
|
|
15
|
+
-- WHY THE BACKFILL IS IN THE MIGRATION. The good classification is done by
|
|
16
|
+
-- scripts/gds/backfill-disciplines.js, which runs the real JS heuristic
|
|
17
|
+
-- (classifyTaskDiscipline) and cannot be expressed in SQL. But a migration that
|
|
18
|
+
-- assumes a script was run by hand is a migration that breaks the next fresh
|
|
19
|
+
-- install, the next instance, and any checkout where nobody remembered. So the
|
|
20
|
+
-- UPDATE below is the LAST-RESORT NET, not the plan: it catches whatever the script
|
|
21
|
+
-- left (or everything, if the script never ran) and parks it on 'engineer' — the
|
|
22
|
+
-- value ~99% of confidently-classified rows resolve to.
|
|
23
|
+
--
|
|
24
|
+
-- That fallback is a GUESS and is recorded as one. The owner chose it on 2026-09-17
|
|
25
|
+
-- over the alternative (backfill live work only, leave history alone), accepting
|
|
26
|
+
-- that the ~273 rows the classifier refuses get a best guess rather than a known
|
|
27
|
+
-- answer. Do not read a post-migration `discipline` on an old row as evidence that
|
|
28
|
+
-- anyone determined it.
|
|
29
|
+
--
|
|
30
|
+
-- Idempotent: safe to re-run. The UPDATE matches nothing on a second pass, and the
|
|
31
|
+
-- constraint is dropped before being re-added (the pattern of migrations 086/176).
|
|
32
|
+
-- Forward-safe: the column is unchanged and the previous release wrote only values
|
|
33
|
+
-- this constraint still accepts, EXCEPT 'unclassified' — a rolled-back deploy whose
|
|
34
|
+
-- code still defaults to 'unclassified' would have its INSERTs rejected. That is the
|
|
35
|
+
-- intended behaviour (it is the hole being closed), and it is why the application
|
|
36
|
+
-- side of this task resolves the discipline at db-tasks.createTask, the single
|
|
37
|
+
-- chokepoint every create vector passes through, rather than only at the two routes.
|
|
38
|
+
|
|
39
|
+
BEGIN;
|
|
40
|
+
|
|
41
|
+
-- 1. The last-resort net (see header). Everything the classifier could call should
|
|
42
|
+
-- already have been PATCHed by backfill-disciplines.js before this runs.
|
|
43
|
+
UPDATE tasks SET discipline = 'engineer' WHERE discipline = 'unclassified' OR discipline IS NULL;
|
|
44
|
+
|
|
45
|
+
-- 2. Narrow the accepted set: drop 'unclassified', keep 'ui' (ADR 0272).
|
|
46
|
+
ALTER TABLE tasks DROP CONSTRAINT IF EXISTS tasks_discipline_check;
|
|
47
|
+
ALTER TABLE tasks ADD CONSTRAINT tasks_discipline_check
|
|
48
|
+
CHECK (discipline IN ('engineer', 'artist', 'ideator', 'ui'));
|
|
49
|
+
|
|
50
|
+
-- builders.preferred_disciplines already excludes 'unclassified' (migration 176 —
|
|
51
|
+
-- "Never carries 'unclassified' — that's a task state, not an interest area"), so it
|
|
52
|
+
-- needs no change here. Named rather than silently skipped, so the next reader does
|
|
53
|
+
-- not have to re-check.
|
|
54
|
+
|
|
55
|
+
INSERT INTO schema_migrations (version) VALUES ('core_247_discipline_required')
|
|
56
|
+
ON CONFLICT (version) DO NOTHING;
|
|
57
|
+
|
|
58
|
+
COMMIT;
|
|
@@ -429,6 +429,12 @@ module.exports = function buildCopyDeskRouter() {
|
|
|
429
429
|
// opinion mint claimable work.
|
|
430
430
|
status: 'backlog',
|
|
431
431
|
kind: 'feature',
|
|
432
|
+
// task 1002988: named explicitly rather than inferred. A copy patch is
|
|
433
|
+
// reader-facing wording, which is artist work — cascade.js already treats
|
|
434
|
+
// `source: 'copy-proposal'` as an artist surface (hasCopyOrVisualWork), so
|
|
435
|
+
// inferring it from the patch text would risk the two disagreeing about
|
|
436
|
+
// the same row.
|
|
437
|
+
discipline: 'artist',
|
|
432
438
|
priority: 4,
|
|
433
439
|
touches: [patch.file],
|
|
434
440
|
source: 'copy-proposal',
|
package/modules/ideas/inbox.js
CHANGED
|
@@ -405,9 +405,11 @@ async function routeIdeaToGoal({
|
|
|
405
405
|
// own floor and takes the HIGHER of the two, so this can raise the bar and
|
|
406
406
|
// never lower it. Deliberately NOT passed at all: `creditsReward` (server
|
|
407
407
|
// default — createTask auto-assigns the capped suggestion) and
|
|
408
|
-
// `discipline`
|
|
409
|
-
//
|
|
408
|
+
// `discipline` — NO LONGER 'unclassified' (task 1002988). createTask refuses a
|
|
409
|
+
// craft it cannot infer; refusing a filer is the wrong trade for an INBOX, so we
|
|
410
|
+
// ask the same question and fall back to 'engineer' (free to re-triage; a lost capture is not).
|
|
410
411
|
const task = await lifecycle.createTask({
|
|
412
|
+
discipline: (lifecycle.resolveWriteDiscipline?.({ title, description: bodyMd, touches: declaredTouches })?.discipline) || 'engineer',
|
|
411
413
|
versionId: goal.version_id,
|
|
412
414
|
title,
|
|
413
415
|
description: bodyMd,
|
|
@@ -18,6 +18,7 @@ const { matchProtected } = api.permissionPathCheck;
|
|
|
18
18
|
const { decorateGoalMembership, capNewcomerTasks } = require('./claim-feed.js');
|
|
19
19
|
const { deriveRequiredRank, highestRank } = require('./db-rank-authz.js');
|
|
20
20
|
const { executorFor } = require('./db-executor.js');
|
|
21
|
+
const { resolveWriteDiscipline, WRITABLE_DISCIPLINES } = require('./task-classifier.js');
|
|
21
22
|
|
|
22
23
|
// -------------------------------------------------------------------------
|
|
23
24
|
// Tasks
|
|
@@ -123,7 +124,13 @@ async function createTask({
|
|
|
123
124
|
sourceRef = null,
|
|
124
125
|
valueSummary = null,
|
|
125
126
|
kind = 'unclassified',
|
|
126
|
-
|
|
127
|
+
// task 1002988 — NOT defaulted to 'unclassified' any more. This is the single
|
|
128
|
+
// chokepoint every create vector passes through, including the ones that never
|
|
129
|
+
// touch an HTTP route (idea promotion, copy-desk, newcomer-restock, kickoff-seed,
|
|
130
|
+
// cascade), so resolving here is what makes "no row is born role-less" true of
|
|
131
|
+
// the whole path rather than of the two routes. Resolved just below; `undefined`
|
|
132
|
+
// means "work it out from the text".
|
|
133
|
+
discipline = undefined,
|
|
127
134
|
parallelSafe = null,
|
|
128
135
|
newcomerFriendly = false,
|
|
129
136
|
needsMigration = false,
|
|
@@ -198,6 +205,25 @@ async function createTask({
|
|
|
198
205
|
}
|
|
199
206
|
goalIdEffective = await generalGoalIdForVersion(versionId);
|
|
200
207
|
}
|
|
208
|
+
|
|
209
|
+
// task 1002988 (owner decision 2026-09-17): every row is born with a craft.
|
|
210
|
+
// An absent or 'unclassified' discipline is INFERRED from title + description +
|
|
211
|
+
// touches; a task whose text says nothing about its craft is REFUSED rather than
|
|
212
|
+
// filed under a word that means "nobody looked". Throwing here rather than
|
|
213
|
+
// silently defaulting is the point — 1,189 role-less rows accumulated precisely
|
|
214
|
+
// because the default was quiet. A caller that cannot tolerate a refusal passes
|
|
215
|
+
// an explicit discipline, which is then visible at its call site instead of
|
|
216
|
+
// hidden in this signature.
|
|
217
|
+
const resolvedDiscipline = resolveWriteDiscipline({ discipline, title, description, touches });
|
|
218
|
+
if (resolvedDiscipline.error) {
|
|
219
|
+
const err = new Error(resolvedDiscipline.unclassifiable
|
|
220
|
+
? `Cannot infer a discipline for "${String(title).slice(0, 80)}" from its title and description. Pass discipline explicitly: ${Array.from(WRITABLE_DISCIPLINES).join(' | ')}.`
|
|
221
|
+
: `discipline must be one of ${Array.from(WRITABLE_DISCIPLINES).join(' | ')} (got ${JSON.stringify(discipline)}).`);
|
|
222
|
+
err.code = 'BAD_DISCIPLINE';
|
|
223
|
+
err.unclassifiable = resolvedDiscipline.unclassifiable === true;
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
const disciplineEffective = resolvedDiscipline.discipline;
|
|
201
227
|
// task 1498 / ADR 0084: every task carries a deliberate rank floor. Derive it,
|
|
202
228
|
// then let an explicit requiresRank override only UPWARD (highestRank) — a
|
|
203
229
|
// requested rank below the derived floor is clamped up to the floor
|
|
@@ -247,7 +273,7 @@ async function createTask({
|
|
|
247
273
|
sourceRef,
|
|
248
274
|
valueSummary,
|
|
249
275
|
kind,
|
|
250
|
-
|
|
276
|
+
disciplineEffective,
|
|
251
277
|
parallelSafe,
|
|
252
278
|
newcomerFriendly === true,
|
|
253
279
|
needsMigration === true,
|
|
@@ -37,6 +37,12 @@ module.exports = {
|
|
|
37
37
|
// (consumer: core cascade-dispatch.js, the dormant System-3 lib).
|
|
38
38
|
get VOTABLE_TASK_STATUSES() { return classifier.VOTABLE_TASK_STATUSES; },
|
|
39
39
|
classifyTaskKind: (...a) => classifier.classifyTaskKind(...a),
|
|
40
|
+
// task 1002988: createTask now REFUSES a task whose craft it cannot work out, so
|
|
41
|
+
// a consumer that must not fail (idea capture, copy-desk) needs to ask the same
|
|
42
|
+
// question first and name its own fallback. Exposed on the port because a module
|
|
43
|
+
// may not require another module — the boundary rule — and a second copy of this
|
|
44
|
+
// heuristic living in modules/ideas is exactly the drift this port exists to stop.
|
|
45
|
+
resolveWriteDiscipline: (...a) => classifier.resolveWriteDiscipline(...a),
|
|
40
46
|
// TASK_KINDS: the kind vocabulary, as an ordered array (consumer: GET /me,
|
|
41
47
|
// which hands it to the hall so a filter can be BUILT from it instead of
|
|
42
48
|
// retyped — task 1003453). VALID_KINDS is a Set and Sets do not survive
|
|
@@ -31,7 +31,7 @@ const goalEdits = require('../goal-edits.js'); // task 1003102 — goal self-edi
|
|
|
31
31
|
const goalCategory = require('../goal-category.js'); // task 1003275 — the ADVISORY human category (gates nothing)
|
|
32
32
|
const goalRollup = require('../goal-rollup.js'); // task 1003278 — per-goal rollups computed in SQL, not in the browser
|
|
33
33
|
const { suggestCriteria } = require('../criterion-suggest');
|
|
34
|
-
const { classifyTaskKind, VALID_KINDS,
|
|
34
|
+
const { classifyTaskKind, VALID_KINDS, WRITABLE_DISCIPLINES, resolveWriteDiscipline, TASK_FIELD_BOUNDS } = require('../task-classifier');
|
|
35
35
|
const { validateOrRespond, parseId, LIMITS } = api;
|
|
36
36
|
const { isModuleKey, scopeIncludesProtected, moduleKeys } = api.moduleScopeMap;
|
|
37
37
|
// goalScopeCheck — the PURE scope wall (BV1.R59 / ADR 0086 §3), reached through
|
|
@@ -1388,10 +1388,23 @@ module.exports = function buildGoalsRouter() {
|
|
|
1388
1388
|
const result = classifyTaskKind(body.title, body.description || '');
|
|
1389
1389
|
kind = result.confidence >= 0.5 ? result.kind : 'unclassified';
|
|
1390
1390
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1391
|
+
// task 1002988 — the SECOND create path. It carried the same
|
|
1392
|
+
// `body.discipline ?? 'unclassified'` line as POST /tasks, so a fix applied
|
|
1393
|
+
// only there would have left this one minting the rows the task exists to
|
|
1394
|
+
// remove. Both now share resolveWriteDiscipline().
|
|
1395
|
+
const resolvedDiscipline = resolveWriteDiscipline({
|
|
1396
|
+
discipline: body.discipline, title: body.title, description: body.description, touches: body.touches,
|
|
1397
|
+
});
|
|
1398
|
+
if (resolvedDiscipline.error) {
|
|
1399
|
+
return res.fail('bad_discipline', {
|
|
1400
|
+
status: 400,
|
|
1401
|
+
message: resolvedDiscipline.unclassifiable
|
|
1402
|
+
? `discipline was not supplied and could not be inferred from the title and description. Pass one of ${Array.from(WRITABLE_DISCIPLINES).join(' | ')}.`
|
|
1403
|
+
: `discipline must be one of ${Array.from(WRITABLE_DISCIPLINES).join(' | ')}.`,
|
|
1404
|
+
details: { valid: Array.from(WRITABLE_DISCIPLINES) },
|
|
1405
|
+
});
|
|
1394
1406
|
}
|
|
1407
|
+
const discipline = resolvedDiscipline.discipline;
|
|
1395
1408
|
|
|
1396
1409
|
// Create. version = the goal's (not client-supplied); status 'ready' so the
|
|
1397
1410
|
// author can claim it immediately; requiresRank OMITTED → auto-derived
|
|
@@ -22,7 +22,7 @@ const db = require('../db');
|
|
|
22
22
|
const hierarchy = require('../hierarchy');
|
|
23
23
|
const { suggestCriteria } = require('../criterion-suggest');
|
|
24
24
|
const { buildGoalAdvisory } = require('../goal-advisory');
|
|
25
|
-
const { classifyTaskKind, VALID_KINDS,
|
|
25
|
+
const { classifyTaskKind, VALID_KINDS, WRITABLE_DISCIPLINES, resolveWriteDiscipline, VOTABLE_TASK_STATUSES, TASK_FIELD_BOUNDS } = require('../task-classifier');
|
|
26
26
|
const { isModuleKey, moduleKeys } = api.moduleScopeMap;
|
|
27
27
|
const { validateOrRespond, LIMITS, parseId } = api;
|
|
28
28
|
const { badKind, detectsMigration } = require('./task-route-helpers.js');
|
|
@@ -141,10 +141,22 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
141
141
|
const result = classifyTaskKind(body.title, body.description || '');
|
|
142
142
|
kind = result.confidence >= 0.5 ? result.kind : 'unclassified';
|
|
143
143
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
// task 1002988: a create must never mint a role-less row. An omitted (or
|
|
145
|
+
// explicitly 'unclassified') discipline is INFERRED from the text; only a
|
|
146
|
+
// genuinely unclassifiable task is refused.
|
|
147
|
+
const resolved = resolveWriteDiscipline({
|
|
148
|
+
discipline: body.discipline, title: body.title, description: body.description, touches: body.touches,
|
|
149
|
+
});
|
|
150
|
+
if (resolved.error) {
|
|
151
|
+
return res.fail('bad_discipline', {
|
|
152
|
+
status: 400,
|
|
153
|
+
message: resolved.unclassifiable
|
|
154
|
+
? `discipline was not supplied and could not be inferred from the title and description. Pass one of ${Array.from(WRITABLE_DISCIPLINES).join(' | ')}.`
|
|
155
|
+
: `discipline must be one of ${Array.from(WRITABLE_DISCIPLINES).join(' | ')}.`,
|
|
156
|
+
details: { valid: Array.from(WRITABLE_DISCIPLINES), inferred: false },
|
|
157
|
+
});
|
|
147
158
|
}
|
|
159
|
+
const discipline = resolved.discipline;
|
|
148
160
|
// task 1498 / ADR 0084: an explicit requires_rank must be a live rank. The
|
|
149
161
|
// floor auto-derives from touches + security_sensitive; createTask clamps
|
|
150
162
|
// any explicit value UP to that floor (never below), so this only rejects a
|
|
@@ -496,8 +508,15 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
496
508
|
if (hasKind && !VALID_KINDS.has(body.kind)) {
|
|
497
509
|
return badKind(res);
|
|
498
510
|
}
|
|
499
|
-
|
|
500
|
-
|
|
511
|
+
// task 1002988: a PATCH may not push a row BACK to 'unclassified' — that is a
|
|
512
|
+
// triage state, not a craft, and letting an edit re-create one would reopen the
|
|
513
|
+
// hole the create path above just closed.
|
|
514
|
+
if (hasDiscipline && !WRITABLE_DISCIPLINES.has(body.discipline)) {
|
|
515
|
+
return res.fail('bad_discipline', {
|
|
516
|
+
status: 400,
|
|
517
|
+
message: `discipline must be one of ${Array.from(WRITABLE_DISCIPLINES).join(' | ')}.`,
|
|
518
|
+
details: { valid: Array.from(WRITABLE_DISCIPLINES) },
|
|
519
|
+
});
|
|
501
520
|
}
|
|
502
521
|
if (hasNewcomerOrXenos) {
|
|
503
522
|
// V3.R15 #239: prefer newcomer_friendly (canonical); fall back to
|
|
@@ -163,6 +163,42 @@ function classifyTaskKind(title, description) {
|
|
|
163
163
|
// /design work-mode (discipline-modes.json) instead of /paint.
|
|
164
164
|
const VALID_DISCIPLINES = new Set(['engineer', 'artist', 'ideator', 'ui', 'unclassified']);
|
|
165
165
|
|
|
166
|
+
// The disciplines a WRITE may set (task 1002988). 'unclassified' is deliberately
|
|
167
|
+
// absent: it is a triage state, not a craft, and a create that lands one produces
|
|
168
|
+
// a row no craft is accountable for — which is how 1,189 of them accumulated.
|
|
169
|
+
// VALID_DISCIPLINES above stays the READ-side set so existing rows and the
|
|
170
|
+
// ?discipline=unclassified filter keep resolving while the backfill runs.
|
|
171
|
+
// 'ui' stays: ADR 0272 keeps it as a module-contributed discipline, and the owner
|
|
172
|
+
// reconfirmed that on 2026-09-17 when abandoning task 1002987.
|
|
173
|
+
const WRITABLE_DISCIPLINES = new Set(['engineer', 'artist', 'ideator', 'ui']);
|
|
174
|
+
|
|
175
|
+
// Mirrors the `kind` branch in the create routes: act on the classifier only when
|
|
176
|
+
// it is at least half sure, otherwise treat it as no answer at all.
|
|
177
|
+
const DISCIPLINE_CONFIDENCE_FLOOR = 0.5;
|
|
178
|
+
|
|
179
|
+
// Resolve the discipline for a task WRITE. Honours an explicit valid value,
|
|
180
|
+
// otherwise INFERS from title + description + touches, and reports failure so the
|
|
181
|
+
// caller can 400 — per the owner decision of 2026-09-17, a bare 400 on a missing
|
|
182
|
+
// discipline was rejected because four live paths (idea capture, idea promotion,
|
|
183
|
+
// adopt-import and the new-instance scaffold) create tasks without one.
|
|
184
|
+
//
|
|
185
|
+
// Shared deliberately: POST /tasks and POST /goals/:id/tasks BOTH carried the
|
|
186
|
+
// `body.discipline ?? 'unclassified'` line, so a fix applied to one of them would
|
|
187
|
+
// have left the other quietly minting the very rows this task exists to remove.
|
|
188
|
+
// Returns { discipline, inferred?, confidence? } or { error, unclassifiable? }.
|
|
189
|
+
function resolveWriteDiscipline({ discipline, title, description, touches } = {}) {
|
|
190
|
+
if (discipline != null && discipline !== 'unclassified') {
|
|
191
|
+
return WRITABLE_DISCIPLINES.has(discipline) ? { discipline } : { error: 'bad_discipline' };
|
|
192
|
+
}
|
|
193
|
+
// Absent, null, or an explicit 'unclassified' (what modules/ideas/inbox.js sends
|
|
194
|
+
// on promotion) all mean the same thing: nobody told us, so work it out.
|
|
195
|
+
const guess = classifyTaskDiscipline(title, description || '', touches);
|
|
196
|
+
if (guess.confidence >= DISCIPLINE_CONFIDENCE_FLOOR && WRITABLE_DISCIPLINES.has(guess.discipline)) {
|
|
197
|
+
return { discipline: guess.discipline, inferred: true, confidence: guess.confidence };
|
|
198
|
+
}
|
|
199
|
+
return { error: 'bad_discipline', unclassifiable: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
166
202
|
// Numeric field bounds for task creation (POST /tasks). The single source of
|
|
167
203
|
// truth: the route schema in routes/tasks.js references these so the validator
|
|
168
204
|
// and the claim-time context pack (V4.R04 / sessions-start-knowing) can never
|
|
@@ -516,6 +552,9 @@ module.exports = {
|
|
|
516
552
|
_resetVendorCacheForTests,
|
|
517
553
|
VALID_KINDS,
|
|
518
554
|
VALID_DISCIPLINES,
|
|
555
|
+
WRITABLE_DISCIPLINES,
|
|
556
|
+
DISCIPLINE_CONFIDENCE_FLOOR,
|
|
557
|
+
resolveWriteDiscipline,
|
|
519
558
|
VOTABLE_TASK_STATUSES,
|
|
520
559
|
TASK_FIELD_BOUNDS,
|
|
521
560
|
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.843",
|
|
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.843",
|
|
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.843",
|
|
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",
|