bmad-module-skill-forge 0.10.0 → 1.0.0-rc.3
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 +56 -0
- package/.gitattributes +16 -0
- package/README.md +105 -38
- package/docs/404.md +13 -10
- package/docs/RELEASING.md +616 -0
- package/docs/STABILITY.md +123 -0
- package/docs/_data/pinned.yaml +98 -0
- package/docs/agents.md +16 -11
- package/docs/architecture.md +221 -0
- package/docs/bmad-synergy.md +11 -13
- package/docs/concepts.md +18 -108
- package/docs/examples.md +121 -80
- package/docs/getting-started.md +85 -147
- package/docs/how-it-works.md +25 -536
- package/docs/index.md +44 -27
- package/docs/skill-model.md +328 -0
- package/docs/troubleshooting.md +39 -0
- package/docs/verifying-a-skill.md +232 -0
- package/docs/why-skf.md +93 -0
- package/docs/workflows.md +119 -13
- package/package.json +30 -9
- package/release-audits/v1.0.0-launch-audit.md +207 -0
- package/src/README.md +23 -25
- package/src/forger/forge-tier.yaml +4 -2
- package/src/forger/preferences.yaml +7 -1
- package/src/knowledge/agentskills-spec.md +1 -1
- package/src/knowledge/architecture-verification.md +1 -1
- package/src/knowledge/ccc-bridge.md +16 -15
- package/src/knowledge/overview.md +11 -11
- package/src/knowledge/progressive-capability.md +3 -3
- package/src/knowledge/provenance-tracking.md +9 -3
- package/src/knowledge/qmd-registry.md +8 -8
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +16 -1
- package/src/knowledge/split-body-strategy.md +1 -1
- package/src/knowledge/version-paths.md +17 -12
- package/src/module-help.csv +18 -17
- package/src/shared/health-check.md +372 -0
- package/src/shared/references/feasibility-report-schema.md +86 -0
- package/src/shared/references/headless-gate-convention.md +62 -0
- package/src/shared/references/output-contract-schema.md +35 -0
- package/src/shared/references/pipeline-contracts.md +102 -0
- package/src/shared/scripts/skf-atomic-write.py +370 -0
- package/src/shared/scripts/skf-manifest-ops.py +236 -0
- package/src/shared/scripts/skf-preflight.py +164 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +201 -0
- package/src/shared/scripts/skf-severity-classify.py +163 -0
- package/src/shared/scripts/skf-skill-inventory.py +210 -0
- package/src/shared/scripts/skf-structural-diff.py +257 -0
- package/src/shared/scripts/skf-validate-frontmatter.py +318 -0
- package/src/shared/scripts/skf-validate-output.py +247 -0
- package/src/skf-analyze-source/SKILL.md +57 -0
- package/src/skf-analyze-source/assets/skill-brief-schema.md +125 -0
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +124 -0
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-01-init.md +6 -66
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-01b-continue.md +4 -59
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-02-scan-project.md +7 -68
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-03-identify-units.md +6 -66
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-04-map-and-detect.md +7 -68
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-05-recommend.md +6 -66
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-06-generate-briefs.md +14 -71
- package/src/skf-analyze-source/steps-c/step-07-health-check.md +22 -0
- package/src/skf-audit-skill/SKILL.md +59 -0
- package/src/{workflows/audit-skill/data → skf-audit-skill/assets}/drift-report-template.md +7 -7
- package/src/{workflows/audit-skill/data → skf-audit-skill/references}/severity-rules.md +11 -11
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-01-init.md +24 -68
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-02-re-index.md +36 -71
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-03-structural-diff.md +21 -63
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-04-semantic-diff.md +6 -65
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-05-severity-classify.md +5 -63
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-06-report.md +10 -64
- package/src/skf-audit-skill/steps-c/step-07-health-check.md +22 -0
- package/src/skf-brief-skill/SKILL.md +53 -0
- package/src/{workflows/brief-skill/data → skf-brief-skill/assets}/scope-templates.md +23 -0
- package/src/skf-brief-skill/assets/skill-brief-schema.md +206 -0
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-01-gather-intent.md +6 -67
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-02-analyze-target.md +6 -66
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-03-scope-definition.md +12 -68
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-04-confirm-brief.md +5 -63
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-05-write-brief.md +10 -70
- package/src/skf-brief-skill/steps-c/step-06-health-check.md +22 -0
- package/src/skf-create-skill/SKILL.md +63 -0
- package/src/{workflows/create-skill/data → skf-create-skill/assets}/compile-assembly-rules.md +60 -0
- package/src/{workflows/create-skill/data → skf-create-skill/assets}/skill-sections.md +92 -23
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +102 -0
- package/src/{workflows/create-skill/data → skf-create-skill/references}/extraction-patterns-tracing.md +13 -13
- package/src/{workflows/create-skill/data → skf-create-skill/references}/extraction-patterns.md +8 -3
- package/src/skf-create-skill/references/source-resolution-protocols.md +239 -0
- package/src/{workflows/create-skill/data → skf-create-skill/references}/tier-degradation-rules.md +8 -7
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-01-load-brief.md +25 -63
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-02-ecosystem-check.md +8 -64
- package/src/skf-create-skill/steps-c/step-03-extract.md +379 -0
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-03d-component-extraction.md +18 -35
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-04-enrich.md +12 -69
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-05-compile.md +71 -75
- package/src/skf-create-skill/steps-c/step-06-validate.md +267 -0
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-07-generate-artifacts.md +30 -74
- package/src/skf-create-skill/steps-c/step-08-report.md +145 -0
- package/src/skf-create-skill/steps-c/step-09-health-check.md +23 -0
- package/src/{workflows/create-skill/steps-c → skf-create-skill/steps-c/sub}/step-02b-ccc-discover.md +8 -66
- package/src/skf-create-skill/steps-c/sub/step-03b-fetch-temporal.md +229 -0
- package/src/{workflows/create-skill/steps-c → skf-create-skill/steps-c/sub}/step-03c-fetch-docs.md +18 -42
- package/src/skf-create-stack-skill/SKILL.md +64 -0
- package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/assets}/stack-skill-template.md +2 -1
- package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/compose-mode-rules.md +19 -5
- package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/integration-patterns.md +1 -1
- package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/manifest-patterns.md +10 -10
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-01-init.md +14 -55
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-02-detect-manifests.md +29 -63
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-03-rank-and-confirm.md +9 -59
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-04-parallel-extract.md +29 -64
- package/src/skf-create-stack-skill/steps-c/step-05-detect-integrations.md +164 -0
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-06-compile-stack.md +7 -60
- package/src/skf-create-stack-skill/steps-c/step-07-generate-output.md +328 -0
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-08-validate.md +17 -63
- package/src/skf-create-stack-skill/steps-c/step-09-report.md +128 -0
- package/src/skf-create-stack-skill/steps-c/step-10-health-check.md +22 -0
- package/src/skf-drop-skill/SKILL.md +57 -0
- package/src/{workflows/drop-skill → skf-drop-skill}/steps-c/step-01-select.md +9 -69
- package/src/{workflows/drop-skill → skf-drop-skill}/steps-c/step-02-execute.md +30 -106
- package/src/skf-drop-skill/steps-c/step-03-report.md +83 -0
- package/src/skf-drop-skill/steps-c/step-04-health-check.md +22 -0
- package/src/skf-export-skill/SKILL.md +57 -0
- package/src/skf-export-skill/assets/managed-section-format.md +138 -0
- package/src/{workflows/export-skill/data → skf-export-skill/assets}/snippet-format.md +8 -12
- package/src/skf-export-skill/steps-c/step-01-load-skill.md +247 -0
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-02-package.md +5 -58
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-03-generate-snippet.md +15 -77
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-04-update-context.md +59 -133
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-05-token-report.md +4 -58
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-06-summary.md +16 -68
- package/src/skf-export-skill/steps-c/step-07-health-check.md +22 -0
- package/src/skf-forger/SKILL.md +110 -0
- package/src/skf-forger/bmad-skill-manifest.yaml +11 -0
- package/src/skf-quick-skill/SKILL.md +56 -0
- package/src/{workflows/quick-skill/data → skf-quick-skill/references}/registry-resolution.md +6 -6
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-01-resolve-target.md +9 -62
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-02-ecosystem-check.md +5 -59
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-03-quick-extract.md +18 -61
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-04-compile.md +6 -60
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-05-validate.md +58 -78
- package/src/skf-quick-skill/steps-c/step-06-write.md +73 -0
- package/src/skf-quick-skill/steps-c/step-07-health-check.md +22 -0
- package/src/skf-refine-architecture/SKILL.md +57 -0
- package/src/{workflows/refine-architecture/data → skf-refine-architecture/references}/refinement-rules.md +16 -16
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-01-init.md +6 -63
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-02-gap-analysis.md +29 -71
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-03-issue-detection.md +5 -65
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-04-improvements.md +5 -64
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-05-compile.md +5 -56
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-06-report.md +11 -64
- package/src/skf-refine-architecture/steps-c/step-07-health-check.md +22 -0
- package/src/skf-rename-skill/SKILL.md +58 -0
- package/src/{workflows/rename-skill → skf-rename-skill}/steps-c/step-01-select.md +12 -73
- package/src/{workflows/rename-skill → skf-rename-skill}/steps-c/step-02-execute.md +32 -113
- package/src/skf-rename-skill/steps-c/step-03-report.md +83 -0
- package/src/skf-rename-skill/steps-c/step-04-health-check.md +22 -0
- package/src/skf-setup/SKILL.md +55 -0
- package/src/{workflows/setup-forge → skf-setup}/steps-c/step-01-detect-and-tier.md +6 -62
- package/src/{workflows/setup-forge → skf-setup}/steps-c/step-01b-ccc-index.md +9 -65
- package/src/{workflows/setup-forge → skf-setup}/steps-c/step-02-write-config.md +15 -60
- package/src/{workflows/setup-forge → skf-setup}/steps-c/step-03-auto-index.md +7 -66
- package/src/skf-setup/steps-c/step-04-report.md +92 -0
- package/src/skf-setup/steps-c/step-05-health-check.md +22 -0
- package/src/skf-test-skill/SKILL.md +59 -0
- package/src/skf-test-skill/references/migration-section-rules.md +114 -0
- package/src/skf-test-skill/references/scoring-rules.md +207 -0
- package/src/skf-test-skill/references/source-access-protocol.md +109 -0
- package/src/skf-test-skill/scripts/compute-score.py +310 -0
- package/src/skf-test-skill/steps-c/step-01-init.md +238 -0
- package/src/{workflows/test-skill → skf-test-skill}/steps-c/step-02-detect-mode.md +4 -62
- package/src/skf-test-skill/steps-c/step-03-coverage-check.md +347 -0
- package/src/skf-test-skill/steps-c/step-04-coherence-check.md +239 -0
- package/src/{workflows/test-skill → skf-test-skill}/steps-c/step-04b-external-validators.md +42 -77
- package/src/skf-test-skill/steps-c/step-05-score.md +266 -0
- package/src/skf-test-skill/steps-c/step-06-report.md +290 -0
- package/src/skf-test-skill/steps-c/step-07-health-check.md +25 -0
- package/src/skf-test-skill/templates/test-report-template.md +58 -0
- package/src/skf-update-skill/SKILL.md +58 -0
- package/src/{workflows/update-skill/data → skf-update-skill/references}/manual-section-rules.md +6 -6
- package/src/{workflows/update-skill/data → skf-update-skill/references}/merge-conflict-rules.md +5 -5
- package/src/skf-update-skill/references/remote-source-resolution.md +94 -0
- package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-01-init.md +22 -69
- package/src/skf-update-skill/steps-c/step-02-detect-changes.md +255 -0
- package/src/skf-update-skill/steps-c/step-03-re-extract.md +336 -0
- package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-04-merge.md +60 -72
- package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-05-validate.md +5 -62
- package/src/skf-update-skill/steps-c/step-06-write.md +278 -0
- package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-07-report.md +11 -69
- package/src/skf-update-skill/steps-c/step-08-health-check.md +22 -0
- package/src/skf-verify-stack/SKILL.md +60 -0
- package/src/skf-verify-stack/assets/feasibility-report-template.md +76 -0
- package/src/{workflows/verify-stack/data → skf-verify-stack/references}/coverage-patterns.md +12 -4
- package/src/skf-verify-stack/references/integration-verification-rules.md +78 -0
- package/src/skf-verify-stack/steps-c/step-01-init.md +178 -0
- package/src/{workflows/verify-stack → skf-verify-stack}/steps-c/step-02-coverage.md +27 -72
- package/src/skf-verify-stack/steps-c/step-03-integrations.md +179 -0
- package/src/{workflows/verify-stack → skf-verify-stack}/steps-c/step-04-requirements.md +14 -67
- package/src/skf-verify-stack/steps-c/step-05-synthesize.md +147 -0
- package/src/skf-verify-stack/steps-c/step-06-report.md +156 -0
- package/src/skf-verify-stack/steps-c/step-07-health-check.md +25 -0
- package/tools/cli/commands/install.js +4 -1
- package/tools/cli/commands/status.js +21 -23
- package/tools/cli/commands/uninstall.js +11 -19
- package/tools/cli/commands/update.js +6 -4
- package/tools/cli/lib/ide-skills.js +216 -0
- package/tools/cli/lib/installer.js +29 -52
- package/tools/cli/lib/manifest.js +11 -5
- package/tools/cli/lib/platform-codes.yaml +223 -0
- package/tools/cli/lib/ui.js +76 -47
- package/tools/validate-docs-drift.js +193 -0
- package/src/agents/forger.agent.yaml +0 -114
- package/src/workflows/README.md +0 -174
- package/src/workflows/analyze-source/data/skill-brief-schema.md +0 -125
- package/src/workflows/analyze-source/data/unit-detection-heuristics.md +0 -124
- package/src/workflows/analyze-source/workflow.md +0 -61
- package/src/workflows/audit-skill/workflow.md +0 -64
- package/src/workflows/brief-skill/data/skill-brief-schema.md +0 -159
- package/src/workflows/brief-skill/workflow.md +0 -58
- package/src/workflows/create-skill/data/source-resolution-protocols.md +0 -200
- package/src/workflows/create-skill/steps-c/step-03-extract.md +0 -301
- package/src/workflows/create-skill/steps-c/step-03b-fetch-temporal.md +0 -253
- package/src/workflows/create-skill/steps-c/step-06-validate.md +0 -254
- package/src/workflows/create-skill/steps-c/step-08-report.md +0 -179
- package/src/workflows/create-skill/workflow.md +0 -53
- package/src/workflows/create-stack-skill/steps-c/step-05-detect-integrations.md +0 -196
- package/src/workflows/create-stack-skill/steps-c/step-07-generate-output.md +0 -270
- package/src/workflows/create-stack-skill/steps-c/step-09-report.md +0 -150
- package/src/workflows/create-stack-skill/workflow.md +0 -59
- package/src/workflows/drop-skill/steps-c/step-03-report.md +0 -136
- package/src/workflows/drop-skill/workflow.md +0 -63
- package/src/workflows/export-skill/data/managed-section-format.md +0 -103
- package/src/workflows/export-skill/steps-c/step-01-load-skill.md +0 -237
- package/src/workflows/export-skill/workflow.md +0 -58
- package/src/workflows/quick-skill/steps-c/step-06-write.md +0 -178
- package/src/workflows/quick-skill/workflow.md +0 -58
- package/src/workflows/refine-architecture/workflow.md +0 -61
- package/src/workflows/rename-skill/steps-c/step-03-report.md +0 -133
- package/src/workflows/rename-skill/workflow.md +0 -64
- package/src/workflows/setup-forge/steps-c/step-04-report.md +0 -159
- package/src/workflows/setup-forge/workflow.md +0 -51
- package/src/workflows/shared/health-check.md +0 -231
- package/src/workflows/test-skill/data/scoring-rules.md +0 -123
- package/src/workflows/test-skill/data/source-access-protocol.md +0 -51
- package/src/workflows/test-skill/steps-c/step-01-init.md +0 -217
- package/src/workflows/test-skill/steps-c/step-03-coverage-check.md +0 -280
- package/src/workflows/test-skill/steps-c/step-04-coherence-check.md +0 -271
- package/src/workflows/test-skill/steps-c/step-05-score.md +0 -222
- package/src/workflows/test-skill/steps-c/step-06-report.md +0 -208
- package/src/workflows/test-skill/templates/test-report-template.md +0 -28
- package/src/workflows/test-skill/workflow.md +0 -57
- package/src/workflows/update-skill/data/remote-source-resolution.md +0 -57
- package/src/workflows/update-skill/steps-c/step-02-detect-changes.md +0 -221
- package/src/workflows/update-skill/steps-c/step-03-re-extract.md +0 -256
- package/src/workflows/update-skill/steps-c/step-06-write.md +0 -265
- package/src/workflows/update-skill/workflow.md +0 -56
- package/src/workflows/verify-stack/data/feasibility-report-template.md +0 -63
- package/src/workflows/verify-stack/data/integration-verification-rules.md +0 -73
- package/src/workflows/verify-stack/steps-c/step-01-init.md +0 -179
- package/src/workflows/verify-stack/steps-c/step-03-integrations.md +0 -195
- package/src/workflows/verify-stack/steps-c/step-05-synthesize.md +0 -191
- package/src/workflows/verify-stack/steps-c/step-06-report.md +0 -200
- package/src/workflows/verify-stack/workflow.md +0 -61
- package/tools/cli/lib/compiler.js +0 -306
- package/tools/cli/lib/ide-commands.js +0 -244
- /package/src/{workflows/analyze-source → skf-analyze-source}/templates/analysis-report-template.md +0 -0
- /package/src/{workflows/quick-skill/data → skf-quick-skill/assets}/skill-template.md +0 -0
- /package/src/{workflows/setup-forge/data → skf-setup/references}/tier-rules.md +0 -0
- /package/src/{workflows/test-skill/data → skf-test-skill/assets}/output-section-formats.md +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Feasibility Report Schema
|
|
2
|
+
|
|
3
|
+
**Purpose:** Canonical contract shared between `skf-create-stack-skill` (consumer) and `skf-verify-stack` (producer). Any change here must be applied to both skills in lockstep.
|
|
4
|
+
|
|
5
|
+
## Filename
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
{forge_data_folder}/feasibility-report-{project_slug}-{YYYYMMDD-HHmmss}.md
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`{project_slug}` is the slugified `project_name` (lowercase, hyphens only, no unicode). A stable `feasibility-report-{project_slug}-latest.md` copy (not symlink) is written next to the timestamped file for pipeline consumers.
|
|
12
|
+
|
|
13
|
+
## Frontmatter (required)
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
---
|
|
17
|
+
schemaVersion: "1.0"
|
|
18
|
+
reportType: feasibility
|
|
19
|
+
projectName: "{project_name}"
|
|
20
|
+
projectSlug: "{project_slug}"
|
|
21
|
+
generatedAt: "{ISO-8601 UTC}"
|
|
22
|
+
generatedBy: skf-verify-stack
|
|
23
|
+
overallVerdict: "FEASIBLE|CONDITIONALLY_FEASIBLE|NOT_FEASIBLE"
|
|
24
|
+
coveragePercentage: <0..100 integer>
|
|
25
|
+
pairsVerified: <non-negative integer>
|
|
26
|
+
pairsPlausible: <non-negative integer>
|
|
27
|
+
pairsRisky: <non-negative integer>
|
|
28
|
+
pairsBlocked: <non-negative integer>
|
|
29
|
+
recommendationCount: <non-negative integer>
|
|
30
|
+
prdAvailable: <true|false>
|
|
31
|
+
---
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Unknown `schemaVersion` MUST fail loudly in consumers — never silently proceed.** Consumers check `schemaVersion == "1.0"` and emit an explicit error if mismatched.
|
|
35
|
+
|
|
36
|
+
## Per-pair verdict tokens (case-sensitive)
|
|
37
|
+
|
|
38
|
+
Exactly one of:
|
|
39
|
+
|
|
40
|
+
| Token | Meaning | Required evidence |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `Verified` | All compatibility checks pass with declared evidence in both skills | Documentation cross-reference (Check 4) MUST pass with literal substring/name citation; language + protocol + type checks all pass |
|
|
43
|
+
| `Plausible` | Checks pass but at least one relies on inferred rather than declared evidence | Language + protocol + type checks pass; Check 4 weak or missing |
|
|
44
|
+
| `Risky` | At least one check produced incompatibility that a workaround may resolve | Any single check fails; workaround cited in recommendation |
|
|
45
|
+
| `Blocked` | Fundamental incompatibility that cannot be worked around | Language mismatch, protocol mismatch with no bridge, or type mismatch with no adapter |
|
|
46
|
+
|
|
47
|
+
## Overall verdict tokens (case-sensitive)
|
|
48
|
+
|
|
49
|
+
Exactly one of `FEASIBLE`, `CONDITIONALLY_FEASIBLE`, `NOT_FEASIBLE`.
|
|
50
|
+
|
|
51
|
+
- `FEASIBLE` — 100% coverage AND zero Blocked pairs AND zero pairs with Check 4 missing.
|
|
52
|
+
- `NOT_FEASIBLE` — Any Blocked pair OR zero coverage.
|
|
53
|
+
- `CONDITIONALLY_FEASIBLE` — Everything else.
|
|
54
|
+
|
|
55
|
+
## Body section headings (required, in order)
|
|
56
|
+
|
|
57
|
+
```markdown
|
|
58
|
+
## Executive Summary
|
|
59
|
+
## Coverage Analysis
|
|
60
|
+
## Integration Verdicts
|
|
61
|
+
## Recommendations
|
|
62
|
+
## Evidence Sources
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Consumers grep for `## Integration Verdicts` to locate the pair table. The table header is fixed:
|
|
66
|
+
|
|
67
|
+
```markdown
|
|
68
|
+
| lib_a | lib_b | verdict | rationale |
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Producer obligations (skf-verify-stack)
|
|
72
|
+
|
|
73
|
+
- Set `schemaVersion: "1.0"` in frontmatter.
|
|
74
|
+
- Never emit a verdict token outside the defined set.
|
|
75
|
+
- When Check 4 (documentation cross-reference) produces weak/missing evidence, cap the per-pair verdict at `Plausible`.
|
|
76
|
+
- When `coveragePercentage == 0`, force `overallVerdict: NOT_FEASIBLE` regardless of pair results.
|
|
77
|
+
|
|
78
|
+
## Consumer obligations (skf-create-stack-skill)
|
|
79
|
+
|
|
80
|
+
- Verify `schemaVersion == "1.0"`; halt with explicit error on mismatch.
|
|
81
|
+
- Treat any unknown verdict token as a hard error (do not silently drop or map).
|
|
82
|
+
- Use filename pattern above when referencing prior reports.
|
|
83
|
+
|
|
84
|
+
## Versioning policy
|
|
85
|
+
|
|
86
|
+
Any change to the verdict token set, frontmatter keys, or section headers is a schema-breaking change and MUST bump `schemaVersion`. Additive changes (new optional frontmatter keys) bump the minor version; breaking changes bump the major version.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Headless Gate Convention
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Gates are user interaction points where a workflow pauses for confirmation or input. In headless mode, gates auto-resolve with their default action. This convention ensures one code path with two behaviors — interactive and headless use the same gates, the same output, and the same progression logic.
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
Every gate in a step file follows this pattern:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
**GATE: [default action]** — Present [options] to user.
|
|
13
|
+
If `{headless_mode}`: auto-proceed with [default action], log: "headless: auto-[action]".
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The gate always:
|
|
17
|
+
1. Prepares the same output (summary, preview, menu) regardless of mode
|
|
18
|
+
2. In interactive mode: displays the output and waits for user input
|
|
19
|
+
3. In headless mode: displays the output, logs the auto-action, and proceeds with the default
|
|
20
|
+
|
|
21
|
+
## Resolving `{headless_mode}`
|
|
22
|
+
|
|
23
|
+
`{headless_mode}` is resolved during activation from:
|
|
24
|
+
1. **Args:** `--headless` or `-H` passed to the skill invocation
|
|
25
|
+
2. **Preferences:** `headless_mode: true` in `{sidecar_path}/preferences.yaml`
|
|
26
|
+
3. **Default:** `false`
|
|
27
|
+
|
|
28
|
+
Each workflow's On Activation section resolves this variable alongside other config. The forger passes it through when dispatching to workflows.
|
|
29
|
+
|
|
30
|
+
## Gate Types
|
|
31
|
+
|
|
32
|
+
### Confirm Gate (default: Continue)
|
|
33
|
+
The most common gate. Presents a summary and asks to continue.
|
|
34
|
+
- Default action: `[C] Continue`
|
|
35
|
+
- Headless behavior: auto-continue after displaying summary
|
|
36
|
+
|
|
37
|
+
### Review Gate (default: Approve)
|
|
38
|
+
Presents compiled output for review before writing.
|
|
39
|
+
- Default action: `[C] Continue` (approve)
|
|
40
|
+
- Headless behavior: auto-approve after displaying preview
|
|
41
|
+
|
|
42
|
+
### Input Gate (default: use provided args)
|
|
43
|
+
Requires user-supplied data (skill name, path, etc.).
|
|
44
|
+
- Default action: use `{headless_args}` if provided
|
|
45
|
+
- Headless behavior: consume pre-supplied arguments; halt if missing required input
|
|
46
|
+
|
|
47
|
+
### Choice Gate (default: first safe option)
|
|
48
|
+
Presents a menu with multiple options (P/I/A, etc.).
|
|
49
|
+
- Default action: varies per gate (documented in step file)
|
|
50
|
+
- Headless behavior: auto-select the default, log the choice
|
|
51
|
+
|
|
52
|
+
## Headless Args
|
|
53
|
+
|
|
54
|
+
For skills that require user input (skill name, target path, etc.), headless mode accepts arguments via the invocation. Each skill's Invocation Contract documents its required headless args.
|
|
55
|
+
|
|
56
|
+
Example: `@Ferris QS cocoindex --headless` passes `cocoindex` as the target and skips all gates.
|
|
57
|
+
|
|
58
|
+
## What Headless Does NOT Skip
|
|
59
|
+
|
|
60
|
+
- Error halts (hard halts on missing files, invalid state)
|
|
61
|
+
- Progress output (summaries, status updates still display)
|
|
62
|
+
- Quality thresholds (if a step produces output below spec, it still reports the issue)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Output Contract Schema
|
|
2
|
+
|
|
3
|
+
Every pipeline-capable skill writes a result JSON file at its final step. This enables reliable CI integration and pipeline chaining.
|
|
4
|
+
|
|
5
|
+
## Schema
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"skill": "skf-skill-name",
|
|
10
|
+
"status": "success" | "failed" | "partial",
|
|
11
|
+
"timestamp": "ISO-8601",
|
|
12
|
+
"outputs": [
|
|
13
|
+
{"type": "report|skill|manifest|config", "path": "relative/path/to/file"}
|
|
14
|
+
],
|
|
15
|
+
"summary": {
|
|
16
|
+
// skill-specific summary fields
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Filenames
|
|
22
|
+
|
|
23
|
+
Each run writes **two files** to `{output_dir}`:
|
|
24
|
+
|
|
25
|
+
1. **Per-run record** (audit trail): `{skill-name}-result-{YYYYMMDD-HHmmss}.json`
|
|
26
|
+
- Timestamp is UTC, resolution to seconds — e.g., `update-skill-result-20260413-145230.json`
|
|
27
|
+
- Never overwritten by subsequent runs — preserves a durable audit trail across retries, aborts, and re-runs
|
|
28
|
+
2. **Stable latest pointer** (pipeline consumption): `{skill-name}-result-latest.json`
|
|
29
|
+
- A **copy** (not a symlink) of the per-run record just written
|
|
30
|
+
- Always present at a deterministic path so CI / pipelines / the forger can read `summary.*` without enumerating timestamps
|
|
31
|
+
- Overwritten on every successful write
|
|
32
|
+
|
|
33
|
+
Write the per-run record first, then copy it to the `-latest.json` path. If the copy fails, the per-run record still exists — the run is not lost.
|
|
34
|
+
|
|
35
|
+
**Consumers (forger, CI, chained workflows):** read from `{skill-name}-result-latest.json`. Do not enumerate timestamped files unless inspecting prior-run history.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Pipeline Contracts
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Pipelines chain multiple SKF workflows in sequence. The forger orchestrates the chain, passing data between workflows via filesystem artifacts and validating output contracts at each transition.
|
|
6
|
+
|
|
7
|
+
## Syntax
|
|
8
|
+
|
|
9
|
+
The forger recognizes pipeline invocations when the user provides multiple workflow codes:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
AN CS TS EX — space-separated codes
|
|
13
|
+
AN -> CS -> TS -> EX — arrow-separated (equivalent)
|
|
14
|
+
BS CS[cocoindex] TS EX — with target argument in brackets
|
|
15
|
+
CS TS[min:80] EX — with circuit breaker threshold
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The forger also accepts common pipeline aliases:
|
|
19
|
+
|
|
20
|
+
| Alias | Expands To | Description |
|
|
21
|
+
|-------|-----------|-------------|
|
|
22
|
+
| `forge` | `BS CS TS EX` | Full skill creation pipeline (brief through export) |
|
|
23
|
+
| `forge-quick` | `QS TS EX` | Quick skill pipeline |
|
|
24
|
+
| `onboard` | `AN CS TS EX` | Full brownfield onboarding (AN generates briefs, CS consumes them directly) |
|
|
25
|
+
| `maintain` | `AS US TS EX` | Maintenance cycle (audit → update → test → export) |
|
|
26
|
+
|
|
27
|
+
## Pipeline Rules
|
|
28
|
+
|
|
29
|
+
1. **Left to right execution** — each workflow completes before the next begins
|
|
30
|
+
2. **Headless implied** — pipelines activate `{headless_mode}` automatically for all workflows in the chain (the user already committed to the sequence)
|
|
31
|
+
3. **Data forwarding** — the forger resolves output-to-input mapping between adjacent workflows (see Data Flow table)
|
|
32
|
+
4. **Circuit breakers** — if a workflow's output fails its quality check, the pipeline halts with a summary of what completed and what remains
|
|
33
|
+
5. **Error halts propagate** — if any workflow hard-halts, the pipeline stops immediately
|
|
34
|
+
6. **Progress reporting** — the forger reports completion of each workflow before starting the next
|
|
35
|
+
|
|
36
|
+
## Data Flow
|
|
37
|
+
|
|
38
|
+
How outputs from one workflow become inputs to the next:
|
|
39
|
+
|
|
40
|
+
| From | To | Data Passed | How |
|
|
41
|
+
|------|-----|------------|-----|
|
|
42
|
+
| AN | CS | `skill-brief.yaml` paths from generated briefs | Forger passes each `brief_path` written by AN to CS; in batch mode, CS processes all sequentially |
|
|
43
|
+
| BS | CS | `skill-brief.yaml` path | Forger passes the brief path written by BS as `brief_path` to CS |
|
|
44
|
+
| CS | TS | skill name (derived from brief) | Forger passes the `skill_name` from the completed CS to TS |
|
|
45
|
+
| CS | EX | skill name | Same — forger resolves the created skill's name |
|
|
46
|
+
| TS | EX | skill name + test result | Forger checks `result` field in test report; if FAIL and circuit breaker active, halts |
|
|
47
|
+
| QS | TS | skill name (from `repo_name`) | Forger passes the quick-skill's output name to TS |
|
|
48
|
+
| QS | EX | skill name | Same |
|
|
49
|
+
| AS | US | skill name + drift severity | Forger checks `summary.severity` in `audit-skill-result-latest.json`; if CLEAN, skips US |
|
|
50
|
+
| VS | RA | architecture doc path | Already known from VS invocation |
|
|
51
|
+
|
|
52
|
+
## Circuit Breakers
|
|
53
|
+
|
|
54
|
+
Circuit breakers halt the pipeline when a workflow's output doesn't meet a quality threshold:
|
|
55
|
+
|
|
56
|
+
| Workflow | Check | Default Threshold | Halt Condition |
|
|
57
|
+
|----------|-------|-------------------|----------------|
|
|
58
|
+
| AN | recommended units count | min: 1 | Zero skillable units found |
|
|
59
|
+
| CS | compilation success | must complete | Hard error during compilation |
|
|
60
|
+
| TS | completeness score | min: 60 | Score below threshold |
|
|
61
|
+
| AS | drift score | not CRITICAL | Critical drift found |
|
|
62
|
+
| VS | feasibility verdict | not BLOCKED | All integrations blocked |
|
|
63
|
+
|
|
64
|
+
Override syntax: `TS[min:80]` sets the test-skill threshold to 80 for this pipeline run.
|
|
65
|
+
|
|
66
|
+
### Bracket Syntax
|
|
67
|
+
|
|
68
|
+
Brackets after a workflow code (`CODE[value]`) are parsed as follows:
|
|
69
|
+
|
|
70
|
+
- **Circuit breaker override**: `min:N` where N is a number — e.g., `TS[min:80]` sets the threshold for that workflow
|
|
71
|
+
- **Target argument**: any other value — e.g., `CS[cocoindex]` passes "cocoindex" as the target to CS
|
|
72
|
+
|
|
73
|
+
Only workflows with a circuit breaker entry (AN, CS, TS, AS, VS) accept `min:N` overrides. All other workflows ignore `min:N` brackets. Target arguments are valid for any workflow that accepts a named input (CS, QS, BS, US, etc.).
|
|
74
|
+
|
|
75
|
+
## Pipeline State
|
|
76
|
+
|
|
77
|
+
The forger tracks pipeline state in memory during execution:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
pipeline:
|
|
81
|
+
workflows: [AN, CS, TS, EX]
|
|
82
|
+
current_index: 1
|
|
83
|
+
completed:
|
|
84
|
+
- {code: AN, status: ok, output: {units: 3, briefs: [...]}}
|
|
85
|
+
pending: [CS, TS, EX]
|
|
86
|
+
data:
|
|
87
|
+
skill_name: "cocoindex"
|
|
88
|
+
brief_path: "/path/to/skill-brief.yaml"
|
|
89
|
+
target: "cocoindex"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Anti-Patterns
|
|
93
|
+
|
|
94
|
+
The forger validates the pipeline sequence and warns about:
|
|
95
|
+
|
|
96
|
+
| Pattern | Issue | Suggestion |
|
|
97
|
+
|---------|-------|------------|
|
|
98
|
+
| EX before TS | Exporting untested skill | Add TS before EX |
|
|
99
|
+
| US without AS | Updating without audit | Run AS first to detect what changed |
|
|
100
|
+
| CS without BS or AN | Compiling without brief | Need a brief — use QS for quick path, or AN for brownfield |
|
|
101
|
+
| TS after EX | Testing after export | Move TS before EX |
|
|
102
|
+
| Duplicate codes | Same workflow twice | Remove duplicate |
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Atomic Write — Crash-safe artifact writing for skill workflows.
|
|
6
|
+
|
|
7
|
+
Provides three CLI subcommands skills can invoke via bash to avoid
|
|
8
|
+
partial-write corruption and active-symlink races.
|
|
9
|
+
|
|
10
|
+
Subcommands:
|
|
11
|
+
write Stage content into <target>.skf-tmp, fsync, then rename to <target>.
|
|
12
|
+
Content comes from stdin. Creates parent dirs as needed.
|
|
13
|
+
|
|
14
|
+
stage-dir Create <target>.skf-tmp/ as a staging directory (mkdir -p).
|
|
15
|
+
Caller writes files into it, then calls commit-dir to atomically
|
|
16
|
+
swap it into place as <target>/ (with prior target moved aside
|
|
17
|
+
to <target>.skf-rollback-<pid> and removed on success).
|
|
18
|
+
|
|
19
|
+
commit-dir Atomically swap <target>.skf-tmp/ into <target>/. If <target>/
|
|
20
|
+
exists, move it to <target>.skf-rollback-<pid> first; on failure,
|
|
21
|
+
restore. Supports rollback via --rollback to undo the most recent
|
|
22
|
+
commit by restoring the rollback dir if still present.
|
|
23
|
+
|
|
24
|
+
flip-link Atomically update symlink <link> to point at <target> using
|
|
25
|
+
the `ln -sfn tmp && mv -Tf tmp link` pattern (or equivalent via
|
|
26
|
+
os.replace on the link path). Holds an flock on <link>.lock.
|
|
27
|
+
|
|
28
|
+
Cross-platform: locking branches between fcntl (POSIX) and msvcrt
|
|
29
|
+
(Windows). Symlink semantics on Windows require dev mode or admin —
|
|
30
|
+
flip-link surfaces a clear error rather than silently falling back.
|
|
31
|
+
Native Windows is untested in CI; the supported path is WSL2.
|
|
32
|
+
|
|
33
|
+
Exit codes:
|
|
34
|
+
0 on success
|
|
35
|
+
1 on user error (bad args, missing input)
|
|
36
|
+
2 on operation failure (disk full, permission, race-detected)
|
|
37
|
+
|
|
38
|
+
CLI examples:
|
|
39
|
+
cat metadata.json | python3 skf-atomic-write.py write --target /path/to/metadata.json
|
|
40
|
+
python3 skf-atomic-write.py stage-dir --target /path/to/1.0.0
|
|
41
|
+
python3 skf-atomic-write.py commit-dir --target /path/to/1.0.0
|
|
42
|
+
python3 skf-atomic-write.py flip-link --link /path/to/active --target 1.0.0
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import argparse
|
|
48
|
+
import errno
|
|
49
|
+
import json
|
|
50
|
+
import os
|
|
51
|
+
import shutil
|
|
52
|
+
import subprocess
|
|
53
|
+
import sys
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
|
|
56
|
+
if os.name == "nt":
|
|
57
|
+
import msvcrt
|
|
58
|
+
else:
|
|
59
|
+
import fcntl
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _create_symlink_or_junction(target: str, link_path: Path) -> str:
|
|
63
|
+
"""Create a directory link from link_path to target.
|
|
64
|
+
|
|
65
|
+
On POSIX: standard symlink. On Windows: try symlink first (works under
|
|
66
|
+
Developer Mode or admin), fall back to a directory junction via `mklink /J`
|
|
67
|
+
on PRIVILEGE_NOT_HELD / ACCESS_DENIED. Junctions don't need elevation and
|
|
68
|
+
behave like directory symlinks for the resolve() / is_dir() consumers in
|
|
69
|
+
skf-skill-inventory.py.
|
|
70
|
+
|
|
71
|
+
Returns "symlink" or "junction" so callers can report which kind was made.
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
os.symlink(target, link_path)
|
|
75
|
+
return "symlink"
|
|
76
|
+
except OSError as e:
|
|
77
|
+
if os.name != "nt" or getattr(e, "winerror", None) not in (1314, 5):
|
|
78
|
+
raise
|
|
79
|
+
# Junctions are absolute-path-only and target must be a real directory
|
|
80
|
+
# at creation time. Resolve relative `target` against link_path's parent.
|
|
81
|
+
abs_target = (link_path.parent / target).resolve()
|
|
82
|
+
if not abs_target.is_dir():
|
|
83
|
+
raise OSError(
|
|
84
|
+
errno.ENOTDIR,
|
|
85
|
+
f"junction fallback requires existing directory target: {abs_target}",
|
|
86
|
+
) from e
|
|
87
|
+
result = subprocess.run(
|
|
88
|
+
["cmd", "/c", "mklink", "/J", str(link_path), str(abs_target)],
|
|
89
|
+
capture_output=True,
|
|
90
|
+
text=True,
|
|
91
|
+
timeout=30,
|
|
92
|
+
)
|
|
93
|
+
if result.returncode != 0:
|
|
94
|
+
raise OSError(
|
|
95
|
+
e.errno,
|
|
96
|
+
f"junction fallback failed: {result.stderr.strip() or result.stdout.strip()}",
|
|
97
|
+
) from e
|
|
98
|
+
return "junction"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _acquire_lock(fd: int) -> None:
|
|
102
|
+
"""Acquire an exclusive non-blocking lock on fd (auto-released on close/exit)."""
|
|
103
|
+
if os.name == "nt":
|
|
104
|
+
try:
|
|
105
|
+
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
|
|
106
|
+
except OSError as e:
|
|
107
|
+
if e.errno in (errno.EAGAIN, errno.EACCES, errno.EDEADLK):
|
|
108
|
+
raise OSError(errno.EAGAIN, "lock held") from e
|
|
109
|
+
raise
|
|
110
|
+
else:
|
|
111
|
+
try:
|
|
112
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
113
|
+
except OSError as e:
|
|
114
|
+
if e.errno in (errno.EAGAIN, errno.EACCES):
|
|
115
|
+
raise OSError(errno.EAGAIN, "lock held") from e
|
|
116
|
+
raise
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _release_lock(fd: int) -> None:
|
|
120
|
+
if os.name == "nt":
|
|
121
|
+
try:
|
|
122
|
+
msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
|
|
123
|
+
except OSError:
|
|
124
|
+
pass
|
|
125
|
+
else:
|
|
126
|
+
try:
|
|
127
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
128
|
+
except OSError:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _die(code: int, message: str) -> None:
|
|
133
|
+
print(json.dumps({"status": "error", "message": message}), file=sys.stderr)
|
|
134
|
+
sys.exit(code)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _ok(payload: dict) -> None:
|
|
138
|
+
payload.setdefault("status", "ok")
|
|
139
|
+
print(json.dumps(payload))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def cmd_write(target: Path) -> None:
|
|
143
|
+
"""Write stdin to target atomically via temp + rename."""
|
|
144
|
+
data = sys.stdin.buffer.read()
|
|
145
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
tmp = target.with_name(target.name + ".skf-tmp")
|
|
147
|
+
try:
|
|
148
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
|
|
149
|
+
try:
|
|
150
|
+
os.write(fd, data)
|
|
151
|
+
os.fsync(fd)
|
|
152
|
+
finally:
|
|
153
|
+
os.close(fd)
|
|
154
|
+
os.replace(tmp, target)
|
|
155
|
+
except OSError as e:
|
|
156
|
+
if tmp.exists():
|
|
157
|
+
try:
|
|
158
|
+
tmp.unlink()
|
|
159
|
+
except OSError:
|
|
160
|
+
pass
|
|
161
|
+
_die(2, f"atomic write failed: {e}")
|
|
162
|
+
_ok({"wrote": str(target), "bytes": len(data)})
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def cmd_stage_dir(target: Path) -> None:
|
|
166
|
+
"""Create <target>.skf-tmp/ staging directory (clean if present)."""
|
|
167
|
+
staging = target.with_name(target.name + ".skf-tmp")
|
|
168
|
+
# A prior interrupted run can leave staging as a regular file or a dangling
|
|
169
|
+
# symlink; shutil.rmtree only handles real directories, so dispatch by type.
|
|
170
|
+
if staging.is_symlink() or (staging.exists() and not staging.is_dir()):
|
|
171
|
+
try:
|
|
172
|
+
staging.unlink()
|
|
173
|
+
except OSError as e:
|
|
174
|
+
_die(2, f"failed to clear stale staging entry {staging}: {e}")
|
|
175
|
+
elif staging.is_dir():
|
|
176
|
+
shutil.rmtree(staging)
|
|
177
|
+
staging.mkdir(parents=True)
|
|
178
|
+
_ok({"staging": str(staging)})
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def cmd_commit_dir(target: Path, rollback: bool = False) -> None:
|
|
182
|
+
"""Swap <target>.skf-tmp/ into <target>/.
|
|
183
|
+
|
|
184
|
+
The swap is two os.replace calls (move prior aside, then move staging
|
|
185
|
+
into place); a crash between them leaves the prior snapshot at
|
|
186
|
+
<target>.skf-rollback-<pid>/ recoverable via --rollback. Concurrent
|
|
187
|
+
commits against the same target are NOT supported — callers must
|
|
188
|
+
serialize via the per-target flock provided by flip-link or external
|
|
189
|
+
coordination.
|
|
190
|
+
"""
|
|
191
|
+
staging = target.with_name(target.name + ".skf-tmp")
|
|
192
|
+
rollback_dir = target.with_name(target.name + f".skf-rollback-{os.getpid()}")
|
|
193
|
+
|
|
194
|
+
if rollback:
|
|
195
|
+
# Pick the newest rollback dir by mtime, not by lexical PID sort.
|
|
196
|
+
# Concurrent commits leave one rollback per PID; sorting by PID-as-string
|
|
197
|
+
# could restore an older snapshot (PID "9999" sorts after "10001"). mtime
|
|
198
|
+
# of the rollback dir = moment os.replace moved the prior target aside,
|
|
199
|
+
# which is the correct "most recent" anchor.
|
|
200
|
+
candidates = list(target.parent.glob(target.name + ".skf-rollback-*"))
|
|
201
|
+
if not candidates:
|
|
202
|
+
_die(1, f"no rollback dir for {target}")
|
|
203
|
+
chosen = max(candidates, key=lambda p: p.stat().st_mtime)
|
|
204
|
+
if target.exists():
|
|
205
|
+
shutil.rmtree(target)
|
|
206
|
+
os.replace(chosen, target)
|
|
207
|
+
_ok({"restored": str(target), "from": str(chosen)})
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
if not staging.is_dir():
|
|
211
|
+
_die(1, f"staging dir missing: {staging}")
|
|
212
|
+
|
|
213
|
+
# First-install case: target's parent may not exist yet (e.g. fresh
|
|
214
|
+
# {skill_group}/{version}/ install). os.replace requires the destination
|
|
215
|
+
# parent to exist, so create it before either replace fires.
|
|
216
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
217
|
+
|
|
218
|
+
prior_moved = False
|
|
219
|
+
if target.exists():
|
|
220
|
+
if target.is_symlink() or target.is_file():
|
|
221
|
+
_die(2, f"target is not a directory: {target}")
|
|
222
|
+
try:
|
|
223
|
+
os.replace(target, rollback_dir)
|
|
224
|
+
prior_moved = True
|
|
225
|
+
except OSError as e:
|
|
226
|
+
_die(2, f"failed to move prior target aside: {e}")
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
os.replace(staging, target)
|
|
230
|
+
except OSError as e:
|
|
231
|
+
if prior_moved:
|
|
232
|
+
try:
|
|
233
|
+
os.replace(rollback_dir, target)
|
|
234
|
+
except OSError:
|
|
235
|
+
pass
|
|
236
|
+
_die(2, f"commit swap failed: {e}")
|
|
237
|
+
|
|
238
|
+
if prior_moved:
|
|
239
|
+
try:
|
|
240
|
+
shutil.rmtree(rollback_dir)
|
|
241
|
+
except OSError:
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
_ok({"committed": str(target)})
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _is_link_or_junction(p: Path) -> bool:
|
|
248
|
+
"""True for POSIX symlinks AND Windows junctions/symlinks.
|
|
249
|
+
|
|
250
|
+
`Path.is_symlink()` is False for Windows junctions; os.readlink succeeds
|
|
251
|
+
for both symlinks and junctions (since CPython 3.8 on Windows). A regular
|
|
252
|
+
directory raises OSError on readlink, which is the signal we want to
|
|
253
|
+
refuse replacement.
|
|
254
|
+
"""
|
|
255
|
+
if p.is_symlink():
|
|
256
|
+
return True
|
|
257
|
+
if not p.exists() and not p.is_symlink():
|
|
258
|
+
return False
|
|
259
|
+
try:
|
|
260
|
+
os.readlink(p)
|
|
261
|
+
return True
|
|
262
|
+
except OSError:
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def cmd_flip_link(link: Path, target: str) -> None:
|
|
267
|
+
"""Atomically point <link> at <target> using rename-over-symlink pattern.
|
|
268
|
+
|
|
269
|
+
target is the value of the symlink (may be relative, as is convention
|
|
270
|
+
for `active -> 1.0.0`). Held lock on <link>.lock prevents concurrent flips.
|
|
271
|
+
|
|
272
|
+
On Windows, os.symlink requires Developer Mode or admin. When that fails
|
|
273
|
+
with PRIVILEGE_NOT_HELD/ACCESS_DENIED the helper falls back to a directory
|
|
274
|
+
junction (no elevation needed); junctions resolve identically for
|
|
275
|
+
skf-skill-inventory's resolve_active_version().
|
|
276
|
+
"""
|
|
277
|
+
lock_path = link.with_name(link.name + ".skf-lock")
|
|
278
|
+
link.parent.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
|
|
280
|
+
# Refuse only if <link> is a real directory/file (not a symlink or junction).
|
|
281
|
+
# Replacing a real dir would lose user data; replacing a link is the point.
|
|
282
|
+
if link.exists() and not _is_link_or_junction(link):
|
|
283
|
+
_die(2, f"refusing to replace non-link: {link}")
|
|
284
|
+
|
|
285
|
+
lock_fd = os.open(lock_path, os.O_WRONLY | os.O_CREAT, 0o644)
|
|
286
|
+
lock_held = False
|
|
287
|
+
try:
|
|
288
|
+
try:
|
|
289
|
+
_acquire_lock(lock_fd)
|
|
290
|
+
lock_held = True
|
|
291
|
+
except OSError as e:
|
|
292
|
+
if e.errno == errno.EAGAIN:
|
|
293
|
+
_die(2, f"another process holds flip lock on {link}")
|
|
294
|
+
raise
|
|
295
|
+
|
|
296
|
+
tmp_link = link.with_name(link.name + ".skf-tmp-link")
|
|
297
|
+
if tmp_link.is_symlink() or tmp_link.exists():
|
|
298
|
+
# On Windows a junction returns is_dir()=True, exists()=True but
|
|
299
|
+
# is_symlink()=False — must rmdir/unlink based on type.
|
|
300
|
+
if tmp_link.is_dir() and not tmp_link.is_symlink():
|
|
301
|
+
tmp_link.rmdir()
|
|
302
|
+
else:
|
|
303
|
+
tmp_link.unlink()
|
|
304
|
+
try:
|
|
305
|
+
link_kind = _create_symlink_or_junction(target, tmp_link)
|
|
306
|
+
except OSError as e:
|
|
307
|
+
if os.name == "nt" and getattr(e, "winerror", None) in (1314, 5):
|
|
308
|
+
_die(
|
|
309
|
+
2,
|
|
310
|
+
"symlink/junction creation failed on Windows. Junction fallback "
|
|
311
|
+
"requires the target directory to exist. Either enable Developer "
|
|
312
|
+
"Mode (Settings → Privacy & Security → For Developers) or use WSL2.",
|
|
313
|
+
)
|
|
314
|
+
raise
|
|
315
|
+
# os.replace can swap a symlink-over-symlink atomically, but Windows
|
|
316
|
+
# rejects renaming a directory (junction) over an existing directory
|
|
317
|
+
# (junction or real). Drop the existing link first when junction-based.
|
|
318
|
+
if link_kind == "junction" and (link.is_dir() or link.is_symlink() or link.exists()):
|
|
319
|
+
if link.is_symlink() or not link.is_dir():
|
|
320
|
+
link.unlink()
|
|
321
|
+
else:
|
|
322
|
+
link.rmdir()
|
|
323
|
+
os.replace(tmp_link, link)
|
|
324
|
+
finally:
|
|
325
|
+
if lock_held:
|
|
326
|
+
_release_lock(lock_fd)
|
|
327
|
+
os.close(lock_fd)
|
|
328
|
+
# Lock file is bookkeeping only — once the fd is closed the lock is
|
|
329
|
+
# gone, so leaving the file around just litters the skill_group dir.
|
|
330
|
+
# Best-effort unlink; a concurrent flipper may have already removed it.
|
|
331
|
+
try:
|
|
332
|
+
lock_path.unlink()
|
|
333
|
+
except OSError:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
_ok({"link": str(link), "points_to": target, "kind": link_kind})
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def main() -> None:
|
|
340
|
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
341
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
342
|
+
|
|
343
|
+
p_write = sub.add_parser("write", help="Atomic file write from stdin")
|
|
344
|
+
p_write.add_argument("--target", type=Path, required=True)
|
|
345
|
+
|
|
346
|
+
p_stage = sub.add_parser("stage-dir", help="Create staging directory")
|
|
347
|
+
p_stage.add_argument("--target", type=Path, required=True)
|
|
348
|
+
|
|
349
|
+
p_commit = sub.add_parser("commit-dir", help="Commit staging directory to target")
|
|
350
|
+
p_commit.add_argument("--target", type=Path, required=True)
|
|
351
|
+
p_commit.add_argument("--rollback", action="store_true", help="Restore from rollback dir instead of committing")
|
|
352
|
+
|
|
353
|
+
p_flip = sub.add_parser("flip-link", help="Atomic symlink flip")
|
|
354
|
+
p_flip.add_argument("--link", type=Path, required=True)
|
|
355
|
+
p_flip.add_argument("--target", type=str, required=True)
|
|
356
|
+
|
|
357
|
+
args = parser.parse_args()
|
|
358
|
+
|
|
359
|
+
if args.cmd == "write":
|
|
360
|
+
cmd_write(args.target)
|
|
361
|
+
elif args.cmd == "stage-dir":
|
|
362
|
+
cmd_stage_dir(args.target)
|
|
363
|
+
elif args.cmd == "commit-dir":
|
|
364
|
+
cmd_commit_dir(args.target, rollback=args.rollback)
|
|
365
|
+
elif args.cmd == "flip-link":
|
|
366
|
+
cmd_flip_link(args.link, args.target)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
if __name__ == "__main__":
|
|
370
|
+
main()
|