okstra 0.179.2 → 0.183.2
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 +1 -1
- package/dist/cli-registry.mjs +30 -2
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/incremental-carry.mjs +9 -8
- package/dist/commands/execute/incremental-carry.mjs.map +1 -1
- package/dist/commands/execute/plan-verify.mjs +3 -1
- package/dist/commands/execute/plan-verify.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.d.mts +4 -2
- package/dist/commands/execute/render-bundle.mjs +46 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +11 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/inspect/model-io.d.mts +1 -0
- package/dist/commands/inspect/model-io.mjs +25 -0
- package/dist/commands/inspect/model-io.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.mjs +29 -8
- package/dist/commands/inspect/stage-map.mjs.map +1 -1
- package/dist/commands/inspect/task-list.mjs +52 -6
- package/dist/commands/inspect/task-list.mjs.map +1 -1
- package/dist/commands/inspect/user-response.mjs +14 -4
- package/dist/commands/inspect/user-response.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.d.mts +1 -0
- package/dist/commands/lifecycle/check-project.mjs +69 -50
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/contract-check.d.mts +1 -0
- package/dist/commands/lifecycle/contract-check.mjs +18 -0
- package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.mjs +154 -51
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/pr/pr.d.mts +1 -0
- package/dist/commands/pr/pr.mjs +19 -1
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/dist/commands/report/agent-activity.mjs +2 -2
- package/dist/commands/report/approval-decision.d.mts +1 -0
- package/dist/commands/report/approval-decision.mjs +21 -0
- package/dist/commands/report/approval-decision.mjs.map +1 -0
- package/dist/commands/report/design-snapshot.d.mts +1 -0
- package/dist/commands/report/design-snapshot.mjs +19 -0
- package/dist/commands/report/design-snapshot.mjs.map +1 -0
- package/dist/commands/report/translate.mjs +3 -0
- package/dist/commands/report/translate.mjs.map +1 -1
- package/dist/lib/host-registry-client.mjs +13 -9
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +21 -10
- package/docs/cli.md +36 -20
- package/docs/container.md +6 -4
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
- package/docs/for-ai/skills/okstra-code-review.md +4 -4
- package/docs/for-ai/skills/okstra-container-build.md +20 -17
- package/docs/for-ai/skills/okstra-inspect.md +20 -23
- package/docs/for-ai/skills/okstra-manager.md +19 -18
- package/docs/for-ai/skills/okstra-memory.md +2 -2
- package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
- package/docs/for-ai/skills/okstra-rollup.md +14 -13
- package/docs/for-ai/skills/okstra-run.md +7 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
- package/docs/for-ai/skills/okstra-setup.md +7 -7
- package/docs/for-ai/skills/okstra-usage.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +50 -32
- package/docs/project-structure-overview.md +43 -31
- package/docs/task-process/implementation-planning.md +2 -2
- package/docs/task-process/implementation.md +7 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/report-writer-worker.md +15 -164
- package/runtime/agents/workers/translator-worker.md +5 -13
- package/runtime/bin/okstra-error-log.py +51 -11
- package/runtime/bin/okstra-report-translate.py +210 -23
- package/runtime/prompts/host-orchestration/implementation.md +1 -1
- package/runtime/prompts/launch.template.md +9 -12
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/context-loader.md +41 -141
- package/runtime/prompts/lead/convergence.md +10 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +40 -49
- package/runtime/prompts/lead/plan-body-verification.md +217 -23
- package/runtime/prompts/lead/report-writer.md +81 -421
- package/runtime/prompts/lead/team-contract.md +9 -54
- package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/feature-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +20 -12
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/project-analysis.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +0 -1
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +8 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
- package/runtime/python/okstra_ctl/agent_activity.py +141 -38
- package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +70 -23
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +124 -3
- package/runtime/python/okstra_ctl/approval_decisions.py +328 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
- package/runtime/python/okstra_ctl/backfill.py +13 -10
- package/runtime/python/okstra_ctl/batch.py +2 -4
- package/runtime/python/okstra_ctl/build_tools.py +6 -3
- package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
- package/runtime/python/okstra_ctl/clarification_items.py +27 -13
- package/runtime/python/okstra_ctl/cmux.py +130 -52
- package/runtime/python/okstra_ctl/code_review_target.py +34 -8
- package/runtime/python/okstra_ctl/consumers.py +5 -4
- package/runtime/python/okstra_ctl/container.py +103 -8
- package/runtime/python/okstra_ctl/context_cost.py +2 -1
- package/runtime/python/okstra_ctl/contract_graph.py +497 -0
- package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
- package/runtime/python/okstra_ctl/convergence.py +338 -17
- package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
- package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
- package/runtime/python/okstra_ctl/convergence_store.py +55 -34
- package/runtime/python/okstra_ctl/design_prep.py +7 -4
- package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +97 -69
- package/runtime/python/okstra_ctl/dispatch_state.py +163 -63
- package/runtime/python/okstra_ctl/doctor.py +6 -3
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
- package/runtime/python/okstra_ctl/error_log_write.py +35 -30
- package/runtime/python/okstra_ctl/error_report.py +26 -1
- package/runtime/python/okstra_ctl/error_zip.py +27 -5
- package/runtime/python/okstra_ctl/execution_identity.py +3 -2
- package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +26 -17
- package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
- package/runtime/python/okstra_ctl/fixed_text.py +39 -0
- package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
- package/runtime/python/okstra_ctl/handoff.py +5 -4
- package/runtime/python/okstra_ctl/i18n.py +4 -2
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
- package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
- package/runtime/python/okstra_ctl/incremental_carry.py +130 -17
- package/runtime/python/okstra_ctl/incremental_scope.py +12 -4
- package/runtime/python/okstra_ctl/index.py +8 -11
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
- package/runtime/python/okstra_ctl/invocation.py +3 -6
- package/runtime/python/okstra_ctl/json_boundary.py +366 -0
- package/runtime/python/okstra_ctl/json_registry.py +10 -12
- package/runtime/python/okstra_ctl/jsonl.py +19 -2
- package/runtime/python/okstra_ctl/lead_events.py +33 -1
- package/runtime/python/okstra_ctl/listing.py +3 -3
- package/runtime/python/okstra_ctl/log_report.py +24 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -7
- package/runtime/python/okstra_ctl/manager_store.py +12 -10
- package/runtime/python/okstra_ctl/material.py +5 -1
- package/runtime/python/okstra_ctl/migrate.py +29 -25
- package/runtime/python/okstra_ctl/model_cli.py +3 -15
- package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
- package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
- package/runtime/python/okstra_ctl/path_hints.py +12 -0
- package/runtime/python/okstra_ctl/paths.py +21 -0
- package/runtime/python/okstra_ctl/plan_items.py +115 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +626 -34
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
- package/runtime/python/okstra_ctl/pr_template.py +3 -2
- package/runtime/python/okstra_ctl/project_meta.py +5 -7
- package/runtime/python/okstra_ctl/recap.py +5 -4
- package/runtime/python/okstra_ctl/reconcile.py +21 -27
- package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
- package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
- package/runtime/python/okstra_ctl/render.py +74 -16
- package/runtime/python/okstra_ctl/render_final_report.py +10 -8
- package/runtime/python/okstra_ctl/report_assembly.py +509 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -1
- package/runtime/python/okstra_ctl/report_finalize.py +67 -32
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_inputs.py +72 -0
- package/runtime/python/okstra_ctl/report_language.py +3 -2
- package/runtime/python/okstra_ctl/report_markdown.py +81 -8
- package/runtime/python/okstra_ctl/report_narrative.py +351 -0
- package/runtime/python/okstra_ctl/report_projections.py +265 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
- package/runtime/python/okstra_ctl/report_views.py +3 -2
- package/runtime/python/okstra_ctl/rollup.py +65 -4
- package/runtime/python/okstra_ctl/run.py +184 -65
- package/runtime/python/okstra_ctl/run_audit.py +3 -2
- package/runtime/python/okstra_ctl/run_context.py +6 -9
- package/runtime/python/okstra_ctl/run_index_row.py +2 -8
- package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +15 -8
- package/runtime/python/okstra_ctl/session_transcript.py +27 -1
- package/runtime/python/okstra_ctl/set_work_status.py +64 -38
- package/runtime/python/okstra_ctl/stage_fix_carry.py +8 -6
- package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
- package/runtime/python/okstra_ctl/stage_map.py +96 -28
- package/runtime/python/okstra_ctl/stage_targets.py +3 -4
- package/runtime/python/okstra_ctl/team.py +3 -2
- package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
- package/runtime/python/okstra_ctl/time_report.py +51 -4
- package/runtime/python/okstra_ctl/usage_identity.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +58 -4
- package/runtime/python/okstra_ctl/user_response.py +1431 -66
- package/runtime/python/okstra_ctl/wizard.py +11 -44
- package/runtime/python/okstra_ctl/work_categories.py +3 -2
- package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +49 -3
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
- package/runtime/python/okstra_ctl/worker_runner.py +14 -12
- package/runtime/python/okstra_ctl/workflow.py +2 -1
- package/runtime/python/okstra_ctl/worktree.py +3 -2
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
- package/runtime/python/okstra_ctl/write_policy.py +4 -2
- package/runtime/python/okstra_token_usage/antigravity.py +39 -12
- package/runtime/python/okstra_token_usage/collect.py +90 -38
- package/runtime/python/okstra_token_usage/grok.py +127 -0
- package/runtime/schemas/final-report-v2.0.schema.json +9 -0
- package/runtime/schemas/final-report-v3.0.schema.json +10219 -0
- package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
- package/runtime/skills/okstra-code-review/SKILL.md +21 -11
- package/runtime/skills/okstra-container-build/SKILL.md +18 -18
- package/runtime/skills/okstra-inspect/SKILL.md +12 -11
- package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
- package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
- package/runtime/skills/okstra-inspect/facets/history.md +9 -14
- package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
- package/runtime/skills/okstra-inspect/facets/report.md +6 -10
- package/runtime/skills/okstra-inspect/facets/status.md +9 -8
- package/runtime/skills/okstra-inspect/facets/time.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +16 -14
- package/runtime/skills/okstra-memory/SKILL.md +3 -3
- package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
- package/runtime/skills/okstra-rollup/SKILL.md +6 -16
- package/runtime/skills/okstra-run/SKILL.md +7 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
- package/runtime/skills/okstra-setup/SKILL.md +21 -13
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/skills/okstra-usage/SKILL.md +10 -10
- package/runtime/skills/okstra-user-response/SKILL.md +78 -107
- package/runtime/templates/report-writer-prompt-preamble.md +29 -19
- package/runtime/templates/reports/html/macros/forms.html +6 -4
- package/runtime/templates/reports/schedule.template.md +4 -4
- package/runtime/templates/worker-error-contract.md +17 -29
- package/runtime/validators/validate-run.py +636 -72
- package/runtime/validators/validate_session_conformance.py +43 -7
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""사실 주장의 재현.
|
|
2
|
+
|
|
3
|
+
계획 검증의 1표 차단(`kind a`, `P-Req` 의 `kind f`)에는 근거가 있다 — 명시된 두
|
|
4
|
+
인용이 서로 모순이라는 것은 한 명이 실측으로 확정할 수 있는 사실이고, 사실을
|
|
5
|
+
다수결로 기각하면 안 된다. 문제는 1표라는 것이 아니라 **1표에 재현 요구가 없다**는
|
|
6
|
+
것이었다. 검증자가 "이 경로는 존재하지 않는다" 라고 쓰면 그대로 차단이 됐다.
|
|
7
|
+
|
|
8
|
+
여기서 재현은 워커가 아니라 이 코드가 실행한다. 워커가 결과까지 신고하면 판정이
|
|
9
|
+
자기신고 위에 서고, 같은 주장이 워커마다 다르게 결론난다.
|
|
10
|
+
|
|
11
|
+
대가는 표현력이다. 여기서 돌릴 수 있는 형태로만 사실 주장을 쓸 수 있고, 그 밖의
|
|
12
|
+
것은 `judgement` 로 내어 정족수로 판정한다. 강등이 아니라 분류다 — 기계가 확인할
|
|
13
|
+
수 없는 것에 1표 차단을 주는 것이 애초에 틀렸다.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Mapping
|
|
19
|
+
|
|
20
|
+
REPRODUCED = "reproduced"
|
|
21
|
+
NOT_REPRODUCED = "not-reproduced"
|
|
22
|
+
NOT_RUNNABLE = "not-runnable"
|
|
23
|
+
|
|
24
|
+
PROBE_KINDS = (
|
|
25
|
+
"path-exists",
|
|
26
|
+
"path-absent",
|
|
27
|
+
"literal-present",
|
|
28
|
+
"literal-absent",
|
|
29
|
+
"citations-differ",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_MAX_READ_BYTES = 4_000_000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _within(project_root: Path, value: object) -> Path | None:
|
|
36
|
+
"""프로젝트 안의 경로. 밖으로 나가거나 모양이 아니면 ``None``.
|
|
37
|
+
|
|
38
|
+
재현은 검증자가 쓴 문자열을 그대로 받는다. 담아 두지 않으면 그 문자열 하나로
|
|
39
|
+
프로젝트 밖을 읽게 된다.
|
|
40
|
+
"""
|
|
41
|
+
if not isinstance(value, str) or not value.strip():
|
|
42
|
+
return None
|
|
43
|
+
candidate = Path(value)
|
|
44
|
+
resolved = (
|
|
45
|
+
candidate if candidate.is_absolute() else project_root / candidate
|
|
46
|
+
)
|
|
47
|
+
try:
|
|
48
|
+
resolved = resolved.resolve()
|
|
49
|
+
root = project_root.resolve()
|
|
50
|
+
except (OSError, RuntimeError):
|
|
51
|
+
return None
|
|
52
|
+
return resolved if resolved == root or root in resolved.parents else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _read(path: Path) -> str | None:
|
|
56
|
+
try:
|
|
57
|
+
if not path.is_file() or path.stat().st_size > _MAX_READ_BYTES:
|
|
58
|
+
return None
|
|
59
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
60
|
+
except (OSError, ValueError):
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def reproduce(probe: object, *, project_root: Path) -> str:
|
|
65
|
+
"""이 프로브를 실행한 결과.
|
|
66
|
+
|
|
67
|
+
``reproduced`` 는 주장이 성립한다는 뜻이고, ``not-reproduced`` 는 근거가
|
|
68
|
+
성립하지 않는다는 뜻이다. ``not-runnable`` 은 판정하지 않았다는 뜻이며 —
|
|
69
|
+
프로브의 모양이 아니거나, 경로가 프로젝트 밖이거나, 파일을 못 읽은 경우다 —
|
|
70
|
+
소비처는 이것을 `judgement` 와 같게 다뤄야 한다. 못 돌린 것을 재현 실패로
|
|
71
|
+
읽으면 도구의 한계가 주장의 기각이 된다.
|
|
72
|
+
"""
|
|
73
|
+
if not isinstance(probe, Mapping):
|
|
74
|
+
return NOT_RUNNABLE
|
|
75
|
+
kind = str(probe.get("kind") or "")
|
|
76
|
+
if kind not in PROBE_KINDS:
|
|
77
|
+
return NOT_RUNNABLE
|
|
78
|
+
|
|
79
|
+
if kind in ("path-exists", "path-absent"):
|
|
80
|
+
path = _within(project_root, probe.get("path"))
|
|
81
|
+
if path is None:
|
|
82
|
+
return NOT_RUNNABLE
|
|
83
|
+
exists = path.exists()
|
|
84
|
+
return REPRODUCED if exists == (kind == "path-exists") else NOT_REPRODUCED
|
|
85
|
+
|
|
86
|
+
if kind in ("literal-present", "literal-absent"):
|
|
87
|
+
path = _within(project_root, probe.get("path"))
|
|
88
|
+
literal = probe.get("literal")
|
|
89
|
+
if path is None or not isinstance(literal, str) or not literal:
|
|
90
|
+
return NOT_RUNNABLE
|
|
91
|
+
text = _read(path)
|
|
92
|
+
if text is None:
|
|
93
|
+
return NOT_RUNNABLE
|
|
94
|
+
present = literal in text
|
|
95
|
+
return REPRODUCED if present == (kind == "literal-present") else NOT_REPRODUCED
|
|
96
|
+
|
|
97
|
+
left, right = probe.get("left"), probe.get("right")
|
|
98
|
+
if not isinstance(left, str) or not isinstance(right, str) or not left or not right:
|
|
99
|
+
return NOT_RUNNABLE
|
|
100
|
+
# 주장은 "이 둘이 서로 모순이다" 이므로, 다르면 재현된 것이다.
|
|
101
|
+
return REPRODUCED if left.strip() != right.strip() else NOT_REPRODUCED
|
|
@@ -37,6 +37,7 @@ from pathlib import Path
|
|
|
37
37
|
from typing import Optional
|
|
38
38
|
|
|
39
39
|
from okstra_ctl.final_report_paths import final_report_data_path
|
|
40
|
+
from okstra_ctl.json_boundary import load_owned_object
|
|
40
41
|
from okstra_ctl.md_table import is_separator_row, split_pipe_row, to_cell_text
|
|
41
42
|
|
|
42
43
|
|
|
@@ -224,28 +225,41 @@ def parse_clarification_items(report_text: str) -> Optional[list[ClarificationIt
|
|
|
224
225
|
# 워크는 거기서 아무것도 못 찾으므로, 게이트가 v1 파서만 쓰면 열린 항목이 있는
|
|
225
226
|
# v2 리포트를 "읽을 수 없음"으로 떨어뜨린다. data.json 은 두 렌더러가 이미 읽는
|
|
226
227
|
# SSOT 다 — clarification 상태도 여기서 읽는다.
|
|
227
|
-
|
|
228
|
+
# 리포트 정본의 구조화 판본. 2.0 과 3.0 은 `clarificationItems[]` 를 같은 자리에
|
|
229
|
+
# 같은 모양으로 들고 있으므로 이 좁히기에는 차이가 없다. ADR-0019 가 2.0 을 계속
|
|
230
|
+
# 판독하라고 정했으므로 둘 다 받는다 — `stage_map.structured_report` 와 같은 규칙.
|
|
231
|
+
STRUCTURED_REPORT_VERSIONS = frozenset({"2.0", "3.0"})
|
|
228
232
|
|
|
229
233
|
|
|
230
234
|
def _read_report_text(report_path: Path) -> str:
|
|
231
235
|
return report_path.read_text(encoding="utf-8", errors="replace")
|
|
232
236
|
|
|
233
237
|
|
|
234
|
-
def
|
|
235
|
-
"""``report_path`` 의
|
|
238
|
+
def _structured_report_data(report_path: Path) -> Optional[dict]:
|
|
239
|
+
"""``report_path`` 의 구조화 리포트 레코드, 아니면 ``None``.
|
|
236
240
|
|
|
237
|
-
파일이 없거나 JSON 이 깨졌거나
|
|
238
|
-
마크다운 경로로 폴백하고, 진짜
|
|
239
|
-
"§1 없음" fail-closed 로 잡는다.
|
|
241
|
+
파일이 없거나 JSON 이 깨졌거나 구조화 판본이 아니면 ``None`` — 호출자는 v1
|
|
242
|
+
마크다운 경로로 폴백하고, 진짜 구조화 리포트인데 레코드가 깨진 경우는 그
|
|
243
|
+
폴백이 "§1 없음" fail-closed 로 잡는다.
|
|
244
|
+
|
|
245
|
+
판본을 `2.0` 하나로 못박아 두던 동안 3.0 리포트가 여기서 ``None`` 이 됐고,
|
|
246
|
+
그러면 호출자가 JSON 본문에서 `## 1.` 헤딩을 찾다 실패해 **리포트 전문을
|
|
247
|
+
그대로 복사**했다 — 이 좁히기가 막으려던 바로 그 중복이다. 실측: 66K 짜리
|
|
248
|
+
추출본이 나와야 할 자리에 693K data.json 이 스테이징됐고, 패킷의
|
|
249
|
+
`## Clarification Carry-In Extract` 가 0행이 되어 워커가 사용자 답변을
|
|
250
|
+
전혀 받지 못했다.
|
|
240
251
|
"""
|
|
241
252
|
data_path = final_report_data_path(report_path)
|
|
242
253
|
if not data_path.is_file():
|
|
243
254
|
return None
|
|
244
255
|
try:
|
|
245
|
-
data =
|
|
256
|
+
data = load_owned_object(data_path, artifact="final report record")
|
|
246
257
|
except (OSError, ValueError):
|
|
247
258
|
return None
|
|
248
|
-
if
|
|
259
|
+
if (
|
|
260
|
+
not isinstance(data, dict)
|
|
261
|
+
or data.get("schemaVersion") not in STRUCTURED_REPORT_VERSIONS
|
|
262
|
+
):
|
|
249
263
|
return None
|
|
250
264
|
return data
|
|
251
265
|
|
|
@@ -287,7 +301,7 @@ def _v2_row(entry: dict) -> Optional[dict]:
|
|
|
287
301
|
def _v2_clarification_rows(report_path: Path) -> Optional[list[dict]]:
|
|
288
302
|
"""schema-v2 clarification 행들, 이 리포트가 v2 가 아니면 ``None``.
|
|
289
303
|
필수 필드가 빠진 행은 건너뛰는 lenient 계약(§1 파서와 동일)."""
|
|
290
|
-
data =
|
|
304
|
+
data = _structured_report_data(report_path)
|
|
291
305
|
if data is None:
|
|
292
306
|
return None
|
|
293
307
|
entries = data.get("clarificationItems")
|
|
@@ -503,7 +517,7 @@ def _scan_v2_blockers(
|
|
|
503
517
|
) -> Optional[ClarificationScan]:
|
|
504
518
|
"""schema-v2 data.json 기준 스캔, 이 리포트가 v2 가 아니면 ``None``.
|
|
505
519
|
필수 필드가 빠진 행은 §1 의 unparsed row 와 같이 fail-closed 로 다룬다."""
|
|
506
|
-
data =
|
|
520
|
+
data = _structured_report_data(report_path)
|
|
507
521
|
if data is None:
|
|
508
522
|
return None
|
|
509
523
|
entries = data.get("clarificationItems")
|
|
@@ -758,10 +772,10 @@ def _carry_section_1(source: Path, text: str) -> Optional[tuple[str, str]]:
|
|
|
758
772
|
복사**됐다 — 이 좁히기가 막으려던 바로 그 중복이다. carry-in 은 파생 문서이고
|
|
759
773
|
다운스트림(승인 게이트·프롬프트 빌더·검증 워커)이 §1 표 하나만 읽으므로, v2
|
|
760
774
|
행도 같은 표로 렌더한다."""
|
|
761
|
-
data =
|
|
775
|
+
data = _structured_report_data(source)
|
|
762
776
|
if data is not None:
|
|
763
777
|
entries = data.get("clarificationItems")
|
|
764
|
-
return SECTION_1_HEADING,
|
|
778
|
+
return SECTION_1_HEADING, _structured_section_1_body(
|
|
765
779
|
entries if isinstance(entries, list) else []
|
|
766
780
|
)
|
|
767
781
|
slice_ = _section_1_slice(text)
|
|
@@ -772,7 +786,7 @@ def _carry_section_1(source: Path, text: str) -> Optional[tuple[str, str]]:
|
|
|
772
786
|
return heading.group(0), slice_.rstrip()
|
|
773
787
|
|
|
774
788
|
|
|
775
|
-
def
|
|
789
|
+
def _structured_section_1_body(entries: list) -> str:
|
|
776
790
|
"""schema-v2 `clarificationItems[]` 를 §1 표 본문으로.
|
|
777
791
|
|
|
778
792
|
메타 셀은 렌더러가 쓰는 모양 그대로다 — `Status:` 는 따옴표 없이 써야
|
|
@@ -32,9 +32,9 @@ SHIM_DIR_MARKER = "cmux-cli-shims"
|
|
|
32
32
|
|
|
33
33
|
# The workspace is split in fifths: two for the lead, three for the workers.
|
|
34
34
|
# Every worker lands in that one column and the column divides downward, so all
|
|
35
|
-
# workers hold the same width
|
|
36
|
-
#
|
|
37
|
-
#
|
|
35
|
+
# workers hold the same width. Splitting sideways instead would make each
|
|
36
|
+
# worker's width a function of how many rounds preceded it. The split itself
|
|
37
|
+
# halves the pane it lands on; worker heights are evened out afterwards.
|
|
38
38
|
LEAD_SHARE_WITH_WORKERS = 2 / 5
|
|
39
39
|
|
|
40
40
|
# Three fifths is what a worker needs to be worth watching: measured against a
|
|
@@ -46,10 +46,9 @@ LEAD_SHARE_WITH_WORKERS = 2 / 5
|
|
|
46
46
|
# whole workspace back rather than sitting at its working width.
|
|
47
47
|
LEAD_SHARE_ALONE = 1.0
|
|
48
48
|
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
WORKER_MIN_ROWS = 20
|
|
49
|
+
# Two passes per pane above the bottom. A roster of four analysers is four
|
|
50
|
+
# workers in the column; eight moves is that bound with one extra pass.
|
|
51
|
+
MAX_WORKER_HEIGHT_MOVES = 8
|
|
53
52
|
|
|
54
53
|
# Sidebar entries are keyed by source so tools do not overwrite each other's.
|
|
55
54
|
SIDEBAR_SOURCE = "okstra"
|
|
@@ -163,15 +162,29 @@ class PaneGeometry:
|
|
|
163
162
|
width_points: float = 0.0
|
|
164
163
|
ref: str = ""
|
|
165
164
|
selected_surface_id: str = ""
|
|
165
|
+
height_points: float = 0.0
|
|
166
166
|
|
|
167
167
|
|
|
168
168
|
@dataclass(frozen=True)
|
|
169
169
|
class Placement:
|
|
170
|
-
"""Where the next worker goes: split `pane_id
|
|
170
|
+
"""Where the next worker goes: split `pane_id` in `direction`."""
|
|
171
171
|
|
|
172
172
|
pane_id: str
|
|
173
173
|
direction: str
|
|
174
|
-
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass(frozen=True)
|
|
177
|
+
class WorkerHeightResize:
|
|
178
|
+
"""One vertical border move that equalizes the worker column.
|
|
179
|
+
|
|
180
|
+
`down` on a pane grows it into the neighbour below. `up` on a pane pulls
|
|
181
|
+
the shared border up, shrinking the neighbour above — the top pane has no
|
|
182
|
+
upper border, so shrinking it is the pane below's request.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
pane_id: str
|
|
186
|
+
direction: str
|
|
187
|
+
amount: int
|
|
175
188
|
|
|
176
189
|
|
|
177
190
|
def plan_worker_placement(
|
|
@@ -179,15 +192,14 @@ def plan_worker_placement(
|
|
|
179
192
|
*,
|
|
180
193
|
lead_pane_id: str,
|
|
181
194
|
owned_surface_ids: Collection[str],
|
|
182
|
-
min_rows: int,
|
|
183
195
|
) -> Placement:
|
|
184
196
|
"""Pick the next worker slot from the workspace's current geometry.
|
|
185
197
|
|
|
186
198
|
One column for every worker: the first split takes it off the lead, and each
|
|
187
|
-
one after that divides the
|
|
188
|
-
by where the lead's border sits, and every worker
|
|
189
|
-
computes a width, and no worker's width depends
|
|
190
|
-
before it.
|
|
199
|
+
one after that divides the worker next to the lead downward. Width is
|
|
200
|
+
therefore decided once, by where the lead's border sits, and every worker
|
|
201
|
+
inherits it — nothing here computes a width, and no worker's width depends
|
|
202
|
+
on how many rounds ran before it.
|
|
191
203
|
|
|
192
204
|
Stateless by design: okstra records surface UUIDs, never a layout, so a
|
|
193
205
|
resumed or crashed run cannot carry a layout model that no longer matches
|
|
@@ -207,8 +219,8 @@ def plan_worker_placement(
|
|
|
207
219
|
and _holds_an_okstra_surface(pane, owned_surface_ids)
|
|
208
220
|
]
|
|
209
221
|
if not workers:
|
|
210
|
-
return Placement(pane_id=lead_pane_id, direction="right"
|
|
211
|
-
return _extend_the_worker_column(workers
|
|
222
|
+
return Placement(pane_id=lead_pane_id, direction="right")
|
|
223
|
+
return _extend_the_worker_column(workers)
|
|
212
224
|
|
|
213
225
|
|
|
214
226
|
def lead_target_width(
|
|
@@ -273,28 +285,52 @@ def _holds_an_okstra_surface(
|
|
|
273
285
|
) -> bool:
|
|
274
286
|
"""Whether okstra opened anything in this pane.
|
|
275
287
|
|
|
276
|
-
Any one recorded surface is enough:
|
|
277
|
-
|
|
288
|
+
Any one recorded surface is enough: a pane may hold more than one surface,
|
|
289
|
+
and only one of those UUIDs has to be in the ledger.
|
|
278
290
|
"""
|
|
279
291
|
return any(surface_id in owned_surface_ids for surface_id in pane.surface_ids)
|
|
280
292
|
|
|
281
293
|
|
|
282
|
-
def _extend_the_worker_column(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
294
|
+
def _extend_the_worker_column(workers: Sequence[PaneGeometry]) -> Placement:
|
|
295
|
+
"""Divide the worker next to the lead downward. A tab would hide the new worker.
|
|
296
|
+
|
|
297
|
+
The first worker is split off the lead, so it stays at the lead's y. Later
|
|
298
|
+
workers stack below it. Splitting the bottom one parks each new pane at the
|
|
299
|
+
far end of the column; splitting the neighbour keeps the new pane next to
|
|
300
|
+
the lead.
|
|
301
|
+
"""
|
|
302
|
+
neighbour = min(workers, key=lambda pane: pane.y)
|
|
303
|
+
return Placement(pane_id=neighbour.pane_id, direction="down")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def plan_worker_height_resize(
|
|
307
|
+
workers: Sequence[PaneGeometry],
|
|
308
|
+
) -> WorkerHeightResize | None:
|
|
309
|
+
"""The next vertical move that gives every worker an equal share of the column.
|
|
310
|
+
|
|
311
|
+
Only the first off-share pane above the bottom is reported. Growing or
|
|
312
|
+
shrinking it changes every pane below, so the caller re-reads and asks
|
|
313
|
+
again. The bottom pane is never the one named: it absorbs the remainder.
|
|
286
314
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
worse than a visible pane, but better than two panes too short to show a
|
|
291
|
-
command and its first lines of output.
|
|
315
|
+
`new-split` halves the pane it lands on and does not take a ratio, so a
|
|
316
|
+
third worker split off the top would otherwise stay at a quarter while
|
|
317
|
+
the bottom one keeps a half.
|
|
292
318
|
"""
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
319
|
+
if len(workers) < 2:
|
|
320
|
+
return None
|
|
321
|
+
ordered = sorted(workers, key=lambda pane: pane.y)
|
|
322
|
+
total = sum(pane.height_points for pane in ordered)
|
|
323
|
+
if total <= 0:
|
|
324
|
+
return None
|
|
325
|
+
share = total / len(ordered)
|
|
326
|
+
for index, pane in enumerate(ordered[:-1]):
|
|
327
|
+
delta = round(pane.height_points - share)
|
|
328
|
+
if delta == 0:
|
|
329
|
+
continue
|
|
330
|
+
if delta < 0:
|
|
331
|
+
return WorkerHeightResize(pane.pane_id, "down", -delta)
|
|
332
|
+
return WorkerHeightResize(ordered[index + 1].pane_id, "up", delta)
|
|
333
|
+
return None
|
|
298
334
|
|
|
299
335
|
|
|
300
336
|
def shim_free_login_path() -> str:
|
|
@@ -331,6 +367,17 @@ def worker_command_line(
|
|
|
331
367
|
The `cd` is part of the command because neither `new-split` nor
|
|
332
368
|
`respawn-pane` accepts a working directory the way `tmux split-window -c`
|
|
333
369
|
does.
|
|
370
|
+
|
|
371
|
+
`exec` is here for termination, not for the pane's life, and removing it
|
|
372
|
+
does not keep a finished worker's pane on screen. Measured 2026-08-21: a
|
|
373
|
+
surface's life is bound to the command `respawn-pane` starts — cmux runs it
|
|
374
|
+
through `login … bash --noprofile --norc` and closes the surface when it
|
|
375
|
+
exits — and cmux's settings schema carries no option that keeps a surface
|
|
376
|
+
open past its process. Dropping `exec` was tried against a live dispatch and
|
|
377
|
+
the pane still went with the worker; what it changed was that
|
|
378
|
+
`close_surface` would kill an intermediate shell rather than the worker
|
|
379
|
+
itself. So the adapter contract's "finished workers leave their panes
|
|
380
|
+
behind" does not hold under cmux, and no edit to this line makes it hold.
|
|
334
381
|
"""
|
|
335
382
|
return (
|
|
336
383
|
f"cd {shlex.quote(str(cwd))} && "
|
|
@@ -366,13 +413,14 @@ def spawn_worker_surface(
|
|
|
366
413
|
panes,
|
|
367
414
|
lead_pane_id=lead.pane_id,
|
|
368
415
|
owned_surface_ids=owned_surface_ids,
|
|
369
|
-
min_rows=WORKER_MIN_ROWS,
|
|
370
416
|
)
|
|
371
417
|
target = _pane_by_id(panes, placement.pane_id)
|
|
372
418
|
surface_uuid = _open_worker_surface(workspace, placement, target)
|
|
373
419
|
run_cmux(["rename-tab", "--surface", surface_uuid, "--title", title])
|
|
374
420
|
_exec_worker(surface_uuid, cwd=cwd, command=command)
|
|
375
|
-
|
|
421
|
+
owned = (*owned_surface_ids, surface_uuid)
|
|
422
|
+
_size_lead_pane(workspace, owned)
|
|
423
|
+
_equalize_worker_heights(workspace, owned)
|
|
376
424
|
return surface_uuid
|
|
377
425
|
|
|
378
426
|
|
|
@@ -575,21 +623,16 @@ def open_surface_ids(workspace: str) -> set[str]:
|
|
|
575
623
|
def _create_surface(
|
|
576
624
|
workspace: str, placement: Placement, target: PaneGeometry
|
|
577
625
|
) -> None:
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
workspace,
|
|
589
|
-
"--surface",
|
|
590
|
-
target.selected_surface_id or target.surface_ids[0],
|
|
591
|
-
]
|
|
592
|
-
)
|
|
626
|
+
created = run_cmux(
|
|
627
|
+
[
|
|
628
|
+
"new-split",
|
|
629
|
+
placement.direction,
|
|
630
|
+
"--workspace",
|
|
631
|
+
workspace,
|
|
632
|
+
"--surface",
|
|
633
|
+
target.selected_surface_id or target.surface_ids[0],
|
|
634
|
+
]
|
|
635
|
+
)
|
|
593
636
|
if created.returncode != 0:
|
|
594
637
|
raise RuntimeError(created.stderr.strip() or "cmux could not open a pane")
|
|
595
638
|
|
|
@@ -606,9 +649,10 @@ def _exec_worker(surface_uuid: str, *, cwd: Path, command: Sequence[str]) -> Non
|
|
|
606
649
|
def _size_lead_pane(workspace: str, owned_surface_ids: Collection[str]) -> None:
|
|
607
650
|
"""Move the lead's border to its share of the workspace.
|
|
608
651
|
|
|
609
|
-
This is the
|
|
610
|
-
and
|
|
611
|
-
|
|
652
|
+
This is the horizontal border okstra places. Workers divide their column
|
|
653
|
+
downward and inherit the remaining width, so this one move sizes the lead
|
|
654
|
+
and every worker's width. Worker heights are equalized separately after
|
|
655
|
+
the split that just halved one of them.
|
|
612
656
|
|
|
613
657
|
Which pane carries the request follows from what `pane.resize` does: it
|
|
614
658
|
moves the named pane's own border in the direction given. The lead can push
|
|
@@ -659,6 +703,39 @@ def _size_lead_pane(workspace: str, owned_surface_ids: Collection[str]) -> None:
|
|
|
659
703
|
)
|
|
660
704
|
|
|
661
705
|
|
|
706
|
+
def _equalize_worker_heights(
|
|
707
|
+
workspace: str, owned_surface_ids: Collection[str]
|
|
708
|
+
) -> None:
|
|
709
|
+
"""Give every owned worker the same height in the column.
|
|
710
|
+
|
|
711
|
+
`new-split` halves whichever pane it lands on, so the third worker of a
|
|
712
|
+
round would otherwise sit at a quarter with the bottom one still at a half.
|
|
713
|
+
Each call moves one border and re-reads: growing the top pane steals from
|
|
714
|
+
the one below, and that is what the next plan has to see.
|
|
715
|
+
"""
|
|
716
|
+
for _ in range(MAX_WORKER_HEIGHT_MOVES):
|
|
717
|
+
panes = list_panes(workspace)
|
|
718
|
+
lead = _lead_pane(panes)
|
|
719
|
+
workers = [
|
|
720
|
+
pane
|
|
721
|
+
for pane in panes
|
|
722
|
+
if pane.pane_id != lead.pane_id
|
|
723
|
+
and _holds_an_okstra_surface(pane, owned_surface_ids)
|
|
724
|
+
]
|
|
725
|
+
move = plan_worker_height_resize(workers)
|
|
726
|
+
if move is None:
|
|
727
|
+
return
|
|
728
|
+
rpc(
|
|
729
|
+
"pane.resize",
|
|
730
|
+
{
|
|
731
|
+
"workspace_id": workspace,
|
|
732
|
+
"pane_id": move.pane_id,
|
|
733
|
+
"direction": move.direction,
|
|
734
|
+
"amount": move.amount,
|
|
735
|
+
},
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
|
|
662
739
|
def _lead_pane(panes: Sequence[PaneGeometry]) -> PaneGeometry:
|
|
663
740
|
caller_pane_ref = identify_caller().get("pane_ref", "")
|
|
664
741
|
for pane in panes:
|
|
@@ -706,6 +783,7 @@ def _pane_geometry(entry: dict[str, Any]) -> PaneGeometry:
|
|
|
706
783
|
width_points=float(frame.get("width") or 0),
|
|
707
784
|
ref=str(entry.get("ref", "")),
|
|
708
785
|
selected_surface_id=str(entry.get("selected_surface_id", "")),
|
|
786
|
+
height_points=float(frame.get("height") or 0),
|
|
709
787
|
)
|
|
710
788
|
|
|
711
789
|
|
|
@@ -24,6 +24,7 @@ from okstra_project import (
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
from .code_review_paths import branch_review_dir, next_branch_review
|
|
27
|
+
from .fixed_text import line
|
|
27
28
|
|
|
28
29
|
DEFAULT_BRANCH_CANDIDATES = ("main", "master")
|
|
29
30
|
|
|
@@ -105,7 +106,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
105
106
|
parser.add_argument("--date", default="", help="YYYY-MM-DD for branch result naming")
|
|
106
107
|
parser.add_argument("--project-root", default="", help="explicit project root")
|
|
107
108
|
parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
|
|
108
|
-
|
|
109
|
+
output = parser.add_mutually_exclusive_group()
|
|
110
|
+
output.add_argument("--json", action="store_true", help="emit JSON (default)")
|
|
111
|
+
output.add_argument("--text", action="store_true", help="emit fixed text fields")
|
|
109
112
|
return parser
|
|
110
113
|
|
|
111
114
|
|
|
@@ -121,6 +124,25 @@ def _resolve(args: argparse.Namespace, project_root: Path) -> dict:
|
|
|
121
124
|
raise StateError("pass either --task-key --stage or --branch", stage="args")
|
|
122
125
|
|
|
123
126
|
|
|
127
|
+
def render_code_review_target_text(payload: dict) -> str:
|
|
128
|
+
"""모델이 소비할 리뷰 범위와 결과 경로만 고정 줄로 투영한다."""
|
|
129
|
+
rows = ["Okstra code review target\n"]
|
|
130
|
+
rows.append(line("Status", "ready" if payload.get("ok") else "error"))
|
|
131
|
+
fields = (
|
|
132
|
+
("Project root", "projectRoot"), ("Mode", "mode"),
|
|
133
|
+
("Worktree path", "worktreePath"), ("Branch", "branch"),
|
|
134
|
+
("Base commit", "baseCommit"), ("Head commit", "headCommit"),
|
|
135
|
+
("Review path", "reviewPath"), ("Round", "round"),
|
|
136
|
+
("Task key", "taskKey"), ("Task root", "taskRoot"), ("Stage", "stage"),
|
|
137
|
+
("Failure stage", "stage"), ("Failure reason", "reason"),
|
|
138
|
+
)
|
|
139
|
+
for label, key in fields:
|
|
140
|
+
failure_field = label.startswith("Failure")
|
|
141
|
+
if key in payload and failure_field != bool(payload.get("ok")):
|
|
142
|
+
rows.append(line(label, payload.get(key)))
|
|
143
|
+
return "".join(rows)
|
|
144
|
+
|
|
145
|
+
|
|
124
146
|
def main(argv: list[str] | None = None) -> int:
|
|
125
147
|
args = _build_parser().parse_args(argv)
|
|
126
148
|
try:
|
|
@@ -128,23 +150,27 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
128
150
|
explicit_root=args.project_root, cwd=args.cwd
|
|
129
151
|
)
|
|
130
152
|
except ResolverError as exc:
|
|
131
|
-
|
|
153
|
+
payload = {"ok": False, "stage": "resolve", "reason": str(exc)}
|
|
154
|
+
print(render_code_review_target_text(payload) if args.text else json.dumps(payload))
|
|
132
155
|
return 2
|
|
133
156
|
|
|
134
157
|
try:
|
|
135
158
|
snapshot = _resolve(args, Path(project_root))
|
|
136
159
|
except StateError as exc:
|
|
137
|
-
|
|
160
|
+
payload = {
|
|
138
161
|
"ok": False,
|
|
139
162
|
"stage": exc.stage or "target",
|
|
140
163
|
"reason": str(exc),
|
|
141
|
-
}
|
|
164
|
+
}
|
|
165
|
+
print(render_code_review_target_text(payload) if args.text else json.dumps(payload))
|
|
142
166
|
return 1
|
|
143
167
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
168
|
+
payload = {"ok": True, "projectRoot": str(project_root), **snapshot}
|
|
169
|
+
print(
|
|
170
|
+
render_code_review_target_text(payload)
|
|
171
|
+
if args.text
|
|
172
|
+
else json.dumps(payload, ensure_ascii=False, indent=2)
|
|
173
|
+
)
|
|
148
174
|
return 0
|
|
149
175
|
|
|
150
176
|
|
|
@@ -16,6 +16,8 @@ from dataclasses import dataclass
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Any, Dict, List, Optional
|
|
18
18
|
|
|
19
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
20
|
+
from .jsonl import append_jsonl
|
|
19
21
|
from .run_context import consumers_mutex
|
|
20
22
|
|
|
21
23
|
CONSUMERS_FILENAME = "consumers.jsonl"
|
|
@@ -265,8 +267,7 @@ def _release_stage_occupancy_keeping_branch(
|
|
|
265
267
|
|
|
266
268
|
|
|
267
269
|
def _append_row(plan_run_root: Path, record: Dict[str, Any]) -> None:
|
|
268
|
-
|
|
269
|
-
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
270
|
+
append_jsonl(_path(plan_run_root), record, ensure_ascii=False, compact=False)
|
|
270
271
|
|
|
271
272
|
|
|
272
273
|
def append_verified(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
@@ -414,8 +415,8 @@ def backfill_done_from_carry(plan_run_root: Path) -> int:
|
|
|
414
415
|
recovered = 0
|
|
415
416
|
for cf in sorted(carry_dir.glob("stage-*.json")):
|
|
416
417
|
try:
|
|
417
|
-
carry =
|
|
418
|
-
except (
|
|
418
|
+
carry = load_owned_object(cf, artifact="implementation stage carry")
|
|
419
|
+
except (JsonBoundaryError, OSError):
|
|
419
420
|
continue
|
|
420
421
|
if not isinstance(carry, dict):
|
|
421
422
|
continue
|