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.
- package/README.md +1 -1
- package/docs/architecture.md +12 -8
- package/docs/cli.md +7 -3
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-inspect.md +2 -2
- package/docs/for-ai/skills/okstra-user-response.md +2 -2
- package/docs/project-structure-overview.md +15 -9
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +9 -7
- package/runtime/agents/workers/codex-worker.md +9 -7
- package/runtime/agents/workers/grok-worker.md +6 -4
- package/runtime/agents/workers/kimi-worker.md +6 -4
- package/runtime/bin/okstra-antigravity-exec.sh +1 -340
- package/runtime/bin/okstra-claude-exec.sh +1 -178
- package/runtime/bin/okstra-codex-exec.sh +1 -467
- package/runtime/bin/okstra-provider-exec.py +165 -190
- package/runtime/bin/okstra-trace-cleanup.sh +14 -7
- package/runtime/bin/okstra-wrapper-status.py +26 -19
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/convergence.md +36 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +23 -1
- package/runtime/prompts/lead/plan-body-verification.md +9 -1
- package/runtime/prompts/lead/report-writer.md +1 -0
- package/runtime/prompts/lead/team-contract.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +9 -1
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/implementation-planning.md +5 -3
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +148 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +55 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +41 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +44 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +42 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +5 -1
- package/runtime/python/okstra_ctl/dispatch_state.py +10 -0
- package/runtime/python/okstra_ctl/domain/provider.py +5 -1
- package/runtime/python/okstra_ctl/domain/worker_exec.py +102 -0
- package/runtime/python/okstra_ctl/domain/worker_role.py +34 -0
- package/runtime/python/okstra_ctl/domain/worker_stream.py +261 -0
- package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
- package/runtime/python/okstra_ctl/report_html/common.py +71 -25
- package/runtime/python/okstra_ctl/report_html/models.py +5 -0
- package/runtime/python/okstra_ctl/report_html/render.py +1 -1
- package/runtime/python/okstra_ctl/report_html/run_usage.py +19 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -0
- package/runtime/python/okstra_ctl/report_views.py +44 -16
- package/runtime/python/okstra_ctl/stage_citations.py +52 -15
- package/runtime/python/okstra_ctl/user_response.py +45 -29
- package/runtime/python/okstra_ctl/wizard.py +13 -9
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -3
- package/runtime/python/okstra_ctl/worker_request.py +140 -0
- package/runtime/python/okstra_ctl/worker_runner.py +622 -0
- package/runtime/python/okstra_token_usage/collect.py +8 -1
- package/runtime/python/okstra_token_usage/report.py +42 -0
- package/runtime/python/okstra_token_usage/task_totals.py +88 -0
- package/runtime/schemas/final-report-v1.0.schema.json +70 -0
- package/runtime/schemas/final-report-v2.0.schema.json +90 -0
- package/runtime/skills/okstra-inspect/SKILL.md +1 -2
- package/runtime/skills/okstra-inspect/facets/logs.md +5 -5
- package/runtime/skills/okstra-inspect/facets/run-audit.md +3 -3
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-user-response/SKILL.md +15 -5
- package/runtime/templates/report-writer-prompt-preamble.md +1 -0
- package/runtime/templates/reports/html/assets/base.css +8 -4
- package/runtime/templates/reports/html/base.template.html +12 -6
- package/runtime/templates/reports/html/i18n/en.json +29 -6
- package/runtime/templates/reports/html/i18n/ko.json +29 -6
- package/runtime/templates/reports/html/macros/forms.html +9 -3
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +14 -19
- package/runtime/templates/reports/report.js +59 -26
- package/runtime/templates/reports/user-response.template.md +12 -8
- package/runtime/validators/validate-run.py +88 -7
- package/runtime/validators/validate_session_conformance.py +62 -1
- package/src/cli-registry.mjs +0 -7
- package/runtime/bin/okstra-wrapper-agy-stream.py +0 -61
- package/runtime/python/okstra_ctl/error_issue.py +0 -640
- package/runtime/python/okstra_ctl/issue_signals.py +0 -186
- package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
- 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
|
-
|
|
105
|
-
|
|
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,
|
|
686
|
-
"""plan 의 run 디렉토리 sibling ``user-responses/`` 에서
|
|
687
|
-
|
|
688
|
-
|
|
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,
|
|
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 =
|
|
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=
|
|
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=
|
|
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=
|
|
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)
|