okstra 0.180.0 → 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/dist/cli-registry.mjs +16 -2
- package/dist/cli-registry.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/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.md +11 -0
- package/docs/cli.md +26 -13
- 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 +29 -26
- 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 +3 -3
- 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 +3 -7
- package/runtime/prompts/lead/context-loader.md +41 -141
- package/runtime/prompts/lead/convergence.md +8 -6
- package/runtime/prompts/lead/okstra-lead-contract.md +25 -35
- package/runtime/prompts/lead/plan-body-verification.md +187 -14
- package/runtime/prompts/lead/report-writer.md +20 -1
- package/runtime/prompts/lead/team-contract.md +8 -53
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
- package/runtime/prompts/profiles/_common-contract.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/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +8 -2
- package/runtime/prompts/profiles/release-handoff.md +0 -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 +6 -1
- 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/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 +118 -35
- package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +65 -18
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +81 -1
- package/runtime/python/okstra_ctl/approval_decisions.py +3 -2
- 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/dispatch_core.py +35 -65
- package/runtime/python/okstra_ctl/dispatch_state.py +134 -59
- 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/final_report_schema.py +2 -2
- 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 +2 -1
- package/runtime/python/okstra_ctl/incremental_scope.py +8 -3
- 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/paths.py +9 -0
- package/runtime/python/okstra_ctl/plan_items.py +115 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +513 -18
- 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 +26 -15
- package/runtime/python/okstra_ctl/render_final_report.py +3 -2
- package/runtime/python/okstra_ctl/report_assembly.py +172 -17
- package/runtime/python/okstra_ctl/report_finalize.py +7 -10
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_language.py +3 -2
- package/runtime/python/okstra_ctl/report_markdown.py +13 -1
- package/runtime/python/okstra_ctl/report_narrative.py +40 -8
- 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 +159 -56
- 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 +4 -2
- 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 +4 -2
- package/runtime/python/okstra_ctl/stage_map.py +26 -6
- package/runtime/python/okstra_ctl/stage_targets.py +3 -4
- package/runtime/python/okstra_ctl/team.py +2 -1
- 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_prompt_body.py +18 -7
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
- 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 +9 -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 +17 -1
- package/runtime/templates/reports/schedule.template.md +4 -4
- package/runtime/templates/worker-error-contract.md +17 -29
- package/runtime/validators/validate-run.py +379 -63
- package/runtime/validators/validate_session_conformance.py +43 -7
|
@@ -34,6 +34,8 @@ from .agent_invocation import (
|
|
|
34
34
|
verify_agent_invocation,
|
|
35
35
|
v2_role_assignment_authority_errors,
|
|
36
36
|
)
|
|
37
|
+
from .fixed_text import line
|
|
38
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
37
39
|
from .convergence_store import (
|
|
38
40
|
DYNAMIC_VERIFIER_SOURCE_ROLES,
|
|
39
41
|
reserve_dynamic_verifier,
|
|
@@ -48,6 +50,7 @@ from .wrapper_status import (
|
|
|
48
50
|
prompt_derived_paths,
|
|
49
51
|
status_path_for_prompt,
|
|
50
52
|
)
|
|
53
|
+
from .report_inputs import report_narrative_path, uses_report_contract_v3
|
|
51
54
|
from .worker_prompt_policy import (
|
|
52
55
|
CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT,
|
|
53
56
|
resolve_prompt_plan_for_manifest,
|
|
@@ -73,6 +76,13 @@ class AgentPromptCliError(RuntimeError):
|
|
|
73
76
|
def _parser() -> argparse.ArgumentParser:
|
|
74
77
|
parser = argparse.ArgumentParser(prog="okstra agent-prompt")
|
|
75
78
|
commands = parser.add_subparsers(dest="command", required=True)
|
|
79
|
+
_add_materialize_parser(commands)
|
|
80
|
+
_add_completion_parsers(commands)
|
|
81
|
+
_add_dispatch_parsers(commands)
|
|
82
|
+
return parser
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _add_materialize_parser(commands: argparse._SubParsersAction) -> None:
|
|
76
86
|
materialize = commands.add_parser("materialize")
|
|
77
87
|
_common_paths(materialize)
|
|
78
88
|
materialize.add_argument("--invocation-id", required=True)
|
|
@@ -113,7 +123,10 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
113
123
|
verify.add_argument("--run-manifest")
|
|
114
124
|
verify.add_argument("--metadata", required=True)
|
|
115
125
|
verify.add_argument("--json", action="store_true")
|
|
126
|
+
verify.add_argument("--text", action="store_true")
|
|
116
127
|
|
|
128
|
+
|
|
129
|
+
def _add_completion_parsers(commands: argparse._SubParsersAction) -> None:
|
|
117
130
|
materialize_result = commands.add_parser("materialize-result")
|
|
118
131
|
_standalone_completion_args(materialize_result)
|
|
119
132
|
materialize_result.add_argument("--returned-body-file", required=True)
|
|
@@ -127,6 +140,8 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
127
140
|
verify_completion.add_argument("--completion", required=True)
|
|
128
141
|
verify_completion.add_argument("--json", action="store_true")
|
|
129
142
|
|
|
143
|
+
|
|
144
|
+
def _add_dispatch_parsers(commands: argparse._SubParsersAction) -> None:
|
|
130
145
|
record_dispatch = commands.add_parser("record-dispatch")
|
|
131
146
|
_common_paths(record_dispatch)
|
|
132
147
|
record_dispatch.add_argument("--run-manifest", required=True)
|
|
@@ -156,7 +171,6 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
156
171
|
link_result.add_argument("--dispatch-id", required=True)
|
|
157
172
|
link_result.add_argument("--result", required=True)
|
|
158
173
|
link_result.add_argument("--json", action="store_true")
|
|
159
|
-
return parser
|
|
160
174
|
|
|
161
175
|
|
|
162
176
|
def _common_paths(parser: argparse.ArgumentParser) -> None:
|
|
@@ -182,6 +196,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
182
196
|
_emit(
|
|
183
197
|
{"ok": True, "metadataPath": str(Path(args.metadata).resolve())},
|
|
184
198
|
args.json,
|
|
199
|
+
args.text,
|
|
185
200
|
)
|
|
186
201
|
return 0
|
|
187
202
|
if args.command == "materialize-result":
|
|
@@ -317,16 +332,32 @@ def _materialize_run(
|
|
|
317
332
|
)
|
|
318
333
|
if args.audience == "report-writer" and not args.audit_source:
|
|
319
334
|
# The report writer is the one audience whose result path is not its own
|
|
320
|
-
# worker result: it writes the report
|
|
321
|
-
#
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
#
|
|
335
|
+
# worker result: it writes the report body, while the audit sidecar is
|
|
336
|
+
# derived from its `.md`. With both collapsed into one value the prompt
|
|
337
|
+
# loses its `**Worker Result Path:**` anchor and the writer puts the
|
|
338
|
+
# report where the audit file belongs — silently, because every header
|
|
339
|
+
# is still present and well-formed. The roster path derives both from
|
|
340
|
+
# the manifest; a dynamic call has to name them.
|
|
341
|
+
#
|
|
342
|
+
# Which artifact `--result` names depends on the report contract, and
|
|
343
|
+
# `dispatch_state.dispatch_result_path` is what decides it. Naming only
|
|
344
|
+
# the v2 answer here sent v3 runs to the data.json, so the narrative
|
|
345
|
+
# `report-finalize` assembles from was never written and the phase
|
|
346
|
+
# failed later, at a place that does not point back here.
|
|
347
|
+
if uses_report_contract_v3(manifest):
|
|
348
|
+
expected = report_narrative_path(project_root, manifest)
|
|
349
|
+
raise AgentPromptCliError(
|
|
350
|
+
"report-writer materialization requires --audit-source: under "
|
|
351
|
+
f"report contract 3.0 --result is the narrative ({expected}), "
|
|
352
|
+
"which `report-finalize` assembles the report data.json from, "
|
|
353
|
+
"and --audit-source the worker-result .md the audit sidecar is "
|
|
354
|
+
"derived from"
|
|
355
|
+
)
|
|
326
356
|
raise AgentPromptCliError(
|
|
327
|
-
"report-writer materialization requires --audit-source:
|
|
328
|
-
"
|
|
329
|
-
"audit sidecar is
|
|
357
|
+
"report-writer materialization requires --audit-source: under "
|
|
358
|
+
"report contract 2.0 --result is the report data.json, and "
|
|
359
|
+
"--audit-source the worker-result .md the audit sidecar is "
|
|
360
|
+
"derived from"
|
|
330
361
|
)
|
|
331
362
|
audit_source_path = (
|
|
332
363
|
_authorized_path(
|
|
@@ -636,11 +667,12 @@ def _validate_run_identity(
|
|
|
636
667
|
expected = CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT.get(scope, "")
|
|
637
668
|
if not expected or dispatch_kind != "critic":
|
|
638
669
|
raise AgentPromptCliError("critic assignment identity is invalid")
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
670
|
+
# provider 대조는 여기 두지 않는다. critic 의 `worker_id` 는 배정 참조의
|
|
671
|
+
# 마지막 마디(`scope` / `acceptance`)이고 provider 이름이 아니다
|
|
672
|
+
# (`render.py` 의 로스터 행이 `assignment_ref.rsplit("/", 1)[-1]` 로 만든다).
|
|
673
|
+
# 종전에 있던 `provider != worker_id` 검사는 두 어휘를 비교해서 늘 참이 되는
|
|
674
|
+
# 형태였고, 설정을 run manifest 에서 찾다 못 찾아 조용히 건너뛰었기에
|
|
675
|
+
# 드러나지 않았다. 정본에서 읽게 고치면 모든 critic 디스패치가 거부된다.
|
|
644
676
|
else:
|
|
645
677
|
try:
|
|
646
678
|
expected = resolve_prompt_plan_for_manifest(
|
|
@@ -1039,8 +1071,8 @@ def _relative(project_root: Path, path: Path) -> str:
|
|
|
1039
1071
|
|
|
1040
1072
|
def _read_json_object(path: Path, label: str) -> dict[str, Any]:
|
|
1041
1073
|
try:
|
|
1042
|
-
value =
|
|
1043
|
-
except
|
|
1074
|
+
value = load_owned_object(path, artifact="agent prompt metadata")
|
|
1075
|
+
except JsonBoundaryError as exc:
|
|
1044
1076
|
raise AgentPromptCliError(f"{label} is invalid: {path}") from exc
|
|
1045
1077
|
if not isinstance(value, dict):
|
|
1046
1078
|
raise AgentPromptCliError(f"{label} must be a JSON object: {path}")
|
|
@@ -1098,12 +1130,27 @@ def _prepared_payload(
|
|
|
1098
1130
|
}
|
|
1099
1131
|
|
|
1100
1132
|
|
|
1101
|
-
def _emit(
|
|
1133
|
+
def _emit(
|
|
1134
|
+
payload: Mapping[str, object], as_json: bool, as_text: bool = False
|
|
1135
|
+
) -> None:
|
|
1102
1136
|
if as_json:
|
|
1103
1137
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
1138
|
+
elif as_text:
|
|
1139
|
+
print(render_agent_prompt_text(payload), end="")
|
|
1104
1140
|
else:
|
|
1105
1141
|
print(payload.get("promptPath") or payload.get("metadataPath") or "ok")
|
|
1106
1142
|
|
|
1107
1143
|
|
|
1144
|
+
def render_agent_prompt_text(payload: Mapping[str, object]) -> str:
|
|
1145
|
+
"""agent prompt 검증 결과의 승인된 경로만 투영한다."""
|
|
1146
|
+
rows = ["Okstra agent prompt\n"]
|
|
1147
|
+
rows.append(line("Status", "ready" if payload.get("ok", True) else "error"))
|
|
1148
|
+
for label, key in (("Prompt path", "promptPath"), ("Metadata path", "metadataPath"),
|
|
1149
|
+
("Assignment ID", "assignmentId"), ("Worker role", "workerRole")):
|
|
1150
|
+
if key in payload:
|
|
1151
|
+
rows.append(line(label, payload.get(key)))
|
|
1152
|
+
return "".join(rows)
|
|
1153
|
+
|
|
1154
|
+
|
|
1108
1155
|
if __name__ == "__main__":
|
|
1109
1156
|
raise SystemExit(main())
|
|
@@ -11,6 +11,7 @@ from typing import Mapping, Sequence
|
|
|
11
11
|
|
|
12
12
|
from .final_report_paths import final_report_data_path
|
|
13
13
|
from .paths import task_manifest_file
|
|
14
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
14
15
|
from .user_response import (
|
|
15
16
|
UserResponseError,
|
|
16
17
|
load_authoritative_analysis_review,
|
|
@@ -196,8 +197,8 @@ def _task_key_from_manifest(task_root: Path, okstra_root: Path) -> str:
|
|
|
196
197
|
task_manifest_file(task_root), okstra_root, "task manifest path"
|
|
197
198
|
)
|
|
198
199
|
try:
|
|
199
|
-
manifest =
|
|
200
|
-
except
|
|
200
|
+
manifest = load_owned_object(manifest_path, artifact="task manifest")
|
|
201
|
+
except JsonBoundaryError as exc:
|
|
201
202
|
raise AnalysisInputError(f"cannot read task manifest: {manifest_path}") from exc
|
|
202
203
|
if not isinstance(manifest, dict):
|
|
203
204
|
raise AnalysisInputError("task manifest must contain an object")
|
|
@@ -219,8 +220,8 @@ def load_analysis_report_candidate(
|
|
|
219
220
|
raise AnalysisInputError("report filename is not a final report")
|
|
220
221
|
data_path = _resolved_within(final_report_data_path(report), okstra_root, "data.json path")
|
|
221
222
|
try:
|
|
222
|
-
data =
|
|
223
|
-
except
|
|
223
|
+
data = load_owned_object(data_path, artifact="analysis report record")
|
|
224
|
+
except JsonBoundaryError as exc:
|
|
224
225
|
raise AnalysisInputError(f"cannot read data.json: {data_path}") from exc
|
|
225
226
|
if not isinstance(data, dict):
|
|
226
227
|
raise AnalysisInputError("data.json must contain an object")
|
|
@@ -128,7 +128,87 @@ def build_analysis_packet(
|
|
|
128
128
|
parts.extend(_stage_ledger_unavailable_block(stage_ledger_notice))
|
|
129
129
|
parts.extend(_clarification_block(clarification_text))
|
|
130
130
|
parts.extend(_directive_block(directive))
|
|
131
|
-
|
|
131
|
+
body = "\n".join(part.rstrip() for part in parts).rstrip() + "\n"
|
|
132
|
+
return _with_section_index(body)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
_HEADING_RE = re.compile(r"\A#{1,2} \S")
|
|
136
|
+
_FENCE_RE = re.compile(r"\A\s*(```|~~~)")
|
|
137
|
+
# 목차 자신이 차지하는 줄 수 중 표제 행을 뺀 나머지. 행 수는 표제 개수로
|
|
138
|
+
# 정해지므로 삽입 전에 전체 이동량을 계산할 수 있다.
|
|
139
|
+
_INDEX_PREAMBLE_LINES = 5
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _with_section_index(body: str) -> str:
|
|
143
|
+
"""Prefix the rendered packet with every section's line range.
|
|
144
|
+
|
|
145
|
+
The packet's own structure is the only thing a worker can navigate it by,
|
|
146
|
+
and a worker cannot navigate what it cannot see. Measured 2026-08-21: the
|
|
147
|
+
antigravity CLI's `view_file` truncates at 46,080 bytes per call, carries
|
|
148
|
+
no line-range parameter, and reports only `<n> lines, <m> bytes` — the same
|
|
149
|
+
string whether it returned the file or 40% of it. On a 114KB packet that
|
|
150
|
+
first call ended at line 510, so `## Stage Ledger` (572) and
|
|
151
|
+
`## Clarification Carry-In Extract` (680) never arrived and the worker
|
|
152
|
+
planned from the pre-migration brief that fills the first 42KB. Every
|
|
153
|
+
evidence line it cited was under 400. With the ranges here a truncated
|
|
154
|
+
reader can fetch the block it is missing by range instead of re-reading
|
|
155
|
+
the head; the lead had to write those ranges by hand to get a usable
|
|
156
|
+
retry.
|
|
157
|
+
"""
|
|
158
|
+
lines = body.splitlines()
|
|
159
|
+
anchor = next(
|
|
160
|
+
(i for i, line in enumerate(lines)
|
|
161
|
+
if line.startswith("# OKSTRA Analysis Packet")),
|
|
162
|
+
None,
|
|
163
|
+
)
|
|
164
|
+
if anchor is None:
|
|
165
|
+
return body
|
|
166
|
+
headings = _heading_lines(lines)
|
|
167
|
+
if not headings:
|
|
168
|
+
return body
|
|
169
|
+
shift = _INDEX_PREAMBLE_LINES + len(headings)
|
|
170
|
+
total = len(lines) + shift
|
|
171
|
+
starts = [number + shift for number, _ in headings]
|
|
172
|
+
rows = [
|
|
173
|
+
f"- {start}-{end} `{text}`"
|
|
174
|
+
for start, end, (_, text) in zip(
|
|
175
|
+
starts, [nxt - 1 for nxt in starts[1:]] + [total], headings
|
|
176
|
+
)
|
|
177
|
+
]
|
|
178
|
+
block = [
|
|
179
|
+
"",
|
|
180
|
+
"## Section Index",
|
|
181
|
+
"",
|
|
182
|
+
f"This packet is {total} lines. Read a section by line range with"
|
|
183
|
+
" `sed -n '<start>,<end>p'` rather than opening the whole file: a"
|
|
184
|
+
" whole-file read is truncated by some worker CLIs without reporting"
|
|
185
|
+
" it, and the sections that describe current state are last.",
|
|
186
|
+
"",
|
|
187
|
+
*rows,
|
|
188
|
+
]
|
|
189
|
+
assert len(block) == shift, (len(block), shift)
|
|
190
|
+
return "\n".join(lines[:anchor + 1] + block + lines[anchor + 1:]) + "\n"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _heading_lines(lines: list[str]) -> list[tuple[int, str]]:
|
|
194
|
+
"""Every `#`/`##` heading outside a fence, as (1-based line, text).
|
|
195
|
+
|
|
196
|
+
Fences are tracked because the Stage Ledger and the carried-in
|
|
197
|
+
clarification rows embed JSON and shell text where a `#` starts a comment,
|
|
198
|
+
not a section.
|
|
199
|
+
"""
|
|
200
|
+
out: list[tuple[int, str]] = []
|
|
201
|
+
fenced = False
|
|
202
|
+
for number, line in enumerate(lines, start=1):
|
|
203
|
+
if _FENCE_RE.match(line):
|
|
204
|
+
fenced = not fenced
|
|
205
|
+
continue
|
|
206
|
+
if fenced or not _HEADING_RE.match(line):
|
|
207
|
+
continue
|
|
208
|
+
if line.startswith("# OKSTRA Analysis Packet"):
|
|
209
|
+
continue
|
|
210
|
+
out.append((number, line.strip()))
|
|
211
|
+
return out
|
|
132
212
|
|
|
133
213
|
|
|
134
214
|
def _packet_frontmatter(brief_text: str, task_key: str) -> str:
|
|
@@ -11,6 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Any, Mapping, Sequence
|
|
12
12
|
|
|
13
13
|
from .convergence_store import write_json_atomic
|
|
14
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
DISPOSITIONS = frozenset({"select", "accept-risk", "request-revision", "reject"})
|
|
@@ -87,8 +88,8 @@ def _new_ledger(task_key: str, task_type: str, run_seq: str) -> dict[str, Any]:
|
|
|
87
88
|
|
|
88
89
|
def _read_ledger(path: Path) -> dict[str, Any]:
|
|
89
90
|
try:
|
|
90
|
-
value =
|
|
91
|
-
except
|
|
91
|
+
value = load_owned_object(path, artifact="approval decision ledger")
|
|
92
|
+
except JsonBoundaryError as exc:
|
|
92
93
|
raise ApprovalDecisionError(f"cannot read approval ledger {path}: {exc}") from exc
|
|
93
94
|
if not isinstance(value, dict) or value.get("owner") != "lead":
|
|
94
95
|
raise ApprovalDecisionError(f"approval ledger owner must be lead: {path}")
|
|
@@ -12,6 +12,7 @@ from .domain.worker_stream import host_event_row
|
|
|
12
12
|
from .execution_identity import Attempt, ExecutionManifestError
|
|
13
13
|
from .execution_manifest import read_execution_manifest
|
|
14
14
|
from .execution_manifest import _replace_manifest, _write_manifest_over_existing
|
|
15
|
+
from .jsonl import append_jsonl
|
|
15
16
|
from .run_context import task_mutex
|
|
16
17
|
|
|
17
18
|
|
|
@@ -104,8 +105,7 @@ class HostEventStreamWriter:
|
|
|
104
105
|
payload=payload,
|
|
105
106
|
timestamp=_utc_now(),
|
|
106
107
|
)
|
|
107
|
-
|
|
108
|
-
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
108
|
+
append_jsonl(self.path, row, ensure_ascii=False, compact=False)
|
|
109
109
|
return row
|
|
110
110
|
|
|
111
111
|
|
|
@@ -13,6 +13,7 @@ from .ids import build_run_id
|
|
|
13
13
|
from .paths import runs_dir_of
|
|
14
14
|
from .invocation import save_invocation
|
|
15
15
|
from .jsonl import append_jsonl, read_jsonl, rotate_recent_if_needed
|
|
16
|
+
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
16
17
|
from .project_meta import _project_meta_path
|
|
17
18
|
from .reconcile import _now_iso, normalize_central_status
|
|
18
19
|
from .run_index_row import build_run_index_row
|
|
@@ -64,8 +65,8 @@ def discover_project_roots(home: Path) -> List[tuple]:
|
|
|
64
65
|
if not meta_file.is_file():
|
|
65
66
|
continue
|
|
66
67
|
try:
|
|
67
|
-
meta =
|
|
68
|
-
except
|
|
68
|
+
meta = load_owned_object(meta_file, artifact="project metadata")
|
|
69
|
+
except JsonBoundaryError:
|
|
69
70
|
continue
|
|
70
71
|
pid = str(meta.get("projectId") or "")
|
|
71
72
|
root = str(meta.get("projectRoot") or "")
|
|
@@ -89,7 +90,7 @@ def _apply_backfill_meta(home: Path, project_id: str, project_root: Path, *,
|
|
|
89
90
|
target = _project_meta_path(home, project_id)
|
|
90
91
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
91
92
|
if target.is_file():
|
|
92
|
-
meta =
|
|
93
|
+
meta = load_owned_object(target, artifact="project metadata")
|
|
93
94
|
else:
|
|
94
95
|
meta = {"projectId": project_id, "projectRoot": str(project_root),
|
|
95
96
|
"firstRunAt": "", "lastRunAt": "",
|
|
@@ -108,9 +109,7 @@ def _apply_backfill_meta(home: Path, project_id: str, project_root: Path, *,
|
|
|
108
109
|
meta["runCount"] = int(meta.get("runCount", 0)) + run_count_inc
|
|
109
110
|
meta["activeCount"] = max(
|
|
110
111
|
0, int(meta.get("activeCount", 0)) + active_count_inc)
|
|
111
|
-
|
|
112
|
-
tmp.write_text(json.dumps(meta, indent=2) + "\n")
|
|
113
|
-
os.replace(tmp, target)
|
|
112
|
+
write_owned_object_atomic(target, meta, artifact="project metadata")
|
|
114
113
|
|
|
115
114
|
|
|
116
115
|
def backfill_project(home: Path, project_id: str, project_root: Path) -> int:
|
|
@@ -142,8 +141,8 @@ def backfill_project(home: Path, project_id: str, project_root: Path) -> int:
|
|
|
142
141
|
task_type = m.group("tt")
|
|
143
142
|
seq = int(m.group("seq"))
|
|
144
143
|
try:
|
|
145
|
-
manifest =
|
|
146
|
-
except
|
|
144
|
+
manifest = load_owned_object(mf, artifact="run manifest")
|
|
145
|
+
except JsonBoundaryError:
|
|
147
146
|
# 부분 기록 / 손상된 매니페스트를 빈 dict 로 강등해 row 를
|
|
148
147
|
# 만들면, 빈 status 가 'completed' 로 normalize 되어
|
|
149
148
|
# recent 에 박히고, 같은 runId 가 existing_ids 로 인덱싱
|
|
@@ -268,6 +267,10 @@ def backfill_project(home: Path, project_id: str, project_root: Path) -> int:
|
|
|
268
267
|
|
|
269
268
|
def mark_backfilled(home: Path) -> None:
|
|
270
269
|
state_file = home / "state.json"
|
|
271
|
-
state =
|
|
270
|
+
state = (
|
|
271
|
+
load_owned_object(state_file, artifact="okstra home state")
|
|
272
|
+
if state_file.is_file()
|
|
273
|
+
else {}
|
|
274
|
+
)
|
|
272
275
|
state["backfilledAt"] = _now_iso()
|
|
273
|
-
state_file
|
|
276
|
+
write_owned_object_atomic(state_file, state, artifact="okstra home state")
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from typing import List, Optional
|
|
10
10
|
|
|
11
11
|
from .listing import list_runs
|
|
12
|
+
from .json_boundary import write_owned_object_atomic
|
|
12
13
|
from .resolver import resolve_last, resolve_run_id
|
|
13
14
|
|
|
14
15
|
|
|
@@ -20,10 +21,7 @@ def make_batch_id() -> str:
|
|
|
20
21
|
|
|
21
22
|
def write_batch_meta(home: Path, batch_id: str, payload: dict) -> Path:
|
|
22
23
|
target = home / "batches" / f"{batch_id}.json"
|
|
23
|
-
target
|
|
24
|
-
tmp = target.with_suffix(".json.tmp")
|
|
25
|
-
tmp.write_text(json.dumps(payload, indent=2) + "\n")
|
|
26
|
-
os.replace(tmp, target)
|
|
24
|
+
write_owned_object_atomic(target, payload, artifact="batch metadata")
|
|
27
25
|
return target
|
|
28
26
|
|
|
29
27
|
|
|
@@ -16,6 +16,8 @@ import json
|
|
|
16
16
|
import shlex
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
20
|
+
|
|
19
21
|
DEFAULT_BUILD_TOOL_TOKENS: tuple[str, ...] = (
|
|
20
22
|
"npm",
|
|
21
23
|
"yarn",
|
|
@@ -49,10 +51,11 @@ def resolve_build_tool_tokens(project_root: Path) -> tuple[str, ...]:
|
|
|
49
51
|
uses, so one rule covers both.
|
|
50
52
|
"""
|
|
51
53
|
try:
|
|
52
|
-
payload =
|
|
53
|
-
|
|
54
|
+
payload = load_owned_object(
|
|
55
|
+
Path(project_root) / ".okstra" / "project.json",
|
|
56
|
+
artifact="project configuration",
|
|
54
57
|
)
|
|
55
|
-
except
|
|
58
|
+
except JsonBoundaryError:
|
|
56
59
|
return DEFAULT_BUILD_TOOL_TOKENS
|
|
57
60
|
configured = payload.get("buildToolTokens") if isinstance(payload, dict) else None
|
|
58
61
|
if not isinstance(configured, list):
|
|
@@ -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:` 는 따옴표 없이 써야
|