ultra-builder-pro-cli 0.10.0 → 0.12.1
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/specs/architecture.md +54 -223
- package/.ultra-template/specs/discovery.md +34 -231
- package/.ultra-template/specs/product.md +44 -200
- package/.ultra-template/tasks/contexts/TEMPLATE.md +36 -0
- package/.ultra-template/tasks/tasks.json +6 -0
- package/CHANGELOG.md +86 -1
- package/README.md +21 -9
- package/adapters/_shared/codex-assets.cjs +24 -342
- package/adapters/_shared/handbook.cjs +4 -3
- package/adapters/_shared/runtime-assets.cjs +24 -0
- package/adapters/_shared/tests/handbook.test.cjs +4 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +11 -4
- package/adapters/claude.js +28 -5
- package/adapters/kimi.js +15 -138
- package/adapters/opencode.js +22 -49
- package/adapters/tests/claude.test.cjs +22 -8
- package/adapters/tests/codex.test.cjs +21 -8
- package/adapters/tests/kimi-hook.test.cjs +1 -1
- package/adapters/tests/kimi.test.cjs +11 -11
- package/adapters/tests/opencode.test.cjs +47 -8
- 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 +16 -8
- package/docs/ARCHITECTURE.md +32 -5
- package/docs/PLAN.zh-CN.md +8 -6
- package/docs/ROADMAP.md +5 -3
- package/docs/RUNTIME-COMPAT-MATRIX.md +8 -5
- package/docs/SKILL-AUTHORING.md +75 -0
- package/docs/STATE-DB-ACCESS-POLICY.md +17 -11
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -5
- package/hooks/README.md +4 -3
- package/hooks/adapters/codex.py +1 -1
- package/hooks/adapters/kimi.py +1 -1
- package/hooks/context_spine.py +120 -16
- package/hooks/health_check.py +11 -0
- package/hooks/pre_stop_check.py +6 -5
- package/mcp-server/lib/baseline-workflow.cjs +415 -0
- package/mcp-server/lib/baseline-workflow.test.cjs +225 -0
- package/mcp-server/lib/change-workflow.cjs +24 -27
- package/mcp-server/lib/change-workflow.test.cjs +202 -6
- package/mcp-server/lib/context-spine.cjs +74 -9
- package/mcp-server/lib/context-spine.test.cjs +15 -0
- package/mcp-server/lib/doctor.cjs +14 -0
- package/mcp-server/lib/doctor.test.cjs +25 -1
- package/mcp-server/lib/init-project.cjs +49 -14
- package/mcp-server/lib/provider-refs.cjs +42 -0
- package/mcp-server/lib/spec-learning.test.cjs +5 -0
- package/mcp-server/lib/state-db.cjs +63 -4
- package/mcp-server/lib/state-db.test.cjs +8 -3
- package/mcp-server/server.cjs +48 -3
- package/mcp-server/tests/init-project.test.cjs +54 -8
- package/mcp-server/tests/server-contract.test.cjs +99 -2
- package/mcp-server/tests/state-db.test.cjs +11 -2
- 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 -106
- package/skills/security-rules/SKILL.md +34 -56
- package/skills/testing-rules/SKILL.md +28 -52
- package/skills/ultra-change/SKILL.md +68 -114
- package/skills/ultra-deliver/SKILL.md +58 -86
- package/skills/ultra-dev/SKILL.md +46 -105
- package/skills/ultra-doctor/SKILL.md +45 -72
- package/skills/ultra-init/SKILL.md +62 -147
- package/skills/ultra-init/references/brownfield-adoption.md +65 -0
- package/skills/ultra-plan/SKILL.md +56 -89
- package/skills/ultra-research/SKILL.md +64 -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 +54 -98
- 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 +41 -56
- package/skills/ultra-test/SKILL.md +37 -72
- package/skills/ultra-think/SKILL.md +43 -102
- package/skills/ultra-verify/SKILL.md +44 -84
- package/skills/ultra-verify/evals/evals.json +24 -27
- package/spec/cli-protocol.md +4 -0
- package/spec/mcp-tools.yaml +223 -4
- package/spec/schemas/command-manifest.schema.json +7 -25
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/skill-manifest.schema.json +9 -79
- package/spec/schemas/state-db.sql +33 -2
- package/ultra-tools/commands/status.cjs +20 -2
- package/ultra-tools/commands/task.cjs +4 -1
- package/ultra-tools/status-command.test.cjs +20 -0
- package/ultra-tools/system-command.test.cjs +5 -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
package/hooks/context_spine.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""Read and render the compact DB-derived Ultra Context Spine breadcrumb."""
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import hashlib
|
|
5
6
|
import sqlite3
|
|
6
7
|
import subprocess
|
|
7
8
|
from pathlib import Path
|
|
@@ -13,6 +14,12 @@ CONTEXT_COLUMNS = {
|
|
|
13
14
|
"role", "gate", "next_action", "readiness", "blockers_json",
|
|
14
15
|
"context_json", "manifest_path", "manifest_hash", "git_head", "task_id",
|
|
15
16
|
}
|
|
17
|
+
ADVISORY_CONTEXT_CODES = {
|
|
18
|
+
"CONTEXT_FILE_BUDGET_EXCEEDED",
|
|
19
|
+
"CONTEXT_TOKEN_BUDGET_EXCEEDED",
|
|
20
|
+
"EXECUTION_CONTEXT_BUDGET_EXCEEDED",
|
|
21
|
+
"EXECUTION_CONTEXT_BUDGET_ADVISORY",
|
|
22
|
+
}
|
|
16
23
|
|
|
17
24
|
|
|
18
25
|
def find_root(start: Path):
|
|
@@ -49,8 +56,57 @@ def git_head(root: Path):
|
|
|
49
56
|
return result.stdout.strip() if result.returncode == 0 else None
|
|
50
57
|
|
|
51
58
|
|
|
52
|
-
def
|
|
59
|
+
def baseline_health(conn, root: Path):
|
|
60
|
+
row = conn.execute(
|
|
61
|
+
"""SELECT id, mode, status, repository_revision, spec_refs_json
|
|
62
|
+
FROM baselines WHERE status != 'superseded'
|
|
63
|
+
ORDER BY updated_at DESC, rowid DESC LIMIT 1"""
|
|
64
|
+
).fetchone()
|
|
65
|
+
if row is None:
|
|
66
|
+
return {"status": "fail", "blockers": ["BASELINE_MISSING"], "warnings": [], "baseline": None}
|
|
67
|
+
baseline = dict(row)
|
|
68
|
+
blockers = []
|
|
69
|
+
warnings = []
|
|
70
|
+
if baseline["status"] != "ready":
|
|
71
|
+
blockers.append(f"BASELINE_NOT_READY:{baseline['status']}")
|
|
72
|
+
elif baseline["mode"] == "migrated":
|
|
73
|
+
warnings.append("BASELINE_MIGRATION_REVIEW_RECOMMENDED")
|
|
74
|
+
else:
|
|
75
|
+
for spec in safe_json(baseline.get("spec_refs_json"), []):
|
|
76
|
+
ref = spec.get("path") if isinstance(spec, dict) else None
|
|
77
|
+
expected = spec.get("digest") if isinstance(spec, dict) else None
|
|
78
|
+
if not isinstance(ref, str) or not ref or Path(ref).is_absolute():
|
|
79
|
+
blockers.append(f"BASELINE_SPEC_INVALID:{ref}")
|
|
80
|
+
continue
|
|
81
|
+
candidate = (root / ref).resolve()
|
|
82
|
+
try:
|
|
83
|
+
candidate.relative_to(root.resolve())
|
|
84
|
+
except ValueError:
|
|
85
|
+
blockers.append(f"BASELINE_SPEC_INVALID:{ref}")
|
|
86
|
+
continue
|
|
87
|
+
if not candidate.is_file():
|
|
88
|
+
blockers.append(f"BASELINE_SPEC_MISSING:{ref}")
|
|
89
|
+
continue
|
|
90
|
+
digest = hashlib.sha256(candidate.read_bytes()).hexdigest()
|
|
91
|
+
if not expected or digest != expected:
|
|
92
|
+
blockers.append(f"BASELINE_SPEC_STALE:{ref}")
|
|
93
|
+
head = git_head(root)
|
|
94
|
+
revision = baseline.get("repository_revision")
|
|
95
|
+
if head and revision and not str(revision).startswith("workspace:") and head != revision:
|
|
96
|
+
blockers.append("BASELINE_HEAD_STALE")
|
|
97
|
+
baseline.pop("spec_refs_json", None)
|
|
98
|
+
return {
|
|
99
|
+
"status": "fail" if blockers else "pass",
|
|
100
|
+
"blockers": blockers,
|
|
101
|
+
"warnings": warnings,
|
|
102
|
+
"baseline": baseline,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _next_action(change, task, tasks, role, readiness, stored, state_changed, blockers=None):
|
|
53
107
|
if readiness != "ready":
|
|
108
|
+
if any(str(item).startswith("BASELINE_") for item in (blockers or [])):
|
|
109
|
+
return "Complete or refresh the Ultra project baseline through ultra-init."
|
|
54
110
|
return "Resolve the context readiness blockers, then recompile change.context."
|
|
55
111
|
if not state_changed and isinstance(stored, str) and stored.strip():
|
|
56
112
|
return stored.strip()
|
|
@@ -77,7 +133,7 @@ def _next_action(change, task, tasks, role, readiness, stored, state_changed):
|
|
|
77
133
|
return f"Create the first fresh-context tracer-bullet task for change {change['id']}."
|
|
78
134
|
|
|
79
135
|
|
|
80
|
-
def _route(change, task, tasks, readiness):
|
|
136
|
+
def _route(change, task, tasks, readiness, blockers=None):
|
|
81
137
|
if readiness != "ready" or change["status"] == "blocked":
|
|
82
138
|
return "ultra-change"
|
|
83
139
|
if change["status"] == "ready":
|
|
@@ -99,15 +155,39 @@ def read_breadcrumb(root: Path):
|
|
|
99
155
|
row[0]
|
|
100
156
|
for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
101
157
|
}
|
|
102
|
-
if not {"changes", "tasks", "context_snapshots"}.issubset(tables):
|
|
158
|
+
if not {"baselines", "changes", "tasks", "context_snapshots"}.issubset(tables):
|
|
103
159
|
return None
|
|
160
|
+
baseline = baseline_health(conn, root)
|
|
104
161
|
change = conn.execute(
|
|
105
162
|
"""SELECT id, status FROM changes
|
|
106
163
|
WHERE status IN ('active', 'blocked', 'ready')
|
|
107
164
|
ORDER BY updated_at DESC LIMIT 1"""
|
|
108
165
|
).fetchone()
|
|
109
166
|
if change is None:
|
|
110
|
-
|
|
167
|
+
current = baseline["baseline"]
|
|
168
|
+
if baseline["status"] == "pass":
|
|
169
|
+
next_action = "Start daily work with ultra-change."
|
|
170
|
+
route = "ultra-change"
|
|
171
|
+
readiness = "ready"
|
|
172
|
+
else:
|
|
173
|
+
next_action = "Initialize or complete project baseline adoption through ultra-init."
|
|
174
|
+
route = "ultra-init"
|
|
175
|
+
readiness = "blocked"
|
|
176
|
+
return {
|
|
177
|
+
"change_id": None,
|
|
178
|
+
"task_id": None,
|
|
179
|
+
"role": "plan",
|
|
180
|
+
"gate": "alignment",
|
|
181
|
+
"readiness": readiness,
|
|
182
|
+
"blockers": baseline["blockers"],
|
|
183
|
+
"warnings": baseline["warnings"],
|
|
184
|
+
"next_action": next_action,
|
|
185
|
+
"recommended_workflow": route,
|
|
186
|
+
"context_manifest_path": None,
|
|
187
|
+
"context_manifest_hash": None,
|
|
188
|
+
"git_head": git_head(root),
|
|
189
|
+
"baseline": current,
|
|
190
|
+
}
|
|
111
191
|
tasks = list(conn.execute(
|
|
112
192
|
"SELECT id, status, session_id FROM tasks WHERE change_id = ? ORDER BY created_at ASC",
|
|
113
193
|
(change["id"],),
|
|
@@ -145,6 +225,7 @@ def read_breadcrumb(root: Path):
|
|
|
145
225
|
not isinstance(context.get("resume"), dict)
|
|
146
226
|
or not isinstance(context.get("context"), dict)
|
|
147
227
|
or not isinstance(context.get("readiness"), dict)
|
|
228
|
+
or "baseline" not in context
|
|
148
229
|
)
|
|
149
230
|
)
|
|
150
231
|
stored_status = context.get("resume", {}).get("task_status")
|
|
@@ -152,6 +233,7 @@ def read_breadcrumb(root: Path):
|
|
|
152
233
|
state_changed = stored_status is not None and stored_status != current_status
|
|
153
234
|
stored_action = snapshot["next_action"] if snapshot else None
|
|
154
235
|
blockers = []
|
|
236
|
+
warnings = list(dict.fromkeys([*baseline["blockers"], *baseline["warnings"]]))
|
|
155
237
|
if snapshot_missing:
|
|
156
238
|
blockers.append("CONTEXT_NOT_COMPILED")
|
|
157
239
|
else:
|
|
@@ -162,8 +244,14 @@ def read_breadcrumb(root: Path):
|
|
|
162
244
|
if state_changed:
|
|
163
245
|
blockers.append("CONTEXT_TASK_STATE_STALE")
|
|
164
246
|
for blocker in safe_json(snapshot["blockers_json"], []):
|
|
165
|
-
if blocker
|
|
247
|
+
if blocker in ADVISORY_CONTEXT_CODES or str(blocker).startswith("BASELINE_"):
|
|
248
|
+
if blocker not in warnings:
|
|
249
|
+
warnings.append(blocker)
|
|
250
|
+
elif blocker not in blockers:
|
|
166
251
|
blockers.append(blocker)
|
|
252
|
+
for warning in context.get("readiness", {}).get("warnings", []):
|
|
253
|
+
if warning not in warnings:
|
|
254
|
+
warnings.append(warning)
|
|
167
255
|
readiness = "blocked" if blockers else snapshot["readiness"]
|
|
168
256
|
if snapshot_missing:
|
|
169
257
|
next_action = f"Compile change.context for change {change['id']} before continuing."
|
|
@@ -174,7 +262,9 @@ def read_breadcrumb(root: Path):
|
|
|
174
262
|
elif state_changed:
|
|
175
263
|
next_action = "Task state changed after context compilation; recompile change.context."
|
|
176
264
|
else:
|
|
177
|
-
next_action = _next_action(
|
|
265
|
+
next_action = _next_action(
|
|
266
|
+
change, task, tasks, role, readiness, stored_action, False, blockers
|
|
267
|
+
)
|
|
178
268
|
return {
|
|
179
269
|
"change_id": change["id"],
|
|
180
270
|
"change_status": change["status"],
|
|
@@ -185,28 +275,42 @@ def read_breadcrumb(root: Path):
|
|
|
185
275
|
"gate": gate,
|
|
186
276
|
"readiness": readiness,
|
|
187
277
|
"blockers": blockers,
|
|
278
|
+
"warnings": warnings,
|
|
188
279
|
"next_action": next_action,
|
|
189
|
-
"recommended_workflow": _route(change, task, tasks, readiness),
|
|
280
|
+
"recommended_workflow": _route(change, task, tasks, readiness, blockers),
|
|
190
281
|
"context_manifest_path": snapshot["manifest_path"] if snapshot else None,
|
|
191
282
|
"context_manifest_hash": snapshot["manifest_hash"] if snapshot else None,
|
|
192
283
|
"git_head": current_head,
|
|
284
|
+
"baseline": baseline["baseline"],
|
|
193
285
|
}
|
|
194
286
|
|
|
195
287
|
|
|
196
288
|
def render_breadcrumb(root: Path, breadcrumb: dict) -> str:
|
|
197
289
|
task = breadcrumb.get("task_id") or "none"
|
|
198
290
|
task_status = breadcrumb.get("task_status") or "none"
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
291
|
+
baseline = breadcrumb.get("baseline") or {}
|
|
292
|
+
if breadcrumb.get("change_id"):
|
|
293
|
+
lines = [
|
|
294
|
+
"[Ultra context spine]",
|
|
295
|
+
f"Project: {root}",
|
|
296
|
+
f"Baseline: {baseline.get('id', 'unknown')} ({baseline.get('mode', 'unknown')}/{baseline.get('status', 'unknown')})",
|
|
297
|
+
f"Change: {breadcrumb['change_id']} ({breadcrumb.get('change_status', 'active')})",
|
|
298
|
+
f"Task: {task} ({task_status})",
|
|
299
|
+
f"Role: {breadcrumb.get('role', 'plan')}",
|
|
300
|
+
f"Gate: {breadcrumb.get('gate', 'alignment')}",
|
|
301
|
+
f"Readiness: {breadcrumb.get('readiness', 'ready')}",
|
|
302
|
+
]
|
|
303
|
+
else:
|
|
304
|
+
lines = [
|
|
305
|
+
"[Ultra baseline]",
|
|
306
|
+
f"Project: {root}",
|
|
307
|
+
f"Baseline: {baseline.get('id', 'missing')} ({baseline.get('mode', 'unknown')}/{baseline.get('status', 'missing')})",
|
|
308
|
+
f"Readiness: {breadcrumb.get('readiness', 'blocked')}",
|
|
309
|
+
]
|
|
208
310
|
if breadcrumb.get("blockers"):
|
|
209
311
|
lines.append("Blockers: " + ", ".join(breadcrumb["blockers"]))
|
|
312
|
+
if breadcrumb.get("warnings"):
|
|
313
|
+
lines.append("Warnings: " + ", ".join(breadcrumb["warnings"]))
|
|
210
314
|
lines.extend([
|
|
211
315
|
f"Next: {breadcrumb.get('next_action', 'Inspect Ultra status.')}",
|
|
212
316
|
f"Route: {breadcrumb.get('recommended_workflow', 'ultra-change')}",
|
package/hooks/health_check.py
CHANGED
|
@@ -6,8 +6,11 @@ import sqlite3
|
|
|
6
6
|
import sys
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
+
from context_spine import baseline_health
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
REQUIRED_TABLES = {
|
|
13
|
+
"baselines",
|
|
11
14
|
"tasks", "events", "sessions", "schema_version", "migration_history",
|
|
12
15
|
"telemetry", "specs_refs", "circuit_breaker", "changes", "artifacts",
|
|
13
16
|
"context_snapshots", "spec_learning_candidates", "trace_links", "incidents", "projection_jobs",
|
|
@@ -48,6 +51,7 @@ def inspect(root: Path) -> dict:
|
|
|
48
51
|
try:
|
|
49
52
|
uri = f"file:{db_path}?mode=ro"
|
|
50
53
|
with sqlite3.connect(uri, uri=True, timeout=1) as conn:
|
|
54
|
+
conn.row_factory = sqlite3.Row
|
|
51
55
|
quick = conn.execute("PRAGMA quick_check").fetchone()[0]
|
|
52
56
|
tables = {
|
|
53
57
|
row[0] for row in conn.execute(
|
|
@@ -66,6 +70,13 @@ def inspect(root: Path) -> dict:
|
|
|
66
70
|
report["status"] = "degraded"
|
|
67
71
|
return report
|
|
68
72
|
|
|
73
|
+
baseline = baseline_health(conn, root)
|
|
74
|
+
report["checks"]["baseline"] = {
|
|
75
|
+
**baseline,
|
|
76
|
+
"readiness": baseline["status"],
|
|
77
|
+
"status": "pass" if baseline["status"] == "pass" else "warning",
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
incidents = [
|
|
70
81
|
{"id": row[0], "code": row[1], "severity": row[2], "retryable": bool(row[3])}
|
|
71
82
|
for row in conn.execute(
|
package/hooks/pre_stop_check.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Report unfinished Ultra workflow position without trapping session stop."""
|
|
3
3
|
|
|
4
4
|
import sys
|
|
5
5
|
import json
|
|
@@ -12,8 +12,9 @@ def allow_stop() -> None:
|
|
|
12
12
|
print(json.dumps({}))
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def
|
|
16
|
-
print(
|
|
15
|
+
def advise_stop(reason: str) -> None:
|
|
16
|
+
print(f"[Ultra stop advisory] {reason}", file=sys.stderr)
|
|
17
|
+
allow_stop()
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def main():
|
|
@@ -41,12 +42,12 @@ def main():
|
|
|
41
42
|
if not isinstance(state, dict) or state.get("status") in TERMINAL:
|
|
42
43
|
allow_stop()
|
|
43
44
|
return
|
|
44
|
-
|
|
45
|
+
advise_stop(
|
|
45
46
|
"Active Ultra workflow is incomplete: "
|
|
46
47
|
f"command={state.get('command', 'unknown')}, "
|
|
47
48
|
f"task={state.get('task_id', state.get('task', 'unknown'))}, "
|
|
48
49
|
f"step={state.get('step', 'unknown')}, status={state.get('status', 'active')}. "
|
|
49
|
-
"
|
|
50
|
+
"Resume or explicitly cancel it in a later session; stop is allowed."
|
|
50
51
|
)
|
|
51
52
|
return
|
|
52
53
|
allow_stop()
|