okstra 0.86.0 → 0.87.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/docs/kr/architecture.md +27 -27
  2. package/docs/kr/cli.md +21 -21
  3. package/docs/kr/performance-improvement-plan-v2.md +4 -4
  4. package/docs/kr/performance-improvement-plan.md +3 -3
  5. package/docs/project-structure-overview.md +6 -6
  6. package/docs/superpowers/plans/2026-06-17-okstra-error-report.md +724 -0
  7. package/docs/superpowers/specs/2026-06-17-okstra-error-report-design.md +123 -0
  8. package/docs/task-process/error-analysis.md +1 -1
  9. package/docs/task-process/final-verification.md +4 -4
  10. package/docs/task-process/implementation-planning.md +2 -2
  11. package/docs/task-process/implementation.md +3 -3
  12. package/docs/task-process/requirements-discovery.md +2 -2
  13. package/package.json +1 -1
  14. package/runtime/BUILD.json +2 -2
  15. package/runtime/agents/workers/{gemini-worker.md → antigravity-worker.md} +56 -56
  16. package/runtime/agents/workers/claude-worker.md +2 -2
  17. package/runtime/agents/workers/codex-worker.md +1 -1
  18. package/runtime/agents/workers/report-writer-worker.md +2 -2
  19. package/runtime/bin/lib/okstra/cli.sh +3 -3
  20. package/runtime/bin/lib/okstra/globals.sh +8 -8
  21. package/runtime/bin/lib/okstra/interactive.sh +1 -1
  22. package/runtime/bin/lib/okstra/usage.sh +7 -7
  23. package/runtime/bin/lib/okstra-ctl/cmd-reconcile.sh +5 -3
  24. package/runtime/bin/lib/okstra-ctl/prepare.sh +4 -2
  25. package/runtime/bin/{okstra-gemini-exec.sh → okstra-antigravity-exec.sh} +67 -60
  26. package/runtime/bin/okstra-error-log.py +2 -1
  27. package/runtime/bin/okstra-token-usage.py +3 -4
  28. package/runtime/bin/okstra-trace-cleanup.sh +4 -4
  29. package/runtime/bin/okstra-wrapper-status.py +3 -3
  30. package/runtime/bin/okstra.sh +1 -1
  31. package/runtime/prompts/launch.template.md +3 -3
  32. package/runtime/prompts/lead/context-loader.md +1 -1
  33. package/runtime/prompts/lead/convergence.md +8 -8
  34. package/runtime/prompts/lead/okstra-lead-contract.md +15 -15
  35. package/runtime/prompts/lead/report-writer.md +6 -6
  36. package/runtime/prompts/lead/team-contract.md +31 -31
  37. package/runtime/prompts/profiles/_coding-conventions-preflight.md +2 -2
  38. package/runtime/prompts/profiles/_common-contract.md +7 -7
  39. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  40. package/runtime/prompts/profiles/_implementation-executor.md +6 -6
  41. package/runtime/prompts/profiles/_implementation-verifier.md +4 -4
  42. package/runtime/prompts/profiles/_stage-discipline.md +1 -1
  43. package/runtime/prompts/profiles/error-analysis.md +1 -1
  44. package/runtime/prompts/profiles/final-verification.md +1 -1
  45. package/runtime/prompts/profiles/forbidden-actions.json +2 -2
  46. package/runtime/prompts/profiles/implementation-planning.md +1 -1
  47. package/runtime/prompts/profiles/implementation.md +3 -3
  48. package/runtime/prompts/profiles/improvement-discovery.md +3 -3
  49. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  50. package/runtime/prompts/wizard/prompts.ko.json +9 -5
  51. package/runtime/python/okstra_ctl/__init__.py +2 -0
  52. package/runtime/python/okstra_ctl/analysis_packet.py +1 -1
  53. package/runtime/python/okstra_ctl/clarification_items.py +17 -7
  54. package/runtime/python/okstra_ctl/codex_dispatch.py +2 -2
  55. package/runtime/python/okstra_ctl/context_cost.py +10 -44
  56. package/runtime/python/okstra_ctl/doctor.py +1 -1
  57. package/runtime/python/okstra_ctl/error_report.py +185 -0
  58. package/runtime/python/okstra_ctl/improvement_lenses.py +1 -1
  59. package/runtime/python/okstra_ctl/models.py +10 -10
  60. package/runtime/python/okstra_ctl/paths.py +9 -9
  61. package/runtime/python/okstra_ctl/reconcile.py +74 -11
  62. package/runtime/python/okstra_ctl/render.py +21 -21
  63. package/runtime/python/okstra_ctl/report_views.py +7 -13
  64. package/runtime/python/okstra_ctl/run.py +16 -16
  65. package/runtime/python/okstra_ctl/run_context.py +1 -1
  66. package/runtime/python/okstra_ctl/seeding.py +1 -1
  67. package/runtime/python/okstra_ctl/task_target.py +44 -0
  68. package/runtime/python/okstra_ctl/team.py +1 -1
  69. package/runtime/python/okstra_ctl/wizard.py +55 -30
  70. package/runtime/python/okstra_ctl/workers.py +3 -3
  71. package/runtime/python/okstra_ctl/workflow.py +1 -1
  72. package/runtime/python/okstra_ctl/worktree.py +1 -1
  73. package/runtime/python/okstra_token_usage/__init__.py +2 -3
  74. package/runtime/python/okstra_token_usage/antigravity.py +26 -0
  75. package/runtime/python/okstra_token_usage/blocks.py +4 -0
  76. package/runtime/python/okstra_token_usage/collect.py +20 -15
  77. package/runtime/python/okstra_token_usage/paths.py +0 -1
  78. package/runtime/python/okstra_token_usage/pricing.py +6 -3
  79. package/runtime/schemas/final-report-v1.0.schema.json +1 -1
  80. package/runtime/skills/okstra-brief/SKILL.md +1 -1
  81. package/runtime/skills/okstra-inspect/SKILL.md +85 -21
  82. package/runtime/skills/okstra-run/SKILL.md +3 -3
  83. package/runtime/skills/okstra-setup/SKILL.md +2 -2
  84. package/runtime/templates/prd/brief.template.md +2 -2
  85. package/runtime/templates/reports/i18n/en.json +1 -1
  86. package/runtime/templates/reports/i18n/ko.json +1 -1
  87. package/runtime/templates/reports/report.js +5 -11
  88. package/runtime/templates/reports/settings.template.json +2 -2
  89. package/runtime/templates/reports/task-brief.template.md +2 -2
  90. package/runtime/templates/worker-prompt-preamble.md +7 -7
  91. package/runtime/validators/forbidden_actions.py +1 -1
  92. package/runtime/validators/lib/fixtures.sh +3 -3
  93. package/runtime/validators/lib/validate-assets.sh +1 -1
  94. package/runtime/validators/validate-run.py +8 -8
  95. package/src/cli-registry.mjs +7 -0
  96. package/src/codex-dispatch.mjs +3 -3
  97. package/src/codex-run.mjs +2 -2
  98. package/src/doctor.mjs +1 -1
  99. package/src/error-report.mjs +27 -0
  100. package/src/install.mjs +1 -1
  101. package/src/render-bundle.mjs +1 -1
  102. package/src/uninstall.mjs +2 -2
  103. package/runtime/python/okstra_token_usage/gemini.py +0 -82
@@ -294,7 +294,7 @@ class PrepareInputs:
294
294
  lead_model: str = ""
295
295
  claude_model: str = ""
296
296
  codex_model: str = ""
297
- gemini_model: str = ""
297
+ antigravity_model: str = ""
298
298
  report_writer_model: str = ""
299
299
  lead_runtime: str = "claude-code"
300
300
  lead_runtime_request: str = ""
@@ -989,7 +989,7 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
989
989
  ("--lead-model", inp.lead_model or ctx.get("LEAD_MODEL", "")),
990
990
  ("--claude-model", inp.claude_model or ctx.get("CLAUDE_WORKER_MODEL", "")),
991
991
  ("--codex-model", inp.codex_model or ctx.get("CODEX_WORKER_MODEL", "")),
992
- ("--gemini-model", inp.gemini_model or ctx.get("GEMINI_WORKER_MODEL", "")),
992
+ ("--antigravity-model", inp.antigravity_model or ctx.get("ANTIGRAVITY_WORKER_MODEL", "")),
993
993
  ("--report-writer-model", inp.report_writer_model or ctx.get("REPORT_WRITER_MODEL", "")),
994
994
  ("--lead-runtime", inp.lead_runtime if inp.lead_runtime != "claude-code" else ""),
995
995
  ("--executor", inp.executor or ctx.get("EXECUTOR_PROVIDER", "")),
@@ -1441,11 +1441,11 @@ class _ModelBindings:
1441
1441
 
1442
1442
 
1443
1443
  def _resolve_worker_models(inp: PrepareInputs) -> dict:
1444
- """lead/claude/codex/gemini/report-writer 모델을 alias → (display, execution) 로 해소."""
1444
+ """lead/claude/codex/antigravity/report-writer 모델을 alias → (display, execution) 로 해소."""
1445
1445
  lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", "opus")
1446
1446
  claude_default = _default("OKSTRA_DEFAULT_CLAUDE_MODEL", "opus")
1447
1447
  codex_default = _default("OKSTRA_DEFAULT_CODEX_MODEL", "gpt-5.5")
1448
- gemini_default = _default("OKSTRA_DEFAULT_GEMINI_MODEL", "auto")
1448
+ antigravity_default = _default("OKSTRA_DEFAULT_ANTIGRAVITY_MODEL", "auto")
1449
1449
  report_writer_default = _default("OKSTRA_DEFAULT_REPORT_WRITER_MODEL", lead_default)
1450
1450
  return {
1451
1451
  "lead": resolve_model_metadata(
@@ -1461,8 +1461,8 @@ def _resolve_worker_models(inp: PrepareInputs) -> dict:
1461
1461
  default_display=codex_default, default_execution=codex_default,
1462
1462
  ),
1463
1463
  "ge": resolve_model_metadata(
1464
- provider="gemini", raw_value=inp.gemini_model,
1465
- default_display=gemini_default, default_execution=gemini_default,
1464
+ provider="antigravity", raw_value=inp.antigravity_model,
1465
+ default_display=antigravity_default, default_execution=antigravity_default,
1466
1466
  ),
1467
1467
  "rw": resolve_model_metadata(
1468
1468
  provider="claude", raw_value=inp.report_writer_model,
@@ -1475,15 +1475,15 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
1475
1475
  """worker 모델 + critic 선택 + executor 바인딩을 한 묶음으로 해소·검증한다."""
1476
1476
  m = _resolve_worker_models(inp)
1477
1477
  critic_choice = (inp.critic or "").strip().lower()
1478
- if critic_choice not in ("", "off", "claude", "codex", "gemini"):
1478
+ if critic_choice not in ("", "off", "claude", "codex", "antigravity"):
1479
1479
  raise PrepareError(
1480
- f"--critic must be one of: off, claude, codex, gemini (got: {critic_choice!r})"
1480
+ f"--critic must be one of: off, claude, codex, antigravity (got: {critic_choice!r})"
1481
1481
  )
1482
1482
  executor_default = _default("OKSTRA_DEFAULT_EXECUTOR", "claude")
1483
1483
  executor_provider = (inp.executor or executor_default).strip().lower()
1484
- if executor_provider not in ("claude", "codex", "gemini"):
1484
+ if executor_provider not in ("claude", "codex", "antigravity"):
1485
1485
  raise PrepareError(
1486
- f"--executor must be one of: claude, codex, gemini (got: {executor_provider!r})"
1486
+ f"--executor must be one of: claude, codex, antigravity (got: {executor_provider!r})"
1487
1487
  )
1488
1488
  if inp.task_type == "implementation" and executor_provider not in workers:
1489
1489
  raise PrepareError(
@@ -1494,7 +1494,7 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
1494
1494
  provider_to_meta = {
1495
1495
  "claude": ("Claude executor", "claude-worker", m["cw"]),
1496
1496
  "codex": ("Codex executor", "codex-worker", m["co"]),
1497
- "gemini": ("Gemini executor", "gemini-worker", m["ge"]),
1497
+ "antigravity": ("Antigravity executor", "antigravity-worker", m["ge"]),
1498
1498
  }
1499
1499
  display_name, worker_agent, model_meta = provider_to_meta[executor_provider]
1500
1500
  return _ModelBindings(
@@ -1916,7 +1916,7 @@ def _persist_run_inputs(
1916
1916
  "leadModel": models.lead.display,
1917
1917
  "claudeModel": models.cw.display,
1918
1918
  "codexModel": models.co.display,
1919
- "geminiModel": models.ge.display,
1919
+ "antigravityModel": models.ge.display,
1920
1920
  "reportWriterModel": models.rw.display,
1921
1921
  "executor": models.executor_provider,
1922
1922
  "relatedTasks": inp.related_tasks_raw,
@@ -2273,8 +2273,8 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
2273
2273
  "CLAUDE_WORKER_MODEL_EXECUTION_VALUE": cw.execution,
2274
2274
  "CODEX_WORKER_MODEL": co.display,
2275
2275
  "CODEX_WORKER_MODEL_EXECUTION_VALUE": co.execution,
2276
- "GEMINI_WORKER_MODEL": ge.display,
2277
- "GEMINI_WORKER_MODEL_EXECUTION_VALUE": ge.execution,
2276
+ "ANTIGRAVITY_WORKER_MODEL": ge.display,
2277
+ "ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE": ge.execution,
2278
2278
  "REPORT_WRITER_MODEL": rw.display,
2279
2279
  "REPORT_WRITER_MODEL_EXECUTION_VALUE": rw.execution,
2280
2280
  "EXECUTOR_PROVIDER": executor_provider,
@@ -2423,7 +2423,7 @@ def main(argv: list[str]) -> int:
2423
2423
  p.add_argument("--lead-model", default="")
2424
2424
  p.add_argument("--claude-model", default="")
2425
2425
  p.add_argument("--codex-model", default="")
2426
- p.add_argument("--gemini-model", default="")
2426
+ p.add_argument("--antigravity-model", default="")
2427
2427
  p.add_argument("--report-writer-model", default="")
2428
2428
  p.add_argument(
2429
2429
  "--lead-runtime",
@@ -2582,7 +2582,7 @@ def main(argv: list[str]) -> int:
2582
2582
  lead_model=args.lead_model,
2583
2583
  claude_model=args.claude_model,
2584
2584
  codex_model=args.codex_model,
2585
- gemini_model=args.gemini_model,
2585
+ antigravity_model=args.antigravity_model,
2586
2586
  report_writer_model=args.report_writer_model,
2587
2587
  lead_runtime=args.lead_runtime,
2588
2588
  lead_runtime_request=args.lead_runtime_request,
@@ -205,7 +205,7 @@ def write_run_inputs(
205
205
 
206
206
  inputs schema (모든 키 optional):
207
207
  taskBriefPath, directive, workers, leadModel, claudeModel, codexModel,
208
- geminiModel, reportWriterModel, relatedTasks, approvedPlanPath,
208
+ antigravityModel, reportWriterModel, relatedTasks, approvedPlanPath,
209
209
  clarificationResponsePath, renderOnly
210
210
  """
211
211
  run_manifests_dir = Path(run_manifests_dir)
@@ -135,7 +135,7 @@ def ensure_project_settings_symlink(*, project_root: Path) -> Optional[Path]:
135
135
 
136
136
  Claude Code 가 그 프로젝트에서 host 세션으로 실행될 때 이 파일을
137
137
  자동으로 로드하므로, okstra worker wrapper 호출(`okstra-codex-exec.sh`,
138
- `okstra-gemini-exec.sh`) 이 별도 `--settings` 인자 없이도 허용된다.
138
+ `okstra-antigravity-exec.sh`) 이 별도 `--settings` 인자 없이도 허용된다.
139
139
 
140
140
  반환값:
141
141
  - target Path: symlink 가 새로 생성되었거나 이미 올바른 위치를
@@ -0,0 +1,44 @@
1
+ """task-key → (task_root, project_root) 해석 공유 헬퍼.
2
+
3
+ read-side CLI(context-cost, error-report)가 동일한 입력 계약을 공유하기 위한
4
+ SSOT. target 은 task-root 경로 / task-key 둘 다 받는다.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ from okstra_project import ResolverError, find_task_root, resolve_project_root
11
+
12
+
13
+ def resolve_task_root(target: str, project_root: str, cwd: str) -> tuple[Path, Path]:
14
+ target_path = Path(target).expanduser()
15
+ if target_path.exists():
16
+ task_root = target_path.resolve()
17
+ return task_root, infer_project_root(task_root)
18
+
19
+ try:
20
+ resolved_project = Path(
21
+ resolve_project_root(explicit_root=project_root, cwd=cwd)
22
+ ).resolve()
23
+ except ResolverError as exc:
24
+ raise SystemExit(f"project root resolution failed: {exc}") from exc
25
+
26
+ task_root = find_task_root(resolved_project, target)
27
+ if task_root is None:
28
+ raise SystemExit(f"no task root for task key: {target}")
29
+ return task_root.resolve(), resolved_project
30
+
31
+
32
+ def infer_project_root(task_root: Path) -> Path:
33
+ parts = task_root.parts
34
+ if ".okstra" not in parts:
35
+ return task_root
36
+ idx = parts.index(".okstra")
37
+ return Path(*parts[:idx]).resolve()
38
+
39
+
40
+ def project_rel(path: Path, project_root: Path) -> str:
41
+ try:
42
+ return str(path.resolve().relative_to(project_root.resolve()))
43
+ except ValueError:
44
+ return str(path)
@@ -20,7 +20,7 @@ from .dispatch_core import (
20
20
  _SUPPORTED_WRAPPERS = {
21
21
  "claude": "okstra-claude-exec.sh",
22
22
  "codex": "okstra-codex-exec.sh",
23
- "gemini": "okstra-gemini-exec.sh",
23
+ "antigravity": "okstra-antigravity-exec.sh",
24
24
  "report-writer": "okstra-claude-exec.sh",
25
25
  }
26
26
  _TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
@@ -31,7 +31,7 @@ from okstra_ctl.models import (
31
31
  UnknownModelError,
32
32
  resolve_model_metadata,
33
33
  )
34
- from okstra_ctl.clarification_items import scan_approval_gate
34
+ from okstra_ctl.clarification_items import scan_approval_gate, user_response_sidecars
35
35
  from okstra_ctl.pr_template import PrTemplateError, resolve_pr_template_path
36
36
  from okstra_ctl.run import (
37
37
  APPROVED_FRONTMATTER_PATTERN,
@@ -89,7 +89,7 @@ TASK_TYPES: list[tuple[str, str]] = [
89
89
  ]
90
90
  TASK_TYPE_VALUES = [tt for tt, _ in TASK_TYPES]
91
91
 
92
- EXECUTORS = ["claude", "codex", "gemini"]
92
+ EXECUTORS = ["claude", "codex", "antigravity"]
93
93
 
94
94
  # Task types that consume an approved plan and need a stage-scope pick:
95
95
  # implementation executes a stage, final-verification verifies a stage
@@ -108,7 +108,7 @@ BASE_REF_FREE_INPUT_TOKEN = "__free_input__"
108
108
 
109
109
  CLAUDE_MODEL_OPTIONS = ["default", "fable", "opus", "sonnet", "haiku"]
110
110
  CODEX_MODEL_OPTIONS = ["default", "gpt-5.5", "gpt-5.4", "gpt-5.4-mini"]
111
- GEMINI_MODEL_OPTIONS = ["default", "gemini-3-pro-preview", "gemini-3-flash-preview", "auto"]
111
+ ANTIGRAVITY_MODEL_OPTIONS = ["default", "gemini-3.1-pro", "gemini-3.5-flash"]
112
112
 
113
113
  # special pick value: start a brand-new task
114
114
  TASK_PICK_NEW_TOKEN = "__new__"
@@ -263,7 +263,7 @@ S_LEAD_MODEL = "lead_model"
263
263
  S_EXECUTOR_MODEL = "executor_model"
264
264
  S_CLAUDE_MODEL = "claude_model"
265
265
  S_CODEX_MODEL = "codex_model"
266
- S_GEMINI_MODEL = "gemini_model"
266
+ S_ANTIGRAVITY_MODEL = "antigravity_model"
267
267
  S_REPORT_WRITER_MODEL = "report_writer_model"
268
268
  S_DIRECTIVE_PICK = "directive_pick"
269
269
  S_DIRECTIVE = "directive"
@@ -291,7 +291,7 @@ GROUP_MAX_TABS = 4 # AskUserQuestion 의 질문(탭) 수 한도
291
291
  # *_TEXT 후속 / workers_override / pr_template_scope 는 의존성 때문에 개별 유지.
292
292
  PROMPT_GROUPS: dict[str, tuple[str, ...]] = {
293
293
  GROUP_MODELS: (S_LEAD_MODEL, S_EXECUTOR_MODEL, S_CLAUDE_MODEL,
294
- S_CODEX_MODEL, S_GEMINI_MODEL, S_REPORT_WRITER_MODEL),
294
+ S_CODEX_MODEL, S_ANTIGRAVITY_MODEL, S_REPORT_WRITER_MODEL),
295
295
  GROUP_OPTIONS: (S_DIRECTIVE_PICK, S_RELATED_TASKS_PICK,
296
296
  S_CLARIFICATION_PICK, S_PR_TEMPLATE_PICK),
297
297
  }
@@ -371,7 +371,7 @@ class WizardState:
371
371
  lead_model: str = ""
372
372
  claude_model: str = ""
373
373
  codex_model: str = ""
374
- gemini_model: str = ""
374
+ antigravity_model: str = ""
375
375
  report_writer_model: str = ""
376
376
  directive: str = ""
377
377
  directive_pending_text: bool = False
@@ -689,13 +689,13 @@ def _validate_model(provider: str, raw: str) -> str:
689
689
  def _executor_model_field(executor: str) -> str:
690
690
  return {"claude": "claude_model",
691
691
  "codex": "codex_model",
692
- "gemini": "gemini_model"}[executor]
692
+ "antigravity": "antigravity_model"}[executor]
693
693
 
694
694
 
695
695
  def _executor_model_options(executor: str) -> list[str]:
696
696
  return {"claude": CLAUDE_MODEL_OPTIONS,
697
697
  "codex": CODEX_MODEL_OPTIONS,
698
- "gemini": GEMINI_MODEL_OPTIONS}[executor]
698
+ "antigravity": ANTIGRAVITY_MODEL_OPTIONS}[executor]
699
699
 
700
700
 
701
701
  # ---- Roster / profile helpers -------------------------------------------
@@ -797,6 +797,7 @@ def _p(workspace_root: str, step_id: str, **vars: str) -> dict:
797
797
  "echo_variants": raw.get("echo_variants", {}),
798
798
  "errors": raw.get("errors", {}),
799
799
  "labels": raw.get("labels", {}),
800
+ "sidecar_note": raw.get("sidecar_note", {}),
800
801
  "echo_suffixes": raw.get("echo_suffixes", {}),
801
802
  "recent_label_prefix": raw.get("recent_label_prefix", ""),
802
803
  }
@@ -1951,6 +1952,20 @@ def _suggest_latest_final_report(state: WizardState) -> str:
1951
1952
  return str(best)
1952
1953
 
1953
1954
 
1955
+ def _clarification_sidecar_note(state: WizardState, suggestion: str) -> str:
1956
+ """추천 final-report 와 함께 carry-in 될 ``user-responses/`` 사이드카 현황 문구.
1957
+
1958
+ "final-report 만 물어보니 user-responses 파일이 쓰이는지 불확실하다" 는 혼선을
1959
+ 없애기 위해, picker 단계에서 실제 첨부될 답변 파일 개수를 그 자리에서 보여준다.
1960
+ """
1961
+ p = Path(suggestion)
1962
+ if not p.is_absolute() and state.project_root:
1963
+ p = Path(state.project_root) / p
1964
+ count = len(user_response_sidecars(p))
1965
+ note = _p(state.workspace_root, "clarification_pick")["sidecar_note"]
1966
+ return note["attached"].format(count=count) if count else note["empty"]
1967
+
1968
+
1954
1969
  def _latest_run_inputs(
1955
1970
  state: WizardState, *, phase_segment: str = ""
1956
1971
  ) -> dict:
@@ -1991,6 +2006,9 @@ class _OptionalCachedPickSpec:
1991
2006
  target_attr: str # 확정값을 쓸 state 속성
1992
2007
  pending_attr: str # 직접 입력 대기 플래그 state 속성
1993
2008
  extra_clear_attrs: tuple[str, ...] = () # skip 시 추가로 비울 속성
2009
+ # 추천 옵션 라벨·echo 뒤에 덧붙일 동적 안내(예: 함께 첨부될 사이드카 개수).
2010
+ # (state, 추천 relpath) -> note. 미설정 picker 는 영향 없음.
2011
+ recommend_note: Optional[Callable[["WizardState", str], str]] = None
1994
2012
 
1995
2013
 
1996
2014
  def _pick_snippet(value: str, style: str) -> str:
@@ -2007,7 +2025,10 @@ def _build_optional_cached_pick(state: WizardState, spec: _OptionalCachedPickSpe
2007
2025
  options: list[Option] = []
2008
2026
  if suggestion:
2009
2027
  snippet = _pick_snippet(suggestion, spec.snippet_style)
2010
- options.append(_opt(spec.recommend_token, t["labels"][spec.label_key].format(snippet=snippet)))
2028
+ label = t["labels"][spec.label_key].format(snippet=snippet)
2029
+ if spec.recommend_note:
2030
+ label += spec.recommend_note(state, suggestion)
2031
+ options.append(_opt(spec.recommend_token, label))
2011
2032
  setattr(state, spec.cache_attr, suggestion)
2012
2033
  options.append(_opt(PICK_SKIP, t["options"][PICK_SKIP]))
2013
2034
  options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
@@ -2032,7 +2053,10 @@ def _submit_optional_cached_pick(
2032
2053
  cached = getattr(state, spec.cache_attr)
2033
2054
  setattr(state, spec.target_attr, cached)
2034
2055
  setattr(state, spec.pending_attr, False)
2035
- return t["echo_suffixes"][spec.echo_suffix_key].format(value=cached)
2056
+ echo = t["echo_suffixes"][spec.echo_suffix_key].format(value=cached)
2057
+ if spec.recommend_note:
2058
+ echo += spec.recommend_note(state, cached)
2059
+ return echo
2036
2060
  if value == PICK_TYPE_CUSTOM:
2037
2061
  setattr(state, spec.pending_attr, True)
2038
2062
  return None
@@ -2104,6 +2128,7 @@ _CLARIFICATION_PICK_SPEC = _OptionalCachedPickSpec(
2104
2128
  suggest=_suggest_latest_final_report, snippet_style="suffix",
2105
2129
  cache_attr="last_final_report_cached", target_attr="clarification_response_path",
2106
2130
  pending_attr="clarification_pending_text",
2131
+ recommend_note=_clarification_sidecar_note,
2107
2132
  )
2108
2133
 
2109
2134
 
@@ -2251,7 +2276,7 @@ _RESUME_REUSE_PHASES = (
2251
2276
  # YES 선택 시 prefill 로 충족되어 다시 묻지 않는 설정 단계들.
2252
2277
  _REUSE_FILLED_STEPS = (
2253
2278
  S_DEFAULTS_OR_CUSTOM, S_WORKERS_OVERRIDE, S_LEAD_MODEL, S_EXECUTOR_MODEL,
2254
- S_CLAUDE_MODEL, S_CODEX_MODEL, S_GEMINI_MODEL, S_REPORT_WRITER_MODEL,
2279
+ S_CLAUDE_MODEL, S_CODEX_MODEL, S_ANTIGRAVITY_MODEL, S_REPORT_WRITER_MODEL,
2255
2280
  S_DIRECTIVE_PICK, S_DIRECTIVE, S_RELATED_TASKS_PICK, S_RELATED_TASKS,
2256
2281
  S_CLARIFICATION_PICK, S_CLARIFICATION)
2257
2282
 
@@ -2299,7 +2324,7 @@ def _submit_reuse_previous(state: WizardState, value: str) -> Optional[str]:
2299
2324
  state.lead_model = _safe_model("claude", inputs.get("leadModel"))
2300
2325
  state.claude_model = _safe_model("claude", inputs.get("claudeModel"))
2301
2326
  state.codex_model = _safe_model("codex", inputs.get("codexModel"))
2302
- state.gemini_model = _safe_model("gemini", inputs.get("geminiModel"))
2327
+ state.antigravity_model = _safe_model("antigravity", inputs.get("antigravityModel"))
2303
2328
  state.report_writer_model = _safe_model(
2304
2329
  "claude", inputs.get("reportWriterModel"))
2305
2330
  directive = inputs.get("directive")
@@ -2445,15 +2470,15 @@ def _submit_codex_model(state: WizardState, value: str) -> Optional[str]:
2445
2470
  return f"codex-model: {state.codex_model or 'default'}"
2446
2471
 
2447
2472
 
2448
- def _build_gemini_model(state: WizardState) -> Prompt:
2449
- t = _p(state.workspace_root, "gemini_model")
2450
- return _model_pick(S_GEMINI_MODEL, t["label"],
2451
- GEMINI_MODEL_OPTIONS, t["echo_template"])
2473
+ def _build_antigravity_model(state: WizardState) -> Prompt:
2474
+ t = _p(state.workspace_root, "antigravity_model")
2475
+ return _model_pick(S_ANTIGRAVITY_MODEL, t["label"],
2476
+ ANTIGRAVITY_MODEL_OPTIONS, t["echo_template"])
2452
2477
 
2453
2478
 
2454
- def _submit_gemini_model(state: WizardState, value: str) -> Optional[str]:
2455
- state.gemini_model = _validate_model("gemini", value)
2456
- return f"gemini-model: {state.gemini_model or 'default'}"
2479
+ def _submit_antigravity_model(state: WizardState, value: str) -> Optional[str]:
2480
+ state.antigravity_model = _validate_model("antigravity", value)
2481
+ return f"antigravity-model: {state.antigravity_model or 'default'}"
2457
2482
 
2458
2483
 
2459
2484
  def _build_report_writer_model(state: WizardState) -> Prompt:
@@ -2930,7 +2955,7 @@ STEPS: list[Step] = [
2930
2955
  and s.task_type == "implementation"
2931
2956
  and S_EXECUTOR_MODEL not in s.answered),
2932
2957
  build=_build_executor_model, submit=_submit_executor_model,
2933
- owns=("claude_model", "codex_model", "gemini_model")),
2958
+ owns=("claude_model", "codex_model", "antigravity_model")),
2934
2959
  Step(S_CLAUDE_MODEL,
2935
2960
  applies=lambda s: (s.use_defaults is False
2936
2961
  and s.task_type != "implementation"
@@ -2945,13 +2970,13 @@ STEPS: list[Step] = [
2945
2970
  and S_CODEX_MODEL not in s.answered),
2946
2971
  build=_build_codex_model, submit=_submit_codex_model,
2947
2972
  owns=("codex_model",)),
2948
- Step(S_GEMINI_MODEL,
2973
+ Step(S_ANTIGRAVITY_MODEL,
2949
2974
  applies=lambda s: (s.use_defaults is False
2950
2975
  and s.task_type != "implementation"
2951
- and "gemini" in _resolved_roster(s)
2952
- and S_GEMINI_MODEL not in s.answered),
2953
- build=_build_gemini_model, submit=_submit_gemini_model,
2954
- owns=("gemini_model",)),
2976
+ and "antigravity" in _resolved_roster(s)
2977
+ and S_ANTIGRAVITY_MODEL not in s.answered),
2978
+ build=_build_antigravity_model, submit=_submit_antigravity_model,
2979
+ owns=("antigravity_model",)),
2955
2980
  Step(S_REPORT_WRITER_MODEL,
2956
2981
  applies=lambda s: (s.use_defaults is False
2957
2982
  and (s.task_type == "implementation"
@@ -3088,7 +3113,7 @@ def _ready_for_confirm(s: WizardState) -> bool:
3088
3113
  return True
3089
3114
  # customize: every customize-branch step must be answered or not-applicable.
3090
3115
  custom_ids = [S_LEAD_MODEL, S_EXECUTOR_MODEL,
3091
- S_CLAUDE_MODEL, S_CODEX_MODEL, S_GEMINI_MODEL,
3116
+ S_CLAUDE_MODEL, S_CODEX_MODEL, S_ANTIGRAVITY_MODEL,
3092
3117
  S_REPORT_WRITER_MODEL, S_DIRECTIVE, S_RELATED_TASKS,
3093
3118
  S_CLARIFICATION, S_PR_TEMPLATE, S_PR_TEMPLATE_SCOPE]
3094
3119
  for sid in custom_ids:
@@ -3130,7 +3155,7 @@ _FIELD_DEFAULTS: dict[str, Any] = {
3130
3155
  "reuse_previous": None,
3131
3156
  "use_defaults": None, "workers_override": "",
3132
3157
  "lead_model": "", "claude_model": "", "codex_model": "",
3133
- "gemini_model": "", "report_writer_model": "", "directive": "",
3158
+ "antigravity_model": "", "report_writer_model": "", "directive": "",
3134
3159
  "directive_pending_text": False,
3135
3160
  "related_tasks_raw": "", "related_tasks_pending_text": False,
3136
3161
  "clarification_response_path": "", "clarification_pending_text": False,
@@ -3299,7 +3324,7 @@ def render_args(state: WizardState) -> dict[str, str]:
3299
3324
  "lead-model": state.lead_model,
3300
3325
  "claude-model": state.claude_model,
3301
3326
  "codex-model": state.codex_model,
3302
- "gemini-model": state.gemini_model,
3327
+ "antigravity-model": state.antigravity_model,
3303
3328
  "report-writer-model": state.report_writer_model,
3304
3329
  "related-tasks": state.related_tasks_raw,
3305
3330
  "clarification-response": state.clarification_response_path,
@@ -3339,8 +3364,8 @@ def confirmation_block(state: WizardState) -> str:
3339
3364
  lines.append(f" claude-model : {state.claude_model}")
3340
3365
  if state.codex_model:
3341
3366
  lines.append(f" codex-model : {state.codex_model}")
3342
- if state.gemini_model:
3343
- lines.append(f" gemini-model : {state.gemini_model}")
3367
+ if state.antigravity_model:
3368
+ lines.append(f" antigravity-model : {state.antigravity_model}")
3344
3369
  if state.report_writer_model:
3345
3370
  lines.append(f" report-writer : {state.report_writer_model}")
3346
3371
  lines.append(f" directive : {state.directive or '(none)'}")
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  from pathlib import Path
10
10
 
11
- ALLOWED_WORKERS = ["claude", "codex", "gemini", "report-writer"]
11
+ ALLOWED_WORKERS = ["claude", "codex", "antigravity", "report-writer"]
12
12
  DEFAULT_WORKERS = ["claude", "codex", "report-writer"]
13
13
  PROFILE_BULLET_HEADERS = {
14
14
  "- Workers:",
@@ -64,7 +64,7 @@ def resolve_profile_workers(profile_path: Path) -> list[str]:
64
64
 
65
65
  def resolve_optional_workers(profile_path: Path) -> list[str]:
66
66
  r"""`- Optional workers (opt-in via \`--workers\`):` 섹션 아래 sub-bullet
67
- 들에서 worker id 만 추출한다. (` - gemini — when added …` → `gemini`)
67
+ 들에서 worker id 만 추출한다. (` - antigravity — when added …` → `antigravity`)
68
68
  Required 와 중복되는 항목은 제거. ALLOWED_WORKERS 밖 토큰도 제거.
69
69
  """
70
70
  raw = _resolve_workers_under(profile_path, PROFILE_BULLET_HEADERS_OPTIONAL)
@@ -83,7 +83,7 @@ def normalize_workers(value: str) -> list[str]:
83
83
  """CSV 입력을 정규화한다.
84
84
 
85
85
  - 공백 strip, 소문자화, 중복 제거(첫 출현 우선).
86
- - 빈 입력이면 `DEFAULT_WORKERS` 를 default 로 사용 (gemini 제외).
86
+ - 빈 입력이면 `DEFAULT_WORKERS` 를 default 로 사용 (antigravity 제외).
87
87
  - 허용 외 worker 가 포함되면 `WorkersError`.
88
88
  """
89
89
  items = [v.strip().lower() for v in (value or "").split(",") if v.strip()]
@@ -83,7 +83,7 @@ PHASE_RULES: dict[str, dict[str, str]] = {
83
83
  " - `Out-of-plan edits` block listing every file touched outside the approved plan with rationale (empty block preferred)\n"
84
84
  ' - validation evidence: actual stdout/stderr and exit code for every pre / mid / post command from the plan (no paraphrased "tests pass")\n'
85
85
  " - TDD evidence for TDD-applicable steps: failing-test output before implementation commit and passing-test output after, with framing SHAs\n"
86
- " - per-verifier sections (`Gemini verifier`, `Codex verifier`, `Claude verifier`) with independent verdict (PASS / CONCERNS / FAIL) and cited diff snippets; dissent is preserved by `Claude lead`\n"
86
+ " - per-verifier sections (`Antigravity verifier`, `Codex verifier`, `Claude verifier`) with independent verdict (PASS / CONCERNS / FAIL) and cited diff snippets; dissent is preserved by `Claude lead`\n"
87
87
  " - rollback verification (revert SHA reachable, feature flag toggle works, migration down step valid; dry-run preferred)\n"
88
88
  " - routing recommendation for `final-verification` (ready / needs new error-analysis or planning loop)"
89
89
  ),
@@ -479,7 +479,7 @@ def _link_sync_files(source_root: Path, worktree_path: Path) -> list[str]:
479
479
 
480
480
  def _seed_worktree_settings_symlink(worktree_path: Path) -> None:
481
481
  """Seed `.claude/settings.local.json` in the worker worktree so dispatched
482
- Claude / codex / gemini sessions inherit the okstra read-only / write
482
+ Claude / codex / antigravity sessions inherit the okstra read-only / write
483
483
  allowlist. Mirrors the main-project seeding done in `run.py` — needed
484
484
  because `_link_sync_dirs` skips `.claude/` whenever `git worktree add`
485
485
  already materialised the directory (e.g. tracked `.claude/handoff-*.md`).
@@ -5,12 +5,11 @@ from .blocks import na_block, usage_block
5
5
  from .claude import claude_session_totals, find_claude_team_sessions
6
6
  from .codex import codex_session_total, find_codex_session
7
7
  from .collect import collect
8
- from .gemini import find_gemini_session, gemini_session_total
8
+ from .antigravity import antigravity_session_total, find_antigravity_session
9
9
  from .jsonl_io import iter_jsonl
10
10
  from .paths import (
11
11
  CLAUDE_PROJECTS,
12
12
  CODEX_SESSIONS,
13
- GEMINI_TMP,
14
13
  claude_project_dir,
15
14
  utc_now,
16
15
  )
@@ -21,7 +20,7 @@ from .pricing import (
21
20
  claude_billable_equivalent,
22
21
  claude_cost_usd,
23
22
  codex_cost_usd,
24
- gemini_cost_usd,
23
+ antigravity_cost_usd,
25
24
  )
26
25
  from .report import (
27
26
  SubstituteRefusedError,
@@ -0,0 +1,26 @@
1
+ """Antigravity (`agy`) CLI usage collector.
2
+
3
+ okstra dispatches the antigravity-worker via `agy --print` (see
4
+ `okstra-antigravity-exec.sh`). That non-interactive print mode does not persist
5
+ a parseable transcript with per-call token counts to any known location, so
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).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+
17
+ def antigravity_session_total(json_path: Path) -> dict:
18
+ return {"totalTokens": 0, "available": False}
19
+
20
+
21
+ def find_antigravity_session(project_root: Path, started_at: str, ended_at: str) -> Path | None:
22
+ return None
23
+
24
+
25
+ def find_antigravity_sessions(project_root: Path, started_at: str, ended_at: str) -> list[Path]:
26
+ return []
@@ -15,6 +15,8 @@ def usage_block(totals: dict, source: str, note: str | None = None) -> dict:
15
15
  "outputTokens": totals.get("outputTokens", 0) or 0,
16
16
  "toolUses": totals.get("toolUses", 0) or 0,
17
17
  "durationMs": totals.get("durationMs", 0) or 0,
18
+ "startedAt": totals.get("startedAt"),
19
+ "endedAt": totals.get("endedAt"),
18
20
  "source": source,
19
21
  "collectedAt": utc_now(),
20
22
  }
@@ -57,6 +59,8 @@ def na_block(reason: str) -> dict:
57
59
  "totalTokens": 0,
58
60
  "toolUses": 0,
59
61
  "durationMs": 0,
62
+ "startedAt": None,
63
+ "endedAt": None,
60
64
  "source": "unavailable",
61
65
  "collectedAt": utc_now(),
62
66
  "note": reason,
@@ -14,9 +14,13 @@ from .claude import (
14
14
  find_claude_team_sessions,
15
15
  )
16
16
  from .codex import codex_session_total, find_codex_session, find_codex_sessions
17
- from .gemini import find_gemini_session, find_gemini_sessions, gemini_session_total
17
+ from .antigravity import (
18
+ antigravity_session_total,
19
+ find_antigravity_session,
20
+ find_antigravity_sessions,
21
+ )
18
22
  from .paths import claude_project_dir, utc_now
19
- from .pricing import codex_cost_usd, gemini_cost_usd
23
+ from .pricing import codex_cost_usd, antigravity_cost_usd
20
24
 
21
25
 
22
26
  def match_prefixes(worker_id: str) -> list[str]:
@@ -275,7 +279,7 @@ def _codex_runtime_provider(worker: dict) -> str:
275
279
  agent = str(worker.get("agent") or "").strip()
276
280
  if worker_id == "report-writer":
277
281
  return "codex"
278
- if agent in {"codex", "gemini"}:
282
+ if agent in {"codex", "antigravity"}:
279
283
  return agent
280
284
  return ""
281
285
 
@@ -290,8 +294,8 @@ def _cli_sessions_for_windows(
290
294
  for started_at, ended_at in windows:
291
295
  if provider == "codex":
292
296
  matches = find_codex_sessions(project_root, started_at, ended_at)
293
- elif provider == "gemini":
294
- matches = find_gemini_sessions(project_root, started_at, ended_at)
297
+ elif provider == "antigravity":
298
+ matches = find_antigravity_sessions(project_root, started_at, ended_at)
295
299
  else:
296
300
  matches = []
297
301
  for path in matches:
@@ -307,7 +311,7 @@ def _cli_session_totals(provider: str, session_paths: list[Path]) -> list[dict]:
307
311
  if provider == "codex":
308
312
  total = codex_session_total(session_path)
309
313
  else:
310
- total = gemini_session_total(session_path)
314
+ total = antigravity_session_total(session_path)
311
315
  if total.get("available"):
312
316
  totals.append(total)
313
317
  return totals
@@ -328,7 +332,7 @@ def _cli_usage_block(provider: str, totals: dict, session_paths: list[Path]) ->
328
332
  totals.get("outputTokens", 0) or 0,
329
333
  )
330
334
  else:
331
- cost = gemini_cost_usd(
335
+ cost = antigravity_cost_usd(
332
336
  totals.get("model"),
333
337
  totals.get("inputTokens", 0) or 0,
334
338
  totals.get("outputTokens", 0) or 0,
@@ -403,7 +407,7 @@ def _populate_usage_summary(
403
407
  for w in workers:
404
408
  u = w.get("usage") or {}
405
409
  if (
406
- u.get("source") not in {"codex-cli", "gemini-cli"}
410
+ u.get("source") not in {"codex-cli", "agy-cli"}
407
411
  and u.get("model")
408
412
  and u.get("estimatedCostUsd") is None
409
413
  and (u.get("totalTokens") or 0) > 0
@@ -433,7 +437,7 @@ def _populate_usage_summary(
433
437
  "definitions": {
434
438
  "totalTokens": "Sum of input + output + cache_creation + cache_read tokens (raw processed volume; matches Anthropic API breakdown). Cache reads are 95%+ in long sessions.",
435
439
  "billableEquivalentTokens": "Tokens normalized to base-input-price units (cache_creation_5m x1.25, cache_creation_1h x2.0, cache_read x0.1, output x5). 5m vs 1h is split from usage.cache_creation when the API breakdown is present; otherwise all cache_creation falls into 5m.",
436
- "estimatedCostUsd": "USD cost using public list pricing for the model recorded in the session. cliWorkers covers Codex/Gemini CLI calls billed under those providers.",
440
+ "estimatedCostUsd": "USD cost using public list pricing for the model recorded in the session. cliWorkers covers Codex/Antigravity CLI calls billed under those providers.",
437
441
  },
438
442
  }
439
443
 
@@ -547,17 +551,18 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
547
551
  block["additionalSessionIds"] = [sid for sid, _, _ in matched[1:]]
548
552
  block["matchedAgentNames"] = sorted({t.get("agentName") for _, _, t in matched if t.get("agentName")})
549
553
 
550
- # For codex/gemini workers, find every CLI session that fell inside the
551
- # aggregated wrapper window.
554
+ # For codex/antigravity workers, find every CLI session that fell inside
555
+ # the aggregated wrapper window. (agy print mode leaves no transcript, so
556
+ # the antigravity path always resolves to "session not found" → na cost.)
552
557
  wrapper_start = aggregate.get("startedAt") or ""
553
558
  wrapper_end = aggregate.get("endedAt") or ""
554
- if agent in ("codex", "gemini"):
559
+ if agent in ("codex", "antigravity"):
555
560
  if agent == "codex":
556
561
  cli = find_codex_session(cwd, wrapper_start, wrapper_end)
557
562
  cli_totals = codex_session_total(cli) if cli else None
558
563
  else:
559
- cli = find_gemini_session(cwd, wrapper_start, wrapper_end)
560
- cli_totals = gemini_session_total(cli) if cli else None
564
+ cli = find_antigravity_session(cwd, wrapper_start, wrapper_end)
565
+ cli_totals = antigravity_session_total(cli) if cli else None
561
566
  if cli is None:
562
567
  block["cliNote"] = f"{agent} CLI session not found in wrapper window (fallback or never invoked)"
563
568
  else:
@@ -574,7 +579,7 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
574
579
  cli_totals.get("outputTokens", 0) or 0,
575
580
  )
576
581
  else:
577
- cost = gemini_cost_usd(
582
+ cost = antigravity_cost_usd(
578
583
  cli_totals.get("model"),
579
584
  cli_totals.get("inputTokens", 0) or 0,
580
585
  cli_totals.get("outputTokens", 0) or 0,