ultra-builder-pro-cli 0.26.1 → 3.0.2

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 (75) hide show
  1. package/.ultra-template/contexts/TEMPLATE.md +64 -29
  2. package/.ultra-template/north-star.md +36 -30
  3. package/.ultra-template/project-brief.md +28 -0
  4. package/.ultra-template/specs/product.md +4 -0
  5. package/.ultra-template/specs/research-distillate.md +8 -0
  6. package/.ultra-template/tasks.json +1 -0
  7. package/.ultra-template/test-report.json +18 -1
  8. package/CHANGELOG.md +227 -1
  9. package/README.md +161 -59
  10. package/adapters/_shared/host-profile.cjs +68 -12
  11. package/adapters/_shared/plugin-core.cjs +4 -5
  12. package/adapters/_shared/runtime-assets.cjs +11 -2
  13. package/adapters/zcode.js +375 -0
  14. package/bin/delegate-worker.cjs +20 -0
  15. package/bin/delegate.cjs +94 -25
  16. package/bin/install.js +11 -6
  17. package/docs/ARCHITECTURE.md +109 -25
  18. package/docs/ARTIFACT-AUTHORITY.md +204 -30
  19. package/docs/DECISIONS.md +141 -10
  20. package/docs/PHILOSOPHY.md +61 -8
  21. package/docs/PLUGIN-ISOLATION-CONTRACT.md +6 -0
  22. package/docs/RUNTIME-COMPAT-MATRIX.md +106 -13
  23. package/docs/SKILL-AUTHORING.md +14 -9
  24. package/docs/WORKFLOW-LIFECYCLE.md +211 -53
  25. package/docs/evals/adversarial-review-2026-08-14.md +84 -0
  26. package/docs/evals/zcode-automation-2026-08-14.md +103 -0
  27. package/hooks/README.md +31 -3
  28. package/hooks/_common.py +1699 -33
  29. package/hooks/adapters/zcode.py +227 -0
  30. package/hooks/compact_context.py +176 -17
  31. package/hooks/mid_workflow_recall.py +27 -5
  32. package/hooks/post_edit_guard.py +133 -23
  33. package/hooks/session_context.py +722 -11
  34. package/package.json +6 -3
  35. package/skills/ultra-change/SKILL.md +84 -64
  36. package/skills/ultra-change/references/change-contract.md +130 -32
  37. package/skills/ultra-change/references/execution-grant.md +109 -0
  38. package/skills/ultra-change/references/primary-transfer.md +273 -0
  39. package/skills/ultra-change/scripts/validate_primary_transfer.cjs +1928 -0
  40. package/skills/ultra-delegate/SKILL.md +38 -5
  41. package/skills/ultra-delegate/references/delegation-contract.md +21 -1
  42. package/skills/ultra-deliver/SKILL.md +123 -44
  43. package/skills/ultra-deliver/references/baseline-reconciliation.md +5 -3
  44. package/skills/ultra-dev/SKILL.md +173 -43
  45. package/skills/ultra-grilling/SKILL.md +19 -16
  46. package/skills/ultra-grilling/references/reframing.md +7 -3
  47. package/skills/ultra-init/SKILL.md +99 -87
  48. package/skills/ultra-init/references/brownfield-adoption.md +23 -40
  49. package/skills/ultra-init/scripts/init_project.cjs +968 -21
  50. package/skills/ultra-plan/SKILL.md +83 -46
  51. package/skills/ultra-plan/references/task-evidence-v2.md +300 -0
  52. package/skills/ultra-plan/scripts/validate_task_evidence.cjs +859 -0
  53. package/skills/ultra-research/SKILL.md +119 -68
  54. package/skills/ultra-research/references/{03-competitive-landscape.md → 03-alternatives.md} +6 -6
  55. package/skills/ultra-research/references/04-product-strategy.md +11 -2
  56. package/skills/ultra-research/references/21-features-scope.md +8 -0
  57. package/skills/ultra-research/references/22-success-metrics.md +9 -4
  58. package/skills/ultra-research/references/99-synthesis.md +25 -1
  59. package/skills/ultra-research/references/north-star-v2.md +168 -0
  60. package/skills/ultra-research/references/wayfinding.md +50 -0
  61. package/skills/ultra-research/scripts/validate_north_star.cjs +1174 -0
  62. package/skills/ultra-review/SKILL.md +201 -24
  63. package/skills/ultra-review/references/adversarial-evaluation.md +31 -0
  64. package/skills/ultra-review/references/spec.md +5 -0
  65. package/skills/ultra-review/references/unified-schema.md +146 -11
  66. package/skills/ultra-review/references/worker-packet.md +151 -8
  67. package/skills/ultra-review/scripts/review_wait.py +2653 -63
  68. package/skills/ultra-status/SKILL.md +146 -32
  69. package/skills/ultra-tdd/SKILL.md +14 -4
  70. package/skills/ultra-tdd/references/test-execution.md +6 -1
  71. package/skills/ultra-test/SKILL.md +157 -15
  72. package/skills/ultra-test/scripts/validate_review_transport.cjs +549 -0
  73. package/skills/ultra-test/scripts/worktree_digest.cjs +551 -29
  74. package/skills/ultra-think/SKILL.md +5 -10
  75. package/skills/ultra-think/references/interaction-boundary.md +2 -1
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env python3
2
+ """ZCode wire adapter for Ultra's five file-first lifecycle hooks."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import copy
7
+ import json
8
+ import os
9
+ import re
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+
16
+ HOOK_ROOT = Path(__file__).resolve().parent.parent
17
+ ALLOWED_FEATURES = {
18
+ "block_dangerous_commands.py",
19
+ "compact_context.py",
20
+ "mid_workflow_recall.py",
21
+ "post_edit_guard.py",
22
+ "session_context.py",
23
+ }
24
+ PATCH_FILE_RE = re.compile(r"^\*\*\* (?:Add|Update|Delete) File: (.+?)\s*$", re.MULTILINE)
25
+ PATCH_MOVE_RE = re.compile(r"^\*\*\* Move to: (.+?)\s*$", re.MULTILINE)
26
+
27
+
28
+ def normalize_input(payload: dict[str, Any]) -> dict[str, Any]:
29
+ """Translate ZCode's camelCase hook wire names to the shared contract."""
30
+ normalized = copy.deepcopy(payload)
31
+ aliases = {
32
+ "hookEventName": "hook_event_name",
33
+ "sessionId": "session_id",
34
+ "toolName": "tool_name",
35
+ "toolInput": "tool_input",
36
+ "toolResult": "tool_output",
37
+ "toolResponse": "tool_output",
38
+ "workspaceRoot": "workspace_root",
39
+ }
40
+ for source, target in aliases.items():
41
+ if source in normalized and target not in normalized:
42
+ normalized[target] = normalized[source]
43
+ if "cwd" not in normalized and isinstance(normalized.get("workspace_root"), str):
44
+ normalized["cwd"] = normalized["workspace_root"]
45
+ return normalized
46
+
47
+
48
+ def _patch_paths(tool_input: Any) -> list[str]:
49
+ if not isinstance(tool_input, dict):
50
+ return []
51
+ patch = tool_input.get("command") or tool_input.get("patch") or ""
52
+ if not isinstance(patch, str):
53
+ return []
54
+ paths: list[str] = []
55
+ for match in [*PATCH_FILE_RE.findall(patch), *PATCH_MOVE_RE.findall(patch)]:
56
+ candidate = match.strip()
57
+ if candidate and candidate not in paths:
58
+ paths.append(candidate)
59
+ return paths
60
+
61
+
62
+ def normalize_inputs(payload: dict[str, Any]) -> list[dict[str, Any]]:
63
+ normalized = normalize_input(payload)
64
+ if normalized.get("tool_name") != "ApplyPatch":
65
+ return [normalized]
66
+ paths = _patch_paths(normalized.get("tool_input"))
67
+ if not paths:
68
+ normalized["tool_name"] = "Edit"
69
+ return [normalized]
70
+ results: list[dict[str, Any]] = []
71
+ for file_path in paths:
72
+ item = copy.deepcopy(normalized)
73
+ item["tool_name"] = "Edit"
74
+ tool_input = item.get("tool_input")
75
+ item["tool_input"] = dict(tool_input) if isinstance(tool_input, dict) else {}
76
+ item["tool_input"]["file_path"] = file_path
77
+ results.append(item)
78
+ return results
79
+
80
+
81
+ def _parse_output(stdout: str) -> dict[str, Any]:
82
+ text = stdout.strip()
83
+ if not text:
84
+ return {}
85
+ try:
86
+ value = json.loads(text)
87
+ except json.JSONDecodeError:
88
+ return {"systemMessage": f"Ultra hook returned non-JSON output: {text[:500]}"}
89
+ return value if isinstance(value, dict) else {}
90
+
91
+
92
+ def adapt_output(output: dict[str, Any], event: str) -> dict[str, Any]:
93
+ """Project shared output onto ZCode's strict event-specific schema."""
94
+ if not isinstance(output, dict):
95
+ return {}
96
+ specific = output.get("hookSpecificOutput")
97
+ specific = specific if isinstance(specific, dict) else {}
98
+ context = specific.get("additionalContext") or output.get("additionalContext")
99
+
100
+ if event == "PreToolUse":
101
+ decision = specific.get("permissionDecision")
102
+ if decision in {"allow", "ask", "deny"}:
103
+ projected: dict[str, Any] = {
104
+ "hookEventName": "PreToolUse",
105
+ "permissionDecision": decision,
106
+ }
107
+ reason = specific.get("permissionDecisionReason") or output.get("reason")
108
+ if reason:
109
+ projected["permissionDecisionReason"] = str(reason)
110
+ if context:
111
+ projected["additionalContext"] = str(context)
112
+ return {"hookSpecificOutput": projected}
113
+ if output.get("decision") == "block":
114
+ reason = str(output.get("reason") or "Ultra blocked this tool call")
115
+ return {
116
+ "hookSpecificOutput": {
117
+ "hookEventName": "PreToolUse",
118
+ "permissionDecision": "deny",
119
+ "permissionDecisionReason": reason,
120
+ }
121
+ }
122
+ return {}
123
+
124
+ if event in {
125
+ "SessionStart", "UserPromptSubmit", "PostToolUse",
126
+ "PostToolUseFailure", "Stop",
127
+ } and context:
128
+ return {
129
+ "hookSpecificOutput": {
130
+ "hookEventName": event,
131
+ "additionalContext": str(context),
132
+ }
133
+ }
134
+ return {}
135
+
136
+
137
+ def _merge_outputs(outputs: list[dict[str, Any]], event: str) -> dict[str, Any]:
138
+ if not outputs:
139
+ return {}
140
+ for output in outputs:
141
+ specific = output.get("hookSpecificOutput")
142
+ if isinstance(specific, dict) and specific.get("permissionDecision") == "deny":
143
+ return output
144
+ contexts = []
145
+ for output in outputs:
146
+ specific = output.get("hookSpecificOutput")
147
+ if isinstance(specific, dict) and specific.get("additionalContext"):
148
+ contexts.append(str(specific["additionalContext"]))
149
+ if contexts:
150
+ return {
151
+ "hookSpecificOutput": {
152
+ "hookEventName": event,
153
+ "additionalContext": "\n\n".join(contexts),
154
+ }
155
+ }
156
+ return outputs[-1]
157
+
158
+
159
+ def run_feature(feature: str, payload: dict[str, Any], feature_args: list[str]) -> dict[str, Any]:
160
+ if feature not in ALLOWED_FEATURES or Path(feature).name != feature:
161
+ return {"systemMessage": f"Ultra hook adapter refused unknown feature: {feature}"}
162
+ script = HOOK_ROOT / feature
163
+ if not script.is_file():
164
+ return {"systemMessage": f"Ultra hook feature is missing: {feature}"}
165
+
166
+ event = str(payload.get("hookEventName") or payload.get("hook_event_name") or "")
167
+ normalized_items = normalize_inputs(payload)
168
+ env = os.environ.copy()
169
+ env["UBP_HOOK_RUNTIME"] = "zcode"
170
+ plugin_data = env.get("ZCODE_PLUGIN_DATA") or env.get("CLAUDE_PLUGIN_DATA")
171
+ if plugin_data:
172
+ env["UBP_HOOK_DATA"] = plugin_data
173
+ cwd = normalized_items[0].get("cwd") if normalized_items else None
174
+ run_cwd = str(cwd) if isinstance(cwd, str) and Path(cwd).is_dir() else os.getcwd()
175
+
176
+ outputs: list[dict[str, Any]] = []
177
+ for normalized in normalized_items:
178
+ proc = subprocess.run(
179
+ [sys.executable, str(script), *feature_args],
180
+ input=json.dumps(normalized),
181
+ text=True,
182
+ capture_output=True,
183
+ cwd=run_cwd,
184
+ env=env,
185
+ timeout=30,
186
+ check=False,
187
+ )
188
+ if proc.stderr:
189
+ print(proc.stderr.rstrip(), file=sys.stderr)
190
+ if proc.returncode != 0:
191
+ outputs.append({
192
+ "systemMessage": (
193
+ f"Ultra hook {feature} failed with exit {proc.returncode}: "
194
+ f"{proc.stderr.strip()[:500]}"
195
+ )
196
+ })
197
+ continue
198
+ outputs.append(adapt_output(_parse_output(proc.stdout), event))
199
+ return _merge_outputs(outputs, event)
200
+
201
+
202
+ def main() -> int:
203
+ if len(sys.argv) < 2:
204
+ print(json.dumps({"systemMessage": "Ultra ZCode hook adapter requires a feature name"}))
205
+ return 0
206
+ try:
207
+ payload = json.loads(sys.stdin.read() or "{}")
208
+ if not isinstance(payload, dict):
209
+ payload = {}
210
+ except json.JSONDecodeError as exc:
211
+ print(json.dumps({"systemMessage": f"Ultra hook input parse error: {exc}"}))
212
+ return 0
213
+ try:
214
+ output = run_feature(sys.argv[1], payload, sys.argv[2:])
215
+ except subprocess.TimeoutExpired:
216
+ output = {"systemMessage": f"Ultra hook {sys.argv[1]} timed out"}
217
+ except Exception as exc:
218
+ output = {"systemMessage": f"Ultra hook adapter error in {sys.argv[1]}: {exc}"}
219
+ # ZCode's hook wire schema is strict. Its official contract explicitly accepts an
220
+ # empty stdout stream as a successful no-op, so do not serialize an empty object.
221
+ if output:
222
+ print(json.dumps(output, ensure_ascii=False))
223
+ return 0
224
+
225
+
226
+ if __name__ == "__main__":
227
+ raise SystemExit(main())
@@ -6,24 +6,140 @@ from __future__ import annotations
6
6
  import subprocess
7
7
  from datetime import datetime, timezone
8
8
 
9
- from _common import current_task, emit_context, project_root, read_payload, task_sections
9
+ from _common import (
10
+ RESUME_NAVIGATION_LIMITATION,
11
+ current_task_selection,
12
+ emit_context,
13
+ project_root,
14
+ read_derived_project_file_snapshot,
15
+ read_payload,
16
+ render_task_diagnostics,
17
+ task_sections,
18
+ write_derived_project_file_atomic,
19
+ )
20
+ from session_context import _read_bounded_process_output
10
21
 
11
22
 
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
23
+ COMPACT_SNAPSHOT_RELATIVE = ".ultra/.runtime/compact-snapshot.md"
24
+ COMPACT_SNAPSHOT_MAX_BYTES = 1024 * 1024
25
+ COMPACT_GIT_TIMEOUT_SECONDS = 5
26
+ COMPACT_GIT_MAX_OUTPUT_BYTES = 256 * 1024
27
+
28
+
29
+ def git_output(root, *args: str):
30
+ command = ["git", *args]
31
+ rendered = " ".join(command)
32
+ try:
33
+ process = subprocess.Popen(
34
+ command,
35
+ cwd=root,
36
+ stdin=subprocess.PIPE,
37
+ stdout=subprocess.PIPE,
38
+ stderr=subprocess.PIPE,
39
+ shell=False,
40
+ )
41
+ except FileNotFoundError:
42
+ return "unavailable", {
43
+ "code": "compact_git_missing",
44
+ "message": f"Compact snapshot could not run `{rendered}` because Git is unavailable.",
45
+ "path": str(root),
46
+ "command": command,
47
+ "repair": (
48
+ "Restore a working Git executable and retry PreCompact; canonical task "
49
+ "Acceptance Criteria and the navigational Resume Note remain recoverable."
50
+ ),
51
+ }
52
+ except OSError as error:
53
+ return "unavailable", {
54
+ "code": "compact_git_execution_error",
55
+ "message": f"Compact snapshot could not execute `{rendered}`: {error}",
56
+ "path": str(root),
57
+ "command": command,
58
+ "repair": (
59
+ "Repair local Git execution and retry PreCompact; canonical task Acceptance "
60
+ "Criteria and the navigational Resume Note remain recoverable."
61
+ ),
62
+ }
63
+
64
+ stdout, _stderr, failure = _read_bounded_process_output(
65
+ process,
66
+ COMPACT_GIT_TIMEOUT_SECONDS,
67
+ COMPACT_GIT_MAX_OUTPUT_BYTES,
68
+ b"",
15
69
  )
16
- return result.stdout.strip() if result.returncode == 0 else "unavailable"
70
+ if failure == "timeout":
71
+ return "unavailable", {
72
+ "code": "compact_git_timeout",
73
+ "message": (
74
+ f"Compact snapshot stopped `{rendered}` after the "
75
+ f"{COMPACT_GIT_TIMEOUT_SECONDS}-second execution ceiling."
76
+ ),
77
+ "path": str(root),
78
+ "command": command,
79
+ "repair": (
80
+ "Retry PreCompact after Git becomes responsive; canonical task Acceptance "
81
+ "Criteria and the navigational Resume Note remain recoverable."
82
+ ),
83
+ }
84
+ if failure == "output_too_large":
85
+ return "unavailable", {
86
+ "code": "compact_git_output_too_large",
87
+ "message": (
88
+ f"Compact snapshot stopped `{rendered}` after combined stdout and stderr "
89
+ f"exceeded the {COMPACT_GIT_MAX_OUTPUT_BYTES}-byte ceiling."
90
+ ),
91
+ "path": str(root),
92
+ "command": command,
93
+ "repair": (
94
+ f"Retry PreCompact after reducing Git command output below the "
95
+ f"{COMPACT_GIT_MAX_OUTPUT_BYTES}-byte combined stdout/stderr ceiling; "
96
+ "canonical task Acceptance Criteria and the navigational Resume Note remain recoverable."
97
+ ),
98
+ }
99
+ if process.returncode != 0:
100
+ return "unavailable", {
101
+ "code": "compact_git_nonzero",
102
+ "message": (
103
+ f"Compact snapshot observed `{rendered}` exit with code "
104
+ f"{process.returncode}."
105
+ ),
106
+ "path": str(root),
107
+ "command": command,
108
+ "returncode": process.returncode,
109
+ "repair": (
110
+ "Repair the local Git worktree or repository state and retry PreCompact; "
111
+ "canonical task Acceptance Criteria and the navigational Resume Note remain recoverable."
112
+ ),
113
+ }
114
+ return stdout.decode("utf-8", errors="replace").strip(), None
17
115
 
18
116
 
19
- def save_snapshot(root) -> None:
20
- task = current_task(root)
21
- sections = task_sections(root, task) if task else {"acceptance": "", "resume": ""}
117
+ def save_snapshot(root):
118
+ selection = current_task_selection(root)
119
+ task = selection["task"]
120
+ sections = (
121
+ task_sections(root, task)
122
+ if task
123
+ else {
124
+ "acceptance": "",
125
+ "resume": "",
126
+ "diagnostics": selection["diagnostics"],
127
+ }
128
+ )
129
+ head, head_failure = git_output(root, "rev-parse", "HEAD")
130
+ worktree, worktree_failure = git_output(root, "status", "--short")
131
+ git_diagnostics = [
132
+ failure
133
+ for failure in (head_failure, worktree_failure)
134
+ if failure is not None
135
+ ]
136
+ snapshot_diagnostics = [*sections["diagnostics"], *git_diagnostics]
137
+ diagnostics = render_task_diagnostics(snapshot_diagnostics)
22
138
  text = [
23
139
  "# Ultra Compact Snapshot",
24
140
  "",
25
141
  f"Generated: {datetime.now(timezone.utc).isoformat()}",
26
- f"HEAD: {git_output(root, 'rev-parse', 'HEAD')}",
142
+ f"HEAD: {head}",
27
143
  f"Task: {task.get('id') if task else 'none'}",
28
144
  "",
29
145
  "## Acceptance Criteria",
@@ -32,15 +148,30 @@ def save_snapshot(root) -> None:
32
148
  "## Resume Note",
33
149
  sections["resume"] or "_(none)_",
34
150
  "",
151
+ RESUME_NAVIGATION_LIMITATION,
152
+ "",
153
+ "## Task Diagnostics",
154
+ diagnostics or "_(none)_",
155
+ "",
35
156
  "## Worktree",
36
157
  "```text",
37
- git_output(root, "status", "--short"),
158
+ worktree,
38
159
  "```",
39
160
  "",
40
161
  ]
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")
162
+ write_failure = write_derived_project_file_atomic(
163
+ root,
164
+ COMPACT_SNAPSHOT_RELATIVE,
165
+ "\n".join(text).encode("utf-8"),
166
+ max_bytes=COMPACT_SNAPSHOT_MAX_BYTES,
167
+ code_prefix="compact_snapshot_write",
168
+ label="Ultra compact snapshot",
169
+ )
170
+ published_diagnostics = list(sections["diagnostics"])
171
+ published_diagnostics.extend(git_diagnostics)
172
+ if write_failure is not None:
173
+ published_diagnostics.append(write_failure)
174
+ return published_diagnostics
44
175
 
45
176
 
46
177
  def main() -> int:
@@ -50,14 +181,42 @@ def main() -> int:
50
181
  return 0
51
182
  event = str(payload.get("hook_event_name") or payload.get("hookEventName") or "")
52
183
  if event == "PreCompact":
53
- save_snapshot(root)
184
+ diagnostics = save_snapshot(root)
185
+ if diagnostics:
186
+ emit_context(
187
+ "PreCompact",
188
+ "Ultra compact snapshot diagnostic:\n"
189
+ + render_task_diagnostics(diagnostics),
190
+ )
54
191
  return 0
55
192
  if event in {"PostCompact", "SessionStart"} and (
56
193
  event == "PostCompact" or payload.get("source") == "compact"
57
194
  ):
58
- file = root / ".ultra" / ".runtime" / "compact-snapshot.md"
59
- if file.is_file():
60
- emit_context("SessionStart", file.read_text(encoding="utf-8"))
195
+ snapshot, failure = read_derived_project_file_snapshot(
196
+ root,
197
+ COMPACT_SNAPSHOT_RELATIVE,
198
+ max_bytes=COMPACT_SNAPSHOT_MAX_BYTES,
199
+ code_prefix="compact_snapshot_read",
200
+ label="Ultra compact snapshot",
201
+ )
202
+ if snapshot is not None:
203
+ try:
204
+ text = snapshot["bytes"].decode("utf-8")
205
+ except UnicodeDecodeError:
206
+ failure = {
207
+ "code": "compact_snapshot_read_invalid_utf8",
208
+ "message": "Ultra compact snapshot must be valid UTF-8 Markdown.",
209
+ "path": snapshot["path"],
210
+ }
211
+ else:
212
+ emit_context("SessionStart", text)
213
+ return 0
214
+ if failure is not None and not failure.get("code", "").endswith("_missing"):
215
+ emit_context(
216
+ "SessionStart",
217
+ "Ultra compact snapshot diagnostic:\n"
218
+ + render_task_diagnostics([failure]),
219
+ )
61
220
  return 0
62
221
 
63
222
 
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env python3
2
2
  """Recall current acceptance before a source-reading or editing tool call."""
3
3
 
4
- from _common import current_task, emit_context, project_root, read_payload, task_sections
4
+ from _common import (
5
+ current_task_selection,
6
+ emit_context,
7
+ project_root,
8
+ read_payload,
9
+ render_task_diagnostics,
10
+ task_sections,
11
+ )
5
12
 
6
13
 
7
14
  def main() -> int:
@@ -9,14 +16,29 @@ def main() -> int:
9
16
  root = project_root(payload) # Idle guard: no `.ultra/` means silent exit.
10
17
  if root is None:
11
18
  return 0
12
- task = current_task(root)
19
+ selection = current_task_selection(root)
20
+ task = selection["task"]
13
21
  if not task:
22
+ diagnostics = render_task_diagnostics(selection["diagnostics"])
23
+ if diagnostics:
24
+ emit_context(
25
+ "PreToolUse",
26
+ f"Ultra task ledger diagnostics:\n{diagnostics}",
27
+ )
14
28
  return 0
15
- acceptance = task_sections(root, task)["acceptance"]
16
- if acceptance:
29
+ sections = task_sections(root, task)
30
+ parts = []
31
+ if sections["acceptance"]:
32
+ parts.append(
33
+ f"Ultra task {task.get('id')} acceptance reminder:\n{sections['acceptance']}"
34
+ )
35
+ diagnostics = render_task_diagnostics(sections["diagnostics"])
36
+ if diagnostics:
37
+ parts.append(f"Ultra task {task.get('id')} diagnostics:\n{diagnostics}")
38
+ if parts:
17
39
  emit_context(
18
40
  "PreToolUse",
19
- f"Ultra task {task.get('id')} acceptance reminder:\n{acceptance}",
41
+ "\n\n".join(parts),
20
42
  )
21
43
  return 0
22
44