bmad-module-skill-forge 2.0.2 → 2.2.0
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +3 -1
- package/docs/404.md +4 -4
- package/docs/_data/pinned.yaml +1 -1
- package/docs/_internal/RELEASING.md +41 -4
- package/docs/agents.md +2 -2
- package/docs/architecture.md +1 -1
- package/docs/bmad-synergy.md +13 -13
- package/docs/campaign.md +3 -3
- package/docs/concepts.md +3 -3
- package/docs/examples.md +9 -9
- package/docs/forge-auto.md +6 -6
- package/docs/getting-started.md +12 -12
- package/docs/how-it-works.md +7 -7
- package/docs/index.md +7 -1
- package/docs/skill-model.md +2 -2
- package/docs/troubleshooting.md +7 -7
- package/docs/why-skf.md +8 -8
- package/docs/workflows.md +5 -5
- package/package.json +7 -3
- package/src/shared/references/description-guard-protocol.md +2 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +6 -0
- package/src/shared/scripts/skf-description-guard.py +22 -2
- package/src/shared/scripts/skf-detect-docs.py +198 -2
- package/src/shared/scripts/skf-detect-language.py +20 -0
- package/src/shared/scripts/skf-detect-tools.py +40 -0
- package/src/shared/scripts/skf-emit-result-envelope.py +20 -0
- package/src/shared/scripts/skf-language-corpora.py +20 -0
- package/src/shared/scripts/skf-merge-doc-urls.py +20 -0
- package/src/shared/scripts/skf-qmd-classify-collections.py +44 -2
- package/src/shared/scripts/skf-recommend-scope-type.py +20 -0
- package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
- package/src/shared/scripts/skf-shape-detect.py +20 -0
- package/src/skf-analyze-source/assets/skill-brief-schema.md +2 -1
- package/src/skf-analyze-source/references/recommend.md +1 -1
- package/src/skf-audit-skill/references/re-index.md +1 -1
- package/src/skf-brief-skill/assets/description-voice-examples.md +5 -3
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
- package/src/skf-brief-skill/references/analyze-target.md +2 -2
- package/src/skf-brief-skill/references/gather-intent.md +4 -2
- package/src/skf-campaign/references/step-05-skill-loop.md +3 -1
- package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -2
- package/src/skf-create-skill/assets/skill-sections.md +3 -2
- package/src/skf-create-skill/references/compile.md +12 -3
- package/src/skf-create-skill/references/extract.md +1 -1
- package/src/skf-create-skill/references/generate-artifacts.md +5 -3
- package/src/skf-create-skill/references/step-doc-rot.md +23 -6
- package/src/skf-create-skill/references/step-doc-sources.md +31 -3
- package/src/skf-create-skill/references/sub/fetch-docs.md +4 -2
- package/src/skf-create-skill/references/validate.md +2 -2
- package/src/skf-create-skill/scripts/scan-doc-rot.py +191 -27
- package/src/skf-create-stack-skill/references/detect-integrations.md +1 -1
- package/src/skf-setup/references/ccc-index.md +5 -4
- package/src/skf-setup/references/detect-and-tier.md +1 -0
- package/src/skf-setup/references/report.md +1 -1
- package/src/skf-test-skill/references/coverage-check.md +11 -5
- package/src/skf-test-skill/references/score.md +7 -1
- package/src/skf-test-skill/scripts/aggregate-coherence.py +13 -3
- package/src/skf-test-skill/scripts/compute-score.py +17 -4
- package/src/skf-test-skill/scripts/reconcile-coverage.py +36 -9
- package/src/skf-update-skill/references/write.md +2 -2
- package/tools/cli/lib/installer.js +31 -1
- package/tools/validate-docs-links.js +665 -0
|
@@ -496,7 +496,27 @@ def cmd_emit_blocked() -> None:
|
|
|
496
496
|
print(emit_envelope_line(envelope))
|
|
497
497
|
|
|
498
498
|
|
|
499
|
+
def _force_utf8(*streams) -> None:
|
|
500
|
+
"""Reconfigure JSON-carrying streams to UTF-8 (issue #465).
|
|
501
|
+
|
|
502
|
+
A default Windows console decodes stdio as cp1252, which cannot carry
|
|
503
|
+
non-ASCII JSON (ensure_ascii=False output, raw UTF-8 input). Preserves
|
|
504
|
+
each stream's existing error handler — reconfigure(encoding=...) alone
|
|
505
|
+
would reset it to 'strict', downgrading e.g. an already-UTF-8 stderr on
|
|
506
|
+
Linux. For stdin this must run before the first read. Skips in-process
|
|
507
|
+
test doubles without reconfigure().
|
|
508
|
+
"""
|
|
509
|
+
for stream in streams:
|
|
510
|
+
if hasattr(stream, "reconfigure"):
|
|
511
|
+
errors = getattr(stream, "errors", None)
|
|
512
|
+
if errors is None:
|
|
513
|
+
stream.reconfigure(encoding="utf-8")
|
|
514
|
+
else:
|
|
515
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
516
|
+
|
|
517
|
+
|
|
499
518
|
def main() -> None:
|
|
519
|
+
_force_utf8(sys.stdin, sys.stdout)
|
|
500
520
|
parser = argparse.ArgumentParser(
|
|
501
521
|
description=__doc__,
|
|
502
522
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
@@ -80,7 +80,27 @@ def corpora_for(language: str) -> list[dict]:
|
|
|
80
80
|
return out
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def _force_utf8(*streams) -> None:
|
|
84
|
+
"""Reconfigure JSON-carrying streams to UTF-8 (issue #465).
|
|
85
|
+
|
|
86
|
+
A default Windows console decodes stdio as cp1252, which cannot carry
|
|
87
|
+
non-ASCII JSON (ensure_ascii=False output, raw UTF-8 input). Preserves
|
|
88
|
+
each stream's existing error handler — reconfigure(encoding=...) alone
|
|
89
|
+
would reset it to 'strict', downgrading e.g. an already-UTF-8 stderr on
|
|
90
|
+
Linux. For stdin this must run before the first read. Skips in-process
|
|
91
|
+
test doubles without reconfigure().
|
|
92
|
+
"""
|
|
93
|
+
for stream in streams:
|
|
94
|
+
if hasattr(stream, "reconfigure"):
|
|
95
|
+
errors = getattr(stream, "errors", None)
|
|
96
|
+
if errors is None:
|
|
97
|
+
stream.reconfigure(encoding="utf-8")
|
|
98
|
+
else:
|
|
99
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
100
|
+
|
|
101
|
+
|
|
83
102
|
def main(argv: list[str]) -> int:
|
|
103
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
84
104
|
parser = argparse.ArgumentParser(
|
|
85
105
|
description="Look up canonical companion prose corpora for a language.",
|
|
86
106
|
)
|
|
@@ -213,7 +213,27 @@ def merge_doc_urls(
|
|
|
213
213
|
return kept, suppressed
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def _force_utf8(*streams) -> None:
|
|
217
|
+
"""Reconfigure JSON-carrying streams to UTF-8 (issue #465).
|
|
218
|
+
|
|
219
|
+
A default Windows console decodes stdio as cp1252, which cannot carry
|
|
220
|
+
non-ASCII JSON (ensure_ascii=False output, raw UTF-8 input). Preserves
|
|
221
|
+
each stream's existing error handler — reconfigure(encoding=...) alone
|
|
222
|
+
would reset it to 'strict', downgrading e.g. an already-UTF-8 stderr on
|
|
223
|
+
Linux. For stdin this must run before the first read. Skips in-process
|
|
224
|
+
test doubles without reconfigure().
|
|
225
|
+
"""
|
|
226
|
+
for stream in streams:
|
|
227
|
+
if hasattr(stream, "reconfigure"):
|
|
228
|
+
errors = getattr(stream, "errors", None)
|
|
229
|
+
if errors is None:
|
|
230
|
+
stream.reconfigure(encoding="utf-8")
|
|
231
|
+
else:
|
|
232
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
233
|
+
|
|
234
|
+
|
|
216
235
|
def main() -> int:
|
|
236
|
+
_force_utf8(sys.stdin, sys.stdout)
|
|
217
237
|
raw = sys.stdin.read()
|
|
218
238
|
if not raw or not raw.strip():
|
|
219
239
|
sys.stderr.write("skf-merge-doc-urls: empty stdin (expected JSON object)\n")
|
|
@@ -79,7 +79,9 @@ from __future__ import annotations
|
|
|
79
79
|
|
|
80
80
|
import argparse
|
|
81
81
|
import json
|
|
82
|
+
import os
|
|
82
83
|
import re
|
|
84
|
+
import shutil
|
|
83
85
|
import sys
|
|
84
86
|
from pathlib import Path
|
|
85
87
|
|
|
@@ -92,6 +94,10 @@ FOREIGN_SAMPLE_CAP = 5
|
|
|
92
94
|
# Header line emitted by newer qmd builds, e.g. "Collections (56):".
|
|
93
95
|
_QMD_HEADER_RE = re.compile(r"^Collections \(\d+\):\s*$")
|
|
94
96
|
|
|
97
|
+
# Empty-state message from `qmd collection list`, e.g.
|
|
98
|
+
# "No collections found. Run 'qmd collection add .' to create one."
|
|
99
|
+
_QMD_EMPTY_STATE_PREFIX = "No collections found"
|
|
100
|
+
|
|
95
101
|
|
|
96
102
|
def _die(code: int, message: str) -> None:
|
|
97
103
|
print(json.dumps({"status": "error", "message": message}), file=sys.stderr)
|
|
@@ -199,7 +205,12 @@ def parse_collection_list_output(raw: str) -> list[str]:
|
|
|
199
205
|
the trailing ` (qmd://name/)` URI and is a no-op for the bare-name form.
|
|
200
206
|
Without this, suffixed entries fail the `is_forge_owned` suffix check and
|
|
201
207
|
every forge collection is mis-classified as foreign.
|
|
208
|
+
|
|
209
|
+
The empty-state message ("No collections found. …") is recognized before
|
|
210
|
+
per-line tokenizing — it would otherwise parse as a collection named "No".
|
|
202
211
|
"""
|
|
212
|
+
if raw.lstrip().startswith(_QMD_EMPTY_STATE_PREFIX):
|
|
213
|
+
return []
|
|
203
214
|
names: list[str] = []
|
|
204
215
|
for line in raw.splitlines():
|
|
205
216
|
if not line.strip():
|
|
@@ -212,6 +223,29 @@ def parse_collection_list_output(raw: str) -> list[str]:
|
|
|
212
223
|
return names
|
|
213
224
|
|
|
214
225
|
|
|
226
|
+
def _resolve_outside_cwd(command: str) -> str | None:
|
|
227
|
+
"""shutil.which with a CWD-shim guard. Returns the resolved path or None.
|
|
228
|
+
|
|
229
|
+
shutil.which on Windows searches the current directory ahead of PATH,
|
|
230
|
+
and CWD here is the repo under analysis — a bare-name lookup resolving
|
|
231
|
+
into CWD would execute a repo-planted shim (e.g. qmd.cmd). Such a
|
|
232
|
+
resolution is treated as not-found. Explicit paths supplied by callers
|
|
233
|
+
(containing a separator) are honored as-is. Keep identical to the
|
|
234
|
+
sibling guard in skf-detect-tools.py.
|
|
235
|
+
"""
|
|
236
|
+
resolved = shutil.which(command)
|
|
237
|
+
if resolved is None:
|
|
238
|
+
return None
|
|
239
|
+
if os.sep in command or (os.altsep and os.altsep in command):
|
|
240
|
+
return resolved
|
|
241
|
+
resolved_dir = os.path.dirname(resolved)
|
|
242
|
+
if resolved_dir:
|
|
243
|
+
cwd = os.path.normcase(os.path.abspath(os.getcwd()))
|
|
244
|
+
if os.path.normcase(os.path.abspath(resolved_dir)) == cwd:
|
|
245
|
+
return None
|
|
246
|
+
return resolved
|
|
247
|
+
|
|
248
|
+
|
|
215
249
|
def fetch_live_names_from_qmd() -> tuple[list[str], str | None]:
|
|
216
250
|
"""Invoke `qmd collection list` and return (names, error).
|
|
217
251
|
|
|
@@ -221,11 +255,19 @@ def fetch_live_names_from_qmd() -> tuple[list[str], str | None]:
|
|
|
221
255
|
counts as a "live collection name".
|
|
222
256
|
"""
|
|
223
257
|
import subprocess
|
|
258
|
+
# Resolve before spawning: on Windows qmd ships from npm as a .CMD
|
|
259
|
+
# shim, which a bare-name subprocess.run cannot launch (WinError 2).
|
|
260
|
+
qmd = _resolve_outside_cwd("qmd")
|
|
261
|
+
if qmd is None:
|
|
262
|
+
return [], "qmd not found on PATH"
|
|
224
263
|
try:
|
|
264
|
+
# qmd emits UTF-8; a locale-default decode (cp1252 on Windows)
|
|
265
|
+
# mojibakes names or raises UnicodeDecodeError on unmapped bytes.
|
|
225
266
|
result = subprocess.run(
|
|
226
|
-
[
|
|
267
|
+
[qmd, "collection", "list"],
|
|
227
268
|
capture_output=True,
|
|
228
|
-
|
|
269
|
+
encoding="utf-8",
|
|
270
|
+
errors="replace",
|
|
229
271
|
timeout=30,
|
|
230
272
|
check=False,
|
|
231
273
|
)
|
|
@@ -356,7 +356,27 @@ def _parse_argv(argv: list[str]) -> dict:
|
|
|
356
356
|
return payload
|
|
357
357
|
|
|
358
358
|
|
|
359
|
+
def _force_utf8(*streams) -> None:
|
|
360
|
+
"""Reconfigure JSON-carrying streams to UTF-8 (issue #465).
|
|
361
|
+
|
|
362
|
+
A default Windows console decodes stdio as cp1252, which cannot carry
|
|
363
|
+
non-ASCII JSON (ensure_ascii=False output, raw UTF-8 input). Preserves
|
|
364
|
+
each stream's existing error handler — reconfigure(encoding=...) alone
|
|
365
|
+
would reset it to 'strict', downgrading e.g. an already-UTF-8 stderr on
|
|
366
|
+
Linux. For stdin this must run before the first read. Skips in-process
|
|
367
|
+
test doubles without reconfigure().
|
|
368
|
+
"""
|
|
369
|
+
for stream in streams:
|
|
370
|
+
if hasattr(stream, "reconfigure"):
|
|
371
|
+
errors = getattr(stream, "errors", None)
|
|
372
|
+
if errors is None:
|
|
373
|
+
stream.reconfigure(encoding="utf-8")
|
|
374
|
+
else:
|
|
375
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
376
|
+
|
|
377
|
+
|
|
359
378
|
def main(argv: list[str]) -> int:
|
|
379
|
+
_force_utf8(sys.stdin, sys.stdout)
|
|
360
380
|
payload = _parse_argv(argv)
|
|
361
381
|
result = recommend(payload)
|
|
362
382
|
json.dump(result, sys.stdout, ensure_ascii=False)
|
|
@@ -64,7 +64,7 @@ Judgment payload (stdin JSON, compute mode)::
|
|
|
64
64
|
{
|
|
65
65
|
"exports_public_api": 40, # int — public-entry-point export count (judgment)
|
|
66
66
|
"exports_internal": 19, # int — non-underscore internal export count (judgment)
|
|
67
|
-
"effective_denominator": 12, # int|null — optional,
|
|
67
|
+
"effective_denominator": 12, # int|null — optional, curated-subset scopes only
|
|
68
68
|
"pattern_surfaces_documented": 8, # int — reference-app only (exports_documented proxy)
|
|
69
69
|
"exports_documented": 0, # int — stack only (own-barrel count, usually 0)
|
|
70
70
|
"scripts": [ ... ], # list — scripts[] array (or "scripts_count": int)
|
|
@@ -1075,7 +1075,27 @@ def path_to_manifest_name(ecosystem: str) -> str:
|
|
|
1075
1075
|
# CLI wiring
|
|
1076
1076
|
# ---------------------------------------------------------------------------
|
|
1077
1077
|
|
|
1078
|
+
def _force_utf8(*streams) -> None:
|
|
1079
|
+
"""Reconfigure JSON-carrying streams to UTF-8 (issue #465).
|
|
1080
|
+
|
|
1081
|
+
A default Windows console decodes stdio as cp1252, which cannot carry
|
|
1082
|
+
non-ASCII JSON (ensure_ascii=False output, raw UTF-8 input). Preserves
|
|
1083
|
+
each stream's existing error handler — reconfigure(encoding=...) alone
|
|
1084
|
+
would reset it to 'strict', downgrading e.g. an already-UTF-8 stderr on
|
|
1085
|
+
Linux. For stdin this must run before the first read. Skips in-process
|
|
1086
|
+
test doubles without reconfigure().
|
|
1087
|
+
"""
|
|
1088
|
+
for stream in streams:
|
|
1089
|
+
if hasattr(stream, "reconfigure"):
|
|
1090
|
+
errors = getattr(stream, "errors", None)
|
|
1091
|
+
if errors is None:
|
|
1092
|
+
stream.reconfigure(encoding="utf-8")
|
|
1093
|
+
else:
|
|
1094
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
1095
|
+
|
|
1096
|
+
|
|
1078
1097
|
def main(argv: list[str]) -> int:
|
|
1098
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
1079
1099
|
parser = argparse.ArgumentParser(
|
|
1080
1100
|
description="Classify a repo into a known skill shape from its manifest files.",
|
|
1081
1101
|
)
|
|
@@ -13,7 +13,7 @@ Defines the output contract for skill-brief.yaml files generated by analyze-sour
|
|
|
13
13
|
| source_repo | string | GitHub URL or local path | Repository or project root (optional when `source_type: "docs-only"`) |
|
|
14
14
|
| language | string | Recognized language | Primary programming language |
|
|
15
15
|
| scope | object | See Scope Object below | Boundary definition |
|
|
16
|
-
| description | string | 1-3 sentences | What the skill covers
|
|
16
|
+
| description | string | 1-3 sentences | What the skill covers. Must contain a literal `Use when` clause — validators test for that exact phrase. |
|
|
17
17
|
| forge_tier | string | `Quick` / `Forge` / `Forge+` / `Deep` | Inherited from forge-tier.yaml (Title Case) |
|
|
18
18
|
| created | string | ISO date `YYYY-MM-DD` | Generation date |
|
|
19
19
|
| created_by | string | user_name from config | Who generated the brief |
|
|
@@ -31,6 +31,7 @@ Defines the output contract for skill-brief.yaml files generated by analyze-sour
|
|
|
31
31
|
| `constituent_refs` | object | map of path→ref strings | Optional. Per-constituent git ref overrides for composite (multi-repo or multi-ref) sources. Keys must match entries in the analyze-source `project_paths` array. When absent, a single `target_ref` (or auto-detected ref) applies to all paths. Mutually exclusive with `target_ref`. |
|
|
32
32
|
| `source_authority` | string | `official` / `community` / `internal` | Default `community`. Set to `official` only when the skill creator is the library maintainer. Forced to `community` when `source_type: "docs-only"`. |
|
|
33
33
|
| `source_ref` | string | Git ref (tag/branch/HEAD) | Resolved git ref used for source access. Set automatically during tag resolution — do not set manually. |
|
|
34
|
+
| `scope.tier_a_include` | array | Glob patterns (min 1, non-empty) | Optional narrower tier-A include list for a curated-subset skill — stratified monorepo packages, single-crate `specific-modules` scopes, and multi-entry exports-map packages. When present, `skf-test-skill` re-derives the coverage denominator from this list instead of the coarse `scope.include`. **It also exempts the brief from the denominator deflation guard**, so supply it only when it genuinely narrows the surface — never as an empty list, and never as a way to widen. See `skf-test-skill/references/source-access-protocol.md`. |
|
|
34
35
|
|
|
35
36
|
When `source_type: "docs-only"`:
|
|
36
37
|
- `source_repo` becomes optional (set to doc site URL for reference)
|
|
@@ -59,7 +59,7 @@ For each qualifying unit, prepare a recommendation card:
|
|
|
59
59
|
- name: {suggested kebab-case name}
|
|
60
60
|
- scope.type: {full-library / specific-modules / public-api / component-library / reference-app / docs-only}
|
|
61
61
|
- scope.include: {suggested glob patterns}
|
|
62
|
-
- description: {suggested 1-3 sentence description}
|
|
62
|
+
- description: {suggested 1-3 sentence description, containing a literal `Use when` clause}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
### 3. Present Recommendations
|
|
@@ -143,7 +143,7 @@ Skip this section. Temporal context requires Deep tier.
|
|
|
143
143
|
|
|
144
144
|
For each export in the skill baseline that was NOT found at its recorded file path during re-extraction (potential "deleted" export):
|
|
145
145
|
|
|
146
|
-
1. Run `ccc_bridge.search("{export_name}", source_root, top_k=5)` — **Tool resolution:** Use `/ccc` skill search (Claude Code), ccc MCP server (Cursor), or `ccc search "{export_name}"
|
|
146
|
+
1. Run `ccc_bridge.search("{export_name}", source_root, top_k=5)` — **Tool resolution:** Use `/ccc` skill search (Claude Code), ccc MCP server (Cursor), or `cd {source_root} && ccc search --limit 5 "{export_name}"` (CLI) — to find candidate current locations. `ccc search` reads the index in the current working directory and has no project-selector flag (`--path` is a file-path glob filter *within* the index, and the result cap is `--limit`, not `--top`) — see `knowledge/ccc-bridge.md`.
|
|
147
147
|
2. If CCC returns files containing the export name:
|
|
148
148
|
- Run ast-grep verification on each candidate file
|
|
149
149
|
- If verified at a new location: reclassify from "deleted" to "moved" with the new file:line reference
|
|
@@ -6,9 +6,9 @@ Loaded by step 1 §7b only. The five examples below show the *range* of acceptab
|
|
|
6
6
|
|
|
7
7
|
> Render Markdown to HTML using the marked library. Use when the user pastes raw Markdown and wants formatted output, or asks how to convert MD files in a build pipeline.
|
|
8
8
|
|
|
9
|
-
> Stripe API client for Node.js — payment intents, subscriptions, customer portal, webhooks.
|
|
9
|
+
> Stripe API client for Node.js — payment intents, subscriptions, customer portal, webhooks. Use when the task involves Stripe-managed payments, subscription billing, or webhook event handling.
|
|
10
10
|
|
|
11
|
-
> Charts and visualizations powered by D3.js.
|
|
11
|
+
> Charts and visualizations powered by D3.js. Use when the user asks to plot data, build interactive graphs, or wants bare D3 control instead of a React-charts abstraction.
|
|
12
12
|
|
|
13
13
|
> Lint Python code with Ruff. Use when the user wants to add or configure Ruff in a Python project, debug rule selectors, or understand why a specific check fired.
|
|
14
14
|
|
|
@@ -16,4 +16,6 @@ Loaded by step 1 §7b only. The five examples below show the *range* of acceptab
|
|
|
16
16
|
|
|
17
17
|
## Notes on Voice
|
|
18
18
|
|
|
19
|
-
Each example leads differently (verb / noun / "Charts and..." / verb / noun-phrase)
|
|
19
|
+
Each example leads differently (verb / noun / "Charts and..." / verb / noun-phrase) rather than copy-pasting a single template. Compose in that spirit using the gathered material — the target repo, the user's intent, the version if set, and any scope hints — but **do not template-stamp**.
|
|
20
|
+
|
|
21
|
+
The **lead** is where the voice varies; the **trigger clause** is fixed. Every description must contain a literal `Use when` clause somewhere, because validators test for that exact phrase — `skill-check`'s `description.use_when_phrase` rule scores a description below 100 without it. Phrasings like "Triggers on…" or "Reach for this when…" read well but do not satisfy it on their own, so keep them for additional text rather than as the only trigger.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
| source_repo | string | GitHub URL or local path | Repository or project root (optional when `source_type: "docs-only"`) |
|
|
10
10
|
| language | string | Recognized language | Primary programming language |
|
|
11
11
|
| scope | object | See Scope Object below | Boundary definition |
|
|
12
|
-
| description | string | 1-3 sentences | What the skill covers
|
|
12
|
+
| description | string | 1-3 sentences | What the skill covers. Must contain a literal `Use when` clause — validators test for that exact phrase. |
|
|
13
13
|
| forge_tier | string | `Quick` / `Forge` / `Forge+` / `Deep` | Inherited from forge-tier.yaml (Title Case) |
|
|
14
14
|
| created | string | ISO date `YYYY-MM-DD` | Generation date |
|
|
15
15
|
| created_by | string | user_name from config | Who generated the brief |
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
| `target_ref` | string | Git ref (tag or branch) | Optional. Explicit git ref used verbatim as the resolved `source_ref`, bypassing version-to-tag matching. Escape hatch for monorepo crate tags whose prefix differs from the skill name (e.g. tag `livekit/v0.7.42` for skill `livekit-rust`). Remote sources only. |
|
|
27
27
|
| `source_authority` | string | `official` / `community` / `internal` | Default `community`. Set to `official` only when the skill creator is the library maintainer. Forced to `community` when `source_type: "docs-only"`. |
|
|
28
28
|
| `source_ref` | string | Git ref (tag/branch/HEAD) | Resolved git ref used for source access. Set automatically during tag resolution — do not set manually. |
|
|
29
|
-
| `scope.tier_a_include` | array | Glob patterns
|
|
29
|
+
| `scope.tier_a_include` | array | Glob patterns (min 1, non-empty) | Optional. Narrower tier-A include list for a curated-subset skill — stratified monorepo packages, single-crate `specific-modules` scopes, and multi-entry exports-map packages. When present, `skf-test-skill` re-derives the coverage denominator from this list instead of the coarse `scope.include`, so the denominator reflects the authoring surface rather than incidentally-matched internal infrastructure. **It also exempts the brief from the denominator deflation guard**, so supply it only when it genuinely narrows the surface — never as an empty list, and never as a way to widen (listing files outside `scope.include` inverts its meaning). See `skf-test-skill/references/source-access-protocol.md` stratified-scope resolution. |
|
|
30
30
|
|
|
31
31
|
When `source_type: "docs-only"`:
|
|
32
32
|
- `source_repo` becomes optional (set to doc site URL for reference)
|
|
@@ -206,9 +206,9 @@ Fall back to ad-hoc inspection — `Gemfile` / `*.csproj` / `*.sln` / `Package.s
|
|
|
206
206
|
If `tools.ccc` is true in forge-tier.yaml, supplement the module listing with a semantic discovery pass:
|
|
207
207
|
|
|
208
208
|
**CCC Semantic Discovery:**
|
|
209
|
-
- **Claude Code:** Use `/ccc search "{repo_name} public API exports modules" {source_path}`
|
|
209
|
+
- **Claude Code:** Use `/ccc search "{repo_name} public API exports modules"` from `{source_path}` — the query is variadic, so a trailing path is swallowed into the search string rather than selecting a project
|
|
210
210
|
- **Cursor:** Use `ccc` MCP server `search` tool with query `"{repo_name} public API exports modules"` and path `{source_path}`
|
|
211
|
-
- **CLI fallback:** `ccc search "{repo_name} public API exports modules"
|
|
211
|
+
- **CLI fallback:** `cd {source_path} && ccc search --limit 10 "{repo_name} public API exports modules"` — `ccc search` reads the index in the current working directory and has no project-selector flag (`--path` is a file-path glob filter *within* the index)
|
|
212
212
|
|
|
213
213
|
See `knowledge/tool-resolution.md` for full bridge-to-tool mapping.
|
|
214
214
|
|
|
@@ -352,7 +352,9 @@ The schema's `description` field is 1-3 sentences and surfaces in skill registri
|
|
|
352
352
|
|
|
353
353
|
Compose a candidate 1-3 sentence description from the gathered material. **Write like a human library maintainer would** — what does an agent get from this skill, and when should it route here? Two facts must come through (what the skill is, when to use it); everything else is voice. Resist filling in the same skeleton every time.
|
|
354
354
|
|
|
355
|
-
Load `{descriptionVoiceExamplesPath}` for the five voice examples (range of acceptable leads
|
|
355
|
+
Load `{descriptionVoiceExamplesPath}` for the five voice examples (range of acceptable leads and structures) and the "do not template-stamp" guidance, then compose in that spirit. The asset documents what "in that spirit" means; the gathered material to draw on is the target repo, the user's intent, the version if set, and any scope hints.
|
|
356
|
+
|
|
357
|
+
**Whatever lead you choose, the description must contain a literal `Use when` clause somewhere** — validators test for that exact phrase, so alternatives like "Triggers on…" or "Reach for this when…" do not satisfy it on their own. The clause need not lead: a descriptive opener followed by `Use when …` is both good voice and validator-clean.
|
|
356
358
|
|
|
357
359
|
Present:
|
|
358
360
|
|
|
@@ -360,7 +362,7 @@ Present:
|
|
|
360
362
|
|
|
361
363
|
> {synthesized description}
|
|
362
364
|
|
|
363
|
-
This is the text agents read when deciding whether to route to your skill — it sits in the registry row alongside dozens of other skills.
|
|
365
|
+
This is the text agents read when deciding whether to route to your skill — it sits in the registry row alongside dozens of other skills. A specific 'use when…' trigger helps agents match real user requests; generic descriptions blend in and get skipped. Edit, replace, or accept as-is."
|
|
364
366
|
|
|
365
367
|
Wait for user confirmation or alternative.
|
|
366
368
|
|
|
@@ -81,7 +81,9 @@ For each ready Tier A skill:
|
|
|
81
81
|
- `{{directive_content}}` — raw content of the file at `campaign.directive_path`, or "No directive configured" if unset/missing.
|
|
82
82
|
Present the completed kickoff message as the context for the skill's pipeline run.
|
|
83
83
|
- **AN → BS → CS → TS** — standard forge pipeline for this skill. When BS (Brief Synthesis) produces the skill's brief, set `skills[current].brief_path` to the brief path from the BS result envelope so the field the schema declares is populated and available for resume and reporting.
|
|
84
|
-
- **Doc-rot check** —
|
|
84
|
+
- **Doc-rot check** — **record** what CS step 5c already produced; do not re-derive it. Because the pipeline body runs inline (see the §5 note above), that step's context is directly readable here: `corrections_added`, `correction_matches` (the enriched match records — `source`, `pattern`, `category`, `context_line`, `affected`), `corrections_deduped` and `corrections_capped`. Append one `[doc-rot]`-prefixed entry per `correction_matches` record to the skill's `workarounds_applied` array so they survive state write and are available for §6 propagation. When step 5c took its skip branch (`doc_rot_triggered: false`, `corrections_added: 0`), there is nothing to record — append nothing and move on.
|
|
85
|
+
|
|
86
|
+
**Do not hand-grep the feeder artifacts here, and do not re-run the scan.** `skf-create-skill/references/step-doc-rot.md` §2 requires the scan to run in its helper precisely so identical feeders yield identical matches; a second pass in this loop is free to disagree with the corrections CS actually wrote. It would also be self-contaminating: step 5c has already written `## CORRECTION` blocks into the compiled SKILL.md, and the helper's exclusion windows cover only that file's frontmatter and its Migration & Deprecation Warnings section — so re-scanning it would re-match the blocks 5c itself authored and record them as fresh findings.
|
|
85
87
|
3. **Record results:**
|
|
86
88
|
- On success: set `status` to `"completed"`, set `completed_at` to current ISO-8601 with timezone, record `quality_score`. Backup and write state.
|
|
87
89
|
- On failure: set `status` to `"failed"`. Log the failure reason (the sub-skill's `halt_reason`/exit code, or "unparseable result envelope") to the decision log so `campaign status` and the report surface *why* a skill failed without the operator opening sub-skill logs. Backup and write state. Downstream skills whose `depends_on` does NOT include the failed skill continue processing normally; those that DO depend on it are blocked at §4's dependency gate.
|
|
@@ -15,7 +15,7 @@ description: >
|
|
|
15
15
|
**Frontmatter rules:**
|
|
16
16
|
|
|
17
17
|
- `name`: lowercase alphanumeric + hyphens only, must match the skill output directory name. Prefer gerund form (`processing-pdfs`, `analyzing-spreadsheets`) for clarity.
|
|
18
|
-
- `description`: non-empty, max 1024 chars, optimized for agent discovery. Use third-person voice ("Processes Excel files..." not "I can help you..." or "You can use this to...") and include a trigger phrase so agents know when to match
|
|
18
|
+
- `description`: non-empty, max 1024 chars, optimized for agent discovery. Use third-person voice ("Processes Excel files..." not "I can help you..." or "You can use this to...") and include a trigger phrase so agents know when to match. **The description must contain a literal `Use when` clause somewhere** — `skill-check`'s `description.use_when_phrase` rule tests for that exact phrase anywhere in the string, so `Triggers on …` or `Reach for this when …` on their own score below 100 with a `description should contain "Use when" phrasing` warning. The lead is still free: a descriptive opener followed by a `Use when …` clause (`Charts and visualizations powered by D3.js. Use when plotting data …`) passes cleanly, and the other two phrases are fine as *additional* text. When using `Use when `, follow with a **gerund** (`Use when building/processing/analyzing X…`) or a noun-phrase clause (`Use when the user requests to "X"`); never a bare indicative verb like `builds`/`processes`, because `skill-check --fix` will prepend a literal `Use when ` to a description missing the trigger phrase, producing ungrammatical output (`Use when builds X…`). Inconsistent point-of-view or a missing trigger phrase causes discovery problems since the description is injected into the system prompt. See `skf-brief-skill/assets/description-voice-examples.md` for the canonical voice palette.
|
|
19
19
|
- **The `description` cannot contain angle brackets** — neither standalone placeholders like `<name>`, `<component>`, `<path>`, nor inline generics like `` `Array<T>` `` or `` `Meta<typeof X>` ``. Both `skill-check`'s `description_field` validator and `tessl`'s deterministic description check parse the frontmatter description as a raw string and reject any `<` or `>`, regardless of markdown context (backticks do not protect content here). A rejected description fails the review with 0% score. When the natural phrasing would use angle brackets:
|
|
20
20
|
- Prefer the curly-brace form in prose: `{name}`, `{component-id}`, `{path}` — readable and tessl-safe.
|
|
21
21
|
- Uppercase placeholders are also acceptable: `NAME`, `COMPONENT_ID`.
|
|
@@ -62,6 +62,7 @@ These sections form the essential standalone body. Target: **under 300 lines tot
|
|
|
62
62
|
- Provenance citation per function
|
|
63
63
|
|
|
64
64
|
**Section 4b — Migration & Deprecation Warnings (~10 lines, Deep tier only):**
|
|
65
|
+
- Emit the heading verbatim as `## Migration & Deprecation Warnings`. Downstream gates match that exact text, so a naturally-retitled section (`## Deprecations & pre-1.0 hazards`, `## v2 → v3 breaking changes`) is reported as *missing* while its bullets are re-emitted as duplicate `## CORRECTION` blocks.
|
|
65
66
|
- Only populated when step 4 enrichment produced **T2-future** annotations (deprecation warnings, breaking changes, planned renames)
|
|
66
67
|
- List each warning as a single-line bullet: function name, what changed or will change, source citation
|
|
67
68
|
- Max 10 lines — just the actionable warnings, not full context
|
|
@@ -268,7 +269,7 @@ Replace per-function subsections with `references/pattern-*.md` groupings: one r
|
|
|
268
269
|
- `stats.exports_documented` MAY be set to the Pattern Surface row count as a proxy, but its semantics change: it counts authored pattern surfaces, not public exports. Emit an adjacent `stats.pattern_surfaces_documented` with the same integer so downstream consumers (test-skill, feasibility, discovery) can discriminate.
|
|
269
270
|
- `exports_public_api` / `exports_internal` / `exports_total` are not meaningful for a reference app — omit them, or set all three to the Pattern Surface count with a note in `stats.notes`: `"reference-app: export counts are pattern-surface proxies, not library exports"`.
|
|
270
271
|
- Do not fabricate signature / type-coverage data from pattern surfaces. skf-test-skill will skip those categories when metadata flags a reference-app (same skip path as `stackSkill` once that flag is wired — see scoring-rules.md).
|
|
271
|
-
- Do not emit `effective_denominator` for reference-app scope, even when the source is a monorepo. `effective_denominator` counts public exports from the authoring-surface barrel — a library concept that `pattern_surfaces_documented` already replaces here. A reference-app-in-monorepo literally satisfies `compile.md` §4's emit-conditions (
|
|
272
|
+
- Do not emit `effective_denominator` for reference-app scope, even when the source is a monorepo. `effective_denominator` counts public exports from the authoring-surface barrel — a library concept that `pattern_surfaces_documented` already replaces here. A reference-app-in-monorepo literally satisfies `compile.md` §4's emit-conditions (condition 1 arm (a) + non-`full-library` + stratified `scope.notes`), so this carve-out takes precedence: pattern-surface coverage is the reference-app basis, and skf-test-skill skips library-export coverage for it.
|
|
272
273
|
|
|
273
274
|
**Language / spec-reference sub-shape:** A reference app that documents an engine- or spec-versioned **language** — a query language, grammar, or DSL (e.g. SurrealQL @ SurrealDB) whose value is construct idioms rather than wiring or exports — is a recognized reference-app sub-shape. Keep `scope.type: "reference-app"` (there is no separate enum value), so every carve-out above applies unchanged (`pattern_surfaces_documented` proxy, no `effective_denominator`, test-skill signature/type skip). Adapt the three overrides to the language's surface instead of app wiring:
|
|
274
275
|
|
|
@@ -134,13 +134,14 @@ Indexed pipe-delimited format for CLAUDE.md managed section (~80-120 tokens per
|
|
|
134
134
|
"total_coverage": 0.0,
|
|
135
135
|
"scripts_count": 0,
|
|
136
136
|
"assets_count": 0
|
|
137
|
-
// "effective_denominator": 0 // optional — emitted for
|
|
137
|
+
// "effective_denominator": 0 // optional — emitted for the curated-subset shapes in compile.md §4 condition 1; test-skill uses this as the coverage denominator when present. See compile.md §4 emit rules.
|
|
138
138
|
},
|
|
139
139
|
// scripts[] and assets[] — include ONLY when inventories are non-empty; omit entirely otherwise
|
|
140
140
|
// "scripts": [{ "file": "scripts/{name}", "purpose": "{description}", "source_file": "{source-path}", "confidence": "T1-low" }],
|
|
141
141
|
// "assets": [{ "file": "assets/{name}", "purpose": "{description}", "source_file": "{source-path}", "confidence": "T1-low" }],
|
|
142
142
|
// doc_sources[] — include ONLY when doc detection ran; omit if detection was skipped entirely
|
|
143
|
-
// "doc_sources": [{ "url": "https://...", "detected_via": "homepageUrl|readme_link|pages_api|docs_folder|readme_always", "content_hash": "sha256:{hex}|null", "recorded_at": "ISO-8601" }],
|
|
143
|
+
// "doc_sources": [{ "url": "https://...", "detected_via": "homepageUrl|readme_link|pages_api|docs_folder|readme_always|brief_doc_urls", "content_hash": "sha256:{hex}|null", "recorded_at": "ISO-8601" }],
|
|
144
|
+
// detected_via "brief_doc_urls" = docs-only skills: the brief's doc_urls (plus subpages step 3c fetched), hashed by `skf-detect-docs.py hash-urls`
|
|
144
145
|
"generated_by": "{quick-skill|create-skill}",
|
|
145
146
|
"dependencies": [],
|
|
146
147
|
"compatibility": "{semver-range}"
|
|
@@ -111,12 +111,21 @@ Following the structure from the skill-sections data file:
|
|
|
111
111
|
- `exports_public_api`: count of exports from public entry points (`__init__.py`, `index.ts`, `lib.rs`, or equivalent) — derive this from step 3's entry-point validation (section 4b), not from the provenance-map entry count (which may be incomplete if extraction patterns missed some export types)
|
|
112
112
|
- `exports_internal`: count of all other non-underscore-prefixed exports (internal modules, helpers, adapters)
|
|
113
113
|
- `scripts` / `assets`: the `scripts_inventory` / `assets_inventory` arrays (or `[]` when empty) — the helper sets `stats.scripts_count` / `stats.assets_count` from their lengths
|
|
114
|
-
- `effective_denominator` (**optional** — include in the helper's judgment payload only for
|
|
115
|
-
1. The
|
|
114
|
+
- `effective_denominator` (**optional** — include in the helper's judgment payload only for the curated-subset shapes enumerated in condition 1 below): the count of public exports from files matched by the brief's authoring-surface globs, filtered by `scope.exclude`, resolved against `source_path`. **Prefer `scope.tier_a_include` when the brief supplies it** — that narrow list represents the authoring surface the brief intends to document; resolve its globs across `source_path` and count the union of **named exports** — items reachable from the language's public entry-point barrel (`lib.rs` `pub use`, `index.ts` / `index.js` re-exports, `__init__.py` exports), where a type counts **once** with its methods and impl-block members rolling up under it (count a `pub fn` only when it is a free function reachable from the barrel, never a method on an already-counted type). **Otherwise use `scope.include`** — the coarse list. This counting unit matches what `skf-test-skill` re-derives on the consumer side (`coverage-check.md` §2c excludes `kind: "method"` from `documented_set`), so the producer- and test-side denominators stay aligned; without it, a type-heavy API (a few handle types carrying hundreds of methods) inflates the denominator by an order of magnitude and auto-fails the coverage gate. This is the coverage denominator `skf-test-skill` uses when the brief documents a curated subset rather than a package's whole barrel, so it must match the brief's authoring intent. Compute when ALL of the following hold:
|
|
115
|
+
1. The skill's coverage denominator is resolved by a `skf-test-skill` `references/source-access-protocol.md` §Source API Surface Definition clause that actually consumes `effective_denominator` — **any one** of:
|
|
116
|
+
- **(a) stratified-scope:** the source is a monorepo (detected via `packages/` layout, `workspaces` field in root `package.json`, `lerna.json`, `rush.json`, `nx.json`, or Cargo `[workspace]`);
|
|
117
|
+
- **(b) single-crate curated subset:** `scope.type` is `specific-modules` on a non-monorepo repo;
|
|
118
|
+
- **(c) multi-entry (exports-map):** the in-scope `package.json` declares an `exports` map with more than one non-root subpath, and the repo carries no monorepo markers.
|
|
119
|
+
|
|
120
|
+
Arms (b) and (c) are not a widening for its own sake — both consumer clauses already say "prefer `metadata.json.stats.effective_denominator`", and a monorepo-only emit condition makes those two rungs unreachable, so a non-monorepo curated scope has no way to lock its denominator.
|
|
116
121
|
2. `scope.type` is not `full-library` (and not `reference-app` — see carve-out below), AND the resolved include list (`tier_a_include` if present, else `scope.include`) lists a curated file/directory subset rather than the full workspace.
|
|
117
122
|
3. `scope.notes` is present and documents the stratification strategy (e.g., a tiered A/B/C plan) — this serves as the intent marker confirming the subset is by design.
|
|
118
123
|
|
|
119
|
-
Otherwise omit it from the payload entirely — when absent, `skf-test-skill` falls back to `exports_public_api`. See `skf-test-skill` `references/source-access-protocol.md` §Source API Surface Definition ("Stratified-scope monorepo packages") for the test-side consumption rules.
|
|
124
|
+
Otherwise omit it from the payload entirely — when absent, `skf-test-skill` falls back to `exports_public_api`. See `skf-test-skill` `references/source-access-protocol.md` §Source API Surface Definition ("Stratified-scope monorepo packages", "Single-crate curated subset", "Multi-entry (exports-map) packages") for the test-side consumption rules.
|
|
125
|
+
|
|
126
|
+
**A `public-api` scope on a single-package repo with no multi-subpath `exports` map matches none of the three arms** — that shape falls to the standard root-barrel rule, which honors neither `effective_denominator` nor `tier_a_include`. Emitting the field there has no effect on the test-side denominator, so omit it.
|
|
127
|
+
|
|
128
|
+
**That shape has no brief-side denominator lever, and `scope.exclude` is not one.** The root-barrel rule counts the named exports of a single entry-point file (`index.ts` / `__init__.py` / `lib.rs`), so excluding *files* cannot remove exports from the barrel's own export list — the denominator is unchanged. When the brief genuinely documents a curated subset of a single package's surface (citation-only files alongside a narrow `tier_a_include`), express that with `scope.type: "specific-modules"` rather than `public-api`: the single-crate curated-subset clause in `skf-test-skill` `references/source-access-protocol.md` filters the barrel by `scope.include` / `scope.exclude` **and** honors `effective_denominator`, so condition 1 arm (b) applies and the denominator matches the authoring intent. Reserve `public-api` for the case where the whole root barrel is the intended surface.
|
|
120
129
|
|
|
121
130
|
**Reference-app carve-out:** never emit `effective_denominator` for `scope.type: "reference-app"`, even when the three conditions above are literally satisfied (a reference-app-in-monorepo matches all of them). A reference app's coverage basis is `pattern_surfaces_documented`, not library exports — see the Reference-App stats semantics in `assets/compile-assembly-rules.md`.
|
|
122
131
|
- **Shape:** pass `--shape reference-app` or `--shape stack` when the matching assembly-rules override applies (default `library`). For a reference app, also put `pattern_surfaces_documented` (the Pattern Surface row count) in the payload — the helper uses it as `exports_documented`, emits `stats.pattern_surfaces_documented`, and refuses to emit `effective_denominator`; the distribution then sums to the per-citation count, not to `exports_documented`. For a stack, put the own-barrel `exports_documented` (usually `0`) in the payload; the distribution sums to the cited-constituent count.
|
|
@@ -101,7 +101,7 @@ Then run CCC indexing and discovery on the resolved clone (workspace or ephemera
|
|
|
101
101
|
node_modules/, dist/, build/, .git/, vendor/, __pycache__/, .cache/, .next/, .nuxt/, target/, out/, .venv/, .tox/
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
Read `settings.yml`, append any patterns not already present to the `exclude_patterns` array, write back. **Reuse check:** if an existing `.cocoindex_code/settings.yml` was already present (workspace hit), read its `exclude_patterns` first and diff against the standard-exclusion list above. If ANY standard entry is missing from the existing list, append only the missing entries (preserving any user-added patterns) AND force a re-index by running `ccc
|
|
104
|
+
Read `settings.yml`, append any patterns not already present to the `exclude_patterns` array, write back. **Reuse check:** if an existing `.cocoindex_code/settings.yml` was already present (workspace hit), read its `exclude_patterns` first and diff against the standard-exclusion list above. If ANY standard entry is missing from the existing list, append only the missing entries (preserving any user-added patterns) AND force a re-index by running `ccc reset --force` (discards the stale databases) followed by `ccc index` — `ccc index` itself takes no rebuild flag, so re-running it alone would reuse the existing index. If every standard entry is already present, skip the write and skip the forced re-index — the existing index is valid. Record `ccc_exclusions_augmented: {count}` in context for the evidence report.
|
|
105
105
|
|
|
106
106
|
**Note:** Brief-specific `include_patterns` and `exclude_patterns` are not written to `settings.yml`. The CCC index is general-purpose — it indexes everything (minus standard artifacts). Brief-specific filtering happens at search result time, not index time. This allows a single workspace CCC index to serve multiple briefs with different scope filters.
|
|
107
107
|
|
|
@@ -194,11 +194,13 @@ Ensure the source path used for extraction is indexed by ccc and registered in t
|
|
|
194
194
|
|
|
195
195
|
**Index verification:**
|
|
196
196
|
|
|
197
|
-
`ccc index`
|
|
197
|
+
**Working-directory contract:** `ccc init`, `ccc index`, and `ccc status` take **no positional path argument** — each operates on the current working directory, and passing a path makes them exit non-zero with `Got unexpected extra argument`. Run them from `{source_root}` (`cd {source_root} && ccc …`, the same form `extract.md` step 2b and `knowledge/ccc-bridge.md` use), then return to `{project-root}` before the registry update below, which resolves `{forgeTierConfig}` relative to the project.
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
`ccc index` requires the directory to be initialized first. Run `cd {source_root} && ccc init` (idempotent — a no-op once initialized) before `cd {source_root} && ccc index`, or use the ccc MCP tool. `ccc_bridge.ensure_index` is a conceptual interface, not a callable function. Indexing is a no-op if the source was already indexed during setup or step 2b.
|
|
200
200
|
|
|
201
|
-
**
|
|
201
|
+
**Nested project marker:** when `{source_root}` is a subtree of a repo that already carries a project marker (e.g. a cloned source tree under `.forge-sources/`, or a `.cocoindex_code` / VCS marker in a parent), `ccc init` exits non-zero with `A parent directory has a project marker`. This is expected — re-run as `cd {source_root} && ccc init -f` to initialize at the subtree anyway, then `ccc index`. Do not treat the parent-marker warning as fatal.
|
|
202
|
+
|
|
203
|
+
**Verify the index is not degraded:** after `ccc index`, run `cd {source_root} && ccc status` and read the `Languages:` breakdown — a non-zero `Chunks`/`Files` total is not sufficient. Confirm the source's primary language (`{brief.language}`) reports a non-trivial chunk count. An index dominated by `markdown`/config chunks with the source language absent or near-zero means `ccc index` ran against degraded settings (a stale `.cocoindex_code/` inherited from a parent project, or a no-op `ccc init` over a prior partial index that returned `Project already initialized.`) — the source code was never indexed, which silently cripples later `skf-audit-skill` / `skf-update-skill` searches with no error surfaced. When the source language is absent, rebuild from a clean init (`cd {source_root} && ccc init -f` then `ccc index`) and re-check the breakdown. Note that `-f` only skips the parent-marker warning — it does not clear a stale `.cocoindex_code/`; when the degraded settings survive the forced re-init, `ccc reset` (run from `{source_root}`) is what discards the existing databases. If the source language is still missing after a forced re-init, log it and continue per the error handling below — a degraded index is not workflow-fatal, but it must be recorded so the gap is visible.
|
|
202
204
|
|
|
203
205
|
**Registry update:**
|
|
204
206
|
|
|
@@ -29,7 +29,7 @@ Identify the feeder artifacts in the **staging directory** for the current skill
|
|
|
29
29
|
1. **Evidence report:** `_bmad-output/{skill-name}/evidence-report.md`
|
|
30
30
|
2. **Provenance map:** `_bmad-output/{skill-name}/provenance-map.json` — focus on T2/T3 entries with temporal annotations
|
|
31
31
|
3. **Temporal context:** changelogs, migration guides, and issue/PR data fetched by step 3b and enriched by step 4 (available in workflow context)
|
|
32
|
-
4. **Compiled SKILL.md:** the staged `_bmad-output/{skill-name}/SKILL.md` itself — check for `[QMD:...]` or `[DOC:...]` annotations referencing corrections. **Do not treat its own `## Migration & Deprecation Warnings` section (§4b)
|
|
32
|
+
4. **Compiled SKILL.md:** the staged `_bmad-output/{skill-name}/SKILL.md` itself — check for `[QMD:...]` or `[DOC:...]` annotations referencing corrections. **Do not treat its own self-authored regions as correction sources:** compile already wrote the `## Migration & Deprecation Warnings` section (step 5 §4b) and the frontmatter `description` (step 5 §2) from the same T2-future annotations, so both restate already-surfaced corrections — §2 discards matches that land in either.
|
|
33
33
|
|
|
34
34
|
For each artifact, attempt to load its content. If an artifact does not exist or is empty, skip it — this is not an error.
|
|
35
35
|
|
|
@@ -42,6 +42,7 @@ The scan is deterministic plumbing — a fixed-table substring grep with one cor
|
|
|
42
42
|
```bash
|
|
43
43
|
uv run {scanDocRotHelper} \
|
|
44
44
|
--skill-md _bmad-output/{skill-name}/SKILL.md \
|
|
45
|
+
--max-corrections 10 \
|
|
45
46
|
_bmad-output/{skill-name}/evidence-report.md \
|
|
46
47
|
_bmad-output/{skill-name}/provenance-map.json \
|
|
47
48
|
_bmad-output/{skill-name}-temporal/*.md
|
|
@@ -49,13 +50,15 @@ uv run {scanDocRotHelper} \
|
|
|
49
50
|
|
|
50
51
|
(Pass whichever of the §1 feeder paths actually exist — the script skips any that are missing or empty, and reports the loaded set in `scanned`.)
|
|
51
52
|
|
|
52
|
-
The script emits `{scanned: [...], matches: [...], match_count, excluded_count}` on stdout. Each entry in `matches` is a match record with the deterministic fields the script owns:
|
|
53
|
+
The script emits `{scanned: [...], matches: [...], match_count, excluded_count, deduped_count, capped_count, cap}` on stdout. Each entry in `matches` is a match record with the deterministic fields the script owns:
|
|
53
54
|
|
|
54
55
|
- `source`: the feeder artifact path where the match was found
|
|
55
56
|
- `pattern`: the specific pattern string that matched
|
|
56
57
|
- `category`: the category label
|
|
57
58
|
- `context_line`: the line containing the match
|
|
58
|
-
- `line_number`: the 1-indexed line the match sits on
|
|
59
|
+
- `line_number`: the 1-indexed line the match sits on — a representative occurrence when duplicates were collapsed
|
|
60
|
+
- `occurrences`: how many feeder lines collapsed into this record (1 when it is unique)
|
|
61
|
+
- `duplicate_of`: the `{source, line_number}` of every collapsed occurrence (empty when it is unique)
|
|
59
62
|
|
|
60
63
|
Read `matches` into `correction_matches: [{match records}]`. Then add the one judgment field the script cannot infer:
|
|
61
64
|
|
|
@@ -79,7 +82,19 @@ Read `matches` into `correction_matches: [{match records}]`. Then add the one ju
|
|
|
79
82
|
| `migration required` | Migration |
|
|
80
83
|
| `signature changed` | Signature change |
|
|
81
84
|
|
|
82
|
-
**Exclusion — drop
|
|
85
|
+
**Exclusion — drop the compiled SKILL.md's self-authored content (deterministic, no AI judgment, applied inside the script before it emits):** the script discards any match whose `source` is the compiled SKILL.md (feeder #4) **and** whose `line_number` sits in either of two positional windows. `excluded_count` reports the combined total.
|
|
86
|
+
|
|
87
|
+
1. **Its own `## Migration & Deprecation Warnings` section** — a line at or after the heading and before the next `##` heading. Compile (step 5 §4b) authored that section from the same T2-future annotations, so re-emitting its bullets as `## CORRECTION` blocks in §3 would duplicate already-surfaced content verbatim.
|
|
88
|
+
2. **Its own YAML frontmatter** — a line at or inside the leading `---` … `---` fences. Compile (step 5 §2) writes the frontmatter `description` from the same annotations, and it routinely restates the release's headline breaking change, so a match there is already-surfaced too. The frontmatter is a closed key set of pipeline scalars, so it can never carry an upstream correction the body has missed — and §3 forbids annotating it in any case, which would leave such a match with nowhere legitimate to go.
|
|
89
|
+
|
|
90
|
+
Both are positional boundary checks on text already loaded, not semantic assessments. The heading search starts after the frontmatter, so an indented restatement of the heading inside a folded `description:` cannot anchor the window in the wrong place. Set `feeder_artifacts_scanned` from the script's `scanned` list.
|
|
91
|
+
|
|
92
|
+
**Bounding — collapse duplicates, then cap (deterministic, applied inside the script after the exclusions):** the temporal feeder above is a verbatim upstream changelog, so a single run can match hundreds of "breaking"/"deprecated" lines drawn from years of release history. Two bounds keep §3's output proportionate:
|
|
93
|
+
|
|
94
|
+
- **Duplicate collapse.** Matches sharing a `category` and the same whitespace-normalized, lowercased `context_line` collapse into their first occurrence, which carries `occurrences` and `duplicate_of`. `deduped_count` reports how many collapsed. A changelog that restates one deprecation across many releases yields one block, not one per release.
|
|
95
|
+
- **Cap.** At most `--max-corrections` matches survive (default 10; pass `0` for unlimited). `capped_count` reports how many were dropped and `cap` the limit in force. Selection prefers the compiled SKILL.md's own annotations over other feeders, then scan order. Ten blocks at ~7 lines each fits step 5b's 400-line body budget with headroom.
|
|
96
|
+
|
|
97
|
+
Neither bound is a judgment call, and neither is silent — §4 logs both counts.
|
|
83
98
|
|
|
84
99
|
**IF `correction_matches` is empty** (the script returned `match_count: 0`, or the run had no feeder files to pass)**:**
|
|
85
100
|
- Log: `"doc-rot: skipped (no correction indicators found in feeder artifacts)"`
|
|
@@ -90,7 +105,7 @@ Read `matches` into `correction_matches: [{match records}]`. Then add the one ju
|
|
|
90
105
|
|
|
91
106
|
### §3. Annotate SKILL.md with Correction Blocks
|
|
92
107
|
|
|
93
|
-
For each entry in `correction_matches`, insert a `## CORRECTION` block into the compiled SKILL.md.
|
|
108
|
+
For each entry in `correction_matches`, insert a `## CORRECTION` block into the compiled SKILL.md. The list is already collapsed and capped by §2's bounding, so this loop is bounded by construction — do not re-expand it from `duplicate_of`, and do not write the drop counts into the artifact (they belong in the §4 log, not in a third-party skill).
|
|
94
109
|
|
|
95
110
|
**Block format:**
|
|
96
111
|
|
|
@@ -114,11 +129,13 @@ Store: `corrections_added: {count of blocks inserted}`
|
|
|
114
129
|
|
|
115
130
|
### §4. Log Results
|
|
116
131
|
|
|
117
|
-
Log: `"doc-rot: {corrections_added} correction blocks added from {feeder_artifacts_scanned_count} feeder artifacts"`
|
|
132
|
+
Log: `"doc-rot: {corrections_added} correction blocks added from {feeder_artifacts_scanned_count} feeder artifacts ({deduped_count} duplicates collapsed, {capped_count} dropped over cap {cap})"`
|
|
118
133
|
|
|
119
134
|
Set context:
|
|
120
135
|
- `doc_rot_triggered: true`
|
|
121
136
|
- `corrections_added: {count}`
|
|
137
|
+
- `corrections_deduped: {deduped_count}`
|
|
138
|
+
- `corrections_capped: {capped_count}`
|
|
122
139
|
- `feeder_artifacts_scanned: [{list}]`
|
|
123
140
|
- `correction_matches: [{match records}]`
|
|
124
141
|
|