ultra-builder-pro-cli 0.25.0 → 0.26.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 (241) hide show
  1. package/.ultra-template/contexts/TEMPLATE.md +87 -0
  2. package/.ultra-template/decisions/.gitkeep +1 -0
  3. package/.ultra-template/evidence/.gitkeep +1 -0
  4. package/.ultra-template/north-star.md +46 -0
  5. package/.ultra-template/research/.gitkeep +1 -0
  6. package/.ultra-template/specs/discovery.md +2 -2
  7. package/.ultra-template/tasks.json +3 -0
  8. package/.ultra-template/test-report.json +27 -0
  9. package/CHANGELOG.md +49 -0
  10. package/README.md +217 -579
  11. package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
  12. package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
  13. package/adapters/_shared/file-ops.cjs +76 -2
  14. package/adapters/_shared/host-profile.cjs +97 -0
  15. package/adapters/_shared/plugin-core.cjs +204 -0
  16. package/adapters/_shared/runtime-assets.cjs +31 -96
  17. package/adapters/claude.js +92 -291
  18. package/adapters/codex.js +309 -842
  19. package/adapters/grok.js +143 -1469
  20. package/adapters/kimi.js +124 -1053
  21. package/adapters/opencode.js +230 -742
  22. package/bin/delegate-worker.cjs +323 -0
  23. package/bin/delegate.cjs +456 -0
  24. package/bin/install.js +38 -11
  25. package/docs/ARCHITECTURE.md +122 -216
  26. package/docs/ARTIFACT-AUTHORITY.md +90 -268
  27. package/docs/DECISIONS.md +75 -87
  28. package/docs/PHILOSOPHY.md +177 -0
  29. package/docs/PLUGIN-ISOLATION-CONTRACT.md +62 -39
  30. package/docs/RUNTIME-COMPAT-MATRIX.md +99 -156
  31. package/docs/SKILL-AUTHORING.md +94 -73
  32. package/docs/WORKFLOW-LIFECYCLE.md +93 -189
  33. package/hooks/README.md +17 -37
  34. package/hooks/_common.py +113 -0
  35. package/hooks/adapters/codex.py +9 -16
  36. package/hooks/adapters/grok.py +5 -5
  37. package/hooks/adapters/kimi.py +8 -14
  38. package/hooks/block_dangerous_commands.py +95 -0
  39. package/hooks/compact_context.py +65 -0
  40. package/hooks/mid_workflow_recall.py +25 -0
  41. package/hooks/post_edit_guard.py +105 -0
  42. package/hooks/session_context.py +30 -0
  43. package/package.json +17 -52
  44. package/skills/ultra-change/SKILL.md +97 -50
  45. package/skills/ultra-change/references/change-contract.md +40 -1
  46. package/skills/ultra-delegate/SKILL.md +82 -0
  47. package/skills/ultra-delegate/references/delegation-contract.md +51 -0
  48. package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
  49. package/skills/ultra-deliver/SKILL.md +61 -42
  50. package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
  51. package/skills/ultra-dev/SKILL.md +96 -62
  52. package/skills/ultra-dev/references/debugging.md +22 -0
  53. package/skills/ultra-domain-modeling/SKILL.md +79 -0
  54. package/skills/ultra-grilling/SKILL.md +79 -0
  55. package/skills/ultra-init/SKILL.md +94 -37
  56. package/skills/ultra-init/references/brownfield-adoption.md +33 -83
  57. package/skills/ultra-init/scripts/init_project.cjs +84 -0
  58. package/skills/ultra-plan/SKILL.md +88 -68
  59. package/skills/ultra-research/SKILL.md +62 -48
  60. package/skills/ultra-research/references/00-problem-validation.md +5 -5
  61. package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
  62. package/skills/ultra-research/references/02-market-assessment.md +5 -4
  63. package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
  64. package/skills/ultra-research/references/04-product-strategy.md +3 -3
  65. package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
  66. package/skills/ultra-research/references/10-user-personas.md +3 -2
  67. package/skills/ultra-research/references/11-user-scenarios.md +3 -3
  68. package/skills/ultra-research/references/20-user-stories.md +4 -3
  69. package/skills/ultra-research/references/21-features-scope.md +3 -2
  70. package/skills/ultra-research/references/22-success-metrics.md +3 -3
  71. package/skills/ultra-research/references/30-architecture-context.md +3 -3
  72. package/skills/ultra-research/references/31-solution-strategy.md +3 -3
  73. package/skills/ultra-research/references/32-building-blocks.md +3 -3
  74. package/skills/ultra-research/references/40-deployment.md +3 -3
  75. package/skills/ultra-research/references/41-quality-risks.md +3 -3
  76. package/skills/ultra-research/references/99-synthesis.md +7 -7
  77. package/skills/ultra-review/SKILL.md +59 -32
  78. package/skills/ultra-review/references/code.md +17 -0
  79. package/skills/ultra-review/references/comments.md +12 -0
  80. package/skills/ultra-review/references/design.md +12 -0
  81. package/skills/ultra-review/references/errors.md +13 -0
  82. package/skills/ultra-review/references/spec.md +13 -0
  83. package/skills/ultra-review/references/tests.md +12 -0
  84. package/skills/ultra-review/references/worker-packet.md +40 -0
  85. package/skills/ultra-review/scripts/review_wait.py +26 -15
  86. package/skills/ultra-status/SKILL.md +61 -36
  87. package/skills/ultra-tdd/SKILL.md +81 -0
  88. package/skills/ultra-tdd/references/templates/README.md +20 -0
  89. package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
  90. package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
  91. package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
  92. package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
  93. package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
  94. package/skills/ultra-tdd/references/test-execution.md +18 -0
  95. package/skills/ultra-test/SKILL.md +70 -30
  96. package/skills/ultra-test/references/export-syntax.md +18 -0
  97. package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
  98. package/skills/ultra-think/SKILL.md +49 -27
  99. package/skills/ultra-think/references/autonomy-boundary.md +48 -0
  100. package/skills/ultra-think/references/interaction-boundary.md +18 -33
  101. package/.ultra-template/docs/research/README.md +0 -55
  102. package/.ultra-template/reports/templates/delivery-report.json +0 -17
  103. package/.ultra-template/reports/templates/test-report.json +0 -18
  104. package/.ultra-template/tasks/tasks.json +0 -15
  105. package/.ultra-template/templates/task-context.md +0 -19
  106. package/adapters/_shared/codex-assets.cjs +0 -725
  107. package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
  108. package/adapters/_shared/interaction-contract.cjs +0 -129
  109. package/adapters/_shared/path-rewrite.cjs +0 -79
  110. package/adapters/_shared/runtime-native.cjs +0 -132
  111. package/adapters/_shared/settings-merge.cjs +0 -76
  112. package/agents/code-reviewer.md +0 -42
  113. package/agents/debugger.md +0 -40
  114. package/agents/review-code.md +0 -49
  115. package/agents/review-comments.md +0 -38
  116. package/agents/review-coordinator.md +0 -56
  117. package/agents/review-design.md +0 -40
  118. package/agents/review-errors.md +0 -43
  119. package/agents/review-spec.md +0 -39
  120. package/agents/review-tests.md +0 -40
  121. package/agents/tdd-runner.md +0 -38
  122. package/bin/orchestrator.js +0 -383
  123. package/commands/ultra-change.md +0 -10
  124. package/commands/ultra-deliver.md +0 -10
  125. package/commands/ultra-dev.md +0 -10
  126. package/commands/ultra-doctor.md +0 -10
  127. package/commands/ultra-init.md +0 -10
  128. package/commands/ultra-plan.md +0 -10
  129. package/commands/ultra-research.md +0 -10
  130. package/commands/ultra-review.md +0 -10
  131. package/commands/ultra-status.md +0 -10
  132. package/commands/ultra-test.md +0 -10
  133. package/commands/ultra-think.md +0 -10
  134. package/docs/AGENT-CONTEXT.md +0 -191
  135. package/docs/COMMIT-HASH-BACKFILL.md +0 -51
  136. package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
  137. package/docs/ROADMAP.md +0 -101
  138. package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
  139. package/hooks/active_task_context.py +0 -152
  140. package/hooks/context_envelope.py +0 -100
  141. package/hooks/health_check.py +0 -196
  142. package/hooks/pre_stop_check.py +0 -66
  143. package/hooks/runtime_paths.py +0 -396
  144. package/hooks/subagent_tracker.py +0 -88
  145. package/hooks/workflow_checkpoint.py +0 -76
  146. package/hooks/workflow_context.py +0 -48
  147. package/hooks/workflow_resume.py +0 -84
  148. package/mcp-server/hook-context.cjs +0 -38
  149. package/mcp-server/hook-event.cjs +0 -34
  150. package/mcp-server/lib/archive-journal.cjs +0 -1119
  151. package/mcp-server/lib/archive-mutation-worker.py +0 -365
  152. package/mcp-server/lib/artifact-registry.cjs +0 -1232
  153. package/mcp-server/lib/baseline-workflow.cjs +0 -1193
  154. package/mcp-server/lib/canonical-json.cjs +0 -19
  155. package/mcp-server/lib/change-authority.cjs +0 -26
  156. package/mcp-server/lib/change-packet.cjs +0 -790
  157. package/mcp-server/lib/change-workflow.cjs +0 -65
  158. package/mcp-server/lib/context-envelope.cjs +0 -581
  159. package/mcp-server/lib/context-paths.cjs +0 -142
  160. package/mcp-server/lib/decision-records.cjs +0 -423
  161. package/mcp-server/lib/delivery-transaction.cjs +0 -471
  162. package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
  163. package/mcp-server/lib/doctor.cjs +0 -627
  164. package/mcp-server/lib/git-bootstrap.cjs +0 -651
  165. package/mcp-server/lib/init-project.cjs +0 -1021
  166. package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
  167. package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
  168. package/mcp-server/lib/managed-file-write.cjs +0 -172
  169. package/mcp-server/lib/plan-checkpoint.cjs +0 -177
  170. package/mcp-server/lib/plan-publication-worker.cjs +0 -830
  171. package/mcp-server/lib/plan-store.cjs +0 -794
  172. package/mcp-server/lib/prd-parser.cjs +0 -83
  173. package/mcp-server/lib/pricing.cjs +0 -44
  174. package/mcp-server/lib/project-context.cjs +0 -178
  175. package/mcp-server/lib/projector.cjs +0 -386
  176. package/mcp-server/lib/provider-refs.cjs +0 -42
  177. package/mcp-server/lib/runtime-paths.cjs +0 -1687
  178. package/mcp-server/lib/runtime-state.cjs +0 -222
  179. package/mcp-server/lib/safe-project-file.cjs +0 -385
  180. package/mcp-server/lib/stage-checkpoints.cjs +0 -463
  181. package/mcp-server/lib/state-authority.cjs +0 -95
  182. package/mcp-server/lib/state-db.cjs +0 -1580
  183. package/mcp-server/lib/state-ops.cjs +0 -1428
  184. package/mcp-server/lib/task-contract.cjs +0 -404
  185. package/mcp-server/lib/task-expander.cjs +0 -105
  186. package/mcp-server/lib/task-ledger.cjs +0 -1964
  187. package/mcp-server/lib/telemetry.cjs +0 -85
  188. package/mcp-server/lib/topo.cjs +0 -131
  189. package/mcp-server/lib/ultra-facade.cjs +0 -2684
  190. package/mcp-server/lib/worker-packet.cjs +0 -358
  191. package/mcp-server/server.cjs +0 -437
  192. package/orchestrator/auto-merge.cjs +0 -128
  193. package/orchestrator/daemon.cjs +0 -264
  194. package/orchestrator/dispatch-rules.cjs +0 -78
  195. package/orchestrator/parallel-orchestrator.cjs +0 -903
  196. package/orchestrator/planner/plan-builder.cjs +0 -125
  197. package/orchestrator/recovery.cjs +0 -188
  198. package/orchestrator/session-close-journal-worker.cjs +0 -5
  199. package/orchestrator/session-close-journal.cjs +0 -1014
  200. package/orchestrator/session-runner.cjs +0 -1153
  201. package/orchestrator/worktree-manager.cjs +0 -282
  202. package/settings.json +0 -7
  203. package/skills/cc-collab/SKILL.md +0 -42
  204. package/skills/code-review-expert/SKILL.md +0 -43
  205. package/skills/code-review-expert/references/correctness-reliability.md +0 -30
  206. package/skills/code-review-expert/references/design-boundaries.md +0 -26
  207. package/skills/code-review-expert/references/removal-plan.md +0 -24
  208. package/skills/codex-collab/SKILL.md +0 -49
  209. package/skills/codex-collab/evals/evals.json +0 -35
  210. package/skills/integration-rules/SKILL.md +0 -32
  211. package/skills/security-rules/SKILL.md +0 -38
  212. package/skills/testing-rules/SKILL.md +0 -32
  213. package/skills/ultra-doctor/SKILL.md +0 -41
  214. package/skills/ultra-plan/references/semantic-preflight.md +0 -20
  215. package/skills/ultra-verify/SKILL.md +0 -46
  216. package/skills/ultra-verify/evals/evals.json +0 -46
  217. package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
  218. package/skills/ultra-verify/references/evidence-status.md +0 -21
  219. package/skills/ultra-verify/scripts/verify_wait.py +0 -94
  220. package/spec/cli-protocol.md +0 -102
  221. package/spec/mcp-tools.yaml +0 -312
  222. package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
  223. package/spec/schemas/change-delta.v1.schema.json +0 -153
  224. package/spec/schemas/command-manifest.schema.json +0 -29
  225. package/spec/schemas/context-file.v4.5.schema.json +0 -54
  226. package/spec/schemas/delivery-report.v1.schema.json +0 -66
  227. package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
  228. package/spec/schemas/mcp-tools.schema.json +0 -136
  229. package/spec/schemas/skill-manifest.schema.json +0 -24
  230. package/spec/schemas/state-db.sql +0 -641
  231. package/spec/schemas/task-ledger.v2.schema.json +0 -296
  232. package/spec/schemas/tasks.v4.5.schema.json +0 -96
  233. package/spec/schemas/test-report.v1.schema.json +0 -225
  234. package/ultra-tools/cli.cjs +0 -129
  235. package/ultra-tools/commands/db.cjs +0 -226
  236. package/ultra-tools/commands/legacy-memory.cjs +0 -159
  237. package/ultra-tools/commands/migrate.cjs +0 -1386
  238. package/ultra-tools/commands/session.cjs +0 -213
  239. package/ultra-tools/commands/status.cjs +0 -559
  240. package/ultra-tools/commands/system.cjs +0 -498
  241. package/ultra-tools/commands/task.cjs +0 -105
@@ -0,0 +1,113 @@
1
+ """Small file-first helpers shared by Ultra hooks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ def read_payload() -> dict[str, Any]:
14
+ try:
15
+ value = json.loads(sys.stdin.read() or "{}")
16
+ except json.JSONDecodeError:
17
+ return {}
18
+ return value if isinstance(value, dict) else {}
19
+
20
+
21
+ def project_root(payload: dict[str, Any]) -> Path | None:
22
+ candidate = payload.get("cwd") or payload.get("workspace_root") or os.getcwd()
23
+ path = Path(str(candidate)).resolve()
24
+ if path.is_file():
25
+ path = path.parent
26
+ for current in (path, *path.parents):
27
+ if (current / ".ultra").is_dir():
28
+ return current
29
+ return None
30
+
31
+
32
+ def markdown_section(text: str, heading: str) -> str:
33
+ pattern = re.compile(
34
+ rf"^##\s+{re.escape(heading)}\s*$\n(?P<body>.*?)(?=^##\s+|\Z)",
35
+ re.MULTILINE | re.DOTALL | re.IGNORECASE,
36
+ )
37
+ match = pattern.search(text)
38
+ return match.group("body").strip() if match else ""
39
+
40
+
41
+ def read_tasks(root: Path) -> list[dict[str, Any]]:
42
+ try:
43
+ value = json.loads((root / ".ultra" / "tasks.json").read_text(encoding="utf-8"))
44
+ except (OSError, json.JSONDecodeError):
45
+ return []
46
+ tasks = value.get("tasks", []) if isinstance(value, dict) else value
47
+ return [item for item in tasks if isinstance(item, dict)] if isinstance(tasks, list) else []
48
+
49
+
50
+ def current_task(root: Path) -> dict[str, Any] | None:
51
+ tasks = read_tasks(root)
52
+ for status in ("in_progress", "pending"):
53
+ for task in tasks:
54
+ if task.get("status") == status:
55
+ return task
56
+ return None
57
+
58
+
59
+ def project_file(root: Path, value: str, *, ultra_default: bool = False) -> Path | None:
60
+ relative = Path(value)
61
+ if relative.is_absolute():
62
+ return None
63
+ ultra_shorthands = {"changes", "contexts", "decisions", "evidence", "progress", "reviews", "specs"}
64
+ first = relative.parts[0] if relative.parts else ""
65
+ if first == ".ultra":
66
+ base = root
67
+ elif ultra_default and first in ultra_shorthands:
68
+ base = root / ".ultra"
69
+ else:
70
+ base = root
71
+ candidate = (base / relative).resolve()
72
+ try:
73
+ candidate.relative_to(root.resolve())
74
+ except ValueError:
75
+ return None
76
+ return candidate
77
+
78
+
79
+ def context_path(root: Path, task: dict[str, Any]) -> Path | None:
80
+ value = task.get("context_file")
81
+ if not isinstance(value, str) or not value:
82
+ return None
83
+ return project_file(root, value, ultra_default=True)
84
+
85
+
86
+ def task_sections(root: Path, task: dict[str, Any]) -> dict[str, str]:
87
+ file = context_path(root, task)
88
+ try:
89
+ text = file.read_text(encoding="utf-8") if file else ""
90
+ except OSError:
91
+ text = ""
92
+ return {
93
+ "acceptance": markdown_section(text, "Acceptance Criteria"),
94
+ "resume": markdown_section(text, "Resume Note"),
95
+ }
96
+
97
+
98
+ def emit_context(event: str, text: str) -> None:
99
+ if not text.strip():
100
+ return
101
+ print(json.dumps({
102
+ "hookSpecificOutput": {
103
+ "hookEventName": event,
104
+ "additionalContext": text.strip(),
105
+ }
106
+ }))
107
+
108
+
109
+ def write_json_atomic(file: Path, payload: dict[str, Any]) -> None:
110
+ file.parent.mkdir(parents=True, exist_ok=True)
111
+ temporary = file.with_suffix(f"{file.suffix}.tmp-{os.getpid()}")
112
+ temporary.write_text(f"{json.dumps(payload, indent=2)}\n", encoding="utf-8")
113
+ temporary.replace(file)
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env python3
2
- """Codex wire adapter for Ultra Builder Pro hooks.
2
+ """Codex wire adapter for Ultra Builder Pro's five file-first hooks.
3
3
 
4
4
  Codex and Claude Code share the JSON-on-stdin command-hook shape, but differ
5
5
  at three important boundaries:
6
6
 
7
7
  - Codex emits ``apply_patch`` with the patch text in ``tool_input.command``;
8
- legacy Ultra edit hooks expect one ``Edit`` event per ``file_path``.
8
+ the post-edit sensor expects one ``Edit`` event per ``file_path``.
9
9
  - Codex rejects ``permissionDecision: ask`` in PreToolUse output. Ultra warning
10
10
  decisions therefore become advisory ``systemMessage`` values.
11
11
  - Each event has a strict output schema. This adapter removes foreign fields
@@ -30,13 +30,11 @@ from typing import Any
30
30
 
31
31
  HOOK_ROOT = Path(__file__).resolve().parent.parent
32
32
  ALLOWED_FEATURES = {
33
- "active_task_context.py",
34
- "health_check.py",
35
- "pre_stop_check.py",
36
- "subagent_tracker.py",
37
- "workflow_checkpoint.py",
38
- "workflow_context.py",
39
- "workflow_resume.py",
33
+ "block_dangerous_commands.py",
34
+ "compact_context.py",
35
+ "mid_workflow_recall.py",
36
+ "post_edit_guard.py",
37
+ "session_context.py",
40
38
  }
41
39
  UNIVERSAL_OUTPUT_FIELDS = {"continue", "stopReason", "suppressOutput", "systemMessage"}
42
40
  PATCH_FILE_RE = re.compile(r"^\*\*\* (?:Add|Update|Delete) File: (.+?)\s*$", re.MULTILINE)
@@ -58,7 +56,7 @@ def _patch_paths(tool_input: Any) -> list[str]:
58
56
 
59
57
 
60
58
  def normalize_inputs(payload: dict[str, Any]) -> list[dict[str, Any]]:
61
- """Return legacy-compatible input(s) for one Codex hook payload."""
59
+ """Return file-first hook input(s) for one Codex hook payload."""
62
60
  normalized = copy.deepcopy(payload)
63
61
  if normalized.get("tool_response") is not None and normalized.get("tool_output") is None:
64
62
  normalized["tool_output"] = normalized["tool_response"]
@@ -90,7 +88,7 @@ def _universal(output: dict[str, Any]) -> dict[str, Any]:
90
88
 
91
89
 
92
90
  def adapt_output(output: dict[str, Any], event: str) -> dict[str, Any]:
93
- """Convert a legacy hook result to the strict Codex event output schema."""
91
+ """Convert a shared hook result to the strict Codex event output schema."""
94
92
  if not isinstance(output, dict):
95
93
  return {}
96
94
  result = _universal(output)
@@ -239,11 +237,6 @@ def run_feature(feature: str, payload: dict[str, Any], feature_args: list[str])
239
237
  return {"systemMessage": f"Ultra hook feature is missing: {feature}"}
240
238
 
241
239
  event = str(payload.get("hook_event_name") or "")
242
- if feature == "workflow_resume.py" and event == "PostCompact":
243
- payload = dict(payload)
244
- payload["hook_event_name"] = "SessionStart"
245
- payload["source"] = "compact"
246
-
247
240
  env = os.environ.copy()
248
241
  env["UBP_HOOK_RUNTIME"] = "codex"
249
242
  plugin_data = env.get("PLUGIN_DATA") or env.get("CLAUDE_PLUGIN_DATA")
@@ -14,11 +14,11 @@ from typing import Any
14
14
 
15
15
  HOOK_ROOT = Path(__file__).resolve().parent.parent
16
16
  ALLOWED_FEATURES = {
17
- "active_task_context.py",
18
- "health_check.py",
19
- "pre_stop_check.py",
20
- "subagent_tracker.py",
21
- "workflow_checkpoint.py",
17
+ "block_dangerous_commands.py",
18
+ "compact_context.py",
19
+ "mid_workflow_recall.py",
20
+ "post_edit_guard.py",
21
+ "session_context.py",
22
22
  }
23
23
 
24
24
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
- """Kimi Code wire adapter for Ultra Builder Pro lifecycle hooks.
2
+ """Kimi Code wire adapter for Ultra Builder Pro's five file-first hooks.
3
3
 
4
- Kimi 0.26 uses snake_case JSON input compatible with the workflow hooks, but
4
+ Kimi uses snake_case JSON input compatible with the shared hooks, but
5
5
  its output contract accepts only ``message`` plus an optional structured deny.
6
6
  This adapter preserves explicit projection denials and optional Stop decisions, converts foreign context
7
7
  fields into Kimi messages, and fails open with diagnostic evidence.
@@ -20,18 +20,16 @@ from typing import Any
20
20
 
21
21
  HOOK_ROOT = Path(__file__).resolve().parent.parent
22
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",
23
+ "block_dangerous_commands.py",
24
+ "compact_context.py",
25
+ "mid_workflow_recall.py",
26
+ "post_edit_guard.py",
27
+ "session_context.py",
30
28
  }
31
29
 
32
30
 
33
31
  def normalize_input(payload: dict[str, Any]) -> dict[str, Any]:
34
- """Add only the legacy aliases required by the shared workflow hooks."""
32
+ """Add only aliases required by the shared file-first hooks."""
35
33
  normalized = copy.deepcopy(payload)
36
34
  agent_name = normalized.get("agent_name")
37
35
  if isinstance(agent_name, str) and agent_name:
@@ -101,10 +99,6 @@ def run_feature(feature: str, payload: dict[str, Any], feature_args: list[str])
101
99
 
102
100
  event = str(payload.get("hook_event_name") or "")
103
101
  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
102
  env = os.environ.copy()
109
103
  env["UBP_HOOK_RUNTIME"] = "kimi"
110
104
  plugin_data = env.get("PLUGIN_DATA") or env.get("KIMI_PLUGIN_ROOT")
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ """Block a narrow set of externally destructive shell effects."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import re
10
+ import subprocess
11
+
12
+ from _common import emit_context, project_root, read_payload
13
+
14
+
15
+ def protected_push(command: str, root) -> bool:
16
+ if not re.search(r"\bgit\s+push\b", command, re.IGNORECASE):
17
+ return False
18
+ if re.search(r"\b(?:main|master|production|prod)\b", command, re.IGNORECASE):
19
+ return True
20
+ result = subprocess.run(
21
+ ["git", "branch", "--show-current"], cwd=root, text=True,
22
+ capture_output=True, check=False, timeout=3,
23
+ )
24
+ return result.stdout.strip() in {"main", "master", "production", "prod"}
25
+
26
+
27
+ def classify(command: str, root) -> str | None:
28
+ if protected_push(command, root):
29
+ return "protected branch push"
30
+ if re.search(r"\b(?:drop\s+(?:table|database|schema)|truncate\s+(?:table\s+)?)\b", command, re.IGNORECASE):
31
+ return "destructive database operation"
32
+ if re.search(r"\b(?:cast\s+send|solana\s+transfer|bitcoin-cli\s+send|near\s+send|aptos\s+move\s+run)\b", command, re.IGNORECASE):
33
+ return "funds or on-chain transaction"
34
+ if re.search(
35
+ r"\b(?:API_KEY|ACCESS_TOKEN|AUTH_TOKEN|SECRET|PASSWORD)\s*=\s*['\"]?(?!\$|\$\{)[^\s'\"]{8,}",
36
+ command,
37
+ re.IGNORECASE,
38
+ ):
39
+ return "hard-coded credential"
40
+ if re.search(r"\beval\s+['\"]?\$|\bexec\s*\([^)]*\binput\s*\(", command, re.IGNORECASE):
41
+ return "user-controlled code execution"
42
+ return None
43
+
44
+
45
+ def advisory(command: str) -> str | None:
46
+ if re.search(
47
+ r"\b(?:alembic\s+upgrade|prisma\s+migrate|knex\s+migrate|sequelize\s+db:migrate|rails\s+db:migrate|manage\.py\s+migrate)\b",
48
+ command,
49
+ re.IGNORECASE,
50
+ ):
51
+ return (
52
+ "Ultra observed a database migration command. Confirm the target environment, "
53
+ "backup or rollback path, and owner authorization before any irreversible effect."
54
+ )
55
+ return None
56
+
57
+
58
+ def main() -> int:
59
+ payload = read_payload()
60
+ root = project_root(payload) # Idle guard: no `.ultra/` means silent exit.
61
+ if root is None:
62
+ return 0
63
+ tool_input = payload.get("tool_input")
64
+ tool_input = tool_input if isinstance(tool_input, dict) else {}
65
+ command = tool_input.get("command")
66
+ if not isinstance(command, str) or not command.strip():
67
+ return 0
68
+ threat = classify(command, root)
69
+ if threat is None:
70
+ note = advisory(command)
71
+ if note:
72
+ emit_context("PreToolUse", note)
73
+ return 0
74
+ digest = hashlib.sha256(command.encode()).hexdigest()
75
+ if os.environ.get("UBP_DANGEROUS_COMMAND_APPROVED") == digest:
76
+ return 0
77
+ reason = (
78
+ f"Ultra blocked {threat}. Protected effect: the exact shell command. "
79
+ f"After explicit owner authorization, rerun with "
80
+ f"UBP_DANGEROUS_COMMAND_APPROVED={digest}."
81
+ )
82
+ print(json.dumps({
83
+ "decision": "block",
84
+ "reason": reason,
85
+ "hookSpecificOutput": {
86
+ "hookEventName": "PreToolUse",
87
+ "permissionDecision": "deny",
88
+ "permissionDecisionReason": reason,
89
+ },
90
+ }))
91
+ return 0
92
+
93
+
94
+ if __name__ == "__main__":
95
+ raise SystemExit(main())
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python3
2
+ """Save and restore a disposable compact snapshot from canonical files."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import subprocess
7
+ from datetime import datetime, timezone
8
+
9
+ from _common import current_task, emit_context, project_root, read_payload, task_sections
10
+
11
+
12
+ def git_output(root, *args: str) -> str:
13
+ result = subprocess.run(
14
+ ["git", *args], cwd=root, text=True, capture_output=True, check=False, timeout=5
15
+ )
16
+ return result.stdout.strip() if result.returncode == 0 else "unavailable"
17
+
18
+
19
+ def save_snapshot(root) -> None:
20
+ task = current_task(root)
21
+ sections = task_sections(root, task) if task else {"acceptance": "", "resume": ""}
22
+ text = [
23
+ "# Ultra Compact Snapshot",
24
+ "",
25
+ f"Generated: {datetime.now(timezone.utc).isoformat()}",
26
+ f"HEAD: {git_output(root, 'rev-parse', 'HEAD')}",
27
+ f"Task: {task.get('id') if task else 'none'}",
28
+ "",
29
+ "## Acceptance Criteria",
30
+ sections["acceptance"] or "_(none)_",
31
+ "",
32
+ "## Resume Note",
33
+ sections["resume"] or "_(none)_",
34
+ "",
35
+ "## Worktree",
36
+ "```text",
37
+ git_output(root, "status", "--short"),
38
+ "```",
39
+ "",
40
+ ]
41
+ file = root / ".ultra" / ".runtime" / "compact-snapshot.md"
42
+ file.parent.mkdir(parents=True, exist_ok=True)
43
+ file.write_text("\n".join(text), encoding="utf-8")
44
+
45
+
46
+ def main() -> int:
47
+ payload = read_payload()
48
+ root = project_root(payload) # Idle guard: no `.ultra/` means silent exit.
49
+ if root is None:
50
+ return 0
51
+ event = str(payload.get("hook_event_name") or payload.get("hookEventName") or "")
52
+ if event == "PreCompact":
53
+ save_snapshot(root)
54
+ return 0
55
+ if event in {"PostCompact", "SessionStart"} and (
56
+ event == "PostCompact" or payload.get("source") == "compact"
57
+ ):
58
+ file = root / ".ultra" / ".runtime" / "compact-snapshot.md"
59
+ if file.is_file():
60
+ emit_context("SessionStart", file.read_text(encoding="utf-8"))
61
+ return 0
62
+
63
+
64
+ if __name__ == "__main__":
65
+ raise SystemExit(main())
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ """Recall current acceptance before a source-reading or editing tool call."""
3
+
4
+ from _common import current_task, emit_context, project_root, read_payload, task_sections
5
+
6
+
7
+ def main() -> int:
8
+ payload = read_payload()
9
+ root = project_root(payload) # Idle guard: no `.ultra/` means silent exit.
10
+ if root is None:
11
+ return 0
12
+ task = current_task(root)
13
+ if not task:
14
+ return 0
15
+ acceptance = task_sections(root, task)["acceptance"]
16
+ if acceptance:
17
+ emit_context(
18
+ "PreToolUse",
19
+ f"Ultra task {task.get('id')} acceptance reminder:\n{acceptance}",
20
+ )
21
+ return 0
22
+
23
+
24
+ if __name__ == "__main__":
25
+ raise SystemExit(main())
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ """Record mechanical task evidence observations after an edit."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import re
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+
11
+ from _common import (
12
+ current_task,
13
+ emit_context,
14
+ project_file,
15
+ project_root,
16
+ read_payload,
17
+ write_json_atomic,
18
+ )
19
+
20
+
21
+ DIMENSIONS = (
22
+ "tests_written",
23
+ "tests_passed",
24
+ "persistence_real",
25
+ "feature_flags_audit",
26
+ "vertical_slice",
27
+ "spec_trace",
28
+ )
29
+
30
+
31
+ def slug(value: str) -> str:
32
+ return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
33
+
34
+
35
+ def trace_observation(root: Path, task: dict) -> str:
36
+ trace = task.get("trace_to")
37
+ if not isinstance(trace, str) or "#" not in trace:
38
+ return "missing"
39
+ name, anchor = trace.rsplit("#", 1)
40
+ file = project_file(root, name, ultra_default=True)
41
+ if file is None:
42
+ return "missing"
43
+ try:
44
+ headings = re.findall(r"^#{1,6}\s+(.+?)\s*$", file.read_text(encoding="utf-8"), re.MULTILINE)
45
+ except OSError:
46
+ return "missing"
47
+ return "observed" if slug(anchor) in {slug(heading) for heading in headings} else "missing"
48
+
49
+
50
+ def main() -> int:
51
+ payload = read_payload()
52
+ root = project_root(payload) # Idle guard: no `.ultra/` means silent exit.
53
+ if root is None:
54
+ return 0
55
+ task = current_task(root)
56
+ if not task:
57
+ return 0
58
+ tool_input = payload.get("tool_input")
59
+ tool_input = tool_input if isinstance(tool_input, dict) else {}
60
+ changed = str(tool_input.get("file_path") or tool_input.get("path") or "")
61
+ relative = ""
62
+ if changed:
63
+ try:
64
+ relative = str(Path(changed).resolve().relative_to(root))
65
+ except ValueError:
66
+ relative = ""
67
+
68
+ file = root / ".ultra" / "progress" / f"{task.get('id')}.json"
69
+ try:
70
+ progress = json.loads(file.read_text(encoding="utf-8"))
71
+ except (OSError, json.JSONDecodeError):
72
+ progress = {}
73
+ if not isinstance(progress, dict):
74
+ progress = {}
75
+ existing_evidence = progress.get("evidence")
76
+ existing_evidence = existing_evidence if isinstance(existing_evidence, dict) else {}
77
+ progress["task_id"] = str(task.get("id"))
78
+ progress["evidence"] = {
79
+ name: existing_evidence.get(name, "unknown")
80
+ if isinstance(existing_evidence.get(name, "unknown"), str)
81
+ else "unknown"
82
+ for name in DIMENSIONS
83
+ }
84
+ touched_files = progress.get("touched_files")
85
+ progress["touched_files"] = list(dict.fromkeys(
86
+ item for item in touched_files if isinstance(item, str) and item
87
+ )) if isinstance(touched_files, list) else []
88
+ if relative and relative not in progress["touched_files"]:
89
+ progress["touched_files"].append(relative)
90
+ if re.search(r"(?:^|/)(?:test|tests|__tests__)/|(?:\.test|\.spec)\.", relative, re.IGNORECASE):
91
+ progress["evidence"]["tests_written"] = "observed"
92
+ progress["evidence"]["spec_trace"] = trace_observation(root, task)
93
+ progress["updated_at"] = datetime.now(timezone.utc).isoformat()
94
+ write_json_atomic(file, progress)
95
+
96
+ unknown = [name for name, value in progress["evidence"].items() if value == "unknown"]
97
+ emit_context(
98
+ "PostToolUse",
99
+ f"Ultra evidence sensor updated task {task.get('id')}. Still unobserved: {', '.join(unknown) or 'none'}.",
100
+ )
101
+ return 0
102
+
103
+
104
+ if __name__ == "__main__":
105
+ raise SystemExit(main())
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ """Inject the one-line goal and current task acceptance at session start."""
3
+
4
+ from _common import current_task, emit_context, markdown_section, project_root, read_payload, task_sections
5
+
6
+
7
+ def main() -> int:
8
+ payload = read_payload()
9
+ root = project_root(payload) # Idle guard: no `.ultra/` means silent exit.
10
+ if root is None:
11
+ return 0
12
+ try:
13
+ north_star = (root / ".ultra" / "north-star.md").read_text(encoding="utf-8")
14
+ except OSError:
15
+ north_star = ""
16
+ parts = []
17
+ one_line = markdown_section(north_star, "One-line")
18
+ if one_line:
19
+ parts.append(f"Ultra north star:\n{one_line}")
20
+ task = current_task(root)
21
+ if task:
22
+ acceptance = task_sections(root, task)["acceptance"]
23
+ if acceptance:
24
+ parts.append(f"Current task {task.get('id')} acceptance:\n{acceptance}")
25
+ emit_context("SessionStart", "\n\n".join(parts))
26
+ return 0
27
+
28
+
29
+ if __name__ == "__main__":
30
+ raise SystemExit(main())