okstra 0.138.0 → 0.139.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.
Files changed (28) hide show
  1. package/README.md +1 -0
  2. package/docs/architecture.md +4 -4
  3. package/docs/cli.md +4 -3
  4. package/docs/for-ai/README.md +2 -0
  5. package/docs/for-ai/skills/okstra-code-review.md +57 -0
  6. package/docs/project-structure-overview.md +5 -0
  7. package/package.json +1 -1
  8. package/runtime/BUILD.json +2 -2
  9. package/runtime/prompts/coding-preflight/architectures/hexagonal.md +34 -0
  10. package/runtime/prompts/coding-preflight/clean-code.md +54 -0
  11. package/runtime/prompts/coding-preflight/languages/javascript-typescript.md +11 -0
  12. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  13. package/runtime/prompts/profiles/_implementation-diff-review.md +8 -3
  14. package/runtime/prompts/profiles/_implementation-self-check.md +2 -1
  15. package/runtime/prompts/profiles/_implementation-verifier.md +12 -3
  16. package/runtime/prompts/profiles/implementation-planning.md +1 -0
  17. package/runtime/prompts/profiles/release-handoff.md +2 -1
  18. package/runtime/python/okstra_ctl/code_review_paths.py +64 -0
  19. package/runtime/python/okstra_ctl/code_review_target.py +152 -0
  20. package/runtime/python/okstra_ctl/worktree.py +3 -2
  21. package/runtime/python/okstra_project/__init__.py +2 -0
  22. package/runtime/python/okstra_project/state.py +142 -0
  23. package/runtime/skills/okstra-code-review/SKILL.md +180 -0
  24. package/runtime/skills/okstra-code-review/references/census-rules.md +100 -0
  25. package/runtime/skills/okstra-code-review/references/review-calibration.md +86 -0
  26. package/src/cli-registry.mjs +7 -0
  27. package/src/commands/inspect/code-review.mjs +35 -0
  28. package/src/lib/skill-catalog.mjs +1 -0
@@ -0,0 +1,152 @@
1
+ """코드 리뷰가 무엇을 읽고 결과 파일을 어디에 쓰는지 해소한다.
2
+
3
+ 두 모드를 한 진입점에 모아 스킬이 base 커밋도, 리뷰 파일 경로도, 회차도 다시
4
+ 유도하지 않게 한다. stage 모드는 read side 에 그대로 위임하고, branch 모드만
5
+ 여기서 해소한다 — caller 가 base 를 지정하지 않으면 기본 브랜치와의
6
+ merge-base 를 쓴다. 이 모듈은 인자 검증과 JSON 정형화만 하고, 경로·회차 조립은
7
+ code_review_paths 가, stage 규칙은 code_review_target_snapshot 이 소유한다.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import re
14
+ import subprocess
15
+ import sys
16
+ from datetime import date
17
+ from pathlib import Path
18
+
19
+ from okstra_project import (
20
+ ResolverError,
21
+ StateError,
22
+ code_review_target_snapshot,
23
+ resolve_project_root,
24
+ )
25
+
26
+ from .code_review_paths import branch_review_dir, next_branch_review
27
+
28
+ DEFAULT_BRANCH_CANDIDATES = ("main", "master")
29
+
30
+ _DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
31
+
32
+
33
+ def _git_out(repo: Path, *args: str) -> str:
34
+ result = subprocess.run(
35
+ ["git", "-C", str(repo), *args],
36
+ capture_output=True,
37
+ text=True,
38
+ check=False,
39
+ )
40
+ return result.stdout.strip() if result.returncode == 0 else ""
41
+
42
+
43
+ def _default_branch_ref(repo: Path) -> str:
44
+ """merge-base 를 잴 기준 ref — origin/HEAD 가 있으면 그것, 없으면 관례 이름."""
45
+ remote_head = _git_out(repo, "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD")
46
+ if remote_head:
47
+ return remote_head
48
+ for candidate in DEFAULT_BRANCH_CANDIDATES:
49
+ if _git_out(repo, "rev-parse", "--verify", "--quiet", candidate):
50
+ return candidate
51
+ return ""
52
+
53
+
54
+ def _merge_base(repo: Path, branch: str) -> str:
55
+ default_ref = _default_branch_ref(repo)
56
+ merge_base = _git_out(repo, "merge-base", default_ref, branch) if default_ref else ""
57
+ if not merge_base:
58
+ raise StateError(
59
+ f"no merge-base for {branch!r} against the default branch — "
60
+ "pass --base explicitly",
61
+ stage="base_unresolved",
62
+ )
63
+ return merge_base
64
+
65
+
66
+ def _review_date(raw: str) -> str:
67
+ """branch 결과 파일명에 쓸 날짜.
68
+
69
+ next_branch_review 는 넘겨받은 문자열을 그대로 파일명에 쓰지만 되읽을 때는
70
+ `YYYY-MM-DD` 만 인식한다. 어긋난 날짜를 통과시키면 회차가 늘 1 로 돌아가
71
+ 직전 리뷰 파일을 덮어쓴다.
72
+ """
73
+ if not raw:
74
+ return date.today().isoformat()
75
+ if not _DATE_RE.match(raw):
76
+ raise StateError(f"--date must be YYYY-MM-DD, got {raw!r}", stage="args")
77
+ return raw
78
+
79
+
80
+ def _branch_snapshot(repo: Path, branch: str, base: str, review_date: str) -> dict:
81
+ head = _git_out(repo, "rev-parse", branch)
82
+ if not head:
83
+ raise StateError(f"branch {branch!r} not found", stage="branch_missing")
84
+ base_commit = base or _merge_base(repo, branch)
85
+ review_path, seq = next_branch_review(branch_review_dir(repo, branch), review_date)
86
+ return {
87
+ "mode": "branch",
88
+ "worktreePath": str(repo),
89
+ "branch": branch,
90
+ "baseCommit": base_commit,
91
+ "headCommit": head,
92
+ "reviewPath": str(review_path),
93
+ "round": seq,
94
+ }
95
+
96
+
97
+ def _build_parser() -> argparse.ArgumentParser:
98
+ parser = argparse.ArgumentParser(
99
+ description="Resolve a code review's diff range and result file path."
100
+ )
101
+ parser.add_argument("--task-key", default="", help="project-id:task-group:task-id")
102
+ parser.add_argument("--stage", type=int, default=0, help="stage number to review")
103
+ parser.add_argument("--branch", default="", help="branch to review")
104
+ parser.add_argument("--base", default="", help="explicit base ref for branch mode")
105
+ parser.add_argument("--date", default="", help="YYYY-MM-DD for branch result naming")
106
+ parser.add_argument("--project-root", default="", help="explicit project root")
107
+ parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
108
+ parser.add_argument("--json", action="store_true", help="emit JSON (always on)")
109
+ return parser
110
+
111
+
112
+ def _resolve(args: argparse.Namespace, project_root: Path) -> dict:
113
+ if args.task_key:
114
+ if args.stage < 1:
115
+ raise StateError("--task-key needs --stage <N>, N >= 1", stage="args")
116
+ return code_review_target_snapshot(project_root, args.task_key, args.stage)
117
+ if args.branch:
118
+ return _branch_snapshot(
119
+ project_root, args.branch, args.base, _review_date(args.date)
120
+ )
121
+ raise StateError("pass either --task-key --stage or --branch", stage="args")
122
+
123
+
124
+ def main(argv: list[str] | None = None) -> int:
125
+ args = _build_parser().parse_args(argv)
126
+ try:
127
+ project_root = resolve_project_root(
128
+ explicit_root=args.project_root, cwd=args.cwd
129
+ )
130
+ except ResolverError as exc:
131
+ print(json.dumps({"ok": False, "stage": "resolve", "reason": str(exc)}))
132
+ return 2
133
+
134
+ try:
135
+ snapshot = _resolve(args, Path(project_root))
136
+ except StateError as exc:
137
+ print(json.dumps({
138
+ "ok": False,
139
+ "stage": exc.stage or "target",
140
+ "reason": str(exc),
141
+ }))
142
+ return 1
143
+
144
+ print(json.dumps(
145
+ {"ok": True, "projectRoot": str(project_root), **snapshot},
146
+ ensure_ascii=False, indent=2,
147
+ ))
148
+ return 0
149
+
150
+
151
+ if __name__ == "__main__":
152
+ raise SystemExit(main(sys.argv[1:]))
@@ -57,8 +57,9 @@ from .seeding import (
57
57
  # worktree at provision time. Symlinks (not copies) so every task sees the
58
58
  # live shared state and disk/CPU cost stays near zero; the trade-off is
59
59
  # that any write through the link reaches the main worktree, which is
60
- # acceptable because okstra only writes inside its own task-scoped
61
- # subdirectory (e.g. `.okstra/tasks/<task-id>/runs/...`).
60
+ # acceptable because okstra writes only to paths it owns: its task-scoped
61
+ # subdirectory (e.g. `.okstra/tasks/<task-id>/runs/...`) and, for a
62
+ # branch-mode code review, `.project-docs/code-reviews/<branch>/`.
62
63
  #
63
64
  # Override precedence (most-specific first):
64
65
  # 1. `OKSTRA_WORKTREE_SYNC_DIRS` env var — colon-separated list, REPLACES
@@ -28,6 +28,7 @@ from .resolver import (
28
28
  )
29
29
  from .state import (
30
30
  StateError,
31
+ code_review_target_snapshot,
31
32
  find_task_root,
32
33
  list_project_tasks,
33
34
  parse_task_key,
@@ -53,6 +54,7 @@ __all__ = [
53
54
  "StateError",
54
55
  "TASKS_RELATIVE",
55
56
  "TASK_CATALOG_RELATIVE",
57
+ "code_review_target_snapshot",
56
58
  "discovery_dir",
57
59
  "find_task_root",
58
60
  "list_project_tasks",
@@ -23,6 +23,7 @@ from __future__ import annotations
23
23
 
24
24
  import json
25
25
  import re
26
+ import subprocess
26
27
  from pathlib import Path
27
28
  from typing import Optional
28
29
 
@@ -404,3 +405,144 @@ def stage_map_read_side_snapshot(project_root: Path, task_key: str) -> dict:
404
405
  "stages": stages,
405
406
  "doneStages": done,
406
407
  }
408
+
409
+
410
+ def code_review_target_snapshot(
411
+ project_root: Path, task_key: str, stage: int
412
+ ) -> dict:
413
+ """stage 코드 리뷰가 무엇을 읽고 결과를 어디에 쓰는지 해소한다.
414
+
415
+ stage_map_read_side_snapshot 과 같은 결: caller 는 registry 행도, run-root
416
+ 레이아웃도, stage base 규칙도 보지 않는다. stage_targets 의
417
+ StageTargetError 도 StateError 로 바꿔 던진다 — caller 가 그 예외를 잡으려고
418
+ okstra_ctl 을 import 하지 않도록.
419
+
420
+ base 는 stage worktree 를 만들 때 registry 행에 적힌 `base_ref` 다 — 그
421
+ stage 가 실제로 갈라져 나온 커밋. 리뷰 시점에 규칙으로 다시 계산하면
422
+ 표류한다: 다중의존 stage 는 오늘의 task-key worktree HEAD 를 받게 되어
423
+ whole-task final-verification 이 stage 들을 병합한 뒤에는 base..head 구간이
424
+ 비거나 뒤집히고, 단일의존 stage 는 선행 stage 가 재실행되면 어긋난다.
425
+ """
426
+ from okstra_ctl import code_review_paths, worktree_registry
427
+ from okstra_ctl.implementation_outcome import load_stage_map
428
+
429
+ identity = resolve_task_identity(project_root, task_key)
430
+ task_root = Path(identity["taskRoot"])
431
+ stages = load_stage_map(task_root, identity["manifest"])
432
+ selected = next((s for s in stages if s["stage_number"] == stage), None)
433
+ if selected is None:
434
+ raise StateError(
435
+ f"stage {stage} is not in the Stage Map for {task_key}",
436
+ stage="stage_missing",
437
+ )
438
+
439
+ coords = (identity["projectId"], identity["taskGroup"], identity["taskId"])
440
+ stage_row = worktree_registry.get_stage_row(*coords, stage) or {}
441
+ worktree_view = _stage_worktree_view(project_root, stage_row, stage)
442
+ base_commit = stage_row.get("base_ref") or _legacy_stage_base_commit(
443
+ identity, stages, selected
444
+ )
445
+ review_path, round_no = code_review_paths.next_stage_review(
446
+ code_review_paths.stage_review_dir(
447
+ project_root, identity["taskGroup"], identity["taskId"]
448
+ ),
449
+ stage,
450
+ )
451
+ return {
452
+ "mode": "stage",
453
+ "taskKey": identity["taskKey"],
454
+ "taskRoot": identity["taskRoot"],
455
+ "stage": stage,
456
+ **worktree_view,
457
+ "baseCommit": base_commit,
458
+ "reviewPath": str(review_path),
459
+ "round": round_no,
460
+ }
461
+
462
+
463
+ def _legacy_stage_base_commit(
464
+ identity: dict, stages: list[dict], selected: dict
465
+ ) -> str:
466
+ """`base_ref` 를 남기지 않은 옛 registry 행의 base 를 규칙에서 되살린다.
467
+
468
+ provision 이 base_ref 를 기록하기 전에 만들어진 stage 행에만 쓰인다. 규칙
469
+ 자체는 stage_targets 가 소유하며 여기서 다시 유도하지 않는다.
470
+ """
471
+ from okstra_ctl import stage_targets, worktree_registry
472
+ from okstra_ctl.paths import RunRef
473
+
474
+ coords = (identity["projectId"], identity["taskGroup"], identity["taskId"])
475
+ plan_run_root = RunRef.from_task_root(
476
+ Path(identity["taskRoot"]), "implementation-planning"
477
+ ).run_dir
478
+ lifecycle = stage_targets.read_stage_lifecycle_snapshot(
479
+ stages, plan_run_root, recover_from_carry=True
480
+ )
481
+ # The anchor and the multi-dep candidate base must come from the task-key
482
+ # worktree HEAD, where stage work accumulates — not from the invocation
483
+ # cwd, which may sit on an older or unrelated commit.
484
+ task_entry = worktree_registry.lookup(*coords)
485
+ task_worktree = Path(
486
+ (task_entry.worktree_path if task_entry else "") or identity["projectRoot"]
487
+ )
488
+ anchor = worktree_registry.get_implementation_base(*coords) or ""
489
+ try:
490
+ return stage_targets.resolve_stage_base_commit(
491
+ selected,
492
+ lifecycle.done_rows,
493
+ anchor_base_commit=anchor,
494
+ candidate_base=_git_out(task_worktree, "rev-parse", "HEAD"),
495
+ project_root=task_worktree,
496
+ plan_run_root=plan_run_root,
497
+ )
498
+ except stage_targets.StageTargetError as exc:
499
+ raise StateError(
500
+ f"stage {selected['stage_number']} has no recorded base_ref and its "
501
+ f"base could not be derived: {exc}",
502
+ stage="stage_base_unresolved",
503
+ ) from exc
504
+
505
+
506
+ def _stage_worktree_view(project_root: Path, stage_row: dict, stage: int) -> dict:
507
+ """한 stage 의 worktree 경로 / 브랜치 / head 커밋.
508
+
509
+ 철거된 stage worktree 는 오류가 아니다: whole-task final-verification 이
510
+ 디렉터리를 지워도 브랜치는 남고, 리뷰가 필요한 커밋은 그 브랜치에 그대로
511
+ 있다.
512
+ """
513
+ branch = stage_row.get("branch") or ""
514
+ if not branch:
515
+ raise StateError(
516
+ f"stage {stage} has no registry branch — review it in branch mode",
517
+ stage="stage_branch_missing",
518
+ )
519
+ worktree_path = ""
520
+ if stage_row.get("status") == "active" and stage_row.get("worktree_path"):
521
+ candidate = Path(stage_row["worktree_path"])
522
+ if candidate.is_dir():
523
+ worktree_path = str(candidate)
524
+ head_source = Path(worktree_path) if worktree_path else Path(project_root)
525
+ head_ref = "HEAD" if worktree_path else branch
526
+ head_commit = _git_out(head_source, "rev-parse", head_ref)
527
+ if not head_commit:
528
+ raise StateError(
529
+ f"stage {stage} head could not be resolved: branch {branch!r} is "
530
+ f"not readable from {head_source}",
531
+ stage="stage_head_unresolved",
532
+ )
533
+ return {
534
+ "worktreePath": worktree_path,
535
+ "branch": branch,
536
+ "headCommit": head_commit,
537
+ }
538
+
539
+
540
+ def _git_out(repo: Path, *args: str) -> str:
541
+ """git 호출의 stdout. 실패는 빈 문자열 — 호출자가 부재를 판단한다."""
542
+ result = subprocess.run(
543
+ ["git", "-C", str(repo), *args],
544
+ capture_output=True,
545
+ text=True,
546
+ check=False,
547
+ )
548
+ return result.stdout.strip() if result.returncode == 0 else ""
@@ -0,0 +1,180 @@
1
+ ---
2
+ name: okstra-code-review
3
+ description: >-
4
+ Use when the user wants changed code reviewed and the result written to a
5
+ file — either an okstra task's implementation stage, or any branch
6
+ unrelated to an okstra run. The tell is a review request over a diff:
7
+ "review this stage", "review my branch", "code review", "leave the review
8
+ in a file". The orchestrator censuses the diff into an explicit worklist,
9
+ four parallel reviewers return a verdict for every cell against this
10
+ project's coding-preflight rules, and a coverage audit re-dispatches any
11
+ gap. NOT for writing a PR body (okstra-pr-gen), starting a run
12
+ (okstra-run), or inspecting a finished task (okstra-inspect).
13
+ ---
14
+
15
+ # OKSTRA Code Review
16
+
17
+ Review what a diff changed — one okstra implementation stage, or any branch — against this project's coding-preflight rules, and leave the result as a file under the project.
18
+
19
+ ## What makes this one-run-complete
20
+
21
+ A reviewer left to build its own worklist enumerates the diff differently every run, so every run finds different things. Here the worklist — the **census** — is built **once, by you, deterministically**, before any reviewer is dispatched. Reviewers receive their slice as input; they never rebuild, reinterpret, or extend it.
22
+
23
+ Each reviewer returns a **verdict for every cell it was given** (`clean`, or findings), so a skipped cell is visible instead of silently absent, and Step 3.5 re-dispatches every cell that came back without one. Same census + full verdicts = the same process every run.
24
+
25
+ ## Rules
26
+
27
+ The rules themselves are **not in this skill**. They live in this project's coding-preflight pack — `overview.md` (core principles plus the three-stage resource routing), `clean-code.md`, and the routed `languages/` / `frameworks/` / `architectures/` resources. Step 2 decides which of them apply; reviewers read them from the absolute paths in their brief. Never restate a rule in a brief or in the report — point at it.
28
+
29
+ This skill owns review calibration only:
30
+
31
+ - `references/census-rules.md` — how a diff becomes cells: the four axes, pack allocation, exclusions, and the two completion criteria.
32
+ - `references/review-calibration.md` — the per-cell verdict format, severity definitions and points, when `clean` is the correct verdict, and the report's section order.
33
+
34
+ ## Step 0 — Preflight
35
+
36
+ <!-- BEGIN FRAGMENT: bash-invocation-rule -->
37
+ Run one Bash tool call, starting with the literal token `okstra` (never wrapped in `if`/`eval`/`export`/`$(...)`/`VAR=...`/`||`/`&&`/`npx` — a non-literal leading token defeats the `Bash(okstra:*)` permission match):
38
+ <!-- END FRAGMENT: bash-invocation-rule -->
39
+
40
+ ```bash
41
+ okstra preflight --runtime claude-code --json
42
+ ```
43
+
44
+ Branch on the stdout JSON:
45
+ - `ok: true` → carry `projectRoot` as a literal string; every later step is anchored on it.
46
+ - `ok: false` → the check only sees the cwd of the Bash call, so a project that is not the cwd reads as missing setup. Ask whether the user pointed at a specific project directory; if they did, re-run targeting it: `okstra preflight --runtime claude-code --cwd <that-dir> --json` (`--cwd` is the sanctioned way to target a project — a leading `cd` would break the permission match). Only if this **also** returns `ok:false` do you tell the user: "this project has no okstra setup. Run `/okstra-setup` first." Then stop.
47
+
48
+ <!-- BEGIN FRAGMENT: preflight-outdated-cli -->
49
+ If the call fails with `unknown command: preflight`, the `okstra` binary on PATH predates this skill — tell the user to update it (`npm i -g okstra@latest`), then stop (`/okstra-setup` does not update the binary).
50
+ <!-- END FRAGMENT: preflight-outdated-cli -->
51
+
52
+ <!-- BEGIN FRAGMENT: python-bootstrap-note -->
53
+ Every subsequent `okstra <subcmd>` call self-bootstraps its Python path, so this skill never needs `okstra paths --shell` / `export PYTHONPATH=...`.
54
+ <!-- END FRAGMENT: python-bootstrap-note -->
55
+
56
+ ## Step 1 — Resolve the review target
57
+
58
+ Pick the mode from what the user actually gave you:
59
+
60
+ - an okstra task token (a task-id like `DEV-9184`, or a full `project-id:task-group:task-id` key) → **stage mode**
61
+ - a branch name → **branch mode**
62
+ - neither → ask which of the two first. Do not guess a mode.
63
+
64
+ **Stage mode.**
65
+
66
+ 1. `okstra resolve-task-key <token> --project-root <projectRoot> --json` → branch on `matches[]`: **0** → report the task cannot be found and stop; **1** → take that entry's `taskKey`; **N** → list the candidates (with `updatedAt`) and ask via a 3-option picker (1–2 recommendations + `Enter directly`).
67
+ 2. `okstra stage-map <taskKey> --project <projectRoot> --json` → `stages[]` (`stage_number`, `title`, `depends_on`) and `doneStages[]`.
68
+ 3. Choose the stage with a 3-option picker — recommend the highest done stage (the work that just finished) first, a second plausible stage if there is one, and `Enter directly` last. Pick silently only when exactly one stage exists.
69
+
70
+ **Branch mode.**
71
+
72
+ 1. If the user named no branch, list `git -C <projectRoot> branch --format='%(refname:short)'` and offer a 3-option picker with the current branch as the first recommendation.
73
+ 2. Detached HEAD — `git -C <projectRoot> symbolic-ref --quiet HEAD` prints nothing — is refused: ask the user to name a branch. Do not review a bare commit.
74
+
75
+ **Then call the target CLI.** One call resolves everything the rest of this skill needs; the only reason to call it again is a base the user overrides in the confirmation step below.
76
+
77
+ ```bash
78
+ okstra code-review target --task-key <taskKey> --stage <N> --project-root <projectRoot> --json
79
+ okstra code-review target --branch <name> [--base <ref>] --project-root <projectRoot> --json
80
+ ```
81
+
82
+ `ok: true` carries `projectRoot`, `mode`, `worktreePath`, `branch`, `baseCommit`, `headCommit`, `reviewPath`, `round`; stage mode adds `taskKey`, `taskRoot`, `stage`. Carry every field verbatim into the later steps — none of them is recomputed anywhere below.
83
+
84
+ `ok: false` carries `stage` (a failure code) and `reason`. Report both and stop, unless the Exceptions table names that case.
85
+
86
+ **You never derive the base.** Pass `--base <ref>` only when the user named one; otherwise the CLI resolves it. `baseCommit` is a **ref, not necessarily a commit id** — a caller-supplied `--base` passes through verbatim — so use it as given in `git diff <baseCommit>..<headCommit>` and never present it as "commit `<sha>`".
87
+
88
+ **Where to run git.** Use `worktreePath` when it is non-empty; otherwise run git in `projectRoot` and read the stage's `branch` ref. An empty `worktreePath` does **not** mean the stage is gone: a completed stage's registry row is `released`, so the field is empty even when the directory is still on disk. Either way the commits are on the branch.
89
+
90
+ **Then show the base and confirm it — branch mode and stage mode alike.** The CLI's answer is a recommendation the user has not seen yet, and a base nobody looked at is how unrelated commits slip into a review unnoticed. Print `baseCommit` verbatim next to `git -C <workdir> log -1 --oneline <baseCommit>` so the commit it names is legible, then ask with a 3-option picker:
91
+
92
+ 1. `Use <baseCommit>` — **the recommendation**, the value the CLI returned.
93
+ 2. `Show what this range covers` — print `git -C <workdir> log --oneline <baseCommit>..<headCommit>`, then ask the same three options again.
94
+ 3. `Enter directly` — always last: the user types a ref, and you call the target CLI a second time with `--base <that ref>`. Every field from the second call replaces the first call's, including `reviewPath` and `round`.
95
+
96
+ ## Step 2 — Census the diff
97
+
98
+ 1. **Read `references/census-rules.md`** (next to this file) and follow it. It owns the cells; this step owns only the inputs and what you print.
99
+ 2. Collect the diff from the work directory chosen in Step 1 — `git -C <workdir> diff --name-status <baseCommit>..<headCommit>` for the file list, and `git -C <workdir> diff <baseCommit>..<headCommit>` for the hunks. Before trusting that range, run `git -C <workdir> rev-list --count <headCommit>..<baseCommit>`: anything but `0` is the rewritten-history case in the Exceptions table. An empty diff skips Steps 3–3.5 (also in Exceptions).
100
+ 3. **Route the packs once, here — and fix both absolute paths the briefs carry.** `okstra paths --field home` prints the okstra home; read `<okstraHome>/prompts/coding-preflight/overview.md` and walk all three stages of its routed resource selection over the changed-file list. The result is the applied pack list — the absolute paths reviewers will read. This routing happens exactly once per run; no reviewer repeats it.
101
+
102
+ The second path is this skill's own calibration file. A subagent has no "next to this file" coordinate, so the brief must spell it out: the installed skill home is `~/.claude/skills/okstra-code-review/`, making the literal path `~/.claude/skills/okstra-code-review/references/review-calibration.md` — the same string whether the skill was copied in or dev-linked. Carry it, together with the pack list, into every Step 3 brief.
103
+ 4. Build the cells per `references/census-rules.md` and print, in this response: one cell table per axis, the exclusion list with a reason on every entry, and the applied pack list.
104
+ 5. Restate both completion criteria and show they hold: censused files + exclusions = files in the diff, and every hunk maps to a censused function or to file-level code.
105
+
106
+ **Membership is mechanical; judgment only ever decides a cell's verdict.** Never drop a cell because you doubt the rule applies to it — that doubt is the reviewer's answer, and its name is `clean`. Over-inclusion costs one line; a judged exclusion is the run-to-run variance this census exists to remove.
107
+
108
+ A large census is never truncated. Report the cell count and confirm before dispatching — a silent cut is a false "I looked at everything" signal.
109
+
110
+ ## Step 3 — Dispatch four reviewers in parallel
111
+
112
+ Send all four Agent calls in **one message**, `subagent_type: "general-purpose"`. Every brief carries:
113
+
114
+ - the diff, plus the work directory path so the reviewer can read whole files for context
115
+ - the project layout in one or two lines (where source, tests, and — if the routing found one — domain / ports / adapters live)
116
+ - **its own axis's cell list**, verbatim from the census
117
+ - the absolute paths of the packs its axis reads (from step 2's routing)
118
+ - the calibration path, written out in full as Step 2 fixed it — `~/.claude/skills/okstra-code-review/references/review-calibration.md`. The verdict format, the severity points, and the rules for a legitimate `clean` are defined there, not in the brief; a reviewer that cannot open this file cannot return a usable verdict, so never hand it a relative path or a "next to the skill" hint
119
+
120
+ Each axis is **one rule group**, so a cell is `target × <axis>` — never `target × <individual rule>`. The reviewer names the specific rule it found violated inside the verdict's `rule` field, and one cell may carry findings from several rules of its group.
121
+
122
+ Axis scope — the `Reads` column names that group's rules, and a brief never restates them; the bodies are in the packs:
123
+
124
+ | Axis | Cell | Reads |
125
+ |---|---|---|
126
+ | `structural` | non-test source file × `structural` | core principles 1, 2, 3, 6; `clean-code.md` DRY / KISS / SOLID / YAGNI; the completion sweep's domain-literal and documented-fork items; plus the architecture pack when one was routed |
127
+ | `semantic` | function × `semantic` | core principles 4, 5; `clean-code.md` meaningful naming, functions-do-one-thing, the plain-English summary test, the 50-line cap, nesting depth, magic numbers, comments-explain-why |
128
+ | `state-and-tests` | non-test source file × `state-and-tests`, test file × `state-and-tests` | `clean-code.md` "Mutation and state boundaries" and "Testing discipline"; plus the routed language pack's self-mock signals |
129
+ | `general` | non-test source file × `general` | correctness bugs, swallowed errors, security, clear performance problems, core principle 7 (fix at the cause — nothing previously green edited to silence a failure); plus the routed language pack's language-specific traps |
130
+
131
+ Say it plainly in every brief: **the census is law** — work the cells exactly as given, return a verdict for every one, never re-derive the list. There is no length budget; dropping a finding to stay brief is the failure this skill exists to prevent.
132
+
133
+ ## Step 3.5 — Audit the coverage
134
+
135
+ Diff each reviewer's returned cells against the slice you handed it. Any cell without a verdict → re-dispatch **one gap-fill agent per axis**, carrying only the missing cells and the same brief. Repeat until every cell of every axis has a verdict.
136
+
137
+ A missing verdict is unfinished work, never an implicit `clean`. Do not start Step 4 while a single cell is unaccounted for.
138
+
139
+ ## Step 4 — Merge and write
140
+
141
+ 1. **Read `references/review-calibration.md`** (next to this file) and follow its report section — you are the one writing the file, and it fixes the Coverage sentence, the per-finding line format, the Score table columns, and the total row. The reviewers were given it for their verdicts; the report obeys it too.
142
+ 2. **Dedupe across axes.** The same defect surfaced by two axes stays once, under the rule that explains it best.
143
+ 3. **Severity is the reviewer's.** The merge concatenates and dedupes; it never re-grades. If a verdict looks wrong, the brief was wrong — improve the brief for next run and ship this one as returned.
144
+ 4. **Write the report to `reviewPath`** with the Write tool (it creates the parent directories). Frontmatter fields, in this order:
145
+
146
+ ```yaml
147
+ mode: stage | branch
148
+ taskKey: <task-key> # stage mode
149
+ branch: <branch> # branch mode
150
+ stage: <N> # stage mode only
151
+ round: <round>
152
+ baseCommit: <exactly as the CLI returned it>
153
+ headCommit: <headCommit>
154
+ packs: [<applied coding-preflight pack paths>]
155
+ generatedAt: <YYYY-MM-DD HH:MM>
156
+ ```
157
+
158
+ Body sections, in this order: `## Coverage`, `## Must-fix`, `## Should-fix`, `## Nits`, `## Score`. Empty severity sections are omitted; `Coverage` and `Score` are always present, and a review with no findings still emits the Score table with a total of 0.
159
+ 5. **In the session, print only** the `reviewPath`, the count per severity, and the score total. The file is the deliverable — do not replay the findings in chat.
160
+
161
+ ## Exceptions
162
+
163
+ | Situation | Handling |
164
+ |---|---|
165
+ | `preflight` returns `ok:false` | retry with `--cwd <dir>`; if that also fails, tell the user to run `/okstra-setup` first and stop |
166
+ | `unknown command: code-review` | the `okstra` binary predates this skill — tell the user to update it (`npm i -g okstra@latest`) and stop |
167
+ | `worktreePath` is empty | not a hard stop and not a missing stage: run git in `projectRoot` against the stage's `branch` ref and continue |
168
+ | `baseCommit` is not an ancestor of `headCommit` — `git -C <workdir> rev-list --count <headCommit>..<baseCommit>` returns a **non-zero** count, meaning a rebase or squash rewrote the history the stage was recorded against, and `<baseCommit>..<headCommit>` would drag predecessor work in backwards | code-review is read-only, so do not force a reconcile. Offer two options: review against the branch's current tip, or run `okstra git-reconcile` first and retry |
169
+ | the diff is empty | dispatch no reviewers; write the "no changes" report to `reviewPath` and stop. It is a normal report, not a free-form note: the same frontmatter, `## Coverage` reading "0 changed files → 0 cells on every axis, 0 files excluded" plus the applied packs, every severity section omitted, and `## Score` carrying the table with its single total row reading 0 |
170
+ | the census is large | never truncate — report the cell count and confirm before dispatching |
171
+ | detached HEAD (branch mode) | refuse and ask the user to name a branch |
172
+
173
+ ## Principles
174
+
175
+ - **Stay in the diff.** Every finding cites a line this diff changed. A cell whose only wart sits on untouched lines verdicts `clean` — pre-existing issues are not this change's problem.
176
+ - **Don't manufacture findings.** A census fully verdicted `clean` is a valid, useful result.
177
+ - **No finding without a fix.** Readability findings carry a pseudocode sketch; naming findings carry a concrete alternative name.
178
+ - **The census is law.** A reviewer that rebuilds its own worklist reintroduces exactly the run-to-run variance this skill exists to kill.
179
+ - **Every cell gets a verdict.** `clean` is a result, not an omission; the audit treats a gap as unfinished work.
180
+ - **The report prose is Korean.** Paths, identifiers, rule names, and quoted code stay verbatim.
@@ -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.