bmad-module-skill-forge 1.8.0 → 2.0.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 +10 -5
- package/docs/_data/pinned.yaml +2 -2
- package/docs/agents.md +11 -2
- package/docs/architecture.md +5 -4
- package/docs/bmad-synergy.md +42 -3
- package/docs/campaign.md +172 -0
- package/docs/examples.md +7 -3
- package/docs/forge-auto.md +90 -0
- package/docs/getting-started.md +9 -0
- package/docs/how-it-works.md +6 -4
- package/docs/index.md +4 -3
- package/docs/skill-model.md +1 -1
- package/docs/troubleshooting.md +17 -1
- package/docs/verifying-a-skill.md +9 -3
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +73 -27
- package/package.json +2 -2
- package/src/module-help.csv +2 -1
- package/src/shared/_known-workarounds.yaml +155 -0
- package/src/shared/data/language-corpora.json +32 -0
- package/src/shared/references/pipeline-contracts.md +14 -2
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +6 -1
- package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
- package/src/shared/scripts/skf-detect-docs.py +417 -0
- package/src/shared/scripts/skf-detect-language.py +5 -3
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +12 -1
- package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
- package/src/shared/scripts/skf-extract-public-api.py +53 -0
- package/src/shared/scripts/skf-language-corpora.py +100 -0
- package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
- package/src/shared/scripts/skf-preapply.py +192 -0
- package/src/shared/scripts/skf-shape-detect.py +1114 -0
- package/src/shared/scripts/skf-validate-brief-schema.py +2 -5
- package/src/shared/scripts/skf-validate-pins.py +368 -0
- package/src/shared/scripts/skf-write-skill-brief.py +21 -5
- package/src/skf-analyze-source/SKILL.md +26 -20
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/references/init.md +30 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +677 -0
- package/src/skf-analyze-source/references/step-shape-detect.md +86 -0
- package/src/skf-audit-skill/SKILL.md +1 -0
- package/src/skf-audit-skill/assets/drift-report-template.md +6 -0
- package/src/skf-audit-skill/references/init.md +1 -1
- package/src/skf-audit-skill/references/report.md +4 -0
- package/src/skf-audit-skill/references/severity-classify.md +1 -1
- package/src/skf-audit-skill/references/step-doc-drift.md +147 -0
- package/src/skf-brief-skill/SKILL.md +7 -3
- package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
- package/src/skf-brief-skill/references/analyze-target.md +7 -4
- package/src/skf-brief-skill/references/confirm-brief.md +0 -1
- package/src/skf-brief-skill/references/gather-intent.md +22 -2
- package/src/skf-brief-skill/references/scope-definition.md +2 -1
- package/src/skf-brief-skill/references/step-auto-brief.md +188 -0
- package/src/skf-brief-skill/references/step-auto-validate.md +209 -0
- package/src/skf-brief-skill/references/write-brief.md +2 -3
- package/src/skf-campaign/SKILL.md +190 -0
- package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
- package/src/skf-campaign/customize.toml +73 -0
- package/src/skf-campaign/manifest.yaml +11 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +121 -0
- package/src/skf-campaign/references/health-check.md +35 -0
- package/src/skf-campaign/references/step-01-setup.md +122 -0
- package/src/skf-campaign/references/step-02-strategy.md +97 -0
- package/src/skf-campaign/references/step-03-pins.md +56 -0
- package/src/skf-campaign/references/step-04-provenance.md +63 -0
- package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
- package/src/skf-campaign/references/step-06-batch.md +87 -0
- package/src/skf-campaign/references/step-07-capstone.md +63 -0
- package/src/skf-campaign/references/step-08-verify.md +75 -0
- package/src/skf-campaign/references/step-09-refine.md +83 -0
- package/src/skf-campaign/references/step-10-export.md +106 -0
- package/src/skf-campaign/references/step-11-maintenance.md +84 -0
- package/src/skf-campaign/references/step-resume.md +114 -0
- package/src/skf-campaign/scripts/.gitkeep +0 -0
- package/src/skf-campaign/scripts/campaign-deps.py +235 -0
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
- package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
- package/src/skf-campaign/scripts/campaign-report.py +249 -0
- package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
- package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
- package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
- package/src/skf-campaign/templates/campaign-report-template.md +54 -0
- package/src/skf-campaign/templates/kickoff-template.md +48 -0
- package/src/skf-create-skill/SKILL.md +4 -1
- package/src/skf-create-skill/assets/compile-assembly-rules.md +22 -0
- package/src/skf-create-skill/assets/skill-sections.md +2 -0
- package/src/skf-create-skill/references/compile.md +5 -2
- package/src/skf-create-skill/references/extract.md +9 -1
- package/src/skf-create-skill/references/extraction-patterns.md +3 -1
- package/src/skf-create-skill/references/generate-artifacts.md +11 -2
- package/src/skf-create-skill/references/health-check.md +2 -2
- package/src/skf-create-skill/references/report.md +17 -1
- package/src/skf-create-skill/references/source-resolution-protocols.md +1 -1
- package/src/skf-create-skill/references/step-auto-shard.md +110 -0
- package/src/skf-create-skill/references/step-doc-rot.md +109 -0
- package/src/skf-create-skill/references/step-doc-sources.md +123 -0
- package/src/skf-create-skill/references/sub/fetch-docs.md +29 -0
- package/src/skf-create-skill/references/validate.md +12 -3
- package/src/skf-drop-skill/SKILL.md +2 -2
- package/src/skf-drop-skill/references/execute.md +20 -9
- package/src/skf-drop-skill/references/report.md +2 -0
- package/src/skf-drop-skill/references/select.md +7 -2
- package/src/skf-forger/SKILL.md +15 -4
- package/src/skf-refine-architecture/SKILL.md +2 -1
- package/src/skf-refine-architecture/references/compile.md +1 -1
- package/src/skf-refine-architecture/references/report.md +1 -1
- package/src/skf-rename-skill/SKILL.md +2 -2
- package/src/skf-rename-skill/references/execute.md +5 -4
- package/src/skf-rename-skill/references/rebuild-context.md +2 -2
- package/src/skf-rename-skill/references/select.md +3 -3
- package/src/skf-setup/SKILL.md +1 -1
- package/src/skf-setup/references/auto-index.md +3 -1
- package/src/skf-setup/references/detect-and-tier.md +4 -0
- package/src/skf-setup/references/report.md +4 -1
- package/src/skf-setup/references/tier-rules.md +1 -1
- package/src/skf-test-skill/SKILL.md +8 -7
- package/src/skf-test-skill/customize.toml +2 -1
- package/src/skf-test-skill/references/coverage-check.md +10 -0
- package/src/skf-test-skill/references/external-validators.md +1 -1
- package/src/skf-test-skill/references/init.md +16 -1
- package/src/skf-test-skill/references/report.md +5 -1
- package/src/skf-test-skill/references/score.md +95 -6
- package/src/skf-test-skill/references/source-access-protocol.md +13 -3
- package/src/skf-test-skill/references/step-hard-gate.md +73 -0
- package/src/skf-test-skill/scripts/compute-score.py +4 -3
- package/src/skf-test-skill/templates/test-report-template.md +1 -0
- package/src/skf-update-skill/customize.toml +0 -9
- package/src/skf-update-skill/references/detect-changes.md +33 -3
- package/src/skf-update-skill/references/health-check.md +2 -2
- package/src/skf-update-skill/references/init.md +1 -0
- package/src/skf-update-skill/references/re-extract.md +15 -1
- package/src/skf-verify-stack/references/report.md +1 -1
- package/tools/cli/lib/ui.js +3 -2
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
---
|
|
2
|
+
nextStepFile: 'health-check.md'
|
|
3
|
+
outputFile: '{forge_data_folder}/analyze-source-report-{project_name}.md'
|
|
4
|
+
shapeDetectProbeOrder:
|
|
5
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-shape-detect.py'
|
|
6
|
+
- '{project-root}/src/shared/scripts/skf-shape-detect.py'
|
|
7
|
+
validatePinsProbeOrder:
|
|
8
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-validate-pins.py'
|
|
9
|
+
- '{project-root}/src/shared/scripts/skf-validate-pins.py'
|
|
10
|
+
skillInventoryProbeOrder:
|
|
11
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-skill-inventory.py'
|
|
12
|
+
- '{project-root}/src/shared/scripts/skf-skill-inventory.py'
|
|
13
|
+
scanManifestsProbeOrder:
|
|
14
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-scan-manifests.py'
|
|
15
|
+
- '{project-root}/src/shared/scripts/skf-scan-manifests.py'
|
|
16
|
+
detectLanguageProbeOrder:
|
|
17
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-detect-language.py'
|
|
18
|
+
- '{project-root}/src/shared/scripts/skf-detect-language.py'
|
|
19
|
+
languageCorporaProbeOrder:
|
|
20
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-language-corpora.py'
|
|
21
|
+
- '{project-root}/src/shared/scripts/skf-language-corpora.py'
|
|
22
|
+
writeSkillBriefProbeOrder:
|
|
23
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-write-skill-brief.py'
|
|
24
|
+
- '{project-root}/src/shared/scripts/skf-write-skill-brief.py'
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<!-- Config: communicate in {communication_language}. -->
|
|
28
|
+
|
|
29
|
+
# Step 1a: Auto-Scope Analysis
|
|
30
|
+
|
|
31
|
+
## STEP GOAL:
|
|
32
|
+
|
|
33
|
+
To automatically scope a repo using shape detection and export surface analysis, producing a scope and skill-brief.yaml without requiring manual input. This step replaces the interactive scan-project → identify-units → map-and-detect → recommend → generate-briefs chain when `{auto_mode}` is true.
|
|
34
|
+
|
|
35
|
+
## Rules
|
|
36
|
+
|
|
37
|
+
- Auto-proceed step — no user interaction required
|
|
38
|
+
- This step is conditional — only loaded when `[auto]` flag is present in the pipeline context
|
|
39
|
+
- Must produce the same output artifacts as the interactive chain: analysis report + skill-brief.yaml
|
|
40
|
+
- On unknown shape (exit code 1), fall back to `scan-project.md` (the normal interactive entry point)
|
|
41
|
+
- On error (exit code 2), HARD HALT with exit code 3 (`resolution-failure`)
|
|
42
|
+
|
|
43
|
+
## MANDATORY SEQUENCE
|
|
44
|
+
|
|
45
|
+
### 0. URL Type Detection
|
|
46
|
+
|
|
47
|
+
Read the target URL or path from the pipeline context (`{project_path}` or the first entry in `project_paths[]`).
|
|
48
|
+
|
|
49
|
+
Apply the following heuristic to classify the input:
|
|
50
|
+
|
|
51
|
+
| Input Pattern | Classification | Route |
|
|
52
|
+
|---------------|---------------|-------|
|
|
53
|
+
| `github.com/{owner}/{repo}` (with or without `.git` suffix, with or without scheme prefix) | GitHub repo | §1 (standard auto-scope) |
|
|
54
|
+
| `gitlab.com/...`, `bitbucket.org/...` | Git hosting | §1 (standard auto-scope) |
|
|
55
|
+
| Starts with `/`, `./`, `~/`, or `~` | Local filesystem path | §1 (standard auto-scope) |
|
|
56
|
+
| Any other `https://` or `http://` URL | Documentation URL | §0a (docs-only) |
|
|
57
|
+
| Anything else (SSH URLs, `git://`, bare hostnames, etc.) | Unclassified | §1 (standard auto-scope) |
|
|
58
|
+
|
|
59
|
+
Store the classification result (documentation URL vs. repo/local/other). For all input types, continue to §0b (Pin Resolution).
|
|
60
|
+
|
|
61
|
+
### 0b. Pin Resolution
|
|
62
|
+
|
|
63
|
+
This section validates and resolves version pins. It runs for repo URLs and local paths only — skip for documentation URLs (doc URLs have no git repo to pin against). Initialize `{pinned_ref}`, `{pinned_ref_type}`, and `{pinned_version}` as null.
|
|
64
|
+
|
|
65
|
+
**For documentation URLs:** Skip this section entirely. Continue to §0c.
|
|
66
|
+
|
|
67
|
+
**For local paths when `--pin` is provided:** Emit a warning: "**Local source may not match pinned version {pin_value}.** Ensure you've checked out the correct version locally, or use a remote GitHub URL so SKF can clone from the git tag automatically." Store `{pinned_ref}` = `{pin_value}`, `{pinned_ref_type}` = `"local"`, `{pinned_version}` = `{pin_value}`. Continue to §0c without running `skf-validate-pins.py`.
|
|
68
|
+
|
|
69
|
+
**For repo URLs when `--pin` is provided:**
|
|
70
|
+
|
|
71
|
+
**Resolve `{validatePinsHelper}`** from `{validatePinsProbeOrder}`; first existing path wins; HALT if neither resolves.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv run {validatePinsHelper} --repo-url {project_path} --pin {pin_value}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Handle exit codes:
|
|
78
|
+
|
|
79
|
+
- **Exit 0** (`status: "valid"`): Store `{pinned_ref}` = `resolved_ref`, `{pinned_ref_type}` = `ref_type`, `{pinned_version}` = `version`. Continue to §0c.
|
|
80
|
+
- **Exit 1** (`status: "invalid"`): HARD HALT with exit code 3 (`resolution-failure`). Emit error: `"Version pin '{pin_value}' not found in {project_path}. Available matches: {suggestions}. Use a valid tag, branch, or omit --pin for latest."` Emit error envelope:
|
|
81
|
+
```
|
|
82
|
+
SKF_ANALYZE_RESULT_JSON: {"status":"error","report_path":null,"brief_paths":[],"unit_counts":{"confirmed":0,"skipped":0,"maybe":0},"exit_code":3,"halt_reason":"pin-invalid","mode":"auto"}
|
|
83
|
+
```
|
|
84
|
+
- **Exit 2** (error): HARD HALT with exit code 3 (`resolution-failure`). Emit error envelope with `halt_reason: "resolution-failure"`.
|
|
85
|
+
|
|
86
|
+
**For repo URLs when `--pin` is NOT provided (default):**
|
|
87
|
+
|
|
88
|
+
Using the same `{validatePinsHelper}` resolved above:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uv run {validatePinsHelper} --repo-url {project_path}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Handle exit codes:
|
|
95
|
+
|
|
96
|
+
- **Exit 0** (`status: "resolved"`): Store `{pinned_ref}` = `resolved_ref`, `{pinned_ref_type}` = `ref_type`, `{pinned_version}` = `version`. Log: "Default pin resolved: {resolved_ref}". Continue to §0c.
|
|
97
|
+
- **Exit 1** (no releases found): Set `{pinned_ref}` = null, `{pinned_ref_type}` = null, `{pinned_version}` = null. Log: "No release tags found — using HEAD." Continue to §0c without pinning.
|
|
98
|
+
- **Exit 2**: Log warning, continue without pinning (same as exit 1 behavior).
|
|
99
|
+
|
|
100
|
+
### 0c. Coexistence Detection
|
|
101
|
+
|
|
102
|
+
This section checks for existing skills matching the target before proceeding. It runs for all input types (repo URLs, doc URLs, and local paths). Initialize `{coexistence_suffix}` as empty.
|
|
103
|
+
|
|
104
|
+
**1. Load skill inventory:**
|
|
105
|
+
|
|
106
|
+
**Resolve `{skillInventoryHelper}`** from `{skillInventoryProbeOrder}`; first existing path wins; HALT if neither resolves.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
uv run {skillInventoryHelper} {skills_output_folder}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Parse the JSON output. If the exit code is non-zero or the `skills` array is empty, skip coexistence detection silently (no existing skills to conflict with) and continue to the next section: §0a for documentation URLs, §1 for all other input types.
|
|
113
|
+
|
|
114
|
+
**2. Match target against existing skills:**
|
|
115
|
+
|
|
116
|
+
For each skill in the inventory, check two match conditions (either triggers a hit):
|
|
117
|
+
|
|
118
|
+
- **URL match:** Normalize both the target URL/path and the skill's `metadata.source_repo` — strip scheme (`http://`, `https://`), strip trailing `.git`, strip trailing `/`, compare case-insensitively. A match on the normalized values is a hit.
|
|
119
|
+
- **Name match:** Derive the expected skill name from the target (same logic as §6 for repo URLs, §0a for doc URLs — kebab-case from the project/domain name), then compare against each skill's `name`.
|
|
120
|
+
|
|
121
|
+
**3. If zero matches:**
|
|
122
|
+
|
|
123
|
+
Complete silently. Continue to §0a for documentation URLs, §1 for all other input types. No user output.
|
|
124
|
+
|
|
125
|
+
**4. If one or more matches — coexistence gate:**
|
|
126
|
+
|
|
127
|
+
Present the user with the coexistence decision:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
⚠️ Existing skill(s) found for {target_name}:
|
|
131
|
+
|
|
132
|
+
• {skill_name} (v{version}) — source: {source_repo}
|
|
133
|
+
[repeat for each match]
|
|
134
|
+
|
|
135
|
+
Actions:
|
|
136
|
+
[A]longside — Create a new wiki skill with "-wiki" suffix (existing skill untouched)
|
|
137
|
+
[M]erge — Update the existing skill via US workflow (wiki data enriches it)
|
|
138
|
+
[S]kip — Do not create or modify any skill for this library
|
|
139
|
+
|
|
140
|
+
Choose [A/M/S]:
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
In headless mode (`{headless_mode}` is true): auto-select `[A]longside` and log: "Headless: coexistence detected for {target_name}, auto-selecting [A]longside"
|
|
144
|
+
|
|
145
|
+
**5. Handle user selection:**
|
|
146
|
+
|
|
147
|
+
- **[A]longside:** Set `{coexistence_suffix}` to `-wiki`. Continue to §0a for documentation URLs, §1 for all other input types. The existing skill is untouched.
|
|
148
|
+
|
|
149
|
+
- **[M]erge:** If multiple skills match, prompt the user to select which one to merge into before proceeding. Emit a redirect envelope signaling the forger to route to the US workflow for the selected skill:
|
|
150
|
+
```
|
|
151
|
+
SKF_ANALYZE_RESULT_JSON: {"status":"redirect","redirect_to":"US","skill_name":"{matched_skill_name}","skill_path":"{matched_active_path}","exit_code":0,"halt_reason":null,"mode":"auto","coexistence":"merge"}
|
|
152
|
+
```
|
|
153
|
+
Write the result contract per `shared/references/output-contract-schema.md` with `status: "redirect"`.
|
|
154
|
+
Chain to {nextStepFile} (health-check.md). **STOP HERE — do not proceed to §0a or §1.**
|
|
155
|
+
|
|
156
|
+
- **[S]kip:** Emit a skip envelope:
|
|
157
|
+
```
|
|
158
|
+
SKF_ANALYZE_RESULT_JSON: {"status":"skipped","report_path":null,"brief_paths":[],"unit_counts":{"confirmed":0,"skipped":1,"maybe":0},"exit_code":0,"halt_reason":null,"mode":"auto","coexistence":"skip","skipped_reason":"Existing skill for {matched_skill_name}"}
|
|
159
|
+
```
|
|
160
|
+
Write the result contract with `status: "skipped"`.
|
|
161
|
+
Chain to {nextStepFile} (health-check.md). **STOP HERE — do not proceed to §0a or §1.**
|
|
162
|
+
|
|
163
|
+
### 0a. Docs-Only Short-Circuit
|
|
164
|
+
|
|
165
|
+
This section handles documentation URLs that are not GitHub repos or local paths. It validates the URL, writes a minimal brief and analysis report, emits the envelope, and chains directly to health-check — skipping §1 through §11 entirely.
|
|
166
|
+
|
|
167
|
+
**1. Validate URL reachability:**
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
curl -sI --max-time 5 {url}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- On **2xx/3xx** response: URL is reachable. Continue.
|
|
174
|
+
- On **4xx/5xx**, DNS failure, or timeout: HARD HALT with exit code 3 (`resolution-failure`). Emit error message: `"Documentation URL unreachable: {url} — {status or error}"`. Emit error envelope:
|
|
175
|
+
```
|
|
176
|
+
SKF_ANALYZE_RESULT_JSON: {"status":"error","report_path":null,"brief_paths":[],"unit_counts":{"confirmed":0,"skipped":0,"maybe":0},"exit_code":3,"halt_reason":"path-invalid","mode":"auto","source_type":"docs-only"}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**2. Derive skill name from URL domain:**
|
|
180
|
+
|
|
181
|
+
Extract the hostname from the URL (e.g., `docs.example.com` from `https://docs.example.com/guide/intro`), convert to kebab-case (replace `.` with `-`), yielding e.g. `docs-example-com`. If `{coexistence_suffix}` is non-empty, append it to the skill name (e.g., `docs-example-com-wiki`).
|
|
182
|
+
|
|
183
|
+
**3. Write analysis report:**
|
|
184
|
+
|
|
185
|
+
Update {outputFile} with docs-only results.
|
|
186
|
+
|
|
187
|
+
**Update frontmatter:**
|
|
188
|
+
```yaml
|
|
189
|
+
stepsCompleted: ['init', 'auto-scope']
|
|
190
|
+
lastStep: 'auto-scope'
|
|
191
|
+
source_type: docs-only
|
|
192
|
+
confirmed_units:
|
|
193
|
+
- name: '{skill_name}'
|
|
194
|
+
shape: 'docs-only'
|
|
195
|
+
confidence: 1.0
|
|
196
|
+
export_count: 0
|
|
197
|
+
package_count: 0
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Append body section:**
|
|
201
|
+
```markdown
|
|
202
|
+
## Auto-Scope Analysis
|
|
203
|
+
|
|
204
|
+
**Mode:** auto (docs-only short-circuit)
|
|
205
|
+
**Source Type:** docs-only
|
|
206
|
+
**Documentation URL:** {url}
|
|
207
|
+
**Skill Name:** {skill_name}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**4. Write skill brief via canonical writer:**
|
|
211
|
+
|
|
212
|
+
**Resolve `{writeSkillBriefHelper}`** from `{writeSkillBriefProbeOrder}`; first existing path wins; HALT if neither resolves.
|
|
213
|
+
|
|
214
|
+
Create directory `{forge_data_folder}/{skill_name}/` if it does not exist.
|
|
215
|
+
|
|
216
|
+
Pipe the flat context JSON below into the resolved writer with the `--from-flat` flag:
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"name": "{skill_name}",
|
|
221
|
+
"target_version": null,
|
|
222
|
+
"detected_version": null,
|
|
223
|
+
"source_type": "docs-only",
|
|
224
|
+
"source_repo": "{url}",
|
|
225
|
+
"language": "documentation",
|
|
226
|
+
"description": "Skill created from documentation at {url}",
|
|
227
|
+
"forge_tier": "{forge_tier}",
|
|
228
|
+
"created": "{current_date}",
|
|
229
|
+
"created_by": "{user_name}",
|
|
230
|
+
"scope_type": "docs-only",
|
|
231
|
+
"scope_include": [],
|
|
232
|
+
"scope_exclude": [],
|
|
233
|
+
"scope_notes": "Docs-only skill created from documentation URL",
|
|
234
|
+
"scope_rationale": null,
|
|
235
|
+
"scope_tier_a_include": null,
|
|
236
|
+
"scope_amendments": null,
|
|
237
|
+
"doc_urls": [{"url": "{url}", "label": "Primary Documentation"}],
|
|
238
|
+
"scripts_intent": null,
|
|
239
|
+
"assets_intent": null,
|
|
240
|
+
"source_authority": "community",
|
|
241
|
+
"target_ref": null,
|
|
242
|
+
"source_ref": null,
|
|
243
|
+
"version_resolved": "1.0.0"
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
echo '<context-json>' | uv run {writeSkillBriefHelper} write --target {forge_data_folder}/{skill_name}/skill-brief.yaml --from-flat
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**5. Emit success envelope:**
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
SKF_ANALYZE_RESULT_JSON: {"status":"success","report_path":"{outputFile_path}","brief_paths":["{brief_path}"],"unit_counts":{"confirmed":1,"skipped":0,"maybe":0},"exit_code":0,"halt_reason":null,"mode":"auto","source_type":"docs-only"}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
If `{coexistence_suffix}` is non-empty (i.e., [A]longside was selected in §0c), include `"coexistence":"alongside"` in the envelope.
|
|
258
|
+
|
|
259
|
+
The `source_type` field signals downstream consumers (BS) to skip repo-based enrichment.
|
|
260
|
+
|
|
261
|
+
**6. Write result contract** per `shared/references/output-contract-schema.md`: the per-run record and latest copy, same as §10.
|
|
262
|
+
|
|
263
|
+
If `{onCompleteCommand}` is non-empty, invoke it now with `--result-path={result_json_path}`.
|
|
264
|
+
|
|
265
|
+
**7. Chain to health check:**
|
|
266
|
+
|
|
267
|
+
Load, read fully, then execute {nextStepFile} to run the shared workflow health check. **Skip §1 through §11 entirely.**
|
|
268
|
+
|
|
269
|
+
### 1. Load Context
|
|
270
|
+
|
|
271
|
+
Read {outputFile} frontmatter to obtain:
|
|
272
|
+
- `project_paths[]` — the root(s) to analyze
|
|
273
|
+
- `forge_tier` — for brief generation
|
|
274
|
+
- `project_name`, `user_name`, `date`
|
|
275
|
+
|
|
276
|
+
Load `references/step-shape-detect.md` as reference for shape detection invocation contract and shape→scope mapping.
|
|
277
|
+
|
|
278
|
+
### 2. Manifest Scan
|
|
279
|
+
|
|
280
|
+
Enumerate package manifests **deterministically** via `{scanManifestsHelper}` (the same helper the interactive `scan-project.md` uses) — do not hand-scan. Resolve `{scanManifestsHelper}` as the first path in `{scanManifestsProbeOrder}` that exists. The scanner reads a **local directory**, so how you point it at the target depends on the input form classified in §0:
|
|
281
|
+
|
|
282
|
+
**For each path in `project_paths[]`:**
|
|
283
|
+
|
|
284
|
+
- **Local filesystem path** (starts with `/`, `./`, `~/`, `~`, or is an existing directory) — scan it directly:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
uv run {scanManifestsHelper} scan {path}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
- **Remote git URL** (e.g. `github.com/{owner}/{repo}`) — auto-scope has no working tree yet and the scanner cannot read a URL. Fetch **just the manifests** first (blobless + sparse + depth-1 — no source blobs, typically KB–MB even for large monorepos), then scan that tree:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
tmp="$(mktemp -d)"
|
|
294
|
+
git clone --filter=blob:none --no-checkout --depth 1 {pinned_branch_flag} {path} "$tmp"
|
|
295
|
+
git -C "$tmp" sparse-checkout set --no-cone '**/package.json' '**/Cargo.toml' '**/pyproject.toml' '**/go.mod' '**/pom.xml' '**/build.gradle' '**/build.gradle.kts' '**/Package.swift' 'pnpm-workspace.yaml' '**/pnpm-workspace.yaml'
|
|
296
|
+
git -C "$tmp" checkout
|
|
297
|
+
uv run {scanManifestsHelper} scan "$tmp"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
where `{pinned_branch_flag}` is `--branch {pinned_ref}` when a pin was resolved in §0b (so manifests match the target version), otherwise omitted. **Retain `"$tmp"` through §3** — shape detection reads the discovered manifest files from it — then it may be discarded.
|
|
301
|
+
|
|
302
|
+
Parse the JSON envelope: `{manifests: [{path, ecosystem, ...}], total_unique, monorepo, warnings?}`. The scanner discovers the project root plus monorepo workspace members (npm/pnpm/yarn `workspaces`, Cargo `[workspace]`, and other ecosystems) and sets the `monorepo` flag — so members are found without hand-listing each workspace convention, for both local trees and remote fetches.
|
|
303
|
+
|
|
304
|
+
From the envelope, record:
|
|
305
|
+
|
|
306
|
+
1. **Supported manifest paths** — filter `manifests[].path` to the types `skf-shape-detect.py` accepts (`package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `pom.xml`, `build.gradle`, `build.gradle.kts`, `Package.swift`). Each `manifests[].path` is **relative to the scan root**, so resolve them against that root (`{path}` for a local scan, `"$tmp"` for a remote fetch) before use. This filtered, comma-joined list of resolved paths is fed to shape detection in §3. For a monorepo, it includes each workspace member's manifest, so the package surface is classified accurately rather than from a bare (and often export-less) repo root. The scanner may discover ecosystems shape detection does not yet classify; those are excluded here, so a repo with no supported manifest falls back to interactive at the next check rather than auto-scoping.
|
|
307
|
+
2. **`monorepo` flag** and the count of discovered supported packages — carried forward as a signal for the decomposition decision in §3a.
|
|
308
|
+
|
|
309
|
+
**Harvest tree-level language signals.** A whole-language repo may declare no parser-generator dependency (a hand-written compiler such as rustc, TypeScript, or the Go toolchain) or carry no supported manifest at all (CPython, Ruby). From the **same** fetched tree — no second clone and no blobs, since tree objects are already present in the blobless clone — collect two signals for shape detection. These are pure path listings (`git ls-tree` reads tree objects; no checkout, no blob download):
|
|
310
|
+
|
|
311
|
+
- **Remote fetch** (`"$tmp"`), or a **local path** that is a git repo (`git -C {path}`):
|
|
312
|
+
```bash
|
|
313
|
+
files="$(git -C "$tmp" ls-tree -r --name-only HEAD)" # every file path
|
|
314
|
+
dirs="$( git -C "$tmp" ls-tree -r -d --name-only HEAD)" # every directory
|
|
315
|
+
# Grammar files (depth-capped to skip deep vendored fixtures, hard-capped):
|
|
316
|
+
grammar_matches="$(printf '%s\n' "$files" \
|
|
317
|
+
| grep -Ei '\.(g4|pest|lalrpop|y|gram|lark|ebnf|peg|ungram)$|/grammar\.(js|json)$' \
|
|
318
|
+
| awk -F/ 'NF<=4' | head -n 50 | paste -sd, -)"
|
|
319
|
+
# Directory signals (trailing /) + depth-capped file basenames, narrowed to
|
|
320
|
+
# compiler-relevant paths so the argument stays bounded on huge repos. The
|
|
321
|
+
# filter is a loose superset of shape detection's gates — the script does the
|
|
322
|
+
# precise matching; this only keeps the list small.
|
|
323
|
+
tree_paths="$({ printf '%s\n' "$dirs" | sed 's#$#/#'; \
|
|
324
|
+
printf '%s\n' "$files" | awk -F/ 'NF<=5'; } \
|
|
325
|
+
| grep -Ei '(^|/)(compiler|compile|syntax|scanner|lexer|tokeniz|parser|parse|ast|binder|checker|codegen|ssagen|interpreter|vm|eval|rustc_[a-z]+)' \
|
|
326
|
+
| head -n 400 | paste -sd, -)"
|
|
327
|
+
```
|
|
328
|
+
- **Local path that is not a git repo** (`{path}`): list the tree with `find` instead, then derive `grammar_matches` / `tree_paths` the same way:
|
|
329
|
+
```bash
|
|
330
|
+
files="$(cd {path} && find . -type f -not -path '*/.git/*' | sed 's#^\./##')"
|
|
331
|
+
dirs="$( cd {path} && find . -type d -not -path '*/.git/*' | sed 's#^\./##')"
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Record `<grammar_matches>` and `<tree_paths>` (each a comma-joined list, possibly empty) for §3.
|
|
335
|
+
|
|
336
|
+
**IF no supported manifests are found** (the filtered list is empty):
|
|
337
|
+
- **AND** `<grammar_matches>` is empty **AND** `<tree_paths>` shows no compiler directory (none of `compiler/`, `src/compiler/`, `cmd/compile/`, `internal/syntax/`, or a `Parser/`): the repo carries no language signal — emit fallback message: "**Auto-scope could not find any supported package manifests — switching to interactive mode.**" Load, read fully, then execute `references/scan-project.md`. **STOP HERE.**
|
|
338
|
+
- **Otherwise** (a grammar file or a compiler directory is present) the repo is a manifest-less language toolchain (CPython, Ruby): proceed to §3 with an **empty** `--manifests` and the harvested `--grammar-files` / `--tree-paths`.
|
|
339
|
+
|
|
340
|
+
### 3. Invoke Shape Detection
|
|
341
|
+
|
|
342
|
+
**Resolve `{shapeDetectHelper}`** from `{shapeDetectProbeOrder}`; first existing path wins; HALT if neither resolves.
|
|
343
|
+
|
|
344
|
+
Invoke the shape detection script with the discovered manifests and the harvested tree-level signals:
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
uv run {shapeDetectHelper} --repo-url <project_path_or_url> \
|
|
348
|
+
--manifests <comma_separated_manifest_paths> \
|
|
349
|
+
--grammar-files <grammar_matches> --tree-paths <tree_paths>
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`<comma_separated_manifest_paths>` may be empty for a manifest-less language repo, provided `<grammar_matches>` or `<tree_paths>` carries the signal. Parse the JSON output: `{shape, signals, confidence, export_count, package_count}`
|
|
353
|
+
|
|
354
|
+
**Handle exit codes:**
|
|
355
|
+
|
|
356
|
+
- **Exit 0 (shape classified):** Continue to §3a.
|
|
357
|
+
- **Exit 1 (unknown shape):** Emit fallback message: "**Auto-scope could not classify this repo — switching to interactive mode.**" Load, read fully, then execute `references/scan-project.md`. **STOP HERE.**
|
|
358
|
+
- **Exit 2 (error):** HARD HALT with exit code 3 (`resolution-failure`). Emit the error envelope:
|
|
359
|
+
```
|
|
360
|
+
SKF_ANALYZE_RESULT_JSON: {"status":"error","report_path":null,"brief_paths":[],"unit_counts":{"confirmed":0,"skipped":0,"maybe":0},"exit_code":3,"halt_reason":"resolution-failure","mode":"auto"}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### 3a. Check Decomposition Thresholds
|
|
364
|
+
|
|
365
|
+
Evaluate the shape detection output to determine whether this **monorepo** should be decomposed into multiple skills.
|
|
366
|
+
|
|
367
|
+
**Threshold condition:**
|
|
368
|
+
|
|
369
|
+
| Threshold | Condition | Signal |
|
|
370
|
+
|-----------|-----------|--------|
|
|
371
|
+
| Monorepo / multi-package | `package_count > 3` | 4+ packages — a decomposition **candidate** (empirically validated: fires on real 15-, 38-, and 442-package workspaces) |
|
|
372
|
+
|
|
373
|
+
> A *single* package with a large API surface is **not** a decomposition trigger — it produces one cohesive skill that `skf-create-skill`'s auto-shard step splits into `references/` shards at the 400-line ceiling (a single library is one install / one import namespace; fragmenting it by source directory loses cohesion). Decomposition is for genuinely multi-package monorepos only.
|
|
374
|
+
|
|
375
|
+
**Decision:**
|
|
376
|
+
|
|
377
|
+
- **Threshold not met** (`package_count ≤ 3`) → Continue to §4 (single-scope flow, entirely unchanged).
|
|
378
|
+
- **Threshold met** (`package_count > 3`) → this repo is a **decomposition candidate**. A threshold firing means the repo *could* decompose, not that it *should* — continue to §3b to decide merge-vs-split. Log: "Auto-decomposition candidate: package_threshold ({value} packages exceeds 3)".
|
|
379
|
+
|
|
380
|
+
### 3b. Cohesion Check — Merge to One Skill vs Split into N
|
|
381
|
+
|
|
382
|
+
Reached only when §3a flagged a decomposition candidate. Most published monorepos are **cohesive** and produce a better single skill than a pile of fragments — empirically, 5/5 real monorepos (animato 15 crates, trpc, react 38 packages, aws-sdk-js-v3 442 packages, plus zod) were best served as one cohesive skill or a curated few, not one-skill-per-package. Decide deliberately:
|
|
383
|
+
|
|
384
|
+
**Merge into ONE cohesive skill** (override the threshold → continue to §4 single-scope) when **any** of these hold:
|
|
385
|
+
|
|
386
|
+
- **Umbrella facade** — one package re-exports the members: a root or named package whose dependencies include the other workspace members, or which `pub use` / `export *`s them. The facade *is* the public surface (e.g. animato's `crates/animato` re-exporting its 15 sub-crates).
|
|
387
|
+
- **Shared runtime contract** — the members are consumed together through one entry point, and teaching the shared invariant covers them (e.g. tRPC's adapters around `@trpc/server`; aws-sdk's `new XClient(...) → client.send(new YCommand(...))` shared by every `@aws-sdk/client-*`).
|
|
388
|
+
- **Internal building blocks** — the members are private/internal pieces of one product, not independently meaningful to a consumer.
|
|
389
|
+
|
|
390
|
+
**Split into N skills** (→ §4a) when:
|
|
391
|
+
|
|
392
|
+
- The members are **independently published with distinct public surfaces serving different concerns**, **and no umbrella re-exports them** — e.g. `react-dom` and `react-server-dom-*` are separate installs with separate jobs, or a federated SDK where a consumer only ever wants one service. Each genuinely-distinct facet earns its own skill.
|
|
393
|
+
|
|
394
|
+
If genuinely unsure, **prefer merge** — a too-broad single skill is recoverable with `US`; N fragmented skills are not.
|
|
395
|
+
|
|
396
|
+
**Facet-coverage guard (merged facet-diverse repos only).** When you merge a repo whose members have genuinely distinct surfaces and you scope to only some of them, record the decision explicitly — never drop a facet silently:
|
|
397
|
+
|
|
398
|
+
- In `scope.notes`, name the in-scope facets **and** the excluded major facets, e.g. _"Scoped to react + react-dom core; excludes react-server-dom-\* (RSC), the specialized renderers (react-art/native/test), and the compiler — forge a separate skill for those."_
|
|
399
|
+
- Surface the excluded facets in the analysis report (§7) so the operator can re-scope or forge a companion skill.
|
|
400
|
+
|
|
401
|
+
### 4. Map Shape to Scope
|
|
402
|
+
|
|
403
|
+
Apply the shape→scope.type mapping:
|
|
404
|
+
|
|
405
|
+
| Shape (from skf-shape-detect.py) | scope.type | Condition |
|
|
406
|
+
|----------------------------------|------------|-----------|
|
|
407
|
+
| `library-API` | `full-library` | export_count ≤ 200 |
|
|
408
|
+
| `library-API` | `public-api` | export_count > 200 |
|
|
409
|
+
| `reference-app` | `reference-app` | — |
|
|
410
|
+
| `language-reference` | `full-library` | — |
|
|
411
|
+
| `stack-compose` | `full-library` | Decomposition candidate when `package_count > 3` — cohesion-checked at §3b |
|
|
412
|
+
|
|
413
|
+
### 5. Generate Include/Exclude Patterns
|
|
414
|
+
|
|
415
|
+
Generate `scope.include` and `scope.exclude` arrays from the detected language and project structure.
|
|
416
|
+
|
|
417
|
+
**Detect primary language** from manifest type (the same set shape detection classifies):
|
|
418
|
+
- `package.json` → TypeScript/JavaScript
|
|
419
|
+
- `pyproject.toml` → Python
|
|
420
|
+
- `Cargo.toml` → Rust
|
|
421
|
+
- `go.mod` → Go
|
|
422
|
+
- `pom.xml` → Java
|
|
423
|
+
- `build.gradle` / `build.gradle.kts` → Java or Kotlin (Kotlin when a `src/main/kotlin/` tree exists)
|
|
424
|
+
- `Package.swift` → Swift
|
|
425
|
+
|
|
426
|
+
**Default patterns (adjust based on actual project structure):**
|
|
427
|
+
|
|
428
|
+
| Language | Default include | Default exclude |
|
|
429
|
+
|----------|-----------------|-----------------|
|
|
430
|
+
| TypeScript/JavaScript | `['src/**/*.ts', 'src/**/*.tsx']` | `['**/*.test.ts', '**/*.spec.ts', '**/node_modules/**']` |
|
|
431
|
+
| Python | `['src/**/*.py']` or `['{package_name}/**/*.py']` | `['**/*_test.py', '**/test_*.py', '**/tests/**']` |
|
|
432
|
+
| Rust | `['src/**/*.rs']` | `['**/tests/**', '**/benches/**']` |
|
|
433
|
+
| Go | `['**/*.go']` | `['**/*_test.go', '**/vendor/**']` |
|
|
434
|
+
| Java | `['src/main/java/**/*.java']` | `['**/src/test/**']` |
|
|
435
|
+
| Kotlin | `['src/main/kotlin/**/*.kt']` | `['**/src/test/**']` |
|
|
436
|
+
| Swift | `['Sources/**/*.swift']` | `['**/Tests/**']` |
|
|
437
|
+
|
|
438
|
+
**Adjust for actual layout:** If the project uses a non-standard layout (e.g., `lib/` instead of `src/`, or a named package directory for Python), detect and use the actual paths. Check for the existence of common source directories (`src/`, `lib/`, `pkg/`, the package name directory) and prefer the one that exists.
|
|
439
|
+
|
|
440
|
+
### 6. Build Scope and Determine Skill Name
|
|
441
|
+
|
|
442
|
+
Build the scope object:
|
|
443
|
+
```yaml
|
|
444
|
+
scope:
|
|
445
|
+
type: '{mapped_scope_type}'
|
|
446
|
+
include: ['{generated_include_patterns}']
|
|
447
|
+
exclude: ['{generated_exclude_patterns}']
|
|
448
|
+
notes: 'Auto-scoped from shape detection (shape: {shape}, confidence: {confidence}).{corpus_caveat}'
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Determine the skill name from the project name or package name (kebab-case, lowercase). Use the manifest `name` field if available, otherwise derive from the project directory name. If `{coexistence_suffix}` is non-empty, append it to the skill name.
|
|
452
|
+
|
|
453
|
+
Detect the primary language from the manifest ecosystem:
|
|
454
|
+
- `npm` → `typescript` (or `javascript` if no `.ts` files in includes)
|
|
455
|
+
- `python` → `python`
|
|
456
|
+
- `rust` → `rust`
|
|
457
|
+
- `go` → `go`
|
|
458
|
+
- `maven` → `java`
|
|
459
|
+
- `gradle` → `java` (or `kotlin` when a `src/main/kotlin/` tree exists)
|
|
460
|
+
- `swift` → `swift`
|
|
461
|
+
|
|
462
|
+
### 6b. Seed Companion Corpora (whole-language references only)
|
|
463
|
+
|
|
464
|
+
Runs only when §3 classified the repo as `language-reference` **via a whole-language signal** — the `signals` array contains a `grammar_file:` or `tree_triad:` entry (a compiler / interpreter / grammar repo such as rust-lang/rust, TypeScript, CPython). **Skip** when `language-reference` fired only from `parser_producer:` / `parser_dep:` signals (a parser *library* such as pest or lalrpop): there the code **is** the product, so no companion prose is needed and the §6/§7 caveat below does not apply.
|
|
465
|
+
|
|
466
|
+
A whole-language skill's value is in the language's **prose** — the guide/Book, the standard/library API docs, idioms — not the compiler internals. Seed those canonical corpora so the forged skill teaches the language rather than its implementation.
|
|
467
|
+
|
|
468
|
+
**Resolve `{detectLanguageHelper}`** from `{detectLanguageProbeOrder}` and **`{languageCorporaHelper}`** from `{languageCorporaProbeOrder}` (first existing path wins).
|
|
469
|
+
|
|
470
|
+
1. **Derive the corpus language key `{corpus_language}`.** Prefer the §6 manifest language when non-empty. Otherwise — a manifest-less toolchain such as CPython or Ruby — resolve it from the file paths harvested in §2:
|
|
471
|
+
```bash
|
|
472
|
+
echo '{"tree": [<harvested §2 file paths>]}' | uv run {detectLanguageHelper}
|
|
473
|
+
```
|
|
474
|
+
Use its `language` field. (`.c`/`.h`/`.y` are not in the detector's extension map, so a C-hosted language resolves by its real sources — Ruby via `.rb`.)
|
|
475
|
+
2. **Look up canonical corpora:**
|
|
476
|
+
```bash
|
|
477
|
+
uv run {languageCorporaHelper} --language {corpus_language}
|
|
478
|
+
```
|
|
479
|
+
- exit 0 → parse the `[{url, label, source}]` array (each seed carries `source: language-registry`) → these are `{corpus_seeds}`.
|
|
480
|
+
- exit 1 → no registry entry (long-tail language) → `{corpus_seeds}` is empty (README detection in brief-skill remains the only source).
|
|
481
|
+
- exit 2 → log a warning and treat as empty (best-effort; never halt).
|
|
482
|
+
3. Record `{N}` = number of seeds and `{corpus_labels}` = comma-joined labels, carried into the brief `doc_urls` (§8) and the honest caveat (§6/§7).
|
|
483
|
+
4. Build `{corpus_caveat}` (appended to `scope.notes` in §6/§8 and surfaced in §7) so the operator knows a code-only whole-language skill is low-value:
|
|
484
|
+
- `{N}` ≥ 1: `" LANGUAGE-REFERENCE CAVEAT: this skill's value is the {corpus_language} prose (guide/Book + std/library docs), not compiler internals. Seeded {N} corpus URL(s): {corpus_labels}. create-skill foregrounds this registry prose as the skill's Language Guide and demotes compiler-internal signatures to a reference-only section — review the forged skill if compiler internals still dominate."`
|
|
485
|
+
- `{N}` == 0: `" LANGUAGE-REFERENCE CAVEAT: no canonical corpora were found for {corpus_language} (README detection and the registry both came up empty). This skill is LOW-VALUE as code-only — attach the {corpus_language} guide + std/library docs manually (re-run with a doc URL, or enrich via US) before forging."`
|
|
486
|
+
|
|
487
|
+
For a parser-library `language-reference` (skipped above) and every other shape, `{corpus_caveat}` is empty.
|
|
488
|
+
|
|
489
|
+
### 4a. Multi-Scope Decomposition
|
|
490
|
+
|
|
491
|
+
This section is reached only from §3b when the cohesion check decided to **split** a monorepo (members are independently published with distinct surfaces and no umbrella re-exports them). It replaces §4→§5→§6 for repos that will produce N > 1 skills.
|
|
492
|
+
|
|
493
|
+
**Decompose by workspace package:** Use workspace package discovery from §2 manifest scan results. Each workspace package with its own manifest becomes a separate skill boundary. Name each skill as `{project_name}-{package_name}` (kebab-case); if `{coexistence_suffix}` is non-empty, append it. Trivial workspace members (no source files, no exports) are excluded.
|
|
494
|
+
|
|
495
|
+
**Per-boundary shape→scope mapping:**
|
|
496
|
+
|
|
497
|
+
For each decomposed boundary, apply the shape→scope mapping from §4 independently — re-run the shape→scope heuristic ladder from `step-shape-detect.md` per package using each package's own manifest data. Packages may have different shapes (e.g., a `library-API` core + a `reference-app` CLI).
|
|
498
|
+
|
|
499
|
+
### 5a. Generate Multi-Scope Patterns
|
|
500
|
+
|
|
501
|
+
For each decomposed boundary, generate include/exclude patterns using the same language-aware rules as §5, but scoped to the boundary's source paths. Monorepo boundaries are rooted at the package path (e.g., `packages/auth/src/**/*.ts` instead of `src/**/*.ts`).
|
|
502
|
+
|
|
503
|
+
### 6a. Build Multi-Scope
|
|
504
|
+
|
|
505
|
+
For each boundary, build a scope object following the same structure as §6.
|
|
506
|
+
|
|
507
|
+
Include decomposition metadata in `scope.notes`: "Decomposed from {project_name} — boundary {i}/{N} ({reason})"
|
|
508
|
+
|
|
509
|
+
Determine each boundary's skill name from the boundary-derived name (kebab-case, lowercase). If `{coexistence_suffix}` is non-empty, append it to each skill name. Detect the primary language from each boundary's manifest ecosystem (same rules as §6).
|
|
510
|
+
|
|
511
|
+
**Pin data (from §0b):** All N decomposed briefs share the same pin — the pin targets a repo-level ref, not a package-level version. Apply the same `target_version`/`target_ref` values from §0b to all N boundaries at brief write time (§8).
|
|
512
|
+
|
|
513
|
+
After building all N scopes, continue to §7 with the full set of boundaries.
|
|
514
|
+
|
|
515
|
+
### 7. Write Analysis Report
|
|
516
|
+
|
|
517
|
+
Update {outputFile} with auto-scope results.
|
|
518
|
+
|
|
519
|
+
**Update frontmatter:**
|
|
520
|
+
```yaml
|
|
521
|
+
stepsCompleted: ['init', 'auto-scope']
|
|
522
|
+
lastStep: 'auto-scope'
|
|
523
|
+
confirmed_units:
|
|
524
|
+
- name: '{skill_name}'
|
|
525
|
+
shape: '{shape}'
|
|
526
|
+
confidence: {confidence}
|
|
527
|
+
export_count: {export_count}
|
|
528
|
+
package_count: {package_count}
|
|
529
|
+
boundary_path: '{boundary_path}' # present only for decomposed units
|
|
530
|
+
# ... N entries when decomposition is active
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**When decomposition was triggered (N > 1 units):**
|
|
534
|
+
|
|
535
|
+
Add `decomposition` to frontmatter:
|
|
536
|
+
```yaml
|
|
537
|
+
decomposition:
|
|
538
|
+
triggered: true
|
|
539
|
+
reason: 'package_threshold'
|
|
540
|
+
boundary_count: N
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Each `confirmed_units` entry includes `boundary_path` — the relative path to the boundary's root (e.g., `packages/core`). Omit the `decomposition` key entirely when single-scope (N = 1).
|
|
544
|
+
|
|
545
|
+
**When single-scope (N = 1):** No `decomposition` key. `confirmed_units` contains a single entry (existing behavior).
|
|
546
|
+
|
|
547
|
+
**Append body section:**
|
|
548
|
+
|
|
549
|
+
For single-scope (unchanged):
|
|
550
|
+
```markdown
|
|
551
|
+
## Auto-Scope Analysis
|
|
552
|
+
|
|
553
|
+
**Mode:** auto
|
|
554
|
+
**Shape:** {shape} (confidence: {confidence})
|
|
555
|
+
**Signals:** {signals list}
|
|
556
|
+
**Export Count:** {export_count}
|
|
557
|
+
**Package Count:** {package_count}
|
|
558
|
+
**Resolved Scope Type:** {scope_type}
|
|
559
|
+
**Include Patterns:** {include patterns}
|
|
560
|
+
**Exclude Patterns:** {exclude patterns}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**When the shape is a whole-language `language-reference`** (§6b ran — a `grammar_file:`/`tree_triad:` signal), append a Companion Corpora subsection so the operator sees whether the skill has the prose that makes it useful. The status is computed from the **final** brief `doc_urls` (the entries that will actually be fetched), not the seed count alone:
|
|
564
|
+
|
|
565
|
+
```markdown
|
|
566
|
+
## Companion Corpora (language-reference)
|
|
567
|
+
|
|
568
|
+
**Why:** A whole-language skill's value is its prose (guide/Book, std/library docs, idioms), not compiler internals.
|
|
569
|
+
**Corpora in brief doc_urls:** {final_doc_urls_count}
|
|
570
|
+
- {label}: {url} # one line per doc_urls entry
|
|
571
|
+
**Status:** {ATTACHED — canonical corpora present | DEGRADED — code-only, no canonical corpora; attach the {corpus_language} guide + std/library docs before forging}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
For multi-scope (N > 1):
|
|
575
|
+
```markdown
|
|
576
|
+
## Auto-Scope Analysis — Decomposition ({N} skills)
|
|
577
|
+
|
|
578
|
+
**Mode:** auto
|
|
579
|
+
**Decomposition:** {reason} ({N} boundaries)
|
|
580
|
+
**Parent Shape:** {shape} (confidence: {confidence})
|
|
581
|
+
**Export Count:** {export_count}
|
|
582
|
+
**Package Count:** {package_count}
|
|
583
|
+
|
|
584
|
+
### Boundary 1: {boundary_name}
|
|
585
|
+
**Scope Type:** {scope_type}
|
|
586
|
+
**Boundary Path:** {boundary_path}
|
|
587
|
+
**Include Patterns:** {include patterns}
|
|
588
|
+
**Exclude Patterns:** {exclude patterns}
|
|
589
|
+
**Rationale:** {boundary_rationale}
|
|
590
|
+
|
|
591
|
+
### Boundary 2: {boundary_name}
|
|
592
|
+
...
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### 8. Write Skill Brief
|
|
596
|
+
|
|
597
|
+
**For each confirmed unit** (1 for single-scope, N for decomposition):
|
|
598
|
+
|
|
599
|
+
Create directory `{forge_data_folder}/{skill_name}/` if it does not exist.
|
|
600
|
+
|
|
601
|
+
Write `{forge_data_folder}/{skill_name}/skill-brief.yaml` conforming to the skill-brief schema (`assets/skill-brief-schema.md`):
|
|
602
|
+
|
|
603
|
+
```yaml
|
|
604
|
+
name: '{skill_name}'
|
|
605
|
+
version: '{detected_version or 1.0.0}'
|
|
606
|
+
source_repo: '{project_path}'
|
|
607
|
+
language: '{detected_language}'
|
|
608
|
+
scope:
|
|
609
|
+
type: '{scope_type}'
|
|
610
|
+
include:
|
|
611
|
+
- '{include_patterns}'
|
|
612
|
+
exclude:
|
|
613
|
+
- '{exclude_patterns}'
|
|
614
|
+
notes: 'Auto-scoped from shape detection (shape: {shape}, confidence: {confidence}).{corpus_caveat}'
|
|
615
|
+
description: '{1-3 sentence description based on shape, language, and manifest name}'
|
|
616
|
+
forge_tier: '{forge_tier}'
|
|
617
|
+
created: '{current_date}'
|
|
618
|
+
created_by: '{user_name}'
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
**Companion corpora (whole-language references).** When §6b produced `{corpus_seeds}` (`{N}` ≥ 1), add them as the brief's `doc_urls` so the language's prose is fetched and assembled alongside the code:
|
|
622
|
+
|
|
623
|
+
```yaml
|
|
624
|
+
doc_urls:
|
|
625
|
+
- { url: '{seed.url}', label: '{seed.label}', source: '{seed.source}' } # one entry per §6b seed; source is 'language-registry'
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
These are the brief's *existing* `doc_urls`; brief-skill's README detection then merges additional discovered docs on top (existing entries win). **When `{N}` is 0, omit the `doc_urls` key entirely** — the schema requires at least one entry when the key is present.
|
|
629
|
+
|
|
630
|
+
**When decomposition is active (N > 1 units):**
|
|
631
|
+
|
|
632
|
+
Loop over all N boundaries. For each boundary:
|
|
633
|
+
- `name` is the boundary-derived skill name (e.g., `my-monorepo-core`)
|
|
634
|
+
- `include`/`exclude` patterns are boundary-scoped (from §5a)
|
|
635
|
+
- `scope.notes` includes decomposition context: "Decomposed from {project_name} ({N} skills) — boundary {i}/{N}: {boundary_description}"
|
|
636
|
+
- `description` references the parent project and boundary role (e.g., "Core library package of the my-monorepo project, providing...")
|
|
637
|
+
- All N briefs share the same `version`, `source_repo`, `language`, `forge_tier`, `created`, `created_by` values as the parent project
|
|
638
|
+
|
|
639
|
+
**Version detection:** Attempt to auto-detect the source version per the version detection rules in `assets/skill-brief-schema.md`. Fall back to `1.0.0` if detection fails.
|
|
640
|
+
|
|
641
|
+
**Pin data (from §0b):** When `{pinned_ref}` is non-null, enrich the brief with pin data:
|
|
642
|
+
|
|
643
|
+
- If `{pinned_ref_type}` is `"tag"`: set `target_version` = `{pinned_version}`, `target_ref` = `{pinned_ref}`, `version` = `{pinned_version}`.
|
|
644
|
+
- If `{pinned_ref_type}` is `"branch"`: set `target_ref` = `{pinned_ref}`, leave `target_version` = null, `version` = auto-detected or `1.0.0`.
|
|
645
|
+
- If `{pinned_ref_type}` is `"local"`: set `target_version` = `{pinned_version}`, `target_ref` = null, `version` = `{pinned_version}`.
|
|
646
|
+
|
|
647
|
+
When `{pinned_ref}` is null (no pin, no releases): leave `target_version` = null, `target_ref` = null — existing version detection applies unchanged.
|
|
648
|
+
|
|
649
|
+
In the docs-only path (§0a), `--pin` is ignored (already skipped at §0b). No changes to §0a.
|
|
650
|
+
|
|
651
|
+
### 9. Emit Result Envelope
|
|
652
|
+
|
|
653
|
+
Emit the `SKF_ANALYZE_RESULT_JSON` envelope on stdout:
|
|
654
|
+
|
|
655
|
+
```
|
|
656
|
+
SKF_ANALYZE_RESULT_JSON: {"status":"success","report_path":"{outputFile_path}","brief_paths":["{brief_path_1}","{brief_path_2}",...,"{brief_path_N}"],"unit_counts":{"confirmed":N,"skipped":0,"maybe":0},"exit_code":0,"halt_reason":null,"mode":"auto"}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
`brief_paths` contains N paths (one per confirmed unit). `unit_counts.confirmed` is N. The envelope JSON format is structurally unchanged — `brief_paths` was already an array and `unit_counts.confirmed` was already a number. No breaking change for downstream consumers.
|
|
660
|
+
|
|
661
|
+
If `{coexistence_suffix}` is non-empty (i.e., [A]longside was selected in §0c), include `"coexistence":"alongside"` in the envelope.
|
|
662
|
+
|
|
663
|
+
When `{pinned_ref}` is non-null, include `"pinned_ref":"{pinned_ref}"` and `"pinned_version":"{pinned_version}"` in the envelope. These flow downstream to BS/CS for provenance recording. When `{pinned_ref}` is null, omit these fields (backward-compatible — existing consumers don't expect them).
|
|
664
|
+
|
|
665
|
+
### 10. Write Result Contract
|
|
666
|
+
|
|
667
|
+
Write the result contract per `shared/references/output-contract-schema.md`: the per-run record at `{forge_data_folder}/analyze-source-result-{YYYYMMDD-HHmmss}.json` (UTC timestamp, resolution to seconds) and a copy at `{forge_data_folder}/analyze-source-result-latest.json`. `outputs` lists all N brief paths and `summary` includes the brief count N.
|
|
668
|
+
|
|
669
|
+
If `{onCompleteCommand}` is non-empty, invoke it now with `--result-path={result_json_path}`.
|
|
670
|
+
|
|
671
|
+
### 11. Chain to Health Check
|
|
672
|
+
|
|
673
|
+
Load, read fully, then execute {nextStepFile} to run the shared workflow health check.
|
|
674
|
+
|
|
675
|
+
## CRITICAL STEP COMPLETION NOTE
|
|
676
|
+
|
|
677
|
+
ONLY WHEN the analysis report has been updated, the skill-brief.yaml written and validated, the result envelope emitted, and the result contract saved will you load and read fully {nextStepFile} to begin the health check.
|