okstra 0.163.2 → 0.165.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 +8 -6
- package/docs/architecture.md +24 -15
- package/docs/cli.md +15 -8
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-inspect.md +2 -2
- package/docs/for-ai/skills/okstra-user-response.md +2 -2
- package/docs/project-structure-overview.md +22 -14
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +9 -7
- package/runtime/agents/workers/claude-worker.md +1 -0
- package/runtime/agents/workers/codex-worker.md +9 -7
- package/runtime/agents/workers/grok-worker.md +6 -4
- package/runtime/agents/workers/kimi-worker.md +6 -4
- package/runtime/bin/lib/okstra/cli.sh +5 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/bin/okstra-antigravity-exec.sh +1 -340
- package/runtime/bin/okstra-claude-exec.sh +1 -178
- package/runtime/bin/okstra-codex-exec.sh +1 -467
- package/runtime/bin/okstra-provider-exec.py +165 -190
- package/runtime/bin/okstra-trace-cleanup.sh +14 -7
- package/runtime/bin/okstra-wrapper-status.py +26 -19
- package/runtime/bin/okstra.sh +87 -91
- package/runtime/prompts/lead/adapters/cmux.md +2 -2
- package/runtime/prompts/lead/convergence.md +36 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +24 -1
- package/runtime/prompts/lead/plan-body-verification.md +9 -1
- package/runtime/prompts/lead/report-writer.md +1 -0
- package/runtime/prompts/lead/team-contract.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +9 -1
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/implementation-planning.md +6 -4
- package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
- package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +112 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +53 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +183 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +110 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +84 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +76 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +80 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/application/__init__.py +1 -0
- package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
- package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
- package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
- package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
- package/runtime/python/okstra_ctl/application/start_run.py +21 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
- package/runtime/python/okstra_ctl/dispatch_core.py +244 -29
- package/runtime/python/okstra_ctl/dispatch_state.py +27 -0
- package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
- package/runtime/python/okstra_ctl/domain/host.py +100 -0
- package/runtime/python/okstra_ctl/domain/provider.py +70 -0
- package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +102 -0
- package/runtime/python/okstra_ctl/domain/worker_role.py +34 -0
- package/runtime/python/okstra_ctl/domain/worker_stream.py +261 -0
- package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
- package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
- package/runtime/python/okstra_ctl/models.py +54 -269
- package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
- package/runtime/python/okstra_ctl/ports/host.py +32 -0
- package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
- package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
- package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
- package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
- package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
- package/runtime/python/okstra_ctl/render.py +145 -47
- package/runtime/python/okstra_ctl/report_html/common.py +71 -25
- package/runtime/python/okstra_ctl/report_html/models.py +5 -0
- package/runtime/python/okstra_ctl/report_html/render.py +1 -1
- package/runtime/python/okstra_ctl/report_html/run_usage.py +19 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -0
- package/runtime/python/okstra_ctl/report_views.py +44 -16
- package/runtime/python/okstra_ctl/run.py +80 -58
- package/runtime/python/okstra_ctl/session.py +1 -1
- package/runtime/python/okstra_ctl/stage_citations.py +52 -15
- package/runtime/python/okstra_ctl/team.py +44 -32
- package/runtime/python/okstra_ctl/user_response.py +45 -29
- package/runtime/python/okstra_ctl/wizard.py +175 -73
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -3
- package/runtime/python/okstra_ctl/worker_request.py +140 -0
- package/runtime/python/okstra_ctl/worker_runner.py +622 -0
- package/runtime/python/okstra_token_usage/collect.py +42 -7
- package/runtime/python/okstra_token_usage/report.py +42 -0
- package/runtime/python/okstra_token_usage/task_totals.py +88 -0
- package/runtime/schemas/final-report-v1.0.schema.json +4040 -1066
- package/runtime/schemas/final-report-v2.0.schema.json +5673 -1412
- package/runtime/skills/okstra-inspect/SKILL.md +1 -2
- package/runtime/skills/okstra-inspect/facets/logs.md +5 -5
- package/runtime/skills/okstra-inspect/facets/run-audit.md +3 -3
- package/runtime/skills/okstra-run/SKILL.md +74 -29
- package/runtime/skills/okstra-user-response/SKILL.md +15 -5
- package/runtime/templates/implementation-worker-preamble.md +1 -1
- package/runtime/templates/report-writer-prompt-preamble.md +1 -0
- package/runtime/templates/reports/final-report.template.md +3 -3
- package/runtime/templates/reports/html/assets/base.css +8 -4
- package/runtime/templates/reports/html/base.template.html +12 -6
- package/runtime/templates/reports/html/i18n/en.json +32 -7
- package/runtime/templates/reports/html/i18n/ko.json +32 -7
- package/runtime/templates/reports/html/macros/forms.html +9 -3
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +14 -19
- package/runtime/templates/reports/report.js +59 -26
- package/runtime/templates/reports/user-response.template.md +12 -8
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +17 -22
- package/runtime/validators/validate-report-views.py +0 -39
- package/runtime/validators/validate-run.py +96 -14
- package/runtime/validators/validate_session_conformance.py +69 -3
- package/src/cli-registry.mjs +0 -7
- package/src/commands/execute/render-bundle.mjs +4 -4
- package/src/commands/execute/run.mjs +8 -25
- package/src/commands/execute/wizard.mjs +33 -13
- package/src/commands/lifecycle/doctor.mjs +10 -10
- package/src/commands/lifecycle/install.mjs +53 -30
- package/src/commands/lifecycle/preflight.mjs +14 -4
- package/src/lib/host-registry-client.mjs +176 -0
- package/src/lib/runtime-manifest.mjs +6 -8
- package/runtime/bin/okstra-wrapper-agy-stream.py +0 -61
- package/runtime/python/okstra_ctl/error_issue.py +0 -640
- package/runtime/python/okstra_ctl/issue_signals.py +0 -186
- package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
- package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
- package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
- package/src/commands/inspect/error-issue.mjs +0 -27
- package/src/lib/runtime-readiness.mjs +0 -90
- package/src/lib/runtime-resolver.mjs +0 -123
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The sidecar format is documented in ``templates/reports/user-response.template.md``
|
|
4
4
|
and produced byte-identically by ``report_views.serialize_user_response`` (Python)
|
|
5
5
|
and ``templates/reports/report.js`` (browser). This module owns the read side of
|
|
6
|
-
the ``##
|
|
6
|
+
the ``## PLAN DECISION`` block used by the implementation wizard and the optional
|
|
7
7
|
``## ANALYSIS REVIEW`` block used by analysis reruns.
|
|
8
8
|
"""
|
|
9
9
|
from __future__ import annotations
|
|
@@ -19,7 +19,8 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Optional
|
|
20
20
|
|
|
21
21
|
from okstra_ctl.report_views import (
|
|
22
|
-
|
|
22
|
+
PLAN_DECISION_APPROVED,
|
|
23
|
+
serialize_user_response, UserResponseEntry, UserPlanDecision, infer_run_meta,
|
|
23
24
|
parse_expected_form_options,
|
|
24
25
|
)
|
|
25
26
|
from okstra_ctl.report_view_artifacts import user_responses_dir_for_report
|
|
@@ -32,7 +33,7 @@ from okstra_ctl.clarification_items import (
|
|
|
32
33
|
_section_1_slice,
|
|
33
34
|
)
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
_PLAN_DECISION_HEADING_RE = re.compile(r"^## PLAN DECISION\s*$", re.MULTILINE)
|
|
36
37
|
_NEXT_RESPONSE_HEADING_RE = re.compile(r"^## ", re.MULTILINE)
|
|
37
38
|
_ANALYSIS_REVIEW_HEADING_RE = re.compile(r"^## ANALYSIS REVIEW\s*$", re.MULTILINE)
|
|
38
39
|
_ANALYSIS_SIDECAR_HEADING_RE = re.compile(
|
|
@@ -50,13 +51,18 @@ class UserResponseError(ValueError):
|
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
@dataclass(frozen=True)
|
|
53
|
-
class
|
|
54
|
-
"""sidecar 의 ``##
|
|
55
|
-
|
|
54
|
+
class PlanDecisionRecord:
|
|
55
|
+
"""sidecar 의 ``## PLAN DECISION`` 블록 + 매칭에 필요한 frontmatter 필드."""
|
|
56
|
+
status: str
|
|
56
57
|
implementation_option: str
|
|
58
|
+
reason: str
|
|
57
59
|
source_report: str
|
|
58
60
|
seq: str
|
|
59
61
|
|
|
62
|
+
@property
|
|
63
|
+
def approved(self) -> bool:
|
|
64
|
+
return self.status == PLAN_DECISION_APPROVED
|
|
65
|
+
|
|
60
66
|
|
|
61
67
|
@dataclass(frozen=True)
|
|
62
68
|
class AnalysisReviewRecord:
|
|
@@ -353,31 +359,37 @@ def load_authoritative_analysis_review(
|
|
|
353
359
|
return review
|
|
354
360
|
|
|
355
361
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
+
_PLAN_DECISION_STATUS_RE = re.compile(
|
|
363
|
+
r"^- Status:\s*(approved|revision-requested|rejected)\s*$", re.MULTILINE
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def parse_plan_decision(sidecar_text: str) -> Optional[PlanDecisionRecord]:
|
|
368
|
+
"""``## PLAN DECISION`` 블록을 읽어 record 로 돌려준다. 승인·재작업·반려를
|
|
369
|
+
구분하지 않고 그대로 싣는다 — 어느 판정을 받아들일지는 소비 측 정책이다.
|
|
370
|
+
옵션·사유 라인은 블록 내부에서만 읽는다 (다른 응답 본문의 우연한 동일
|
|
371
|
+
문구를 옵션으로 오인하지 않도록).
|
|
362
372
|
|
|
363
|
-
strictness 는 의도적이다: producer 출력과 byte-identical 한 소문자
|
|
364
|
-
|
|
373
|
+
strictness 는 의도적이다: producer 출력과 byte-identical 한 소문자 status
|
|
374
|
+
만 인정하며, 손편집 변형(``Approved``/``REJECTED``)은 fail-closed 로
|
|
365
375
|
불인정한다."""
|
|
366
|
-
m =
|
|
376
|
+
m = _PLAN_DECISION_HEADING_RE.search(sidecar_text)
|
|
367
377
|
if not m:
|
|
368
378
|
return None
|
|
369
379
|
block = sidecar_text[m.end():]
|
|
370
380
|
nxt = _NEXT_RESPONSE_HEADING_RE.search(block)
|
|
371
381
|
if nxt:
|
|
372
382
|
block = block[: nxt.start()]
|
|
373
|
-
|
|
383
|
+
status = _PLAN_DECISION_STATUS_RE.search(block)
|
|
384
|
+
if status is None:
|
|
374
385
|
return None
|
|
375
386
|
om = re.search(r"^- Implementation-Option:\s*(\S.*?)\s*$", block, re.MULTILINE)
|
|
376
387
|
sm = re.search(r"^seq:\s*(\S+)\s*$", sidecar_text, re.MULTILINE)
|
|
377
388
|
rm = re.search(r"^source-report:\s*(\S.*?)\s*$", sidecar_text, re.MULTILINE)
|
|
378
|
-
return
|
|
379
|
-
|
|
389
|
+
return PlanDecisionRecord(
|
|
390
|
+
status=status.group(1),
|
|
380
391
|
implementation_option=om.group(1) if om else "",
|
|
392
|
+
reason=_quoted_review_value(block, "Reason"),
|
|
381
393
|
source_report=rm.group(1) if rm else "",
|
|
382
394
|
seq=sm.group(1) if sm else "",
|
|
383
395
|
)
|
|
@@ -404,8 +416,8 @@ def _value(block: str) -> str:
|
|
|
404
416
|
|
|
405
417
|
def parse_user_response_entries(sidecar_text: str) -> list[UserResponseEntry]:
|
|
406
418
|
"""Reverse of ``serialize_user_response`` for the per-response ``## C-*``
|
|
407
|
-
blocks. The ``##
|
|
408
|
-
``
|
|
419
|
+
blocks. The ``## PLAN DECISION`` block is skipped (read separately by
|
|
420
|
+
``parse_plan_decision``)."""
|
|
409
421
|
entries: list[UserResponseEntry] = []
|
|
410
422
|
matches = list(_RESPONSE_HEADING_RE.finditer(sidecar_text))
|
|
411
423
|
for i, m in enumerate(matches):
|
|
@@ -581,7 +593,7 @@ def show_open_rows(report_path: Path) -> dict:
|
|
|
581
593
|
|
|
582
594
|
|
|
583
595
|
def write_sidecar(report_path: Path, answers: list[dict],
|
|
584
|
-
|
|
596
|
+
plan_decision: Optional[dict], created_at: str,
|
|
585
597
|
task_key: str = "") -> Path:
|
|
586
598
|
run_meta = infer_run_meta(report_path, task_key=task_key or None)
|
|
587
599
|
out_dir = user_responses_dir_for_report(report_path)
|
|
@@ -597,13 +609,15 @@ def write_sidecar(report_path: Path, answers: list[dict],
|
|
|
597
609
|
response_id=a["id"], kind=a.get("kind", ""), value=a["value"],
|
|
598
610
|
rationale=a.get("rationale"), disposition=a.get("disposition", "answer"))
|
|
599
611
|
|
|
600
|
-
|
|
601
|
-
if
|
|
602
|
-
|
|
603
|
-
|
|
612
|
+
decision = None
|
|
613
|
+
if plan_decision and plan_decision.get("status"):
|
|
614
|
+
decision = UserPlanDecision(
|
|
615
|
+
status=plan_decision["status"],
|
|
616
|
+
implementation_option=plan_decision.get("implementationOption", ""),
|
|
617
|
+
reason=plan_decision.get("reason", ""))
|
|
604
618
|
sidecar.write_text(
|
|
605
619
|
serialize_user_response(run_meta=run_meta, entries=list(merged.values()),
|
|
606
|
-
created_at=created_at,
|
|
620
|
+
created_at=created_at, plan_decision=decision),
|
|
607
621
|
encoding="utf-8")
|
|
608
622
|
return sidecar
|
|
609
623
|
|
|
@@ -623,7 +637,9 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|
|
623
637
|
pw = sub.add_parser("write")
|
|
624
638
|
pw.add_argument("--report", required=True)
|
|
625
639
|
pw.add_argument("--answers", required=True, help="JSON array of answer entries")
|
|
626
|
-
pw.add_argument(
|
|
640
|
+
pw.add_argument(
|
|
641
|
+
"--plan-decision", default="",
|
|
642
|
+
help='JSON plan decision, e.g. {"status":"rejected","reason":"..."}')
|
|
627
643
|
pw.add_argument("--task-key", default="", help="task-key from list/show context")
|
|
628
644
|
|
|
629
645
|
ns = parser.parse_args(argv)
|
|
@@ -636,9 +652,9 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|
|
636
652
|
return 0
|
|
637
653
|
if ns.cmd == "write":
|
|
638
654
|
answers = json.loads(ns.answers)
|
|
639
|
-
|
|
655
|
+
decision = json.loads(ns.plan_decision) if ns.plan_decision else None
|
|
640
656
|
created_at = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
641
|
-
p = write_sidecar(Path(ns.report), answers,
|
|
657
|
+
p = write_sidecar(Path(ns.report), answers, decision, created_at,
|
|
642
658
|
task_key=ns.task_key)
|
|
643
659
|
json.dump({"sidecar": str(p)}, sys.stdout, ensure_ascii=False)
|
|
644
660
|
return 0
|
|
@@ -29,6 +29,23 @@ from datetime import datetime
|
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
from typing import Any, Callable, Optional
|
|
31
31
|
|
|
32
|
+
from okstra_ctl.application.advance_wizard import plan_prompt
|
|
33
|
+
from okstra_ctl.application.resolve_assignment import resolve_lead_provider
|
|
34
|
+
from okstra_ctl.domain.host import (
|
|
35
|
+
HostCapabilityMismatch,
|
|
36
|
+
HostNotRegistered,
|
|
37
|
+
HostSessionContext,
|
|
38
|
+
ProviderUnavailable,
|
|
39
|
+
)
|
|
40
|
+
from okstra_ctl.domain.wizard.interaction import (
|
|
41
|
+
InteractionPlan,
|
|
42
|
+
WizardAnswerError,
|
|
43
|
+
WizardOption,
|
|
44
|
+
WizardPrompt,
|
|
45
|
+
normalize_planned_answer,
|
|
46
|
+
)
|
|
47
|
+
from okstra_ctl.registry.host_registry import default_host_registry
|
|
48
|
+
from okstra_ctl.registry.provider_registry import default_provider_registry
|
|
32
49
|
from okstra_ctl.ids import slugify_task_segment
|
|
33
50
|
from okstra_ctl.brief_frontmatter import read_brief_frontmatter
|
|
34
51
|
from okstra_ctl.analysis_inputs import (
|
|
@@ -44,13 +61,12 @@ from okstra_ctl.analysis_inputs import (
|
|
|
44
61
|
from okstra_ctl.models import (
|
|
45
62
|
PROVIDER_MAPPINGS,
|
|
46
63
|
UnknownModelError,
|
|
64
|
+
UnknownProviderError,
|
|
47
65
|
picker_options,
|
|
48
66
|
provider_ids,
|
|
49
67
|
provider_supports_role,
|
|
50
68
|
resolve_model_metadata,
|
|
51
69
|
)
|
|
52
|
-
from okstra_ctl.lead_runtime import lead_runtime_info
|
|
53
|
-
from okstra_ctl.runner_resolution import native_provider_for_host
|
|
54
70
|
from okstra_ctl.clarification_items import (
|
|
55
71
|
scan_approval_gate,
|
|
56
72
|
sidecar_answers,
|
|
@@ -85,8 +101,8 @@ from okstra_ctl.stage_map import (
|
|
|
85
101
|
stage_map_records,
|
|
86
102
|
)
|
|
87
103
|
from okstra_ctl.user_response import (
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
PlanDecisionRecord,
|
|
105
|
+
parse_plan_decision,
|
|
90
106
|
)
|
|
91
107
|
from okstra_ctl.workers import (
|
|
92
108
|
ALLOWED_WORKERS,
|
|
@@ -176,7 +192,6 @@ _BRIEF_ENTRY_TASK_TYPES = (
|
|
|
176
192
|
|
|
177
193
|
CANONICAL_BASE_REFS = ["main", "dev", "staging", "preprod", "prod"]
|
|
178
194
|
BASE_REF_FREE_INPUT_TOKEN = "__free_input__"
|
|
179
|
-
HOST_PICK_OPTION_CAP = 4
|
|
180
195
|
|
|
181
196
|
CLAUDE_MODEL_OPTIONS = ["default", *picker_options("claude")]
|
|
182
197
|
CODEX_MODEL_OPTIONS = ["default", *picker_options("codex")]
|
|
@@ -389,6 +404,8 @@ class WizardState:
|
|
|
389
404
|
project_root: str = ""
|
|
390
405
|
project_id: str = ""
|
|
391
406
|
host_runtime: str = "claude-code"
|
|
407
|
+
host_entry_mode: str = "current-session"
|
|
408
|
+
available_functions: list[str] = field(default_factory=list)
|
|
392
409
|
|
|
393
410
|
# task identity
|
|
394
411
|
is_new_task: Optional[bool] = None
|
|
@@ -535,8 +552,6 @@ class Prompt:
|
|
|
535
552
|
"echoTemplate": self.echo_template,
|
|
536
553
|
"multi": self.multi,
|
|
537
554
|
}
|
|
538
|
-
if self.kind == "pick" and len(self.options) > HOST_PICK_OPTION_CAP:
|
|
539
|
-
out["presentation"] = "numbered-text"
|
|
540
555
|
if self.kind == "pick_group":
|
|
541
556
|
out["questions"] = [
|
|
542
557
|
{"step": q.step, "label": q.label,
|
|
@@ -551,35 +566,6 @@ class WizardError(Exception):
|
|
|
551
566
|
"""validation failure surfaced to user."""
|
|
552
567
|
|
|
553
568
|
|
|
554
|
-
def _normalize_numbered_pick_item(prompt: Prompt, answer: str) -> str:
|
|
555
|
-
candidate = (answer or "").strip()
|
|
556
|
-
exact_values = [option.value for option in prompt.options if option.value == candidate]
|
|
557
|
-
if exact_values:
|
|
558
|
-
return exact_values[0]
|
|
559
|
-
|
|
560
|
-
if candidate.isdecimal():
|
|
561
|
-
number = int(candidate)
|
|
562
|
-
if 1 <= number <= len(prompt.options):
|
|
563
|
-
return prompt.options[number - 1].value
|
|
564
|
-
raise WizardError(f"numbered-text answer is out of range: {candidate}")
|
|
565
|
-
|
|
566
|
-
label_values = [option.value for option in prompt.options if option.label == candidate]
|
|
567
|
-
if len(label_values) == 1:
|
|
568
|
-
return label_values[0]
|
|
569
|
-
if len(label_values) > 1:
|
|
570
|
-
raise WizardError(f"numbered-text answer label is ambiguous: {candidate}")
|
|
571
|
-
raise WizardError(f"numbered-text answer is not an option: {candidate}")
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
def _normalize_numbered_pick_answer(prompt: Prompt, answer: str) -> str:
|
|
575
|
-
if prompt.multi:
|
|
576
|
-
return ",".join(
|
|
577
|
-
_normalize_numbered_pick_item(prompt, item)
|
|
578
|
-
for item in (answer or "").split(",")
|
|
579
|
-
)
|
|
580
|
-
return _normalize_numbered_pick_item(prompt, answer)
|
|
581
|
-
|
|
582
|
-
|
|
583
569
|
# ---- Validation helpers --------------------------------------------------
|
|
584
570
|
|
|
585
571
|
_SLUG_OK = re.compile(r"[a-z0-9]")
|
|
@@ -696,23 +682,27 @@ def _approve_plan_in_place(plan_path: Path) -> None:
|
|
|
696
682
|
|
|
697
683
|
def _find_html_approval_sidecar(
|
|
698
684
|
plan_path: Path,
|
|
699
|
-
) -> Optional[tuple[Path,
|
|
700
|
-
"""plan 의 run 디렉토리 sibling ``user-responses/`` 에서
|
|
701
|
-
|
|
702
|
-
|
|
685
|
+
) -> Optional[tuple[Path, PlanDecisionRecord]]:
|
|
686
|
+
"""plan 의 run 디렉토리 sibling ``user-responses/`` 에서 승인 판정을 담은
|
|
687
|
+
sidecar 를 찾는다. source-report 파일명과 seq 가 plan 과 일치해야 하며,
|
|
688
|
+
복수면 mtime 최신을 택한다.
|
|
689
|
+
|
|
690
|
+
승인이 아닌 판정(반려·재작업 요청)은 여기서 걸러진다 — 이 단계가 묻는 것은
|
|
691
|
+
"사용자가 이 plan 을 승인해 두었는가" 뿐이고, 반려 사유는 다음 planning
|
|
692
|
+
run 이 sidecar 를 통째로 읽어 처리한다."""
|
|
703
693
|
responses_dir = plan_path.parent.parent / "user-responses"
|
|
704
694
|
if not responses_dir.is_dir():
|
|
705
695
|
return None
|
|
706
696
|
m = re.search(r"-(\d+)\.md$", plan_path.name)
|
|
707
697
|
plan_seq = m.group(1) if m else ""
|
|
708
|
-
best: Optional[tuple[float, Path,
|
|
698
|
+
best: Optional[tuple[float, Path, PlanDecisionRecord]] = None
|
|
709
699
|
for f in sorted(responses_dir.glob("user-response-*.md")):
|
|
710
700
|
try:
|
|
711
701
|
text = f.read_text(encoding="utf-8", errors="replace")
|
|
712
702
|
except OSError:
|
|
713
703
|
continue
|
|
714
|
-
rec =
|
|
715
|
-
if rec is None or rec.seq != plan_seq:
|
|
704
|
+
rec = parse_plan_decision(text)
|
|
705
|
+
if rec is None or not rec.approved or rec.seq != plan_seq:
|
|
716
706
|
continue
|
|
717
707
|
if Path(rec.source_report).name != plan_path.name:
|
|
718
708
|
continue
|
|
@@ -3369,6 +3359,28 @@ def _build_reuse_previous(state: WizardState) -> Prompt:
|
|
|
3369
3359
|
)
|
|
3370
3360
|
|
|
3371
3361
|
|
|
3362
|
+
def _restore_reused_lead(state: WizardState, inputs: dict) -> None:
|
|
3363
|
+
requested_provider = _safe_provider(inputs.get("leadProvider"), "lead")
|
|
3364
|
+
try:
|
|
3365
|
+
assignment = resolve_lead_provider(
|
|
3366
|
+
host_id=state.host_runtime,
|
|
3367
|
+
requested_provider=requested_provider,
|
|
3368
|
+
host_registry=default_host_registry(),
|
|
3369
|
+
provider_registry=default_provider_registry(),
|
|
3370
|
+
)
|
|
3371
|
+
except (
|
|
3372
|
+
HostCapabilityMismatch,
|
|
3373
|
+
HostNotRegistered,
|
|
3374
|
+
ProviderUnavailable,
|
|
3375
|
+
UnknownProviderError,
|
|
3376
|
+
) as exc:
|
|
3377
|
+
raise WizardError(
|
|
3378
|
+
f"previous run cannot be reused: {exc}; choose step-by-step re-entry"
|
|
3379
|
+
) from exc
|
|
3380
|
+
state.lead_provider = assignment.provider
|
|
3381
|
+
state.lead_model = _safe_model(assignment.provider, inputs.get("leadModel"))
|
|
3382
|
+
|
|
3383
|
+
|
|
3372
3384
|
def _submit_reuse_previous(state: WizardState, value: str) -> Optional[str]:
|
|
3373
3385
|
if value not in ("yes", "no"):
|
|
3374
3386
|
raise WizardError(f"expected 'yes' or 'no', got: {value!r}")
|
|
@@ -3383,16 +3395,7 @@ def _submit_reuse_previous(state: WizardState, value: str) -> Optional[str]:
|
|
|
3383
3395
|
state.workers_override = (
|
|
3384
3396
|
",".join(w for w in workers if isinstance(w, str))
|
|
3385
3397
|
if isinstance(workers, list) else "")
|
|
3386
|
-
state
|
|
3387
|
-
lead_provider = state.lead_provider or "claude"
|
|
3388
|
-
native_provider = native_provider_for_host(state.host_runtime)
|
|
3389
|
-
if native_provider and lead_provider != native_provider:
|
|
3390
|
-
raise WizardError(
|
|
3391
|
-
f"previous run used provider {lead_provider!r}, but "
|
|
3392
|
-
f"{state.host_runtime} requires provider {native_provider!r}; "
|
|
3393
|
-
"choose step-by-step re-entry"
|
|
3394
|
-
)
|
|
3395
|
-
state.lead_model = _safe_model(lead_provider, inputs.get("leadModel"))
|
|
3398
|
+
_restore_reused_lead(state, inputs)
|
|
3396
3399
|
state.claude_model = _safe_model("claude", inputs.get("claudeModel"))
|
|
3397
3400
|
state.codex_model = _safe_model("codex", inputs.get("codexModel"))
|
|
3398
3401
|
state.antigravity_model = _safe_model("antigravity", inputs.get("antigravityModel"))
|
|
@@ -3601,16 +3604,27 @@ def _model_pick(step: str, label: str, options: list[str], echo: str) -> Prompt:
|
|
|
3601
3604
|
|
|
3602
3605
|
|
|
3603
3606
|
def _qualified_role_model_options(role: str) -> list[str]:
|
|
3604
|
-
|
|
3605
|
-
|
|
3607
|
+
registry = default_provider_registry()
|
|
3608
|
+
options = ["default", *_provider_picker_options("claude", registry)]
|
|
3609
|
+
for provider in registry.ids(role):
|
|
3606
3610
|
if provider == "claude":
|
|
3607
3611
|
continue
|
|
3608
3612
|
options.extend(
|
|
3609
|
-
f"{provider}:{model}"
|
|
3613
|
+
f"{provider}:{model}"
|
|
3614
|
+
for model in _provider_picker_options(provider, registry)
|
|
3610
3615
|
)
|
|
3611
3616
|
return options
|
|
3612
3617
|
|
|
3613
3618
|
|
|
3619
|
+
def _provider_picker_options(provider: str, registry=None) -> list[str]:
|
|
3620
|
+
provider_registry = registry or default_provider_registry()
|
|
3621
|
+
return [
|
|
3622
|
+
alias
|
|
3623
|
+
for alias, model in provider_registry.resolve(provider).models.items()
|
|
3624
|
+
if model.in_picker
|
|
3625
|
+
]
|
|
3626
|
+
|
|
3627
|
+
|
|
3614
3628
|
def _submit_qualified_role_model(
|
|
3615
3629
|
state: WizardState, value: str, role: str, model_field: str, provider_field: str,
|
|
3616
3630
|
) -> str:
|
|
@@ -3629,32 +3643,44 @@ def _submit_qualified_role_model(
|
|
|
3629
3643
|
|
|
3630
3644
|
def _build_lead_model(state: WizardState) -> Prompt:
|
|
3631
3645
|
t = _p(state.workspace_root, "lead_model")
|
|
3632
|
-
provider =
|
|
3646
|
+
provider = default_host_registry().resolve(
|
|
3647
|
+
state.host_runtime
|
|
3648
|
+
).descriptor.native_provider_id
|
|
3633
3649
|
options = (
|
|
3634
|
-
["default", *
|
|
3650
|
+
["default", *_provider_picker_options(provider)]
|
|
3635
3651
|
if provider else _qualified_role_model_options("lead")
|
|
3636
3652
|
)
|
|
3637
3653
|
return _model_pick(S_LEAD_MODEL, t["label"], options, t["echo_template"])
|
|
3638
3654
|
|
|
3639
3655
|
|
|
3640
3656
|
def _submit_lead_model(state: WizardState, value: str) -> Optional[str]:
|
|
3641
|
-
native_provider =
|
|
3657
|
+
native_provider = default_host_registry().resolve(
|
|
3658
|
+
state.host_runtime
|
|
3659
|
+
).descriptor.native_provider_id
|
|
3642
3660
|
if native_provider:
|
|
3643
3661
|
raw = (value or "").strip()
|
|
3662
|
+
requested_provider = ""
|
|
3644
3663
|
if ":" in raw:
|
|
3645
|
-
|
|
3664
|
+
requested_provider, raw = (
|
|
3646
3665
|
part.strip().lower() for part in raw.split(":", 1)
|
|
3647
3666
|
)
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3667
|
+
try:
|
|
3668
|
+
assignment = resolve_lead_provider(
|
|
3669
|
+
host_id=state.host_runtime,
|
|
3670
|
+
requested_provider=requested_provider,
|
|
3671
|
+
host_registry=default_host_registry(),
|
|
3672
|
+
provider_registry=default_provider_registry(),
|
|
3673
|
+
)
|
|
3674
|
+
except (
|
|
3675
|
+
HostCapabilityMismatch,
|
|
3676
|
+
HostNotRegistered,
|
|
3677
|
+
ProviderUnavailable,
|
|
3678
|
+
UnknownProviderError,
|
|
3679
|
+
) as exc:
|
|
3680
|
+
raise WizardError(str(exc)) from exc
|
|
3681
|
+
state.lead_provider = assignment.provider
|
|
3682
|
+
state.lead_model = _validate_model(assignment.provider, raw)
|
|
3683
|
+
return f"lead-model: {assignment.provider}:{state.lead_model or 'default'}"
|
|
3658
3684
|
selected = _submit_qualified_role_model(
|
|
3659
3685
|
state, value, "lead", "lead_model", "lead_provider",
|
|
3660
3686
|
)
|
|
@@ -4529,6 +4555,8 @@ def _reset_field(state: WizardState, fname: str) -> None:
|
|
|
4529
4555
|
def init_state(
|
|
4530
4556
|
*, workspace_root: str, project_root: str, project_id: str,
|
|
4531
4557
|
host_runtime: str = "claude-code",
|
|
4558
|
+
host_entry_mode: str = "current-session",
|
|
4559
|
+
available_functions: Optional[list[str]] = None,
|
|
4532
4560
|
) -> WizardState:
|
|
4533
4561
|
"""Bootstrap a new wizard state."""
|
|
4534
4562
|
return WizardState(
|
|
@@ -4536,6 +4564,8 @@ def init_state(
|
|
|
4536
4564
|
project_root=project_root,
|
|
4537
4565
|
project_id=project_id,
|
|
4538
4566
|
host_runtime=host_runtime,
|
|
4567
|
+
host_entry_mode=host_entry_mode,
|
|
4568
|
+
available_functions=list(available_functions or []),
|
|
4539
4569
|
)
|
|
4540
4570
|
|
|
4541
4571
|
|
|
@@ -4668,9 +4698,73 @@ def _screen_progress(state: WizardState) -> dict[str, Any]:
|
|
|
4668
4698
|
}
|
|
4669
4699
|
|
|
4670
4700
|
|
|
4701
|
+
def _domain_prompt(prompt: Prompt) -> WizardPrompt:
|
|
4702
|
+
return WizardPrompt(
|
|
4703
|
+
step=prompt.step,
|
|
4704
|
+
kind=prompt.kind,
|
|
4705
|
+
label=prompt.label,
|
|
4706
|
+
options=tuple(
|
|
4707
|
+
WizardOption(option.value, option.label, option.description)
|
|
4708
|
+
for option in prompt.options
|
|
4709
|
+
),
|
|
4710
|
+
help=prompt.help,
|
|
4711
|
+
echo_template=prompt.echo_template,
|
|
4712
|
+
multi=prompt.multi,
|
|
4713
|
+
questions=tuple(_domain_prompt(question) for question in prompt.questions),
|
|
4714
|
+
)
|
|
4715
|
+
|
|
4716
|
+
|
|
4717
|
+
def _interaction_plan(state: WizardState, prompt: Prompt) -> InteractionPlan:
|
|
4718
|
+
context = HostSessionContext(
|
|
4719
|
+
host_id=state.host_runtime,
|
|
4720
|
+
entry_mode=state.host_entry_mode,
|
|
4721
|
+
available_functions=frozenset(state.available_functions),
|
|
4722
|
+
interaction_surface="terminal",
|
|
4723
|
+
)
|
|
4724
|
+
adapter = default_host_registry().resolve(state.host_runtime)
|
|
4725
|
+
return plan_prompt(_domain_prompt(prompt), context, adapter.interaction()).interaction
|
|
4726
|
+
|
|
4727
|
+
|
|
4728
|
+
def _interaction_payload(plan: InteractionPlan) -> dict[str, Any]:
|
|
4729
|
+
return {
|
|
4730
|
+
"kind": plan.kind,
|
|
4731
|
+
"answerProtocol": {
|
|
4732
|
+
"kind": plan.answer_protocol.kind,
|
|
4733
|
+
"multi": plan.answer_protocol.multi,
|
|
4734
|
+
},
|
|
4735
|
+
}
|
|
4736
|
+
|
|
4737
|
+
|
|
4738
|
+
def _normalize_interaction_answer(
|
|
4739
|
+
state: WizardState,
|
|
4740
|
+
prompt: Prompt,
|
|
4741
|
+
plan: InteractionPlan,
|
|
4742
|
+
value: str,
|
|
4743
|
+
) -> str:
|
|
4744
|
+
try:
|
|
4745
|
+
return normalize_planned_answer(
|
|
4746
|
+
_domain_prompt(prompt),
|
|
4747
|
+
plan.answer_protocol,
|
|
4748
|
+
value,
|
|
4749
|
+
)
|
|
4750
|
+
except WizardAnswerError as exc:
|
|
4751
|
+
is_legacy_raw_answer = (
|
|
4752
|
+
not state.available_functions
|
|
4753
|
+
and "not an option" in str(exc)
|
|
4754
|
+
and not (value or "").strip().isdecimal()
|
|
4755
|
+
)
|
|
4756
|
+
if is_legacy_raw_answer:
|
|
4757
|
+
return value
|
|
4758
|
+
raise WizardError(str(exc)) from exc
|
|
4759
|
+
|
|
4760
|
+
|
|
4671
4761
|
def prompt_payload(state: WizardState, prompt: Prompt) -> dict[str, Any]:
|
|
4672
4762
|
"""Prompt JSON 에 진행 카운터를 덧붙인다. done/aborted 는 progress 를 생략."""
|
|
4673
4763
|
out = prompt.to_json()
|
|
4764
|
+
plan = _interaction_plan(state, prompt)
|
|
4765
|
+
out["interaction"] = _interaction_payload(plan)
|
|
4766
|
+
if plan.kind in {"numbered-single", "numbered-multi"}:
|
|
4767
|
+
out["presentation"] = "numbered-text"
|
|
4674
4768
|
if prompt.kind not in ("done", "aborted"):
|
|
4675
4769
|
out["progress"] = _screen_progress(state)
|
|
4676
4770
|
return out
|
|
@@ -4711,10 +4805,10 @@ def submit(state: WizardState, value: str) -> dict[str, Any]:
|
|
|
4711
4805
|
prompt = next_prompt(state)
|
|
4712
4806
|
if prompt.kind in ("done", "aborted"):
|
|
4713
4807
|
return {"echo": "", "next": prompt_payload(state, prompt)}
|
|
4808
|
+
plan = _interaction_plan(state, prompt)
|
|
4809
|
+
value = _normalize_interaction_answer(state, prompt, plan, value)
|
|
4714
4810
|
if prompt.kind == "pick_group":
|
|
4715
4811
|
return _submit_group(state, prompt, value)
|
|
4716
|
-
if prompt.kind == "pick" and len(prompt.options) > HOST_PICK_OPTION_CAP:
|
|
4717
|
-
value = _normalize_numbered_pick_answer(prompt, value)
|
|
4718
4812
|
step = STEP_BY_ID[prompt.step]
|
|
4719
4813
|
echo = step.submit(state, value or "")
|
|
4720
4814
|
if prompt.step not in state.answered:
|
|
@@ -5048,8 +5142,14 @@ def _cli(argv: list[str]) -> int:
|
|
|
5048
5142
|
p_init.add_argument(
|
|
5049
5143
|
"--host-runtime",
|
|
5050
5144
|
default="claude-code",
|
|
5051
|
-
choices=(
|
|
5145
|
+
choices=default_host_registry().ids(),
|
|
5146
|
+
)
|
|
5147
|
+
p_init.add_argument(
|
|
5148
|
+
"--entry-mode",
|
|
5149
|
+
default="current-session",
|
|
5150
|
+
choices=("spawn-process", "current-session"),
|
|
5052
5151
|
)
|
|
5152
|
+
p_init.add_argument("--available-function", action="append", default=[])
|
|
5053
5153
|
p_init.add_argument("--critic", default="")
|
|
5054
5154
|
|
|
5055
5155
|
p_step = sub.add_parser("step")
|
|
@@ -5079,6 +5179,8 @@ def _cli(argv: list[str]) -> int:
|
|
|
5079
5179
|
project_root=args.project_root,
|
|
5080
5180
|
project_id=args.project_id,
|
|
5081
5181
|
host_runtime=args.host_runtime,
|
|
5182
|
+
host_entry_mode=args.entry_mode,
|
|
5183
|
+
available_functions=args.available_function,
|
|
5082
5184
|
)
|
|
5083
5185
|
if args.critic:
|
|
5084
5186
|
state.critic = args.critic
|
|
@@ -29,8 +29,13 @@ READING_CONFIRMATION_HEADING_RE = re.compile(
|
|
|
29
29
|
r"^##[ \t]+0\.[ \t]+Reading Confirmation\b", re.MULTILINE
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
+
# The row's prefix opens it and the end of the line closes it, so the backticks
|
|
33
|
+
# the template prescribes are decoration and are accepted either way. Requiring
|
|
34
|
+
# them once cost a run: a worker wrote six correct paths bare, none of the rows
|
|
35
|
+
# parsed, and every citation it made read as unbacked — a file it demonstrably
|
|
36
|
+
# opened counted as unread over markdown punctuation.
|
|
32
37
|
_EVIDENCE_READ_RE = re.compile(
|
|
33
|
-
r"^- Evidence read:
|
|
38
|
+
r"^- Evidence read: `?(?P<path>[^`\n]+?)`?\s*$",
|
|
34
39
|
re.MULTILINE,
|
|
35
40
|
)
|
|
36
41
|
_FILE_LINE_CITATION_RE = re.compile(
|
|
@@ -106,17 +111,37 @@ def _evidence_read_ledger_failures(
|
|
|
106
111
|
except OSError as exc:
|
|
107
112
|
return [f"worker audit sidecar unreadable: {audit_path.name} ({exc})"]
|
|
108
113
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
114
|
+
read_paths = _audit_evidence_read_paths(audit_content)
|
|
115
|
+
missing_paths = sorted(_cited_file_paths(result_content) - read_paths)
|
|
112
116
|
return [
|
|
113
117
|
f"worker `{worker_role}` result `{result_name}` cites "
|
|
114
118
|
f"`{missing_path}:line` without an Evidence read row for "
|
|
115
119
|
f"`{missing_path}` in `{audit_path.name}`"
|
|
120
|
+
+ _ledger_path_hint(missing_path, read_paths)
|
|
116
121
|
for missing_path in missing_paths
|
|
117
122
|
]
|
|
118
123
|
|
|
119
124
|
|
|
125
|
+
def _ledger_path_hint(cited_path: str, read_paths: set[str]) -> str:
|
|
126
|
+
"""What to cite instead, when the ledger plainly holds the same file.
|
|
127
|
+
|
|
128
|
+
A worker that cites the full path once and the bare filename afterwards
|
|
129
|
+
reads to this check as never having opened the file, and the bare failure
|
|
130
|
+
text sends the reader looking for a missing read that is not missing. One
|
|
131
|
+
unambiguous ledger row makes the remedy a rewritten citation; two rows
|
|
132
|
+
ending the same way make it a guess, so those say nothing.
|
|
133
|
+
"""
|
|
134
|
+
if "/" in cited_path:
|
|
135
|
+
return ""
|
|
136
|
+
same_name = [path for path in read_paths if path.rsplit("/", 1)[-1] == cited_path]
|
|
137
|
+
if len(same_name) != 1:
|
|
138
|
+
return ""
|
|
139
|
+
return (
|
|
140
|
+
f" — the ledger records it as `{same_name[0]}`; cite that "
|
|
141
|
+
f"project-relative path, not the bare filename"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
120
145
|
def _result_files(run_dir: Path, task_type: str, seq: str | None, worker: str | None):
|
|
121
146
|
"""Every worker-results file in *run_dir* this check owns, in name order."""
|
|
122
147
|
for path in sorted((run_dir / "worker-results").glob("*.md")):
|
|
@@ -81,6 +81,13 @@ def resolve_prompt_plan(
|
|
|
81
81
|
raise ValueError("worker ID is required")
|
|
82
82
|
if dispatch_kind.startswith("reverify-r"):
|
|
83
83
|
return _plan("reverify")
|
|
84
|
+
# A critic pass keeps the full analysis contract — worker anchor headers, the
|
|
85
|
+
# audit sidecar, the packet boundary — but is exempt from the equality group.
|
|
86
|
+
# Its body is deliberately unlike the initial one (it asks for coverage gaps
|
|
87
|
+
# and unrequested work, not findings), so grouping it with the initial
|
|
88
|
+
# analysis prompts makes `validate_analysis_prompt_set` compare two prompts
|
|
89
|
+
# that are *supposed* to differ and fail every critic-enabled run.
|
|
90
|
+
analysis_equality_group = None if dispatch_kind == "critic" else "analysis-core"
|
|
84
91
|
if task_type == "implementation" and not executor_worker_id:
|
|
85
92
|
raise ValueError("implementation executor worker ID is required")
|
|
86
93
|
if worker_id == "report-writer":
|
|
@@ -101,7 +108,7 @@ def resolve_prompt_plan(
|
|
|
101
108
|
if task_type == "final-verification":
|
|
102
109
|
return _plan(
|
|
103
110
|
"analysis",
|
|
104
|
-
equality_group=
|
|
111
|
+
equality_group=analysis_equality_group,
|
|
105
112
|
packet_only=True,
|
|
106
113
|
required_headers=FINAL_VERIFICATION_HEADERS,
|
|
107
114
|
max_body_lines=96,
|
|
@@ -110,11 +117,11 @@ def resolve_prompt_plan(
|
|
|
110
117
|
if task_type == "improvement-discovery":
|
|
111
118
|
return _plan(
|
|
112
119
|
"analysis",
|
|
113
|
-
equality_group=
|
|
120
|
+
equality_group=analysis_equality_group,
|
|
114
121
|
packet_only=True,
|
|
115
122
|
required_headers=(GRILLING_LOG_HEADER,),
|
|
116
123
|
)
|
|
117
|
-
return _plan("analysis", equality_group=
|
|
124
|
+
return _plan("analysis", equality_group=analysis_equality_group, packet_only=True)
|
|
118
125
|
|
|
119
126
|
|
|
120
127
|
def resolve_prompt_plan_for_manifest(
|