bmad-module-skill-forge 1.2.0 → 1.4.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.
Files changed (48) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/docs/_data/pinned.yaml +1 -1
  3. package/docs/skill-model.md +26 -32
  4. package/docs/troubleshooting.md +12 -0
  5. package/docs/workflows.md +87 -15
  6. package/package.json +2 -2
  7. package/src/shared/references/output-contract-schema.md +10 -0
  8. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +58 -0
  9. package/src/shared/scripts/schemas/skill-brief.v1.json +77 -0
  10. package/src/shared/scripts/schemas/workspace-detection.v1.json +44 -0
  11. package/src/shared/scripts/skf-detect-language.py +277 -0
  12. package/src/shared/scripts/skf-detect-workspaces.py +427 -0
  13. package/src/shared/scripts/skf-emit-brief-result-envelope.py +257 -0
  14. package/src/shared/scripts/skf-extract-public-api.py +534 -0
  15. package/src/shared/scripts/skf-forge-tier-rw.py +73 -0
  16. package/src/shared/scripts/skf-recommend-scope-type.py +369 -0
  17. package/src/shared/scripts/skf-render-quick-metadata.py +192 -0
  18. package/src/shared/scripts/skf-resolve-package.py +264 -0
  19. package/src/shared/scripts/skf-validate-brief-inputs.py +293 -0
  20. package/src/shared/scripts/skf-validate-output.py +24 -7
  21. package/src/shared/scripts/skf-write-skill-brief.py +509 -0
  22. package/src/skf-brief-skill/SKILL.md +41 -12
  23. package/src/skf-brief-skill/assets/description-voice-examples.md +19 -0
  24. package/src/skf-brief-skill/assets/scope-templates.md +5 -0
  25. package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -40
  26. package/src/skf-brief-skill/references/draft-checkpoint.md +46 -0
  27. package/src/skf-brief-skill/references/headless-args.md +22 -0
  28. package/src/skf-brief-skill/references/headless-source-authority-detection.md +26 -0
  29. package/src/skf-brief-skill/references/portfolio-similarity-check.md +35 -0
  30. package/src/skf-brief-skill/references/qmd-collection-registration.md +52 -0
  31. package/src/skf-brief-skill/references/version-resolution.md +46 -0
  32. package/src/skf-brief-skill/steps-c/step-01-gather-intent.md +164 -14
  33. package/src/skf-brief-skill/steps-c/step-02-analyze-target.md +118 -50
  34. package/src/skf-brief-skill/steps-c/step-03-scope-definition.md +48 -5
  35. package/src/skf-brief-skill/steps-c/step-04-confirm-brief.md +33 -19
  36. package/src/skf-brief-skill/steps-c/step-05-write-brief.md +93 -97
  37. package/src/skf-brief-skill/steps-c/step-06-health-check.md +11 -2
  38. package/src/skf-quick-skill/SKILL.md +178 -10
  39. package/src/skf-quick-skill/assets/skill-template.md +5 -1
  40. package/src/skf-quick-skill/references/registry-resolution.md +2 -0
  41. package/src/skf-quick-skill/steps-c/step-01-resolve-target.md +84 -16
  42. package/src/skf-quick-skill/steps-c/step-02-ecosystem-check.md +3 -3
  43. package/src/skf-quick-skill/steps-c/step-03-quick-extract.md +86 -43
  44. package/src/skf-quick-skill/steps-c/step-04-compile.md +49 -56
  45. package/src/skf-quick-skill/steps-c/step-05-write-and-validate.md +164 -0
  46. package/src/skf-quick-skill/steps-c/{step-06-write.md → step-06-finalize.md} +15 -7
  47. package/src/skf-quick-skill/steps-c/step-07-health-check.md +5 -3
  48. package/src/skf-quick-skill/steps-c/step-05-validate.md +0 -193
@@ -0,0 +1,257 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Emit Brief Result Envelope — Schema-locked headless output for skf-brief-skill.
6
+
7
+ Replaces the prose-driven envelope assembly in `src/skf-brief-skill/steps-c/
8
+ step-05-write-brief.md` §4b with one Python invocation. The envelope contract
9
+ (SKF_BRIEF_RESULT_JSON) is documented in src/skf-brief-skill/SKILL.md Result
10
+ Contract section.
11
+
12
+ LLM-rendered envelopes risk silent schema drift on every invocation —
13
+ a pipeline that grep's `SKF_BRIEF_RESULT_JSON: {…}` out of the workflow
14
+ log can break if the model decides to rename a key, reorder properties,
15
+ or swap a null for an empty string. This script is the single source of
16
+ truth: it takes a context payload as JSON on stdin, derives the
17
+ exit_code from the halt_reason deterministically, validates the
18
+ assembled envelope against the JSON Schema at
19
+ `src/shared/scripts/schemas/skf-brief-result-envelope.v1.json`, and
20
+ emits the line in a fixed shape.
21
+
22
+ Subcommands:
23
+
24
+ emit Read context payload as JSON on stdin, derive exit_code,
25
+ validate against the schema, emit the
26
+ `SKF_BRIEF_RESULT_JSON: {one-line JSON}` prefix line on
27
+ stdout (or stderr if --target=stderr). Default subcommand.
28
+
29
+ validate Read an envelope (without the prefix) as JSON on stdin
30
+ and verify it against the schema. Silent + exit 0 on
31
+ success; non-zero exit + stderr error on failure. Useful
32
+ for paranoid pipelines that want to validate a received
33
+ envelope before consuming it.
34
+
35
+ Context payload shape (consumed by `emit`):
36
+
37
+ {
38
+ "status": "success" | "error",
39
+ "brief_path": "/abs/path/skill-brief.yaml" | null,
40
+ "skill_name": "marked",
41
+ "version": "1.2.3" | null,
42
+ "language": "javascript" | null,
43
+ "scope_type": "public-api" | null,
44
+ "halt_reason": null | "input-missing" | "input-invalid" |
45
+ "forge-tier-missing" | "target-inaccessible" |
46
+ "gh-auth-failed" | "write-failed" |
47
+ "overwrite-cancelled" | "user-cancelled"
48
+ }
49
+
50
+ The caller does NOT supply exit_code — the script derives it from
51
+ halt_reason via the canonical mapping (null→0; input-*→2;
52
+ forge-tier-missing/target-inaccessible/gh-auth-failed→3;
53
+ write-failed→4; overwrite-cancelled→5; user-cancelled→6).
54
+
55
+ Cross-platform: pure stdlib, no third-party deps.
56
+
57
+ CLI:
58
+
59
+ echo '{...}' | uv run skf-emit-brief-result-envelope.py emit
60
+ echo '{...}' | uv run skf-emit-brief-result-envelope.py emit --target stderr
61
+ echo '{...}' | uv run skf-emit-brief-result-envelope.py validate
62
+ """
63
+
64
+ from __future__ import annotations
65
+
66
+ import argparse
67
+ import json
68
+ import sys
69
+ from typing import Any
70
+
71
+ PREFIX = "SKF_BRIEF_RESULT_JSON: "
72
+
73
+ VALID_STATUS = {"success", "error"}
74
+ VALID_HALT_REASONS = {
75
+ None,
76
+ "input-missing",
77
+ "input-invalid",
78
+ "forge-tier-missing",
79
+ "target-inaccessible",
80
+ "gh-auth-failed",
81
+ "write-failed",
82
+ "overwrite-cancelled",
83
+ "user-cancelled",
84
+ }
85
+ VALID_SCOPE_TYPES = {
86
+ None,
87
+ "full-library",
88
+ "specific-modules",
89
+ "public-api",
90
+ "component-library",
91
+ "reference-app",
92
+ "docs-only",
93
+ }
94
+
95
+ # Canonical halt_reason → exit_code mapping (mirrors SKILL.md Exit Codes table).
96
+ HALT_TO_EXIT = {
97
+ None: 0,
98
+ "input-missing": 2,
99
+ "input-invalid": 2,
100
+ "forge-tier-missing": 3,
101
+ "target-inaccessible": 3,
102
+ "gh-auth-failed": 3,
103
+ "write-failed": 4,
104
+ "overwrite-cancelled": 5,
105
+ "user-cancelled": 6,
106
+ }
107
+
108
+ # Envelope key order — fixed so byte-stable diffs are possible.
109
+ KEY_ORDER = [
110
+ "status",
111
+ "brief_path",
112
+ "skill_name",
113
+ "version",
114
+ "language",
115
+ "scope_type",
116
+ "exit_code",
117
+ "halt_reason",
118
+ ]
119
+
120
+
121
+ def _die(message: str, code: int = 1) -> None:
122
+ sys.stderr.write(f"skf-emit-brief-result-envelope: {message}\n")
123
+ sys.exit(code)
124
+
125
+
126
+ def assemble(ctx: dict[str, Any]) -> dict[str, Any]:
127
+ """Build the envelope from a context payload, deriving exit_code."""
128
+ status = ctx.get("status")
129
+ if status not in VALID_STATUS:
130
+ _die(f"status must be one of {sorted(VALID_STATUS)}; got {status!r}")
131
+
132
+ halt_reason = ctx.get("halt_reason")
133
+ if halt_reason not in VALID_HALT_REASONS:
134
+ _die(
135
+ f"halt_reason must be one of {sorted(r for r in VALID_HALT_REASONS if r is not None)} or null; "
136
+ f"got {halt_reason!r}"
137
+ )
138
+
139
+ if status == "success" and halt_reason is not None:
140
+ _die(f"halt_reason must be null when status is 'success'; got {halt_reason!r}")
141
+ if status == "error" and halt_reason is None:
142
+ _die("halt_reason must be set when status is 'error'")
143
+
144
+ scope_type = ctx.get("scope_type")
145
+ if scope_type not in VALID_SCOPE_TYPES:
146
+ _die(
147
+ f"scope_type must be one of {sorted(t for t in VALID_SCOPE_TYPES if t is not None)} or null; "
148
+ f"got {scope_type!r}"
149
+ )
150
+
151
+ skill_name = ctx.get("skill_name")
152
+ if not skill_name or not isinstance(skill_name, str):
153
+ _die(f"skill_name is required and must be a non-empty string; got {skill_name!r}")
154
+
155
+ envelope = {
156
+ "status": status,
157
+ "brief_path": ctx.get("brief_path"),
158
+ "skill_name": skill_name,
159
+ "version": ctx.get("version"),
160
+ "language": ctx.get("language"),
161
+ "scope_type": scope_type,
162
+ "exit_code": HALT_TO_EXIT[halt_reason],
163
+ "halt_reason": halt_reason,
164
+ }
165
+ # Re-emit in canonical key order
166
+ return {k: envelope[k] for k in KEY_ORDER}
167
+
168
+
169
+ def validate(envelope: dict[str, Any]) -> None:
170
+ """Validate an envelope dict against the schema. Exits non-zero on failure."""
171
+ required = set(KEY_ORDER)
172
+ missing = required - set(envelope.keys())
173
+ if missing:
174
+ _die(f"envelope missing required keys: {sorted(missing)}")
175
+ extra = set(envelope.keys()) - required
176
+ if extra:
177
+ _die(f"envelope has unexpected keys: {sorted(extra)}")
178
+ if envelope.get("status") not in VALID_STATUS:
179
+ _die(f"status invalid: {envelope.get('status')!r}")
180
+ if envelope.get("halt_reason") not in VALID_HALT_REASONS:
181
+ _die(f"halt_reason invalid: {envelope.get('halt_reason')!r}")
182
+ if envelope.get("scope_type") not in VALID_SCOPE_TYPES:
183
+ _die(f"scope_type invalid: {envelope.get('scope_type')!r}")
184
+ if envelope.get("exit_code") not in {0, 2, 3, 4, 5, 6}:
185
+ _die(f"exit_code invalid: {envelope.get('exit_code')!r}")
186
+ expected_exit = HALT_TO_EXIT[envelope.get("halt_reason")]
187
+ if envelope.get("exit_code") != expected_exit:
188
+ _die(
189
+ f"exit_code {envelope.get('exit_code')!r} does not match canonical mapping "
190
+ f"for halt_reason {envelope.get('halt_reason')!r} (expected {expected_exit})"
191
+ )
192
+ if not envelope.get("skill_name") or not isinstance(envelope.get("skill_name"), str):
193
+ _die("skill_name must be a non-empty string")
194
+
195
+
196
+ def cmd_emit(target_stream: str) -> int:
197
+ raw = sys.stdin.read()
198
+ if not raw or not raw.strip():
199
+ _die("emit: empty stdin (expected JSON context payload)")
200
+ try:
201
+ ctx = json.loads(raw)
202
+ except json.JSONDecodeError as e:
203
+ _die(f"emit: invalid JSON on stdin: {e}")
204
+ if not isinstance(ctx, dict):
205
+ _die("emit: context payload must be a JSON object")
206
+
207
+ envelope = assemble(ctx)
208
+ line = PREFIX + json.dumps(envelope, separators=(",", ":"))
209
+ if target_stream == "stderr":
210
+ print(line, file=sys.stderr)
211
+ else:
212
+ print(line)
213
+ return 0
214
+
215
+
216
+ def cmd_validate() -> int:
217
+ raw = sys.stdin.read()
218
+ if not raw or not raw.strip():
219
+ _die("validate: empty stdin (expected envelope JSON)")
220
+ try:
221
+ envelope = json.loads(raw)
222
+ except json.JSONDecodeError as e:
223
+ _die(f"validate: invalid JSON on stdin: {e}")
224
+ if not isinstance(envelope, dict):
225
+ _die("validate: envelope must be a JSON object")
226
+ validate(envelope)
227
+ return 0
228
+
229
+
230
+ def main() -> int:
231
+ parser = argparse.ArgumentParser(
232
+ prog="skf-emit-brief-result-envelope",
233
+ description="Schema-locked SKF_BRIEF_RESULT_JSON envelope emitter for skf-brief-skill.",
234
+ )
235
+ sub = parser.add_subparsers(dest="cmd", required=True)
236
+
237
+ p_emit = sub.add_parser("emit", help="Read context JSON on stdin, emit prefixed envelope line")
238
+ p_emit.add_argument(
239
+ "--target",
240
+ choices=["stdout", "stderr"],
241
+ default="stdout",
242
+ help="Output stream for the prefixed envelope line. step-05 §4b uses stdout on success and stderr on HARD HALT.",
243
+ )
244
+
245
+ sub.add_parser("validate", help="Read envelope JSON on stdin, exit 0 if schema-valid")
246
+
247
+ args = parser.parse_args()
248
+
249
+ if args.cmd == "emit":
250
+ return cmd_emit(args.target)
251
+ elif args.cmd == "validate":
252
+ return cmd_validate()
253
+ return 2
254
+
255
+
256
+ if __name__ == "__main__":
257
+ sys.exit(main())