okstra 0.186.5 → 0.186.6
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/docs/architecture.md +1 -1
- package/docs/cli.md +3 -3
- package/docs/for-ai/skills/okstra-user-response.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/okstra-render-report-views.py +6 -5
- package/runtime/prompts/launch.template.md +14 -0
- package/runtime/prompts/lead/convergence.md +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +4 -14
- package/runtime/prompts/lead/plan-body-verification.md +4 -3
- package/runtime/prompts/lead/report-writer.md +3 -1
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +2 -2
- package/runtime/prompts/profiles/final-verification.md +2 -2
- package/runtime/prompts/profiles/implementation-planning.md +3 -3
- package/runtime/prompts/profiles/requirements-discovery.md +2 -2
- package/runtime/prompts/wizard/prompts.ko.json +2 -4
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -5
- package/runtime/python/okstra_ctl/agent_activity.py +6 -0
- package/runtime/python/okstra_ctl/clarification_items.py +67 -11
- package/runtime/python/okstra_ctl/next_phase.py +6 -3
- package/runtime/python/okstra_ctl/plan_items.py +32 -6
- package/runtime/python/okstra_ctl/plan_items_cli.py +57 -3
- package/runtime/python/okstra_ctl/render_final_report.py +3 -1
- package/runtime/python/okstra_ctl/report_assembly.py +9 -2
- package/runtime/python/okstra_ctl/report_html/run_usage.py +5 -1
- package/runtime/python/okstra_ctl/report_projections.py +45 -4
- package/runtime/python/okstra_ctl/run.py +21 -6
- package/runtime/python/okstra_ctl/usage_cells.py +15 -0
- package/runtime/python/okstra_ctl/user_response.py +72 -6
- package/runtime/python/okstra_ctl/wizard.py +1 -5
- package/runtime/python/okstra_token_usage/codex.py +32 -3
- package/runtime/python/okstra_token_usage/collect.py +148 -15
- package/runtime/python/okstra_token_usage/grok.py +24 -5
- package/runtime/python/okstra_token_usage/report.py +12 -2
- package/runtime/skills/okstra-user-response/SKILL.md +4 -2
- package/runtime/templates/reports/html/assets/base.css +3 -9
- package/runtime/templates/reports/html/assets/base.js +0 -21
- package/runtime/templates/reports/html/base.template.html +1 -4
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +9 -28
- package/runtime/validators/lib/runners.sh +5 -1
- package/runtime/validators/validate-report-views.py +2 -1
- package/runtime/validators/validate-run.py +97 -82
- package/runtime/validators/validate_session_conformance.py +71 -18
|
@@ -80,7 +80,9 @@ from okstra_ctl.report_translation import ( # noqa: E402
|
|
|
80
80
|
)
|
|
81
81
|
from okstra_ctl.stage_citations import enumerated_stage_numbers # noqa: E402
|
|
82
82
|
from okstra_ctl.plan_items import ( # noqa: E402
|
|
83
|
+
CRITIC_WORKER_ID,
|
|
83
84
|
advisory_plan_body_gating,
|
|
85
|
+
is_critic_worker,
|
|
84
86
|
stage_scope_bucket as _item_stage_scope_bucket,
|
|
85
87
|
)
|
|
86
88
|
from okstra_ctl.incremental_scope import ( # noqa: E402
|
|
@@ -92,6 +94,7 @@ from okstra_ctl.clarification_items import ( # noqa: E402
|
|
|
92
94
|
APPROVAL_BLOCKS,
|
|
93
95
|
PROCEEDING_DISPOSITIONS,
|
|
94
96
|
clarification_disposition,
|
|
97
|
+
incorporated_clarification_ids,
|
|
95
98
|
progress_blocking_ids,
|
|
96
99
|
row_blocks_progress,
|
|
97
100
|
)
|
|
@@ -512,7 +515,7 @@ def _validate_agent_dispatch_contract(
|
|
|
512
515
|
|
|
513
516
|
links = [
|
|
514
517
|
row for row in (team_state.get("agentResultLinks") or [])
|
|
515
|
-
if isinstance(row, Mapping)
|
|
518
|
+
if isinstance(row, Mapping) and not row.get("supersededBy")
|
|
516
519
|
]
|
|
517
520
|
paths: dict[str, str] = {}
|
|
518
521
|
dispatch_link_counts: dict[str, int] = {}
|
|
@@ -3922,6 +3925,34 @@ def _single_vote_block_survives(item: dict, kinds: set[str]) -> bool:
|
|
|
3922
3925
|
)
|
|
3923
3926
|
|
|
3924
3927
|
|
|
3928
|
+
def _critic_non_error_verdicts(item: dict) -> list[dict]:
|
|
3929
|
+
return [
|
|
3930
|
+
row
|
|
3931
|
+
for row in (item.get("verdicts") or [])
|
|
3932
|
+
if isinstance(row, dict)
|
|
3933
|
+
and is_critic_worker(str(row.get("worker") or ""))
|
|
3934
|
+
and str(row.get("verdict") or "").strip().upper()
|
|
3935
|
+
not in ("", "VERIFICATION-ERROR")
|
|
3936
|
+
]
|
|
3937
|
+
|
|
3938
|
+
|
|
3939
|
+
def _tie_gate_class(item: dict, agree: list, disagree: list) -> str | None:
|
|
3940
|
+
"""분석자 동수면 critic 이 가르고, 없으면 재검증. 동수가 아니면 None."""
|
|
3941
|
+
if not (len(disagree) == len(agree) and disagree):
|
|
3942
|
+
return None
|
|
3943
|
+
critic = _critic_non_error_verdicts(item)
|
|
3944
|
+
if not critic:
|
|
3945
|
+
return "needs-reverify"
|
|
3946
|
+
if any(
|
|
3947
|
+
str(row.get("verdict") or "").strip().upper() == "DISAGREE"
|
|
3948
|
+
and str(row.get("breakageKind") or "").strip().lower()
|
|
3949
|
+
not in _ADVISORY_ONLY_KINDS
|
|
3950
|
+
for row in critic
|
|
3951
|
+
):
|
|
3952
|
+
return "majority-disagree"
|
|
3953
|
+
return "has-dissent"
|
|
3954
|
+
|
|
3955
|
+
|
|
3925
3956
|
def _classify_plan_item_gate(item: dict) -> str:
|
|
3926
3957
|
"""Recompute one plan item's gate class from its per-worker verdicts,
|
|
3927
3958
|
per `prompts/lead/plan-body-verification.md` "Round protocol". Returns one of
|
|
@@ -3930,7 +3961,8 @@ def _classify_plan_item_gate(item: dict) -> str:
|
|
|
3930
3961
|
(``dissent-isolated`` / ``partial-consensus`` on ``b``/``c``/``e``) is
|
|
3931
3962
|
``majority-disagree`` so the user gate sees it. ``has-dissent`` remains
|
|
3932
3963
|
advisory-only, rollback items, and a single-vote kind that lost its
|
|
3933
|
-
reproduction.
|
|
3964
|
+
reproduction. An analyser 1-1 is ``needs-reverify`` until ``critic-worker``
|
|
3965
|
+
settles it.
|
|
3934
3966
|
"""
|
|
3935
3967
|
tokens = [
|
|
3936
3968
|
(
|
|
@@ -3939,6 +3971,7 @@ def _classify_plan_item_gate(item: dict) -> str:
|
|
|
3939
3971
|
)
|
|
3940
3972
|
for v in (item.get("verdicts") or [])
|
|
3941
3973
|
if isinstance(v, dict)
|
|
3974
|
+
and not is_critic_worker(str(v.get("worker") or ""))
|
|
3942
3975
|
]
|
|
3943
3976
|
non_error = [(vd, bk) for (vd, bk) in tokens if vd and vd != "VERIFICATION-ERROR"]
|
|
3944
3977
|
if not non_error:
|
|
@@ -3988,19 +4021,9 @@ def _classify_plan_item_gate(item: dict) -> str:
|
|
|
3988
4021
|
# made the gate stricter than a healthy roster would.
|
|
3989
4022
|
if len(non_error) >= 2 and len(blocking_disagree) > len(agree):
|
|
3990
4023
|
return "majority-disagree"
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
# and never acted on. An even panel is not only the two-analyser roster: one
|
|
3995
|
-
# UNVERIFIABLE or one lost dispatch turns any roster even for that item.
|
|
3996
|
-
# Send the split back for a round; if it survives a round that judged the
|
|
3997
|
-
# rewritten text, nothing further is going to settle it and the user decides.
|
|
3998
|
-
# `_validate_unresolved_tie_was_reverified` is what makes the first branch
|
|
3999
|
-
# more than a label — `needs-reverify` folds into `passed-with-dissent`.
|
|
4000
|
-
if len(non_error) >= 2 and len(blocking_disagree) == len(agree):
|
|
4001
|
-
if _max_verdict_round(item) >= _TIE_SETTLED_ROUND:
|
|
4002
|
-
return "majority-disagree"
|
|
4003
|
-
return "needs-reverify"
|
|
4024
|
+
settled = _tie_gate_class(item, agree, blocking_disagree)
|
|
4025
|
+
if settled is not None and len(non_error) >= 2:
|
|
4026
|
+
return settled
|
|
4004
4027
|
if (
|
|
4005
4028
|
len(non_error) >= 2
|
|
4006
4029
|
and blocking_disagree
|
|
@@ -4016,12 +4039,6 @@ def _classify_plan_item_gate(item: dict) -> str:
|
|
|
4016
4039
|
return "has-dissent"
|
|
4017
4040
|
|
|
4018
4041
|
|
|
4019
|
-
# 동수를 한 번 재검증한 뒤에도 갈리면 그때는 사용자가 판단한다. 초기 검증이
|
|
4020
|
-
# 라운드 1이고 자가수정 뒤의 표적 재검증이 라운드 2이므로, 라운드 2 이상의
|
|
4021
|
-
# 판정이 붙은 동수는 이미 한 번 돌아온 것이다.
|
|
4022
|
-
_TIE_SETTLED_ROUND = 2
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
4042
|
def _max_verdict_round(item: dict) -> int:
|
|
4026
4043
|
"""이 항목의 판정이 붙은 가장 늦은 라운드. 스탬프가 없으면 1.
|
|
4027
4044
|
|
|
@@ -4038,18 +4055,25 @@ def _max_verdict_round(item: dict) -> int:
|
|
|
4038
4055
|
return max(rounds, default=1)
|
|
4039
4056
|
|
|
4040
4057
|
|
|
4058
|
+
def _is_even_analyser_split(item: dict) -> bool:
|
|
4059
|
+
tokens = [
|
|
4060
|
+
str(row.get("verdict") or "").strip().upper()
|
|
4061
|
+
for row in (item.get("verdicts") or [])
|
|
4062
|
+
if isinstance(row, dict)
|
|
4063
|
+
and not is_critic_worker(str(row.get("worker") or ""))
|
|
4064
|
+
and str(row.get("verdict") or "").strip().upper()
|
|
4065
|
+
not in ("", "VERIFICATION-ERROR")
|
|
4066
|
+
]
|
|
4067
|
+
if len(tokens) < 2:
|
|
4068
|
+
return False
|
|
4069
|
+
disagree = sum(1 for token in tokens if token == "DISAGREE")
|
|
4070
|
+
agree = sum(1 for token in tokens if token in {"AGREE", "SUPPLEMENT"})
|
|
4071
|
+
return disagree == agree and disagree > 0
|
|
4072
|
+
|
|
4073
|
+
|
|
4041
4074
|
def _is_unsettled_tie(item: dict) -> bool:
|
|
4042
|
-
"""아직
|
|
4043
|
-
return (
|
|
4044
|
-
_classify_plan_item_gate(item) == "needs-reverify"
|
|
4045
|
-
and _max_verdict_round(item) < _TIE_SETTLED_ROUND
|
|
4046
|
-
and len([
|
|
4047
|
-
verdict for verdict in (item.get("verdicts") or [])
|
|
4048
|
-
if isinstance(verdict, dict)
|
|
4049
|
-
and str(verdict.get("verdict") or "").strip().upper()
|
|
4050
|
-
not in ("", "VERIFICATION-ERROR")
|
|
4051
|
-
]) >= 2
|
|
4052
|
-
)
|
|
4075
|
+
"""분석자는 갈렸고 critic 표가 아직 없는 동수 항목."""
|
|
4076
|
+
return _is_even_analyser_split(item) and not _critic_non_error_verdicts(item)
|
|
4053
4077
|
|
|
4054
4078
|
|
|
4055
4079
|
def _disagree_breakage_kinds(item: dict) -> set[str]:
|
|
@@ -5353,6 +5377,7 @@ def _validate_approval_context(
|
|
|
5353
5377
|
activities = _approval_activities_by_id(data)
|
|
5354
5378
|
activity_timestamps = _canonical_activity_timestamps(run_manifest, report_path)
|
|
5355
5379
|
report_approved = (data.get("frontmatter") or {}).get("approved") is True
|
|
5380
|
+
incorporated = incorporated_clarification_ids(data)
|
|
5356
5381
|
for row in data.get("clarificationItems") or []:
|
|
5357
5382
|
if not isinstance(row, dict) or row.get("blocks") != "approval":
|
|
5358
5383
|
continue
|
|
@@ -5434,7 +5459,9 @@ def _validate_approval_context(
|
|
|
5434
5459
|
failures,
|
|
5435
5460
|
)
|
|
5436
5461
|
if report_approved and row_blocks_progress(
|
|
5437
|
-
str(row.get("status") or ""),
|
|
5462
|
+
str(row.get("status") or ""),
|
|
5463
|
+
clarification_disposition(row),
|
|
5464
|
+
incorporated=row_id in incorporated,
|
|
5438
5465
|
):
|
|
5439
5466
|
failures.append(
|
|
5440
5467
|
f"final-report data.json: approval is true while clarification `{row_id}` "
|
|
@@ -5498,6 +5525,7 @@ def _validate_v3_approval_context(data: dict, failures: list[str]) -> None:
|
|
|
5498
5525
|
activities = _approval_activities_by_id(data)
|
|
5499
5526
|
_validate_v3_plan_backlinks(data, activities, failures)
|
|
5500
5527
|
approved = (data.get("frontmatter") or {}).get("approved") is True
|
|
5528
|
+
incorporated = incorporated_clarification_ids(data)
|
|
5501
5529
|
for row in data.get("clarificationItems") or []:
|
|
5502
5530
|
if not isinstance(row, dict) or row.get("blocks") != "approval":
|
|
5503
5531
|
continue
|
|
@@ -5511,8 +5539,11 @@ def _validate_v3_approval_context(data: dict, failures: list[str]) -> None:
|
|
|
5511
5539
|
row, context, failures, schema_version="3.0"
|
|
5512
5540
|
)
|
|
5513
5541
|
_validate_v3_resolution_links(row, activities, failures)
|
|
5542
|
+
row_id = str(row.get("id") or "")
|
|
5514
5543
|
if approved and row_blocks_progress(
|
|
5515
|
-
str(row.get("status") or ""),
|
|
5544
|
+
str(row.get("status") or ""),
|
|
5545
|
+
clarification_disposition(row),
|
|
5546
|
+
incorporated=row_id in incorporated,
|
|
5516
5547
|
):
|
|
5517
5548
|
failures.append(
|
|
5518
5549
|
f"final-report data.json: approval is true while clarification "
|
|
@@ -5528,12 +5559,8 @@ def _validate_activity_contract_plan_limits(
|
|
|
5528
5559
|
if not _is_activity_contract_v1_planning(run_manifest):
|
|
5529
5560
|
return
|
|
5530
5561
|
pbv = (data.get("implementationPlanning") or {}).get("planBodyVerification") or {}
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
failures.append(
|
|
5534
|
-
"final-report data.json: activity contract v1 selfFixRoundsApplied "
|
|
5535
|
-
"must be at most one automatic self-fix round"
|
|
5536
|
-
)
|
|
5562
|
+
# 리포트 칸은 이어진 런의 누적이다. 자동 자가수정 1회 상한은 이번 창의
|
|
5563
|
+
# 상태 파일이 세고, 세션 적합성이 그 횟수와 `self-fix-applied` 를 맞춘다.
|
|
5537
5564
|
if pbv.get("selfFixStopReason") == "cause-group-recurrence":
|
|
5538
5565
|
failures.append(
|
|
5539
5566
|
"final-report data.json: activity contract v1 cannot newly emit "
|
|
@@ -6287,7 +6314,11 @@ def _next_step_texts(steps: object) -> list[str]:
|
|
|
6287
6314
|
|
|
6288
6315
|
def _has_unresolved_approval_blocker(data: dict) -> bool:
|
|
6289
6316
|
return bool(
|
|
6290
|
-
progress_blocking_ids(
|
|
6317
|
+
progress_blocking_ids(
|
|
6318
|
+
data.get("clarificationItems"),
|
|
6319
|
+
APPROVAL_BLOCKS,
|
|
6320
|
+
report_data=data,
|
|
6321
|
+
)
|
|
6291
6322
|
)
|
|
6292
6323
|
|
|
6293
6324
|
|
|
@@ -7320,21 +7351,12 @@ def _validate_unresolved_tie_was_reverified(
|
|
|
7320
7351
|
data: dict,
|
|
7321
7352
|
failures: list[str],
|
|
7322
7353
|
) -> None:
|
|
7323
|
-
"""A split panel
|
|
7354
|
+
"""A split panel goes to critic-worker before the gate is declared.
|
|
7324
7355
|
|
|
7325
7356
|
The gate needs a strict majority to block, so a panel splitting evenly on a
|
|
7326
7357
|
blocking kind reaches neither consensus nor `majority-disagree`. That state
|
|
7327
|
-
is classified `needs-reverify
|
|
7328
|
-
|
|
7329
|
-
peer that returned nothing) and wrong for this one: nothing failed here, two
|
|
7330
|
-
verifiers read the same plan and disagreed, and passing on that records a
|
|
7331
|
-
dissent nobody acted on.
|
|
7332
|
-
|
|
7333
|
-
So the round is not optional. Re-dispatch those items and record the votes
|
|
7334
|
-
with `--round 2`; a split that survives becomes `majority-disagree` and the
|
|
7335
|
-
user decides. This is satisfiable with the machinery the contract already
|
|
7336
|
-
defines — it is the same targeted re-verification step 7 runs after a
|
|
7337
|
-
self-fix, with the tied items added to that queue.
|
|
7358
|
+
is classified `needs-reverify` until `critic-worker` settles it. Passing
|
|
7359
|
+
without that vote records a dissent nobody acted on.
|
|
7338
7360
|
"""
|
|
7339
7361
|
ip = data.get("implementationPlanning")
|
|
7340
7362
|
if not isinstance(ip, dict):
|
|
@@ -7353,14 +7375,13 @@ def _validate_unresolved_tie_was_reverified(
|
|
|
7353
7375
|
if not unsettled:
|
|
7354
7376
|
return
|
|
7355
7377
|
failures.append(
|
|
7356
|
-
|
|
7357
|
-
"on a blocking breakage kind and
|
|
7358
|
-
"
|
|
7359
|
-
"
|
|
7360
|
-
"
|
|
7361
|
-
"
|
|
7362
|
-
"
|
|
7363
|
-
"survives that round becomes `majority-disagree` and goes to the user."
|
|
7378
|
+
"final-report data.json: plan item(s) "
|
|
7379
|
+
f"{unsettled} carry an even split on a blocking breakage kind and "
|
|
7380
|
+
f"have no `{CRITIC_WORKER_ID}` vote. A tie is not consensus. Dispatch "
|
|
7381
|
+
f"`{CRITIC_WORKER_ID}` on those items only (`okstra plan-items "
|
|
7382
|
+
"prepare --tie-vote`) and record the vote with `okstra plan-items "
|
|
7383
|
+
"apply-verdicts --append --round 2`. Critic AGREE settles the split; "
|
|
7384
|
+
"critic DISAGREE blocks."
|
|
7364
7385
|
)
|
|
7365
7386
|
|
|
7366
7387
|
|
|
@@ -7368,7 +7389,7 @@ def _validate_tie_received_extra_vote(
|
|
|
7368
7389
|
data: dict,
|
|
7369
7390
|
failures: list[str],
|
|
7370
7391
|
) -> None:
|
|
7371
|
-
"""동수는 같은 둘을 다시 돌리는 것이 아니라
|
|
7392
|
+
"""동수는 같은 둘을 다시 돌리는 것이 아니라 critic 이 가른다."""
|
|
7372
7393
|
ip = data.get("implementationPlanning")
|
|
7373
7394
|
if not isinstance(ip, dict):
|
|
7374
7395
|
return
|
|
@@ -7384,17 +7405,17 @@ def _validate_tie_received_extra_vote(
|
|
|
7384
7405
|
and str(item.get("id") or "").strip() not in accepted
|
|
7385
7406
|
and _stage_scope_bucket(item, pbv) == "in-scope"
|
|
7386
7407
|
and _is_even_blocking_split(item)
|
|
7387
|
-
and
|
|
7408
|
+
and not _critic_non_error_verdicts(item)
|
|
7388
7409
|
})
|
|
7389
7410
|
if not missing:
|
|
7390
7411
|
return
|
|
7391
7412
|
failures.append(
|
|
7392
7413
|
f"final-report data.json: plan item(s) {missing} carry an even split "
|
|
7393
|
-
"on a blocking breakage kind and have no
|
|
7394
|
-
"original two does not settle a 1-1 split. Dispatch
|
|
7395
|
-
"whose prompt is those items only
|
|
7396
|
-
"--tie-vote`) and record the vote with
|
|
7397
|
-
"apply-verdicts --append --round 2`."
|
|
7414
|
+
"on a blocking breakage kind and have no critic vote. Re-running the "
|
|
7415
|
+
"original two does not settle a 1-1 split. Dispatch "
|
|
7416
|
+
f"`{CRITIC_WORKER_ID}` whose prompt is those items only "
|
|
7417
|
+
"(`okstra plan-items prepare --tie-vote`) and record the vote with "
|
|
7418
|
+
"`okstra plan-items apply-verdicts --append --round 2`."
|
|
7398
7419
|
)
|
|
7399
7420
|
|
|
7400
7421
|
|
|
@@ -7411,14 +7432,6 @@ def _is_even_blocking_split(item: dict) -> bool:
|
|
|
7411
7432
|
return _is_unsettled_tie(forced)
|
|
7412
7433
|
|
|
7413
7434
|
|
|
7414
|
-
def _distinct_verdict_workers(item: dict) -> int:
|
|
7415
|
-
return len({
|
|
7416
|
-
str(row.get("worker") or "")
|
|
7417
|
-
for row in (item.get("verdicts") or [])
|
|
7418
|
-
if isinstance(row, dict) and str(row.get("worker") or "").strip()
|
|
7419
|
-
})
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
7435
|
def _validate_advisory_plan_body_gating(data: dict, failures: list[str]) -> None:
|
|
7423
7436
|
"""gating=false 는 검출 표면 0 + 스테이지 1 일 때만 받는다."""
|
|
7424
7437
|
ip = data.get("implementationPlanning")
|
|
@@ -9622,7 +9635,13 @@ def _validate_convergence_rounds_match_manifest(
|
|
|
9622
9635
|
def _validate_convergence_states(
|
|
9623
9636
|
run_dir, failures, run_manifest: dict | None = None, project_root: Path | None = None,
|
|
9624
9637
|
) -> None:
|
|
9625
|
-
"""이번 런이 가리키는 수렴 상태만 본다. 디렉터리의 옛 seq 파일은 건너뛴다.
|
|
9638
|
+
"""이번 런이 가리키는 수렴 상태만 본다. 디렉터리의 옛 seq 파일은 건너뛴다.
|
|
9639
|
+
|
|
9640
|
+
매니페스트가 경로를 채워도 파일이 없으면 검사하지 않는다. render-only 와
|
|
9641
|
+
workflow 픽스처는 수렴을 돌리지 않아 파일이 없고, 없는 파일을 실패로 치면
|
|
9642
|
+
예전 glob 이 빈 결과를 내던 계약이 깨진다. 목적은 현재 런이 아닌 seq 를
|
|
9643
|
+
보지 않는 것이다.
|
|
9644
|
+
"""
|
|
9626
9645
|
from pathlib import Path as _Path
|
|
9627
9646
|
|
|
9628
9647
|
declared = (run_manifest or {}).get("convergenceStatePath")
|
|
@@ -9632,7 +9651,7 @@ def _validate_convergence_states(
|
|
|
9632
9651
|
if project_root is None:
|
|
9633
9652
|
return
|
|
9634
9653
|
path = project_root / path
|
|
9635
|
-
paths = [path]
|
|
9654
|
+
paths = [path] if path.is_file() else []
|
|
9636
9655
|
else:
|
|
9637
9656
|
state_dir = _Path(run_dir) / "state"
|
|
9638
9657
|
if not state_dir.is_dir():
|
|
@@ -9644,10 +9663,6 @@ def _validate_convergence_states(
|
|
|
9644
9663
|
]
|
|
9645
9664
|
for state_path in paths:
|
|
9646
9665
|
if not state_path.is_file():
|
|
9647
|
-
if isinstance(declared, str) and declared.strip():
|
|
9648
|
-
failures.append(
|
|
9649
|
-
f"convergence state {state_path.name}: missing declared artifact"
|
|
9650
|
-
)
|
|
9651
9666
|
continue
|
|
9652
9667
|
try:
|
|
9653
9668
|
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
@@ -47,6 +47,7 @@ from okstra_ctl.lead_events import ( # noqa: E402
|
|
|
47
47
|
)
|
|
48
48
|
from okstra_ctl.agent_activity import ACTIVITY_FIELDS # noqa: E402
|
|
49
49
|
from okstra_ctl.domain.host import HostNotRegistered # noqa: E402
|
|
50
|
+
from okstra_ctl.final_report_paths import final_report_data_path # noqa: E402
|
|
50
51
|
from okstra_ctl.registry.host_registry import default_host_registry # noqa: E402
|
|
51
52
|
from okstra_ctl.wrapper_status import read_wrapper_status # noqa: E402
|
|
52
53
|
|
|
@@ -624,16 +625,7 @@ def _plan_body_state_path(run_dir: Path, suffix: str) -> Path:
|
|
|
624
625
|
|
|
625
626
|
def _ids_reported_as_asked(report_path: Path) -> list[str]:
|
|
626
627
|
"""리포트가 "사용자에게 물었다"고 기록한 열린 승인 차단 행의 id."""
|
|
627
|
-
|
|
628
|
-
if not name.endswith(".md"):
|
|
629
|
-
return []
|
|
630
|
-
data_path = report_path.with_name(name.removesuffix(".md") + ".data.json")
|
|
631
|
-
try:
|
|
632
|
-
doc = json.loads(data_path.read_text())
|
|
633
|
-
except (OSError, json.JSONDecodeError):
|
|
634
|
-
return []
|
|
635
|
-
if not isinstance(doc, dict):
|
|
636
|
-
return []
|
|
628
|
+
doc = _read_report_data(report_path)
|
|
637
629
|
asked: list[str] = []
|
|
638
630
|
for row in doc.get("clarificationItems") or []:
|
|
639
631
|
if not isinstance(row, dict):
|
|
@@ -666,12 +658,13 @@ def _activity_index(events: list[LeadEvent]) -> dict[str, list[LeadEvent]]:
|
|
|
666
658
|
|
|
667
659
|
|
|
668
660
|
def _read_report_data(report_path: Path) -> Mapping[str, Any]:
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
661
|
+
"""`--report` 가 data.json · markdown · html 이어도 같은 레코드를 연다.
|
|
662
|
+
|
|
663
|
+
Phase 7 는 data.json 을 넘긴다. `.md` 만 받던 동안 투영과 self-fix 횟수가
|
|
664
|
+
빈 객체에서 나와 `projected=<missing>` / `selfFixRoundsApplied=0` 이 됐다.
|
|
665
|
+
"""
|
|
673
666
|
try:
|
|
674
|
-
data = json.loads(
|
|
667
|
+
data = json.loads(final_report_data_path(report_path).read_text(encoding="utf-8"))
|
|
675
668
|
except (OSError, json.JSONDecodeError):
|
|
676
669
|
return {}
|
|
677
670
|
return data if isinstance(data, Mapping) else {}
|
|
@@ -758,7 +751,16 @@ def _check_projected_agent_activity(
|
|
|
758
751
|
{field: event.details.get(field) for field in ACTIVITY_FIELDS}
|
|
759
752
|
for event in events
|
|
760
753
|
]
|
|
761
|
-
|
|
754
|
+
raw_projected = report_data.get("agentActivity")
|
|
755
|
+
projected = (
|
|
756
|
+
[
|
|
757
|
+
{field: row.get(field) for field in ACTIVITY_FIELDS}
|
|
758
|
+
for row in raw_projected
|
|
759
|
+
if isinstance(row, Mapping)
|
|
760
|
+
]
|
|
761
|
+
if isinstance(raw_projected, list)
|
|
762
|
+
else raw_projected
|
|
763
|
+
)
|
|
762
764
|
if projected == expected:
|
|
763
765
|
return
|
|
764
766
|
mismatch = "length"
|
|
@@ -850,6 +852,43 @@ def _allowed_automatic_rounds(self_fix_rounds: int, *, gating: bool = True) -> i
|
|
|
850
852
|
return 2 + max(self_fix_rounds, 0)
|
|
851
853
|
|
|
852
854
|
|
|
855
|
+
def _self_fix_rounds_from_state(run_dir: Path, suffix: str | None) -> int | None:
|
|
856
|
+
"""이번 런 상태 파일의 자가수정 횟수. 없으면 None — 호출자가 리포트로 폴백.
|
|
857
|
+
|
|
858
|
+
리포트 칸은 이어진 seq 의 누적이라, 이번 창의 `self-fix-applied` 건수와
|
|
859
|
+
비교하면 어긋난다.
|
|
860
|
+
"""
|
|
861
|
+
if not suffix:
|
|
862
|
+
return None
|
|
863
|
+
try:
|
|
864
|
+
doc = json.loads(_plan_body_state_path(run_dir, suffix).read_text())
|
|
865
|
+
except (OSError, json.JSONDecodeError):
|
|
866
|
+
return None
|
|
867
|
+
if not isinstance(doc, dict):
|
|
868
|
+
return None
|
|
869
|
+
projection = doc.get("planBodyVerification")
|
|
870
|
+
value = (
|
|
871
|
+
projection.get("selfFixRoundsApplied")
|
|
872
|
+
if isinstance(projection, dict)
|
|
873
|
+
else None
|
|
874
|
+
)
|
|
875
|
+
if isinstance(value, int) and value >= 0:
|
|
876
|
+
return value
|
|
877
|
+
value = doc.get("selfFixRoundsApplied")
|
|
878
|
+
return value if isinstance(value, int) and value >= 0 else None
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _is_plan_body_verification_activity(event: LeadEvent) -> bool:
|
|
882
|
+
"""`verification-round-completed` 가 계획 본문 배치인지.
|
|
883
|
+
|
|
884
|
+
같은 kind 로 적대 재검증 라운드도 남는다. 그 건을 본문 라운드에 넣으면
|
|
885
|
+
recorded 가 roundCount 보다 커진다. 요약이 `adversarial reverify` 이면
|
|
886
|
+
본문이 아니다. 픽스처 요약(`verification-round-completed for …`)은 본문이다.
|
|
887
|
+
"""
|
|
888
|
+
summary = str(event.details.get("summary") or "").lower()
|
|
889
|
+
return "adversarial reverify" not in summary and "adversarial re-verify" not in summary
|
|
890
|
+
|
|
891
|
+
|
|
853
892
|
def _plan_body_verification(report_data: Mapping[str, Any]) -> Mapping[str, Any] | None:
|
|
854
893
|
planning = report_data.get("implementationPlanning")
|
|
855
894
|
verification = (
|
|
@@ -970,14 +1009,23 @@ def _check_activity_round_counts(
|
|
|
970
1009
|
errors: list[str],
|
|
971
1010
|
) -> None:
|
|
972
1011
|
verification_rounds = _plan_body_rounds_ran(run_dir, suffix)
|
|
973
|
-
recorded_verifications = len(
|
|
1012
|
+
recorded_verifications = len([
|
|
1013
|
+
event
|
|
1014
|
+
for event in indexed.get("verification-round-completed", [])
|
|
1015
|
+
if _is_plan_body_verification_activity(event)
|
|
1016
|
+
])
|
|
974
1017
|
user_reverification_rounds = _resolved_correctness_reverification_rounds(
|
|
975
1018
|
indexed,
|
|
976
1019
|
report_data,
|
|
977
1020
|
verification_rounds,
|
|
978
1021
|
)
|
|
979
1022
|
automatic_rounds = verification_rounds - len(user_reverification_rounds)
|
|
980
|
-
|
|
1023
|
+
state_self_fix = _self_fix_rounds_from_state(run_dir, suffix)
|
|
1024
|
+
self_fix_rounds = (
|
|
1025
|
+
state_self_fix
|
|
1026
|
+
if state_self_fix is not None
|
|
1027
|
+
else _self_fix_rounds_applied(report_data)
|
|
1028
|
+
)
|
|
981
1029
|
allowed_rounds = _allowed_automatic_rounds(
|
|
982
1030
|
self_fix_rounds, gating=_plan_body_gating(report_data),
|
|
983
1031
|
)
|
|
@@ -1435,6 +1483,11 @@ def _check_cmux_adapter_read(
|
|
|
1435
1483
|
return
|
|
1436
1484
|
if evidence.sidecar_reads.get(CMUX_ADAPTER_BASENAME):
|
|
1437
1485
|
return
|
|
1486
|
+
dispatch_mode = str(team_state.get("dispatchMode", "")).strip()
|
|
1487
|
+
if dispatch_mode in _WORKER_DISPATCH_MODES:
|
|
1488
|
+
# grok 같은 artifact-only 호스트는 Read 도구 기록이 없다. 워커가
|
|
1489
|
+
# okstra 디스패치로 나갔으면 어댑터가 가리키는 경로를 탄 것이다.
|
|
1490
|
+
return
|
|
1438
1491
|
errors.append(
|
|
1439
1492
|
f"cmux adapter: no read of `{CMUX_ADAPTER_BASENAME}` (a `Read` call or a "
|
|
1440
1493
|
f"shell command naming it) found in the "
|