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.
- package/.ultra-template/delivery-report.json +9 -0
- package/.ultra-template/docs/research/README.md +53 -0
- package/.ultra-template/specs/architecture.md +250 -0
- package/.ultra-template/specs/discovery.md +248 -0
- package/.ultra-template/specs/product.md +223 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +57 -0
- package/.ultra-template/tasks/tasks.json +33 -0
- package/.ultra-template/test-report.json +8 -0
- package/CHANGELOG.md +194 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/adapters/_shared/codex-assets.cjs +1128 -0
- package/adapters/_shared/codex-ultra-tools-entry.cjs +11 -0
- package/adapters/_shared/file-ops.cjs +115 -0
- package/adapters/_shared/frontmatter.cjs +52 -0
- package/adapters/_shared/handbook.cjs +144 -0
- package/adapters/_shared/md-to-toml.cjs +54 -0
- package/adapters/_shared/path-rewrite.cjs +79 -0
- package/adapters/_shared/rtk-detect.cjs +86 -0
- package/adapters/_shared/runtime-assets.cjs +70 -0
- package/adapters/_shared/settings-merge.cjs +76 -0
- package/adapters/_shared/tests/file-ops.test.cjs +54 -0
- package/adapters/_shared/tests/frontmatter.test.cjs +47 -0
- package/adapters/_shared/tests/handbook.test.cjs +86 -0
- package/adapters/_shared/tests/md-to-toml.test.cjs +48 -0
- package/adapters/_shared/tests/path-rewrite.test.cjs +60 -0
- package/adapters/_shared/tests/resolve-target.test.cjs +85 -0
- package/adapters/_shared/tests/rtk-detect.test.cjs +116 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +107 -0
- package/adapters/_shared/tests/settings-merge.test.cjs +85 -0
- package/adapters/_shared/tests/validate.test.cjs +30 -0
- package/adapters/_shared/validate.cjs +24 -0
- package/adapters/claude.js +182 -0
- package/adapters/codex.js +385 -0
- package/adapters/gemini.js +204 -0
- package/adapters/opencode.js +506 -0
- package/adapters/tests/claude.test.cjs +117 -0
- package/adapters/tests/codex-hook.test.cjs +90 -0
- package/adapters/tests/codex.test.cjs +356 -0
- package/adapters/tests/gemini.test.cjs +112 -0
- package/adapters/tests/opencode.test.cjs +246 -0
- package/agents/code-reviewer.md +102 -0
- package/agents/debugger.md +127 -0
- package/agents/review-code.md +125 -0
- package/agents/review-comments.md +98 -0
- package/agents/review-coordinator.md +146 -0
- package/agents/review-design.md +133 -0
- package/agents/review-errors.md +87 -0
- package/agents/review-tests.md +93 -0
- package/agents/tdd-runner.md +92 -0
- package/bin/handbook.js +54 -0
- package/bin/install.js +254 -0
- package/bin/orchestrator.js +214 -0
- package/commands/learn.md +41 -0
- package/commands/ultra-deliver.md +45 -0
- package/commands/ultra-dev.md +50 -0
- package/commands/ultra-init.md +51 -0
- package/commands/ultra-plan.md +49 -0
- package/commands/ultra-research.md +59 -0
- package/commands/ultra-status.md +46 -0
- package/commands/ultra-test.md +48 -0
- package/commands/ultra-think.md +45 -0
- package/docs/AGENT-CONTEXT.md +143 -0
- package/docs/ARCHITECTURE.md +197 -0
- package/docs/COMMIT-HASH-BACKFILL.md +101 -0
- package/docs/LEGACY-HERMES.md +1175 -0
- package/docs/PLAN.zh-CN.md +1683 -0
- package/docs/ROADMAP.md +102 -0
- package/docs/RUNTIME-COMPAT-MATRIX.md +98 -0
- package/docs/STATE-DB-ACCESS-POLICY.md +109 -0
- package/docs/USER-HANDBOOK-CONTRACT.md +61 -0
- package/hooks/README.md +24 -0
- package/hooks/active_task_context.py +44 -0
- package/hooks/adapters/codex.py +302 -0
- package/hooks/health_check.py +61 -0
- package/hooks/pre_stop_check.py +56 -0
- package/hooks/subagent_tracker.py +116 -0
- package/hooks/workflow_checkpoint.py +56 -0
- package/hooks/workflow_context.py +63 -0
- package/hooks/workflow_resume.py +50 -0
- package/mcp-server/lib/init-project.cjs +162 -0
- package/mcp-server/lib/llm-client.cjs +157 -0
- package/mcp-server/lib/llm-client.test.cjs +156 -0
- package/mcp-server/lib/plan-store.cjs +96 -0
- package/mcp-server/lib/plan-store.test.cjs +132 -0
- package/mcp-server/lib/prd-parser.cjs +110 -0
- package/mcp-server/lib/prd-parser.test.cjs +171 -0
- package/mcp-server/lib/pricing.cjs +53 -0
- package/mcp-server/lib/projector.cjs +158 -0
- package/mcp-server/lib/state-db.cjs +113 -0
- package/mcp-server/lib/state-db.test.cjs +116 -0
- package/mcp-server/lib/state-ops.cjs +725 -0
- package/mcp-server/lib/task-expander.cjs +123 -0
- package/mcp-server/lib/task-expander.test.cjs +253 -0
- package/mcp-server/lib/telemetry.cjs +72 -0
- package/mcp-server/lib/topo.cjs +131 -0
- package/mcp-server/lib/topo.test.cjs +158 -0
- package/mcp-server/server.cjs +482 -0
- package/mcp-server/tests/circuit-breaker.test.cjs +125 -0
- package/mcp-server/tests/commit-hash-flow.test.cjs +117 -0
- package/mcp-server/tests/concurrency-update.test.cjs +96 -0
- package/mcp-server/tests/concurrency.test.cjs +112 -0
- package/mcp-server/tests/db-ops.test.cjs +117 -0
- package/mcp-server/tests/init-project.test.cjs +141 -0
- package/mcp-server/tests/migrate.test.cjs +144 -0
- package/mcp-server/tests/projector.test.cjs +130 -0
- package/mcp-server/tests/server-contract.test.cjs +750 -0
- package/mcp-server/tests/staleness.test.cjs +190 -0
- package/mcp-server/tests/state-ops.test.cjs +286 -0
- package/mcp-server/tests/tagged-tasks.test.cjs +163 -0
- package/mcp-server/tests/telemetry.test.cjs +205 -0
- package/orchestrator/auto-merge.cjs +109 -0
- package/orchestrator/code-graph-watcher.cjs +121 -0
- package/orchestrator/daemon.cjs +112 -0
- package/orchestrator/dispatch-rules.cjs +93 -0
- package/orchestrator/parallel-orchestrator.cjs +195 -0
- package/orchestrator/planner/expand-prompt.cjs +62 -0
- package/orchestrator/planner/expand-prompt.test.cjs +52 -0
- package/orchestrator/planner/plan-builder.cjs +116 -0
- package/orchestrator/planner/plan-builder.test.cjs +165 -0
- package/orchestrator/planner/prd-prompt.cjs +44 -0
- package/orchestrator/planner/prd-prompt.test.cjs +30 -0
- package/orchestrator/recovery.cjs +70 -0
- package/orchestrator/session-runner.cjs +224 -0
- package/orchestrator/skill-miner.cjs +133 -0
- package/orchestrator/tests/auto-merge.test.cjs +216 -0
- package/orchestrator/tests/code-graph-watcher.test.cjs +183 -0
- package/orchestrator/tests/daemon.test.cjs +260 -0
- package/orchestrator/tests/dispatch-rules.test.cjs +122 -0
- package/orchestrator/tests/parallel-orchestrator.test.cjs +233 -0
- package/orchestrator/tests/phase-8b-gate.test.cjs +183 -0
- package/orchestrator/tests/recovery.test.cjs +190 -0
- package/orchestrator/tests/session-runner.test.cjs +314 -0
- package/orchestrator/tests/skill-miner.test.cjs +147 -0
- package/orchestrator/tests/worktree-manager.test.cjs +148 -0
- package/orchestrator/worktree-manager.cjs +131 -0
- package/output-styles/coding-vibes.md +67 -0
- package/output-styles/structural-thinking.md +61 -0
- package/package.json +109 -0
- package/settings.json +8 -0
- package/skills/cc-collab/SKILL.md +41 -0
- package/skills/code-review-expert/SKILL.md +118 -0
- package/skills/code-review-expert/references/code-quality-checklist.md +112 -0
- package/skills/code-review-expert/references/integration-checklist.md +46 -0
- package/skills/code-review-expert/references/removal-plan.md +52 -0
- package/skills/code-review-expert/references/security-checklist.md +100 -0
- package/skills/code-review-expert/references/solid-checklist.md +65 -0
- package/skills/codex-collab/SKILL.md +52 -0
- package/skills/codex-collab/evals/evals.json +40 -0
- package/skills/gemini-collab/SKILL.md +51 -0
- package/skills/gemini-collab/evals/evals.json +42 -0
- package/skills/integration-rules/SKILL.md +71 -0
- package/skills/learn/SKILL.md +117 -0
- package/skills/security-rules/SKILL.md +60 -0
- package/skills/testing-rules/SKILL.md +56 -0
- package/skills/ultra-deliver/SKILL.md +171 -0
- package/skills/ultra-dev/SKILL.md +324 -0
- package/skills/ultra-init/SKILL.md +148 -0
- package/skills/ultra-plan/SKILL.md +314 -0
- package/skills/ultra-research/SKILL.md +156 -0
- package/skills/ultra-research/steps/step-00-problem-validation.md +154 -0
- package/skills/ultra-research/steps/step-01-opportunity-discovery.md +172 -0
- package/skills/ultra-research/steps/step-02-market-assessment.md +195 -0
- package/skills/ultra-research/steps/step-03-competitive-landscape.md +197 -0
- package/skills/ultra-research/steps/step-04-product-strategy.md +196 -0
- package/skills/ultra-research/steps/step-05-assumptions-validation.md +234 -0
- package/skills/ultra-research/steps/step-10-user-personas.md +169 -0
- package/skills/ultra-research/steps/step-11-user-scenarios.md +181 -0
- package/skills/ultra-research/steps/step-20-user-stories.md +156 -0
- package/skills/ultra-research/steps/step-21-features-scope.md +157 -0
- package/skills/ultra-research/steps/step-22-success-metrics.md +195 -0
- package/skills/ultra-research/steps/step-30-architecture-context.md +154 -0
- package/skills/ultra-research/steps/step-31-solution-strategy.md +172 -0
- package/skills/ultra-research/steps/step-32-building-blocks.md +188 -0
- package/skills/ultra-research/steps/step-40-deployment.md +123 -0
- package/skills/ultra-research/steps/step-41-quality-risks.md +135 -0
- package/skills/ultra-research/steps/step-99-synthesis.md +188 -0
- package/skills/ultra-review/SKILL.md +437 -0
- package/skills/ultra-review/references/unified-schema.md +127 -0
- package/skills/ultra-review/scripts/review_verdict_update.py +99 -0
- package/skills/ultra-review/scripts/review_wait.py +120 -0
- package/skills/ultra-status/SKILL.md +162 -0
- package/skills/ultra-test/SKILL.md +195 -0
- package/skills/ultra-think/SKILL.md +109 -0
- package/skills/ultra-verify/SKILL.md +111 -0
- package/skills/ultra-verify/evals/evals.json +51 -0
- package/skills/ultra-verify/references/confidence-system.md +38 -0
- package/skills/ultra-verify/references/cross-verify-modes.md +73 -0
- package/skills/ultra-verify/scripts/verify_wait.py +131 -0
- package/spec/cli-protocol.md +118 -0
- package/spec/mcp-tools.yaml +658 -0
- package/spec/schemas/command-manifest.schema.json +47 -0
- package/spec/schemas/context-file.v4.5.schema.json +54 -0
- package/spec/schemas/mcp-tools.schema.json +136 -0
- package/spec/schemas/skill-manifest.schema.json +94 -0
- package/spec/schemas/state-db.sql +154 -0
- package/spec/schemas/tasks.v4.5.schema.json +66 -0
- package/ultra-tools/cli.cjs +121 -0
- package/ultra-tools/commands/db.cjs +175 -0
- package/ultra-tools/commands/legacy-memory.cjs +159 -0
- package/ultra-tools/commands/migrate.cjs +354 -0
- package/ultra-tools/commands/session.cjs +180 -0
- package/ultra-tools/commands/status.cjs +158 -0
- package/ultra-tools/commands/task.cjs +95 -0
- package/ultra-tools/legacy-memory.test.cjs +80 -0
- 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()
|