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.
- package/README.md +4 -1
- package/docs/architecture.md +22 -4
- package/docs/cli.md +53 -6
- package/docs/project-structure-overview.md +19 -9
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +5 -6
- package/runtime/bin/okstra-trace-cleanup.sh +28 -2
- package/runtime/prompts/lead/adapters/claude-code.md +3 -3
- package/runtime/prompts/lead/convergence.md +30 -5
- package/runtime/prompts/lead/okstra-lead-contract.md +9 -3
- package/runtime/prompts/lead/report-writer.md +20 -14
- package/runtime/prompts/lead/team-contract.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/change-impact-analysis.md +24 -0
- package/runtime/prompts/profiles/feature-analysis.md +24 -0
- package/runtime/prompts/profiles/forbidden-actions.json +18 -0
- package/runtime/prompts/profiles/project-analysis.md +24 -0
- package/runtime/prompts/wizard/prompts.ko.json +44 -1
- package/runtime/python/okstra_ctl/analysis_inputs.py +369 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +4 -10
- package/runtime/python/okstra_ctl/clarification_items.py +74 -1
- package/runtime/python/okstra_ctl/codex_dispatch.py +117 -58
- package/runtime/python/okstra_ctl/convergence_engine.py +3 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +19 -56
- package/runtime/python/okstra_ctl/dispatch_state.py +167 -3
- package/runtime/python/okstra_ctl/path_hints.py +6 -0
- package/runtime/python/okstra_ctl/paths.py +7 -44
- package/runtime/python/okstra_ctl/render.py +79 -4
- package/runtime/python/okstra_ctl/render_final_report.py +13 -4
- package/runtime/python/okstra_ctl/report_views.py +134 -3
- package/runtime/python/okstra_ctl/run.py +118 -0
- package/runtime/python/okstra_ctl/run_context.py +34 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +12 -4
- package/runtime/python/okstra_ctl/user_response.py +309 -3
- package/runtime/python/okstra_ctl/wizard.py +579 -32
- package/runtime/python/okstra_ctl/worker_liveness.py +84 -21
- package/runtime/python/okstra_ctl/worker_prompt_body.py +24 -4
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +57 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -0
- package/runtime/python/okstra_ctl/worker_state.py +65 -0
- package/runtime/python/okstra_ctl/workflow.py +22 -0
- package/runtime/python/okstra_token_usage/antigravity.py +3 -0
- package/runtime/python/okstra_token_usage/codex.py +54 -23
- package/runtime/python/okstra_token_usage/collect.py +141 -33
- package/runtime/python/okstra_token_usage/paths.py +27 -0
- package/runtime/python/okstra_vendor/__init__.py +15 -2
- package/runtime/schemas/convergence-groups-v1.0.schema.json +0 -1
- package/runtime/schemas/final-report-v1.0.schema.json +849 -3
- package/runtime/skills/okstra-run/SKILL.md +27 -5
- package/runtime/skills/okstra-setup/references/project-config.md +13 -4
- package/runtime/templates/reports/change-impact-analysis-input.template.md +58 -0
- package/runtime/templates/reports/feature-analysis-input.template.md +59 -0
- package/runtime/templates/reports/final-report.template.md +220 -0
- package/runtime/templates/reports/i18n/en.json +8 -0
- package/runtime/templates/reports/i18n/ko.json +8 -0
- package/runtime/templates/reports/project-analysis-input.template.md +58 -0
- package/runtime/templates/reports/report.js +84 -5
- package/runtime/templates/reports/user-response.template.md +19 -1
- package/runtime/validators/lib/fixtures.sh +1 -1
- package/runtime/validators/validate-report-views.py +61 -7
- package/runtime/validators/validate-run.py +94 -1
- package/runtime/validators/validate_analysis_report.py +895 -0
- package/src/cli-registry.mjs +7 -10
- package/src/commands/execute/render-bundle.mjs +3 -0
- package/src/commands/execute/worker-state.mjs +29 -0
- package/src/commands/inspect/worker-liveness.mjs +5 -3
- package/src/commands/lifecycle/preflight.mjs +13 -3
- package/src/lib/runtime-readiness.mjs +90 -0
- package/runtime/python/okstra_ctl/phase_cleanup.py +0 -235
- 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")
|
|
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
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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 =
|
|
564
|
-
|
|
565
|
-
worker
|
|
566
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
787
|
-
|
|
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
|
-
|
|
47
|
+
_alias_loaded_submodules(_markupsafe, "markupsafe")
|
|
35
48
|
|
|
36
49
|
from . import jinja2 as _jinja2 # noqa: E402
|
|
37
|
-
|
|
50
|
+
_alias_loaded_submodules(_jinja2, "jinja2")
|