okstra 0.138.0 → 0.140.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/README.md +1 -0
- package/docs/architecture.md +4 -4
- package/docs/cli.md +4 -3
- package/docs/for-ai/README.md +2 -0
- package/docs/for-ai/skills/okstra-code-review.md +57 -0
- package/docs/project-structure-overview.md +5 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/coding-preflight/architectures/hexagonal.md +34 -0
- package/runtime/prompts/coding-preflight/clean-code.md +109 -0
- package/runtime/prompts/coding-preflight/languages/javascript-typescript.md +11 -0
- package/runtime/prompts/coding-preflight/overview.md +3 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +10 -4
- package/runtime/prompts/profiles/_implementation-self-check.md +2 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +12 -3
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/prompts/profiles/release-handoff.md +2 -1
- package/runtime/python/okstra_ctl/code_review_paths.py +64 -0
- package/runtime/python/okstra_ctl/code_review_target.py +152 -0
- package/runtime/python/okstra_ctl/worktree.py +3 -2
- package/runtime/python/okstra_project/__init__.py +2 -0
- package/runtime/python/okstra_project/state.py +142 -0
- package/runtime/skills/okstra-code-review/SKILL.md +180 -0
- package/runtime/skills/okstra-code-review/references/census-rules.md +100 -0
- package/runtime/skills/okstra-code-review/references/review-calibration.md +86 -0
- package/src/cli-registry.mjs +7 -0
- package/src/commands/inspect/code-review.mjs +35 -0
- package/src/lib/skill-catalog.mjs +1 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Census rules — turning a diff into the worklist
|
|
2
|
+
|
|
3
|
+
The census is the worklist: an explicit list of cells, each one a `target × axis` pair, built once by the orchestrator before any reviewer is dispatched. This file defines how a diff becomes cells. The rules a cell is judged against live in the coding-preflight pack; the verdict format lives in `review-calibration.md`.
|
|
4
|
+
|
|
5
|
+
## Cell granularity — one rule group per axis
|
|
6
|
+
|
|
7
|
+
**Each axis carries exactly one rule group, and that group is the axis itself.** A cell is `target × <axis>`, never `target × <individual rule>`. Twelve changed files therefore produce twelve `structural` cells, not one per file per rule.
|
|
8
|
+
|
|
9
|
+
This is what makes the cell count reproducible: the axis list is fixed at four, so the census size depends only on the diff. Splitting an axis per individual rule would make the count depend on how many rules the reader chose to enumerate that day — the exact variance this census exists to remove.
|
|
10
|
+
|
|
11
|
+
The specific rule a finding violates is named **inside the verdict**, in its `rule` field (`review-calibration.md`), not in the cell label. One cell can therefore carry findings from several rules of its group, and it still counts as one cell for coverage.
|
|
12
|
+
|
|
13
|
+
## The one principle
|
|
14
|
+
|
|
15
|
+
**Membership is mechanical; judgment only ever decides a cell's verdict.**
|
|
16
|
+
|
|
17
|
+
Never ask "does this rule apply to this file?" while building the census — that question is the reviewer's, and its answer is a verdict (`clean` when the rule turns out not to apply). Over-inclusion is cheap: a clean verdict costs one line. A judged exclusion is variance — it is the exact failure this census exists to remove, because the next run judges differently and the review changes shape.
|
|
18
|
+
|
|
19
|
+
Two consequences:
|
|
20
|
+
|
|
21
|
+
- A cell exists because a mechanical test matched (file extension, path, "the diff touched this function"), never because the orchestrator expected a violation.
|
|
22
|
+
- The orchestrator never reads a file to decide membership. It reads the diff's file list and hunks, and nothing else.
|
|
23
|
+
|
|
24
|
+
## The four axes
|
|
25
|
+
|
|
26
|
+
| Axis | Cell unit | Membership test (mechanical) |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `structural` | non-test source file × `structural` | every changed non-test source file |
|
|
29
|
+
| `semantic` | function × `semantic` | every function the diff touched — at least one changed line anywhere in its signature or body. Signature-only and type-annotation-only changes count |
|
|
30
|
+
| `state-and-tests` | non-test source file × `state-and-tests`, test file × `state-and-tests` | every changed non-test source file gets one cell; every changed test file gets one cell |
|
|
31
|
+
| `general` | non-test source file × `general` | every changed non-test source file |
|
|
32
|
+
|
|
33
|
+
"Is there a mutation or a state boundary in this file?" is **not** a membership question — the `state-and-tests` cell exists for every changed non-test source file, and the reviewer answers with a verdict.
|
|
34
|
+
|
|
35
|
+
Test files (the project's test-path or test-suffix convention — `*_test.*`, `*.test.*`, `*.spec.*`, `tests/`, `spec/`) are censused on `state-and-tests` only. They are not excluded: they are covered by a different rule group.
|
|
36
|
+
|
|
37
|
+
Functions are enumerated from the diff's hunks. A hunk that lands outside any function (imports, module wiring, top-level declarations, class-level constants) is file-level code — it belongs to the file cells, not to a function cell, and it is what the second completion criterion checks for.
|
|
38
|
+
|
|
39
|
+
## Exclusions
|
|
40
|
+
|
|
41
|
+
Only these are excluded from the census, and each exclusion is listed **with its reason** in the response and in the report's Coverage section:
|
|
42
|
+
|
|
43
|
+
- lockfiles (`package-lock.json`, `poetry.lock`, `Cargo.lock`, `*.sum`, …)
|
|
44
|
+
- generated code (anything under a build/output directory, or carrying a "do not edit" generation header)
|
|
45
|
+
- pure configuration and data (`.json`, `.yml`, `.toml`, fixtures) that contains no executable project code
|
|
46
|
+
- prose documentation with no executable or agent-directive content
|
|
47
|
+
- binary files and vendored third-party trees
|
|
48
|
+
|
|
49
|
+
**Skill, prompt, and agent-directive markdown IS censused.** In a repo whose behaviour is carried by prompts, a markdown file that instructs an agent is the executable surface, and a review that skips it skips the change. Extension is not the test — a `.md` file is excluded only when it is prose a human reads and nothing acts on.
|
|
50
|
+
|
|
51
|
+
To keep that mechanical rather than judged, a path allowlist overrides the extension list: anything under `skills/**`, `prompts/**`, `agents/**`, or `templates/**` is **always censused**, whatever its extension.
|
|
52
|
+
|
|
53
|
+
Everything else is censused. When a file is arguably code — a build script, a shell entrypoint, a template that carries logic — it is censused, not excluded; if the rules turn out not to apply, that is a `clean` verdict.
|
|
54
|
+
|
|
55
|
+
An exclusion without a stated reason is a defect in the census: it is indistinguishable from a file that was forgotten.
|
|
56
|
+
|
|
57
|
+
## Pack allocation
|
|
58
|
+
|
|
59
|
+
Step 2 of the skill routes the coding-preflight packs once. Distribute the resulting absolute paths across the axes like this:
|
|
60
|
+
|
|
61
|
+
| Pack | Goes to |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `clean-code.md` | all four axes — it is language-agnostic and every axis draws from a different part of it |
|
|
64
|
+
| `overview.md` core principles | all four axes (each brief names the principles that are its own; see the skill's axis table) |
|
|
65
|
+
| routed `languages/*.md` | `state-and-tests` (self-mock signals) and `general` (language-specific traps) |
|
|
66
|
+
| routed `frameworks/*.md` | `general` |
|
|
67
|
+
| routed `architectures/*.md` | `structural` |
|
|
68
|
+
|
|
69
|
+
A pack that the routing did not select is not handed to anyone. A pack a reviewer receives is one it must read before verdicting.
|
|
70
|
+
|
|
71
|
+
## Unregistered language
|
|
72
|
+
|
|
73
|
+
When no Stage 1 language rule in `overview.md` matches any changed file, the project's language has no pack. Do not invent one.
|
|
74
|
+
|
|
75
|
+
1. Ask the user for the project's canonical style guide (a path, a URL, or a named standard) and, if they name one, hand its path to `state-and-tests` and `general` in place of the language pack.
|
|
76
|
+
2. If the user does not answer, run all four axes anyway with `clean-code.md` alone, and state in the report's Coverage section that language-specific rules were not applied and why.
|
|
77
|
+
|
|
78
|
+
Never skip an axis for a missing pack, and never silently proceed as if the language pack existed.
|
|
79
|
+
|
|
80
|
+
## Completion criteria
|
|
81
|
+
|
|
82
|
+
Both must hold exactly, and both are shown in the response before dispatch:
|
|
83
|
+
|
|
84
|
+
1. **Files:** distinct files censused (in any axis) + files excluded = files in the diff.
|
|
85
|
+
|
|
86
|
+
One `git diff --name-status` row is one file, whatever its status letter. A rename row carries two paths (`R100 <old> <new>`) and still counts **once**: census the `<new>` path and never count `<old>` on either side of the equation. A delete row (`D <path>`) counts once too, and it is always an exclusion, with the reason `deleted — no remaining line to anchor a finding on`.
|
|
87
|
+
2. **Hunks:** every hunk in a censused file maps either to a censused function or to file-level code (imports, module wiring, declarations).
|
|
88
|
+
|
|
89
|
+
A count that does not balance means a file was dropped, not that the diff was odd. Find it before dispatching.
|
|
90
|
+
|
|
91
|
+
## What to emit
|
|
92
|
+
|
|
93
|
+
In the orchestrator's response, before Step 3:
|
|
94
|
+
|
|
95
|
+
- one table per axis, in the order `structural`, `semantic`, `state-and-tests`, `general`, listing every cell
|
|
96
|
+
- the exclusion list, one line per file, each with its reason
|
|
97
|
+
- the applied pack list (absolute paths)
|
|
98
|
+
- the two completion criteria with their actual numbers
|
|
99
|
+
|
|
100
|
+
Never truncate a table to save space. If the census is large, report the cell count and ask whether to proceed — a silent cut turns "every cell was verdicted" into a false claim.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Review calibration — verdicts, severity, and the report
|
|
2
|
+
|
|
3
|
+
Read this before verdicting. It defines the shape of an answer, not the rules — the rules are in the coding-preflight pack paths your brief listed, and you read those directly.
|
|
4
|
+
|
|
5
|
+
## One verdict per cell
|
|
6
|
+
|
|
7
|
+
Return verdicts in census order, one per cell. A clean cell is a single line — do not pad it with prose:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
- cell: src/orders/pricing.py × structural — verdict: clean
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
A cell covers its whole rule group, so it can carry several findings from several rules; repeat the cell once per finding and name the violated rule in each:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
- cell: src/orders/pricing.py × structural
|
|
17
|
+
verdict: finding
|
|
18
|
+
rule: DRY
|
|
19
|
+
line: 42
|
|
20
|
+
severity: must-fix
|
|
21
|
+
snippet: `discount = subtotal * 0.15 if tier == "gold" else 0`
|
|
22
|
+
note: The same tier→rate table is already in `domain/tiers.py:18`; a rate change now has two homes. Call the existing lookup instead of re-expressing it here.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Fields: `cell` (`target × axis`, exactly as the census wrote it — the axis is the rule group, and there is never one cell per individual rule), `verdict` (`clean` | `finding`), `rule` (the specific rule this finding violates, spelled as its pack spells it; omitted on `clean`), `line` (a line **this diff changed**), `severity`, `snippet` (the quoted changed line), `note` (1–2 sentences: what is wrong plus a concrete fix).
|
|
26
|
+
|
|
27
|
+
There is no length budget. Dropping a real finding to stay brief is the failure this review exists to prevent, and a missing cell is re-dispatched as unfinished work, never read as a clean.
|
|
28
|
+
|
|
29
|
+
## Severity and points
|
|
30
|
+
|
|
31
|
+
| Severity | Meaning | Points |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `must-fix` | a defect, or a rule violation that will cost real work later — wrong behavior, a swallowed error, a duplicated decision, a self-mocked test that proves nothing | 3 |
|
|
34
|
+
| `should-fix` | a clear rule violation with no immediate breakage — a name that misleads, a function that fails the plain-English test, business logic in the wrong layer | 2 |
|
|
35
|
+
| `nit` | real but small; the fix is cheap and obviously better | 1 |
|
|
36
|
+
|
|
37
|
+
Grade the defect, not your confidence. If you are not confident, the verdict is `clean` — see the hedge test below.
|
|
38
|
+
|
|
39
|
+
## When `clean` is the right verdict
|
|
40
|
+
|
|
41
|
+
`clean` is a result, not a concession. Return it when:
|
|
42
|
+
|
|
43
|
+
- **The defect sits only on lines this diff did not change.** Findings anchor on changed lines; pre-existing warts are not this change's problem.
|
|
44
|
+
- **The rule does not apply to this cell.** The census over-includes on purpose, so cells where a rule turns out to be irrelevant are expected — that is exactly what a `clean` verdict records.
|
|
45
|
+
- **The change is mechanical for this rule.** A signature-only or type-annotation-only edit does not reopen a function's body-level rules; a function the diff neither added nor renamed does not reopen its name.
|
|
46
|
+
|
|
47
|
+
**The hedge test breaks a borderline `must-fix` or `should-fix`.** At those two severities the finding is one you would defend without qualifiers. If your draft note reaches for a softener that questions whether the point is worth raising at all — "marginal", "arguably", "if you are touching it anyway" — you are looking at a `nit` or at nothing: downgrade it or verdict `clean`, but never ship a hedged `should-fix`.
|
|
48
|
+
|
|
49
|
+
The test does not empty the `nit` tier. A `nit` says the defect is small because that is the tier's definition, not because the reviewer is hedging — it still names a concrete defect on a changed line and a concrete fix.
|
|
50
|
+
|
|
51
|
+
## Every finding carries its fix
|
|
52
|
+
|
|
53
|
+
A finding without a fix is a complaint. Specifically:
|
|
54
|
+
|
|
55
|
+
- **Readability findings** carry a pseudocode sketch — 4 to 6 lines that convey the intended reading experience, not a full refactor. If the sketch does not make a reader think "yes, that reads like prose", rewrite the sketch.
|
|
56
|
+
- **Naming findings** carry a concrete alternative name, and one clause saying what the new name encodes that the old one did not.
|
|
57
|
+
- **Structural findings** name the destination: which layer, module, or existing symbol the code should move to or call.
|
|
58
|
+
|
|
59
|
+
## Test files
|
|
60
|
+
|
|
61
|
+
Loose typing in test files (`any`, dynamic casts, untyped fixtures) is a **typing** carve-out only — never flag it on typing grounds alone. It stops being a carve-out the moment the cast is the vehicle for another violation: reaching into privates, or stubbing the unit under test. The self-mock rules apply through the cast exactly as they do without it — what is judged is the behavior, not the cast.
|
|
62
|
+
|
|
63
|
+
## The report
|
|
64
|
+
|
|
65
|
+
Merged by the orchestrator, written to `reviewPath`. Sections, in this order, empty ones omitted except `Coverage` and `Score`:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
## Coverage
|
|
69
|
+
## Must-fix
|
|
70
|
+
## Should-fix
|
|
71
|
+
## Nits
|
|
72
|
+
## Score
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- **Coverage** — one or two lines: N changed files → S `structural` / F `semantic` / T `state-and-tests` / G `general` cells, all verdicted; M files excluded with reasons; the applied packs, and any pack that was unavailable. The four counts are cell counts at the census's granularity — one cell per target per axis.
|
|
76
|
+
- **Findings** — each one opens with `` `path/to/file.py:42` `` + the verdict's `rule` name + severity + points, then the snippet as a blockquote, then the 1–2 sentence note with its fix.
|
|
77
|
+
- **Score** — every finding gets a row, and the table is emitted even when there are none, with a single total row reading 0:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
| # | Location | Rule | Severity | Points |
|
|
81
|
+
|---|---|---|---|---|
|
|
82
|
+
| 1 | `src/orders/pricing.py:42` | DRY | must-fix | 3 |
|
|
83
|
+
| | | | **Total** | **3** |
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The report's prose is written in Korean. Paths, identifiers, rule names, and quoted code stay verbatim.
|
package/src/cli-registry.mjs
CHANGED
|
@@ -145,6 +145,13 @@ export const COMMAND_REGISTRY = [
|
|
|
145
145
|
category: "introspection",
|
|
146
146
|
summary: ["Resolve a bare task-id to candidate task-keys from the catalog"],
|
|
147
147
|
},
|
|
148
|
+
{
|
|
149
|
+
name: "code-review",
|
|
150
|
+
module: "./commands/inspect/code-review.mjs",
|
|
151
|
+
export: "run",
|
|
152
|
+
category: "introspection",
|
|
153
|
+
summary: ["Resolve a code review's diff range and result file path"],
|
|
154
|
+
},
|
|
148
155
|
{
|
|
149
156
|
name: "set-work-status",
|
|
150
157
|
module: "./commands/inspect/set-work-status.mjs",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra code-review — resolve a code review's diff range and result path
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
okstra code-review target --task-key <k> --stage <N> [--project-root <dir>] [--cwd <dir>] --json
|
|
7
|
+
okstra code-review target --branch <name> [--base <ref>] [--date <YYYY-MM-DD>] [--project-root <dir>] --json
|
|
8
|
+
|
|
9
|
+
Output: JSON { ok, mode, worktreePath, branch, baseCommit, headCommit,
|
|
10
|
+
reviewPath, round }. Stage mode reads the base commit from the stage
|
|
11
|
+
registry row; branch mode falls back to the merge-base with the default
|
|
12
|
+
branch. An empty worktreePath means the review reads the branch ref
|
|
13
|
+
instead of a checked-out tree. This is read-only — it never creates
|
|
14
|
+
directories or files.
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
export async function run(args) {
|
|
18
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
19
|
+
process.stdout.write(USAGE);
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
const [subcommand, ...rest] = args;
|
|
23
|
+
if (subcommand !== "target") {
|
|
24
|
+
process.stderr.write(
|
|
25
|
+
`error: unknown subcommand '${subcommand ?? ""}'\n\n${USAGE}`,
|
|
26
|
+
);
|
|
27
|
+
return 2;
|
|
28
|
+
}
|
|
29
|
+
const result = await runPythonModule({
|
|
30
|
+
module: "okstra_ctl.code_review_target",
|
|
31
|
+
args: rest,
|
|
32
|
+
stdio: "inherit-stdout",
|
|
33
|
+
});
|
|
34
|
+
return result.code;
|
|
35
|
+
}
|