okstra 0.180.0 → 0.183.2
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/dist/cli-registry.mjs +16 -2
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.d.mts +4 -2
- package/dist/commands/execute/render-bundle.mjs +46 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +11 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/inspect/model-io.d.mts +1 -0
- package/dist/commands/inspect/model-io.mjs +25 -0
- package/dist/commands/inspect/model-io.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.mjs +29 -8
- package/dist/commands/inspect/stage-map.mjs.map +1 -1
- package/dist/commands/inspect/task-list.mjs +52 -6
- package/dist/commands/inspect/task-list.mjs.map +1 -1
- package/dist/commands/inspect/user-response.mjs +14 -4
- package/dist/commands/inspect/user-response.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.d.mts +1 -0
- package/dist/commands/lifecycle/check-project.mjs +69 -50
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/contract-check.d.mts +1 -0
- package/dist/commands/lifecycle/contract-check.mjs +18 -0
- package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.mjs +154 -51
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/pr/pr.d.mts +1 -0
- package/dist/commands/pr/pr.mjs +19 -1
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/dist/commands/report/agent-activity.mjs +2 -2
- package/dist/commands/report/translate.mjs +3 -0
- package/dist/commands/report/translate.mjs.map +1 -1
- package/dist/lib/host-registry-client.mjs +13 -9
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/docs/architecture.md +11 -0
- package/docs/cli.md +26 -13
- package/docs/container.md +6 -4
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
- package/docs/for-ai/skills/okstra-code-review.md +4 -4
- package/docs/for-ai/skills/okstra-container-build.md +20 -17
- package/docs/for-ai/skills/okstra-inspect.md +20 -23
- package/docs/for-ai/skills/okstra-manager.md +19 -18
- package/docs/for-ai/skills/okstra-memory.md +2 -2
- package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
- package/docs/for-ai/skills/okstra-rollup.md +14 -13
- package/docs/for-ai/skills/okstra-run.md +7 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
- package/docs/for-ai/skills/okstra-setup.md +7 -7
- package/docs/for-ai/skills/okstra-usage.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +50 -32
- package/docs/project-structure-overview.md +29 -26
- package/docs/task-process/implementation.md +7 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/report-writer-worker.md +3 -3
- package/runtime/agents/workers/translator-worker.md +5 -13
- package/runtime/bin/okstra-error-log.py +51 -11
- package/runtime/bin/okstra-report-translate.py +210 -23
- package/runtime/prompts/host-orchestration/implementation.md +1 -1
- package/runtime/prompts/launch.template.md +3 -7
- package/runtime/prompts/lead/context-loader.md +41 -141
- package/runtime/prompts/lead/convergence.md +8 -6
- package/runtime/prompts/lead/okstra-lead-contract.md +25 -35
- package/runtime/prompts/lead/plan-body-verification.md +187 -14
- package/runtime/prompts/lead/report-writer.md +20 -1
- package/runtime/prompts/lead/team-contract.md +8 -53
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +8 -2
- package/runtime/prompts/profiles/release-handoff.md +0 -1
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +6 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
- package/runtime/python/okstra_ctl/agent_activity.py +118 -35
- package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +65 -18
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +81 -1
- package/runtime/python/okstra_ctl/approval_decisions.py +3 -2
- package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
- package/runtime/python/okstra_ctl/backfill.py +13 -10
- package/runtime/python/okstra_ctl/batch.py +2 -4
- package/runtime/python/okstra_ctl/build_tools.py +6 -3
- package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
- package/runtime/python/okstra_ctl/clarification_items.py +27 -13
- package/runtime/python/okstra_ctl/cmux.py +130 -52
- package/runtime/python/okstra_ctl/code_review_target.py +34 -8
- package/runtime/python/okstra_ctl/consumers.py +5 -4
- package/runtime/python/okstra_ctl/container.py +103 -8
- package/runtime/python/okstra_ctl/context_cost.py +2 -1
- package/runtime/python/okstra_ctl/contract_graph.py +497 -0
- package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
- package/runtime/python/okstra_ctl/convergence.py +338 -17
- package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
- package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
- package/runtime/python/okstra_ctl/convergence_store.py +55 -34
- package/runtime/python/okstra_ctl/design_prep.py +7 -4
- package/runtime/python/okstra_ctl/dispatch_core.py +35 -65
- package/runtime/python/okstra_ctl/dispatch_state.py +134 -59
- package/runtime/python/okstra_ctl/doctor.py +6 -3
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
- package/runtime/python/okstra_ctl/error_log_write.py +35 -30
- package/runtime/python/okstra_ctl/error_report.py +26 -1
- package/runtime/python/okstra_ctl/error_zip.py +27 -5
- package/runtime/python/okstra_ctl/execution_identity.py +3 -2
- package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
- package/runtime/python/okstra_ctl/final_report_schema.py +2 -2
- package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
- package/runtime/python/okstra_ctl/fixed_text.py +39 -0
- package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
- package/runtime/python/okstra_ctl/handoff.py +5 -4
- package/runtime/python/okstra_ctl/i18n.py +4 -2
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
- package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
- package/runtime/python/okstra_ctl/incremental_carry.py +2 -1
- package/runtime/python/okstra_ctl/incremental_scope.py +8 -3
- package/runtime/python/okstra_ctl/index.py +8 -11
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
- package/runtime/python/okstra_ctl/invocation.py +3 -6
- package/runtime/python/okstra_ctl/json_boundary.py +366 -0
- package/runtime/python/okstra_ctl/json_registry.py +10 -12
- package/runtime/python/okstra_ctl/jsonl.py +19 -2
- package/runtime/python/okstra_ctl/lead_events.py +33 -1
- package/runtime/python/okstra_ctl/listing.py +3 -3
- package/runtime/python/okstra_ctl/log_report.py +24 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -7
- package/runtime/python/okstra_ctl/manager_store.py +12 -10
- package/runtime/python/okstra_ctl/material.py +5 -1
- package/runtime/python/okstra_ctl/migrate.py +29 -25
- package/runtime/python/okstra_ctl/model_cli.py +3 -15
- package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
- package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
- package/runtime/python/okstra_ctl/paths.py +9 -0
- package/runtime/python/okstra_ctl/plan_items.py +115 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +513 -18
- package/runtime/python/okstra_ctl/pr_template.py +3 -2
- package/runtime/python/okstra_ctl/project_meta.py +5 -7
- package/runtime/python/okstra_ctl/recap.py +5 -4
- package/runtime/python/okstra_ctl/reconcile.py +21 -27
- package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
- package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
- package/runtime/python/okstra_ctl/render.py +26 -15
- package/runtime/python/okstra_ctl/render_final_report.py +3 -2
- package/runtime/python/okstra_ctl/report_assembly.py +172 -17
- package/runtime/python/okstra_ctl/report_finalize.py +7 -10
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_language.py +3 -2
- package/runtime/python/okstra_ctl/report_markdown.py +13 -1
- package/runtime/python/okstra_ctl/report_narrative.py +40 -8
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
- package/runtime/python/okstra_ctl/report_views.py +3 -2
- package/runtime/python/okstra_ctl/rollup.py +65 -4
- package/runtime/python/okstra_ctl/run.py +159 -56
- package/runtime/python/okstra_ctl/run_audit.py +3 -2
- package/runtime/python/okstra_ctl/run_context.py +6 -9
- package/runtime/python/okstra_ctl/run_index_row.py +2 -8
- package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +4 -2
- package/runtime/python/okstra_ctl/session_transcript.py +27 -1
- package/runtime/python/okstra_ctl/set_work_status.py +64 -38
- package/runtime/python/okstra_ctl/stage_fix_carry.py +4 -2
- package/runtime/python/okstra_ctl/stage_map.py +26 -6
- package/runtime/python/okstra_ctl/stage_targets.py +3 -4
- package/runtime/python/okstra_ctl/team.py +2 -1
- package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
- package/runtime/python/okstra_ctl/time_report.py +51 -4
- package/runtime/python/okstra_ctl/usage_identity.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +58 -4
- package/runtime/python/okstra_ctl/user_response.py +1431 -66
- package/runtime/python/okstra_ctl/wizard.py +11 -44
- package/runtime/python/okstra_ctl/work_categories.py +3 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -7
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
- package/runtime/python/okstra_ctl/worker_runner.py +14 -12
- package/runtime/python/okstra_ctl/workflow.py +2 -1
- package/runtime/python/okstra_ctl/worktree.py +3 -2
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
- package/runtime/python/okstra_ctl/write_policy.py +4 -2
- package/runtime/python/okstra_token_usage/antigravity.py +39 -12
- package/runtime/python/okstra_token_usage/collect.py +90 -38
- package/runtime/python/okstra_token_usage/grok.py +127 -0
- package/runtime/schemas/final-report-v2.0.schema.json +9 -0
- package/runtime/schemas/final-report-v3.0.schema.json +9 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
- package/runtime/skills/okstra-code-review/SKILL.md +21 -11
- package/runtime/skills/okstra-container-build/SKILL.md +18 -18
- package/runtime/skills/okstra-inspect/SKILL.md +12 -11
- package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
- package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
- package/runtime/skills/okstra-inspect/facets/history.md +9 -14
- package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
- package/runtime/skills/okstra-inspect/facets/report.md +6 -10
- package/runtime/skills/okstra-inspect/facets/status.md +9 -8
- package/runtime/skills/okstra-inspect/facets/time.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +16 -14
- package/runtime/skills/okstra-memory/SKILL.md +3 -3
- package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
- package/runtime/skills/okstra-rollup/SKILL.md +6 -16
- package/runtime/skills/okstra-run/SKILL.md +7 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
- package/runtime/skills/okstra-setup/SKILL.md +21 -13
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/skills/okstra-usage/SKILL.md +10 -10
- package/runtime/skills/okstra-user-response/SKILL.md +78 -107
- package/runtime/templates/report-writer-prompt-preamble.md +17 -1
- package/runtime/templates/reports/schedule.template.md +4 -4
- package/runtime/templates/worker-error-contract.md +17 -29
- package/runtime/validators/validate-run.py +379 -63
- package/runtime/validators/validate_session_conformance.py +43 -7
|
@@ -94,6 +94,7 @@ from okstra_ctl.clarification_items import (
|
|
|
94
94
|
user_response_sidecars,
|
|
95
95
|
)
|
|
96
96
|
from okstra_ctl.convergence_store import write_json_atomic
|
|
97
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
97
98
|
from okstra_ctl.incremental_scope import (
|
|
98
99
|
parse_stage_graph,
|
|
99
100
|
preview_link_availability_for_report,
|
|
@@ -869,13 +870,13 @@ def _validate_sidecar_option(plan_path: Path, option_name: str, errors_t: dict)
|
|
|
869
870
|
candidates: list[str] = []
|
|
870
871
|
if data_path.is_file():
|
|
871
872
|
try:
|
|
872
|
-
data =
|
|
873
|
+
data = load_owned_object(data_path, artifact="planning final report")
|
|
873
874
|
planning = data.get("implementationPlanning") or {}
|
|
874
875
|
candidates = [
|
|
875
876
|
c.get("name", "") for c in planning.get("optionCandidates") or []
|
|
876
877
|
if isinstance(c, dict) and c.get("name")
|
|
877
878
|
]
|
|
878
|
-
except (OSError,
|
|
879
|
+
except (OSError, JsonBoundaryError):
|
|
879
880
|
candidates = []
|
|
880
881
|
if option_name not in candidates:
|
|
881
882
|
raise WizardError(
|
|
@@ -2002,8 +2003,8 @@ def _load_wizard_root(workspace_root: str) -> dict:
|
|
|
2002
2003
|
"Re-run `okstra install` or check the workspace_root."
|
|
2003
2004
|
)
|
|
2004
2005
|
try:
|
|
2005
|
-
raw =
|
|
2006
|
-
except
|
|
2006
|
+
raw = load_owned_object(path, artifact="wizard prompts")
|
|
2007
|
+
except JsonBoundaryError as exc:
|
|
2007
2008
|
raise WizardError(f"wizard prompt JSON malformed: {path}: {exc}") from exc
|
|
2008
2009
|
if not isinstance(raw, dict):
|
|
2009
2010
|
raise WizardError(f"wizard prompt JSON root must be an object: {path}")
|
|
@@ -4217,7 +4218,7 @@ def _prior_stage_numbers(state: WizardState) -> set[int]:
|
|
|
4217
4218
|
)
|
|
4218
4219
|
data_path = final_report_data_path(report)
|
|
4219
4220
|
try:
|
|
4220
|
-
data =
|
|
4221
|
+
data = load_owned_object(data_path, artifact="planning final report")
|
|
4221
4222
|
stages = {num for num, _ in parse_stage_graph(data)}
|
|
4222
4223
|
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
4223
4224
|
raise WizardError(
|
|
@@ -4300,8 +4301,8 @@ def _suggest_project_pr_template(state: WizardState) -> str:
|
|
|
4300
4301
|
if not project_json.is_file():
|
|
4301
4302
|
return ""
|
|
4302
4303
|
try:
|
|
4303
|
-
data =
|
|
4304
|
-
except (OSError,
|
|
4304
|
+
data = load_owned_object(project_json, artifact="project config")
|
|
4305
|
+
except (OSError, JsonBoundaryError):
|
|
4305
4306
|
return ""
|
|
4306
4307
|
val = data.get("prTemplatePath") or ""
|
|
4307
4308
|
return val if isinstance(val, str) else ""
|
|
@@ -5138,39 +5139,6 @@ def _worktree_preview_line_impl(state: WizardState) -> str:
|
|
|
5138
5139
|
stage=stage, path=str(decision.path), branch=decision.branch)
|
|
5139
5140
|
|
|
5140
5141
|
|
|
5141
|
-
def _critic_collision_lines(state: WizardState) -> list[str]:
|
|
5142
|
-
"""critic provider 가 분석 로스터와 겹칠 때 그 대가를 확인 화면에서 말한다.
|
|
5143
|
-
|
|
5144
|
-
gap 검증 투표자는 `convergence_engine._is_critic_worker` 가 critic **provider
|
|
5145
|
-
이름**으로 걸러낸다 — critic 워커 id 가 아니라 provider 다. 그래서 같은
|
|
5146
|
-
provider 의 분석 워커가 투표에서 빠지고, 분석 워커가 둘뿐인 로스터에서는
|
|
5147
|
-
정족수가 1로 준다. 그 사실이 선택 시점에는 어디에도 드러나지 않아, critic 도
|
|
5148
|
-
돌고 gap 검증 라운드도 완료된 뒤 `apply-critic-gaps` 가 거부할 때에야 알려졌다
|
|
5149
|
-
— 이미 워커 실행 비용을 쓴 다음이다.
|
|
5150
|
-
|
|
5151
|
-
막지는 않는다. 겹치는 provider 를 고르는 것이 잘못은 아니고, 그 대가를 알고
|
|
5152
|
-
고르는 것과 모르고 고르는 것의 차이만 없앤다.
|
|
5153
|
-
"""
|
|
5154
|
-
if not state.critic or state.critic == "off":
|
|
5155
|
-
return []
|
|
5156
|
-
analysers = [
|
|
5157
|
-
worker for worker in _resolved_roster(state) if worker != "report-writer"
|
|
5158
|
-
]
|
|
5159
|
-
excluded = [
|
|
5160
|
-
worker for worker in analysers
|
|
5161
|
-
if worker in {state.critic, f"{state.critic}-worker"}
|
|
5162
|
-
]
|
|
5163
|
-
if not excluded:
|
|
5164
|
-
return []
|
|
5165
|
-
return [
|
|
5166
|
-
_msg(
|
|
5167
|
-
state.workspace_root, "confirmation", "critic_shares_analyser_provider",
|
|
5168
|
-
worker=excluded[0],
|
|
5169
|
-
remaining=len(analysers) - len(excluded),
|
|
5170
|
-
)
|
|
5171
|
-
]
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
5142
|
def _build_confirm(state: WizardState) -> Prompt:
|
|
5175
5143
|
t = _p(state.workspace_root, "confirm")
|
|
5176
5144
|
return Prompt(
|
|
@@ -6496,7 +6464,6 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
6496
6464
|
if (not role_selection
|
|
6497
6465
|
and state.task_type in ("requirements-discovery", "error-analysis", "implementation-planning", "final-verification")):
|
|
6498
6466
|
lines.append(f" critic : {state.critic or '(off)'}")
|
|
6499
|
-
lines.extend(_critic_collision_lines(state))
|
|
6500
6467
|
if state.task_type in _STAGE_SCOPED_TASK_TYPES:
|
|
6501
6468
|
lines.append(f" approved-plan : {state.approved_plan_path}")
|
|
6502
6469
|
stage = wizard_stage_confirmation_label(
|
|
@@ -6631,8 +6598,8 @@ def _matching_v2_companion(
|
|
|
6631
6598
|
source_metadata: dict[str, str],
|
|
6632
6599
|
) -> dict[str, Any] | None:
|
|
6633
6600
|
try:
|
|
6634
|
-
payload =
|
|
6635
|
-
except (OSError, UnicodeError,
|
|
6601
|
+
payload = load_owned_object(path, artifact="wizard state")
|
|
6602
|
+
except (OSError, UnicodeError, JsonBoundaryError):
|
|
6636
6603
|
return None
|
|
6637
6604
|
if not isinstance(payload, dict):
|
|
6638
6605
|
return None
|
|
@@ -6661,7 +6628,7 @@ def _load_matching_v2_state(
|
|
|
6661
6628
|
def load_state_file(path: Path) -> WizardState:
|
|
6662
6629
|
source_path = Path(path)
|
|
6663
6630
|
source = source_path.read_bytes()
|
|
6664
|
-
data =
|
|
6631
|
+
data = load_owned_object(source_path, artifact="wizard state")
|
|
6665
6632
|
source_version = data.get("executionIdentityVersion", 1)
|
|
6666
6633
|
source_metadata = _legacy_source_metadata(source_path, source)
|
|
6667
6634
|
source_digest = source_metadata["sha256"]
|
|
@@ -12,6 +12,7 @@ import json
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
14
|
from .paths import task_dir, task_manifest_file
|
|
15
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
15
16
|
|
|
16
17
|
WORK_CATEGORIES: tuple[str, ...] = (
|
|
17
18
|
"bugfix",
|
|
@@ -32,8 +33,8 @@ def recorded_work_category(project_root: Path, task_group: str, task_id: str) ->
|
|
|
32
33
|
"""task-manifest.json 에 이미 기록된 work-category. 없거나 enum 밖이면 ""."""
|
|
33
34
|
manifest = task_manifest_file(task_dir(project_root, task_group, task_id))
|
|
34
35
|
try:
|
|
35
|
-
data =
|
|
36
|
-
except
|
|
36
|
+
data = load_owned_object(manifest, artifact="task manifest")
|
|
37
|
+
except JsonBoundaryError:
|
|
37
38
|
return ""
|
|
38
39
|
value = str(data.get("workCategory") or "").strip()
|
|
39
40
|
return value if is_valid_category(value) else ""
|
|
@@ -89,11 +89,17 @@ def report_writer_prompt_body(
|
|
|
89
89
|
team_state: Mapping[str, Any],
|
|
90
90
|
model: str,
|
|
91
91
|
report_language: str,
|
|
92
|
+
synthesis_packet_path: str = "",
|
|
92
93
|
) -> list[str]:
|
|
93
94
|
"""Render the provider-neutral body for the report-writer audience."""
|
|
94
95
|
if str(manifest.get("reportContractVersion") or "") == "3.0":
|
|
95
96
|
return _report_writer_v3_body(
|
|
96
|
-
manifest,
|
|
97
|
+
manifest,
|
|
98
|
+
active_context,
|
|
99
|
+
team_state,
|
|
100
|
+
model,
|
|
101
|
+
report_language,
|
|
102
|
+
synthesis_packet_path,
|
|
97
103
|
)
|
|
98
104
|
return [
|
|
99
105
|
f"**Model:** Report writer worker, {model}",
|
|
@@ -106,9 +112,11 @@ def report_writer_prompt_body(
|
|
|
106
112
|
f"- Task type: `{_require_string(manifest, 'taskType')}`",
|
|
107
113
|
"",
|
|
108
114
|
"## Inputs",
|
|
109
|
-
*
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
*(
|
|
116
|
+
[f"- Report synthesis packet: `{synthesis_packet_path}`"]
|
|
117
|
+
if synthesis_packet_path
|
|
118
|
+
else report_writer_input_lines(manifest, active_context, team_state)
|
|
119
|
+
),
|
|
112
120
|
"",
|
|
113
121
|
"## Output Contract",
|
|
114
122
|
"You are the author of TWO files:",
|
|
@@ -134,6 +142,7 @@ def _report_writer_v3_body(
|
|
|
134
142
|
team_state: Mapping[str, Any],
|
|
135
143
|
model: str,
|
|
136
144
|
report_language: str,
|
|
145
|
+
synthesis_packet_path: str,
|
|
137
146
|
) -> list[str]:
|
|
138
147
|
return [
|
|
139
148
|
f"**Model:** Report writer worker, {model}",
|
|
@@ -146,9 +155,11 @@ def _report_writer_v3_body(
|
|
|
146
155
|
f"- Task type: `{_require_string(manifest, 'taskType')}`",
|
|
147
156
|
"",
|
|
148
157
|
"## Inputs",
|
|
149
|
-
*
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
*(
|
|
159
|
+
[f"- Report synthesis packet: `{synthesis_packet_path}`"]
|
|
160
|
+
if synthesis_packet_path
|
|
161
|
+
else report_writer_input_lines(manifest, active_context, team_state)
|
|
162
|
+
),
|
|
152
163
|
"",
|
|
153
164
|
"## Output Contract",
|
|
154
165
|
"You are the author of the report narrative Markdown at Result Path.",
|
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from typing import Any, Iterable, Mapping, Sequence
|
|
9
9
|
|
|
10
10
|
from .worker_prompt_body import analysis_worker_label
|
|
11
|
+
from .json_boundary import load_owned_object
|
|
11
12
|
from .worker_prompt_policy import (
|
|
12
13
|
ERRORS_PATH_HEADERS,
|
|
13
14
|
IMPLEMENTATION_HEADERS,
|
|
@@ -389,8 +390,8 @@ def _validate_record_metadata(text: str, record: PromptRecord) -> list[str]:
|
|
|
389
390
|
if record.metadata_path is None:
|
|
390
391
|
return [*errors, "agent invocation metadata path is required"]
|
|
391
392
|
try:
|
|
392
|
-
metadata =
|
|
393
|
-
record.metadata_path
|
|
393
|
+
metadata = load_owned_object(
|
|
394
|
+
record.metadata_path, artifact="agent invocation metadata"
|
|
394
395
|
)
|
|
395
396
|
except (OSError, UnicodeError, ValueError):
|
|
396
397
|
return [*errors, "agent invocation metadata is invalid"]
|
|
@@ -28,6 +28,7 @@ from .domain.worker_exec import (
|
|
|
28
28
|
)
|
|
29
29
|
from .domain.worker_presentation import JsonEvents, Presentation
|
|
30
30
|
from .session_transcript import SessionTranscript
|
|
31
|
+
from .json_boundary import JsonBoundaryError, write_owned_object_atomic
|
|
31
32
|
|
|
32
33
|
LIVE = "live"
|
|
33
34
|
QUIET = "quiet"
|
|
@@ -225,7 +226,11 @@ def _with_served_model_observation(
|
|
|
225
226
|
observation.record(observed)
|
|
226
227
|
return observed
|
|
227
228
|
|
|
228
|
-
return JsonEvents(
|
|
229
|
+
return JsonEvents(
|
|
230
|
+
normalise=presentation.normalise,
|
|
231
|
+
observe=observe,
|
|
232
|
+
join_adjacent_text=presentation.join_adjacent_text,
|
|
233
|
+
)
|
|
229
234
|
|
|
230
235
|
|
|
231
236
|
class _ServedModelObservation:
|
|
@@ -350,9 +355,7 @@ def _pump(
|
|
|
350
355
|
stdin_text: str | None = None,
|
|
351
356
|
) -> tuple[int, bool, int]:
|
|
352
357
|
selector = selectors.DefaultSelector()
|
|
353
|
-
readers = _register_streams(
|
|
354
|
-
selector, process, strategy, transcript, presentation == LIVE
|
|
355
|
-
)
|
|
358
|
+
readers = _register_streams(selector, process, strategy, transcript)
|
|
356
359
|
outgoing = _register_prompt(selector, process, stdin_text)
|
|
357
360
|
closing_text: str | None = None
|
|
358
361
|
last_output = time.monotonic()
|
|
@@ -387,6 +390,7 @@ def _pump(
|
|
|
387
390
|
|
|
388
391
|
for reader in readers.values():
|
|
389
392
|
closing_text = reader.flush() or closing_text
|
|
393
|
+
strategy.flush(transcript)
|
|
390
394
|
|
|
391
395
|
exit_code = process.wait()
|
|
392
396
|
if closing_text is None and isinstance(strategy, JsonEvents):
|
|
@@ -441,13 +445,12 @@ def _register_streams(
|
|
|
441
445
|
process: subprocess.Popen[bytes],
|
|
442
446
|
presentation: Presentation,
|
|
443
447
|
transcript: SessionTranscript,
|
|
444
|
-
live: bool,
|
|
445
448
|
) -> dict[int, _LineReader]:
|
|
446
449
|
"""이 자식이 말하는 스트림마다 전략이 지정한 싱크를 붙인다."""
|
|
447
450
|
assert process.stdout is not None
|
|
448
451
|
streams = {"stdout": process.stdout, "stderr": process.stderr}
|
|
449
452
|
readers: dict[int, _LineReader] = {}
|
|
450
|
-
for channel, sink in presentation.sinks(transcript
|
|
453
|
+
for channel, sink in presentation.sinks(transcript):
|
|
451
454
|
stream = streams[channel]
|
|
452
455
|
assert stream is not None, channel
|
|
453
456
|
selector.register(stream, selectors.EVENT_READ)
|
|
@@ -599,12 +602,11 @@ def _write_status(path: Path, status: Mapping[str, Any]) -> None:
|
|
|
599
602
|
Replaced whole rather than rewritten in place: readers poll this file while
|
|
600
603
|
the worker is still running and must never see a half-written document.
|
|
601
604
|
"""
|
|
602
|
-
temporary = Path(f"{path}.tmp")
|
|
603
605
|
try:
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
606
|
+
write_owned_object_atomic(
|
|
607
|
+
path,
|
|
608
|
+
status,
|
|
609
|
+
artifact="worker wrapper status",
|
|
607
610
|
)
|
|
608
|
-
|
|
609
|
-
except OSError:
|
|
611
|
+
except (OSError, JsonBoundaryError):
|
|
610
612
|
return
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
16
|
from pathlib import Path
|
|
17
|
+
from .json_boundary import load_owned_object
|
|
17
18
|
|
|
18
19
|
PHASE_SEQUENCE = [
|
|
19
20
|
"requirements-discovery",
|
|
@@ -160,7 +161,7 @@ def load_phase_forbidden(workspace_root: Path) -> dict[str, str]:
|
|
|
160
161
|
`prompts/profiles/forbidden-actions.json`.
|
|
161
162
|
"""
|
|
162
163
|
path = Path(workspace_root) / "prompts" / "profiles" / "forbidden-actions.json"
|
|
163
|
-
data =
|
|
164
|
+
data = load_owned_object(path, artifact="forbidden actions profile")
|
|
164
165
|
if "unknown" not in data:
|
|
165
166
|
# compute_workflow_state falls back to the "unknown" entry for any
|
|
166
167
|
# unmapped task_type; without it a missing/renamed key would surface as
|
|
@@ -44,6 +44,7 @@ from typing import Optional
|
|
|
44
44
|
from okstra_project.dirs import okstra_home, project_json_path
|
|
45
45
|
|
|
46
46
|
from .ids import _safe_fs_segment
|
|
47
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
47
48
|
from . import worktree_registry
|
|
48
49
|
from .seeding import (
|
|
49
50
|
SettingsLinkError,
|
|
@@ -415,8 +416,8 @@ def _read_project_json_field(project_root: Path, field: str) -> Optional[tuple[s
|
|
|
415
416
|
if not target.is_file():
|
|
416
417
|
return None
|
|
417
418
|
try:
|
|
418
|
-
data =
|
|
419
|
-
except (OSError,
|
|
419
|
+
data = load_owned_object(target, artifact="project config")
|
|
420
|
+
except (OSError, JsonBoundaryError):
|
|
420
421
|
return None
|
|
421
422
|
if not isinstance(data, dict):
|
|
422
423
|
return None
|
|
@@ -6,6 +6,8 @@ from dataclasses import dataclass
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, Mapping
|
|
8
8
|
|
|
9
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
@dataclass(frozen=True)
|
|
11
13
|
class WrapperStatus:
|
|
@@ -65,8 +67,8 @@ def prompt_derived_paths(prompt_path: Path) -> tuple[Path, ...]:
|
|
|
65
67
|
|
|
66
68
|
def read_wrapper_status(path: Path) -> WrapperStatus | None:
|
|
67
69
|
try:
|
|
68
|
-
raw =
|
|
69
|
-
except (OSError,
|
|
70
|
+
raw = load_owned_object(path, artifact="worker wrapper status")
|
|
71
|
+
except (OSError, JsonBoundaryError):
|
|
70
72
|
return None
|
|
71
73
|
if not isinstance(raw, dict):
|
|
72
74
|
return None
|
|
@@ -9,6 +9,8 @@ from dataclasses import dataclass
|
|
|
9
9
|
from pathlib import Path, PurePosixPath
|
|
10
10
|
from typing import Any, Literal
|
|
11
11
|
|
|
12
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
13
|
+
|
|
12
14
|
BoundaryPrecision = Literal["exact-path", "directory-boundary", "none"]
|
|
13
15
|
|
|
14
16
|
_PRECISION_RANK = {"none": 0, "directory-boundary": 1, "exact-path": 2}
|
|
@@ -538,8 +540,8 @@ def _rooted(project_root: Path, value: str) -> Path:
|
|
|
538
540
|
|
|
539
541
|
def _read_json(path: Path, label: str) -> Mapping[str, Any]:
|
|
540
542
|
try:
|
|
541
|
-
value =
|
|
542
|
-
except (OSError, UnicodeError,
|
|
543
|
+
value = load_owned_object(path, artifact=label)
|
|
544
|
+
except (OSError, UnicodeError, JsonBoundaryError) as exc:
|
|
543
545
|
raise WritePolicyError(f"{label} is invalid: {path}") from exc
|
|
544
546
|
if not isinstance(value, Mapping):
|
|
545
547
|
raise WritePolicyError(f"{label} must be an object: {path}")
|
|
@@ -1,29 +1,56 @@
|
|
|
1
1
|
"""Antigravity (`agy`) CLI usage collector.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
per-run token/cost usage cannot be attributed to the antigravity worker. These
|
|
7
|
-
collectors therefore report "unavailable" rather than fabricating numbers — the
|
|
8
|
-
worker's usage block degrades to `na` and `/okstra-inspect context-cost` shows
|
|
9
|
-
N/A for antigravity. If a future `agy` build exposes a transcript, wire its
|
|
10
|
-
parser here (model ids stay `gemini-*`, so `antigravity_cost_usd` already prices it).
|
|
3
|
+
공식 headless 문서는 ``--output-format json`` / ``stream-json`` 의 최종
|
|
4
|
+
``usage`` 객체에 토큰을 둔다. okstra 는 이미 ``stream-json`` 으로 호출하고
|
|
5
|
+
그 줄을 워커 로그에 남긴다. 정본은 마지막 ``result.usage`` 이다.
|
|
11
6
|
"""
|
|
12
7
|
from __future__ import annotations
|
|
13
8
|
|
|
14
9
|
from pathlib import Path
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
PRINT_MODE_USAGE_NOTE = "N/A — print mode exposes no transcript"
|
|
11
|
+
from .jsonl_io import iter_jsonl
|
|
18
12
|
|
|
19
13
|
|
|
20
14
|
def antigravity_session_total(json_path: Path) -> dict:
|
|
21
|
-
|
|
15
|
+
"""stream-json 로그에서 마지막 result.usage 를 읽는다."""
|
|
16
|
+
result_usage: dict | None = None
|
|
17
|
+
step_usage: dict | None = None
|
|
18
|
+
model: str | None = None
|
|
19
|
+
for record in iter_jsonl(json_path):
|
|
20
|
+
kind = record.get("event")
|
|
21
|
+
if kind == "init":
|
|
22
|
+
init = record.get("init")
|
|
23
|
+
if isinstance(init, dict) and isinstance(init.get("model"), str):
|
|
24
|
+
model = init["model"]
|
|
25
|
+
continue
|
|
26
|
+
if kind == "result":
|
|
27
|
+
result = record.get("result")
|
|
28
|
+
if isinstance(result, dict) and isinstance(result.get("usage"), dict):
|
|
29
|
+
result_usage = result["usage"]
|
|
30
|
+
continue
|
|
31
|
+
if kind == "step_update":
|
|
32
|
+
step = record.get("step_update")
|
|
33
|
+
if isinstance(step, dict) and isinstance(step.get("usage"), dict):
|
|
34
|
+
step_usage = step["usage"]
|
|
35
|
+
usage = result_usage or step_usage
|
|
36
|
+
if usage is None:
|
|
37
|
+
return {"totalTokens": 0, "available": False}
|
|
38
|
+
return {
|
|
39
|
+
"totalTokens": usage.get("total_tokens", 0) or 0,
|
|
40
|
+
"inputTokens": usage.get("input_tokens", 0) or 0,
|
|
41
|
+
"outputTokens": usage.get("output_tokens", 0) or 0,
|
|
42
|
+
"thoughtsTokens": usage.get("thinking_tokens", 0) or 0,
|
|
43
|
+
"cacheReadTokens": usage.get("cache_read_tokens", 0) or 0,
|
|
44
|
+
"model": model,
|
|
45
|
+
"available": True,
|
|
46
|
+
}
|
|
22
47
|
|
|
23
48
|
|
|
24
49
|
def find_antigravity_session(project_root: Path, started_at: str, ended_at: str) -> Path | None:
|
|
25
|
-
|
|
50
|
+
sessions = find_antigravity_sessions(project_root, started_at, ended_at)
|
|
51
|
+
return sessions[-1] if sessions else None
|
|
26
52
|
|
|
27
53
|
|
|
28
54
|
def find_antigravity_sessions(project_root: Path, started_at: str, ended_at: str) -> list[Path]:
|
|
55
|
+
# 세션 파일은 홈 디렉터리가 아니라 워커 로그다. 경로는 호출부가 prompt 에서 붙인다.
|
|
29
56
|
return []
|
|
@@ -14,16 +14,17 @@ from .claude import (
|
|
|
14
14
|
find_claude_team_sessions,
|
|
15
15
|
)
|
|
16
16
|
from .codex import codex_session_total, find_codex_sessions
|
|
17
|
-
from .antigravity import
|
|
18
|
-
|
|
19
|
-
antigravity_session_total,
|
|
20
|
-
find_antigravity_sessions,
|
|
21
|
-
)
|
|
17
|
+
from .antigravity import antigravity_session_total, find_antigravity_sessions
|
|
18
|
+
from .grok import find_grok_sessions, grok_session_total
|
|
22
19
|
from .paths import claude_project_dir, utc_now
|
|
23
20
|
from .pricing import antigravity_cost_usd, provider_cost_usd
|
|
24
21
|
from okstra_ctl.dispatch_state import worker_session_ids
|
|
25
22
|
from okstra_ctl.models import provider_wrappers
|
|
26
|
-
from okstra_ctl.wrapper_status import
|
|
23
|
+
from okstra_ctl.wrapper_status import (
|
|
24
|
+
log_path_for_prompt,
|
|
25
|
+
read_wrapper_status,
|
|
26
|
+
status_path_for_prompt,
|
|
27
|
+
)
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def match_prefixes(worker_id: str) -> list[str]:
|
|
@@ -472,6 +473,8 @@ def _cli_sessions_for_windows(
|
|
|
472
473
|
matches = find_codex_sessions(project_root, started_at, ended_at)
|
|
473
474
|
elif provider == "antigravity":
|
|
474
475
|
matches = find_antigravity_sessions(project_root, started_at, ended_at)
|
|
476
|
+
elif provider == "grok":
|
|
477
|
+
matches = find_grok_sessions(project_root, started_at, ended_at)
|
|
475
478
|
else:
|
|
476
479
|
matches = []
|
|
477
480
|
for path in matches:
|
|
@@ -488,6 +491,8 @@ def _cli_session_totals(provider: str, session_paths: list[Path]) -> list[dict]:
|
|
|
488
491
|
total = codex_session_total(session_path)
|
|
489
492
|
elif provider == "antigravity":
|
|
490
493
|
total = antigravity_session_total(session_path)
|
|
494
|
+
elif provider == "grok":
|
|
495
|
+
total = grok_session_total(session_path)
|
|
491
496
|
else:
|
|
492
497
|
continue
|
|
493
498
|
if total.get("available"):
|
|
@@ -582,24 +587,20 @@ def collect_cli_usage(
|
|
|
582
587
|
execution = wrapper_execution(status_path)
|
|
583
588
|
execution_note = _execution_note(execution)
|
|
584
589
|
|
|
585
|
-
|
|
586
|
-
|
|
590
|
+
totals = _cli_session_totals(provider, sessions)
|
|
591
|
+
if not sessions:
|
|
592
|
+
block = na_block(
|
|
593
|
+
f"{execution_note}; CLI usage attribution unavailable because no transcript was found"
|
|
594
|
+
)
|
|
587
595
|
block["cliNote"] = block["note"]
|
|
596
|
+
elif not totals:
|
|
597
|
+
block = na_block(
|
|
598
|
+
f"{execution_note}; transcript found but no final token snapshot was recorded"
|
|
599
|
+
)
|
|
600
|
+
block["cliNote"] = block["note"]
|
|
601
|
+
block["cliSessionPaths"] = [str(path) for path in sessions]
|
|
588
602
|
else:
|
|
589
|
-
|
|
590
|
-
if not sessions:
|
|
591
|
-
block = na_block(
|
|
592
|
-
f"{execution_note}; CLI usage attribution unavailable because no transcript was found"
|
|
593
|
-
)
|
|
594
|
-
block["cliNote"] = block["note"]
|
|
595
|
-
elif not totals:
|
|
596
|
-
block = na_block(
|
|
597
|
-
f"{execution_note}; transcript found but no final token snapshot was recorded"
|
|
598
|
-
)
|
|
599
|
-
block["cliNote"] = block["note"]
|
|
600
|
-
block["cliSessionPaths"] = [str(path) for path in sessions]
|
|
601
|
-
else:
|
|
602
|
-
block = _cli_usage_block(provider, _aggregate_totals(totals), sessions)
|
|
603
|
+
block = _cli_usage_block(provider, _aggregate_totals(totals), sessions)
|
|
603
604
|
|
|
604
605
|
block["cliExecutionStatus"] = execution["status"]
|
|
605
606
|
if fallback_window_used:
|
|
@@ -624,6 +625,51 @@ def _worker_cli_windows(
|
|
|
624
625
|
return fallback_windows, status_path, True
|
|
625
626
|
|
|
626
627
|
|
|
628
|
+
def _antigravity_log_paths(
|
|
629
|
+
project_root: Path,
|
|
630
|
+
worker: dict,
|
|
631
|
+
status_path: Path | None,
|
|
632
|
+
) -> list[Path]:
|
|
633
|
+
"""agy stream-json 은 워커 로그에 남는다. 홈 세션 디렉터리가 아니다."""
|
|
634
|
+
paths: list[Path] = []
|
|
635
|
+
prompt_path = _resolve_project_path(project_root, str(worker.get("promptPath") or ""))
|
|
636
|
+
if prompt_path is not None:
|
|
637
|
+
log_path = log_path_for_prompt(prompt_path)
|
|
638
|
+
if log_path.is_file():
|
|
639
|
+
paths.append(log_path)
|
|
640
|
+
status = read_wrapper_status(status_path) if status_path else None
|
|
641
|
+
if status is not None and status.log_path is not None and status.log_path.is_file():
|
|
642
|
+
if status.log_path not in paths:
|
|
643
|
+
paths.append(status.log_path)
|
|
644
|
+
return paths
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _worker_cli_usage_block(
|
|
648
|
+
*,
|
|
649
|
+
provider: str,
|
|
650
|
+
project_root: Path,
|
|
651
|
+
worker: dict,
|
|
652
|
+
fallback_windows: list[tuple[str, str]],
|
|
653
|
+
) -> dict:
|
|
654
|
+
"""공급자 프로세스 트랜스크립트와 실행 증거로 워커 사용량 블록을 만든다."""
|
|
655
|
+
windows, status_path, used_fallback = _worker_cli_windows(
|
|
656
|
+
project_root,
|
|
657
|
+
worker,
|
|
658
|
+
fallback_windows,
|
|
659
|
+
)
|
|
660
|
+
session_paths = _cli_sessions_for_windows(provider, project_root, windows)
|
|
661
|
+
if provider == "antigravity":
|
|
662
|
+
for log_path in _antigravity_log_paths(project_root, worker, status_path):
|
|
663
|
+
if log_path not in session_paths:
|
|
664
|
+
session_paths.append(log_path)
|
|
665
|
+
return collect_cli_usage(
|
|
666
|
+
provider=provider,
|
|
667
|
+
status_path=status_path,
|
|
668
|
+
sessions=session_paths,
|
|
669
|
+
fallback_window_used=used_fallback,
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
627
673
|
def _attach_cli_usage(
|
|
628
674
|
block: dict,
|
|
629
675
|
provider: str,
|
|
@@ -638,8 +684,8 @@ def _attach_cli_usage(
|
|
|
638
684
|
window produces several rollout jsonls; pricing only the latest (the old
|
|
639
685
|
``find_codex_session`` -> ``sessions[-1]`` behavior) dropped the earlier
|
|
640
686
|
attempts' tokens. We sum every in-window session here so the redispatched
|
|
641
|
-
CLI spend is fully reported.
|
|
642
|
-
|
|
687
|
+
CLI spend is fully reported. antigravity 는 워커 로그의 stream-json
|
|
688
|
+
``result.usage`` 를 같은 합산 경로에 붙인다.
|
|
643
689
|
"""
|
|
644
690
|
session_paths = _cli_sessions_for_windows(provider, project_root, windows)
|
|
645
691
|
cli = collect_cli_usage(
|
|
@@ -676,17 +722,11 @@ def _collect_cli_runtime_usage(state: dict, project_root: Path) -> dict:
|
|
|
676
722
|
)
|
|
677
723
|
continue
|
|
678
724
|
worker_id = str(worker.get("workerId") or "").strip()
|
|
679
|
-
|
|
680
|
-
project_root,
|
|
681
|
-
worker,
|
|
682
|
-
windows_by_worker.get(worker_id, []),
|
|
683
|
-
)
|
|
684
|
-
session_paths = _cli_sessions_for_windows(provider, project_root, windows)
|
|
685
|
-
worker["usage"] = collect_cli_usage(
|
|
725
|
+
worker["usage"] = _worker_cli_usage_block(
|
|
686
726
|
provider=provider,
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
727
|
+
project_root=project_root,
|
|
728
|
+
worker=worker,
|
|
729
|
+
fallback_windows=windows_by_worker.get(worker_id, []),
|
|
690
730
|
)
|
|
691
731
|
_populate_usage_summary(state, team_name=resolve_team_name(state),
|
|
692
732
|
sessions_found=0, needle_source="none")
|
|
@@ -871,6 +911,7 @@ def collect_claude_runtime_usage(
|
|
|
871
911
|
sessions_dir = claude_project_dir(cwd)
|
|
872
912
|
# sid 로 귀속한 세션 id 전체 — 아래 unattributed 폴드에서 빼는 데 쓴다.
|
|
873
913
|
attributed_sids: set[str] = set()
|
|
914
|
+
cli_windows_by_worker = _codex_worker_windows(cwd, state)
|
|
874
915
|
for worker in state.get("workers", []):
|
|
875
916
|
worker_id = worker.get("workerId")
|
|
876
917
|
agent = worker.get("agent")
|
|
@@ -922,10 +963,21 @@ def collect_claude_runtime_usage(
|
|
|
922
963
|
matched_sids.add(entry[0])
|
|
923
964
|
|
|
924
965
|
if not matched:
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
966
|
+
provider = _cli_assignment_provider(worker)
|
|
967
|
+
if provider and str(worker.get("runner") or "") == "cli-wrapper":
|
|
968
|
+
worker["usage"] = _worker_cli_usage_block(
|
|
969
|
+
provider=provider,
|
|
970
|
+
project_root=cwd,
|
|
971
|
+
worker=worker,
|
|
972
|
+
fallback_windows=cli_windows_by_worker.get(
|
|
973
|
+
str(worker_id or "").strip(), []
|
|
974
|
+
),
|
|
975
|
+
)
|
|
976
|
+
else:
|
|
977
|
+
worker["usage"] = na_block(
|
|
978
|
+
"no Claude session jsonl in the run window for dispatched "
|
|
979
|
+
f"sessionIds {dispatched_sids} or agentName prefixes {prefixes}"
|
|
980
|
+
)
|
|
929
981
|
continue
|
|
930
982
|
|
|
931
983
|
# Stable order by startedAt so the "primary" session is the first one.
|