ultra-builder-pro-cli 0.13.0 → 0.14.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 (45) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +7 -8
  3. package/adapters/_shared/codex-assets.cjs +23 -163
  4. package/adapters/_shared/runtime-assets.cjs +1 -0
  5. package/adapters/_shared/tests/runtime-assets.test.cjs +18 -0
  6. package/adapters/opencode.js +30 -120
  7. package/adapters/tests/claude.test.cjs +3 -2
  8. package/adapters/tests/codex-hook.test.cjs +34 -7
  9. package/adapters/tests/codex.test.cjs +13 -6
  10. package/adapters/tests/kimi-hook.test.cjs +34 -7
  11. package/adapters/tests/opencode.test.cjs +70 -84
  12. package/docs/AGENT-CONTEXT.md +6 -3
  13. package/docs/ARCHITECTURE.md +8 -1
  14. package/docs/ROADMAP.md +1 -1
  15. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  16. package/hooks/active_task_context.py +22 -50
  17. package/hooks/context_spine.py +61 -332
  18. package/hooks/health_check.py +19 -17
  19. package/hooks/pre_stop_check.py +28 -33
  20. package/hooks/subagent_tracker.py +13 -19
  21. package/hooks/workflow_checkpoint.py +39 -47
  22. package/hooks/workflow_context.py +4 -9
  23. package/hooks/workflow_resume.py +48 -128
  24. package/mcp-server/breadcrumb.cjs +22 -0
  25. package/mcp-server/lib/prd-parser.cjs +14 -41
  26. package/mcp-server/lib/prd-parser.test.cjs +49 -138
  27. package/mcp-server/lib/project-breadcrumb.cjs +139 -0
  28. package/mcp-server/lib/project-breadcrumb.test.cjs +112 -0
  29. package/mcp-server/lib/task-expander.cjs +9 -38
  30. package/mcp-server/lib/task-expander.test.cjs +54 -209
  31. package/mcp-server/server.cjs +21 -35
  32. package/mcp-server/tests/server-contract.test.cjs +41 -45
  33. package/package.json +2 -4
  34. package/skills/cc-collab/SKILL.md +2 -2
  35. package/skills/codex-collab/SKILL.md +2 -3
  36. package/skills/ultra-plan/SKILL.md +15 -5
  37. package/skills/ultra-verify/SKILL.md +21 -30
  38. package/skills/ultra-verify/scripts/verify_wait.py +54 -54
  39. package/spec/mcp-tools.yaml +38 -10
  40. package/mcp-server/lib/llm-client.cjs +0 -157
  41. package/mcp-server/lib/llm-client.test.cjs +0 -156
  42. package/orchestrator/planner/expand-prompt.cjs +0 -62
  43. package/orchestrator/planner/expand-prompt.test.cjs +0 -52
  44. package/orchestrator/planner/prd-prompt.cjs +0 -44
  45. package/orchestrator/planner/prd-prompt.test.cjs +0 -30
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Write a minimal, mechanical compaction checkpoint for an active workflow."""
2
+ """Write a recovery projection of the canonical DB breadcrumb before compaction."""
3
3
 
4
4
  import json
5
5
  import os
@@ -8,18 +8,12 @@ import tempfile
8
8
  from datetime import datetime, timezone
9
9
  from pathlib import Path
10
10
 
11
- TERMINAL = {"committed", "completed", "done", "cancelled"}
12
-
13
-
14
- def valid_workflow(state: object) -> bool:
15
- return (
16
- isinstance(state, dict)
17
- and isinstance(state.get("command"), str)
18
- and bool(state["command"].strip())
19
- and state.get("step") is not None
20
- and isinstance(state.get("status"), str)
21
- and bool(state["status"].strip())
22
- )
11
+ from context_spine import (
12
+ ContextSpineError,
13
+ find_root,
14
+ read_breadcrumb,
15
+ render_breadcrumb,
16
+ )
23
17
 
24
18
 
25
19
  def main() -> None:
@@ -29,41 +23,39 @@ def main() -> None:
29
23
  except json.JSONDecodeError as exc:
30
24
  print(f"[workflow_checkpoint] invalid hook input: {exc}", file=sys.stderr)
31
25
  data = {}
32
- start = Path(data.get("cwd") or Path.cwd()).resolve()
33
- for root in (start, *start.parents):
34
- state_file = root / ".ultra" / "workflow-state.json"
35
- if not state_file.is_file():
36
- continue
37
- try:
38
- state = json.loads(state_file.read_text(encoding="utf-8"))
39
- except (OSError, json.JSONDecodeError) as exc:
40
- print(f"[workflow_checkpoint] cannot read {state_file}: {exc}", file=sys.stderr)
41
- break
42
- if not valid_workflow(state):
43
- print(f"[workflow_checkpoint] invalid workflow state in {state_file}", file=sys.stderr)
44
- break
45
- if state.get("status") in TERMINAL:
46
- break
47
- runtime_dir = root / ".ultra" / "runtime"
48
- runtime_dir.mkdir(parents=True, exist_ok=True)
49
- checkpoint = {
50
- "schema": 1,
51
- "captured_at": datetime.now(timezone.utc).isoformat(),
52
- "session_id": data.get("session_id", ""),
53
- "workflow": state,
54
- }
55
- fd, temp_name = tempfile.mkstemp(prefix="checkpoint.", suffix=".json", dir=runtime_dir)
56
- try:
57
- with os.fdopen(fd, "w", encoding="utf-8") as handle:
58
- json.dump(checkpoint, handle, ensure_ascii=False, indent=2)
59
- handle.write("\n")
60
- os.replace(temp_name, runtime_dir / "checkpoint.json")
61
- finally:
62
- if os.path.exists(temp_name):
63
- os.unlink(temp_name)
64
- print(json.dumps({"systemMessage": "Ultra workflow checkpoint saved."}))
26
+ root = find_root(Path(data.get("cwd") or Path.cwd()).resolve())
27
+ if root is None:
28
+ print(json.dumps({}))
29
+ return
30
+ try:
31
+ breadcrumb = read_breadcrumb(root)
32
+ except ContextSpineError as exc:
33
+ print(f"[workflow_checkpoint] cannot inspect Context Spine: {exc}", file=sys.stderr)
34
+ print(json.dumps({}))
65
35
  return
66
- print(json.dumps({}))
36
+ if not breadcrumb or not breadcrumb.get("change_id"):
37
+ print(json.dumps({}))
38
+ return
39
+
40
+ runtime_dir = root / ".ultra" / "runtime"
41
+ runtime_dir.mkdir(parents=True, exist_ok=True)
42
+ checkpoint = {
43
+ "schema": 2,
44
+ "captured_at": datetime.now(timezone.utc).isoformat(),
45
+ "session_id": data.get("session_id", ""),
46
+ "breadcrumb": dict(breadcrumb),
47
+ "rendered": render_breadcrumb(root, breadcrumb),
48
+ }
49
+ fd, temp_name = tempfile.mkstemp(prefix="checkpoint.", suffix=".json", dir=runtime_dir)
50
+ try:
51
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
52
+ json.dump(checkpoint, handle, ensure_ascii=False, indent=2)
53
+ handle.write("\n")
54
+ os.replace(temp_name, runtime_dir / "checkpoint.json")
55
+ finally:
56
+ if os.path.exists(temp_name):
57
+ os.unlink(temp_name)
58
+ print(json.dumps({"systemMessage": "Ultra workflow checkpoint saved."}))
67
59
 
68
60
 
69
61
  if __name__ == "__main__":
@@ -2,11 +2,10 @@
2
2
  """Inject one compact Ultra state position without persistent memory."""
3
3
 
4
4
  import json
5
- import sqlite3
6
5
  import sys
7
6
  from pathlib import Path
8
7
 
9
- from context_spine import find_root, read_breadcrumb, render_breadcrumb
8
+ from context_spine import ContextSpineError, find_root, read_breadcrumb, render_breadcrumb
10
9
 
11
10
 
12
11
  def payload() -> dict:
@@ -26,18 +25,14 @@ def main() -> None:
26
25
  return
27
26
  try:
28
27
  breadcrumb = read_breadcrumb(root)
29
- except (sqlite3.Error, OSError) as exc:
28
+ except ContextSpineError as exc:
30
29
  print(f"[workflow_context] cannot inspect Context Spine: {exc}", file=sys.stderr)
31
30
  breadcrumb = None
32
31
  if breadcrumb:
33
32
  context = render_breadcrumb(root, breadcrumb)
34
33
  else:
35
- context = "\n".join([
36
- "[Ultra baseline]",
37
- f"Project: {root}",
38
- "No active continuous change. Start daily work with the ultra-change workflow.",
39
- "Authority: .ultra/state.db; JSON/Markdown remain projections or evidence artifacts.",
40
- ])
34
+ print(json.dumps({}))
35
+ return
41
36
  print(json.dumps({"hookSpecificOutput": {
42
37
  "hookEventName": "SessionStart",
43
38
  "additionalContext": context,
@@ -1,84 +1,53 @@
1
1
  #!/usr/bin/env python3
2
- """Restore only the current Ultra workflow boundary after compaction."""
2
+ """Re-inject live DB authority after compaction; checkpoints are advisory only."""
3
3
 
4
4
  import json
5
- import os
6
- import sqlite3
7
5
  import sys
8
- import tempfile
9
- from datetime import datetime, timezone
6
+ from datetime import datetime
10
7
  from pathlib import Path
11
8
 
12
- from context_spine import find_root, read_breadcrumb, render_breadcrumb
9
+ from context_spine import (
10
+ ContextSpineError,
11
+ find_root,
12
+ read_breadcrumb,
13
+ render_breadcrumb,
14
+ )
13
15
 
14
- TERMINAL = {"committed", "completed", "done", "cancelled"}
15
16
 
17
+ def _hook_output(event: str, text: str) -> dict:
18
+ if event == "SessionStart":
19
+ return {"hookSpecificOutput": {
20
+ "hookEventName": "SessionStart",
21
+ "additionalContext": text,
22
+ }}
23
+ return {"additionalContext": text}
16
24
 
17
- def valid_workflow(state: object) -> bool:
18
- return (
19
- isinstance(state, dict)
20
- and isinstance(state.get("command"), str)
21
- and bool(state["command"].strip())
22
- and state.get("step") is not None
23
- and isinstance(state.get("status"), str)
24
- and bool(state["status"].strip())
25
- )
26
25
 
27
-
28
- def read_json(file: Path, label: str):
26
+ def _checkpoint_advisory(root: Path):
27
+ file = root / ".ultra" / "runtime" / "checkpoint.json"
29
28
  if not file.is_file():
30
29
  return None
31
30
  try:
32
- return json.loads(file.read_text(encoding="utf-8"))
33
- except (OSError, json.JSONDecodeError) as exc:
34
- print(f"[workflow_resume] cannot read {label} {file}: {exc}", file=sys.stderr)
35
- return None
36
-
37
-
38
- def parse_time(value: object):
39
- if not isinstance(value, str) or not value.strip():
40
- return None
41
- try:
42
- return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc)
43
- except ValueError:
44
- return None
45
-
46
-
47
- def newest_time(file: Path, *values: object) -> datetime:
48
- candidates = [parsed for value in values if (parsed := parse_time(value)) is not None]
49
- try:
50
- candidates.append(datetime.fromtimestamp(file.stat().st_mtime, tz=timezone.utc))
51
- except OSError:
52
- pass
53
- return max(candidates, default=datetime.min.replace(tzinfo=timezone.utc))
54
-
55
-
56
- def load_checkpoint(file: Path):
57
- checkpoint = read_json(file, "checkpoint")
58
- if checkpoint is None:
31
+ value = json.loads(file.read_text(encoding="utf-8"))
32
+ datetime.fromisoformat(str(value.get("captured_at", "")).replace("Z", "+00:00"))
33
+ except (OSError, json.JSONDecodeError, ValueError, AttributeError) as exc:
34
+ print(f"[workflow_resume] cannot read checkpoint {file}: {exc}", file=sys.stderr)
59
35
  return None
60
- if not isinstance(checkpoint, dict):
36
+ if (
37
+ not isinstance(value, dict)
38
+ or value.get("schema") != 2
39
+ or not isinstance(value.get("breadcrumb"), dict)
40
+ or not isinstance(value.get("rendered"), str)
41
+ or not value["rendered"].strip()
42
+ ):
61
43
  print(f"[workflow_resume] invalid checkpoint schema in {file}", file=sys.stderr)
62
44
  return None
63
- captured_at = checkpoint.get("captured_at")
64
- workflow = checkpoint.get("workflow")
65
- if checkpoint.get("schema") != 1 or parse_time(captured_at) is None or not valid_workflow(workflow):
66
- print(f"[workflow_resume] invalid checkpoint schema in {file}", file=sys.stderr)
67
- return None
68
- return checkpoint
69
-
70
-
71
- def restore_state(file: Path, state: dict) -> None:
72
- file.parent.mkdir(parents=True, exist_ok=True)
73
- fd, temp_name = tempfile.mkstemp(prefix="workflow-state.", suffix=".json", dir=file.parent)
74
- try:
75
- with os.fdopen(fd, "w", encoding="utf-8") as handle:
76
- json.dump(state, handle, ensure_ascii=False, indent=2)
77
- handle.write("\n")
78
- os.replace(temp_name, file)
79
- finally:
80
- if os.path.exists(temp_name):
81
- os.unlink(temp_name)
45
+ return "\n".join([
46
+ "[Ultra checkpoint advisory]",
47
+ "Live .ultra/state.db could not be read; this checkpoint is not authority.",
48
+ value["rendered"],
49
+ "Run ultra-doctor before mutation.",
50
+ ])
82
51
 
83
52
 
84
53
  def main() -> None:
@@ -88,72 +57,23 @@ def main() -> None:
88
57
  except json.JSONDecodeError as exc:
89
58
  print(f"[workflow_resume] invalid hook input: {exc}", file=sys.stderr)
90
59
  data = {}
91
- start = Path(data.get("cwd") or Path.cwd()).resolve()
92
- root = find_root(start)
93
- if root is not None:
94
- try:
95
- breadcrumb = read_breadcrumb(root)
96
- except (sqlite3.Error, OSError) as exc:
97
- print(f"[workflow_resume] cannot inspect Context Spine: {exc}", file=sys.stderr)
98
- breadcrumb = None
60
+ root = find_root(Path(data.get("cwd") or Path.cwd()).resolve())
61
+ if root is None:
62
+ print(json.dumps({}))
63
+ return
64
+ text = None
65
+ try:
66
+ breadcrumb = read_breadcrumb(root)
99
67
  if breadcrumb:
100
68
  text = render_breadcrumb(root, breadcrumb)
101
- event = data.get("hook_event_name", "SessionStart")
102
- if event == "SessionStart":
103
- print(json.dumps({"hookSpecificOutput": {
104
- "hookEventName": "SessionStart",
105
- "additionalContext": text,
106
- }}))
107
- else:
108
- print(json.dumps({"additionalContext": text}))
109
- return
110
- for root in (start, *start.parents):
111
- state_file = root / ".ultra" / "workflow-state.json"
112
- checkpoint_file = root / ".ultra" / "runtime" / "checkpoint.json"
113
- if not state_file.is_file() and not checkpoint_file.is_file():
114
- continue
115
- live = read_json(state_file, "workflow state")
116
- if live is not None and not valid_workflow(live):
117
- print(f"[workflow_resume] invalid workflow state in {state_file}", file=sys.stderr)
118
- live = None
119
- if live is not None and live.get("status") in TERMINAL:
120
- break
121
- checkpoint = load_checkpoint(checkpoint_file)
122
- checkpoint_state = checkpoint["workflow"] if checkpoint is not None else None
123
- if checkpoint_state is not None and checkpoint_state.get("status") in TERMINAL:
124
- checkpoint_state = None
125
-
126
- source = ".ultra/workflow-state.json"
127
- state = live
128
- if checkpoint_state is not None:
129
- checkpoint_time = newest_time(
130
- checkpoint_file, checkpoint_state.get("ts"), checkpoint.get("captured_at")
131
- )
132
- live_time = newest_time(state_file, live.get("ts")) if live is not None else None
133
- if live_time is None or checkpoint_time >= live_time:
134
- state = checkpoint_state
135
- source = ".ultra/runtime/checkpoint.json"
136
- restore_state(state_file, state)
137
- if state is None:
138
- break
139
- text = (
140
- "[Ultra workflow resumed] "
141
- f"command={state.get('command', 'unknown')}; "
142
- f"task={state.get('task_id', state.get('task', 'unknown'))}; "
143
- f"step={state.get('step', 'unknown')}; status={state.get('status', 'active')}. "
144
- f"source={source}. Continue from the recovered workflow boundary and verify "
145
- "task/session/change state through .ultra/state.db."
146
- )
147
- event = data.get("hook_event_name", "SessionStart")
148
- if event == "SessionStart":
149
- print(json.dumps({"hookSpecificOutput": {
150
- "hookEventName": "SessionStart",
151
- "additionalContext": text,
152
- }}))
153
- else:
154
- print(json.dumps({"additionalContext": text}))
69
+ except ContextSpineError as exc:
70
+ print(f"[workflow_resume] cannot inspect Context Spine: {exc}", file=sys.stderr)
71
+ text = _checkpoint_advisory(root)
72
+ if not text:
73
+ print(json.dumps({}))
155
74
  return
156
- print(json.dumps({}))
75
+ event = data.get("hook_event_name", "SessionStart")
76
+ print(json.dumps(_hook_output(event, text)))
157
77
 
158
78
 
159
79
  if __name__ == "__main__":
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const runtime = fs.existsSync(path.join(__dirname, 'index.cjs'))
8
+ ? require('./index.cjs')
9
+ : require('./server.cjs');
10
+
11
+ function main() {
12
+ const rootDir = path.resolve(process.argv[2] || process.cwd());
13
+ const breadcrumb = runtime.readProjectBreadcrumb(rootDir);
14
+ process.stdout.write(`${JSON.stringify({
15
+ breadcrumb,
16
+ text: breadcrumb ? runtime.renderProjectBreadcrumb(rootDir, breadcrumb) : null,
17
+ })}\n`);
18
+ }
19
+
20
+ if (require.main === module) main();
21
+
22
+ module.exports = { main };
@@ -1,17 +1,11 @@
1
1
  'use strict';
2
2
 
3
- // Phase 8A.1 — PRD → task[] parser. Delegates LLM call to an injected
4
- // llmClient (see lib/llm-client.cjs). The parser itself is pure logic:
5
- // prompt assembly → schema validation → id normalization. Keeping the
6
- // llmClient as a parameter makes unit tests a Test Double exercise without
7
- // mocking network I/O.
3
+ // Validate and normalize a task graph derived from a PRD by the current host.
4
+ // The MCP runtime is deliberately deterministic: model judgment stays with
5
+ // the host that already owns the user interaction, while this module owns the
6
+ // machine contract before state.db is mutated.
8
7
 
9
8
  const Ajv = require('ajv/dist/2020');
10
- const {
11
- buildSystemPrompt,
12
- buildUserPrompt,
13
- } = require('../../orchestrator/planner/prd-prompt.cjs');
14
-
15
9
  const TASK_LIST_SCHEMA = Object.freeze({
16
10
  type: 'object',
17
11
  required: ['tasks'],
@@ -48,39 +42,22 @@ class PrdParseError extends Error {
48
42
  }
49
43
  }
50
44
 
51
- async function parsePrd(prdText, { llmClient, tag } = {}) {
52
- if (!prdText || typeof prdText !== 'string' || !prdText.trim()) {
53
- throw new PrdParseError('NO_INPUT', 'prd text is empty');
54
- }
55
- if (!llmClient || typeof llmClient.completeJson !== 'function') {
56
- throw new PrdParseError('NO_LLM_CLIENT', 'llmClient.completeJson is required');
45
+ function parsePrd(tasks, { tag, changeId } = {}) {
46
+ if (!Array.isArray(tasks) || tasks.length === 0) {
47
+ throw new PrdParseError('NO_INPUT', 'at least one host-derived task is required');
57
48
  }
58
-
59
- const system = buildSystemPrompt();
60
- const user = buildUserPrompt(prdText);
61
-
62
- let reply;
63
- try {
64
- reply = await llmClient.completeJson({ system, user });
65
- } catch (err) {
66
- throw new PrdParseError(
67
- 'PARSE_FAILED',
68
- `LLM call failed: ${err.message}`,
69
- err,
70
- );
71
- }
72
-
73
- if (!validateTaskList(reply.json)) {
49
+ const input = { tasks };
50
+ if (!validateTaskList(input)) {
74
51
  throw new PrdParseError(
75
52
  'PARSE_FAILED',
76
- `LLM output failed schema: ${ajv.errorsText(validateTaskList.errors)}`,
53
+ `task graph failed schema: ${ajv.errorsText(validateTaskList.errors)}`,
77
54
  );
78
55
  }
79
56
 
80
57
  const ids = new Set();
81
- const tasks = reply.json.tasks.map((t) => {
58
+ const normalized = tasks.map((t) => {
82
59
  if (ids.has(t.id)) {
83
- throw new PrdParseError('PARSE_FAILED', `duplicate task id "${t.id}" in LLM output`);
60
+ throw new PrdParseError('PARSE_FAILED', `duplicate task id "${t.id}" in task graph`);
84
61
  }
85
62
  ids.add(t.id);
86
63
  return {
@@ -92,15 +69,11 @@ async function parsePrd(prdText, { llmClient, tag } = {}) {
92
69
  deps: Array.isArray(t.deps) ? t.deps : [],
93
70
  files_modified: Array.isArray(t.files_modified) ? t.files_modified : [],
94
71
  tag: tag || null,
72
+ change_id: changeId || null,
95
73
  };
96
74
  });
97
75
 
98
- return {
99
- tasks,
100
- usage: reply.usage || {},
101
- model: reply.model,
102
- provider: reply.provider,
103
- };
76
+ return { tasks: normalized };
104
77
  }
105
78
 
106
79
  module.exports = {