@bongos/core 1.19.794 → 1.19.795
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 +32 -17
- package/docs/module-api-changelog.md +2 -0
- package/migrations/core_244_skill_friction_kind.sql +74 -0
- package/modules/ideas/inbox.js +27 -59
- package/modules/ideas/skill-friction.js +97 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/claude-materialize.js +70 -4
- package/scripts/gds/ship-flow.js +7 -1
- package/scripts/gds/ship-honesty.js +55 -1
- package/src/module-api.js +1 -1
- package/tests/claude_materialize.mjs +71 -0
- package/tests/ship_cannot_lie.mjs +75 -1
- package/tests/skill_friction_kind.mjs +163 -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.795",
|
|
6
|
+
"core_contract": "1.19.795",
|
|
7
|
+
"source_commit": "59f35c05e7edb8428f4c532e5032e9333bd7a0b9",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-17T00:
|
|
9
|
+
"built_at": "2026-09-17T00:54:38.883Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 486,
|
|
13
13
|
"agent_docs_stubbed": 25,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2182,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2693,
|
|
20
|
+
"tree_sha256": "bb912c280be9ea67a9825f8ee7e6bbc68b2be165c826b3fb40185a0badd7cd9d",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2527,7 +2527,7 @@
|
|
|
2527
2527
|
{
|
|
2528
2528
|
"path": "docs/module-api-changelog.md",
|
|
2529
2529
|
"mode": "0000644",
|
|
2530
|
-
"sha256": "
|
|
2530
|
+
"sha256": "d9df5939232118367f1db9c0127c514a19f625aa46a8375e65dda21705e84671"
|
|
2531
2531
|
},
|
|
2532
2532
|
{
|
|
2533
2533
|
"path": "docs/modules-contract.md",
|
|
@@ -3614,6 +3614,11 @@
|
|
|
3614
3614
|
"mode": "0000644",
|
|
3615
3615
|
"sha256": "8e3e2db2ab3b7775bddf12f3a7ec3ed74477334b6388487ecd919cd61a62b0ba"
|
|
3616
3616
|
},
|
|
3617
|
+
{
|
|
3618
|
+
"path": "migrations/core_244_skill_friction_kind.sql",
|
|
3619
|
+
"mode": "0000644",
|
|
3620
|
+
"sha256": "e298a5b2e9ffade95fae0e68910a25396fab560e6e54dd745cfc122c0dfdf403"
|
|
3621
|
+
},
|
|
3617
3622
|
{
|
|
3618
3623
|
"path": "modules/agents/CLAUDE.md",
|
|
3619
3624
|
"mode": "0000644",
|
|
@@ -5377,7 +5382,7 @@
|
|
|
5377
5382
|
{
|
|
5378
5383
|
"path": "modules/ideas/inbox.js",
|
|
5379
5384
|
"mode": "0000644",
|
|
5380
|
-
"sha256": "
|
|
5385
|
+
"sha256": "6c619200312b32cbc268c44b41c4f3f574f7917a31187b69cfeba105c804ddee"
|
|
5381
5386
|
},
|
|
5382
5387
|
{
|
|
5383
5388
|
"path": "modules/ideas/migrations/ideas_001_grades.sql",
|
|
@@ -5429,6 +5434,11 @@
|
|
|
5429
5434
|
"mode": "0000644",
|
|
5430
5435
|
"sha256": "da653f475d6c91db8e84bff46c797bb68b61e48d561eacf7bfd228a119ac7266"
|
|
5431
5436
|
},
|
|
5437
|
+
{
|
|
5438
|
+
"path": "modules/ideas/skill-friction.js",
|
|
5439
|
+
"mode": "0000644",
|
|
5440
|
+
"sha256": "eb6a5cecb3ce54aeb0c85ffb752eb12a2400d0d4f5c7d48e4e05ff3cf7baf712"
|
|
5441
|
+
},
|
|
5432
5442
|
{
|
|
5433
5443
|
"path": "modules/ideas/templates.js",
|
|
5434
5444
|
"mode": "0000644",
|
|
@@ -7692,12 +7702,12 @@
|
|
|
7692
7702
|
{
|
|
7693
7703
|
"path": "package-lock.json",
|
|
7694
7704
|
"mode": "0000644",
|
|
7695
|
-
"sha256": "
|
|
7705
|
+
"sha256": "abe39cacffdf6a8359f19b7e7a5a06a7055f99637edc556db2124968bdae5eb8"
|
|
7696
7706
|
},
|
|
7697
7707
|
{
|
|
7698
7708
|
"path": "package.json",
|
|
7699
7709
|
"mode": "0000644",
|
|
7700
|
-
"sha256": "
|
|
7710
|
+
"sha256": "6f9922b6e8c2eb446a10daa28bd6a34da2097b36098a70db45f16010c8529217"
|
|
7701
7711
|
},
|
|
7702
7712
|
{
|
|
7703
7713
|
"path": "public-docs/index.html",
|
|
@@ -7987,7 +7997,7 @@
|
|
|
7987
7997
|
{
|
|
7988
7998
|
"path": "scripts/gds/claude-materialize.js",
|
|
7989
7999
|
"mode": "0000644",
|
|
7990
|
-
"sha256": "
|
|
8000
|
+
"sha256": "8cf425aed2764fc043c8a8757f178f7514a907567d559e7f0b11e091f33b66e5"
|
|
7991
8001
|
},
|
|
7992
8002
|
{
|
|
7993
8003
|
"path": "scripts/gds/claude-md-template.js",
|
|
@@ -8932,7 +8942,7 @@
|
|
|
8932
8942
|
{
|
|
8933
8943
|
"path": "scripts/gds/ship-flow.js",
|
|
8934
8944
|
"mode": "0000644",
|
|
8935
|
-
"sha256": "
|
|
8945
|
+
"sha256": "fe64da47ac66f589d0245e02b321dbd708e695dd3875e75e8b4ac36e6aa31636"
|
|
8936
8946
|
},
|
|
8937
8947
|
{
|
|
8938
8948
|
"path": "scripts/gds/ship-git.js",
|
|
@@ -8952,7 +8962,7 @@
|
|
|
8952
8962
|
{
|
|
8953
8963
|
"path": "scripts/gds/ship-honesty.js",
|
|
8954
8964
|
"mode": "0000644",
|
|
8955
|
-
"sha256": "
|
|
8965
|
+
"sha256": "3ab4816fdd43d589cda73698a514f8e23103be166c1650fd24beeee9f529bb5b"
|
|
8956
8966
|
},
|
|
8957
8967
|
{
|
|
8958
8968
|
"path": "scripts/gds/ship-io.js",
|
|
@@ -9577,7 +9587,7 @@
|
|
|
9577
9587
|
{
|
|
9578
9588
|
"path": "src/module-api.js",
|
|
9579
9589
|
"mode": "0000644",
|
|
9580
|
-
"sha256": "
|
|
9590
|
+
"sha256": "e21607486d5b15d09ea4e0f1ad1a3a626c9dbaf3713aa601dd113e5e3d7bd13e"
|
|
9581
9591
|
},
|
|
9582
9592
|
{
|
|
9583
9593
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10247,7 +10257,7 @@
|
|
|
10247
10257
|
{
|
|
10248
10258
|
"path": "tests/claude_materialize.mjs",
|
|
10249
10259
|
"mode": "0000644",
|
|
10250
|
-
"sha256": "
|
|
10260
|
+
"sha256": "7acfe432856010881b234b258d45fcf65f976e0e711d01b823d90990a0c7179c"
|
|
10251
10261
|
},
|
|
10252
10262
|
{
|
|
10253
10263
|
"path": "tests/clawback_confirmed_backlog.mjs",
|
|
@@ -12892,7 +12902,7 @@
|
|
|
12892
12902
|
{
|
|
12893
12903
|
"path": "tests/ship_cannot_lie.mjs",
|
|
12894
12904
|
"mode": "0000644",
|
|
12895
|
-
"sha256": "
|
|
12905
|
+
"sha256": "55e94ed59bc1bd19e4a3dd0e9a9a415dde659c5d71af0dd827d1b288e66de3c7"
|
|
12896
12906
|
},
|
|
12897
12907
|
{
|
|
12898
12908
|
"path": "tests/ship_card.mjs",
|
|
@@ -12984,6 +12994,11 @@
|
|
|
12984
12994
|
"mode": "0000644",
|
|
12985
12995
|
"sha256": "0eeccae9320f419667cf9ede3c13671a5b03571f7f2f17067bff1857234ddf91"
|
|
12986
12996
|
},
|
|
12997
|
+
{
|
|
12998
|
+
"path": "tests/skill_friction_kind.mjs",
|
|
12999
|
+
"mode": "0000644",
|
|
13000
|
+
"sha256": "baaf5935cc748e7eb9596d1c96c3c20aa2ee3917377d32b9104408b7b07165a4"
|
|
13001
|
+
},
|
|
12987
13002
|
{
|
|
12988
13003
|
"path": "tests/skill_grade_audit.mjs",
|
|
12989
13004
|
"mode": "0000644",
|
|
@@ -2062,5 +2062,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2062
2062
|
landed since 1.19.792 with no explicit bump. run 35167484049. (task 1002620)
|
|
2063
2063
|
1.19.794 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2064
2064
|
landed since 1.19.793 with no explicit bump. run 35167814500. (task 1002620)
|
|
2065
|
+
1.19.795 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2066
|
+
landed since 1.19.794 with no explicit bump. run 35168408728. (task 1002620)
|
|
2065
2067
|
---------------------------------------------------------------------------
|
|
2066
2068
|
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
-- core_244_skill_friction_kind.sql — 'skill-friction' becomes a real kind
|
|
2
|
+
-- (task 1003105, goal 1000095, promoted from idea 1000759).
|
|
3
|
+
--
|
|
4
|
+
-- WHY. Two skills instruct the agent to file a retrospective finding as
|
|
5
|
+
-- `kind='skill-friction'` — `.claude/skills/planning-session/SKILL.md` ("After
|
|
6
|
+
-- the session — improvements pass") and `.claude/skills/priority-session/
|
|
7
|
+
-- SKILL.md`. Neither works: the CHECK constraint on `idea_inbox.kind` (migration
|
|
8
|
+
-- 006, extended by 042) admits only the fixed task-kind enum, so `POST /inbox`
|
|
9
|
+
-- fails with BAD_KIND. Hit live on 2026-08-16 — all three retrospective ideas
|
|
10
|
+
-- from the Board Room planning session failed on the first attempt and had to be
|
|
11
|
+
-- re-homed under unrelated kinds. The cost is not the error message: this is the
|
|
12
|
+
-- step that makes the planning skill self-improving, and because it fails closed
|
|
13
|
+
-- the findings are either dropped or filed somewhere nobody looks for them.
|
|
14
|
+
--
|
|
15
|
+
-- WHY NOW, AND NOT AS A TITLE PREFIX. The nightly `skill-usage-review` routine
|
|
16
|
+
-- already files these rows, and it works — by tagging a `[skill-friction]`
|
|
17
|
+
-- title prefix over a valid enum `kind`. That workaround names its own exit
|
|
18
|
+
-- condition, in `modules/ideas/inbox.js` and in the routine's SKILL.md: "when
|
|
19
|
+
-- the rank system lands a real builders.rank column + idea_inbox.kind
|
|
20
|
+
-- migration, this prefix convention can be promoted to a first-class kind."
|
|
21
|
+
-- The rank system HAS landed — `builders.rank` is live and enforced server-side
|
|
22
|
+
-- (ADR 0016), and `viewerRank` is threaded into all four inbox read paths from
|
|
23
|
+
-- `req.builder.rank`. So the condition the workaround was waiting on is met,
|
|
24
|
+
-- and this is the migration it asked for.
|
|
25
|
+
--
|
|
26
|
+
-- WHAT THIS DOES NOT DO. It does not backfill the existing `[skill-friction]`-
|
|
27
|
+
-- prefixed rows, and it does not change what the nightly routine writes. Both
|
|
28
|
+
-- are deliberate: the deployed core pin runs behind `main` by design (gate 3 of
|
|
29
|
+
-- docs/recipes/core-release-pipeline.md), so between this landing and the pin
|
|
30
|
+
-- moving there is a window in which the DB accepts the new kind and the running
|
|
31
|
+
-- instance's VALID_KINDS does not offer it. A filer flipped to the new kind in
|
|
32
|
+
-- that window would start failing where it currently succeeds — strictly worse
|
|
33
|
+
-- than the bug being fixed. So the marker stays DUAL: `inbox.js` recognises a
|
|
34
|
+
-- row as skill-friction by its kind OR its legacy title prefix, permanently and
|
|
35
|
+
-- by design, and flipping the routine is a follow-up for after the pin moves.
|
|
36
|
+
--
|
|
37
|
+
-- WHY CORE AND NOT modules/ideas/migrations/. `idea_inbox` is a CORE table: it
|
|
38
|
+
-- was created by migration 006 and every later change to it — 042 (the last
|
|
39
|
+
-- widening of this same constraint), core_195, core_222, core_228 — is a core
|
|
40
|
+
-- migration. The `ideas` module owns the `ideas_*` tables, not this one, and a
|
|
41
|
+
-- module migration may only touch its own namespace and must be additive.
|
|
42
|
+
-- Widening a CHECK requires dropping it, so this could not have been a module
|
|
43
|
+
-- migration even if the table had been the module's. `fitness.js` enforces both
|
|
44
|
+
-- rules and caught the first attempt at this file in the wrong place.
|
|
45
|
+
--
|
|
46
|
+
-- The constraint is dropped by LOOKUP rather than by name, exactly as 042 does
|
|
47
|
+
-- it, and for 042's reason: a database that somehow never ran 042 still carries
|
|
48
|
+
-- the 006-era constraint under its original name, and a `DROP CONSTRAINT IF
|
|
49
|
+
-- EXISTS idea_inbox_kind_check` there would silently no-op and leave the old,
|
|
50
|
+
-- narrower constraint in force — which rejects the very kind this adds.
|
|
51
|
+
--
|
|
52
|
+
-- REVERSIBILITY. Dropping back is the same DO block with 'skill-friction'
|
|
53
|
+
-- removed from the list, and is safe only while no row carries the kind.
|
|
54
|
+
|
|
55
|
+
DO $$
|
|
56
|
+
DECLARE cname text;
|
|
57
|
+
BEGIN
|
|
58
|
+
SELECT conname INTO cname
|
|
59
|
+
FROM pg_constraint
|
|
60
|
+
WHERE conrelid = 'idea_inbox'::regclass
|
|
61
|
+
AND contype = 'c'
|
|
62
|
+
AND pg_get_constraintdef(oid) ILIKE '%kind%IN%';
|
|
63
|
+
IF cname IS NOT NULL THEN
|
|
64
|
+
EXECUTE format('ALTER TABLE idea_inbox DROP CONSTRAINT %I', cname);
|
|
65
|
+
END IF;
|
|
66
|
+
END $$;
|
|
67
|
+
|
|
68
|
+
ALTER TABLE idea_inbox
|
|
69
|
+
ADD CONSTRAINT idea_inbox_kind_check
|
|
70
|
+
CHECK (kind IN (
|
|
71
|
+
'feature','bug','infra','refactor','spike',
|
|
72
|
+
'learning-capture','cleanup','decision','blocker-resolution',
|
|
73
|
+
'unclassified','criterion-proposal','skill-friction'
|
|
74
|
+
));
|
package/modules/ideas/inbox.js
CHANGED
|
@@ -42,6 +42,12 @@ const VALID_KINDS = [
|
|
|
42
42
|
// ratify endpoint (assert kind='criterion-proposal' before promotion).
|
|
43
43
|
// CHECK constraint updated in migration 042.
|
|
44
44
|
'criterion-proposal',
|
|
45
|
+
// task 1003105 — `/planning-session` and `/priority-session` both instruct the
|
|
46
|
+
// agent to file a retrospective finding as kind='skill-friction', and both
|
|
47
|
+
// failed BAD_KIND until now. The DB CHECK constraint is widened to match in
|
|
48
|
+
// migrations/core_244_skill_friction_kind.sql, which also
|
|
49
|
+
// explains why the legacy title-prefix marker below stays alongside it.
|
|
50
|
+
'skill-friction',
|
|
45
51
|
];
|
|
46
52
|
const VALID_STATUSES = ['open', 'promoted', 'discarded', 'merged'];
|
|
47
53
|
|
|
@@ -121,45 +127,13 @@ function ungradedProjection() {
|
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
// -------------------------------------------------------------------------
|
|
124
|
-
// Skill-friction
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// this task. So we tag friction rows the same way overnight-code-review tags
|
|
132
|
-
// its findings: a stable title prefix. The underlying `kind` stays a valid
|
|
133
|
-
// enum value (cleanup), and SKILL_FRICTION_TAG is the machine-checkable marker.
|
|
134
|
-
//
|
|
135
|
-
// When the GDS-V3 rank system (V3.R04) lands a real builders.rank column +
|
|
136
|
-
// idea_inbox.kind migration, this prefix convention can be promoted to a
|
|
137
|
-
// first-class kind value and this marker collapses to a kind check.
|
|
138
|
-
const SKILL_FRICTION_TAG = '[skill-friction]';
|
|
139
|
-
|
|
140
|
-
// -------------------------------------------------------------------------
|
|
141
|
-
// Rank visibility (task V3.R70 / #289)
|
|
142
|
-
//
|
|
143
|
-
// Three ranks are planned in GDS-V3 (V3.R04): Xenos (default for any
|
|
144
|
-
// GitHub-authed person), Metic (Archon-approved), Archon. Skill-friction
|
|
145
|
-
// ideas are a system-evolution signal meant for trusted builders — Xenos
|
|
146
|
-
// must NOT see them in their inbox view; Metic and Archon may.
|
|
147
|
-
//
|
|
148
|
-
// `builders.rank` does not exist in the schema yet (it lands with R04), and
|
|
149
|
-
// requireBuilder does not yet populate req.builder.rank — so today every
|
|
150
|
-
// caller passes viewerRank=null. The gate is written to be correct the moment
|
|
151
|
-
// rank is threaded through: it hides skill-friction rows ONLY when the viewer
|
|
152
|
-
// is positively known to be Xenos. Unknown/absent rank (today's reality, where
|
|
153
|
-
// no Xenos exist yet) is treated as Metic+ and sees everything — so wiring this
|
|
154
|
-
// in does not regress current all-builders behavior.
|
|
155
|
-
const XENOS_RANK = 'xenos';
|
|
156
|
-
|
|
157
|
-
function rankCanSeeSkillFriction(viewerRank) {
|
|
158
|
-
// Hide only when we KNOW the viewer is Xenos. null/undefined/anything else
|
|
159
|
-
// (Metic, Archon, or "rank system not deployed") => allowed.
|
|
160
|
-
if (viewerRank == null) return true;
|
|
161
|
-
return String(viewerRank).toLowerCase() !== XENOS_RANK;
|
|
162
|
-
}
|
|
130
|
+
// Skill-friction: the marker a row carries and the rank gate that hides it.
|
|
131
|
+
// Pure, and its own file (./skill-friction.js) for the reason written there —
|
|
132
|
+
// re-exported below so every existing caller keeps working.
|
|
133
|
+
const {
|
|
134
|
+
SKILL_FRICTION_TAG, SKILL_FRICTION_KIND, XENOS_RANK,
|
|
135
|
+
rankCanSeeSkillFriction, skillFrictionHiddenClause,
|
|
136
|
+
} = require('./skill-friction');
|
|
163
137
|
|
|
164
138
|
// -------------------------------------------------------------------------
|
|
165
139
|
// Fallback classifier — keyword heuristics over title+body.
|
|
@@ -511,9 +485,8 @@ async function routeIdeaToGoal({
|
|
|
511
485
|
// see skill-friction rows; Metic+ (or unknown rank, pre-R04) see everything.
|
|
512
486
|
// We filter in SQL when the viewer is positively Xenos so the LIMIT still
|
|
513
487
|
// returns up to `limit` *visible* rows rather than `limit` rows that then get
|
|
514
|
-
// thinned out in JS. The
|
|
515
|
-
//
|
|
516
|
-
// without a migration.
|
|
488
|
+
// thinned out in JS. The marker is a kind OR a legacy title prefix — see
|
|
489
|
+
// skillFrictionHiddenClause, which is the only place that knows that.
|
|
517
490
|
async function listOpenIdeas({ kind = null, limit = 50, viewerRank = null } = {}) {
|
|
518
491
|
const params = [];
|
|
519
492
|
const where = [`i.status = 'open'`];
|
|
@@ -521,10 +494,8 @@ async function listOpenIdeas({ kind = null, limit = 50, viewerRank = null } = {}
|
|
|
521
494
|
params.push(kind);
|
|
522
495
|
where.push(`i.kind = $${params.length}`);
|
|
523
496
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
where.push(`i.title NOT LIKE $${params.length}`);
|
|
527
|
-
}
|
|
497
|
+
const hideFriction = skillFrictionHiddenClause(viewerRank, params, 'i');
|
|
498
|
+
if (hideFriction) where.push(hideFriction);
|
|
528
499
|
params.push(limit);
|
|
529
500
|
const { rows } = await pool.query(
|
|
530
501
|
`SELECT ${IDEA_COLS_Q}, ${GRADE_COLS_Q}
|
|
@@ -570,10 +541,8 @@ async function getIdea(id) {
|
|
|
570
541
|
async function countOpenIdeas({ viewerRank = null } = {}) {
|
|
571
542
|
const params = [];
|
|
572
543
|
const where = [`status = 'open'`];
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
where.push(`title NOT LIKE $${params.length}`);
|
|
576
|
-
}
|
|
544
|
+
const hideFriction = skillFrictionHiddenClause(viewerRank, params);
|
|
545
|
+
if (hideFriction) where.push(hideFriction);
|
|
577
546
|
const { rows } = await pool.query(
|
|
578
547
|
`SELECT count(*)::int AS n FROM idea_inbox WHERE ${where.join(' AND ')}`,
|
|
579
548
|
params
|
|
@@ -619,10 +588,8 @@ const IDEA_TRAIL_MAX_LIMIT = 100;
|
|
|
619
588
|
function ideaTrailWhere(builderId, viewerRank, params) {
|
|
620
589
|
params.push(builderId);
|
|
621
590
|
const where = [`i.captured_by = $${params.length}`];
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
where.push(`i.title NOT LIKE $${params.length}`);
|
|
625
|
-
}
|
|
591
|
+
const hideFriction = skillFrictionHiddenClause(viewerRank, params, 'i');
|
|
592
|
+
if (hideFriction) where.push(hideFriction);
|
|
626
593
|
return where.join(' AND ');
|
|
627
594
|
}
|
|
628
595
|
|
|
@@ -1029,10 +996,8 @@ async function countRoutedAwaitingNod(deps = {}) {
|
|
|
1029
996
|
// agreeing by construction beats them agreeing by accident.
|
|
1030
997
|
function sparkQueueWhere(viewerRank, params) {
|
|
1031
998
|
const where = [`g.grade = 'spark'`, `i.status = 'open'`];
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
where.push(`i.title NOT LIKE $${params.length}`);
|
|
1035
|
-
}
|
|
999
|
+
const hideFriction = skillFrictionHiddenClause(viewerRank, params, 'i');
|
|
1000
|
+
if (hideFriction) where.push(hideFriction);
|
|
1036
1001
|
return where;
|
|
1037
1002
|
}
|
|
1038
1003
|
|
|
@@ -1455,10 +1420,13 @@ module.exports = {
|
|
|
1455
1420
|
DEFAULT_TEMPLATE_KEY,
|
|
1456
1421
|
TEMPLATE_KEY_MAX,
|
|
1457
1422
|
FIELD_VALUES_MAX,
|
|
1458
|
-
// skill-friction
|
|
1423
|
+
// skill-friction marker + rank visibility gate (V3.R70 / #289; the kind
|
|
1424
|
+
// itself landed in task 1003105 — both markers are live, see above)
|
|
1459
1425
|
SKILL_FRICTION_TAG,
|
|
1426
|
+
SKILL_FRICTION_KIND,
|
|
1460
1427
|
XENOS_RANK,
|
|
1461
1428
|
rankCanSeeSkillFriction,
|
|
1429
|
+
skillFrictionHiddenClause,
|
|
1462
1430
|
// capture
|
|
1463
1431
|
captureIdea,
|
|
1464
1432
|
// read
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// modules/ideas/skill-friction.js — the skill-friction marker and the rank
|
|
2
|
+
// gate that hides those rows, extracted from inbox.js (task 1003105).
|
|
3
|
+
//
|
|
4
|
+
// WHY ITS OWN FILE. Two reasons, and the second is the real one.
|
|
5
|
+
//
|
|
6
|
+
// `inbox.js` sits against the repo's per-file size budget, and adding the kind
|
|
7
|
+
// plus the reasoning for keeping the legacy marker pushed it over — the fitness
|
|
8
|
+
// ratchet's whole job. Trimming the explanation to fit would have deleted the
|
|
9
|
+
// part a later reader needs most: WHY there are two markers rather than one.
|
|
10
|
+
//
|
|
11
|
+
// And this was always a separable concern. It is pure — no pool, no I/O, no
|
|
12
|
+
// knowledge of a query beyond emitting a fragment — which is the same test
|
|
13
|
+
// `./routing.js` and `./homeless-home.js` already passed when they were split
|
|
14
|
+
// out of the same file. `inbox.js` re-exports everything here, so nothing that
|
|
15
|
+
// reached these through it has to change.
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
// Skill-friction tagging (task V3.R70 / #289)
|
|
20
|
+
//
|
|
21
|
+
// A skill-friction row carries TWO markers, both first-class, both permanent.
|
|
22
|
+
//
|
|
23
|
+
// The nightly `skill-usage-review` routine files one idea_inbox row per
|
|
24
|
+
// friction pattern it detects, and it has always worked — by tagging a stable
|
|
25
|
+
// title prefix over a valid enum `kind`, because the CHECK constraint on
|
|
26
|
+
// idea_inbox.kind admitted no such value. That workaround named its own exit
|
|
27
|
+
// condition, here and in the routine's SKILL.md: promote the prefix to a real
|
|
28
|
+
// kind once the rank system lands a `builders.rank` column and a migration.
|
|
29
|
+
//
|
|
30
|
+
// Both have landed — rank is live and enforced server-side (ADR 0016), and
|
|
31
|
+
// `viewerRank` reaches all four read paths below from `req.builder.rank` — so
|
|
32
|
+
// task 1003105 added the kind (core_244_skill_friction_kind.sql). What it did
|
|
33
|
+
// NOT do is retire the prefix, and that is the part worth reading twice: the
|
|
34
|
+
// deployed core pin runs behind `main` BY DESIGN (gate 3 of the release
|
|
35
|
+
// pipeline), so there is a window where the database accepts the new kind and
|
|
36
|
+
// the running instance does not yet offer it. A filer flipped inside that
|
|
37
|
+
// window starts failing where it currently succeeds. So the prefix is not a
|
|
38
|
+
// deprecated alias to be swept later — it is the marker every historical row
|
|
39
|
+
// carries and the one the nightly routine still writes, and
|
|
40
|
+
// `skillFrictionHiddenClause` below is the single place that knows both.
|
|
41
|
+
const SKILL_FRICTION_TAG = '[skill-friction]';
|
|
42
|
+
const SKILL_FRICTION_KIND = 'skill-friction';
|
|
43
|
+
|
|
44
|
+
// -------------------------------------------------------------------------
|
|
45
|
+
// Rank visibility (task V3.R70 / #289)
|
|
46
|
+
//
|
|
47
|
+
// Three ranks are planned in GDS-V3 (V3.R04): Xenos (default for any
|
|
48
|
+
// GitHub-authed person), Metic (Archon-approved), Archon. Skill-friction
|
|
49
|
+
// ideas are a system-evolution signal meant for trusted builders — Xenos
|
|
50
|
+
// must NOT see them in their inbox view; Metic and Archon may.
|
|
51
|
+
//
|
|
52
|
+
// `builders.rank` does not exist in the schema yet (it lands with R04), and
|
|
53
|
+
// requireBuilder does not yet populate req.builder.rank — so today every
|
|
54
|
+
// caller passes viewerRank=null. The gate is written to be correct the moment
|
|
55
|
+
// rank is threaded through: it hides skill-friction rows ONLY when the viewer
|
|
56
|
+
// is positively known to be Xenos. Unknown/absent rank (today's reality, where
|
|
57
|
+
// no Xenos exist yet) is treated as Metic+ and sees everything — so wiring this
|
|
58
|
+
// in does not regress current all-builders behavior.
|
|
59
|
+
const XENOS_RANK = 'xenos';
|
|
60
|
+
|
|
61
|
+
function rankCanSeeSkillFriction(viewerRank) {
|
|
62
|
+
// Hide only when we KNOW the viewer is Xenos. null/undefined/anything else
|
|
63
|
+
// (Metic, Archon, or "rank system not deployed") => allowed.
|
|
64
|
+
if (viewerRank == null) return true;
|
|
65
|
+
return String(viewerRank).toLowerCase() !== XENOS_RANK;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// skillFrictionHiddenClause — the ONE predicate meaning "this row is not a
|
|
69
|
+
// skill-friction row", for the four read paths that hide them from a Xenos.
|
|
70
|
+
// It tests BOTH markers (see SKILL_FRICTION_TAG above).
|
|
71
|
+
//
|
|
72
|
+
// It exists as a function rather than four copied conditions because the four
|
|
73
|
+
// gates have to agree and nothing made them: a copy that learned about the new
|
|
74
|
+
// kind while its siblings did not would leak skill-friction rows into exactly
|
|
75
|
+
// one Xenos-facing listing, which is the kind of gap nobody notices until it is
|
|
76
|
+
// in front of the wrong reader. Filtering stays in SQL so a LIMIT still returns
|
|
77
|
+
// up to `limit` VISIBLE rows rather than `limit` rows thinned out afterwards.
|
|
78
|
+
//
|
|
79
|
+
// Pushes its own params onto `params`. Returns null when the viewer may see
|
|
80
|
+
// everything — callers push the clause only when it is non-null, so a viewer
|
|
81
|
+
// with no rank restriction costs no bind parameters at all.
|
|
82
|
+
function skillFrictionHiddenClause(viewerRank, params, alias = '') {
|
|
83
|
+
if (rankCanSeeSkillFriction(viewerRank)) return null;
|
|
84
|
+
const col = alias ? `${alias}.` : '';
|
|
85
|
+
params.push(SKILL_FRICTION_KIND);
|
|
86
|
+
const kindParam = params.length;
|
|
87
|
+
params.push(`${SKILL_FRICTION_TAG}%`);
|
|
88
|
+
return `(${col}kind <> $${kindParam} AND ${col}title NOT LIKE $${params.length})`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
SKILL_FRICTION_TAG,
|
|
93
|
+
SKILL_FRICTION_KIND,
|
|
94
|
+
XENOS_RANK,
|
|
95
|
+
rankCanSeeSkillFriction,
|
|
96
|
+
skillFrictionHiddenClause,
|
|
97
|
+
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.795",
|
|
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.795",
|
|
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.795",
|
|
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",
|
|
@@ -16,8 +16,12 @@
|
|
|
16
16
|
// package; this module copies+transforms them when they ARE present at the core root (today's single
|
|
17
17
|
// checkout, and the core repo itself), and no-ops with a note when they aren't (an installed package).
|
|
18
18
|
// Hooks self-resolve their core require()s at RUNTIME via the copied _core-resolve.js sibling
|
|
19
|
-
// (task 2139, the former W3 follow-up), so no require-path rewriting happens here
|
|
20
|
-
//
|
|
19
|
+
// (task 2139, the former W3 follow-up), so no require-path rewriting happens here. The topology
|
|
20
|
+
// STARTER for a package-only install is now HALF built: settings.json is written from
|
|
21
|
+
// renderStarterSettings() below — permissions only, no hooks — because an instance that received
|
|
22
|
+
// the skills but no permissions met the classifier on every blessed command (task 1003962). A hooks
|
|
23
|
+
// starter remains a follow-up (task 1883), and is a different question: hooks ARE gate machinery,
|
|
24
|
+
// where a list of allowed command names is not.
|
|
21
25
|
//
|
|
22
26
|
// SELF-COPY GUARD. When the instance dir IS the core root (running init inside the core checkout /
|
|
23
27
|
// OTB today), there is nothing to materialize — a self-copy would be a no-op at best and corrupting
|
|
@@ -296,6 +300,50 @@ function copyTreeTransformed(srcDir, destDir, { dryRun, onFile, overwrite = true
|
|
|
296
300
|
|
|
297
301
|
// Transform settings.json: parse, rewrite every hook command through bongosify, re-serialize. Keeps
|
|
298
302
|
// .claude/hooks/*.js commands (instance-local) untouched and rewrites the scripts/gds/*.js ones.
|
|
303
|
+
// The blessed commands an instance builder runs to do the methodology, one rule
|
|
304
|
+
// each (task 1003962). This MIRRORS the core repo's own `.claude/settings.json`
|
|
305
|
+
// allow-list (ADR 0104 / task 1051), translated from `node scripts/gds/<x>.js`
|
|
306
|
+
// into the verb form a materialized instance actually invokes — bongosify()
|
|
307
|
+
// rewrites every skill to `bongos <verb>`, so a path-shaped rule would match
|
|
308
|
+
// nothing there.
|
|
309
|
+
//
|
|
310
|
+
// THE GAP THIS CLOSES. `.claude/settings.json` is excluded from the published
|
|
311
|
+
// package on purpose (PROTECTED_TOPOLOGY_EXCLUDE, beside infra/ and .github/):
|
|
312
|
+
// the core's own copy is deploy and gate machinery that means nothing on an
|
|
313
|
+
// instance. But the skills ship, so an instance received the methodology and
|
|
314
|
+
// none of the permissions that let it run — every instance builder met the
|
|
315
|
+
// auto-mode classifier on the normal workflow, which is how a live builder came
|
|
316
|
+
// to be stuck mid-re-auth with no way forward.
|
|
317
|
+
//
|
|
318
|
+
// WHY THIS IS NOT AN AUTHORITY GRANT (ADR 0016). An allow-rule stops the local
|
|
319
|
+
// agent REFUSING to run a command; it changes nothing about what the server
|
|
320
|
+
// permits. Rank is read fresh from the database on every request by
|
|
321
|
+
// `requireRank`, so a builder who edits this file grants themselves exactly
|
|
322
|
+
// nothing. That is the distinction that makes shipping a list of command names
|
|
323
|
+
// safe where shipping hooks — actual local gate machinery — is not.
|
|
324
|
+
//
|
|
325
|
+
// DELIBERATELY ABSENT: `bongos exec`. It runs any script inside the core by
|
|
326
|
+
// path and is the agent's escape hatch, so allow-listing it would be a blanket
|
|
327
|
+
// grant wearing a narrow rule's clothes. The one script the core's own list
|
|
328
|
+
// blesses (`api.js`) is named in full instead.
|
|
329
|
+
const STARTER_ALLOW = [
|
|
330
|
+
'Bash(bongos claim:*)',
|
|
331
|
+
'Bash(bongos ship:*)',
|
|
332
|
+
'Bash(bongos release:*)',
|
|
333
|
+
'Bash(bongos reauth:*)',
|
|
334
|
+
'Bash(bongos paste-token:*)',
|
|
335
|
+
'Bash(bongos exec scripts/gds/api.js:*)',
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
// The starter an instance gets when the core root carries no settings.json — a
|
|
339
|
+
// package-only install. Permissions ONLY: no hooks (they are topology), no env.
|
|
340
|
+
function renderStarterSettings() {
|
|
341
|
+
return `${JSON.stringify({
|
|
342
|
+
_doc: 'Starter written by scripts/gds/claude-materialize.js (task 1003962). Permissions only — the blessed Bongos commands, mirroring the core repo\'s own allow-list (ADR 0104). An allow-rule lets the local agent RUN a command; it grants no authority, which the server decides per request from your live rank (ADR 0016). Yours to edit: a later materialize never clobbers this file.',
|
|
343
|
+
permissions: { allow: STARTER_ALLOW },
|
|
344
|
+
}, null, 2)}\n`;
|
|
345
|
+
}
|
|
346
|
+
|
|
299
347
|
function transformSettings(srcPath) {
|
|
300
348
|
const raw = fs.readFileSync(srcPath, 'utf8');
|
|
301
349
|
let json;
|
|
@@ -484,7 +532,25 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
|
|
|
484
532
|
log(` ${dryRun ? '[dry-run] would write' : '✓ wrote'} .claude/settings.json (hook commands→bongos, identity rebranded)`);
|
|
485
533
|
}
|
|
486
534
|
} else {
|
|
487
|
-
|
|
535
|
+
// Package-only install: the core root carries no settings.json to copy, so
|
|
536
|
+
// write the STARTER instead of leaving the instance with no permissions at
|
|
537
|
+
// all (task 1003962 — the note that used to sit here said "provide it from a
|
|
538
|
+
// starter" and nothing ever did). Written only when ABSENT, the same rule
|
|
539
|
+
// the starter CLAUDE.md below follows: an instance that has grown its own
|
|
540
|
+
// permissions owns them, and a later materialize must never clobber them.
|
|
541
|
+
const settingsDest = path.join(destClaude, 'settings.json');
|
|
542
|
+
if (fs.existsSync(settingsDest)) {
|
|
543
|
+
summary.skippedFiles.push('settings.json');
|
|
544
|
+
log(' • .claude/settings.json already present — left untouched (the instance owns it).');
|
|
545
|
+
} else {
|
|
546
|
+
if (!dryRun) {
|
|
547
|
+
fs.mkdirSync(destClaude, { recursive: true });
|
|
548
|
+
fs.writeFileSync(settingsDest, renderStarterSettings());
|
|
549
|
+
}
|
|
550
|
+
summary.settings = true;
|
|
551
|
+
summary.notes.push(`.claude/settings.json written from the starter — ${STARTER_ALLOW.length} blessed command(s), permissions only.`);
|
|
552
|
+
log(` ${dryRun ? '[dry-run] would write' : '✓ wrote'} .claude/settings.json (starter: ${STARTER_ALLOW.length} blessed commands, no hooks)`);
|
|
553
|
+
}
|
|
488
554
|
}
|
|
489
555
|
|
|
490
556
|
// 3. hooks — topology; copied WITH an identity rebrand (their .js internals carried
|
|
@@ -650,4 +716,4 @@ if (require.main === module) {
|
|
|
650
716
|
process.exit(s.refused.length ? 1 : 0);
|
|
651
717
|
}
|
|
652
718
|
|
|
653
|
-
module.exports = { scriptToVerb, bongosify, transformSettings, copyTreeTransformed, materializeClaude, materializeModuleSkillsIntoCore, isCoreCheckoutRoot, readModuleSkillsManifest, MODULE_SKILLS_MANIFEST, readInstanceSkillsManifest, INSTANCE_SKILLS_MANIFEST, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills, moduleSkillRoster, moduleSkillSources, vendoredSkillGate, SKILL_NAME_RE, VENDOR_DEFAULT_POLICY };
|
|
719
|
+
module.exports = { scriptToVerb, bongosify, transformSettings, renderStarterSettings, STARTER_ALLOW, copyTreeTransformed, materializeClaude, materializeModuleSkillsIntoCore, isCoreCheckoutRoot, readModuleSkillsManifest, MODULE_SKILLS_MANIFEST, readInstanceSkillsManifest, INSTANCE_SKILLS_MANIFEST, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills, moduleSkillRoster, moduleSkillSources, vendoredSkillGate, SKILL_NAME_RE, VENDOR_DEFAULT_POLICY };
|
package/scripts/gds/ship-flow.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
11
|
const { cliClient, requireSession, arg, detectWorktreeName, cardFlagsFromMe } = require('./cli-lib');
|
|
12
|
-
const { resolveProse, markShipProgress, smokeGateMessages, proseFlagPresent, regradeNotesRefusal } = require('./ship-honesty.js');
|
|
12
|
+
const { resolveProse, markShipProgress, smokeGateMessages, proseFlagPresent, regradeNotesRefusal, staleSummaryNotice } = require('./ship-honesty.js');
|
|
13
13
|
const { classifyEmptyShip } = require('../../modules/lifecycle/ship-preflight');
|
|
14
14
|
const { scopeForModule, isModuleKey } = require('../../src/bongos/module-scope-map');
|
|
15
15
|
const { matchOne } = require('../../src/bongos/path-match');
|
|
@@ -826,6 +826,12 @@ async function regradeMain() {
|
|
|
826
826
|
// PATCH /tasks/:id is the route it uses, and `value_summary` was added to that
|
|
827
827
|
// schema by this same task. tests/ship_cannot_lie.mjs pins the wiring.
|
|
828
828
|
const summary = summaryArg || task.value_summary || '';
|
|
829
|
+
// task 1003693: the fallback above is silent, and on a re-grade silence means
|
|
830
|
+
// re-publishing the failed attempt's sentence. Say so while it can still be
|
|
831
|
+
// corrected — see staleSummaryNotice for the incident this comes from.
|
|
832
|
+
if (!summaryArg) {
|
|
833
|
+
for (const line of staleSummaryNotice(taskId, task.value_summary)) console.log(line);
|
|
834
|
+
}
|
|
829
835
|
|
|
830
836
|
// The grader helpers want a claim-like object for touches[] (preflight's
|
|
831
837
|
// working-tree-drift check) and head_sha (baseline fallback). The claim is
|
|
@@ -195,9 +195,63 @@ function regradeNotesRefusal(taskId) {
|
|
|
195
195
|
];
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// THE FOURTH WAY TO LOSE THE BUILDER'S PROSE — and the only one where what
|
|
199
|
+
// survives is worse than nothing, because it is a sentence that was true once.
|
|
200
|
+
//
|
|
201
|
+
// regradeMain falls back to the stored value_summary when --summary is omitted
|
|
202
|
+
// (`summaryArg || task.value_summary`), and persistRegradeSummary deliberately
|
|
203
|
+
// writes nothing on an omission. Both postures are right alone: a re-grade with
|
|
204
|
+
// no summary must not blank the row, and an empty broadcast would be worse than
|
|
205
|
+
// a stale one. Together they mean the DEFAULT re-grade re-publishes the first
|
|
206
|
+
// attempt's sentence with nothing saying so.
|
|
207
|
+
//
|
|
208
|
+
// That is the task-1002343 incident exactly. Shipped 2026-09-04 asserting "No
|
|
209
|
+
// project was created and nothing on the live system was left changed"; parked
|
|
210
|
+
// at completed on a failing grade; redone 2026-09-07, when a project WAS created
|
|
211
|
+
// (instance 40, r24-proof). The re-grade omitted --summary, so the ledger and the
|
|
212
|
+
// #ship-news post both announced the opposite of what landed. Nobody ignored a
|
|
213
|
+
// correction — there was no prompt to make one.
|
|
214
|
+
//
|
|
215
|
+
// So this is a NOTICE, not a refusal and not a behaviour change. The fallback is
|
|
216
|
+
// correct and stays; task 1003817 already built the correction path (--summary on
|
|
217
|
+
// a re-grade now reaches the row, the hall, /status and the broadcast). What was
|
|
218
|
+
// missing is the moment that tells the builder that path exists, while the run
|
|
219
|
+
// can still be corrected. Printed BEFORE the grade spend for the reason the
|
|
220
|
+
// --notes refusal is: a warning that first costs a 4-worker panel run is barely
|
|
221
|
+
// a warning at all.
|
|
222
|
+
//
|
|
223
|
+
// Returns [] when the row holds no stored summary — there is no stale text to
|
|
224
|
+
// warn about then, and an empty summary is the merge leg's problem, not this
|
|
225
|
+
// one. Pure, so the WORDING is testable: a notice that fails to name the fix is
|
|
226
|
+
// noise with a warning sign on it.
|
|
227
|
+
function staleSummaryNotice(taskId, storedSummary) {
|
|
228
|
+
// Control characters are stripped BEFORE the whitespace collapse, which only
|
|
229
|
+
// matches \s and would leave an ESC through. The stored summary is builder prose
|
|
230
|
+
// that has already been server-side stored and is already bound for Discord and
|
|
231
|
+
// the hall, so this crosses no new trust boundary — but this is the one place it
|
|
232
|
+
// reaches a TERMINAL, where an escape sequence could repaint the notice around it.
|
|
233
|
+
const stored = String(storedSummary || '')
|
|
234
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ')
|
|
235
|
+
.replace(/\s+/g, ' ')
|
|
236
|
+
.trim();
|
|
237
|
+
if (!stored) return [];
|
|
238
|
+
const shown = stored.length > 140 ? `${stored.slice(0, 139)}…` : stored;
|
|
239
|
+
return [
|
|
240
|
+
'⚠ No --summary given — this re-grade reuses the summary stored at the FIRST ship:',
|
|
241
|
+
` "${shown}"`,
|
|
242
|
+
' That sentence describes the attempt that FAILED its grade. A re-grade usually means the',
|
|
243
|
+
' work changed since, and this text is what the panel reads, what the merge commit and PR',
|
|
244
|
+
' say, and what goes to Discord #ship-news as the durable record of this ship.',
|
|
245
|
+
'',
|
|
246
|
+
' If it still describes what landed, ignore this. If not, correct it in this same run:',
|
|
247
|
+
` node scripts/gds/ship.js ${taskId} --regrade --summary-file summary.txt`,
|
|
248
|
+
' (the -file form is the safe one for prose — a shell eats backticks in an inline value)',
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
|
|
198
252
|
module.exports = {
|
|
199
253
|
resolveProse, markShipProgress, fatalRecoveryLines, smokeGateMessages,
|
|
200
|
-
proseFlagPresent, regradeNotesRefusal,
|
|
254
|
+
proseFlagPresent, regradeNotesRefusal, staleSummaryNotice,
|
|
201
255
|
_resetForTest: () => { shipProgress = { stage: 'preflight', taskId: null }; },
|
|
202
256
|
_peekForTest: () => shipProgress,
|
|
203
257
|
};
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.795'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -26,6 +26,9 @@ const require = createRequire(import.meta.url);
|
|
|
26
26
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
27
|
const m = require('../scripts/gds/claude-materialize.js');
|
|
28
28
|
const claim = require('../scripts/gds/claim.js');
|
|
29
|
+
// The live verb table, so a starter allow-rule naming a verb that does not exist
|
|
30
|
+
// fails here rather than shipping as an inert permission (task 1003962).
|
|
31
|
+
const { VERBS: cliVerbs } = require('../bin/bongos.js');
|
|
29
32
|
|
|
30
33
|
let passed = 0;
|
|
31
34
|
let failed = 0;
|
|
@@ -704,5 +707,73 @@ t('materializeModeSkills lands the real module-owned skills, self-ignored, and i
|
|
|
704
707
|
rmSync(core, { recursive: true, force: true });
|
|
705
708
|
});
|
|
706
709
|
|
|
710
|
+
// ---------------------------------------------------------------------------
|
|
711
|
+
// task 1003962 — the starter settings.json for a package-only install.
|
|
712
|
+
//
|
|
713
|
+
// THE GAP: `.claude/skills/` ships in the core package but `.claude/settings.json`
|
|
714
|
+
// is excluded as topology, so an instance received the methodology and none of the
|
|
715
|
+
// permissions that let it run — every blessed command met the auto-mode classifier.
|
|
716
|
+
// ---------------------------------------------------------------------------
|
|
717
|
+
|
|
718
|
+
t('the starter carries PERMISSIONS ONLY — no hooks, no env (those are topology)', () => {
|
|
719
|
+
const parsed = JSON.parse(m.renderStarterSettings());
|
|
720
|
+
assert.ok(Array.isArray(parsed.permissions.allow), 'must carry permissions.allow');
|
|
721
|
+
assert.ok(!('hooks' in parsed), 'hooks are gate machinery and must NOT ship to an instance');
|
|
722
|
+
assert.ok(!('env' in parsed), 'env is topology and must NOT ship to an instance');
|
|
723
|
+
assert.ok(parsed._doc.includes('grants no authority'),
|
|
724
|
+
'the file must say out loud that it grants no authority (ADR 0016)');
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
t('every allow-rule names a verb the CLI actually registers', () => {
|
|
728
|
+
// The drift this catches: a rule naming a verb that was renamed or never existed
|
|
729
|
+
// is silently inert — it looks like a permission and grants nothing, so the
|
|
730
|
+
// builder still meets the classifier and nobody can see why.
|
|
731
|
+
for (const rule of m.STARTER_ALLOW) {
|
|
732
|
+
const inner = rule.replace(/^Bash\(/, '').replace(/:\*\)$/, '');
|
|
733
|
+
const verb = inner.split(' ')[1];
|
|
734
|
+
assert.ok(cliVerbs[verb], `allow-rule "${rule}" names "${verb}", which is not a registered bongos verb`);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
t('the escape hatch is NOT blanket-allowed', () => {
|
|
739
|
+
// `bongos exec` runs any script inside the core by path. Allow-listing it bare
|
|
740
|
+
// would be a blanket grant wearing a narrow rule's clothes.
|
|
741
|
+
assert.ok(!m.STARTER_ALLOW.includes('Bash(bongos exec:*)'),
|
|
742
|
+
'bongos exec must never be allowed bare');
|
|
743
|
+
const execRules = m.STARTER_ALLOW.filter((r) => r.includes('bongos exec'));
|
|
744
|
+
for (const r of execRules) {
|
|
745
|
+
assert.match(r, /bongos exec scripts\/gds\/[a-z-]+\.js/,
|
|
746
|
+
`an exec rule must name its script in full, got: ${r}`);
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
t('a package-only install (no settings.json at the core root) GETS the starter', () => {
|
|
751
|
+
const core = mkdtempSync(join(tmpdir(), 'w3-starter-core-'));
|
|
752
|
+
const inst = mkdtempSync(join(tmpdir(), 'w3-starter-inst-'));
|
|
753
|
+
mkdirSync(join(core, '.claude', 'skills'), { recursive: true }); // skills ship; settings.json does not
|
|
754
|
+
const res = m.materializeClaude({ coreRoot: core, instanceDir: inst, dryRun: false });
|
|
755
|
+
assert.equal(res.settings, true, 'materialize should report it wrote settings');
|
|
756
|
+
const written = join(inst, '.claude', 'settings.json');
|
|
757
|
+
assert.ok(existsSync(written), 'instance must receive a starter settings.json');
|
|
758
|
+
const parsed = JSON.parse(readFileSync(written, 'utf8'));
|
|
759
|
+
assert.deepEqual(parsed.permissions.allow, m.STARTER_ALLOW);
|
|
760
|
+
rmSync(core, { recursive: true, force: true });
|
|
761
|
+
rmSync(inst, { recursive: true, force: true });
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
t("an instance's OWN settings.json is never clobbered", () => {
|
|
765
|
+
const core = mkdtempSync(join(tmpdir(), 'w3-keep-core-'));
|
|
766
|
+
const inst = mkdtempSync(join(tmpdir(), 'w3-keep-inst-'));
|
|
767
|
+
mkdirSync(join(core, '.claude', 'skills'), { recursive: true });
|
|
768
|
+
mkdirSync(join(inst, '.claude'), { recursive: true });
|
|
769
|
+
const mine = JSON.stringify({ permissions: { allow: ['Bash(echo hello:*)'] } }, null, 2);
|
|
770
|
+
writeFileSync(join(inst, '.claude', 'settings.json'), mine);
|
|
771
|
+
m.materializeClaude({ coreRoot: core, instanceDir: inst, dryRun: false });
|
|
772
|
+
assert.equal(readFileSync(join(inst, '.claude', 'settings.json'), 'utf8'), mine,
|
|
773
|
+
'an upgrade must never overwrite permissions the instance has grown');
|
|
774
|
+
rmSync(core, { recursive: true, force: true });
|
|
775
|
+
rmSync(inst, { recursive: true, force: true });
|
|
776
|
+
});
|
|
777
|
+
|
|
707
778
|
console.log(`\nclaude_materialize: ${passed} passed, ${failed} failed`);
|
|
708
779
|
process.exit(failed ? 1 : 0);
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
//
|
|
18
18
|
// (3) --regrade must not swallow --notes. Same shape as (1): prose handed to
|
|
19
19
|
// ship.js that reaches nothing, silently (task 1003702).
|
|
20
|
+
//
|
|
21
|
+
// (4) --regrade must not silently REUSE the failed attempt's summary. The same
|
|
22
|
+
// silence as (3) from the other side: prose the builder never handed over,
|
|
23
|
+
// published as though they had (task 1003693).
|
|
20
24
|
import { strict as assert } from 'node:assert';
|
|
21
25
|
import { test } from 'node:test';
|
|
22
26
|
import { createRequire } from 'node:module';
|
|
@@ -26,7 +30,7 @@ const require = createRequire(import.meta.url);
|
|
|
26
30
|
const { argText } = require('../scripts/gds/cli-lib.js');
|
|
27
31
|
const {
|
|
28
32
|
fatalRecoveryLines, markShipProgress, _peekForTest,
|
|
29
|
-
proseFlagPresent, regradeNotesRefusal,
|
|
33
|
+
proseFlagPresent, regradeNotesRefusal, staleSummaryNotice,
|
|
30
34
|
} = require('../scripts/gds/ship-honesty.js');
|
|
31
35
|
|
|
32
36
|
// Injected IO so no case touches the filesystem.
|
|
@@ -297,3 +301,73 @@ test('a re-grade that omits --summary leaves the stored value_summary alone', ()
|
|
|
297
301
|
// And a failed write never aborts the re-grade — no process.exit, no throw.
|
|
298
302
|
assert.ok(!/process\.exit/.test(code), 'a summary that will not persist must not kill the re-grade');
|
|
299
303
|
});
|
|
304
|
+
|
|
305
|
+
// ── (4) an omitted --summary on a re-grade says so (task 1003693) ────────────
|
|
306
|
+
//
|
|
307
|
+
// The sibling of the ACCEPTANCE 2 test above. That one pins that an omitted
|
|
308
|
+
// --summary does not OVERWRITE the row; this one pins that it does not pass in
|
|
309
|
+
// SILENCE. Both postures were already right and the gap was between them: the
|
|
310
|
+
// default re-grade re-publishes the failed attempt's sentence to #ship-news
|
|
311
|
+
// with nothing saying so (task 1002343, 2026-09-07).
|
|
312
|
+
|
|
313
|
+
test('the stale-summary notice quotes the sentence and names the fix', () => {
|
|
314
|
+
const lines = staleSummaryNotice(1002343, 'No project was created and nothing on the live system was left changed.');
|
|
315
|
+
const text = lines.join('\n');
|
|
316
|
+
// The builder cannot judge a summary they are not shown — the whole point is
|
|
317
|
+
// that the stored sentence is plausible until you read it next to the work.
|
|
318
|
+
assert.match(text, /No project was created and nothing on the live system was left changed\./,
|
|
319
|
+
'quotes the stored sentence verbatim');
|
|
320
|
+
// A notice that names no fix is noise with a warning sign on it.
|
|
321
|
+
assert.match(text, /--regrade --summary-file/, 'names the correcting flag');
|
|
322
|
+
assert.match(text, /ship\.js 1002343 --regrade/, 'as a runnable command with the real task id');
|
|
323
|
+
// WHY it matters: this text becomes the durable public record, not just a log line.
|
|
324
|
+
assert.match(text, /#ship-news/, 'says where the sentence ends up');
|
|
325
|
+
assert.match(text, /FAILED its grade/, 'says which attempt the stored sentence describes');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('the notice stays silent when there is no stored summary to go stale', () => {
|
|
329
|
+
// No stored text means nothing can be re-published wrongly; an empty summary
|
|
330
|
+
// is the merge leg's problem. Warning here would train the builder to skip it.
|
|
331
|
+
for (const empty of ['', ' ', null, undefined]) {
|
|
332
|
+
assert.deepEqual(staleSummaryNotice(1, empty), [], `silent for ${JSON.stringify(empty)}`);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('the quoted sentence is always one readable line', () => {
|
|
337
|
+
// value_summary allows up to LIMITS.VALUE_SUMMARY characters and may hold
|
|
338
|
+
// newlines. Either would break the quoted line out of the notice block and
|
|
339
|
+
// bury the runnable command that follows it.
|
|
340
|
+
const long = staleSummaryNotice(7, 'x'.repeat(400));
|
|
341
|
+
assert.ok(long[1].length < 160, 'a long summary is truncated');
|
|
342
|
+
assert.match(long[1], /…/, 'and says it was truncated');
|
|
343
|
+
const multi = staleSummaryNotice(9, 'line one\n\nline two\ttabbed');
|
|
344
|
+
assert.equal(multi[1].split('\n').length, 1, 'newlines collapse');
|
|
345
|
+
assert.match(multi[1], /"line one line two tabbed"/, 'into single spaces');
|
|
346
|
+
|
|
347
|
+
// This is the one place a stored summary reaches a TERMINAL. \s does not match
|
|
348
|
+
// ESC, so the collapse above would pass an escape sequence straight through and
|
|
349
|
+
// let it repaint the notice around itself.
|
|
350
|
+
const esc = staleSummaryNotice(11, 'safe\u001b[31mred\u0007 text');
|
|
351
|
+
assert.ok(!/[\u0000-\u001f\u007f-\u009f]/.test(esc.join(' ')),
|
|
352
|
+
'terminal control characters are stripped before display');
|
|
353
|
+
assert.match(esc[1], /"safe \[31mred text"/, 'and the readable text survives');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('the notice is wired into the regrade path, before any grade spend', () => {
|
|
357
|
+
// The (1) lesson again: a helper nothing calls is not a fix. And the ORDER is
|
|
358
|
+
// load-bearing for the same reason the --notes refusal's is — a warning the
|
|
359
|
+
// builder reads only after a 4-worker panel has run cannot change that run.
|
|
360
|
+
const src = readFileSync(new URL('../scripts/gds/ship-flow.js', import.meta.url), 'utf8');
|
|
361
|
+
const regrade = src.slice(src.indexOf('async function regradeMain'));
|
|
362
|
+
const code = regrade.replace(/\/\/[^\n]*/g, ''); // comments describe, they do not print
|
|
363
|
+
|
|
364
|
+
const call = code.indexOf('staleSummaryNotice(taskId, task.value_summary)');
|
|
365
|
+
assert.ok(call !== -1, 'regradeMain emits the notice');
|
|
366
|
+
// Keyed on the RAW flag, like persistRegradeSummary — `summary` has already
|
|
367
|
+
// absorbed the fallback by this point, so testing it would never be falsy.
|
|
368
|
+
// Brace-form or one-liner, but it must branch on summaryArg and nothing else.
|
|
369
|
+
assert.match(code, /if \(!summaryArg\)\s*\{?\s*(\n\s*)?for \(const line of staleSummaryNotice/,
|
|
370
|
+
'it fires on the raw omitted flag, not the resolved fallback');
|
|
371
|
+
const preflight = code.indexOf('preflight(claimLike');
|
|
372
|
+
assert.ok(preflight !== -1 && call < preflight, 'and prints before the preflight and the panel');
|
|
373
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// tests/skill_friction_kind.mjs
|
|
2
|
+
//
|
|
3
|
+
// 'skill-friction' as a real idea kind, and the one predicate that hides those
|
|
4
|
+
// rows from a Xenos (task 1003105, goal 1000095, from idea 1000759).
|
|
5
|
+
//
|
|
6
|
+
// Two things are pinned here, and they fail in different directions.
|
|
7
|
+
//
|
|
8
|
+
// THE DRIFT TRIPWIRE. `VALID_KINDS` in JS and the CHECK constraint in SQL are
|
|
9
|
+
// two lists that must say the same thing, and nothing made them. Widening one
|
|
10
|
+
// and not the other is silent in both directions: a kind added only to JS gets
|
|
11
|
+
// past validation and dies on INSERT with a constraint violation instead of a
|
|
12
|
+
// clean BAD_KIND, and a kind added only to SQL is unreachable. That is the exact
|
|
13
|
+
// shape of the bug this task fixed, so the fix ships with the tripwire.
|
|
14
|
+
//
|
|
15
|
+
// THE FOUR GATES. Skill-friction rows are hidden from Xenos-rank builders in
|
|
16
|
+
// four separate read paths. They used to be four hand-copied conditions testing
|
|
17
|
+
// a title prefix; now that the marker can ALSO be a kind, four copies would be
|
|
18
|
+
// four chances for one to learn about the kind and the others not to — and a
|
|
19
|
+
// gate that silently stops hiding is invisible until the wrong reader sees the
|
|
20
|
+
// row. So there is one predicate, and these tests hold it to both markers, to
|
|
21
|
+
// correct bind-parameter numbering, and to costing nothing for a viewer who may
|
|
22
|
+
// see everything.
|
|
23
|
+
//
|
|
24
|
+
// Pure: no DB, no network.
|
|
25
|
+
//
|
|
26
|
+
// Run: node tests/skill_friction_kind.mjs
|
|
27
|
+
|
|
28
|
+
import { strict as assert } from 'node:assert';
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { createRequire } from 'node:module';
|
|
33
|
+
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
const inbox = require('../modules/ideas/inbox.js');
|
|
36
|
+
|
|
37
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
38
|
+
|
|
39
|
+
// The migration that currently defines the constraint. If a later migration
|
|
40
|
+
// widens it again, point this at that file — forcing that edit is the job.
|
|
41
|
+
const CONSTRAINT_MIGRATION = path.join(
|
|
42
|
+
ROOT, 'migrations', 'core_244_skill_friction_kind.sql'
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
let passed = 0;
|
|
46
|
+
let failed = 0;
|
|
47
|
+
function test(name, fn) {
|
|
48
|
+
try {
|
|
49
|
+
fn();
|
|
50
|
+
passed++;
|
|
51
|
+
console.log(` ok ${name}`);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
failed++;
|
|
54
|
+
console.log(` FAIL ${name}`);
|
|
55
|
+
console.log(` ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---- the kind exists at all ------------------------------------------------
|
|
60
|
+
|
|
61
|
+
test('skill-friction is a valid kind, so the two skills that file one stop failing BAD_KIND', () => {
|
|
62
|
+
assert.ok(
|
|
63
|
+
inbox.VALID_KINDS.includes('skill-friction'),
|
|
64
|
+
"/planning-session and /priority-session both instruct kind='skill-friction'"
|
|
65
|
+
);
|
|
66
|
+
assert.equal(inbox.SKILL_FRICTION_KIND, 'skill-friction');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('widening the enum did not drop anything that was already in it', () => {
|
|
70
|
+
for (const k of [
|
|
71
|
+
'feature', 'bug', 'infra', 'refactor', 'spike', 'learning-capture',
|
|
72
|
+
'cleanup', 'decision', 'blocker-resolution', 'unclassified', 'criterion-proposal',
|
|
73
|
+
]) {
|
|
74
|
+
assert.ok(inbox.VALID_KINDS.includes(k), `${k} missing from VALID_KINDS`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ---- JS and SQL say the same thing -----------------------------------------
|
|
79
|
+
|
|
80
|
+
test('the SQL CHECK constraint and VALID_KINDS list exactly the same kinds', () => {
|
|
81
|
+
const sql = fs.readFileSync(CONSTRAINT_MIGRATION, 'utf8');
|
|
82
|
+
const m = /CHECK\s*\(\s*kind\s+IN\s*\(([\s\S]*?)\)\s*\)/i.exec(sql);
|
|
83
|
+
assert.ok(m, 'the migration declares a CHECK (kind IN (...)) constraint');
|
|
84
|
+
|
|
85
|
+
const fromSql = [...m[1].matchAll(/'([^']+)'/g)].map((x) => x[1]);
|
|
86
|
+
assert.ok(fromSql.length > 0, 'the constraint lists at least one kind');
|
|
87
|
+
|
|
88
|
+
const a = [...fromSql].sort();
|
|
89
|
+
const b = [...inbox.VALID_KINDS].sort();
|
|
90
|
+
assert.deepEqual(
|
|
91
|
+
a, b,
|
|
92
|
+
`SQL and JS disagree — only in SQL: [${a.filter((k) => !b.includes(k))}], ` +
|
|
93
|
+
`only in JS: [${b.filter((k) => !a.includes(k))}]`
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ---- the visibility gate ---------------------------------------------------
|
|
98
|
+
|
|
99
|
+
test('a viewer who may see everything costs no clause and no bind parameters', () => {
|
|
100
|
+
for (const rank of [null, undefined, 'metic', 'archon', 'Archon', 'something-new']) {
|
|
101
|
+
const params = [];
|
|
102
|
+
assert.equal(inbox.skillFrictionHiddenClause(rank, params), null, `rank=${rank}`);
|
|
103
|
+
assert.equal(params.length, 0, `rank=${rank} pushed no params`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('a Xenos is hidden from BOTH markers — the kind and the legacy title prefix', () => {
|
|
108
|
+
const params = [];
|
|
109
|
+
const clause = inbox.skillFrictionHiddenClause('xenos', params);
|
|
110
|
+
assert.ok(clause, 'a Xenos gets a clause');
|
|
111
|
+
assert.match(clause, /kind <> \$1/, 'the kind marker is tested');
|
|
112
|
+
assert.match(clause, /title NOT LIKE \$2/, 'the legacy prefix marker is tested too');
|
|
113
|
+
assert.deepEqual(params, [inbox.SKILL_FRICTION_KIND, `${inbox.SKILL_FRICTION_TAG}%`]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('the rank test is case-insensitive, so a capitalised Xenos is still hidden', () => {
|
|
117
|
+
const params = [];
|
|
118
|
+
assert.ok(inbox.skillFrictionHiddenClause('XENOS', params), 'XENOS is still a Xenos');
|
|
119
|
+
assert.equal(inbox.rankCanSeeSkillFriction('Xenos'), false);
|
|
120
|
+
assert.equal(inbox.rankCanSeeSkillFriction('metic'), true);
|
|
121
|
+
assert.equal(inbox.rankCanSeeSkillFriction(null), true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('bind parameters are numbered from where the caller already was, not from 1', () => {
|
|
125
|
+
const params = ['open', 42];
|
|
126
|
+
const clause = inbox.skillFrictionHiddenClause('xenos', params, 'i');
|
|
127
|
+
assert.match(clause, /i\.kind <> \$3/, 'continues the caller\'s numbering');
|
|
128
|
+
assert.match(clause, /i\.title NOT LIKE \$4/);
|
|
129
|
+
assert.equal(params.length, 4, 'and appended exactly two');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('the table alias is applied to both columns or to neither', () => {
|
|
133
|
+
const withAlias = inbox.skillFrictionHiddenClause('xenos', [], 'i');
|
|
134
|
+
assert.match(withAlias, /i\.kind/);
|
|
135
|
+
assert.match(withAlias, /i\.title/);
|
|
136
|
+
const bare = inbox.skillFrictionHiddenClause('xenos', []);
|
|
137
|
+
assert.doesNotMatch(bare, /\bi\./, 'no alias means no alias on either column');
|
|
138
|
+
assert.match(bare, /\bkind <>/);
|
|
139
|
+
assert.match(bare, /\btitle NOT LIKE/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('the clause is parenthesised, so ANDing it into a WHERE cannot change the other terms', () => {
|
|
143
|
+
const clause = inbox.skillFrictionHiddenClause('xenos', []);
|
|
144
|
+
assert.ok(clause.startsWith('(') && clause.endsWith(')'), `not parenthesised: ${clause}`);
|
|
145
|
+
assert.match(clause, / AND /, 'both markers must hold for the row to be shown');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ---- the legacy marker is not quietly retired ------------------------------
|
|
149
|
+
|
|
150
|
+
test('the legacy title prefix is still exported and still the nightly routine\'s marker', () => {
|
|
151
|
+
assert.equal(inbox.SKILL_FRICTION_TAG, '[skill-friction]');
|
|
152
|
+
const routine = fs.readFileSync(
|
|
153
|
+
path.join(ROOT, '.claude', 'scheduled-tasks', 'skill-usage-review', 'SKILL.md'), 'utf8'
|
|
154
|
+
);
|
|
155
|
+
assert.ok(
|
|
156
|
+
routine.includes('[skill-friction]'),
|
|
157
|
+
'skill-usage-review still tags the prefix — the gate must keep recognising it'
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(`skill-friction kind + visibility gate (task 1003105): ${passed} passed, ${failed} failed`);
|
|
163
|
+
if (failed > 0) process.exit(1);
|