okstra 0.163.2 → 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 +8 -6
- package/docs/architecture.md +24 -15
- package/docs/cli.md +15 -8
- 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 +22 -14
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +9 -7
- package/runtime/agents/workers/claude-worker.md +1 -0
- 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/lib/okstra/cli.sh +5 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- 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/bin/okstra.sh +87 -91
- package/runtime/prompts/lead/adapters/cmux.md +2 -2
- package/runtime/prompts/lead/convergence.md +36 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +24 -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 +6 -4
- package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
- package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +112 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +53 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +183 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +110 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +84 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +76 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +80 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/application/__init__.py +1 -0
- package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
- package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
- package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
- package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
- package/runtime/python/okstra_ctl/application/start_run.py +21 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
- package/runtime/python/okstra_ctl/dispatch_core.py +244 -29
- package/runtime/python/okstra_ctl/dispatch_state.py +27 -0
- package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
- package/runtime/python/okstra_ctl/domain/host.py +100 -0
- package/runtime/python/okstra_ctl/domain/provider.py +70 -0
- package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
- 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/entrypoints/__init__.py +1 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
- package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
- package/runtime/python/okstra_ctl/models.py +54 -269
- package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
- package/runtime/python/okstra_ctl/ports/host.py +32 -0
- package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
- package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
- package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
- package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
- package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
- package/runtime/python/okstra_ctl/render.py +145 -47
- 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/run.py +80 -58
- package/runtime/python/okstra_ctl/session.py +1 -1
- package/runtime/python/okstra_ctl/stage_citations.py +52 -15
- package/runtime/python/okstra_ctl/team.py +44 -32
- package/runtime/python/okstra_ctl/user_response.py +45 -29
- package/runtime/python/okstra_ctl/wizard.py +175 -73
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
- 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 +42 -7
- 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 +4040 -1066
- package/runtime/schemas/final-report-v2.0.schema.json +5673 -1412
- 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 +74 -29
- package/runtime/skills/okstra-user-response/SKILL.md +15 -5
- package/runtime/templates/implementation-worker-preamble.md +1 -1
- package/runtime/templates/report-writer-prompt-preamble.md +1 -0
- package/runtime/templates/reports/final-report.template.md +3 -3
- 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 +32 -7
- package/runtime/templates/reports/html/i18n/ko.json +32 -7
- 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/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +17 -22
- package/runtime/validators/validate-report-views.py +0 -39
- package/runtime/validators/validate-run.py +96 -14
- package/runtime/validators/validate_session_conformance.py +69 -3
- package/src/cli-registry.mjs +0 -7
- package/src/commands/execute/render-bundle.mjs +4 -4
- package/src/commands/execute/run.mjs +8 -25
- package/src/commands/execute/wizard.mjs +33 -13
- package/src/commands/lifecycle/doctor.mjs +10 -10
- package/src/commands/lifecycle/install.mjs +53 -30
- package/src/commands/lifecycle/preflight.mjs +14 -4
- package/src/lib/host-registry-client.mjs +176 -0
- package/src/lib/runtime-manifest.mjs +6 -8
- 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/python/okstra_ctl/lead_runtime.py +0 -115
- package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
- package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
- package/src/commands/inspect/error-issue.mjs +0 -27
- package/src/lib/runtime-readiness.mjs +0 -90
- package/src/lib/runtime-resolver.mjs +0 -123
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
"""이슈 판정 신호 — 원본(익명화 전) 레코드에서만 계산되는 순수 함수.
|
|
2
|
-
|
|
3
|
-
익명화는 message/stderrExcerpt/command 의 경로를 <path> 로 붕괴시키므로
|
|
4
|
-
(error_zip._PATHISH), 경로 기반 신호는 익명화 뒤에는 계산할 수 없다.
|
|
5
|
-
신호 값 자체는 불리언·카운트라 공개 이슈에 실어도 안전하다.
|
|
6
|
-
"""
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
OKSTRA_DEFECT = "okstra-defect"
|
|
10
|
-
ENVIRONMENT_POLICY = "environment-policy"
|
|
11
|
-
TARGET_CODE = "target-code"
|
|
12
|
-
|
|
13
|
-
# okstra-defect 판정에 필요한 강한 지지 신호 수. 한 신호만으로는 공개 이슈를
|
|
14
|
-
# 만들지 않는다.
|
|
15
|
-
MIN_SUPPORTING_SIGNALS = 2
|
|
16
|
-
|
|
17
|
-
# 단일 phase 클러스터면 무조건 켜지므로 판정을 지탱하지 못한다. 근거로 보고하되
|
|
18
|
-
# 최소 근거 수에는 세지 않는다 — 세면 "근거 2개"가 "진짜 근거 1개 + 공짜 한 자리"가 된다.
|
|
19
|
-
WEAK_SIGNALS = ("phaseLock",)
|
|
20
|
-
|
|
21
|
-
# 리드가 규칙 위반을 관측한 지점에서 찍는 값이라(prompts/lead/okstra-lead-contract.md)
|
|
22
|
-
# 워커 자기신고와 성격이 다르고, 그 자체로 okstra 배선 문제를 가리킨다. 확산이 없어도
|
|
23
|
-
# — 한 프로젝트·한 태스크 안에서만 반복돼도 — 최소 근거 수를 우회한다.
|
|
24
|
-
SELF_SUFFICIENT_SIGNALS = ("contractViolation",)
|
|
25
|
-
|
|
26
|
-
SIGNAL_SUPPORT = {
|
|
27
|
-
"contractViolation": OKSTRA_DEFECT,
|
|
28
|
-
"okstraOwnedPath": OKSTRA_DEFECT,
|
|
29
|
-
"invariantViolated": OKSTRA_DEFECT,
|
|
30
|
-
"taskSpread": OKSTRA_DEFECT,
|
|
31
|
-
"projectSpread": OKSTRA_DEFECT,
|
|
32
|
-
"agentSpread": OKSTRA_DEFECT,
|
|
33
|
-
"phaseLock": OKSTRA_DEFECT,
|
|
34
|
-
"declaredCause": ENVIRONMENT_POLICY,
|
|
35
|
-
"targetSourcePath": TARGET_CODE,
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
# okstra 가 소유한 표면. 여기서 난 실패는 타겟 코드가 아니라 okstra 배선 문제다.
|
|
39
|
-
# 태스크 워크트리(~/.okstra/worktrees/...)는 제외한다 — 그 디렉터리는 타겟 저장소의
|
|
40
|
-
# 체크아웃이라 거기서 난 실패는 남의 코드에서 난 것이다.
|
|
41
|
-
_OKSTRA_PATH_MARKERS = (
|
|
42
|
-
"/.okstra/tasks/", "/.okstra/runs/", "/.okstra/decisions/",
|
|
43
|
-
"/.okstra/project.json", "/.okstra/glossary.md",
|
|
44
|
-
"/.okstra/recent.jsonl", "/.okstra/active.jsonl", "/.okstra/bin/",
|
|
45
|
-
"okstra_ctl", "scripts/okstra-",
|
|
46
|
-
)
|
|
47
|
-
# 환경 정책 사안으로 미리 분류된 cause. sandbox-denied 는 실측상 오진이 많아
|
|
48
|
-
# 여기 넣지 않는다 — 프로브 증거 검사는 error_issue.validate_candidate 가 맡는다.
|
|
49
|
-
_ENVIRONMENT_CAUSES = ("auth-failed", "service-unavailable")
|
|
50
|
-
|
|
51
|
-
_FREETEXT_FIELDS = ("message", "stderrExcerpt", "command")
|
|
52
|
-
|
|
53
|
-
# 경로 신호가 어느 필드를 읽었는지 알리는 라벨. 이 문자열은 공개 이슈 본문의
|
|
54
|
-
# source 열에 그대로 실리므로 error_issue 의 유출 허용목록이 같은 값을 참조한다 —
|
|
55
|
-
# 여기서 필드가 늘면 라벨과 허용목록이 함께 따라간다.
|
|
56
|
-
FREETEXT_SOURCE = "/".join(_FREETEXT_FIELDS)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _freetext(rec: dict) -> str:
|
|
60
|
-
"""자유 텍스트 필드를 잇되 프로젝트 루트 접두는 지우고 본다.
|
|
61
|
-
|
|
62
|
-
`message`·`stderrExcerpt`·`command` 에는 타겟의 절대 프로젝트 루트가 통째로
|
|
63
|
-
박혀 있다. 루트 경로 자체에 `app/`·`lib/` 같은 조각이 들어 있으면 실패가
|
|
64
|
-
어디서 났든 `targetSourcePath` 가 켜진다 — 실측에서 `okstraOwnedPath` 가
|
|
65
|
-
켜진 103개 클러스터 중 86개가 `targetSourcePath` 도 함께 켰고, 그중 83개는
|
|
66
|
-
사용자의 프로젝트가 `.../FontsNinja/app/` 아래 있다는 이유뿐이었다.
|
|
67
|
-
|
|
68
|
-
루트를 지운 텍스트로 계산하면 31개 클러스터의 판정·반대신호가 달라진다.
|
|
69
|
-
반대 신호는 후보를 `skip` 으로 떨어뜨리므로 이 오발화는 본문 잡음이 아니라
|
|
70
|
-
진짜 결함을 조용히 묻는다 — 실측 7건의 반대신호 후보 중 4건이 깨끗해지고
|
|
71
|
-
그중 하나는 `contract-violation` 이다."""
|
|
72
|
-
root = str(rec.get("_projectRoot", "") or "")
|
|
73
|
-
parts = []
|
|
74
|
-
for field in _FREETEXT_FIELDS:
|
|
75
|
-
value = str(rec.get(field, "") or "")
|
|
76
|
-
parts.append(value.replace(root, "") if root else value)
|
|
77
|
-
return " ".join(parts)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def _has_okstra_path(rec: dict) -> bool:
|
|
81
|
-
text = _freetext(rec)
|
|
82
|
-
return any(marker in text for marker in _OKSTRA_PATH_MARKERS)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _has_target_path(rec: dict) -> bool:
|
|
86
|
-
"""okstra 경로가 함께 잡혀도 단락하지 않는다 — 둘 다 켜두면 모순이
|
|
87
|
-
conflicting 으로 드러나 사람이 판단할 수 있다. 단락시키면 반대 근거가
|
|
88
|
-
아예 사라진다."""
|
|
89
|
-
text = _freetext(rec)
|
|
90
|
-
return any(seg in text for seg in ("src/", "lib/", "app/", "tests/"))
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _distinct(records: list[dict], field: str) -> int:
|
|
94
|
-
return len({str(r.get(field, "")) for r in records if r.get(field)})
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def _signal(name: str, value, source: str) -> dict:
|
|
98
|
-
return {"signal": name, "value": value, "source": source}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def compute_signals(records: list[dict]) -> list[dict]:
|
|
102
|
-
"""한 클러스터의 원본 레코드에서 신호를 계산한다."""
|
|
103
|
-
if not records:
|
|
104
|
-
return []
|
|
105
|
-
phases = {str(r.get("phase", "")) for r in records if r.get("phase")}
|
|
106
|
-
causes = [
|
|
107
|
-
str(r.get("context", {}).get("cause", ""))
|
|
108
|
-
for r in records
|
|
109
|
-
if isinstance(r.get("context"), dict) and r.get("context", {}).get("cause")
|
|
110
|
-
]
|
|
111
|
-
# cause 는 워커가 스스로 적는 값이라, 1건짜리 자기신고가 클러스터 전체를
|
|
112
|
-
# 환경 사안으로 돌리지 못하게 과반을 요구한다. sandbox-denied 를 아예 뺀 것과
|
|
113
|
-
# 같은 불신을 나머지 cause 에도 건다.
|
|
114
|
-
declared = [c for c in causes if c in _ENVIRONMENT_CAUSES]
|
|
115
|
-
environment_cause = ""
|
|
116
|
-
if declared:
|
|
117
|
-
top = max(sorted(set(declared)), key=declared.count)
|
|
118
|
-
if declared.count(top) * 2 > len(records):
|
|
119
|
-
environment_cause = top
|
|
120
|
-
return [
|
|
121
|
-
_signal("contractViolation",
|
|
122
|
-
any(r.get("errorType") == "contract-violation" for r in records),
|
|
123
|
-
"errorType"),
|
|
124
|
-
_signal("okstraOwnedPath",
|
|
125
|
-
any(_has_okstra_path(r) for r in records),
|
|
126
|
-
FREETEXT_SOURCE),
|
|
127
|
-
_signal("targetSourcePath",
|
|
128
|
-
any(_has_target_path(r) for r in records),
|
|
129
|
-
FREETEXT_SOURCE),
|
|
130
|
-
_signal("taskSpread", _distinct(records, "taskKey"), "taskKey"),
|
|
131
|
-
_signal("projectSpread", _distinct(records, "_projectRoot"), "_projectRoot"),
|
|
132
|
-
_signal("agentSpread", _distinct(records, "agent"), "agent"),
|
|
133
|
-
_signal("phaseLock", len(phases) == 1, "phase"),
|
|
134
|
-
_signal("declaredCause", environment_cause, "context.cause"),
|
|
135
|
-
]
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def signal_fires(sig: dict) -> bool:
|
|
139
|
-
"""신호가 켜졌는지. 카운트형은 2 이상일 때만 확산 신호로 인정한다.
|
|
140
|
-
|
|
141
|
-
공개 함수다 — 등록 직전 게이트(error_issue.validate_candidate)가 같은 판단을
|
|
142
|
-
다시 걸어야 하는데, 비공개로 두면 그쪽이 자기 판본을 새로 만들면서
|
|
143
|
-
`taskSpread: 1` 같은 꺼진 신호를 근거로 세는 더 약한 규칙이 된다.
|
|
144
|
-
|
|
145
|
-
`value` 가 없는 항목은 꺼진 것으로 본다. 이 함수는 사람이 손으로 고친
|
|
146
|
-
plan.json 을 읽는 자리에 있어, 필드 하나가 빠졌다고 KeyError 로 터지면
|
|
147
|
-
등록 루프가 그 자리에서 끊긴다 — 앞의 후보는 이미 올라간 뒤다."""
|
|
148
|
-
value = sig.get("value")
|
|
149
|
-
if sig["signal"] in ("taskSpread", "projectSpread", "agentSpread"):
|
|
150
|
-
return isinstance(value, int) and value >= 2
|
|
151
|
-
if isinstance(value, bool):
|
|
152
|
-
return value
|
|
153
|
-
return bool(value)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def classify(signals: list[dict]) -> tuple[str, list[dict], list[dict]]:
|
|
157
|
-
"""(분류, 지지 신호, 반대 신호) 를 돌려준다.
|
|
158
|
-
|
|
159
|
-
declaredCause 가 켜지면 환경 정책이 이긴다 — 워커가 명시적으로 분류한
|
|
160
|
-
사안을 신호 개수로 뒤집지 않는다. 그 외에는 최다 지지 분류를 고르고,
|
|
161
|
-
다른 분류를 지지하는 신호는 반대 신호로 그대로 노출한다."""
|
|
162
|
-
fired = [s for s in signals if signal_fires(s)]
|
|
163
|
-
by_class: dict[str, list[dict]] = {}
|
|
164
|
-
for sig in fired:
|
|
165
|
-
by_class.setdefault(SIGNAL_SUPPORT[sig["signal"]], []).append(sig)
|
|
166
|
-
|
|
167
|
-
if by_class.get(ENVIRONMENT_POLICY):
|
|
168
|
-
verdict = ENVIRONMENT_POLICY
|
|
169
|
-
elif not by_class:
|
|
170
|
-
verdict = ""
|
|
171
|
-
else:
|
|
172
|
-
verdict = max(by_class.items(), key=lambda kv: (len(kv[1]), kv[0]))[0]
|
|
173
|
-
|
|
174
|
-
supporting = by_class.get(verdict, [])
|
|
175
|
-
# okstra-defect 만 공개 이슈가 되므로 그 판정에만 최소 근거 수를 요구하고,
|
|
176
|
-
# 약한 신호는 세지 않는다. 다른 분류는 후보에서 빠지는 방향이라 근거 하나로도
|
|
177
|
-
# 해가 없다. supporting 에는 약한 신호도 그대로 담아 이슈 본문에 남긴다.
|
|
178
|
-
if verdict == OKSTRA_DEFECT:
|
|
179
|
-
strong = [s for s in supporting if s["signal"] not in WEAK_SIGNALS]
|
|
180
|
-
alone_is_enough = any(
|
|
181
|
-
s["signal"] in SELF_SUFFICIENT_SIGNALS for s in strong)
|
|
182
|
-
if not alone_is_enough and len(strong) < MIN_SUPPORTING_SIGNALS:
|
|
183
|
-
return "", [], []
|
|
184
|
-
|
|
185
|
-
conflicting = [s for s in fired if SIGNAL_SUPPORT[s["signal"]] != verdict]
|
|
186
|
-
return verdict, supporting, conflicting
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
"""Lead runtime metadata shared by render and prepare paths."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
from dataclasses import dataclass, replace
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@dataclass(frozen=True)
|
|
8
|
-
class LeadRuntimeInfo:
|
|
9
|
-
runtime: str
|
|
10
|
-
agent: str
|
|
11
|
-
agent_label: str
|
|
12
|
-
role: str
|
|
13
|
-
adapter_name: str
|
|
14
|
-
adapter_dispatch_mode: str
|
|
15
|
-
session_accounting: str
|
|
16
|
-
has_claude_session: bool
|
|
17
|
-
adapter_contract_relative_path: str
|
|
18
|
-
|
|
19
|
-
def adapter_payload(self) -> dict[str, str]:
|
|
20
|
-
return {
|
|
21
|
-
"name": self.adapter_name,
|
|
22
|
-
"dispatchMode": self.adapter_dispatch_mode,
|
|
23
|
-
"sessionAccounting": self.session_accounting,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
ARTIFACT_ONLY_LEAD_RUNTIMES = frozenset({"codex", "antigravity", "external"})
|
|
28
|
-
|
|
29
|
-
# Not a lead runtime — an adapter selected by the environment, so it is absent
|
|
30
|
-
# from ALLOWED_LEAD_RUNTIMES and from _LEAD_RUNTIMES on purpose. `--lead-runtime
|
|
31
|
-
# cmux` is not a thing a user can ask for; see `with_cmux_dispatch`.
|
|
32
|
-
CMUX_ADAPTER_NAME = "cmux"
|
|
33
|
-
CMUX_ADAPTER_RELATIVE_PATH = "lead/adapters/cmux.md"
|
|
34
|
-
|
|
35
|
-
_LEAD_RUNTIMES = {
|
|
36
|
-
"claude-code": LeadRuntimeInfo(
|
|
37
|
-
runtime="claude-code",
|
|
38
|
-
agent="claude",
|
|
39
|
-
agent_label="Claude Code",
|
|
40
|
-
role="Claude lead",
|
|
41
|
-
adapter_name="claude-code",
|
|
42
|
-
adapter_dispatch_mode="team",
|
|
43
|
-
session_accounting="claude-jsonl",
|
|
44
|
-
has_claude_session=True,
|
|
45
|
-
adapter_contract_relative_path="lead/adapters/claude-code.md",
|
|
46
|
-
),
|
|
47
|
-
"codex": LeadRuntimeInfo(
|
|
48
|
-
runtime="codex",
|
|
49
|
-
agent="codex",
|
|
50
|
-
agent_label="Codex CLI",
|
|
51
|
-
role="Codex lead",
|
|
52
|
-
adapter_name="codex",
|
|
53
|
-
adapter_dispatch_mode="render-only",
|
|
54
|
-
session_accounting="artifact-only",
|
|
55
|
-
has_claude_session=False,
|
|
56
|
-
adapter_contract_relative_path="lead/adapters/codex.md",
|
|
57
|
-
),
|
|
58
|
-
"antigravity": LeadRuntimeInfo(
|
|
59
|
-
runtime="antigravity",
|
|
60
|
-
agent="antigravity",
|
|
61
|
-
agent_label="Antigravity CLI",
|
|
62
|
-
role="Antigravity lead",
|
|
63
|
-
adapter_name="antigravity",
|
|
64
|
-
adapter_dispatch_mode="render-only",
|
|
65
|
-
session_accounting="artifact-only",
|
|
66
|
-
has_claude_session=False,
|
|
67
|
-
adapter_contract_relative_path="lead/adapters/antigravity.md",
|
|
68
|
-
),
|
|
69
|
-
"external": LeadRuntimeInfo(
|
|
70
|
-
runtime="external",
|
|
71
|
-
agent="external",
|
|
72
|
-
agent_label="External harness",
|
|
73
|
-
role="Okstra lead",
|
|
74
|
-
adapter_name="external",
|
|
75
|
-
adapter_dispatch_mode="render-only",
|
|
76
|
-
session_accounting="artifact-only",
|
|
77
|
-
has_claude_session=False,
|
|
78
|
-
adapter_contract_relative_path="lead/adapters/external.md",
|
|
79
|
-
),
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# Derived rather than restated: a hand-written copy of the registry's own keys
|
|
84
|
-
# is a list that can disagree with the thing it describes.
|
|
85
|
-
ALLOWED_LEAD_RUNTIMES = tuple(_LEAD_RUNTIMES)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def lead_runtime_info(runtime: str) -> LeadRuntimeInfo:
|
|
89
|
-
try:
|
|
90
|
-
return _LEAD_RUNTIMES[runtime]
|
|
91
|
-
except KeyError as exc:
|
|
92
|
-
allowed = ", ".join(ALLOWED_LEAD_RUNTIMES)
|
|
93
|
-
raise ValueError(f"unsupported lead runtime: {runtime} (allowed: {allowed})") from exc
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def is_artifact_only_runtime(runtime: str) -> bool:
|
|
97
|
-
return runtime in ARTIFACT_ONLY_LEAD_RUNTIMES
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def with_cmux_dispatch(info: LeadRuntimeInfo) -> LeadRuntimeInfo:
|
|
101
|
-
"""The same lead, minus the two fields cmux takes over.
|
|
102
|
-
|
|
103
|
-
On the cmux path okstra owns the worker panes rather than the host, so
|
|
104
|
-
"which adapter" and "does this lead dispatch or only render" stop depending
|
|
105
|
-
on who the lead is — they are the same for every runtime. Overriding the two
|
|
106
|
-
fields keeps that answer in one adapter file rather than repeating it in
|
|
107
|
-
four. Everything else (agent, label, role, session accounting) still
|
|
108
|
-
describes the lead itself and is left untouched.
|
|
109
|
-
"""
|
|
110
|
-
return replace(
|
|
111
|
-
info,
|
|
112
|
-
adapter_name=CMUX_ADAPTER_NAME,
|
|
113
|
-
adapter_dispatch_mode="team",
|
|
114
|
-
adapter_contract_relative_path=CMUX_ADAPTER_RELATIVE_PATH,
|
|
115
|
-
)
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"""Resolve native versus CLI execution from host and provider capabilities."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
|
|
6
|
-
from .models import (
|
|
7
|
-
UnknownProviderError,
|
|
8
|
-
provider_spec,
|
|
9
|
-
provider_supports_role,
|
|
10
|
-
provider_wrappers,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
NATIVE_SESSION = "native-session"
|
|
14
|
-
CLI_WRAPPER = "cli-wrapper"
|
|
15
|
-
RUNNER_VALUES = frozenset({NATIVE_SESSION, CLI_WRAPPER})
|
|
16
|
-
|
|
17
|
-
_HOST_NATIVE_PROVIDERS = {
|
|
18
|
-
"claude-code": frozenset({"claude"}),
|
|
19
|
-
"codex": frozenset({"codex"}),
|
|
20
|
-
"antigravity": frozenset({"antigravity"}),
|
|
21
|
-
"external": frozenset(),
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def native_provider_for_host(host_runtime: str) -> str:
|
|
26
|
-
"""Return the single provider owned by an in-session host, if any."""
|
|
27
|
-
providers = _HOST_NATIVE_PROVIDERS.get(host_runtime)
|
|
28
|
-
if providers is None:
|
|
29
|
-
allowed = ", ".join(_HOST_NATIVE_PROVIDERS)
|
|
30
|
-
raise RunnerResolutionError(
|
|
31
|
-
f"unknown host runtime {host_runtime!r}. Allowed values: {allowed}"
|
|
32
|
-
)
|
|
33
|
-
return next(iter(providers), "")
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class RunnerResolutionError(ValueError):
|
|
37
|
-
"""Raised when an assignment cannot run on the selected host."""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@dataclass(frozen=True)
|
|
41
|
-
class RunnerResolution:
|
|
42
|
-
runner: str
|
|
43
|
-
wrapper: str
|
|
44
|
-
reason: str
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def resolve_runner(
|
|
48
|
-
host_runtime: str,
|
|
49
|
-
provider: str,
|
|
50
|
-
role: str,
|
|
51
|
-
requested_runner: str = "",
|
|
52
|
-
) -> RunnerResolution:
|
|
53
|
-
"""Resolve an assignment without inferring the host from installed CLIs."""
|
|
54
|
-
native_providers = _HOST_NATIVE_PROVIDERS.get(host_runtime)
|
|
55
|
-
if native_providers is None:
|
|
56
|
-
allowed = ", ".join(_HOST_NATIVE_PROVIDERS)
|
|
57
|
-
raise RunnerResolutionError(
|
|
58
|
-
f"unknown host runtime {host_runtime!r}. Allowed values: {allowed}"
|
|
59
|
-
)
|
|
60
|
-
try:
|
|
61
|
-
spec = provider_spec(provider)
|
|
62
|
-
except UnknownProviderError as exc:
|
|
63
|
-
raise RunnerResolutionError(str(exc)) from exc
|
|
64
|
-
if not provider_supports_role(provider, role):
|
|
65
|
-
raise RunnerResolutionError(
|
|
66
|
-
f"provider {provider!r} does not support role {role!r}"
|
|
67
|
-
)
|
|
68
|
-
requested = (requested_runner or "").strip()
|
|
69
|
-
if requested and requested not in RUNNER_VALUES:
|
|
70
|
-
allowed = ", ".join(sorted(RUNNER_VALUES))
|
|
71
|
-
raise RunnerResolutionError(
|
|
72
|
-
f"unknown runner {requested!r}. Allowed values: {allowed}"
|
|
73
|
-
)
|
|
74
|
-
if requested == NATIVE_SESSION and provider not in native_providers:
|
|
75
|
-
raise RunnerResolutionError(
|
|
76
|
-
f"host runtime {host_runtime!r} does not provide native {provider!r} execution"
|
|
77
|
-
)
|
|
78
|
-
if requested == NATIVE_SESSION or (not requested and provider in native_providers):
|
|
79
|
-
return RunnerResolution(
|
|
80
|
-
runner=NATIVE_SESSION,
|
|
81
|
-
wrapper="",
|
|
82
|
-
reason=f"{host_runtime} provides native {provider} execution",
|
|
83
|
-
)
|
|
84
|
-
if not spec.wrapper:
|
|
85
|
-
raise RunnerResolutionError(
|
|
86
|
-
f"provider {provider!r} has no CLI wrapper for role {role!r}"
|
|
87
|
-
)
|
|
88
|
-
return RunnerResolution(
|
|
89
|
-
runner=CLI_WRAPPER,
|
|
90
|
-
wrapper=spec.wrapper,
|
|
91
|
-
reason=f"{host_runtime} uses the registered {provider} CLI wrapper",
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def worker_wrapper_registry(report_writer_provider: str = "claude") -> dict[str, str]:
|
|
96
|
-
"""Worker-id wrapper map for dispatch front ends."""
|
|
97
|
-
wrappers = provider_wrappers("analyser")
|
|
98
|
-
report_writer = provider_spec(report_writer_provider)
|
|
99
|
-
if not provider_supports_role(report_writer_provider, "report-writer"):
|
|
100
|
-
raise RunnerResolutionError(
|
|
101
|
-
f"provider {report_writer_provider!r} does not support report-writer"
|
|
102
|
-
)
|
|
103
|
-
return {**wrappers, "report-writer": report_writer.wrapper}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# okstra-inspect facet — error-issue
|
|
2
|
-
|
|
3
|
-
Loaded lazily by the dispatch table in `SKILL.md` (core). Shared rules — Step 0 preflight, the standard task-key resolution rule (0/1/N), the no-task fallback, and Output Rules — live in the core file and still apply here.
|
|
4
|
-
|
|
5
|
-
## error-issue
|
|
6
|
-
|
|
7
|
-
Trigger phrases: "okstra error-issue", "issue candidates", "file okstra issues", "report okstra defects".
|
|
8
|
-
|
|
9
|
-
Turn okstra run anomalies into GitHub issue candidates, then file the approved ones. Read-only over every target's `.okstra/`. Apart from the plan file you edit between the two commands, the only write is the GitHub issue — and that only after the user approves.
|
|
10
|
-
|
|
11
|
-
**Never run `submit` without an explicit user approval in this session.** `plan` is free to run unattended; `submit` is not.
|
|
12
|
-
|
|
13
|
-
### error-issue.1 — Build the plan
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
okstra error-issue plan --out ~/.okstra/error-issue-plan.json
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Parse the stdout JSON: `candidateCount`, `belowGate`, `notOkstraDefect`, `conflicted`, `unreachableRuns`.
|
|
20
|
-
|
|
21
|
-
`candidateCount` includes candidates whose action is already `skip`, so it is not the number of issues that will be filed. Report the filed count from the create/comment rows of the board below.
|
|
22
|
-
|
|
23
|
-
### error-issue.2 — Review each candidate's classification
|
|
24
|
-
|
|
25
|
-
Read the plan file. For each candidate, verify the recorded `classification` against its `signals` list. The classification was computed by `issue_signals.classify`; your job is to confirm it reads correctly against the raw evidence, not to invent a new one.
|
|
26
|
-
|
|
27
|
-
If a candidate's signals do not support its classification, drop it from the plan file rather than arguing with it in prose. If deciding requires reading the raw `stderrExcerpt`, add an evidence entry and quote the line — an evidence entry always carries all three of `signal`, `value`, `source`, so write `{"signal": "<the signal it backs>", "value": "<the quoted line>", "source": "message excerpt"}`. `submit` rejects an entry missing any of the three; a two-key entry is not a lighter form of evidence, it is an unreadable one.
|
|
28
|
-
|
|
29
|
-
### error-issue.3 — Present the approval board (Korean)
|
|
30
|
-
|
|
31
|
-
First state the destination on its own line — `plan.json` 의 `repo` 값을 그대로 읽어 `등록 대상: <repo>` 로 적는다. 이 값은 `~/.okstra/error-issue.json` 이 덮어쓸 수 있으므로 okstra 레포라고 단정하지 않는다. CLI 가 `repo` 없는 plan 을 거부하는 이유가 바로 이것 — 목적지는 승인 화면에 반드시 있어야 할 값이다.
|
|
32
|
-
|
|
33
|
-
Then show two tables, never merged into one.
|
|
34
|
-
|
|
35
|
-
**등록 예정** — candidates whose action is `create` or `comment`. `title` 열은 실제로 올라갈 제목 그대로 싣는다. 그것이 없으면 "승인 화면에서 본 것과 올라가는 것이 같다"가 성립하지 않는다:
|
|
36
|
-
|
|
37
|
-
| # | action | title | fingerprint | errorType / phase | 발생 | 지지 신호(`evidence`) | 기존 이슈 |
|
|
38
|
-
|---|---|---|---|---|---:|---|---|
|
|
39
|
-
|
|
40
|
-
**보류** — candidates whose action is already `skip`. They are NOT filed; they are shown so the human sees what was set aside. `skip` has two distinct origins and the row must say which:
|
|
41
|
-
|
|
42
|
-
- **반대 신호** — `conflictingSignals` is non-empty. `classify` called it an okstra defect while another signal pointed elsewhere.
|
|
43
|
-
- **이미 보고됨** — `conflictingSignals` is empty and `existingIssue` is set. An open issue already covers the last occurrence, so there is nothing new to say.
|
|
44
|
-
|
|
45
|
-
| # | fingerprint | errorType / phase | 발생 | 보류 사유 | 지지 신호 | 반대 신호 / 기존 이슈 |
|
|
46
|
-
|---|---|---|---:|---|---|---|
|
|
47
|
-
|
|
48
|
-
**본문** — then, for every `등록 예정` row, print that candidate's `body` **whole and verbatim** in a fenced block, one block per row, labelled with the same `#`. Never elide a section. The table summarizes; the body is what actually gets posted, and "승인 화면에서 본 것과 올라가는 것이 같다" is this facet's whole claim — a title plus a signal list does not carry it.
|
|
49
|
-
|
|
50
|
-
Bodies are bounded by construction: every line is derived (counts, signal values, invariant names) except the one representative message, which shares its source with the `title` in the same row. The raw error records are deliberately NOT in the body — their free text carries the reporting project's ticket ids and source filenames, and no shape rule can separate those from okstra's own vocabulary. They stay in the candidate's `records` in `plan.json` and in the `okstra error-zip` archive. If you need to vet them, read `plan.json` locally; do not paste them into the body.
|
|
51
|
-
|
|
52
|
-
Then ask via `AskUserQuestion` (options: 전부 등록 / 일부만 선택 / 등록 안 함). Deselected candidates are removed from the plan file before submit.
|
|
53
|
-
|
|
54
|
-
A held candidate is promoted only when the user explicitly says so, and the action you set depends on `existingIssue`:
|
|
55
|
-
|
|
56
|
-
- `existingIssue` is null → set `action` to `create`.
|
|
57
|
-
- `existingIssue` is set → set `action` to `comment`. **Never `create` over an existing issue** — that files a duplicate carrying the same `okstra-fingerprint` marker, and from then on `find_existing_issue` matches whichever of the two the list returns first, so every later run reports against an arbitrary one.
|
|
58
|
-
|
|
59
|
-
Say which you set and why.
|
|
60
|
-
|
|
61
|
-
Surface `unreachableRuns > 0` — no silent omission.
|
|
62
|
-
|
|
63
|
-
### error-issue.4 — Submit
|
|
64
|
-
|
|
65
|
-
Only after an explicit approval:
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
okstra error-issue submit --plan ~/.okstra/error-issue-plan.json
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Report `created` / `commented` / `skipped` / `rejected` / `failed`. If `rejected > 0`, print each rejection's `reasons` verbatim — a rejection means the outbound text or the evidence failed the final gate, and it is never something to work around.
|
|
72
|
-
|
|
73
|
-
If `failed > 0`, print each failure's `error` and **do not re-run `submit` on the same plan file**. A failure is a candidate that crashed or whose `gh` call errored; the candidates before it were already filed, and their `action` in the plan file still says `create`. Re-run `error-issue plan` instead — the fingerprint lookup turns the already-filed ones into `comment`/`skip`, which is the only thing that keeps a retry from filing duplicates.
|
|
74
|
-
|
|
75
|
-
### error-issue.5 — Next step
|
|
76
|
-
|
|
77
|
-
End with: "이 이슈를 실제로 고치려면 `/okstra-brief-gen` 에 이슈 URL 을 주고, okstra 레포에서 `okstra-run --task-type error-analysis` 로 진행하세요."
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
|
-
|
|
3
|
-
const USAGE = `okstra error-issue — turn okstra run anomalies into GitHub issue candidates
|
|
4
|
-
|
|
5
|
-
Usage:
|
|
6
|
-
okstra error-issue plan --out <path>
|
|
7
|
-
okstra error-issue submit --plan <path>
|
|
8
|
-
|
|
9
|
-
'plan' walks the global run-index, gates clusters by occurrence and recency,
|
|
10
|
-
computes classification signals from the raw records, checks GitHub for an
|
|
11
|
-
existing issue carrying the same fingerprint, and writes plan.json.
|
|
12
|
-
'submit' reads that file only, re-validates every candidate, and files or
|
|
13
|
-
comments via gh. A candidate that fails validation is never sent.
|
|
14
|
-
`;
|
|
15
|
-
|
|
16
|
-
export async function run(args) {
|
|
17
|
-
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
|
|
18
|
-
process.stdout.write(USAGE);
|
|
19
|
-
return 0;
|
|
20
|
-
}
|
|
21
|
-
const result = await runPythonModule({
|
|
22
|
-
module: "okstra_ctl.error_issue",
|
|
23
|
-
args,
|
|
24
|
-
stdio: "inherit-stdout",
|
|
25
|
-
});
|
|
26
|
-
return result.code;
|
|
27
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
const WORKSPACE_TRUST_CHECK_ID = "workspace-trust";
|
|
6
|
-
|
|
7
|
-
function ready(runtime, check) {
|
|
8
|
-
return { runtime, status: "ready", checks: check ? [check] : [] };
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function blocked(runtime) {
|
|
12
|
-
return {
|
|
13
|
-
runtime,
|
|
14
|
-
status: "blocked",
|
|
15
|
-
checks: [{
|
|
16
|
-
id: WORKSPACE_TRUST_CHECK_ID,
|
|
17
|
-
status: "required",
|
|
18
|
-
action: "restart-and-trust",
|
|
19
|
-
}],
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function unknown(runtime) {
|
|
24
|
-
return {
|
|
25
|
-
runtime,
|
|
26
|
-
status: "unknown",
|
|
27
|
-
checks: [{
|
|
28
|
-
id: WORKSPACE_TRUST_CHECK_ID,
|
|
29
|
-
status: "unavailable",
|
|
30
|
-
action: "open-project-and-trust",
|
|
31
|
-
}],
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isJsonObject(value) {
|
|
36
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function hasProjectSettings(projectRoot) {
|
|
40
|
-
try {
|
|
41
|
-
await fs.lstat(join(projectRoot, ".claude", "settings.local.json"));
|
|
42
|
-
return true;
|
|
43
|
-
} catch (error) {
|
|
44
|
-
if (error?.code === "ENOENT") return false;
|
|
45
|
-
throw error;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function checkClaudeCodeReadiness({ projectRoot, homeDir }) {
|
|
50
|
-
const runtime = "claude-code";
|
|
51
|
-
try {
|
|
52
|
-
if (!(await hasProjectSettings(projectRoot))) {
|
|
53
|
-
return ready(runtime, {
|
|
54
|
-
id: WORKSPACE_TRUST_CHECK_ID,
|
|
55
|
-
status: "not-applicable",
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const rawState = await fs.readFile(join(homeDir, ".claude.json"), "utf8");
|
|
60
|
-
const state = JSON.parse(rawState);
|
|
61
|
-
if (!isJsonObject(state?.projects)) return unknown(runtime);
|
|
62
|
-
|
|
63
|
-
const projectState = state.projects[resolve(projectRoot)];
|
|
64
|
-
if (projectState === undefined) return blocked(runtime);
|
|
65
|
-
if (!isJsonObject(projectState)) return unknown(runtime);
|
|
66
|
-
if (projectState.hasTrustDialogAccepted === true) {
|
|
67
|
-
return ready(runtime, { id: WORKSPACE_TRUST_CHECK_ID, status: "accepted" });
|
|
68
|
-
}
|
|
69
|
-
if (projectState.hasTrustDialogAccepted === false
|
|
70
|
-
|| projectState.hasTrustDialogAccepted === null
|
|
71
|
-
|| projectState.hasTrustDialogAccepted === undefined) {
|
|
72
|
-
return blocked(runtime);
|
|
73
|
-
}
|
|
74
|
-
return unknown(runtime);
|
|
75
|
-
} catch {
|
|
76
|
-
return unknown(runtime);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export async function checkRuntimeReadiness({
|
|
81
|
-
runtime,
|
|
82
|
-
projectRoot,
|
|
83
|
-
homeDir = homedir(),
|
|
84
|
-
}) {
|
|
85
|
-
const selectedRuntime = runtime ?? "auto";
|
|
86
|
-
if (selectedRuntime !== "claude-code") {
|
|
87
|
-
return ready(selectedRuntime);
|
|
88
|
-
}
|
|
89
|
-
return checkClaudeCodeReadiness({ projectRoot, homeDir });
|
|
90
|
-
}
|