ultra-builder-pro-cli 0.7.0 → 0.9.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 (47) hide show
  1. package/CHANGELOG.md +60 -1
  2. package/README.md +49 -23
  3. package/adapters/_shared/codex-assets.cjs +28 -0
  4. package/adapters/_shared/handbook.cjs +9 -1
  5. package/adapters/_shared/path-rewrite.cjs +3 -0
  6. package/adapters/_shared/provenance.cjs +286 -0
  7. package/adapters/_shared/runtime-assets.cjs +2 -1
  8. package/adapters/_shared/tests/handbook.test.cjs +6 -1
  9. package/adapters/_shared/tests/path-rewrite.test.cjs +3 -1
  10. package/adapters/_shared/tests/provenance.test.cjs +133 -0
  11. package/adapters/_shared/tests/runtime-assets.test.cjs +4 -1
  12. package/adapters/claude.js +33 -0
  13. package/adapters/codex.js +87 -0
  14. package/adapters/kimi.js +642 -0
  15. package/adapters/opencode.js +66 -0
  16. package/adapters/tests/codex.test.cjs +37 -0
  17. package/adapters/tests/kimi-hook.test.cjs +122 -0
  18. package/adapters/tests/kimi.test.cjs +255 -0
  19. package/agents/debugger.md +12 -0
  20. package/bin/handbook.js +2 -2
  21. package/bin/install.js +72 -12
  22. package/bin/orchestrator.js +1 -1
  23. package/commands/ultra-change.md +3 -2
  24. package/docs/AGENT-CONTEXT.md +5 -3
  25. package/docs/ARCHITECTURE.md +40 -6
  26. package/docs/PLAN.zh-CN.md +8 -6
  27. package/docs/ROADMAP.md +4 -4
  28. package/docs/RUNTIME-COMPAT-MATRIX.md +109 -58
  29. package/docs/USER-HANDBOOK-CONTRACT.md +4 -1
  30. package/hooks/README.md +6 -2
  31. package/hooks/adapters/kimi.py +162 -0
  32. package/hooks/workflow_checkpoint.py +15 -1
  33. package/hooks/workflow_resume.py +96 -7
  34. package/mcp-server/lib/change-workflow.cjs +69 -0
  35. package/mcp-server/lib/change-workflow.test.cjs +87 -0
  36. package/mcp-server/lib/state-db.cjs +85 -2
  37. package/mcp-server/lib/state-db.test.cjs +56 -0
  38. package/mcp-server/tests/init-project.test.cjs +2 -2
  39. package/mcp-server/tests/state-db.test.cjs +3 -3
  40. package/package.json +4 -2
  41. package/skills/ultra-change/SKILL.md +12 -3
  42. package/skills/ultra-deliver/SKILL.md +3 -1
  43. package/skills/ultra-doctor/SKILL.md +10 -3
  44. package/skills/ultra-review/SKILL.md +3 -3
  45. package/spec/mcp-tools.yaml +5 -5
  46. package/spec/schemas/skill-manifest.schema.json +2 -2
  47. package/spec/schemas/state-db.sql +4 -2
package/CHANGELOG.md CHANGED
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.0] — 2026-07-17
11
+
12
+ ### Added
13
+
14
+ - Added Kimi Code 0.26+ as a fourth native plugin host with namespaced commands,
15
+ twenty allowlisted skills including its session bootstrap, lifecycle hooks,
16
+ bundled review-worker prompt templates, native plugin registration, and
17
+ read-only install diagnostics.
18
+ - Added packed-package and live MCP conformance coverage for the Kimi-managed
19
+ plugin layout while preserving unrelated registry entries and `config.toml`.
20
+
21
+ ### Changed
22
+
23
+ - Advanced `.ultra/state.db` to schema `9.1` so Kimi is a valid runtime for
24
+ events and sessions. Existing databases rebuild only the constrained tables
25
+ transactionally while preserving rows, foreign keys, telemetry, incidents,
26
+ and migration history.
27
+ - Extended CLI, handbook, orchestrator runtime selection, package metadata, and
28
+ runtime compatibility documentation to include Kimi without reintroducing
29
+ Ultra-owned memory, graph, browser, deployment, or framework capabilities.
30
+
31
+ ### Fixed
32
+
33
+ - Made the Kimi MCP launcher recover the active project from the inherited
34
+ working-directory contract even though Kimi starts plugin MCP processes from
35
+ the managed plugin root. It fails closed instead of creating `.ultra` inside
36
+ the installed plugin.
37
+ - Avoided Kimi's embedded Node native-module ABI mismatch by launching the
38
+ bundled MCP with the installer-compatible Node executable from `PATH`.
39
+
40
+ ## [0.8.1] — 2026-07-17
41
+
42
+ ### Fixed
43
+
44
+ - Removed a retired-surface name accidentally reintroduced in the D53 decision
45
+ note. The unchanged retirement contract caught it before `v0.8.0` reached npm;
46
+ this immutable patch release carries the complete harness closure forward.
47
+
48
+ ## [0.8.0] — 2026-07-17
49
+
50
+ ### Added
51
+
52
+ - Added a first-class incident debug lane: incident changes now create a
53
+ registered `diagnosis.md` artifact whose reproduction, hypotheses, root
54
+ cause, regression-test, and recovery sections are required by convergence.
55
+ - Added normalized install-provenance manifests for Claude Code, OpenCode, and
56
+ Codex plus read-only `ubp --doctor [--json]` checks for asset hashes and
57
+ host-specific plugin, hook, MCP, launcher, and runtime entry points.
58
+
59
+ ### Fixed
60
+
61
+ - Turned workflow compaction checkpoints into a real recovery consumer:
62
+ resume now rejects malformed or terminal checkpoints, selects the newest
63
+ valid live/checkpoint state, and atomically restores missing or older live
64
+ workflow state before re-injecting context.
65
+
10
66
  ## [0.7.0] — 2026-07-17
11
67
 
12
68
  ### Added
@@ -306,7 +362,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
306
362
  skill manifest, CLI protocol + mapping table; 5 spec validators.
307
363
  - **Phase 0 — skeleton**: multi-runtime installer scaffolding.
308
364
 
309
- [Unreleased]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.7.0...HEAD
365
+ [Unreleased]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.9.0...HEAD
366
+ [0.9.0]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.8.1...v0.9.0
367
+ [0.8.1]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.8.0...v0.8.1
368
+ [0.8.0]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.7.0...v0.8.0
310
369
  [0.7.0]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.6.0...v0.7.0
311
370
  [0.6.0]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.5.3...v0.6.0
312
371
  [0.5.3]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.5.2...v0.5.3
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  Multi-runtime plugin suite + autonomous coding factory for the Ultra Builder Pro
4
4
  agent engineering system. It ships native plugins for **Claude Code · OpenCode ·
5
- Codex** and orchestrates PRD →
5
+ Codex · Kimi Code** and orchestrates PRD →
6
6
  dependency graph → parallel session execution → auto-merge with a single
7
7
  authoritative `.ultra/state.db`.
8
8
 
9
9
  <div align="center">
10
10
 
11
- [![Version](https://img.shields.io/badge/version-0.7.0-blue)](./CHANGELOG.md)
11
+ [![Version](https://img.shields.io/badge/version-0.9.0-blue)](./CHANGELOG.md)
12
12
  [![Tests](https://img.shields.io/badge/tests-passing-brightgreen)](#verification)
13
13
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
14
14
  [![Node](https://img.shields.io/badge/node-%E2%89%A522-informational)](./package.json)
@@ -21,9 +21,11 @@ authoritative `.ultra/state.db`.
21
21
 
22
22
  ## What it does
23
23
 
24
- - **Builds host-native plugins from one allowlist.** Claude Code, OpenCode, and
25
- Codex receive their own command/skill, agent, hook, and MCP representation.
26
- Install and uninstall are symmetric on every supported host.
24
+ - **Builds host-native plugins from one allowlist.** Claude Code, OpenCode,
25
+ Codex, and Kimi Code receive their own command/skill, agent, hook, and MCP
26
+ representation.
27
+ Install and uninstall are symmetric on every supported host; `ubp --doctor`
28
+ verifies the installed asset hashes and host entry points without mutation.
27
29
  - **Shares state across runtimes.** `.ultra/state.db` (SQLite + WAL) is the
28
30
  authoritative source for changes, tasks, sessions, events, incidents,
29
31
  projection jobs, and telemetry. `tasks.json` and context markdown are
@@ -37,6 +39,8 @@ authoritative `.ultra/state.db`.
37
39
  context manifest; `/ultra-deliver` requires docs/test/review evidence and
38
40
  baseline reconciliation before archive. `/ultra-doctor` reports incidents,
39
41
  projection lag, orphan sessions, and backup-first mechanical recovery.
42
+ Incident changes additionally require a five-section `diagnosis.md` covering
43
+ reproduction, hypotheses, root cause, regression test, and recovery.
40
44
  - **Keeps external context external.** Memory and code-graph providers retain
41
45
  their own content; Ultra stores only provider/project/revision/status metadata
42
46
  references in a compiled change context.
@@ -48,22 +52,31 @@ authoritative `.ultra/state.db`.
48
52
  npx ultra-builder-pro-cli --claude --local
49
53
  npx ultra-builder-pro-cli --opencode --local
50
54
  npx ultra-builder-pro-cli --codex --local
55
+ npx ultra-builder-pro-cli --kimi --local
51
56
 
52
57
  # Or blanket-install to every supported runtime you have
53
58
  npx ultra-builder-pro-cli --all --local
54
59
 
55
60
  # Global (into the runtime's ~/.config-style dir)
56
61
  npx ultra-builder-pro-cli --claude --global
62
+ npx ultra-builder-pro-cli --kimi --global
57
63
 
58
64
  # Uninstall (symmetric)
59
65
  npx ultra-builder-pro-cli --all --local --uninstall
66
+
67
+ # Read-only installation provenance and drift diagnosis
68
+ npx ultra-builder-pro-cli --all --local --doctor
69
+ npx ultra-builder-pro-cli --all --local --doctor --json
60
70
  ```
61
71
 
62
72
  After install, start a new host session/task and point it at the project.
63
73
  Claude Code and OpenCode expose native command forms. Codex exposes the same workflows as namespaced plugin
64
74
  skills such as `$ultra-builder-pro:ultra-init`, `$ultra-builder-pro:ultra-plan`,
65
75
  and `$ultra-builder-pro:ultra-dev`; `command-map.json` records the eleven legacy
66
- command mappings (`ultra-review` remains a directly invocable skill).
76
+ command mappings (`ultra-review` remains a directly invocable skill). Kimi Code
77
+ uses native namespaced commands such as `/ultra-builder-pro:ultra-init` and
78
+ `/ultra-builder-pro:ultra-plan`; run `/reload` or start a new Kimi session after
79
+ install or update.
67
80
  See [`docs/RUNTIME-COMPAT-MATRIX.md`](./docs/RUNTIME-COMPAT-MATRIX.md)
68
81
  for per-runtime capabilities.
69
82
 
@@ -84,20 +97,21 @@ Host's native surfaces; the generated Codex capability map documents those
84
97
  replacements without advertising non-existent MCP tools.
85
98
 
86
99
  The package boundary is deliberate: twelve public Ultra workflow skills, four
87
- internal review-rule skills, and host-specific collaboration companions. Browser,
88
- deployment, skill-discovery, and framework guidance belong to their original plugins.
100
+ internal review-rule skills, host-specific collaboration companions, and the
101
+ minimal host bootstrap required by Kimi. Browser, deployment, skill-discovery,
102
+ and framework guidance belong to their original plugins.
89
103
 
90
104
  ## Runtime capability matrix
91
105
 
92
- | Feature | Claude Code | OpenCode | Codex CLI |
93
- |------------------------------|:-----------:|:--------:|:---------:|
94
- | Custom commands | ✅ | ✅ | ✅ |
95
- | Skill loader | ✅ | ✅ | ✅ (personal plugin) |
96
- | MCP server (stdio) | ✅ | ✅ | ✅ (plugin `.mcp.json`) |
97
- | Workflow hooks | ✅ (native plugin) | ✅ (native JS plugin) | ✅ (native plugin) |
98
- | Sub-agents | ✅ | ✅ | ✅ (9 native TOML agents) |
99
- | Session worktree isolation | ✅ (driven by `orchestrator/session-runner.cjs`) | ✅ | ✅ |
100
- | Parallel dispatch + auto-merge | ✅ (`ubp-orchestrator run`) | ✅ | ✅ |
106
+ | Feature | Claude Code | OpenCode | Codex CLI | Kimi Code 0.26+ |
107
+ |------------------------------|:-----------:|:--------:|:---------:|:---------------:|
108
+ | Custom commands | ✅ | ✅ | ✅ | ✅ (namespaced) |
109
+ | Skill loader | ✅ | ✅ | ✅ (personal plugin) | ✅ (native plugin) |
110
+ | MCP server (stdio) | ✅ | ✅ | ✅ (plugin `.mcp.json`) | ✅ (`mcpServers`) |
111
+ | Workflow hooks | ✅ (native plugin) | ✅ (native JS plugin) | ✅ (native plugin) | ✅ with documented compact limitation |
112
+ | Sub-agents | ✅ | ✅ | ✅ (9 native TOML agents) | ✅ via `Agent` / `AgentSwarm` prompt templates |
113
+ | Session worktree isolation | ✅ (driven by `orchestrator/session-runner.cjs`) | ✅ | ✅ | ✅ |
114
+ | Parallel dispatch + auto-merge | ✅ (`ubp-orchestrator run`) | ✅ | ✅ | ✅ |
101
115
 
102
116
  Full details in [`docs/RUNTIME-COMPAT-MATRIX.md`](./docs/RUNTIME-COMPAT-MATRIX.md)
103
117
  (10 sections, with per-runtime deviations called out).
@@ -124,14 +138,15 @@ ultra-tools system doctor
124
138
  Or let the skills drive it. In Codex, invoke
125
139
  `$ultra-builder-pro:ultra-plan` → `$ultra-builder-pro:ultra-dev` →
126
140
  `$ultra-builder-pro:ultra-status`; after the baseline is delivered, daily work
127
- starts with `$ultra-builder-pro:ultra-change`. Other runtimes retain their
128
- native command form.
141
+ starts with `$ultra-builder-pro:ultra-change`. Kimi uses the corresponding
142
+ `/ultra-builder-pro:ultra-*` namespace. Other runtimes retain their native
143
+ command form.
129
144
 
130
145
  ## CLI surface
131
146
 
132
147
  | Binary | Purpose |
133
148
  |--------|---------|
134
- | `ultra-builder-pro-cli` / `ubp` | Installer — `--claude / --opencode / --codex / --all`, `--local / --global`, `--uninstall` |
149
+ | `ultra-builder-pro-cli` / `ubp` | Installer — `--claude / --opencode / --codex / --kimi / --all`, `--local / --global`, `--uninstall`; read-only install checks via `--doctor [--json]` |
135
150
  | `ubp-orchestrator` | Session dispatch daemon — `run`, `start`, `stop`, `status` |
136
151
  | `ultra-tools` | State-layer CLI — `task`, `session`, `status`, `db`, `migrate`; explicit `legacy-memory` archive/prune migration |
137
152
  | `ubp-handbook` | Preview/apply the managed Ultra contract in `CLAUDE.md` / `AGENTS.md`, with backup |
@@ -161,10 +176,21 @@ Individual suites: `test:state`, `test:orch`, `test:spec`, `test:rest`.
161
176
  and `node -e "require('./orchestrator/worktree-manager.cjs').releaseAll(process.cwd())"`
162
177
  cleans them up.
163
178
  - **Installed commands don't show up**: check the runtime's actual config
164
- dir (`ultra-builder-pro-cli --<runtime> --local` only writes to `./.claude`
165
- or `./.opencode` etc.; `--global` writes to the user-level dir). The
179
+ dir (`ultra-builder-pro-cli --<runtime> --local` writes to the host-specific
180
+ project dir such as `./.claude`, `./.opencode`, or `./.kimi-code`;
181
+ `--global` writes to the user-level dir). The
166
182
  install log prints the exact target path. Restart Claude Code/OpenCode or
167
- start a new Codex task after changing an installed plugin.
183
+ start a new Codex task after changing an installed plugin. In Kimi Code,
184
+ run `/reload` or start a new session.
185
+ - **Kimi MCP reports a native-module ABI error**: Kimi 0.26/0.27 embeds a different
186
+ Node ABI from the package installer. The generated plugin intentionally runs
187
+ the MCP with `env node`; keep a Node.js `>=22` executable on `PATH` when
188
+ starting Kimi.
189
+ - **Installed hook/MCP path is missing or stale**: run
190
+ `npx ultra-builder-pro-cli --<runtime> --local --doctor` first. It compares
191
+ the recorded install provenance with current asset hashes and validates the
192
+ host-specific hook, MCP, plugin, and runtime-manifest entry points. Reinstall
193
+ only after preserving the degraded report as evidence.
168
194
  - **Legacy Ultra memory data remains on disk**: inspect first with
169
195
  `ultra-tools legacy-memory inspect`, archive with
170
196
  `ultra-tools legacy-memory archive`, then prune only with the explicit
@@ -802,11 +802,39 @@ function buildMcpRuntime(repoRoot, pluginRoot, { runtime = 'codex' } = {}) {
802
802
  removeTree(cliBuildRoot);
803
803
  }
804
804
 
805
+ const runtimeBootstrap = runtime === 'kimi' ? `
806
+ const fs = require('node:fs');
807
+ const pluginRoot = path.resolve(__dirname, '..');
808
+
809
+ function usableProjectRoot(value) {
810
+ if (typeof value !== 'string' || !value.trim()) return null;
811
+ const candidate = path.resolve(value);
812
+ if (candidate === pluginRoot || candidate.startsWith(pluginRoot + path.sep)) return null;
813
+ try {
814
+ return fs.statSync(candidate).isDirectory() ? candidate : null;
815
+ } catch (_) {
816
+ return null;
817
+ }
818
+ }
819
+
820
+ if (!process.env.UBP_ROOT_DIR) {
821
+ const rootDir = usableProjectRoot(process.cwd()) || usableProjectRoot(process.env.PWD);
822
+ if (!rootDir) {
823
+ throw new Error('Kimi Ultra MCP could not resolve the active project root; start Kimi from the project directory');
824
+ }
825
+ process.env.UBP_ROOT_DIR = rootDir;
826
+ }
827
+ if (!process.env.UBP_DB_PATH) {
828
+ process.env.UBP_DB_PATH = path.join(process.env.UBP_ROOT_DIR, '.ultra', 'state.db');
829
+ }
830
+ ` : '';
831
+
805
832
  const launcher = `'use strict';
806
833
 
807
834
  const path = require('node:path');
808
835
 
809
836
  process.env.UBP_RUNTIME_ROOT = path.resolve(__dirname, '..');
837
+ ${runtimeBootstrap}
810
838
 
811
839
  const { main } = require('./index.cjs');
812
840
 
@@ -32,6 +32,13 @@ const HOSTS = Object.freeze({
32
32
  collaboration: '`/cc-collab` and `/codex-collab` are explicitly requested read-only advisors',
33
33
  coordination: 'OpenCode session coordination does not replace Ultra task state',
34
34
  }),
35
+ kimi: Object.freeze({
36
+ name: 'Kimi Code',
37
+ handbook: ['.kimi-code', 'AGENTS.md'],
38
+ invocation: '`/ultra-builder-pro:ultra-init`, `/ultra-builder-pro:ultra-research`, `/ultra-builder-pro:ultra-plan`, `/ultra-builder-pro:ultra-change`, `/ultra-builder-pro:ultra-dev`, `/ultra-builder-pro:ultra-test`, `/ultra-builder-pro:ultra-review`, and `/ultra-builder-pro:ultra-deliver`; diagnostics use `/ultra-builder-pro:ultra-status`, `/ultra-builder-pro:ultra-think`, and `/ultra-builder-pro:ultra-doctor`',
39
+ collaboration: '`/skill:cc-collab` and `/skill:codex-collab` are explicitly requested read-only advisors',
40
+ coordination: 'Kimi `TodoList` and `Agent` / `AgentSwarm` are host coordination; they do not replace Ultra task state',
41
+ }),
35
42
  });
36
43
 
37
44
  function host(runtime) {
@@ -55,8 +62,9 @@ function renderHandbook(runtime) {
55
62
  '- Authority: `.ultra/state.db` is the only durable Ultra authority for changes, tasks, sessions, events, incidents, projection state, telemetry, and review evidence. Generated JSON and Markdown are projections or workflow artifacts.',
56
63
  '- Memory and graph boundary: Ultra Builder Pro does not capture prompts, transcripts, observations, summaries, cross-session memory, or code-graph content. Separately installed providers own that data; Ultra may store only their metadata references in a change context manifest.',
57
64
  '- Hook boundary: Ultra hooks observe workflow/change lifecycle only. Health/context may run when `.ultra/state.db` exists; projection protection is always authoritative, while edit/compact/stop/subagent enforcement remains active-workflow scoped. Generic command blocking and post-edit policy stay in user or repository governance.',
65
+ '- Installation boundary: `ubp --doctor` is the read-only authority for installed asset provenance, content hashes, and host entry-point wiring; `system.doctor` remains scoped to project state and workflow recovery.',
58
66
  '- Agent boundary: the bundled review and debugging agents are bounded workers. They use the current checkout and parent-supplied context, do not own private persistent state, and never replace the primary agent.',
59
- '- Package boundary: only the twelve Ultra workflows, four internal review-rule skills, and host-specific collaboration companions belong to this plugin. General browser, deployment, discovery, and framework skills must be installed from their owners.',
67
+ '- Package boundary: only the twelve Ultra workflows, four internal review-rule skills, host-specific collaboration companions, and the minimal host bootstrap belong to this plugin. General browser, deployment, discovery, and framework skills must be installed from their owners.',
60
68
  END_MARKER,
61
69
  ].join('\n');
62
70
  }
@@ -14,6 +14,7 @@ const RUNTIME_SKILL_ROOT = {
14
14
  claude: '.claude/skills',
15
15
  opencode: '.config/opencode/skills',
16
16
  codex: '.agents/skills',
17
+ kimi: '.kimi-code/skills',
17
18
  };
18
19
 
19
20
  function parseSkillRef(ref) {
@@ -51,6 +52,7 @@ function runtimeRoot(runtime) {
51
52
  case 'claude': return '.claude';
52
53
  case 'opencode': return '.config/opencode';
53
54
  case 'codex': return '.agents';
55
+ case 'kimi': return '.kimi-code';
54
56
  default: throw new Error(`unknown runtime: ${runtime}`);
55
57
  }
56
58
  }
@@ -60,6 +62,7 @@ function localRoot(runtime) {
60
62
  case 'claude': return '.claude';
61
63
  case 'opencode': return '.opencode';
62
64
  case 'codex': return '.agents';
65
+ case 'kimi': return '.kimi-code';
63
66
  default: throw new Error(`unknown runtime: ${runtime}`);
64
67
  }
65
68
  }
@@ -0,0 +1,286 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+
8
+ const { listRelative, writeAtomic } = require('./file-ops.cjs');
9
+
10
+ const SCHEMA_VERSION = 1;
11
+ const DIGEST_ALGORITHM = 'sha256';
12
+
13
+ function issue(code, details = {}) {
14
+ return { code, ...details };
15
+ }
16
+
17
+ function safeRelative(value) {
18
+ if (typeof value !== 'string' || !value.trim() || path.isAbsolute(value)) {
19
+ throw new Error(`provenance asset path must be relative: ${String(value)}`);
20
+ }
21
+ const normalized = path.normalize(value);
22
+ if (normalized === '..' || normalized.startsWith(`..${path.sep}`)) {
23
+ throw new Error(`provenance asset path escapes its root: ${value}`);
24
+ }
25
+ return normalized;
26
+ }
27
+
28
+ function normalizeRoots(roots) {
29
+ if (!roots || typeof roots !== 'object' || Array.isArray(roots)) {
30
+ throw new Error('provenance roots must be an object');
31
+ }
32
+ const normalized = {};
33
+ for (const [name, root] of Object.entries(roots)) {
34
+ if (!/^[a-z][a-z0-9_-]*$/.test(name) || typeof root !== 'string' || !root.trim()) {
35
+ throw new Error(`invalid provenance root: ${name}`);
36
+ }
37
+ normalized[name] = path.resolve(root);
38
+ }
39
+ if (Object.keys(normalized).length === 0) throw new Error('at least one provenance root is required');
40
+ return normalized;
41
+ }
42
+
43
+ function normalizeRef(ref, roots) {
44
+ if (!ref || typeof ref !== 'object' || Array.isArray(ref) || !roots[ref.root]) {
45
+ throw new Error(`invalid provenance asset root: ${ref && ref.root}`);
46
+ }
47
+ return { root: ref.root, path: safeRelative(ref.path) };
48
+ }
49
+
50
+ function resolveRef(roots, ref) {
51
+ const normalized = normalizeRef(ref, roots);
52
+ const root = roots[normalized.root];
53
+ const target = path.resolve(root, normalized.path);
54
+ if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
55
+ throw new Error(`provenance asset escapes root ${normalized.root}: ${normalized.path}`);
56
+ }
57
+ return { ...normalized, target };
58
+ }
59
+
60
+ function hashFile(file) {
61
+ return crypto.createHash(DIGEST_ALGORITHM).update(fs.readFileSync(file)).digest('hex');
62
+ }
63
+
64
+ function normalizeAssetRefs(assets, roots) {
65
+ if (!Array.isArray(assets) || assets.length === 0) {
66
+ throw new Error('at least one provenance asset is required');
67
+ }
68
+ const unique = new Map();
69
+ for (const input of assets) {
70
+ const ref = normalizeRef(input, roots);
71
+ unique.set(`${ref.root}\0${ref.path}`, ref);
72
+ }
73
+ return [...unique.values()].sort((a, b) => (
74
+ a.root.localeCompare(b.root) || a.path.localeCompare(b.path)
75
+ ));
76
+ }
77
+
78
+ function digestEntries(entries) {
79
+ const hash = crypto.createHash(DIGEST_ALGORITHM);
80
+ for (const entry of entries) {
81
+ hash.update(entry.root);
82
+ hash.update('\0');
83
+ hash.update(entry.path);
84
+ hash.update('\0');
85
+ hash.update(entry.sha256);
86
+ hash.update('\0');
87
+ hash.update(String(entry.size_bytes));
88
+ hash.update('\0');
89
+ }
90
+ return hash.digest('hex');
91
+ }
92
+
93
+ function sourceCommit(repoRoot) {
94
+ if (!fs.existsSync(path.join(repoRoot, '.git'))) return null;
95
+ const result = spawnSync('git', ['rev-parse', 'HEAD'], {
96
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
97
+ });
98
+ const value = result.status === 0 ? result.stdout.trim() : '';
99
+ return /^[0-9a-f]{40,64}$/i.test(value) ? value : null;
100
+ }
101
+
102
+ function repositoryUrl(pkg) {
103
+ if (typeof pkg.repository === 'string') return pkg.repository;
104
+ if (pkg.repository && typeof pkg.repository.url === 'string') return pkg.repository.url;
105
+ return typeof pkg.homepage === 'string' ? pkg.homepage : null;
106
+ }
107
+
108
+ function packageSource(repoRoot) {
109
+ const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
110
+ return {
111
+ packageInfo: { name: pkg.name, version: pkg.version },
112
+ repository: repositoryUrl(pkg),
113
+ sourceCommit: sourceCommit(repoRoot),
114
+ };
115
+ }
116
+
117
+ function assetRefsForTree(rootName, root, { exclude = [] } = {}) {
118
+ const excluded = new Set(exclude.map((entry) => path.normalize(entry)));
119
+ return listRelative(root)
120
+ .filter((entry) => !excluded.has(path.normalize(entry)))
121
+ .map((entry) => ({ root: rootName, path: entry }));
122
+ }
123
+
124
+ function writeProvenance({
125
+ file, adapter, packageInfo, repository, sourceCommit: commit = null,
126
+ roots: inputRoots, assets: inputAssets, contracts: inputContracts = {},
127
+ }) {
128
+ if (!file || !adapter || !packageInfo?.name || !packageInfo?.version || !repository) {
129
+ throw new Error('provenance file, adapter, package, version, and repository are required');
130
+ }
131
+ const roots = normalizeRoots(inputRoots);
132
+ const refs = normalizeAssetRefs(inputAssets, roots);
133
+ const assets = refs.map((ref) => {
134
+ const { target } = resolveRef(roots, ref);
135
+ const stat = fs.statSync(target);
136
+ if (!stat.isFile()) throw new Error(`provenance asset is not a file: ${target}`);
137
+ return { ...ref, size_bytes: stat.size, sha256: hashFile(target) };
138
+ });
139
+ const contracts = {};
140
+ for (const [name, ref] of Object.entries(inputContracts || {})) {
141
+ if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`invalid provenance contract: ${name}`);
142
+ contracts[name] = normalizeRef(ref, roots);
143
+ }
144
+ const manifest = {
145
+ schema_version: SCHEMA_VERSION,
146
+ adapter,
147
+ package: { name: packageInfo.name, version: packageInfo.version },
148
+ source: { repository, commit: commit || null },
149
+ installed_at: new Date().toISOString(),
150
+ roots,
151
+ content: { algorithm: DIGEST_ALGORITHM, digest: digestEntries(assets) },
152
+ assets,
153
+ contracts,
154
+ };
155
+ writeAtomic(path.resolve(file), `${JSON.stringify(manifest, null, 2)}\n`);
156
+ return manifest;
157
+ }
158
+
159
+ function invalidReport(file, status, problem, adapter = null) {
160
+ return {
161
+ adapter,
162
+ status,
163
+ manifest_path: path.resolve(file),
164
+ package: null,
165
+ source: null,
166
+ content_digest: null,
167
+ checks: {
168
+ manifest: { status: 'fail' },
169
+ assets: { status: 'unknown', checked: 0, failed: 0 },
170
+ contracts: { status: 'unknown', checked: 0, failed: 0 },
171
+ },
172
+ issues: [problem],
173
+ };
174
+ }
175
+
176
+ function inspectProvenance({ file, expectedAdapter = null, expectedPackageVersion = null }) {
177
+ const manifestPath = path.resolve(file);
178
+ if (!fs.existsSync(manifestPath)) {
179
+ return invalidReport(
180
+ manifestPath, 'missing', issue('PROVENANCE_MISSING', { path: manifestPath }), expectedAdapter,
181
+ );
182
+ }
183
+ let manifest;
184
+ try {
185
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
186
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) throw new Error('manifest must be an object');
187
+ if (manifest.schema_version !== SCHEMA_VERSION) throw new Error(`unsupported schema ${manifest.schema_version}`);
188
+ if (!manifest.package?.name || !manifest.package?.version || !manifest.source?.repository) {
189
+ throw new Error('package and source metadata required');
190
+ }
191
+ } catch (error) {
192
+ return invalidReport(
193
+ manifestPath, 'degraded', issue('PROVENANCE_INVALID', { message: error.message }), expectedAdapter,
194
+ );
195
+ }
196
+
197
+ const issues = [];
198
+ let roots;
199
+ try {
200
+ roots = normalizeRoots(manifest.roots);
201
+ } catch (error) {
202
+ return invalidReport(
203
+ manifestPath, 'degraded', issue('PROVENANCE_INVALID', { message: error.message }), expectedAdapter,
204
+ );
205
+ }
206
+ if (expectedAdapter && manifest.adapter !== expectedAdapter) {
207
+ issues.push(issue('ADAPTER_MISMATCH', { expected: expectedAdapter, actual: manifest.adapter }));
208
+ }
209
+ if (expectedPackageVersion && manifest.package.version !== expectedPackageVersion) {
210
+ issues.push(issue('PACKAGE_VERSION_MISMATCH', {
211
+ expected: expectedPackageVersion, actual: manifest.package.version,
212
+ }));
213
+ }
214
+
215
+ const assets = Array.isArray(manifest.assets) ? manifest.assets : [];
216
+ if (assets.length === 0) issues.push(issue('PROVENANCE_INVALID', { message: 'assets required' }));
217
+ let assetFailures = 0;
218
+ for (const asset of assets) {
219
+ try {
220
+ const { target, root, path: assetPath } = resolveRef(roots, asset);
221
+ if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {
222
+ assetFailures += 1;
223
+ issues.push(issue('ASSET_MISSING', { root, path: assetPath }));
224
+ continue;
225
+ }
226
+ const actual = hashFile(target);
227
+ if (actual !== asset.sha256) {
228
+ assetFailures += 1;
229
+ issues.push(issue('ASSET_HASH_MISMATCH', {
230
+ root, path: assetPath, expected: asset.sha256, actual,
231
+ }));
232
+ }
233
+ } catch (error) {
234
+ assetFailures += 1;
235
+ issues.push(issue('ASSET_INVALID', { message: error.message }));
236
+ }
237
+ }
238
+ if (manifest.content?.algorithm !== DIGEST_ALGORITHM
239
+ || manifest.content?.digest !== digestEntries(assets)) {
240
+ issues.push(issue('MANIFEST_DIGEST_MISMATCH'));
241
+ }
242
+
243
+ const contracts = manifest.contracts && typeof manifest.contracts === 'object'
244
+ && !Array.isArray(manifest.contracts) ? manifest.contracts : {};
245
+ let contractFailures = 0;
246
+ for (const [name, ref] of Object.entries(contracts)) {
247
+ try {
248
+ const { target, root, path: contractPath } = resolveRef(roots, ref);
249
+ if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {
250
+ contractFailures += 1;
251
+ issues.push(issue('CONTRACT_TARGET_MISSING', { contract: name, root, path: contractPath }));
252
+ }
253
+ } catch (error) {
254
+ contractFailures += 1;
255
+ issues.push(issue('CONTRACT_INVALID', { contract: name, message: error.message }));
256
+ }
257
+ }
258
+
259
+ return {
260
+ adapter: manifest.adapter,
261
+ status: issues.length === 0 ? 'healthy' : 'degraded',
262
+ manifest_path: manifestPath,
263
+ package: manifest.package,
264
+ source: manifest.source,
265
+ content_digest: manifest.content?.digest || null,
266
+ checks: {
267
+ manifest: { status: 'pass' },
268
+ assets: { status: assetFailures === 0 ? 'pass' : 'fail', checked: assets.length, failed: assetFailures },
269
+ contracts: {
270
+ status: contractFailures === 0 ? 'pass' : 'fail',
271
+ checked: Object.keys(contracts).length,
272
+ failed: contractFailures,
273
+ },
274
+ },
275
+ issues,
276
+ };
277
+ }
278
+
279
+ module.exports = {
280
+ SCHEMA_VERSION,
281
+ DIGEST_ALGORITHM,
282
+ assetRefsForTree,
283
+ inspectProvenance,
284
+ packageSource,
285
+ writeProvenance,
286
+ };
@@ -29,12 +29,13 @@ const INTERNAL_AGENT_SKILLS = Object.freeze([
29
29
  'testing-rules',
30
30
  ]);
31
31
 
32
- const SUPPORTED_RUNTIMES = Object.freeze(['claude', 'opencode', 'codex']);
32
+ const SUPPORTED_RUNTIMES = Object.freeze(['claude', 'opencode', 'codex', 'kimi']);
33
33
 
34
34
  const COLLAB_SKILLS_BY_RUNTIME = Object.freeze({
35
35
  claude: Object.freeze(['codex-collab', 'ultra-verify']),
36
36
  codex: Object.freeze(['cc-collab', 'ultra-verify']),
37
37
  opencode: Object.freeze(['cc-collab', 'codex-collab', 'ultra-verify']),
38
+ kimi: Object.freeze(['cc-collab', 'codex-collab', 'ultra-verify']),
38
39
  });
39
40
 
40
41
  const RETIRED_SKILLS = Object.freeze([
@@ -19,11 +19,15 @@ test('renders one common contract with host-native invocation syntax', () => {
19
19
  const claude = renderHandbook('claude');
20
20
  const codex = renderHandbook('codex');
21
21
  const opencode = renderHandbook('opencode');
22
+ const kimi = renderHandbook('kimi');
22
23
 
23
24
  assert.match(claude, /\/ultra-plan/);
24
25
  assert.match(codex, /\$ultra-builder-pro:ultra-plan/);
25
26
  assert.match(opencode, /\/ultra-plan/);
26
- for (const rendered of [claude, codex, opencode]) {
27
+ assert.match(kimi, /\/ultra-builder-pro:ultra-plan/);
28
+ assert.match(kimi, /Kimi Code/);
29
+ assert.match(kimi, /AgentSwarm/);
30
+ for (const rendered of [claude, codex, opencode, kimi]) {
27
31
  assert.match(rendered, /\.ultra\/state\.db/);
28
32
  assert.match(rendered, /Separately installed providers/);
29
33
  assert.match(rendered, /ultra-change/);
@@ -86,4 +90,5 @@ test('default handbook paths are host-specific', () => {
86
90
  assert.equal(resolveHandbookFile('claude', { homeDir }), path.join(homeDir, '.claude', 'CLAUDE.md'));
87
91
  assert.equal(resolveHandbookFile('codex', { homeDir }), path.join(homeDir, '.codex', 'AGENTS.md'));
88
92
  assert.equal(resolveHandbookFile('opencode', { homeDir }), path.join(homeDir, '.config', 'opencode', 'AGENTS.md'));
93
+ assert.equal(resolveHandbookFile('kimi', { homeDir }), path.join(homeDir, '.kimi-code', 'AGENTS.md'));
89
94
  });
@@ -26,10 +26,12 @@ test('resolveSkillRef routes to runtime-specific root under homeDir', () => {
26
26
  const claude = resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'claude', homeDir: home });
27
27
  const opencode = resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'opencode', homeDir: home });
28
28
  const codex = resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'codex', homeDir: home });
29
+ const kimi = resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'kimi', homeDir: home });
29
30
 
30
31
  assert.equal(claude, path.join(home, '.claude/skills/ultra-init/SKILL.md'));
31
32
  assert.equal(opencode, path.join(home, '.config/opencode/skills/ultra-init/SKILL.md'));
32
33
  assert.equal(codex, path.join(home, '.agents/skills/ultra-init/SKILL.md'));
34
+ assert.equal(kimi, path.join(home, '.kimi-code/skills/ultra-init/SKILL.md'));
33
35
 
34
36
  assert.throws(
35
37
  () => resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'unknown', homeDir: home }),
@@ -45,7 +47,7 @@ test('rewriteWorkflowRefInText + resolveAssetTarget give predictable paths', ()
45
47
  assert.equal(out, `see ${path.join(home, '.claude/skills/ultra-init/SKILL.md')} for details`);
46
48
 
47
49
  // Cover every runtime × every scope so runtimeRoot/localRoot branches hit
48
- for (const rt of ['claude', 'opencode', 'codex']) {
50
+ for (const rt of ['claude', 'opencode', 'codex', 'kimi']) {
49
51
  const g = resolveAssetTarget('x', { runtime: rt, scope: 'global', homeDir: home, cwd });
50
52
  const l = resolveAssetTarget('x', { runtime: rt, scope: 'local', homeDir: home, cwd });
51
53
  assert.ok(g.startsWith(home));