@ikon85/agent-workflow-kit 0.34.5 → 0.35.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/.agents/skills/ask-matt/SKILL.md +8 -0
- package/.agents/skills/audit-skills/SKILL.md +8 -0
- package/.agents/skills/board-to-waves/SKILL.md +8 -0
- package/.agents/skills/census-update/SKILL.md +8 -0
- package/.agents/skills/code-review/SKILL.md +8 -0
- package/.agents/skills/codebase-design/SKILL.md +8 -0
- package/.agents/skills/codex-adapter-sync/SKILL.md +8 -0
- package/.agents/skills/decision-gate/SKILL.md +8 -0
- package/.agents/skills/diagnose/SKILL.md +8 -0
- package/.agents/skills/domain-modeling/SKILL.md +8 -0
- package/.agents/skills/git-worktree-recover/SKILL.md +8 -0
- package/.agents/skills/grill-me/SKILL.md +8 -0
- package/.agents/skills/grill-with-docs/SKILL.md +8 -0
- package/.agents/skills/implement/SKILL.md +8 -0
- package/.agents/skills/improve-codebase-architecture/SKILL.md +8 -0
- package/.agents/skills/kit-release/SKILL.md +37 -21
- package/.agents/skills/kit-update/SKILL.md +18 -4
- package/.agents/skills/local-ci/SKILL.md +8 -0
- package/.agents/skills/memory-lifecycle/SKILL.md +8 -0
- package/.agents/skills/orchestrate-wave/SKILL.md +2 -2
- package/.agents/skills/project-release/SKILL.md +8 -0
- package/.agents/skills/prototype/SKILL.md +8 -0
- package/.agents/skills/research/SKILL.md +8 -0
- package/.agents/skills/resolving-merge-conflicts/SKILL.md +8 -0
- package/.agents/skills/retro/SKILL.md +8 -0
- package/.agents/skills/scale-check/SKILL.md +8 -0
- package/.agents/skills/security-audit/SKILL.md +8 -0
- package/.agents/skills/setup-workflow/SKILL.md +30 -0
- package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +1 -0
- package/.agents/skills/setup-workflow/spec-self-critique-seed.md +1 -0
- package/.agents/skills/spec-self-critique/SKILL.md +8 -0
- package/.agents/skills/tdd/SKILL.md +8 -0
- package/.agents/skills/to-issues/SKILL.md +8 -0
- package/.agents/skills/to-prd/SKILL.md +8 -0
- package/.agents/skills/to-waves/SKILL.md +8 -0
- package/.agents/skills/triage/SKILL.md +8 -0
- package/.agents/skills/verify-spike/SKILL.md +8 -0
- package/.agents/skills/wayfinder/SKILL.md +8 -0
- package/.agents/skills/wrapup/SKILL.md +8 -0
- package/.claude/skills/ask-matt/SKILL.md +8 -0
- package/.claude/skills/audit-skills/SKILL.md +8 -0
- package/.claude/skills/board-to-waves/SKILL.md +8 -0
- package/.claude/skills/census-update/SKILL.md +8 -0
- package/.claude/skills/code-review/SKILL.md +8 -0
- package/.claude/skills/codebase-design/SKILL.md +8 -0
- package/.claude/skills/codex-build/SKILL.md +8 -0
- package/.claude/skills/codex-review/SKILL.md +8 -0
- package/.claude/skills/decision-gate/SKILL.md +8 -0
- package/.claude/skills/diagnose/SKILL.md +8 -0
- package/.claude/skills/domain-modeling/SKILL.md +8 -0
- package/.claude/skills/git-guardrails-claude-code/SKILL.md +8 -0
- package/.claude/skills/git-worktree-recover/SKILL.md +8 -0
- package/.claude/skills/grill-me/SKILL.md +8 -0
- package/.claude/skills/grill-me-codex/SKILL.md +8 -0
- package/.claude/skills/grill-with-docs/SKILL.md +8 -0
- package/.claude/skills/grill-with-docs-codex/SKILL.md +8 -0
- package/.claude/skills/implement/SKILL.md +8 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +8 -0
- package/.claude/skills/kit-release/SKILL.md +37 -21
- package/.claude/skills/kit-update/SKILL.md +18 -4
- package/.claude/skills/local-ci/SKILL.md +8 -0
- package/.claude/skills/memory-lifecycle/SKILL.md +8 -0
- package/.claude/skills/orchestrate-wave/SKILL.md +2 -2
- package/.claude/skills/project-release/SKILL.md +8 -0
- package/.claude/skills/prototype/SKILL.md +8 -0
- package/.claude/skills/research/SKILL.md +8 -0
- package/.claude/skills/resolving-merge-conflicts/SKILL.md +8 -0
- package/.claude/skills/retro/SKILL.md +8 -0
- package/.claude/skills/scale-check/SKILL.md +8 -0
- package/.claude/skills/security-audit/SKILL.md +8 -0
- package/.claude/skills/setup-pre-commit/SKILL.md +8 -0
- package/.claude/skills/setup-workflow/SKILL.md +30 -0
- package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +1 -0
- package/.claude/skills/setup-workflow/spec-self-critique-seed.md +1 -0
- package/.claude/skills/spec-self-critique/SKILL.md +8 -0
- package/.claude/skills/tdd/SKILL.md +8 -0
- package/.claude/skills/to-issues/SKILL.md +8 -0
- package/.claude/skills/to-prd/SKILL.md +8 -0
- package/.claude/skills/to-waves/SKILL.md +8 -0
- package/.claude/skills/triage/SKILL.md +8 -0
- package/.claude/skills/verify-spike/SKILL.md +8 -0
- package/.claude/skills/wayfinder/SKILL.md +8 -0
- package/.claude/skills/wrapup/SKILL.md +8 -0
- package/.claude/skills/write-a-skill/SKILL.md +8 -0
- package/README.md +123 -4
- package/agent-workflow-kit.package.json +127 -87
- package/docs/adr/0004-release-intent-is-a-version-tag.md +18 -5
- package/docs/agents/skills/local-ci.md +1 -0
- package/docs/agents/skills/orchestrate-wave.md +1 -1
- package/docs/agents/skills/spec-self-critique.md +1 -1
- package/package.json +1 -1
- package/scripts/kit-release.test.mjs +10 -4
- package/scripts/project-skill-extension.mjs +37 -0
- package/scripts/readiness.mjs +2 -1
- package/scripts/test_release_authorization_contract.py +101 -0
- package/scripts/test_skill_publish_audit.py +9 -1
- package/src/cli.mjs +29 -12
- package/src/commands/init.mjs +24 -0
- package/src/commands/own.mjs +4 -3
- package/src/commands/update.mjs +86 -21
- package/src/lib/bundle.mjs +5 -0
- package/src/lib/manifest.mjs +13 -2
- package/src/lib/ownershipClassifier.mjs +54 -0
- package/src/lib/projectSkillExtension.mjs +60 -0
- package/src/lib/skillRegistry.mjs +167 -0
- package/src/lib/updateCandidate.mjs +105 -4
- package/src/lib/updateDecisions.mjs +5 -0
- package/src/lib/updateReconcile.mjs +64 -8
- package/src/lib/verifyUpdateCandidate.mjs +13 -1
- package/src/lib/verifyUpdateCandidateProtocol.mjs +2 -0
- package/src/lib/verifyUpdateCandidateTransaction.mjs +9 -3
|
@@ -6,6 +6,17 @@ Clarified 2026-07-25 by issue #239: an explicit AFK end-to-end mandate may
|
|
|
6
6
|
authorize deterministic reversible release preparation, but never the
|
|
7
7
|
publication tag itself.
|
|
8
8
|
|
|
9
|
+
Amended 2026-07-25 by issue #257: the confirmed Semver authorizes the whole
|
|
10
|
+
release, through tag and publish. The separation of integration and publication
|
|
11
|
+
below is unchanged — merging still cannot publish, and only an annotated tag
|
|
12
|
+
records release intent — but that intent no longer waits for a second human
|
|
13
|
+
confirmation. The maintainer chooses the version once; the agent then merges,
|
|
14
|
+
tags, and monitors to `released`. The second gate had proven worse than no gate:
|
|
15
|
+
it left prepared versions sitting in `awaiting-tag` until someone returned,
|
|
16
|
+
which is how 0.34.2 was skipped and buried under 0.34.3 (#243). Safety stays in
|
|
17
|
+
the gates that run regardless of who is watching — guard, staleness, suite,
|
|
18
|
+
pack, and the workflow's own tag/version/ancestry validation.
|
|
19
|
+
|
|
9
20
|
The release workflow currently publishes whenever a merge to `main` changes
|
|
10
21
|
`package.json`. That keeps the canonical branch and npm close together, but it
|
|
11
22
|
also makes an ordinary merge an irreversible public action whose consequence is
|
|
@@ -39,11 +50,13 @@ We decided that integration and publication are separate transitions:
|
|
|
39
50
|
|
|
40
51
|
## Consequences
|
|
41
52
|
|
|
42
|
-
- A prepared version
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
- A prepared version exists on `main` in an `awaiting-tag` state only for as
|
|
54
|
+
long as the agent needs to tag it — not until a human returns (amended by
|
|
55
|
+
#257).
|
|
56
|
+
- The authorizing act is the confirmed Semver. An explicit AFK Wave/Program
|
|
57
|
+
mandate that names release preparation carries the same weight, and either
|
|
58
|
+
route reaches through tag and publish for that one target. A narrower
|
|
59
|
+
build-only or single-action request never becomes this authority.
|
|
47
60
|
- A single-Wave mandate does not authorize the next Program Wave. A later
|
|
48
61
|
explicit whole-Program mandate does authorize all planned Waves and remains in
|
|
49
62
|
force across their boundaries.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<!-- setup-workflow: state=filled -->
|
|
2
|
+
<!-- agent-workflow-kit: project-extension/v1; skill=orchestrate-wave -->
|
|
2
3
|
# Project layer — orchestrate-wave
|
|
3
4
|
|
|
4
5
|
These stable sections are intentionally empty until the kit has proven its own landing recipe. The skill uses its generic fallback meanwhile.
|
|
@@ -10,4 +11,3 @@ These stable sections are intentionally empty until the kit has proven its own l
|
|
|
10
11
|
## §Verify Recipe
|
|
11
12
|
## §Headless Login
|
|
12
13
|
## §Landing
|
|
13
|
-
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<!-- setup-workflow: state=filled -->
|
|
2
|
+
<!-- agent-workflow-kit: project-extension/v1; skill=spec-self-critique -->
|
|
2
3
|
# Project layer — spec-self-critique
|
|
3
4
|
|
|
4
5
|
The generic checklist owns the checks; `/retro` appends project-specific incidents below these stable anchors.
|
|
@@ -15,4 +16,3 @@ The generic checklist owns the checks; `/retro` appends project-specific inciden
|
|
|
15
16
|
## Point 10
|
|
16
17
|
## Point 11
|
|
17
18
|
## Point 12
|
|
18
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikon85/agent-workflow-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
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
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { inspectProjectSkillExtension } from '../src/lib/projectSkillExtension.mjs';
|
|
5
|
+
|
|
6
|
+
function argumentsFor(argv) {
|
|
7
|
+
const args = [...argv];
|
|
8
|
+
if (args.shift() !== 'inspect') {
|
|
9
|
+
throw new Error('usage: project-skill-extension.mjs inspect --skill <name> [--root <path>] --json');
|
|
10
|
+
}
|
|
11
|
+
const options = { root: process.cwd(), json: false };
|
|
12
|
+
while (args.length) {
|
|
13
|
+
const flag = args.shift();
|
|
14
|
+
if (flag === '--skill') options.skill = args.shift();
|
|
15
|
+
else if (flag === '--root') options.root = resolve(args.shift());
|
|
16
|
+
else if (flag === '--json') options.json = true;
|
|
17
|
+
else throw new Error(`unknown option: ${flag}`);
|
|
18
|
+
}
|
|
19
|
+
if (!options.skill) throw new Error('--skill is required');
|
|
20
|
+
if (!options.json) throw new Error('--json is required');
|
|
21
|
+
return options;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
25
|
+
const options = argumentsFor(argv);
|
|
26
|
+
return inspectProjectSkillExtension(options);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
30
|
+
main().then(
|
|
31
|
+
(result) => console.log(JSON.stringify(result)),
|
|
32
|
+
(error) => {
|
|
33
|
+
console.log(JSON.stringify({ state: 'blocked', diagnostic: error.message }));
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
}
|
package/scripts/readiness.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { join, resolve } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { firstLineState } from '../src/lib/sentinel.mjs';
|
|
6
6
|
import { CONSUMER_MANIFEST_NAME, readManifest, writeManifest } from '../src/lib/manifest.mjs';
|
|
7
|
+
import { readComposedSkillRegistry } from '../src/lib/skillRegistry.mjs';
|
|
7
8
|
|
|
8
9
|
const SOURCE_MANIFEST = '.claude/skills/skill-manifest.json';
|
|
9
10
|
const DECISIONS = new Set(['pending', 'not-applicable']);
|
|
@@ -176,7 +177,7 @@ export async function evaluateCapability({ root, capability, decision }) {
|
|
|
176
177
|
async function loadManifest(root) {
|
|
177
178
|
const body = await readText(root, SOURCE_MANIFEST);
|
|
178
179
|
if (body === null) throw new Error(`readiness manifest not found: ${SOURCE_MANIFEST}`);
|
|
179
|
-
return JSON.parse(body);
|
|
180
|
+
return readComposedSkillRegistry(root, JSON.parse(body));
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
export async function checkSkill({ root, skill, manifest }) {
|
|
@@ -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()
|
|
@@ -94,7 +94,7 @@ BARE_PRIVATE = [
|
|
|
94
94
|
]
|
|
95
95
|
PARENT_REF = re.compile(r"\.\./")
|
|
96
96
|
SHIPPED_SRC_IMPORT = re.compile(
|
|
97
|
-
r"^\s*import\b.*\bfrom\s+['\"]\.\./src/lib/[
|
|
97
|
+
r"^\s*import\b.*\bfrom\s+['\"]\.\./src/lib/[A-Za-z0-9-]+\.mjs['\"];\s*$"
|
|
98
98
|
)
|
|
99
99
|
LONG_HEX = re.compile(r"\b[0-9a-f]{40,}\b")
|
|
100
100
|
GH_TOKEN = re.compile(r"\b(?:ghp|gho|ghu|ghs|github_pat)_[A-Za-z0-9_]{20,}")
|
|
@@ -317,6 +317,14 @@ class Exemptions(unittest.TestCase):
|
|
|
317
317
|
encoding="utf-8")
|
|
318
318
|
self.assertEqual(audit_dir(self.dir), [])
|
|
319
319
|
|
|
320
|
+
def test_top_level_script_may_import_camel_case_shipped_deep_module(self):
|
|
321
|
+
p = self.dir / "scripts"
|
|
322
|
+
p.mkdir()
|
|
323
|
+
(p / "readiness.mjs").write_text(
|
|
324
|
+
"import { load } from '../src/lib/skillRegistry.mjs';\n",
|
|
325
|
+
encoding="utf-8")
|
|
326
|
+
self.assertEqual(audit_dir(self.dir), [])
|
|
327
|
+
|
|
320
328
|
|
|
321
329
|
class RealDistKitIsClean(unittest.TestCase):
|
|
322
330
|
"""Build dist-kit/ from SSOT and assert the audit passes (the publish gate)."""
|
package/src/cli.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { diff } from './commands/diff.mjs';
|
|
|
8
8
|
import { uninstall } from './commands/uninstall.mjs';
|
|
9
9
|
import { setOwnership } from './commands/own.mjs';
|
|
10
10
|
import { CONSUMER_ORIGIN, KIT_ORIGIN } from './lib/manifest.mjs';
|
|
11
|
+
import { nonInteractiveUpdateDecision } from './lib/updateDecisions.mjs';
|
|
11
12
|
import { currentAgentSurface } from './lib/agentSurfaceRegistry.mjs';
|
|
12
13
|
import { createCommandAdapter } from '../scripts/release-state.mjs';
|
|
13
14
|
import { installedIdentityFromDir } from '../scripts/release-parity.mjs';
|
|
@@ -25,6 +26,7 @@ const cmd = args[0];
|
|
|
25
26
|
const force = args.includes('--force');
|
|
26
27
|
const yes = args.includes('--yes') || args.includes('-y');
|
|
27
28
|
const owned = args.includes('--owned');
|
|
29
|
+
const ownershipState = args.find((arg) => arg.startsWith('--as='))?.slice('--as='.length);
|
|
28
30
|
|
|
29
31
|
p.intro('agent-workflow-kit');
|
|
30
32
|
|
|
@@ -49,7 +51,9 @@ try {
|
|
|
49
51
|
printPlan(r);
|
|
50
52
|
p.outro('Dry run — nothing written. Run `update` to apply.');
|
|
51
53
|
} else if (cmd === 'update') {
|
|
52
|
-
const decide = (action, path) =>
|
|
54
|
+
const decide = (action, path, classification) => (
|
|
55
|
+
decideUpdate(action, path, yes, classification)
|
|
56
|
+
);
|
|
53
57
|
const releaseIdentities = await readUpdateRelease();
|
|
54
58
|
const r = await update({
|
|
55
59
|
kitRoot: KIT_ROOT,
|
|
@@ -65,7 +69,10 @@ try {
|
|
|
65
69
|
if (r.state === 'failed') throw new Error(renderUpdateFailure(r));
|
|
66
70
|
if (r.state === 'conflicted') {
|
|
67
71
|
p.note(r.report.recommendation, 'recommendation');
|
|
68
|
-
p.outro(
|
|
72
|
+
p.outro(
|
|
73
|
+
`not applied · conflicts ${r.conflicts.length} · ` +
|
|
74
|
+
`ownership collisions ${r.collisions.length}`,
|
|
75
|
+
);
|
|
69
76
|
process.exitCode = 2;
|
|
70
77
|
} else if (r.status === 'current') {
|
|
71
78
|
p.outro(`aktuell · unchanged ${r.unchanged.length} · local modifications ${r.userModified.length}`);
|
|
@@ -78,12 +85,19 @@ try {
|
|
|
78
85
|
const r = await uninstall({ consumerRoot });
|
|
79
86
|
p.outro(`removed ${r.removed.length} · retained (edited/referenced) ${r.retained.length}`);
|
|
80
87
|
} else if (cmd === 'own' || cmd === 'disown') {
|
|
81
|
-
if (!args[1])
|
|
88
|
+
if (!args[1]) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Usage: agent-workflow-kit ${cmd} <path>` +
|
|
91
|
+
(cmd === 'own' ? ' [--as=contribution-bridge|explicit-fork]' : ''),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
82
94
|
const origin = cmd === 'own' ? CONSUMER_ORIGIN : KIT_ORIGIN;
|
|
83
|
-
await setOwnership({ consumerRoot, path: args[1], origin });
|
|
84
|
-
p.outro(`${args[1]} is now ${origin}-owned`
|
|
95
|
+
await setOwnership({ consumerRoot, path: args[1], origin, ownershipState });
|
|
96
|
+
p.outro(`${args[1]} is now ${origin}-owned` +
|
|
97
|
+
(origin === CONSUMER_ORIGIN ? ` (${ownershipState ?? 'explicit-fork'})` : ''));
|
|
85
98
|
} else {
|
|
86
|
-
p.note('Usage: agent-workflow-kit <init|update|diff|uninstall|own|disown>
|
|
99
|
+
p.note('Usage: agent-workflow-kit <init|update|diff|uninstall|own|disown> ' +
|
|
100
|
+
'[<path>] [--force] [--yes] [--owned] [--as=contribution-bridge|explicit-fork]');
|
|
87
101
|
p.outro('');
|
|
88
102
|
}
|
|
89
103
|
} catch (err) {
|
|
@@ -119,18 +133,21 @@ function printPlan(r) {
|
|
|
119
133
|
p.note(lines.join('\n') || 'no changes', 'plan');
|
|
120
134
|
}
|
|
121
135
|
|
|
122
|
-
async function decideUpdate(action, path, yes) {
|
|
136
|
+
async function decideUpdate(action, path, yes, classification) {
|
|
137
|
+
if (yes) return nonInteractiveUpdateDecision(action);
|
|
123
138
|
if (action === 'delete') {
|
|
124
|
-
if (yes) return true;
|
|
125
139
|
return (await p.confirm({ message: `Upstream removed ${path} — delete it locally?` })) === true;
|
|
126
140
|
}
|
|
127
141
|
if (action === 'collision') {
|
|
128
|
-
if (yes) return 'replace';
|
|
129
142
|
const choice = await p.select({
|
|
130
|
-
message: `${path}
|
|
143
|
+
message: `${path} exists without ownership evidence. Choose an explicit route:`,
|
|
131
144
|
options: [
|
|
132
|
-
|
|
133
|
-
|
|
145
|
+
...(classification?.state === 'project-extension'
|
|
146
|
+
? [{ value: 'project-extension', label: 'Keep as declared Project extension' }]
|
|
147
|
+
: []),
|
|
148
|
+
{ value: 'contribution-bridge', label: 'Keep as temporary contribution bridge' },
|
|
149
|
+
{ value: 'explicit-fork', label: 'Keep as explicit fork' },
|
|
150
|
+
{ value: 'replace', label: 'Replace explicitly with Kit Core' },
|
|
134
151
|
],
|
|
135
152
|
});
|
|
136
153
|
if (p.isCancel(choice)) throw new Error(`collision decision cancelled for ${path}`);
|
package/src/commands/init.mjs
CHANGED
|
@@ -9,9 +9,13 @@ import {
|
|
|
9
9
|
filesForInstallRole, CONSUMER_INSTALL_ROLE,
|
|
10
10
|
indexByPath,
|
|
11
11
|
readReadinessContract,
|
|
12
|
+
READINESS_MANIFEST_PATH,
|
|
12
13
|
CONSUMER_ORIGIN,
|
|
13
14
|
PACKAGE_MANIFEST_NAME, CONSUMER_MANIFEST_NAME,
|
|
14
15
|
} from '../lib/manifest.mjs';
|
|
16
|
+
import {
|
|
17
|
+
PROJECT_SKILL_REGISTRY_PATH, emptyProjectSkillRegistry,
|
|
18
|
+
} from '../lib/skillRegistry.mjs';
|
|
15
19
|
import {
|
|
16
20
|
inspectRoutingProfile, reconcileRoutingProfile, setupRoutingProfile,
|
|
17
21
|
} from '../lib/routingProfile.mjs';
|
|
@@ -71,6 +75,26 @@ export async function init({ kitRoot, consumerRoot, force = false, routingProfil
|
|
|
71
75
|
result.copied.push(f.path);
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
if (packageIdx.has(READINESS_MANIFEST_PATH)
|
|
79
|
+
&& !installed.some(({ path }) => path === PROJECT_SKILL_REGISTRY_PATH)) {
|
|
80
|
+
const projectRegistryPath = join(consumerRoot, PROJECT_SKILL_REGISTRY_PATH);
|
|
81
|
+
if (!await exists(projectRegistryPath)) {
|
|
82
|
+
const core = await readManifest(join(kitRoot, READINESS_MANIFEST_PATH));
|
|
83
|
+
await writeAtomic(
|
|
84
|
+
projectRegistryPath,
|
|
85
|
+
`${JSON.stringify(emptyProjectSkillRegistry(core), null, 2)}\n`,
|
|
86
|
+
);
|
|
87
|
+
result.seeded.push(PROJECT_SKILL_REGISTRY_PATH);
|
|
88
|
+
}
|
|
89
|
+
installed.push({
|
|
90
|
+
path: PROJECT_SKILL_REGISTRY_PATH,
|
|
91
|
+
kind: 'doc',
|
|
92
|
+
installedSha256: await sha256File(projectRegistryPath),
|
|
93
|
+
origin: CONSUMER_ORIGIN,
|
|
94
|
+
installRole: CONSUMER_INSTALL_ROLE,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
74
98
|
await writeManifest(
|
|
75
99
|
join(consumerRoot, CONSUMER_MANIFEST_NAME),
|
|
76
100
|
{ ...emptyConsumerManifest(pkg.kitVersion, prior, readiness), installed }
|
package/src/commands/own.mjs
CHANGED
|
@@ -5,12 +5,13 @@ import {
|
|
|
5
5
|
} from '../lib/manifest.mjs';
|
|
6
6
|
|
|
7
7
|
/** Mark one tracked consumer file as kit- or consumer-owned. */
|
|
8
|
-
export async function setOwnership({ consumerRoot, path, origin }) {
|
|
8
|
+
export async function setOwnership({ consumerRoot, path, origin, ownershipState }) {
|
|
9
9
|
const manifestPath = join(consumerRoot, CONSUMER_MANIFEST_NAME);
|
|
10
10
|
const manifest = await readManifest(manifestPath);
|
|
11
11
|
if (!manifest) throw new Error('not initialised — run `init` first');
|
|
12
|
-
const next = withOrigin(manifest, path, origin);
|
|
12
|
+
const next = withOrigin(manifest, path, origin, ownershipState);
|
|
13
13
|
await validateConsumerFile(consumerRoot, path);
|
|
14
14
|
await writeManifest(manifestPath, next);
|
|
15
|
-
return { path, origin
|
|
15
|
+
return { path, origin, ownershipState: origin === 'consumer'
|
|
16
|
+
? (ownershipState ?? 'explicit-fork') : undefined };
|
|
16
17
|
}
|
package/src/commands/update.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '../lib/verifyUpdateCandidate.mjs';
|
|
10
10
|
import { reconcile } from '../lib/updateReconcile.mjs';
|
|
11
11
|
import {
|
|
12
|
-
CONSUMER_MANIFEST_NAME, PACKAGE_MANIFEST_NAME, readManifest,
|
|
12
|
+
CONSUMER_MANIFEST_NAME, PACKAGE_MANIFEST_NAME, PROJECT_SKILL_REGISTRY_PATH, readManifest,
|
|
13
13
|
} from '../lib/manifest.mjs';
|
|
14
14
|
import {
|
|
15
15
|
inspectRoutingProfile, reconcileRoutingProfile,
|
|
@@ -81,11 +81,7 @@ async function updatePackage(options) {
|
|
|
81
81
|
Object.assign(preview, await previewReadinessAdoption({
|
|
82
82
|
kitRoot, consumerRoot, pkg, priorReadinessManifest, nextReadinessManifest,
|
|
83
83
|
}));
|
|
84
|
-
preview.conflicts.push(...(preview.migrationConflicts ?? []).map(
|
|
85
|
-
path,
|
|
86
|
-
kind: 'prod-section',
|
|
87
|
-
diff: 'Prod section differs from the other instruction surface or is malformed.',
|
|
88
|
-
})));
|
|
84
|
+
preview.conflicts.push(...(preview.migrationConflicts ?? []).map(migrationConflictRecord));
|
|
89
85
|
} catch (error) {
|
|
90
86
|
previewFailure = error;
|
|
91
87
|
}
|
|
@@ -104,6 +100,9 @@ async function updatePackage(options) {
|
|
|
104
100
|
const resolvedPreview = await resolvePreview({
|
|
105
101
|
kitRoot, consumerRoot, preview, decisions, decide, transition,
|
|
106
102
|
});
|
|
103
|
+
if (resolvedPreview.collisions.length) {
|
|
104
|
+
return terminal(resolvedPreview, 'conflicted', history, transition);
|
|
105
|
+
}
|
|
107
106
|
if (resolvedPreview.conflicts.length) {
|
|
108
107
|
return terminal(resolvedPreview, 'conflicted', history, transition);
|
|
109
108
|
}
|
|
@@ -129,13 +128,19 @@ async function previewReadinessAdoption(context) {
|
|
|
129
128
|
kitRoot, consumerRoot: candidateRoot,
|
|
130
129
|
decide: (action) => action === 'collision' ? 'keep-as-owned' : false,
|
|
131
130
|
});
|
|
132
|
-
await
|
|
133
|
-
|
|
134
|
-
});
|
|
135
|
-
return await adoptReadinessCandidate({
|
|
136
|
-
candidateRoot, consumerRoot, priorManifest: priorReadinessManifest,
|
|
131
|
+
const readiness = await adoptReadinessCandidate({
|
|
132
|
+
candidateRoot, consumerRoot, kitRoot, priorManifest: priorReadinessManifest,
|
|
137
133
|
nextManifest: nextReadinessManifest,
|
|
138
134
|
});
|
|
135
|
+
candidatePreview.generated = readiness.generated;
|
|
136
|
+
candidatePreview.migrations = readiness.migrations;
|
|
137
|
+
candidatePreview.migrated = readiness.migrated;
|
|
138
|
+
if (!readiness.migrationConflicts.length) {
|
|
139
|
+
await verifyCandidateSchema(candidateRoot, {
|
|
140
|
+
pkg, preview: candidatePreview, priorReadinessManifest, nextReadinessManifest,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return readiness;
|
|
139
144
|
} finally {
|
|
140
145
|
await rm(candidateRoot, { recursive: true, force: true });
|
|
141
146
|
}
|
|
@@ -146,7 +151,13 @@ async function resolvePreview({ kitRoot, consumerRoot, preview, decisions, decid
|
|
|
146
151
|
await transition('awaiting_decision');
|
|
147
152
|
}
|
|
148
153
|
for (const path of preview.collisions) {
|
|
149
|
-
|
|
154
|
+
const classification = preview.ownershipStates?.find(
|
|
155
|
+
(candidate) => candidate.path === path,
|
|
156
|
+
);
|
|
157
|
+
decisions.set(
|
|
158
|
+
decisionKey('collision', path),
|
|
159
|
+
await decide('collision', path, classification),
|
|
160
|
+
);
|
|
150
161
|
}
|
|
151
162
|
if (!preview.collisions.length) return preview;
|
|
152
163
|
return reconcile({
|
|
@@ -192,9 +203,8 @@ async function applyTransaction(context) {
|
|
|
192
203
|
priorReadinessManifest: structuredClone(priorReadinessManifest),
|
|
193
204
|
nextReadinessManifest: structuredClone(nextReadinessManifest),
|
|
194
205
|
};
|
|
195
|
-
await verifyCandidateSchema(candidateRoot, canonicalContext);
|
|
196
206
|
const readiness = await adoptReadinessCandidate({
|
|
197
|
-
candidateRoot, consumerRoot, priorManifest: priorReadinessManifest,
|
|
207
|
+
candidateRoot, consumerRoot, kitRoot, priorManifest: priorReadinessManifest,
|
|
198
208
|
nextManifest: nextReadinessManifest,
|
|
199
209
|
});
|
|
200
210
|
preview.generated = readiness.generated;
|
|
@@ -203,12 +213,13 @@ async function applyTransaction(context) {
|
|
|
203
213
|
preview.migrationConflicts = readiness.migrationConflicts;
|
|
204
214
|
preview.availability = readiness.availability;
|
|
205
215
|
if (readiness.migrationConflicts.length) {
|
|
206
|
-
throw new Error(`
|
|
216
|
+
throw new Error(`readiness migration conflict: ${readiness.migrationConflicts.join(', ')}`);
|
|
207
217
|
}
|
|
208
218
|
if (readiness.incompatible.length) {
|
|
209
219
|
throw new Error(`monotonic compatibility would block existing skill core: ${readiness.incompatible.join(', ')}`);
|
|
210
220
|
}
|
|
211
221
|
canonicalContext.preview = structuredClone(preview);
|
|
222
|
+
await verifyCandidateSchema(candidateRoot, canonicalContext);
|
|
212
223
|
await verifyUpdateCandidate(candidateRoot, canonicalContext);
|
|
213
224
|
if (verify !== verifyUpdateCandidate) {
|
|
214
225
|
const extensionContext = structuredClone(canonicalContext);
|
|
@@ -274,14 +285,68 @@ async function terminal(result, state, history, transition) {
|
|
|
274
285
|
conflicts: result.conflicts.map(({ path }) => path),
|
|
275
286
|
keptDeleted: result.keptDeleted,
|
|
276
287
|
},
|
|
277
|
-
recommendation: result
|
|
278
|
-
? `Prod sections differ or are malformed in: ${result.migrationConflicts.join(', ')}. ` +
|
|
279
|
-
'Resolve them manually; no consumer file was changed.'
|
|
280
|
-
: (result.conflicts.length
|
|
281
|
-
? 'Review each named conflict; keep the local file or apply the incoming diff manually.'
|
|
282
|
-
: null),
|
|
288
|
+
recommendation: updateRecommendation(result),
|
|
283
289
|
},
|
|
284
290
|
};
|
|
285
291
|
}
|
|
286
292
|
|
|
293
|
+
function updateRecommendation(result) {
|
|
294
|
+
if (result.migrationConflicts?.length) {
|
|
295
|
+
return migrationRecommendation(result.migrationConflicts);
|
|
296
|
+
}
|
|
297
|
+
if (result.collisions.length
|
|
298
|
+
|| result.conflicts.some(({ kind }) => kind === 'ownership-lifecycle')) {
|
|
299
|
+
return ownershipRecommendation(result);
|
|
300
|
+
}
|
|
301
|
+
return result.conflicts.length
|
|
302
|
+
? 'Review each named conflict; keep the local file or apply the incoming diff manually.'
|
|
303
|
+
: null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function migrationConflictRecord(detail) {
|
|
307
|
+
const path = detail.split(': ', 1)[0];
|
|
308
|
+
const projectRegistry = path === PROJECT_SKILL_REGISTRY_PATH
|
|
309
|
+
|| path === '.claude/skills/skill-manifest.json';
|
|
310
|
+
return {
|
|
311
|
+
path,
|
|
312
|
+
kind: projectRegistry ? 'skill-registry' : 'prod-section',
|
|
313
|
+
diff: projectRegistry
|
|
314
|
+
? `Skill registry migration is ambiguous: ${detail}`
|
|
315
|
+
: 'Prod section differs from the other instruction surface or is malformed.',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function migrationRecommendation(conflicts) {
|
|
320
|
+
const registryConflict = conflicts.some((detail) => (
|
|
321
|
+
detail.startsWith(`${PROJECT_SKILL_REGISTRY_PATH}:`)
|
|
322
|
+
|| detail.startsWith('.claude/skills/skill-manifest.json:')
|
|
323
|
+
));
|
|
324
|
+
return (registryConflict
|
|
325
|
+
? `Readiness migration is ambiguous in: ${conflicts.join(', ')}. `
|
|
326
|
+
: `Prod sections differ or are malformed in: ${conflicts.join(', ')}. `) +
|
|
327
|
+
'Resolve them manually; no consumer file was changed.';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function ownershipRecommendation(result) {
|
|
331
|
+
const paths = new Set([
|
|
332
|
+
...result.collisions,
|
|
333
|
+
...result.conflicts.filter(({ kind }) => kind === 'ownership-lifecycle')
|
|
334
|
+
.map(({ path }) => path),
|
|
335
|
+
]);
|
|
336
|
+
const unresolved = (result.ownershipStates ?? []).filter(
|
|
337
|
+
({ path }) => paths.has(path),
|
|
338
|
+
);
|
|
339
|
+
return unresolved.map(({ path, state, evidence, routes }) => (
|
|
340
|
+
`${path}: ${state} ` +
|
|
341
|
+
`(package=${evidence.packageDeclared ? 'declared' : 'absent'}, ` +
|
|
342
|
+
`ledger=${evidence.ledgerOrigin}, destination=${evidence.destination}, ` +
|
|
343
|
+
`extension=${evidence.projectExtension}` +
|
|
344
|
+
(evidence.extensionDiagnostic
|
|
345
|
+
? `, extensionDiagnostic=${evidence.extensionDiagnostic}` : '') +
|
|
346
|
+
'); explicit routes: ' +
|
|
347
|
+
routes.map(({ id, action }) => `${id} (${action})`).join(', ')
|
|
348
|
+
)).join('\n') +
|
|
349
|
+
'. No consumer file was changed; --yes cannot choose a route.';
|
|
350
|
+
}
|
|
351
|
+
|
|
287
352
|
export { verifyUpdateCandidate, verifyUpdateCandidate as verifyCandidate } from '../lib/verifyUpdateCandidate.mjs';
|
package/src/lib/bundle.mjs
CHANGED
|
@@ -8,6 +8,7 @@ export const HELPER_FILES = [
|
|
|
8
8
|
// together; the manifest remains the only capability/dependency registry.
|
|
9
9
|
{ path: '.claude/skills/skill-manifest.json', kind: 'doc', mode: 0o644 },
|
|
10
10
|
{ path: 'scripts/readiness.mjs', kind: 'script', mode: 0o755 },
|
|
11
|
+
{ path: 'scripts/project-skill-extension.mjs', kind: 'script', mode: 0o755 },
|
|
11
12
|
{ path: 'src/lib/sentinel.mjs', kind: 'script', mode: 0o644 },
|
|
12
13
|
{ path: 'src/lib/manifest.mjs', kind: 'script', mode: 0o644 },
|
|
13
14
|
{ path: 'src/lib/atomicWrite.mjs', kind: 'script', mode: 0o644 },
|
|
@@ -77,6 +78,10 @@ export const HELPER_FILES = [
|
|
|
77
78
|
{ path: 'src/lib/reportValidator.mjs', kind: 'script', mode: 0o644 },
|
|
78
79
|
// Fail-closed orchestration selector and its host-inventory adapters.
|
|
79
80
|
{ path: 'src/lib/capabilityMatrix.mjs', kind: 'script', mode: 0o644 },
|
|
81
|
+
{ path: 'src/lib/skillRegistry.mjs', kind: 'script', mode: 0o644 },
|
|
82
|
+
{ path: 'src/lib/projectSkillExtension.mjs', kind: 'script', mode: 0o644 },
|
|
83
|
+
{ path: 'src/lib/ownershipClassifier.mjs', kind: 'script', mode: 0o644 },
|
|
84
|
+
{ path: 'src/lib/updateDecisions.mjs', kind: 'script', mode: 0o644 },
|
|
80
85
|
// Provider-neutral routing runtime: one Evidence catalog, Access graph,
|
|
81
86
|
// Routing policy, resolver, spawn guard, receipt v2, and surface attestations.
|
|
82
87
|
// These modules form one consumer unit; omitting one leaves shipped dispatch
|
package/src/lib/manifest.mjs
CHANGED
|
@@ -18,6 +18,7 @@ export const KIT_ORIGIN = 'kit';
|
|
|
18
18
|
export const CONSUMER_ORIGIN = 'consumer';
|
|
19
19
|
export const READINESS_CONTRACT_VERSION = 1;
|
|
20
20
|
export const READINESS_MANIFEST_PATH = '.claude/skills/skill-manifest.json';
|
|
21
|
+
export { PROJECT_SKILL_REGISTRY_PATH } from './skillRegistry.mjs';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Parse a JSON manifest, or null if the file does not exist. A corrupt file
|
|
@@ -94,7 +95,7 @@ export function indexByPath(manifest, key) {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
/** Return a manifest with one tracked entry moved to the requested ownership state. */
|
|
97
|
-
export function withOrigin(manifest, path, origin) {
|
|
98
|
+
export function withOrigin(manifest, path, origin, ownershipState = 'explicit-fork') {
|
|
98
99
|
if (![KIT_ORIGIN, CONSUMER_ORIGIN].includes(origin)) {
|
|
99
100
|
throw new Error(`invalid manifest origin: ${origin}`);
|
|
100
101
|
}
|
|
@@ -105,6 +106,16 @@ export function withOrigin(manifest, path, origin) {
|
|
|
105
106
|
}
|
|
106
107
|
return {
|
|
107
108
|
...manifest,
|
|
108
|
-
installed: manifest.installed.map((entry) =>
|
|
109
|
+
installed: manifest.installed.map((entry) => {
|
|
110
|
+
if (entry.path !== path) return entry;
|
|
111
|
+
if (origin === KIT_ORIGIN) {
|
|
112
|
+
const { ownershipState: _removed, ...core } = entry;
|
|
113
|
+
return { ...core, origin };
|
|
114
|
+
}
|
|
115
|
+
if (!['contribution-bridge', 'explicit-fork'].includes(ownershipState)) {
|
|
116
|
+
throw new Error(`invalid consumer ownership state: ${ownershipState}`);
|
|
117
|
+
}
|
|
118
|
+
return { ...entry, origin, ownershipState };
|
|
119
|
+
}),
|
|
109
120
|
};
|
|
110
121
|
}
|