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
|
@@ -21,6 +21,7 @@ from dataclasses import dataclass
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
23
|
from okstra_project.dirs import okstra_home, project_json_path
|
|
24
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
24
25
|
|
|
25
26
|
_DEFAULT_FILENAME = "pr-body.template.md"
|
|
26
27
|
|
|
@@ -49,8 +50,8 @@ def _read_json_field(path: Path, field: str) -> str:
|
|
|
49
50
|
if not path.is_file():
|
|
50
51
|
return ""
|
|
51
52
|
try:
|
|
52
|
-
data =
|
|
53
|
-
except
|
|
53
|
+
data = load_owned_object(path, artifact="PR template configuration")
|
|
54
|
+
except JsonBoundaryError:
|
|
54
55
|
return ""
|
|
55
56
|
if not isinstance(data, dict):
|
|
56
57
|
return ""
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""프로젝트 meta.json 업서트."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
4
|
from pathlib import Path
|
|
7
5
|
from typing import Optional
|
|
8
6
|
|
|
7
|
+
from .json_boundary import load_owned_object, write_owned_object_atomic
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
def _project_meta_path(home: Path, project_id: str) -> Path:
|
|
11
11
|
return home / "projects" / project_id / "meta.json"
|
|
@@ -16,7 +16,7 @@ def load_project_meta(home: Path, project_id: str) -> Optional[dict]:
|
|
|
16
16
|
p = _project_meta_path(home, project_id)
|
|
17
17
|
if not p.is_file():
|
|
18
18
|
return None
|
|
19
|
-
return
|
|
19
|
+
return load_owned_object(p, artifact="project metadata")
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def upsert_project_meta(home: Path, project_id: str, *,
|
|
@@ -29,7 +29,7 @@ def upsert_project_meta(home: Path, project_id: str, *,
|
|
|
29
29
|
target = _project_meta_path(home, project_id)
|
|
30
30
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
31
31
|
if target.is_file():
|
|
32
|
-
meta =
|
|
32
|
+
meta = load_owned_object(target, artifact="project metadata")
|
|
33
33
|
else:
|
|
34
34
|
meta = {
|
|
35
35
|
"projectId": project_id,
|
|
@@ -46,6 +46,4 @@ def upsert_project_meta(home: Path, project_id: str, *,
|
|
|
46
46
|
meta["activeCount"] = int(meta.get("activeCount", 0)) + 1
|
|
47
47
|
if finished:
|
|
48
48
|
meta["activeCount"] = max(0, int(meta.get("activeCount", 0)) - 1)
|
|
49
|
-
|
|
50
|
-
tmp.write_text(json.dumps(meta, indent=2) + "\n")
|
|
51
|
-
os.replace(tmp, target)
|
|
49
|
+
write_owned_object_atomic(target, meta, artifact="project metadata")
|
|
@@ -15,6 +15,8 @@ from okstra_ctl import next_phase
|
|
|
15
15
|
from okstra_ctl.clarification_items import sidecar_answers, user_response_sidecars
|
|
16
16
|
from okstra_ctl.ids import slugify_task_segment
|
|
17
17
|
from okstra_ctl.incremental_scope import preview_link_availability_for_report
|
|
18
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
19
|
+
from okstra_ctl.jsonl import append_jsonl
|
|
18
20
|
from okstra_ctl.paths import task_timeline_file
|
|
19
21
|
from okstra_project import read_task_key
|
|
20
22
|
from okstra_ctl.run_context import dir_flock
|
|
@@ -29,8 +31,8 @@ def _load_timeline(task_root: Path) -> list[dict]:
|
|
|
29
31
|
if not path.is_file():
|
|
30
32
|
return []
|
|
31
33
|
try:
|
|
32
|
-
data =
|
|
33
|
-
except
|
|
34
|
+
data = load_owned_object(path, artifact="task timeline")
|
|
35
|
+
except (JsonBoundaryError, OSError):
|
|
34
36
|
return []
|
|
35
37
|
runs = data.get("runs", [])
|
|
36
38
|
return runs if isinstance(runs, list) else []
|
|
@@ -150,8 +152,7 @@ def append_recap_entry(task_root, project_root, *, kind, mode, question,
|
|
|
150
152
|
# 과 같은 append-only idiom — 데이터 fd 가 아닌 별도 .lock 파일을 잠가야
|
|
151
153
|
# flush 가 락 안에서 끝난다).
|
|
152
154
|
with dir_flock(recap_dir, ".recap-log.lock"):
|
|
153
|
-
|
|
154
|
-
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
155
|
+
append_jsonl(log_path, entry, ensure_ascii=False, compact=False)
|
|
155
156
|
return {"logPath": project_rel(log_path, project_root), "entry": entry}
|
|
156
157
|
|
|
157
158
|
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
"""reconciliation/normalization."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
4
|
from datetime import datetime, timezone
|
|
7
5
|
from pathlib import Path
|
|
8
6
|
from typing import Optional
|
|
9
7
|
|
|
10
|
-
from .jsonl import append_jsonl, rotate_recent_if_needed
|
|
8
|
+
from .jsonl import append_jsonl, rewrite_jsonl, rotate_recent_if_needed
|
|
11
9
|
from .locks import central_lock
|
|
12
10
|
from .paths import resolve_under_root
|
|
13
11
|
from .project_meta import upsert_project_meta
|
|
14
12
|
from .run_index_row import read_run_index, slim_run_row
|
|
13
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
DEFAULT_ABORT_AFTER_SECONDS = 12 * 3600 # 12시간 무진척이면 aborted 로 간주
|
|
@@ -70,8 +69,8 @@ def _read_run_manifest_validation(project_root: Path, run_dir_rel: str,
|
|
|
70
69
|
if not manifest.is_file():
|
|
71
70
|
return "not-run"
|
|
72
71
|
try:
|
|
73
|
-
data =
|
|
74
|
-
except
|
|
72
|
+
data = load_owned_object(manifest, artifact="run manifest")
|
|
73
|
+
except JsonBoundaryError:
|
|
75
74
|
return "not-run"
|
|
76
75
|
validation = data.get("validation")
|
|
77
76
|
if isinstance(validation, dict):
|
|
@@ -94,11 +93,7 @@ def _sync_project_index(home: Path, project_id: str, updated_rows: list) -> None
|
|
|
94
93
|
src = by_id.get(er.get("runId"))
|
|
95
94
|
if src:
|
|
96
95
|
er.update({k: src[k] for k in ("status", "finishedAt", "validation")})
|
|
97
|
-
|
|
98
|
-
with tmp.open("w") as f:
|
|
99
|
-
for er in existing:
|
|
100
|
-
f.write(json.dumps(slim_run_row(er), separators=(",", ":")) + "\n")
|
|
101
|
-
os.replace(tmp, proj_index)
|
|
96
|
+
rewrite_jsonl(proj_index, [slim_run_row(row) for row in existing])
|
|
102
97
|
|
|
103
98
|
|
|
104
99
|
def reconcile_active(home: Path, *,
|
|
@@ -116,6 +111,20 @@ def reconcile_active(home: Path, *,
|
|
|
116
111
|
project=project)
|
|
117
112
|
|
|
118
113
|
|
|
114
|
+
def _publish_promoted(
|
|
115
|
+
home: Path, active: Path, survivors: list[dict], promoted: list[dict]
|
|
116
|
+
) -> None:
|
|
117
|
+
rewrite_jsonl(active, [slim_run_row(row) for row in survivors])
|
|
118
|
+
for row in promoted:
|
|
119
|
+
append_jsonl(home / "recent.jsonl", slim_run_row(row))
|
|
120
|
+
_sync_project_index(home, row["projectId"], [row])
|
|
121
|
+
upsert_project_meta(
|
|
122
|
+
home, row["projectId"], project_root=row["projectRoot"],
|
|
123
|
+
when=row["finishedAt"], finished=True,
|
|
124
|
+
)
|
|
125
|
+
rotate_recent_if_needed(home)
|
|
126
|
+
|
|
127
|
+
|
|
119
128
|
def _reconcile_active_locked(home: Path, *,
|
|
120
129
|
abort_after_seconds: int,
|
|
121
130
|
project: Optional[str] = None) -> dict:
|
|
@@ -171,18 +180,7 @@ def _reconcile_active_locked(home: Path, *,
|
|
|
171
180
|
summary["running"] += 1
|
|
172
181
|
if not promoted:
|
|
173
182
|
return summary
|
|
174
|
-
|
|
175
|
-
with tmp.open("w") as f:
|
|
176
|
-
for row in survivors:
|
|
177
|
-
f.write(json.dumps(slim_run_row(row), separators=(",", ":")) + "\n")
|
|
178
|
-
os.replace(tmp, active)
|
|
179
|
-
for row in promoted:
|
|
180
|
-
append_jsonl(home / "recent.jsonl", slim_run_row(row))
|
|
181
|
-
_sync_project_index(home, row["projectId"], [row])
|
|
182
|
-
upsert_project_meta(home, row["projectId"],
|
|
183
|
-
project_root=row["projectRoot"],
|
|
184
|
-
when=row["finishedAt"], finished=True)
|
|
185
|
-
rotate_recent_if_needed(home)
|
|
183
|
+
_publish_promoted(home, active, survivors, promoted)
|
|
186
184
|
return summary
|
|
187
185
|
|
|
188
186
|
|
|
@@ -225,11 +223,7 @@ def _reconcile_recent_locked(home: Path, *, project: Optional[str] = None) -> di
|
|
|
225
223
|
summary["completed" if new_status == "completed" else "failed"] += 1
|
|
226
224
|
if not updated_by_project:
|
|
227
225
|
return summary
|
|
228
|
-
|
|
229
|
-
with tmp.open("w") as f:
|
|
230
|
-
for row in rows:
|
|
231
|
-
f.write(json.dumps(slim_run_row(row), separators=(",", ":")) + "\n")
|
|
232
|
-
os.replace(tmp, recent)
|
|
226
|
+
rewrite_jsonl(recent, [slim_run_row(row) for row in rows])
|
|
233
227
|
for pid, urows in updated_by_project.items():
|
|
234
228
|
_sync_project_index(home, pid, urows)
|
|
235
229
|
return summary
|
|
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from ..domain.host import HostAdapterContractError
|
|
10
|
+
from ..json_boundary import JsonBoundaryError, load_owned_object
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
@dataclass(frozen=True)
|
|
@@ -66,8 +67,8 @@ def _read_manifest(manifest_path: Path) -> HostManifest:
|
|
|
66
67
|
|
|
67
68
|
def _read_json_object(manifest_path: Path) -> dict[str, object]:
|
|
68
69
|
try:
|
|
69
|
-
raw =
|
|
70
|
-
except
|
|
70
|
+
raw = load_owned_object(manifest_path, artifact="host adapter manifest")
|
|
71
|
+
except JsonBoundaryError as exc:
|
|
71
72
|
raise HostAdapterContractError(
|
|
72
73
|
f"invalid host manifest: {manifest_path}"
|
|
73
74
|
) from exc
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from okstra_project.dirs import okstra_home
|
|
10
10
|
|
|
11
11
|
from ..domain.provider import ProviderSpec, UnknownProviderError
|
|
12
|
+
from ..json_boundary import JsonBoundaryError, load_owned_object
|
|
12
13
|
from .factory_loader import FactoryLoadError, load_relative_factory
|
|
13
14
|
|
|
14
15
|
|
|
@@ -121,8 +122,8 @@ def _manifest_paths(roots: Iterable[Path]) -> tuple[Path, ...]:
|
|
|
121
122
|
|
|
122
123
|
def _read_manifest(manifest_path: Path) -> tuple[str, str]:
|
|
123
124
|
try:
|
|
124
|
-
raw =
|
|
125
|
-
except
|
|
125
|
+
raw = load_owned_object(manifest_path, artifact="provider adapter manifest")
|
|
126
|
+
except JsonBoundaryError as exc:
|
|
126
127
|
raise ProviderAdapterContractError(f"invalid provider manifest: {manifest_path}") from exc
|
|
127
128
|
if not isinstance(raw, dict) or raw.get("schemaVersion") != 1:
|
|
128
129
|
raise ProviderAdapterContractError(f"invalid provider manifest: {manifest_path}")
|
|
@@ -46,6 +46,7 @@ from .registry.host_registry import default_host_registry
|
|
|
46
46
|
from .registry.provider_registry import default_provider_registry
|
|
47
47
|
from .path_hints import compact_active_run_context, hydrate_run_context
|
|
48
48
|
from .paths import task_timeline_file
|
|
49
|
+
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
49
50
|
from .report_contract import CURRENT_REPORT_SCHEMA_VERSION
|
|
50
51
|
from .worker_artifacts import artifacts_from_context
|
|
51
52
|
from .workflow import PHASE_SEQUENCE
|
|
@@ -74,7 +75,9 @@ def _load_ctx(ctx_arg: str) -> dict:
|
|
|
74
75
|
s = ctx_arg.lstrip()
|
|
75
76
|
if s.startswith("{"):
|
|
76
77
|
return hydrate_run_context(json.loads(ctx_arg))
|
|
77
|
-
return hydrate_run_context(
|
|
78
|
+
return hydrate_run_context(
|
|
79
|
+
load_owned_object(Path(ctx_arg), artifact="render context")
|
|
80
|
+
)
|
|
78
81
|
|
|
79
82
|
|
|
80
83
|
def _write_text(path: Path, text: str) -> None:
|
|
@@ -83,7 +86,7 @@ def _write_text(path: Path, text: str) -> None:
|
|
|
83
86
|
|
|
84
87
|
|
|
85
88
|
def _write_json(path: Path, payload: dict) -> None:
|
|
86
|
-
|
|
89
|
+
write_owned_object_atomic(path, payload, artifact="rendered runtime object")
|
|
87
90
|
|
|
88
91
|
|
|
89
92
|
def _execution_identity(ctx: Mapping[str, object]) -> dict:
|
|
@@ -874,7 +877,7 @@ def render_task_catalog_discovery(output_path: str, ctx: dict) -> None:
|
|
|
874
877
|
entries = []
|
|
875
878
|
for manifest_path in sorted(tasks_root.rglob(TASK_MANIFEST_FILENAME)):
|
|
876
879
|
try:
|
|
877
|
-
manifest =
|
|
880
|
+
manifest = load_owned_object(manifest_path, artifact="task manifest")
|
|
878
881
|
except Exception:
|
|
879
882
|
continue
|
|
880
883
|
if not isinstance(manifest, dict):
|
|
@@ -892,7 +895,7 @@ def render_task_catalog_discovery(output_path: str, ctx: dict) -> None:
|
|
|
892
895
|
latest_run = {}
|
|
893
896
|
if timeline_file.is_file():
|
|
894
897
|
try:
|
|
895
|
-
payload =
|
|
898
|
+
payload = load_owned_object(timeline_file, artifact="task timeline")
|
|
896
899
|
except Exception:
|
|
897
900
|
payload = {}
|
|
898
901
|
runs = payload.get("runs", []) if isinstance(payload, dict) else []
|
|
@@ -982,7 +985,9 @@ def render_latest_task_discovery(output_path: str, ctx: dict) -> None:
|
|
|
982
985
|
task_manifest = {}
|
|
983
986
|
if task_manifest_path.exists():
|
|
984
987
|
try:
|
|
985
|
-
task_manifest =
|
|
988
|
+
task_manifest = load_owned_object(
|
|
989
|
+
task_manifest_path, artifact="task manifest"
|
|
990
|
+
)
|
|
986
991
|
except Exception:
|
|
987
992
|
task_manifest = {}
|
|
988
993
|
workflow = (
|
|
@@ -1344,7 +1349,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
1344
1349
|
existing = {}
|
|
1345
1350
|
if path.exists():
|
|
1346
1351
|
try:
|
|
1347
|
-
existing =
|
|
1352
|
+
existing = load_owned_object(path, artifact="task manifest")
|
|
1348
1353
|
except Exception:
|
|
1349
1354
|
existing = {}
|
|
1350
1355
|
reviewers = _resolve_workers(ctx)
|
|
@@ -1646,21 +1651,23 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1646
1651
|
existing_run_manifest = {}
|
|
1647
1652
|
if run_manifest_exists:
|
|
1648
1653
|
try:
|
|
1649
|
-
loaded_run_manifest =
|
|
1650
|
-
run_manifest_file
|
|
1654
|
+
loaded_run_manifest = load_owned_object(
|
|
1655
|
+
run_manifest_file, artifact="run manifest"
|
|
1651
1656
|
)
|
|
1652
1657
|
existing_run_manifest = (
|
|
1653
1658
|
loaded_run_manifest
|
|
1654
1659
|
if isinstance(loaded_run_manifest, dict)
|
|
1655
1660
|
else {}
|
|
1656
1661
|
)
|
|
1657
|
-
except
|
|
1662
|
+
except JsonBoundaryError:
|
|
1658
1663
|
existing_run_manifest = {}
|
|
1659
1664
|
task_manifest_path = Path(ctx.get("TASK_MANIFEST_PATH", ""))
|
|
1660
1665
|
task_manifest = {}
|
|
1661
1666
|
if task_manifest_path.exists():
|
|
1662
1667
|
try:
|
|
1663
|
-
task_manifest =
|
|
1668
|
+
task_manifest = load_owned_object(
|
|
1669
|
+
task_manifest_path, artifact="task manifest"
|
|
1670
|
+
)
|
|
1664
1671
|
except Exception:
|
|
1665
1672
|
task_manifest = {}
|
|
1666
1673
|
reviewers = _resolve_workers(ctx)
|
|
@@ -2086,7 +2093,9 @@ def render_timeline(timeline_path: str, ctx: dict) -> None:
|
|
|
2086
2093
|
task_manifest = {}
|
|
2087
2094
|
if task_manifest_path.exists():
|
|
2088
2095
|
try:
|
|
2089
|
-
task_manifest =
|
|
2096
|
+
task_manifest = load_owned_object(
|
|
2097
|
+
task_manifest_path, artifact="task manifest"
|
|
2098
|
+
)
|
|
2090
2099
|
except Exception:
|
|
2091
2100
|
task_manifest = {}
|
|
2092
2101
|
reviewers = _resolve_workers(ctx)
|
|
@@ -2095,7 +2104,7 @@ def render_timeline(timeline_path: str, ctx: dict) -> None:
|
|
|
2095
2104
|
existing = {}
|
|
2096
2105
|
if path.exists():
|
|
2097
2106
|
try:
|
|
2098
|
-
existing =
|
|
2107
|
+
existing = load_owned_object(path, artifact="task timeline")
|
|
2099
2108
|
except Exception:
|
|
2100
2109
|
existing = {}
|
|
2101
2110
|
runs = existing.get("runs", [])
|
|
@@ -2190,7 +2199,9 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
|
|
|
2190
2199
|
task_manifest = {}
|
|
2191
2200
|
if task_manifest_path.exists():
|
|
2192
2201
|
try:
|
|
2193
|
-
task_manifest =
|
|
2202
|
+
task_manifest = load_owned_object(
|
|
2203
|
+
task_manifest_path, artifact="task manifest"
|
|
2204
|
+
)
|
|
2194
2205
|
except Exception:
|
|
2195
2206
|
task_manifest = {}
|
|
2196
2207
|
workflow = (
|
|
@@ -2385,8 +2396,8 @@ def build_available_mcp_servers_block(project_root: Path) -> str:
|
|
|
2385
2396
|
"""
|
|
2386
2397
|
config_path = project_json_path(project_root)
|
|
2387
2398
|
try:
|
|
2388
|
-
raw =
|
|
2389
|
-
except
|
|
2399
|
+
raw = load_owned_object(config_path, artifact="project config")
|
|
2400
|
+
except JsonBoundaryError:
|
|
2390
2401
|
return _NO_MCP_SERVERS_LINE
|
|
2391
2402
|
servers = raw.get("mcpServers") if isinstance(raw, dict) else None
|
|
2392
2403
|
if not isinstance(servers, list) or not servers:
|
|
@@ -49,6 +49,7 @@ from okstra_ctl.final_report_schema import (
|
|
|
49
49
|
load_schema_for_data,
|
|
50
50
|
validate as schema_validate,
|
|
51
51
|
)
|
|
52
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
52
53
|
from okstra_ctl.i18n import I18nError, SUPPORTED_LANGS, load_dictionary, make_jinja_global
|
|
53
54
|
from okstra_ctl.md_table import UNESCAPED_PIPE_RE, to_cell_text
|
|
54
55
|
from okstra_ctl.models import UnknownModelError, resolve_model_metadata
|
|
@@ -803,8 +804,8 @@ def render_to_file(
|
|
|
803
804
|
if not data_path.is_file():
|
|
804
805
|
raise FinalReportRenderError(f"data file not found: {data_path}")
|
|
805
806
|
try:
|
|
806
|
-
data =
|
|
807
|
-
except
|
|
807
|
+
data = load_owned_object(data_path, artifact="final report record")
|
|
808
|
+
except JsonBoundaryError as exc:
|
|
808
809
|
raise FinalReportRenderError(f"invalid JSON in {data_path}: {exc}") from exc
|
|
809
810
|
|
|
810
811
|
# 템플릿은 data.json 위치가 아니라 이 모듈(설치본은 ~/.okstra/lib/python,
|
|
@@ -6,12 +6,17 @@ import os
|
|
|
6
6
|
import tempfile
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Any, Callable, Mapping
|
|
9
|
+
from typing import Any, Callable, Mapping, Sequence
|
|
10
10
|
|
|
11
11
|
from .agent_activity import agent_activity_rows
|
|
12
12
|
from .final_report_schema import load_schema_version, validate
|
|
13
13
|
from .report_inputs import ReportInputPath, report_input_paths, uses_report_contract_v3
|
|
14
|
+
from .json_boundary import JsonBoundaryError, load_owned_object, serialize_owned_object
|
|
14
15
|
from .report_narrative import parse_narrative
|
|
16
|
+
from .report_synthesis_packet import (
|
|
17
|
+
report_synthesis_packet_paths,
|
|
18
|
+
verify_report_synthesis_packet_sources,
|
|
19
|
+
)
|
|
15
20
|
from .report_projections import (
|
|
16
21
|
ReportProjectionError,
|
|
17
22
|
project_convergence,
|
|
@@ -32,12 +37,20 @@ class AssemblyIssue:
|
|
|
32
37
|
class ReportAssemblyError(ValueError):
|
|
33
38
|
"""게시 전 입력 검증 실패와 실제 소유자를 전달한다."""
|
|
34
39
|
|
|
35
|
-
def __init__(self,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
def __init__(self, issues: AssemblyIssue | Sequence[AssemblyIssue]) -> None:
|
|
41
|
+
rows = (issues,) if isinstance(issues, AssemblyIssue) else tuple(issues)
|
|
42
|
+
if not rows:
|
|
43
|
+
raise ValueError("report assembly error requires at least one issue")
|
|
44
|
+
self.issues = rows
|
|
45
|
+
self.issue = rows[0]
|
|
46
|
+
super().__init__("; ".join(_issue_message(issue) for issue in rows))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _issue_message(issue: AssemblyIssue) -> str:
|
|
50
|
+
return (
|
|
51
|
+
f"owner={issue.owner} artifactPath={issue.artifact_path} "
|
|
52
|
+
f"fieldPath={issue.field_path} reason={issue.reason}"
|
|
53
|
+
)
|
|
41
54
|
|
|
42
55
|
|
|
43
56
|
def _fail(owner: str, path: Path, field: str, reason: object) -> None:
|
|
@@ -48,8 +61,8 @@ def _read_json(row: ReportInputPath) -> dict[str, Any]:
|
|
|
48
61
|
if not row.path.is_file():
|
|
49
62
|
_fail(row.owner, row.path, "$", "required input is missing")
|
|
50
63
|
try:
|
|
51
|
-
value =
|
|
52
|
-
except
|
|
64
|
+
value = load_owned_object(row.path, artifact=f"{row.owner} report input")
|
|
65
|
+
except JsonBoundaryError as exc:
|
|
53
66
|
_fail(row.owner, row.path, "$", exc)
|
|
54
67
|
if not isinstance(value, dict):
|
|
55
68
|
_fail(row.owner, row.path, "$", "input must be a JSON object")
|
|
@@ -63,6 +76,39 @@ def _input_map(project_root: Path, manifest: Mapping[str, Any]) -> dict[str, Rep
|
|
|
63
76
|
_fail("orchestrator", project_root, "manifest", exc)
|
|
64
77
|
|
|
65
78
|
|
|
79
|
+
def _input_preflight_issues(
|
|
80
|
+
inputs: Mapping[str, ReportInputPath],
|
|
81
|
+
schema: Mapping[str, Any],
|
|
82
|
+
) -> tuple[AssemblyIssue, ...]:
|
|
83
|
+
issues: list[AssemblyIssue] = []
|
|
84
|
+
for row in inputs.values():
|
|
85
|
+
if not row.path.is_file():
|
|
86
|
+
issues.append(
|
|
87
|
+
AssemblyIssue(
|
|
88
|
+
row.owner,
|
|
89
|
+
str(row.path),
|
|
90
|
+
"$",
|
|
91
|
+
"required input is missing",
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
continue
|
|
95
|
+
try:
|
|
96
|
+
if row.key == "narrative":
|
|
97
|
+
parse_narrative(row.path.read_text(encoding="utf-8"), schema)
|
|
98
|
+
elif row.key == "agent-activity":
|
|
99
|
+
row.path.read_text(encoding="utf-8")
|
|
100
|
+
else:
|
|
101
|
+
value = load_owned_object(
|
|
102
|
+
row.path,
|
|
103
|
+
artifact=f"{row.owner} report input",
|
|
104
|
+
)
|
|
105
|
+
if not isinstance(value, dict):
|
|
106
|
+
raise ValueError("input must be a JSON object")
|
|
107
|
+
except (JsonBoundaryError, OSError, UnicodeError, ValueError) as exc:
|
|
108
|
+
issues.append(AssemblyIssue(row.owner, str(row.path), "$", str(exc)))
|
|
109
|
+
return tuple(issues)
|
|
110
|
+
|
|
111
|
+
|
|
66
112
|
def _narrative(row: ReportInputPath, schema: Mapping[str, Any]) -> dict[str, Any]:
|
|
67
113
|
if not row.path.is_file():
|
|
68
114
|
_fail(row.owner, row.path, "$", "required input is missing")
|
|
@@ -90,6 +136,81 @@ def _clarifications(
|
|
|
90
136
|
]
|
|
91
137
|
|
|
92
138
|
|
|
139
|
+
def _project_relative(project_root: Path, path: Path) -> str:
|
|
140
|
+
try:
|
|
141
|
+
return path.relative_to(project_root).as_posix()
|
|
142
|
+
except ValueError:
|
|
143
|
+
return str(path)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _clarification_response_value(
|
|
147
|
+
project_root: Path,
|
|
148
|
+
manifest: Mapping[str, Any],
|
|
149
|
+
) -> str:
|
|
150
|
+
direct = manifest.get("clarificationResponsePath")
|
|
151
|
+
if isinstance(direct, str) and direct.strip():
|
|
152
|
+
return direct.strip()
|
|
153
|
+
context_value = manifest.get("activeRunContextPath")
|
|
154
|
+
if isinstance(context_value, str) and context_value.strip():
|
|
155
|
+
context_path = Path(context_value)
|
|
156
|
+
if not context_path.is_absolute():
|
|
157
|
+
context_path = project_root / context_path
|
|
158
|
+
context = _read_json(
|
|
159
|
+
ReportInputPath("active-run-context", "orchestrator", context_path)
|
|
160
|
+
)
|
|
161
|
+
instruction_set = context.get("instructionSet")
|
|
162
|
+
value = (
|
|
163
|
+
instruction_set.get("clarificationResponsePath")
|
|
164
|
+
if isinstance(instruction_set, Mapping)
|
|
165
|
+
else ""
|
|
166
|
+
)
|
|
167
|
+
if isinstance(value, str) and value.strip():
|
|
168
|
+
return value.strip()
|
|
169
|
+
return _latest_user_response_value(project_root, manifest)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _latest_user_response_value(
|
|
173
|
+
project_root: Path,
|
|
174
|
+
manifest: Mapping[str, Any],
|
|
175
|
+
) -> str:
|
|
176
|
+
record_value = manifest.get("expectedReportRecordPath")
|
|
177
|
+
if not isinstance(record_value, str) or not record_value.strip():
|
|
178
|
+
return ""
|
|
179
|
+
record_path = Path(record_value)
|
|
180
|
+
if not record_path.is_absolute():
|
|
181
|
+
record_path = project_root / record_path
|
|
182
|
+
if record_path.parent.name != "reports":
|
|
183
|
+
return ""
|
|
184
|
+
task_type = str(manifest.get("taskType") or "").strip()
|
|
185
|
+
if not task_type:
|
|
186
|
+
return ""
|
|
187
|
+
responses = sorted(
|
|
188
|
+
path
|
|
189
|
+
for path in (record_path.parent.parent / "user-responses").glob(
|
|
190
|
+
f"user-response-{task_type}-*.md"
|
|
191
|
+
)
|
|
192
|
+
if path.is_file()
|
|
193
|
+
)
|
|
194
|
+
if not responses:
|
|
195
|
+
return ""
|
|
196
|
+
return _project_relative(project_root, responses[-1])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _clarification_carry_in(
|
|
200
|
+
project_root: Path,
|
|
201
|
+
manifest: Mapping[str, Any],
|
|
202
|
+
) -> dict[str, str] | None:
|
|
203
|
+
value = _clarification_response_value(project_root, manifest)
|
|
204
|
+
if not value:
|
|
205
|
+
return None
|
|
206
|
+
path = Path(value)
|
|
207
|
+
if not path.is_absolute():
|
|
208
|
+
path = project_root / path
|
|
209
|
+
if not path.is_file():
|
|
210
|
+
_fail("user", path, "$", "attached clarification response is missing")
|
|
211
|
+
return {"sourceFile": _project_relative(project_root, path)}
|
|
212
|
+
|
|
213
|
+
|
|
93
214
|
def _clarification_row(
|
|
94
215
|
source: Mapping[str, Any], activities: Mapping[object, Mapping[str, Any]], path: Path,
|
|
95
216
|
) -> dict[str, Any]:
|
|
@@ -148,10 +269,12 @@ def _attach_plan_backlinks(data: dict[str, Any], activities: list[dict[str, Any]
|
|
|
148
269
|
|
|
149
270
|
|
|
150
271
|
def _compose(
|
|
151
|
-
project_root: Path,
|
|
272
|
+
project_root: Path,
|
|
273
|
+
manifest_path: Path,
|
|
274
|
+
manifest: Mapping[str, Any],
|
|
275
|
+
inputs: Mapping[str, ReportInputPath],
|
|
276
|
+
schema: Mapping[str, Any],
|
|
152
277
|
) -> dict[str, Any]:
|
|
153
|
-
schema = load_schema_version("3.0")
|
|
154
|
-
inputs = _input_map(project_root, manifest)
|
|
155
278
|
narrative = _narrative(inputs["narrative"], schema)
|
|
156
279
|
ledger = _read_json(inputs["approval-decisions"])
|
|
157
280
|
team_state = _read_json(inputs["execution-status"])
|
|
@@ -175,6 +298,9 @@ def _compose(
|
|
|
175
298
|
data["tokenUsage"] = usage
|
|
176
299
|
data["crossVerification"] = convergence_data["crossVerification"]
|
|
177
300
|
data["clarificationItems"] = _clarifications(ledger, activities, inputs["approval-decisions"].path)
|
|
301
|
+
carry_in = _clarification_carry_in(project_root, manifest)
|
|
302
|
+
if carry_in is not None:
|
|
303
|
+
data["clarificationCarryIn"] = carry_in
|
|
178
304
|
data["agentActivity"] = activities
|
|
179
305
|
data["evidence"] = {"primary": _promoted_evidence(convergence_data)}
|
|
180
306
|
data["missingInformation"] = []
|
|
@@ -317,11 +443,11 @@ def _publish(
|
|
|
317
443
|
target: Path, data: Mapping[str, Any], replace: Callable[[str, str], None],
|
|
318
444
|
) -> None:
|
|
319
445
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
446
|
+
serialized = serialize_owned_object(target, data, artifact="final report record")
|
|
320
447
|
descriptor, name = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
|
|
321
448
|
try:
|
|
322
449
|
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
323
|
-
|
|
324
|
-
handle.write("\n")
|
|
450
|
+
handle.write(serialized)
|
|
325
451
|
handle.flush()
|
|
326
452
|
os.fsync(handle.fileno())
|
|
327
453
|
replace(name, str(target))
|
|
@@ -339,12 +465,41 @@ def assemble_report(
|
|
|
339
465
|
manifest = _read_json(manifest_row)
|
|
340
466
|
if not uses_report_contract_v3(manifest):
|
|
341
467
|
_fail("orchestrator", manifest_path, "reportContractVersion", "requires 3.0")
|
|
342
|
-
|
|
343
|
-
|
|
468
|
+
schema = load_schema_version("3.0")
|
|
469
|
+
inputs = _input_map(project_root, manifest)
|
|
470
|
+
input_issues = list(_input_preflight_issues(inputs, schema))
|
|
471
|
+
packet_data_path, _ = report_synthesis_packet_paths(inputs["narrative"].path)
|
|
472
|
+
if packet_data_path.is_file():
|
|
473
|
+
input_issues.extend(
|
|
474
|
+
AssemblyIssue(
|
|
475
|
+
issue.owner,
|
|
476
|
+
str(issue.path),
|
|
477
|
+
f"sources.{issue.label}",
|
|
478
|
+
issue.reason,
|
|
479
|
+
)
|
|
480
|
+
for issue in verify_report_synthesis_packet_sources(
|
|
481
|
+
project_root,
|
|
482
|
+
packet_data_path,
|
|
483
|
+
)
|
|
484
|
+
)
|
|
485
|
+
if input_issues:
|
|
486
|
+
raise ReportAssemblyError(tuple(input_issues))
|
|
487
|
+
data = _compose(project_root, manifest_path, manifest, inputs, schema)
|
|
488
|
+
errors = validate(data, schema)
|
|
344
489
|
if errors:
|
|
345
490
|
target = Path(str(manifest.get("expectedReportRecordPath") or "data.json"))
|
|
346
491
|
target = target if target.is_absolute() else project_root / target
|
|
347
|
-
|
|
492
|
+
raise ReportAssemblyError(
|
|
493
|
+
tuple(
|
|
494
|
+
AssemblyIssue(
|
|
495
|
+
_schema_owner(error),
|
|
496
|
+
str(target),
|
|
497
|
+
error.split(" ", 1)[0],
|
|
498
|
+
error,
|
|
499
|
+
)
|
|
500
|
+
for error in errors
|
|
501
|
+
)
|
|
502
|
+
)
|
|
348
503
|
value = manifest.get("expectedReportRecordPath")
|
|
349
504
|
if not isinstance(value, str) or not value:
|
|
350
505
|
_fail("orchestrator", manifest_path, "expectedReportRecordPath", "required")
|