@bongos/core 1.19.518 → 1.19.520
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 +29 -19
- package/docs/copy-inventory.md +13 -13
- package/docs/copy-registry.json +13 -13
- package/docs/module-api-changelog.md +4 -0
- package/modules/agents/lib/validate.js +306 -0
- package/modules/hall-ui/public/builders.js +8 -4
- package/modules/hall-ui/public/copy-desk.css +5 -5
- package/modules/hall-ui/public/style.css +13 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/agents_validate.mjs +354 -0
- package/tests/hall_audit.mjs +68 -0
- package/tests/hall_collab_world.mjs +6 -3
- package/tests/rank_tier_single_source.mjs +11 -0
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.520",
|
|
6
|
+
"core_contract": "1.19.520",
|
|
7
|
+
"source_commit": "0b8a696f772ade91f8d36d53dece40421efe5b80",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-03T21:58:56.926Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 431,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1969,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2424,
|
|
20
|
+
"tree_sha256": "9c5de15455eed40571117e977c54f1970629a306a7334b87f1327361cde79375",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1747,12 +1747,12 @@
|
|
|
1747
1747
|
{
|
|
1748
1748
|
"path": "docs/copy-inventory.md",
|
|
1749
1749
|
"mode": "0000644",
|
|
1750
|
-
"sha256": "
|
|
1750
|
+
"sha256": "804238f39cba79b0503aee0079bde0272398c96b713e26c4f32298cd1db0fc06"
|
|
1751
1751
|
},
|
|
1752
1752
|
{
|
|
1753
1753
|
"path": "docs/copy-registry.json",
|
|
1754
1754
|
"mode": "0000644",
|
|
1755
|
-
"sha256": "
|
|
1755
|
+
"sha256": "2df6e1780324aa67c25e347010082441d8267ff754ca3bb76c4071228f0c579d"
|
|
1756
1756
|
},
|
|
1757
1757
|
{
|
|
1758
1758
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2597,7 +2597,7 @@
|
|
|
2597
2597
|
{
|
|
2598
2598
|
"path": "docs/module-api-changelog.md",
|
|
2599
2599
|
"mode": "0000644",
|
|
2600
|
-
"sha256": "
|
|
2600
|
+
"sha256": "53c9b5ab7fac89d337952508b1d3b649d48c8ce68c69542d7bc12bdb2847ca75"
|
|
2601
2601
|
},
|
|
2602
2602
|
{
|
|
2603
2603
|
"path": "docs/modules-contract.md",
|
|
@@ -3574,6 +3574,11 @@
|
|
|
3574
3574
|
"mode": "0000644",
|
|
3575
3575
|
"sha256": "8ae32be783a4ca41637f56626bd83375aa4ae0755e701a0c0170eada8d8303b8"
|
|
3576
3576
|
},
|
|
3577
|
+
{
|
|
3578
|
+
"path": "modules/agents/lib/validate.js",
|
|
3579
|
+
"mode": "0000644",
|
|
3580
|
+
"sha256": "505b62e453d62b820720c00abf661f1ca1ffbfd93d3eba1bdcdf20c7cca95ba7"
|
|
3581
|
+
},
|
|
3577
3582
|
{
|
|
3578
3583
|
"path": "modules/agents/migrations/agents_001_definitions.sql",
|
|
3579
3584
|
"mode": "0000644",
|
|
@@ -4512,7 +4517,7 @@
|
|
|
4512
4517
|
{
|
|
4513
4518
|
"path": "modules/hall-ui/public/builders.js",
|
|
4514
4519
|
"mode": "0000644",
|
|
4515
|
-
"sha256": "
|
|
4520
|
+
"sha256": "b9cf0c5dc9e0df09eb16a8c14b3d50d9d8ebcf899da5c7b10318f81fe5615d45"
|
|
4516
4521
|
},
|
|
4517
4522
|
{
|
|
4518
4523
|
"path": "modules/hall-ui/public/chime.wav",
|
|
@@ -4547,7 +4552,7 @@
|
|
|
4547
4552
|
{
|
|
4548
4553
|
"path": "modules/hall-ui/public/copy-desk.css",
|
|
4549
4554
|
"mode": "0000644",
|
|
4550
|
-
"sha256": "
|
|
4555
|
+
"sha256": "d67e5be0a1f2497d5c28d8daa2206cccf1eb81c8aaa2c41b4e41230e88a4baac"
|
|
4551
4556
|
},
|
|
4552
4557
|
{
|
|
4553
4558
|
"path": "modules/hall-ui/public/copy-desk.html",
|
|
@@ -5072,7 +5077,7 @@
|
|
|
5072
5077
|
{
|
|
5073
5078
|
"path": "modules/hall-ui/public/style.css",
|
|
5074
5079
|
"mode": "0000644",
|
|
5075
|
-
"sha256": "
|
|
5080
|
+
"sha256": "2b9c0df306a1749975b101e2ac0af36fa72f78d633a187bd32dba0e5beb4bdfc"
|
|
5076
5081
|
},
|
|
5077
5082
|
{
|
|
5078
5083
|
"path": "modules/hall-ui/public/task-detail.js",
|
|
@@ -7292,12 +7297,12 @@
|
|
|
7292
7297
|
{
|
|
7293
7298
|
"path": "package-lock.json",
|
|
7294
7299
|
"mode": "0000644",
|
|
7295
|
-
"sha256": "
|
|
7300
|
+
"sha256": "23214b713047eeeb22c4a48cabff0d29f24cf56e036c92878db86f628b6ba8d2"
|
|
7296
7301
|
},
|
|
7297
7302
|
{
|
|
7298
7303
|
"path": "package.json",
|
|
7299
7304
|
"mode": "0000644",
|
|
7300
|
-
"sha256": "
|
|
7305
|
+
"sha256": "1c8758322c6d80615ad5dccdc9ac9493299105cf0edf15510e5c30fcb4ed9d15"
|
|
7301
7306
|
},
|
|
7302
7307
|
{
|
|
7303
7308
|
"path": "public-docs/index.html",
|
|
@@ -8967,7 +8972,7 @@
|
|
|
8967
8972
|
{
|
|
8968
8973
|
"path": "src/module-api.js",
|
|
8969
8974
|
"mode": "0000644",
|
|
8970
|
-
"sha256": "
|
|
8975
|
+
"sha256": "4f10f2246fe56fc5cd7e786de45b5f5bb4268bb62a59755428d6d91088f397b7"
|
|
8971
8976
|
},
|
|
8972
8977
|
{
|
|
8973
8978
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9059,6 +9064,11 @@
|
|
|
9059
9064
|
"mode": "0000644",
|
|
9060
9065
|
"sha256": "8cc261471a36e3c447df57b46082997b6a50da3c8ff4b214787ae55ab27cb9ef"
|
|
9061
9066
|
},
|
|
9067
|
+
{
|
|
9068
|
+
"path": "tests/agents_validate.mjs",
|
|
9069
|
+
"mode": "0000644",
|
|
9070
|
+
"sha256": "c698b80ab8d64421b5c81e91172e1f219c41924af19721106f1899cd29c2fabc"
|
|
9071
|
+
},
|
|
9062
9072
|
{
|
|
9063
9073
|
"path": "tests/api_cli_flags.mjs",
|
|
9064
9074
|
"mode": "0000644",
|
|
@@ -10347,7 +10357,7 @@
|
|
|
10347
10357
|
{
|
|
10348
10358
|
"path": "tests/hall_audit.mjs",
|
|
10349
10359
|
"mode": "0000644",
|
|
10350
|
-
"sha256": "
|
|
10360
|
+
"sha256": "a7e7fceda066fbb6a279d3e249afbb29ac225396f16b377cc910854d5ef35165"
|
|
10351
10361
|
},
|
|
10352
10362
|
{
|
|
10353
10363
|
"path": "tests/hall_board_world.mjs",
|
|
@@ -10362,7 +10372,7 @@
|
|
|
10362
10372
|
{
|
|
10363
10373
|
"path": "tests/hall_collab_world.mjs",
|
|
10364
10374
|
"mode": "0000644",
|
|
10365
|
-
"sha256": "
|
|
10375
|
+
"sha256": "b597173a0d280286f0f5ff5d13e0065b2c1dc311024d1f13b5c2d0c3915daf45"
|
|
10366
10376
|
},
|
|
10367
10377
|
{
|
|
10368
10378
|
"path": "tests/hall_error_envelope.mjs",
|
|
@@ -11337,7 +11347,7 @@
|
|
|
11337
11347
|
{
|
|
11338
11348
|
"path": "tests/rank_tier_single_source.mjs",
|
|
11339
11349
|
"mode": "0000644",
|
|
11340
|
-
"sha256": "
|
|
11350
|
+
"sha256": "6436fe1c0779514294cdd5391149b54d3afd634501ae7a70166348fa25739dce"
|
|
11341
11351
|
},
|
|
11342
11352
|
{
|
|
11343
11353
|
"path": "tests/rate_limit_public.mjs",
|
package/docs/copy-inventory.md
CHANGED
|
@@ -132,7 +132,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
132
132
|
| `1b326b0be1a4` | certain | A {…} — you are a {…}. Mingle pairs across crafts. | `modules/hall-ui/public/collab.js:173` |
|
|
133
133
|
| `996edffc1966` | certain | Access requests | `modules/hall-ui/public/watch.html:46` |
|
|
134
134
|
| `996edffc1966` | certain | Access requests | `modules/hall-ui/public/watch.html:61` |
|
|
135
|
-
| `303f8d90266f` | certain | Achievements could not be loaded. | `modules/hall-ui/public/builders.js:
|
|
135
|
+
| `303f8d90266f` | certain | Achievements could not be loaded. | `modules/hall-ui/public/builders.js:1154` |
|
|
136
136
|
| `6ca31dd5eb91` | certain | Acknowledge the primer | `modules/hall-ui/public/primer.html:99` |
|
|
137
137
|
| `4461b13f95c2` | certain | Actions apply at once. Closing stops and deletes the box; you can start a fresh one anytime. | `modules/hall-ui/public/settings.html:209` |
|
|
138
138
|
| `524188fe0078` | certain | Active sessions | `modules/hall-ui/public/settings.html:162` |
|
|
@@ -365,7 +365,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
365
365
|
| `541127601291` | certain | Download for Mac | `modules/hall-ui/public/settings.html:224` |
|
|
366
366
|
| `2ed2fd8376c0` | certain | Download for Windows | `modules/hall-ui/public/settings.html:225` |
|
|
367
367
|
| `c7af57097d94` | certain | Download the Dev Box app: | `modules/hall-ui/public/hall-render.js:750` |
|
|
368
|
-
| `9cf07334a5a9` | certain | Downvote (−1) | `modules/hall-ui/public/builders.js:
|
|
368
|
+
| `9cf07334a5a9` | certain | Downvote (−1) | `modules/hall-ui/public/builders.js:739` |
|
|
369
369
|
| `9c728558d6e7` | certain | Drachmae & karma | `modules/hall-ui/public/drachmae.html:22` |
|
|
370
370
|
| `9c728558d6e7` | certain | Drachmae & karma | `modules/hall-ui/public/drachmae.html:22` |
|
|
371
371
|
| `9c728558d6e7` | certain | Drachmae & karma | `modules/hall-ui/public/drachmae.html:37` |
|
|
@@ -474,7 +474,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
474
474
|
| `a4ff1e9c34e3` | certain | Idea {…} | `modules/hall-ui/public/idea.js:267` |
|
|
475
475
|
| `ba82ec56f1e3` | certain | Idea — {{worldName}} | `modules/hall-ui/public/idea.html:6` |
|
|
476
476
|
| `ba82ec56f1e3` | certain | Idea — {{worldName}} | `modules/hall-ui/public/idea.html:6` |
|
|
477
|
-
| `e181637c5a5e` | certain | Ideas awaiting triage | `modules/hall-ui/public/builders.js:
|
|
477
|
+
| `e181637c5a5e` | certain | Ideas awaiting triage | `modules/hall-ui/public/builders.js:859` |
|
|
478
478
|
| `d4e21769ce1c` | certain | Ideas filed, and what became of them | `modules/hall-ui/public/profile.html:152` |
|
|
479
479
|
| `379b20d548ad` | certain | Ideas whose author has said "I had the thought but not the full picture". Anyone can develop one — the author | `modules/hall-ui/public/ideas.html:42` |
|
|
480
480
|
| `ef224c9de7c8` | certain | Ideas — {{worldName}} | `modules/hall-ui/public/ideas.html:6` |
|
|
@@ -509,7 +509,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
509
509
|
| `93c17262bacf` | certain | Key storage is being set up by an Archon — check back shortly. {…} | `modules/hall-ui/public/settings.js:1072` |
|
|
510
510
|
| `2a3c9e501f8e` | certain | Key storage isn't set up yet. As an Archon you can turn it on now — the server generates and safely stores the | `modules/hall-ui/public/settings.js:1068` |
|
|
511
511
|
| `7d6eaa8d4514` | certain | Kinds of work shipped | `modules/hall-ui/public/hall-render.js:1165` |
|
|
512
|
-
| `81f62e066dbf` | certain | Last 14 days: {…} / 10 average across {…} graded work{…} ({…} passed). | `modules/hall-ui/public/builders.js:
|
|
512
|
+
| `81f62e066dbf` | certain | Last 14 days: {…} / 10 average across {…} graded work{…} ({…} passed). | `modules/hall-ui/public/builders.js:595` |
|
|
513
513
|
| `35a15eb0a117` | certain | Last 30 days | `modules/hall-ui/public/hall-widgets.js:129` |
|
|
514
514
|
| `9be8b839eff8` | certain | Last 7 days | `modules/hall-ui/public/hall-widgets.js:129` |
|
|
515
515
|
| `59f0bc78a28f` | certain | Last 90 days | `modules/hall-ui/public/hall-widgets.js:130` |
|
|
@@ -552,7 +552,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
552
552
|
| `df335a1224ae` | certain | Member since {…} | `modules/hall-ui/public/hall-render.js:499` |
|
|
553
553
|
| `df335a1224ae` | certain | Member since {…} | `modules/hall-ui/public/profile.js:65` |
|
|
554
554
|
| `c7a670e9552d` | certain | Merged into | `modules/hall-ui/public/profile.js:377` |
|
|
555
|
-
| `7637613c231b` | certain | Metic rank and above may vote | `modules/hall-ui/public/builders.js:
|
|
555
|
+
| `7637613c231b` | certain | Metic rank and above may vote | `modules/hall-ui/public/builders.js:743` |
|
|
556
556
|
| `1c485c956f90` | certain | Mingle pairs you with someone from a different craft. Saying no costs nothing at all — no rank, no karma, no s | `modules/hall-ui/public/collab.html:57` |
|
|
557
557
|
| `d7ce4a3bb745` | certain | Minimize the welcome panel | `modules/hall-ui/public/hall-widgets.js:52` |
|
|
558
558
|
| `19b0ed5781ae` | certain | Minimum rank required to claim this task | `modules/hall-ui/public/task-lib.js:65` |
|
|
@@ -873,7 +873,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
873
873
|
| `381f29be7497` | certain | The most recently shipped tasks. | `modules/hall-ui/public/work.html:108` |
|
|
874
874
|
| `82479c539fca` | certain | The name shown beside your work — in the roster, the leaderboard, and the work feed. Your sign-in and the cred | `modules/hall-ui/public/settings.html:75` |
|
|
875
875
|
| `b88dd8b96b72` | certain | The next person to notice this sentence reads your note instead of re-litigating it. | `modules/hall-ui/public/copy-desk.html:135` |
|
|
876
|
-
| `aa716f3dd551` | certain | The page could not be loaded. Try again. | `modules/hall-ui/public/builders.js:
|
|
876
|
+
| `aa716f3dd551` | certain | The page could not be loaded. Try again. | `modules/hall-ui/public/builders.js:1396` |
|
|
877
877
|
| `74bc22f38140` | certain | The platform confirmed this request came from the account it names. | `modules/hall-ui/public/watch.js:349` |
|
|
878
878
|
| `6c3d88de42de` | certain | The primer | `modules/hall-ui/public/primer.html:41` |
|
|
879
879
|
| `dea378cd4837` | certain | The primer did not load | `modules/hall-ui/public/primer.html:88` |
|
|
@@ -935,14 +935,14 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
935
935
|
| `1a64679fbb42` | certain | Typo or grammar | `modules/hall-ui/public/copy-desk.html:94` |
|
|
936
936
|
| `2c1c20785a6e` | certain | Unclear — a person would not know what to do | `modules/hall-ui/public/copy-desk.html:91` |
|
|
937
937
|
| `aa445f06cc5e` | certain | Unlink failed: {…} | `modules/hall-ui/public/settings.js:944` |
|
|
938
|
-
| `67fbc770b3b3` | certain | Upvote (+1) | `modules/hall-ui/public/builders.js:
|
|
938
|
+
| `67fbc770b3b3` | certain | Upvote (+1) | `modules/hall-ui/public/builders.js:739` |
|
|
939
939
|
| `6200bac6bdca` | certain | View all sessions | `modules/hall-ui/public/watch.html:106` |
|
|
940
940
|
| `9b92aa01516d` | certain | View all {…} {…} → | `modules/hall-ui/public/goals-render.js:585` |
|
|
941
941
|
| `4babdfeaf27f` | certain | View full standing → | `modules/hall-ui/public/hall-widgets.js:86` |
|
|
942
942
|
| `f8546b3e285e` | certain | View tasks {…} | `modules/hall-ui/public/roadmap.js:134` |
|
|
943
943
|
| `71c09cda8246` | certain | Voice for {…} | `modules/hall-ui/public/settings.js:76` |
|
|
944
944
|
| `2940b7828d26` | certain | Volume {…}%. | `modules/hall-ui/public/settings.js:218` |
|
|
945
|
-
| `6905f2ee5702` | certain | Vote on this work | `modules/hall-ui/public/builders.js:
|
|
945
|
+
| `6905f2ee5702` | certain | Vote on this work | `modules/hall-ui/public/builders.js:739` |
|
|
946
946
|
| `b2937e14ee9b` | certain | Vote yes | `modules/hall-ui/public/government.js:962` |
|
|
947
947
|
| `8cc425582984` | certain | Vulnerability reports | `modules/hall-ui/public/watch.html:128` |
|
|
948
948
|
| `b1e63e33f739` | certain | Vulnerability reports, engagement reports, override requests, and the audit log. | `modules/hall-ui/public/watch.html:125` |
|
|
@@ -1069,7 +1069,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1069
1069
|
| `89cc9754d633` | certain | e.g. runaway cost — paused pending review | `modules/hall-ui/public/harbor.js:148` |
|
|
1070
1070
|
| `12a23cc46ace` | certain | ends every session including this one — you will be asked to sign in again. | `modules/hall-ui/public/settings.html:172` |
|
|
1071
1071
|
| `5321f9da0841` | certain | exists in schema, no behavior yet | `modules/hall-ui/public/diagrams.html:44` |
|
|
1072
|
-
| `df941da50c7c` | certain | fell short | `modules/hall-ui/public/builders.js:
|
|
1072
|
+
| `df941da50c7c` | certain | fell short | `modules/hall-ui/public/builders.js:572` |
|
|
1073
1073
|
| `31735e119a04` | certain | first vote | `modules/hall-ui/public/government.js:584` |
|
|
1074
1074
|
| `9563bd9f4971` | certain | first — your username is all we need. | `modules/hall-ui/public/hall-render.js:284` |
|
|
1075
1075
|
| `2e111f3a4912` | certain | flagged by | `modules/hall-ui/public/copy-desk.js:148` |
|
|
@@ -1114,8 +1114,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1114
1114
|
| `33ca71bef112` | certain | no tasks yet | `modules/hall-ui/public/goals-render.js:163` |
|
|
1115
1115
|
| `739a4bd6bd83` | certain | node scripts/setup-builder.js | `modules/hall-ui/public/hall-render.js:817` |
|
|
1116
1116
|
| `61f0f4b2f4ab` | certain | not categorised yet | `modules/hall-ui/public/goals-page.js:277` |
|
|
1117
|
-
| `01a3d68d80c1` | certain | not graded (grader outage) | `modules/hall-ui/public/builders.js:
|
|
1118
|
-
| `d43b9bbb8d4d` | certain | not graded (quality lens unavailable) | `modules/hall-ui/public/builders.js:
|
|
1117
|
+
| `01a3d68d80c1` | certain | not graded (grader outage) | `modules/hall-ui/public/builders.js:567` |
|
|
1118
|
+
| `d43b9bbb8d4d` | certain | not graded (quality lens unavailable) | `modules/hall-ui/public/builders.js:569` |
|
|
1119
1119
|
| `3916a3dc8e98` | certain | not started | `modules/hall-ui/public/roadmap.js:108` |
|
|
1120
1120
|
| `3916a3dc8e98` | certain | not started | `modules/hall-ui/public/roadmap.js:263` |
|
|
1121
1121
|
| `b5567b42fa52` | certain | not yet | `modules/hall-ui/public/not-ready.html:25` |
|
|
@@ -1240,7 +1240,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1240
1240
|
| `3d74668f69e5` | certain | {…}{…} · {…} | `modules/hall-ui/public/task.js:546` |
|
|
1241
1241
|
| `9588652a7aff` | certain | · follows your profile setting | `modules/hall-ui/public/settings.js:636` |
|
|
1242
1242
|
| `4a5ff4f662df` | certain | — ${…} of ${…} this month ({…}%). {…} | `modules/hall-ui/public/hall-render.js:461` |
|
|
1243
|
-
| `a6ae9cbdee30` | certain | — all builders — | `modules/hall-ui/public/builders.js:
|
|
1243
|
+
| `a6ae9cbdee30` | certain | — all builders — | `modules/hall-ui/public/builders.js:675` |
|
|
1244
1244
|
| `729d4ce22b99` | certain | — any — | `modules/hall-ui/public/hall-widgets.js:132` |
|
|
1245
1245
|
| `9ac518dfa95e` | certain | — none (the version's general bucket) — | `modules/hall-ui/public/task-detail.js:320` |
|
|
1246
1246
|
| `da9962dc1729` | certain | — or open any diagram fullscreen. | `modules/hall-ui/public/diagrams.html:33` |
|
|
@@ -1253,7 +1253,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1253
1253
|
| `5b9fa7cb4fb0` | certain | …and {…} more waiting — | `modules/hall-ui/public/goal-inbox.js:121` |
|
|
1254
1254
|
| `b2a04abfead0` | certain | …{…} more awaiting a nod. | `modules/hall-ui/public/goal-inbox.js:264` |
|
|
1255
1255
|
| `616fb9adf3cf` | certain | …{…} more going quiet. | `modules/hall-ui/public/goal-inbox.js:353` |
|
|
1256
|
-
| `e35f11bdc5d7` | certain | …{…} more open ideas — triage clears them one at a time. | `modules/hall-ui/public/builders.js:
|
|
1256
|
+
| `e35f11bdc5d7` | certain | …{…} more open ideas — triage clears them one at a time. | `modules/hall-ui/public/builders.js:857` |
|
|
1257
1257
|
| `de8fa5223aee` | certain | ‹ Prev | `modules/hall-ui/public/hall-kit.js:71` |
|
|
1258
1258
|
| `d4b84b7fbdeb` | certain | ← All goals | `modules/hall-ui/public/goals.html:80` |
|
|
1259
1259
|
| `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:184` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -2099,7 +2099,7 @@
|
|
|
2099
2099
|
"surface": "builders-hall",
|
|
2100
2100
|
"text": "Achievements could not be loaded.",
|
|
2101
2101
|
"file": "modules/hall-ui/public/builders.js",
|
|
2102
|
-
"line":
|
|
2102
|
+
"line": 1154,
|
|
2103
2103
|
"origin": "js-markup",
|
|
2104
2104
|
"confidence": "certain"
|
|
2105
2105
|
},
|
|
@@ -4196,7 +4196,7 @@
|
|
|
4196
4196
|
"surface": "builders-hall",
|
|
4197
4197
|
"text": "Downvote (−1)",
|
|
4198
4198
|
"file": "modules/hall-ui/public/builders.js",
|
|
4199
|
-
"line":
|
|
4199
|
+
"line": 739,
|
|
4200
4200
|
"origin": "js-markup",
|
|
4201
4201
|
"confidence": "certain"
|
|
4202
4202
|
},
|
|
@@ -5177,7 +5177,7 @@
|
|
|
5177
5177
|
"surface": "builders-hall",
|
|
5178
5178
|
"text": "Ideas awaiting triage",
|
|
5179
5179
|
"file": "modules/hall-ui/public/builders.js",
|
|
5180
|
-
"line":
|
|
5180
|
+
"line": 859,
|
|
5181
5181
|
"origin": "js-markup",
|
|
5182
5182
|
"confidence": "certain"
|
|
5183
5183
|
},
|
|
@@ -5492,7 +5492,7 @@
|
|
|
5492
5492
|
"surface": "builders-hall",
|
|
5493
5493
|
"text": "Last 14 days: {…} / 10 average across {…} graded work{…} ({…} passed).",
|
|
5494
5494
|
"file": "modules/hall-ui/public/builders.js",
|
|
5495
|
-
"line":
|
|
5495
|
+
"line": 595,
|
|
5496
5496
|
"origin": "js-text-assign",
|
|
5497
5497
|
"confidence": "certain"
|
|
5498
5498
|
},
|
|
@@ -5879,7 +5879,7 @@
|
|
|
5879
5879
|
"surface": "builders-hall",
|
|
5880
5880
|
"text": "Metic rank and above may vote",
|
|
5881
5881
|
"file": "modules/hall-ui/public/builders.js",
|
|
5882
|
-
"line":
|
|
5882
|
+
"line": 743,
|
|
5883
5883
|
"origin": "js-markup",
|
|
5884
5884
|
"confidence": "certain"
|
|
5885
5885
|
},
|
|
@@ -8768,7 +8768,7 @@
|
|
|
8768
8768
|
"surface": "builders-hall",
|
|
8769
8769
|
"text": "The page could not be loaded. Try again.",
|
|
8770
8770
|
"file": "modules/hall-ui/public/builders.js",
|
|
8771
|
-
"line":
|
|
8771
|
+
"line": 1396,
|
|
8772
8772
|
"origin": "js-notice",
|
|
8773
8773
|
"confidence": "certain"
|
|
8774
8774
|
},
|
|
@@ -9326,7 +9326,7 @@
|
|
|
9326
9326
|
"surface": "builders-hall",
|
|
9327
9327
|
"text": "Upvote (+1)",
|
|
9328
9328
|
"file": "modules/hall-ui/public/builders.js",
|
|
9329
|
-
"line":
|
|
9329
|
+
"line": 739,
|
|
9330
9330
|
"origin": "js-markup",
|
|
9331
9331
|
"confidence": "certain"
|
|
9332
9332
|
},
|
|
@@ -9389,7 +9389,7 @@
|
|
|
9389
9389
|
"surface": "builders-hall",
|
|
9390
9390
|
"text": "Vote on this work",
|
|
9391
9391
|
"file": "modules/hall-ui/public/builders.js",
|
|
9392
|
-
"line":
|
|
9392
|
+
"line": 739,
|
|
9393
9393
|
"origin": "js-markup",
|
|
9394
9394
|
"confidence": "certain"
|
|
9395
9395
|
},
|
|
@@ -10532,7 +10532,7 @@
|
|
|
10532
10532
|
"surface": "builders-hall",
|
|
10533
10533
|
"text": "fell short",
|
|
10534
10534
|
"file": "modules/hall-ui/public/builders.js",
|
|
10535
|
-
"line":
|
|
10535
|
+
"line": 572,
|
|
10536
10536
|
"origin": "js-markup",
|
|
10537
10537
|
"confidence": "certain"
|
|
10538
10538
|
},
|
|
@@ -10937,7 +10937,7 @@
|
|
|
10937
10937
|
"surface": "builders-hall",
|
|
10938
10938
|
"text": "not graded (grader outage)",
|
|
10939
10939
|
"file": "modules/hall-ui/public/builders.js",
|
|
10940
|
-
"line":
|
|
10940
|
+
"line": 567,
|
|
10941
10941
|
"origin": "js-markup",
|
|
10942
10942
|
"confidence": "certain"
|
|
10943
10943
|
},
|
|
@@ -10946,7 +10946,7 @@
|
|
|
10946
10946
|
"surface": "builders-hall",
|
|
10947
10947
|
"text": "not graded (quality lens unavailable)",
|
|
10948
10948
|
"file": "modules/hall-ui/public/builders.js",
|
|
10949
|
-
"line":
|
|
10949
|
+
"line": 569,
|
|
10950
10950
|
"origin": "js-markup",
|
|
10951
10951
|
"confidence": "certain"
|
|
10952
10952
|
},
|
|
@@ -12071,7 +12071,7 @@
|
|
|
12071
12071
|
"surface": "builders-hall",
|
|
12072
12072
|
"text": "— all builders —",
|
|
12073
12073
|
"file": "modules/hall-ui/public/builders.js",
|
|
12074
|
-
"line":
|
|
12074
|
+
"line": 675,
|
|
12075
12075
|
"origin": "js-markup",
|
|
12076
12076
|
"confidence": "certain"
|
|
12077
12077
|
},
|
|
@@ -12188,7 +12188,7 @@
|
|
|
12188
12188
|
"surface": "builders-hall",
|
|
12189
12189
|
"text": "…{…} more open ideas — triage clears them one at a time.",
|
|
12190
12190
|
"file": "modules/hall-ui/public/builders.js",
|
|
12191
|
-
"line":
|
|
12191
|
+
"line": 857,
|
|
12192
12192
|
"origin": "js-markup",
|
|
12193
12193
|
"confidence": "certain"
|
|
12194
12194
|
},
|
|
@@ -1473,5 +1473,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1473
1473
|
landed since 1.19.516 with no explicit bump. run 33799755382. (task 1002620)
|
|
1474
1474
|
1.19.518 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1475
1475
|
landed since 1.19.517 with no explicit bump. run 33801792364. (task 1002620)
|
|
1476
|
+
1.19.519 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1477
|
+
landed since 1.19.518 with no explicit bump. run 33802471495. (task 1002620)
|
|
1478
|
+
1.19.520 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1479
|
+
landed since 1.19.519 with no explicit bump. run 33810716476. (task 1002620)
|
|
1476
1480
|
---------------------------------------------------------------------------
|
|
1477
1481
|
```
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/agents/lib/validate.js — the pure agent-definition validator (task
|
|
4
|
+
// 1002487, goal 1000038 Phase 1; criterion `agents-malformed-rejected`).
|
|
5
|
+
//
|
|
6
|
+
// Answers one question with no I/O: "is this object a definition this instance is
|
|
7
|
+
// willing to register?" Given a candidate definition and the caller's context
|
|
8
|
+
// (the author's LIVE rank, the module keys in bounds), it returns a verdict plus
|
|
9
|
+
// the full list of what is wrong with it.
|
|
10
|
+
//
|
|
11
|
+
// WHY IT IS PURE, AND WHY THAT IS THE POINT. Definitions arrive from two places —
|
|
12
|
+
// a committed `.claude/agents/*.md` file reconciled at deploy (agents-sync, task
|
|
13
|
+
// 1002490) and an instance-authored DB row. Both paths need the SAME answer, and
|
|
14
|
+
// the answer must be checkable without a database, a model call, or a booted
|
|
15
|
+
// server. So this file requires NOTHING: not the module doorway, not a sibling,
|
|
16
|
+
// not even a node builtin. That also keeps it trivially inside the ADR 0083
|
|
17
|
+
// boundary — `modules/**` may import only `src/module-api.js`, and importing
|
|
18
|
+
// nothing clears that bar by construction.
|
|
19
|
+
//
|
|
20
|
+
// IT MUST NEVER THROW. The loader's contract is log-and-skip: one malformed
|
|
21
|
+
// definition makes the registry smaller, never makes the boot fail. Every access
|
|
22
|
+
// below is therefore defensive — a hostile getter, a circular `trigger_spec`, a
|
|
23
|
+
// frozen object, `null`: all become a returned error, none become an exception
|
|
24
|
+
// into the caller. The tests assert this directly (tests/agents_validate.mjs).
|
|
25
|
+
//
|
|
26
|
+
// FAIL CLOSED IS THE HOUSE RULE, in three specific places a future reader will be
|
|
27
|
+
// tempted to relax:
|
|
28
|
+
// 1. EVENT_CATALOG is the WIRED set, not the imaginable set. v1 dispatches
|
|
29
|
+
// exactly `task.shipped`. An unwired event name is refused rather than
|
|
30
|
+
// stored, because a definition carrying one looks armed in the registry and
|
|
31
|
+
// can never fire — the worst of both readings.
|
|
32
|
+
// 2. An EMPTY allowed-module set means NOTHING is in scope, never everything.
|
|
33
|
+
// A caller that forgets to pass `allowedModules` gets refusals, not a
|
|
34
|
+
// wide-open agent. Same rule, same reason as goal-scope-check's `empty_scope`.
|
|
35
|
+
// 3. An UNRESOLVED author rank has no authority over a protected module. A file
|
|
36
|
+
// cannot grant authority (ADR 0016), so "we could not determine the rank" is
|
|
37
|
+
// treated as "not enough rank", not as "skip the check".
|
|
38
|
+
//
|
|
39
|
+
// THE DB HOLDS THESE BOUNDS TOO, DELIBERATELY. Every limit here mirrors a CHECK
|
|
40
|
+
// constraint in agents_001_definitions.sql. That duplication is defense in depth
|
|
41
|
+
// and is meant to stay: the constraint is what stops a row that skipped this
|
|
42
|
+
// validator, and this validator is what turns a would-be constraint violation
|
|
43
|
+
// into a readable message naming the offending field — an insert that throws at
|
|
44
|
+
// boot is precisely the crash the log-and-skip contract exists to prevent.
|
|
45
|
+
|
|
46
|
+
// The WIRED event set for v1. Widening this is a deliberate act that must land
|
|
47
|
+
// together with something that actually dispatches the new event.
|
|
48
|
+
const EVENT_CATALOG = Object.freeze(['task.shipped']);
|
|
49
|
+
|
|
50
|
+
// Mirrors agents_definitions_trigger_type_chk. 'event' consumes the catalog above;
|
|
51
|
+
// 'on-demand' is the historian shape (a builder asks, the agent answers).
|
|
52
|
+
const TRIGGER_TYPES = Object.freeze(['event', 'on-demand']);
|
|
53
|
+
|
|
54
|
+
// Mirrors agents_definitions_model_tier_chk. No 'main' tier by design — an agent
|
|
55
|
+
// is a subagent, never the main session.
|
|
56
|
+
const MODEL_TIERS = Object.freeze(['default', 'routine']);
|
|
57
|
+
|
|
58
|
+
// The rank ladder, low → high. Mirrors government/catalog.js RANK_ORDER and the
|
|
59
|
+
// agents_definitions_author_rank_chk vocabulary. Duplicated rather than imported
|
|
60
|
+
// because the module boundary forbids reaching into core (ADR 0083) — and it is a
|
|
61
|
+
// closed, rarely-changing vocabulary the DB constraint pins independently.
|
|
62
|
+
const RANK_ORDER = Object.freeze(['xenos', 'thetes', 'metic', 'archon']);
|
|
63
|
+
|
|
64
|
+
// Sub-Metic builders stay out of the permission/methodology core (ADR 0043), and
|
|
65
|
+
// that applies to an agent they author just as it applies to a task they claim.
|
|
66
|
+
const SCOPE_RANK_FLOOR = 'metic';
|
|
67
|
+
|
|
68
|
+
// Filename / URL-segment / CLI-argument safe — the same shape a module key takes,
|
|
69
|
+
// mirroring agents_definitions_name_fmt_chk.
|
|
70
|
+
const NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
71
|
+
const NAME_MAX = 64;
|
|
72
|
+
// Mirrors agents_definitions_persona_size_chk / _trigger_spec_size_chk. A persona
|
|
73
|
+
// is a prompt sent to a paid model on every fire, so an unbounded one is both a
|
|
74
|
+
// cost and a blast radius.
|
|
75
|
+
const PERSONA_MAX = 32768;
|
|
76
|
+
const TRIGGER_SPEC_MAX = 8192;
|
|
77
|
+
|
|
78
|
+
// A plain data object — not an array, not null, not a boxed primitive. Arrays are
|
|
79
|
+
// excluded explicitly because `typeof [] === 'object'` would otherwise let a list
|
|
80
|
+
// masquerade as a spec.
|
|
81
|
+
function isPlainObject(v) {
|
|
82
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isNonEmptyString(v) {
|
|
86
|
+
return typeof v === 'string' && v.trim().length > 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Read one property without trusting the object. A definition can arrive as a
|
|
90
|
+
// db-sourced object with an accessor on it; a throwing getter must become a
|
|
91
|
+
// refusal, not an exception into the loader.
|
|
92
|
+
function safeGet(obj, key) {
|
|
93
|
+
try { return obj[key]; } catch (_) { return undefined; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A scope path must stay inside the tree. Rejects absolute POSIX and Windows
|
|
97
|
+
// paths, and any `..` segment on either separator — the escape a scope wall
|
|
98
|
+
// exists to stop.
|
|
99
|
+
function isSafeScopePath(p) {
|
|
100
|
+
if (!isNonEmptyString(p)) return false;
|
|
101
|
+
if (p.startsWith('/') || p.startsWith('\\')) return false;
|
|
102
|
+
if (/^[a-zA-Z]:/.test(p)) return false;
|
|
103
|
+
return !p.split(/[\\/]/).includes('..');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Validate one candidate agent definition. Pure, total, and never throws.
|
|
108
|
+
*
|
|
109
|
+
* @param {object} obj the candidate definition (file frontmatter or a db row).
|
|
110
|
+
* @param {object} [opts]
|
|
111
|
+
* @param {string|null} [opts.authorRank] the author's LIVE DB rank (ADR 0016).
|
|
112
|
+
* Never read from the definition itself.
|
|
113
|
+
* @param {string[]} [opts.allowedModules] module keys this definition may be
|
|
114
|
+
* scoped to. Empty ⇒ nothing is in scope.
|
|
115
|
+
* @param {string[]} [opts.protectedModules] the subset of those that require Metic+.
|
|
116
|
+
* @returns {{ok: boolean, errors: Array<{code: string, field: string, message: string}>, value: object|null}}
|
|
117
|
+
* `value` is the normalized definition (defaults applied) on success, and
|
|
118
|
+
* `null` on any failure — a rejected definition must never hand back a row
|
|
119
|
+
* something downstream could insert.
|
|
120
|
+
*/
|
|
121
|
+
function validateAgentDefinition(obj, opts = {}) {
|
|
122
|
+
const errors = [];
|
|
123
|
+
const E = (code, field, message) => errors.push({ code, field, message });
|
|
124
|
+
|
|
125
|
+
if (!isPlainObject(obj)) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
errors: [{
|
|
129
|
+
code: 'not_an_object',
|
|
130
|
+
field: '(root)',
|
|
131
|
+
message: `An agent definition must be an object; received ${obj === null ? 'null' : Array.isArray(obj) ? 'an array' : typeof obj}.`,
|
|
132
|
+
}],
|
|
133
|
+
value: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Read through safeGet for the same reason the definition is: a caller may hand
|
|
138
|
+
// us a proxy or an accessor-bearing object, and the never-throws contract covers
|
|
139
|
+
// the whole call, not just the definition half of it.
|
|
140
|
+
const options = isPlainObject(opts) ? opts : {};
|
|
141
|
+
const authorRank = safeGet(options, 'authorRank');
|
|
142
|
+
const rawAllowed = safeGet(options, 'allowedModules');
|
|
143
|
+
const rawProtected = safeGet(options, 'protectedModules');
|
|
144
|
+
const allowedModules = Array.isArray(rawAllowed) ? rawAllowed : [];
|
|
145
|
+
const protectedModules = Array.isArray(rawProtected) ? rawProtected : [];
|
|
146
|
+
|
|
147
|
+
// --- name ----------------------------------------------------------------
|
|
148
|
+
const name = safeGet(obj, 'name');
|
|
149
|
+
if (!isNonEmptyString(name) || !NAME_RE.test(name) || name.length > NAME_MAX) {
|
|
150
|
+
E('name_invalid', 'name',
|
|
151
|
+
`name: ${JSON.stringify(name)} must be lowercase kebab-case matching ${NAME_RE} and at most ${NAME_MAX} characters — it is used as a filename, a URL segment and a CLI argument.`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- persona --------------------------------------------------------------
|
|
155
|
+
const persona = safeGet(obj, 'persona');
|
|
156
|
+
if (!isNonEmptyString(persona)) {
|
|
157
|
+
E('persona_missing', 'persona',
|
|
158
|
+
'persona: required — an agent with no role prompt has nothing to run. Give it a non-empty string saying what it observes and reports.');
|
|
159
|
+
} else if (persona.length > PERSONA_MAX) {
|
|
160
|
+
E('persona_too_long', 'persona',
|
|
161
|
+
`persona: ${persona.length} characters exceeds the ${PERSONA_MAX} limit — the persona is sent to a paid model on every fire.`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- trigger type ---------------------------------------------------------
|
|
165
|
+
const triggerType = safeGet(obj, 'trigger_type');
|
|
166
|
+
const triggerTypeOk = typeof triggerType === 'string' && TRIGGER_TYPES.includes(triggerType);
|
|
167
|
+
if (!triggerTypeOk) {
|
|
168
|
+
E('trigger_type_unknown', 'trigger_type',
|
|
169
|
+
`trigger_type: ${JSON.stringify(triggerType)} is not a wired trigger type. v1 wires exactly: ${TRIGGER_TYPES.join(', ')}.`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- trigger spec ---------------------------------------------------------
|
|
173
|
+
// Defaults to {} when absent, matching the column default. Present-but-wrong is
|
|
174
|
+
// a refusal; absent is a legitimate on-demand definition.
|
|
175
|
+
const rawSpec = safeGet(obj, 'trigger_spec');
|
|
176
|
+
let spec = null;
|
|
177
|
+
if (rawSpec === undefined || rawSpec === null) {
|
|
178
|
+
spec = {};
|
|
179
|
+
} else if (!isPlainObject(rawSpec)) {
|
|
180
|
+
E('trigger_spec_not_object', 'trigger_spec',
|
|
181
|
+
`trigger_spec: must be an object of parameters; received ${Array.isArray(rawSpec) ? 'an array' : typeof rawSpec}. It carries the trigger's parameters, never a payload.`);
|
|
182
|
+
} else {
|
|
183
|
+
spec = rawSpec;
|
|
184
|
+
// The size bound is over the SERIALIZED spec, exactly as the DB CHECK measures
|
|
185
|
+
// it — and JSON.stringify throws on a cycle, which is why this is guarded.
|
|
186
|
+
let serialized = null;
|
|
187
|
+
try {
|
|
188
|
+
serialized = JSON.stringify(spec);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
E('trigger_spec_unserializable', 'trigger_spec',
|
|
191
|
+
`trigger_spec: could not be serialized (${(err && err.message) || 'circular structure'}) — it must be plain JSON data.`);
|
|
192
|
+
spec = null;
|
|
193
|
+
}
|
|
194
|
+
if (serialized !== null && serialized.length > TRIGGER_SPEC_MAX) {
|
|
195
|
+
E('trigger_spec_too_long', 'trigger_spec',
|
|
196
|
+
`trigger_spec: ${serialized.length} serialized characters exceeds the ${TRIGGER_SPEC_MAX} limit — it is parameters, never payload.`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- the event catalog, fail-closed ---------------------------------------
|
|
201
|
+
// Checked whenever an event NAME is present, and additionally REQUIRED for an
|
|
202
|
+
// event trigger. An inert `event:` on an on-demand definition is either a
|
|
203
|
+
// typo'd trigger_type or dead config; refusing an unwired name in both places
|
|
204
|
+
// is what "fails closed on any unwired event name" actually means.
|
|
205
|
+
if (spec) {
|
|
206
|
+
const event = safeGet(spec, 'event');
|
|
207
|
+
const hasEventKey = event !== undefined && event !== null;
|
|
208
|
+
if (triggerType === 'event' && !isNonEmptyString(event)) {
|
|
209
|
+
E('trigger_event_missing', 'trigger_spec.event',
|
|
210
|
+
`trigger_spec.event: required for an 'event' trigger — a definition with no event name can never fire. Wired events: ${EVENT_CATALOG.join(', ')}.`);
|
|
211
|
+
} else if (hasEventKey && isNonEmptyString(event) && !EVENT_CATALOG.includes(event)) {
|
|
212
|
+
E('trigger_event_unwired', 'trigger_spec.event',
|
|
213
|
+
`trigger_spec.event: ${JSON.stringify(event)} is not a wired event and is refused rather than stored — nothing dispatches it, so the agent would look armed and never fire. Wired events: ${EVENT_CATALOG.join(', ')}.`);
|
|
214
|
+
} else if (hasEventKey && !isNonEmptyString(event) && triggerType !== 'event') {
|
|
215
|
+
E('trigger_event_unwired', 'trigger_spec.event',
|
|
216
|
+
`trigger_spec.event: ${JSON.stringify(event)} is not a wired event name. Wired events: ${EVENT_CATALOG.join(', ')}.`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- model tier -----------------------------------------------------------
|
|
221
|
+
const rawTier = safeGet(obj, 'model_tier');
|
|
222
|
+
const modelTier = rawTier === undefined || rawTier === null ? 'default' : rawTier;
|
|
223
|
+
if (!MODEL_TIERS.includes(modelTier)) {
|
|
224
|
+
E('model_tier_unknown', 'model_tier',
|
|
225
|
+
`model_tier: ${JSON.stringify(rawTier)} is not a known tier (${MODEL_TIERS.join(', ')}). The tier is stored and resolved to a concrete model at fire time; there is deliberately no 'main' tier.`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- scope: shape ---------------------------------------------------------
|
|
229
|
+
const rawScopeModules = safeGet(obj, 'scope_modules');
|
|
230
|
+
let scopeModules = null;
|
|
231
|
+
if (rawScopeModules === undefined || rawScopeModules === null) {
|
|
232
|
+
scopeModules = [];
|
|
233
|
+
} else if (!Array.isArray(rawScopeModules) || !rawScopeModules.every(isNonEmptyString)) {
|
|
234
|
+
E('scope_modules_invalid', 'scope_modules',
|
|
235
|
+
'scope_modules: must be an array of non-empty module-key strings.');
|
|
236
|
+
} else {
|
|
237
|
+
scopeModules = rawScopeModules;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const rawScopePaths = safeGet(obj, 'scope_paths');
|
|
241
|
+
let scopePaths = null;
|
|
242
|
+
if (rawScopePaths === undefined || rawScopePaths === null) {
|
|
243
|
+
scopePaths = [];
|
|
244
|
+
} else if (!Array.isArray(rawScopePaths) || !rawScopePaths.every(isNonEmptyString)) {
|
|
245
|
+
E('scope_paths_invalid', 'scope_paths',
|
|
246
|
+
'scope_paths: must be an array of non-empty repo-relative path strings.');
|
|
247
|
+
} else {
|
|
248
|
+
const escaping = rawScopePaths.filter((p) => !isSafeScopePath(p));
|
|
249
|
+
if (escaping.length > 0) {
|
|
250
|
+
E('scope_paths_invalid', 'scope_paths',
|
|
251
|
+
`scope_paths: ${escaping.map((p) => JSON.stringify(p)).join(', ')} escape the repository tree — a scope path must be relative and contain no '..' segment.`);
|
|
252
|
+
} else {
|
|
253
|
+
scopePaths = rawScopePaths;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// --- scope: the wall ------------------------------------------------------
|
|
258
|
+
// Note the empty-allowlist behaviour: with nothing allowed, every declared
|
|
259
|
+
// module is out of bounds. That is the fail-closed default, not an oversight.
|
|
260
|
+
if (scopeModules && scopeModules.length > 0) {
|
|
261
|
+
const outOfBounds = scopeModules.filter((m) => !allowedModules.includes(m));
|
|
262
|
+
if (outOfBounds.length > 0) {
|
|
263
|
+
E('scope_out_of_bounds', 'scope_modules',
|
|
264
|
+
`scope_modules: ${outOfBounds.map((m) => JSON.stringify(m)).join(', ')} ${outOfBounds.length === 1 ? 'is' : 'are'} outside the allowed module set (${allowedModules.length ? allowedModules.join(', ') : 'none — no allowed modules were supplied, so nothing is in scope'}).`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// The rank wall, over whatever scope survived the bounds check. An unresolved
|
|
268
|
+
// rank is treated as insufficient, never as a reason to skip the check.
|
|
269
|
+
const protectedHits = scopeModules.filter((m) => protectedModules.includes(m));
|
|
270
|
+
if (protectedHits.length > 0) {
|
|
271
|
+
const rankIdx = typeof authorRank === 'string' ? RANK_ORDER.indexOf(authorRank) : -1;
|
|
272
|
+
if (rankIdx < RANK_ORDER.indexOf(SCOPE_RANK_FLOOR)) {
|
|
273
|
+
E('scope_protected_rank', 'scope_modules',
|
|
274
|
+
`scope_modules: ${protectedHits.map((m) => JSON.stringify(m)).join(', ')} ${protectedHits.length === 1 ? 'is a protected module' : 'are protected modules'} and require a ${SCOPE_RANK_FLOOR}+ author; the author's resolved rank is ${JSON.stringify(authorRank ?? null)} (ADR 0016 / ADR 0043). Authority comes from the author's live rank, never from the definition.`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (errors.length > 0) return { ok: false, errors, value: null };
|
|
280
|
+
|
|
281
|
+
// The normalized row, built fresh — the caller's object is never mutated, and
|
|
282
|
+
// arrays are copied so a later mutation of the input cannot reach the registry.
|
|
283
|
+
return {
|
|
284
|
+
ok: true,
|
|
285
|
+
errors: [],
|
|
286
|
+
value: {
|
|
287
|
+
name,
|
|
288
|
+
title: isNonEmptyString(safeGet(obj, 'title')) ? safeGet(obj, 'title') : null,
|
|
289
|
+
persona,
|
|
290
|
+
trigger_type: triggerType,
|
|
291
|
+
trigger_spec: { ...spec },
|
|
292
|
+
model_tier: modelTier,
|
|
293
|
+
scope_modules: [...scopeModules],
|
|
294
|
+
scope_paths: [...scopePaths],
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
validateAgentDefinition,
|
|
301
|
+
EVENT_CATALOG,
|
|
302
|
+
TRIGGER_TYPES,
|
|
303
|
+
MODEL_TIERS,
|
|
304
|
+
RANK_ORDER,
|
|
305
|
+
SCOPE_RANK_FLOOR,
|
|
306
|
+
};
|
|
@@ -559,13 +559,17 @@
|
|
|
559
559
|
// is likewise not a judgment: the score is the surviving axis, not a fail.
|
|
560
560
|
const isLensOut = !isOutage && !!g.rubric_scores && g.rubric_scores.code_quality === null;
|
|
561
561
|
const score = isOutage || isLensOut ? '—' : Number(g.score).toFixed(1);
|
|
562
|
+
// A verdict is verification state: it wears the fixed vocabulary through
|
|
563
|
+
// .ledger__verdict--* (style.css), never an inline colour. The literals
|
|
564
|
+
// that stood here painted `passed` from a --olive that is not a token of
|
|
565
|
+
// the world, so every hall ever rendered it as the fallback (task 1003495).
|
|
562
566
|
const verdict = isOutage
|
|
563
|
-
? `<span
|
|
567
|
+
? `<span class="ledger__verdict--none">not graded (grader outage)</span>`
|
|
564
568
|
: isLensOut
|
|
565
|
-
? `<span
|
|
569
|
+
? `<span class="ledger__verdict--none">not graded (quality lens unavailable)</span>`
|
|
566
570
|
: g.passed
|
|
567
|
-
? `<span
|
|
568
|
-
: `<span
|
|
571
|
+
? `<span class="ledger__verdict--ok">passed</span>`
|
|
572
|
+
: `<span class="ledger__verdict--danger">fell short</span>`;
|
|
569
573
|
return `
|
|
570
574
|
<tr>
|
|
571
575
|
<td class="ledger__num">#${g.task_id}</td>
|
|
@@ -68,12 +68,12 @@
|
|
|
68
68
|
.cd-search-count { margin: 0 0 14px; }
|
|
69
69
|
|
|
70
70
|
/* The three dialogs are the family modal on a native <dialog>: bg-card, one rule
|
|
71
|
-
hairline, no shadow. The backdrop is
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
in
|
|
71
|
+
hairline, no shadow. The backdrop is a SCRIM — it has to darken what is behind
|
|
72
|
+
it in EITHER mode, so it can take no ground token and no mix of --ink (which is
|
|
73
|
+
white on black). That is why it was a literal; task 1003495 gave the value a
|
|
74
|
+
home in style.css as --scrim instead, so the sheet keeps none. */
|
|
75
75
|
.cd-dialog { width: min(480px, calc(100vw - 32px)); color: var(--ink); }
|
|
76
|
-
.cd-dialog::backdrop { background:
|
|
76
|
+
.cd-dialog::backdrop { background: var(--scrim); }
|
|
77
77
|
.cd-dialog .ov-form { margin: 14px 0 0; }
|
|
78
78
|
.cd-dialog textarea { resize: vertical; line-height: 1.5; }
|
|
79
79
|
.ov-modal .cd-dialog__err { min-height: 18px; margin: 8px 0 0; font-size: 13px; line-height: 1.45; color: var(--danger); }
|
|
@@ -146,6 +146,12 @@
|
|
|
146
146
|
its own edge over whatever it lands on. ---- */
|
|
147
147
|
--focus: color-mix(in srgb, var(--accent-deep) 82%, #3a1400);
|
|
148
148
|
|
|
149
|
+
/* ---- derived: the modal scrim. A backdrop DARKENS whatever is behind it
|
|
150
|
+
in BOTH modes, so it cannot be mixed from --ink (white on black) —
|
|
151
|
+
it is black with alpha, and lives here so no sheet keeps its own
|
|
152
|
+
literal. Both modes take this one value (task 1003495). ---- */
|
|
153
|
+
--scrim: rgba(0, 0, 0, .55);
|
|
154
|
+
|
|
149
155
|
/* ---- derived: the label face flips. Mono caps read as precise on paper and
|
|
150
156
|
thin on black, so light takes the mono and dark takes the body face.
|
|
151
157
|
Take the token, never the family. ---- */
|
|
@@ -1476,6 +1482,13 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
1476
1482
|
.ledger__date { color: var(--ink-soft); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
1477
1483
|
.ledger__rank { white-space: nowrap; }
|
|
1478
1484
|
.ledger__note { font-size: 12.5px; color: var(--ink-faint); margin: 10px 0 0; }
|
|
1485
|
+
/* A grade verdict is verification STATE, so it wears the fixed vocabulary
|
|
1486
|
+
(--ok / --danger / --ink-soft) the same way .rec-pill does on the record
|
|
1487
|
+
pages — never a page-local literal, and never a hex fallback standing in
|
|
1488
|
+
for a token the world does not define (task 1003495). */
|
|
1489
|
+
.ledger__verdict--ok { color: var(--ok); font-weight: 600; }
|
|
1490
|
+
.ledger__verdict--danger { color: var(--danger); font-weight: 600; }
|
|
1491
|
+
.ledger__verdict--none { color: var(--ink-soft); }
|
|
1479
1492
|
.ledger th.ledger__sortable { cursor: pointer; user-select: none; }
|
|
1480
1493
|
.ledger th.ledger__sortable:hover { color: var(--ink); }
|
|
1481
1494
|
.ledger th.ledger__sortable[aria-sort="ascending"]::after { content: " ↑"; }
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.520",
|
|
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.520",
|
|
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.520",
|
|
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",
|
package/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.520'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// tests/agents_validate.mjs — the pure agent-definition validator (task 1002487,
|
|
2
|
+
// goal 1000038).
|
|
3
|
+
//
|
|
4
|
+
// Gates criterion `agents-malformed-rejected`: a malformed definition (unknown
|
|
5
|
+
// trigger; missing persona; `trigger_spec.event` not in EVENT_CATALOG; scope
|
|
6
|
+
// exceeding an allowed module) is refused with a clear error, and the validator
|
|
7
|
+
// NEVER throws — the loader logs and skips, it does not crash the boot.
|
|
8
|
+
//
|
|
9
|
+
// No database, no fixtures, no fs. Every input is a literal, because the thing
|
|
10
|
+
// under test is a pure decision.
|
|
11
|
+
//
|
|
12
|
+
// Run: node tests/agents_validate.mjs
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const {
|
|
19
|
+
validateAgentDefinition,
|
|
20
|
+
EVENT_CATALOG,
|
|
21
|
+
TRIGGER_TYPES,
|
|
22
|
+
MODEL_TIERS,
|
|
23
|
+
RANK_ORDER,
|
|
24
|
+
SCOPE_RANK_FLOOR,
|
|
25
|
+
} = require('../modules/agents/lib/validate.js');
|
|
26
|
+
|
|
27
|
+
let passed = 0;
|
|
28
|
+
let failed = 0;
|
|
29
|
+
async function test(name, fn) {
|
|
30
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
31
|
+
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A definition that passes everything, so each case below can break exactly one
|
|
35
|
+
// thing and attribute the refusal to that one thing.
|
|
36
|
+
const OK_DEF = Object.freeze({
|
|
37
|
+
name: 'historian',
|
|
38
|
+
persona: 'Observe the shipped task and report what changed.',
|
|
39
|
+
trigger_type: 'event',
|
|
40
|
+
trigger_spec: { event: 'task.shipped' },
|
|
41
|
+
model_tier: 'default',
|
|
42
|
+
scope_modules: ['agents'],
|
|
43
|
+
});
|
|
44
|
+
const OPTS = Object.freeze({ authorRank: 'archon', allowedModules: ['agents'] });
|
|
45
|
+
|
|
46
|
+
const codes = (r) => r.errors.map((e) => e.code);
|
|
47
|
+
const hasCode = (r, code) => codes(r).includes(code);
|
|
48
|
+
|
|
49
|
+
// --- THE BASELINE -----------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
await test('the exemplar definition validates, and normalizes into a value', () => {
|
|
52
|
+
const r = validateAgentDefinition(OK_DEF, OPTS);
|
|
53
|
+
assert.equal(r.ok, true, `expected ok, got errors: ${JSON.stringify(r.errors)}`);
|
|
54
|
+
assert.deepEqual(r.errors, []);
|
|
55
|
+
assert.equal(r.value.name, 'historian');
|
|
56
|
+
assert.equal(r.value.model_tier, 'default');
|
|
57
|
+
assert.deepEqual(r.value.scope_modules, ['agents']);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await test('a failed validation returns NO value — nothing downstream can insert it', () => {
|
|
61
|
+
const r = validateAgentDefinition({ ...OK_DEF, persona: '' }, OPTS);
|
|
62
|
+
assert.equal(r.ok, false);
|
|
63
|
+
assert.equal(r.value, null, 'a rejected definition must not hand back a usable row');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// --- THE EVENT CATALOG — the fail-closed rule -------------------------------
|
|
67
|
+
|
|
68
|
+
await test('EVENT_CATALOG is exactly the v1 wired set, and is frozen', () => {
|
|
69
|
+
assert.deepEqual(EVENT_CATALOG, ['task.shipped']);
|
|
70
|
+
assert.equal(Object.isFrozen(EVENT_CATALOG), true, 'a caller must not be able to widen the catalog at runtime');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await test('an UNWIRED event name is REFUSED, never silently accepted', () => {
|
|
74
|
+
// The whole point of the catalog: an event nothing dispatches would produce an
|
|
75
|
+
// agent that looks armed and never fires.
|
|
76
|
+
for (const bad of ['task.completed', 'task.shipped ', 'TASK.SHIPPED', 'claim.created', 'task.*']) {
|
|
77
|
+
const r = validateAgentDefinition({ ...OK_DEF, trigger_spec: { event: bad } }, OPTS);
|
|
78
|
+
assert.equal(r.ok, false, `"${bad}" must be refused`);
|
|
79
|
+
assert.ok(hasCode(r, 'trigger_event_unwired'), `"${bad}" → ${JSON.stringify(codes(r))}`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await test('the refusal NAMES the offending event and the catalog', () => {
|
|
84
|
+
const r = validateAgentDefinition({ ...OK_DEF, trigger_spec: { event: 'task.completed' } }, OPTS);
|
|
85
|
+
const msg = r.errors.find((e) => e.code === 'trigger_event_unwired').message;
|
|
86
|
+
assert.ok(msg.includes('task.completed'), `message must name the offender: ${msg}`);
|
|
87
|
+
assert.ok(msg.includes('task.shipped'), `message must name what IS wired: ${msg}`);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await test('an event trigger with no event name is refused', () => {
|
|
91
|
+
for (const spec of [{}, { event: '' }, { event: ' ' }, { event: null }, { event: 42 }]) {
|
|
92
|
+
const r = validateAgentDefinition({ ...OK_DEF, trigger_spec: spec }, OPTS);
|
|
93
|
+
assert.equal(r.ok, false, `${JSON.stringify(spec)} must be refused`);
|
|
94
|
+
assert.ok(
|
|
95
|
+
hasCode(r, 'trigger_event_missing') || hasCode(r, 'trigger_event_unwired'),
|
|
96
|
+
`${JSON.stringify(spec)} → ${JSON.stringify(codes(r))}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await test('an on-demand definition needs no event at all', () => {
|
|
102
|
+
const r = validateAgentDefinition(
|
|
103
|
+
{ ...OK_DEF, trigger_type: 'on-demand', trigger_spec: {} }, OPTS,
|
|
104
|
+
);
|
|
105
|
+
assert.equal(r.ok, true, JSON.stringify(r.errors));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await test('an on-demand definition carrying an UNWIRED event is still refused', () => {
|
|
109
|
+
// Fails closed on the event NAME wherever it appears. An inert `event:` key on
|
|
110
|
+
// an on-demand agent is either a typo'd trigger_type or dead config; both are
|
|
111
|
+
// bugs the author should be shown rather than have silently accepted.
|
|
112
|
+
const r = validateAgentDefinition(
|
|
113
|
+
{ ...OK_DEF, trigger_type: 'on-demand', trigger_spec: { event: 'task.completed' } }, OPTS,
|
|
114
|
+
);
|
|
115
|
+
assert.equal(r.ok, false);
|
|
116
|
+
assert.ok(hasCode(r, 'trigger_event_unwired'));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await test('an on-demand definition carrying a WIRED event is accepted', () => {
|
|
120
|
+
const r = validateAgentDefinition(
|
|
121
|
+
{ ...OK_DEF, trigger_type: 'on-demand', trigger_spec: { event: 'task.shipped' } }, OPTS,
|
|
122
|
+
);
|
|
123
|
+
assert.equal(r.ok, true, JSON.stringify(r.errors));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// --- THE TRIGGER TYPE -------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
await test('TRIGGER_TYPES is the v1 pair the DB CHECK also pins', () => {
|
|
129
|
+
assert.deepEqual(TRIGGER_TYPES, ['event', 'on-demand']);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await test('an unknown trigger type is refused', () => {
|
|
133
|
+
for (const bad of ['cron', 'webhook', 'Event', 'on demand', '', null, undefined, 7, {}]) {
|
|
134
|
+
const r = validateAgentDefinition({ ...OK_DEF, trigger_type: bad }, OPTS);
|
|
135
|
+
assert.equal(r.ok, false, `${JSON.stringify(bad)} must be refused`);
|
|
136
|
+
assert.ok(hasCode(r, 'trigger_type_unknown'), `${JSON.stringify(bad)} → ${JSON.stringify(codes(r))}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await test('a non-object trigger_spec is refused rather than coerced', () => {
|
|
141
|
+
for (const bad of ['task.shipped', 42, [], true]) {
|
|
142
|
+
const r = validateAgentDefinition({ ...OK_DEF, trigger_spec: bad }, OPTS);
|
|
143
|
+
assert.equal(r.ok, false, `${JSON.stringify(bad)} must be refused`);
|
|
144
|
+
assert.ok(hasCode(r, 'trigger_spec_not_object'), `${JSON.stringify(bad)} → ${JSON.stringify(codes(r))}`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// --- THE PERSONA ------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
await test('a missing or empty persona is refused', () => {
|
|
151
|
+
for (const bad of [undefined, null, '', ' ', 42, {}]) {
|
|
152
|
+
const r = validateAgentDefinition({ ...OK_DEF, persona: bad }, OPTS);
|
|
153
|
+
assert.equal(r.ok, false, `${JSON.stringify(bad)} must be refused`);
|
|
154
|
+
assert.ok(hasCode(r, 'persona_missing'), `${JSON.stringify(bad)} → ${JSON.stringify(codes(r))}`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await test('an oversized persona is refused at the same bound the DB CHECK holds', () => {
|
|
159
|
+
const r = validateAgentDefinition({ ...OK_DEF, persona: 'x'.repeat(32769) }, OPTS);
|
|
160
|
+
assert.equal(r.ok, false);
|
|
161
|
+
assert.ok(hasCode(r, 'persona_too_long'));
|
|
162
|
+
// ...and exactly at the bound it is fine.
|
|
163
|
+
assert.equal(validateAgentDefinition({ ...OK_DEF, persona: 'x'.repeat(32768) }, OPTS).ok, true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// --- THE NAME ---------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
await test('a name that could not be a filename / URL segment / CLI arg is refused', () => {
|
|
169
|
+
for (const bad of ['Historian', '1historian', 'the historian', 'his/torian', '-historian', '', 'a'.repeat(65)]) {
|
|
170
|
+
const r = validateAgentDefinition({ ...OK_DEF, name: bad }, OPTS);
|
|
171
|
+
assert.equal(r.ok, false, `"${bad}" must be refused`);
|
|
172
|
+
assert.ok(hasCode(r, 'name_invalid'), `"${bad}" → ${JSON.stringify(codes(r))}`);
|
|
173
|
+
}
|
|
174
|
+
assert.equal(validateAgentDefinition({ ...OK_DEF, name: 'a'.repeat(64) }, OPTS).ok, true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// --- THE MODEL TIER ---------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
await test('model_tier defaults to `default` when absent, and rejects an unknown tier', () => {
|
|
180
|
+
assert.deepEqual(MODEL_TIERS, ['default', 'routine']);
|
|
181
|
+
const { model_tier: _omit, ...noTier } = OK_DEF;
|
|
182
|
+
const r = validateAgentDefinition(noTier, OPTS);
|
|
183
|
+
assert.equal(r.ok, true, JSON.stringify(r.errors));
|
|
184
|
+
assert.equal(r.value.model_tier, 'default');
|
|
185
|
+
|
|
186
|
+
// spawn.js's resolveModel() deliberately DEGRADES an unknown tier to the default
|
|
187
|
+
// tier at fire time; that is a runtime safety net, not a licence to author one.
|
|
188
|
+
const bad = validateAgentDefinition({ ...OK_DEF, model_tier: 'main' }, OPTS);
|
|
189
|
+
assert.equal(bad.ok, false);
|
|
190
|
+
assert.ok(hasCode(bad, 'model_tier_unknown'));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await test('there is no `main` tier — an agent is a subagent, never the main session', () => {
|
|
194
|
+
assert.equal(MODEL_TIERS.includes('main'), false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// --- THE SCOPE WALL ---------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
await test('scope exceeding the allowed modules is refused, naming the offenders', () => {
|
|
200
|
+
const r = validateAgentDefinition(
|
|
201
|
+
{ ...OK_DEF, scope_modules: ['agents', 'government', 'lifecycle'] },
|
|
202
|
+
{ authorRank: 'archon', allowedModules: ['agents'] },
|
|
203
|
+
);
|
|
204
|
+
assert.equal(r.ok, false);
|
|
205
|
+
assert.ok(hasCode(r, 'scope_out_of_bounds'));
|
|
206
|
+
const msg = r.errors.find((e) => e.code === 'scope_out_of_bounds').message;
|
|
207
|
+
assert.ok(msg.includes('government') && msg.includes('lifecycle'), `must name the offenders: ${msg}`);
|
|
208
|
+
assert.ok(!msg.includes("'agents'"), 'must not accuse the module that WAS allowed');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await test('scope FAILS CLOSED when the caller supplies no allowed-module set', () => {
|
|
212
|
+
// The dangerous default. An empty allowlist must mean "nothing is in scope",
|
|
213
|
+
// never "everything is" — the same rule goal-scope-check's empty_scope holds.
|
|
214
|
+
for (const opts of [{ authorRank: 'archon' }, { authorRank: 'archon', allowedModules: [] }, undefined]) {
|
|
215
|
+
const r = validateAgentDefinition(OK_DEF, opts);
|
|
216
|
+
assert.equal(r.ok, false, `${JSON.stringify(opts)} must fail closed`);
|
|
217
|
+
assert.ok(hasCode(r, 'scope_out_of_bounds'), `${JSON.stringify(opts)} → ${JSON.stringify(codes(r))}`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await test('an empty scope_modules is allowed — it grants nothing', () => {
|
|
222
|
+
const r = validateAgentDefinition({ ...OK_DEF, scope_modules: [] }, { authorRank: 'archon', allowedModules: [] });
|
|
223
|
+
assert.equal(r.ok, true, JSON.stringify(r.errors));
|
|
224
|
+
assert.deepEqual(r.value.scope_modules, []);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await test('a non-array / non-string scope is refused rather than coerced', () => {
|
|
228
|
+
for (const bad of ['agents', 42, {}, ['agents', 7], ['agents', '']]) {
|
|
229
|
+
const r = validateAgentDefinition({ ...OK_DEF, scope_modules: bad }, OPTS);
|
|
230
|
+
assert.equal(r.ok, false, `${JSON.stringify(bad)} must be refused`);
|
|
231
|
+
assert.ok(hasCode(r, 'scope_modules_invalid'), `${JSON.stringify(bad)} → ${JSON.stringify(codes(r))}`);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await test('a scope_path that escapes the tree is refused', () => {
|
|
236
|
+
for (const bad of ['../../etc/passwd', '/etc/passwd', 'modules/../../..', 'C:\\Windows']) {
|
|
237
|
+
const r = validateAgentDefinition({ ...OK_DEF, scope_paths: [bad] }, OPTS);
|
|
238
|
+
assert.equal(r.ok, false, `"${bad}" must be refused`);
|
|
239
|
+
assert.ok(hasCode(r, 'scope_paths_invalid'), `"${bad}" → ${JSON.stringify(codes(r))}`);
|
|
240
|
+
}
|
|
241
|
+
assert.equal(validateAgentDefinition({ ...OK_DEF, scope_paths: ['modules/agents/'] }, OPTS).ok, true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// --- THE RANK WALL (ADR 0016 — authority is the author's LIVE rank) ---------
|
|
245
|
+
|
|
246
|
+
await test('RANK_ORDER is the ladder low→high, and the scope floor is Metic', () => {
|
|
247
|
+
assert.deepEqual(RANK_ORDER, ['xenos', 'thetes', 'metic', 'archon']);
|
|
248
|
+
assert.equal(SCOPE_RANK_FLOOR, 'metic');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await test('a sub-Metic author may not scope an agent onto a PROTECTED module', () => {
|
|
252
|
+
const opts = { allowedModules: ['agents', 'government'], protectedModules: ['government'] };
|
|
253
|
+
for (const rank of ['xenos', 'thetes']) {
|
|
254
|
+
const r = validateAgentDefinition({ ...OK_DEF, scope_modules: ['government'] }, { ...opts, authorRank: rank });
|
|
255
|
+
assert.equal(r.ok, false, `${rank} must not reach a protected module`);
|
|
256
|
+
assert.ok(hasCode(r, 'scope_protected_rank'), `${rank} → ${JSON.stringify(codes(r))}`);
|
|
257
|
+
}
|
|
258
|
+
// Metic+ clear the floor.
|
|
259
|
+
for (const rank of ['metic', 'archon']) {
|
|
260
|
+
const r = validateAgentDefinition({ ...OK_DEF, scope_modules: ['government'] }, { ...opts, authorRank: rank });
|
|
261
|
+
assert.equal(r.ok, true, `${rank} should clear the floor: ${JSON.stringify(r.errors)}`);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await test('an UNKNOWN author rank fails CLOSED against a protected module', () => {
|
|
266
|
+
// A file cannot grant authority (ADR 0016). If the caller could not name a live
|
|
267
|
+
// rank, the definition has no authority to reach a protected module.
|
|
268
|
+
const opts = { allowedModules: ['government'], protectedModules: ['government'] };
|
|
269
|
+
for (const rank of [undefined, null, '', 'admin', 'ARCHON']) {
|
|
270
|
+
const r = validateAgentDefinition({ ...OK_DEF, scope_modules: ['government'] }, { ...opts, authorRank: rank });
|
|
271
|
+
assert.equal(r.ok, false, `${JSON.stringify(rank)} must fail closed`);
|
|
272
|
+
assert.ok(hasCode(r, 'scope_protected_rank'), `${JSON.stringify(rank)} → ${JSON.stringify(codes(r))}`);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await test('an unknown rank is NOT fatal when no protected module is in scope', () => {
|
|
277
|
+
// The rank wall guards protected scope only; an ordinary agent does not need a
|
|
278
|
+
// resolved rank to be structurally valid.
|
|
279
|
+
const r = validateAgentDefinition(OK_DEF, { authorRank: 'nonsense', allowedModules: ['agents'] });
|
|
280
|
+
assert.equal(r.ok, true, JSON.stringify(r.errors));
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// --- NEVER CRASH THE BOOT ---------------------------------------------------
|
|
284
|
+
|
|
285
|
+
await test('a non-object definition is refused, not thrown on', () => {
|
|
286
|
+
for (const bad of [null, undefined, 'historian', 42, [], true]) {
|
|
287
|
+
let r;
|
|
288
|
+
assert.doesNotThrow(() => { r = validateAgentDefinition(bad, OPTS); }, `${JSON.stringify(bad)} threw`);
|
|
289
|
+
assert.equal(r.ok, false);
|
|
290
|
+
assert.ok(hasCode(r, 'not_an_object'), `${JSON.stringify(bad)} → ${JSON.stringify(codes(r))}`);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await test('a CIRCULAR trigger_spec is refused, not thrown on', () => {
|
|
295
|
+
// The size check is over the serialized spec, and JSON.stringify THROWS on a
|
|
296
|
+
// cycle. A definition file cannot express one, but a db-sourced object can —
|
|
297
|
+
// and this validator is the thing standing between that and the boot.
|
|
298
|
+
const spec = { event: 'task.shipped' };
|
|
299
|
+
spec.self = spec;
|
|
300
|
+
let r;
|
|
301
|
+
assert.doesNotThrow(() => { r = validateAgentDefinition({ ...OK_DEF, trigger_spec: spec }, OPTS); });
|
|
302
|
+
assert.equal(r.ok, false);
|
|
303
|
+
assert.ok(hasCode(r, 'trigger_spec_unserializable'), JSON.stringify(codes(r)));
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await test('an oversized trigger_spec is refused at the DB CHECK bound', () => {
|
|
307
|
+
const r = validateAgentDefinition(
|
|
308
|
+
{ ...OK_DEF, trigger_spec: { event: 'task.shipped', pad: 'x'.repeat(9000) } }, OPTS,
|
|
309
|
+
);
|
|
310
|
+
assert.equal(r.ok, false);
|
|
311
|
+
assert.ok(hasCode(r, 'trigger_spec_too_long'));
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
await test('a hostile getter cannot escape the validator', () => {
|
|
315
|
+
// trigger_spec is PARAMETERS, never payload — and a throwing accessor must
|
|
316
|
+
// become a refusal, not an exception into the loader.
|
|
317
|
+
const hostile = { name: 'historian', persona: 'x', trigger_type: 'event' };
|
|
318
|
+
Object.defineProperty(hostile, 'trigger_spec', { get() { throw new Error('boom'); }, enumerable: true });
|
|
319
|
+
let r;
|
|
320
|
+
assert.doesNotThrow(() => { r = validateAgentDefinition(hostile, OPTS); });
|
|
321
|
+
assert.equal(r.ok, false);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await test('EVERY error carries a code, a field and a human message', () => {
|
|
325
|
+
const r = validateAgentDefinition(
|
|
326
|
+
{ name: 'BAD NAME', trigger_type: 'cron', trigger_spec: 'nope', scope_modules: ['government'] },
|
|
327
|
+
{ authorRank: 'xenos', allowedModules: ['agents'] },
|
|
328
|
+
);
|
|
329
|
+
assert.equal(r.ok, false);
|
|
330
|
+
assert.ok(r.errors.length >= 4, `expected the whole list, got ${JSON.stringify(codes(r))}`);
|
|
331
|
+
for (const e of r.errors) {
|
|
332
|
+
assert.equal(typeof e.code, 'string', JSON.stringify(e));
|
|
333
|
+
assert.equal(typeof e.field, 'string', JSON.stringify(e));
|
|
334
|
+
assert.ok(e.message && e.message.length > 10, `message too thin: ${JSON.stringify(e)}`);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await test('validation reports ALL faults at once, not just the first', () => {
|
|
339
|
+
// An author fixing a definition file should not have to re-run four times.
|
|
340
|
+
const r = validateAgentDefinition({ name: 'X', persona: '', trigger_type: 'cron' }, OPTS);
|
|
341
|
+
for (const code of ['name_invalid', 'persona_missing', 'trigger_type_unknown']) {
|
|
342
|
+
assert.ok(hasCode(r, code), `${code} missing from ${JSON.stringify(codes(r))}`);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
await test('the validator never MUTATES the definition it was handed', () => {
|
|
347
|
+
const def = { ...OK_DEF, trigger_spec: { event: 'task.shipped' } };
|
|
348
|
+
const before = JSON.stringify(def);
|
|
349
|
+
validateAgentDefinition(def, OPTS);
|
|
350
|
+
assert.equal(JSON.stringify(def), before);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
console.log(`\nagents_validate: ${passed} passed, ${failed} failed`);
|
|
354
|
+
if (failed > 0) process.exit(1);
|
package/tests/hall_audit.mjs
CHANGED
|
@@ -384,3 +384,71 @@ test('no page declares a private copy of a pack color var', () => {
|
|
|
384
384
|
}
|
|
385
385
|
assert.deepEqual(offenders, [], 'palette comes from style.css + the instance pack (themeMarkup), never from a page');
|
|
386
386
|
});
|
|
387
|
+
|
|
388
|
+
// ---- 9. verification state wears the token tier (task 1003495) ------------
|
|
389
|
+
|
|
390
|
+
// The walk (task 1003309) found Standing's grade column painting its verdicts
|
|
391
|
+
// with inline literals — including a `var(--olive, #5b7a4d)` whose token the
|
|
392
|
+
// world never defined, so every hall that ever rendered it showed the fallback.
|
|
393
|
+
// A colour in a DECLARATION VALUE is the thing worth catching; a bare `#807`
|
|
394
|
+
// in a comment is a task reference, which is why this matches a colour property
|
|
395
|
+
// and not a hash. style.css is the token tier itself and is where literals
|
|
396
|
+
// belong; the two exceptions below carry their reason.
|
|
397
|
+
const LITERAL_EXEMPT = {
|
|
398
|
+
'style.css': 'the token tier — this is the ONE file whose job is to hold the literals',
|
|
399
|
+
'settings.css': 'the Discord button wears Discord\'s brand colour (#5865F2); a third-party mark is not a theme colour',
|
|
400
|
+
};
|
|
401
|
+
const COLOUR_PROPS = 'color|background|background-color|border|border-color|border-top-color|border-bottom-color|fill|stroke|outline|outline-color|box-shadow|text-shadow';
|
|
402
|
+
const colourLiteralRe = () =>
|
|
403
|
+
new RegExp(`(?:^|[;{"'\\s(])(${COLOUR_PROPS})\\s*:\\s*[^;{}"'\\n]*?(#[0-9a-fA-F]{3,8}\\b|\\b(?:rgb|rgba|hsl|hsla)\\()`, 'g');
|
|
404
|
+
|
|
405
|
+
function literalOffenders(files) {
|
|
406
|
+
const out = [];
|
|
407
|
+
for (const f of files) {
|
|
408
|
+
if (LITERAL_EXEMPT[f]) continue;
|
|
409
|
+
for (const m of read(f).matchAll(colourLiteralRe())) out.push(`${f}: ${m[1]}: … ${m[2]}`);
|
|
410
|
+
}
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const hallSources = () =>
|
|
415
|
+
fs.readdirSync(HALL).filter((f) => /\.(css|js|html)$/.test(f)).sort();
|
|
416
|
+
|
|
417
|
+
test('no hall sheet or script paints from a colour literal — style.css holds them', () => {
|
|
418
|
+
assert.deepEqual(literalOffenders(hallSources()), [],
|
|
419
|
+
'a colour in a declaration value belongs to the token tier (style.css), not to a page');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('the colour-literal guard CATCHES a reintroduction, and tolerates a task ref', () => {
|
|
423
|
+
const re = colourLiteralRe();
|
|
424
|
+
assert.ok('a { color: #a04040; }'.match(re), 'a bare hex in a colour value is caught');
|
|
425
|
+
assert.ok('x = \'<span style="color:var(--olive,#5b7a4d)">\''.match(colourLiteralRe()),
|
|
426
|
+
'a hex FALLBACK behind a var() is caught — that is how --olive hid');
|
|
427
|
+
assert.equal('// #807: the dev-box panel'.match(colourLiteralRe()), null,
|
|
428
|
+
'a task reference in a comment is not a colour');
|
|
429
|
+
assert.equal('a { color: var(--ok); }'.match(colourLiteralRe()), null,
|
|
430
|
+
'a token is exactly what this rule wants');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("Standing's grade column renders its three verdicts through the vocabulary", () => {
|
|
434
|
+
const js = read('builders.js');
|
|
435
|
+
const start = js.indexOf('function renderRecentGrades');
|
|
436
|
+
assert.ok(start > 0, 'renderRecentGrades present');
|
|
437
|
+
const body = js.slice(start, js.indexOf('\n }', start));
|
|
438
|
+
for (const cls of ['ledger__verdict--ok', 'ledger__verdict--danger', 'ledger__verdict--none']) {
|
|
439
|
+
assert.ok(body.includes(cls), `the grade column uses .${cls}`);
|
|
440
|
+
}
|
|
441
|
+
assert.equal(/style="color:/.test(body), false, 'no inline colour survives in the grade column');
|
|
442
|
+
assert.equal(/var\(\s*--olive/.test(js), false, 'nothing PAINTS from --olive — the world never defined it (naming it in a comment is the point)');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test('the verdict classes and the scrim exist in the token tier, in both modes', () => {
|
|
446
|
+
const css = read('style.css');
|
|
447
|
+
// A verdict wears --ok / --danger / --ink-soft, which style.css redefines per
|
|
448
|
+
// mode — so pinning the RULE pins both modes at once.
|
|
449
|
+
assert.match(css, /\.ledger__verdict--ok\s*\{[^}]*var\(--ok\)/, 'passed → --ok');
|
|
450
|
+
assert.match(css, /\.ledger__verdict--danger\s*\{[^}]*var\(--danger\)/, 'fell short → --danger');
|
|
451
|
+
assert.match(css, /\.ledger__verdict--none\s*\{[^}]*var\(--ink-soft\)/, 'not graded → --ink-soft');
|
|
452
|
+
assert.match(css, /--scrim:/, 'the modal scrim is a token, not a sheet-local literal');
|
|
453
|
+
assert.match(read('copy-desk.css'), /::backdrop\s*\{[^}]*var\(--scrim\)/, 'the copy-desk backdrop takes it');
|
|
454
|
+
});
|
|
@@ -46,11 +46,14 @@ test('the page sheets spend only the fifteen, in px, and never opacity', () => {
|
|
|
46
46
|
assert.ok(!/opacity\s*:/.test(css), `${p}.css carries state as colour, never opacity`);
|
|
47
47
|
assert.ok(!/border-left/.test(css), `${p}.css paints no coloured band`);
|
|
48
48
|
}
|
|
49
|
-
// The
|
|
49
|
+
// The copy desk's last literal was the native dialog's scrim. A scrim still
|
|
50
|
+
// cannot be mixed from a ground token — it darkens in BOTH modes — but the
|
|
51
|
+
// value now has a home in style.css as --scrim, so this sheet keeps none
|
|
52
|
+
// (task 1003495).
|
|
50
53
|
const cd = read('copy-desk.css');
|
|
51
54
|
const literals = cd.match(/rgba?\([^)]*\)/g) || [];
|
|
52
|
-
assert.deepEqual(literals, [
|
|
53
|
-
assert.match(cd, /\.cd-dialog::backdrop \{ background:
|
|
55
|
+
assert.deepEqual(literals, [], 'not even the scrim is a literal here any more');
|
|
56
|
+
assert.match(cd, /\.cd-dialog::backdrop \{ background: var\(--scrim\); \}/, 'the backdrop spends the token');
|
|
54
57
|
});
|
|
55
58
|
|
|
56
59
|
test('every list is the family\'s row, and the card-in-a-card is gone', () => {
|
|
@@ -116,6 +116,17 @@ const STATIC_LADDER_FILES = [
|
|
|
116
116
|
// which holds it against catalog.RANK_ORDER and holds the whole browser parser
|
|
117
117
|
// equal to the server one.
|
|
118
118
|
'modules/hall-ui/public/government.js',
|
|
119
|
+
// task 1002487: the pure agent-definition validator applies the ADR 0043
|
|
120
|
+
// sub-Metic floor to an agent's declared scope, so it needs the ladder to
|
|
121
|
+
// compare against. It is one of the "dependency-free" cases this array exists
|
|
122
|
+
// for, and for a sharper reason than the browser statics above: its whole
|
|
123
|
+
// contract is to be checkable with NO database, and the doorway it would
|
|
124
|
+
// otherwise import (src/module-api.js) eagerly requires ./bongos/pool at load.
|
|
125
|
+
// Deriving the ladder from api.LIVE_RANK_LADDER would therefore drag a
|
|
126
|
+
// Postgres pool into a validator that must run in a bare unit test — so the
|
|
127
|
+
// copy is pinned here instead, which is what makes it provable rather than
|
|
128
|
+
// merely allowed.
|
|
129
|
+
'modules/agents/lib/validate.js',
|
|
119
130
|
];
|
|
120
131
|
test('static ladder copies carry the exact canonical ladder', () => {
|
|
121
132
|
for (const rel of STATIC_LADDER_FILES) {
|