okstra 0.223.2 → 0.225.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/dist/commands/execute/render-bundle.mjs +7 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/docs/cli.md +10 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/lib/okstra/globals.sh +1 -0
- package/runtime/bin/lib/okstra/usage.sh +4 -1
- package/runtime/bin/okstra-provider-exec.py +32 -16
- package/runtime/bin/okstra.sh +1 -0
- package/runtime/prompts/coding-preflight/clean-code.md +8 -1
- package/runtime/prompts/duties/code-reviewer.json +5 -3
- package/runtime/prompts/wizard/prompts.ko.json +10 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +3 -1
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +5 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/results.py +25 -0
- package/runtime/python/okstra_ctl/code_review_coverage.py +213 -0
- package/runtime/python/okstra_ctl/code_review_target.py +31 -2
- package/runtime/python/okstra_ctl/coverage_census.py +2 -2
- package/runtime/python/okstra_ctl/dispatch_core.py +15 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +6 -0
- package/runtime/python/okstra_ctl/incremental_carry.py +10 -16
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-diff-review.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +2 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +164 -33
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +27 -3
- package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +33 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/round_completion.py +177 -0
- package/runtime/python/okstra_ctl/plan_item_evidence.py +237 -0
- package/runtime/python/okstra_ctl/plan_items.py +12 -9
- package/runtime/python/okstra_ctl/plan_review_context.py +82 -0
- package/runtime/python/okstra_ctl/render.py +2 -0
- package/runtime/python/okstra_ctl/run.py +23 -2
- package/runtime/python/okstra_ctl/team.py +12 -18
- package/runtime/python/okstra_ctl/verdict_blocks.py +17 -1
- package/runtime/python/okstra_ctl/wizard/__init__.py +1 -0
- package/runtime/python/okstra_ctl/wizard/confirmation.py +2 -0
- package/runtime/python/okstra_ctl/wizard/engine.py +26 -5
- package/runtime/python/okstra_ctl/wizard/ids.py +3 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +13 -0
- package/runtime/python/okstra_ctl/wizard/render.py +1 -0
- package/runtime/python/okstra_ctl/wizard/state.py +8 -0
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +99 -16
- package/runtime/python/okstra_ctl/wizard/steps_options.py +52 -1
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +14 -0
- package/runtime/python/okstra_ctl/worker_dispatch.py +9 -0
- package/runtime/python/okstra_ctl/worker_request.py +12 -1
- package/runtime/schemas/execution-manifest-v2.schema.json +1 -0
- package/runtime/schemas/final-report-v2.0.schema.json +3 -1
- package/runtime/schemas/final-report-v3.0.schema.json +3 -1
- package/runtime/skills/_fragments/bash-invocation-rule.md +1 -1
- package/runtime/skills/_fragments/host-question-interface.md +1 -0
- package/runtime/skills/_fragments/host-runtime-selection.md +1 -0
- package/runtime/skills/_fragments/wizard-bash-invocation-rule.md +1 -1
- package/runtime/skills/_fragments/wizard-preflight.md +1 -1
- package/runtime/skills/_fragments/wizard-render-bundle.md +4 -4
- package/runtime/skills/okstra-brief-gen/SKILL.md +45 -47
- package/runtime/skills/okstra-brief-gen/references/reporter-confirmations.md +3 -3
- package/runtime/skills/okstra-brief-gen/references/tracker-recursion.md +1 -1
- package/runtime/skills/okstra-chat/SKILL.md +1 -1
- package/runtime/skills/okstra-code-review/SKILL.md +26 -15
- package/runtime/skills/okstra-code-review/references/census-rules.md +38 -2
- package/runtime/skills/okstra-code-review/references/review-calibration.md +39 -15
- package/runtime/skills/okstra-container-build/SKILL.md +6 -2
- package/runtime/skills/okstra-discussion/SKILL.md +6 -6
- package/runtime/skills/okstra-inspect/SKILL.md +5 -3
- package/runtime/skills/okstra-manager/SKILL.md +8 -4
- package/runtime/skills/okstra-memory/SKILL.md +1 -1
- package/runtime/skills/okstra-pr-gen/SKILL.md +10 -6
- package/runtime/skills/okstra-rollup/SKILL.md +6 -2
- package/runtime/skills/okstra-run/SKILL.md +8 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +13 -6
- package/runtime/skills/okstra-setup/SKILL.md +8 -4
- package/runtime/skills/okstra-setup/references/project-config.md +3 -3
- package/runtime/skills/okstra-usage/SKILL.md +5 -1
- package/runtime/skills/okstra-user-response/SKILL.md +7 -3
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
|
|
8
|
+
from .agent.invocation import AgentInvocationError, verify_standalone_completion
|
|
9
|
+
from .convergence_provenance import id_occurs_wordbounded
|
|
10
|
+
from .coverage_census import CellVerdicts, split_coverage_verdict_section
|
|
11
|
+
from .json_boundary import load_owned_object
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ReviewCell:
|
|
16
|
+
target: str
|
|
17
|
+
group: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ReviewCensus:
|
|
22
|
+
rules: dict[str, tuple[str, ...]]
|
|
23
|
+
cells: dict[str, ReviewCell]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _table_fields(line: str) -> list[str]:
|
|
27
|
+
return [field.strip() for field in line.strip().strip("|").split("|")]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read_review_census(path: Path) -> ReviewCensus:
|
|
31
|
+
rules: dict[str, list[str]] = {}
|
|
32
|
+
cells: dict[str, ReviewCell] = {}
|
|
33
|
+
rule_ids: set[str] = set()
|
|
34
|
+
pairs: set[tuple[str, str]] = set()
|
|
35
|
+
references: set[tuple[str, str]] = set()
|
|
36
|
+
section = ""
|
|
37
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
38
|
+
if line.startswith("## "):
|
|
39
|
+
section = line.strip()
|
|
40
|
+
if section not in ("## Rules", "## Cells") or not line.strip().startswith("|"):
|
|
41
|
+
continue
|
|
42
|
+
fields = _table_fields(line)
|
|
43
|
+
header = fields[0] in ("Rule", "Cell")
|
|
44
|
+
if header or all(re.fullmatch(r"[-:]+", f) for f in fields):
|
|
45
|
+
continue
|
|
46
|
+
if len(fields) != 3 or not all(fields):
|
|
47
|
+
raise ValueError("malformed grouped census row")
|
|
48
|
+
identity, value, reference = fields
|
|
49
|
+
if section == "## Rules":
|
|
50
|
+
if not re.fullmatch(r"[A-Z][A-Z0-9]*", identity) or identity in rule_ids:
|
|
51
|
+
raise ValueError("invalid or duplicate rule ID")
|
|
52
|
+
if (value, reference) in references:
|
|
53
|
+
raise ValueError("duplicate group and rule reference")
|
|
54
|
+
if value not in ("structural", "semantic", "state-and-tests", "general"):
|
|
55
|
+
raise ValueError("unknown rule group")
|
|
56
|
+
rule_ids.add(identity)
|
|
57
|
+
references.add((value, reference))
|
|
58
|
+
rules.setdefault(value, []).append(identity)
|
|
59
|
+
else:
|
|
60
|
+
if not re.fullmatch(r"C-CR-[0-9]+", identity) or identity in cells:
|
|
61
|
+
raise ValueError("invalid or duplicate cell ID")
|
|
62
|
+
if (value, reference) in pairs:
|
|
63
|
+
raise ValueError("duplicate target and group")
|
|
64
|
+
target = PurePosixPath(value.partition(":")[0])
|
|
65
|
+
if target.is_absolute() or ".." in target.parts:
|
|
66
|
+
raise ValueError("target must be project-relative")
|
|
67
|
+
pairs.add((value, reference))
|
|
68
|
+
cells[identity] = ReviewCell(value, reference)
|
|
69
|
+
if not rules or not cells:
|
|
70
|
+
raise ValueError("census needs Rules and Cells tables")
|
|
71
|
+
if any(cell.group not in rules for cell in cells.values()):
|
|
72
|
+
raise ValueError("cell names an unknown rule group")
|
|
73
|
+
return ReviewCensus({group: tuple(ids) for group, ids in rules.items()}, cells)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _keyed_values(text: str) -> dict[str, str]:
|
|
77
|
+
values: dict[str, str] = {}
|
|
78
|
+
if text == "-":
|
|
79
|
+
return values
|
|
80
|
+
for entry in text.split(";"):
|
|
81
|
+
key, separator, value = entry.partition("=")
|
|
82
|
+
key, value = key.strip(), value.strip()
|
|
83
|
+
if not separator or not key or not value or key in values:
|
|
84
|
+
raise ValueError("empty or duplicate rule assignment")
|
|
85
|
+
values[key] = value
|
|
86
|
+
return values
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _source_evidence(project_root: Path, cell: ReviewCell, evidence: str) -> str:
|
|
90
|
+
match = re.fullmatch(r"(.+):([1-9][0-9]*)", evidence.strip("`"))
|
|
91
|
+
target = cell.target.partition(":")[0]
|
|
92
|
+
if match is None or match[1] != target:
|
|
93
|
+
raise ValueError("evidence must cite the target source path:line")
|
|
94
|
+
relative = PurePosixPath(target)
|
|
95
|
+
source = (project_root / target).resolve()
|
|
96
|
+
if (relative.is_absolute() or ".." in relative.parts
|
|
97
|
+
or not source.is_relative_to(project_root.resolve())):
|
|
98
|
+
raise ValueError("source evidence escapes project")
|
|
99
|
+
if not source.is_file():
|
|
100
|
+
raise ValueError("source evidence file does not exist")
|
|
101
|
+
if int(match[2]) > len(source.read_text(encoding="utf-8").splitlines()):
|
|
102
|
+
raise ValueError("source evidence line does not exist")
|
|
103
|
+
return evidence
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _group_verdict(
|
|
107
|
+
fields: Sequence[str], expected: Sequence[str], cell: ReviewCell,
|
|
108
|
+
project_root: Path, references: Sequence[str],
|
|
109
|
+
) -> tuple[str, str]:
|
|
110
|
+
if len(fields) != 5:
|
|
111
|
+
raise ValueError("expected Cell, Checked, N/A, Findings, Evidence columns")
|
|
112
|
+
checked = [] if fields[1] == "-" else [rule.strip() for rule in fields[1].split(",")]
|
|
113
|
+
excluded = _keyed_values(fields[2])
|
|
114
|
+
findings = _keyed_values(fields[3])
|
|
115
|
+
assigned = checked + list(excluded) + list(findings)
|
|
116
|
+
if len(set(assigned)) != len(assigned):
|
|
117
|
+
raise ValueError("duplicate or conflicting rule verdicts")
|
|
118
|
+
unknown = set(assigned) - set(expected)
|
|
119
|
+
if unknown:
|
|
120
|
+
raise ValueError("unknown rules: " + ", ".join(sorted(unknown)))
|
|
121
|
+
missing = set(expected) - set(assigned)
|
|
122
|
+
if missing:
|
|
123
|
+
raise ValueError("unchecked rules: " + ", ".join(sorted(missing)))
|
|
124
|
+
for reason in excluded.values():
|
|
125
|
+
if re.fullmatch(r"(?:[A-Z][0-9]+|[a-z]+:[0-9]+)", reason):
|
|
126
|
+
raise ValueError("n/a needs an inline applicability reason, not an alias")
|
|
127
|
+
for ids in findings.values():
|
|
128
|
+
for item in ids.split(","):
|
|
129
|
+
item = item.strip()
|
|
130
|
+
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", item):
|
|
131
|
+
raise ValueError("invalid finding ID")
|
|
132
|
+
if not any(id_occurs_wordbounded(body, item) for body in references):
|
|
133
|
+
raise ValueError(f"finding {item} has no finding body")
|
|
134
|
+
evidence = _source_evidence(project_root, cell, fields[4])
|
|
135
|
+
if findings:
|
|
136
|
+
return "finding", "; ".join(findings.values())
|
|
137
|
+
if checked:
|
|
138
|
+
return "clean", evidence
|
|
139
|
+
return "n/a", "; ".join(excluded.values())
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _group_results(
|
|
143
|
+
body: str, census: ReviewCensus, project_root: Path, references: Sequence[str],
|
|
144
|
+
) -> CellVerdicts:
|
|
145
|
+
section, _ = split_coverage_verdict_section(body)
|
|
146
|
+
verdicts: dict[str, tuple[str, str]] = {}
|
|
147
|
+
reasons: dict[str, str] = {}
|
|
148
|
+
seen: set[str] = set()
|
|
149
|
+
unknown: list[str] = []
|
|
150
|
+
for line in section:
|
|
151
|
+
if not line.strip().startswith("|"):
|
|
152
|
+
continue
|
|
153
|
+
fields = _table_fields(line)
|
|
154
|
+
identity = fields[0]
|
|
155
|
+
if identity not in census.cells:
|
|
156
|
+
if identity.startswith("C-") and identity not in unknown:
|
|
157
|
+
unknown.append(identity)
|
|
158
|
+
continue
|
|
159
|
+
if identity in seen:
|
|
160
|
+
verdicts.pop(identity, None)
|
|
161
|
+
reasons[identity] = "duplicate cell row"
|
|
162
|
+
continue
|
|
163
|
+
seen.add(identity)
|
|
164
|
+
cell = census.cells[identity]
|
|
165
|
+
try:
|
|
166
|
+
verdicts[identity] = _group_verdict(
|
|
167
|
+
fields, census.rules[cell.group], cell, project_root, references,
|
|
168
|
+
)
|
|
169
|
+
except (OSError, ValueError) as exc:
|
|
170
|
+
reasons[identity] = str(exc)
|
|
171
|
+
return CellVerdicts(
|
|
172
|
+
verdicts,
|
|
173
|
+
{key: reasons.get(key, "no grouped verdict row") for key in census.cells if key not in verdicts},
|
|
174
|
+
tuple(unknown),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def check_review_coverage(
|
|
179
|
+
project_root: Path, census_path: Path, completions: Sequence[Path],
|
|
180
|
+
) -> CellVerdicts:
|
|
181
|
+
census = read_review_census(census_path)
|
|
182
|
+
if not completions or len(set(completions)) != len(completions):
|
|
183
|
+
raise ValueError("pass distinct completion paths for one reviewer")
|
|
184
|
+
bodies: list[str] = []
|
|
185
|
+
assignment: object = None
|
|
186
|
+
for path in completions:
|
|
187
|
+
verified = verify_standalone_completion(
|
|
188
|
+
path, project_root=project_root, expected_purpose="code-review",
|
|
189
|
+
)
|
|
190
|
+
metadata = load_owned_object(
|
|
191
|
+
verified.prompt_metadata_path, artifact="agent invocation metadata",
|
|
192
|
+
)
|
|
193
|
+
if metadata["audience"] != "code-reviewer":
|
|
194
|
+
raise AgentInvocationError("completion is not a code reviewer")
|
|
195
|
+
current = metadata["modelAssignment"]
|
|
196
|
+
if assignment is not None and current != assignment:
|
|
197
|
+
raise AgentInvocationError("coverage cannot mix reviewer model assignments")
|
|
198
|
+
assignment = current
|
|
199
|
+
bodies.append(verified.returned_body)
|
|
200
|
+
verdicts: dict[str, tuple[str, str]] = {}
|
|
201
|
+
missing: dict[str, str] = {}
|
|
202
|
+
unknown: list[str] = []
|
|
203
|
+
references = [split_coverage_verdict_section(body)[1] for body in bodies]
|
|
204
|
+
for body in bodies:
|
|
205
|
+
parsed = _group_results(body, census, project_root, references)
|
|
206
|
+
unknown.extend(key for key in parsed.unknown if key not in unknown)
|
|
207
|
+
for key, verdict in parsed.verdicts.items():
|
|
208
|
+
verdicts.setdefault(key, verdict)
|
|
209
|
+
missing.update(parsed.missing)
|
|
210
|
+
return CellVerdicts(
|
|
211
|
+
verdicts, {key: missing[key] for key in census.cells if key not in verdicts},
|
|
212
|
+
tuple(unknown),
|
|
213
|
+
)
|
|
@@ -102,6 +102,7 @@ def _branch_snapshot(repo: Path, branch: str, base: str, review_date: str) -> di
|
|
|
102
102
|
_CLI_EPILOG = r"""Usage:
|
|
103
103
|
okstra code-review target --task-key <k> --stage <N> [--base <ref>] [--project-root <dir>] [--cwd <dir>] --json
|
|
104
104
|
okstra code-review target --branch <name> [--base <ref>] [--date <YYYY-MM-DD>] [--project-root <dir>] --json
|
|
105
|
+
okstra code-review check-coverage --census <table-path> --completion <completion-path> [--completion ...] [--project-root <dir>]
|
|
105
106
|
okstra code-review check-lines --base <ref> --head <ref> --cite <path:line> [--cite ...] [--project-root <dir>]
|
|
106
107
|
|
|
107
108
|
Output: JSON { ok, mode, worktreePath, branch, baseCommit, headCommit,
|
|
@@ -111,7 +112,8 @@ any citation is not a line the range changed. `--base` sets the base in either m
|
|
|
111
112
|
mode reads the base commit from the stage registry row and branch mode falls
|
|
112
113
|
back to the merge-base with the default branch. An empty worktreePath means the review reads the branch ref
|
|
113
114
|
instead of a checked-out tree. This is read-only — it never creates
|
|
114
|
-
directories or files.
|
|
115
|
+
directories or files. `check-coverage` prints covered cells, missing rules and unknown cells;
|
|
116
|
+
it exits 1 for incomplete coverage and 2 for invalid input or completion evidence.
|
|
115
117
|
"""
|
|
116
118
|
|
|
117
119
|
|
|
@@ -122,7 +124,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
122
124
|
prog="okstra code-review",
|
|
123
125
|
description="Resolve a code review's diff range and result file path, or check finding citations."
|
|
124
126
|
)
|
|
125
|
-
parser.add_argument("command", choices=("target", "check-lines"))
|
|
127
|
+
parser.add_argument("command", choices=("target", "check-lines", "check-coverage"))
|
|
126
128
|
parser.add_argument("--task-key", default="", help="project-id:task-group:task-id")
|
|
127
129
|
parser.add_argument("--stage", type=int, default=0, help="stage number to review")
|
|
128
130
|
parser.add_argument("--branch", default="", help="branch to review")
|
|
@@ -135,6 +137,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
135
137
|
"--cite", action="append", default=[],
|
|
136
138
|
help="check-lines: a finding's `path:line` (repeatable)",
|
|
137
139
|
)
|
|
140
|
+
parser.add_argument("--census", default="", help="check-coverage: shared Rules and grouped Cells tables")
|
|
141
|
+
parser.add_argument("--completion", action="append", default=[],
|
|
142
|
+
help="check-coverage: verified reviewer completion (repeat for gap-fills)")
|
|
138
143
|
parser.add_argument("--date", default="", help="YYYY-MM-DD for branch result naming")
|
|
139
144
|
parser.add_argument("--project-root", default="", help="explicit project root")
|
|
140
145
|
parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
|
|
@@ -249,6 +254,28 @@ def _check_lines(args: argparse.Namespace, project_root: Path) -> int:
|
|
|
249
254
|
return 0 if all_ok else 1
|
|
250
255
|
|
|
251
256
|
|
|
257
|
+
def _check_coverage(args: argparse.Namespace, project_root: Path) -> int:
|
|
258
|
+
from .agent.invocation import AgentInvocationError
|
|
259
|
+
from .code_review_coverage import check_review_coverage
|
|
260
|
+
|
|
261
|
+
if not args.census or not args.completion:
|
|
262
|
+
print("check-coverage: pass --census and at least one --completion", file=sys.stderr)
|
|
263
|
+
return 2
|
|
264
|
+
try:
|
|
265
|
+
result = check_review_coverage(
|
|
266
|
+
project_root, Path(args.census), [Path(path) for path in args.completion],
|
|
267
|
+
)
|
|
268
|
+
except (OSError, ValueError, AgentInvocationError) as exc:
|
|
269
|
+
print(f"check-coverage: {exc}", file=sys.stderr)
|
|
270
|
+
return 2
|
|
271
|
+
print(f"Covered cells: {len(result.verdicts)}")
|
|
272
|
+
for key, reason in result.missing.items():
|
|
273
|
+
print(f"Missing: {key} — {reason}")
|
|
274
|
+
for key in result.unknown:
|
|
275
|
+
print(f"Unknown: {key}")
|
|
276
|
+
return 1 if result.missing or result.unknown else 0
|
|
277
|
+
|
|
278
|
+
|
|
252
279
|
def main(argv: list[str] | None = None) -> int:
|
|
253
280
|
args = _build_parser().parse_args(argv)
|
|
254
281
|
try:
|
|
@@ -260,6 +287,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
260
287
|
print(render_code_review_target_text(payload) if args.text else json.dumps(payload))
|
|
261
288
|
return 2
|
|
262
289
|
|
|
290
|
+
if args.command == "check-coverage":
|
|
291
|
+
return _check_coverage(args, Path(project_root))
|
|
263
292
|
if args.command == "check-lines":
|
|
264
293
|
return _check_lines(args, Path(project_root))
|
|
265
294
|
try:
|
|
@@ -233,7 +233,7 @@ class CellVerdicts:
|
|
|
233
233
|
unknown: tuple[str, ...]
|
|
234
234
|
|
|
235
235
|
|
|
236
|
-
def
|
|
236
|
+
def split_coverage_verdict_section(text: str) -> tuple[list[str], str]:
|
|
237
237
|
"""The Coverage Verdicts lines, and the rest of the result."""
|
|
238
238
|
lines = text.splitlines()
|
|
239
239
|
start = next(
|
|
@@ -288,7 +288,7 @@ def parse_cell_verdicts(
|
|
|
288
288
|
gap-fill result can cite an item its worker's first result already raised.
|
|
289
289
|
The first usable line for a cell wins.
|
|
290
290
|
"""
|
|
291
|
-
section, body =
|
|
291
|
+
section, body = split_coverage_verdict_section(result_text)
|
|
292
292
|
wanted = list(dict.fromkeys(cell_ids))
|
|
293
293
|
known = set(wanted)
|
|
294
294
|
verdicts: dict[str, tuple[str, str]] = {}
|
|
@@ -335,6 +335,21 @@ class DispatchPlan:
|
|
|
335
335
|
return payload
|
|
336
336
|
|
|
337
337
|
|
|
338
|
+
def existing_dispatch_plan(
|
|
339
|
+
project_root: Path, workspace_root: Path, run_manifest_path: Path,
|
|
340
|
+
manifest: Mapping[str, Any],
|
|
341
|
+
) -> DispatchPlan:
|
|
342
|
+
root = project_root.resolve()
|
|
343
|
+
return DispatchPlan(
|
|
344
|
+
project_root=root, workspace_root=workspace_root.resolve(),
|
|
345
|
+
manifest_path=_resolve_project_path(root, str(run_manifest_path)),
|
|
346
|
+
team_state_path=_resolve_required_path(root, manifest, "teamStatePath"),
|
|
347
|
+
lead_events_path=_resolve_required_path(root, manifest, "leadEventsPath"),
|
|
348
|
+
manifest=manifest, jobs=(),
|
|
349
|
+
default_backend=str(manifest.get("terminalBackend") or BACKEND_CLI_WRAPPER),
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
338
353
|
@dataclass(frozen=True)
|
|
339
354
|
class _BuildOptions:
|
|
340
355
|
okstra_bin: Path | None
|
|
@@ -18,6 +18,9 @@ SERVED_MODEL_MISMATCH_EXIT_CODE = 78
|
|
|
18
18
|
# JSON 이벤트 스트림이 result 이벤트 없이 깨끗하게(exit 0) 끝난 run.
|
|
19
19
|
# CLI 가 성공을 주장했지만 답을 내지 않았다 — sysexits 의 EX_PROTOCOL.
|
|
20
20
|
NO_RESULT_EXIT_CODE = 76
|
|
21
|
+
# run 하나에 한 단계, okstra-run 위저드에서 고른다. 강도를 받는 CLI 들이 이 넷을
|
|
22
|
+
# 공통으로 받으므로 각 전략은 철자만 옮긴다.
|
|
23
|
+
REASONING_EFFORTS = ("low", "medium", "high", "xhigh")
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
@dataclass(frozen=True)
|
|
@@ -62,6 +65,9 @@ class WorkerExecRequest:
|
|
|
62
65
|
# issue its own, so the recorded id and the session's own jsonl are
|
|
63
66
|
# guaranteed to name the same session.
|
|
64
67
|
session_id: str = ""
|
|
68
|
+
# `REASONING_EFFORTS` 중 하나. run 이 고르지 않았으면 빈 값이고, 그때 CLI 는
|
|
69
|
+
# 자기 설정을 따른다.
|
|
70
|
+
reasoning_effort: str = ""
|
|
65
71
|
|
|
66
72
|
|
|
67
73
|
@dataclass(frozen=True)
|
|
@@ -13,12 +13,13 @@ from .convergence_store import write_json_atomic
|
|
|
13
13
|
from .final_report_schema import load_schema_version
|
|
14
14
|
from .report_narrative import parse_narrative, writer_owned_data
|
|
15
15
|
from .json_boundary import load_owned_object
|
|
16
|
+
from .plan_item_evidence import dependency_scopes, reusable_item_ids
|
|
16
17
|
from .plan_items import (
|
|
17
18
|
PlanItemContractError,
|
|
18
19
|
content_hash,
|
|
20
|
+
dispatch_item_ids,
|
|
19
21
|
extract_plan_items,
|
|
20
22
|
planning_stage_ledger,
|
|
21
|
-
reverify_item_ids,
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
|
|
@@ -386,7 +387,6 @@ def _carry_unchanged_checklists(
|
|
|
386
387
|
|
|
387
388
|
def _recompute_dispatch_queue(
|
|
388
389
|
narrative: Mapping[str, Any],
|
|
389
|
-
state_items: dict[str, dict],
|
|
390
390
|
stage_ledger: Mapping[str, Any] | None = None,
|
|
391
391
|
) -> list[str]:
|
|
392
392
|
"""이월 뒤 남은 디스패치 큐.
|
|
@@ -401,18 +401,6 @@ def _recompute_dispatch_queue(
|
|
|
401
401
|
extracted = extract_plan_items(_planning(dict(narrative)))
|
|
402
402
|
except (CarryError, PlanItemContractError, KeyError, TypeError):
|
|
403
403
|
return []
|
|
404
|
-
by_id = {
|
|
405
|
-
item["id"]: item
|
|
406
|
-
for item in extracted
|
|
407
|
-
if isinstance(item.get("id"), str)
|
|
408
|
-
}
|
|
409
|
-
previous_hashes = {
|
|
410
|
-
item_id: content_hash(by_id[item_id])
|
|
411
|
-
for item_id, row in state_items.items()
|
|
412
|
-
if row.get("carriedForwardFromSeq") and item_id in by_id
|
|
413
|
-
}
|
|
414
|
-
if not previous_hashes:
|
|
415
|
-
return []
|
|
416
404
|
ledger = planning_stage_ledger(
|
|
417
405
|
_planning(dict(narrative)),
|
|
418
406
|
{
|
|
@@ -420,7 +408,7 @@ def _recompute_dispatch_queue(
|
|
|
420
408
|
for stage, status in (stage_ledger or {}).items()
|
|
421
409
|
},
|
|
422
410
|
)
|
|
423
|
-
return
|
|
411
|
+
return dispatch_item_ids(extracted, ledger)
|
|
424
412
|
|
|
425
413
|
|
|
426
414
|
def _writer_stage_rows(data: Mapping[str, Any], *, snapshot: str) -> dict[int, dict]:
|
|
@@ -495,7 +483,7 @@ def merge_v3_plan_state(
|
|
|
495
483
|
state_items[item_id] for item_id in sorted(state_items)
|
|
496
484
|
]
|
|
497
485
|
queue = _recompute_dispatch_queue(
|
|
498
|
-
narrative,
|
|
486
|
+
narrative, pbv.get("stageLedger"),
|
|
499
487
|
)
|
|
500
488
|
if queue:
|
|
501
489
|
pbv["dispatchQueue"] = queue
|
|
@@ -533,6 +521,12 @@ def sync_prepared_dispatch_queue(state_path: Path, merged: Mapping[str, Any]) ->
|
|
|
533
521
|
if not isinstance(envelope, dict):
|
|
534
522
|
return
|
|
535
523
|
envelope["dispatchQueue"] = queue
|
|
524
|
+
inputs = envelope.get("reviewInputs")
|
|
525
|
+
if isinstance(inputs, dict):
|
|
526
|
+
rows = merged.get("planBodyVerification", {}).get("planItems", [])
|
|
527
|
+
inputs["dependencies"] = dependency_scopes(rows)
|
|
528
|
+
inputs["priorReviewHashes"] = {str(row["id"]): str(row.get("verifiedReviewHash", "")) for row in rows}
|
|
529
|
+
envelope["reusedItems"] = sorted(reusable_item_ids(rows, inputs) - set(queue))
|
|
536
530
|
write_json_atomic(prepared, envelope)
|
|
537
531
|
|
|
538
532
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
- no new defects introduced — the diff does not break previously-working behaviour and adds no new bug (logic/off-by-one, null/empty handling, resource leaks, broken error paths)
|
|
15
15
|
- no security defect on input from outside the process — for every changed file that reads a request body, query, header, upload, message or webhook, or acts on its behalf: work that grows faster than the input (pairwise uniqueness, nested scans, a backtracking regex, input-driven recursion) runs only after a size/count bound has rejected oversized input — check the validation framework's configuration, since class-validator without `stopAtFirstError` still runs `@ArrayUnique` after `@ArrayMaxSize` fails and a byte-size body limit does not bound element count; input reaches SQL, shell commands, file paths, server-side fetch targets, templates and regexes only through parameters, allowlists or escaping; a new route / handler / resolver carries the project's auth guard and compares the caller with the owner of an input-chosen id; no secret, full token, password or personal data reaches source, logs, error messages or responses; nothing evaluates or unsafely deserializes input. Each hit is an Acceptance Blocker citing the triggering input and its effect. The same rules, with examples, are `clean-code.md` "Untrusted input and security" in the installed coding-preflight pack (`~/.okstra/prompts/coding-preflight/clean-code.md`).
|
|
16
16
|
- scope conformance — the delivered diff stays within the approved plan's scope; flag out-of-scope edits, unrelated file changes, leftover debug/commented-out code, and unintended deletions
|
|
17
|
-
- project review-rule packs — a pack applies when either source names it: the task brief's `Source Material` / `Reporter Confirmations` cites its exact `SKILL.md` path, or `<PROJECT_ROOT>/.okstra/project.json` lists it under `reviewRulePacks` (the project's standing standard, applying whether or not the brief mentions it). The two sources are a union. Read only those files and the `references/*.md` files they directly name. Do not search parent directories or host skill catalogs. Apply the rules as an acceptance overlay (record `project-review-rules: <paths read>`, `project-review-rules: declared <path> unreadable`, or `project-review-rules: none declared or cited`). This is a static review pass, not a PR-comment workflow — do NOT dispatch reviewer subagents. Because this phase verifies the **whole-task merged diff**, it is the gate that catches **cross-stage findings a per-stage `implementation` verifier
|
|
17
|
+
- project review-rule packs — a pack applies when either source names it: the task brief's `Source Material` / `Reporter Confirmations` cites its exact `SKILL.md` path, or `<PROJECT_ROOT>/.okstra/project.json` lists it under `reviewRulePacks` (the project's standing standard, applying whether or not the brief mentions it). The two sources are a union. Read only those files and the `references/*.md` files they directly name. Do not search parent directories or host skill catalogs. Apply the rules as an acceptance overlay (record `project-review-rules: <paths read>`, `project-review-rules: declared <path> unreadable`, or `project-review-rules: none declared or cited`). This is a static review pass, not a PR-comment workflow — do NOT dispatch reviewer subagents. Because this phase verifies the **whole-task merged diff**, it is the gate that catches **cross-stage findings a per-stage `implementation` verifier can miss** (each implementation run reviews only its own stage diff; it compares new blocks with code already on its base, but not with stages that land after it): most importantly two cross-stage conditions: (a) the same helper stack / transform / domain rule duplicated across stages or services, test helpers and test support code included — byte-identical duplication is always an Acceptance Blocker, and semantically-equivalent transforms across services are blockers unless the approved plan explicitly justified keeping them separate; (b) an API newly orphaned because its only caller was removed in a different stage. A confirmed cross-stage duplication of this kind is an Acceptance Blocker (`major`+) that cites every `path:line` location and names the shared-module location to converge on. (Single-stage scope sees only one stage, so it cannot raise cross-stage findings — note that limitation rather than implying coverage.)
|
|
18
18
|
- Residual-tracked — note as Residual Risk unless severe enough to block:
|
|
19
19
|
- unresolved edge cases
|
|
20
20
|
- regression risk in adjacent code paths not directly changed
|
package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-diff-review.md
CHANGED
|
@@ -38,7 +38,7 @@ Do not scan holistically and stop when it "looks fine". Work the matrix exhausti
|
|
|
38
38
|
- port / adapter / domain file, when the hexagonal overlay is loaded → `architectures/hexagonal.md`: no business logic in a port body, adapter methods are I/O only (no post-fetch filtering on domain state, no `findValid*`/`findActive*` names hiding a rule), domain objects declared under the domain boundary, no changed domain file importing an ORM / framework / adapter / service (read the import list — mechanical), and a service dependency you add or modify goes through a port rather than a concrete adapter (advisory only while the project has not declared `architecture.style = hexagonal` in `.okstra/project.json` — record it with the port sketch; blocking under that declaration, so fix it in place before the commit rather than record-and-pass, exactly as `_implementation-verifier.md` re-grades this same diff. Either way, the codebase already injecting concrete classes is the debt this pays down, not a reason to skip it).
|
|
39
39
|
- a service file, when the hexagonal overlay is loaded → `architectures/hexagonal.md` Rule H6: a decision this diff puts in a service — an `if`/`else` chain over domain fields, a business formula, a private method named for a domain concept — belongs in a domain function. Orchestration control flow, DTO mapping and calling a domain predicate are not violations.
|
|
40
40
|
- a file that reads input from outside the process (request body, query, header, upload, message, webhook) or acts on its behalf → `clean-code.md` "Untrusted input and security": a size/count bound rejects oversized input before any check that grows faster than the input (a validator that runs every rule, such as class-validator without `stopAtFirstError`, does not stop at a failed `@ArrayMaxSize`), input reaches SQL / shell / paths / server-side fetches / templates / regexes only through parameters, allowlists or escaping, a new entry point carries the auth guard and compares the caller with the owner of an input-chosen id, no secret or personal data reaches code, logs or responses, and nothing evaluates or unsafely deserializes input.
|
|
41
|
-
- every changed source file → `clean-code.md` "Trace what this change can do wrong": walk the error, partial, concurrent and selection paths this diff creates to their end and fix wherever a wrong result comes out. Name the input that produces it — if you cannot name one, there is nothing to fix here and you move on rather than restructuring code that works.
|
|
41
|
+
- every changed source file → `clean-code.md` "Trace what this change can do wrong": walk the error, partial, concurrent and selection paths this diff creates to their end — past the failures the plan lists, and including where a cleaning `try` registers its cleanup target relative to each call that can throw — and fix wherever a wrong result comes out. Name the input that produces it — if you cannot name one, there is nothing to fix here and you move on rather than restructuring code that works.
|
|
42
42
|
A file can hold several roles — apply every rule set that fits it.
|
|
43
43
|
3. **Decide clean-or-finding for each cell.** Read the full file when a rule needs context (never judge a port/adapter/domain or a naming rule from the hunk alone).
|
|
44
44
|
4. **Fix each finding in place** before the commit. When a readability finding is real, the fix is a named helper or named intermediate value — sketch the cleaner shape (a few lines) in your audit note, then apply it. When the fix is genuinely out of this stage's scope, record it as an `Out-of-plan` note instead of silently leaving it.
|
package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md
CHANGED
|
@@ -194,12 +194,12 @@ Re-running commands proves the diff *builds and passes*; it does NOT prove the d
|
|
|
194
194
|
- **Declared architecture style promotes the placement overlay from advisory to binding.** Take `Architecture style` from that projection and record `architecture-style: <hexagonal|layered|none>` in the worker result next to the `coding-conventions:` line. A declared `hexagonal` counts the overlay as loaded even when none of the router's Stage 3 layout signals matched, so the **Hexagonal** blocking check below applies in full, and the concrete-adapter injection listed under Advisory findings is promoted to a blocking finding → verdict `FAIL`, not a `should-fix`. A declared `layered` has no pack resource; its binding invariant is direction — an upper layer may import a lower one, never the reverse — so a changed file whose import list reaches back up a layer, or around a layer boundary, is a blocking placement violation cited `path:line` from that import list. The `layered` half is worker judgement: no machine check reads layer names, so a missed reverse dependency is a missed finding, not a validator failure. A `none` or absent projected style leaves Stage 3 detection-driven and the placement items advisory. **Enforced:** `scripts/okstra_project/resolver.py` `resolve_architecture` reads the same stored field for the planning-side rule in `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_variation_point_analysis`, and `_validate_verifier_fail_blocks_verdict` keeps the resulting `FAIL` from being dropped during synthesis.
|
|
195
195
|
- **Blocking checks (any hit → verdict `FAIL`, cited `path:line` + rule name, recommended fix recorded — the verifier does NOT apply it):**
|
|
196
196
|
- **Security (every changed file that reads input from outside the process or acts on its behalf — `clean-code.md` "Untrusted input and security"):** work that grows faster than the input — pairwise uniqueness, nested scans, a backtracking regex, input-driven recursion — reachable before a size/count bound has rejected oversized input (read the validation framework's configuration: class-validator without `stopAtFirstError` still runs `@ArrayUnique` after `@ArrayMaxSize` fails, and a byte-size body limit does not bound element count); input concatenated into SQL, a shell command, a file path, a server-side fetch target, a template or a regex; a new route / handler / resolver without the project's auth guard, or a lookup by an input-chosen id that never compares the caller with the owner; a secret, full token, password or personal data written to source, logs, an error message or a response; `eval`, dynamic code, or unsafe deserialization applied to input. Cite the input that triggers it and its effect — for unbounded work, the input size and the measured or derived cost.
|
|
197
|
-
- **New duplication / DRY:** two or more newly added or meaningfully modified blocks implement the same helper stack, transform, or domain rule. Literal copy-paste is always blocking; semantically equivalent transforms across services are blocking unless the approved plan explicitly justified keeping them separate. Recommend the shared module location.
|
|
197
|
+
- **New duplication / DRY:** two or more newly added or meaningfully modified blocks implement the same helper stack, transform, or domain rule. Literal copy-paste is always blocking; semantically equivalent transforms across services are blocking unless the approved plan explicitly justified keeping them separate. The comparison is not limited to this diff: a newly added block that is byte-identical to a block already in the repository — base code, or a copy an earlier stage landed — is the same blocking finding, cited at both the new `path:line` and the existing one, because `final-verification` grades that cross-stage copy as an Acceptance Blocker and a per-stage `clean` only defers it. Test helpers and test support code count. Recommend the shared module location.
|
|
198
198
|
- **Self-mocking:** a test for `Foo` stubs/spies a method on the `Foo` instance under test (`jest.spyOn(sut, ...)`, `spyOn(FooService.prototype, ...)` in `foo.*.spec.*`, `vi.mocked(sut)` + stub). Mocking injected collaborators is fine.
|
|
199
199
|
- **Interaction-only assertion:** a test whose only/primary assertion is `toHaveBeenCalled*` / `toHaveBeenCalledTimes` on an internal helper or a non-side-effecting collaborator, with no assertion on the returned value / resulting state / persisted row / emitted event.
|
|
200
200
|
- **Tautological delegation assertion:** a test asserts the SUT result equals a direct call to the same pure helper/collaborator that the SUT delegates to, instead of asserting an independent literal value or observable state.
|
|
201
201
|
- **Untruthful name:** a read-named function (`get*` / `find*` / `load*`) that writes/inserts/mutates; an adapter or repository name encoding the caller's use-case (`*ForInit`) or hiding a domain rule (`findValid*` / `findActive*`).
|
|
202
|
-
- **Wrong-result trace (every changed source file):** follow the paths this diff creates or alters to their end — error, partial, concurrent, selection — and name the input or state that produces a wrong result (`clean-code.md` §"Trace what this change can do wrong"). This one is an obligation, not a pattern: the defects that reach production are usually ordinary code with no name on this list. The bar is also the noise filter — a finding names the failing input; an alternative structure, a guard for a state no caller reaches, or a "consider extracting" is an improvement and verdicts `clean`. **The unit is the changed source file**: record `clean` or findings for each one, list every file you excluded with its reason (lockfiles, generated code, pure config), and close the section with `general: <N> files — all verdicted, <M> excluded`.
|
|
202
|
+
- **Wrong-result trace (every changed source file):** follow the paths this diff creates or alters to their end — error, partial, concurrent, selection — and name the input or state that produces a wrong result (`clean-code.md` §"Trace what this change can do wrong"). This one is an obligation, not a pattern: the defects that reach production are usually ordinary code with no name on this list. The bar is also the noise filter — a finding names the failing input; an alternative structure, a guard for a state no caller reaches, or a "consider extracting" is an improvement and verdicts `clean`. The failure modes the plan lists are the floor of this trace, not its ceiling: a verdict scoped to "the modeled failures" is not a verdict on the file. For every `try` whose `catch` / `finally` cleans up, check at each call inside it that can throw (a property write, a read, a call that starts other work) whether the thing to clean up is already registered at that point; when the diff moves or restructures existing code, compare that order with the base version and cite the base `path:line` when it changed. A failure the test fakes cannot produce (a throwing setter or read the fake does not model) is recorded as unverified with the failure named, never folded into `clean`. **The unit is the changed source file**: record `clean` or findings for each one, list every file you excluded with its reason (lockfiles, generated code, pure config), and close the section with `general: <N> files — all verdicted, <M> excluded`.
|
|
203
203
|
- **Business rules in an application service (only when the hexagonal overlay is loaded):** changed service code that decides a business outcome inline — an `if`/`else` chain over domain fields, a business formula in arithmetic, a private method named for a domain concept — instead of calling a domain function (`architectures/hexagonal.md` Rule H6). Orchestration control flow, DTO mapping, and calling a domain predicate are not violations. Blocking when the embedded rule is substantial: money, permissions, a state machine.
|
|
204
204
|
- **Hexagonal (only when the overlay is loaded):** business logic inside a port body; an adapter method that is not pure I/O (post-fetch JS filtering on domain state, domain-rule evaluation); a domain object declared outside the `domain/` boundary; a changed file under the domain folder importing an ORM / DB layer, a framework or its DI decorators, or anything under adapters / infrastructure / services (read the import list — this verdict is mechanical, not a judgment). **The routed pack's project-convention latitude does not reach this cell.** `architectures/hexagonal.md` lets a documented convention resolve conflicts in the project's favour elsewhere in that overlay; here the import list decides, and "matches existing convention" is not an answer to it (see that file's §"How far a project convention reaches"). A run that resolved exactly this finding as project convention returned `clean` and the team's PR review flagged the same import.
|
|
205
205
|
- **gitignored file committed to the branch:** any path in the `git diff --name-only <base>...HEAD` enumeration that `.gitignore` excludes — enumerate them by piping that list through `git check-ignore --stdin --no-index`. A committed ignored file means the executor bulk-added (`git add .`/`-A`) or force-staged (`git add -f`) it, leaking build output, scratch files, or verification artifacts into the eventual PR. This explicitly includes `.okstra/` paths (and `.project-docs/` when the legacy symlink is present): `.okstra/**` is gitignored, so a committed okstra file (qa scripts, conformance results) is always this defect. Cite each path; recommend `git rm --cached <path>` to untrack it while keeping the file on disk. Conformance/qa evidence belongs in the carry sidecar / verifier result, never in git history.
|