ultra-builder-pro-cli 0.8.1 → 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.
- package/CHANGELOG.md +32 -1
- package/README.md +36 -22
- package/adapters/_shared/codex-assets.cjs +28 -0
- package/adapters/_shared/handbook.cjs +8 -1
- package/adapters/_shared/path-rewrite.cjs +3 -0
- package/adapters/_shared/runtime-assets.cjs +2 -1
- package/adapters/_shared/tests/handbook.test.cjs +6 -1
- package/adapters/_shared/tests/path-rewrite.test.cjs +3 -1
- package/adapters/_shared/tests/runtime-assets.test.cjs +4 -1
- package/adapters/kimi.js +642 -0
- package/adapters/tests/kimi-hook.test.cjs +122 -0
- package/adapters/tests/kimi.test.cjs +255 -0
- package/bin/handbook.js +2 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +1 -1
- package/docs/AGENT-CONTEXT.md +5 -3
- package/docs/ARCHITECTURE.md +4 -4
- package/docs/PLAN.zh-CN.md +4 -4
- package/docs/ROADMAP.md +4 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +106 -62
- package/docs/USER-HANDBOOK-CONTRACT.md +4 -1
- package/hooks/adapters/kimi.py +162 -0
- package/mcp-server/lib/state-db.cjs +85 -2
- package/mcp-server/lib/state-db.test.cjs +56 -0
- package/mcp-server/tests/init-project.test.cjs +2 -2
- package/mcp-server/tests/state-db.test.cjs +3 -3
- package/package.json +4 -2
- package/skills/ultra-review/SKILL.md +3 -3
- package/spec/mcp-tools.yaml +3 -3
- package/spec/schemas/skill-manifest.schema.json +2 -2
- package/spec/schemas/state-db.sql +4 -2
|
@@ -1,97 +1,139 @@
|
|
|
1
1
|
# Runtime Compatibility Matrix
|
|
2
2
|
|
|
3
|
-
Current presentation contract for Ultra Builder Pro. Claude Code, OpenCode,
|
|
4
|
-
Codex are
|
|
5
|
-
in `adapters/_shared/runtime-assets.cjs`.
|
|
3
|
+
Current presentation contract for Ultra Builder Pro. Claude Code, OpenCode,
|
|
4
|
+
Codex, and Kimi Code 0.26+ are supported native plugin hosts. The canonical
|
|
5
|
+
asset boundary lives in `adapters/_shared/runtime-assets.cjs`.
|
|
6
6
|
|
|
7
|
-
Legend: **FULL** = native host surface, **
|
|
8
|
-
|
|
7
|
+
Legend: **FULL** = native host surface, **FUNCTIONAL** = the complete outcome is
|
|
8
|
+
available through a different native host primitive, **DEGRADED** = supported
|
|
9
|
+
but missing an equivalent lifecycle/control surface, **N/A** = intentionally
|
|
10
|
+
not installed.
|
|
9
11
|
|
|
10
12
|
## 1. Plugin and workflow surface
|
|
11
13
|
|
|
12
|
-
| Capability | Claude Code | OpenCode | Codex |
|
|
13
|
-
|
|
14
|
-
| Package form | `.claude-plugin` native plugin | native config bundle + JS plugin | personal `.codex-plugin` |
|
|
15
|
-
| Public entry | `/ultra-*`, `/learn` | `/ultra-*`, `/learn` | `$ultra-builder-pro:<skill>` |
|
|
16
|
-
| Public Ultra workflows | 12 | 12 | 12 |
|
|
17
|
-
| Internal agent-rule skills | 4, non-user-facing | 4, non-user-facing | 4 with implicit invocation disabled |
|
|
18
|
-
| Collaboration companions | `codex-collab`, `ultra-verify` | `cc-collab`, `codex-collab`, `ultra-verify` | `cc-collab`, `ultra-verify` |
|
|
19
|
-
|
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
| Capability | Claude Code | OpenCode | Codex | Kimi Code 0.26+ |
|
|
15
|
+
|---|---|---|---|---|
|
|
16
|
+
| Package form | `.claude-plugin` native plugin | native config bundle + JS plugin | personal `.codex-plugin` | managed `kimi.plugin.json` plugin |
|
|
17
|
+
| Public entry | `/ultra-*`, `/learn` | `/ultra-*`, `/learn` | `$ultra-builder-pro:<skill>` | `/ultra-builder-pro:<command>` |
|
|
18
|
+
| Public Ultra workflows | 12 | 12 | 12 | 12 |
|
|
19
|
+
| Internal agent-rule skills | 4, non-user-facing | 4, non-user-facing | 4 with implicit invocation disabled | 4, consumed by review workers |
|
|
20
|
+
| Collaboration companions | `codex-collab`, `ultra-verify` | `cc-collab`, `codex-collab`, `ultra-verify` | `cc-collab`, `ultra-verify` | `cc-collab`, `codex-collab`, `ultra-verify` |
|
|
21
|
+
| Host bootstrap | native plugin context | native plugin context | native plugin context | `using-ultra-builder-pro` session-start skill |
|
|
22
|
+
| External browser/deploy/framework skills | N/A | N/A | N/A | N/A |
|
|
23
|
+
|
|
24
|
+
Codex converts workflows into namespaced skills and records eleven legacy
|
|
22
25
|
command mappings in `command-map.json`; `ultra-review` remains directly
|
|
23
|
-
invocable as a skill.
|
|
26
|
+
invocable as a skill. Kimi registers all twelve commands through its native
|
|
27
|
+
plugin command directory and exposes the allowlisted skills independently.
|
|
24
28
|
|
|
25
29
|
## 2. Agents and collaboration
|
|
26
30
|
|
|
27
|
-
| Capability | Claude Code | OpenCode | Codex |
|
|
28
|
-
|
|
29
|
-
| Bundled review/debug
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
31
|
+
| Capability | Claude Code | OpenCode | Codex | Kimi Code 0.26+ |
|
|
32
|
+
|---|---|---|---|---|
|
|
33
|
+
| Bundled review/debug workers | FULL, native Markdown agents | FULL, native agent frontmatter | FULL, nine managed TOML agents | FUNCTIONAL, nine prompt templates consumed by `Agent` / `AgentSwarm` |
|
|
34
|
+
| Plugin custom-agent registration | FULL | FULL | FULL | N/A; the 0.26 manifest has no custom-agent field |
|
|
35
|
+
| Native bounded delegation | native agent surface | native agent surface | native Codex subagents | native `Agent` / `AgentSwarm` |
|
|
36
|
+
| Cross-host advisor | explicit and read-only | explicit and read-only | explicit and read-only | explicit and read-only |
|
|
37
|
+
| Primary-agent ownership | FULL | FULL | FULL | FULL |
|
|
33
38
|
|
|
34
|
-
No bundled
|
|
39
|
+
No bundled worker owns private persistent memory. It receives current-checkout
|
|
35
40
|
evidence and bounded context from the primary host, then returns a result for
|
|
36
|
-
primary verification.
|
|
41
|
+
primary verification. On Kimi, the review workflow reads the bundled worker
|
|
42
|
+
template before creating the corresponding `AgentSwarm` item; the templates are
|
|
43
|
+
not presented as nonexistent custom agents.
|
|
37
44
|
|
|
38
45
|
## 3. Workflow hooks
|
|
39
46
|
|
|
40
|
-
| Lifecycle | Claude Code | OpenCode | Codex |
|
|
41
|
-
|
|
42
|
-
| Session context/health | FULL, native `SessionStart` | DEGRADED, native context; health via `system.doctor` | FULL, native `SessionStart` |
|
|
43
|
-
| Active edit boundary | FULL, `PreToolUse Edit|Write` | FULL, `tool.execute.before` rejects projection writes | FULL, `PreToolUse Edit|Write|apply_patch` |
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
+
| Lifecycle | Claude Code | OpenCode | Codex | Kimi Code 0.26+ |
|
|
48
|
+
|---|---|---|---|---|
|
|
49
|
+
| Session context/health | FULL, native `SessionStart` | DEGRADED, native context; health via `system.doctor` | FULL, native `SessionStart` | FULL, session-start skill + `SessionStart` hooks |
|
|
50
|
+
| Active edit boundary | FULL, `PreToolUse Edit|Write` | FULL, `tool.execute.before` rejects projection writes | FULL, `PreToolUse Edit|Write|apply_patch` | FULL, native `PreToolUse Edit|Write` deny contract |
|
|
51
|
+
| Pre-compact checkpoint | FULL | FULL | FULL | FULL, native `PreCompact` |
|
|
52
|
+
| Post-compact context injection | FULL | FULL, native compacting context | FULL, native `PostCompact` restore | DEGRADED; checkpoint restoration runs, but Kimi 0.26/0.27 does not reinject fire-and-forget hook text |
|
|
53
|
+
| Incomplete-stop gate | FULL, native blocking `Stop` | DEGRADED, no equivalent blocking stop hook | FULL, native blocking `Stop` | FULL, native structured deny |
|
|
54
|
+
| Subagent lifecycle evidence | FULL | DEGRADED, no equivalent packaged event | FULL | FULL, native `SubagentStart` / `SubagentStop` |
|
|
55
|
+
|
|
56
|
+
Kimi's session bootstrap therefore also instructs recovery to inspect
|
|
57
|
+
`.ultra/runtime/checkpoint.json` and call Ultra status/doctor after compaction.
|
|
58
|
+
This keeps durable recovery intact without claiming dynamic context injection
|
|
59
|
+
that the host does not guarantee.
|
|
47
60
|
|
|
48
61
|
Health/context hooks inspect initialized projects; projection protection also
|
|
49
|
-
applies at baseline, while compact/stop/subagent enforcement remains
|
|
50
|
-
No host receives prompt capture, transcript
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
applies at baseline, while compact/stop/subagent enforcement remains
|
|
63
|
+
active-workflow scoped. No host receives prompt capture, transcript capture,
|
|
64
|
+
observation journaling, session-summary memory, generic command blocking, or
|
|
65
|
+
generic post-edit policy.
|
|
53
66
|
|
|
54
67
|
## 4. MCP and state
|
|
55
68
|
|
|
56
|
-
| Capability | Claude Code | OpenCode | Codex |
|
|
57
|
-
|
|
58
|
-
| stdio MCP registration | plugin `.mcp.json` | `opencode.json` local MCP entry | plugin `.mcp.json` |
|
|
59
|
-
| Live/declared contracts | 29 | 29 | 29 |
|
|
60
|
-
| Host-native review/discovery/ask | native | native | documented in `codex-capability-map.json` |
|
|
61
|
-
| Durable authority | project `.ultra/state.db` | project `.ultra/state.db` | project `.ultra/state.db` |
|
|
62
|
-
| Ultra memory API | N/A | N/A | N/A |
|
|
69
|
+
| Capability | Claude Code | OpenCode | Codex | Kimi Code 0.26+ |
|
|
70
|
+
|---|---|---|---|---|
|
|
71
|
+
| stdio MCP registration | plugin `.mcp.json` | `opencode.json` local MCP entry | plugin `.mcp.json` | plugin `mcpServers` entry |
|
|
72
|
+
| Live/declared contracts | 29 | 29 | 29 | 29 |
|
|
73
|
+
| Host-native review/discovery/ask | native | native | documented in `codex-capability-map.json` | native Kimi tools and workers |
|
|
74
|
+
| Durable authority | project `.ultra/state.db` | project `.ultra/state.db` | project `.ultra/state.db` | project `.ultra/state.db` |
|
|
75
|
+
| Ultra memory API | N/A | N/A | N/A | N/A |
|
|
63
76
|
|
|
64
77
|
All 29 `task.*`, `session.*`, `change.*`, `system.*`, and `plan.*` operations
|
|
65
78
|
registered by `mcp-server/server.cjs` are live. Review, impact discovery, skill
|
|
66
79
|
loading, and user interaction remain host-native surfaces.
|
|
67
80
|
|
|
81
|
+
Kimi starts plugin MCP processes with the managed plugin root as `cwd`. The
|
|
82
|
+
generated launcher recovers the project from the inherited `PWD`, sets
|
|
83
|
+
`UBP_ROOT_DIR` and `UBP_DB_PATH`, and refuses to start if that project boundary
|
|
84
|
+
cannot be established. On POSIX it intentionally uses `env node` because Kimi
|
|
85
|
+
0.26/0.27's embedded Node ABI differs from the ABI used to install `better-sqlite3`.
|
|
86
|
+
|
|
68
87
|
## 5. User handbook presentation
|
|
69
88
|
|
|
70
|
-
| Capability | Claude Code | OpenCode | Codex |
|
|
71
|
-
|
|
72
|
-
| Durable user file | `~/.claude/CLAUDE.md` | `~/.config/opencode/AGENTS.md` | `~/.codex/AGENTS.md` |
|
|
73
|
-
| Automatic overwrite by plugin install | no | no | no |
|
|
74
|
-
| Explicit managed-block sync | `ubp-handbook --runtime claude` | `ubp-handbook --runtime opencode` | `ubp-handbook --runtime codex` |
|
|
75
|
-
| Backup before change | FULL | FULL | FULL |
|
|
89
|
+
| Capability | Claude Code | OpenCode | Codex | Kimi Code 0.26+ |
|
|
90
|
+
|---|---|---|---|---|
|
|
91
|
+
| Durable user file | `~/.claude/CLAUDE.md` | `~/.config/opencode/AGENTS.md` | `~/.codex/AGENTS.md` | `~/.kimi-code/AGENTS.md` |
|
|
92
|
+
| Automatic overwrite by plugin install | no | no | no | no |
|
|
93
|
+
| Explicit managed-block sync | `ubp-handbook --runtime claude` | `ubp-handbook --runtime opencode` | `ubp-handbook --runtime codex` | `ubp-handbook --runtime kimi` |
|
|
94
|
+
| Backup before change | FULL | FULL | FULL | FULL |
|
|
76
95
|
|
|
77
|
-
|
|
96
|
+
The Kimi plugin installer does not edit `~/.kimi-code/config.toml`. Handbook
|
|
97
|
+
sync remains a separate, previewable action so user policy and plugin mechanics
|
|
98
|
+
do not become one irreversible mutation.
|
|
78
99
|
|
|
79
|
-
|
|
80
|
-
|---|---|---|---|
|
|
81
|
-
| Adapter install | FULL | FULL | FULL |
|
|
82
|
-
| Native update | next host session | host restart | cachebuster + `codex plugin add`, then new task |
|
|
83
|
-
| Removes stale managed assets | FULL | FULL | FULL |
|
|
84
|
-
| Preserves unrelated config | FULL | FULL | FULL |
|
|
85
|
-
| Uninstall ownership guard | managed plugin root | sentinels + owned MCP entry | managed root/manifest/agent headers |
|
|
86
|
-
| Normalized install provenance | FULL | FULL | FULL |
|
|
87
|
-
| Read-only `ubp --doctor` | FULL | FULL | FULL |
|
|
88
|
-
| Asset-content and host-contract drift detection | FULL | FULL | FULL |
|
|
100
|
+
## 6. Install, update, and uninstall
|
|
89
101
|
|
|
90
|
-
|
|
102
|
+
| Capability | Claude Code | OpenCode | Codex | Kimi Code 0.26+ |
|
|
103
|
+
|---|---|---|---|---|
|
|
104
|
+
| Adapter install | FULL | FULL | FULL | FULL |
|
|
105
|
+
| Global install command | `ubp --claude --global` | `ubp --opencode --global` | `ubp --codex --global` | `ubp --kimi --global` |
|
|
106
|
+
| Native update visibility | next host session | host restart | cachebuster + new task | `/reload` or new session |
|
|
107
|
+
| Removes stale managed assets | FULL | FULL | FULL | FULL |
|
|
108
|
+
| Preserves unrelated config | FULL | FULL | FULL | FULL, including `config.toml` and unrelated registry records |
|
|
109
|
+
| Uninstall ownership guard | managed plugin root | sentinels + owned MCP entry | managed root/manifest/agent headers | managed root + exact registry record |
|
|
110
|
+
| Normalized install provenance | FULL | FULL | FULL | FULL |
|
|
111
|
+
| Read-only `ubp --doctor` | FULL | FULL | FULL | FULL |
|
|
112
|
+
| Asset-content and host-contract drift detection | FULL | FULL | FULL | FULL |
|
|
113
|
+
|
|
114
|
+
Kimi's global managed root is
|
|
115
|
+
`~/.kimi-code/plugins/managed/ultra-builder-pro`; its only registry mutation is
|
|
116
|
+
the owned `ultra-builder-pro` entry in `~/.kimi-code/plugins/installed.json`.
|
|
117
|
+
Reinstall preserves the user's enabled/capability choices and unrelated plugin
|
|
118
|
+
records. Uninstall refuses an unmanaged or conflicting root.
|
|
119
|
+
|
|
120
|
+
## 7. Durable schema compatibility
|
|
121
|
+
|
|
122
|
+
| Capability | Contract |
|
|
123
|
+
|---|---|
|
|
124
|
+
| Current state schema | `9.1` |
|
|
125
|
+
| Runtime values | `claude`, `opencode`, `codex`, `kimi` |
|
|
126
|
+
| Upgrade from pre-Kimi schema | transactionally rebuilds only constrained `events` and `sessions` tables |
|
|
127
|
+
| Preservation gate | rows, IDs, indexes, foreign keys, telemetry, incidents, and migration history remain intact |
|
|
128
|
+
| Failure behavior | rollback; no partially upgraded authority database |
|
|
129
|
+
|
|
130
|
+
## 8. Verification sources
|
|
91
131
|
|
|
92
132
|
- `adapters/tests/*.test.cjs` verifies each installer and native manifest.
|
|
93
133
|
- `tests/conformance/<runtime>/*.test.cjs` verifies command/skill, hook, MCP,
|
|
94
|
-
idempotency, and smoke presentation.
|
|
134
|
+
idempotency, and smoke presentation for all four hosts.
|
|
135
|
+
- `tests/conformance/kimi/smoke.test.cjs` reproduces Kimi's plugin-root MCP
|
|
136
|
+
`cwd` and proves that the first state write lands in the active project.
|
|
95
137
|
- `adapters/_shared/tests/runtime-assets.test.cjs` prevents retired or external
|
|
96
138
|
skills and generic/memory hooks from re-entering a package.
|
|
97
139
|
- `tests/retired-runtime.test.cjs` prevents retired runtime code or prompt text
|
|
@@ -100,7 +142,9 @@ loading, and user interaction remain host-native surfaces.
|
|
|
100
142
|
migration, and idempotency.
|
|
101
143
|
- `adapters/_shared/tests/provenance.test.cjs` verifies normalized manifests,
|
|
102
144
|
content hashes, source attribution, corruption handling, and contract drift.
|
|
103
|
-
- `tests/install.test.cjs` verifies healthy
|
|
145
|
+
- `tests/install.test.cjs` verifies healthy four-host doctor output and
|
|
104
146
|
host-specific hook/MCP degradation after controlled tampering.
|
|
147
|
+
- `tests/package-smoke.test.cjs` installs the packed tarball into a clean
|
|
148
|
+
consumer and executes a live MCP round trip through every generated launcher.
|
|
105
149
|
|
|
106
150
|
Any capability claim elsewhere must match current adapter code and these tests.
|
|
@@ -15,8 +15,9 @@ syntax:
|
|
|
15
15
|
| Claude Code | `~/.claude/CLAUDE.md` | `/ultra-plan` |
|
|
16
16
|
| Codex | `~/.codex/AGENTS.md` | `$ultra-builder-pro:ultra-plan` |
|
|
17
17
|
| OpenCode | `~/.config/opencode/AGENTS.md` | `/ultra-plan` |
|
|
18
|
+
| Kimi Code | `~/.kimi-code/AGENTS.md` | `/ultra-builder-pro:ultra-plan` |
|
|
18
19
|
|
|
19
|
-
The renderer never copies Claude Code-only APIs into Codex or
|
|
20
|
+
The renderer never copies Claude Code-only APIs into Codex, OpenCode, or Kimi. Each
|
|
20
21
|
host receives only its native invocation and collaboration names.
|
|
21
22
|
|
|
22
23
|
## Managed-block contract
|
|
@@ -42,6 +43,7 @@ Preview the full merged result before applying:
|
|
|
42
43
|
ubp-handbook preview --runtime claude
|
|
43
44
|
ubp-handbook preview --runtime codex
|
|
44
45
|
ubp-handbook preview --runtime opencode
|
|
46
|
+
ubp-handbook preview --runtime kimi
|
|
45
47
|
```
|
|
46
48
|
|
|
47
49
|
Apply when the preview is correct:
|
|
@@ -50,6 +52,7 @@ Apply when the preview is correct:
|
|
|
50
52
|
ubp-handbook apply --runtime claude
|
|
51
53
|
ubp-handbook apply --runtime codex
|
|
52
54
|
ubp-handbook apply --runtime opencode
|
|
55
|
+
ubp-handbook apply --runtime kimi
|
|
53
56
|
```
|
|
54
57
|
|
|
55
58
|
An existing target is copied to
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Kimi Code wire adapter for Ultra Builder Pro lifecycle hooks.
|
|
3
|
+
|
|
4
|
+
Kimi 0.26 uses snake_case JSON input compatible with the workflow hooks, but
|
|
5
|
+
its output contract accepts only ``message`` plus an optional structured deny.
|
|
6
|
+
This adapter preserves real projection/stop blocks, converts foreign context
|
|
7
|
+
fields into Kimi messages, and fails open with diagnostic evidence.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import copy
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
HOOK_ROOT = Path(__file__).resolve().parent.parent
|
|
22
|
+
ALLOWED_FEATURES = {
|
|
23
|
+
"active_task_context.py",
|
|
24
|
+
"health_check.py",
|
|
25
|
+
"pre_stop_check.py",
|
|
26
|
+
"subagent_tracker.py",
|
|
27
|
+
"workflow_checkpoint.py",
|
|
28
|
+
"workflow_context.py",
|
|
29
|
+
"workflow_resume.py",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def normalize_input(payload: dict[str, Any]) -> dict[str, Any]:
|
|
34
|
+
"""Add only the legacy aliases required by the shared workflow hooks."""
|
|
35
|
+
normalized = copy.deepcopy(payload)
|
|
36
|
+
agent_name = normalized.get("agent_name")
|
|
37
|
+
if isinstance(agent_name, str) and agent_name:
|
|
38
|
+
normalized.setdefault("agent_id", agent_name)
|
|
39
|
+
normalized.setdefault("agent_type", agent_name)
|
|
40
|
+
if normalized.get("tool_response") is not None and normalized.get("tool_output") is None:
|
|
41
|
+
normalized["tool_output"] = normalized["tool_response"]
|
|
42
|
+
return normalized
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _text(value: Any) -> str:
|
|
46
|
+
return str(value).strip() if value is not None else ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def adapt_output(output: dict[str, Any], event: str) -> dict[str, Any]:
|
|
50
|
+
"""Convert one legacy hook result to Kimi's exact structured schema."""
|
|
51
|
+
if not isinstance(output, dict):
|
|
52
|
+
return {}
|
|
53
|
+
specific = output.get("hookSpecificOutput")
|
|
54
|
+
specific = specific if isinstance(specific, dict) else {}
|
|
55
|
+
|
|
56
|
+
decision = specific.get("permissionDecision")
|
|
57
|
+
blocked = output.get("decision") == "block" or decision == "deny"
|
|
58
|
+
reason = (
|
|
59
|
+
_text(specific.get("permissionDecisionReason"))
|
|
60
|
+
or _text(output.get("reason"))
|
|
61
|
+
or f"Ultra {event} gate blocked continuation"
|
|
62
|
+
)
|
|
63
|
+
if blocked:
|
|
64
|
+
return {
|
|
65
|
+
"hookSpecificOutput": {
|
|
66
|
+
"message": reason,
|
|
67
|
+
"permissionDecision": "deny",
|
|
68
|
+
"permissionDecisionReason": reason,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
context = (
|
|
73
|
+
_text(specific.get("additionalContext"))
|
|
74
|
+
or _text(output.get("additionalContext"))
|
|
75
|
+
or _text(output.get("systemMessage"))
|
|
76
|
+
or _text(specific.get("message"))
|
|
77
|
+
or _text(output.get("message"))
|
|
78
|
+
)
|
|
79
|
+
advisory = _text(specific.get("permissionDecisionReason")) if decision == "ask" else ""
|
|
80
|
+
message = "\n\n".join(part for part in [advisory, context] if part)
|
|
81
|
+
return {"message": message} if message else {}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _parse_output(stdout: str) -> dict[str, Any]:
|
|
85
|
+
text = stdout.strip()
|
|
86
|
+
if not text:
|
|
87
|
+
return {}
|
|
88
|
+
try:
|
|
89
|
+
value = json.loads(text)
|
|
90
|
+
except json.JSONDecodeError:
|
|
91
|
+
return {"message": f"Ultra hook returned non-JSON output: {text[:500]}"}
|
|
92
|
+
return value if isinstance(value, dict) else {}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def run_feature(feature: str, payload: dict[str, Any], feature_args: list[str]) -> dict[str, Any]:
|
|
96
|
+
if feature not in ALLOWED_FEATURES or Path(feature).name != feature:
|
|
97
|
+
return {"message": f"Ultra hook adapter refused unknown feature: {feature}"}
|
|
98
|
+
script = HOOK_ROOT / feature
|
|
99
|
+
if not script.is_file():
|
|
100
|
+
return {"message": f"Ultra hook feature is missing: {feature}"}
|
|
101
|
+
|
|
102
|
+
event = str(payload.get("hook_event_name") or "")
|
|
103
|
+
normalized = normalize_input(payload)
|
|
104
|
+
if feature == "workflow_resume.py" and event == "PostCompact":
|
|
105
|
+
normalized["hook_event_name"] = "SessionStart"
|
|
106
|
+
normalized["source"] = "compact"
|
|
107
|
+
|
|
108
|
+
env = os.environ.copy()
|
|
109
|
+
env["UBP_HOOK_RUNTIME"] = "kimi"
|
|
110
|
+
plugin_data = env.get("PLUGIN_DATA") or env.get("KIMI_PLUGIN_ROOT")
|
|
111
|
+
if plugin_data:
|
|
112
|
+
env["UBP_HOOK_DATA"] = plugin_data
|
|
113
|
+
|
|
114
|
+
cwd = normalized.get("cwd")
|
|
115
|
+
run_cwd = str(cwd) if isinstance(cwd, str) and Path(cwd).is_dir() else os.getcwd()
|
|
116
|
+
proc = subprocess.run(
|
|
117
|
+
[sys.executable, str(script), *feature_args],
|
|
118
|
+
input=json.dumps(normalized),
|
|
119
|
+
text=True,
|
|
120
|
+
capture_output=True,
|
|
121
|
+
cwd=run_cwd,
|
|
122
|
+
env=env,
|
|
123
|
+
timeout=30,
|
|
124
|
+
check=False,
|
|
125
|
+
)
|
|
126
|
+
if proc.stderr:
|
|
127
|
+
print(proc.stderr.rstrip(), file=sys.stderr)
|
|
128
|
+
if proc.returncode != 0:
|
|
129
|
+
return {
|
|
130
|
+
"message": (
|
|
131
|
+
f"Ultra hook {feature} failed with exit {proc.returncode}: "
|
|
132
|
+
f"{proc.stderr.strip()[:500]}"
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
return adapt_output(_parse_output(proc.stdout), event)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def main() -> int:
|
|
139
|
+
if len(sys.argv) < 2:
|
|
140
|
+
print(json.dumps({"message": "Ultra Kimi hook adapter requires a feature name"}))
|
|
141
|
+
return 0
|
|
142
|
+
feature = sys.argv[1]
|
|
143
|
+
feature_args = sys.argv[2:]
|
|
144
|
+
try:
|
|
145
|
+
payload = json.loads(sys.stdin.read() or "{}")
|
|
146
|
+
if not isinstance(payload, dict):
|
|
147
|
+
payload = {}
|
|
148
|
+
except json.JSONDecodeError as exc:
|
|
149
|
+
print(json.dumps({"message": f"Ultra hook input parse error: {exc}"}))
|
|
150
|
+
return 0
|
|
151
|
+
try:
|
|
152
|
+
output = run_feature(feature, payload, feature_args)
|
|
153
|
+
except subprocess.TimeoutExpired:
|
|
154
|
+
output = {"message": f"Ultra hook {feature} timed out"}
|
|
155
|
+
except Exception as exc: # Kimi hooks are fail-open at this adapter boundary.
|
|
156
|
+
output = {"message": f"Ultra hook adapter error in {feature}: {exc}"}
|
|
157
|
+
print(json.dumps(output, ensure_ascii=False))
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
raise SystemExit(main())
|
|
@@ -9,7 +9,7 @@ const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
|
9
9
|
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
10
10
|
: path.resolve(__dirname, '..', '..');
|
|
11
11
|
const SCHEMA_FILE = path.join(REPO_ROOT, 'spec', 'schemas', 'state-db.sql');
|
|
12
|
-
const EXPECTED_VERSION = '9.
|
|
12
|
+
const EXPECTED_VERSION = '9.1';
|
|
13
13
|
|
|
14
14
|
const REQUIRED_TABLES = Object.freeze([
|
|
15
15
|
'tasks',
|
|
@@ -87,14 +87,97 @@ function applyCompatibleColumns(db) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function tableSupportsKimi(db, table) {
|
|
91
|
+
const row = db.prepare(
|
|
92
|
+
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
93
|
+
).get(table);
|
|
94
|
+
return typeof row?.sql === 'string' && row.sql.includes("'kimi'");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function upgradeRuntimeConstraints(db) {
|
|
98
|
+
if (tableSupportsKimi(db, 'events') && tableSupportsKimi(db, 'sessions')) return false;
|
|
99
|
+
const previousVersion = db.prepare(
|
|
100
|
+
'SELECT version FROM schema_version ORDER BY applied_at DESC LIMIT 1',
|
|
101
|
+
).get()?.version || 'unknown';
|
|
102
|
+
const foreignKeys = db.pragma('foreign_keys', { simple: true });
|
|
103
|
+
db.pragma('foreign_keys = OFF');
|
|
104
|
+
try {
|
|
105
|
+
db.transaction(() => {
|
|
106
|
+
db.exec(`
|
|
107
|
+
DROP TABLE IF EXISTS events_runtime_upgrade;
|
|
108
|
+
CREATE TABLE events_runtime_upgrade (
|
|
109
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
110
|
+
ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
111
|
+
type TEXT NOT NULL,
|
|
112
|
+
task_id TEXT,
|
|
113
|
+
change_id TEXT,
|
|
114
|
+
session_id TEXT,
|
|
115
|
+
runtime TEXT CHECK (runtime IS NULL OR runtime IN ('claude', 'opencode', 'codex', 'kimi')),
|
|
116
|
+
payload_json TEXT
|
|
117
|
+
);
|
|
118
|
+
INSERT INTO events_runtime_upgrade
|
|
119
|
+
(id, ts, type, task_id, change_id, session_id, runtime, payload_json)
|
|
120
|
+
SELECT id, ts, type, task_id, change_id, session_id, runtime, payload_json FROM events;
|
|
121
|
+
DROP TABLE events;
|
|
122
|
+
ALTER TABLE events_runtime_upgrade RENAME TO events;
|
|
123
|
+
CREATE INDEX events_ts_type ON events(ts, type);
|
|
124
|
+
CREATE INDEX events_task ON events(task_id, id);
|
|
125
|
+
CREATE INDEX events_session ON events(session_id, id);
|
|
126
|
+
CREATE INDEX events_change ON events(change_id, id);
|
|
127
|
+
|
|
128
|
+
DROP TABLE IF EXISTS sessions_runtime_upgrade;
|
|
129
|
+
CREATE TABLE sessions_runtime_upgrade (
|
|
130
|
+
sid TEXT PRIMARY KEY,
|
|
131
|
+
task_id TEXT NOT NULL REFERENCES tasks(id),
|
|
132
|
+
runtime TEXT NOT NULL CHECK (runtime IN ('claude', 'opencode', 'codex', 'kimi')),
|
|
133
|
+
pid INTEGER,
|
|
134
|
+
worktree_path TEXT NOT NULL,
|
|
135
|
+
artifact_dir TEXT NOT NULL,
|
|
136
|
+
status TEXT NOT NULL DEFAULT 'running'
|
|
137
|
+
CHECK (status IN ('running', 'completed', 'crashed', 'orphan')),
|
|
138
|
+
lease_expires_at TEXT NOT NULL,
|
|
139
|
+
heartbeat_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
140
|
+
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
141
|
+
);
|
|
142
|
+
INSERT INTO sessions_runtime_upgrade
|
|
143
|
+
(sid, task_id, runtime, pid, worktree_path, artifact_dir, status,
|
|
144
|
+
lease_expires_at, heartbeat_at, started_at)
|
|
145
|
+
SELECT sid, task_id, runtime, pid, worktree_path, artifact_dir, status,
|
|
146
|
+
lease_expires_at, heartbeat_at, started_at FROM sessions;
|
|
147
|
+
DROP TABLE sessions;
|
|
148
|
+
ALTER TABLE sessions_runtime_upgrade RENAME TO sessions;
|
|
149
|
+
CREATE INDEX sessions_active ON sessions(status, task_id);
|
|
150
|
+
CREATE INDEX sessions_lease ON sessions(lease_expires_at) WHERE status = 'running';
|
|
151
|
+
`);
|
|
152
|
+
db.prepare(
|
|
153
|
+
`INSERT INTO migration_history
|
|
154
|
+
(from_version, to_version, direction, status, notes)
|
|
155
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
156
|
+
).run(
|
|
157
|
+
previousVersion,
|
|
158
|
+
EXPECTED_VERSION,
|
|
159
|
+
'Add Kimi to durable event and session runtime constraints',
|
|
160
|
+
);
|
|
161
|
+
const violations = db.pragma('foreign_key_check');
|
|
162
|
+
if (violations.length > 0) {
|
|
163
|
+
throw new Error(`runtime constraint migration produced ${violations.length} foreign key violation(s)`);
|
|
164
|
+
}
|
|
165
|
+
})();
|
|
166
|
+
} finally {
|
|
167
|
+
db.pragma(`foreign_keys = ${foreignKeys ? 'ON' : 'OFF'}`);
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
90
172
|
function applyCompatibleUpgrades(db) {
|
|
173
|
+
upgradeRuntimeConstraints(db);
|
|
91
174
|
db.transaction(() => {
|
|
92
175
|
applyCompatibleColumns(db);
|
|
93
176
|
db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
|
|
94
177
|
db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
|
|
95
178
|
db.prepare(
|
|
96
179
|
'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
|
|
97
|
-
).run(EXPECTED_VERSION, '
|
|
180
|
+
).run(EXPECTED_VERSION, 'Kimi runtime support with durable event and session state');
|
|
98
181
|
})();
|
|
99
182
|
}
|
|
100
183
|
|
|
@@ -9,11 +9,13 @@ const path = require('node:path');
|
|
|
9
9
|
const {
|
|
10
10
|
EXPECTED_VERSION,
|
|
11
11
|
REQUIRED_TABLES,
|
|
12
|
+
SCHEMA_FILE,
|
|
12
13
|
initStateDb,
|
|
13
14
|
closeStateDb,
|
|
14
15
|
openStateDb,
|
|
15
16
|
tableNames,
|
|
16
17
|
} = require('./state-db.cjs');
|
|
18
|
+
const stateOps = require('./state-ops.cjs');
|
|
17
19
|
|
|
18
20
|
function tmpDbPath(prefix = 'ubp-state') {
|
|
19
21
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
|
|
@@ -114,3 +116,57 @@ test('Phase 8A.1 schema: tasks.parent_id column + tasks_parent partial index + s
|
|
|
114
116
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
115
117
|
}
|
|
116
118
|
});
|
|
119
|
+
|
|
120
|
+
test('initStateDb migrates existing runtime constraints to Kimi without losing references', () => {
|
|
121
|
+
const { dir, file } = tmpDbPath('ubp-kimi-runtime-upgrade');
|
|
122
|
+
try {
|
|
123
|
+
const legacy = openStateDb(file);
|
|
124
|
+
const legacySchema = fs.readFileSync(SCHEMA_FILE, 'utf8').replaceAll(", 'kimi'", '');
|
|
125
|
+
legacy.exec(legacySchema);
|
|
126
|
+
legacy.prepare("DELETE FROM schema_version WHERE version = '9.1'").run();
|
|
127
|
+
legacy.prepare(
|
|
128
|
+
"INSERT INTO tasks (id, title, type, priority) VALUES ('task-old', 'Old', 'feature', 'P1')",
|
|
129
|
+
).run();
|
|
130
|
+
legacy.prepare(
|
|
131
|
+
`INSERT INTO sessions
|
|
132
|
+
(sid, task_id, runtime, worktree_path, artifact_dir, lease_expires_at)
|
|
133
|
+
VALUES ('session-old', 'task-old', 'codex', '/tmp/worktree', '/tmp/artifacts', '2099-01-01T00:00:00.000Z')`,
|
|
134
|
+
).run();
|
|
135
|
+
legacy.prepare(
|
|
136
|
+
"INSERT INTO telemetry (session_id, event_type, tool_name) VALUES ('session-old', 'tool_call', 'task.list')",
|
|
137
|
+
).run();
|
|
138
|
+
legacy.prepare(
|
|
139
|
+
`INSERT INTO incidents
|
|
140
|
+
(id, code, severity, message, session_id)
|
|
141
|
+
VALUES ('incident-old', 'OLD', 'warning', 'preserve me', 'session-old')`,
|
|
142
|
+
).run();
|
|
143
|
+
legacy.prepare(
|
|
144
|
+
"INSERT INTO events (type, session_id, runtime) VALUES ('session_spawned', 'session-old', 'codex')",
|
|
145
|
+
).run();
|
|
146
|
+
closeStateDb(legacy);
|
|
147
|
+
|
|
148
|
+
const upgraded = initStateDb(file);
|
|
149
|
+
assert.equal(upgraded.schema_version, '9.1');
|
|
150
|
+
assert.equal(upgraded.db.prepare("SELECT runtime FROM sessions WHERE sid = 'session-old'").get().runtime, 'codex');
|
|
151
|
+
assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE session_id = 'session-old'").get().n, 1);
|
|
152
|
+
assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM incidents WHERE session_id = 'session-old'").get().n, 1);
|
|
153
|
+
assert.deepEqual(upgraded.db.pragma('foreign_key_check'), []);
|
|
154
|
+
|
|
155
|
+
upgraded.db.prepare(
|
|
156
|
+
"INSERT INTO tasks (id, title, type, priority) VALUES ('task-kimi', 'Kimi', 'feature', 'P1')",
|
|
157
|
+
).run();
|
|
158
|
+
const session = stateOps.createSession(upgraded.db, {
|
|
159
|
+
sid: 'session-kimi',
|
|
160
|
+
task_id: 'task-kimi',
|
|
161
|
+
runtime: 'kimi',
|
|
162
|
+
worktree_path: '/tmp/kimi-worktree',
|
|
163
|
+
artifact_dir: '/tmp/kimi-artifacts',
|
|
164
|
+
});
|
|
165
|
+
assert.equal(session.runtime, 'kimi');
|
|
166
|
+
const event = stateOps.appendEvent(upgraded.db, { type: 'kimi-ready', runtime: 'kimi' });
|
|
167
|
+
assert.ok(event.event_id > 0);
|
|
168
|
+
closeStateDb(upgraded.db);
|
|
169
|
+
} finally {
|
|
170
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
@@ -36,8 +36,8 @@ test('initProject copies bundled template into .ultra/', () => {
|
|
|
36
36
|
assert.ok(fs.existsSync(r.state_db_path));
|
|
37
37
|
const db = new Database(r.state_db_path, { readonly: true });
|
|
38
38
|
try {
|
|
39
|
-
const version = db.prepare("SELECT version FROM schema_version WHERE version = '9.
|
|
40
|
-
assert.equal(version.version, '9.
|
|
39
|
+
const version = db.prepare("SELECT version FROM schema_version WHERE version = '9.1'").get();
|
|
40
|
+
assert.equal(version.version, '9.1');
|
|
41
41
|
assert.ok(db.prepare("SELECT name FROM sqlite_master WHERE name = 'changes'").get());
|
|
42
42
|
} finally {
|
|
43
43
|
db.close();
|
|
@@ -8,7 +8,7 @@ const path = require('node:path');
|
|
|
8
8
|
|
|
9
9
|
const stateDb = require('../lib/state-db.cjs');
|
|
10
10
|
|
|
11
|
-
test('initStateDb upgrades a pre-9.
|
|
11
|
+
test('initStateDb upgrades a pre-9.1 database in place with task/event change linkage', () => {
|
|
12
12
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-db-upgrade-'));
|
|
13
13
|
const file = path.join(dir, 'state.db');
|
|
14
14
|
try {
|
|
@@ -59,8 +59,8 @@ test('initStateDb upgrades a pre-9.0 database in place with task/event change li
|
|
|
59
59
|
assert.ok(columns.includes('change_id'));
|
|
60
60
|
const eventColumns = upgraded.db.prepare('PRAGMA table_info(events)').all().map((row) => row.name);
|
|
61
61
|
assert.ok(eventColumns.includes('change_id'));
|
|
62
|
-
assert.equal(upgraded.schema_version, '9.
|
|
63
|
-
assert.ok(upgraded.db.prepare("SELECT 1 FROM schema_version WHERE version = '9.
|
|
62
|
+
assert.equal(upgraded.schema_version, '9.1');
|
|
63
|
+
assert.ok(upgraded.db.prepare("SELECT 1 FROM schema_version WHERE version = '9.1'").get());
|
|
64
64
|
stateDb.closeStateDb(upgraded.db);
|
|
65
65
|
} finally {
|
|
66
66
|
fs.rmSync(dir, { recursive: true, force: true });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra-builder-pro-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Multi-runtime installer for the Ultra Builder Pro agent engineering system — deploys commands, agents, skills, hooks, and MCP server to Claude Code, OpenCode, and
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Multi-runtime installer for the Ultra Builder Pro agent engineering system — deploys commands, agents, skills, hooks, and MCP server to Claude Code, OpenCode, Codex CLI, and Kimi Code.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ultra-builder-pro-cli": "bin/install.js",
|
|
7
7
|
"ubp": "bin/install.js",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"claude-code",
|
|
61
61
|
"opencode",
|
|
62
62
|
"codex",
|
|
63
|
+
"kimi-code",
|
|
63
64
|
"agent",
|
|
64
65
|
"cli",
|
|
65
66
|
"multi-runtime",
|
|
@@ -92,6 +93,7 @@
|
|
|
92
93
|
"verify:release": "npm run test:all && npm run test:hooks && npm audit --omit=dev --audit-level=high",
|
|
93
94
|
"prepublishOnly": "npm run verify:release",
|
|
94
95
|
"install:claude": "node bin/install.js --claude --local",
|
|
96
|
+
"install:kimi": "node bin/install.js --kimi --local",
|
|
95
97
|
"install:all": "node bin/install.js --all --local"
|
|
96
98
|
},
|
|
97
99
|
"dependencies": {
|