@ikon85/agent-workflow-kit 0.32.0 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/census-update/SKILL.md +34 -3
- package/.agents/skills/kit-update/SKILL.md +18 -4
- package/.agents/skills/setup-workflow/census.md +14 -7
- package/.claude/skills/census-update/SKILL.md +34 -3
- package/.claude/skills/kit-update/SKILL.md +18 -4
- package/.claude/skills/setup-workflow/census.md +14 -7
- package/README.md +14 -1
- package/agent-workflow-kit.package.json +8 -8
- package/package.json +1 -1
- package/scripts/readiness.mjs +48 -14
- package/scripts/test_census_update_contract.test.mjs +14 -0
- package/src/cli.mjs +2 -2
- package/src/commands/update.mjs +21 -3
- package/src/lib/updateCandidate.mjs +78 -4
|
@@ -72,6 +72,35 @@ proof is still open for state resolution even when Git tracking lets the base
|
|
|
72
72
|
scan enumerate it. Overrides are report input only and are never passed into
|
|
73
73
|
scanning, fingerprinting, verification, or state resolution.
|
|
74
74
|
|
|
75
|
+
## Durable enforcement
|
|
76
|
+
|
|
77
|
+
An activated census is a durable consumer contract, not a one-time activation
|
|
78
|
+
check. Before activating a candidate or reporting an unchanged census as
|
|
79
|
+
`current`, derive the complete set of every `localScanners[].test` from the
|
|
80
|
+
profile and inspect the repository's documented local CI and pre-push paths.
|
|
81
|
+
|
|
82
|
+
Reuse one existing shared project-local census check entry point when the
|
|
83
|
+
repository has one. Otherwise add the smallest such entry point through the
|
|
84
|
+
repository's established script mechanism; do not create a second census
|
|
85
|
+
engine or duplicate scanner logic. It must execute every declared focused test,
|
|
86
|
+
and both local CI and pre-push must transitively reach that same entry point.
|
|
87
|
+
Add or extend an executable wiring test that fails when either durable path or
|
|
88
|
+
any declared scanner test becomes unreachable; matching prose or a bare grep is
|
|
89
|
+
not sufficient proof.
|
|
90
|
+
|
|
91
|
+
Keep enforcement reconciliation idempotent. An already complete wiring graph
|
|
92
|
+
is a no-write result. Mark only the narrow gate block added for this lifecycle
|
|
93
|
+
with the repository's existing managed marker, or with
|
|
94
|
+
`agent-workflow-kit:census` when no marker convention exists; that block is the
|
|
95
|
+
kit-owned census wiring. Preserve surrounding consumer-owned gate logic.
|
|
96
|
+
|
|
97
|
+
Missing or partial durable wiring is a failed candidate verification. If the
|
|
98
|
+
repository has no safe documented gate seam, ask one bounded wiring question
|
|
99
|
+
and stop before activation. On a scanner-test or wiring-proof failure, report
|
|
100
|
+
`failed`, discard the candidate when safe, and confirm that the previous active
|
|
101
|
+
census bytes are unchanged. Never report `current` from activation-time test
|
|
102
|
+
success alone.
|
|
103
|
+
|
|
75
104
|
## Workflow
|
|
76
105
|
|
|
77
106
|
1. **Check.** Read the local profile and active census, if present. Report a
|
|
@@ -95,8 +124,9 @@ scanning, fingerprinting, verification, or state resolution.
|
|
|
95
124
|
and a focused passing test for that pattern. Run that test before rescanning.
|
|
96
125
|
6. **Verify a candidate.** Build the candidate from the fresh scan and recorded
|
|
97
126
|
decisions. Fail verification when any surface or behavior remains `offen`,
|
|
98
|
-
when a required local scanner test fails,
|
|
99
|
-
|
|
127
|
+
when a required local scanner test fails, when durable enforcement or its
|
|
128
|
+
executable wiring test is incomplete, or when the candidate fingerprints do
|
|
129
|
+
not describe the current repository.
|
|
100
130
|
7. **Activate.** Call `activateCensus` with a real verifier so it stages, verifies,
|
|
101
131
|
and atomically swaps under its local lock. On `CensusTransactionError`, report
|
|
102
132
|
`updating` or `failed` and keep the previous active census authoritative.
|
|
@@ -137,4 +167,5 @@ Return only the useful audit trail:
|
|
|
137
167
|
- surface coverage `X of Y`;
|
|
138
168
|
- separate behavior overview;
|
|
139
169
|
- visible `nicht relevant` justifications and active override, if any; <!-- language-census: ok -->
|
|
140
|
-
- local scanner tests run
|
|
170
|
+
- local scanner tests run, durable local-CI/pre-push reachability, and the
|
|
171
|
+
transaction/no-write result.
|
|
@@ -43,11 +43,25 @@ release contain the same artifact.
|
|
|
43
43
|
data, infer an external fact, or manufacture `pending`/`not-applicable` to
|
|
44
44
|
make a capability appear ready.
|
|
45
45
|
|
|
46
|
+
Readiness-schema adoption also keeps the Claude and Codex instruction
|
|
47
|
+
surfaces compatible. When exactly one applicable instruction surface has a
|
|
48
|
+
non-empty `## Prod` section and another has no such section, the staged
|
|
49
|
+
candidate mirrors the same section body into the missing surface. This
|
|
50
|
+
narrow migration is previewed as `migrated`, destination-race checked,
|
|
51
|
+
idempotent, and covered by the same verification and rollback transaction.
|
|
52
|
+
It may create a missing instruction file, but it never rewrites an existing
|
|
53
|
+
`## Prod` body. Empty, malformed, or divergent sections are named as
|
|
54
|
+
conflicts and leave every consumer file untouched. This migration belongs
|
|
55
|
+
to `kit update`; do not rerun `setup-workflow` after an ordinary update.
|
|
56
|
+
Readiness JSON diagnoses only the affected path and problem category
|
|
57
|
+
(`missing-file`, `missing-section`, `empty-section`, or
|
|
58
|
+
`divergent-section`); it never echoes consumer content.
|
|
59
|
+
|
|
46
60
|
3. Read the terminal report. `aktuell` proves a second run found no upstream
|
|
47
|
-
delta. A conflict report names and counts
|
|
48
|
-
consumer file untouched. Follow its
|
|
49
|
-
conflict manually; never auto-merge,
|
|
50
|
-
the incoming copy.
|
|
61
|
+
delta or pending readiness migration. A conflict report names and counts
|
|
62
|
+
every category and leaves every consumer file untouched. Follow its
|
|
63
|
+
recommendation and resolve each named conflict manually; never auto-merge,
|
|
64
|
+
delete a local edit, or silently choose the incoming copy.
|
|
51
65
|
|
|
52
66
|
Read all four availability categories alongside the file delta: newly
|
|
53
67
|
available skill core, newly degraded optional blocks, newly blocked skill
|
|
@@ -66,13 +66,20 @@ delegates this route to the shipped `census-update` contract and its focused
|
|
|
66
66
|
`scripts/test_census_update_contract.test.mjs` proof; it does not reproduce
|
|
67
67
|
activation, snapshots, or enforcement. Setup itself never calls `activateCensus`.
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
Activation owns durable enforcement: every focused test declared by the active
|
|
70
|
+
profile must remain transitively reachable through one shared project-local
|
|
71
|
+
census check entry point from both local CI and pre-push. `census-update`
|
|
72
|
+
reconciles that narrow kit-owned census wiring idempotently and requires an
|
|
73
|
+
executable wiring proof before it can report `current`; setup does not pre-wire
|
|
74
|
+
the optional bootstrap state.
|
|
75
|
+
|
|
76
|
+
Disable follows the ordered contract: remove only the kit-owned census wiring
|
|
77
|
+
(including the kit-owned hook and gate block), then atomically replace only the
|
|
78
|
+
profile's `enabled` value with `false`, preserving unknown keys, and verify
|
|
79
|
+
`disabled` through `resolveCensusState`. Enforcement removal must finish before
|
|
80
|
+
any profile mutation. Treat the choice document, local scanners, their tests,
|
|
81
|
+
the profile, and the active snapshot as consumer-owned evidence. List those
|
|
82
|
+
files and ask for separate deletion approval; without that approval, retain them.
|
|
76
83
|
Setup never deletes consumer-owned files as part of disable.
|
|
77
84
|
|
|
78
85
|
## Setup report
|
|
@@ -72,6 +72,35 @@ proof is still open for state resolution even when Git tracking lets the base
|
|
|
72
72
|
scan enumerate it. Overrides are report input only and are never passed into
|
|
73
73
|
scanning, fingerprinting, verification, or state resolution.
|
|
74
74
|
|
|
75
|
+
## Durable enforcement
|
|
76
|
+
|
|
77
|
+
An activated census is a durable consumer contract, not a one-time activation
|
|
78
|
+
check. Before activating a candidate or reporting an unchanged census as
|
|
79
|
+
`current`, derive the complete set of every `localScanners[].test` from the
|
|
80
|
+
profile and inspect the repository's documented local CI and pre-push paths.
|
|
81
|
+
|
|
82
|
+
Reuse one existing shared project-local census check entry point when the
|
|
83
|
+
repository has one. Otherwise add the smallest such entry point through the
|
|
84
|
+
repository's established script mechanism; do not create a second census
|
|
85
|
+
engine or duplicate scanner logic. It must execute every declared focused test,
|
|
86
|
+
and both local CI and pre-push must transitively reach that same entry point.
|
|
87
|
+
Add or extend an executable wiring test that fails when either durable path or
|
|
88
|
+
any declared scanner test becomes unreachable; matching prose or a bare grep is
|
|
89
|
+
not sufficient proof.
|
|
90
|
+
|
|
91
|
+
Keep enforcement reconciliation idempotent. An already complete wiring graph
|
|
92
|
+
is a no-write result. Mark only the narrow gate block added for this lifecycle
|
|
93
|
+
with the repository's existing managed marker, or with
|
|
94
|
+
`agent-workflow-kit:census` when no marker convention exists; that block is the
|
|
95
|
+
kit-owned census wiring. Preserve surrounding consumer-owned gate logic.
|
|
96
|
+
|
|
97
|
+
Missing or partial durable wiring is a failed candidate verification. If the
|
|
98
|
+
repository has no safe documented gate seam, ask one bounded wiring question
|
|
99
|
+
and stop before activation. On a scanner-test or wiring-proof failure, report
|
|
100
|
+
`failed`, discard the candidate when safe, and confirm that the previous active
|
|
101
|
+
census bytes are unchanged. Never report `current` from activation-time test
|
|
102
|
+
success alone.
|
|
103
|
+
|
|
75
104
|
## Workflow
|
|
76
105
|
|
|
77
106
|
1. **Check.** Read the local profile and active census, if present. Report a
|
|
@@ -95,8 +124,9 @@ scanning, fingerprinting, verification, or state resolution.
|
|
|
95
124
|
and a focused passing test for that pattern. Run that test before rescanning.
|
|
96
125
|
6. **Verify a candidate.** Build the candidate from the fresh scan and recorded
|
|
97
126
|
decisions. Fail verification when any surface or behavior remains `offen`,
|
|
98
|
-
when a required local scanner test fails,
|
|
99
|
-
|
|
127
|
+
when a required local scanner test fails, when durable enforcement or its
|
|
128
|
+
executable wiring test is incomplete, or when the candidate fingerprints do
|
|
129
|
+
not describe the current repository.
|
|
100
130
|
7. **Activate.** Call `activateCensus` with a real verifier so it stages, verifies,
|
|
101
131
|
and atomically swaps under its local lock. On `CensusTransactionError`, report
|
|
102
132
|
`updating` or `failed` and keep the previous active census authoritative.
|
|
@@ -137,4 +167,5 @@ Return only the useful audit trail:
|
|
|
137
167
|
- surface coverage `X of Y`;
|
|
138
168
|
- separate behavior overview;
|
|
139
169
|
- visible `nicht relevant` justifications and active override, if any; <!-- language-census: ok -->
|
|
140
|
-
- local scanner tests run
|
|
170
|
+
- local scanner tests run, durable local-CI/pre-push reachability, and the
|
|
171
|
+
transaction/no-write result.
|
|
@@ -43,11 +43,25 @@ release contain the same artifact.
|
|
|
43
43
|
data, infer an external fact, or manufacture `pending`/`not-applicable` to
|
|
44
44
|
make a capability appear ready.
|
|
45
45
|
|
|
46
|
+
Readiness-schema adoption also keeps the Claude and Codex instruction
|
|
47
|
+
surfaces compatible. When exactly one applicable instruction surface has a
|
|
48
|
+
non-empty `## Prod` section and another has no such section, the staged
|
|
49
|
+
candidate mirrors the same section body into the missing surface. This
|
|
50
|
+
narrow migration is previewed as `migrated`, destination-race checked,
|
|
51
|
+
idempotent, and covered by the same verification and rollback transaction.
|
|
52
|
+
It may create a missing instruction file, but it never rewrites an existing
|
|
53
|
+
`## Prod` body. Empty, malformed, or divergent sections are named as
|
|
54
|
+
conflicts and leave every consumer file untouched. This migration belongs
|
|
55
|
+
to `kit update`; do not rerun `setup-workflow` after an ordinary update.
|
|
56
|
+
Readiness JSON diagnoses only the affected path and problem category
|
|
57
|
+
(`missing-file`, `missing-section`, `empty-section`, or
|
|
58
|
+
`divergent-section`); it never echoes consumer content.
|
|
59
|
+
|
|
46
60
|
3. Read the terminal report. `aktuell` proves a second run found no upstream
|
|
47
|
-
delta. A conflict report names and counts
|
|
48
|
-
consumer file untouched. Follow its
|
|
49
|
-
conflict manually; never auto-merge,
|
|
50
|
-
the incoming copy.
|
|
61
|
+
delta or pending readiness migration. A conflict report names and counts
|
|
62
|
+
every category and leaves every consumer file untouched. Follow its
|
|
63
|
+
recommendation and resolve each named conflict manually; never auto-merge,
|
|
64
|
+
delete a local edit, or silently choose the incoming copy.
|
|
51
65
|
|
|
52
66
|
Read all four availability categories alongside the file delta: newly
|
|
53
67
|
available skill core, newly degraded optional blocks, newly blocked skill
|
|
@@ -66,13 +66,20 @@ delegates this route to the shipped `census-update` contract and its focused
|
|
|
66
66
|
`scripts/test_census_update_contract.test.mjs` proof; it does not reproduce
|
|
67
67
|
activation, snapshots, or enforcement. Setup itself never calls `activateCensus`.
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
Activation owns durable enforcement: every focused test declared by the active
|
|
70
|
+
profile must remain transitively reachable through one shared project-local
|
|
71
|
+
census check entry point from both local CI and pre-push. `census-update`
|
|
72
|
+
reconciles that narrow kit-owned census wiring idempotently and requires an
|
|
73
|
+
executable wiring proof before it can report `current`; setup does not pre-wire
|
|
74
|
+
the optional bootstrap state.
|
|
75
|
+
|
|
76
|
+
Disable follows the ordered contract: remove only the kit-owned census wiring
|
|
77
|
+
(including the kit-owned hook and gate block), then atomically replace only the
|
|
78
|
+
profile's `enabled` value with `false`, preserving unknown keys, and verify
|
|
79
|
+
`disabled` through `resolveCensusState`. Enforcement removal must finish before
|
|
80
|
+
any profile mutation. Treat the choice document, local scanners, their tests,
|
|
81
|
+
the profile, and the active snapshot as consumer-owned evidence. List those
|
|
82
|
+
files and ask for separate deletion approval; without that approval, retain them.
|
|
76
83
|
Setup never deletes consumer-owned files as part of disable.
|
|
77
84
|
|
|
78
85
|
## Setup report
|
package/README.md
CHANGED
|
@@ -305,7 +305,10 @@ any language works — map each role once, rename an option with one profile edi
|
|
|
305
305
|
`init` records a sha256 of every file it installs. That's the line between the
|
|
306
306
|
two: **edit any skill or script freely** — `update` detects your edits and backs
|
|
307
307
|
them up rather than clobbering them. Your **project layer** (`docs/agents/*`, the
|
|
308
|
-
board profile, `CLAUDE.md`)
|
|
308
|
+
board profile, `CLAUDE.md`, `AGENTS.md`) remains consumer-owned. `update` may
|
|
309
|
+
only apply a previewed, schema-driven, idempotent compatibility migration that
|
|
310
|
+
fills missing evidence without rewriting an existing value; it verifies and
|
|
311
|
+
rolls that migration back with the rest of the candidate.
|
|
309
312
|
|
|
310
313
|
## Updating & removing
|
|
311
314
|
|
|
@@ -348,6 +351,16 @@ concurrency-safe. Do not run manifest-mutating commands concurrently. Flags:
|
|
|
348
351
|
|
|
349
352
|
## Release notes
|
|
350
353
|
|
|
354
|
+
### 0.32.1
|
|
355
|
+
|
|
356
|
+
- changed: `.agents/skills/census-update/SKILL.md`
|
|
357
|
+
- changed: `.agents/skills/kit-update/SKILL.md`
|
|
358
|
+
- changed: `.agents/skills/setup-workflow/census.md`
|
|
359
|
+
- changed: `.claude/skills/census-update/SKILL.md`
|
|
360
|
+
- changed: `.claude/skills/kit-update/SKILL.md`
|
|
361
|
+
- changed: `.claude/skills/setup-workflow/census.md`
|
|
362
|
+
- changed: `scripts/readiness.mjs`
|
|
363
|
+
|
|
351
364
|
### 0.32.0
|
|
352
365
|
|
|
353
366
|
- changed: `.agents/skills/audit-skills/SKILL.md`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kitVersion": "0.32.
|
|
2
|
+
"kitVersion": "0.32.1",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": ".agents/skills/ask-matt/SKILL.md",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"ownerSkill": "census-update",
|
|
48
48
|
"surface": "codex",
|
|
49
49
|
"installRole": "consumer",
|
|
50
|
-
"sha256": "
|
|
50
|
+
"sha256": "b122fe2eb46c070696e38660b021411458b455b7a4e977174f943f2ce4c00819",
|
|
51
51
|
"mode": 420,
|
|
52
52
|
"origin": "kit"
|
|
53
53
|
},
|
|
@@ -377,7 +377,7 @@
|
|
|
377
377
|
"ownerSkill": "kit-update",
|
|
378
378
|
"surface": "codex",
|
|
379
379
|
"installRole": "consumer",
|
|
380
|
-
"sha256": "
|
|
380
|
+
"sha256": "249a45761bcb95148b1567d1bc4d1b622b9b8c41519de4e67b1ef4e33c9ee9fd",
|
|
381
381
|
"mode": 420,
|
|
382
382
|
"origin": "kit"
|
|
383
383
|
},
|
|
@@ -567,7 +567,7 @@
|
|
|
567
567
|
"ownerSkill": "setup-workflow",
|
|
568
568
|
"surface": "codex",
|
|
569
569
|
"installRole": "consumer",
|
|
570
|
-
"sha256": "
|
|
570
|
+
"sha256": "d495758ae52bebd11c81bbe28fc5a1f9b614f972afdba55d1b728630d12cfee8",
|
|
571
571
|
"mode": 420,
|
|
572
572
|
"origin": "kit"
|
|
573
573
|
},
|
|
@@ -1209,7 +1209,7 @@
|
|
|
1209
1209
|
"ownerSkill": "census-update",
|
|
1210
1210
|
"surface": "claude",
|
|
1211
1211
|
"installRole": "consumer",
|
|
1212
|
-
"sha256": "
|
|
1212
|
+
"sha256": "b122fe2eb46c070696e38660b021411458b455b7a4e977174f943f2ce4c00819",
|
|
1213
1213
|
"mode": 420,
|
|
1214
1214
|
"origin": "kit"
|
|
1215
1215
|
},
|
|
@@ -1659,7 +1659,7 @@
|
|
|
1659
1659
|
"ownerSkill": "kit-update",
|
|
1660
1660
|
"surface": "claude",
|
|
1661
1661
|
"installRole": "consumer",
|
|
1662
|
-
"sha256": "
|
|
1662
|
+
"sha256": "249a45761bcb95148b1567d1bc4d1b622b9b8c41519de4e67b1ef4e33c9ee9fd",
|
|
1663
1663
|
"mode": 420,
|
|
1664
1664
|
"origin": "kit"
|
|
1665
1665
|
},
|
|
@@ -1869,7 +1869,7 @@
|
|
|
1869
1869
|
"ownerSkill": "setup-workflow",
|
|
1870
1870
|
"surface": "claude",
|
|
1871
1871
|
"installRole": "consumer",
|
|
1872
|
-
"sha256": "
|
|
1872
|
+
"sha256": "d495758ae52bebd11c81bbe28fc5a1f9b614f972afdba55d1b728630d12cfee8",
|
|
1873
1873
|
"mode": 420,
|
|
1874
1874
|
"origin": "kit"
|
|
1875
1875
|
},
|
|
@@ -2587,7 +2587,7 @@
|
|
|
2587
2587
|
"path": "scripts/readiness.mjs",
|
|
2588
2588
|
"kind": "script",
|
|
2589
2589
|
"installRole": "consumer",
|
|
2590
|
-
"sha256": "
|
|
2590
|
+
"sha256": "9e7a9c6882e372f38383fe92d98ff2f818bae0be61a62301b7e9a6a2842dd21b",
|
|
2591
2591
|
"mode": 493,
|
|
2592
2592
|
"origin": "kit"
|
|
2593
2593
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikon85/agent-workflow-kit",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.1",
|
|
4
4
|
"description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/scripts/readiness.mjs
CHANGED
|
@@ -99,22 +99,53 @@ function section(text, heading) {
|
|
|
99
99
|
return body.join('\n').trim();
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
async function
|
|
103
|
-
const
|
|
102
|
+
export async function inspectProdSections(root, paths) {
|
|
103
|
+
const sections = [];
|
|
104
104
|
for (const path of paths) {
|
|
105
105
|
const text = await readText(root, path);
|
|
106
|
-
if (text === null) continue;
|
|
107
106
|
const body = section(text, '## Prod');
|
|
108
|
-
|
|
107
|
+
const problem = text === null ? 'missing-file'
|
|
108
|
+
: (body === null ? 'missing-section' : (body ? null : 'empty-section'));
|
|
109
|
+
sections.push({
|
|
110
|
+
path,
|
|
111
|
+
state: body === null ? 'missing' : (body ? 'valid' : 'invalid'),
|
|
112
|
+
body,
|
|
113
|
+
problem,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return sections;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function prodEvidence(root, paths) {
|
|
120
|
+
const sections = await inspectProdSections(root, paths);
|
|
121
|
+
const present = sections.filter(({ problem }) => problem !== 'missing-file');
|
|
122
|
+
const valid = present.filter(({ state }) => state === 'valid');
|
|
123
|
+
const empty = present.filter(({ problem }) => problem === 'empty-section');
|
|
124
|
+
if (!valid.length && !empty.length) {
|
|
125
|
+
return {
|
|
126
|
+
verdict: 'absent',
|
|
127
|
+
diagnostics: sections.map(({ path, problem }) => ({ path, problem })),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const malformed = present.filter(({ state }) => state !== 'valid');
|
|
131
|
+
if (malformed.length) {
|
|
132
|
+
return {
|
|
133
|
+
verdict: 'invalid',
|
|
134
|
+
diagnostics: malformed.map(({ path, problem }) => ({ path, problem })),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const bodies = [...new Set(present.map(({ body }) => body))];
|
|
138
|
+
if (bodies.length > 1) {
|
|
139
|
+
return {
|
|
140
|
+
verdict: 'invalid',
|
|
141
|
+
diagnostics: present.map(({ path }) => ({ path, problem: 'divergent-section' })),
|
|
142
|
+
};
|
|
109
143
|
}
|
|
110
|
-
|
|
111
|
-
if (bodies.every((body) => body === null)) return 'absent';
|
|
112
|
-
if (bodies.some((body) => !body)) return 'invalid';
|
|
113
|
-
return bodies.every((body) => body === bodies[0]) ? 'valid' : 'invalid';
|
|
144
|
+
return { verdict: 'valid', diagnostics: [] };
|
|
114
145
|
}
|
|
115
146
|
|
|
116
147
|
async function evidenceVerdict(root, evidence) {
|
|
117
|
-
if (evidence.type === 'prod-section') return
|
|
148
|
+
if (evidence.type === 'prod-section') return (await prodEvidence(root, evidence.paths)).verdict;
|
|
118
149
|
if (evidence.type === 'runbook-reference') return runbookVerdict(root, evidence);
|
|
119
150
|
const verdicts = await Promise.all((evidence.paths ?? []).map(async (path) => {
|
|
120
151
|
const text = await readText(root, path);
|
|
@@ -129,14 +160,17 @@ async function evidenceVerdict(root, evidence) {
|
|
|
129
160
|
}
|
|
130
161
|
|
|
131
162
|
export async function evaluateCapability({ root, capability, decision }) {
|
|
132
|
-
const
|
|
133
|
-
|
|
163
|
+
const prod = capability.evidence.type === 'prod-section'
|
|
164
|
+
? await prodEvidence(root, capability.evidence.paths) : null;
|
|
165
|
+
const evidence = prod?.verdict ?? await evidenceVerdict(root, capability.evidence);
|
|
166
|
+
const diagnostics = prod?.diagnostics?.length ? { diagnostics: prod.diagnostics } : {};
|
|
167
|
+
if (evidence === 'invalid') return { state: 'invalid', clearDecision: false, ...diagnostics };
|
|
134
168
|
if (evidence === 'valid') return { state: 'ready', clearDecision: Boolean(decision) };
|
|
135
|
-
if (decision === 'pending') return { state: 'pending', clearDecision: false };
|
|
169
|
+
if (decision === 'pending') return { state: 'pending', clearDecision: false, ...diagnostics };
|
|
136
170
|
if (decision === 'not-applicable' && capability.allowNotApplicable) {
|
|
137
|
-
return { state: 'not-applicable', clearDecision: false };
|
|
171
|
+
return { state: 'not-applicable', clearDecision: false, ...diagnostics };
|
|
138
172
|
}
|
|
139
|
-
return { state: 'missing', clearDecision: false };
|
|
173
|
+
return { state: 'missing', clearDecision: false, ...diagnostics };
|
|
140
174
|
}
|
|
141
175
|
|
|
142
176
|
async function loadManifest(root) {
|
|
@@ -169,6 +169,20 @@ test('census-update coordinates the stable foundation API without a second engin
|
|
|
169
169
|
assert.match(skill, /\.census\/profile\.json/);
|
|
170
170
|
assert.match(skill, /\.census\/active\.json/);
|
|
171
171
|
assert.match(skill, /schemaVersion/);
|
|
172
|
+
assert.match(skill, /every `localScanners\[\]\.test`/);
|
|
173
|
+
assert.match(skill, /shared project-local census check entry point/i);
|
|
174
|
+
assert.match(
|
|
175
|
+
skill,
|
|
176
|
+
/It must execute every declared focused test,\s+and both local CI and pre-push must transitively reach that same entry point\./i,
|
|
177
|
+
);
|
|
178
|
+
assert.match(skill, /executable wiring test/i);
|
|
179
|
+
assert.match(skill, /missing or partial[\s\S]*failed/i);
|
|
180
|
+
assert.match(skill, /previous active\s+census bytes are unchanged/i);
|
|
181
|
+
assert.match(skill, /idempotent/i);
|
|
182
|
+
|
|
183
|
+
const setupCensus = await text('.claude/skills/setup-workflow/census.md');
|
|
184
|
+
assert.match(setupCensus, /activation[\s\S]*durable[\s\S]*local CI[\s\S]*pre-push/i);
|
|
185
|
+
assert.match(setupCensus, /remove only[\s\S]*kit-owned census wiring/i);
|
|
172
186
|
});
|
|
173
187
|
|
|
174
188
|
test('brownfield bootstrap activates real surface coverage with a separate behavior overview', async () => {
|
package/src/cli.mjs
CHANGED
|
@@ -57,7 +57,7 @@ try {
|
|
|
57
57
|
} else if (r.status === 'current') {
|
|
58
58
|
p.outro(`aktuell · unchanged ${r.unchanged.length} · local modifications ${r.userModified.length}`);
|
|
59
59
|
} else {
|
|
60
|
-
p.outro(`updated ${r.updated.length} · added ${r.added.length} · deleted ${r.deleted.length}`);
|
|
60
|
+
p.outro(`updated ${r.updated.length} · added ${r.added.length} · migrated ${r.migrated?.length ?? 0} · deleted ${r.deleted.length}`);
|
|
61
61
|
}
|
|
62
62
|
} else if (cmd === 'uninstall') {
|
|
63
63
|
const ok = yes || (await p.confirm({ message: 'Remove kit-installed files?' })) === true;
|
|
@@ -82,7 +82,7 @@ function printPlan(r) {
|
|
|
82
82
|
const lines = [];
|
|
83
83
|
for (const k of [
|
|
84
84
|
'added', 'updated', 'userModified', 'consumerOwned', 'unchanged',
|
|
85
|
-
'deleted', 'keptDeleted', 'collisions',
|
|
85
|
+
'deleted', 'keptDeleted', 'collisions', 'migrated',
|
|
86
86
|
])
|
|
87
87
|
if (r[k]?.length) lines.push(`${k}: ${r[k].length}`);
|
|
88
88
|
if (r.conflicts?.length) lines.push(`conflicts: ${r.conflicts.length}`);
|
package/src/commands/update.mjs
CHANGED
|
@@ -55,6 +55,11 @@ export async function update(options) {
|
|
|
55
55
|
Object.assign(preview, await previewReadinessAdoption({
|
|
56
56
|
kitRoot, consumerRoot, priorReadinessManifest, nextReadinessManifest,
|
|
57
57
|
}));
|
|
58
|
+
preview.conflicts.push(...(preview.migrationConflicts ?? []).map((path) => ({
|
|
59
|
+
path,
|
|
60
|
+
kind: 'prod-section',
|
|
61
|
+
diff: 'Prod section differs from the other instruction surface or is malformed.',
|
|
62
|
+
})));
|
|
58
63
|
} catch (error) {
|
|
59
64
|
previewFailure = error;
|
|
60
65
|
}
|
|
@@ -66,6 +71,9 @@ export async function update(options) {
|
|
|
66
71
|
};
|
|
67
72
|
}
|
|
68
73
|
if (dryRun) return { ...preview, state: 'preview', history };
|
|
74
|
+
if (preview.migrationConflicts?.length) {
|
|
75
|
+
return terminal(preview, 'conflicted', history, transition);
|
|
76
|
+
}
|
|
69
77
|
if (preview.conflicts.length) return terminal(preview, 'conflicted', history, transition);
|
|
70
78
|
const resolvedPreview = await resolvePreview({
|
|
71
79
|
kitRoot, consumerRoot, preview, decisions, decide, transition,
|
|
@@ -138,7 +146,13 @@ async function applyTransaction(context) {
|
|
|
138
146
|
nextManifest: nextReadinessManifest,
|
|
139
147
|
});
|
|
140
148
|
preview.generated = readiness.generated;
|
|
149
|
+
preview.migrations = readiness.migrations;
|
|
150
|
+
preview.migrated = readiness.migrated;
|
|
151
|
+
preview.migrationConflicts = readiness.migrationConflicts;
|
|
141
152
|
preview.availability = readiness.availability;
|
|
153
|
+
if (readiness.migrationConflicts.length) {
|
|
154
|
+
throw new Error(`Prod section migration conflict: ${readiness.migrationConflicts.join(', ')}`);
|
|
155
|
+
}
|
|
142
156
|
if (readiness.incompatible.length) {
|
|
143
157
|
throw new Error(`monotonic compatibility would block existing skill core: ${readiness.incompatible.join(', ')}`);
|
|
144
158
|
}
|
|
@@ -180,6 +194,7 @@ function verifyRelease(identities, kitVersion) {
|
|
|
180
194
|
|
|
181
195
|
function hasUpstreamDelta(result) {
|
|
182
196
|
return result.manifestChanged ||
|
|
197
|
+
(result.migrations?.length ?? 0) > 0 ||
|
|
183
198
|
result.added.length + result.updated.length + result.deleted.length > 0;
|
|
184
199
|
}
|
|
185
200
|
|
|
@@ -205,9 +220,12 @@ async function terminal(result, state, history, transition) {
|
|
|
205
220
|
conflicts: result.conflicts.map(({ path }) => path),
|
|
206
221
|
keptDeleted: result.keptDeleted,
|
|
207
222
|
},
|
|
208
|
-
recommendation: result.
|
|
209
|
-
?
|
|
210
|
-
|
|
223
|
+
recommendation: result.migrationConflicts?.length
|
|
224
|
+
? `Prod sections differ or are malformed in: ${result.migrationConflicts.join(', ')}. ` +
|
|
225
|
+
'Resolve them manually; no consumer file was changed.'
|
|
226
|
+
: (result.conflicts.length
|
|
227
|
+
? 'Review each named conflict; keep the local file or apply the incoming diff manually.'
|
|
228
|
+
: null),
|
|
211
229
|
},
|
|
212
230
|
};
|
|
213
231
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { access, cp, mkdtemp, readFile, rm, stat, symlink } from 'node:fs/promises';
|
|
2
|
+
import { access, cp, lstat, mkdtemp, readFile, rm, stat, symlink } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
@@ -12,10 +12,15 @@ import {
|
|
|
12
12
|
CONSUMER_MANIFEST_NAME, CONSUMER_ORIGIN, READINESS_MANIFEST_PATH,
|
|
13
13
|
indexByPath, readManifest, writeManifest,
|
|
14
14
|
} from './manifest.mjs';
|
|
15
|
-
import { checkSkill, evaluateCapability } from '../../scripts/readiness.mjs';
|
|
15
|
+
import { checkSkill, evaluateCapability, inspectProdSections } from '../../scripts/readiness.mjs';
|
|
16
16
|
|
|
17
17
|
const run = promisify(execFile);
|
|
18
18
|
const exists = (path) => access(path).then(() => true, () => false);
|
|
19
|
+
const pathEntryExists = (path) => lstat(path).then(() => true, (error) => {
|
|
20
|
+
if (error.code === 'ENOENT') return false;
|
|
21
|
+
throw error;
|
|
22
|
+
});
|
|
23
|
+
const MIGRATABLE_INSTRUCTION_PATHS = new Set(['CLAUDE.md', 'AGENTS.md']);
|
|
19
24
|
|
|
20
25
|
/** Copy a verification candidate without duplicating git metadata or dependencies. */
|
|
21
26
|
export async function stageConsumer(consumerRoot) {
|
|
@@ -40,7 +45,11 @@ export async function activateCandidate({
|
|
|
40
45
|
}) {
|
|
41
46
|
const changed = [...preview.added, ...preview.updated];
|
|
42
47
|
const generated = preview.generated ?? [];
|
|
43
|
-
const
|
|
48
|
+
const migrations = preview.migrations ?? [];
|
|
49
|
+
const touched = [
|
|
50
|
+
...changed, ...generated, ...migrations.map(({ path }) => path),
|
|
51
|
+
...preview.deleted, CONSUMER_MANIFEST_NAME,
|
|
52
|
+
];
|
|
44
53
|
const currentManifest = await readFile(join(consumerRoot, CONSUMER_MANIFEST_NAME));
|
|
45
54
|
if (!currentManifest.equals(consumerManifestBefore)) {
|
|
46
55
|
throw new Error('consumer manifest changed during verification');
|
|
@@ -58,6 +67,11 @@ export async function activateCandidate({
|
|
|
58
67
|
throw new Error(`generated candidate hash mismatch: ${path}`);
|
|
59
68
|
}
|
|
60
69
|
}
|
|
70
|
+
for (const migration of migrations) {
|
|
71
|
+
if (await sha256File(join(candidateRoot, migration.path)) !== migration.afterSha256) {
|
|
72
|
+
throw new Error(`migrated candidate hash mismatch: ${migration.path}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
61
75
|
await assertConsumerStillMatchesPreview(consumerRoot, preview);
|
|
62
76
|
const rollback = new Map();
|
|
63
77
|
for (const path of touched) rollback.set(path, await snapshot(join(consumerRoot, path)));
|
|
@@ -68,6 +82,9 @@ export async function activateCandidate({
|
|
|
68
82
|
for (const path of generated) {
|
|
69
83
|
await writeAtomic(join(consumerRoot, path), await readFile(join(candidateRoot, path)));
|
|
70
84
|
}
|
|
85
|
+
for (const { path } of migrations) {
|
|
86
|
+
await writeAtomic(join(consumerRoot, path), await readFile(join(candidateRoot, path)));
|
|
87
|
+
}
|
|
71
88
|
await afterGenerated();
|
|
72
89
|
for (const path of preview.deleted) await rm(join(consumerRoot, path), { force: true });
|
|
73
90
|
await writeAtomic(
|
|
@@ -105,6 +122,17 @@ async function assertConsumerStillMatchesPreview(consumerRoot, preview) {
|
|
|
105
122
|
throw new Error(`consumer changed during verification: ${path}`);
|
|
106
123
|
}
|
|
107
124
|
}
|
|
125
|
+
for (const migration of preview.migrations ?? []) {
|
|
126
|
+
const present = await pathEntryExists(join(consumerRoot, migration.path));
|
|
127
|
+
if (present) await validateConsumerFile(consumerRoot, migration.path);
|
|
128
|
+
else if (!MIGRATABLE_INSTRUCTION_PATHS.has(migration.path)) {
|
|
129
|
+
throw new Error(`unsafe consumer path: ${migration.path}`);
|
|
130
|
+
}
|
|
131
|
+
const current = present ? await sha256File(join(consumerRoot, migration.path)) : null;
|
|
132
|
+
if (current !== migration.beforeSha256) {
|
|
133
|
+
throw new Error(`consumer changed during verification: ${migration.path}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
108
136
|
for (const path of [...preview.updated, ...preview.deleted]) {
|
|
109
137
|
const prior = installed.get(path);
|
|
110
138
|
const current = await exists(join(consumerRoot, path))
|
|
@@ -144,6 +172,9 @@ export async function adoptReadinessCandidate({ candidateRoot, consumerRoot, pri
|
|
|
144
172
|
}
|
|
145
173
|
await writeManifest(manifestPath, { ...manifest, installed });
|
|
146
174
|
}
|
|
175
|
+
const { migrations, migrationConflicts } = await migrateProdSections({
|
|
176
|
+
candidateRoot, consumerRoot, nextManifest,
|
|
177
|
+
});
|
|
147
178
|
const before = await readinessSnapshot(consumerRoot, priorManifest);
|
|
148
179
|
const after = await readinessSnapshot(candidateRoot, nextManifest);
|
|
149
180
|
const incompatible = Object.entries(after.skills)
|
|
@@ -151,7 +182,50 @@ export async function adoptReadinessCandidate({ candidateRoot, consumerRoot, pri
|
|
|
151
182
|
&& before.skills[skill] && current.verdict === 'blocked')
|
|
152
183
|
.map(([skill]) => skill)
|
|
153
184
|
.sort();
|
|
154
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
generated,
|
|
187
|
+
migrations,
|
|
188
|
+
migrated: migrations.map(({ path }) => path),
|
|
189
|
+
migrationConflicts,
|
|
190
|
+
availability: readinessDiff(before, after),
|
|
191
|
+
incompatible,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function migrateProdSections({ candidateRoot, consumerRoot, nextManifest }) {
|
|
196
|
+
const paths = [...new Set(Object.values(nextManifest?.readiness?.capabilities ?? {})
|
|
197
|
+
.flatMap(({ evidence }) => evidence?.type === 'prod-section' ? evidence.paths ?? [] : []))];
|
|
198
|
+
if (paths.length < 2) return { migrations: [], migrationConflicts: [] };
|
|
199
|
+
if (paths.some((path) => !MIGRATABLE_INSTRUCTION_PATHS.has(path))) {
|
|
200
|
+
return { migrations: [], migrationConflicts: paths };
|
|
201
|
+
}
|
|
202
|
+
const sections = await inspectProdSections(candidateRoot, paths);
|
|
203
|
+
const invalid = sections.filter(({ state }) => state === 'invalid');
|
|
204
|
+
const validBodies = [...new Set(
|
|
205
|
+
sections.filter(({ state }) => state === 'valid').map(({ body }) => body),
|
|
206
|
+
)];
|
|
207
|
+
if (invalid.length || validBodies.length > 1) {
|
|
208
|
+
return { migrations: [], migrationConflicts: paths };
|
|
209
|
+
}
|
|
210
|
+
if (validBodies.length !== 1) return { migrations: [], migrationConflicts: [] };
|
|
211
|
+
|
|
212
|
+
const body = validBodies[0];
|
|
213
|
+
const migrations = [];
|
|
214
|
+
for (const entry of sections.filter(({ state }) => state === 'missing')) {
|
|
215
|
+
const candidatePath = join(candidateRoot, entry.path);
|
|
216
|
+
const present = await pathEntryExists(candidatePath);
|
|
217
|
+
if (present) await validateConsumerFile(candidateRoot, entry.path);
|
|
218
|
+
const before = present ? await readFile(candidatePath, 'utf8') : '';
|
|
219
|
+
const separator = before && !before.endsWith('\n') ? '\n\n' : (before ? '\n' : '');
|
|
220
|
+
await writeAtomic(candidatePath, `${before}${separator}## Prod\n\n${body}\n`);
|
|
221
|
+
migrations.push({
|
|
222
|
+
path: entry.path,
|
|
223
|
+
beforeSha256: await exists(join(consumerRoot, entry.path))
|
|
224
|
+
? await sha256File(join(consumerRoot, entry.path)) : null,
|
|
225
|
+
afterSha256: await sha256File(candidatePath),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return { migrations, migrationConflicts: [] };
|
|
155
229
|
}
|
|
156
230
|
|
|
157
231
|
function readinessStubPaths(manifest) {
|