okstra 0.164.0 → 0.165.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 (84) hide show
  1. package/README.md +1 -1
  2. package/docs/architecture.md +12 -8
  3. package/docs/cli.md +7 -3
  4. package/docs/for-ai/README.md +2 -2
  5. package/docs/for-ai/skills/okstra-inspect.md +2 -2
  6. package/docs/for-ai/skills/okstra-user-response.md +2 -2
  7. package/docs/project-structure-overview.md +15 -9
  8. package/package.json +1 -1
  9. package/runtime/BUILD.json +2 -2
  10. package/runtime/agents/workers/antigravity-worker.md +9 -7
  11. package/runtime/agents/workers/codex-worker.md +9 -7
  12. package/runtime/agents/workers/grok-worker.md +6 -4
  13. package/runtime/agents/workers/kimi-worker.md +6 -4
  14. package/runtime/bin/okstra-antigravity-exec.sh +1 -340
  15. package/runtime/bin/okstra-claude-exec.sh +1 -178
  16. package/runtime/bin/okstra-codex-exec.sh +1 -467
  17. package/runtime/bin/okstra-provider-exec.py +165 -190
  18. package/runtime/bin/okstra-trace-cleanup.sh +14 -7
  19. package/runtime/bin/okstra-wrapper-status.py +26 -19
  20. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  21. package/runtime/prompts/lead/convergence.md +36 -8
  22. package/runtime/prompts/lead/okstra-lead-contract.md +23 -1
  23. package/runtime/prompts/lead/plan-body-verification.md +9 -1
  24. package/runtime/prompts/lead/report-writer.md +1 -0
  25. package/runtime/prompts/lead/team-contract.md +3 -3
  26. package/runtime/prompts/profiles/_common-contract.md +9 -1
  27. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  28. package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
  29. package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
  30. package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
  31. package/runtime/prompts/profiles/implementation-planning.md +5 -3
  32. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +1 -1
  33. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
  34. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +148 -0
  35. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +55 -0
  36. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +41 -0
  37. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +44 -0
  38. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +42 -0
  39. package/runtime/python/okstra_ctl/dispatch_core.py +5 -1
  40. package/runtime/python/okstra_ctl/dispatch_state.py +10 -0
  41. package/runtime/python/okstra_ctl/domain/provider.py +5 -1
  42. package/runtime/python/okstra_ctl/domain/worker_exec.py +102 -0
  43. package/runtime/python/okstra_ctl/domain/worker_role.py +34 -0
  44. package/runtime/python/okstra_ctl/domain/worker_stream.py +261 -0
  45. package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
  46. package/runtime/python/okstra_ctl/report_html/common.py +71 -25
  47. package/runtime/python/okstra_ctl/report_html/models.py +5 -0
  48. package/runtime/python/okstra_ctl/report_html/render.py +1 -1
  49. package/runtime/python/okstra_ctl/report_html/run_usage.py +19 -0
  50. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -0
  51. package/runtime/python/okstra_ctl/report_views.py +44 -16
  52. package/runtime/python/okstra_ctl/stage_citations.py +52 -15
  53. package/runtime/python/okstra_ctl/user_response.py +45 -29
  54. package/runtime/python/okstra_ctl/wizard.py +13 -9
  55. package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -3
  56. package/runtime/python/okstra_ctl/worker_request.py +140 -0
  57. package/runtime/python/okstra_ctl/worker_runner.py +622 -0
  58. package/runtime/python/okstra_token_usage/collect.py +8 -1
  59. package/runtime/python/okstra_token_usage/report.py +42 -0
  60. package/runtime/python/okstra_token_usage/task_totals.py +88 -0
  61. package/runtime/schemas/final-report-v1.0.schema.json +70 -0
  62. package/runtime/schemas/final-report-v2.0.schema.json +90 -0
  63. package/runtime/skills/okstra-inspect/SKILL.md +1 -2
  64. package/runtime/skills/okstra-inspect/facets/logs.md +5 -5
  65. package/runtime/skills/okstra-inspect/facets/run-audit.md +3 -3
  66. package/runtime/skills/okstra-run/SKILL.md +1 -1
  67. package/runtime/skills/okstra-user-response/SKILL.md +15 -5
  68. package/runtime/templates/report-writer-prompt-preamble.md +1 -0
  69. package/runtime/templates/reports/html/assets/base.css +8 -4
  70. package/runtime/templates/reports/html/base.template.html +12 -6
  71. package/runtime/templates/reports/html/i18n/en.json +29 -6
  72. package/runtime/templates/reports/html/i18n/ko.json +29 -6
  73. package/runtime/templates/reports/html/macros/forms.html +9 -3
  74. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +14 -19
  75. package/runtime/templates/reports/report.js +59 -26
  76. package/runtime/templates/reports/user-response.template.md +12 -8
  77. package/runtime/validators/validate-run.py +88 -7
  78. package/runtime/validators/validate_session_conformance.py +62 -1
  79. package/src/cli-registry.mjs +0 -7
  80. package/runtime/bin/okstra-wrapper-agy-stream.py +0 -61
  81. package/runtime/python/okstra_ctl/error_issue.py +0 -640
  82. package/runtime/python/okstra_ctl/issue_signals.py +0 -186
  83. package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
  84. package/src/commands/inspect/error-issue.mjs +0 -27
@@ -101,8 +101,8 @@ from okstra_ctl.stage_map import (
101
101
  stage_map_records,
102
102
  )
103
103
  from okstra_ctl.user_response import (
104
- UserResponseApprovalRecord,
105
- parse_user_response_approval,
104
+ PlanDecisionRecord,
105
+ parse_plan_decision,
106
106
  )
107
107
  from okstra_ctl.workers import (
108
108
  ALLOWED_WORKERS,
@@ -682,23 +682,27 @@ def _approve_plan_in_place(plan_path: Path) -> None:
682
682
 
683
683
  def _find_html_approval_sidecar(
684
684
  plan_path: Path,
685
- ) -> Optional[tuple[Path, UserResponseApprovalRecord]]:
686
- """plan 의 run 디렉토리 sibling ``user-responses/`` 에서 APPROVAL 블록을
687
- 가진 sidecar 를 찾는다. source-report 파일명과 seq 가 plan 과 일치해야
688
- 하며, 복수면 mtime 최신을 택한다."""
685
+ ) -> Optional[tuple[Path, PlanDecisionRecord]]:
686
+ """plan 의 run 디렉토리 sibling ``user-responses/`` 에서 승인 판정을 담은
687
+ sidecar 를 찾는다. source-report 파일명과 seq 가 plan 과 일치해야 하며,
688
+ 복수면 mtime 최신을 택한다.
689
+
690
+ 승인이 아닌 판정(반려·재작업 요청)은 여기서 걸러진다 — 이 단계가 묻는 것은
691
+ "사용자가 이 plan 을 승인해 두었는가" 뿐이고, 반려 사유는 다음 planning
692
+ run 이 sidecar 를 통째로 읽어 처리한다."""
689
693
  responses_dir = plan_path.parent.parent / "user-responses"
690
694
  if not responses_dir.is_dir():
691
695
  return None
692
696
  m = re.search(r"-(\d+)\.md$", plan_path.name)
693
697
  plan_seq = m.group(1) if m else ""
694
- best: Optional[tuple[float, Path, UserResponseApprovalRecord]] = None
698
+ best: Optional[tuple[float, Path, PlanDecisionRecord]] = None
695
699
  for f in sorted(responses_dir.glob("user-response-*.md")):
696
700
  try:
697
701
  text = f.read_text(encoding="utf-8", errors="replace")
698
702
  except OSError:
699
703
  continue
700
- rec = parse_user_response_approval(text)
701
- if rec is None or rec.seq != plan_seq:
704
+ rec = parse_plan_decision(text)
705
+ if rec is None or not rec.approved or rec.seq != plan_seq:
702
706
  continue
703
707
  if Path(rec.source_report).name != plan_path.name:
704
708
  continue
@@ -81,6 +81,13 @@ def resolve_prompt_plan(
81
81
  raise ValueError("worker ID is required")
82
82
  if dispatch_kind.startswith("reverify-r"):
83
83
  return _plan("reverify")
84
+ # A critic pass keeps the full analysis contract — worker anchor headers, the
85
+ # audit sidecar, the packet boundary — but is exempt from the equality group.
86
+ # Its body is deliberately unlike the initial one (it asks for coverage gaps
87
+ # and unrequested work, not findings), so grouping it with the initial
88
+ # analysis prompts makes `validate_analysis_prompt_set` compare two prompts
89
+ # that are *supposed* to differ and fail every critic-enabled run.
90
+ analysis_equality_group = None if dispatch_kind == "critic" else "analysis-core"
84
91
  if task_type == "implementation" and not executor_worker_id:
85
92
  raise ValueError("implementation executor worker ID is required")
86
93
  if worker_id == "report-writer":
@@ -101,7 +108,7 @@ def resolve_prompt_plan(
101
108
  if task_type == "final-verification":
102
109
  return _plan(
103
110
  "analysis",
104
- equality_group="analysis-core",
111
+ equality_group=analysis_equality_group,
105
112
  packet_only=True,
106
113
  required_headers=FINAL_VERIFICATION_HEADERS,
107
114
  max_body_lines=96,
@@ -110,11 +117,11 @@ def resolve_prompt_plan(
110
117
  if task_type == "improvement-discovery":
111
118
  return _plan(
112
119
  "analysis",
113
- equality_group="analysis-core",
120
+ equality_group=analysis_equality_group,
114
121
  packet_only=True,
115
122
  required_headers=(GRILLING_LOG_HEADER,),
116
123
  )
117
- return _plan("analysis", equality_group="analysis-core", packet_only=True)
124
+ return _plan("analysis", equality_group=analysis_equality_group, packet_only=True)
118
125
 
119
126
 
120
127
  def resolve_prompt_plan_for_manifest(
@@ -0,0 +1,140 @@
1
+ """Assemble the request every provider strategy then takes on trust.
2
+
3
+ A strategy translates a ``WorkerExecRequest`` into its CLI's syntax and nothing
4
+ more: it does not resolve paths, does not decide what may be written, and does
5
+ not know which role it is running. Whoever builds the request answers all
6
+ three, so the answers live here — importable — rather than inside a dispatch
7
+ entrypoint where the next caller would have to copy them.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import subprocess
13
+ from pathlib import Path
14
+
15
+ from .domain.worker_exec import ExecutionPolicy, WorkerExecRequest
16
+ from .domain.worker_role import role_spec
17
+
18
+ # Carried over from the codex wrapper, where it was one provider's rule: cargo's
19
+ # package-cache flock and its registry live outside every workspace, so a
20
+ # verifier bounded to the tree alone fails each `cargo build/test/clippy` on the
21
+ # global lock. The rule follows the role rather than the provider now — every
22
+ # provider's verifier receives these — so the name no longer says codex. Set it
23
+ # to a colon-separated list of absolute paths to override, or to empty to
24
+ # disable.
25
+ VERIFIER_EXTRA_DIRS_ENV = "OKSTRA_VERIFIER_EXTRA_DIRS"
26
+
27
+ _VERIFIER_ROLE = "verifier"
28
+
29
+
30
+ def build_request(
31
+ *,
32
+ prompt_text: str,
33
+ model: str,
34
+ project_root: Path,
35
+ worktree_path: Path | None,
36
+ role: str,
37
+ idle_timeout_seconds: int,
38
+ ) -> WorkerExecRequest:
39
+ """One dispatch, with every value the strategies will not re-derive.
40
+
41
+ Paths are resolved here because nothing downstream resolves them: the
42
+ strategies hand what they are given straight to a CLI, so an unresolved
43
+ `..` or symlink would survive into the sandbox boundary itself.
44
+ """
45
+ root = project_root.resolve()
46
+ worktree = worktree_path.resolve() if worktree_path is not None else None
47
+ return WorkerExecRequest(
48
+ prompt_text=prompt_text,
49
+ model=model,
50
+ project_root=root,
51
+ worktree_path=worktree,
52
+ # Nobody is at the keyboard to answer an approval prompt, so the gate is
53
+ # open and the boundary comes from the write scope instead.
54
+ policy=ExecutionPolicy(
55
+ auto_approve=True, write_scope=write_scope(root, worktree, role)
56
+ ),
57
+ idle_timeout_seconds=idle_timeout_seconds,
58
+ )
59
+
60
+
61
+ def write_scope(
62
+ project_root: Path, worktree: Path | None, role: str
63
+ ) -> tuple[Path, ...]:
64
+ """What this worker may write, in the order the CLIs are told it.
65
+
66
+ The order is contract, not taste: strategies translate this tuple into
67
+ repeated `--add-dir` positionally, so it mirrors what the shell wrappers
68
+ claimed. The project root leads (the antigravity wrapper's first
69
+ `--add-dir`; codex names it with `-C` and its strategy skips the repeat),
70
+ then the stage tree, then the git directory that tree commits through.
71
+
72
+ The worktree belongs here as well as in the request: passing it as the
73
+ working directory alone produces an argv with no `--add-dir`, and the worker
74
+ then fails by not writing files rather than by erroring.
75
+ """
76
+ scope = [project_root]
77
+ if worktree is not None:
78
+ scope.append(worktree)
79
+ common_git_dir = git_common_dir(worktree)
80
+ if common_git_dir is not None:
81
+ scope.append(common_git_dir)
82
+ scope.extend(verifier_extra_dirs(role))
83
+ return tuple(scope)
84
+
85
+
86
+ def git_common_dir(worktree: Path) -> Path | None:
87
+ """The main repository's `.git`, which a linked worktree commits through.
88
+
89
+ A linked worktree keeps its index and refs under the main repo's `.git` and
90
+ shares its object database, so a worker bounded to the tree alone cannot
91
+ `git commit` from it. A directory that is no worktree at all simply has
92
+ nothing extra to grant.
93
+ """
94
+ try:
95
+ probe = subprocess.run(
96
+ ["git", "-C", str(worktree), "rev-parse", "--git-common-dir"],
97
+ capture_output=True,
98
+ text=True,
99
+ check=False,
100
+ )
101
+ except OSError:
102
+ return None
103
+ if probe.returncode != 0 or not probe.stdout.strip():
104
+ return None
105
+ candidate = Path(probe.stdout.strip())
106
+ if not candidate.is_absolute():
107
+ candidate = worktree / candidate
108
+ return candidate.resolve() if candidate.is_dir() else None
109
+
110
+
111
+ def verifier_extra_dirs(role: str) -> tuple[Path, ...]:
112
+ """Toolchain directories granted to the verifier role only.
113
+
114
+ Role decides this, not the provider, so it is assembled here rather than in
115
+ a strategy — and not in `WorkerRoleSpec`, which holds values that hold for
116
+ every machine while these paths exist only where the toolchain is.
117
+ """
118
+ if role != _VERIFIER_ROLE:
119
+ return ()
120
+ home = Path.home()
121
+ raw = os.environ.get(VERIFIER_EXTRA_DIRS_ENV, f"{home}/.cargo:{home}/.rustup")
122
+ candidates = (Path(entry) for entry in raw.split(":") if entry)
123
+ return tuple(path.resolve() for path in candidates if path.is_dir())
124
+
125
+
126
+ def idle_timeout(raw: str, role: str) -> int:
127
+ """The explicit budget, or the one the role's spec owns.
128
+
129
+ The default is not written here. The same 1500/600 pair used to live in
130
+ every wrapper and in the python entrypoint, and each copy could drift from
131
+ the policy. Raises ``ValueError`` on a malformed budget; the exit code that
132
+ failure earns belongs to the entrypoint, not to this module.
133
+ """
134
+ if not raw:
135
+ return role_spec(role).idle_timeout_seconds
136
+ if not raw.isdigit():
137
+ raise ValueError(
138
+ f"idle-timeout-seconds must be a non-negative integer: {raw!r}"
139
+ )
140
+ return int(raw)