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
|
@@ -13,12 +13,14 @@ CLI 는 갈라 읽어야 한다.
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
|
-
from dataclasses import dataclass
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Any, Callable, Literal, Mapping, Protocol, runtime_checkable
|
|
19
19
|
|
|
20
20
|
from .worker_stream import (
|
|
21
21
|
Normalise,
|
|
22
|
+
StreamEvent,
|
|
23
|
+
Text,
|
|
22
24
|
final_text,
|
|
23
25
|
format_live,
|
|
24
26
|
format_log,
|
|
@@ -33,16 +35,22 @@ WORKER = "worker"
|
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
class TranscriptWriter(Protocol):
|
|
36
|
-
"""세션 기록에
|
|
38
|
+
"""세션 기록에 남긴다. 화면 출력도 이 구현이 함께 맡는다."""
|
|
37
39
|
|
|
38
40
|
def write(self, speaker: str, line: str) -> None: ...
|
|
39
41
|
|
|
42
|
+
def write_event(
|
|
43
|
+
self, speaker: str, *, screen: list[str], archive: list[str]
|
|
44
|
+
) -> None: ...
|
|
45
|
+
|
|
40
46
|
|
|
41
47
|
@runtime_checkable
|
|
42
48
|
class Presentation(Protocol):
|
|
43
49
|
def merges_stderr(self) -> bool: ...
|
|
44
50
|
|
|
45
|
-
def sinks(self, writer: TranscriptWriter
|
|
51
|
+
def sinks(self, writer: TranscriptWriter) -> tuple[SinkSpec, ...]: ...
|
|
52
|
+
|
|
53
|
+
def flush(self, writer: TranscriptWriter) -> None: ...
|
|
46
54
|
|
|
47
55
|
|
|
48
56
|
@dataclass(frozen=True)
|
|
@@ -58,13 +66,16 @@ class MergedText:
|
|
|
58
66
|
def merges_stderr(self) -> bool:
|
|
59
67
|
return True
|
|
60
68
|
|
|
61
|
-
def sinks(self, writer: TranscriptWriter
|
|
69
|
+
def sinks(self, writer: TranscriptWriter) -> tuple[SinkSpec, ...]:
|
|
62
70
|
def emit(line: str) -> str | None:
|
|
63
71
|
writer.write(WORKER, line)
|
|
64
72
|
return line
|
|
65
73
|
|
|
66
74
|
return (("stdout", emit),)
|
|
67
75
|
|
|
76
|
+
def flush(self, writer: TranscriptWriter) -> None:
|
|
77
|
+
return
|
|
78
|
+
|
|
68
79
|
|
|
69
80
|
@dataclass(frozen=True)
|
|
70
81
|
class SplitText:
|
|
@@ -77,7 +88,7 @@ class SplitText:
|
|
|
77
88
|
def merges_stderr(self) -> bool:
|
|
78
89
|
return False
|
|
79
90
|
|
|
80
|
-
def sinks(self, writer: TranscriptWriter
|
|
91
|
+
def sinks(self, writer: TranscriptWriter) -> tuple[SinkSpec, ...]:
|
|
81
92
|
def result(line: str) -> str | None:
|
|
82
93
|
writer.write(WORKER, line)
|
|
83
94
|
return line
|
|
@@ -88,6 +99,9 @@ class SplitText:
|
|
|
88
99
|
|
|
89
100
|
return (("stdout", result), ("stderr", progress))
|
|
90
101
|
|
|
102
|
+
def flush(self, writer: TranscriptWriter) -> None:
|
|
103
|
+
return
|
|
104
|
+
|
|
91
105
|
|
|
92
106
|
@dataclass(frozen=True)
|
|
93
107
|
class JsonEvents:
|
|
@@ -99,11 +113,16 @@ class JsonEvents:
|
|
|
99
113
|
|
|
100
114
|
normalise: Normalise
|
|
101
115
|
observe: ObserveServedModel
|
|
116
|
+
# 어떤 CLI 의 streaming-json 은 문장이 아니라 토큰을 한 줄씩 보낸다. 붙이지
|
|
117
|
+
# 않으면 pane 이 한 글자 한 줄이 된다. 기본은 끄고, 그런 CLI 의 어댑터가 켠다 —
|
|
118
|
+
# 어느 CLI 가 그런지는 이 계층이 알 필요도, 알아서도 안 되는 사실이다.
|
|
119
|
+
join_adjacent_text: bool = False
|
|
120
|
+
_pending_text: list[str] = field(default_factory=list, repr=False, compare=False)
|
|
102
121
|
|
|
103
122
|
def merges_stderr(self) -> bool:
|
|
104
123
|
return True
|
|
105
124
|
|
|
106
|
-
def sinks(self, writer: TranscriptWriter
|
|
125
|
+
def sinks(self, writer: TranscriptWriter) -> tuple[SinkSpec, ...]:
|
|
107
126
|
def emit(line: str) -> str | None:
|
|
108
127
|
stripped = line.strip()
|
|
109
128
|
if not stripped:
|
|
@@ -113,6 +132,7 @@ class JsonEvents:
|
|
|
113
132
|
except ValueError:
|
|
114
133
|
# 이벤트가 아니다. CLI 자신의 오류 텍스트가 이 스트림으로
|
|
115
134
|
# 오므로 삼키면 아무도 못 본다.
|
|
135
|
+
self._publish_text(writer)
|
|
116
136
|
writer.write(WORKER, stripped)
|
|
117
137
|
return None
|
|
118
138
|
if not isinstance(event, dict):
|
|
@@ -120,9 +140,50 @@ class JsonEvents:
|
|
|
120
140
|
self.observe(event)
|
|
121
141
|
closing: str | None = None
|
|
122
142
|
for entry in self.normalise(event):
|
|
123
|
-
|
|
124
|
-
writer.write(WORKER, row)
|
|
125
|
-
closing = final_text(entry) or closing
|
|
143
|
+
closing = self._write_entry(writer, entry) or closing
|
|
126
144
|
return closing
|
|
127
145
|
|
|
128
146
|
return (("stdout", emit),)
|
|
147
|
+
|
|
148
|
+
def flush(self, writer: TranscriptWriter) -> None:
|
|
149
|
+
self._publish_text(writer)
|
|
150
|
+
|
|
151
|
+
def _write_entry(
|
|
152
|
+
self, writer: TranscriptWriter, entry: StreamEvent
|
|
153
|
+
) -> str | None:
|
|
154
|
+
if isinstance(entry, Text) and self.join_adjacent_text:
|
|
155
|
+
self._pending_text.append(entry.body)
|
|
156
|
+
self._publish_complete_lines(writer)
|
|
157
|
+
return None
|
|
158
|
+
self._publish_text(writer)
|
|
159
|
+
# 두 투영을 고르는 것이 아니라 둘 다 보낸다. 화면은 요약, 기록은
|
|
160
|
+
# 본문까지. 고르던 동안에는 pane 이 붙은 run 의 기록에서 도구 결과
|
|
161
|
+
# 본문이 통째로 사라졌다.
|
|
162
|
+
writer.write_event(
|
|
163
|
+
WORKER,
|
|
164
|
+
screen=format_live(entry),
|
|
165
|
+
archive=format_log(entry),
|
|
166
|
+
)
|
|
167
|
+
return final_text(entry)
|
|
168
|
+
|
|
169
|
+
def _publish_complete_lines(self, writer: TranscriptWriter) -> None:
|
|
170
|
+
combined = "".join(self._pending_text)
|
|
171
|
+
if "\n" not in combined:
|
|
172
|
+
return
|
|
173
|
+
*complete, remainder = combined.split("\n")
|
|
174
|
+
self._pending_text.clear()
|
|
175
|
+
if remainder:
|
|
176
|
+
self._pending_text.append(remainder)
|
|
177
|
+
self._write_text_rows(writer, "\n".join(complete))
|
|
178
|
+
|
|
179
|
+
def _publish_text(self, writer: TranscriptWriter) -> None:
|
|
180
|
+
combined = "".join(self._pending_text)
|
|
181
|
+
self._pending_text.clear()
|
|
182
|
+
self._write_text_rows(writer, combined)
|
|
183
|
+
|
|
184
|
+
def _write_text_rows(self, writer: TranscriptWriter, body: str) -> None:
|
|
185
|
+
event = Text(body=body)
|
|
186
|
+
screen = format_live(event)
|
|
187
|
+
archive = format_log(event)
|
|
188
|
+
if screen or archive:
|
|
189
|
+
writer.write_event(WORKER, screen=screen, archive=archive)
|
|
@@ -16,6 +16,7 @@ from ..agent_invocation import (
|
|
|
16
16
|
verify_agent_invocation,
|
|
17
17
|
)
|
|
18
18
|
from ..dispatch_state import DispatchError, record_verified_agent_dispatch
|
|
19
|
+
from ..json_boundary import JsonBoundaryError, load_owned_object
|
|
19
20
|
|
|
20
21
|
from ..application.start_run import start_run
|
|
21
22
|
from ..application.resume_run import resume_run
|
|
@@ -108,7 +109,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
108
109
|
resolve = commands.add_parser("resolve")
|
|
109
110
|
resolve.add_argument("--requested", required=True)
|
|
110
111
|
resolve.add_argument("--command", dest="runtime_command", required=True)
|
|
111
|
-
resolve.add_argument("--
|
|
112
|
+
resolve.add_argument("--environment-host", default="")
|
|
113
|
+
resolve.add_argument(
|
|
114
|
+
"--capability",
|
|
115
|
+
action="append",
|
|
116
|
+
choices=("claude-skill-handoff", "tmux"),
|
|
117
|
+
default=[],
|
|
118
|
+
)
|
|
112
119
|
launch = commands.add_parser("launch-plan")
|
|
113
120
|
_add_session_plan_args(launch)
|
|
114
121
|
launch.add_argument("--prepared-launch-json", default="")
|
|
@@ -152,7 +159,9 @@ def _command_payload(
|
|
|
152
159
|
if args.command == "catalog":
|
|
153
160
|
return catalog_payload(registry)
|
|
154
161
|
if args.command == "resolve":
|
|
155
|
-
context =
|
|
162
|
+
context = HostResolutionContext(
|
|
163
|
+
args.environment_host, frozenset(args.capability)
|
|
164
|
+
)
|
|
156
165
|
try:
|
|
157
166
|
return resolve_payload(registry, args.requested, context)
|
|
158
167
|
except HostUnavailable as exc:
|
|
@@ -215,31 +224,6 @@ def _probe_paths(project_root: str, home_dir: str):
|
|
|
215
224
|
os.environ[key] = value
|
|
216
225
|
|
|
217
226
|
|
|
218
|
-
def _parse_resolution_context(raw_context: str) -> HostResolutionContext:
|
|
219
|
-
try:
|
|
220
|
-
raw = json.loads(raw_context)
|
|
221
|
-
except json.JSONDecodeError as exc:
|
|
222
|
-
raise ValueError("invalid host resolution context JSON") from exc
|
|
223
|
-
if not isinstance(raw, dict):
|
|
224
|
-
raise ValueError("invalid host resolution context JSON")
|
|
225
|
-
env = raw.get("env", {})
|
|
226
|
-
capabilities = raw.get("capabilities", {})
|
|
227
|
-
if not isinstance(env, dict) or not isinstance(capabilities, dict):
|
|
228
|
-
raise ValueError("invalid host resolution context JSON")
|
|
229
|
-
explicit_env_host = env.get("OKSTRA_RUNTIME_HOST", "")
|
|
230
|
-
if not isinstance(explicit_env_host, str):
|
|
231
|
-
raise ValueError("invalid host resolution context JSON")
|
|
232
|
-
signals = frozenset(
|
|
233
|
-
signal
|
|
234
|
-
for capability, signal in (
|
|
235
|
-
("claudeSkillHandoff", "claude-skill-handoff"),
|
|
236
|
-
("tmuxAvailable", "tmux"),
|
|
237
|
-
)
|
|
238
|
-
if capabilities.get(capability) is True
|
|
239
|
-
)
|
|
240
|
-
return HostResolutionContext(explicit_env_host, signals)
|
|
241
|
-
|
|
242
|
-
|
|
243
227
|
def _unclaimed_host_message(registry: HostAdapterRegistry, command: str) -> str:
|
|
244
228
|
runtime_flag = "--runtime" if command in {
|
|
245
229
|
"install", "ensure-installed", "doctor"
|
|
@@ -336,17 +320,19 @@ def _launch_request(
|
|
|
336
320
|
run_manifest_path = Path(run_manifest_value)
|
|
337
321
|
metadata_path = Path(metadata_value)
|
|
338
322
|
try:
|
|
339
|
-
manifest =
|
|
323
|
+
manifest = load_owned_object(run_manifest_path, artifact="run manifest")
|
|
340
324
|
resources = manifest["resources"]
|
|
341
325
|
assignment = agent_model_assignment_from_payload(
|
|
342
326
|
manifest["invocationAssignments"]["lead"]
|
|
343
327
|
)
|
|
344
328
|
prompt_path = root / resources["leadExecutionPromptPath"]
|
|
345
329
|
canonical_metadata = root / resources["leadPromptMetadataPath"]
|
|
346
|
-
metadata =
|
|
330
|
+
metadata = load_owned_object(
|
|
331
|
+
canonical_metadata, artifact="lead prompt metadata"
|
|
332
|
+
)
|
|
347
333
|
if metadata["prompt"]["path"] != resources["leadExecutionPromptPath"]:
|
|
348
334
|
raise KeyError("lead prompt resource does not match metadata")
|
|
349
|
-
except (OSError,
|
|
335
|
+
except (OSError, JsonBoundaryError, KeyError, TypeError, AgentInvocationError) as exc:
|
|
350
336
|
raise ValueError("lead invocation authority is incomplete") from exc
|
|
351
337
|
if metadata_path.resolve(strict=False) != canonical_metadata.resolve(strict=False):
|
|
352
338
|
raise ValueError("prepared lead metadata does not match run manifest")
|
|
@@ -15,6 +15,7 @@ import os
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
17
|
from .models import provider_ids
|
|
18
|
+
from .json_boundary import external_error_sidecar_source, load_external_json
|
|
18
19
|
|
|
19
20
|
STDERR_EXCERPT_MAX_BYTES = 2048
|
|
20
21
|
TRUNCATION_SUFFIX = "...[truncated]"
|
|
@@ -240,6 +241,31 @@ def append_observed(
|
|
|
240
241
|
return rec
|
|
241
242
|
|
|
242
243
|
|
|
244
|
+
def _worker_sidecar_record(entry, *, recorded_at, task_key, agent, agent_role, model, identity):
|
|
245
|
+
error_type = entry.get("errorType")
|
|
246
|
+
if error_type not in ALLOWED_ERROR_TYPES:
|
|
247
|
+
raise ValueError(f"invalid errorType in sidecar: {error_type!r}")
|
|
248
|
+
rec = {
|
|
249
|
+
"ts": entry.get("ts"), "recordedAt": recorded_at,
|
|
250
|
+
"taskKey": task_key,
|
|
251
|
+
"phase": str(entry.get("phase")) if entry.get("phase") is not None else None,
|
|
252
|
+
"agent": agent, "agentRole": agent_role, "model": model,
|
|
253
|
+
"source": "worker-reported", "errorType": error_type,
|
|
254
|
+
"command": entry.get("command"), "commandKind": entry.get("commandKind"),
|
|
255
|
+
"exitCode": entry.get("exitCode"), "durationMs": entry.get("durationMs"),
|
|
256
|
+
"message": entry.get("message"),
|
|
257
|
+
"stderrExcerpt": truncate_stderr(entry.get("stderrExcerpt")),
|
|
258
|
+
"context": normalize_cause_context(
|
|
259
|
+
entry.get("context"), message=entry.get("message")
|
|
260
|
+
),
|
|
261
|
+
}
|
|
262
|
+
from okstra_ctl.execution_identity import stored_identity
|
|
263
|
+
|
|
264
|
+
rec.update(stored_identity(identity))
|
|
265
|
+
rec.update(stored_identity(entry))
|
|
266
|
+
return rec
|
|
267
|
+
|
|
268
|
+
|
|
243
269
|
def dump_from_worker_sidecar(
|
|
244
270
|
*,
|
|
245
271
|
sidecar_path,
|
|
@@ -276,42 +302,21 @@ def dump_from_worker_sidecar(
|
|
|
276
302
|
p = Path(sidecar_path)
|
|
277
303
|
if not p.exists():
|
|
278
304
|
return 0
|
|
279
|
-
|
|
305
|
+
# 외부 입력: 레거시 자동화가 제공하는 오류 sidecar 호환 경로다.
|
|
306
|
+
payload = load_external_json(
|
|
307
|
+
external_error_sidecar_source(p),
|
|
308
|
+
artifact="legacy error sidecar",
|
|
309
|
+
)
|
|
280
310
|
schema = payload.get("schemaVersion")
|
|
281
311
|
if schema not in SUPPORTED_SIDECAR_SCHEMA_VERSIONS:
|
|
282
312
|
raise ValueError(f"unsupported sidecar schemaVersion: {schema!r}")
|
|
283
|
-
entries = payload.get("errors") or []
|
|
284
313
|
recorded_at = _iso(now or _now_utc())
|
|
285
314
|
count = 0
|
|
286
|
-
for
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
entry_context = normalize_cause_context(
|
|
291
|
-
e.get("context"), message=e.get("message")
|
|
315
|
+
for entry in payload.get("errors") or []:
|
|
316
|
+
rec = _worker_sidecar_record(
|
|
317
|
+
entry, recorded_at=recorded_at, task_key=task_key, agent=agent,
|
|
318
|
+
agent_role=agent_role, model=model, identity=identity,
|
|
292
319
|
)
|
|
293
|
-
rec = {
|
|
294
|
-
"ts": e.get("ts"),
|
|
295
|
-
"recordedAt": recorded_at,
|
|
296
|
-
"taskKey": task_key,
|
|
297
|
-
"phase": str(e.get("phase")) if e.get("phase") is not None else None,
|
|
298
|
-
"agent": agent,
|
|
299
|
-
"agentRole": agent_role,
|
|
300
|
-
"model": model,
|
|
301
|
-
"source": "worker-reported",
|
|
302
|
-
"errorType": et,
|
|
303
|
-
"command": e.get("command"),
|
|
304
|
-
"commandKind": e.get("commandKind"),
|
|
305
|
-
"exitCode": e.get("exitCode"),
|
|
306
|
-
"durationMs": e.get("durationMs"),
|
|
307
|
-
"message": e.get("message"),
|
|
308
|
-
"stderrExcerpt": truncate_stderr(e.get("stderrExcerpt")),
|
|
309
|
-
"context": entry_context,
|
|
310
|
-
}
|
|
311
|
-
from okstra_ctl.execution_identity import stored_identity
|
|
312
|
-
|
|
313
|
-
rec.update(stored_identity(identity))
|
|
314
|
-
rec.update(stored_identity(e))
|
|
315
320
|
append_jsonl_line(out_path, rec)
|
|
316
321
|
count += 1
|
|
317
322
|
return count
|
|
@@ -12,6 +12,8 @@ import json
|
|
|
12
12
|
import sys
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
|
+
from .fixed_text import line
|
|
16
|
+
|
|
15
17
|
from okstra_ctl.task_target import resolve_task_root, project_rel
|
|
16
18
|
from okstra_project import read_task_key
|
|
17
19
|
from okstra_ctl.error_log_core import (
|
|
@@ -187,6 +189,27 @@ def build_and_write(task_root: Path, project_root: Path, now: dt.datetime) -> di
|
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
|
|
192
|
+
def render_result_text(result: dict) -> str:
|
|
193
|
+
totals = result.get("totals") if isinstance(result.get("totals"), dict) else {}
|
|
194
|
+
parts = ["# Okstra Error Report Input\n\n", line("Task key", result.get("taskKey")), line("Report path", result.get("reportPath")), line("Total errors", totals.get("errorCount")), line("Run count", totals.get("runCount")), line("Parse skipped", result.get("parseSkipped"))]
|
|
195
|
+
for key, label in (("byErrorType", "Error type"), ("bySource", "Source")):
|
|
196
|
+
values = totals.get(key)
|
|
197
|
+
if isinstance(values, dict):
|
|
198
|
+
for name, count in sorted(values.items()):
|
|
199
|
+
parts.append(line(f"{label} {name}", count))
|
|
200
|
+
for key, label in (("byPhase", "Phase"), ("byAgent", "Agent")):
|
|
201
|
+
values = result.get(key)
|
|
202
|
+
if isinstance(values, dict):
|
|
203
|
+
for name, count in sorted(values.items()):
|
|
204
|
+
parts.append(line(f"{label} {name}", count))
|
|
205
|
+
elif isinstance(values, list):
|
|
206
|
+
identity = "phase" if key == "byPhase" else "agent"
|
|
207
|
+
for row in values:
|
|
208
|
+
if isinstance(row, dict):
|
|
209
|
+
parts.append(line(f"{label} {row.get(identity)}", row.get("count")))
|
|
210
|
+
return "".join(parts)
|
|
211
|
+
|
|
212
|
+
|
|
190
213
|
def main(argv: list[str] | None = None) -> int:
|
|
191
214
|
parser = argparse.ArgumentParser(
|
|
192
215
|
description="Aggregate okstra-run error logs into a task-level report."
|
|
@@ -194,11 +217,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
194
217
|
parser.add_argument("target", help="task root path or task-key")
|
|
195
218
|
parser.add_argument("--project-root", default="", help="project root for task-key lookup")
|
|
196
219
|
parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
|
|
220
|
+
parser.add_argument("--text", action="store_true", help="emit fixed text fields")
|
|
197
221
|
args = parser.parse_args(argv)
|
|
198
222
|
|
|
199
223
|
task_root, project_root = resolve_task_root(args.target, args.project_root, args.cwd)
|
|
200
224
|
result = build_and_write(task_root, project_root, dt.datetime.now(dt.timezone.utc))
|
|
201
|
-
|
|
225
|
+
output = render_result_text(result) if args.text else json.dumps(result, ensure_ascii=False, indent=2)
|
|
226
|
+
print(output, end="" if args.text else "\n")
|
|
202
227
|
return 0
|
|
203
228
|
|
|
204
229
|
|
|
@@ -13,10 +13,13 @@ import sys
|
|
|
13
13
|
import zipfile
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
|
+
from .fixed_text import line
|
|
17
|
+
|
|
16
18
|
from okstra_ctl.error_log_core import aggregate, parse_records
|
|
17
19
|
from okstra_ctl.jsonl import read_jsonl
|
|
18
20
|
from okstra_ctl.locks import central_lock
|
|
19
21
|
from okstra_ctl.paths import okstra_home, resolve_under_root
|
|
22
|
+
from okstra_ctl.json_boundary import load_owned_object, write_owned_object_atomic
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def run_dirs(home: Path) -> list[tuple[str, Path, dict]]:
|
|
@@ -262,9 +265,10 @@ _CONFIG_NAME = "error-zip.json"
|
|
|
262
265
|
|
|
263
266
|
def remember_output_path(home: Path, out_path: Path) -> None:
|
|
264
267
|
cfg = home / _CONFIG_NAME
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
+
write_owned_object_atomic(
|
|
269
|
+
cfg,
|
|
270
|
+
{"lastOutputPath": str(out_path)},
|
|
271
|
+
artifact="error zip configuration",
|
|
268
272
|
)
|
|
269
273
|
|
|
270
274
|
|
|
@@ -273,7 +277,11 @@ def last_output_path(home: Path) -> str:
|
|
|
273
277
|
if not cfg.is_file():
|
|
274
278
|
return ""
|
|
275
279
|
try:
|
|
276
|
-
return str(
|
|
280
|
+
return str(
|
|
281
|
+
load_owned_object(cfg, artifact="error zip configuration").get(
|
|
282
|
+
"lastOutputPath", ""
|
|
283
|
+
)
|
|
284
|
+
)
|
|
277
285
|
except Exception:
|
|
278
286
|
return ""
|
|
279
287
|
|
|
@@ -315,16 +323,30 @@ def build_zip(home: Path, out_path: Path, now: dt.datetime) -> dict:
|
|
|
315
323
|
}
|
|
316
324
|
|
|
317
325
|
|
|
326
|
+
def render_result_text(result: dict) -> str:
|
|
327
|
+
labels = (
|
|
328
|
+
("outPath", "Output zip"), ("errorCount", "Total errors"),
|
|
329
|
+
("runCount", "Run count"), ("unreachableRuns", "Unreachable runs"),
|
|
330
|
+
("clusterCount", "Cluster count"), ("projectCount", "Project count"),
|
|
331
|
+
)
|
|
332
|
+
lines = ["# Okstra Error Zip Result\n\n"]
|
|
333
|
+
for key, label in labels:
|
|
334
|
+
lines.append(line(label, result.get(key)))
|
|
335
|
+
return "".join(lines)
|
|
336
|
+
|
|
337
|
+
|
|
318
338
|
def main(argv: list[str] | None = None) -> int:
|
|
319
339
|
parser = argparse.ArgumentParser(
|
|
320
340
|
prog="okstra error-zip",
|
|
321
341
|
description="cross-project okstra 에러를 수집·익명화해 zip 으로 묶는다.",
|
|
322
342
|
)
|
|
323
343
|
parser.add_argument("--out", required=True, help="출력 .zip 절대/상대 경로")
|
|
344
|
+
parser.add_argument("--text", action="store_true", help="emit fixed text fields")
|
|
324
345
|
args = parser.parse_args(argv)
|
|
325
346
|
home = okstra_home()
|
|
326
347
|
result = build_zip(home, Path(args.out), dt.datetime.now(dt.timezone.utc))
|
|
327
|
-
|
|
348
|
+
output = render_result_text(result) if args.text else json.dumps(result, ensure_ascii=False, indent=2)
|
|
349
|
+
print(output, end="" if args.text else "\n")
|
|
328
350
|
return 0
|
|
329
351
|
|
|
330
352
|
|
|
@@ -10,6 +10,7 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
from .domain.provider import HostModelBinding, ServedModelAttestation
|
|
12
12
|
from .domain.worker_exec import WorkerWriteCapability
|
|
13
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
V2_SCHEMA_VERSION = "2.0"
|
|
@@ -479,8 +480,8 @@ def _capability_payload(
|
|
|
479
480
|
|
|
480
481
|
def _read_json_object(path: Path) -> dict[str, Any]:
|
|
481
482
|
try:
|
|
482
|
-
payload =
|
|
483
|
-
except
|
|
483
|
+
payload = load_owned_object(path, artifact="execution identity")
|
|
484
|
+
except JsonBoundaryError as exc:
|
|
484
485
|
raise ExecutionManifestError(f"cannot read execution identity: {path}") from exc
|
|
485
486
|
if not isinstance(payload, dict):
|
|
486
487
|
raise ExecutionManifestError("execution identity surface must be an object")
|
|
@@ -27,6 +27,7 @@ from .execution_identity import (
|
|
|
27
27
|
validate_task_role_execution_index,
|
|
28
28
|
)
|
|
29
29
|
from .final_report_schema import SchemaError, load_schema, validate as validate_schema
|
|
30
|
+
from .json_boundary import JsonBoundaryError, load_owned_object, serialize_owned_object
|
|
30
31
|
from .run_context import task_mutex
|
|
31
32
|
from .write_policy import (
|
|
32
33
|
WritePolicyError,
|
|
@@ -1257,8 +1258,8 @@ def _required_text(row: Mapping[str, Any], key: str) -> str:
|
|
|
1257
1258
|
|
|
1258
1259
|
def _read_json_object(path: Path) -> dict[str, Any]:
|
|
1259
1260
|
try:
|
|
1260
|
-
payload =
|
|
1261
|
-
except (OSError,
|
|
1261
|
+
payload = load_owned_object(path, artifact="execution manifest")
|
|
1262
|
+
except (OSError, JsonBoundaryError) as exc:
|
|
1262
1263
|
raise ExecutionManifestError(f"cannot read JSON object: {path}") from exc
|
|
1263
1264
|
if not isinstance(payload, dict):
|
|
1264
1265
|
raise ExecutionManifestError(f"JSON surface must be an object: {path}")
|
|
@@ -1272,14 +1273,16 @@ def _write_json_atomic(
|
|
|
1272
1273
|
create_only: bool = False,
|
|
1273
1274
|
) -> None:
|
|
1274
1275
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1276
|
+
serialized = serialize_owned_object(
|
|
1277
|
+
path, payload, artifact="execution manifest"
|
|
1278
|
+
)
|
|
1275
1279
|
descriptor, temporary_name = tempfile.mkstemp(
|
|
1276
1280
|
prefix=f".{path.name}.", suffix=".tmp", dir=path.parent
|
|
1277
1281
|
)
|
|
1278
1282
|
temporary = Path(temporary_name)
|
|
1279
1283
|
try:
|
|
1280
1284
|
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
|
1281
|
-
|
|
1282
|
-
stream.write("\n")
|
|
1285
|
+
stream.write(serialized)
|
|
1283
1286
|
stream.flush()
|
|
1284
1287
|
os.fsync(stream.fileno())
|
|
1285
1288
|
if create_only:
|
|
@@ -32,6 +32,10 @@ class MutationSnapshot:
|
|
|
32
32
|
scratch_digests: Mapping[str, str]
|
|
33
33
|
git_projection: Mapping[str, Any]
|
|
34
34
|
digest: str
|
|
35
|
+
# 오케스트레이터가 쓰는 경로. 파일 하나를 싣거나 디렉터리를 실어 그 트리
|
|
36
|
+
# 전체를 덮을 수 있다 — 한 배치가 무엇을 쓸지 파일 단위로 미리 셀 수 없기
|
|
37
|
+
# 때문이다(다른 라운드의 재시도 프롬프트, 로그, 상태 사이드카가 그 창에
|
|
38
|
+
# 들어온다).
|
|
35
39
|
orchestrator_paths: tuple[str, ...] = ()
|
|
36
40
|
|
|
37
41
|
def to_payload(self) -> dict[str, Any]:
|
|
@@ -445,7 +449,7 @@ def _source_changes(
|
|
|
445
449
|
snapshot.artifact_root == snapshot.root
|
|
446
450
|
and any(_is_within(path, item) for item in artifacts)
|
|
447
451
|
)
|
|
448
|
-
and path
|
|
452
|
+
and not any(_is_within(path, item) for item in orchestrator)
|
|
449
453
|
and not _excluded(Path(path), generated)
|
|
450
454
|
}
|
|
451
455
|
|
|
@@ -464,7 +468,7 @@ def _artifact_policy_failures(
|
|
|
464
468
|
unauthorized = {
|
|
465
469
|
path for path in changed
|
|
466
470
|
if not any(_is_within(path, item) for item in allowed)
|
|
467
|
-
and path
|
|
471
|
+
and not any(_is_within(path, item) for item in orchestrator)
|
|
468
472
|
}
|
|
469
473
|
if snapshot.artifact_root == snapshot.root:
|
|
470
474
|
return []
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""Mini JSON Schema validator for
|
|
1
|
+
"""Mini JSON Schema validator for versioned final-report data.json records.
|
|
2
2
|
|
|
3
3
|
This is a deliberately narrow JSON Schema implementation that supports
|
|
4
4
|
exactly the keywords used by ``schemas/final-report-v2.0.schema.json``:
|
|
5
5
|
|
|
6
6
|
type, required, properties, additionalProperties, enum, const, pattern,
|
|
7
7
|
minLength, minItems, maxItems, uniqueItems, items, minimum, maximum, $ref ($defs),
|
|
8
|
-
oneOf, allOf, if/then/else, contains, not
|
|
8
|
+
oneOf, allOf, if/then/else, contains, minContains, maxContains, not
|
|
9
9
|
|
|
10
10
|
We do NOT depend on the ``jsonschema`` PyPI package because its
|
|
11
11
|
dependency tree (``referencing``, ``rpds-py``) includes a Rust C
|
|
@@ -13,8 +13,8 @@ extension which we would have to vendor or ship pre-built per platform.
|
|
|
13
13
|
The ~250 lines below cover everything the final-report schema needs;
|
|
14
14
|
strict spec compliance is not a goal — strict validation of OUR schema is.
|
|
15
15
|
|
|
16
|
-
Error messages include the JSON pointer of the failing field so
|
|
17
|
-
|
|
16
|
+
Error messages include the JSON pointer of the failing field so report
|
|
17
|
+
assembly can return the defect to the input owner.
|
|
18
18
|
"""
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
@@ -23,9 +23,6 @@ import re
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import Any
|
|
25
25
|
|
|
26
|
-
from okstra_ctl.report_contract import CURRENT_REPORT_SCHEMA_VERSION
|
|
27
|
-
|
|
28
|
-
|
|
29
26
|
class SchemaError(ValueError):
|
|
30
27
|
"""Raised when a schema itself is malformed (e.g. a $ref points
|
|
31
28
|
nowhere). Distinct from a data validation failure.
|
|
@@ -204,10 +201,20 @@ class _Validator:
|
|
|
204
201
|
for i, value in enumerate(instance):
|
|
205
202
|
self.validate(value, items, path + (i,))
|
|
206
203
|
contains = schema.get("contains")
|
|
207
|
-
if contains is not None
|
|
208
|
-
self._matches(value, contains) for value in instance
|
|
209
|
-
|
|
210
|
-
|
|
204
|
+
if contains is not None:
|
|
205
|
+
matches = sum(self._matches(value, contains) for value in instance)
|
|
206
|
+
minimum = schema.get("minContains", 1)
|
|
207
|
+
maximum = schema.get("maxContains")
|
|
208
|
+
if matches < minimum:
|
|
209
|
+
self._err(
|
|
210
|
+
path,
|
|
211
|
+
f"array contains {matches} matching item(s), below minContains {minimum}",
|
|
212
|
+
)
|
|
213
|
+
if maximum is not None and matches > maximum:
|
|
214
|
+
self._err(
|
|
215
|
+
path,
|
|
216
|
+
f"array contains {matches} matching item(s), above maxContains {maximum}",
|
|
217
|
+
)
|
|
211
218
|
|
|
212
219
|
def _validate_conditional(self, instance: Any, schema: dict, path: tuple[str | int, ...]) -> None:
|
|
213
220
|
if_schema = schema.get("if")
|
|
@@ -263,10 +270,11 @@ def validate(data: Any, schema: dict) -> list[str]:
|
|
|
263
270
|
|
|
264
271
|
SCHEMA_FILENAMES = {
|
|
265
272
|
"2.0": "final-report-v2.0.schema.json",
|
|
273
|
+
"3.0": "final-report-v3.0.schema.json",
|
|
266
274
|
}
|
|
267
275
|
|
|
268
276
|
|
|
269
|
-
def
|
|
277
|
+
def load_named_schema(filename: str, start: Path | None = None) -> dict:
|
|
270
278
|
here = (start or Path(__file__)).resolve()
|
|
271
279
|
if here.is_file():
|
|
272
280
|
here = here.parent
|
|
@@ -283,7 +291,7 @@ def load_schema_version(version: str, start: Path | None = None) -> dict:
|
|
|
283
291
|
filename = SCHEMA_FILENAMES[version]
|
|
284
292
|
except KeyError as exc:
|
|
285
293
|
raise SchemaError(f"unsupported final-report schemaVersion: {version}") from exc
|
|
286
|
-
return
|
|
294
|
+
return load_named_schema(filename, start=start)
|
|
287
295
|
|
|
288
296
|
|
|
289
297
|
def load_schema_for_data(data: dict, start: Path | None = None) -> dict:
|
|
@@ -295,10 +303,11 @@ def load_schema_for_data(data: dict, start: Path | None = None) -> dict:
|
|
|
295
303
|
|
|
296
304
|
|
|
297
305
|
def load_schema(schema_path: Path | None = None) -> dict:
|
|
298
|
-
"""Load the
|
|
299
|
-
|
|
300
|
-
|
|
306
|
+
"""Load the compatibility schema used by historical direct callers.
|
|
307
|
+
|
|
308
|
+
New write paths select contract 3.0 explicitly. Readers select from the
|
|
309
|
+
record's ``schemaVersion`` through :func:`load_schema_for_data`.
|
|
301
310
|
"""
|
|
302
311
|
if schema_path is not None:
|
|
303
312
|
return json.loads(Path(schema_path).read_text(encoding="utf-8"))
|
|
304
|
-
return load_schema_version(
|
|
313
|
+
return load_schema_version("2.0")
|
|
@@ -20,6 +20,7 @@ from pathlib import Path
|
|
|
20
20
|
from typing import Any, Dict, List, Optional
|
|
21
21
|
|
|
22
22
|
from .run_context import dir_flock
|
|
23
|
+
from .jsonl import append_jsonl
|
|
23
24
|
|
|
24
25
|
FIX_CYCLES_FILENAME = "fix-cycles.jsonl"
|
|
25
26
|
_LOCK_FILENAME = ".fix-cycles.lock"
|
|
@@ -122,8 +123,7 @@ def append_closed(task_root: Path, *, cycle: str, closed_by: str,
|
|
|
122
123
|
def _append_row(task_root: Path, record: Dict[str, Any]) -> None:
|
|
123
124
|
p = fix_cycles_path(task_root)
|
|
124
125
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
-
|
|
126
|
-
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
126
|
+
append_jsonl(p, record, ensure_ascii=False, compact=False)
|
|
127
127
|
|
|
128
128
|
|
|
129
129
|
def summarize(rows: List[Dict[str, Any]]) -> Dict[str, Any]:
|