ultra-builder-pro-cli 0.9.0 → 0.11.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/CHANGELOG.md +92 -1
- package/README.md +11 -6
- package/adapters/_shared/codex-assets.cjs +23 -331
- package/adapters/_shared/handbook.cjs +2 -0
- package/adapters/_shared/runtime-assets.cjs +25 -0
- package/adapters/_shared/tests/handbook.test.cjs +3 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +12 -4
- package/adapters/claude.js +28 -5
- package/adapters/kimi.js +15 -138
- package/adapters/opencode.js +59 -63
- package/adapters/tests/claude.test.cjs +26 -9
- package/adapters/tests/codex.test.cjs +23 -9
- package/adapters/tests/kimi.test.cjs +15 -11
- package/adapters/tests/opencode.test.cjs +57 -10
- package/agents/code-reviewer.md +24 -85
- package/agents/debugger.md +30 -132
- package/agents/review-code.md +24 -104
- package/agents/review-comments.md +20 -83
- package/agents/review-coordinator.md +40 -137
- package/agents/review-design.md +21 -118
- package/agents/review-errors.md +22 -70
- package/agents/review-spec.md +36 -0
- package/agents/review-tests.md +20 -76
- package/agents/tdd-runner.md +26 -84
- package/commands/learn.md +5 -36
- package/commands/ultra-change.md +5 -43
- package/commands/ultra-deliver.md +4 -46
- package/commands/ultra-dev.md +4 -48
- package/commands/ultra-doctor.md +4 -30
- package/commands/ultra-init.md +5 -47
- package/commands/ultra-plan.md +5 -48
- package/commands/ultra-research.md +5 -54
- package/commands/ultra-status.md +5 -44
- package/commands/ultra-test.md +5 -45
- package/commands/ultra-think.md +5 -40
- package/docs/AGENT-CONTEXT.md +34 -12
- package/docs/ARCHITECTURE.md +31 -5
- package/docs/PLAN.zh-CN.md +3 -2
- package/docs/ROADMAP.md +5 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +7 -5
- package/docs/SKILL-AUTHORING.md +75 -0
- package/docs/USER-HANDBOOK-CONTRACT.md +9 -2
- package/hooks/README.md +6 -4
- package/hooks/active_task_context.py +14 -0
- package/hooks/context_spine.py +218 -0
- package/hooks/health_check.py +1 -1
- package/hooks/workflow_context.py +16 -123
- package/hooks/workflow_resume.py +21 -0
- package/mcp-server/lib/change-workflow.cjs +89 -11
- package/mcp-server/lib/change-workflow.test.cjs +202 -7
- package/mcp-server/lib/context-spine.cjs +331 -0
- package/mcp-server/lib/context-spine.test.cjs +73 -0
- package/mcp-server/lib/spec-learning.cjs +191 -0
- package/mcp-server/lib/spec-learning.test.cjs +62 -0
- package/mcp-server/lib/state-db.cjs +72 -10
- package/mcp-server/lib/state-db.test.cjs +7 -2
- package/mcp-server/server.cjs +31 -1
- package/mcp-server/tests/db-ops.test.cjs +3 -3
- package/mcp-server/tests/server-contract.test.cjs +74 -0
- package/mcp-server/tests/state-db.test.cjs +20 -3
- package/package.json +3 -2
- package/skills/cc-collab/SKILL.md +16 -18
- package/skills/code-review-expert/SKILL.md +39 -114
- package/skills/code-review-expert/references/correctness-reliability.md +30 -0
- package/skills/code-review-expert/references/design-boundaries.md +26 -0
- package/skills/code-review-expert/references/removal-plan.md +17 -45
- package/skills/codex-collab/SKILL.md +23 -28
- package/skills/codex-collab/evals/evals.json +18 -23
- package/skills/integration-rules/SKILL.md +28 -67
- package/skills/learn/SKILL.md +46 -105
- package/skills/security-rules/SKILL.md +34 -56
- package/skills/testing-rules/SKILL.md +28 -52
- package/skills/ultra-change/SKILL.md +61 -145
- package/skills/ultra-deliver/SKILL.md +63 -229
- package/skills/ultra-dev/SKILL.md +46 -338
- package/skills/ultra-doctor/SKILL.md +39 -71
- package/skills/ultra-init/SKILL.md +59 -139
- package/skills/ultra-plan/SKILL.md +50 -310
- package/skills/ultra-research/SKILL.md +59 -152
- package/skills/ultra-research/references/architecture.md +38 -0
- package/skills/ultra-research/references/problem-product.md +39 -0
- package/skills/ultra-research/references/quality-delivery.md +34 -0
- package/skills/ultra-research/references/synthesis.md +38 -0
- package/skills/ultra-research/references/users-scope.md +40 -0
- package/skills/ultra-review/SKILL.md +55 -427
- package/skills/ultra-review/references/unified-schema.md +76 -101
- package/skills/ultra-review/scripts/review_wait.py +242 -77
- package/skills/ultra-status/SKILL.md +38 -173
- package/skills/ultra-test/SKILL.md +41 -195
- package/skills/ultra-think/SKILL.md +43 -101
- package/skills/ultra-verify/SKILL.md +44 -84
- package/skills/ultra-verify/evals/evals.json +24 -27
- package/spec/cli-protocol.md +3 -0
- package/spec/mcp-tools.yaml +133 -4
- package/spec/schemas/command-manifest.schema.json +7 -25
- package/spec/schemas/skill-manifest.schema.json +9 -79
- package/spec/schemas/state-db.sql +31 -0
- package/skills/code-review-expert/references/code-quality-checklist.md +0 -112
- package/skills/code-review-expert/references/integration-checklist.md +0 -46
- package/skills/code-review-expert/references/security-checklist.md +0 -100
- package/skills/code-review-expert/references/solid-checklist.md +0 -65
- package/skills/ultra-research/steps/step-00-problem-validation.md +0 -154
- package/skills/ultra-research/steps/step-01-opportunity-discovery.md +0 -172
- package/skills/ultra-research/steps/step-02-market-assessment.md +0 -195
- package/skills/ultra-research/steps/step-03-competitive-landscape.md +0 -197
- package/skills/ultra-research/steps/step-04-product-strategy.md +0 -196
- package/skills/ultra-research/steps/step-05-assumptions-validation.md +0 -234
- package/skills/ultra-research/steps/step-10-user-personas.md +0 -169
- package/skills/ultra-research/steps/step-11-user-scenarios.md +0 -181
- package/skills/ultra-research/steps/step-20-user-stories.md +0 -156
- package/skills/ultra-research/steps/step-21-features-scope.md +0 -157
- package/skills/ultra-research/steps/step-22-success-metrics.md +0 -195
- package/skills/ultra-research/steps/step-30-architecture-context.md +0 -154
- package/skills/ultra-research/steps/step-31-solution-strategy.md +0 -172
- package/skills/ultra-research/steps/step-32-building-blocks.md +0 -188
- package/skills/ultra-research/steps/step-40-deployment.md +0 -123
- package/skills/ultra-research/steps/step-41-quality-risks.md +0 -135
- package/skills/ultra-research/steps/step-99-synthesis.md +0 -188
- package/skills/ultra-review/scripts/review_verdict_update.py +0 -99
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
"""Protect Ultra projections and restate an active task boundary before edits."""
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import sqlite3
|
|
5
6
|
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
9
|
+
from context_spine import read_breadcrumb, render_breadcrumb
|
|
10
|
+
|
|
8
11
|
TERMINAL = {"committed", "completed", "done", "cancelled"}
|
|
9
12
|
TASKS_PROJECTION = Path(".ultra/tasks/tasks.json")
|
|
10
13
|
|
|
@@ -53,6 +56,17 @@ def main() -> None:
|
|
|
53
56
|
),
|
|
54
57
|
}}))
|
|
55
58
|
return
|
|
59
|
+
try:
|
|
60
|
+
breadcrumb = read_breadcrumb(root)
|
|
61
|
+
except (sqlite3.Error, OSError) as exc:
|
|
62
|
+
print(f"[active_task_context] cannot inspect Context Spine: {exc}", file=sys.stderr)
|
|
63
|
+
breadcrumb = None
|
|
64
|
+
if breadcrumb and breadcrumb.get("task_id"):
|
|
65
|
+
print(json.dumps({"hookSpecificOutput": {
|
|
66
|
+
"hookEventName": "PreToolUse",
|
|
67
|
+
"additionalContext": render_breadcrumb(root, breadcrumb),
|
|
68
|
+
}}))
|
|
69
|
+
return
|
|
56
70
|
if not state_file.is_file():
|
|
57
71
|
break
|
|
58
72
|
try:
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read and render the compact DB-derived Ultra Context Spine breadcrumb."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import sqlite3
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ACTIVE_CHANGE_STATUSES = ("active", "blocked", "ready")
|
|
11
|
+
TERMINAL_TASK_STATUSES = ("completed", "expanded")
|
|
12
|
+
CONTEXT_COLUMNS = {
|
|
13
|
+
"role", "gate", "next_action", "readiness", "blockers_json",
|
|
14
|
+
"context_json", "manifest_path", "manifest_hash", "git_head", "task_id",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def find_root(start: Path):
|
|
19
|
+
for root in (start, *start.parents):
|
|
20
|
+
ultra = root / ".ultra"
|
|
21
|
+
if (
|
|
22
|
+
(ultra / "state.db").is_file()
|
|
23
|
+
or (ultra / "workflow-state.json").is_file()
|
|
24
|
+
or (ultra / "changes" / "active").is_dir()
|
|
25
|
+
):
|
|
26
|
+
return root
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def safe_json(raw, fallback):
|
|
31
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
32
|
+
return fallback
|
|
33
|
+
try:
|
|
34
|
+
value = json.loads(raw)
|
|
35
|
+
return value
|
|
36
|
+
except json.JSONDecodeError:
|
|
37
|
+
return fallback
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def git_head(root: Path):
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
["git", "rev-parse", "HEAD"],
|
|
43
|
+
cwd=root,
|
|
44
|
+
text=True,
|
|
45
|
+
capture_output=True,
|
|
46
|
+
check=False,
|
|
47
|
+
timeout=2,
|
|
48
|
+
)
|
|
49
|
+
return result.stdout.strip() if result.returncode == 0 else None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _next_action(change, task, tasks, role, readiness, stored, state_changed):
|
|
53
|
+
if readiness != "ready":
|
|
54
|
+
return "Resolve the context readiness blockers, then recompile change.context."
|
|
55
|
+
if not state_changed and isinstance(stored, str) and stored.strip():
|
|
56
|
+
return stored.strip()
|
|
57
|
+
if change["status"] == "ready":
|
|
58
|
+
return f"Run ultra-deliver for change {change['id']}."
|
|
59
|
+
if change["status"] == "blocked":
|
|
60
|
+
return f"Resolve the blockers for change {change['id']}, then recompile context."
|
|
61
|
+
if task and task["status"] == "pending":
|
|
62
|
+
return f"Start task {task['id']} through ultra-dev."
|
|
63
|
+
if task and task["status"] == "in_progress":
|
|
64
|
+
if role == "check":
|
|
65
|
+
return f"Run the exact verification command for task {task['id']}."
|
|
66
|
+
if role == "review":
|
|
67
|
+
return (
|
|
68
|
+
"Complete independent spec-fidelity and engineering-standards "
|
|
69
|
+
f"review for task {task['id']}."
|
|
70
|
+
)
|
|
71
|
+
return f"Continue the approved vertical slice for task {task['id']}."
|
|
72
|
+
if tasks and all(row["status"] in TERMINAL_TASK_STATUSES for row in tasks):
|
|
73
|
+
return f"Run ultra-test for change {change['id']}."
|
|
74
|
+
pending = next((row for row in tasks if row["status"] == "pending"), None)
|
|
75
|
+
if pending:
|
|
76
|
+
return f"Compile implement context for task {pending['id']}."
|
|
77
|
+
return f"Create the first fresh-context tracer-bullet task for change {change['id']}."
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _route(change, task, tasks, readiness):
|
|
81
|
+
if readiness != "ready" or change["status"] == "blocked":
|
|
82
|
+
return "ultra-change"
|
|
83
|
+
if change["status"] == "ready":
|
|
84
|
+
return "ultra-deliver"
|
|
85
|
+
if task and task["status"] in ("pending", "in_progress"):
|
|
86
|
+
return "ultra-dev"
|
|
87
|
+
if tasks and all(row["status"] in TERMINAL_TASK_STATUSES for row in tasks):
|
|
88
|
+
return "ultra-test"
|
|
89
|
+
return "ultra-change"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def read_breadcrumb(root: Path):
|
|
93
|
+
db_path = root / ".ultra" / "state.db"
|
|
94
|
+
if not db_path.is_file():
|
|
95
|
+
return None
|
|
96
|
+
with sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=1) as conn:
|
|
97
|
+
conn.row_factory = sqlite3.Row
|
|
98
|
+
tables = {
|
|
99
|
+
row[0]
|
|
100
|
+
for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
101
|
+
}
|
|
102
|
+
if not {"changes", "tasks", "context_snapshots"}.issubset(tables):
|
|
103
|
+
return None
|
|
104
|
+
change = conn.execute(
|
|
105
|
+
"""SELECT id, status FROM changes
|
|
106
|
+
WHERE status IN ('active', 'blocked', 'ready')
|
|
107
|
+
ORDER BY updated_at DESC LIMIT 1"""
|
|
108
|
+
).fetchone()
|
|
109
|
+
if change is None:
|
|
110
|
+
return None
|
|
111
|
+
tasks = list(conn.execute(
|
|
112
|
+
"SELECT id, status, session_id FROM tasks WHERE change_id = ? ORDER BY created_at ASC",
|
|
113
|
+
(change["id"],),
|
|
114
|
+
))
|
|
115
|
+
columns = {
|
|
116
|
+
row[1] for row in conn.execute("PRAGMA table_info(context_snapshots)")
|
|
117
|
+
}
|
|
118
|
+
snapshot = None
|
|
119
|
+
if CONTEXT_COLUMNS.issubset(columns):
|
|
120
|
+
snapshot = conn.execute(
|
|
121
|
+
"""SELECT task_id, git_head, manifest_path, manifest_hash, role, gate,
|
|
122
|
+
next_action, readiness, blockers_json, context_json
|
|
123
|
+
FROM context_snapshots WHERE change_id = ?
|
|
124
|
+
ORDER BY created_at DESC, rowid DESC LIMIT 1""",
|
|
125
|
+
(change["id"],),
|
|
126
|
+
).fetchone()
|
|
127
|
+
|
|
128
|
+
task_id = snapshot["task_id"] if snapshot else None
|
|
129
|
+
if not task_id:
|
|
130
|
+
active = next((row for row in tasks if row["status"] == "in_progress"), None)
|
|
131
|
+
pending = next((row for row in tasks if row["status"] == "pending"), None)
|
|
132
|
+
task_id = (active or pending)["id"] if (active or pending) else None
|
|
133
|
+
task = next((row for row in tasks if row["id"] == task_id), None)
|
|
134
|
+
context = safe_json(snapshot["context_json"], {}) if snapshot else {}
|
|
135
|
+
role = snapshot["role"] if snapshot else "plan"
|
|
136
|
+
gate = snapshot["gate"] if snapshot else "alignment"
|
|
137
|
+
current_head = git_head(root)
|
|
138
|
+
head_stale = bool(
|
|
139
|
+
snapshot and snapshot["git_head"] and current_head
|
|
140
|
+
and snapshot["git_head"] != current_head
|
|
141
|
+
)
|
|
142
|
+
snapshot_missing = snapshot is None
|
|
143
|
+
legacy_context = bool(
|
|
144
|
+
snapshot and (
|
|
145
|
+
not isinstance(context.get("resume"), dict)
|
|
146
|
+
or not isinstance(context.get("context"), dict)
|
|
147
|
+
or not isinstance(context.get("readiness"), dict)
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
stored_status = context.get("resume", {}).get("task_status")
|
|
151
|
+
current_status = task["status"] if task else None
|
|
152
|
+
state_changed = stored_status is not None and stored_status != current_status
|
|
153
|
+
stored_action = snapshot["next_action"] if snapshot else None
|
|
154
|
+
blockers = []
|
|
155
|
+
if snapshot_missing:
|
|
156
|
+
blockers.append("CONTEXT_NOT_COMPILED")
|
|
157
|
+
else:
|
|
158
|
+
if legacy_context:
|
|
159
|
+
blockers.append("CONTEXT_SNAPSHOT_UPGRADE_REQUIRED")
|
|
160
|
+
if head_stale:
|
|
161
|
+
blockers.append("CONTEXT_HEAD_STALE")
|
|
162
|
+
if state_changed:
|
|
163
|
+
blockers.append("CONTEXT_TASK_STATE_STALE")
|
|
164
|
+
for blocker in safe_json(snapshot["blockers_json"], []):
|
|
165
|
+
if blocker not in blockers:
|
|
166
|
+
blockers.append(blocker)
|
|
167
|
+
readiness = "blocked" if blockers else snapshot["readiness"]
|
|
168
|
+
if snapshot_missing:
|
|
169
|
+
next_action = f"Compile change.context for change {change['id']} before continuing."
|
|
170
|
+
elif legacy_context:
|
|
171
|
+
next_action = "Recompile change.context to upgrade this legacy context snapshot."
|
|
172
|
+
elif head_stale:
|
|
173
|
+
next_action = "Git HEAD changed after context compilation; recompile change.context."
|
|
174
|
+
elif state_changed:
|
|
175
|
+
next_action = "Task state changed after context compilation; recompile change.context."
|
|
176
|
+
else:
|
|
177
|
+
next_action = _next_action(change, task, tasks, role, readiness, stored_action, False)
|
|
178
|
+
return {
|
|
179
|
+
"change_id": change["id"],
|
|
180
|
+
"change_status": change["status"],
|
|
181
|
+
"task_id": task["id"] if task else None,
|
|
182
|
+
"task_status": task["status"] if task else None,
|
|
183
|
+
"session_id": task["session_id"] if task else None,
|
|
184
|
+
"role": role,
|
|
185
|
+
"gate": gate,
|
|
186
|
+
"readiness": readiness,
|
|
187
|
+
"blockers": blockers,
|
|
188
|
+
"next_action": next_action,
|
|
189
|
+
"recommended_workflow": _route(change, task, tasks, readiness),
|
|
190
|
+
"context_manifest_path": snapshot["manifest_path"] if snapshot else None,
|
|
191
|
+
"context_manifest_hash": snapshot["manifest_hash"] if snapshot else None,
|
|
192
|
+
"git_head": current_head,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def render_breadcrumb(root: Path, breadcrumb: dict) -> str:
|
|
197
|
+
task = breadcrumb.get("task_id") or "none"
|
|
198
|
+
task_status = breadcrumb.get("task_status") or "none"
|
|
199
|
+
lines = [
|
|
200
|
+
"[Ultra context spine]",
|
|
201
|
+
f"Project: {root}",
|
|
202
|
+
f"Change: {breadcrumb['change_id']} ({breadcrumb.get('change_status', 'active')})",
|
|
203
|
+
f"Task: {task} ({task_status})",
|
|
204
|
+
f"Role: {breadcrumb.get('role', 'plan')}",
|
|
205
|
+
f"Gate: {breadcrumb.get('gate', 'alignment')}",
|
|
206
|
+
f"Readiness: {breadcrumb.get('readiness', 'ready')}",
|
|
207
|
+
]
|
|
208
|
+
if breadcrumb.get("blockers"):
|
|
209
|
+
lines.append("Blockers: " + ", ".join(breadcrumb["blockers"]))
|
|
210
|
+
lines.extend([
|
|
211
|
+
f"Next: {breadcrumb.get('next_action', 'Inspect Ultra status.')}",
|
|
212
|
+
f"Route: {breadcrumb.get('recommended_workflow', 'ultra-change')}",
|
|
213
|
+
])
|
|
214
|
+
if breadcrumb.get("context_manifest_path"):
|
|
215
|
+
digest = breadcrumb.get("context_manifest_hash") or "unknown"
|
|
216
|
+
lines.append(f"Context: {breadcrumb['context_manifest_path']} sha256={digest}")
|
|
217
|
+
lines.append("Authority: .ultra/state.db; JSON/Markdown remain projections or evidence artifacts.")
|
|
218
|
+
return "\n".join(lines)
|
package/hooks/health_check.py
CHANGED
|
@@ -10,7 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
REQUIRED_TABLES = {
|
|
11
11
|
"tasks", "events", "sessions", "schema_version", "migration_history",
|
|
12
12
|
"telemetry", "specs_refs", "circuit_breaker", "changes", "artifacts",
|
|
13
|
-
"context_snapshots", "trace_links", "incidents", "projection_jobs",
|
|
13
|
+
"context_snapshots", "spec_learning_candidates", "trace_links", "incidents", "projection_jobs",
|
|
14
14
|
"event_consumers",
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Inject
|
|
2
|
+
"""Inject one compact Ultra state position without persistent memory."""
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import sqlite3
|
|
6
6
|
import sys
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
TERMINAL_WORKFLOW = {"committed", "completed", "done", "cancelled"}
|
|
11
|
-
PROVIDER_FIELDS = {
|
|
12
|
-
"provider", "project", "reference", "revision", "indexed_head", "status",
|
|
13
|
-
}
|
|
9
|
+
from context_spine import find_root, read_breadcrumb, render_breadcrumb
|
|
14
10
|
|
|
15
11
|
|
|
16
12
|
def payload() -> dict:
|
|
@@ -22,132 +18,29 @@ def payload() -> dict:
|
|
|
22
18
|
return {}
|
|
23
19
|
|
|
24
20
|
|
|
25
|
-
def find_root(start: Path):
|
|
26
|
-
for root in (start, *start.parents):
|
|
27
|
-
ultra = root / ".ultra"
|
|
28
|
-
if (
|
|
29
|
-
(ultra / "state.db").is_file()
|
|
30
|
-
or (ultra / "workflow-state.json").is_file()
|
|
31
|
-
or (ultra / "changes" / "active").is_dir()
|
|
32
|
-
):
|
|
33
|
-
return root
|
|
34
|
-
return None
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def read_workflow(root: Path):
|
|
38
|
-
state_file = root / ".ultra" / "workflow-state.json"
|
|
39
|
-
if not state_file.is_file():
|
|
40
|
-
return None
|
|
41
|
-
try:
|
|
42
|
-
state = json.loads(state_file.read_text(encoding="utf-8"))
|
|
43
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
44
|
-
print(f"[workflow_context] cannot read {state_file}: {exc}", file=sys.stderr)
|
|
45
|
-
return None
|
|
46
|
-
if isinstance(state, dict) and state.get("status") not in TERMINAL_WORKFLOW:
|
|
47
|
-
return state
|
|
48
|
-
return None
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def safe_json(raw, fallback):
|
|
52
|
-
try:
|
|
53
|
-
value = json.loads(raw)
|
|
54
|
-
return value
|
|
55
|
-
except (TypeError, json.JSONDecodeError):
|
|
56
|
-
return fallback
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def provider_metadata(raw) -> dict:
|
|
60
|
-
refs = safe_json(raw, {})
|
|
61
|
-
if not isinstance(refs, dict):
|
|
62
|
-
return {}
|
|
63
|
-
clean = {}
|
|
64
|
-
for kind, ref in refs.items():
|
|
65
|
-
if kind not in {"memory", "code_graph"} or not isinstance(ref, dict):
|
|
66
|
-
continue
|
|
67
|
-
clean[kind] = {key: ref[key] for key in PROVIDER_FIELDS if key in ref}
|
|
68
|
-
return clean
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def read_changes(root: Path):
|
|
72
|
-
db_path = root / ".ultra" / "state.db"
|
|
73
|
-
if not db_path.is_file():
|
|
74
|
-
return []
|
|
75
|
-
try:
|
|
76
|
-
with sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=1) as conn:
|
|
77
|
-
exists = conn.execute(
|
|
78
|
-
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='changes'"
|
|
79
|
-
).fetchone()
|
|
80
|
-
if not exists:
|
|
81
|
-
return []
|
|
82
|
-
rows = conn.execute(
|
|
83
|
-
"""SELECT id, title, kind, status, intent, docs_impact_json,
|
|
84
|
-
provider_refs_json, artifact_root
|
|
85
|
-
FROM changes WHERE status IN ('active', 'blocked', 'ready')
|
|
86
|
-
ORDER BY updated_at DESC"""
|
|
87
|
-
).fetchall()
|
|
88
|
-
except sqlite3.Error as exc:
|
|
89
|
-
print(f"[workflow_context] cannot inspect {db_path}: {exc}", file=sys.stderr)
|
|
90
|
-
return []
|
|
91
|
-
return [
|
|
92
|
-
{
|
|
93
|
-
"id": row[0], "title": row[1], "kind": row[2], "status": row[3],
|
|
94
|
-
"intent": row[4], "docs_impact": safe_json(row[5], {}),
|
|
95
|
-
"providers": provider_metadata(row[6]), "artifact_root": row[7],
|
|
96
|
-
}
|
|
97
|
-
for row in rows
|
|
98
|
-
]
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def context_text(root: Path, workflow, changes) -> str:
|
|
102
|
-
lines = []
|
|
103
|
-
if workflow:
|
|
104
|
-
lines.extend([
|
|
105
|
-
"[Ultra active workflow]",
|
|
106
|
-
f"Project: {root}",
|
|
107
|
-
f"Command: {workflow.get('command', 'unknown')}",
|
|
108
|
-
f"Task: {workflow.get('task_id', workflow.get('task', 'unknown'))}",
|
|
109
|
-
f"Step: {workflow.get('step', 'unknown')}",
|
|
110
|
-
f"Status: {workflow.get('status', 'active')}",
|
|
111
|
-
])
|
|
112
|
-
if changes:
|
|
113
|
-
change = changes[0]
|
|
114
|
-
lines.extend([
|
|
115
|
-
"[Ultra continuous change]",
|
|
116
|
-
f"Project: {root}",
|
|
117
|
-
f"Change: {change['id']} ({change['kind']}, {change['status']})",
|
|
118
|
-
f"Title: {change['title']}",
|
|
119
|
-
f"Intent: {change['intent']}",
|
|
120
|
-
"Docs impact: " + json.dumps(change["docs_impact"], ensure_ascii=False, sort_keys=True),
|
|
121
|
-
f"Context manifest: {change['artifact_root']}/context-manifest.json",
|
|
122
|
-
"External providers (metadata references only): "
|
|
123
|
-
+ json.dumps(change["providers"], ensure_ascii=False, sort_keys=True),
|
|
124
|
-
])
|
|
125
|
-
if len(changes) > 1:
|
|
126
|
-
lines.append(f"Active change count: {len(changes)}; resolve scope with change.list before mutation.")
|
|
127
|
-
if not workflow and not changes:
|
|
128
|
-
lines.extend([
|
|
129
|
-
"[Ultra baseline]",
|
|
130
|
-
f"Project: {root}",
|
|
131
|
-
"No active continuous change. Start daily work with the ultra-change workflow.",
|
|
132
|
-
])
|
|
133
|
-
lines.append("Authority: .ultra/state.db; generated JSON/Markdown are projections or artifacts.")
|
|
134
|
-
return "\n".join(lines)
|
|
135
|
-
|
|
136
|
-
|
|
137
21
|
def main() -> None:
|
|
138
22
|
data = payload()
|
|
139
23
|
root = find_root(Path(data.get("cwd") or Path.cwd()).resolve())
|
|
140
24
|
if root is None:
|
|
141
25
|
print(json.dumps({}))
|
|
142
26
|
return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
27
|
+
try:
|
|
28
|
+
breadcrumb = read_breadcrumb(root)
|
|
29
|
+
except (sqlite3.Error, OSError) as exc:
|
|
30
|
+
print(f"[workflow_context] cannot inspect Context Spine: {exc}", file=sys.stderr)
|
|
31
|
+
breadcrumb = None
|
|
32
|
+
if breadcrumb:
|
|
33
|
+
context = render_breadcrumb(root, breadcrumb)
|
|
34
|
+
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
|
+
])
|
|
146
41
|
print(json.dumps({"hookSpecificOutput": {
|
|
147
42
|
"hookEventName": "SessionStart",
|
|
148
43
|
"additionalContext": context,
|
|
149
44
|
}}))
|
|
150
|
-
|
|
151
|
-
|
|
152
45
|
if __name__ == "__main__":
|
|
153
46
|
main()
|
package/hooks/workflow_resume.py
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import sqlite3
|
|
6
7
|
import sys
|
|
7
8
|
import tempfile
|
|
8
9
|
from datetime import datetime, timezone
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
12
|
+
from context_spine import find_root, read_breadcrumb, render_breadcrumb
|
|
13
|
+
|
|
11
14
|
TERMINAL = {"committed", "completed", "done", "cancelled"}
|
|
12
15
|
|
|
13
16
|
|
|
@@ -86,6 +89,24 @@ def main() -> None:
|
|
|
86
89
|
print(f"[workflow_resume] invalid hook input: {exc}", file=sys.stderr)
|
|
87
90
|
data = {}
|
|
88
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
|
|
99
|
+
if breadcrumb:
|
|
100
|
+
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
|
|
89
110
|
for root in (start, *start.parents):
|
|
90
111
|
state_file = root / ".ultra" / "workflow-state.json"
|
|
91
112
|
checkpoint_file = root / ".ultra" / "runtime" / "checkpoint.json"
|
|
@@ -6,6 +6,8 @@ const path = require('node:path');
|
|
|
6
6
|
const { spawnSync } = require('node:child_process');
|
|
7
7
|
|
|
8
8
|
const ops = require('./state-ops.cjs');
|
|
9
|
+
const contextSpine = require('./context-spine.cjs');
|
|
10
|
+
const specLearning = require('./spec-learning.cjs');
|
|
9
11
|
|
|
10
12
|
const CHANGE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
11
13
|
const PROVIDER_KINDS = new Set(['memory', 'code_graph']);
|
|
@@ -227,13 +229,16 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
227
229
|
? change.provider_refs
|
|
228
230
|
: normalizeProviderRefs(input.provider_refs);
|
|
229
231
|
const tasks = db.prepare(
|
|
230
|
-
|
|
232
|
+
`SELECT id, title, type, status, priority, complexity, stale, trace_to,
|
|
233
|
+
context_file, files_modified, session_id, created_at, updated_at
|
|
234
|
+
FROM tasks WHERE change_id = ? ORDER BY created_at ASC`,
|
|
231
235
|
).all(change.id).map((task) => ({ ...task, stale: Boolean(task.stale) }));
|
|
232
236
|
const specs = Array.isArray(input.spec_refs) ? input.spec_refs : [];
|
|
233
237
|
const allowedPaths = Array.isArray(input.allowed_paths) ? input.allowed_paths : [];
|
|
234
238
|
const head = gitHead(rootDir);
|
|
239
|
+
const spine = contextSpine.compileRoleContext(db, { input, change, tasks, rootDir });
|
|
235
240
|
const manifest = {
|
|
236
|
-
schema_version: '
|
|
241
|
+
schema_version: '2.0',
|
|
237
242
|
generated_at: nowIso(),
|
|
238
243
|
change: {
|
|
239
244
|
id: change.id, title: change.title, kind: change.kind, status: change.status,
|
|
@@ -241,7 +246,15 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
241
246
|
},
|
|
242
247
|
git: { head },
|
|
243
248
|
tasks,
|
|
249
|
+
selected_task: spine.selected_task,
|
|
244
250
|
specs,
|
|
251
|
+
role: spine.role,
|
|
252
|
+
gate: spine.gate,
|
|
253
|
+
next_action: spine.next_action,
|
|
254
|
+
readiness: spine.readiness,
|
|
255
|
+
context: spine.context,
|
|
256
|
+
execution_contract: spine.execution_contract,
|
|
257
|
+
resume: spine.resume,
|
|
245
258
|
providers,
|
|
246
259
|
provider_boundary: 'metadata references only; memory and code graph content remain external',
|
|
247
260
|
tool_policy: { allowed_paths: allowedPaths },
|
|
@@ -257,15 +270,24 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
257
270
|
ops.tx(db, () => {
|
|
258
271
|
db.prepare(
|
|
259
272
|
`INSERT INTO context_snapshots
|
|
260
|
-
(id, change_id, task_id, git_head, provider_refs_json, manifest_path, manifest_hash
|
|
261
|
-
|
|
273
|
+
(id, change_id, task_id, git_head, provider_refs_json, manifest_path, manifest_hash,
|
|
274
|
+
role, gate, next_action, readiness, blockers_json, context_json,
|
|
275
|
+
token_estimate, token_budget)
|
|
276
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
262
277
|
).run(
|
|
263
278
|
`ctx-${crypto.randomUUID().slice(0, 12)}`, change.id, input.task_id || null,
|
|
264
279
|
head, JSON.stringify(providers), relative, hash,
|
|
280
|
+
spine.role, spine.gate, spine.next_action, spine.readiness.status,
|
|
281
|
+
JSON.stringify(spine.readiness.blockers), JSON.stringify(spine),
|
|
282
|
+
spine.context.token_estimate, spine.context.budget.max_tokens,
|
|
265
283
|
);
|
|
266
284
|
upsertArtifact(db, {
|
|
267
285
|
change_id: change.id, task_id: input.task_id || null, kind: 'context_manifest',
|
|
268
|
-
artifactPath: relative, contentHash: hash,
|
|
286
|
+
artifactPath: relative, contentHash: hash,
|
|
287
|
+
metadata: {
|
|
288
|
+
git_head: head, role: spine.role, gate: spine.gate,
|
|
289
|
+
readiness: spine.readiness.status, token_estimate: spine.context.token_estimate,
|
|
290
|
+
},
|
|
269
291
|
});
|
|
270
292
|
for (const task of tasks) {
|
|
271
293
|
if (!task.trace_to) continue;
|
|
@@ -278,7 +300,11 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
278
300
|
.run(JSON.stringify(providers), nowIso(), change.id);
|
|
279
301
|
ops.appendEventInTx(db, {
|
|
280
302
|
type: 'context_updated', change_id: change.id, task_id: input.task_id || null,
|
|
281
|
-
payload: {
|
|
303
|
+
payload: {
|
|
304
|
+
manifest_path: relative, manifest_hash: hash, git_head: head,
|
|
305
|
+
role: spine.role, gate: spine.gate, readiness: spine.readiness.status,
|
|
306
|
+
next_action: spine.next_action,
|
|
307
|
+
},
|
|
282
308
|
});
|
|
283
309
|
});
|
|
284
310
|
return { manifest, context_manifest_path: manifestPath, manifest_hash: hash };
|
|
@@ -438,16 +464,43 @@ function filesUnder(dir) {
|
|
|
438
464
|
return out;
|
|
439
465
|
}
|
|
440
466
|
|
|
441
|
-
function evidenceBlockers(change, evidence) {
|
|
467
|
+
function evidenceBlockers(change, evidence, tasks) {
|
|
442
468
|
const blockers = [];
|
|
443
469
|
const rows = Array.isArray(evidence) ? evidence : [];
|
|
444
470
|
const byCategory = new Map(rows.map((row) => [row.category, row]));
|
|
445
|
-
for (const category of REQUIRED_EVIDENCE[change.kind]) {
|
|
471
|
+
for (const category of REQUIRED_EVIDENCE[change.kind].filter((value) => value !== 'review')) {
|
|
446
472
|
const row = byCategory.get(category);
|
|
447
473
|
if (!row) blockers.push(`EVIDENCE_MISSING:${category}`);
|
|
448
474
|
else if (!['pass', 'not_applicable'].includes(row.status)) blockers.push(`EVIDENCE_FAILED:${category}`);
|
|
449
475
|
else if (!row.evidence || String(row.evidence).trim().length < 3) blockers.push(`EVIDENCE_EMPTY:${category}`);
|
|
450
476
|
}
|
|
477
|
+
if (['standard', 'major'].includes(change.kind)) {
|
|
478
|
+
const reviews = rows.filter((row) => row.category === 'review');
|
|
479
|
+
for (const axis of ['spec_fidelity', 'engineering_standards']) {
|
|
480
|
+
const row = reviews.find((candidate) => candidate.axis === axis);
|
|
481
|
+
if (!row) blockers.push(`REVIEW_AXIS_MISSING:${axis}`);
|
|
482
|
+
else if (row.status !== 'pass') blockers.push(`REVIEW_AXIS_FAILED:${axis}`);
|
|
483
|
+
else if (!row.evidence || String(row.evidence).trim().length < 3) {
|
|
484
|
+
blockers.push(`REVIEW_AXIS_EMPTY:${axis}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const tests = byCategory.get('tests');
|
|
489
|
+
if (tests && ['standard', 'major', 'incident'].includes(change.kind)
|
|
490
|
+
&& (!tests.seam || String(tests.seam).trim().length < 3)) {
|
|
491
|
+
blockers.push('TEST_SEAM_MISSING');
|
|
492
|
+
}
|
|
493
|
+
if (tests && (change.kind === 'incident' || tasks.some((task) => task.type === 'bugfix'))) {
|
|
494
|
+
const signal = tests.signal;
|
|
495
|
+
if (!signal) blockers.push('TEST_RED_SIGNAL_MISSING');
|
|
496
|
+
else {
|
|
497
|
+
if (!signal.command || String(signal.command).trim().length < 3) blockers.push('TEST_SIGNAL_COMMAND_MISSING');
|
|
498
|
+
if (!signal.expected_red || String(signal.expected_red).trim().length < 3) blockers.push('TEST_EXPECTED_RED_MISSING');
|
|
499
|
+
if (signal.observed_red !== true) blockers.push('TEST_RED_NOT_OBSERVED');
|
|
500
|
+
if (signal.observed_green !== true) blockers.push('TEST_GREEN_NOT_OBSERVED');
|
|
501
|
+
if (signal.deterministic !== true) blockers.push('TEST_SIGNAL_NOT_DETERMINISTIC');
|
|
502
|
+
}
|
|
503
|
+
}
|
|
451
504
|
return blockers;
|
|
452
505
|
}
|
|
453
506
|
|
|
@@ -457,11 +510,31 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
457
510
|
if (!['active', 'blocked'].includes(change.status)) {
|
|
458
511
|
throw new ChangeWorkflowError('CHANGE_NOT_CONVERGEABLE', `change ${input.id} is ${change.status}`);
|
|
459
512
|
}
|
|
460
|
-
const
|
|
461
|
-
const
|
|
513
|
+
const tasks = db.prepare('SELECT id, type, status, stale FROM tasks WHERE change_id = ? ORDER BY id').all(change.id);
|
|
514
|
+
const blockers = new Set(evidenceBlockers(change, input.evidence, tasks));
|
|
462
515
|
if (tasks.length === 0) blockers.add('NO_TASKS');
|
|
463
516
|
if (tasks.some((task) => !['completed', 'expanded'].includes(task.status))) blockers.add('TASKS_INCOMPLETE');
|
|
464
517
|
if (tasks.some((task) => Boolean(task.stale))) blockers.add('TASK_CONTEXT_STALE');
|
|
518
|
+
for (const task of tasks) {
|
|
519
|
+
const snapshot = db.prepare(
|
|
520
|
+
`SELECT readiness, context_json FROM context_snapshots
|
|
521
|
+
WHERE change_id = ? AND task_id = ?
|
|
522
|
+
ORDER BY created_at DESC, rowid DESC LIMIT 1`,
|
|
523
|
+
).get(change.id, task.id);
|
|
524
|
+
if (!snapshot) {
|
|
525
|
+
blockers.add(`TASK_CONTEXT_MISSING:${task.id}`);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const context = parseJson(snapshot.context_json, 'context_snapshots.context_json');
|
|
529
|
+
if (snapshot.readiness !== 'ready') blockers.add(`TASK_CONTEXT_NOT_READY:${task.id}`);
|
|
530
|
+
if (!context.execution_contract?.public_seam) blockers.add(`TASK_SEAM_MISSING:${task.id}`);
|
|
531
|
+
if (!context.execution_contract?.verification_command) blockers.add(`TASK_VERIFICATION_MISSING:${task.id}`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
for (const candidate of specLearning.listSpecLearning(db, change.id)) {
|
|
535
|
+
if (candidate.status === 'proposed') blockers.add(`SPEC_LEARNING_UNRESOLVED:${candidate.id}`);
|
|
536
|
+
if (candidate.status === 'approved') blockers.add(`SPEC_LEARNING_NOT_APPLIED:${candidate.id}`);
|
|
537
|
+
}
|
|
465
538
|
|
|
466
539
|
if (change.docs_impact.status === 'unknown') blockers.add('DOCS_IMPACT_UNKNOWN');
|
|
467
540
|
if (change.docs_impact.status === 'none' && (!change.docs_impact.rationale || change.docs_impact.rationale.length < 3)) {
|
|
@@ -513,7 +586,8 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
513
586
|
const lines = [
|
|
514
587
|
`# Verification: ${change.title}`, '',
|
|
515
588
|
...input.evidence.flatMap((row) => [
|
|
516
|
-
`## ${row.category}
|
|
589
|
+
`## ${row.category}${row.axis ? `:${row.axis}` : ''}`, '',
|
|
590
|
+
`Status: **${row.status}**`, '', String(row.evidence).trim(), '',
|
|
517
591
|
]),
|
|
518
592
|
];
|
|
519
593
|
fs.writeFileSync(verificationPath, `${lines.join('\n')}\n`);
|
|
@@ -618,6 +692,10 @@ module.exports = {
|
|
|
618
692
|
listChanges,
|
|
619
693
|
updateChange,
|
|
620
694
|
compileContext,
|
|
695
|
+
readBreadcrumb: contextSpine.readBreadcrumb,
|
|
696
|
+
proposeSpecLearning: specLearning.proposeSpecLearning,
|
|
697
|
+
resolveSpecLearning: specLearning.resolveSpecLearning,
|
|
698
|
+
listSpecLearning: specLearning.listSpecLearning,
|
|
621
699
|
convergeChange,
|
|
622
700
|
archiveChange,
|
|
623
701
|
normalizeDocsImpact,
|