@ikon85/agent-workflow-kit 0.34.6 → 0.36.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.
Files changed (116) hide show
  1. package/.agents/skills/ask-matt/SKILL.md +8 -0
  2. package/.agents/skills/audit-skills/SKILL.md +8 -0
  3. package/.agents/skills/board-to-waves/SKILL.md +8 -0
  4. package/.agents/skills/census-update/SKILL.md +8 -0
  5. package/.agents/skills/code-review/SKILL.md +8 -0
  6. package/.agents/skills/codebase-design/SKILL.md +8 -0
  7. package/.agents/skills/codex-adapter-sync/SKILL.md +8 -0
  8. package/.agents/skills/decision-gate/SKILL.md +8 -0
  9. package/.agents/skills/diagnose/SKILL.md +8 -0
  10. package/.agents/skills/domain-modeling/SKILL.md +8 -0
  11. package/.agents/skills/git-worktree-recover/SKILL.md +8 -0
  12. package/.agents/skills/grill-me/SKILL.md +8 -0
  13. package/.agents/skills/grill-with-docs/SKILL.md +8 -0
  14. package/.agents/skills/implement/SKILL.md +8 -0
  15. package/.agents/skills/improve-codebase-architecture/SKILL.md +8 -0
  16. package/.agents/skills/kit-release/SKILL.md +8 -0
  17. package/.agents/skills/kit-update/SKILL.md +32 -11
  18. package/.agents/skills/local-ci/SKILL.md +8 -0
  19. package/.agents/skills/memory-lifecycle/SKILL.md +8 -0
  20. package/.agents/skills/orchestrate-wave/SKILL.md +2 -2
  21. package/.agents/skills/project-release/SKILL.md +8 -0
  22. package/.agents/skills/prototype/SKILL.md +8 -0
  23. package/.agents/skills/research/SKILL.md +8 -0
  24. package/.agents/skills/resolving-merge-conflicts/SKILL.md +8 -0
  25. package/.agents/skills/retro/SKILL.md +19 -3
  26. package/.agents/skills/scale-check/SKILL.md +8 -0
  27. package/.agents/skills/security-audit/SKILL.md +8 -0
  28. package/.agents/skills/setup-workflow/SKILL.md +52 -1
  29. package/.agents/skills/setup-workflow/contribution-routing.md +52 -0
  30. package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +1 -0
  31. package/.agents/skills/setup-workflow/spec-self-critique-seed.md +1 -0
  32. package/.agents/skills/spec-self-critique/SKILL.md +8 -0
  33. package/.agents/skills/tdd/SKILL.md +8 -0
  34. package/.agents/skills/to-issues/SKILL.md +8 -0
  35. package/.agents/skills/to-prd/SKILL.md +8 -0
  36. package/.agents/skills/to-waves/SKILL.md +8 -0
  37. package/.agents/skills/triage/SKILL.md +8 -0
  38. package/.agents/skills/verify-spike/SKILL.md +8 -0
  39. package/.agents/skills/wayfinder/SKILL.md +8 -0
  40. package/.agents/skills/wrapup/SKILL.md +8 -0
  41. package/.claude/hooks/kit-origin-edit-hint.py +18 -13
  42. package/.claude/skills/ask-matt/SKILL.md +8 -0
  43. package/.claude/skills/audit-skills/SKILL.md +8 -0
  44. package/.claude/skills/board-to-waves/SKILL.md +8 -0
  45. package/.claude/skills/census-update/SKILL.md +8 -0
  46. package/.claude/skills/code-review/SKILL.md +8 -0
  47. package/.claude/skills/codebase-design/SKILL.md +8 -0
  48. package/.claude/skills/codex-build/SKILL.md +8 -0
  49. package/.claude/skills/codex-review/SKILL.md +8 -0
  50. package/.claude/skills/decision-gate/SKILL.md +8 -0
  51. package/.claude/skills/diagnose/SKILL.md +8 -0
  52. package/.claude/skills/domain-modeling/SKILL.md +8 -0
  53. package/.claude/skills/git-guardrails-claude-code/SKILL.md +8 -0
  54. package/.claude/skills/git-worktree-recover/SKILL.md +8 -0
  55. package/.claude/skills/grill-me/SKILL.md +8 -0
  56. package/.claude/skills/grill-me-codex/SKILL.md +8 -0
  57. package/.claude/skills/grill-with-docs/SKILL.md +8 -0
  58. package/.claude/skills/grill-with-docs-codex/SKILL.md +8 -0
  59. package/.claude/skills/implement/SKILL.md +8 -0
  60. package/.claude/skills/improve-codebase-architecture/SKILL.md +8 -0
  61. package/.claude/skills/kit-release/SKILL.md +8 -0
  62. package/.claude/skills/kit-update/SKILL.md +32 -11
  63. package/.claude/skills/local-ci/SKILL.md +8 -0
  64. package/.claude/skills/memory-lifecycle/SKILL.md +8 -0
  65. package/.claude/skills/orchestrate-wave/SKILL.md +2 -2
  66. package/.claude/skills/project-release/SKILL.md +8 -0
  67. package/.claude/skills/prototype/SKILL.md +8 -0
  68. package/.claude/skills/research/SKILL.md +8 -0
  69. package/.claude/skills/resolving-merge-conflicts/SKILL.md +8 -0
  70. package/.claude/skills/retro/SKILL.md +19 -3
  71. package/.claude/skills/scale-check/SKILL.md +8 -0
  72. package/.claude/skills/security-audit/SKILL.md +8 -0
  73. package/.claude/skills/setup-pre-commit/SKILL.md +8 -0
  74. package/.claude/skills/setup-workflow/SKILL.md +52 -1
  75. package/.claude/skills/setup-workflow/contribution-routing.md +52 -0
  76. package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +1 -0
  77. package/.claude/skills/setup-workflow/spec-self-critique-seed.md +1 -0
  78. package/.claude/skills/spec-self-critique/SKILL.md +8 -0
  79. package/.claude/skills/tdd/SKILL.md +8 -0
  80. package/.claude/skills/to-issues/SKILL.md +8 -0
  81. package/.claude/skills/to-prd/SKILL.md +8 -0
  82. package/.claude/skills/to-waves/SKILL.md +8 -0
  83. package/.claude/skills/triage/SKILL.md +8 -0
  84. package/.claude/skills/verify-spike/SKILL.md +8 -0
  85. package/.claude/skills/wayfinder/SKILL.md +8 -0
  86. package/.claude/skills/wrapup/SKILL.md +8 -0
  87. package/.claude/skills/write-a-skill/SKILL.md +8 -0
  88. package/README.md +172 -5
  89. package/agent-workflow-kit.package.json +149 -89
  90. package/docs/agents/skills/local-ci.md +1 -0
  91. package/docs/agents/skills/orchestrate-wave.md +1 -1
  92. package/docs/agents/skills/spec-self-critique.md +1 -1
  93. package/package.json +1 -1
  94. package/scripts/find-by-marker.py +2 -0
  95. package/scripts/project-skill-extension.mjs +37 -0
  96. package/scripts/readiness.mjs +2 -1
  97. package/scripts/test_marker_lib.py +8 -0
  98. package/scripts/test_retro_wrapup_contract.py +6 -4
  99. package/scripts/test_skill_publish_audit.py +9 -1
  100. package/src/cli.mjs +86 -12
  101. package/src/commands/init.mjs +24 -0
  102. package/src/commands/own.mjs +7 -3
  103. package/src/commands/update.mjs +87 -21
  104. package/src/lib/bundle.mjs +5 -0
  105. package/src/lib/contributionBridge.mjs +176 -0
  106. package/src/lib/contributionRouting.mjs +152 -0
  107. package/src/lib/manifest.mjs +17 -2
  108. package/src/lib/ownershipClassifier.mjs +65 -0
  109. package/src/lib/projectSkillExtension.mjs +60 -0
  110. package/src/lib/skillRegistry.mjs +167 -0
  111. package/src/lib/updateCandidate.mjs +105 -4
  112. package/src/lib/updateDecisions.mjs +5 -0
  113. package/src/lib/updateReconcile.mjs +102 -9
  114. package/src/lib/verifyUpdateCandidate.mjs +28 -1
  115. package/src/lib/verifyUpdateCandidateProtocol.mjs +2 -0
  116. package/src/lib/verifyUpdateCandidateTransaction.mjs +19 -3
@@ -1,4 +1,5 @@
1
1
  <!-- setup-workflow: state=filled -->
2
+ <!-- agent-workflow-kit: project-extension/v1; skill=local-ci -->
2
3
  # Project layer — local-ci
3
4
 
4
5
  This repo is a Node + stdlib-Python repo with no database, no dev server and no
@@ -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.34.6",
3
+ "version": "0.36.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": {
@@ -29,6 +29,8 @@ def _gh(args: list[str]) -> str:
29
29
  except subprocess.TimeoutExpired as exc:
30
30
  raise RuntimeError(
31
31
  f"gh api timed out after {GH_TIMEOUT_SECONDS}s") from exc
32
+ except OSError as exc:
33
+ raise RuntimeError(f"gh CLI unavailable: {exc.strerror or exc}") from exc
32
34
  if result.returncode != 0:
33
35
  raise RuntimeError(result.stderr.strip() or "gh api failed")
34
36
  return result.stdout
@@ -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
+ }
@@ -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 }) {
@@ -149,6 +149,14 @@ class MarkerLookupTest(unittest.TestCase):
149
149
  with self.assertRaisesRegex(RuntimeError, "timed out after 30s"):
150
150
  cli._gh(["api", "--paginate", "repos/example/repo/issues"])
151
151
 
152
+ def test_cli_classifies_a_missing_github_binary(self):
153
+ with patch.object(
154
+ cli.subprocess, "run",
155
+ side_effect=FileNotFoundError(2, "No such file or directory", "gh"),
156
+ ):
157
+ with self.assertRaisesRegex(RuntimeError, "gh CLI unavailable"):
158
+ cli._gh(["api", "--paginate", "repos/example/repo/issues"])
159
+
152
160
 
153
161
  if __name__ == "__main__":
154
162
  unittest.main()
@@ -42,13 +42,15 @@ class RetroEnforcementContract(unittest.TestCase):
42
42
  for phrase in required:
43
43
  self.assertIn(phrase, text)
44
44
 
45
- def test_upstream_or_own_and_exact_sanitized_approval_survive(self):
45
+ def test_capability_route_and_exact_sanitized_approval_survive(self):
46
46
  required = (
47
47
  "generic or project-specific",
48
48
  "recommend `own`",
49
- "sanitized preview",
50
- "explicitly approves that exact text",
51
- "does not need to be the kit maintainer",
49
+ "contribute status <path> --surface=retro",
50
+ "Missing, disabled, invalid, or unverifiable configuration",
51
+ "never infer maintainer status",
52
+ "sanitized exact preview",
53
+ "separate explicit approval",
52
54
  "docs/agents/skills/<skill>.md",
53
55
  )
54
56
  for surface in SURFACES:
@@ -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/[a-z0-9-]+\.mjs['\"];\s*$"
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
@@ -7,7 +7,14 @@ import { renderUpdateFailure, update } from './commands/update.mjs';
7
7
  import { diff } from './commands/diff.mjs';
8
8
  import { uninstall } from './commands/uninstall.mjs';
9
9
  import { setOwnership } from './commands/own.mjs';
10
+ import {
11
+ beginContributionBridge, prepareContributionArtifact,
12
+ } from './lib/contributionBridge.mjs';
13
+ import {
14
+ inspectContributionRouting,
15
+ } from './lib/contributionRouting.mjs';
10
16
  import { CONSUMER_ORIGIN, KIT_ORIGIN } from './lib/manifest.mjs';
17
+ import { nonInteractiveUpdateDecision } from './lib/updateDecisions.mjs';
11
18
  import { currentAgentSurface } from './lib/agentSurfaceRegistry.mjs';
12
19
  import { createCommandAdapter } from '../scripts/release-state.mjs';
13
20
  import { installedIdentityFromDir } from '../scripts/release-parity.mjs';
@@ -25,6 +32,7 @@ const cmd = args[0];
25
32
  const force = args.includes('--force');
26
33
  const yes = args.includes('--yes') || args.includes('-y');
27
34
  const owned = args.includes('--owned');
35
+ const ownershipState = args.find((arg) => arg.startsWith('--as='))?.slice('--as='.length);
28
36
 
29
37
  p.intro('agent-workflow-kit');
30
38
 
@@ -49,7 +57,9 @@ try {
49
57
  printPlan(r);
50
58
  p.outro('Dry run — nothing written. Run `update` to apply.');
51
59
  } else if (cmd === 'update') {
52
- const decide = (action, path) => decideUpdate(action, path, yes);
60
+ const decide = (action, path, classification) => (
61
+ decideUpdate(action, path, yes, classification)
62
+ );
53
63
  const releaseIdentities = await readUpdateRelease();
54
64
  const r = await update({
55
65
  kitRoot: KIT_ROOT,
@@ -65,7 +75,10 @@ try {
65
75
  if (r.state === 'failed') throw new Error(renderUpdateFailure(r));
66
76
  if (r.state === 'conflicted') {
67
77
  p.note(r.report.recommendation, 'recommendation');
68
- p.outro(`not applied · conflicts ${r.conflicts.length}`);
78
+ p.outro(
79
+ `not applied · conflicts ${r.conflicts.length} · ` +
80
+ `ownership collisions ${r.collisions.length}`,
81
+ );
69
82
  process.exitCode = 2;
70
83
  } else if (r.status === 'current') {
71
84
  p.outro(`aktuell · unchanged ${r.unchanged.length} · local modifications ${r.userModified.length}`);
@@ -77,13 +90,70 @@ try {
77
90
  if (!ok) { p.cancel('Aborted.'); process.exit(0); }
78
91
  const r = await uninstall({ consumerRoot });
79
92
  p.outro(`removed ${r.removed.length} · retained (edited/referenced) ${r.retained.length}`);
93
+ } else if (cmd === 'contribute') {
94
+ const action = args[1];
95
+ const path = args[2];
96
+ if (!['start', 'status', 'prepare'].includes(action) || !path) {
97
+ throw new Error(
98
+ 'Usage: agent-workflow-kit contribute <start|status|prepare> <path> ' +
99
+ '[--surface=retro|pre-update|guard] ' +
100
+ '[--output=.agent-workflow-kit/contributions/<name>.json]',
101
+ );
102
+ }
103
+ if (action === 'start') {
104
+ await beginContributionBridge({ kitRoot: KIT_ROOT, consumerRoot, path });
105
+ p.outro(`${path} is now a contribution bridge; no remote was changed`);
106
+ } else if (action === 'status') {
107
+ const surface = args.find((arg) => arg.startsWith('--surface='))
108
+ ?.slice('--surface='.length) ?? 'guard';
109
+ const routing = await inspectContributionRouting({
110
+ consumerRoot, path, surface,
111
+ });
112
+ p.note(
113
+ [
114
+ `lifecycle: ${routing.lifecycleState}`,
115
+ `capability: ${routing.capabilityState}`,
116
+ `routes: ${routing.routes.map(({ id }) => id).join(', ')}`,
117
+ ...(routing.diagnostic ? [`diagnostic: ${routing.diagnostic}`] : []),
118
+ ].join('\n'),
119
+ `contribution routing (${routing.surface})`,
120
+ );
121
+ p.outro('read-only routing report; no local or remote state changed');
122
+ } else {
123
+ const output = args.find((arg) => arg.startsWith('--output='))?.slice('--output='.length);
124
+ if (!output) throw new Error('contribute prepare requires --output=<path>');
125
+ const routing = await inspectContributionRouting({
126
+ consumerRoot, path, surface: 'guard',
127
+ });
128
+ if (!routing.routes.some(({ id }) => id === 'prepare-local')) {
129
+ throw new Error(
130
+ `local contribution preparation is unavailable: ${routing.capabilityState}` +
131
+ (routing.diagnostic ? ` (${routing.diagnostic})` : ''),
132
+ );
133
+ }
134
+ await prepareContributionArtifact({
135
+ kitRoot: KIT_ROOT, consumerRoot, path, output,
136
+ });
137
+ p.outro(`prepared local contribution artifact ${output}; no remote was changed`);
138
+ }
80
139
  } else if (cmd === 'own' || cmd === 'disown') {
81
- if (!args[1]) throw new Error(`Usage: agent-workflow-kit ${cmd} <path>`);
140
+ if (!args[1]) {
141
+ throw new Error(
142
+ `Usage: agent-workflow-kit ${cmd} <path>` +
143
+ (cmd === 'own' ? ' [--as=explicit-fork]' : ''),
144
+ );
145
+ }
82
146
  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`);
147
+ if (origin === CONSUMER_ORIGIN && ownershipState === 'contribution-bridge') {
148
+ await beginContributionBridge({ kitRoot: KIT_ROOT, consumerRoot, path: args[1] });
149
+ } else {
150
+ await setOwnership({ consumerRoot, path: args[1], origin, ownershipState });
151
+ }
152
+ p.outro(`${args[1]} is now ${origin}-owned` +
153
+ (origin === CONSUMER_ORIGIN ? ` (${ownershipState ?? 'explicit-fork'})` : ''));
85
154
  } else {
86
- p.note('Usage: agent-workflow-kit <init|update|diff|uninstall|own|disown> [<path>] [--force] [--yes] [--owned]');
155
+ p.note('Usage: agent-workflow-kit <init|update|diff|uninstall|own|disown|contribute> ' +
156
+ '[<path>] [--force] [--yes] [--owned] [--as=contribution-bridge|explicit-fork]');
87
157
  p.outro('');
88
158
  }
89
159
  } catch (err) {
@@ -96,6 +166,7 @@ function printPlan(r) {
96
166
  for (const k of [
97
167
  'added', 'updated', 'userModified', 'consumerOwned', 'unchanged',
98
168
  'deleted', 'keptDeleted', 'collisions', 'migrated',
169
+ 'bridgeRetired',
99
170
  ])
100
171
  if (r[k]?.length) lines.push(`${k}: ${r[k].length}`);
101
172
  if (r.conflicts?.length) lines.push(`conflicts: ${r.conflicts.length}`);
@@ -119,18 +190,21 @@ function printPlan(r) {
119
190
  p.note(lines.join('\n') || 'no changes', 'plan');
120
191
  }
121
192
 
122
- async function decideUpdate(action, path, yes) {
193
+ async function decideUpdate(action, path, yes, classification) {
194
+ if (yes) return nonInteractiveUpdateDecision(action);
123
195
  if (action === 'delete') {
124
- if (yes) return true;
125
196
  return (await p.confirm({ message: `Upstream removed ${path} — delete it locally?` })) === true;
126
197
  }
127
198
  if (action === 'collision') {
128
- if (yes) return 'replace';
129
199
  const choice = await p.select({
130
- message: `${path} already exists but is not tracked. Choose its ownership:`,
200
+ message: `${path} exists without ownership evidence. Choose an explicit route:`,
131
201
  options: [
132
- { value: 'keep-as-owned', label: 'Keep existing file as consumer-owned' },
133
- { value: 'replace', label: 'Replace with kit file' },
202
+ ...(classification?.state === 'project-extension'
203
+ ? [{ value: 'project-extension', label: 'Keep as declared Project extension' }]
204
+ : []),
205
+ { value: 'contribution-bridge', label: 'Keep as temporary contribution bridge' },
206
+ { value: 'explicit-fork', label: 'Keep as explicit fork' },
207
+ { value: 'replace', label: 'Replace explicitly with Kit Core' },
134
208
  ],
135
209
  });
136
210
  if (p.isCancel(choice)) throw new Error(`collision decision cancelled for ${path}`);
@@ -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 }
@@ -5,12 +5,16 @@ 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
+ if (origin === 'consumer' && ownershipState === 'contribution-bridge') {
10
+ throw new Error('contribution bridge requires `contribute start` with Kit provenance');
11
+ }
9
12
  const manifestPath = join(consumerRoot, CONSUMER_MANIFEST_NAME);
10
13
  const manifest = await readManifest(manifestPath);
11
14
  if (!manifest) throw new Error('not initialised — run `init` first');
12
- const next = withOrigin(manifest, path, origin);
15
+ const next = withOrigin(manifest, path, origin, ownershipState);
13
16
  await validateConsumerFile(consumerRoot, path);
14
17
  await writeManifest(manifestPath, next);
15
- return { path, origin };
18
+ return { path, origin, ownershipState: origin === 'consumer'
19
+ ? (ownershipState ?? 'explicit-fork') : undefined };
16
20
  }
@@ -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((path) => ({
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 verifyCandidateSchema(candidateRoot, {
133
- pkg, preview: candidatePreview, priorReadinessManifest, nextReadinessManifest,
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
- decisions.set(decisionKey('collision', path), await decide('collision', path));
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(`Prod section migration conflict: ${readiness.migrationConflicts.join(', ')}`);
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);
@@ -249,6 +260,7 @@ function verifyRelease(identities, kitVersion) {
249
260
  function hasUpstreamDelta(result) {
250
261
  return result.manifestChanged ||
251
262
  (result.migrations?.length ?? 0) > 0 ||
263
+ (result.bridgeRetired?.length ?? 0) > 0 ||
252
264
  result.added.length + result.updated.length + result.deleted.length > 0;
253
265
  }
254
266
 
@@ -274,14 +286,68 @@ async function terminal(result, state, history, transition) {
274
286
  conflicts: result.conflicts.map(({ path }) => path),
275
287
  keptDeleted: result.keptDeleted,
276
288
  },
277
- recommendation: result.migrationConflicts?.length
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),
289
+ recommendation: updateRecommendation(result),
283
290
  },
284
291
  };
285
292
  }
286
293
 
294
+ function updateRecommendation(result) {
295
+ if (result.migrationConflicts?.length) {
296
+ return migrationRecommendation(result.migrationConflicts);
297
+ }
298
+ if (result.collisions.length
299
+ || result.conflicts.some(({ kind }) => kind === 'ownership-lifecycle')) {
300
+ return ownershipRecommendation(result);
301
+ }
302
+ return result.conflicts.length
303
+ ? 'Review each named conflict; keep the local file or apply the incoming diff manually.'
304
+ : null;
305
+ }
306
+
307
+ function migrationConflictRecord(detail) {
308
+ const path = detail.split(': ', 1)[0];
309
+ const projectRegistry = path === PROJECT_SKILL_REGISTRY_PATH
310
+ || path === '.claude/skills/skill-manifest.json';
311
+ return {
312
+ path,
313
+ kind: projectRegistry ? 'skill-registry' : 'prod-section',
314
+ diff: projectRegistry
315
+ ? `Skill registry migration is ambiguous: ${detail}`
316
+ : 'Prod section differs from the other instruction surface or is malformed.',
317
+ };
318
+ }
319
+
320
+ function migrationRecommendation(conflicts) {
321
+ const registryConflict = conflicts.some((detail) => (
322
+ detail.startsWith(`${PROJECT_SKILL_REGISTRY_PATH}:`)
323
+ || detail.startsWith('.claude/skills/skill-manifest.json:')
324
+ ));
325
+ return (registryConflict
326
+ ? `Readiness migration is ambiguous in: ${conflicts.join(', ')}. `
327
+ : `Prod sections differ or are malformed in: ${conflicts.join(', ')}. `) +
328
+ 'Resolve them manually; no consumer file was changed.';
329
+ }
330
+
331
+ function ownershipRecommendation(result) {
332
+ const paths = new Set([
333
+ ...result.collisions,
334
+ ...result.conflicts.filter(({ kind }) => kind === 'ownership-lifecycle')
335
+ .map(({ path }) => path),
336
+ ]);
337
+ const unresolved = (result.ownershipStates ?? []).filter(
338
+ ({ path }) => paths.has(path),
339
+ );
340
+ return unresolved.map(({ path, state, evidence, routes }) => (
341
+ `${path}: ${state} ` +
342
+ `(package=${evidence.packageDeclared ? 'declared' : 'absent'}, ` +
343
+ `ledger=${evidence.ledgerOrigin}, destination=${evidence.destination}, ` +
344
+ `extension=${evidence.projectExtension}` +
345
+ (evidence.extensionDiagnostic
346
+ ? `, extensionDiagnostic=${evidence.extensionDiagnostic}` : '') +
347
+ '); explicit routes: ' +
348
+ routes.map(({ id, action }) => `${id} (${action})`).join(', ')
349
+ )).join('\n') +
350
+ '. No consumer file was changed; --yes cannot choose a route.';
351
+ }
352
+
287
353
  export { verifyUpdateCandidate, verifyUpdateCandidate as verifyCandidate } from '../lib/verifyUpdateCandidate.mjs';
@@ -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