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
|
@@ -51,7 +51,8 @@ from okstra_ctl.conformance import ( # noqa: E402
|
|
|
51
51
|
validate_conformance_manifest,
|
|
52
52
|
)
|
|
53
53
|
from okstra_ctl.paths import RunRef # noqa: E402
|
|
54
|
-
from okstra_ctl.
|
|
54
|
+
from okstra_ctl.domain.host import HostNotRegistered # noqa: E402
|
|
55
|
+
from okstra_ctl.registry.host_registry import default_host_registry # noqa: E402
|
|
55
56
|
from okstra_ctl.build_tools import ( # noqa: E402
|
|
56
57
|
command_invokes_build_tool,
|
|
57
58
|
resolve_build_tool_tokens,
|
|
@@ -65,7 +66,7 @@ from okstra_ctl.report_translation import ( # noqa: E402
|
|
|
65
66
|
HANGUL_PROSE_LIMIT,
|
|
66
67
|
hangul_share,
|
|
67
68
|
)
|
|
68
|
-
from okstra_ctl.stage_citations import
|
|
69
|
+
from okstra_ctl.stage_citations import enumerated_stage_numbers # noqa: E402
|
|
69
70
|
from okstra_ctl.incremental_scope import ( # noqa: E402
|
|
70
71
|
coverage_row_blocked_on,
|
|
71
72
|
stages_for_clarification,
|
|
@@ -170,11 +171,11 @@ def _session_accounting(team_state: dict) -> str:
|
|
|
170
171
|
value = str(adapter.get("sessionAccounting", "")).strip()
|
|
171
172
|
if value:
|
|
172
173
|
return value
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
runtime = str(team_state.get("leadRuntime", "")).strip()
|
|
175
|
+
try:
|
|
176
|
+
return default_host_registry().resolve(runtime).descriptor.session_accounting
|
|
177
|
+
except HostNotRegistered:
|
|
178
|
+
return "claude-jsonl"
|
|
178
179
|
|
|
179
180
|
|
|
180
181
|
|
|
@@ -742,7 +743,7 @@ def validate_team_state(
|
|
|
742
743
|
dispatch_mode = str(team_state.get("dispatchMode", "")).strip()
|
|
743
744
|
if (
|
|
744
745
|
dispatch_mode in WORKER_DISPATCH_MODES
|
|
745
|
-
or team_state
|
|
746
|
+
or _session_accounting(team_state) == "artifact-only"
|
|
746
747
|
):
|
|
747
748
|
if dispatch_mode not in WORKER_DISPATCH_MODES:
|
|
748
749
|
expected = ", ".join(sorted(WORKER_DISPATCH_MODES))
|
|
@@ -3132,6 +3133,7 @@ def validate_final_report_data(
|
|
|
3132
3133
|
# Called here rather than from a task-type branch: four profiles raise
|
|
3133
3134
|
# clarification rows, and the gate scopes itself by task type internally.
|
|
3134
3135
|
_validate_clarification_options(data, failures)
|
|
3136
|
+
_validate_open_approval_blocker_provenance(data, failures)
|
|
3135
3137
|
|
|
3136
3138
|
task_type = (data.get("header") or {}).get("taskType")
|
|
3137
3139
|
_validate_verifier_fail_blocks_verdict(data, failures)
|
|
@@ -4125,6 +4127,80 @@ def _validate_option_set(
|
|
|
4125
4127
|
)
|
|
4126
4128
|
|
|
4127
4129
|
|
|
4130
|
+
def _validate_open_approval_blocker_provenance(
|
|
4131
|
+
data: dict, failures: list[str]
|
|
4132
|
+
) -> None:
|
|
4133
|
+
"""An open approval blocker records who raised it and what the lead did about it.
|
|
4134
|
+
|
|
4135
|
+
A lead can foresee a blocker, write it into the report as a
|
|
4136
|
+
`blocks: approval` row, and let the run finish — the row then withholds
|
|
4137
|
+
approval until a separate user-response cycle answers it. That path cost a
|
|
4138
|
+
full planning run: the user pointed at a reduction document and said "remove
|
|
4139
|
+
what it flags", the lead read that document's own "user decision required"
|
|
4140
|
+
note as outranking the instruction, and told the report writer to raise the
|
|
4141
|
+
item instead of applying it. The two verifiers then agreed unanimously that
|
|
4142
|
+
it was a user decision — not an independent finding, but the lead's own
|
|
4143
|
+
instruction returning to it. The run spent its whole self-fix budget on a
|
|
4144
|
+
gate no round could clear, because the thing blocking it was already
|
|
4145
|
+
answered before the run started.
|
|
4146
|
+
|
|
4147
|
+
`origin` names who produced the row, so a lead-authored blocker can no
|
|
4148
|
+
longer arrive dressed as a worker consensus. `userConfirmation` records what
|
|
4149
|
+
the lead did before writing it. Neither is required on a row the user has
|
|
4150
|
+
already answered (`answered` / `resolved`) — the record is in `userInput`.
|
|
4151
|
+
|
|
4152
|
+
The one shape rejected outright is a lead-directed blocker raised with no
|
|
4153
|
+
interactive session: with nobody to ask, a defect the lead itself surfaced
|
|
4154
|
+
belongs in `## 5. Missing Information and Risks` as a Working Assumption,
|
|
4155
|
+
the same outlet the surviving planner-fixable items already use. Blocking a
|
|
4156
|
+
plan on the lead's own judgment, in a run where that judgment cannot be put
|
|
4157
|
+
to the user, only defers the work to a re-run.
|
|
4158
|
+
"""
|
|
4159
|
+
for row in data.get("clarificationItems") or []:
|
|
4160
|
+
if not isinstance(row, dict):
|
|
4161
|
+
continue
|
|
4162
|
+
if row.get("blocks") != "approval" or row.get("status") != "open":
|
|
4163
|
+
continue
|
|
4164
|
+
row_id = str(row.get("id") or "<unknown>")
|
|
4165
|
+
origin = row.get("origin")
|
|
4166
|
+
confirmation = row.get("userConfirmation")
|
|
4167
|
+
if not origin:
|
|
4168
|
+
failures.append(
|
|
4169
|
+
f"final-report data.json: clarification `{row_id}` is an open "
|
|
4170
|
+
"approval blocker with no `origin`. Record who raised it — "
|
|
4171
|
+
"`worker-finding` (an analyser or verifier reached it "
|
|
4172
|
+
"independently), `material-gap` (the brief and the codebase "
|
|
4173
|
+
"both leave it unanswered), or `lead-directed` (the lead's own "
|
|
4174
|
+
"judgment, including anything the lead instructed a worker to "
|
|
4175
|
+
"raise). A lead-authored blocker recorded as a worker consensus "
|
|
4176
|
+
"is how a run blocks on a question the user already answered."
|
|
4177
|
+
)
|
|
4178
|
+
if not confirmation:
|
|
4179
|
+
failures.append(
|
|
4180
|
+
f"final-report data.json: clarification `{row_id}` is an open "
|
|
4181
|
+
"approval blocker with no `userConfirmation`. Record what "
|
|
4182
|
+
"happened before the row was written — `asked-and-answered`, "
|
|
4183
|
+
"`asked-awaiting`, or `deferred-no-interactive-session`. "
|
|
4184
|
+
"Withholding approval is the most expensive thing a report can "
|
|
4185
|
+
"do to a run; doing it without recording whether the user was "
|
|
4186
|
+
"ever asked leaves the next run unable to tell a real blocker "
|
|
4187
|
+
"from a question nobody put to them."
|
|
4188
|
+
)
|
|
4189
|
+
if (
|
|
4190
|
+
origin == "lead-directed"
|
|
4191
|
+
and confirmation == "deferred-no-interactive-session"
|
|
4192
|
+
):
|
|
4193
|
+
failures.append(
|
|
4194
|
+
f"final-report data.json: clarification `{row_id}` blocks "
|
|
4195
|
+
"approval on the lead's own judgment (`origin: lead-directed`) "
|
|
4196
|
+
"in a run with no interactive session to answer it. Record it "
|
|
4197
|
+
"as a Working Assumption in `## 5. Missing Information and "
|
|
4198
|
+
"Risks` and let the plan proceed — that is the outlet surviving "
|
|
4199
|
+
"planner-fixable items already use. A blocker nobody can answer "
|
|
4200
|
+
"inside this run only moves the work to a re-run."
|
|
4201
|
+
)
|
|
4202
|
+
|
|
4203
|
+
|
|
4128
4204
|
def _has_clarification_backtrace(
|
|
4129
4205
|
row_id: str, plan_items: object, coverage: object
|
|
4130
4206
|
) -> bool:
|
|
@@ -6346,10 +6422,14 @@ def _validate_requirement_provenance(
|
|
|
6346
6422
|
# Deliberately not `_COVERED_BY_STAGE_REF_RE`: the two directions disagree on
|
|
6347
6423
|
# what "no match" means — forward treats it as nothing to verify and passes,
|
|
6348
6424
|
# reverse treats it as nothing cited and orphans every stage — so the reverse
|
|
6349
|
-
# reader must cover `Stages 1, 2
|
|
6350
|
-
#
|
|
6351
|
-
#
|
|
6352
|
-
|
|
6425
|
+
# reader must cover `Stages 1, 2` and `Stage 1 and 2` too.
|
|
6426
|
+
#
|
|
6427
|
+
# It reads hand-enumerated numbers only. A range's interior is deliberately NOT
|
|
6428
|
+
# coverage evidence: one `Stages 1-64` cell would otherwise stamp every stage in
|
|
6429
|
+
# the map while the planner confirmed none of them. The incremental-scope
|
|
6430
|
+
# back-trace keeps the widening reader (`cited_stage_numbers`) because "could
|
|
6431
|
+
# this answer reach stage 5" is the opposite question and must over-approximate.
|
|
6432
|
+
_enumerated_stage_numbers = enumerated_stage_numbers
|
|
6353
6433
|
|
|
6354
6434
|
|
|
6355
6435
|
# Statuses under which a row asserts the plan does work for the requirement.
|
|
@@ -6386,14 +6466,16 @@ def _validate_stage_has_requirement(data: dict, failures: list[str]) -> None:
|
|
|
6386
6466
|
continue
|
|
6387
6467
|
if not _COVERAGE_CLAIMING_STATUS_RE.match(str(row.get("status") or "").strip()):
|
|
6388
6468
|
continue
|
|
6389
|
-
cited |=
|
|
6469
|
+
cited |= _enumerated_stage_numbers(str(row.get("coveredBy") or ""))
|
|
6390
6470
|
|
|
6391
6471
|
for orphan in sorted(stage_numbers - cited):
|
|
6392
6472
|
failures.append(
|
|
6393
6473
|
f"final-report data.json: Stage {orphan} is cited by no requirementCoverage "
|
|
6394
6474
|
"row — it traces back to nothing the brief asked for. Either cite the "
|
|
6395
6475
|
"requirement it serves, or drop it and raise it as a `Blocks=approval` "
|
|
6396
|
-
"clarification (profile: scope provenance)."
|
|
6476
|
+
"clarification (profile: scope provenance). A range cites only its "
|
|
6477
|
+
"endpoints here: write `Stages 1, 2, 3` rather than `Stages 1-3`, so each "
|
|
6478
|
+
"stage this requirement covers is one the planner named."
|
|
6397
6479
|
)
|
|
6398
6480
|
|
|
6399
6481
|
|
|
@@ -39,7 +39,8 @@ from okstra_ctl.worker_heartbeat import ( # noqa: E402
|
|
|
39
39
|
IN_STAGE_PREFIX,
|
|
40
40
|
max_gap_seconds_after,
|
|
41
41
|
)
|
|
42
|
-
from okstra_ctl.
|
|
42
|
+
from okstra_ctl.domain.host import HostNotRegistered # noqa: E402
|
|
43
|
+
from okstra_ctl.registry.host_registry import default_host_registry # noqa: E402
|
|
43
44
|
from okstra_ctl.wrapper_status import read_wrapper_status # noqa: E402
|
|
44
45
|
|
|
45
46
|
_DISPATCHED_STATUSES = {"completed", "timeout", "error", "in-progress"}
|
|
@@ -414,7 +415,11 @@ def _conformance_evidence_source(
|
|
|
414
415
|
return None, f"unsupported sessionAccounting: `{accounting}`"
|
|
415
416
|
|
|
416
417
|
legacy_runtime = str(team_state.get("leadRuntime", "") or "claude-code")
|
|
417
|
-
|
|
418
|
+
try:
|
|
419
|
+
accounting = default_host_registry().resolve(legacy_runtime).descriptor.session_accounting
|
|
420
|
+
except HostNotRegistered:
|
|
421
|
+
accounting = "claude-jsonl"
|
|
422
|
+
if accounting == "artifact-only":
|
|
418
423
|
return "artifact-only", None
|
|
419
424
|
return "claude-jsonl", None
|
|
420
425
|
|
|
@@ -452,6 +457,61 @@ def _plan_body_rounds_ran(run_dir: Path, suffix: str | None) -> int:
|
|
|
452
457
|
return rounds if isinstance(rounds, int) and rounds > 0 else 0
|
|
453
458
|
|
|
454
459
|
|
|
460
|
+
def _ids_reported_as_asked(report_path: Path) -> list[str]:
|
|
461
|
+
"""리포트가 "사용자에게 물었다"고 기록한 열린 승인 차단 행의 id."""
|
|
462
|
+
name = report_path.name
|
|
463
|
+
if not name.endswith(".md"):
|
|
464
|
+
return []
|
|
465
|
+
data_path = report_path.with_name(name.removesuffix(".md") + ".data.json")
|
|
466
|
+
try:
|
|
467
|
+
doc = json.loads(data_path.read_text())
|
|
468
|
+
except (OSError, json.JSONDecodeError):
|
|
469
|
+
return []
|
|
470
|
+
if not isinstance(doc, dict):
|
|
471
|
+
return []
|
|
472
|
+
asked: list[str] = []
|
|
473
|
+
for row in doc.get("clarificationItems") or []:
|
|
474
|
+
if not isinstance(row, dict):
|
|
475
|
+
continue
|
|
476
|
+
if row.get("blocks") != "approval" or row.get("status") != "open":
|
|
477
|
+
continue
|
|
478
|
+
if not str(row.get("userConfirmation") or "").startswith("asked-"):
|
|
479
|
+
continue
|
|
480
|
+
row_id = str(row.get("id") or "").strip()
|
|
481
|
+
if row_id:
|
|
482
|
+
asked.append(row_id)
|
|
483
|
+
return asked
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _check_user_confirm_checkpoints(
|
|
487
|
+
by_phase: dict[str, list[tuple[str, str]]],
|
|
488
|
+
asked_ids: list[str],
|
|
489
|
+
errors: list[str],
|
|
490
|
+
) -> None:
|
|
491
|
+
""""물어봤다"는 리포트의 기록에는 물어본 흔적이 세션에 남아 있어야 한다.
|
|
492
|
+
|
|
493
|
+
승인을 막는 행은 run 하나를 통째로 대기 상태로 만든다. 그런 행을 세우기 전에
|
|
494
|
+
사용자에게 그 자리에서 묻는 것이 계약인데, 그 이행 여부를 리포트의 자기 신고
|
|
495
|
+
말고는 확인할 길이 없었다. 한 번은 리드가 차단이 생길 것을 미리 예측해 말해
|
|
496
|
+
놓고도 묻지 않은 채 행으로 만들었고, run 은 사용자가 이미 답해 둔 질문 앞에서
|
|
497
|
+
self-fix 예산을 전부 썼다. 체크포인트가 그 격차를 좁힌다 — 세션에 질문이 남지
|
|
498
|
+
않았다면 리드는 묻지 않은 것이다."""
|
|
499
|
+
if not asked_ids:
|
|
500
|
+
return
|
|
501
|
+
detail = "prompts/lead/okstra-lead-contract.md 'User confirmation before an approval blocker'"
|
|
502
|
+
asked_lines = " ".join(line for _ts, line in by_phase.get("user-confirm", []))
|
|
503
|
+
for row_id in asked_ids:
|
|
504
|
+
if row_id in asked_lines:
|
|
505
|
+
continue
|
|
506
|
+
errors.append(
|
|
507
|
+
f"PROGRESS checkpoint missing: `user-confirm {row_id}` — the final "
|
|
508
|
+
f"report records `{row_id}` as an approval blocker the user was "
|
|
509
|
+
"asked about, but no such line exists in this run's evidence. Either "
|
|
510
|
+
"ask before writing the row, or record what actually happened "
|
|
511
|
+
f"(`deferred-no-interactive-session`) ({detail})."
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
|
|
455
515
|
def _phase_mentions_worker(lines: list[tuple[str, str]], needles: list[str]) -> bool:
|
|
456
516
|
return any(needle in _norm(line) for _ts, line in lines for needle in needles)
|
|
457
517
|
|
|
@@ -555,6 +615,7 @@ def _check_progress_checkpoints(
|
|
|
555
615
|
team_state: dict,
|
|
556
616
|
run_dir: Path,
|
|
557
617
|
suffix: str | None,
|
|
618
|
+
report_path: Path,
|
|
558
619
|
errors: list[str],
|
|
559
620
|
) -> None:
|
|
560
621
|
by_phase: dict[str, list[tuple[str, str]]] = {}
|
|
@@ -617,6 +678,9 @@ def _check_progress_checkpoints(
|
|
|
617
678
|
_check_plan_verify_cleanup_checkpoints(
|
|
618
679
|
by_phase, _plan_body_rounds_ran(run_dir, suffix), errors
|
|
619
680
|
)
|
|
681
|
+
_check_user_confirm_checkpoints(
|
|
682
|
+
by_phase, _ids_reported_as_asked(report_path), errors
|
|
683
|
+
)
|
|
620
684
|
|
|
621
685
|
|
|
622
686
|
def _parse_iso(ts: str) -> datetime | None:
|
|
@@ -869,7 +933,9 @@ def validate_session_conformance(
|
|
|
869
933
|
if error:
|
|
870
934
|
result.errors.append(error)
|
|
871
935
|
return result
|
|
872
|
-
_check_progress_checkpoints(
|
|
936
|
+
_check_progress_checkpoints(
|
|
937
|
+
evidence, team_state, run_dir, suffix, report_path, result.errors
|
|
938
|
+
)
|
|
873
939
|
_check_cmux_adapter_read(evidence, team_state, result.errors)
|
|
874
940
|
if task_type == "implementation":
|
|
875
941
|
_check_implementation_entry_guard(
|
package/src/cli-registry.mjs
CHANGED
|
@@ -218,13 +218,6 @@ export const COMMAND_REGISTRY = [
|
|
|
218
218
|
category: "introspection",
|
|
219
219
|
summary: ["Bundle cross-project okstra error logs into a zip"],
|
|
220
220
|
},
|
|
221
|
-
{
|
|
222
|
-
name: "error-issue",
|
|
223
|
-
module: "./commands/inspect/error-issue.mjs",
|
|
224
|
-
export: "run",
|
|
225
|
-
category: "introspection",
|
|
226
|
-
summary: ["Turn okstra run anomalies into GitHub issue candidates"],
|
|
227
|
-
},
|
|
228
221
|
{
|
|
229
222
|
name: "run-audit",
|
|
230
223
|
module: "./commands/inspect/run-audit.mjs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
2
|
import { resolvePaths } from "../../lib/paths.mjs";
|
|
3
|
-
import { resolveRuntime } from "../../lib/
|
|
3
|
+
import { resolveRuntime } from "../../lib/host-registry-client.mjs";
|
|
4
4
|
|
|
5
5
|
const USAGE = `okstra render-bundle — preview the task bundle without launching a lead
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ Usage:
|
|
|
18
18
|
[--analysis-target <text>] [--evidence-inputs <csv>] \\
|
|
19
19
|
[--lead-model <m>] [--claude-model <m>] [--codex-model <m>] \\
|
|
20
20
|
[--antigravity-model <m>] [--report-writer-model <m>] \\
|
|
21
|
-
[--lead-runtime
|
|
21
|
+
[--lead-runtime auto|<host-id-or-alias>] \\
|
|
22
22
|
[--related-tasks <list>] [--base-ref <ref>] \\
|
|
23
23
|
[--clarification-response <path>] [--work-category <cat>] \\
|
|
24
24
|
[--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>] \\
|
|
@@ -26,7 +26,7 @@ Usage:
|
|
|
26
26
|
|
|
27
27
|
--stage implementation / final-verification only
|
|
28
28
|
--stages release-handoff only: PR stage bundle csv (empty = whole-task)
|
|
29
|
-
--lead-runtime host
|
|
29
|
+
--lead-runtime registered host ID or alias
|
|
30
30
|
--pr-template-path release-handoff only
|
|
31
31
|
--fix-cycle entry phase only: record this re-entry of a done task as a
|
|
32
32
|
bug-fix cycle (yes opens/continues the cycle)
|
|
@@ -85,7 +85,7 @@ export async function run(args) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
const requestedRuntime = findFlagValue(args, "--lead-runtime") || "auto";
|
|
88
|
-
const runtimeResolution = resolveRuntime({
|
|
88
|
+
const runtimeResolution = await resolveRuntime({
|
|
89
89
|
requestedRuntime,
|
|
90
90
|
command: "render",
|
|
91
91
|
env: process.env,
|
|
@@ -5,14 +5,14 @@ import { join } from "node:path";
|
|
|
5
5
|
import { runEnsureInstalled } from "../lifecycle/install.mjs";
|
|
6
6
|
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
7
7
|
import { resolvePaths } from "../../lib/paths.mjs";
|
|
8
|
-
import { resolveRuntime } from "../../lib/
|
|
8
|
+
import { resolveRuntime } from "../../lib/host-registry-client.mjs";
|
|
9
9
|
|
|
10
10
|
const USAGE = `okstra run — host-aware okstra execution front door
|
|
11
11
|
|
|
12
12
|
Usage:
|
|
13
|
-
okstra run [
|
|
13
|
+
okstra run [<host-id-or-alias>] --project-root <dir> \\
|
|
14
14
|
--project-id <id> --task-group <tg> --task-id <tid> --task-type <type> \\
|
|
15
|
-
[--lead-runtime auto
|
|
15
|
+
[--lead-runtime auto|<host-id-or-alias>] [--yes]
|
|
16
16
|
|
|
17
17
|
The leading word selects the lead and is an alias for --lead-runtime, so
|
|
18
18
|
'okstra run codex' starts a Codex lead. Naming a lead starts that CLI with the
|
|
@@ -21,22 +21,6 @@ team. Starting a lead whose CLI sandboxes itself asks for confirmation first,
|
|
|
21
21
|
which --yes answers.
|
|
22
22
|
`;
|
|
23
23
|
|
|
24
|
-
// Leads okstra can start itself. `external` is not one: it names a harness
|
|
25
|
-
// okstra does not own, so those runs are orchestrated rather than launched.
|
|
26
|
-
const LAUNCHABLE_RUNTIMES = new Set(["claude-code", "codex", "antigravity"]);
|
|
27
|
-
|
|
28
|
-
// The leading word users actually type. `claude` is the provider's name while
|
|
29
|
-
// the runtime is `claude-code`; the rest coincide.
|
|
30
|
-
const RUNTIME_ALIASES = new Map([
|
|
31
|
-
["claude", "claude-code"],
|
|
32
|
-
["claude-code", "claude-code"],
|
|
33
|
-
["codex", "codex"],
|
|
34
|
-
["antigravity", "antigravity"],
|
|
35
|
-
["avg", "antigravity"],
|
|
36
|
-
["agy", "antigravity"],
|
|
37
|
-
["external", "external"],
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
24
|
function flagValue(args, flag) {
|
|
41
25
|
const equals = args.find((arg) => arg.startsWith(`${flag}=`));
|
|
42
26
|
if (equals) return equals.slice(flag.length + 1);
|
|
@@ -46,8 +30,7 @@ function flagValue(args, flag) {
|
|
|
46
30
|
|
|
47
31
|
export function positionalRuntime(args) {
|
|
48
32
|
const first = args[0];
|
|
49
|
-
|
|
50
|
-
return RUNTIME_ALIASES.get(first) || "";
|
|
33
|
+
return !first || first.startsWith("-") ? "" : first;
|
|
51
34
|
}
|
|
52
35
|
|
|
53
36
|
export function requestedRuntimeFromArgs(args) {
|
|
@@ -91,7 +74,7 @@ function renderArgs(args, resolution) {
|
|
|
91
74
|
export function buildRunPlan({ args, paths, resolution, runManifestPath = "" }) {
|
|
92
75
|
void paths;
|
|
93
76
|
const projectRoot = projectRootFromArgs(args);
|
|
94
|
-
if (resolution.
|
|
77
|
+
if (resolution.launchMode === "team") {
|
|
95
78
|
return {
|
|
96
79
|
ok: true,
|
|
97
80
|
kind: "team",
|
|
@@ -104,7 +87,7 @@ export function buildRunPlan({ args, paths, resolution, runManifestPath = "" })
|
|
|
104
87
|
};
|
|
105
88
|
}
|
|
106
89
|
|
|
107
|
-
if (
|
|
90
|
+
if (resolution.launchMode === "lead") {
|
|
108
91
|
return {
|
|
109
92
|
ok: true,
|
|
110
93
|
kind: "launch-lead",
|
|
@@ -130,7 +113,7 @@ export function buildRunPlan({ args, paths, resolution, runManifestPath = "" })
|
|
|
130
113
|
};
|
|
131
114
|
}
|
|
132
115
|
|
|
133
|
-
return { ok: false, reason: `unsupported
|
|
116
|
+
return { ok: false, reason: `unsupported launch mode: ${resolution.launchMode}`, commands: [] };
|
|
134
117
|
}
|
|
135
118
|
|
|
136
119
|
async function ensureRuntime(requestedRuntime) {
|
|
@@ -182,7 +165,7 @@ export async function run(args) {
|
|
|
182
165
|
|
|
183
166
|
const paths = await resolvePaths();
|
|
184
167
|
const requestedRuntime = requestedRuntimeFromArgs(args);
|
|
185
|
-
const resolution = resolveRuntime({
|
|
168
|
+
const resolution = await resolveRuntime({
|
|
186
169
|
requestedRuntime,
|
|
187
170
|
command: "run",
|
|
188
171
|
env: process.env,
|
|
@@ -20,7 +20,8 @@ Subcommands:
|
|
|
20
20
|
|
|
21
21
|
Usage:
|
|
22
22
|
okstra wizard new-state-file
|
|
23
|
-
okstra wizard init --state-file <path> --project-root <p> --project-id <id> --host-runtime <runtime>
|
|
23
|
+
okstra wizard init --state-file <path> --project-root <p> --project-id <id> --host-runtime <runtime> \\
|
|
24
|
+
--entry-mode current-session --available-function <semantic-name>
|
|
24
25
|
okstra wizard step --state-file <path> [--answer <value>]
|
|
25
26
|
okstra wizard render-args --state-file <path>
|
|
26
27
|
okstra wizard confirmation --state-file <path>
|
|
@@ -40,6 +41,13 @@ All subcommands emit a single JSON object on stdout. On validation failure
|
|
|
40
41
|
// callers pass any value (including empty) unambiguously.
|
|
41
42
|
export function parseFlags(args) {
|
|
42
43
|
const out = {};
|
|
44
|
+
const setFlag = (key, value) => {
|
|
45
|
+
if (key === "available-function") {
|
|
46
|
+
out[key] = [...(out[key] ?? []), value];
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
out[key] = value;
|
|
50
|
+
};
|
|
43
51
|
for (let i = 0; i < args.length; i += 1) {
|
|
44
52
|
const a = args[i];
|
|
45
53
|
if (!a.startsWith("--")) {
|
|
@@ -47,19 +55,38 @@ export function parseFlags(args) {
|
|
|
47
55
|
}
|
|
48
56
|
const eq = a.indexOf("=");
|
|
49
57
|
if (eq !== -1) {
|
|
50
|
-
|
|
58
|
+
setFlag(a.slice(2, eq), a.slice(eq + 1));
|
|
51
59
|
continue;
|
|
52
60
|
}
|
|
53
61
|
const key = a.slice(2);
|
|
54
62
|
if (i + 1 >= args.length) {
|
|
55
63
|
throw new Error(`flag --${key} requires a value`);
|
|
56
64
|
}
|
|
57
|
-
|
|
65
|
+
setFlag(key, args[i + 1]);
|
|
58
66
|
i += 1;
|
|
59
67
|
}
|
|
60
68
|
return out;
|
|
61
69
|
}
|
|
62
70
|
|
|
71
|
+
export function buildPythonArgs(sub, flags, workspaceRoot = "") {
|
|
72
|
+
const pyArgs = [sub, "--state-file", flags["state-file"]];
|
|
73
|
+
if (sub === "init") {
|
|
74
|
+
pyArgs.push("--workspace-root", workspaceRoot);
|
|
75
|
+
pyArgs.push("--project-root", flags["project-root"]);
|
|
76
|
+
pyArgs.push("--project-id", flags["project-id"]);
|
|
77
|
+
if (flags["host-runtime"]) {
|
|
78
|
+
pyArgs.push("--host-runtime", flags["host-runtime"]);
|
|
79
|
+
}
|
|
80
|
+
for (const value of flags["available-function"] ?? []) {
|
|
81
|
+
pyArgs.push("--available-function", value);
|
|
82
|
+
}
|
|
83
|
+
pyArgs.push("--entry-mode", flags["entry-mode"] ?? "current-session");
|
|
84
|
+
} else if (sub === "step" && flags.answer !== undefined) {
|
|
85
|
+
pyArgs.push("--answer", flags.answer);
|
|
86
|
+
}
|
|
87
|
+
return pyArgs;
|
|
88
|
+
}
|
|
89
|
+
|
|
63
90
|
export async function run(args) {
|
|
64
91
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
65
92
|
process.stdout.write(USAGE);
|
|
@@ -95,23 +122,16 @@ export async function run(args) {
|
|
|
95
122
|
return 2;
|
|
96
123
|
}
|
|
97
124
|
|
|
98
|
-
|
|
99
|
-
const pyArgs = [sub, "--state-file", flags["state-file"]];
|
|
125
|
+
let workspaceRoot = "";
|
|
100
126
|
if (sub === "init") {
|
|
101
127
|
if (!flags["project-root"] || !flags["project-id"]) {
|
|
102
128
|
process.stderr.write("error: init requires --project-root and --project-id\n");
|
|
103
129
|
return 2;
|
|
104
130
|
}
|
|
105
131
|
const paths = await resolvePaths();
|
|
106
|
-
|
|
107
|
-
pyArgs.push("--project-root", flags["project-root"]);
|
|
108
|
-
pyArgs.push("--project-id", flags["project-id"]);
|
|
109
|
-
if (flags["host-runtime"]) {
|
|
110
|
-
pyArgs.push("--host-runtime", flags["host-runtime"]);
|
|
111
|
-
}
|
|
112
|
-
} else if (sub === "step" && flags.answer !== undefined) {
|
|
113
|
-
pyArgs.push("--answer", flags.answer);
|
|
132
|
+
workspaceRoot = paths.workspace;
|
|
114
133
|
}
|
|
134
|
+
const pyArgs = buildPythonArgs(sub, flags, workspaceRoot);
|
|
115
135
|
|
|
116
136
|
const result = await runPythonModule({
|
|
117
137
|
module: "okstra_ctl.wizard",
|
|
@@ -5,7 +5,7 @@ import { resolvePaths } from "../../lib/paths.mjs";
|
|
|
5
5
|
import { fileExists, runProcess } from "../../lib/proc.mjs";
|
|
6
6
|
import { runPythonSnippet } from "../../lib/python-helper.mjs";
|
|
7
7
|
import { runtimeIncludesClaudeAssets, validateInstallRuntime } from "./install.mjs";
|
|
8
|
-
import { resolveRuntime } from "../../lib/
|
|
8
|
+
import { resolveRuntime } from "../../lib/host-registry-client.mjs";
|
|
9
9
|
|
|
10
10
|
const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
11
11
|
const REQUIRED_SKILLS = ["okstra-setup", "okstra-run"];
|
|
@@ -14,7 +14,7 @@ const USAGE = `okstra doctor — diagnose the installed runtime
|
|
|
14
14
|
|
|
15
15
|
Usage:
|
|
16
16
|
okstra doctor Run all checks and print a summary
|
|
17
|
-
okstra doctor --runtime <
|
|
17
|
+
okstra doctor --runtime <host-id|alias|all>
|
|
18
18
|
okstra doctor --phase <phase>
|
|
19
19
|
okstra doctor --json Machine-readable result
|
|
20
20
|
Checks:
|
|
@@ -48,7 +48,7 @@ export function parseDoctorArgs(args) {
|
|
|
48
48
|
} else if (arg === "--runtime" || arg.startsWith("--runtime=")) {
|
|
49
49
|
const value = arg.startsWith("--runtime=") ? arg.slice("--runtime=".length) : args[i + 1];
|
|
50
50
|
if (!value || value.startsWith("--")) {
|
|
51
|
-
throw new Error("--runtime requires
|
|
51
|
+
throw new Error("--runtime requires a host ID, alias, or all");
|
|
52
52
|
}
|
|
53
53
|
result.runtime = validateInstallRuntime(value);
|
|
54
54
|
if (arg === "--runtime") i++;
|
|
@@ -66,8 +66,8 @@ export function parseDoctorArgs(args) {
|
|
|
66
66
|
return result;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export function requiredSkillNamesForRuntime(
|
|
70
|
-
return runtimeIncludesClaudeAssets(
|
|
69
|
+
export function requiredSkillNamesForRuntime(resolution) {
|
|
70
|
+
return runtimeIncludesClaudeAssets(resolution) ? REQUIRED_SKILLS : [];
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
export function phaseDiagnosticsToChecks(phase, phaseDiagnostics) {
|
|
@@ -194,7 +194,7 @@ export async function run(args) {
|
|
|
194
194
|
return 2;
|
|
195
195
|
}
|
|
196
196
|
const paths = await resolvePaths();
|
|
197
|
-
const runtimeResolution = resolveRuntime({
|
|
197
|
+
const runtimeResolution = await resolveRuntime({
|
|
198
198
|
requestedRuntime: opts.runtime,
|
|
199
199
|
command: "doctor",
|
|
200
200
|
env: process.env,
|
|
@@ -204,8 +204,8 @@ export async function run(args) {
|
|
|
204
204
|
// auto-detection only means we cannot attribute the optional skill checks to a
|
|
205
205
|
// runtime, so degrade it to a warning and keep running the host-independent
|
|
206
206
|
// checks. Dispatch callers of resolveRuntime stay fail-closed by design — this
|
|
207
|
-
// leniency lives only here.
|
|
208
|
-
//
|
|
207
|
+
// leniency lives only here. A failed resolution has no install targets, so
|
|
208
|
+
// skill checks skip; pass --runtime to include them.
|
|
209
209
|
const resolvedRuntime = runtimeResolution.ok ? runtimeResolution.resolvedRuntime : null;
|
|
210
210
|
|
|
211
211
|
// docker is OPTIONAL for okstra (only the container runtime needs it). Like
|
|
@@ -231,7 +231,7 @@ export async function run(args) {
|
|
|
231
231
|
await check("bin: okstra-provider-exec.py", () => checkBashEntry(paths.bin, "okstra-provider-exec.py")),
|
|
232
232
|
await check("bin: okstra-central.sh", () => checkBashEntry(paths.bin, "okstra-central.sh")),
|
|
233
233
|
...(await Promise.all(
|
|
234
|
-
requiredSkillNamesForRuntime(
|
|
234
|
+
requiredSkillNamesForRuntime(runtimeResolution.ok ? runtimeResolution : null).map((name) =>
|
|
235
235
|
check(`skill: ${name}`, async () => {
|
|
236
236
|
const p = join(CLAUDE_SKILLS_DIR, name, "SKILL.md");
|
|
237
237
|
return (await fileExists(p))
|
|
@@ -278,7 +278,7 @@ export async function run(args) {
|
|
|
278
278
|
|
|
279
279
|
if (!runtimeResolution.ok) {
|
|
280
280
|
process.stdout.write(
|
|
281
|
-
` [WARN] runtime host: ${runtimeResolution.reason} skill checks skipped — pass --runtime <
|
|
281
|
+
` [WARN] runtime host: ${runtimeResolution.reason} skill checks skipped — pass --runtime <host-id|alias> to include them.\n`,
|
|
282
282
|
);
|
|
283
283
|
}
|
|
284
284
|
if (dockerWarning) {
|