okstra 0.144.0 → 0.146.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.
Files changed (71) hide show
  1. package/README.md +4 -1
  2. package/docs/architecture.md +22 -4
  3. package/docs/cli.md +53 -6
  4. package/docs/project-structure-overview.md +19 -9
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/agents/workers/report-writer-worker.md +5 -6
  8. package/runtime/bin/okstra-trace-cleanup.sh +28 -2
  9. package/runtime/prompts/lead/adapters/claude-code.md +3 -3
  10. package/runtime/prompts/lead/convergence.md +30 -5
  11. package/runtime/prompts/lead/okstra-lead-contract.md +9 -3
  12. package/runtime/prompts/lead/report-writer.md +20 -14
  13. package/runtime/prompts/lead/team-contract.md +3 -3
  14. package/runtime/prompts/profiles/_common-contract.md +1 -1
  15. package/runtime/prompts/profiles/change-impact-analysis.md +24 -0
  16. package/runtime/prompts/profiles/feature-analysis.md +24 -0
  17. package/runtime/prompts/profiles/forbidden-actions.json +18 -0
  18. package/runtime/prompts/profiles/project-analysis.md +24 -0
  19. package/runtime/prompts/wizard/prompts.ko.json +44 -1
  20. package/runtime/python/okstra_ctl/analysis_inputs.py +369 -0
  21. package/runtime/python/okstra_ctl/analysis_packet.py +4 -10
  22. package/runtime/python/okstra_ctl/clarification_items.py +74 -1
  23. package/runtime/python/okstra_ctl/codex_dispatch.py +117 -58
  24. package/runtime/python/okstra_ctl/convergence_engine.py +3 -1
  25. package/runtime/python/okstra_ctl/dispatch_core.py +19 -56
  26. package/runtime/python/okstra_ctl/dispatch_state.py +167 -3
  27. package/runtime/python/okstra_ctl/path_hints.py +6 -0
  28. package/runtime/python/okstra_ctl/paths.py +7 -44
  29. package/runtime/python/okstra_ctl/render.py +79 -4
  30. package/runtime/python/okstra_ctl/render_final_report.py +13 -4
  31. package/runtime/python/okstra_ctl/report_views.py +134 -3
  32. package/runtime/python/okstra_ctl/run.py +118 -0
  33. package/runtime/python/okstra_ctl/run_context.py +34 -2
  34. package/runtime/python/okstra_ctl/schema_excerpt.py +12 -4
  35. package/runtime/python/okstra_ctl/user_response.py +309 -3
  36. package/runtime/python/okstra_ctl/wizard.py +579 -32
  37. package/runtime/python/okstra_ctl/worker_liveness.py +84 -21
  38. package/runtime/python/okstra_ctl/worker_prompt_body.py +24 -4
  39. package/runtime/python/okstra_ctl/worker_prompt_contract.py +57 -0
  40. package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -0
  41. package/runtime/python/okstra_ctl/worker_state.py +65 -0
  42. package/runtime/python/okstra_ctl/workflow.py +22 -0
  43. package/runtime/python/okstra_token_usage/antigravity.py +3 -0
  44. package/runtime/python/okstra_token_usage/codex.py +54 -23
  45. package/runtime/python/okstra_token_usage/collect.py +141 -33
  46. package/runtime/python/okstra_token_usage/paths.py +27 -0
  47. package/runtime/python/okstra_vendor/__init__.py +15 -2
  48. package/runtime/schemas/convergence-groups-v1.0.schema.json +0 -1
  49. package/runtime/schemas/final-report-v1.0.schema.json +849 -3
  50. package/runtime/skills/okstra-run/SKILL.md +27 -5
  51. package/runtime/skills/okstra-setup/references/project-config.md +13 -4
  52. package/runtime/templates/reports/change-impact-analysis-input.template.md +58 -0
  53. package/runtime/templates/reports/feature-analysis-input.template.md +59 -0
  54. package/runtime/templates/reports/final-report.template.md +220 -0
  55. package/runtime/templates/reports/i18n/en.json +8 -0
  56. package/runtime/templates/reports/i18n/ko.json +8 -0
  57. package/runtime/templates/reports/project-analysis-input.template.md +58 -0
  58. package/runtime/templates/reports/report.js +84 -5
  59. package/runtime/templates/reports/user-response.template.md +19 -1
  60. package/runtime/validators/lib/fixtures.sh +1 -1
  61. package/runtime/validators/validate-report-views.py +61 -7
  62. package/runtime/validators/validate-run.py +94 -1
  63. package/runtime/validators/validate_analysis_report.py +895 -0
  64. package/src/cli-registry.mjs +7 -10
  65. package/src/commands/execute/render-bundle.mjs +3 -0
  66. package/src/commands/execute/worker-state.mjs +29 -0
  67. package/src/commands/inspect/worker-liveness.mjs +5 -3
  68. package/src/commands/lifecycle/preflight.mjs +13 -3
  69. package/src/lib/runtime-readiness.mjs +90 -0
  70. package/runtime/python/okstra_ctl/phase_cleanup.py +0 -235
  71. package/src/commands/execute/phase-cleanup.mjs +0 -38
@@ -15,11 +15,13 @@ from .claude import (
15
15
  )
16
16
  from .codex import codex_session_total, find_codex_sessions
17
17
  from .antigravity import (
18
+ PRINT_MODE_USAGE_NOTE,
18
19
  antigravity_session_total,
19
20
  find_antigravity_sessions,
20
21
  )
21
22
  from .paths import claude_project_dir, utc_now
22
23
  from .pricing import codex_cost_usd, antigravity_cost_usd
24
+ from okstra_ctl.wrapper_status import read_wrapper_status, status_path_for_prompt
23
25
 
24
26
 
25
27
  def match_prefixes(worker_id: str) -> list[str]:
@@ -434,7 +436,7 @@ def _codex_worker_windows(project_root: Path, state: dict) -> dict[str, list[tup
434
436
  running_workers = {
435
437
  str(worker.get("workerId") or "").strip()
436
438
  for worker in state.get("workers", [])
437
- if isinstance(worker, dict) and worker.get("status") == "running"
439
+ if isinstance(worker, dict) and worker.get("status") in {"running", "in-progress"}
438
440
  }
439
441
  if running_workers:
440
442
  open_until = utc_now()
@@ -512,12 +514,116 @@ def _cli_usage_block(provider: str, totals: dict, session_paths: list[Path]) ->
512
514
  return block
513
515
 
514
516
 
517
+ def _epoch_iso(value: object) -> str | None:
518
+ if not isinstance(value, (int, float)) or isinstance(value, bool):
519
+ return None
520
+ return datetime.fromtimestamp(value, timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
521
+
522
+
523
+ def wrapper_execution(status_path: Path | None) -> dict:
524
+ """Read wrapper execution evidence without inferring token availability."""
525
+ if status_path is None or not status_path.is_file():
526
+ return {"status": "not-started"}
527
+ status = read_wrapper_status(status_path)
528
+ if status is None:
529
+ return {"status": "failed", "statusPath": str(status_path)}
530
+
531
+ if status.timeout:
532
+ execution_status = "timeout"
533
+ elif status.stage == "started":
534
+ execution_status = "started"
535
+ elif status.stage == "exited" and status.exit_code == 0:
536
+ execution_status = "exited"
537
+ else:
538
+ execution_status = "failed"
539
+
540
+ execution = {
541
+ "status": execution_status,
542
+ "statusPath": str(status_path),
543
+ "startedAt": _epoch_iso(status.raw.get("started_ts")),
544
+ "endedAt": _epoch_iso(status.raw.get("ended_ts")),
545
+ }
546
+ if status.exit_code is not None:
547
+ execution["exitCode"] = status.exit_code
548
+ return execution
549
+
550
+
551
+ def _execution_note(execution: dict) -> str:
552
+ status = execution["status"]
553
+ if status == "exited":
554
+ return f"wrapper exited {execution.get('exitCode', 0)}"
555
+ if status == "timeout":
556
+ return "wrapper timed out"
557
+ if status == "failed":
558
+ exit_code = execution.get("exitCode")
559
+ if exit_code is not None:
560
+ return f"wrapper failed with exit code {exit_code}"
561
+ return "wrapper status is invalid"
562
+ if status == "started":
563
+ return "wrapper started and has not recorded an exit"
564
+ return "wrapper status sidecar is unavailable"
565
+
566
+
567
+ def collect_cli_usage(
568
+ *,
569
+ provider: str,
570
+ status_path: Path | None,
571
+ sessions: list[Path],
572
+ fallback_window_used: bool = False,
573
+ ) -> dict:
574
+ """Collect attributable CLI usage while retaining execution evidence."""
575
+ execution = wrapper_execution(status_path)
576
+ execution_note = _execution_note(execution)
577
+
578
+ if provider == "antigravity":
579
+ block = na_block(f"{PRINT_MODE_USAGE_NOTE}; {execution_note}")
580
+ block["cliNote"] = block["note"]
581
+ else:
582
+ totals = _cli_session_totals(provider, sessions)
583
+ if not sessions:
584
+ block = na_block(
585
+ f"{execution_note}; CLI usage attribution unavailable because no transcript was found"
586
+ )
587
+ block["cliNote"] = block["note"]
588
+ elif not totals:
589
+ block = na_block(
590
+ f"{execution_note}; transcript found but no final token snapshot was recorded"
591
+ )
592
+ block["cliNote"] = block["note"]
593
+ block["cliSessionPaths"] = [str(path) for path in sessions]
594
+ else:
595
+ block = _cli_usage_block(provider, _aggregate_totals(totals), sessions)
596
+
597
+ block["cliExecutionStatus"] = execution["status"]
598
+ if fallback_window_used:
599
+ fallback_note = "wrapper status sidecar unavailable; used aggregate wrapper window fallback"
600
+ prior_note = block.get("cliNote")
601
+ block["cliNote"] = f"{prior_note}; {fallback_note}" if prior_note else fallback_note
602
+ return block
603
+
604
+
605
+ def _worker_cli_windows(
606
+ project_root: Path,
607
+ worker: dict,
608
+ fallback_windows: list[tuple[str, str]],
609
+ ) -> tuple[list[tuple[str, str]], Path | None, bool]:
610
+ prompt_path = _resolve_project_path(project_root, str(worker.get("promptPath") or ""))
611
+ status_path = status_path_for_prompt(prompt_path) if prompt_path is not None else None
612
+ execution = wrapper_execution(status_path)
613
+ started_at = execution.get("startedAt")
614
+ ended_at = execution.get("endedAt")
615
+ if started_at:
616
+ return [(started_at, ended_at or utc_now())], status_path, False
617
+ return fallback_windows, status_path, True
618
+
619
+
515
620
  def _attach_cli_usage(
516
621
  block: dict,
517
622
  provider: str,
518
623
  project_root: Path,
519
- wrapper_start: str,
520
- wrapper_end: str,
624
+ windows: list[tuple[str, str]],
625
+ status_path: Path | None,
626
+ fallback_window_used: bool,
521
627
  ) -> None:
522
628
  """Layer aggregated CLI token/cost onto a Claude-side worker ``block``.
523
629
 
@@ -528,19 +634,21 @@ def _attach_cli_usage(
528
634
  CLI spend is fully reported. (agy print mode leaves no transcript, so the
529
635
  antigravity path still resolves to "session not found" → na cost.)
530
636
  """
531
- session_paths = _cli_sessions_for_windows(
532
- provider, project_root, [(wrapper_start, wrapper_end)]
637
+ session_paths = _cli_sessions_for_windows(provider, project_root, windows)
638
+ cli = collect_cli_usage(
639
+ provider=provider,
640
+ status_path=status_path,
641
+ sessions=session_paths,
642
+ fallback_window_used=fallback_window_used,
533
643
  )
534
- if not session_paths:
535
- block["cliNote"] = f"{provider} CLI session not found in wrapper window (fallback or never invoked)"
536
- return
537
- block["cliSessionPaths"] = [str(path) for path in session_paths]
538
- totals = _cli_session_totals(provider, session_paths)
539
- if not totals:
540
- block["cliNote"] = f"{provider} CLI session found but no usage recorded (likely errored before completion)"
541
- return
542
- cli = _cli_usage_block(provider, _aggregate_totals(totals), session_paths)
543
- for key in ("cliTotalTokens", "cliEstimatedCostUsd", "cliModel"):
644
+ for key in (
645
+ "cliTotalTokens",
646
+ "cliEstimatedCostUsd",
647
+ "cliModel",
648
+ "cliSessionPaths",
649
+ "cliNote",
650
+ "cliExecutionStatus",
651
+ ):
544
652
  if key in cli:
545
653
  block[key] = cli[key]
546
654
 
@@ -560,23 +668,17 @@ def _collect_codex_runtime_usage(state: dict, project_root: Path) -> dict:
560
668
  )
561
669
  continue
562
670
  worker_id = str(worker.get("workerId") or "").strip()
563
- windows = windows_by_worker.get(worker_id, [])
564
- if not windows:
565
- worker["usage"] = na_block(
566
- f"no lead-events dispatch window found for workerId={worker_id}"
567
- )
568
- continue
671
+ windows, status_path, used_fallback = _worker_cli_windows(
672
+ project_root,
673
+ worker,
674
+ windows_by_worker.get(worker_id, []),
675
+ )
569
676
  session_paths = _cli_sessions_for_windows(provider, project_root, windows)
570
- totals = _cli_session_totals(provider, session_paths)
571
- if not totals:
572
- worker["usage"] = na_block(
573
- f"no {provider} CLI session found in lead-events dispatch window"
574
- )
575
- continue
576
- worker["usage"] = _cli_usage_block(
577
- provider,
578
- _aggregate_totals(totals),
579
- session_paths,
677
+ worker["usage"] = collect_cli_usage(
678
+ provider=provider,
679
+ status_path=status_path,
680
+ sessions=session_paths,
681
+ fallback_window_used=used_fallback,
580
682
  )
581
683
  _populate_usage_summary(state, team_name=resolve_team_name(state),
582
684
  sessions_found=0, needle_source="none")
@@ -779,12 +881,18 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
779
881
  # For codex/antigravity workers, sum every CLI session that fell inside
780
882
  # the aggregated wrapper window (re-dispatches leave one rollout each).
781
883
  if agent in ("codex", "antigravity"):
884
+ windows, status_path, used_fallback = _worker_cli_windows(
885
+ cwd,
886
+ worker,
887
+ [(aggregate.get("startedAt") or "", aggregate.get("endedAt") or "")],
888
+ )
782
889
  _attach_cli_usage(
783
890
  block,
784
891
  agent,
785
892
  cwd,
786
- aggregate.get("startedAt") or "",
787
- aggregate.get("endedAt") or "",
893
+ windows,
894
+ status_path,
895
+ used_fallback,
788
896
  )
789
897
  worker["usage"] = block
790
898
 
@@ -1,8 +1,10 @@
1
1
  """Filesystem locations for agent session transcripts and time helpers."""
2
2
  from __future__ import annotations
3
3
 
4
+ import os
4
5
  from datetime import datetime, timezone
5
6
  from pathlib import Path
7
+ from typing import Mapping
6
8
 
7
9
 
8
10
  HOME = Path.home()
@@ -10,6 +12,31 @@ CLAUDE_PROJECTS = HOME / ".claude" / "projects"
10
12
  CODEX_SESSIONS = HOME / ".agent" / "sessions"
11
13
 
12
14
 
15
+ def codex_session_roots(
16
+ home: Path,
17
+ env: Mapping[str, str],
18
+ ) -> tuple[Path, ...]:
19
+ """Return Codex transcript roots in configuration precedence order."""
20
+ candidates: list[Path] = []
21
+ configured_home = env.get("CODEX_HOME", "").strip()
22
+ if configured_home:
23
+ candidates.append(Path(configured_home).expanduser() / "sessions")
24
+ candidates.extend((
25
+ home / ".codex" / "sessions",
26
+ home / ".agent" / "sessions",
27
+ ))
28
+
29
+ roots: list[Path] = []
30
+ seen: set[Path] = set()
31
+ for candidate in candidates:
32
+ normalized = Path(os.path.abspath(candidate))
33
+ if normalized in seen:
34
+ continue
35
+ seen.add(normalized)
36
+ roots.append(candidate)
37
+ return tuple(roots)
38
+
39
+
13
40
  def utc_now() -> str:
14
41
  return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
15
42
 
@@ -26,12 +26,25 @@ imports find them deterministically.
26
26
 
27
27
  import sys as _sys
28
28
 
29
+
30
+ def _alias_loaded_submodules(package: object, canonical_name: str) -> None:
31
+ """Expose one vendored package tree under its canonical import name."""
32
+ vendored_name = package.__name__
33
+ _sys.modules[canonical_name] = package
34
+ vendored_prefix = f"{vendored_name}."
35
+ for module_name, module in tuple(_sys.modules.items()):
36
+ if module is None or not module_name.startswith(vendored_prefix):
37
+ continue
38
+ suffix = module_name[len(vendored_name):]
39
+ _sys.modules[f"{canonical_name}{suffix}"] = module
40
+
41
+
29
42
  # Register markupsafe alias BEFORE importing jinja2 — jinja2's __init__
30
43
  # evaluates `from .environment import ...`, which does `from markupsafe
31
44
  # import Markup` at module-load time. Reversing the order would crash
32
45
  # the import chain before the alias was registered.
33
46
  from . import markupsafe as _markupsafe
34
- _sys.modules.setdefault("markupsafe", _markupsafe)
47
+ _alias_loaded_submodules(_markupsafe, "markupsafe")
35
48
 
36
49
  from . import jinja2 as _jinja2 # noqa: E402
37
- _sys.modules.setdefault("jinja2", _jinja2)
50
+ _alias_loaded_submodules(_jinja2, "jinja2")
@@ -51,7 +51,6 @@
51
51
  "category": { "type": "string", "pattern": "\\S" },
52
52
  "ticketIds": {
53
53
  "type": "array",
54
- "minItems": 1,
55
54
  "items": { "type": "string", "pattern": "\\S" }
56
55
  },
57
56
  "originWorker": { "type": "string", "pattern": "\\S" },