bmad-module-skill-forge 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/docs/_data/pinned.yaml +1 -1
- package/docs/examples.md +1 -1
- package/docs/troubleshooting.md +8 -0
- package/docs/verifying-a-skill.md +7 -3
- package/docs/workflows.md +3 -3
- package/package.json +1 -1
- package/src/knowledge/qmd-registry.md +1 -1
- package/src/knowledge/version-paths.md +1 -1
- package/src/shared/health-check.md +2 -50
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +2 -2
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +2 -2
- package/src/shared/scripts/skf-atomic-write.py +212 -59
- package/src/shared/scripts/skf-build-change-manifest.py +593 -47
- package/src/shared/scripts/skf-classify-changed-files.py +78 -0
- package/src/shared/scripts/skf-compare-file-hashes.py +266 -104
- package/src/shared/scripts/skf-derive-assembly-shape.py +18 -1
- package/src/shared/scripts/skf-detect-scripts-assets.py +196 -54
- package/src/shared/scripts/skf-extract-public-api.py +1309 -157
- package/src/shared/scripts/skf-extraction-inventory.py +580 -46
- package/src/shared/scripts/skf-extraction-snapshot.py +158 -33
- package/src/shared/scripts/skf-forge-tier-rw.py +26 -3
- package/src/shared/scripts/skf-github-probe.py +47 -8
- package/src/shared/scripts/skf-hash-content.py +14 -8
- package/src/shared/scripts/skf-load-provenance.py +3 -1
- package/src/shared/scripts/skf-merge-doc-urls.py +142 -19
- package/src/shared/scripts/skf-preflight.py +15 -1
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +25 -1
- package/src/shared/scripts/skf-resolve-authoritative-files.py +29 -3
- package/src/shared/scripts/skf-structural-diff.py +133 -18
- package/src/shared/scripts/skf-update-active-symlink.py +115 -62
- package/src/shared/scripts/skf-validate-brief-schema.py +19 -1
- package/src/shared/scripts/skf-verify-provenance-completeness.py +977 -49
- package/src/shared/scripts/skf-write-skill-brief.py +41 -8
- package/src/skf-analyze-source/references/map-unit-exports.md +1 -1
- package/src/skf-audit-skill/assets/drift-report-template.md +1 -0
- package/src/skf-audit-skill/references/headless-contract.md +1 -1
- package/src/skf-audit-skill/references/health-check.md +2 -0
- package/src/skf-audit-skill/references/init.md +2 -1
- package/src/skf-audit-skill/references/re-index.md +14 -9
- package/src/skf-audit-skill/references/report.md +2 -2
- package/src/skf-audit-skill/references/semantic-diff.md +15 -15
- package/src/skf-audit-skill/references/structural-diff.md +10 -5
- package/src/skf-audit-skill/scripts/render-drift-tables.py +29 -2
- package/src/skf-brief-skill/SKILL.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -9
- package/src/skf-brief-skill/references/analyze-target.md +6 -6
- package/src/skf-brief-skill/references/confirm-brief.md +1 -1
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-brief-skill/references/step-auto-brief.md +11 -29
- package/src/skf-brief-skill/references/version-resolution.md +5 -9
- package/src/skf-create-skill/assets/compile-assembly-rules.md +5 -3
- package/src/skf-create-skill/assets/skill-sections.md +6 -6
- package/src/skf-create-skill/references/authoritative-files-protocol.md +2 -23
- package/src/skf-create-skill/references/compile.md +31 -60
- package/src/skf-create-skill/references/component-extraction.md +3 -3
- package/src/skf-create-skill/references/enrich.md +2 -2
- package/src/skf-create-skill/references/entry-points-by-hand.md +24 -7
- package/src/skf-create-skill/references/extract.md +22 -19
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +9 -9
- package/src/skf-create-skill/references/extraction-patterns.md +8 -9
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +16 -5
- package/src/skf-create-skill/references/source-resolution-protocols.md +12 -17
- package/src/skf-create-skill/references/sub/fetch-docs.md +1 -1
- package/src/skf-drop-skill/references/execute.md +5 -1
- package/src/skf-drop-skill/references/health-check.md +2 -0
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-drop-skill/references/report.md +2 -2
- package/src/skf-drop-skill/references/select.md +7 -3
- package/src/skf-test-skill/references/coherence-check.md +1 -1
- package/src/skf-test-skill/references/coverage-check-tiers.md +5 -5
- package/src/skf-test-skill/references/coverage-check.md +39 -57
- package/src/skf-test-skill/references/init.md +3 -5
- package/src/skf-test-skill/references/report.md +10 -8
- package/src/skf-test-skill/references/scoring-rules.md +6 -5
- package/src/skf-test-skill/references/source-access-protocol.md +14 -17
- package/src/skf-test-skill/scripts/build-result-context.py +114 -21
- package/src/skf-test-skill/scripts/gap-ledger.py +315 -30
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +534 -96
- package/src/skf-test-skill/scripts/reconcile-coverage.py +276 -2
- package/src/skf-test-skill/scripts/score-signatures.py +36 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +7 -5
- package/src/skf-update-skill/references/detect-changes.md +32 -13
- package/src/skf-update-skill/references/gap-driven.md +3 -3
- package/src/skf-update-skill/references/init.md +6 -2
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +2 -2
- package/src/skf-update-skill/references/re-extract.md +2 -2
- package/src/skf-update-skill/references/report.md +3 -1
- package/src/skf-update-skill/references/write.md +43 -23
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +157 -42
|
@@ -34,6 +34,10 @@ sourceTreeProbeOrder:
|
|
|
34
34
|
findTestReportProbeOrder:
|
|
35
35
|
- '{project-root}/_bmad/skf/shared/scripts/skf-find-test-report.py'
|
|
36
36
|
- '{project-root}/src/shared/scripts/skf-find-test-report.py'
|
|
37
|
+
# `{extractPublicApiHelper}`: §6c's source version.
|
|
38
|
+
extractPublicApiProbeOrder:
|
|
39
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-extract-public-api.py'
|
|
40
|
+
- '{project-root}/src/shared/scripts/skf-extract-public-api.py'
|
|
37
41
|
---
|
|
38
42
|
|
|
39
43
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -343,7 +347,7 @@ Dispatch on `{source_tree_status}`:
|
|
|
343
347
|
|
|
344
348
|
### 6c. Detect the Source Version
|
|
345
349
|
|
|
346
|
-
Run only when `{source_tree_status}` is `ready` or `offline`. Read the source's version from `{source_root}`
|
|
350
|
+
Run only when `{source_tree_status}` is `ready` or `offline`. Read the source's version from `{source_root}` by the Version Reconciliation section of `skf-create-skill/references/source-resolution-protocols.md` (its `{extractPublicApiHelper}` call, from `{extractPublicApiProbeOrder}`), with this skill's `name` and metadata.json `language` for the brief's. When no version file gives one, leave `{source_version_detected}` unset and skip the rest of this section. Otherwise compare it with the metadata.json `version` through `{skillInventoryHelper}` (resolve it ← first existing path in `{skillInventoryProbeOrder}`), from `{project-root}`:
|
|
347
351
|
|
|
348
352
|
```bash
|
|
349
353
|
uv run {skillInventoryHelper} version order "{the source's version}" "{version}"
|
|
@@ -353,7 +357,7 @@ It prints `order` and `major_minor` (the source's version against `{version}`) a
|
|
|
353
357
|
|
|
354
358
|
- **`order` is `higher`** (a higher semantic version than the metadata.json `version`): bind `{source_version_detected}` ← `a.normalized`. Step 4 §6b and step 5 §2 name the version this update writes after it.
|
|
355
359
|
- **Otherwise** leave `{source_version_detected}` unset. When `major_minor` is `lower`, add `source-version-lower: {target_ref} reads {a.normalized}, older than {version}; this update keeps the patch-version rule` to `warnings[]`. A source version lower only in its patch number is expected, not a regression: every update that finds no higher version increments the skill's patch number (step 4 §6b), so a skill forged at the source's 1.2.0 is 1.2.1 after one update while the source still reads 1.2.0.
|
|
356
|
-
- **Exit 1** (`code` `NOT_A_VERSION`: a value that names no version
|
|
360
|
+
- **Exit 1** (`code` `NOT_A_VERSION`: a value that names no version), **no candidate, or no JSON:** leave `{source_version_detected}` unset: step 4 §6b takes the next patch version.
|
|
357
361
|
|
|
358
362
|
### 7. Present Baseline Summary
|
|
359
363
|
|
|
@@ -8,5 +8,5 @@ The argument set, gate map, outputs and headless result line of `skf-update-skil
|
|
|
8
8
|
| **Flags** | `--headless` / `-H`; `--from-test-report` (gap-driven mode); `--target-ref <ref>`; `--allow-workspace-drift` (gap-driven only: update-skill takes nothing from HEAD, and a gap that needs the pinned tree, a rescope included, halts `halted-for-workspace-drift` before merge); `--allow-degraded`; `--detect-only` (status `detect-only`); `--dry-run` (status `dry-run`, writes nothing in any mode; a gap-driven.md halt such as `halted-for-remediation-path` or `halted-for-workspace-drift` still stops it). init.md §1 describes each; `--detect-only` wins over `--dry-run`. |
|
|
9
9
|
| **Gates** | step 1: init.md §4's degraded-rebuild gate [D]/[X] when no provenance map is found (headless: halts, or takes [D] under `--allow-degraded`; a gap-driven run halts `blocked` there instead); init.md §4b's [G]/[S] test-report offer, interactive only (headless warns `unconsumed-test-report`); Confirm Gate [C] (init.md §8) | step 2, each only when triggered: detect-changes.md §1b's promote prompt and §1c's scope-expansion prompt [P]/[S]/[U] (headless: defers each candidate to a person), §2.2's deletion-ratio gate [C]/[B]/[A] (headless: [C]) | step 2g (gap-driven): gap-driven.md §1 rule R1's [D]/[R] for each gap whose remediation names removal (headless: [D], or [R] only when the export is internal or `#[doc(hidden)]`) | step 4: Confirm Gate [C] only after the user resolved [MANUAL] conflicts (merge.md §4); a clean merge auto-proceeds, and a headless run with conflicts halts `halted-for-manual-mismatch` before anything is written |
|
|
10
10
|
| **Outputs** | A new version folder `{skill_group}/{new_version}/` and its forge folder, beside the unchanged previous version (a gap-driven repair writes the current version in place), and a finished run's `update-skill-result-{YYYYMMDD-HHmmss}.json` and `update-skill-result-latest.json` in that version's forge folder (a run that found no change: the current one's). `--detect-only` and `--dry-run` write none of these, nor the skill brief. Run state goes to a run folder under `{project-root}/_bmad-output/.skf-run/` that step 7 removes; a halt keeps it |
|
|
11
|
-
| **Concurrency** | A second real update of the same skill halts `halted-for-concurrent-run` while the first holds its run lock (init.md §1b); `--detect-only` and `--dry-run` take no lock. A remote skill is read from a private tree, and no mode writes to the shared workspace clone before write.md §9. |
|
|
11
|
+
| **Concurrency** | A second real update of the same skill halts `halted-for-concurrent-run` while the first holds its run lock (init.md §1b); `--detect-only` and `--dry-run` take no lock. write.md §8 halts with the same status when another flip of the `active` link holds its lock for 5 seconds. A remote skill is read from a private tree, and no mode writes to the shared workspace clone before write.md §9. |
|
|
12
12
|
| **Headless** | No gate prompts: each resolves as the Gates row says, and the run records the decision. One `SKF_UPDATE_RESULT_JSON` line (report.md §5b), at step 6 or at a HALT, printed by the shared emitter and checked against `shared/scripts/schemas/skf-update-result-envelope.v1.json`. It is the run's last line: the shared health check displays it after everything else; a HALT displays its line and stops. Pipelines branch on `skf_update.status`: `success`, `no-changes`, `detect-only` and `dry-run` are successful exits. |
|
|
@@ -79,7 +79,7 @@ Apply merge in the following priority order:
|
|
|
79
79
|
- **Gap-driven rescopes** (`DELETED_EXPORT` from gap-driven.md §1 rule R1, verification `rescoped`) are processed here with the same removal. §6b writes the entry's `rescope` (its `scope.amendments[]` entry, `action: "excluded"`, and its `scope.exclude` path) to the skill brief before SKILL.md, and step 5 removes the provenance `entries[]` row and recomputes `stats` from the amended `brief.scope` (write.md §2/§3). gap-driven.md §4 HALTs on a rescope that carries no `rescope`, so no unscoped removal reaches here.
|
|
80
80
|
|
|
81
81
|
**Priority 2 — Process MOVED exports:**
|
|
82
|
-
- Update file:line citations in generated content
|
|
82
|
+
- Update file:line citations in generated content: a `MOVED_EXPORT`'s citation takes its `new_line`, and when the change manifest gives it an `old_file` (a move across files), a citation `[AST:{old_file}:L{old_line}]` (or `[SRC:...]`) becomes the file its `per_file` item names and its `new_line`, `[AST:{file_path}:L{new_line}]`, keeping its prefix. A `MODIFIED_EXPORT` with an `old_file` (it also changed) is rewritten whole by Priority 4.
|
|
83
83
|
- **Gap-driven:** move citations only for an export whose gap-driven.md §4 spot-check recorded `moved`. A `MOVED_EXPORT` that recorded `unknown` (the drift override among the causes), `verified` or `missing` moves none: write.md §3 leaves its line as it is.
|
|
84
84
|
- Update provenance map file references
|
|
85
85
|
- [MANUAL] blocks unaffected (content unchanged)
|
|
@@ -99,6 +99,7 @@ Apply merge in the following priority order:
|
|
|
99
99
|
- Append new export content to appropriate section
|
|
100
100
|
- Place before any [MANUAL] blocks at section boundary
|
|
101
101
|
- No conflicts expected (new content, no existing [MANUAL])
|
|
102
|
+
- **A `public-api` skill's public surface (normal mode only):** document no new export, and no export of an ADDED or MOVED file, whose record in `{run_dir}/reextract-records.json` is marked `public: false`, unless the provenance map holds the export (by name at its `old_path` for a MOVED file) or the old name it renames, or this update removes an entry of its name (a `DELETED_EXPORT`, a DELETED file's entry): `records` (re-extract.md §4) found it off the public surface, and step 5's `apply` adds no entry for it, so SKILL.md, the provenance map and metadata.json's `exports[]` name the same exports. A record with no `public` mark is documented as before, and an export the map already holds keeps its content (Priority 4) whatever its mark. In gap-driven and degraded mode document every record whatever its mark: `apply` maps them all.
|
|
102
103
|
- **Gap-driven cited `NEW_EXPORT` whose spot-check pinned a line** (gap-driven.md §4 recorded `verified` or `moved` for an export the provenance map does not hold): cite it as `[SRC:{source_file}:L{line}]`, where `{line}` is the citation's line for `verified` and the `new_location` line for `moved`: the line write.md §3 records in its new `source-read` entry. The spot-check found that line by the verifier's text rules, not by an ast-grep recipe, so the prefix is `SRC`, never `AST`.
|
|
103
104
|
|
|
104
105
|
**Priority 6 — Process script/asset file changes (from Category D in change manifest):**
|
|
@@ -111,7 +112,6 @@ Category D operates on every `file_entries[]` row regardless of `file_type`. Han
|
|
|
111
112
|
- NEW_FILE: queue file for copy from `{source_root}`, add to `file_entries`
|
|
112
113
|
- Files in `scripts/[MANUAL]/` or `assets/[MANUAL]/` are never modified (user-authored)
|
|
113
114
|
- Update Section 7b manifest table to reflect changes
|
|
114
|
-
- Update `metadata.json` `scripts[]`/`assets[]` arrays and `stats.scripts_count`/`stats.assets_count`
|
|
115
115
|
|
|
116
116
|
- **`file_type: "doc"`** (authoritative docs promoted by §2a/§1b):
|
|
117
117
|
- MODIFIED_FILE: update `file_entries` content_hash only. **Do NOT copy the file** — doc-type entries are source-tracked but not bundled. Record the drift in the update report.
|
|
@@ -66,7 +66,7 @@ The halt leaves `{run_dir}` in place.
|
|
|
66
66
|
**Forge tier (AST structural extraction):**
|
|
67
67
|
|
|
68
68
|
- Step 2's Category B ran the recipe runner (`{extractPublicApiHelper}` `--mode full`, the recipes of the AST Extraction Protocol in `{extractionPatternsData}`) over every file to extract, `{run_dir}/extract-files.json`, and wrote `{run_dir}/extraction.json`: never run it again here. §4's helper takes each file's exports from its `exports[]`, each at the line of its name, never a decorator or `export` line above it: T1 (AST-verified structural truth) with `extraction_method: ast-grep` and the recipe's labels.
|
|
69
|
-
- Read by eye only what the recipes leave out: an export a file defines in a form Known Limitation #11 in `{extractionPatternsData}` lists, a file `file_issues[]` names (not UTF-8, or code the parser could not read, where a recipe can miss an export), a name `entry_point_diff.extraction_gaps[]` lists, and each file step 2 told the user it read by eye. Step 2 already read those of the modified and added files into `{run_dir}/export-details.json` (its entries with an `export_type`), and §4's helper takes them from there. An export read by eye is T1-low with `extraction_method: source-read` and `ast_node_type: null`.
|
|
69
|
+
- Read by eye only what the recipes leave out: an export a file defines in a form Known Limitation #11 in `{extractionPatternsData}` lists, a file `file_issues[]` names (not UTF-8, or code the parser could not read, where a recipe can miss an export), a name `entry_point_diff.extraction_gaps[]` lists, a map entry `{run_dir}/baseline-gaps.json` lists (a module, an alias that renames, `__version__` or another underscore name the runner never reports, which its file still declares), and each file step 2 told the user it read by eye. Step 2 already read those of the modified and added files into `{run_dir}/export-details.json` (its entries with an `export_type`), and §4's helper takes them from there. An export read by eye is T1-low with `extraction_method: source-read` and `ast_node_type: null`.
|
|
70
70
|
|
|
71
71
|
**Tier degradation handling (Forge/Forge+/Deep):** If step 2's runner could run no ast-grep, or left files unread (step 2 told the user which), follow `{tierDegradationRulesData}` for fallback strategy and user notification requirements. Silent degradation is forbidden: the user must always know which files were read by eye instead of matched, and why.
|
|
72
72
|
|
|
@@ -135,7 +135,7 @@ uv run {buildChangeManifestHelper} records \
|
|
|
135
135
|
-o "{run_dir}/reextract-records.json"
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
Pass `--extraction` and `--export-details` when step 2 wrote them, and `--patches` when §2 or §3 wrote a patch. It seeds each export's record from step 2's files (name, type, signature, location, labels, return type and `params` in the provenance map's typed form), merges the patches, writes `{run_dir}/reextract-records.json`, where merge and step 5's `apply` read it, and prints `files_extracted`, `exports_extracted` and `confidence_breakdown` (T1, T1-low, T2): never type a record or count one by hand. Add each of its `warnings[]` to `warnings[]`. On exit 1 whose error names a patch file, rewrite that file once in the shape of §2's return contract and run `records` again. On a second exit 1, any other exit 1, no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "re-extract:records"`, its stderr as `reason`).
|
|
138
|
+
Pass `--extraction` and `--export-details` when step 2 wrote them, and `--patches` when §2 or §3 wrote a patch. It seeds each export's record from step 2's files (name, type, signature, location, labels, return type and `params` in the provenance map's typed form), merges the patches, writes `{run_dir}/reextract-records.json`, where merge and step 5's `apply` read it, and prints `files_extracted`, `exports_extracted` and `confidence_breakdown` (T1, T1-low, T2): never type a record or count one by hand. Under a `public-api` scope (the `scope.type` detect-changes.md §1 bound, which `{run_dir}/extraction.json` records) it marks each export `public: true` or `public: false` by the runner's public surface (its `entry_point_diff`, by the rule the "Records" section of the helper's docstring gives), and prints `marked_not_public`, the count of `public: false`: in a normal-mode update merge documents, and step 5's `apply` adds, no export marked `public: false` that the provenance map does not already hold (merge.md Priority 5). In degraded mode the mark is not used. Add each of its `warnings[]` to `warnings[]`. On exit 1 whose error names a patch file, rewrite that file once in the shape of §2's return contract and run `records` again. On a second exit 1, any other exit 1, no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "re-extract:records"`, its stderr as `reason`).
|
|
139
139
|
|
|
140
140
|
### 5. Display Extraction Summary and Auto-Proceed
|
|
141
141
|
|
|
@@ -153,6 +153,8 @@ In `{headless_mode}`, print the line as §5b says, with `status: "dry-run"` and
|
|
|
153
153
|
|
|
154
154
|
{when the change manifest's `category_d` lists a document: **Tracked documents changed upstream:** each `docs_modified` path (modified) and `docs_deleted` path (deleted)}
|
|
155
155
|
|
|
156
|
+
{when step 2 wrote `{run_dir}/new-files.json`: **New scripts and assets:** {the length of its `new_files[]`} new; set aside: {the length of `tracked_code[]`} code files the map cites, {the length of `out_of_scope[]`} outside the brief's scope, {the length of `intent_none[]`} of a kind whose intent is `none` (counted from the lists, never from `stats`)}
|
|
157
|
+
|
|
156
158
|
{in gap-driven mode, when gap-driven.md §1 left gaps unrouted: **Not repaired by this run:** each `{id}: {title} ({category})`}
|
|
157
159
|
|
|
158
160
|
### Export Changes
|
|
@@ -270,7 +272,7 @@ In `{headless_mode}`, bind `{result_envelope_line}` to that line and do not disp
|
|
|
270
272
|
- `status`: `"success"` here; §1 stages `"no-changes"`, §1a `"detect-only"` and §1b `"dry-run"`; a halt prints its own `halted-for-*` or `blocked` line through its step's halt procedure and never reaches this step. The full enum lives in the schema.
|
|
271
273
|
- `headless_decisions[]`: every gate's record in the run's decision log (init.md §4 degraded-rebuild and §8 confirmation, detect-changes.md §1b / §1c / §2.2, gap-driven.md §1 rule R1, merge.md §8). Each entry `{gate, default_action, taken_action, reason, evidence?}`. Empty when no gate auto-resolved.
|
|
272
274
|
- `error`: null on every exit this step prints. A halt's line carries `{phase, path?, reason}`, and pipelines branch on `error !== null` for non-zero exit semantics.
|
|
273
|
-
- `warnings[]`: every entry the run recorded, among them `source-tree:`, `source-not-fetched`, `file-diff-unavailable`, `no-baseline-time`, `moved-check-skipped` and `unknown-language` (detect-changes.md §2.1 Category A), `source-version-lower`, `unconsumed-test-report` (init.md §4b), `interrupted-run-cleaned` and `interrupted-run-not-cleaned` (init.md §1b), `test-report:` entries (what the test report's helpers could not read, and `test-report: not routed: {id} ({category})` for each gap gap-driven.md §1 did not route), `proposed-amendment:` (a read-only run's brief decisions, not written), `doc-fetch-failed`, `doc-not-hashed` and `doc-drift-not-checked` (a docs-only skill's documents), `workspace-clone-not-updated`, `target-ref-not-recorded`, `workspace_drift_overridden` (gap-driven.md §3), `run-state-not-finished` and `provenance:` entries (write.md §3 and §6: provenance findings left for a person, and spot-check entries §3 left for a person to decide).
|
|
275
|
+
- `warnings[]`: every entry the run recorded, among them `source-tree:`, `source-not-fetched`, `file-diff-unavailable`, `no-baseline-time`, `moved-check-skipped` and `unknown-language` (detect-changes.md §2.1 Category A), `new-files-not-checked` (Category D: a skill with no brief, so no new script or asset was looked for), `source-version-lower`, `unconsumed-test-report` (init.md §4b), `interrupted-run-cleaned` and `interrupted-run-not-cleaned` (init.md §1b), `test-report:` entries (what the test report's helpers could not read, and `test-report: not routed: {id} ({category})` for each gap gap-driven.md §1 did not route), `proposed-amendment:` (a read-only run's brief decisions, not written), `doc-fetch-failed`, `doc-not-hashed` and `doc-drift-not-checked` (a docs-only skill's documents), `workspace-clone-not-updated`, `target-ref-not-recorded`, `workspace_drift_overridden` (gap-driven.md §3), `run-state-not-finished`, `public-api-counted-by-hand:` (write.md §2: the recipe runner exited 2, so the public names were found without it; it quotes the runner's error line) and `provenance:` entries (write.md §3 and §6: provenance findings left for a person, and spot-check entries §3 left for a person to decide).
|
|
274
276
|
|
|
275
277
|
**Post-finalization hook.** A finished run, this one or §1's no-change exit, fires the hook; `--detect-only`, `--dry-run` and a halt never do (customize.toml says so). If `{onCompleteCommand}` (resolved in SKILL.md On Activation §4 from `workflow.on_complete`) is non-empty, invoke it after the emitter wrote both result files:
|
|
276
278
|
|
|
@@ -72,8 +72,8 @@ checkWorkspaceDriftProbeOrder:
|
|
|
72
72
|
- '{project-root}/_bmad/skf/shared/scripts/skf-check-workspace-drift.py'
|
|
73
73
|
- '{project-root}/src/shared/scripts/skf-check-workspace-drift.py'
|
|
74
74
|
# Resolve `{buildChangeManifestHelper}` to the first existing path; HALT if
|
|
75
|
-
# neither exists: §3's `apply` writes the provenance map
|
|
76
|
-
# records, never by hand.
|
|
75
|
+
# neither exists: §3's `apply` writes the provenance map and §2's `metadata`
|
|
76
|
+
# call metadata.json, from this run's records, never by hand.
|
|
77
77
|
buildChangeManifestProbeOrder:
|
|
78
78
|
- '{project-root}/_bmad/skf/shared/scripts/skf-build-change-manifest.py'
|
|
79
79
|
- '{project-root}/src/shared/scripts/skf-build-change-manifest.py'
|
|
@@ -89,8 +89,8 @@ compareDocHashesProbeOrder:
|
|
|
89
89
|
validateOutputProbeOrder:
|
|
90
90
|
- '{project-root}/_bmad/skf/shared/scripts/skf-validate-output.py'
|
|
91
91
|
- '{project-root}/src/shared/scripts/skf-validate-output.py'
|
|
92
|
-
# `{extractPublicApiHelper}`: §2's public API counts
|
|
93
|
-
#
|
|
92
|
+
# `{extractPublicApiHelper}`: §2's public API counts, the runner's or those
|
|
93
|
+
# `{entryPointsByHandData}` has it make; if neither exists, §2 halts.
|
|
94
94
|
extractPublicApiProbeOrder:
|
|
95
95
|
- '{project-root}/_bmad/skf/shared/scripts/skf-extract-public-api.py'
|
|
96
96
|
- '{project-root}/src/shared/scripts/skf-extract-public-api.py'
|
|
@@ -183,17 +183,17 @@ uv run {runLockHelper} acquire \
|
|
|
183
183
|
|
|
184
184
|
`{lock_recovery}` is, outside gap-driven mode, "The previous version is unchanged, and the halt removed `{skill_group}/{new_version}/` and `{forge_data_folder}/{skill_name}/{new_version}/`, which step 4 created for this update: re-run update-skill once no other update of {skill_name} runs." and, in gap-driven mode, "This repair already wrote SKILL.md in place; the halt restored the package and the skill brief from the snapshot step 4 took, so the skill is as it was: re-run update-skill once no other update of {skill_name} runs."
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
- **First, apply any queued `metadata_patches[]`** (staged by merge Priority 8b from gap-driven `metadata update` entries)
|
|
188
|
-
- **For gap-driven rescopes** (`DELETED_EXPORT` / verification `rescoped`): the removed exports
|
|
189
|
-
-
|
|
190
|
-
-
|
|
191
|
-
- **Record the source commit** (when `{source_tree_status}` is `ready` or `offline`):
|
|
192
|
-
-
|
|
193
|
-
- **Compute the `stats` block and `confidence_distribution` deterministically** with `{renderMetadataStatsHelper}` (resolve from `{renderMetadataStatsProbeOrder}`; first existing path wins): the same helper sibling create-skill compiles them with, so create and update emit byte-identical stats for identical inputs. The helper owns all the arithmetic: it bins each provenance `entries[]` row once by its `signature_source` tier into `confidence_distribution.{t1,t1_low,t2,t3}`, sets `exports_documented` = the entry count for the library shape (for a reference app, the Pattern Surface row count; see **Shape** below), and derives `exports_total` = `exports_public_api` + `exports_internal`, `public_api_coverage` = documented / public_api (`null` if public_api is 0), `total_coverage` = documented / total (`null` if total is 0), plus `scripts_count` / `assets_count` from the inventory arrays. Run `uv run {renderMetadataStatsHelper} --help` for the full contract. Do not hand-bin the distribution: binning T2 annotations + T3 doc items on top of the per-export tiers double-counts, which per-entry binning by `signature_source` makes structurally impossible. You supply only the payload:
|
|
186
|
+
The `metadata` call below writes `{skill_package}/metadata.json`, never by hand:
|
|
187
|
+
- **First, apply any queued `metadata_patches[]`** (staged by merge Priority 8b from gap-driven `metadata update` entries) to `metadata.json`, before §3's `apply` reads it: each surgical patch described in the gap's remediation (reconcile a divergent count, add an explanatory stat, etc.). The `metadata` call keeps a patch in any field but those its `--help` lists (e.g., `exports_documented`), whose value it writes; apply none to `exports[]`, which §3's `apply` builds from the list it reads. Log each patch the call overwrites or you leave out, so a still-stale value surfaces in the report rather than being silently overwritten.
|
|
188
|
+
- **For gap-driven rescopes** (`DELETED_EXPORT` / verification `rescoped`): §3's `apply` drops the removed exports from `exports`, and `stats` recompute from that reduced surface, which the `brief.scope.exclude` + `scope.amendments[]` (`action: "excluded"`) step 4 §6b wrote from gap-driven.md §1's rule R1 justify.
|
|
189
|
+
- `version` is `{new_version}`, the version step 4 §6b chose: **if `update_mode == "gap-driven"`, that is the unchanged version**: the skill is being repaired against the same source commit, so of the fields that mark an update only `generation_date` below changes, and step 4 wrote into the existing version directory (step 1 §6c records no source version in gap-driven mode). Otherwise it is the version whose folder step 4 §6b created: `{source_version_detected}` when step 1 §6c recorded `source_version_detected`, else the next patch version. Never pick another value here: `version` must name the folder `metadata.json` sits in, the one §8 points the `active` link at. From here on `{version}` is `{new_version}`.
|
|
190
|
+
- Bind `{generation_date}` ← the current UTC time to the second, read from the clock with `date -u +%Y-%m-%dT%H:%M:%SZ`, never typed: step 1 §4b offers a test report only when it is newer. It is the only update bookkeeping metadata.json carries, in every mode: §3's update operation block records `last_update` and `update_type` in provenance-map.json, and the `metadata` call removes either key an older SKF version left in metadata.json.
|
|
191
|
+
- **Record the source commit** (when `{source_tree_status}` is `ready` or `offline`): pass `--source-commit "{target_commit}"`, the commit every step of this run read, and, when `{target_ref_override}` is set, `--source-ref "{target_ref}"`. The call leaves `source_root` as metadata.json records it: it names the clone SKF keeps for the repository; never write `{source_tree}` or any other path of this run into an artifact. For any other source (a local folder, gap-driven mode, docs-only) pass neither, which keeps both. The in-context `{source_commit}` stays the value init.md §6 read; §9 needs it.
|
|
192
|
+
- **Compute the `stats` block and `confidence_distribution` deterministically** with `{renderMetadataStatsHelper}` (resolve from `{renderMetadataStatsProbeOrder}`; first existing path wins): the same helper sibling create-skill compiles them with, so create and update emit byte-identical stats for identical inputs. The helper owns all the arithmetic: it bins each provenance `entries[]` row once by its `signature_source` tier into `confidence_distribution.{t1,t1_low,t2,t3}`, sets `exports_documented` = the entry count for the library shape (for a reference app, the Pattern Surface row count; see **Shape** below), and derives `exports_total` = `exports_public_api` + `exports_internal`, `public_api_coverage` = documented / public_api (`null` if public_api is 0), `total_coverage` = documented / total (`null` if total is 0), plus `scripts_count` / `assets_count` from the inventory arrays. Run `uv run {renderMetadataStatsHelper} --help` for the full contract. You supply only the payload:
|
|
194
193
|
|
|
195
194
|
**Payload (passed as JSON on stdin):**
|
|
196
|
-
- `exports_public_api` and `exports_internal`, unless the drift override below applies. At Quick tier, count
|
|
195
|
+
- `exports_public_api` and `exports_internal`, unless the drift override below applies. At Quick tier, count them by `{entryPointsByHandData}`, as create-skill does, and pass the `counts` of `{run_dir}/denominator.json`. Otherwise, from `{project-root}`, run `uv run {extractPublicApiHelper} --mode full --source-root "{source_root}" [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] [--language "{language}"] [--scope-type "{scope_type}"] --head-cap 0 -o "{run_dir}/public-api.json"` once over the whole scope (no `--files-from`), with the longest timeout your shell tool takes (a large tree takes minutes), as create-skill's extract.md §4 does, and pass its `counts.exports_public_api` and `counts.exports_internal`, as its compile.md §4 does. Pass `--brief` when that file exists (step 4 §6b wrote a rescope's amendment into it), and `--language` and `--scope-type` when metadata.json records them. On exit 1 (`incomplete`) run it once more. When its `files_without_recipes` is not empty (in-scope files no recipe reads), count those files' entry points' names by steps 2 to 4 of `{entryPointsByHandData}`, with `--extraction "{run_dir}/public-api.json"`, as create-skill's extract.md §4b does, and pass the `counts` of `{run_dir}/denominator.json` in place of the runner's: they hold its names. Still incomplete, or on exit 2 or 3, no JSON, no candidate, or `entry_points.status` `no-entry-point`, count them by `{entryPointsByHandData}` and pass the `counts` of `{run_dir}/denominator.json`. On exit 2 (an input error, such as a skill brief it cannot read, or a failure it did not foresee), also add `public-api-counted-by-hand: {line}` to `warnings[]`, `{line}` being the stderr line that holds `error:`, recorded as **Warnings go to the run log** says, and show it to the user: the runner should have made these counts. When a call of `{entryPointsByHandData}` fails as its step 4 says, HALT with status `blocked` (halt procedure: `phase: "write:public-api"`, its `error:` line, or the helper that does not resolve, as `reason`). A docs-only skill has no tree to count: pass the values metadata.json records, as the drift override does.
|
|
196
|
+
- `effective_denominator`: the `counts.effective_denominator` of the JSON whose two counts you passed above, when its `counts.effective_denominator_decision.store` is true and the skill brief's `scope.notes` documents the stratification: `skf-extract-public-api.py` decides `store` as it does for create-skill, so the count follows the source and the brief this update read. Under the drift override, and for a docs-only skill, pass the value `stats.effective_denominator` records once the queued `metadata_patches[]` are applied, when it records one. Otherwise leave it out, and `metadata.json` keeps no `stats.effective_denominator`: remove one an earlier run recorded.
|
|
197
197
|
- `scripts` / `assets`: the scripts / assets inventory arrays (or `[]` when empty) — the helper sets `scripts_count` / `assets_count` from their lengths
|
|
198
198
|
- `pattern_surfaces_documented` (reference app only): the number of rows in the `## Pattern Surface` table of the merged SKILL.md, the count create-skill's compile.md §4 passes. The helper uses it as `exports_documented` and emits it as `stats.pattern_surfaces_documented`, the count test-skill measures a reference app's coverage by.
|
|
199
199
|
|
|
@@ -201,23 +201,36 @@ Update `{skill_package}/metadata.json`:
|
|
|
201
201
|
|
|
202
202
|
**Shape:** read `scope_type` from `{skill_package}/metadata.json` before this section rewrites it (create-skill records the brief's `scope.type` there). When it is `reference-app`, pass `--shape reference-app` and put `pattern_surfaces_documented` in the payload, as create-skill's compile.md §4 does; for any other `scope_type`, or none, pass no `--shape` (the helper's default, the library shape).
|
|
203
203
|
|
|
204
|
-
**Invoke
|
|
204
|
+
**Invoke**: the helper reads `entries[]`, so run §3's `apply` first; the scripts / assets inventory arrays are the `scripts` and `assets` lists it wrote to `{run_dir}/metadata-lists.json`:
|
|
205
205
|
|
|
206
206
|
```bash
|
|
207
207
|
echo '{"exports_public_api": {N}, "exports_internal": {M}, "scripts": {scripts-inventory-or-[]}, "assets": {assets-inventory-or-[]}}' \
|
|
208
|
-
| uv run {renderMetadataStatsHelper} {forge_version}/provenance-map.json
|
|
208
|
+
| uv run {renderMetadataStatsHelper} {forge_version}/provenance-map.json -o "{run_dir}/stats.json"
|
|
209
209
|
```
|
|
210
210
|
|
|
211
211
|
For a reference app, the same call with the Pattern Surface row count and the shape:
|
|
212
212
|
|
|
213
213
|
```bash
|
|
214
214
|
echo '{"exports_public_api": {N}, "exports_internal": {M}, "pattern_surfaces_documented": {P}, "scripts": {scripts-inventory-or-[]}, "assets": {assets-inventory-or-[]}}' \
|
|
215
|
-
| uv run {renderMetadataStatsHelper} {forge_version}/provenance-map.json --shape reference-app
|
|
215
|
+
| uv run {renderMetadataStatsHelper} {forge_version}/provenance-map.json --shape reference-app -o "{run_dir}/stats.json"
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
If its `coherence.ok` (in `{run_dir}/stats.json`, which the `metadata` call reads) is false, fix the provenance map and re-run the helper; never hand-edit the stats. A `confidence_distribution` violation means some provenance entries carry a missing or unrecognized `signature_source` (§3 must write it on every entry).
|
|
219
219
|
|
|
220
|
-
**Label violations.** A violation whose `field` is `provenance.entries[<i>].<field>` means entry `<i>` (its `export_name` is in the violation) carries a label its `extraction_method` does not allow: relabel it by the `## Relabel Rule` section of `{extractionPatternsData}`, with `{source_root}` as the tree its node-kind lookup reads. **Under the drift override** (`{workspace_drift_status}` is `overridden`, gap-driven.md §3) run no recipe at `{source_root}`, so no `kind-at`: HEAD is not the pinned commit, so a kind read there says nothing about the entry. Leave each violation that needs a kind from the tree in place and list it as a WARN ending ` (drift override: HEAD {head_short_sha} is not pinned {pinned_short_sha})`; a relabel that reads nothing from the tree still applies. With the status `ok` or `skipped`, run the lookup as the rule says. Rewrite `{forge_version}/provenance-map.json` with the relabeled entries
|
|
220
|
+
**Label violations.** A violation whose `field` is `provenance.entries[<i>].<field>` means entry `<i>` (its `export_name` is in the violation) carries a label its `extraction_method` does not allow: relabel it by the `## Relabel Rule` section of `{extractionPatternsData}`, with `{source_root}` as the tree its node-kind lookup reads. **Under the drift override** (`{workspace_drift_status}` is `overridden`, gap-driven.md §3) run no recipe at `{source_root}`, so no `kind-at`: HEAD is not the pinned commit, so a kind read there says nothing about the entry. Leave each violation that needs a kind from the tree in place and list it as a WARN ending ` (drift override: HEAD {head_short_sha} is not pinned {pinned_short_sha})`; a relabel that reads nothing from the tree still applies. With the status `ok` or `skipped`, run the lookup as the rule says. Rewrite `{forge_version}/provenance-map.json` with the relabeled entries and re-run the helper with the same payload, `--shape` and `-o`, whose file the `metadata` call reads: a relabeled `signature_source` moves the distribution, and a violation left as a WARN does not block the write. This relabel applies to every entry the helper flags, including a gap-driven `verified` entry (§3). Record the relabeled export names, and each WARN left, as `{provenance_relabels}` in workflow context: the update report lists them under its Confidence Tier Breakdown (report.md), so no entry moves from T1 to T1-low unannounced.
|
|
221
|
+
- **Write metadata.json** once the stats helper's last run wrote `{run_dir}/stats.json`. From `{project-root}`, run:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
uv run {buildChangeManifestHelper} metadata \
|
|
225
|
+
--metadata "{skill_package}/metadata.json" \
|
|
226
|
+
--lists "{run_dir}/metadata-lists.json" \
|
|
227
|
+
--stats "{run_dir}/stats.json" \
|
|
228
|
+
--version "{new_version}" \
|
|
229
|
+
--generation-date "{generation_date}" \
|
|
230
|
+
[--source-commit "{target_commit}"] [--source-ref "{target_ref}"]
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
On exit 1, fix the input its stderr names and run it again; when it fails again, prints no JSON or no candidate resolves, HALT with status `halted-for-write-failure` (halt procedure: `phase: "write:metadata"`, `path: "{skill_package}/metadata.json"`, `reason: "<its stderr>"`).
|
|
221
234
|
|
|
222
235
|
**A docs-only skill's document hashes.** When step 2 wrote `{run_dir}/doc-hashes.json` (its docs-only branch), record the new hash of each document it found changed, after the writes above, so the next update compares against the documents this one read. Resolve `{compareDocHashesHelper}` ← first existing path in `{compareDocHashesProbeOrder}` and, from `{project-root}`, run:
|
|
223
236
|
|
|
@@ -254,21 +267,24 @@ uv run {buildChangeManifestHelper} apply \
|
|
|
254
267
|
--source-commit "{map_source_commit}" \
|
|
255
268
|
--source-ref "{map_source_ref}" \
|
|
256
269
|
[--drift-head "{head_short_sha}" --drift-pinned "{pinned_short_sha}"] \
|
|
270
|
+
--not-public-out "{run_dir}/not-public.json" \
|
|
271
|
+
--metadata-in "{skill_package}/metadata.json" \
|
|
272
|
+
--lists-out "{run_dir}/metadata-lists.json" \
|
|
257
273
|
-o "{forge_version}/provenance-map.json"
|
|
258
274
|
```
|
|
259
275
|
|
|
260
|
-
`{update_type}` follows the run's mode as report.md §2 names it (`{update_mode}`): `incremental` for `normal`, `gap-driven` for `gap-driven`, `full` for `degraded` (a degraded run has no map: leave out `--provenance-map`, and `apply` starts a new one). Pass each `{run_dir}` file the run wrote: the change manifest and the records in every mode, `category-c.json` and `ccc-pairs.json` (step 2's Category C and its CCC check), `extraction.json` and `export-details.json` when step 2 wrote them, `merge-records.json` when step 4 did, the Category D files (`category-d-compare.json`, `new-files.json`) with `--source-root` when step 2 ran Category D, and `promoted-docs.json` when step 2 §1b wrote it. `{generation_date}` is the value §2
|
|
276
|
+
`{update_type}` follows the run's mode as report.md §2 names it (`{update_mode}`): `incremental` for `normal`, `gap-driven` for `gap-driven`, `full` for `degraded` (a degraded run has no map: leave out `--provenance-map`, and `apply` starts a new one). Pass each `{run_dir}` file the run wrote: the change manifest and the records in every mode, `category-c.json` and `ccc-pairs.json` (step 2's Category C and its CCC check), `extraction.json` and `export-details.json` when step 2 wrote them, `merge-records.json` when step 4 did, the Category D files (`category-d-compare.json`, and `new-files.json` when step 2 wrote it: a skill with no brief has none) with `--source-root` when step 2 ran Category D, and `promoted-docs.json` when step 2 §1b wrote it. `{generation_date}` is the value §2 bound, `--test-report-run-id` goes only with a gap-driven run (the `{test_report_run_id}` of the report it applied), `{manual_sections_preserved}` is `len(preserved) + len(moved)` from the §1 `manual-verify` verdict (never a marker count), and `{map_source_commit}` and `{map_source_ref}` are the `source_commit` and `source_ref` §2 writes to `metadata.json` (an empty string for a null one): audit-skill reads its baseline commit from the map's top-level `source_commit` and `source_ref`. Pass `--drift-head` and `--drift-pinned` only when `{workspace_drift_status}` is `overridden` (gap-driven.md §3). Pass `--metadata-in` and `--lists-out` in every mode: `apply` writes there the lists §2's `metadata` call reads. Every entry `apply` writes carries `signature_source`, which §2's stats helper bins, and `apply` sets the map's update operation block (`"last_update"`, `"update_type"` and its other keys), replacing an earlier update's.
|
|
261
277
|
|
|
262
278
|
Dispatch on its exit code:
|
|
263
279
|
|
|
264
|
-
- **0:** it printed `{status: "written", entries, file_entries, warnings, map}`. Bind `{provenance_spot_check_warnings}` ← its `warnings`, and add each to `warnings[]`; the update report lists each as a WARN (report.md §2).
|
|
280
|
+
- **0:** it printed `{status: "written", entries, not_public, file_entries, warnings, map}`. Bind `{provenance_spot_check_warnings}` ← its `warnings`, and add each to `warnings[]`; the update report lists each as a WARN (report.md §2). `not_public` lists, `{name, file}`, each new export of a `public-api` skill it did not add in normal mode as off the public surface (the `public: false` mark of `reextract-records.json`, else the same rule over the `scope.type` and `entry_point_diff` of `extraction.json`), and one of its `warnings` counts them; it also wrote that list to `{run_dir}/not-public.json` (`[]` when it left none out). When `extraction.json` names `public-api` but gives no surface the rule can read (Quick tier, a runner failure, an incomplete run), it adds every name, with one warning that says so.
|
|
265
281
|
- **3** (`status` `refused`): a blocking gap reached the provenance write with no source line (`blocking_unresolved[]`: a `NEW_EXPORT` the map does not hold, `unknown`, with a severity other than `Medium`, `Low` or `Info`, a missing one included). gap-driven.md §4 sends every such gap to its §4a and halts with `halted-for-remediation-path` before merge when §4a does not find it, so this happens only when that check was skipped or bypassed: `apply` wrote nothing, and no null citation is written. HALT with status `blocked` and write no `metadata.json`, `provenance-map.json` or other artifact. Tell the user: "**A blocking gap reached the provenance write with no source line:** {each export name, with its severity or "no severity"}. gap-driven.md §4 should have stopped on it with `halted-for-remediation-path`. The halt restored SKILL.md and references/ from the snapshot step 4 took; give each gap a `file:line` Source or name the file that defines its export in its Remediation (or downgrade it to Medium, Low or Info), then re-run update-skill." The halt procedure takes `phase: "write:provenance-map"`, `path: "{forge_version}/provenance-map.json"`, `reason: "blocking-gap-unresolved: {export names}: gap-driven.md §4 should have halted with halted-for-remediation-path"`.
|
|
266
282
|
- **Exit 1, no JSON, or no candidate resolves:** HALT with status `halted-for-write-failure` (halt procedure: `phase: "write:provenance-map"`, `path: "{forge_version}/provenance-map.json"`, `reason: "<its stderr, or skf-build-change-manifest.py is missing; re-install SKF>"`).
|
|
267
283
|
|
|
268
284
|
**For script/asset file changes:** `apply` updates the rows; copy or remove the files themselves in `{skill_package}`, each path the change manifest's `category_d` lists: a `scripts_*` path under `scripts/`, an `assets_*` path under `assets/`:
|
|
269
285
|
- MODIFIED_FILE (`*_modified`): copy the file from `{source_root}` (the row's `content_hash` is updated by `apply`)
|
|
270
286
|
- DELETED_FILE (`*_deleted`): remove the file
|
|
271
|
-
- NEW_FILE (`*_added`): copy the file from `{source_root}` (`apply` adds its row
|
|
287
|
+
- NEW_FILE (`*_added`): copy the file from `{source_root}` (`apply` adds its row)
|
|
272
288
|
- A `docs_*` path is source-tracked, never bundled: copy and remove nothing for it.
|
|
273
289
|
|
|
274
290
|
### 4. Write Updated evidence-report.md
|
|
@@ -458,7 +474,7 @@ uv run {updateActiveSymlinkHelper} update \
|
|
|
458
474
|
--version {version}
|
|
459
475
|
```
|
|
460
476
|
|
|
461
|
-
The helper emits a result envelope with `status` ∈ `{ok, flipped, mismatch, missing-target}` and a pre-formatted `log_message`. Log the message to the evidence report.
|
|
477
|
+
The helper emits a result envelope with `status` ∈ `{ok, flipped, mismatch, missing-target, busy}` and a pre-formatted `log_message`. Log the message to the evidence report.
|
|
462
478
|
|
|
463
479
|
**Dispatch on `status`:**
|
|
464
480
|
|
|
@@ -466,8 +482,10 @@ The helper emits a result envelope with `status` ∈ `{ok, flipped, mismatch, mi
|
|
|
466
482
|
- **`flipped`** (exit 0): symlink was atomically updated (temp-and-replace). Continue to §8a.
|
|
467
483
|
- **`missing-target`** (exit 2): `{skill_group}/{version}/` does not exist on disk, so step 4 §6b did not create it. HALT with status `halted-for-write-failure` and display `halt_message` verbatim.
|
|
468
484
|
- **`mismatch`** (exit 2): the link still points elsewhere after the flip. HALT with status `halted-for-write-failure` and display `halt_message` verbatim.
|
|
485
|
+
- **`busy`** (exit 2): another flip of the link held its lock for the helper's whole wait, so the link is unchanged. HALT with status `halted-for-concurrent-run` and display `halt_message` verbatim.
|
|
486
|
+
- **Any other exit, or no JSON on stdout** (exit 1, or an exit 2 that prints no JSON: the helper could not run, could not move the link, or failed after moving it, and the last line it prints on stderr says why): HALT with status `halted-for-write-failure` and display that line, which is `exit <code>` when the helper prints nothing on stderr. The halt procedure takes `phase: "write:active-symlink"`, `path: "{skill_group}/active"`, `reason: "<that line>"`.
|
|
469
487
|
|
|
470
|
-
For
|
|
488
|
+
For each exit-2 halt, the halt procedure takes `phase: "write:active-symlink"`, `path: "{skill_group}/active"`, `reason: "<status>: <halt_message>"`. Its rollback removes the new version (the `active` link does not name it) or, in gap-driven mode, restores the package from the snapshot.
|
|
471
489
|
|
|
472
490
|
### 8a. Verify the Active Symlink
|
|
473
491
|
|
|
@@ -481,6 +499,8 @@ uv run {updateActiveSymlinkHelper} verify \
|
|
|
481
499
|
|
|
482
500
|
On `mismatch` (exit 2), HALT with status `halted-for-write-failure` and display the helper's `halt_message` verbatim: it names the diverged target, the expected version and the recovery command. The halt procedure takes `phase: "write:verify-active-symlink"`, `path: "{skill_group}/active"`, `reason: "mismatch: <halt_message>"`.
|
|
483
501
|
|
|
502
|
+
On `ok` (exit 0), continue to §9. On any other exit, or no JSON on stdout (exit 1, or an exit 2 that prints no JSON: the helper could not run or could not read the link, as when a real folder has replaced `active` or the skill folder is gone since §8, and the last line it prints on stderr says why), HALT with status `halted-for-write-failure` and display that line, which is `exit <code>` when the helper prints nothing on stderr. The halt procedure takes `phase: "write:verify-active-symlink"`, `path: "{skill_group}/active"`, `reason: "<that line>"`.
|
|
503
|
+
|
|
484
504
|
### 9. Move the Workspace Clone to the Recorded Commit
|
|
485
505
|
|
|
486
506
|
Run only when `{source_tree_status}` is `ready` and `{workspace_clone}` is not null. test-skill and audit-skill read the skill's source from metadata.json `source_root`, the clone SKF keeps for this repository, and test-skill compares its HEAD with the `source_commit` §2 just wrote. From `{project-root}`, resolve `{sourceTreeHelper}` ← first existing path in `{sourceTreeProbeOrder}`, `{cccGitHygieneHelper}` ← first existing path in `{cccGitHygieneProbeOrder}` and `{checkWorkspaceDriftHelper}` ← first existing path in `{checkWorkspaceDriftProbeOrder}`, then run:
|