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
|
@@ -1,127 +1,102 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Review artifact contract
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
6
|
+
## Specialist artifact
|
|
6
7
|
|
|
7
8
|
```json
|
|
8
9
|
{
|
|
9
|
-
"$schema": "ultra-review-findings-
|
|
10
|
-
"agent": "
|
|
11
|
-
"
|
|
12
|
-
"
|
|
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
|
-
"
|
|
15
|
-
"
|
|
16
|
+
"head": "<full-git-head>",
|
|
17
|
+
"range": "<diff-range>",
|
|
18
|
+
"files_analyzed": ["src/example.ts"],
|
|
16
19
|
"diff_only": true
|
|
17
20
|
},
|
|
18
|
-
"
|
|
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": "
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"category": "
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
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
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
+
## Coordinator summary
|
|
104
64
|
|
|
105
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
"""
|
|
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 <
|
|
9
|
-
python3 review_wait.py <session_path> summary
|
|
5
|
+
python3 review_wait.py <session_path> agents <artifact-stem> [<artifact-stem> ...]
|
|
6
|
+
python3 review_wait.py <session_path> summary
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
41
|
-
|
|
42
|
-
if len(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
""
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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__":
|