@bongos/core 1.20.41 → 1.20.43
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 +94 -64
- package/.claude/skills/backlog-review/SKILL.md +1 -0
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/bug-triage/SKILL.md +1 -0
- package/.claude/skills/builder-backup/SKILL.md +1 -0
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/builder-cost/SKILL.md +1 -0
- package/.claude/skills/builder-exit/SKILL.md +1 -0
- package/.claude/skills/builder-key/SKILL.md +1 -1
- package/.claude/skills/builder-reauth/SKILL.md +1 -1
- package/.claude/skills/builder-redteam/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-setup/SKILL.md +1 -1
- package/.claude/skills/collab-review/SKILL.md +1 -1
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/design-sync/SKILL.md +1 -0
- package/.claude/skills/feedback/SKILL.md +1 -1
- package/.claude/skills/figma-design-sync/SKILL.md +1 -0
- package/.claude/skills/goal-close/SKILL.md +184 -0
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/goal-review/SKILL.md +5 -102
- package/.claude/skills/goal-uat/SKILL.md +5 -81
- package/.claude/skills/grade-audit/SKILL.md +6 -85
- package/.claude/skills/grade-recover/SKILL.md +1 -1
- package/.claude/skills/grade-sweep/SKILL.md +174 -0
- package/.claude/skills/grader-health/SKILL.md +5 -72
- package/.claude/skills/idea-triage/SKILL.md +1 -0
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/owner-review/SKILL.md +40 -0
- package/.claude/skills/planning-session/SKILL.md +1 -0
- package/.claude/skills/priority-session/SKILL.md +1 -0
- package/.claude/skills/read-session-export/SKILL.md +1 -1
- package/.claude/skills/recall/SKILL.md +1 -1
- package/.claude/skills/scan-before-install/SKILL.md +1 -1
- package/.claude/skills/session-handoff/SKILL.md +1 -1
- package/.claude/skills/worktree-clean/SKILL.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/file-map.md +12 -10
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/slash-commands.md +17 -11
- package/docs/packs/artist.md +1 -1
- package/docs/packs/engineer.md +4 -5
- package/modules/copy-desk/module.json +1 -1
- package/{.claude → modules/copy-desk}/skills/tweak/SKILL.md +1 -0
- package/modules/lifecycle/dependency-advisory.js +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/fitness-ratchets.js +2 -0
- package/scripts/gds/module-artifact.js +1 -1
- package/scripts/gds/module-assess-security.js +251 -0
- package/scripts/gds/skill-lint.js +32 -9
- package/src/module-api.js +1 -1
- package/tests/module_assess_security.mjs +216 -0
- package/tests/skill_grade_audit.mjs +34 -17
- package/tests/skill_grader_health.mjs +28 -10
- package/tests/skill_lint.mjs +37 -0
- package/tests/skill_menu_ruling.mjs +99 -0
|
@@ -42,29 +42,31 @@ Every one of these talks to the build system. They're grouped by when you'd reac
|
|
|
42
42
|
| `/builder-setup` | One-time enrolment — signs you in and registers you |
|
|
43
43
|
| `/builder-reauth` | Refresh an expired session |
|
|
44
44
|
| `/builder-key` | Check / set your own API key status |
|
|
45
|
-
| `/builder-cost` | Log
|
|
46
|
-
| `/builder-backup` | Check or trigger a database backup before risky work |
|
|
47
|
-
| `/builder-exit` | Offboard or reactivate a builder *(Archon only)* |
|
|
45
|
+
| `/builder-cost` † | Log or read project spend |
|
|
46
|
+
| `/builder-backup` † | Check or trigger a database backup before risky work |
|
|
47
|
+
| `/builder-exit` † | Offboard or reactivate a builder *(Archon only)* |
|
|
48
48
|
|
|
49
49
|
### Triage and planning *(Metic and above)*
|
|
50
50
|
|
|
51
51
|
| Command | What it does |
|
|
52
52
|
|---|---|
|
|
53
|
-
| `/
|
|
53
|
+
| `/owner-review` | One walk through everything waiting on a decision: backlog → bugs → ideas → priorities. Say *"triage ideas"* or *"review the backlog"* and it opens at that step |
|
|
54
54
|
| `/blocker-review` · `/blocker-solve` | Walk open blockers, or drive one to done |
|
|
55
55
|
| `/goal-create` | Plan and create one goal — scope wall, criteria, seed tasks |
|
|
56
|
-
| `/goal-
|
|
57
|
-
| `/planning-session` | Scope an upcoming version and seed its task list |
|
|
58
|
-
| `/
|
|
56
|
+
| `/goal-close` † | Close a goal's criteria: test them on the live site and sign off, then decide the ones nothing was delivered for |
|
|
57
|
+
| `/planning-session` † | Scope an upcoming version and seed its task list |
|
|
58
|
+
| `/grade-sweep` † | Check the grader is healthy, then audit what shipped past it |
|
|
59
59
|
| `/merge-mode` | Manual fallback when a confirmed task didn't land |
|
|
60
60
|
|
|
61
|
+
The four steps of `/owner-review` still answer to their old names — `/backlog-review`, `/bug-triage`, `/idea-triage`, `/priority-session` — and so do `/goal-uat` · `/goal-review` (now the two parts of `/goal-close`) and `/grader-health` · `/grade-audit` (now the two parts of `/grade-sweep`). All of those are typed-only (†).
|
|
62
|
+
|
|
61
63
|
### Discipline modes — not slash commands any more
|
|
62
64
|
|
|
63
65
|
The three core crafts no longer have a slash command. `/dev`, `/paint` and `/ideate` were **deleted** when the root instructions split into a role-neutral kernel (`CLAUDE.md`) plus one **role pack** per craft: `docs/packs/engineer.md`, `docs/packs/artist.md`, `docs/packs/ideator.md`. A claimed task's discipline routes you to your pack — the Conductor injects its path into your session on the next prompt, and `claim.js` prints a one-line reminder — and you read it. A module-contributed discipline can still ship a skill, which is how `ui` → `/design` below works.
|
|
64
66
|
|
|
65
67
|
### Design and art pipeline
|
|
66
68
|
|
|
67
|
-
`/design` is the ui-discipline playbook (world-first, show-first — shipped by the `ui-design` module, ADR 0197); `/design-sync` and `/figma-design-sync` round-trip the UI design system. The `/otb-*` family — `otb-tile-generate`, `otb-design-review`, `otb-character-review`, `otb-feedback-capture`, `otb-figma-sync` — is the reference instance's pixel-art pipeline; it's only meaningful on an instance that runs that pipeline, so it ships in the `pixel-art` module, which is off unless the project turns it on. The design-style skills (`/impeccable`, `/style`, the taste and look skills) are the same: the `design-styles` module, off unless turned on — `/design` tells you which.
|
|
69
|
+
`/design` is the ui-discipline playbook (world-first, show-first — shipped by the `ui-design` module, ADR 0197); `/design-sync` † and `/figma-design-sync` † round-trip the UI design system. `/tweak` † applies the next page an artist rewrote in the studio; it ships in the `copy-desk` module, where the studio lives. The `/otb-*` family — `otb-tile-generate`, `otb-design-review`, `otb-character-review`, `otb-feedback-capture`, `otb-figma-sync` — is the reference instance's pixel-art pipeline; it's only meaningful on an instance that runs that pipeline, so it ships in the `pixel-art` module, which is off unless the project turns it on. The design-style skills (`/impeccable`, `/style`, the taste and look skills) are the same: the `design-styles` module, off unless turned on — `/design` tells you which.
|
|
68
70
|
|
|
69
71
|
### Session and meta
|
|
70
72
|
|
|
@@ -73,9 +75,13 @@ The three core crafts no longer have a slash command. `/dev`, `/paint` and `/ide
|
|
|
73
75
|
| `/session-handoff` | Emit a paste-ready prompt to start a fresh session with |
|
|
74
76
|
| `/read-session-export` | Read a past session's `/export` zip |
|
|
75
77
|
| `/feedback` | Pull in the latest recorded walkthrough bundle |
|
|
76
|
-
| `/new-project` | Zero-to-live runbook for a brand-new instance |
|
|
78
|
+
| `/new-project` † | Zero-to-live runbook for a brand-new instance |
|
|
77
79
|
| `/builder-redteam` | File a security / vulnerability report |
|
|
78
80
|
|
|
81
|
+
### † Typed-only commands
|
|
82
|
+
|
|
83
|
+
A command marked † is **hidden from Claude's own menu but still works when you type it**. Every listed command's description sits in every session's context whether it is used or not, so the rarely-used admin ones carry `disable-model-invocation: true` in their `SKILL.md`: Claude Code leaves them out of what the model sees, and only a person typing `/name` runs them (task 1004471). The catch: plain English will not reach them, and Claude will not start one on its own — type the slash form.
|
|
84
|
+
|
|
79
85
|
## Claude Code's own commands
|
|
80
86
|
|
|
81
87
|
These work in **any** repository — they have nothing to do with this project. `/help` prints the live, authoritative list for your version; the ones below are simply the ones you'll actually meet.
|
|
@@ -111,11 +117,11 @@ These are the ones that actually cause confusion.
|
|
|
111
117
|
|
|
112
118
|
**`/export` vs `/read-session-export`.** Claude's `/export` *writes* a transcript zip; our `/read-session-export` *reads* one back.
|
|
113
119
|
|
|
114
|
-
**`/review` reviews a GitHub PR.** The project's review commands are `/
|
|
120
|
+
**`/review` reviews a GitHub PR.** The project's review commands are `/owner-review`, `/blocker-review`, and `/goal-close` — different jobs entirely.
|
|
115
121
|
|
|
116
122
|
## Two more things worth knowing
|
|
117
123
|
|
|
118
|
-
**Every command has a plain-English twin.** Each skill lists trigger phrases, so *"what can I work on"* reaches `/builder-start` and *"ship it"* reaches `/builder-ship`. You never have to memorise the slash form.
|
|
124
|
+
**Every listed command has a plain-English twin.** Each skill lists trigger phrases, so *"what can I work on"* reaches `/builder-start` and *"ship it"* reaches `/builder-ship`. You never have to memorise the slash form — except for the typed-only † commands above.
|
|
119
125
|
|
|
120
126
|
**Same commands, outside Claude.** The work-loop commands are also a terminal CLI: `bongos start`, `bongos claim N`, `bongos ship`. Same actions, same API — usable from any terminal, a Chromebook, or CI. Run `bongos help` for the verb list.
|
|
121
127
|
|
package/docs/packs/artist.md
CHANGED
|
@@ -27,7 +27,7 @@ The studio's own words (the greeting and its sub-line) are the studio page itsel
|
|
|
27
27
|
|
|
28
28
|
Only the holder writes. A page someone else holds is read-only and names them. A draft left unsubmitted keeps its words and can be taken up again.
|
|
29
29
|
|
|
30
|
-
**Between submit and approval, a session applies it.** Submitted pages wait for a builder session running `/tweak` ([the skill](
|
|
30
|
+
**Between submit and approval, a session applies it.** Submitted pages wait for a builder session running `/tweak` ([the skill](../../modules/copy-desk/skills/tweak/SKILL.md), Metic+). It transcribes every line, renders the page before and after, and parks it. **It never lands the page.**
|
|
31
31
|
|
|
32
32
|
**The Approval queue.** Pictures of the applied page, desktop or phone, light or dark, flipped **before / after**. Beside them, the changed lines ("was:" / "becomes:"); a line the applier could not apply says why. Then:
|
|
33
33
|
|
package/docs/packs/engineer.md
CHANGED
|
@@ -74,7 +74,7 @@ If this is an autonomous, bypass-permissions, or scheduled run and no one will a
|
|
|
74
74
|
|
|
75
75
|
1. **Resolve your claim** — `/builder-ship` with handoff notes and a value summary, or `/builder-release` on abandonment. Bongos records the session-log row and awards credits.
|
|
76
76
|
2. **Write a session log** as its own file at `docs/session-logs/YYYY-MM-DD-<slug>.md`, using [`docs/handoff-template.md`](../handoff-template.md). Do not hand-edit the kernel's §13 index or `docs/session-log-index.md` — both regenerate from your file at ship. For a single-task session the Bongos `session_logs` row is enough and a separate file isn't needed.
|
|
77
|
-
3. **Review the `idea_inbox`** via `GET /api/bongos/inbox`, or run `/
|
|
77
|
+
3. **Review the `idea_inbox`** via `GET /api/bongos/inbox`, or run `/owner-review ideas`.
|
|
78
78
|
4. **Update the detail files** if anything changed: a new gotcha → a recipe or a captured learning; a new blocker → `POST /api/bongos/blockers`; a decision → a new ADR.
|
|
79
79
|
5. **Update [`docs/architecture.md`](../architecture.md)** if the live system changed, and [`docs/file-map.md`](../file-map.md) if files or folders moved.
|
|
80
80
|
|
|
@@ -82,13 +82,12 @@ If nothing changed in a section, leave it alone — don't churn.
|
|
|
82
82
|
|
|
83
83
|
### Daily cadence (once per day, not per session)
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
Two manual slash-commands keep the methodology surfaces from accumulating drift. Any session can pick them up; it only matters that they happen.
|
|
86
86
|
|
|
87
|
-
- **`/
|
|
87
|
+
- **`/owner-review`** — one walk through backlog → bugs → ideas → priorities (task 1004471); each step is still typeable alone (`/backlog-review`, `/bug-triage`, `/idea-triage`, `/priority-session`). Ideas: promote, discard, merge, or defer — deferring is fine; the cadence is the discipline. Backlog: rows waiting on a **person** get walked (promote / kill / water); rows waiting on a **trigger** are counted, never walked (a satisfied dep auto-promotes), and rows stranded behind an abandoned dep are surfaced. `/demote` is invalid on a backlog row (409 `cannot_demote` — it requires `ready`).
|
|
88
88
|
- **`/blocker-review`** — walk the open blockers: resolved (which auto-promotes linked tasks via a DB trigger), still blocked with a note, or escalate.
|
|
89
|
-
- **`/backlog-review`** — walk `status='backlog'`, the pre-workable state a human must say go on. Rows waiting on a **person** get walked (promote / kill / water); rows waiting on a **trigger** are counted, never walked (a satisfied dep auto-promotes). It also surfaces rows stranded behind an abandoned dep, which the trigger can never fire for. `/demote` is invalid on a backlog row (409 `cannot_demote` — it requires `ready`).
|
|
90
89
|
|
|
91
|
-
**`/tweak`** (Metic
|
|
90
|
+
**`/tweak`** (Metic+, typed-only, in the `copy-desk` module) works a fourth queue: pages an artist submitted from the studio wait there until a session applies them. It never lands one; the artist approves first (ADR 0341).
|
|
92
91
|
|
|
93
92
|
If a day passes without them, the queues quietly grow. Running them is the structural cure for the markdown-graveyard pattern.
|
|
94
93
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"coreVersion": "^1.8.0",
|
|
7
7
|
"default": true,
|
|
8
8
|
"maintenance": { "status": "core-maintained" },
|
|
9
|
-
"contributes": { "routes": ["copy-desk"], "migrations": true },
|
|
9
|
+
"contributes": { "routes": ["copy-desk"], "migrations": true, "skills": ["tweak"] },
|
|
10
10
|
"provides": [],
|
|
11
11
|
"consumes": ["lifecycle"]
|
|
12
12
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: tweak
|
|
3
3
|
description: >-
|
|
4
4
|
Apply the next submitted page tweak: claim the round, apply every line, render the page before and after, attach the renders and ship it to wait for the artist. Metic+. Triggers: "/tweak", "apply the next page tweak", "work the tweak queue".
|
|
5
|
+
disable-model-invocation: true
|
|
5
6
|
plain: >-
|
|
6
7
|
Applies the next set of change requests someone made to a page, and shows before and after pictures for the artist to approve.
|
|
7
8
|
reach-for: >-
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
// 1. POST /tasks attaches `dependency_advisory` to its 201 body (this
|
|
18
18
|
// file's `buildDependencyAdvisory`), read by the /goal-create skill's
|
|
19
19
|
// "sibling advisory" section (.claude/skills/goal-create/SKILL.md).
|
|
20
|
-
// 2. The periodic sweep: `/goal-
|
|
20
|
+
// 2. The periodic sweep: `/goal-close`'s residue walk (was `/goal-review`) runs
|
|
21
21
|
// `node scripts/gds/audit-deps.js --min high` as one more housekeeping
|
|
22
|
-
// step (idea 1000276's half — see .claude/skills/goal-
|
|
22
|
+
// step (idea 1000276's half — see .claude/skills/goal-close/SKILL.md, Part 2).
|
|
23
23
|
//
|
|
24
24
|
// HARD CONSTRAINT, same as goal-advisory.js: advisory only (a finding addressed
|
|
25
25
|
// to a human never acts by itself — ADR 0158 §2). This
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.43",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.43",
|
|
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.20.
|
|
3
|
+
"version": "1.20.43",
|
|
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/release-notes.json
CHANGED
|
@@ -8355,5 +8355,17 @@
|
|
|
8355
8355
|
"id": "1004470",
|
|
8356
8356
|
"text": "Projects that don't do pixel art or design styling no longer carry 18 unused AI commands in every session (about a third less to load); projects that want them turn them on with one setting."
|
|
8357
8357
|
}
|
|
8358
|
+
],
|
|
8359
|
+
"1.20.42": [
|
|
8360
|
+
{
|
|
8361
|
+
"id": "1003792",
|
|
8362
|
+
"text": "The module store can now run a security check on each module version: it fails a version that ships a secret file, pulls code from outside the official package registry, or depends on a package with a known serious vulnerabili"
|
|
8363
|
+
}
|
|
8364
|
+
],
|
|
8365
|
+
"1.20.43": [
|
|
8366
|
+
{
|
|
8367
|
+
"id": "1004471",
|
|
8368
|
+
"text": "Claude's list of project commands is now short enough to read in full every session: the rarely used admin commands still work when you type them but no longer take up room, and one new /owner-review walks the backlog, bugs, i"
|
|
8369
|
+
}
|
|
8358
8370
|
]
|
|
8359
8371
|
}
|
|
@@ -423,6 +423,8 @@ function skillListingMetrics() {
|
|
|
423
423
|
const lint = require('./skill-lint.js');
|
|
424
424
|
// Resident skills only (task 1004470): a skill owned by a module this checkout leaves
|
|
425
425
|
// off (pixel-art, design-styles — both default: false) is in no session's listing here.
|
|
426
|
+
// listingChars also leaves out HIDDEN skills (disable-model-invocation: true, task 1004471):
|
|
427
|
+
// Claude Code keeps their descriptions out of the model's context entirely.
|
|
426
428
|
const files = lint.listSkillFiles();
|
|
427
429
|
return { skill_listing_chars: lint.lintSkills(files, { resident: lint.residentSkillFiles(undefined, { files }) }).listingChars };
|
|
428
430
|
} catch (e) {
|
|
@@ -216,4 +216,4 @@ async function verifyModuleArtifact(tgz, { key, maxBytes = MAX_TARBALL_BYTES, in
|
|
|
216
216
|
return out;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
module.exports = { MAX_TARBALL_BYTES, packModule, verifyModuleArtifact };
|
|
219
|
+
module.exports = { MAX_TARBALL_BYTES, packModule, verifyModuleArtifact, deniedFiles };
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/module-assess-security.js — the Security part of a module's
|
|
3
|
+
// assessment: a pass/fail GATE over one published store version, recorded as a
|
|
4
|
+
// module_assessment_signals row (task 1003792; ADR 0343 D2; table from core_265).
|
|
5
|
+
//
|
|
6
|
+
// WHY a gate and not a score. ADR 0343 D2: a version that fails the security
|
|
7
|
+
// check is not listed, whatever else it scores, and security is never averaged
|
|
8
|
+
// in — so a module cannot buy back a vulnerability with good tests. The outcome
|
|
9
|
+
// is therefore only ever passed / failed, or not_scored when the check could not
|
|
10
|
+
// be completed (which is NOT a pass: a gate that cannot run must not open).
|
|
11
|
+
//
|
|
12
|
+
// The check reuses what the project already enforces rather than re-typing it:
|
|
13
|
+
// 1. the publish denylist — module-artifact.js deniedFiles (the repo-wide
|
|
14
|
+
// matchesDeny list + credential-named files; ADR 0107 §4). Publish already
|
|
15
|
+
// refuses these; re-checking here means a rule added later still reaches
|
|
16
|
+
// versions published before it.
|
|
17
|
+
// 2. dependency SOURCES — every entry in module.json `dependencies` (ADR 0138)
|
|
18
|
+
// must be a plain registry package name with a plain semver range or dist-
|
|
19
|
+
// tag. That is an ALLOWLIST (registrySpec below), deliberately stricter than
|
|
20
|
+
// a list of bad protocols: a git/http/file, aliased or GitHub-shorthand spec
|
|
21
|
+
// — or anything else unrecognised — installs code from somewhere nobody
|
|
22
|
+
// vetted, and fails the gate. (It mirrors the artifact-scan floor's grading,
|
|
23
|
+
// but a core script may not require a module's files, so it is its own
|
|
24
|
+
// small rule.) Floating ranges and wildcards are only noted.
|
|
25
|
+
// 3. KNOWN VULNERABILITIES in those dependencies — only when every spec passed
|
|
26
|
+
// check 2, so nothing a module names is ever fetched from a place of its
|
|
27
|
+
// choosing and no `file:` path is ever resolved on this host. npm resolves the ranges in a
|
|
28
|
+
// throwaway directory (--package-lock-only --ignore-scripts: registry
|
|
29
|
+
// metadata only, no package code downloaded or run), then `npm audit`; any
|
|
30
|
+
// high or critical advisory fails the gate (the threshold scripts/security/
|
|
31
|
+
// dep-audit.js uses for the core, whose parser this reuses).
|
|
32
|
+
// And one input that is reported, never failed on: the `maintenance` posture
|
|
33
|
+
// (ADR 0166). "Undeclared", "deprecated" or "orphaned" is something a buyer
|
|
34
|
+
// should see, but it is upkeep, not a vulnerability — that call is recorded in
|
|
35
|
+
// the row's detail so the hall (task 1003799) can surface it.
|
|
36
|
+
//
|
|
37
|
+
// Nothing here executes the module's own code, so unlike module-assess-tests.js
|
|
38
|
+
// this is safe to run on the control plane.
|
|
39
|
+
//
|
|
40
|
+
// node scripts/gds/module-assess-security.js <store_module_versions.id> run + record
|
|
41
|
+
// node scripts/gds/module-assess-security.js --dir modules/<key> dry run (prints; no DB)
|
|
42
|
+
|
|
43
|
+
const fs = require('node:fs');
|
|
44
|
+
const os = require('node:os');
|
|
45
|
+
const path = require('node:path');
|
|
46
|
+
const { execFile } = require('node:child_process');
|
|
47
|
+
const { deniedFiles, verifyModuleArtifact } = require('./module-artifact');
|
|
48
|
+
const { extractNpmFindings } = require('../security/dep-audit');
|
|
49
|
+
|
|
50
|
+
const AUDIT_TIMEOUT_MS = 120_000;
|
|
51
|
+
// npm is a .cmd shim on Windows, which execFile cannot start without a shell; go
|
|
52
|
+
// through cmd.exe explicitly rather than shell:true (the args are fixed constants).
|
|
53
|
+
const npmCommand = (args) => (process.platform === 'win32'
|
|
54
|
+
? ['cmd.exe', ['/d', '/s', '/c', 'npm', ...args]]
|
|
55
|
+
: ['npm', args]);
|
|
56
|
+
|
|
57
|
+
// Run one npm command in `cwd`; resolve { ok, stdout, stderr, error }. Never rejects.
|
|
58
|
+
function npmRun(args, cwd, { exec = execFile, timeoutMs = AUDIT_TIMEOUT_MS } = {}) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const [cmd, argv] = npmCommand(args);
|
|
61
|
+
exec(cmd, argv, { cwd, timeout: timeoutMs, maxBuffer: 32 * 1024 * 1024, windowsHide: true },
|
|
62
|
+
(err, stdout, stderr) => resolve({ err, stdout: String(stdout || ''), stderr: String(stderr || '') }));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Audit a dependency map { name: range } for known high/critical advisories.
|
|
67
|
+
// Resolves { ok: true, findings } or { ok: false, reason } when npm could not
|
|
68
|
+
// resolve or audit (offline, unknown package, npm missing) — the caller treats
|
|
69
|
+
// that as not_scored, never as a pass.
|
|
70
|
+
async function auditDependencies(deps, { exec, timeoutMs } = {}) {
|
|
71
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'module-assess-security-'));
|
|
72
|
+
try {
|
|
73
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'module-assess-audit', private: true, version: '0.0.0', dependencies: deps }));
|
|
74
|
+
const lock = await npmRun(['install', '--package-lock-only', '--ignore-scripts', '--no-audit', '--no-fund'], dir, { exec, timeoutMs });
|
|
75
|
+
if (lock.err || !fs.existsSync(path.join(dir, 'package-lock.json'))) {
|
|
76
|
+
return { ok: false, reason: 'npm could not resolve the declared dependencies' };
|
|
77
|
+
}
|
|
78
|
+
// npm audit exits 1 when it finds anything; the JSON on stdout is the answer either way.
|
|
79
|
+
const audit = await npmRun(['audit', '--json'], dir, { exec, timeoutMs });
|
|
80
|
+
let parsed;
|
|
81
|
+
try { parsed = JSON.parse(audit.stdout || ''); } catch { return { ok: false, reason: 'npm audit produced no readable report' }; }
|
|
82
|
+
if (parsed && parsed.error) return { ok: false, reason: 'npm audit could not reach the advisory database' };
|
|
83
|
+
return { ok: true, findings: extractNpmFindings(parsed) };
|
|
84
|
+
} finally {
|
|
85
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// npm's package-name rule (scoped or not, lowercase, no path segments).
|
|
90
|
+
const NPM_NAME_RE = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
91
|
+
// A semver range: versions, x/* wildcards, ^ ~ comparators, hyphen and || unions,
|
|
92
|
+
// prerelease/build suffixes. No ':' '/' '#' '@' — so no protocol, path, alias or ref.
|
|
93
|
+
const RANGE_RE = /^[0-9A-Za-z.*^~<>=|\s+-]+$/;
|
|
94
|
+
const DIST_TAGS = new Set(['latest', 'next']);
|
|
95
|
+
|
|
96
|
+
// Grade one declared dependency. { ok: false } means it would install from
|
|
97
|
+
// somewhere other than the public registry (or cannot be read as a registry
|
|
98
|
+
// spec): the gate fails and it is never handed to npm. { ok: true, kind } is a
|
|
99
|
+
// registry spec; kind names how much the registry decides at install time.
|
|
100
|
+
function registrySpec(name, spec) {
|
|
101
|
+
const s = String(spec).trim();
|
|
102
|
+
if (!NPM_NAME_RE.test(name)) return { ok: false, kind: 'invalid package name' };
|
|
103
|
+
if (DIST_TAGS.has(s) || s === '*' || s === '' || /^[xX]$/.test(s)) return { ok: true, kind: 'wildcard' };
|
|
104
|
+
if (/^[a-z+]+:/i.test(s)) return { ok: false, kind: 'non-registry source' };
|
|
105
|
+
// Every whitespace/||-separated token must start with a comparator or a digit/x.
|
|
106
|
+
const tokens = s.split(/\s*\|\|\s*|\s+/).filter(Boolean);
|
|
107
|
+
if (!RANGE_RE.test(s) || !tokens.every((t) => t === '-' || /^(?:[\^~]|[<>]=?|=)?\s*[0-9xX*]/.test(t))) {
|
|
108
|
+
return { ok: false, kind: 'not a registry range' };
|
|
109
|
+
}
|
|
110
|
+
if (/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(s)) return { ok: true, kind: 'pinned' };
|
|
111
|
+
return { ok: true, kind: 'floating range' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The maintenance posture as a buyer would read it (ADR 0166).
|
|
115
|
+
function maintenancePosture(moduleJson) {
|
|
116
|
+
const m = moduleJson && moduleJson.maintenance;
|
|
117
|
+
return m && typeof m.status === 'string' ? m.status : 'undeclared';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The gate over one version's files ([{ path, buf }], module-relative) and its
|
|
121
|
+
// parsed module.json. Returns the signal fields { outcome, score, sample_size,
|
|
122
|
+
// reason, detail }. `audit` is injectable so tests need no network.
|
|
123
|
+
async function assessSecurity(key, files, moduleJson, { audit = auditDependencies } = {}) {
|
|
124
|
+
const failures = [];
|
|
125
|
+
const notes = [];
|
|
126
|
+
|
|
127
|
+
const denied = deniedFiles(key, files.map((f) => f.path));
|
|
128
|
+
if (denied.length) failures.push({ check: 'denylist', files: denied });
|
|
129
|
+
|
|
130
|
+
const deps = (moduleJson && moduleJson.dependencies) || {};
|
|
131
|
+
const names = Object.keys(deps).sort();
|
|
132
|
+
const sources = [];
|
|
133
|
+
for (const name of names) {
|
|
134
|
+
const graded = registrySpec(name, deps[name]);
|
|
135
|
+
const entry = { package: name.slice(0, 214), spec: String(deps[name]).slice(0, 200), kind: graded.kind };
|
|
136
|
+
if (!graded.ok) sources.push(entry);
|
|
137
|
+
else if (graded.kind === 'wildcard') notes.push({ check: 'dependency_wildcard', ...entry });
|
|
138
|
+
else if (graded.kind === 'floating range') notes.push({ check: 'dependency_range', ...entry });
|
|
139
|
+
}
|
|
140
|
+
if (sources.length) failures.push({ check: 'dependency_source', dependencies: sources });
|
|
141
|
+
|
|
142
|
+
// Audit only an all-registry list: a failed source already fails the gate, and
|
|
143
|
+
// handing it to npm would make this host fetch (or resolve a file: path) of the
|
|
144
|
+
// module's choosing.
|
|
145
|
+
let advisories = [];
|
|
146
|
+
if (names.length && !sources.length) {
|
|
147
|
+
const a = await audit(deps);
|
|
148
|
+
if (!a.ok) {
|
|
149
|
+
return {
|
|
150
|
+
outcome: 'not_scored', score: null, sample_size: names.length,
|
|
151
|
+
reason: `the dependency audit could not run: ${a.reason}`,
|
|
152
|
+
detail: { failures, notes, maintenance: maintenancePosture(moduleJson) },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
advisories = a.findings.map((f) => ({ package: f.package, severity: f.severity, id: f.id, summary: f.summary, fix: f.fix }));
|
|
156
|
+
if (advisories.length) failures.push({ check: 'vulnerable_dependency', advisories });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const maintenance = maintenancePosture(moduleJson);
|
|
160
|
+
if (maintenance !== 'maintained' && maintenance !== 'core-maintained') {
|
|
161
|
+
notes.push({ check: 'maintenance', status: maintenance });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const detail = { failures, notes, maintenance, dependencies_checked: names.length };
|
|
165
|
+
if (failures.length) {
|
|
166
|
+
return { outcome: 'failed', score: null, sample_size: names.length, reason: `failed: ${failures.map((f) => f.check).join(', ')}`, detail };
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
outcome: 'passed', score: null, sample_size: names.length,
|
|
170
|
+
reason: names.length ? `passed: ${names.length} dependency(ies) from the registry, no high or critical advisory` : 'passed: no denied files and no dependencies',
|
|
171
|
+
detail,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Append one Security row. Never UPDATEs (core_265 is append-only).
|
|
176
|
+
async function insertSecuritySignal(versionId, s, { db }) {
|
|
177
|
+
const { rows: [row] } = await db.query(
|
|
178
|
+
`INSERT INTO module_assessment_signals (version_id, part, outcome, score, sample_size, reason, detail)
|
|
179
|
+
VALUES ($1, 'security', $2, $3, $4, $5, $6)
|
|
180
|
+
RETURNING id, version_id, part, outcome, score, sample_size, reason, measured_at`,
|
|
181
|
+
[versionId, s.outcome, s.score, s.sample_size, s.reason, JSON.stringify(s.detail || {})]);
|
|
182
|
+
return row;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Read and verify one published version, run the gate, record the result. A
|
|
186
|
+
// tarball that is missing or fails verification is not_scored — the gate could
|
|
187
|
+
// not be run, so it stays closed.
|
|
188
|
+
async function recordSecuritySignal(versionId, { db, storeDir, audit } = {}) {
|
|
189
|
+
const pool = db || require('../../src/bongos/pool').pool;
|
|
190
|
+
const { versionArtifactFile } = require('../../src/bongos/module-store');
|
|
191
|
+
const { rows: [ver] } = await pool.query(
|
|
192
|
+
'SELECT id, module_key, version, artifact_path FROM store_module_versions WHERE id = $1', [versionId]);
|
|
193
|
+
if (!ver) return { ok: false, code: 'version_not_found', message: `no store_module_versions row ${versionId}` };
|
|
194
|
+
|
|
195
|
+
let signal;
|
|
196
|
+
try {
|
|
197
|
+
const tgz = await fs.promises.readFile(versionArtifactFile(ver, storeDir ? { dir: storeDir } : {}));
|
|
198
|
+
const v = await verifyModuleArtifact(tgz, { key: ver.module_key, includeFiles: true });
|
|
199
|
+
if (v.ok) {
|
|
200
|
+
signal = await assessSecurity(ver.module_key, v.files, v.moduleJson, { audit });
|
|
201
|
+
} else if (v.code === 'denied_content') {
|
|
202
|
+
signal = { outcome: 'failed', score: null, sample_size: null, reason: 'failed: denylist', detail: { failures: [{ check: 'denylist' }] } };
|
|
203
|
+
} else {
|
|
204
|
+
signal = { outcome: 'not_scored', score: null, sample_size: null, reason: `the tarball failed verification (${v.code})`, detail: {} };
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
signal = { outcome: 'not_scored', score: null, sample_size: null, reason: `the check could not run (${e.code || 'error'})`, detail: {} };
|
|
208
|
+
}
|
|
209
|
+
const row = await insertSecuritySignal(ver.id, signal, { db: pool });
|
|
210
|
+
return { ok: true, version: ver, signal: row };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function readModuleDir(dir) {
|
|
214
|
+
const out = [];
|
|
215
|
+
const walk = (rel) => {
|
|
216
|
+
for (const e of fs.readdirSync(path.join(dir, rel), { withFileTypes: true })) {
|
|
217
|
+
if (e.name === 'node_modules' || e.name === '.git') continue;
|
|
218
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
219
|
+
if (e.isDirectory()) walk(r);
|
|
220
|
+
else if (e.isFile()) out.push({ path: r, buf: fs.readFileSync(path.join(dir, r)) });
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
walk('');
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function main(argv) {
|
|
228
|
+
const i = argv.indexOf('--dir');
|
|
229
|
+
if (i !== -1) {
|
|
230
|
+
const dir = path.resolve(argv[i + 1] || '');
|
|
231
|
+
const moduleJson = JSON.parse(fs.readFileSync(path.join(dir, 'module.json'), 'utf8'));
|
|
232
|
+
console.log(JSON.stringify(await assessSecurity(moduleJson.key, readModuleDir(dir), moduleJson), null, 2));
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
const id = argv[0];
|
|
236
|
+
if (!/^\d+$/.test(String(id || ''))) {
|
|
237
|
+
console.error('usage: node scripts/gds/module-assess-security.js <store_module_versions.id> | --dir modules/<key>');
|
|
238
|
+
return 2;
|
|
239
|
+
}
|
|
240
|
+
const res = await recordSecuritySignal(id);
|
|
241
|
+
if (!res.ok) { console.error(res.message); return 1; }
|
|
242
|
+
const s = res.signal;
|
|
243
|
+
console.log(`${res.version.module_key} ${res.version.version}: security ${s.outcome} — ${s.reason} (signal ${s.id})`);
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (require.main === module) {
|
|
248
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (e) => { console.error(e.stack || e.message); process.exit(1); });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = { assessSecurity, auditDependencies, recordSecuritySignal, insertSecuritySignal, maintenancePosture, registrySpec };
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
// context window, and a description is resident in EVERY session, so long ones
|
|
15
15
|
// crowd the others out and get truncated (skill routing degrades).
|
|
16
16
|
//
|
|
17
|
+
// A HIDDEN skill — frontmatter `disable-model-invocation: true` — is not in that
|
|
18
|
+
// listing at all: Claude Code keeps its description out of the model's context and
|
|
19
|
+
// loads it only when a person types /name (the model cannot invoke it). So the
|
|
20
|
+
// budget counts LISTED skills only, and hidden ones are reported on their own line
|
|
21
|
+
// (task 1004471, owner ruling on blocker 1000139).
|
|
22
|
+
//
|
|
17
23
|
// NOT A YAML PARSER, on purpose. These are targeted regex checks for the hazards that
|
|
18
24
|
// actually bit us (a plain scalar carrying ": " or " #", an unclosed quote or flow
|
|
19
25
|
// collection, a tab, a continuation line), so the repo gains a CI gate with no new
|
|
@@ -95,11 +101,19 @@ function lintFrontmatter(lines) {
|
|
|
95
101
|
return { errors, warnings, fields };
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
// Whether a skill is HIDDEN from the model's listing: `disable-model-invocation: true`
|
|
105
|
+
// (Claude Code: "description not in context"; typeable as /name, never model-invoked).
|
|
106
|
+
// Anything but a literal true — absent, false, a typo — leaves it listed, so a mistake
|
|
107
|
+
// can only ever over-count the budget, never hide a skill nobody meant to hide.
|
|
108
|
+
function isHidden(fields) {
|
|
109
|
+
return String((fields && fields['disable-model-invocation']) || '').trim().toLowerCase() === 'true';
|
|
110
|
+
}
|
|
111
|
+
|
|
98
112
|
// Lint one SKILL.md file's text. `file` is used for the name check and messages.
|
|
99
113
|
function lintSkillFile(text, { file = 'SKILL.md' } = {}) {
|
|
100
114
|
const dirName = path.basename(path.dirname(file));
|
|
101
115
|
const fm = splitFrontmatter(text);
|
|
102
|
-
const res = { file, errors: [], warnings: [], name: null, description: '', listingChars: 0 };
|
|
116
|
+
const res = { file, errors: [], warnings: [], name: null, description: '', listingChars: 0, hidden: false };
|
|
103
117
|
if (!fm) { res.errors.push('no YAML frontmatter (the file must start with --- and carry name + description)'); return res; }
|
|
104
118
|
const { errors, warnings, fields } = lintFrontmatter(fm.lines);
|
|
105
119
|
res.errors.push(...errors); res.warnings.push(...warnings);
|
|
@@ -107,8 +121,10 @@ function lintSkillFile(text, { file = 'SKILL.md' } = {}) {
|
|
|
107
121
|
if (!name) res.errors.push('missing "name"');
|
|
108
122
|
else if (name !== dirName && dirName !== 'SKILL.md') res.warnings.push(`name "${name}" differs from its folder "${dirName}" — the folder name wins in listings`);
|
|
109
123
|
if (!desc) res.errors.push('missing or empty "description" — the skill would match on its body text');
|
|
124
|
+
res.hidden = isHidden(fields);
|
|
110
125
|
if (desc.length > DESC_HARD_CHARS) res.errors.push(`description is ${desc.length} chars — over Claude Code's ${DESC_HARD_CHARS}-char cap`);
|
|
111
|
-
else if (desc.length > DESC_WARN_CHARS) res.warnings.push(`description is ${desc.length} chars — over the ~${DESC_WARN_CHARS} we aim for; every char is resident in every session`);
|
|
126
|
+
else if (desc.length > DESC_WARN_CHARS && !res.hidden) res.warnings.push(`description is ${desc.length} chars — over the ~${DESC_WARN_CHARS} we aim for; every char is resident in every session`);
|
|
127
|
+
// listingChars is what the entry WOULD cost if listed; lintSkills leaves hidden ones out of the total.
|
|
112
128
|
res.name = name || dirName; res.description = desc; res.listingChars = res.name.length + desc.length;
|
|
113
129
|
return res;
|
|
114
130
|
}
|
|
@@ -164,13 +180,18 @@ function residentSkillFiles(root = REPO_ROOT, { instanceDir = root, files = list
|
|
|
164
180
|
}
|
|
165
181
|
|
|
166
182
|
// Lint a set of files; returns per-file results plus the listing total vs budget. The
|
|
167
|
-
// listing counts `resident` (a list of paths) when given, else every linted file
|
|
183
|
+
// listing counts `resident` (a list of paths) when given, else every linted file — and of
|
|
184
|
+
// those, only the LISTED ones: a hidden skill (isHidden) is resident but costs the model
|
|
185
|
+
// nothing, so it is tallied separately as hiddenSkills / hiddenChars (task 1004471).
|
|
168
186
|
function lintSkills(files, { resident } = {}) {
|
|
169
187
|
const results = files.map((f) => lintSkillFile(fs.readFileSync(f, 'utf8'), { file: f }));
|
|
170
188
|
const counted = resident ? new Set(resident.map((f) => path.resolve(f))) : null;
|
|
171
|
-
const
|
|
189
|
+
const here = counted ? results.filter((r) => counted.has(path.resolve(r.file))) : results;
|
|
190
|
+
const listed = here.filter((r) => !r.hidden);
|
|
191
|
+
const hidden = here.filter((r) => r.hidden);
|
|
172
192
|
const listingChars = listed.reduce((a, r) => a + r.listingChars, 0);
|
|
173
|
-
|
|
193
|
+
const hiddenChars = hidden.reduce((a, r) => a + r.listingChars, 0);
|
|
194
|
+
return { results, listingChars, listedSkills: listed.length, residentSkills: here.length, hiddenSkills: hidden.length, hiddenChars, hiddenNames: hidden.map((r) => r.name), budgetChars: LISTING_BUDGET_CHARS, overBudget: listingChars > LISTING_BUDGET_CHARS };
|
|
174
195
|
}
|
|
175
196
|
|
|
176
197
|
// The PostToolUse hook's gate. It must match a module-owned SKILL.md too — that is the
|
|
@@ -190,8 +211,10 @@ function format(report, { relTo = REPO_ROOT } = {}) {
|
|
|
190
211
|
}
|
|
191
212
|
const errs = report.results.reduce((a, r) => a + r.errors.length, 0);
|
|
192
213
|
const warns = report.results.reduce((a, r) => a + r.warnings.length, 0);
|
|
193
|
-
const
|
|
194
|
-
|
|
214
|
+
const residentN = report.residentSkills !== undefined ? report.residentSkills : report.listedSkills;
|
|
215
|
+
const resident = residentN !== undefined && residentN !== report.results.length ? ` ${residentN} resident here (the rest belong to modules this instance leaves off);` : '';
|
|
216
|
+
out.push(`skill-lint: ${report.results.length} skill(s), ${errs} error(s), ${warns} warning(s);${resident} listing ${report.listingChars} chars over ${report.listedSkills} listed skill(s) (~${Math.round(report.listingChars / 4)} tokens) vs ~${report.budgetChars}-char budget${report.overBudget ? ' — OVER: entries get truncated and routing degrades' : ''}`);
|
|
217
|
+
if (report.hiddenSkills) out.push(`skill-lint: ${report.hiddenSkills} hidden skill(s) (disable-model-invocation — typeable as /name, not in the model's listing, not counted): ${report.hiddenChars} chars — ${report.hiddenNames.join(', ')}`);
|
|
195
218
|
return out.join('\n');
|
|
196
219
|
}
|
|
197
220
|
|
|
@@ -210,7 +233,7 @@ function main() {
|
|
|
210
233
|
if (!isSkillMd(fp) || !fs.existsSync(fp)) return;
|
|
211
234
|
const report = lintSkills([fp]);
|
|
212
235
|
const r = report.results[0];
|
|
213
|
-
if (r.errors.length || r.warnings.length) console.log('[skill-lint] ' + format(report).split('\n').
|
|
236
|
+
if (r.errors.length || r.warnings.length) console.log('[skill-lint] ' + format(report).split('\n').filter((l) => !l.startsWith('skill-lint:')).join('\n[skill-lint] '));
|
|
214
237
|
} catch (_) { /* a hook never breaks the edit */ }
|
|
215
238
|
process.exitCode = 0;
|
|
216
239
|
});
|
|
@@ -223,5 +246,5 @@ function main() {
|
|
|
223
246
|
process.exitCode = report.results.some((r) => r.errors.length) ? 1 : 0;
|
|
224
247
|
}
|
|
225
248
|
|
|
226
|
-
module.exports = { lintFrontmatter, lintSkillFile, lintSkills, listSkillFiles, residentSkillFiles, splitFrontmatter, isSkillMd, format, DESC_WARN_CHARS, DESC_HARD_CHARS, LISTING_BUDGET_CHARS };
|
|
249
|
+
module.exports = { lintFrontmatter, lintSkillFile, lintSkills, listSkillFiles, residentSkillFiles, splitFrontmatter, isSkillMd, isHidden, format, DESC_WARN_CHARS, DESC_HARD_CHARS, LISTING_BUDGET_CHARS };
|
|
227
250
|
if (require.main === module) main();
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.20.
|
|
78
|
+
const CORE_VERSION = '1.20.43'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|