@ikon85/agent-workflow-kit 0.34.6 → 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.
Files changed (108) 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 +18 -4
  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 +8 -0
  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 +30 -0
  29. package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +1 -0
  30. package/.agents/skills/setup-workflow/spec-self-critique-seed.md +1 -0
  31. package/.agents/skills/spec-self-critique/SKILL.md +8 -0
  32. package/.agents/skills/tdd/SKILL.md +8 -0
  33. package/.agents/skills/to-issues/SKILL.md +8 -0
  34. package/.agents/skills/to-prd/SKILL.md +8 -0
  35. package/.agents/skills/to-waves/SKILL.md +8 -0
  36. package/.agents/skills/triage/SKILL.md +8 -0
  37. package/.agents/skills/verify-spike/SKILL.md +8 -0
  38. package/.agents/skills/wayfinder/SKILL.md +8 -0
  39. package/.agents/skills/wrapup/SKILL.md +8 -0
  40. package/.claude/skills/ask-matt/SKILL.md +8 -0
  41. package/.claude/skills/audit-skills/SKILL.md +8 -0
  42. package/.claude/skills/board-to-waves/SKILL.md +8 -0
  43. package/.claude/skills/census-update/SKILL.md +8 -0
  44. package/.claude/skills/code-review/SKILL.md +8 -0
  45. package/.claude/skills/codebase-design/SKILL.md +8 -0
  46. package/.claude/skills/codex-build/SKILL.md +8 -0
  47. package/.claude/skills/codex-review/SKILL.md +8 -0
  48. package/.claude/skills/decision-gate/SKILL.md +8 -0
  49. package/.claude/skills/diagnose/SKILL.md +8 -0
  50. package/.claude/skills/domain-modeling/SKILL.md +8 -0
  51. package/.claude/skills/git-guardrails-claude-code/SKILL.md +8 -0
  52. package/.claude/skills/git-worktree-recover/SKILL.md +8 -0
  53. package/.claude/skills/grill-me/SKILL.md +8 -0
  54. package/.claude/skills/grill-me-codex/SKILL.md +8 -0
  55. package/.claude/skills/grill-with-docs/SKILL.md +8 -0
  56. package/.claude/skills/grill-with-docs-codex/SKILL.md +8 -0
  57. package/.claude/skills/implement/SKILL.md +8 -0
  58. package/.claude/skills/improve-codebase-architecture/SKILL.md +8 -0
  59. package/.claude/skills/kit-release/SKILL.md +8 -0
  60. package/.claude/skills/kit-update/SKILL.md +18 -4
  61. package/.claude/skills/local-ci/SKILL.md +8 -0
  62. package/.claude/skills/memory-lifecycle/SKILL.md +8 -0
  63. package/.claude/skills/orchestrate-wave/SKILL.md +2 -2
  64. package/.claude/skills/project-release/SKILL.md +8 -0
  65. package/.claude/skills/prototype/SKILL.md +8 -0
  66. package/.claude/skills/research/SKILL.md +8 -0
  67. package/.claude/skills/resolving-merge-conflicts/SKILL.md +8 -0
  68. package/.claude/skills/retro/SKILL.md +8 -0
  69. package/.claude/skills/scale-check/SKILL.md +8 -0
  70. package/.claude/skills/security-audit/SKILL.md +8 -0
  71. package/.claude/skills/setup-pre-commit/SKILL.md +8 -0
  72. package/.claude/skills/setup-workflow/SKILL.md +30 -0
  73. package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +1 -0
  74. package/.claude/skills/setup-workflow/spec-self-critique-seed.md +1 -0
  75. package/.claude/skills/spec-self-critique/SKILL.md +8 -0
  76. package/.claude/skills/tdd/SKILL.md +8 -0
  77. package/.claude/skills/to-issues/SKILL.md +8 -0
  78. package/.claude/skills/to-prd/SKILL.md +8 -0
  79. package/.claude/skills/to-waves/SKILL.md +8 -0
  80. package/.claude/skills/triage/SKILL.md +8 -0
  81. package/.claude/skills/verify-spike/SKILL.md +8 -0
  82. package/.claude/skills/wayfinder/SKILL.md +8 -0
  83. package/.claude/skills/wrapup/SKILL.md +8 -0
  84. package/.claude/skills/write-a-skill/SKILL.md +8 -0
  85. package/README.md +118 -4
  86. package/agent-workflow-kit.package.json +127 -87
  87. package/docs/agents/skills/local-ci.md +1 -0
  88. package/docs/agents/skills/orchestrate-wave.md +1 -1
  89. package/docs/agents/skills/spec-self-critique.md +1 -1
  90. package/package.json +1 -1
  91. package/scripts/project-skill-extension.mjs +37 -0
  92. package/scripts/readiness.mjs +2 -1
  93. package/scripts/test_skill_publish_audit.py +9 -1
  94. package/src/cli.mjs +29 -12
  95. package/src/commands/init.mjs +24 -0
  96. package/src/commands/own.mjs +4 -3
  97. package/src/commands/update.mjs +86 -21
  98. package/src/lib/bundle.mjs +5 -0
  99. package/src/lib/manifest.mjs +13 -2
  100. package/src/lib/ownershipClassifier.mjs +54 -0
  101. package/src/lib/projectSkillExtension.mjs +60 -0
  102. package/src/lib/skillRegistry.mjs +167 -0
  103. package/src/lib/updateCandidate.mjs +105 -4
  104. package/src/lib/updateDecisions.mjs +5 -0
  105. package/src/lib/updateReconcile.mjs +64 -8
  106. package/src/lib/verifyUpdateCandidate.mjs +13 -1
  107. package/src/lib/verifyUpdateCandidateProtocol.mjs +2 -0
  108. package/src/lib/verifyUpdateCandidateTransaction.mjs +9 -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.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": {
@@ -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 }) {
@@ -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
@@ -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) => decideUpdate(action, path, yes);
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(`not applied · conflicts ${r.conflicts.length}`);
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]) throw new Error(`Usage: agent-workflow-kit ${cmd} <path>`);
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> [<path>] [--force] [--yes] [--owned]');
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} already exists but is not tracked. Choose its ownership:`,
143
+ message: `${path} exists without ownership evidence. Choose an explicit route:`,
131
144
  options: [
132
- { value: 'keep-as-owned', label: 'Keep existing file as consumer-owned' },
133
- { value: 'replace', label: 'Replace with kit file' },
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}`);
@@ -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,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
  }
@@ -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);
@@ -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.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),
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';
@@ -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
@@ -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) => entry.path === path ? { ...entry, origin } : 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
  }
@@ -0,0 +1,54 @@
1
+ export const OwnershipState = Object.freeze({
2
+ CLEAN_CORE: 'clean-core',
3
+ PROJECT_EXTENSION: 'project-extension',
4
+ CONTRIBUTION_BRIDGE: 'contribution-bridge',
5
+ EXPLICIT_FORK: 'explicit-fork',
6
+ AMBIGUOUS_COLLISION: 'ambiguous-collision',
7
+ });
8
+
9
+ const ROUTES = Object.freeze([
10
+ { id: 'project-extension', action: 'move Project data to docs/agents/skills/<skill>.md' },
11
+ { id: 'contribution-bridge', action: 'register a temporary contribution-bridge' },
12
+ { id: 'explicit-fork', action: 'register an explicit-fork with its own update line' },
13
+ { id: 'clean-core', action: 'explicitly replace the destination with Kit Core' },
14
+ ]);
15
+
16
+ export function classifyOwnershipEvidence({
17
+ path, packageEntry, installedEntry, destinationPresent, projectExtension,
18
+ }) {
19
+ const evidence = {
20
+ packageDeclared: Boolean(packageEntry),
21
+ ledgerOrigin: installedEntry?.origin ?? 'absent',
22
+ destination: destinationPresent ? 'present' : 'absent',
23
+ projectExtension: projectExtension
24
+ ? (projectExtension.invalid ? 'invalid' : `schema-v${projectExtension.schemaVersion}`)
25
+ : 'absent',
26
+ };
27
+ if (projectExtension?.invalid) evidence.extensionDiagnostic = projectExtension.invalid;
28
+ if (installedEntry?.origin === 'kit') {
29
+ return { path, state: OwnershipState.CLEAN_CORE, evidence, routes: [] };
30
+ }
31
+ if (installedEntry?.origin === 'consumer') {
32
+ const state = installedEntry.ownershipState ?? OwnershipState.EXPLICIT_FORK;
33
+ if (![
34
+ OwnershipState.PROJECT_EXTENSION,
35
+ OwnershipState.CONTRIBUTION_BRIDGE,
36
+ OwnershipState.EXPLICIT_FORK,
37
+ ].includes(state)) {
38
+ return { path, state: OwnershipState.AMBIGUOUS_COLLISION, evidence, routes: [...ROUTES] };
39
+ }
40
+ if (state === OwnershipState.PROJECT_EXTENSION
41
+ && (!/^docs\/agents\/skills\/[a-z0-9-]+\.md$/.test(path)
42
+ || !projectExtension || projectExtension.invalid)) {
43
+ return { path, state: OwnershipState.AMBIGUOUS_COLLISION, evidence, routes: [...ROUTES] };
44
+ }
45
+ return { path, state, evidence, routes: [] };
46
+ }
47
+ if (projectExtension && !projectExtension.invalid) {
48
+ return { path, state: OwnershipState.PROJECT_EXTENSION, evidence, routes: [
49
+ { id: 'project-extension', action: 'keep the declared Project extension' },
50
+ { id: 'clean-core', action: 'explicitly replace the destination with Kit Core' },
51
+ ] };
52
+ }
53
+ return { path, state: OwnershipState.AMBIGUOUS_COLLISION, evidence, routes: [...ROUTES] };
54
+ }
@@ -0,0 +1,60 @@
1
+ import { lstat, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { firstLineState } from './sentinel.mjs';
4
+
5
+ const SKILL_NAME = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
6
+ const MARKER_PREFIX = '<!-- agent-workflow-kit: project-extension/';
7
+ const MARKER = /^<!-- agent-workflow-kit: project-extension\/([^;]+); skill=([a-z0-9-]+) -->$/;
8
+
9
+ export function projectSkillExtensionPath(skill) {
10
+ if (typeof skill !== 'string' || !SKILL_NAME.test(skill)) {
11
+ throw new Error(`invalid Project extension skill identity: ${skill}`);
12
+ }
13
+ return `docs/agents/skills/${skill}.md`;
14
+ }
15
+
16
+ export async function inspectProjectSkillExtension({ root, skill }) {
17
+ const path = projectSkillExtensionPath(skill);
18
+ const absolute = join(root, path);
19
+ let state;
20
+ try {
21
+ state = await lstat(absolute);
22
+ } catch (error) {
23
+ if (error.code === 'ENOENT') return { state: 'inactive', reason: 'absent' };
24
+ throw error;
25
+ }
26
+ if (!state.isFile()) {
27
+ throw new Error(`Project extension is not a regular file: ${path}`);
28
+ }
29
+ const body = await readFile(absolute, 'utf8');
30
+ const setupState = firstLineState(body);
31
+ if (setupState === 'stub' || setupState === 'not-applicable') {
32
+ return { state: 'inactive', reason: setupState };
33
+ }
34
+ const lines = body.split('\n').map((line) => line.trim());
35
+ const markerLine = lines.find((line) => line.startsWith(MARKER_PREFIX));
36
+ if (!markerLine) {
37
+ if (!body.trim()) return { state: 'inactive', reason: 'empty' };
38
+ return { state: 'active', schemaVersion: 0, path };
39
+ }
40
+ const match = MARKER.exec(markerLine);
41
+ if (!match || match[1] !== 'v1') {
42
+ throw new Error(
43
+ `Project extension has unsupported schema at ${path}; ` +
44
+ `expected project-extension/v1 for skill=${skill}`,
45
+ );
46
+ }
47
+ if (match[2] !== skill) {
48
+ throw new Error(
49
+ `Project extension identity mismatch at ${path}; expected skill=${skill}, ` +
50
+ `found skill=${match[2]}`,
51
+ );
52
+ }
53
+ const content = lines.filter(
54
+ (line) => line && line !== markerLine && !line.startsWith('<!-- setup-workflow:'),
55
+ );
56
+ if (!content.length) {
57
+ throw new Error(`Project extension has no instructions after its v1 marker: ${path}`);
58
+ }
59
+ return { state: 'active', schemaVersion: 1, path };
60
+ }