Repository navigation
Description
Protects: the published skills/** catalog — the surface npx skills add and @objectstack/skills ship to customer agents.
Filing gate: ③ — maintainer direct order.
Provenance: the maintainer, live PM chat in session session_01CinRQcE6T8SvMbhH8D3NhS (skills seat 1), 2026-10-12. The seat put the prevention half of the inherited question 「对外的 skills 中有issue id…如何避免」 as: on objectstack, widen the EXISTING id-lint to skills/** (a scan-set widening of a gate that exists, not a new gate; 0 hits today); on objectui, no gate. The maintainer selected, verbatim, 「不加,写成文规则(推荐)」 for objectui; the question text they answered stated the objectstack half as the default: 「objectstack 侧扩既有 id-lint 到 skills/** 不算新门禁、默认会做」. The principle is the 2026-08-12 ruling quoted in this gate's own header.
Reading
On origin/main 246edf5b3: scripts/pm/check-skill-id-lint.mjs scans SCAN_ROOT = '.claude/skills/pm-dispatch' plus EXTRA_FILES = ['.claude/agents/os-dev.md', 'AGENTS.md'] (lines 55–56) with the pattern /#[0-9]{3,}/. git grep -c -E '#[0-9]{3,}' origin/main -- 'skills/**' answers 0 files: the published catalog is clean today by discipline alone, and nothing holds it there. The sister tree on objectui accumulated 41 citations in 13 files under the same absence (the objectui cleanup card is filed beside this one).
Change
- Add the published catalog to the scan set: a second scan root,
skills(every.mdunder it, the same walker), besideSCAN_ROOTfor the internal corpus. The header's "What it scans" paragraph names the new root and why: the reader is a customer agent, so the 2026-08-12 ruling applies a fortiori. - Self-test: a case where a
.mdunder the new root carrying#1234is red; the existing cases stay.pnpm check:pm-skill-id-lint(self-test + gate) green on the tree. - Derivation: this gate declares its surface for
scripts/pm/dispatch-gates.mjsthrough the path literals in its own source (the two constants andROOT_FILE_WATCH_HINTS). Confirm withnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack skills/objectstack-data/SKILL.mdthat askills/**change now derivescheck:pm-skill-id-lint. ⛔scripts/pm/dispatch-gates.mjs,dispatch-gates.data.mjsanddispatch-gates.self-test.mjsare FROZEN (ruling 208 R6): if the derivation needs an edit there, stop and reportblockedwith the reading; do not edit them. .github/workflows/lint.ymlalready runspnpm check:pm-skill-id-lint(line 958 today); no workflow change is expected.scripts/pm/**is not published →skip-changeset.
Who acts
The domain:skills seat (scripts/pm/** PM tooling; the gate's SUBJECT is the governed and published skill text). Not a governed path, so the seat lands it through the queue after its own at-tier review.
Dedup
MCP issue search over objectstack-ai/objectstack, open and closed, query "check-skill-id-lint widen scan set published skills catalog skills/** issue number citations": 55 hits. Nearest: #22059 (closed decision on how published-skills teaching errors are found) and #9923 (closed: the published catalog carried no line-ratchet ceiling). None widens this gate's scan set.
Refs: the objectstack-ai/objectui card filed in the same act (the 41-site cleanup + the AGENTS.md rule).
Activity
objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 1 · 2026-10-12T08:36Z
Session:
session_01CinRQcE6T8SvMbhH8D3NhSAccount:
huangyiirene(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-22983-id-lint-published-skillsWorktree:
objectstack-issue-22983Domain:
domain:skillsSeat:
domain:skills#1File surface:
scripts/pm/check-skill-id-lint.mjs(the scan set, the header's "What it scans" paragraph, the--self-test); ⛔ notscripts/pm/dispatch-gates*.mjs(frozen) (stop on breach; explain in the report)Container & model:
S 级机械卡,mode:subagent,model: default — this run's --tier on the path printed 「Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s)」 and 「floor sonnet · default opus · ceiling fable」; default rather than floor because the card asks the dev to judge whether dispatch-gates derives the family from the new root without an edit to a frozen fileClause-②: no — a repository gate script; no published schema, accept set or public surface is touched
Responsibility:
n/a — not a defect card(a scan-set widening of an existing gate, ruled by the maintainer)Thread-read: none
Serial constraints cleared: none in flight — no
pm:dispatchedclaim namesscripts/pm/check-skill-id-lint.mjs(claim threads grepped 2026-10-12T08:36Z); none of the open PRs changes it (files lists read); last touchebc6afe8d(2026-09-23); #22979 (in flight, this seat) edits.claude/skills/pm-dispatch/SKILL.md, a file this gate scans but does not touch — the widened gate must stay green on that PR's head too (the published catalog carries 0 ids today)Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{
"issue": 22983,
"status": "done",
"branch": "claude/issue-22983-id-lint-published-skills",
"pr": "#23002",
"session": "session_01CinRQcE6T8SvMbhH8D3NhS — subagent run; the dispatching PM session id, the same id the Claim comment and the commit Claude-Session trailers carry",
"premise_still_valid": true,
"summary": "scripts/pm/check-skill-id-lint.mjs now walks a second root, PUBLISHED_SCAN_ROOT = skills (every .md, the same mdFilesUnder walker, the same cannot-read-is-red rule), beside SCAN_ROOT. It declares ROOT_DIR_WATCH_HINTS = [skills//*.md] so that dispatch-gates derives the gate for a skills/ card, with no frozen file touched. The header says why: the reader is a customer agent that cannot resolve an issue number. run is split into lintTree(root) plus a printer, so the self-test drives the real walk over fixture trees; ID_PATTERN, LEGACY_EXACT, verdictFor and its messages, and the internal corpus semantics are unchanged. ONE PREMISE WAS FALSIFIED, and two deviations from the dispatch follow from it. (1) The card and dispatch assumed "0 hits today". At BASE 6a40a09, and at the card own reading commit 246edf5, git grep -c -E with the hash-three-digits pattern over skills/ answers 2 files. Both hits are hex colours, not citations: color #3498db at skills/objectstack-data/rules/field-types.md:86 and primaryColor #4169E1 at skills/objectstack-ui/rules/navigation.md:20. So the widening as specified lands red; ablation 1 proves it. The dispatch put "name the hex boundary, do not add an exclusion" in its mechanism-assumption section, premised on zero hex colours. The route taken per the ruling intent: under skills/ ONLY, a match whose digit run continues into a letter or underscore is not counted (NOT_A_CITATION_TAIL). An all-decimal colour such as #000000 stays red; the header names it as the known false-positive boundary and a self-test row pins it red. ID_PATTERN and the internal corpus semantics are untouched, and no skills/** file was edited. (2) The new bare-root constant made node scripts/pm/bare-root-worklist.mjs --self-test refuse it as FRESH (2 failures at 0a07f2f). That tool printed remedy is to record a verdict, so scripts/pm/bare-root-worklist.mjs gained a DECLARED-NARROWER TRIAGE row and a pinned SPELLINGS entry (published skill markdown). This is OUTSIDE the claim file surface (not a frozen file). The in-surface alternative, declaring skills/** wholesale, was refused because it names the gate for 12 eval JSON files it never reads, which the worklist own refusal text calls the costlier error. Both deviations are in open_questions for the PM to accept or redirect. One more reading: the dispatch measuring command git ls-files skills//*.md answers 52, not 53, because git default pathspec misses the top-level skills/README.md, which the walker reads (find and git ls-files skills piped to grep -c .md both answer 53).",
"tests": "All at final head 6744bf9 unless noted. FILE COUNT: pnpm check:pm-skill-id-lint printed "34 file(s) clean" at BASE 6a40a09 and prints "87 file(s) clean (pattern /#[0-9]{3,}/g)" now, +53, equal to the 53 .md files under skills/ (git ls-files skills piped to grep -c .md answers 53; find skills -name *.md answers 53; the dispatch git ls-files skills//.md answers 52, see summary). SELF-TEST: "check-skill-id-lint self-test: 23 cases pass." (14 existing + 9 new; floor pin 14 -> 23). DERIVATION: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack skills/objectstack-data/SKILL.md answered "30 command(s) — 21 pnpm, 9 direct node (22 matched by path ...)" at BASE without pnpm check:pm-skill-id-lint, and "31 command(s) — 22 pnpm, 9 direct node (23 matched by path ...)" at 0a07f2f and again at 6744bf9 (identical lists); diff = the one added line pnpm check:pm-skill-id-lint. extractWatchHints over the gate answers [.claude/skills/pm-dispatch, .claude/agents/os-dev.md, skills/**/.md, AGENTS.md/]. The hint covers 53 of 65 tracked files under skills/, set-equal to the lintTree walk in both directions. GATE UNION: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; change set 2 paths vs merge base 6a40a09) derived 30 commands; all 30 ran at 6744bf9 with exit 0, each exit captured before any pipe. Verdict lines: check:watch-hint-literal "73 declaration(s) across 4 rostered name(s)"; check:pm-dispatch-gates (detached under nohup, waited with tail --pid) "dispatch-gates self-test (fast tier): 1828 cases pass; 6 slow section(s) deferred.", BATTERY_EXIT=0; bare-root-worklist --self-test "OK self-test: 82 live row(s), 60 unreachable as spelled, 47 recorded verdict(s) — none stale, none missing, none contradicted" (it was exit 1 with 2 FRESH/UNJUDGED failures at 0a07f2f before the TRIAGE row); check:nul-bytes "OK (scanned 11000 text file(s) ... no raw ASCII control bytes)"; the rest exit 0. Reconciled: "dispatch-gates --ran: 30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED (a DERIVED zero — all 30 recorded an exit code and none of them is 3)". LINT (narrowed, proven, at 6744bf9): eslint --no-inline-config --format json on the 2 changed files answers 2 files, 0 errors, 0 warnings; isPathIgnored is false for both; eslint.config.mjs enables no type-aware linting (no parserOptions.project), so the diff cannot move a verdict on any untouched file. ABLATIONS (on committed state, scripts/ablation-replace.mjs WRAP mode plus a shell trap with absolute paths; gate blob 9a1b0df4 is unchanged at the final head): (1) "if (published && NOT_A_CITATION_TAIL" changed to "if (false && ...": anchor 1 -> 0, blob 9a1b0df4 -> 235341e2. SELFTEST_EXIT=1, red row "published root: a colour whose digits run into a letter -> green". GATE_EXIT=1, red on exactly field-types.md:86 and navigation.md:20. Restored: blob == HEAD, git diff HEAD empty. (2) "[SCAN_ROOT, PUBLISHED_SCAN_ROOT]" changed to "[SCAN_ROOT]": SELFTEST_EXIT=1 with 5 red rows (the four published-root rows and the missing-root row); GATE_EXIT=0 at "34 file(s) clean", so the self-test is what catches it. Restored the same way. NOT MEASURED, declared to CI: the ci.yml Test Core job these paths schedule (7 CI-only steps) and the 4 type-check lanes (no TypeScript touched). CI on PR 23002: in_progress, not awaited.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 — three relay strokes, each ONE POST /repos/objectstack-ai/objectstack/dispatches (event fleet-write), executed by the relay as objectstack-fleet[bot]: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23002/assignees [huangyiirene], run 38227732551, body read back 9286 bytes sent = 9286 stored; (2) label-write = POST /repos//issues/23002/labels [skip-changeset], run 38227785340, read back matches; (3) this os-dev-report comment = POST /repos//issues/22983/comments. Not REST: 3 git push to the branch (the empty probe push, then 0a07f2f, then 6744bf9). REST reads only otherwise.",
"open_questions": [
{
"question": "The hex-colour route. The dispatch said "name the hex boundary, do not add an exclusion", premised on zero hex colours in skills/. Two exist, so the bare pattern reds the widening. Accept the published-root-only letter-tail rule (NOT_A_CITATION_TAIL)?",
"options": [
"A (taken): under skills/ only, a match whose digits run into a letter or underscore is not counted; an all-decimal colour stays red and is named as the remaining boundary; ID_PATTERN and the internal corpus are untouched",
"B: edit the two skill files to colours that do not start with three digits; this is Tier H (skills/), outside the file surface, and leaves the trap for the next author",
"C: a LEGACY_EXACT waiver at count 1 for each file; this misuses the waiver (a colour swapped for a citation would still pass) and leaves the trap open"
],
"recommendation": "A. Business need: the two colours are real customer teaching content (select-option and branding colours), and an id-lint red on them is a false positive on correct metadata. Long-term: A follows what a citation is (a number with a non-word neighbour), not a per-file waiver. Prevent AI errors: under B or C an agent writing a colour in a skill is told it cited an issue and is pushed to mangle the colour; A removes that misleading red for the letter-tailed shape and keeps the all-decimal shape loudly red. No expansion: no new gate, about 10 lines in the existing one, scoped to the root the ruling added"
},
{
"question": "File surface. The claim named only scripts/pm/check-skill-id-lint.mjs. The PR also edits scripts/pm/bare-root-worklist.mjs (not frozen): one SPELLINGS entry and one DECLARED-NARROWER TRIAGE row, because its self-test refuses the new bare-root constant as FRESH until a verdict is recorded. Accept, or switch to the one-file alternative?",
"options": [
"A (taken): precise declaration skills//.md (53 of 65 files, set-equal to the walk) plus the DECLARED-NARROWER row",
"B: declare skills/** wholesale, drop the worklist edit; one file, but the gate is named for 12 eval JSON files it never opens"
],
"recommendation": "A. Its self-test pins the row LIVE, PRECISE, COMPLETE and set-equal to the gate array, and the worklist own refusal text calls B "the costlier error""
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · The comment above the "PM skill issue-ID lint" step in .github/workflows/lint.yml (around line 951) describes the scan set as the pm-dispatch skill plus os-dev.md and mentions a self-expiring legacy waiver that no longer exists; it already omitted AGENTS.md and now also omits skills/. The comment defers to the script header as authoritative. Comment drift, no filing class; noted in the PR Acceptance notes, not filed.",
"carrier: PM (dispatch template) · noted, not filed · The dispatch measuring command git ls-files skills/**/.md undercounts by one (52 vs 53), because git default pathspec needs a separator after skills/ and so misses skills/README.md; git ls-files skills piped to grep -c .md is the count the walker matches. Separately, the card "0 files" reading at 246edf5 does not reproduce: the same command answers 2 files at that commit."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
ACCEPT — PR #23002 (head
6744bf9bc, 2 files +185/−40, draft, not governed:scripts/pm/check-skill-id-lint.mjs,scripts/pm/bare-root-worklist.mjs) · reviewed 2026-10-12T09:14Z bysession_01CinRQcE6T8SvMbhH8D3NhS(skills seat 1).Checklist, verified on GitHub against the diff, not against the report:
main, first lineFixes #22983,Clause-②: noat the head of a body line, PR assignee = the card's; the only closing keyword is line 1;skip-changesetapplied (scripts/pm/**ships in no package); no HTML comment; session-URL footer.scripts/pm/bare-root-worklist.mjs(+21: oneSPELLINGSentry and oneDECLARED-NARROWERtriage row), because that tool's own self-test refuses a new bare-root constant as FRESH until a verdict is recorded. Accepted (open question 2 → A): the declarationskills/**/*.mdis set-equal to the walk (53 of 65 tracked files, measured both directions) and the one-file alternative would name the gate for 12 eval JSON files it never opens — the costlier error by that tool's own text. Neither file is frozen;dispatch-gates*.mjsuntouched.check-governed-merges --pr 23002: NOT governed, 225 changed lines ≤ 3000.PUBLISHED_SCAN_ROOT = 'skills'walked with the same walker under the same cannot-read-is-red rule;ROOT_DIR_WATCH_HINTS = ['skills/**/*.md']sodispatch-gatesderives the gate for askills/**card (measured: 30 → 31 commands, the one added linepnpm check:pm-skill-id-lint);runsplit intolintTree(root)+ a printer so the self-test drives the real walk on fixture trees;ID_PATTERN,LEGACY_EXACT,verdictForand the internal corpus semantics unchanged. 87 files clean (34 + 53).git grep -c -E '#[0-9]{3,}' origin/main -- 'skills/**'answers 0 files」 was the seat's, and it was wrong — the same command onorigin/mainanswers 2 files (skills/objectstack-data/rules/field-types.md:86color: '#3498db',skills/objectstack-ui/rules/navigation.md:20primaryColor: '#4169E1'), both CSS colours, no citation; the seat's instrument misfired (a zero with no positive control, the exact shape the reading discipline forbids). The dev measured it atBASEand at the card's commit and did not build on the false premise.skills/only, a match whose digit run continues into a letter or underscore is not counted (NOT_A_CITATION_TAIL); an all-decimal colour (#000000) stays red and the header names it as the remaining boundary, pinned red by a self-test row; the internal corpus keeps the bare pattern. Four axes, briefly: the two colours are real customer teaching content, so the bare pattern's red on them is a false positive on correct metadata (business); a citation is a number with a non-word neighbour — the rule follows the thing, not a per-file waiver (long-term); a red that tells an agent its colour is an issue id pushes it to mangle the colour (prevent AI errors); about ten lines inside the existing gate, scoped to the root the ruling added, no new gate (focus). Options B (edit the two skill files — Tier H, outside scope) and C (misuseLEGACY_EXACT) refused.6744bf9bc: 30 derived families,--ran30/30, all exit 0 (watch-hint-literal73 declarations;check:pm-dispatch-gates1828 cases;bare-root-worklist --self-testOK after the row;nul-bytes); narrowed eslint on the two files with population and invariance stated. Check-runs at review: 19 success / 11 skipped / 3 in progress, 0 red.Clause-②: yes, notpackages/spec, not governed text) — the seat's own read suffices, no record.Acceptance notes carried, not filed: the
lint.ymlcomment above the id-lint step (around line 951) describes a scan set and a waiver that no longer exist (it defers to the script header, which is current); the dispatch's counting commandgit ls-files 'skills/**/*.md'undercounts by one (52 vs 53 — git's default pathspec missesskills/README.md); both are the PM's lesson, no action owed on this card.Landing: not governed → the seat lands it. When every check-run is success or an expected skip (
check-expected-skips --head), relaypr_ready+automerge_enable; MERGED confirmed onorigin/main.Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Landed: PR #23002 →
4e6b27099(squash) · skills seat 1 (session_01CinRQcE6T8SvMbhH8D3NhS) · 2026-10-12T10:02ZThe id-lint gate now walks the published catalog:
PUBLISHED_SCAN_ROOT = 'skills'beside the internal corpus,NOT_A_CITATION_TAILunder that root only (a digit run that continues into a letter or underscore is a colour, not a citation),ROOT_DIR_WATCH_HINTS = ['skills/**/*.md']sodispatch-gatesderivespnpm check:pm-skill-id-lintfor askills/**card. This is the 「不加,写成文规则」 half of today's ruling made mechanical on the objectstack side: no new gate, the existing one widened.Content read on
origin/main:scripts/pm/check-skill-id-lint.mjscarriesPUBLISHED_SCAN_ROOT,NOT_A_CITATION_TAILandROOT_DIR_WATCH_HINTS; the gate run by the seat on an export oforigin/mainanswers 「87 file(s) clean」 (34 internal + 53 published), exit 0. Landed by the seat through the merge queue (relaypr_ready+automerge_enable, queue merge). The card closed byFixes;pm:dispatchedstripped in this act.Generated by Claude Code