bmad-module-skill-forge 1.9.0 → 2.0.1
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/.nvmrc +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 +30 -7
- 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 +2 -2
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +64 -24
- package/package.json +2 -2
- package/src/knowledge/skill-lifecycle.md +1 -1
- package/src/module-help.csv +4 -3
- package/src/shared/data/language-corpora.json +32 -0
- package/src/shared/references/pipeline-contracts.md +6 -3
- package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
- package/src/shared/scripts/skf-comention-pairs.py +350 -0
- package/src/shared/scripts/skf-count-tokens.py +222 -0
- package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
- package/src/shared/scripts/skf-detect-docs.py +240 -4
- package/src/shared/scripts/skf-detect-language.py +82 -3
- package/src/shared/scripts/skf-detect-tools.py +105 -2
- package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
- package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
- package/src/shared/scripts/skf-extract-public-api.py +53 -0
- package/src/shared/scripts/skf-find-cycles.py +246 -0
- package/src/shared/scripts/skf-hash-content.py +487 -2
- package/src/shared/scripts/skf-language-corpora.py +100 -0
- package/src/shared/scripts/skf-manifest-ops.py +67 -1
- package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
- package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
- package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
- package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
- package/src/shared/scripts/skf-shape-detect.py +576 -25
- package/src/shared/scripts/skf-shard-body.py +461 -0
- package/src/shared/scripts/skf-skill-inventory.py +139 -3
- package/src/shared/scripts/skf-structural-diff.py +307 -67
- package/src/shared/scripts/skf-validate-brief-schema.py +41 -6
- package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
- package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
- package/src/shared/scripts/skf-validate-output.py +443 -10
- package/src/shared/scripts/skf-validate-pins.py +3 -3
- package/src/shared/scripts/skf-verify-no-trace.py +214 -0
- package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
- package/src/shared/scripts/skf-write-skill-brief.py +21 -5
- package/src/skf-analyze-source/SKILL.md +17 -32
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/customize.toml +2 -0
- package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
- package/src/skf-analyze-source/references/continue.md +8 -13
- package/src/skf-analyze-source/references/generate-briefs.md +28 -12
- package/src/skf-analyze-source/references/headless-contract.md +33 -0
- package/src/skf-analyze-source/references/health-check.md +1 -2
- package/src/skf-analyze-source/references/identify-units.md +25 -36
- package/src/skf-analyze-source/references/init.md +12 -25
- package/src/skf-analyze-source/references/map-and-detect.md +8 -20
- package/src/skf-analyze-source/references/recommend.md +4 -14
- package/src/skf-analyze-source/references/scan-project.md +3 -11
- package/src/skf-analyze-source/references/step-auto-scope.md +211 -197
- package/src/skf-analyze-source/references/step-shape-detect.md +20 -13
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
- package/src/skf-audit-skill/SKILL.md +9 -6
- package/src/skf-audit-skill/customize.toml +2 -0
- package/src/skf-audit-skill/references/init.md +58 -32
- package/src/skf-audit-skill/references/re-index.md +15 -34
- package/src/skf-audit-skill/references/report.md +11 -58
- package/src/skf-audit-skill/references/semantic-diff.md +4 -22
- package/src/skf-audit-skill/references/severity-classify.md +53 -87
- package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
- package/src/skf-audit-skill/references/structural-diff.md +43 -68
- package/src/skf-brief-skill/SKILL.md +7 -36
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
- package/src/skf-brief-skill/customize.toml +14 -0
- package/src/skf-brief-skill/references/analyze-target.md +13 -18
- package/src/skf-brief-skill/references/confirm-brief.md +2 -15
- package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
- package/src/skf-brief-skill/references/gather-intent.md +27 -24
- package/src/skf-brief-skill/references/headless-args.md +1 -1
- package/src/skf-brief-skill/references/health-check.md +1 -22
- package/src/skf-brief-skill/references/invocation-contract.md +37 -0
- package/src/skf-brief-skill/references/scope-definition.md +6 -15
- package/src/skf-brief-skill/references/step-auto-brief.md +22 -9
- package/src/skf-brief-skill/references/step-auto-validate.md +3 -7
- package/src/skf-brief-skill/references/version-resolution.md +1 -1
- package/src/skf-brief-skill/references/write-brief.md +19 -13
- package/src/skf-campaign/SKILL.md +121 -0
- package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
- package/src/skf-campaign/customize.toml +75 -0
- package/src/skf-campaign/manifest.yaml +15 -0
- package/src/skf-campaign/references/campaign-contracts.md +67 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +32 -0
- package/src/skf-campaign/references/health-check.md +35 -0
- package/src/skf-campaign/references/step-01-setup.md +121 -0
- package/src/skf-campaign/references/step-02-strategy.md +102 -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 +88 -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 +115 -0
- package/src/skf-campaign/scripts/.gitkeep +0 -0
- package/src/skf-campaign/scripts/campaign-deps.py +244 -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-batch.py +199 -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-status.py +162 -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 +15 -4
- package/src/skf-create-skill/assets/compile-assembly-rules.md +35 -34
- package/src/skf-create-skill/assets/skill-sections.md +9 -100
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
- package/src/skf-create-skill/customize.toml +13 -0
- package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
- package/src/skf-create-skill/references/compile.md +37 -47
- package/src/skf-create-skill/references/component-extraction.md +8 -35
- package/src/skf-create-skill/references/ecosystem-check.md +9 -25
- package/src/skf-create-skill/references/enrich.md +4 -17
- package/src/skf-create-skill/references/extract.md +20 -27
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
- package/src/skf-create-skill/references/extraction-patterns.md +10 -8
- package/src/skf-create-skill/references/generate-artifacts.md +20 -25
- package/src/skf-create-skill/references/health-check.md +2 -2
- package/src/skf-create-skill/references/load-brief.md +16 -18
- package/src/skf-create-skill/references/report.md +27 -14
- package/src/skf-create-skill/references/source-resolution-protocols.md +4 -4
- package/src/skf-create-skill/references/step-auto-shard.md +52 -25
- package/src/skf-create-skill/references/step-doc-rot.md +33 -15
- package/src/skf-create-skill/references/step-doc-sources.md +13 -4
- package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
- package/src/skf-create-skill/references/sub/fetch-docs.md +33 -46
- package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
- package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
- package/src/skf-create-skill/references/validate.md +69 -29
- package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
- package/src/skf-create-stack-skill/SKILL.md +15 -9
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
- package/src/skf-create-stack-skill/customize.toml +10 -0
- package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
- package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
- package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
- package/src/skf-create-stack-skill/references/generate-output.md +24 -75
- package/src/skf-create-stack-skill/references/init.md +9 -81
- package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
- package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
- package/src/skf-create-stack-skill/references/report.md +27 -62
- package/src/skf-create-stack-skill/references/validate.md +35 -31
- package/src/skf-drop-skill/SKILL.md +20 -23
- package/src/skf-drop-skill/customize.toml +10 -0
- package/src/skf-drop-skill/references/execute.md +67 -36
- package/src/skf-drop-skill/references/headless-contract.md +18 -0
- package/src/skf-drop-skill/references/health-check.md +1 -13
- package/src/skf-drop-skill/references/report.md +44 -10
- package/src/skf-drop-skill/references/select.md +93 -48
- package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
- package/src/skf-export-skill/SKILL.md +17 -14
- package/src/skf-export-skill/assets/managed-section-format.md +2 -5
- package/src/skf-export-skill/customize.toml +10 -6
- package/src/skf-export-skill/references/generate-snippet.md +21 -35
- package/src/skf-export-skill/references/load-skill.md +44 -42
- package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
- package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
- package/src/skf-export-skill/references/package.md +27 -33
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
- package/src/skf-export-skill/references/result-envelope.md +28 -0
- package/src/skf-export-skill/references/summary.md +11 -5
- package/src/skf-export-skill/references/token-report.md +42 -24
- package/src/skf-export-skill/references/update-context.md +32 -33
- package/src/skf-forger/SKILL.md +32 -39
- package/src/skf-forger/references/pipeline-mode.md +34 -0
- package/src/skf-forger/scripts/parse-pipeline.py +270 -0
- package/src/skf-quick-skill/SKILL.md +10 -60
- package/src/skf-quick-skill/customize.toml +9 -0
- package/src/skf-quick-skill/references/batch-mode.md +6 -4
- package/src/skf-quick-skill/references/compile.md +15 -52
- package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
- package/src/skf-quick-skill/references/finalize.md +12 -6
- package/src/skf-quick-skill/references/halt-contract.md +54 -0
- package/src/skf-quick-skill/references/health-check.md +3 -4
- package/src/skf-quick-skill/references/quick-extract.md +7 -18
- package/src/skf-quick-skill/references/registry-resolution.md +1 -23
- package/src/skf-quick-skill/references/resolve-target.md +27 -34
- package/src/skf-quick-skill/references/write-and-validate.md +10 -25
- package/src/skf-refine-architecture/SKILL.md +16 -8
- package/src/skf-refine-architecture/customize.toml +9 -0
- package/src/skf-refine-architecture/references/compile.md +10 -8
- package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
- package/src/skf-refine-architecture/references/health-check.md +1 -1
- package/src/skf-refine-architecture/references/improvements.md +9 -25
- package/src/skf-refine-architecture/references/init.md +9 -16
- package/src/skf-refine-architecture/references/issue-detection.md +10 -25
- package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
- package/src/skf-refine-architecture/references/report.md +14 -11
- package/src/skf-rename-skill/SKILL.md +11 -21
- package/src/skf-rename-skill/customize.toml +10 -0
- package/src/skf-rename-skill/references/execute.md +112 -82
- package/src/skf-rename-skill/references/exit-codes.md +12 -0
- package/src/skf-rename-skill/references/report.md +16 -8
- package/src/skf-rename-skill/references/select.md +69 -42
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
- package/src/skf-setup/SKILL.md +21 -13
- package/src/skf-setup/customize.toml +10 -0
- package/src/skf-setup/references/auto-index.md +11 -10
- package/src/skf-setup/references/ccc-index.md +14 -15
- package/src/skf-setup/references/detect-and-tier.md +10 -8
- package/src/skf-setup/references/report.md +26 -32
- package/src/skf-setup/references/tier-rules.md +2 -32
- package/src/skf-setup/references/write-config.md +9 -10
- package/src/skf-test-skill/SKILL.md +10 -8
- package/src/skf-test-skill/customize.toml +2 -0
- package/src/skf-test-skill/references/coherence-check.md +19 -31
- package/src/skf-test-skill/references/coverage-check.md +111 -67
- package/src/skf-test-skill/references/detect-mode.md +10 -41
- package/src/skf-test-skill/references/external-validators.md +11 -19
- package/src/skf-test-skill/references/init.md +39 -21
- package/src/skf-test-skill/references/migration-section-rules.md +4 -4
- package/src/skf-test-skill/references/report.md +60 -40
- package/src/skf-test-skill/references/score.md +32 -64
- package/src/skf-test-skill/references/scoring-rules.md +6 -62
- package/src/skf-test-skill/references/source-access-protocol.md +15 -5
- package/src/skf-test-skill/references/step-hard-gate.md +1 -3
- package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
- package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
- package/src/skf-test-skill/scripts/compute-score.py +135 -4
- package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
- package/src/skf-update-skill/SKILL.md +17 -5
- package/src/skf-update-skill/customize.toml +11 -6
- package/src/skf-update-skill/references/detect-changes.md +48 -36
- package/src/skf-update-skill/references/health-check.md +5 -11
- package/src/skf-update-skill/references/init.md +40 -32
- package/src/skf-update-skill/references/manual-section-rules.md +1 -2
- package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
- package/src/skf-update-skill/references/merge.md +13 -57
- package/src/skf-update-skill/references/re-extract.md +27 -25
- package/src/skf-update-skill/references/report.md +14 -7
- package/src/skf-update-skill/references/validate.md +32 -30
- package/src/skf-update-skill/references/write.md +86 -51
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
- package/src/skf-verify-stack/SKILL.md +11 -21
- package/src/skf-verify-stack/customize.toml +12 -3
- package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
- package/src/skf-verify-stack/references/coverage.md +21 -10
- package/src/skf-verify-stack/references/exit-codes.md +14 -0
- package/src/skf-verify-stack/references/health-check.md +1 -1
- package/src/skf-verify-stack/references/init.md +20 -25
- package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
- package/src/skf-verify-stack/references/integrations.md +44 -49
- package/src/skf-verify-stack/references/report.md +28 -34
- package/src/skf-verify-stack/references/requirements.md +3 -3
- package/src/skf-verify-stack/references/synthesize.md +36 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
- package/tools/cli/lib/ui.js +3 -2
- package/docs/deepwiki.md +0 -89
- package/src/skf-rename-skill/references/rebuild-context.md +0 -110
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.9"
|
|
3
|
+
# dependencies = ["pyyaml"]
|
|
4
|
+
# ///
|
|
5
|
+
"""Campaign Status — deterministic state summary + optional backup-drift verdict.
|
|
6
|
+
|
|
7
|
+
Collapses two in-prompt determinism leaks into one helper the LLM calls instead
|
|
8
|
+
of hand-deriving:
|
|
9
|
+
|
|
10
|
+
- `campaign status` and the resume summary block tallied `skills[]` by status
|
|
11
|
+
("N completed / M total, K pending, ...") in-prompt over a 15+ skill array.
|
|
12
|
+
- the `.bak` recovery path compared `primary` vs `backup` timestamps and stage
|
|
13
|
+
numbers by hand (an ISO-8601 + int compare across two YAML files).
|
|
14
|
+
|
|
15
|
+
Both are mechanical, one-correct-answer computations. This script owns them so
|
|
16
|
+
identical state always yields identical output.
|
|
17
|
+
|
|
18
|
+
CLI:
|
|
19
|
+
uv run campaign-status.py --state-file <path>
|
|
20
|
+
uv run campaign-status.py --state-file <path> --backup-file <path>
|
|
21
|
+
|
|
22
|
+
Output (JSON on stdout):
|
|
23
|
+
{
|
|
24
|
+
"campaign_name": "...",
|
|
25
|
+
"current_stage": 0,
|
|
26
|
+
"last_updated": "...",
|
|
27
|
+
"total": 0, "completed": 0, "pending": 0,
|
|
28
|
+
"active": 0, "failed": 0, "skipped": 0,
|
|
29
|
+
"backup_comparison": null | {
|
|
30
|
+
"primary_behind": bool,
|
|
31
|
+
"primary_last_updated": "...", "backup_last_updated": "...",
|
|
32
|
+
"primary_stage": 0, "backup_stage": 0
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Exit codes:
|
|
37
|
+
0 ok
|
|
38
|
+
2 error (state missing / unreadable / not a mapping)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import argparse
|
|
44
|
+
import json
|
|
45
|
+
import sys
|
|
46
|
+
from datetime import datetime
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
from typing import Any, Dict, Optional
|
|
49
|
+
|
|
50
|
+
import yaml
|
|
51
|
+
|
|
52
|
+
STATUSES = ("pending", "active", "completed", "failed", "skipped")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _err(message: str, code: str) -> int:
|
|
56
|
+
json.dump({"error": message, "code": code}, sys.stderr)
|
|
57
|
+
sys.stderr.write("\n")
|
|
58
|
+
return 2
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load_state(path: Path) -> Optional[Dict[str, Any]]:
|
|
62
|
+
if not path.is_file():
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
66
|
+
except (yaml.YAMLError, OSError):
|
|
67
|
+
return None
|
|
68
|
+
return data if isinstance(data, dict) else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _parse_iso(value: Any) -> Optional[datetime]:
|
|
72
|
+
if not isinstance(value, str) or not value:
|
|
73
|
+
return None
|
|
74
|
+
text = value[:-1] + "+00:00" if value.endswith("Z") else value
|
|
75
|
+
try:
|
|
76
|
+
return datetime.fromisoformat(text)
|
|
77
|
+
except (ValueError, TypeError):
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _counts(state: Dict[str, Any]) -> Dict[str, int]:
|
|
82
|
+
skills = state.get("skills", []) or []
|
|
83
|
+
tally = {s: 0 for s in STATUSES}
|
|
84
|
+
for skill in skills:
|
|
85
|
+
status = skill.get("status")
|
|
86
|
+
if status in tally:
|
|
87
|
+
tally[status] += 1
|
|
88
|
+
tally["total"] = len(skills)
|
|
89
|
+
return tally
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _compare_backup(primary: Dict[str, Any], backup: Dict[str, Any]) -> Dict[str, Any]:
|
|
93
|
+
p_c = primary.get("campaign", {}) or {}
|
|
94
|
+
b_c = backup.get("campaign", {}) or {}
|
|
95
|
+
p_updated = p_c.get("last_updated")
|
|
96
|
+
b_updated = b_c.get("last_updated")
|
|
97
|
+
p_stage = p_c.get("current_stage")
|
|
98
|
+
b_stage = b_c.get("current_stage")
|
|
99
|
+
|
|
100
|
+
stage_behind = (
|
|
101
|
+
isinstance(p_stage, int) and isinstance(b_stage, int) and p_stage < b_stage
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
ts_behind = False
|
|
105
|
+
p_ts = _parse_iso(p_updated)
|
|
106
|
+
b_ts = _parse_iso(b_updated)
|
|
107
|
+
if p_ts is not None and b_ts is not None:
|
|
108
|
+
try:
|
|
109
|
+
ts_behind = p_ts < b_ts
|
|
110
|
+
except TypeError:
|
|
111
|
+
# naive vs aware datetime — cannot order; defer to stage compare.
|
|
112
|
+
ts_behind = False
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"primary_behind": bool(stage_behind or ts_behind),
|
|
116
|
+
"primary_last_updated": p_updated,
|
|
117
|
+
"backup_last_updated": b_updated,
|
|
118
|
+
"primary_stage": p_stage,
|
|
119
|
+
"backup_stage": b_stage,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def run(state_file: str, backup_file: Optional[str]) -> int:
|
|
124
|
+
state = _load_state(Path(state_file))
|
|
125
|
+
if state is None:
|
|
126
|
+
return _err(f"State not readable as a mapping: {state_file}", "STATE_UNREADABLE")
|
|
127
|
+
|
|
128
|
+
campaign = state.get("campaign", {}) or {}
|
|
129
|
+
result: Dict[str, Any] = {
|
|
130
|
+
"campaign_name": campaign.get("name", ""),
|
|
131
|
+
"current_stage": campaign.get("current_stage"),
|
|
132
|
+
"last_updated": campaign.get("last_updated"),
|
|
133
|
+
**_counts(state),
|
|
134
|
+
"backup_comparison": None,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if backup_file:
|
|
138
|
+
backup = _load_state(Path(backup_file))
|
|
139
|
+
if backup is not None:
|
|
140
|
+
result["backup_comparison"] = _compare_backup(state, backup)
|
|
141
|
+
|
|
142
|
+
json.dump(result, sys.stdout, separators=(",", ":"), default=str)
|
|
143
|
+
sys.stdout.write("\n")
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main(argv: Optional[list] = None) -> int:
|
|
148
|
+
parser = argparse.ArgumentParser(
|
|
149
|
+
prog="campaign-status",
|
|
150
|
+
description="Summarize campaign state and (optionally) compare it to its backup.",
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument("--state-file", required=True, help="Path to _campaign-state.yaml")
|
|
153
|
+
parser.add_argument(
|
|
154
|
+
"--backup-file",
|
|
155
|
+
help="Path to _campaign-state.yaml.bak; when given, emit backup_comparison",
|
|
156
|
+
)
|
|
157
|
+
args = parser.parse_args(argv)
|
|
158
|
+
return run(args.state_file, args.backup_file)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.9"
|
|
3
|
+
# dependencies = ["pyyaml"]
|
|
4
|
+
# ///
|
|
5
|
+
"""Campaign Validate Pins — validate all version pins in a campaign state file.
|
|
6
|
+
|
|
7
|
+
Campaign-specific wrapper around the shared skf-validate-pins.py module.
|
|
8
|
+
Reads the campaign state and brief, validates every skill's pin against
|
|
9
|
+
real GitHub releases/tags, and outputs consolidated JSON.
|
|
10
|
+
|
|
11
|
+
CLI:
|
|
12
|
+
uv run src/skf-campaign/scripts/campaign-validate-pins.py \
|
|
13
|
+
--state-file <path> --brief-file <path>
|
|
14
|
+
|
|
15
|
+
Input:
|
|
16
|
+
--state-file Path to _campaign-state.yaml
|
|
17
|
+
--brief-file Path to campaign-brief.yaml
|
|
18
|
+
|
|
19
|
+
Output (JSON on stdout):
|
|
20
|
+
{
|
|
21
|
+
"results": [
|
|
22
|
+
{
|
|
23
|
+
"name": "skill-name",
|
|
24
|
+
"status": "valid|invalid|resolved",
|
|
25
|
+
"pin": "input-pin-or-null",
|
|
26
|
+
"resolved_ref": "actual-tag-or-branch",
|
|
27
|
+
"ref_type": "tag|branch|null",
|
|
28
|
+
"version": "semver-or-null",
|
|
29
|
+
"suggestions": []
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"all_valid": true,
|
|
33
|
+
"invalid_count": 0,
|
|
34
|
+
"resolved_count": 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Exit codes:
|
|
38
|
+
0 all valid/resolved
|
|
39
|
+
1 one or more invalid pins
|
|
40
|
+
2 error (missing files, bad YAML, gh unavailable)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import argparse
|
|
46
|
+
import importlib.util
|
|
47
|
+
import json
|
|
48
|
+
import sys
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
from typing import Any, Dict, List
|
|
51
|
+
|
|
52
|
+
import yaml
|
|
53
|
+
|
|
54
|
+
SHARED_SCRIPTS = Path(__file__).parent.parent.parent / "shared" / "scripts"
|
|
55
|
+
VALIDATE_PINS_PATH = SHARED_SCRIPTS / "skf-validate-pins.py"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _load_validate_pin():
|
|
59
|
+
spec = importlib.util.spec_from_file_location("skf_validate_pins", VALIDATE_PINS_PATH)
|
|
60
|
+
mod = importlib.util.module_from_spec(spec)
|
|
61
|
+
spec.loader.exec_module(mod)
|
|
62
|
+
return mod.validate_pin
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _emit_error(message: str, code: str) -> None:
|
|
66
|
+
json.dump({"error": message, "code": code}, sys.stderr)
|
|
67
|
+
sys.stderr.write("\n")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _load_yaml(path: Path) -> Any:
|
|
71
|
+
with open(path, encoding="utf-8") as f:
|
|
72
|
+
return yaml.safe_load(f)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run(state_file: str, brief_file: str) -> int:
|
|
76
|
+
state_path = Path(state_file)
|
|
77
|
+
brief_path = Path(brief_file)
|
|
78
|
+
|
|
79
|
+
if not state_path.is_file():
|
|
80
|
+
_emit_error(f"State file not found: {state_file}", "STATE_NOT_FOUND")
|
|
81
|
+
return 2
|
|
82
|
+
|
|
83
|
+
if not brief_path.is_file():
|
|
84
|
+
_emit_error(f"Brief file not found: {brief_file}", "BRIEF_NOT_FOUND")
|
|
85
|
+
return 2
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
state = _load_yaml(state_path)
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
_emit_error(f"Failed to parse state file: {exc}", "STATE_PARSE_ERROR")
|
|
91
|
+
return 2
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
brief = _load_yaml(brief_path)
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
_emit_error(f"Failed to parse brief file: {exc}", "BRIEF_PARSE_ERROR")
|
|
97
|
+
return 2
|
|
98
|
+
|
|
99
|
+
skills = state.get("skills", [])
|
|
100
|
+
if not isinstance(skills, list):
|
|
101
|
+
_emit_error("State file 'skills' is not an array", "INVALID_STATE")
|
|
102
|
+
return 2
|
|
103
|
+
|
|
104
|
+
targets = brief.get("targets", [])
|
|
105
|
+
if not isinstance(targets, list):
|
|
106
|
+
_emit_error("Brief file 'targets' is not an array", "INVALID_BRIEF")
|
|
107
|
+
return 2
|
|
108
|
+
|
|
109
|
+
name_to_repo: Dict[str, str] = {}
|
|
110
|
+
for target in targets:
|
|
111
|
+
name_to_repo[target["name"]] = target["repo_url"]
|
|
112
|
+
|
|
113
|
+
if not VALIDATE_PINS_PATH.is_file():
|
|
114
|
+
_emit_error(
|
|
115
|
+
f"Shared module not found: {VALIDATE_PINS_PATH.as_posix()}",
|
|
116
|
+
"SHARED_MODULE_NOT_FOUND",
|
|
117
|
+
)
|
|
118
|
+
return 2
|
|
119
|
+
|
|
120
|
+
validate_pin = _load_validate_pin()
|
|
121
|
+
|
|
122
|
+
results: List[Dict[str, Any]] = []
|
|
123
|
+
invalid_count = 0
|
|
124
|
+
resolved_count = 0
|
|
125
|
+
|
|
126
|
+
for skill in skills:
|
|
127
|
+
skill_name = skill["name"]
|
|
128
|
+
repo_url = name_to_repo.get(skill_name)
|
|
129
|
+
if repo_url is None:
|
|
130
|
+
_emit_error(
|
|
131
|
+
f"Skill '{skill_name}' not found in brief targets",
|
|
132
|
+
"SKILL_NOT_IN_BRIEF",
|
|
133
|
+
)
|
|
134
|
+
return 2
|
|
135
|
+
|
|
136
|
+
pin = skill.get("pin")
|
|
137
|
+
result = validate_pin(repo_url, pin=pin)
|
|
138
|
+
result["name"] = skill_name
|
|
139
|
+
|
|
140
|
+
if result["status"] == "invalid":
|
|
141
|
+
invalid_count += 1
|
|
142
|
+
elif result["status"] == "resolved":
|
|
143
|
+
resolved_count += 1
|
|
144
|
+
|
|
145
|
+
results.append(result)
|
|
146
|
+
|
|
147
|
+
all_valid = invalid_count == 0
|
|
148
|
+
output = {
|
|
149
|
+
"results": results,
|
|
150
|
+
"all_valid": all_valid,
|
|
151
|
+
"invalid_count": invalid_count,
|
|
152
|
+
"resolved_count": resolved_count,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
json.dump(output, sys.stdout, separators=(",", ":"))
|
|
156
|
+
sys.stdout.write("\n")
|
|
157
|
+
|
|
158
|
+
if not all_valid:
|
|
159
|
+
return 1
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def main() -> int:
|
|
164
|
+
parser = argparse.ArgumentParser(
|
|
165
|
+
description="Validate all version pins in a campaign state file.",
|
|
166
|
+
)
|
|
167
|
+
parser.add_argument("--state-file", required=True, help="Path to _campaign-state.yaml")
|
|
168
|
+
parser.add_argument("--brief-file", required=True, help="Path to campaign-brief.yaml")
|
|
169
|
+
args = parser.parse_args()
|
|
170
|
+
return run(args.state_file, args.brief_file)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.9"
|
|
3
|
+
# dependencies = ["pyyaml", "jsonschema>=4.0"]
|
|
4
|
+
# ///
|
|
5
|
+
"""Campaign Validate State — schema check for _campaign-state.yaml on disk.
|
|
6
|
+
|
|
7
|
+
Replaces the per-step "mentally validate the loaded state against the schema"
|
|
8
|
+
prose with a deterministic check. Every campaign step that loads state runs
|
|
9
|
+
this once on entry instead of asking the LLM to validate a draft-07 schema
|
|
10
|
+
(nested objects, enums, additionalProperties:false, date-time) by hand — the
|
|
11
|
+
exact check an LLM does unreliably and which, when wrong, silently corrupts a
|
|
12
|
+
multi-session campaign.
|
|
13
|
+
|
|
14
|
+
Loads the campaign state YAML, validates it against
|
|
15
|
+
`assets/campaign-state-schema.json` (resolved relative to this script unless
|
|
16
|
+
`--schema-file` overrides), and emits skill-friendly error records the calling
|
|
17
|
+
step can forward verbatim.
|
|
18
|
+
|
|
19
|
+
CLI:
|
|
20
|
+
uv run campaign-validate-state.py --state-file <path>
|
|
21
|
+
uv run campaign-validate-state.py --state-file <path> --schema-file <path>
|
|
22
|
+
|
|
23
|
+
Output (JSON on stdout):
|
|
24
|
+
{
|
|
25
|
+
"valid": bool,
|
|
26
|
+
"errors": [{"field": "campaign.current_stage", "message": "..."}, ...],
|
|
27
|
+
"halt_reason": "state-missing" | "state-malformed" | "state-invalid" | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Exit codes:
|
|
31
|
+
0 valid (errors empty)
|
|
32
|
+
1 invalid (schema violations) OR file/yaml load failed
|
|
33
|
+
2 configuration error (schema file missing or unreadable)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import argparse
|
|
39
|
+
import datetime
|
|
40
|
+
import json
|
|
41
|
+
import sys
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import Any
|
|
44
|
+
|
|
45
|
+
import yaml
|
|
46
|
+
from jsonschema import Draft7Validator
|
|
47
|
+
|
|
48
|
+
DEFAULT_SCHEMA_PATH = Path(__file__).resolve().parent.parent / "assets" / "campaign-state-schema.json"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _emit(envelope: dict) -> None:
|
|
52
|
+
json.dump(envelope, sys.stdout, separators=(",", ":"), default=str)
|
|
53
|
+
sys.stdout.write("\n")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _load_yaml_text(text: str) -> tuple[Any, str | None]:
|
|
57
|
+
try:
|
|
58
|
+
data = yaml.safe_load(text)
|
|
59
|
+
except yaml.YAMLError as exc:
|
|
60
|
+
return None, f"State is not valid YAML: {exc}"
|
|
61
|
+
if data is None:
|
|
62
|
+
return None, "State file is empty"
|
|
63
|
+
if not isinstance(data, dict):
|
|
64
|
+
return None, f"State root must be a YAML mapping; got {type(data).__name__}"
|
|
65
|
+
return data, None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _field_path(error_path) -> str:
|
|
69
|
+
parts: list[str] = []
|
|
70
|
+
for p in error_path:
|
|
71
|
+
if isinstance(p, int):
|
|
72
|
+
parts.append(f"[{p}]")
|
|
73
|
+
else:
|
|
74
|
+
parts.append(f".{p}" if parts else str(p))
|
|
75
|
+
return "".join(parts) or "(root)"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _translate(err) -> dict:
|
|
79
|
+
field = _field_path(err.absolute_path)
|
|
80
|
+
validator = err.validator
|
|
81
|
+
inst = err.instance
|
|
82
|
+
|
|
83
|
+
if validator == "required":
|
|
84
|
+
missing = err.message.split("'")[1] if "'" in err.message else "(unknown)"
|
|
85
|
+
return {
|
|
86
|
+
"field": missing,
|
|
87
|
+
"message": f"State validation failed: missing required field `{missing}`.",
|
|
88
|
+
}
|
|
89
|
+
if validator == "enum":
|
|
90
|
+
return {
|
|
91
|
+
"field": field,
|
|
92
|
+
"message": (
|
|
93
|
+
f"State validation failed: `{field}` value `{inst}` is not one of "
|
|
94
|
+
f"{err.validator_value}."
|
|
95
|
+
),
|
|
96
|
+
}
|
|
97
|
+
if validator == "type":
|
|
98
|
+
if isinstance(inst, datetime.date) and "string" in str(err.validator_value):
|
|
99
|
+
return {
|
|
100
|
+
"field": field,
|
|
101
|
+
"message": (
|
|
102
|
+
f"State validation failed: `{field}` parsed as a YAML date, not a "
|
|
103
|
+
f"string. Quote it (e.g. `'2026-05-01T00:00:00Z'`)."
|
|
104
|
+
),
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
"field": field,
|
|
108
|
+
"message": (
|
|
109
|
+
f"State validation failed: `{field}` has type `{type(inst).__name__}`, "
|
|
110
|
+
f"expected `{err.validator_value}`."
|
|
111
|
+
),
|
|
112
|
+
}
|
|
113
|
+
if validator == "additionalProperties":
|
|
114
|
+
return {
|
|
115
|
+
"field": field,
|
|
116
|
+
"message": f"State validation failed: `{field}` — {err.message}",
|
|
117
|
+
}
|
|
118
|
+
if validator in ("minimum", "maximum"):
|
|
119
|
+
return {
|
|
120
|
+
"field": field,
|
|
121
|
+
"message": f"State validation failed: `{field}` value `{inst}` violates {validator} `{err.validator_value}`.",
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
"field": field,
|
|
125
|
+
"message": f"State validation failed: `{field}` — {err.message}",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def validate_state(state: dict, schema: dict) -> dict:
|
|
130
|
+
validator = Draft7Validator(schema)
|
|
131
|
+
errors = [
|
|
132
|
+
_translate(err)
|
|
133
|
+
for err in sorted(validator.iter_errors(state), key=lambda e: list(e.absolute_path))
|
|
134
|
+
]
|
|
135
|
+
return {"valid": not errors, "errors": errors}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def run(state_file: str, schema_file: str | None = None) -> int:
|
|
139
|
+
schema_path = Path(schema_file) if schema_file else DEFAULT_SCHEMA_PATH
|
|
140
|
+
if not schema_path.is_file():
|
|
141
|
+
_emit(
|
|
142
|
+
{
|
|
143
|
+
"valid": False,
|
|
144
|
+
"errors": [{"field": "(schema)", "message": f"Schema not found at `{schema_path}`."}],
|
|
145
|
+
"halt_reason": "state-invalid",
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
return 2
|
|
149
|
+
try:
|
|
150
|
+
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
151
|
+
except (OSError, ValueError) as exc:
|
|
152
|
+
_emit(
|
|
153
|
+
{
|
|
154
|
+
"valid": False,
|
|
155
|
+
"errors": [{"field": "(schema)", "message": f"Schema unreadable: {exc}"}],
|
|
156
|
+
"halt_reason": "state-invalid",
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
return 2
|
|
160
|
+
|
|
161
|
+
state_path = Path(state_file)
|
|
162
|
+
if not state_path.is_file():
|
|
163
|
+
_emit(
|
|
164
|
+
{
|
|
165
|
+
"valid": False,
|
|
166
|
+
"errors": [{"field": "(file)", "message": f"State not found at `{state_path}`."}],
|
|
167
|
+
"halt_reason": "state-missing",
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
return 1
|
|
171
|
+
|
|
172
|
+
state, load_err = _load_yaml_text(state_path.read_text(encoding="utf-8"))
|
|
173
|
+
if load_err is not None:
|
|
174
|
+
_emit(
|
|
175
|
+
{
|
|
176
|
+
"valid": False,
|
|
177
|
+
"errors": [{"field": "(file)", "message": load_err}],
|
|
178
|
+
"halt_reason": "state-malformed",
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
return 1
|
|
182
|
+
|
|
183
|
+
result = validate_state(state, schema)
|
|
184
|
+
if not result["valid"]:
|
|
185
|
+
_emit({**result, "halt_reason": "state-invalid"})
|
|
186
|
+
return 1
|
|
187
|
+
|
|
188
|
+
_emit({**result, "halt_reason": None})
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main(argv: list[str] | None = None) -> int:
|
|
193
|
+
parser = argparse.ArgumentParser(
|
|
194
|
+
prog="campaign-validate-state",
|
|
195
|
+
description="Validate _campaign-state.yaml against the campaign state schema.",
|
|
196
|
+
)
|
|
197
|
+
parser.add_argument("--state-file", required=True, help="Path to _campaign-state.yaml")
|
|
198
|
+
parser.add_argument(
|
|
199
|
+
"--schema-file",
|
|
200
|
+
help="Path to campaign-state-schema.json (defaults to the bundled schema)",
|
|
201
|
+
)
|
|
202
|
+
args = parser.parse_args(argv)
|
|
203
|
+
return run(args.state_file, args.schema_file)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Machine-generated campaign brief — resume context for fresh sessions
|
|
2
|
+
# Populated by step-01-setup.md from operator inputs
|
|
3
|
+
|
|
4
|
+
# Campaign identifier
|
|
5
|
+
campaign_name: ""
|
|
6
|
+
|
|
7
|
+
# ISO-8601 timestamp of campaign creation
|
|
8
|
+
created_at: ""
|
|
9
|
+
|
|
10
|
+
# Target skills for this campaign run
|
|
11
|
+
targets: []
|
|
12
|
+
# Each target entry:
|
|
13
|
+
# name: ""
|
|
14
|
+
# repo_url: ""
|
|
15
|
+
# tier: "A" # A (full pipeline) or B (batch)
|
|
16
|
+
# pin: null # version pin, or null for latest
|
|
17
|
+
# depends_on: [] # list of skill names this target depends on
|
|
18
|
+
|
|
19
|
+
# Quality thresholds applied to all skills in this campaign
|
|
20
|
+
quality_gate:
|
|
21
|
+
hard: "zero-critical-high"
|
|
22
|
+
soft_target: 90
|
|
23
|
+
soft_fallback: 80
|
|
24
|
+
|
|
25
|
+
# Where health findings are routed: "local" (project-local) or "improvement" (shared queue)
|
|
26
|
+
health_findings_queue: "local"
|
|
27
|
+
|
|
28
|
+
# Optional path to the architecture document the verify (Stage 7) and refine
|
|
29
|
+
# (Stage 8) stages consume. Empty string means those stages discover it at
|
|
30
|
+
# runtime (docs/architecture.md, then _bmad-output/planning-artifacts/architecture.md).
|
|
31
|
+
architecture_doc_path: ""
|
|
32
|
+
|
|
33
|
+
# Operator-provided context or directives for this campaign
|
|
34
|
+
notes: ""
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Campaign Report — {{campaign_name}}
|
|
2
|
+
|
|
3
|
+
## Campaign Summary
|
|
4
|
+
|
|
5
|
+
| Field | Value |
|
|
6
|
+
|-------|-------|
|
|
7
|
+
| **Campaign** | {{campaign_name}} |
|
|
8
|
+
| **Started** | {{started_at}} |
|
|
9
|
+
| **Completed** | {{completed_at}} |
|
|
10
|
+
| **Duration** | {{duration}} |
|
|
11
|
+
| **Quality Gate (Hard)** | {{quality_gate_hard}} |
|
|
12
|
+
| **Quality Gate (Soft Target)** | {{quality_gate_soft_target}} |
|
|
13
|
+
| **Quality Gate (Soft Fallback)** | {{quality_gate_soft_fallback}} |
|
|
14
|
+
| **Skills Completed** | {{skills_completed}} |
|
|
15
|
+
| **Skills Failed** | {{skills_failed}} |
|
|
16
|
+
| **Skills Skipped** | {{skills_skipped}} |
|
|
17
|
+
|
|
18
|
+
## Skills Overview
|
|
19
|
+
|
|
20
|
+
| Name | Tier | Status | Quality Score | Pin | Workarounds |
|
|
21
|
+
|------|------|--------|---------------|-----|-------------|
|
|
22
|
+
{{skills_table}}
|
|
23
|
+
|
|
24
|
+
## Quality Scores
|
|
25
|
+
|
|
26
|
+
| Metric | Value |
|
|
27
|
+
|--------|-------|
|
|
28
|
+
| **Minimum** | {{quality_min}} |
|
|
29
|
+
| **Maximum** | {{quality_max}} |
|
|
30
|
+
| **Average** | {{quality_avg}} |
|
|
31
|
+
|
|
32
|
+
### Per-Skill Breakdown
|
|
33
|
+
|
|
34
|
+
{{quality_breakdown}}
|
|
35
|
+
|
|
36
|
+
## Findings Summary
|
|
37
|
+
|
|
38
|
+
- **Total workarounds applied:** {{total_workarounds}}
|
|
39
|
+
- **Skills with workarounds:** {{skills_with_workarounds}}
|
|
40
|
+
- **Doc-rot corrections:** tracked per-skill in health-check findings (not aggregated in campaign state)
|
|
41
|
+
|
|
42
|
+
## Workarounds Applied
|
|
43
|
+
|
|
44
|
+
{{workarounds_list}}
|
|
45
|
+
|
|
46
|
+
## Duration Breakdown
|
|
47
|
+
|
|
48
|
+
| Skill | Started | Completed | Duration |
|
|
49
|
+
|-------|---------|-----------|----------|
|
|
50
|
+
{{duration_table}}
|
|
51
|
+
|
|
52
|
+
## Failed / Skipped Skills
|
|
53
|
+
|
|
54
|
+
{{failed_skipped_section}}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Skill Kickoff — {{skill_name}}
|
|
2
|
+
|
|
3
|
+
## Campaign Context
|
|
4
|
+
|
|
5
|
+
- **Campaign:** {{campaign_name}}
|
|
6
|
+
- **Current Stage:** {{current_stage}}
|
|
7
|
+
- **Quality Gate:** {{quality_gate_summary}}
|
|
8
|
+
|
|
9
|
+
## Skill Identity
|
|
10
|
+
|
|
11
|
+
- **Skill:** {{skill_name}}
|
|
12
|
+
- **Tier:** {{skill_tier}}
|
|
13
|
+
- **Repository:** {{repo_url}}
|
|
14
|
+
- **Pin:** {{pin}}
|
|
15
|
+
- **Commit:** {{commit_sha}}
|
|
16
|
+
|
|
17
|
+
## Brief Summary
|
|
18
|
+
|
|
19
|
+
{{brief_summary}}
|
|
20
|
+
|
|
21
|
+
## Campaign Facts
|
|
22
|
+
|
|
23
|
+
{{persistent_facts}}
|
|
24
|
+
|
|
25
|
+
## Dependency State
|
|
26
|
+
|
|
27
|
+
{{dependency_status_table}}
|
|
28
|
+
|
|
29
|
+
## Standing Directive
|
|
30
|
+
|
|
31
|
+
{{directive_content}}
|
|
32
|
+
|
|
33
|
+
## Workarounds Applied
|
|
34
|
+
|
|
35
|
+
{{workarounds_list}}
|
|
36
|
+
|
|
37
|
+
## Pipeline Instructions
|
|
38
|
+
|
|
39
|
+
Execute the standard forge pipeline for **{{skill_name}}**:
|
|
40
|
+
|
|
41
|
+
1. **AN** (Analyze) — scope and source intelligence
|
|
42
|
+
2. **BS** (Brief Synthesis) — generate or validate the skill brief
|
|
43
|
+
3. **CS** (Compile Skill) — compile the SKILL.md artifact
|
|
44
|
+
4. **TS** (Test Skill) — run health-check validation
|
|
45
|
+
|
|
46
|
+
**Parameters:**
|
|
47
|
+
- Pin: {{pin}}
|
|
48
|
+
- Quality target: {{quality_gate_summary}}
|