@ikon85/agent-workflow-kit 0.34.4 → 0.34.6
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/kit-release/SKILL.md +29 -21
- package/.agents/skills/kit-update/SKILL.md +6 -3
- package/.agents/skills/setup-workflow/SKILL.md +13 -5
- package/.agents/skills/setup-workflow/assets/agent-workflow-kit-update.yml +1 -3
- package/.claude/skills/kit-release/SKILL.md +29 -21
- package/.claude/skills/kit-update/SKILL.md +6 -3
- package/.claude/skills/setup-workflow/SKILL.md +13 -5
- package/.claude/skills/setup-workflow/assets/agent-workflow-kit-update.yml +1 -3
- package/README.md +15 -0
- package/agent-workflow-kit.package.json +10 -10
- package/docs/adr/0001-consumer-divergence-policy.md +4 -0
- package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +63 -0
- package/docs/adr/0004-release-intent-is-a-version-tag.md +18 -5
- package/docs/agents/board-sync.md +1 -1
- package/docs/agents/workflow-capabilities.json +17 -0
- package/docs/research/benchlm-routing-source.md +198 -0
- package/docs/research/consumer-owned-protocol-files.md +238 -0
- package/docs/research/frontend-agent-benchmarks.md +282 -0
- package/docs/research/model-effort-routing-benchmarks.md +261 -0
- package/docs/research/provider-neutral-agent-routing.md +207 -0
- package/package.json +1 -1
- package/scripts/kit-release.test.mjs +10 -4
- package/scripts/kit-update-pr.mjs +1 -1
- package/scripts/kit-update-pr.test.mjs +2 -0
- package/scripts/test_release_authorization_contract.py +101 -0
- package/scripts/test_skill_readiness_contract.py +6 -1
- package/scripts/test_skill_setup_workflow_seeds.py +18 -17
- package/src/commands/update.mjs +52 -20
- package/src/lib/bundle.mjs +1 -1
- package/src/lib/updateCandidate.mjs +278 -50
- package/src/lib/verifyUpdateCandidate.mjs +220 -0
- package/src/lib/verifyUpdateCandidateArtifacts.mjs +78 -0
- package/src/lib/verifyUpdateCandidateProtocol.mjs +221 -0
- package/src/lib/verifyUpdateCandidateTransaction.mjs +152 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Provider-neutral agent routing for shared work items
|
|
2
|
+
|
|
3
|
+
**Researched:** 2026-07-22
|
|
4
|
+
**Question:** How should shared agent-workflow issues express model-routing intent across Claude Code and OpenAI Codex without embedding short-lived provider model names?
|
|
5
|
+
|
|
6
|
+
## Recommendation
|
|
7
|
+
|
|
8
|
+
Persist **task intent, not a model selection**, in shared issues. Use two small,
|
|
9
|
+
provider-neutral fields:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
routing-intent: judgment | development | mechanical
|
|
13
|
+
reasoning-intent: deep | balanced | light
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The exact vocabulary is a Kit domain decision; the important constraint is that
|
|
17
|
+
the values describe the work and desired trade-off, not Anthropic or OpenAI
|
|
18
|
+
products. A surface-local resolver maps those values at dispatch time:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
shared issue intent
|
|
22
|
+
|
|
|
23
|
+
+-- Claude resolver -> current Claude alias/model + session effort
|
|
24
|
+
|
|
|
25
|
+
`-- Codex resolver -> current Codex model + model_reasoning_effort
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The default resolution must be `inherit` when no local policy exists or when a
|
|
29
|
+
configured target cannot be proven usable. The orchestrator should report the
|
|
30
|
+
effective resolution at session start, but it should not write that volatile
|
|
31
|
+
result back into the issue.
|
|
32
|
+
|
|
33
|
+
This separates three ownership layers:
|
|
34
|
+
|
|
35
|
+
1. **Kit-owned schema and validation:** allowed intent values, inheritance,
|
|
36
|
+
diagnostics, and dispatch contract.
|
|
37
|
+
2. **User/organization-owned surface policy:** how intent maps to models and
|
|
38
|
+
effort for that account, budget, provider, and current catalog.
|
|
39
|
+
3. **Project-owned constraints, only when genuinely shared:** for example, a
|
|
40
|
+
prohibition on a model or a minimum capability required by every
|
|
41
|
+
collaborator. A project should not carry one maintainer's preferred model
|
|
42
|
+
mapping.
|
|
43
|
+
|
|
44
|
+
## Verified platform facts
|
|
45
|
+
|
|
46
|
+
### Claude Code
|
|
47
|
+
|
|
48
|
+
Claude Code has Managed, User, Project, and Local scopes. For ordinary scalar
|
|
49
|
+
settings, precedence is Managed, command-line, Local, Project, then User. A
|
|
50
|
+
committed `.claude/settings.json` therefore affects all collaborators and
|
|
51
|
+
overrides their user settings, while `.claude/settings.local.json` is personal
|
|
52
|
+
and repository-specific ([Claude Code settings: scopes and precedence](https://code.claude.com/docs/en/settings#configuration-scopes)).
|
|
53
|
+
|
|
54
|
+
Claude accepts provider aliases as well as full model IDs. Aliases such as
|
|
55
|
+
`fable` and `sonnet` resolve to current models, while `default` returns to the
|
|
56
|
+
runtime default for the account or organization. Aliases reduce version churn,
|
|
57
|
+
but remain Claude-specific and do not encode a cross-provider task class
|
|
58
|
+
([Claude Code model aliases](https://code.claude.com/docs/en/model-config#model-aliases)).
|
|
59
|
+
|
|
60
|
+
The main-session model can be changed for a session with `/model` or at launch
|
|
61
|
+
with `--model`; `ANTHROPIC_MODEL` and the persistent `model` setting are also
|
|
62
|
+
supported. Organization restrictions and managed settings can replace or
|
|
63
|
+
reject a requested selection, so a repository cannot assume that a named model
|
|
64
|
+
is usable for every collaborator ([Claude Code model selection](https://code.claude.com/docs/en/model-config#setting-your-model),
|
|
65
|
+
[Claude Code model restrictions](https://code.claude.com/docs/en/model-config#restrict-model-selection)).
|
|
66
|
+
|
|
67
|
+
Claude subagent definitions support an alias, a full ID, or `inherit`; omission
|
|
68
|
+
means `inherit`. Resolution currently considers
|
|
69
|
+
`CLAUDE_CODE_SUBAGENT_MODEL`, a per-invocation value, subagent frontmatter, and
|
|
70
|
+
finally the main conversation model. Excluded selections fall back to the
|
|
71
|
+
inherited model. Extended-thinking configuration is inherited from the main
|
|
72
|
+
conversation and has no per-subagent setting
|
|
73
|
+
([Claude Code subagent model resolution](https://code.claude.com/docs/en/sub-agents#choose-a-model)).
|
|
74
|
+
|
|
75
|
+
Claude effort levels are model-dependent. Unsupported levels are reduced to a
|
|
76
|
+
supported level, organization policy can cap them, and some effort choices are
|
|
77
|
+
session-only. Skill and subagent frontmatter can override the inherited session
|
|
78
|
+
effort, subject to higher-precedence environment and organization constraints.
|
|
79
|
+
Therefore a provider-neutral reasoning intent can be resolved on Claude, but a
|
|
80
|
+
shared issue must not claim that one literal effort value has identical
|
|
81
|
+
enforcement everywhere
|
|
82
|
+
([Claude Code effort levels](https://code.claude.com/docs/en/model-config#adjust-effort-level),
|
|
83
|
+
[Claude Code subagent effort](https://code.claude.com/docs/en/sub-agents#supported-frontmatter-fields)).
|
|
84
|
+
|
|
85
|
+
Anthropic exposes `GET /v1/models`, whose result describes models available to
|
|
86
|
+
that API credential ([Anthropic Models API](https://platform.claude.com/docs/en/api/models/list)).
|
|
87
|
+
That is not a universal Claude Code discovery contract: Claude Code may run
|
|
88
|
+
through a Claude subscription, Bedrock, Vertex, Foundry, or a gateway. Gateway
|
|
89
|
+
discovery is optional, limited to compatible `/v1/models` gateways, cached,
|
|
90
|
+
and falls back to built-in entries if discovery fails
|
|
91
|
+
([Claude Code gateway model discovery](https://code.claude.com/docs/en/llm-gateway#model-selection)).
|
|
92
|
+
|
|
93
|
+
### OpenAI Codex
|
|
94
|
+
|
|
95
|
+
Codex reads personal defaults from `~/.codex/config.toml` and project overrides
|
|
96
|
+
from trusted `.codex/config.toml` files. Its documented precedence is CLI,
|
|
97
|
+
project config, selected user profile, user config, system config, then built-in
|
|
98
|
+
defaults. Consequently, a committed project routing table can override a
|
|
99
|
+
collaborator's personal defaults; untrusted projects skip project-local layers
|
|
100
|
+
entirely ([Codex configuration precedence](https://learn.chatgpt.com/docs/config-file/config-basic#configuration-precedence)).
|
|
101
|
+
|
|
102
|
+
Codex exposes `model` and `model_reasoning_effort` as separate configuration
|
|
103
|
+
values ([Codex common configuration options](https://learn.chatgpt.com/docs/config-file/config-basic#common-configuration-options)).
|
|
104
|
+
For custom subagents, a custom-agent file may override both. Otherwise each is
|
|
105
|
+
resolved independently from an explicit spawn value, the `[agents]` default,
|
|
106
|
+
and the parent session. Omitting a setting inherits it; changing the model
|
|
107
|
+
without an explicit effort uses the selected model's default effort
|
|
108
|
+
([Codex custom-agent resolution](https://learn.chatgpt.com/docs/agent-configuration/subagents#custom-agents)).
|
|
109
|
+
|
|
110
|
+
Official Codex guidance explicitly allows leaving both values unpinned so the
|
|
111
|
+
runtime can balance capability, speed, and price. It also documents that
|
|
112
|
+
available reasoning levels depend on the selected model
|
|
113
|
+
([Codex choosing models and reasoning](https://learn.chatgpt.com/docs/agent-configuration/subagents#choosing-models-and-reasoning)).
|
|
114
|
+
|
|
115
|
+
OpenAI exposes `GET /v1/models` for models available to an API key
|
|
116
|
+
([OpenAI Models API](https://platform.openai.com/docs/api-reference/models/list)).
|
|
117
|
+
The Codex documentation does not provide a stable, public model-discovery API
|
|
118
|
+
for the effective catalog of every Codex/ChatGPT account and local client.
|
|
119
|
+
An API-key catalog therefore cannot safely populate routing for users whose
|
|
120
|
+
Codex authentication, plan, organization policy, or client surface differs.
|
|
121
|
+
|
|
122
|
+
## Options compared
|
|
123
|
+
|
|
124
|
+
| Option | Survives catalog changes | Works across Claude and Codex | Multi-user behavior | Offline/update behavior | Verdict |
|
|
125
|
+
|---|---:|---:|---|---|---|
|
|
126
|
+
| Concrete model names in issues | No. Every rename, replacement, or access change makes existing issues stale. | No. Names and effort controls are provider-specific. | Assumes every collaborator has the same entitlement and budget. | Old issues remain wrong until rewritten. | Reject. Suitable only for an explicit reproducibility pin in a provider-specific experiment. |
|
|
127
|
+
| Stable task intent resolved by user/org surface policy | Yes. Existing issues survive mapping changes. | Yes. Each surface resolves independently or inherits. | Preserves personal and organization choices. | Cached/local policy continues to work; unknown targets can inherit and warn. | **Adopt.** |
|
|
128
|
+
| Project-local concrete routing table | Partly. One table can be updated, but every checkout still receives volatile provider data. | Only by maintaining multiple provider sections. | Project config outranks user config on both surfaces, so the maintainer's preferences can override collaborators. | Fresh clones can start stale; offline discovery cannot repair it. | Reject as the default. Permit only explicit team policy/constraints. |
|
|
129
|
+
|
|
130
|
+
## Setup and update implications
|
|
131
|
+
|
|
132
|
+
### `setup-workflow`
|
|
133
|
+
|
|
134
|
+
`setup-workflow` should install or reconcile only the Kit-owned intent schema,
|
|
135
|
+
templates, validation, and a documented inheritance fallback. It may **offer**
|
|
136
|
+
to create a user-local routing policy for the detected surface, but only with
|
|
137
|
+
explicit consent and never as a committed project default.
|
|
138
|
+
|
|
139
|
+
Setup must not attempt to infer a complete routing table from `/v1/models`:
|
|
140
|
+
|
|
141
|
+
- API discovery requires credentials and network access.
|
|
142
|
+
- API availability is not equivalent to Claude Code or Codex subscription
|
|
143
|
+
availability across all supported providers and account types.
|
|
144
|
+
- A discovered model ID says nothing about the user's desired cost/capability
|
|
145
|
+
trade-off.
|
|
146
|
+
|
|
147
|
+
An interactive setup may validate explicitly selected targets using the active
|
|
148
|
+
surface and show a preview. Failure or absence of discovery should leave the
|
|
149
|
+
policy at `inherit`, not install guessed names.
|
|
150
|
+
|
|
151
|
+
### `kit-update`
|
|
152
|
+
|
|
153
|
+
`kit-update` should update the Kit-owned schema, resolver implementation, and
|
|
154
|
+
provider adapter capabilities transactionally. It must preserve user-owned
|
|
155
|
+
mappings and project-owned constraints. If the schema changes, migration must
|
|
156
|
+
be semantic (for example, rename an intent key), previewed, and reversible;
|
|
157
|
+
ordinary updates must not rewrite historical issues or replace personal model
|
|
158
|
+
choices.
|
|
159
|
+
|
|
160
|
+
At dispatch, the resolver should:
|
|
161
|
+
|
|
162
|
+
1. read the issue's stable intent;
|
|
163
|
+
2. apply organization/user policy and explicit session overrides according to
|
|
164
|
+
the active surface;
|
|
165
|
+
3. resolve or inherit without network-dependent mutation;
|
|
166
|
+
4. reject a malformed intent, but fall back to `inherit` with a visible warning
|
|
167
|
+
when a volatile target is unavailable;
|
|
168
|
+
5. record the effective model and effort in run evidence, not in the durable
|
|
169
|
+
issue contract.
|
|
170
|
+
|
|
171
|
+
This lets a later Kit release improve mappings for users who accept Kit
|
|
172
|
+
defaults while allowing a maintainer's user-local policy to evolve independently.
|
|
173
|
+
|
|
174
|
+
## Implication for #213
|
|
175
|
+
|
|
176
|
+
#213 should not hardcode `sol`, `terra`, `luna`, Claude aliases, or full model
|
|
177
|
+
IDs into the issue template or validator. It should instead establish the
|
|
178
|
+
provider-neutral intent schema and make `to-issues` emit that schema. The
|
|
179
|
+
execute-ready check should reject concrete provider model recommendations in
|
|
180
|
+
shared handoffs, while orchestration resolves the intent on the active surface.
|
|
181
|
+
|
|
182
|
+
The existing free-text `Recommended model: <Model [Effort]>` field conflates a
|
|
183
|
+
durable work classification with a volatile local decision. Replace it with
|
|
184
|
+
machine-readable intent and render the effective local recommendation only
|
|
185
|
+
when a session is actually dispatched.
|
|
186
|
+
|
|
187
|
+
## Open risks and decisions
|
|
188
|
+
|
|
189
|
+
1. **Vocabulary quality:** `judgment/development/mechanical` and
|
|
190
|
+
`deep/balanced/light` need task-based definitions and contract tests so they
|
|
191
|
+
do not become disguised provider tiers.
|
|
192
|
+
2. **Enforcement asymmetry:** Both surfaces can override model and effort for
|
|
193
|
+
subagents, but their precedence, supported values, organization controls,
|
|
194
|
+
and fallback behavior differ. The Kit must promise intent resolution, not
|
|
195
|
+
identical low-level enforcement.
|
|
196
|
+
3. **Unavailable targets:** silent substitution hides policy drift. Inheritance
|
|
197
|
+
should be visible in run evidence, and strict team policies may choose to
|
|
198
|
+
fail instead.
|
|
199
|
+
4. **Organization policy:** managed restrictions outrank personal/project
|
|
200
|
+
wishes. The resolver must treat the surface's effective selection as
|
|
201
|
+
authoritative.
|
|
202
|
+
5. **Reproducibility exception:** a benchmark or regression reproduction may
|
|
203
|
+
legitimately pin a provider/model snapshot. That is a distinct,
|
|
204
|
+
provider-specific execution constraint, not the normal issue-routing field.
|
|
205
|
+
6. **No universal discovery source:** model catalogs are useful validation
|
|
206
|
+
inputs where available, but cannot be the SSOT for setup or updates across
|
|
207
|
+
all supported account types and providers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikon85/agent-workflow-kit",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.6",
|
|
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": {
|
|
@@ -184,15 +184,21 @@ test('both release skill surfaces name only the owned scoped npm package', async
|
|
|
184
184
|
}
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
// Amended by #257: one gate, at the Semver. The previous contract required a
|
|
188
|
+
// second confirmation before the tag; that gate left prepared versions stranded
|
|
189
|
+
// in `awaiting-tag` and is gone. What must survive is the narrowing rule — a
|
|
190
|
+
// build-only request never becomes release authority.
|
|
191
|
+
test('one confirmed Semver authorizes the release through tag and publish', async () => {
|
|
188
192
|
const claude = await readFile(join(REPO, '.claude/skills/kit-release/SKILL.md'), 'utf8');
|
|
189
193
|
const codex = await readFile(join(REPO, '.agents/skills/kit-release/SKILL.md'), 'utf8');
|
|
190
|
-
assert.equal(codex, claude);
|
|
194
|
+
assert.equal(codex.split('\n---\n')[1], claude.split('\n---\n')[1]);
|
|
191
195
|
for (const body of [claude, codex]) {
|
|
192
196
|
assert.match(body, /explicit AFK end-to-end mandate/i);
|
|
193
197
|
assert.match(body, /deterministic recommendation/i);
|
|
194
|
-
assert.match(body, /
|
|
195
|
-
assert.match(body, /
|
|
198
|
+
assert.match(body, /annotated\s+`v<version>` tag/i);
|
|
199
|
+
assert.match(body, /confirmed Semver authorizes[\s\S]*without asking again/i);
|
|
200
|
+
assert.match(body, /narrower build-only or single-action request/i);
|
|
201
|
+
assert.doesNotMatch(body, /separate explicit confirmation/i);
|
|
196
202
|
}
|
|
197
203
|
});
|
|
198
204
|
|
|
@@ -11,7 +11,7 @@ export const UPDATE_TITLE = 'chore: update agent workflow kit';
|
|
|
11
11
|
const UPDATE_BODY = [
|
|
12
12
|
'Automated, parity-verified update of `@ikon85/agent-workflow-kit`.',
|
|
13
13
|
'',
|
|
14
|
-
'The transactional update candidate passed the
|
|
14
|
+
'The transactional update candidate passed the built-in Kit invariants before this branch was published.',
|
|
15
15
|
'',
|
|
16
16
|
'This pull request is never merged automatically.',
|
|
17
17
|
].join('\n');
|
|
@@ -46,6 +46,8 @@ test('automated update pull requests carry the behavior availability summary', a
|
|
|
46
46
|
assert.match(created.body, /## Availability/);
|
|
47
47
|
assert.match(created.body, /newly degraded: orchestrate-wave\.projectRecipe/);
|
|
48
48
|
assert.match(created.body, /still unresolved: orchestrateWaveRecipe:invalid/);
|
|
49
|
+
assert.match(created.body, /built-in Kit invariants/);
|
|
50
|
+
assert.doesNotMatch(created.body, /consumer test/i);
|
|
49
51
|
assert.match(created.body, /never merged automatically/);
|
|
50
52
|
});
|
|
51
53
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Release-authorization contract (#257).
|
|
3
|
+
|
|
4
|
+
The confirmed Semver authorizes the whole release, through tag and publish.
|
|
5
|
+
One human gate at version choice, not two — the second gate stalled every
|
|
6
|
+
release at `awaiting-tag` until someone returned, which is how 0.34.2 was
|
|
7
|
+
skipped and buried under 0.34.3 (#243).
|
|
8
|
+
|
|
9
|
+
What this pins:
|
|
10
|
+
|
|
11
|
+
- Both `kit-release` surfaces carry the carried-through authorization, and
|
|
12
|
+
neither demands a separate confirmation before tagging.
|
|
13
|
+
- The safety framing survives the change: irreversibility and the pre-tag
|
|
14
|
+
gates stay named, so autonomy never reads as "tag whatever".
|
|
15
|
+
- `CLAUDE.md`, `AGENTS.md` and ADR-0004 agree with the skill.
|
|
16
|
+
|
|
17
|
+
Run: python3 scripts/test_release_authorization_contract.py
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import unittest
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
25
|
+
SURFACES = (".claude/skills/kit-release/SKILL.md", ".agents/skills/kit-release/SKILL.md")
|
|
26
|
+
ADR = "docs/adr/0004-release-intent-is-a-version-tag.md"
|
|
27
|
+
DOCTRINE_FILES = ("CLAUDE.md", "AGENTS.md")
|
|
28
|
+
# Wording that would reinstate the second gate anywhere in the release surface.
|
|
29
|
+
SECOND_GATE = (
|
|
30
|
+
"separate explicit confirmation",
|
|
31
|
+
"separately confirmed publication intent",
|
|
32
|
+
"obtain a separate",
|
|
33
|
+
"After that confirmation",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read(relative: str) -> str:
|
|
38
|
+
return (ROOT / relative).read_text(encoding="utf-8")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalized(relative: str) -> str:
|
|
42
|
+
return " ".join(read(relative).split())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SkillSurfaceContract(unittest.TestCase):
|
|
46
|
+
def test_both_surfaces_carry_the_authorization_through_to_the_tag(self):
|
|
47
|
+
for surface in SURFACES:
|
|
48
|
+
with self.subTest(surface=surface):
|
|
49
|
+
prose = normalized(surface)
|
|
50
|
+
self.assertIn("confirmed Semver authorizes", prose)
|
|
51
|
+
self.assertIn("tag and publish", prose)
|
|
52
|
+
self.assertIn("without asking again", prose)
|
|
53
|
+
|
|
54
|
+
def test_no_surface_reinstates_a_second_confirmation_gate(self):
|
|
55
|
+
for surface in SURFACES:
|
|
56
|
+
for phrase in SECOND_GATE:
|
|
57
|
+
with self.subTest(surface=surface, phrase=phrase):
|
|
58
|
+
self.assertNotIn(phrase, normalized(surface))
|
|
59
|
+
|
|
60
|
+
def test_autonomy_never_drops_the_safety_framing(self):
|
|
61
|
+
"""Tagging stays irreversible and gated — the gates just move earlier."""
|
|
62
|
+
for surface in SURFACES:
|
|
63
|
+
with self.subTest(surface=surface):
|
|
64
|
+
prose = normalized(surface)
|
|
65
|
+
self.assertIn("irreversible", prose)
|
|
66
|
+
self.assertIn("cannot be reused", prose)
|
|
67
|
+
for gate in ("release:guard", "kit:staleness", "npm pack"):
|
|
68
|
+
self.assertIn(gate, prose)
|
|
69
|
+
|
|
70
|
+
def test_the_tag_remains_the_sole_publication_intent(self):
|
|
71
|
+
for surface in SURFACES:
|
|
72
|
+
with self.subTest(surface=surface):
|
|
73
|
+
prose = normalized(surface)
|
|
74
|
+
self.assertIn("Merging integrates", prose)
|
|
75
|
+
self.assertIn("annotated", prose)
|
|
76
|
+
|
|
77
|
+
def test_both_surfaces_stay_mirrored_below_the_frontmatter(self):
|
|
78
|
+
bodies = [read(surface).split("\n---\n", 1)[1] for surface in SURFACES]
|
|
79
|
+
self.assertEqual(bodies[0], bodies[1])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ProjectDoctrineContract(unittest.TestCase):
|
|
83
|
+
def test_root_convention_files_agree_with_the_skill(self):
|
|
84
|
+
for name in DOCTRINE_FILES:
|
|
85
|
+
with self.subTest(document=name):
|
|
86
|
+
prose = normalized(name)
|
|
87
|
+
self.assertIn("confirmed Semver authorizes", prose)
|
|
88
|
+
for phrase in SECOND_GATE:
|
|
89
|
+
self.assertNotIn(phrase, prose)
|
|
90
|
+
|
|
91
|
+
def test_the_adr_records_an_amendment_instead_of_a_silent_rewrite(self):
|
|
92
|
+
prose = normalized(ADR)
|
|
93
|
+
self.assertIn("Amended", prose)
|
|
94
|
+
self.assertIn("2026-07-25", prose)
|
|
95
|
+
self.assertIn("confirmed Semver authorizes", prose)
|
|
96
|
+
# The original decision must remain readable, not be overwritten.
|
|
97
|
+
self.assertIn("version tag", prose)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
unittest.main()
|
|
@@ -56,7 +56,12 @@ for (const skill of Object.keys(fixture.readinessFixture.skills)) {
|
|
|
56
56
|
const kit = await makeKit({ '.claude/skills/to-prd/SKILL.md': 'fixture\n' });
|
|
57
57
|
const consumer = await makeEmptyDir();
|
|
58
58
|
async function setKitReadiness(manifest) {
|
|
59
|
-
const
|
|
59
|
+
const candidateManifest = structuredClone(manifest);
|
|
60
|
+
for (const [name, declaration] of Object.entries(candidateManifest.skills)) {
|
|
61
|
+
declaration.publish = name === 'to-prd';
|
|
62
|
+
if (name === 'to-prd') declaration.surfaces = ['claude'];
|
|
63
|
+
}
|
|
64
|
+
const content = `${JSON.stringify(candidateManifest, null, 2)}\n`;
|
|
60
65
|
const path = join(kit, readinessPath);
|
|
61
66
|
await mkdir(join(kit, '.claude/skills'), { recursive: true });
|
|
62
67
|
await writeFile(path, content);
|
|
@@ -49,13 +49,12 @@ def classify(first_line, is_empty):
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def update_workflow_action(
|
|
52
|
-
provider, choice, destination_exists,
|
|
53
|
-
pull_requests_allowed=True,
|
|
52
|
+
provider, choice, destination_exists, pull_requests_allowed=True,
|
|
54
53
|
):
|
|
55
54
|
"""Reference decision table for the prompt-driven setup contract."""
|
|
56
55
|
if provider != "github" or destination_exists or choice != "enable":
|
|
57
56
|
return "skip"
|
|
58
|
-
return "create" if
|
|
57
|
+
return "create" if pull_requests_allowed else "skip"
|
|
59
58
|
|
|
60
59
|
|
|
61
60
|
def load_census_setup_effects():
|
|
@@ -421,19 +420,18 @@ class SeedTemplatesValid(unittest.TestCase):
|
|
|
421
420
|
|
|
422
421
|
def test_update_workflow_provider_and_choice_fixtures(self):
|
|
423
422
|
fixtures = [
|
|
424
|
-
("github", "enable", False, True,
|
|
425
|
-
("github", "opt-out", False, True,
|
|
426
|
-
("github", "later", False, True,
|
|
427
|
-
("github", "enable", True, True,
|
|
428
|
-
("github", "enable", False, False,
|
|
429
|
-
("
|
|
430
|
-
("
|
|
431
|
-
("local", "enable", False, True, True, "skip"),
|
|
423
|
+
("github", "enable", False, True, "create"),
|
|
424
|
+
("github", "opt-out", False, True, "skip"),
|
|
425
|
+
("github", "later", False, True, "skip"),
|
|
426
|
+
("github", "enable", True, True, "skip"),
|
|
427
|
+
("github", "enable", False, False, "skip"),
|
|
428
|
+
("gitlab", "enable", False, True, "skip"),
|
|
429
|
+
("local", "enable", False, True, "skip"),
|
|
432
430
|
]
|
|
433
|
-
for provider, choice, exists,
|
|
431
|
+
for provider, choice, exists, allowed, expected in fixtures:
|
|
434
432
|
self.assertEqual(
|
|
435
433
|
update_workflow_action(
|
|
436
|
-
provider, choice, exists,
|
|
434
|
+
provider, choice, exists, allowed,
|
|
437
435
|
), expected,
|
|
438
436
|
)
|
|
439
437
|
|
|
@@ -446,8 +444,8 @@ class SeedTemplatesValid(unittest.TestCase):
|
|
|
446
444
|
"Ask later",
|
|
447
445
|
"GitHub tracker",
|
|
448
446
|
"skipped (already present)",
|
|
449
|
-
"
|
|
450
|
-
"
|
|
447
|
+
"built-in Kit invariant validator",
|
|
448
|
+
"no Consumer package script",
|
|
451
449
|
"can_approve_pull_request_reviews",
|
|
452
450
|
"Allow GitHub Actions to create and approve pull requests",
|
|
453
451
|
"explicit confirmation",
|
|
@@ -461,10 +459,13 @@ class SeedTemplatesValid(unittest.TestCase):
|
|
|
461
459
|
"schedule:", "workflow_dispatch:", "contents: write",
|
|
462
460
|
"pull-requests: write", "agent-workflow-kit-update-pr",
|
|
463
461
|
"@ikon85/agent-workflow-kit@latest", "fetch-depth: 0",
|
|
464
|
-
"node-version: 22.14", "
|
|
462
|
+
"node-version: 22.14", "Verify and upsert the Kit update pull request",
|
|
465
463
|
):
|
|
466
464
|
self.assertIn(token, workflow)
|
|
467
|
-
for forbidden in (
|
|
465
|
+
for forbidden in (
|
|
466
|
+
"npm_token", "NPM_TOKEN", "auto-merge", "gh pr merge",
|
|
467
|
+
"npm ci", "npm test", "package-lock.json",
|
|
468
|
+
):
|
|
468
469
|
self.assertNotIn(forbidden, workflow)
|
|
469
470
|
|
|
470
471
|
mirror = (REPO / ".agents/skills/setup-workflow/assets/agent-workflow-kit-update.yml")
|
package/src/commands/update.mjs
CHANGED
|
@@ -2,8 +2,11 @@ import { readFile, rm } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { assertConsumerReleaseParity } from '../../scripts/release-parity.mjs';
|
|
4
4
|
import {
|
|
5
|
-
activateCandidate, adoptReadinessCandidate,
|
|
5
|
+
activateCandidate, adoptReadinessCandidate, materializeUpdateCandidate, readReadinessManifest,
|
|
6
6
|
} from '../lib/updateCandidate.mjs';
|
|
7
|
+
import {
|
|
8
|
+
verifyCandidateSchema, verifyUpdateCandidate,
|
|
9
|
+
} from '../lib/verifyUpdateCandidate.mjs';
|
|
7
10
|
import { reconcile } from '../lib/updateReconcile.mjs';
|
|
8
11
|
import {
|
|
9
12
|
CONSUMER_MANIFEST_NAME, PACKAGE_MANIFEST_NAME, readManifest,
|
|
@@ -46,7 +49,7 @@ export async function update(options) {
|
|
|
46
49
|
async function updatePackage(options) {
|
|
47
50
|
const {
|
|
48
51
|
kitRoot, consumerRoot, decide = () => false, dryRun = false,
|
|
49
|
-
releaseIdentities, verify =
|
|
52
|
+
releaseIdentities, verify = verifyUpdateCandidate, activate = activateCandidate,
|
|
50
53
|
signal, onState = () => {}, resumeFrom,
|
|
51
54
|
} = options;
|
|
52
55
|
const history = [];
|
|
@@ -57,7 +60,8 @@ async function updatePackage(options) {
|
|
|
57
60
|
if (!pkg) throw new Error('kit package manifest not found');
|
|
58
61
|
if (!dryRun) verifyRelease(releaseIdentities, pkg.kitVersion);
|
|
59
62
|
const consumerManifestPath = join(consumerRoot, CONSUMER_MANIFEST_NAME);
|
|
60
|
-
|
|
63
|
+
const priorConsumerManifest = await readManifest(consumerManifestPath);
|
|
64
|
+
if (!priorConsumerManifest) {
|
|
61
65
|
throw new Error('not initialised — run `init` first');
|
|
62
66
|
}
|
|
63
67
|
const consumerManifestBefore = await readFile(consumerManifestPath);
|
|
@@ -75,7 +79,7 @@ async function updatePackage(options) {
|
|
|
75
79
|
let previewFailure;
|
|
76
80
|
try {
|
|
77
81
|
Object.assign(preview, await previewReadinessAdoption({
|
|
78
|
-
kitRoot, consumerRoot, priorReadinessManifest, nextReadinessManifest,
|
|
82
|
+
kitRoot, consumerRoot, pkg, priorReadinessManifest, nextReadinessManifest,
|
|
79
83
|
}));
|
|
80
84
|
preview.conflicts.push(...(preview.migrationConflicts ?? []).map((path) => ({
|
|
81
85
|
path,
|
|
@@ -108,18 +112,26 @@ async function updatePackage(options) {
|
|
|
108
112
|
}
|
|
109
113
|
return applyTransaction({
|
|
110
114
|
kitRoot, consumerRoot, pkg, preview: resolvedPreview, decisions, verify, activate, signal, resumeFrom,
|
|
111
|
-
consumerManifestBefore,
|
|
115
|
+
consumerManifestBefore, priorConsumerManifest,
|
|
116
|
+
priorReadinessManifest, nextReadinessManifest, history, transition,
|
|
112
117
|
});
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
async function previewReadinessAdoption(context) {
|
|
116
|
-
const {
|
|
117
|
-
|
|
121
|
+
const {
|
|
122
|
+
kitRoot, consumerRoot, pkg, priorReadinessManifest, nextReadinessManifest,
|
|
123
|
+
} = context;
|
|
124
|
+
const candidateRoot = await materializeUpdateCandidate({
|
|
125
|
+
consumerRoot, pkg, priorReadinessManifest, nextReadinessManifest,
|
|
126
|
+
});
|
|
118
127
|
try {
|
|
119
|
-
await reconcile({
|
|
128
|
+
const candidatePreview = await reconcile({
|
|
120
129
|
kitRoot, consumerRoot: candidateRoot,
|
|
121
130
|
decide: (action) => action === 'collision' ? 'keep-as-owned' : false,
|
|
122
131
|
});
|
|
132
|
+
await verifyCandidateSchema(candidateRoot, {
|
|
133
|
+
pkg, preview: candidatePreview, priorReadinessManifest, nextReadinessManifest,
|
|
134
|
+
});
|
|
123
135
|
return await adoptReadinessCandidate({
|
|
124
136
|
candidateRoot, consumerRoot, priorManifest: priorReadinessManifest,
|
|
125
137
|
nextManifest: nextReadinessManifest,
|
|
@@ -146,7 +158,8 @@ async function resolvePreview({ kitRoot, consumerRoot, preview, decisions, decid
|
|
|
146
158
|
async function applyTransaction(context) {
|
|
147
159
|
const {
|
|
148
160
|
kitRoot, consumerRoot, pkg, preview, decisions, verify, activate, signal, resumeFrom,
|
|
149
|
-
consumerManifestBefore,
|
|
161
|
+
consumerManifestBefore, priorConsumerManifest,
|
|
162
|
+
priorReadinessManifest, nextReadinessManifest, history, transition,
|
|
150
163
|
} = context;
|
|
151
164
|
let candidateRoot = resumeFrom;
|
|
152
165
|
let keepCandidate = false;
|
|
@@ -157,12 +170,29 @@ async function applyTransaction(context) {
|
|
|
157
170
|
throw new Error('collision-bearing candidate cannot be resumed safely');
|
|
158
171
|
}
|
|
159
172
|
if (!candidateRoot) {
|
|
160
|
-
candidateRoot = await
|
|
173
|
+
candidateRoot = await materializeUpdateCandidate({
|
|
174
|
+
consumerRoot, pkg, priorReadinessManifest, nextReadinessManifest,
|
|
175
|
+
});
|
|
161
176
|
await reconcile({
|
|
162
177
|
kitRoot, consumerRoot: candidateRoot,
|
|
163
178
|
decide: (action, path) => decisions.get(decisionKey(action, path)),
|
|
164
179
|
});
|
|
165
180
|
}
|
|
181
|
+
phase = 'verification';
|
|
182
|
+
await transition('verifying');
|
|
183
|
+
const abort = async () => {
|
|
184
|
+
keepCandidate = true;
|
|
185
|
+
return { ...await terminal(preview, 'aborted', history, transition), candidateRoot };
|
|
186
|
+
};
|
|
187
|
+
if (signal?.aborted) return abort();
|
|
188
|
+
const canonicalContext = {
|
|
189
|
+
pkg: structuredClone(pkg),
|
|
190
|
+
preview: structuredClone(preview),
|
|
191
|
+
priorConsumerManifest: structuredClone(priorConsumerManifest),
|
|
192
|
+
priorReadinessManifest: structuredClone(priorReadinessManifest),
|
|
193
|
+
nextReadinessManifest: structuredClone(nextReadinessManifest),
|
|
194
|
+
};
|
|
195
|
+
await verifyCandidateSchema(candidateRoot, canonicalContext);
|
|
166
196
|
const readiness = await adoptReadinessCandidate({
|
|
167
197
|
candidateRoot, consumerRoot, priorManifest: priorReadinessManifest,
|
|
168
198
|
nextManifest: nextReadinessManifest,
|
|
@@ -178,18 +208,20 @@ async function applyTransaction(context) {
|
|
|
178
208
|
if (readiness.incompatible.length) {
|
|
179
209
|
throw new Error(`monotonic compatibility would block existing skill core: ${readiness.incompatible.join(', ')}`);
|
|
180
210
|
}
|
|
181
|
-
|
|
182
|
-
await
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
await verify(candidateRoot);
|
|
211
|
+
canonicalContext.preview = structuredClone(preview);
|
|
212
|
+
await verifyUpdateCandidate(candidateRoot, canonicalContext);
|
|
213
|
+
if (verify !== verifyUpdateCandidate) {
|
|
214
|
+
const extensionContext = structuredClone(canonicalContext);
|
|
215
|
+
await verify(candidateRoot, extensionContext);
|
|
216
|
+
await verifyUpdateCandidate(candidateRoot, canonicalContext);
|
|
217
|
+
}
|
|
189
218
|
if (signal?.aborted) return abort();
|
|
190
219
|
phase = 'activation';
|
|
191
220
|
await activate({
|
|
192
|
-
candidateRoot, consumerRoot,
|
|
221
|
+
candidateRoot, consumerRoot,
|
|
222
|
+
pkg: canonicalContext.pkg,
|
|
223
|
+
preview: canonicalContext.preview,
|
|
224
|
+
consumerManifestBefore,
|
|
193
225
|
});
|
|
194
226
|
return { ...await terminal(preview, 'applied', history, transition), status: 'updated' };
|
|
195
227
|
} catch (error) {
|
|
@@ -252,4 +284,4 @@ async function terminal(result, state, history, transition) {
|
|
|
252
284
|
};
|
|
253
285
|
}
|
|
254
286
|
|
|
255
|
-
export { verifyCandidate } from '../lib/
|
|
287
|
+
export { verifyUpdateCandidate, verifyUpdateCandidate as verifyCandidate } from '../lib/verifyUpdateCandidate.mjs';
|
package/src/lib/bundle.mjs
CHANGED
|
@@ -107,7 +107,7 @@ export const HELPER_FILES = [
|
|
|
107
107
|
// wave. Library (imported by the Phase-0 claim protocol) → 0o644.
|
|
108
108
|
{ path: 'src/lib/waveClaim.mjs', kind: 'script', mode: 0o644 },
|
|
109
109
|
// GitHub-consumer automation: invokes the existing update command, then owns
|
|
110
|
-
// only the stable
|
|
110
|
+
// only the stable Kit-verified branch/pull-request upsert.
|
|
111
111
|
{ path: 'scripts/kit-update-pr.mjs', kind: 'script', mode: 0o755 },
|
|
112
112
|
// Stdlib-only project census foundation. index.mjs is the stable consumer
|
|
113
113
|
// entrypoint; its five local modules must ship with it as one helper unit.
|