ultra-builder-pro-cli 0.5.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 (206) hide show
  1. package/.ultra-template/delivery-report.json +9 -0
  2. package/.ultra-template/docs/research/README.md +53 -0
  3. package/.ultra-template/specs/architecture.md +250 -0
  4. package/.ultra-template/specs/discovery.md +248 -0
  5. package/.ultra-template/specs/product.md +223 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +57 -0
  7. package/.ultra-template/tasks/tasks.json +33 -0
  8. package/.ultra-template/test-report.json +8 -0
  9. package/CHANGELOG.md +194 -0
  10. package/LICENSE +21 -0
  11. package/README.md +187 -0
  12. package/adapters/_shared/codex-assets.cjs +1128 -0
  13. package/adapters/_shared/codex-ultra-tools-entry.cjs +11 -0
  14. package/adapters/_shared/file-ops.cjs +115 -0
  15. package/adapters/_shared/frontmatter.cjs +52 -0
  16. package/adapters/_shared/handbook.cjs +144 -0
  17. package/adapters/_shared/md-to-toml.cjs +54 -0
  18. package/adapters/_shared/path-rewrite.cjs +79 -0
  19. package/adapters/_shared/rtk-detect.cjs +86 -0
  20. package/adapters/_shared/runtime-assets.cjs +70 -0
  21. package/adapters/_shared/settings-merge.cjs +76 -0
  22. package/adapters/_shared/tests/file-ops.test.cjs +54 -0
  23. package/adapters/_shared/tests/frontmatter.test.cjs +47 -0
  24. package/adapters/_shared/tests/handbook.test.cjs +86 -0
  25. package/adapters/_shared/tests/md-to-toml.test.cjs +48 -0
  26. package/adapters/_shared/tests/path-rewrite.test.cjs +60 -0
  27. package/adapters/_shared/tests/resolve-target.test.cjs +85 -0
  28. package/adapters/_shared/tests/rtk-detect.test.cjs +116 -0
  29. package/adapters/_shared/tests/runtime-assets.test.cjs +107 -0
  30. package/adapters/_shared/tests/settings-merge.test.cjs +85 -0
  31. package/adapters/_shared/tests/validate.test.cjs +30 -0
  32. package/adapters/_shared/validate.cjs +24 -0
  33. package/adapters/claude.js +182 -0
  34. package/adapters/codex.js +385 -0
  35. package/adapters/gemini.js +204 -0
  36. package/adapters/opencode.js +506 -0
  37. package/adapters/tests/claude.test.cjs +117 -0
  38. package/adapters/tests/codex-hook.test.cjs +90 -0
  39. package/adapters/tests/codex.test.cjs +356 -0
  40. package/adapters/tests/gemini.test.cjs +112 -0
  41. package/adapters/tests/opencode.test.cjs +246 -0
  42. package/agents/code-reviewer.md +102 -0
  43. package/agents/debugger.md +127 -0
  44. package/agents/review-code.md +125 -0
  45. package/agents/review-comments.md +98 -0
  46. package/agents/review-coordinator.md +146 -0
  47. package/agents/review-design.md +133 -0
  48. package/agents/review-errors.md +87 -0
  49. package/agents/review-tests.md +93 -0
  50. package/agents/tdd-runner.md +92 -0
  51. package/bin/handbook.js +54 -0
  52. package/bin/install.js +254 -0
  53. package/bin/orchestrator.js +214 -0
  54. package/commands/learn.md +41 -0
  55. package/commands/ultra-deliver.md +45 -0
  56. package/commands/ultra-dev.md +50 -0
  57. package/commands/ultra-init.md +51 -0
  58. package/commands/ultra-plan.md +49 -0
  59. package/commands/ultra-research.md +59 -0
  60. package/commands/ultra-status.md +46 -0
  61. package/commands/ultra-test.md +48 -0
  62. package/commands/ultra-think.md +45 -0
  63. package/docs/AGENT-CONTEXT.md +143 -0
  64. package/docs/ARCHITECTURE.md +197 -0
  65. package/docs/COMMIT-HASH-BACKFILL.md +101 -0
  66. package/docs/LEGACY-HERMES.md +1175 -0
  67. package/docs/PLAN.zh-CN.md +1683 -0
  68. package/docs/ROADMAP.md +102 -0
  69. package/docs/RUNTIME-COMPAT-MATRIX.md +98 -0
  70. package/docs/STATE-DB-ACCESS-POLICY.md +109 -0
  71. package/docs/USER-HANDBOOK-CONTRACT.md +61 -0
  72. package/hooks/README.md +24 -0
  73. package/hooks/active_task_context.py +44 -0
  74. package/hooks/adapters/codex.py +302 -0
  75. package/hooks/health_check.py +61 -0
  76. package/hooks/pre_stop_check.py +56 -0
  77. package/hooks/subagent_tracker.py +116 -0
  78. package/hooks/workflow_checkpoint.py +56 -0
  79. package/hooks/workflow_context.py +63 -0
  80. package/hooks/workflow_resume.py +50 -0
  81. package/mcp-server/lib/init-project.cjs +162 -0
  82. package/mcp-server/lib/llm-client.cjs +157 -0
  83. package/mcp-server/lib/llm-client.test.cjs +156 -0
  84. package/mcp-server/lib/plan-store.cjs +96 -0
  85. package/mcp-server/lib/plan-store.test.cjs +132 -0
  86. package/mcp-server/lib/prd-parser.cjs +110 -0
  87. package/mcp-server/lib/prd-parser.test.cjs +171 -0
  88. package/mcp-server/lib/pricing.cjs +53 -0
  89. package/mcp-server/lib/projector.cjs +158 -0
  90. package/mcp-server/lib/state-db.cjs +113 -0
  91. package/mcp-server/lib/state-db.test.cjs +116 -0
  92. package/mcp-server/lib/state-ops.cjs +725 -0
  93. package/mcp-server/lib/task-expander.cjs +123 -0
  94. package/mcp-server/lib/task-expander.test.cjs +253 -0
  95. package/mcp-server/lib/telemetry.cjs +72 -0
  96. package/mcp-server/lib/topo.cjs +131 -0
  97. package/mcp-server/lib/topo.test.cjs +158 -0
  98. package/mcp-server/server.cjs +482 -0
  99. package/mcp-server/tests/circuit-breaker.test.cjs +125 -0
  100. package/mcp-server/tests/commit-hash-flow.test.cjs +117 -0
  101. package/mcp-server/tests/concurrency-update.test.cjs +96 -0
  102. package/mcp-server/tests/concurrency.test.cjs +112 -0
  103. package/mcp-server/tests/db-ops.test.cjs +117 -0
  104. package/mcp-server/tests/init-project.test.cjs +141 -0
  105. package/mcp-server/tests/migrate.test.cjs +144 -0
  106. package/mcp-server/tests/projector.test.cjs +130 -0
  107. package/mcp-server/tests/server-contract.test.cjs +750 -0
  108. package/mcp-server/tests/staleness.test.cjs +190 -0
  109. package/mcp-server/tests/state-ops.test.cjs +286 -0
  110. package/mcp-server/tests/tagged-tasks.test.cjs +163 -0
  111. package/mcp-server/tests/telemetry.test.cjs +205 -0
  112. package/orchestrator/auto-merge.cjs +109 -0
  113. package/orchestrator/code-graph-watcher.cjs +121 -0
  114. package/orchestrator/daemon.cjs +112 -0
  115. package/orchestrator/dispatch-rules.cjs +93 -0
  116. package/orchestrator/parallel-orchestrator.cjs +195 -0
  117. package/orchestrator/planner/expand-prompt.cjs +62 -0
  118. package/orchestrator/planner/expand-prompt.test.cjs +52 -0
  119. package/orchestrator/planner/plan-builder.cjs +116 -0
  120. package/orchestrator/planner/plan-builder.test.cjs +165 -0
  121. package/orchestrator/planner/prd-prompt.cjs +44 -0
  122. package/orchestrator/planner/prd-prompt.test.cjs +30 -0
  123. package/orchestrator/recovery.cjs +70 -0
  124. package/orchestrator/session-runner.cjs +224 -0
  125. package/orchestrator/skill-miner.cjs +133 -0
  126. package/orchestrator/tests/auto-merge.test.cjs +216 -0
  127. package/orchestrator/tests/code-graph-watcher.test.cjs +183 -0
  128. package/orchestrator/tests/daemon.test.cjs +260 -0
  129. package/orchestrator/tests/dispatch-rules.test.cjs +122 -0
  130. package/orchestrator/tests/parallel-orchestrator.test.cjs +233 -0
  131. package/orchestrator/tests/phase-8b-gate.test.cjs +183 -0
  132. package/orchestrator/tests/recovery.test.cjs +190 -0
  133. package/orchestrator/tests/session-runner.test.cjs +314 -0
  134. package/orchestrator/tests/skill-miner.test.cjs +147 -0
  135. package/orchestrator/tests/worktree-manager.test.cjs +148 -0
  136. package/orchestrator/worktree-manager.cjs +131 -0
  137. package/output-styles/coding-vibes.md +67 -0
  138. package/output-styles/structural-thinking.md +61 -0
  139. package/package.json +109 -0
  140. package/settings.json +8 -0
  141. package/skills/cc-collab/SKILL.md +41 -0
  142. package/skills/code-review-expert/SKILL.md +118 -0
  143. package/skills/code-review-expert/references/code-quality-checklist.md +112 -0
  144. package/skills/code-review-expert/references/integration-checklist.md +46 -0
  145. package/skills/code-review-expert/references/removal-plan.md +52 -0
  146. package/skills/code-review-expert/references/security-checklist.md +100 -0
  147. package/skills/code-review-expert/references/solid-checklist.md +65 -0
  148. package/skills/codex-collab/SKILL.md +52 -0
  149. package/skills/codex-collab/evals/evals.json +40 -0
  150. package/skills/gemini-collab/SKILL.md +51 -0
  151. package/skills/gemini-collab/evals/evals.json +42 -0
  152. package/skills/integration-rules/SKILL.md +71 -0
  153. package/skills/learn/SKILL.md +117 -0
  154. package/skills/security-rules/SKILL.md +60 -0
  155. package/skills/testing-rules/SKILL.md +56 -0
  156. package/skills/ultra-deliver/SKILL.md +171 -0
  157. package/skills/ultra-dev/SKILL.md +324 -0
  158. package/skills/ultra-init/SKILL.md +148 -0
  159. package/skills/ultra-plan/SKILL.md +314 -0
  160. package/skills/ultra-research/SKILL.md +156 -0
  161. package/skills/ultra-research/steps/step-00-problem-validation.md +154 -0
  162. package/skills/ultra-research/steps/step-01-opportunity-discovery.md +172 -0
  163. package/skills/ultra-research/steps/step-02-market-assessment.md +195 -0
  164. package/skills/ultra-research/steps/step-03-competitive-landscape.md +197 -0
  165. package/skills/ultra-research/steps/step-04-product-strategy.md +196 -0
  166. package/skills/ultra-research/steps/step-05-assumptions-validation.md +234 -0
  167. package/skills/ultra-research/steps/step-10-user-personas.md +169 -0
  168. package/skills/ultra-research/steps/step-11-user-scenarios.md +181 -0
  169. package/skills/ultra-research/steps/step-20-user-stories.md +156 -0
  170. package/skills/ultra-research/steps/step-21-features-scope.md +157 -0
  171. package/skills/ultra-research/steps/step-22-success-metrics.md +195 -0
  172. package/skills/ultra-research/steps/step-30-architecture-context.md +154 -0
  173. package/skills/ultra-research/steps/step-31-solution-strategy.md +172 -0
  174. package/skills/ultra-research/steps/step-32-building-blocks.md +188 -0
  175. package/skills/ultra-research/steps/step-40-deployment.md +123 -0
  176. package/skills/ultra-research/steps/step-41-quality-risks.md +135 -0
  177. package/skills/ultra-research/steps/step-99-synthesis.md +188 -0
  178. package/skills/ultra-review/SKILL.md +437 -0
  179. package/skills/ultra-review/references/unified-schema.md +127 -0
  180. package/skills/ultra-review/scripts/review_verdict_update.py +99 -0
  181. package/skills/ultra-review/scripts/review_wait.py +120 -0
  182. package/skills/ultra-status/SKILL.md +162 -0
  183. package/skills/ultra-test/SKILL.md +195 -0
  184. package/skills/ultra-think/SKILL.md +109 -0
  185. package/skills/ultra-verify/SKILL.md +111 -0
  186. package/skills/ultra-verify/evals/evals.json +51 -0
  187. package/skills/ultra-verify/references/confidence-system.md +38 -0
  188. package/skills/ultra-verify/references/cross-verify-modes.md +73 -0
  189. package/skills/ultra-verify/scripts/verify_wait.py +131 -0
  190. package/spec/cli-protocol.md +118 -0
  191. package/spec/mcp-tools.yaml +658 -0
  192. package/spec/schemas/command-manifest.schema.json +47 -0
  193. package/spec/schemas/context-file.v4.5.schema.json +54 -0
  194. package/spec/schemas/mcp-tools.schema.json +136 -0
  195. package/spec/schemas/skill-manifest.schema.json +94 -0
  196. package/spec/schemas/state-db.sql +154 -0
  197. package/spec/schemas/tasks.v4.5.schema.json +66 -0
  198. package/ultra-tools/cli.cjs +121 -0
  199. package/ultra-tools/commands/db.cjs +175 -0
  200. package/ultra-tools/commands/legacy-memory.cjs +159 -0
  201. package/ultra-tools/commands/migrate.cjs +354 -0
  202. package/ultra-tools/commands/session.cjs +180 -0
  203. package/ultra-tools/commands/status.cjs +158 -0
  204. package/ultra-tools/commands/task.cjs +95 -0
  205. package/ultra-tools/legacy-memory.test.cjs +80 -0
  206. package/ultra-tools/status-command.test.cjs +132 -0
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env python3
2
+ """Codex wire adapter for Ultra Builder Pro hooks.
3
+
4
+ Codex and Claude Code share the JSON-on-stdin command-hook shape, but differ
5
+ at three important boundaries:
6
+
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``.
9
+ - Codex rejects ``permissionDecision: ask`` in PreToolUse output. Ultra warning
10
+ decisions therefore become advisory ``systemMessage`` values.
11
+ - Each event has a strict output schema. This adapter removes foreign fields
12
+ while preserving real deny/block decisions and additional context.
13
+
14
+ The adapter executes only allowlisted sibling hook modules. It is both a CLI
15
+ entrypoint used by ``hooks/hooks.json`` and an importable normalization layer
16
+ for tests.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import copy
22
+ import json
23
+ import os
24
+ import re
25
+ import subprocess
26
+ import sys
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+
31
+ HOOK_ROOT = Path(__file__).resolve().parent.parent
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",
40
+ }
41
+ UNIVERSAL_OUTPUT_FIELDS = {"continue", "stopReason", "suppressOutput", "systemMessage"}
42
+ PATCH_FILE_RE = re.compile(r"^\*\*\* (?:Add|Update|Delete) File: (.+?)\s*$", re.MULTILINE)
43
+ PATCH_MOVE_RE = re.compile(r"^\*\*\* Move to: (.+?)\s*$", re.MULTILINE)
44
+
45
+
46
+ def _patch_paths(tool_input: Any) -> list[str]:
47
+ if not isinstance(tool_input, dict):
48
+ return []
49
+ patch = tool_input.get("command") or tool_input.get("patch") or ""
50
+ if not isinstance(patch, str):
51
+ return []
52
+ paths: list[str] = []
53
+ for match in [*PATCH_FILE_RE.findall(patch), *PATCH_MOVE_RE.findall(patch)]:
54
+ candidate = match.strip()
55
+ if candidate and candidate not in paths:
56
+ paths.append(candidate)
57
+ return paths
58
+
59
+
60
+ def normalize_inputs(payload: dict[str, Any]) -> list[dict[str, Any]]:
61
+ """Return legacy-compatible input(s) for one Codex hook payload."""
62
+ normalized = copy.deepcopy(payload)
63
+ if normalized.get("tool_response") is not None and normalized.get("tool_output") is None:
64
+ normalized["tool_output"] = normalized["tool_response"]
65
+
66
+ if normalized.get("tool_name") != "apply_patch":
67
+ return [normalized]
68
+
69
+ paths = _patch_paths(normalized.get("tool_input"))
70
+ if not paths:
71
+ normalized["tool_name"] = "Edit"
72
+ tool_input = normalized.get("tool_input")
73
+ normalized["tool_input"] = dict(tool_input) if isinstance(tool_input, dict) else {}
74
+ normalized["tool_input"].setdefault("file_path", "")
75
+ return [normalized]
76
+
77
+ outputs: list[dict[str, Any]] = []
78
+ for file_path in paths:
79
+ item = copy.deepcopy(normalized)
80
+ item["tool_name"] = "Edit"
81
+ tool_input = item.get("tool_input")
82
+ item["tool_input"] = dict(tool_input) if isinstance(tool_input, dict) else {}
83
+ item["tool_input"]["file_path"] = file_path
84
+ outputs.append(item)
85
+ return outputs
86
+
87
+
88
+ def _universal(output: dict[str, Any]) -> dict[str, Any]:
89
+ return {key: output[key] for key in UNIVERSAL_OUTPUT_FIELDS if key in output}
90
+
91
+
92
+ 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."""
94
+ if not isinstance(output, dict):
95
+ return {}
96
+ result = _universal(output)
97
+ hook_specific = output.get("hookSpecificOutput")
98
+ hook_specific = dict(hook_specific) if isinstance(hook_specific, dict) else None
99
+
100
+ if event == "PreToolUse":
101
+ if hook_specific:
102
+ decision = hook_specific.get("permissionDecision")
103
+ reason = hook_specific.get("permissionDecisionReason")
104
+ if decision == "ask":
105
+ if reason:
106
+ result["systemMessage"] = str(reason)
107
+ additional = hook_specific.get("additionalContext")
108
+ if additional:
109
+ result["systemMessage"] = "\n".join(
110
+ part for part in [result.get("systemMessage"), str(additional)] if part
111
+ )
112
+ return result
113
+ allowed = {
114
+ "hookEventName": "PreToolUse",
115
+ **{key: value for key, value in hook_specific.items() if key in {
116
+ "permissionDecision", "permissionDecisionReason", "updatedInput", "additionalContext"
117
+ }},
118
+ }
119
+ result["hookSpecificOutput"] = allowed
120
+ if output.get("decision") in {"block"}:
121
+ result["decision"] = "block"
122
+ if output.get("reason"):
123
+ result["reason"] = output["reason"]
124
+ return result
125
+
126
+ if event == "PermissionRequest":
127
+ if hook_specific and hook_specific.get("permissionDecision") in {"allow", "deny"}:
128
+ result["hookSpecificOutput"] = {
129
+ "hookEventName": "PermissionRequest",
130
+ "decision": {
131
+ "behavior": hook_specific["permissionDecision"],
132
+ "message": hook_specific.get("permissionDecisionReason"),
133
+ },
134
+ }
135
+ return result
136
+
137
+ if event == "PostToolUse":
138
+ if output.get("decision") == "block":
139
+ result["decision"] = "block"
140
+ result["reason"] = str(output.get("reason") or "Ultra post-edit gate blocked continuation")
141
+ if hook_specific and hook_specific.get("additionalContext"):
142
+ result["hookSpecificOutput"] = {
143
+ "hookEventName": "PostToolUse",
144
+ "additionalContext": hook_specific["additionalContext"],
145
+ }
146
+ return result
147
+
148
+ if event == "SessionStart":
149
+ if hook_specific and hook_specific.get("additionalContext"):
150
+ result["hookSpecificOutput"] = {
151
+ "hookEventName": "SessionStart",
152
+ "additionalContext": hook_specific["additionalContext"],
153
+ }
154
+ return result
155
+
156
+ if event == "SubagentStart":
157
+ if hook_specific and hook_specific.get("additionalContext"):
158
+ result["hookSpecificOutput"] = {
159
+ "hookEventName": "SubagentStart",
160
+ "additionalContext": hook_specific["additionalContext"],
161
+ }
162
+ return result
163
+
164
+ if event == "UserPromptSubmit":
165
+ if output.get("decision") == "block":
166
+ result["decision"] = "block"
167
+ result["reason"] = str(output.get("reason") or "Ultra prompt gate blocked submission")
168
+ if hook_specific and hook_specific.get("additionalContext"):
169
+ result["hookSpecificOutput"] = {
170
+ "hookEventName": "UserPromptSubmit",
171
+ "additionalContext": hook_specific["additionalContext"],
172
+ }
173
+ return result
174
+
175
+ if event == "Stop":
176
+ if output.get("decision") == "block":
177
+ result["decision"] = "block"
178
+ result["reason"] = str(output.get("reason") or "Ultra completion gate blocked stop")
179
+ return result
180
+
181
+ if event in {"PreCompact", "PostCompact", "SubagentStop"}:
182
+ extra = output.get("additionalContext")
183
+ if not extra and hook_specific:
184
+ extra = hook_specific.get("additionalContext")
185
+ if extra:
186
+ result["systemMessage"] = str(extra)
187
+ return result
188
+
189
+ return result
190
+
191
+
192
+ def _parse_output(stdout: str) -> dict[str, Any]:
193
+ text = stdout.strip()
194
+ if not text:
195
+ return {}
196
+ try:
197
+ value = json.loads(text)
198
+ except json.JSONDecodeError:
199
+ return {"systemMessage": f"Ultra hook returned non-JSON output: {text[:500]}"}
200
+ return value if isinstance(value, dict) else {}
201
+
202
+
203
+ def _merge_outputs(outputs: list[dict[str, Any]], event: str) -> dict[str, Any]:
204
+ if not outputs:
205
+ return {}
206
+ if len(outputs) == 1:
207
+ return outputs[0]
208
+
209
+ messages = [str(item.get("systemMessage")) for item in outputs if item.get("systemMessage")]
210
+ contexts: list[str] = []
211
+ blocking: dict[str, Any] | None = None
212
+ for item in outputs:
213
+ if item.get("decision") == "block":
214
+ blocking = item
215
+ break
216
+ specific = item.get("hookSpecificOutput")
217
+ if isinstance(specific, dict) and specific.get("permissionDecision") == "deny":
218
+ blocking = item
219
+ break
220
+ if isinstance(specific, dict) and specific.get("additionalContext"):
221
+ contexts.append(str(specific["additionalContext"]))
222
+
223
+ result = dict(blocking or {})
224
+ if messages:
225
+ result["systemMessage"] = "\n\n".join(messages)
226
+ if contexts and not blocking and event in {"PostToolUse", "SessionStart", "SubagentStart", "UserPromptSubmit"}:
227
+ result["hookSpecificOutput"] = {
228
+ "hookEventName": event,
229
+ "additionalContext": "\n\n".join(contexts),
230
+ }
231
+ return result
232
+
233
+
234
+ def run_feature(feature: str, payload: dict[str, Any], feature_args: list[str]) -> dict[str, Any]:
235
+ if feature not in ALLOWED_FEATURES or Path(feature).name != feature:
236
+ return {"systemMessage": f"Ultra hook adapter refused unknown feature: {feature}"}
237
+ script = HOOK_ROOT / feature
238
+ if not script.is_file():
239
+ return {"systemMessage": f"Ultra hook feature is missing: {feature}"}
240
+
241
+ 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
+ env = os.environ.copy()
248
+ env["UBP_HOOK_RUNTIME"] = "codex"
249
+ plugin_data = env.get("PLUGIN_DATA") or env.get("CLAUDE_PLUGIN_DATA")
250
+ if plugin_data:
251
+ env["UBP_HOOK_DATA"] = plugin_data
252
+
253
+ cwd = payload.get("cwd")
254
+ run_cwd = str(cwd) if isinstance(cwd, str) and Path(cwd).is_dir() else os.getcwd()
255
+ outputs: list[dict[str, Any]] = []
256
+ for normalized in normalize_inputs(payload):
257
+ proc = subprocess.run(
258
+ [sys.executable, str(script), *feature_args],
259
+ input=json.dumps(normalized),
260
+ text=True,
261
+ capture_output=True,
262
+ cwd=run_cwd,
263
+ env=env,
264
+ timeout=30,
265
+ check=False,
266
+ )
267
+ if proc.stderr:
268
+ print(proc.stderr.rstrip(), file=sys.stderr)
269
+ if proc.returncode != 0:
270
+ outputs.append({
271
+ "systemMessage": f"Ultra hook {feature} failed with exit {proc.returncode}: {proc.stderr.strip()[:500]}"
272
+ })
273
+ continue
274
+ outputs.append(adapt_output(_parse_output(proc.stdout), event))
275
+ return _merge_outputs(outputs, event)
276
+
277
+
278
+ def main() -> int:
279
+ if len(sys.argv) < 2:
280
+ print(json.dumps({"systemMessage": "Ultra Codex hook adapter requires a feature name"}))
281
+ return 0
282
+ feature = sys.argv[1]
283
+ feature_args = sys.argv[2:]
284
+ try:
285
+ payload = json.loads(sys.stdin.read() or "{}")
286
+ if not isinstance(payload, dict):
287
+ payload = {}
288
+ except json.JSONDecodeError as exc:
289
+ print(json.dumps({"systemMessage": f"Ultra hook input parse error: {exc}"}))
290
+ return 0
291
+ try:
292
+ output = run_feature(feature, payload, feature_args)
293
+ except subprocess.TimeoutExpired:
294
+ output = {"systemMessage": f"Ultra hook {feature} timed out"}
295
+ except Exception as exc: # adapter boundary must fail open with evidence
296
+ output = {"systemMessage": f"Ultra hook adapter error in {feature}: {exc}"}
297
+ print(json.dumps(output, ensure_ascii=False))
298
+ return 0
299
+
300
+
301
+ if __name__ == "__main__":
302
+ raise SystemExit(main())
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python3
2
+ """Advisory health check for an active Ultra workflow.
3
+
4
+ No memory, prompt, transcript, or generic host policy is inspected here.
5
+ """
6
+
7
+ import json
8
+ import sqlite3
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ TERMINAL = {"committed", "completed", "done", "cancelled"}
13
+ REQUIRED_TABLES = {"tasks", "events", "sessions", "schema_version"}
14
+
15
+
16
+ def main():
17
+ try:
18
+ raw = sys.stdin.read()
19
+ data = json.loads(raw) if raw.strip() else {}
20
+ except (json.JSONDecodeError, OSError) as exc:
21
+ print(f"[health_check] invalid hook input: {exc}", file=sys.stderr)
22
+ data = {}
23
+ start = Path(data.get("cwd") or Path.cwd()).resolve()
24
+ root = None
25
+ state = None
26
+ for candidate in (start, *start.parents):
27
+ state_file = candidate / ".ultra" / "workflow-state.json"
28
+ if state_file.is_file():
29
+ try:
30
+ value = json.loads(state_file.read_text(encoding="utf-8"))
31
+ except (OSError, json.JSONDecodeError) as exc:
32
+ print(f"[health_check] cannot read {state_file}: {exc}", file=sys.stderr)
33
+ value = None
34
+ if isinstance(value, dict) and value.get("status") not in TERMINAL:
35
+ root, state = candidate, value
36
+ break
37
+ if root is None or state is None:
38
+ print(json.dumps({}))
39
+ return
40
+ issues = []
41
+ db_path = root / ".ultra" / "state.db"
42
+ if not db_path.is_file():
43
+ issues.append(".ultra/state.db is missing")
44
+ else:
45
+ try:
46
+ with sqlite3.connect(str(db_path), timeout=1) as conn:
47
+ tables = {row[0] for row in conn.execute(
48
+ "SELECT name FROM sqlite_master WHERE type='table'"
49
+ )}
50
+ missing = sorted(REQUIRED_TABLES - tables)
51
+ if missing:
52
+ issues.append(f"state.db missing tables: {', '.join(missing)}")
53
+ except sqlite3.Error as exc:
54
+ issues.append(f"state.db unreadable: {exc}")
55
+ if issues:
56
+ print("[Ultra health] " + "; ".join(issues), file=sys.stderr)
57
+ print(json.dumps({}))
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env python3
2
+ """Block stop only when the active Ultra workflow itself is incomplete."""
3
+
4
+ import sys
5
+ import json
6
+ from pathlib import Path
7
+
8
+ TERMINAL = {"committed", "completed", "done", "cancelled"}
9
+
10
+
11
+ def allow_stop() -> None:
12
+ print(json.dumps({}))
13
+
14
+
15
+ def block_stop(reason: str) -> None:
16
+ print(json.dumps({"decision": "block", "reason": reason}))
17
+
18
+
19
+ def main():
20
+ try:
21
+ hook_data = json.loads(sys.stdin.read())
22
+ except (json.JSONDecodeError, OSError) as exc:
23
+ print(f"[pre_stop_check] invalid hook input: {exc}", file=sys.stderr)
24
+ allow_stop()
25
+ return
26
+
27
+ if hook_data.get("stop_hook_active", False):
28
+ allow_stop()
29
+ return
30
+ start = Path(hook_data.get("cwd") or Path.cwd()).resolve()
31
+ for root in (start, *start.parents):
32
+ state_file = root / ".ultra" / "workflow-state.json"
33
+ if not state_file.is_file():
34
+ continue
35
+ try:
36
+ state = json.loads(state_file.read_text(encoding="utf-8"))
37
+ except (OSError, json.JSONDecodeError) as exc:
38
+ print(f"[pre_stop_check] cannot read {state_file}: {exc}", file=sys.stderr)
39
+ allow_stop()
40
+ return
41
+ if not isinstance(state, dict) or state.get("status") in TERMINAL:
42
+ allow_stop()
43
+ return
44
+ block_stop(
45
+ "Active Ultra workflow is incomplete: "
46
+ f"command={state.get('command', 'unknown')}, "
47
+ f"task={state.get('task_id', state.get('task', 'unknown'))}, "
48
+ f"step={state.get('step', 'unknown')}, status={state.get('status', 'active')}. "
49
+ "Complete or explicitly cancel the Ultra workflow before stopping."
50
+ )
51
+ return
52
+ allow_stop()
53
+
54
+
55
+ if __name__ == '__main__':
56
+ main()
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ """Subagent lifecycle tracker.
3
+
4
+ Logs SubagentStart/Stop events to .ultra/debug/subagent-log.jsonl (project-level)
5
+ for debugging and cost analysis.
6
+
7
+ Protocol fields:
8
+ SubagentStart: agent_id, agent_type, session_id
9
+ SubagentStop: agent_id, agent_type, session_id, agent_transcript_path,
10
+ last_assistant_message, stop_hook_active
11
+
12
+ Usage:
13
+ python3 subagent_tracker.py start # called by SubagentStart hook
14
+ python3 subagent_tracker.py stop # called by SubagentStop hook
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import random
20
+ import sys
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+
24
+ TERMINAL = {"committed", "completed", "done", "cancelled"}
25
+ MAX_LOG_LINES = 5000
26
+
27
+
28
+ def get_log_dir(hook_input: dict):
29
+ start = Path(hook_input.get("cwd") or Path.cwd()).resolve()
30
+ for root in (start, *start.parents):
31
+ state_file = root / ".ultra" / "workflow-state.json"
32
+ if not state_file.is_file():
33
+ continue
34
+ try:
35
+ state = json.loads(state_file.read_text(encoding="utf-8"))
36
+ except (OSError, json.JSONDecodeError) as exc:
37
+ print(f"[subagent_tracker] cannot read {state_file}: {exc}", file=sys.stderr)
38
+ return None
39
+ if isinstance(state, dict) and state.get("status") not in TERMINAL:
40
+ return root / ".ultra" / "runtime"
41
+ return None
42
+ return None
43
+
44
+
45
+ def rotate_log(log_file: Path) -> None:
46
+ """Keep last MAX_LOG_LINES entries to prevent unbounded growth."""
47
+ try:
48
+ if not log_file.exists():
49
+ return
50
+ with open(log_file, "r", encoding="utf-8") as f:
51
+ lines = f.readlines()
52
+ if len(lines) <= MAX_LOG_LINES:
53
+ return
54
+ with open(log_file, "w", encoding="utf-8") as f:
55
+ f.writelines(lines[-MAX_LOG_LINES:])
56
+ except OSError as exc:
57
+ print(f"[subagent_tracker] log rotation failed: {exc}", file=sys.stderr)
58
+
59
+
60
+ def main():
61
+ if len(sys.argv) < 2:
62
+ print(json.dumps({}))
63
+ return
64
+
65
+ action = sys.argv[1]
66
+ if action not in ("start", "stop"):
67
+ print(json.dumps({}))
68
+ return
69
+
70
+ # Read hook input from stdin
71
+ try:
72
+ raw = sys.stdin.read()
73
+ hook_input = json.loads(raw) if raw.strip() else {}
74
+ if not isinstance(hook_input, dict):
75
+ hook_input = {}
76
+ except (json.JSONDecodeError, EOFError) as exc:
77
+ print(f"[subagent_tracker] invalid hook input: {exc}", file=sys.stderr)
78
+ hook_input = {}
79
+
80
+ # Lazy init: avoid module-level subprocess
81
+ log_dir = get_log_dir(hook_input)
82
+ if log_dir is None:
83
+ print(json.dumps({}))
84
+ return
85
+ log_file = log_dir / "subagent-log.jsonl"
86
+ log_dir.mkdir(parents=True, exist_ok=True)
87
+
88
+ entry = {
89
+ "timestamp": datetime.now(timezone.utc).isoformat(),
90
+ "event": f"subagent_{action}",
91
+ "agent_id": hook_input.get("agent_id", "unknown"),
92
+ "agent_type": hook_input.get("agent_type", "unknown"),
93
+ "session_id": hook_input.get("session_id", ""),
94
+ }
95
+
96
+ # SubagentStop provides additional fields
97
+ if action == "stop":
98
+ transcript = hook_input.get("agent_transcript_path", "")
99
+ if transcript:
100
+ entry["agent_transcript_path"] = transcript
101
+
102
+ try:
103
+ with open(log_file, "a", encoding="utf-8") as f:
104
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
105
+ except OSError as exc:
106
+ print(f"[subagent_tracker] event write failed: {exc}", file=sys.stderr)
107
+
108
+ # Periodic log rotation (~1% of writes)
109
+ if random.random() < 0.01:
110
+ rotate_log(log_file)
111
+
112
+ print(json.dumps({}))
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env python3
2
+ """Write a minimal, mechanical compaction checkpoint for an active workflow."""
3
+
4
+ import json
5
+ import os
6
+ import sys
7
+ import tempfile
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+
11
+ TERMINAL = {"committed", "completed", "done", "cancelled"}
12
+
13
+
14
+ def main() -> None:
15
+ try:
16
+ data = json.loads(sys.stdin.read() or "{}")
17
+ data = data if isinstance(data, dict) else {}
18
+ except json.JSONDecodeError as exc:
19
+ print(f"[workflow_checkpoint] invalid hook input: {exc}", file=sys.stderr)
20
+ data = {}
21
+ start = Path(data.get("cwd") or Path.cwd()).resolve()
22
+ for root in (start, *start.parents):
23
+ state_file = root / ".ultra" / "workflow-state.json"
24
+ if not state_file.is_file():
25
+ continue
26
+ try:
27
+ state = json.loads(state_file.read_text(encoding="utf-8"))
28
+ except (OSError, json.JSONDecodeError) as exc:
29
+ print(f"[workflow_checkpoint] cannot read {state_file}: {exc}", file=sys.stderr)
30
+ break
31
+ if not isinstance(state, dict) or state.get("status") in TERMINAL:
32
+ break
33
+ runtime_dir = root / ".ultra" / "runtime"
34
+ runtime_dir.mkdir(parents=True, exist_ok=True)
35
+ checkpoint = {
36
+ "schema": 1,
37
+ "captured_at": datetime.now(timezone.utc).isoformat(),
38
+ "session_id": data.get("session_id", ""),
39
+ "workflow": state,
40
+ }
41
+ fd, temp_name = tempfile.mkstemp(prefix="checkpoint.", suffix=".json", dir=runtime_dir)
42
+ try:
43
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
44
+ json.dump(checkpoint, handle, ensure_ascii=False, indent=2)
45
+ handle.write("\n")
46
+ os.replace(temp_name, runtime_dir / "checkpoint.json")
47
+ finally:
48
+ if os.path.exists(temp_name):
49
+ os.unlink(temp_name)
50
+ print(json.dumps({"systemMessage": "Ultra workflow checkpoint saved."}))
51
+ return
52
+ print(json.dumps({}))
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+ """Inject the live Ultra workflow state at session start.
3
+
4
+ This hook is deliberately project-local and contains no cross-session memory.
5
+ It is a no-op unless an active .ultra/workflow-state.json exists.
6
+ """
7
+
8
+ import json
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ TERMINAL = {"committed", "completed", "done", "cancelled"}
13
+
14
+
15
+ def payload() -> dict:
16
+ try:
17
+ value = json.loads(sys.stdin.read() or "{}")
18
+ return value if isinstance(value, dict) else {}
19
+ except json.JSONDecodeError as exc:
20
+ print(f"[workflow_context] invalid hook input: {exc}", file=sys.stderr)
21
+ return {}
22
+
23
+
24
+ def active_state(data: dict):
25
+ start = Path(data.get("cwd") or Path.cwd()).resolve()
26
+ for root in (start, *start.parents):
27
+ state_file = root / ".ultra" / "workflow-state.json"
28
+ if not state_file.is_file():
29
+ continue
30
+ try:
31
+ state = json.loads(state_file.read_text(encoding="utf-8"))
32
+ except (OSError, json.JSONDecodeError) as exc:
33
+ print(f"[workflow_context] cannot read {state_file}: {exc}", file=sys.stderr)
34
+ return None
35
+ if isinstance(state, dict) and state.get("status") not in TERMINAL:
36
+ return root, state
37
+ return None
38
+ return None
39
+
40
+
41
+ def main() -> None:
42
+ found = active_state(payload())
43
+ if not found:
44
+ print(json.dumps({}))
45
+ return
46
+ root, state = found
47
+ context = "\n".join([
48
+ "[Ultra active workflow]",
49
+ f"Project: {root}",
50
+ f"Command: {state.get('command', 'unknown')}",
51
+ f"Task: {state.get('task_id', state.get('task', 'unknown'))}",
52
+ f"Step: {state.get('step', 'unknown')}",
53
+ f"Status: {state.get('status', 'active')}",
54
+ "Authority: .ultra/state.db; workflow recovery: .ultra/workflow-state.json.",
55
+ ])
56
+ print(json.dumps({"hookSpecificOutput": {
57
+ "hookEventName": "SessionStart",
58
+ "additionalContext": context,
59
+ }}))
60
+
61
+
62
+ if __name__ == "__main__":
63
+ main()