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.
Files changed (137) hide show
  1. package/.ultra-template/specs/architecture.md +54 -223
  2. package/.ultra-template/specs/discovery.md +34 -231
  3. package/.ultra-template/specs/product.md +44 -200
  4. package/.ultra-template/tasks/contexts/TEMPLATE.md +36 -0
  5. package/.ultra-template/tasks/tasks.json +6 -0
  6. package/CHANGELOG.md +86 -1
  7. package/README.md +21 -9
  8. package/adapters/_shared/codex-assets.cjs +24 -342
  9. package/adapters/_shared/handbook.cjs +4 -3
  10. package/adapters/_shared/runtime-assets.cjs +24 -0
  11. package/adapters/_shared/tests/handbook.test.cjs +4 -0
  12. package/adapters/_shared/tests/runtime-assets.test.cjs +11 -4
  13. package/adapters/claude.js +28 -5
  14. package/adapters/kimi.js +15 -138
  15. package/adapters/opencode.js +22 -49
  16. package/adapters/tests/claude.test.cjs +22 -8
  17. package/adapters/tests/codex.test.cjs +21 -8
  18. package/adapters/tests/kimi-hook.test.cjs +1 -1
  19. package/adapters/tests/kimi.test.cjs +11 -11
  20. package/adapters/tests/opencode.test.cjs +47 -8
  21. package/agents/code-reviewer.md +24 -85
  22. package/agents/debugger.md +30 -132
  23. package/agents/review-code.md +24 -104
  24. package/agents/review-comments.md +20 -83
  25. package/agents/review-coordinator.md +40 -137
  26. package/agents/review-design.md +21 -118
  27. package/agents/review-errors.md +22 -70
  28. package/agents/review-spec.md +36 -0
  29. package/agents/review-tests.md +20 -76
  30. package/agents/tdd-runner.md +26 -84
  31. package/commands/learn.md +5 -36
  32. package/commands/ultra-change.md +5 -43
  33. package/commands/ultra-deliver.md +4 -46
  34. package/commands/ultra-dev.md +4 -48
  35. package/commands/ultra-doctor.md +4 -30
  36. package/commands/ultra-init.md +5 -47
  37. package/commands/ultra-plan.md +5 -48
  38. package/commands/ultra-research.md +5 -54
  39. package/commands/ultra-status.md +5 -44
  40. package/commands/ultra-test.md +5 -45
  41. package/commands/ultra-think.md +5 -40
  42. package/docs/AGENT-CONTEXT.md +16 -8
  43. package/docs/ARCHITECTURE.md +32 -5
  44. package/docs/PLAN.zh-CN.md +8 -6
  45. package/docs/ROADMAP.md +5 -3
  46. package/docs/RUNTIME-COMPAT-MATRIX.md +8 -5
  47. package/docs/SKILL-AUTHORING.md +75 -0
  48. package/docs/STATE-DB-ACCESS-POLICY.md +17 -11
  49. package/docs/USER-HANDBOOK-CONTRACT.md +8 -5
  50. package/hooks/README.md +4 -3
  51. package/hooks/adapters/codex.py +1 -1
  52. package/hooks/adapters/kimi.py +1 -1
  53. package/hooks/context_spine.py +120 -16
  54. package/hooks/health_check.py +11 -0
  55. package/hooks/pre_stop_check.py +6 -5
  56. package/mcp-server/lib/baseline-workflow.cjs +415 -0
  57. package/mcp-server/lib/baseline-workflow.test.cjs +225 -0
  58. package/mcp-server/lib/change-workflow.cjs +24 -27
  59. package/mcp-server/lib/change-workflow.test.cjs +202 -6
  60. package/mcp-server/lib/context-spine.cjs +74 -9
  61. package/mcp-server/lib/context-spine.test.cjs +15 -0
  62. package/mcp-server/lib/doctor.cjs +14 -0
  63. package/mcp-server/lib/doctor.test.cjs +25 -1
  64. package/mcp-server/lib/init-project.cjs +49 -14
  65. package/mcp-server/lib/provider-refs.cjs +42 -0
  66. package/mcp-server/lib/spec-learning.test.cjs +5 -0
  67. package/mcp-server/lib/state-db.cjs +63 -4
  68. package/mcp-server/lib/state-db.test.cjs +8 -3
  69. package/mcp-server/server.cjs +48 -3
  70. package/mcp-server/tests/init-project.test.cjs +54 -8
  71. package/mcp-server/tests/server-contract.test.cjs +99 -2
  72. package/mcp-server/tests/state-db.test.cjs +11 -2
  73. package/package.json +3 -2
  74. package/skills/cc-collab/SKILL.md +16 -18
  75. package/skills/code-review-expert/SKILL.md +39 -114
  76. package/skills/code-review-expert/references/correctness-reliability.md +30 -0
  77. package/skills/code-review-expert/references/design-boundaries.md +26 -0
  78. package/skills/code-review-expert/references/removal-plan.md +17 -45
  79. package/skills/codex-collab/SKILL.md +23 -28
  80. package/skills/codex-collab/evals/evals.json +18 -23
  81. package/skills/integration-rules/SKILL.md +28 -67
  82. package/skills/learn/SKILL.md +46 -106
  83. package/skills/security-rules/SKILL.md +34 -56
  84. package/skills/testing-rules/SKILL.md +28 -52
  85. package/skills/ultra-change/SKILL.md +68 -114
  86. package/skills/ultra-deliver/SKILL.md +58 -86
  87. package/skills/ultra-dev/SKILL.md +46 -105
  88. package/skills/ultra-doctor/SKILL.md +45 -72
  89. package/skills/ultra-init/SKILL.md +62 -147
  90. package/skills/ultra-init/references/brownfield-adoption.md +65 -0
  91. package/skills/ultra-plan/SKILL.md +56 -89
  92. package/skills/ultra-research/SKILL.md +64 -152
  93. package/skills/ultra-research/references/architecture.md +38 -0
  94. package/skills/ultra-research/references/problem-product.md +39 -0
  95. package/skills/ultra-research/references/quality-delivery.md +34 -0
  96. package/skills/ultra-research/references/synthesis.md +38 -0
  97. package/skills/ultra-research/references/users-scope.md +40 -0
  98. package/skills/ultra-review/SKILL.md +54 -98
  99. package/skills/ultra-review/references/unified-schema.md +76 -101
  100. package/skills/ultra-review/scripts/review_wait.py +242 -77
  101. package/skills/ultra-status/SKILL.md +41 -56
  102. package/skills/ultra-test/SKILL.md +37 -72
  103. package/skills/ultra-think/SKILL.md +43 -102
  104. package/skills/ultra-verify/SKILL.md +44 -84
  105. package/skills/ultra-verify/evals/evals.json +24 -27
  106. package/spec/cli-protocol.md +4 -0
  107. package/spec/mcp-tools.yaml +223 -4
  108. package/spec/schemas/command-manifest.schema.json +7 -25
  109. package/spec/schemas/mcp-tools.schema.json +1 -1
  110. package/spec/schemas/skill-manifest.schema.json +9 -79
  111. package/spec/schemas/state-db.sql +33 -2
  112. package/ultra-tools/commands/status.cjs +20 -2
  113. package/ultra-tools/commands/task.cjs +4 -1
  114. package/ultra-tools/status-command.test.cjs +20 -0
  115. package/ultra-tools/system-command.test.cjs +5 -0
  116. package/skills/code-review-expert/references/code-quality-checklist.md +0 -112
  117. package/skills/code-review-expert/references/integration-checklist.md +0 -46
  118. package/skills/code-review-expert/references/security-checklist.md +0 -100
  119. package/skills/code-review-expert/references/solid-checklist.md +0 -65
  120. package/skills/ultra-research/steps/step-00-problem-validation.md +0 -154
  121. package/skills/ultra-research/steps/step-01-opportunity-discovery.md +0 -172
  122. package/skills/ultra-research/steps/step-02-market-assessment.md +0 -195
  123. package/skills/ultra-research/steps/step-03-competitive-landscape.md +0 -197
  124. package/skills/ultra-research/steps/step-04-product-strategy.md +0 -196
  125. package/skills/ultra-research/steps/step-05-assumptions-validation.md +0 -234
  126. package/skills/ultra-research/steps/step-10-user-personas.md +0 -169
  127. package/skills/ultra-research/steps/step-11-user-scenarios.md +0 -181
  128. package/skills/ultra-research/steps/step-20-user-stories.md +0 -156
  129. package/skills/ultra-research/steps/step-21-features-scope.md +0 -157
  130. package/skills/ultra-research/steps/step-22-success-metrics.md +0 -195
  131. package/skills/ultra-research/steps/step-30-architecture-context.md +0 -154
  132. package/skills/ultra-research/steps/step-31-solution-strategy.md +0 -172
  133. package/skills/ultra-research/steps/step-32-building-blocks.md +0 -188
  134. package/skills/ultra-research/steps/step-40-deployment.md +0 -123
  135. package/skills/ultra-research/steps/step-41-quality-risks.md +0 -135
  136. package/skills/ultra-research/steps/step-99-synthesis.md +0 -188
  137. package/skills/ultra-review/scripts/review_verdict_update.py +0 -99
@@ -1,127 +1,102 @@
1
- # Ultra Review - Unified JSON Schema v1
1
+ # Review artifact contract
2
2
 
3
- All review agents MUST output findings in this exact format. No deviations.
3
+ Use this contract for every specialist artifact. Write JSON to the assigned file and
4
+ return only a one-line acknowledgement to the parent.
4
5
 
5
- ## Schema
6
+ ## Specialist artifact
6
7
 
7
8
  ```json
8
9
  {
9
- "$schema": "ultra-review-findings-v1",
10
- "agent": "<agent-name>",
11
- "session": "<timestamp>-<branch>",
12
- "timestamp": "<ISO 8601>",
10
+ "$schema": "ultra-review-findings-v2",
11
+ "agent": "review-code",
12
+ "axis": "engineering_standards",
13
+ "session": "<session-id>",
14
+ "timestamp": "<ISO-8601>",
13
15
  "scope": {
14
- "files_analyzed": ["src/foo.ts", "src/bar.ts"],
15
- "total_lines_analyzed": 342,
16
+ "head": "<full-git-head>",
17
+ "range": "<diff-range>",
18
+ "files_analyzed": ["src/example.ts"],
16
19
  "diff_only": true
17
20
  },
18
- "summary": {
19
- "total_findings": 5,
20
- "by_severity": { "P0": 1, "P1": 2, "P2": 1, "P3": 1 },
21
- "verdict": "REQUEST_CHANGES"
22
- },
21
+ "status": "complete",
23
22
  "findings": [
24
23
  {
25
- "id": "<agent-name>-001",
26
- "severity": "P0",
27
- "confidence": 95,
28
- "category": "security",
29
- "subcategory": "injection",
30
- "title": "SQL injection via string concatenation",
31
- "file": "src/api/handler.ts",
32
- "line": 45,
24
+ "id": "review-code-001",
25
+ "axis": "engineering_standards",
26
+ "severity": "P1",
27
+ "category": "correctness",
28
+ "title": "Retry path commits the same payment twice",
29
+ "file": "src/payments/retry.ts",
30
+ "line": 42,
33
31
  "line_end": 47,
34
- "code_snippet": "const query = `SELECT * FROM users WHERE id = ${userId}`",
35
- "description": "User-controlled input directly interpolated into SQL query string.",
36
- "suggestion": "Use parameterized query: db.query('SELECT * FROM users WHERE id = $1', [userId])",
37
- "claude_md_rule": "security.forbidden_patterns.sql_string_concat"
32
+ "trigger": "The provider times out after committing the first request.",
33
+ "impact": "A retry can create a duplicate charge.",
34
+ "evidence": "retryPayment calls charge() again without an idempotency key.",
35
+ "suggestion": "Persist and reuse one idempotency key for the operation."
38
36
  }
39
37
  ],
40
- "positive_observations": [
41
- "Clean separation between domain and infrastructure layers"
42
- ],
43
- "metadata": {
44
- "duration_ms": 15000,
45
- "model": "claude-opus-4-6",
46
- "turns_used": 12
47
- }
38
+ "positive_observations": [],
39
+ "limitations": []
48
40
  }
49
41
  ```
50
42
 
51
- ## Field Definitions
52
-
53
- ### Top-Level
43
+ Use `axis: "spec_fidelity"` for the independent acceptance review and
44
+ `axis: "engineering_standards"` for engineering specialists.
54
45
 
55
- | Field | Type | Required | Description |
56
- |-------|------|----------|-------------|
57
- | `$schema` | string | YES | Always `"ultra-review-findings-v1"` |
58
- | `agent` | string | YES | Agent name: `review-code`, `review-tests`, `review-errors`, `review-comments`, `review-design` |
59
- | `session` | string | YES | Session ID provided in the task prompt |
60
- | `timestamp` | string | YES | ISO 8601 timestamp when review completed |
61
- | `scope` | object | YES | What was analyzed |
62
- | `summary` | object | YES | Aggregate counts |
63
- | `findings` | array | YES | Individual findings (can be empty) |
64
- | `positive_observations` | array | YES | Good patterns found (can be empty) |
65
- | `metadata` | object | YES | Execution metadata |
46
+ ## Required finding fields
66
47
 
67
- ### Finding Object
48
+ Every finding needs a stable id, axis, severity, category, concise title, repository-
49
+ relative file, tight line range, triggering condition, observable impact, source
50
+ evidence, and smallest complete remediation.
68
51
 
69
- | Field | Type | Required | Description |
70
- |-------|------|----------|-------------|
71
- | `id` | string | YES | Unique ID: `<agent-name>-<NNN>` (zero-padded 3 digits) |
72
- | `severity` | enum | YES | `P0` (critical), `P1` (high), `P2` (medium), `P3` (low) |
73
- | `confidence` | int | YES | 0-100. Only include findings with confidence >= 75 |
74
- | `category` | enum | YES | See Category Enum below |
75
- | `subcategory` | string | NO | Free-text refinement of category |
76
- | `title` | string | YES | One-line summary (max 80 chars) |
77
- | `file` | string | YES | Relative file path from repo root |
78
- | `line` | int | YES | Start line number |
79
- | `line_end` | int | NO | End line number (if multi-line) |
80
- | `code_snippet` | string | YES | Relevant code (max 5 lines) |
81
- | `description` | string | YES | Detailed explanation |
82
- | `suggestion` | string | YES | Concrete fix or recommendation |
83
- | `claude_md_rule` | string | NO | Reference to CLAUDE.md rule if applicable |
52
+ Severity is impact-based:
84
53
 
85
- ### Category Enum
54
+ - P0: exploitable critical security issue, destructive data loss, or deterministic
55
+ critical outage;
56
+ - P1: material correctness, authorization, reliability, or delivery failure;
57
+ - P2: bounded defect or maintainability risk with a concrete cost;
58
+ - P3: optional improvement that does not block the accepted outcome.
86
59
 
87
- | Value | Used By |
88
- |-------|---------|
89
- | `security` | review-code, review-errors |
90
- | `error-handling` | review-errors, review-code |
91
- | `code-quality` | review-code, review-design |
92
- | `test-quality` | review-tests |
93
- | `type-design` | review-design |
94
- | `comments` | review-comments |
95
- | `simplification` | review-design |
96
- | `performance` | review-code, review-design |
97
- | `architecture` | review-code, review-design |
98
- | `integration` | review-code |
99
- | `scope-drift` | review-code |
100
- | `spec-compliance` | review-code |
101
- | `forbidden-pattern` | review-code, review-tests, review-comments |
60
+ Do not report a concern without a plausible execution path. Do not encode a style
61
+ preference as a defect. Preserve the source specialist's severity during coordination.
102
62
 
103
- ### Verdict Rules
63
+ ## Coordinator summary
104
64
 
105
- | Condition | Verdict |
106
- |-----------|---------|
107
- | Any P0 finding | `REQUEST_CHANGES` |
108
- | P1 count > 3 | `REQUEST_CHANGES` |
109
- | P1 count > 0 | `COMMENT` |
110
- | No P0 or P1 | `APPROVE` |
65
+ `SUMMARY.json` contains:
111
66
 
112
- ### Severity Mapping
113
-
114
- | Level | Name | Criteria | Action |
115
- |-------|------|----------|--------|
116
- | **P0** | Critical | Security vulnerability, data loss, correctness bug, empty catch block | Must block merge |
117
- | **P1** | High | Logic error, SOLID violation, performance regression, mock violation, catch returning null | Should fix before merge |
118
- | **P2** | Medium | Code smell, maintainability, deep nesting, optional chaining hiding errors | Fix or create follow-up |
119
- | **P3** | Low | Style, naming, minor suggestion, informational type quality notes | Optional |
67
+ ```json
68
+ {
69
+ "$schema": "ultra-review-summary-v2",
70
+ "session": "<session-id>",
71
+ "head": "<full-git-head>",
72
+ "status": "complete",
73
+ "verdict": "REQUEST_CHANGES",
74
+ "axes": {
75
+ "spec_fidelity": {
76
+ "verdict": "PASS",
77
+ "evidence_refs": ["spec-fidelity.json"]
78
+ },
79
+ "engineering_standards": {
80
+ "verdict": "FAIL",
81
+ "evidence_refs": ["review-code.json", "review-tests.json"]
82
+ }
83
+ },
84
+ "workers": {
85
+ "completed": ["review-code", "review-tests"],
86
+ "failed": [],
87
+ "skipped": []
88
+ },
89
+ "findings": [],
90
+ "positive_observations": [],
91
+ "limitations": []
92
+ }
93
+ ```
120
94
 
121
- ## Output Rules
95
+ The overall verdict is `APPROVE` only when both axes pass, artifacts are complete and
96
+ current, and no P0 or P1 finding remains. Use `REQUEST_CHANGES` for a failed axis or
97
+ blocking finding, and `INCOMPLETE` when required evidence or workers are missing.
98
+ Each axis verdict is `PASS`, `FAIL`, or `INCOMPLETE`; the overall verdict is `APPROVE`,
99
+ `REQUEST_CHANGES`, or `INCOMPLETE`.
122
100
 
123
- 1. Write the JSON to the file path provided in the task prompt
124
- 2. JSON MUST be valid - use `JSON.stringify` equivalent formatting
125
- 3. Only report findings with confidence >= 75
126
- 4. Do NOT output the JSON to the conversation - write it to the file only
127
- 5. After writing the file, output a one-line summary: `"Wrote N findings (P0:X P1:X P2:X P3:X) to <filepath>"`
101
+ Validate JSON before acknowledgement. Never paste full JSON or worker transcripts into
102
+ the parent conversation.
@@ -1,88 +1,254 @@
1
1
  #!/usr/bin/env python3
2
- """Review Wait - File-based completion waiter for /ultra-review pipeline.
3
-
4
- Polls the session directory for expected review JSON files and/or SUMMARY.json.
5
- Returns a one-line verdict on stdout. Blocks until complete or timeout.
2
+ """Wait for and validate Ultra review v2 artifacts.
6
3
 
7
4
  Usage:
8
- python3 review_wait.py <session_path> agents <count> # Wait for N review-*.json files
9
- python3 review_wait.py <session_path> summary # Wait for SUMMARY.json only
5
+ python3 review_wait.py <session_path> agents <artifact-stem> [<artifact-stem> ...]
6
+ python3 review_wait.py <session_path> summary
10
7
 
11
- Exit codes:
12
- 0 - All expected files found
13
- 1 - Timeout (default 5 minutes)
14
- 2 - Invalid arguments
8
+ The agents mode validates the exact named JSON files. The summary mode validates the
9
+ two-axis verdict contract. Timeout and poll intervals can be shortened in tests with
10
+ UBP_REVIEW_WAIT_TIMEOUT and UBP_REVIEW_WAIT_POLL.
15
11
  """
16
12
 
17
- import glob
18
13
  import json
14
+ import os
15
+ import re
19
16
  import sys
20
17
  import time
21
18
  from pathlib import Path
22
19
 
23
- POLL_INTERVAL = 2 # seconds
24
- DEFAULT_TIMEOUT = 300 # 5 minutes
20
+ FINDINGS_SCHEMA = "ultra-review-findings-v2"
21
+ SUMMARY_SCHEMA = "ultra-review-summary-v2"
22
+ AXES = {"spec_fidelity", "engineering_standards"}
23
+ SEVERITIES = {"P0", "P1", "P2", "P3"}
24
+ AXIS_VERDICTS = {"PASS", "FAIL", "INCOMPLETE"}
25
+ OVERALL_VERDICTS = {"APPROVE", "REQUEST_CHANGES", "INCOMPLETE"}
26
+ ARTIFACT_STEM = re.compile(r"^[a-z][a-z0-9-]*$")
27
+ FINDING_FIELDS = {
28
+ "id", "axis", "severity", "category", "title", "file", "line", "trigger",
29
+ "impact", "evidence", "suggestion",
30
+ }
31
+
32
+
33
+ def env_seconds(name: str, default: float) -> float:
34
+ try:
35
+ value = float(os.environ.get(name, default))
36
+ except (TypeError, ValueError):
37
+ return default
38
+ return value if value > 0 else default
39
+
40
+
41
+ DEFAULT_TIMEOUT = env_seconds("UBP_REVIEW_WAIT_TIMEOUT", 300.0)
42
+ POLL_INTERVAL = env_seconds("UBP_REVIEW_WAIT_POLL", 2.0)
43
+
44
+
45
+ def read_json(path: Path):
46
+ try:
47
+ data = json.loads(path.read_text(encoding="utf-8"))
48
+ except (OSError, json.JSONDecodeError) as error:
49
+ return None, f"unreadable JSON: {error}"
50
+ if not isinstance(data, dict):
51
+ return None, "top-level value must be an object"
52
+ return data, None
53
+
54
+
55
+ def nonempty_string(value) -> bool:
56
+ return isinstance(value, str) and bool(value.strip())
57
+
58
+
59
+ def validate_finding(finding, artifact_axis: str, seen_ids):
60
+ if not isinstance(finding, dict):
61
+ return "finding must be an object"
62
+ missing = sorted(FINDING_FIELDS - set(finding))
63
+ if missing:
64
+ return f"finding missing fields: {', '.join(missing)}"
65
+ if not nonempty_string(finding.get("id")):
66
+ return "finding id must be a non-empty string"
67
+ if finding["id"] in seen_ids:
68
+ return f"duplicate finding id: {finding['id']}"
69
+ seen_ids.add(finding["id"])
70
+ if finding.get("axis") != artifact_axis:
71
+ return "finding axis must match artifact axis"
72
+ if finding.get("severity") not in SEVERITIES:
73
+ return "finding severity must be P0, P1, P2, or P3"
74
+ for field in ["category", "title", "file", "trigger", "impact", "evidence", "suggestion"]:
75
+ if not nonempty_string(finding.get(field)):
76
+ return f"finding {field} must be a non-empty string"
77
+ if not isinstance(finding.get("line"), int) or finding["line"] < 1:
78
+ return "finding line must be a positive integer"
79
+ line_end = finding.get("line_end")
80
+ if line_end is not None and (not isinstance(line_end, int) or line_end < finding["line"]):
81
+ return "finding line_end must be at or after line"
82
+ return None
83
+
84
+
85
+ def validate_specialist(data):
86
+ if data.get("$schema") != FINDINGS_SCHEMA:
87
+ return f"$schema must be {FINDINGS_SCHEMA}"
88
+ if not nonempty_string(data.get("agent")):
89
+ return "agent must be a non-empty string"
90
+ axis = data.get("axis")
91
+ if axis not in AXES:
92
+ return "axis must be spec_fidelity or engineering_standards"
93
+ for field in ["session", "timestamp"]:
94
+ if not nonempty_string(data.get(field)):
95
+ return f"{field} must be a non-empty string"
96
+ scope = data.get("scope")
97
+ if not isinstance(scope, dict):
98
+ return "scope must be an object"
99
+ for field in ["head", "range"]:
100
+ if not nonempty_string(scope.get(field)):
101
+ return f"scope.{field} must be a non-empty string"
102
+ if not isinstance(scope.get("files_analyzed"), list) or not all(
103
+ nonempty_string(item) for item in scope["files_analyzed"]
104
+ ):
105
+ return "scope.files_analyzed must be a string array"
106
+ if not isinstance(scope.get("diff_only"), bool):
107
+ return "scope.diff_only must be boolean"
108
+ if data.get("status") != "complete":
109
+ return "status must be complete"
110
+ findings = data.get("findings")
111
+ if not isinstance(findings, list):
112
+ return "findings must be an array"
113
+ seen_ids = set()
114
+ for finding in findings:
115
+ error = validate_finding(finding, axis, seen_ids)
116
+ if error:
117
+ return error
118
+ for field in ["positive_observations", "limitations"]:
119
+ if not isinstance(data.get(field), list):
120
+ return f"{field} must be an array"
121
+ return None
25
122
 
26
123
 
27
- def wait_for_agents(session_path: Path, expected_count: int, timeout: int) -> bool:
28
- """Wait for expected_count review-*.json files to appear.
124
+ def evaluate_artifacts(session_path: Path, expected):
125
+ done = []
126
+ missing = []
127
+ invalid = []
128
+ errors = {}
129
+ for stem in expected:
130
+ path = session_path / f"{stem}.json"
131
+ if not path.exists():
132
+ missing.append(stem)
133
+ continue
134
+ data, error = read_json(path)
135
+ if error is None:
136
+ error = validate_specialist(data)
137
+ if error:
138
+ invalid.append(stem)
139
+ errors[stem] = error
140
+ else:
141
+ done.append(stem)
142
+ return done, missing, invalid, errors
29
143
 
30
- Returns structured JSON on stdout:
31
- - status: "complete" (all agents) or "partial" (>=1 agent on timeout)
32
- - agents_done / agents_missing: lists of agent names
33
- - count: number of completed agents
34
144
 
35
- Exit code: 0 if all complete OR partial (>=1), 1 if 0 agents.
36
- """
37
- all_agents = ["review-code", "review-tests", "review-errors",
38
- "review-design", "review-comments"]
145
+ def wait_for_agents(session_path: Path, expected, timeout: float) -> bool:
39
146
  deadline = time.monotonic() + timeout
40
- while time.monotonic() < deadline:
41
- found = glob.glob(str(session_path / "review-*.json"))
42
- if len(found) >= expected_count:
43
- names = [Path(f).stem for f in sorted(found)]
44
- result = {"status": "complete", "agents_done": names, "agents_missing": [], "count": len(found)}
45
- print(json.dumps(result))
147
+ while True:
148
+ done, missing, invalid, errors = evaluate_artifacts(session_path, expected)
149
+ if len(done) == len(expected):
150
+ print(json.dumps({
151
+ "status": "complete",
152
+ "artifacts_done": done,
153
+ "artifacts_missing": [],
154
+ "artifacts_invalid": [],
155
+ "errors": {},
156
+ "count": len(done),
157
+ }, sort_keys=True))
46
158
  return True
47
- remaining = int(deadline - time.monotonic())
48
- sys.stderr.write(f"\r Waiting: {len(found)}/{expected_count} ({remaining}s remaining)")
49
- sys.stderr.flush()
50
- time.sleep(POLL_INTERVAL)
51
-
52
- # Timeout — report partial results as structured JSON
53
- found = glob.glob(str(session_path / "review-*.json"))
54
- found_names = [Path(f).stem for f in sorted(found)]
55
- missing = [a for a in all_agents if a not in found_names]
56
- result = {"status": "partial", "agents_done": found_names, "agents_missing": missing, "count": len(found)}
57
- print(json.dumps(result))
58
- return len(found) >= 1 # At least 1 agent = partial success
59
-
60
-
61
- def wait_for_summary(session_path: Path, timeout: int) -> bool:
62
- """Wait for SUMMARY.json to appear."""
159
+ if time.monotonic() >= deadline:
160
+ print(json.dumps({
161
+ "status": "incomplete",
162
+ "artifacts_done": done,
163
+ "artifacts_missing": missing,
164
+ "artifacts_invalid": invalid,
165
+ "errors": errors,
166
+ "count": len(done),
167
+ }, sort_keys=True))
168
+ return False
169
+ time.sleep(min(POLL_INTERVAL, max(0.0, deadline - time.monotonic())))
170
+
171
+
172
+ def expected_overall_verdict(data) -> str:
173
+ verdicts = {data["axes"][axis]["verdict"] for axis in AXES}
174
+ severities = {finding["severity"] for finding in data["findings"]}
175
+ if "INCOMPLETE" in verdicts:
176
+ return "INCOMPLETE"
177
+ if "FAIL" in verdicts or severities.intersection({"P0", "P1"}):
178
+ return "REQUEST_CHANGES"
179
+ return "APPROVE"
180
+
181
+
182
+ def validate_summary(data):
183
+ if data.get("$schema") != SUMMARY_SCHEMA:
184
+ return f"$schema must be {SUMMARY_SCHEMA}"
185
+ for field in ["session", "head"]:
186
+ if not nonempty_string(data.get(field)):
187
+ return f"{field} must be a non-empty string"
188
+ if data.get("status") != "complete":
189
+ return "status must be complete"
190
+ if data.get("verdict") not in OVERALL_VERDICTS:
191
+ return "invalid overall verdict"
192
+ axes = data.get("axes")
193
+ if not isinstance(axes, dict) or set(axes) != AXES:
194
+ return "axes must contain exactly spec_fidelity and engineering_standards"
195
+ for axis in AXES:
196
+ item = axes[axis]
197
+ if not isinstance(item, dict) or item.get("verdict") not in AXIS_VERDICTS:
198
+ return f"invalid {axis} verdict"
199
+ refs = item.get("evidence_refs")
200
+ if not isinstance(refs, list) or not refs or not all(nonempty_string(ref) for ref in refs):
201
+ return f"{axis}.evidence_refs must be a non-empty string array"
202
+ workers = data.get("workers")
203
+ if not isinstance(workers, dict):
204
+ return "workers must be an object"
205
+ for field in ["completed", "failed", "skipped"]:
206
+ if not isinstance(workers.get(field), list):
207
+ return f"workers.{field} must be an array"
208
+ findings = data.get("findings")
209
+ if not isinstance(findings, list):
210
+ return "findings must be an array"
211
+ seen_ids = set()
212
+ for finding in findings:
213
+ axis = finding.get("axis") if isinstance(finding, dict) else None
214
+ if axis not in AXES:
215
+ return "summary finding has invalid axis"
216
+ error = validate_finding(finding, axis, seen_ids)
217
+ if error:
218
+ return error
219
+ for field in ["positive_observations", "limitations"]:
220
+ if not isinstance(data.get(field), list):
221
+ return f"{field} must be an array"
222
+ expected = expected_overall_verdict(data)
223
+ if data["verdict"] != expected:
224
+ return f"verdict {data['verdict']} conflicts with evidence; expected {expected}"
225
+ return None
226
+
227
+
228
+ def wait_for_summary(session_path: Path, timeout: float) -> bool:
63
229
  summary_path = session_path / "SUMMARY.json"
64
230
  deadline = time.monotonic() + timeout
65
- while time.monotonic() < deadline:
66
- if summary_path.exists() and summary_path.stat().st_size > 0:
67
- try:
68
- data = json.loads(summary_path.read_text(encoding="utf-8"))
69
- verdict = data.get("verdict", "UNKNOWN")
70
- p0 = data.get("p0", 0)
71
- p1 = data.get("p1", 0)
72
- total = data.get("total", 0)
73
- print(f"Review complete: {verdict} (P0:{p0} P1:{p1} total:{total})")
231
+ last_error = "SUMMARY.json is missing"
232
+ while True:
233
+ if summary_path.exists():
234
+ data, error = read_json(summary_path)
235
+ if error is None:
236
+ error = validate_summary(data)
237
+ if error is None:
238
+ counts = {severity: 0 for severity in sorted(SEVERITIES)}
239
+ for finding in data["findings"]:
240
+ counts[finding["severity"]] += 1
241
+ print(
242
+ f"Review complete: {data['verdict']} "
243
+ f"(P0:{counts['P0']} P1:{counts['P1']} P2:{counts['P2']} "
244
+ f"P3:{counts['P3']} total:{len(data['findings'])})"
245
+ )
74
246
  return True
75
- except (json.JSONDecodeError, KeyError):
76
- pass # File still being written
77
- remaining = int(deadline - time.monotonic())
78
- print(
79
- f"\r Waiting for coordinator ({remaining}s remaining)",
80
- end="", flush=True
81
- )
82
- time.sleep(POLL_INTERVAL)
83
-
84
- print("\nTimeout: coordinator did not produce SUMMARY.json")
85
- return False
247
+ last_error = error
248
+ if time.monotonic() >= deadline:
249
+ print(json.dumps({"status": "incomplete", "error": last_error}, sort_keys=True))
250
+ return False
251
+ time.sleep(min(POLL_INTERVAL, max(0.0, deadline - time.monotonic())))
86
252
 
87
253
 
88
254
  def main():
@@ -92,28 +258,27 @@ def main():
92
258
 
93
259
  session_path = Path(sys.argv[1])
94
260
  mode = sys.argv[2]
95
-
96
261
  if not session_path.is_dir():
97
262
  print(f"Error: session directory not found: {session_path}", file=sys.stderr)
98
263
  sys.exit(2)
99
264
 
100
- timeout = DEFAULT_TIMEOUT
101
-
102
265
  if mode == "agents":
103
- if len(sys.argv) < 4:
104
- print("Error: agents mode requires <count> argument", file=sys.stderr)
266
+ expected = sys.argv[3:]
267
+ if not expected or len(expected) != len(set(expected)) or not all(
268
+ ARTIFACT_STEM.fullmatch(stem) for stem in expected
269
+ ):
270
+ print("Error: agents mode requires unique safe artifact stems", file=sys.stderr)
105
271
  sys.exit(2)
106
- expected = int(sys.argv[3])
107
- ok = wait_for_agents(session_path, expected, timeout)
108
- sys.exit(0 if ok else 1)
109
-
272
+ ok = wait_for_agents(session_path, expected, DEFAULT_TIMEOUT)
110
273
  elif mode == "summary":
111
- ok = wait_for_summary(session_path, timeout)
112
- sys.exit(0 if ok else 1)
113
-
274
+ if len(sys.argv) != 3:
275
+ print("Error: summary mode accepts no additional arguments", file=sys.stderr)
276
+ sys.exit(2)
277
+ ok = wait_for_summary(session_path, DEFAULT_TIMEOUT)
114
278
  else:
115
279
  print(f"Error: unknown mode '{mode}'. Use 'agents' or 'summary'.", file=sys.stderr)
116
280
  sys.exit(2)
281
+ sys.exit(0 if ok else 1)
117
282
 
118
283
 
119
284
  if __name__ == "__main__":