okstra 0.179.2 → 0.183.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli-registry.mjs +30 -2
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/incremental-carry.mjs +9 -8
- package/dist/commands/execute/incremental-carry.mjs.map +1 -1
- package/dist/commands/execute/plan-verify.mjs +3 -1
- package/dist/commands/execute/plan-verify.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.d.mts +4 -2
- package/dist/commands/execute/render-bundle.mjs +46 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +11 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/inspect/model-io.d.mts +1 -0
- package/dist/commands/inspect/model-io.mjs +25 -0
- package/dist/commands/inspect/model-io.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.mjs +29 -8
- package/dist/commands/inspect/stage-map.mjs.map +1 -1
- package/dist/commands/inspect/task-list.mjs +52 -6
- package/dist/commands/inspect/task-list.mjs.map +1 -1
- package/dist/commands/inspect/user-response.mjs +14 -4
- package/dist/commands/inspect/user-response.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.d.mts +1 -0
- package/dist/commands/lifecycle/check-project.mjs +69 -50
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/contract-check.d.mts +1 -0
- package/dist/commands/lifecycle/contract-check.mjs +18 -0
- package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.mjs +154 -51
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/pr/pr.d.mts +1 -0
- package/dist/commands/pr/pr.mjs +19 -1
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/dist/commands/report/agent-activity.mjs +2 -2
- package/dist/commands/report/approval-decision.d.mts +1 -0
- package/dist/commands/report/approval-decision.mjs +21 -0
- package/dist/commands/report/approval-decision.mjs.map +1 -0
- package/dist/commands/report/design-snapshot.d.mts +1 -0
- package/dist/commands/report/design-snapshot.mjs +19 -0
- package/dist/commands/report/design-snapshot.mjs.map +1 -0
- package/dist/commands/report/translate.mjs +3 -0
- package/dist/commands/report/translate.mjs.map +1 -1
- package/dist/lib/host-registry-client.mjs +13 -9
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +21 -10
- package/docs/cli.md +36 -20
- package/docs/container.md +6 -4
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
- package/docs/for-ai/skills/okstra-code-review.md +4 -4
- package/docs/for-ai/skills/okstra-container-build.md +20 -17
- package/docs/for-ai/skills/okstra-inspect.md +20 -23
- package/docs/for-ai/skills/okstra-manager.md +19 -18
- package/docs/for-ai/skills/okstra-memory.md +2 -2
- package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
- package/docs/for-ai/skills/okstra-rollup.md +14 -13
- package/docs/for-ai/skills/okstra-run.md +7 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
- package/docs/for-ai/skills/okstra-setup.md +7 -7
- package/docs/for-ai/skills/okstra-usage.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +50 -32
- package/docs/project-structure-overview.md +43 -31
- package/docs/task-process/implementation-planning.md +2 -2
- package/docs/task-process/implementation.md +7 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/report-writer-worker.md +15 -164
- package/runtime/agents/workers/translator-worker.md +5 -13
- package/runtime/bin/okstra-error-log.py +51 -11
- package/runtime/bin/okstra-report-translate.py +210 -23
- package/runtime/prompts/host-orchestration/implementation.md +1 -1
- package/runtime/prompts/launch.template.md +9 -12
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/context-loader.md +41 -141
- package/runtime/prompts/lead/convergence.md +10 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +40 -49
- package/runtime/prompts/lead/plan-body-verification.md +217 -23
- package/runtime/prompts/lead/report-writer.md +81 -421
- package/runtime/prompts/lead/team-contract.md +9 -54
- package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/feature-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +20 -12
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/project-analysis.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +0 -1
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +8 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
- package/runtime/python/okstra_ctl/agent_activity.py +141 -38
- package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +70 -23
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +124 -3
- package/runtime/python/okstra_ctl/approval_decisions.py +328 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
- package/runtime/python/okstra_ctl/backfill.py +13 -10
- package/runtime/python/okstra_ctl/batch.py +2 -4
- package/runtime/python/okstra_ctl/build_tools.py +6 -3
- package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
- package/runtime/python/okstra_ctl/clarification_items.py +27 -13
- package/runtime/python/okstra_ctl/cmux.py +130 -52
- package/runtime/python/okstra_ctl/code_review_target.py +34 -8
- package/runtime/python/okstra_ctl/consumers.py +5 -4
- package/runtime/python/okstra_ctl/container.py +103 -8
- package/runtime/python/okstra_ctl/context_cost.py +2 -1
- package/runtime/python/okstra_ctl/contract_graph.py +497 -0
- package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
- package/runtime/python/okstra_ctl/convergence.py +338 -17
- package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
- package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
- package/runtime/python/okstra_ctl/convergence_store.py +55 -34
- package/runtime/python/okstra_ctl/design_prep.py +7 -4
- package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +97 -69
- package/runtime/python/okstra_ctl/dispatch_state.py +163 -63
- package/runtime/python/okstra_ctl/doctor.py +6 -3
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
- package/runtime/python/okstra_ctl/error_log_write.py +35 -30
- package/runtime/python/okstra_ctl/error_report.py +26 -1
- package/runtime/python/okstra_ctl/error_zip.py +27 -5
- package/runtime/python/okstra_ctl/execution_identity.py +3 -2
- package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +26 -17
- package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
- package/runtime/python/okstra_ctl/fixed_text.py +39 -0
- package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
- package/runtime/python/okstra_ctl/handoff.py +5 -4
- package/runtime/python/okstra_ctl/i18n.py +4 -2
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
- package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
- package/runtime/python/okstra_ctl/incremental_carry.py +130 -17
- package/runtime/python/okstra_ctl/incremental_scope.py +12 -4
- package/runtime/python/okstra_ctl/index.py +8 -11
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
- package/runtime/python/okstra_ctl/invocation.py +3 -6
- package/runtime/python/okstra_ctl/json_boundary.py +366 -0
- package/runtime/python/okstra_ctl/json_registry.py +10 -12
- package/runtime/python/okstra_ctl/jsonl.py +19 -2
- package/runtime/python/okstra_ctl/lead_events.py +33 -1
- package/runtime/python/okstra_ctl/listing.py +3 -3
- package/runtime/python/okstra_ctl/log_report.py +24 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -7
- package/runtime/python/okstra_ctl/manager_store.py +12 -10
- package/runtime/python/okstra_ctl/material.py +5 -1
- package/runtime/python/okstra_ctl/migrate.py +29 -25
- package/runtime/python/okstra_ctl/model_cli.py +3 -15
- package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
- package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
- package/runtime/python/okstra_ctl/path_hints.py +12 -0
- package/runtime/python/okstra_ctl/paths.py +21 -0
- package/runtime/python/okstra_ctl/plan_items.py +115 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +626 -34
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
- package/runtime/python/okstra_ctl/pr_template.py +3 -2
- package/runtime/python/okstra_ctl/project_meta.py +5 -7
- package/runtime/python/okstra_ctl/recap.py +5 -4
- package/runtime/python/okstra_ctl/reconcile.py +21 -27
- package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
- package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
- package/runtime/python/okstra_ctl/render.py +74 -16
- package/runtime/python/okstra_ctl/render_final_report.py +10 -8
- package/runtime/python/okstra_ctl/report_assembly.py +509 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -1
- package/runtime/python/okstra_ctl/report_finalize.py +67 -32
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_inputs.py +72 -0
- package/runtime/python/okstra_ctl/report_language.py +3 -2
- package/runtime/python/okstra_ctl/report_markdown.py +81 -8
- package/runtime/python/okstra_ctl/report_narrative.py +351 -0
- package/runtime/python/okstra_ctl/report_projections.py +265 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
- package/runtime/python/okstra_ctl/report_views.py +3 -2
- package/runtime/python/okstra_ctl/rollup.py +65 -4
- package/runtime/python/okstra_ctl/run.py +184 -65
- package/runtime/python/okstra_ctl/run_audit.py +3 -2
- package/runtime/python/okstra_ctl/run_context.py +6 -9
- package/runtime/python/okstra_ctl/run_index_row.py +2 -8
- package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +15 -8
- package/runtime/python/okstra_ctl/session_transcript.py +27 -1
- package/runtime/python/okstra_ctl/set_work_status.py +64 -38
- package/runtime/python/okstra_ctl/stage_fix_carry.py +8 -6
- package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
- package/runtime/python/okstra_ctl/stage_map.py +96 -28
- package/runtime/python/okstra_ctl/stage_targets.py +3 -4
- package/runtime/python/okstra_ctl/team.py +3 -2
- package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
- package/runtime/python/okstra_ctl/time_report.py +51 -4
- package/runtime/python/okstra_ctl/usage_identity.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +58 -4
- package/runtime/python/okstra_ctl/user_response.py +1431 -66
- package/runtime/python/okstra_ctl/wizard.py +11 -44
- package/runtime/python/okstra_ctl/work_categories.py +3 -2
- package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +49 -3
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
- package/runtime/python/okstra_ctl/worker_runner.py +14 -12
- package/runtime/python/okstra_ctl/workflow.py +2 -1
- package/runtime/python/okstra_ctl/worktree.py +3 -2
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
- package/runtime/python/okstra_ctl/write_policy.py +4 -2
- package/runtime/python/okstra_token_usage/antigravity.py +39 -12
- package/runtime/python/okstra_token_usage/collect.py +90 -38
- package/runtime/python/okstra_token_usage/grok.py +127 -0
- package/runtime/schemas/final-report-v2.0.schema.json +9 -0
- package/runtime/schemas/final-report-v3.0.schema.json +10219 -0
- package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
- package/runtime/skills/okstra-code-review/SKILL.md +21 -11
- package/runtime/skills/okstra-container-build/SKILL.md +18 -18
- package/runtime/skills/okstra-inspect/SKILL.md +12 -11
- package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
- package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
- package/runtime/skills/okstra-inspect/facets/history.md +9 -14
- package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
- package/runtime/skills/okstra-inspect/facets/report.md +6 -10
- package/runtime/skills/okstra-inspect/facets/status.md +9 -8
- package/runtime/skills/okstra-inspect/facets/time.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +16 -14
- package/runtime/skills/okstra-memory/SKILL.md +3 -3
- package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
- package/runtime/skills/okstra-rollup/SKILL.md +6 -16
- package/runtime/skills/okstra-run/SKILL.md +7 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
- package/runtime/skills/okstra-setup/SKILL.md +21 -13
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/skills/okstra-usage/SKILL.md +10 -10
- package/runtime/skills/okstra-user-response/SKILL.md +78 -107
- package/runtime/templates/report-writer-prompt-preamble.md +29 -19
- package/runtime/templates/reports/html/macros/forms.html +6 -4
- package/runtime/templates/reports/schedule.template.md +4 -4
- package/runtime/templates/worker-error-contract.md +17 -29
- package/runtime/validators/validate-run.py +636 -72
- package/runtime/validators/validate_session_conformance.py +43 -7
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"""보고서 작성자에게 전달할 동결된 합성 입력 묶음."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import copy
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import tempfile
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Mapping
|
|
12
|
+
|
|
13
|
+
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class ReportSynthesisPacketIssue:
|
|
18
|
+
owner: str
|
|
19
|
+
label: str
|
|
20
|
+
path: Path
|
|
21
|
+
reason: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ReportSynthesisPacketError(ValueError):
|
|
25
|
+
def __init__(self, issues: tuple[ReportSynthesisPacketIssue, ...]) -> None:
|
|
26
|
+
self.issues = issues
|
|
27
|
+
super().__init__("; ".join(issue.reason for issue in issues))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class ReportSynthesisSource:
|
|
32
|
+
label: str
|
|
33
|
+
owner: str
|
|
34
|
+
path: str
|
|
35
|
+
digest: str
|
|
36
|
+
content: str
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict[str, str]:
|
|
39
|
+
return {
|
|
40
|
+
"label": self.label,
|
|
41
|
+
"owner": self.owner,
|
|
42
|
+
"path": self.path,
|
|
43
|
+
"digest": self.digest,
|
|
44
|
+
"content": self.content,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ReportSynthesisPacket:
|
|
50
|
+
task_key: str
|
|
51
|
+
task_type: str
|
|
52
|
+
result_path: str
|
|
53
|
+
sources: tuple[ReportSynthesisSource, ...]
|
|
54
|
+
accounting_snapshot: dict[str, Any]
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict[str, Any]:
|
|
57
|
+
return {
|
|
58
|
+
"schemaVersion": "1.0",
|
|
59
|
+
"taskKey": self.task_key,
|
|
60
|
+
"taskType": self.task_type,
|
|
61
|
+
"authoringContract": {
|
|
62
|
+
"resultPath": self.result_path,
|
|
63
|
+
"format": "report-narrative-v3.0",
|
|
64
|
+
"sourcePolicy": "read-only-synthesis-packet",
|
|
65
|
+
"instructions": [
|
|
66
|
+
"Write the complete human-readable report narrative.",
|
|
67
|
+
"Preserve settled source values, identities, dissent, and user responses.",
|
|
68
|
+
"Do not invent a value when a source is missing or contradictory.",
|
|
69
|
+
"Write only the narrative, pointer, and audit artifacts named by the dispatch.",
|
|
70
|
+
],
|
|
71
|
+
"runtimeOwnedContent": [
|
|
72
|
+
"session identifiers",
|
|
73
|
+
"token usage",
|
|
74
|
+
"estimated cost",
|
|
75
|
+
"user response carry-in",
|
|
76
|
+
],
|
|
77
|
+
"validationRules": [
|
|
78
|
+
"source-digest-match",
|
|
79
|
+
"writer-owned-fields-only",
|
|
80
|
+
"all-defects-collected",
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
"accountingSnapshot": self.accounting_snapshot,
|
|
84
|
+
"sources": [source.to_dict() for source in self.sources],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def to_markdown(self) -> str:
|
|
88
|
+
lines = [
|
|
89
|
+
f"# OKSTRA Report Synthesis Packet - {self.task_key}",
|
|
90
|
+
"",
|
|
91
|
+
"## Authoring Contract",
|
|
92
|
+
"",
|
|
93
|
+
f"- Task type: `{self.task_type}`",
|
|
94
|
+
f"- Result path: `{self.result_path}`",
|
|
95
|
+
"- Output format: `report-narrative-v3.0`",
|
|
96
|
+
"- Input policy: read this synthesis packet as the dispatched source set",
|
|
97
|
+
"- Responsibility: write the complete human-readable narrative while preserving settled values",
|
|
98
|
+
"- Runtime-owned values: session identifiers, token usage, estimated cost, "
|
|
99
|
+
"user response carry-in",
|
|
100
|
+
"- Validation: source digest match, writer-owned fields, all defects collected",
|
|
101
|
+
]
|
|
102
|
+
lines.extend(_accounting_markdown(self.accounting_snapshot))
|
|
103
|
+
for source in self.sources:
|
|
104
|
+
lines.extend(_source_markdown(source))
|
|
105
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
_SOURCE_FIELDS = (
|
|
109
|
+
("Analysis packet", "orchestrator", "instructionSet", "analysisPacketPath", True),
|
|
110
|
+
("Task brief", "reporter", "instructionSet", "taskBriefPath", True),
|
|
111
|
+
("Analysis profile", "orchestrator", "instructionSet", "analysisProfilePath", False),
|
|
112
|
+
("Analysis material", "reporter", "instructionSet", "analysisMaterialPath", False),
|
|
113
|
+
("Reference expectations", "reporter", "instructionSet", "referenceExpectationsPath", False),
|
|
114
|
+
("Clarification response", "user", "instructionSet", "clarificationResponsePath", False),
|
|
115
|
+
("Final report template", "orchestrator", "instructionSet", "reportTemplatePath", True),
|
|
116
|
+
("Final report schema", "orchestrator", "instructionSet", "finalReportSchemaPath", True),
|
|
117
|
+
("Convergence state", "convergence", "run", "convergenceStatePath", True),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _source_markdown(source: ReportSynthesisSource) -> list[str]:
|
|
122
|
+
content = source.content.rstrip("\n")
|
|
123
|
+
longest = max((len(run) for run in re.findall(r"`+", content)), default=0)
|
|
124
|
+
fence = "`" * max(3, longest + 1)
|
|
125
|
+
return [
|
|
126
|
+
"",
|
|
127
|
+
f"## Source: {source.label}",
|
|
128
|
+
"",
|
|
129
|
+
f"- Owner: `{source.owner}`",
|
|
130
|
+
f"- Path: `{source.path}`",
|
|
131
|
+
f"- Digest: `{source.digest}`",
|
|
132
|
+
"",
|
|
133
|
+
f"{fence}text",
|
|
134
|
+
content,
|
|
135
|
+
fence,
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _accounting_markdown(snapshot: Mapping[str, Any]) -> list[str]:
|
|
140
|
+
summary = snapshot.get("usageSummary")
|
|
141
|
+
usage_summary = summary if isinstance(summary, Mapping) else {}
|
|
142
|
+
estimated = usage_summary.get("estimatedCostUsd")
|
|
143
|
+
estimated_cost = estimated if isinstance(estimated, Mapping) else {}
|
|
144
|
+
sessions = snapshot.get("leadSessionIds")
|
|
145
|
+
lead_sessions = sessions if isinstance(sessions, list) else []
|
|
146
|
+
lines = [
|
|
147
|
+
"",
|
|
148
|
+
"## Runtime-owned accounting snapshot",
|
|
149
|
+
"",
|
|
150
|
+
"- Lead sessions: " + (", ".join(map(str, lead_sessions)) or "not recorded"),
|
|
151
|
+
f"- Total tokens: {usage_summary.get('grandTotalTokens', 'not recorded')}",
|
|
152
|
+
f"- Estimated cost USD: {estimated_cost.get('grandTotal', 'not recorded')}",
|
|
153
|
+
]
|
|
154
|
+
workers = snapshot.get("workerUsage")
|
|
155
|
+
if not isinstance(workers, list):
|
|
156
|
+
return lines
|
|
157
|
+
for worker in workers:
|
|
158
|
+
if not isinstance(worker, Mapping):
|
|
159
|
+
continue
|
|
160
|
+
usage = worker.get("usage")
|
|
161
|
+
usage_value = usage if isinstance(usage, Mapping) else {}
|
|
162
|
+
cost = usage_value.get(
|
|
163
|
+
"estimatedCostUsd",
|
|
164
|
+
usage_value.get("cliEstimatedCostUsd", "not recorded"),
|
|
165
|
+
)
|
|
166
|
+
lines.append(
|
|
167
|
+
"- Worker "
|
|
168
|
+
f"{worker.get('workerId', 'unknown')}: "
|
|
169
|
+
f"tokens={usage_value.get('totalTokens', 'not recorded')}, "
|
|
170
|
+
f"costUsd={cost}"
|
|
171
|
+
)
|
|
172
|
+
return lines
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _string(value: object) -> str:
|
|
176
|
+
return value.strip() if isinstance(value, str) else ""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _context_path(
|
|
180
|
+
manifest: Mapping[str, Any],
|
|
181
|
+
active_context: Mapping[str, Any],
|
|
182
|
+
section: str,
|
|
183
|
+
key: str,
|
|
184
|
+
) -> str:
|
|
185
|
+
block = active_context.get(section)
|
|
186
|
+
if isinstance(block, Mapping):
|
|
187
|
+
value = _string(block.get(key))
|
|
188
|
+
if value:
|
|
189
|
+
return value
|
|
190
|
+
return _string(manifest.get(key))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _source_specs(
|
|
194
|
+
project_root: Path,
|
|
195
|
+
manifest: Mapping[str, Any],
|
|
196
|
+
active_context: Mapping[str, Any],
|
|
197
|
+
team_state: Mapping[str, Any],
|
|
198
|
+
narrative_path: Path,
|
|
199
|
+
) -> list[tuple[str, str, str, bool]]:
|
|
200
|
+
specs: list[tuple[str, str, str, bool]] = []
|
|
201
|
+
for label, owner, section, key, required in _SOURCE_FIELDS:
|
|
202
|
+
path = _context_path(manifest, active_context, section, key)
|
|
203
|
+
if label == "Final report template" and not path:
|
|
204
|
+
path = _context_path(
|
|
205
|
+
manifest, active_context, section, "finalReportTemplatePath"
|
|
206
|
+
)
|
|
207
|
+
if label == "Final report schema" and not path:
|
|
208
|
+
instruction_set = _context_path(
|
|
209
|
+
manifest, active_context, section, "instructionSetPath"
|
|
210
|
+
)
|
|
211
|
+
if instruction_set:
|
|
212
|
+
path = str(Path(instruction_set) / "final-report-schema.json")
|
|
213
|
+
if path or required:
|
|
214
|
+
specs.append((label, owner, path, required))
|
|
215
|
+
specs.extend(_user_response_specs(project_root, narrative_path))
|
|
216
|
+
attempt_specs = _attempt_result_specs(manifest)
|
|
217
|
+
if attempt_specs:
|
|
218
|
+
specs.extend(attempt_specs)
|
|
219
|
+
return specs
|
|
220
|
+
workers = team_state.get("workers")
|
|
221
|
+
if not isinstance(workers, list):
|
|
222
|
+
return specs
|
|
223
|
+
for worker in workers:
|
|
224
|
+
if not isinstance(worker, Mapping) or worker.get("workerId") == "report-writer":
|
|
225
|
+
continue
|
|
226
|
+
path = _string(worker.get("resultPath"))
|
|
227
|
+
if path:
|
|
228
|
+
worker_id = _string(worker.get("workerId")) or "unknown"
|
|
229
|
+
specs.append((f"Analysis result ({worker_id})", worker_id, path, True))
|
|
230
|
+
return specs
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _user_response_specs(
|
|
234
|
+
project_root: Path,
|
|
235
|
+
narrative_path: Path,
|
|
236
|
+
) -> list[tuple[str, str, str, bool]]:
|
|
237
|
+
response_dir = narrative_path.parent.parent / "user-responses"
|
|
238
|
+
if not response_dir.is_dir():
|
|
239
|
+
return []
|
|
240
|
+
return [
|
|
241
|
+
(
|
|
242
|
+
f"User response ({path.name})",
|
|
243
|
+
"user",
|
|
244
|
+
_relative(project_root, path),
|
|
245
|
+
True,
|
|
246
|
+
)
|
|
247
|
+
for path in sorted(response_dir.glob("user-response-*.md"))
|
|
248
|
+
if path.is_file()
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _attempt_result_specs(
|
|
253
|
+
manifest: Mapping[str, Any],
|
|
254
|
+
) -> list[tuple[str, str, str, bool]]:
|
|
255
|
+
invocations_value = manifest.get("invocations")
|
|
256
|
+
attempts_value = manifest.get("attempts")
|
|
257
|
+
if not isinstance(invocations_value, list) or not isinstance(attempts_value, list):
|
|
258
|
+
return []
|
|
259
|
+
invocations = {
|
|
260
|
+
_string(row.get("invocationRef")): row
|
|
261
|
+
for row in invocations_value
|
|
262
|
+
if isinstance(row, Mapping) and _string(row.get("invocationRef"))
|
|
263
|
+
}
|
|
264
|
+
excluded_duties = {"lead", "report-writer", "translator"}
|
|
265
|
+
specs: list[tuple[str, str, str, bool]] = []
|
|
266
|
+
seen: set[str] = set()
|
|
267
|
+
for attempt in attempts_value:
|
|
268
|
+
if not isinstance(attempt, Mapping) or attempt.get("status") != "ok":
|
|
269
|
+
continue
|
|
270
|
+
path = _string(attempt.get("resultPath"))
|
|
271
|
+
invocation_ref = _string(attempt.get("invocationRef"))
|
|
272
|
+
invocation = invocations.get(invocation_ref, {})
|
|
273
|
+
duty = _string(invocation.get("dutyId"))
|
|
274
|
+
if not path or path in seen or duty in excluded_duties:
|
|
275
|
+
continue
|
|
276
|
+
seen.add(path)
|
|
277
|
+
attempt_number = attempt.get("attempt")
|
|
278
|
+
specs.append(
|
|
279
|
+
(
|
|
280
|
+
f"Settled result ({invocation_ref}, attempt {attempt_number})",
|
|
281
|
+
duty or invocation_ref,
|
|
282
|
+
path,
|
|
283
|
+
True,
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
return specs
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _accounting_snapshot(team_state: Mapping[str, Any]) -> dict[str, Any]:
|
|
290
|
+
workers = team_state.get("workers")
|
|
291
|
+
worker_usage = []
|
|
292
|
+
if isinstance(workers, list):
|
|
293
|
+
worker_usage = [
|
|
294
|
+
{
|
|
295
|
+
"workerId": _string(worker.get("workerId")) or "unknown",
|
|
296
|
+
"role": _string(worker.get("role")),
|
|
297
|
+
"usage": copy.deepcopy(worker.get("usage"))
|
|
298
|
+
if isinstance(worker.get("usage"), Mapping)
|
|
299
|
+
else {},
|
|
300
|
+
}
|
|
301
|
+
for worker in workers
|
|
302
|
+
if isinstance(worker, Mapping)
|
|
303
|
+
]
|
|
304
|
+
lead_sessions = team_state.get("leadSessionIds")
|
|
305
|
+
return {
|
|
306
|
+
"leadSessionIds": copy.deepcopy(lead_sessions)
|
|
307
|
+
if isinstance(lead_sessions, list)
|
|
308
|
+
else [],
|
|
309
|
+
"leadUsage": copy.deepcopy(team_state.get("leadUsage"))
|
|
310
|
+
if isinstance(team_state.get("leadUsage"), Mapping)
|
|
311
|
+
else {},
|
|
312
|
+
"workerUsage": worker_usage,
|
|
313
|
+
"usageSummary": copy.deepcopy(team_state.get("usageSummary"))
|
|
314
|
+
if isinstance(team_state.get("usageSummary"), Mapping)
|
|
315
|
+
else {},
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _resolve(project_root: Path, value: str) -> Path:
|
|
320
|
+
path = Path(value)
|
|
321
|
+
return path if path.is_absolute() else project_root / path
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _relative(project_root: Path, path: Path) -> str:
|
|
325
|
+
try:
|
|
326
|
+
return path.relative_to(project_root).as_posix()
|
|
327
|
+
except ValueError:
|
|
328
|
+
return str(path)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _read_sources(
|
|
332
|
+
project_root: Path,
|
|
333
|
+
specs: list[tuple[str, str, str, bool]],
|
|
334
|
+
) -> tuple[ReportSynthesisSource, ...]:
|
|
335
|
+
issues: list[ReportSynthesisPacketIssue] = []
|
|
336
|
+
sources: list[ReportSynthesisSource] = []
|
|
337
|
+
for label, owner, value, required in specs:
|
|
338
|
+
if not value:
|
|
339
|
+
if required:
|
|
340
|
+
issues.append(
|
|
341
|
+
ReportSynthesisPacketIssue(
|
|
342
|
+
owner,
|
|
343
|
+
label,
|
|
344
|
+
project_root,
|
|
345
|
+
"required source path is not configured",
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
continue
|
|
349
|
+
path = _resolve(project_root, value)
|
|
350
|
+
if not path.is_file():
|
|
351
|
+
issues.append(
|
|
352
|
+
ReportSynthesisPacketIssue(
|
|
353
|
+
owner, label, path, "required source is missing"
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
continue
|
|
357
|
+
try:
|
|
358
|
+
content = path.read_text(encoding="utf-8")
|
|
359
|
+
except (OSError, UnicodeError) as exc:
|
|
360
|
+
issues.append(ReportSynthesisPacketIssue(owner, label, path, str(exc)))
|
|
361
|
+
continue
|
|
362
|
+
digest = "sha256:" + hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
363
|
+
sources.append(
|
|
364
|
+
ReportSynthesisSource(
|
|
365
|
+
label=label,
|
|
366
|
+
owner=owner,
|
|
367
|
+
path=_relative(project_root, path),
|
|
368
|
+
digest=digest,
|
|
369
|
+
content=content,
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
if issues:
|
|
373
|
+
raise ReportSynthesisPacketError(tuple(issues))
|
|
374
|
+
return tuple(sources)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def build_report_synthesis_packet(
|
|
378
|
+
*,
|
|
379
|
+
project_root: Path,
|
|
380
|
+
manifest: Mapping[str, Any],
|
|
381
|
+
active_context: Mapping[str, Any],
|
|
382
|
+
team_state: Mapping[str, Any],
|
|
383
|
+
narrative_path: Path,
|
|
384
|
+
) -> ReportSynthesisPacket:
|
|
385
|
+
sources = _read_sources(
|
|
386
|
+
project_root,
|
|
387
|
+
_source_specs(
|
|
388
|
+
project_root,
|
|
389
|
+
manifest,
|
|
390
|
+
active_context,
|
|
391
|
+
team_state,
|
|
392
|
+
narrative_path,
|
|
393
|
+
),
|
|
394
|
+
)
|
|
395
|
+
return ReportSynthesisPacket(
|
|
396
|
+
task_key=_string(manifest.get("taskKey")),
|
|
397
|
+
task_type=_string(manifest.get("taskType")),
|
|
398
|
+
result_path=_relative(project_root, narrative_path),
|
|
399
|
+
sources=sources,
|
|
400
|
+
accounting_snapshot=_accounting_snapshot(team_state),
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def report_synthesis_packet_paths(narrative_path: Path) -> tuple[Path, Path]:
|
|
405
|
+
name = narrative_path.name
|
|
406
|
+
prefix = "report-writer-narrative-"
|
|
407
|
+
if name == "report-writer-narrative.md":
|
|
408
|
+
packet_name = "report-writer-synthesis-packet.md"
|
|
409
|
+
elif name.startswith(prefix) and name.endswith(".md"):
|
|
410
|
+
packet_name = "report-writer-synthesis-packet-" + name[len(prefix):]
|
|
411
|
+
else:
|
|
412
|
+
raise ValueError(f"report narrative path has an unsupported name: {narrative_path}")
|
|
413
|
+
markdown_path = narrative_path.with_name(packet_name)
|
|
414
|
+
data_path = markdown_path.with_suffix(".data.json")
|
|
415
|
+
return data_path, markdown_path
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def materialize_report_synthesis_packet(
|
|
419
|
+
*,
|
|
420
|
+
project_root: Path,
|
|
421
|
+
manifest: Mapping[str, Any],
|
|
422
|
+
active_context: Mapping[str, Any],
|
|
423
|
+
team_state: Mapping[str, Any],
|
|
424
|
+
narrative_path: Path,
|
|
425
|
+
) -> tuple[Path, Path]:
|
|
426
|
+
packet = build_report_synthesis_packet(
|
|
427
|
+
project_root=project_root,
|
|
428
|
+
manifest=manifest,
|
|
429
|
+
active_context=active_context,
|
|
430
|
+
team_state=team_state,
|
|
431
|
+
narrative_path=narrative_path,
|
|
432
|
+
)
|
|
433
|
+
data_path, markdown_path = report_synthesis_packet_paths(narrative_path)
|
|
434
|
+
write_owned_object_atomic(
|
|
435
|
+
data_path,
|
|
436
|
+
packet.to_dict(),
|
|
437
|
+
artifact="report synthesis packet",
|
|
438
|
+
)
|
|
439
|
+
_write_text_atomic(markdown_path, packet.to_markdown())
|
|
440
|
+
return data_path, markdown_path
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _write_text_atomic(path: Path, text: str) -> None:
|
|
444
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
445
|
+
descriptor, temporary = tempfile.mkstemp(
|
|
446
|
+
prefix=f".{path.name}.",
|
|
447
|
+
suffix=".tmp",
|
|
448
|
+
dir=path.parent,
|
|
449
|
+
)
|
|
450
|
+
try:
|
|
451
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
452
|
+
handle.write(text)
|
|
453
|
+
handle.flush()
|
|
454
|
+
os.fsync(handle.fileno())
|
|
455
|
+
os.replace(temporary, path)
|
|
456
|
+
finally:
|
|
457
|
+
if os.path.exists(temporary):
|
|
458
|
+
os.unlink(temporary)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def verify_report_synthesis_packet_sources(
|
|
462
|
+
project_root: Path,
|
|
463
|
+
data_path: Path,
|
|
464
|
+
) -> tuple[ReportSynthesisPacketIssue, ...]:
|
|
465
|
+
try:
|
|
466
|
+
payload = load_owned_object(data_path, artifact="report synthesis packet")
|
|
467
|
+
except JsonBoundaryError as exc:
|
|
468
|
+
return (
|
|
469
|
+
ReportSynthesisPacketIssue(
|
|
470
|
+
"orchestrator",
|
|
471
|
+
"Report synthesis packet",
|
|
472
|
+
data_path,
|
|
473
|
+
str(exc),
|
|
474
|
+
),
|
|
475
|
+
)
|
|
476
|
+
sources = payload.get("sources") if isinstance(payload, Mapping) else None
|
|
477
|
+
if not isinstance(sources, list):
|
|
478
|
+
return (
|
|
479
|
+
ReportSynthesisPacketIssue(
|
|
480
|
+
"orchestrator",
|
|
481
|
+
"Report synthesis packet",
|
|
482
|
+
data_path,
|
|
483
|
+
"sources must be an array",
|
|
484
|
+
),
|
|
485
|
+
)
|
|
486
|
+
issues: list[ReportSynthesisPacketIssue] = []
|
|
487
|
+
for source in sources:
|
|
488
|
+
if not isinstance(source, Mapping):
|
|
489
|
+
continue
|
|
490
|
+
label = _string(source.get("label")) or "Unknown source"
|
|
491
|
+
owner = _string(source.get("owner")) or "orchestrator"
|
|
492
|
+
path = _resolve(project_root, _string(source.get("path")))
|
|
493
|
+
if not path.is_file():
|
|
494
|
+
issues.append(
|
|
495
|
+
ReportSynthesisPacketIssue(
|
|
496
|
+
owner,
|
|
497
|
+
label,
|
|
498
|
+
path,
|
|
499
|
+
"frozen source is missing",
|
|
500
|
+
)
|
|
501
|
+
)
|
|
502
|
+
continue
|
|
503
|
+
try:
|
|
504
|
+
content = path.read_text(encoding="utf-8")
|
|
505
|
+
except (OSError, UnicodeError) as exc:
|
|
506
|
+
issues.append(ReportSynthesisPacketIssue(owner, label, path, str(exc)))
|
|
507
|
+
continue
|
|
508
|
+
digest = "sha256:" + hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
509
|
+
if digest != source.get("digest"):
|
|
510
|
+
issues.append(
|
|
511
|
+
ReportSynthesisPacketIssue(
|
|
512
|
+
owner,
|
|
513
|
+
label,
|
|
514
|
+
path,
|
|
515
|
+
"source value changed after packet materialization",
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
return tuple(issues)
|
|
@@ -35,6 +35,7 @@ from pathlib import Path
|
|
|
35
35
|
from typing import Iterable, Optional
|
|
36
36
|
|
|
37
37
|
from .final_report_paths import final_report_data_path
|
|
38
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
38
39
|
from .report_html.view_models.implementation_planning import (
|
|
39
40
|
PlanApprovalState,
|
|
40
41
|
plan_approval_state,
|
|
@@ -1349,8 +1350,8 @@ def _load_report_data(src_md_path: Path) -> dict | None:
|
|
|
1349
1350
|
if not data_path.is_file():
|
|
1350
1351
|
return None
|
|
1351
1352
|
try:
|
|
1352
|
-
data =
|
|
1353
|
-
except
|
|
1353
|
+
data = load_owned_object(data_path, artifact="final report record")
|
|
1354
|
+
except JsonBoundaryError:
|
|
1354
1355
|
return None
|
|
1355
1356
|
return data if isinstance(data, dict) else None
|
|
1356
1357
|
|
|
@@ -17,6 +17,8 @@ from pathlib import Path
|
|
|
17
17
|
from okstra_ctl import next_phase
|
|
18
18
|
from okstra_ctl.error_log_core import glob_error_logs, parse_records
|
|
19
19
|
from okstra_ctl.time_report import aggregate_time, load_runs
|
|
20
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
21
|
+
from okstra_ctl.fixed_text import line
|
|
20
22
|
from okstra_project import (
|
|
21
23
|
ResolverError,
|
|
22
24
|
list_project_tasks,
|
|
@@ -42,8 +44,8 @@ def _collect_critic(task_root: Path) -> dict:
|
|
|
42
44
|
totals = {"runsWithCritic": 0} | {name: 0 for name in _CRITIC_COUNTERS}
|
|
43
45
|
for state_path in sorted(task_root.glob("runs/*/state/convergence-*.json")):
|
|
44
46
|
try:
|
|
45
|
-
payload =
|
|
46
|
-
except
|
|
47
|
+
payload = load_owned_object(state_path, artifact="convergence state")
|
|
48
|
+
except JsonBoundaryError:
|
|
47
49
|
continue
|
|
48
50
|
critic = (payload.get("config") or {}).get("critic")
|
|
49
51
|
if not isinstance(critic, dict):
|
|
@@ -146,6 +148,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
146
148
|
parser.add_argument("--project-root", default="")
|
|
147
149
|
parser.add_argument("--cwd", default=".")
|
|
148
150
|
parser.add_argument("--json", action="store_true", help="emit JSON (always on)")
|
|
151
|
+
parser.add_argument("--text", action="store_true", help="emit fixed text fields")
|
|
149
152
|
args = parser.parse_args(argv)
|
|
150
153
|
|
|
151
154
|
try:
|
|
@@ -153,13 +156,71 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
153
156
|
resolve_project_root(explicit_root=args.project_root, cwd=args.cwd)
|
|
154
157
|
).resolve()
|
|
155
158
|
except ResolverError as exc:
|
|
156
|
-
|
|
159
|
+
payload = {"ok": False, "stage": "resolve", "reason": str(exc)}
|
|
160
|
+
print(render_rollup_text(payload) if args.text else json.dumps(payload))
|
|
157
161
|
return 2
|
|
158
162
|
|
|
159
163
|
result = build_rollup(project_root, args.task_group)
|
|
160
|
-
print(
|
|
164
|
+
print(
|
|
165
|
+
render_rollup_text(result)
|
|
166
|
+
if args.text
|
|
167
|
+
else json.dumps(result, ensure_ascii=False, indent=2)
|
|
168
|
+
)
|
|
161
169
|
return 0
|
|
162
170
|
|
|
163
171
|
|
|
172
|
+
def render_rollup_text(payload: dict) -> str:
|
|
173
|
+
"""rollup 모델 표면에 승인된 필드만 고정 순서로 투영한다."""
|
|
174
|
+
rows = ["Okstra rollup\n"]
|
|
175
|
+
rows.append(line("Status", "ready" if payload.get("ok") else "error"))
|
|
176
|
+
rows.append(line("Task group", payload.get("taskGroup") or "all"))
|
|
177
|
+
rows.append(line("Task count", payload.get("taskCount")))
|
|
178
|
+
totals = payload.get("totals") if isinstance(payload.get("totals"), dict) else {}
|
|
179
|
+
rows.append(line("Total runs", totals.get("runs")))
|
|
180
|
+
rows.append(line("Total errors", totals.get("errors")))
|
|
181
|
+
rows.append(line("CPU sum ms", totals.get("cpuSumMs")))
|
|
182
|
+
rows.append(line("Wall clock ms", totals.get("wallClockMs")))
|
|
183
|
+
rows.extend(_distribution_lines("Work status", totals.get("byWorkStatus")))
|
|
184
|
+
rows.extend(_distribution_lines("Work category", totals.get("byWorkCategory")))
|
|
185
|
+
rows.extend(_distribution_lines("Current phase", totals.get("byCurrentPhase")))
|
|
186
|
+
rows.extend(_distribution_lines("Task type", totals.get("byTaskType")))
|
|
187
|
+
tasks = payload.get("tasks") if isinstance(payload.get("tasks"), list) else []
|
|
188
|
+
for index, task in enumerate(tasks, 1):
|
|
189
|
+
if not isinstance(task, dict):
|
|
190
|
+
continue
|
|
191
|
+
rows.extend(_task_lines(index, task))
|
|
192
|
+
if not payload.get("ok"):
|
|
193
|
+
rows.append(line("Failure stage", payload.get("stage")))
|
|
194
|
+
rows.append(line("Failure reason", payload.get("reason")))
|
|
195
|
+
return "".join(rows)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _distribution_lines(title: str, raw_values: object) -> list[str]:
|
|
199
|
+
values = raw_values if isinstance(raw_values, dict) else {}
|
|
200
|
+
rows: list[str] = []
|
|
201
|
+
for index, (name, count) in enumerate(sorted(values.items()), 1):
|
|
202
|
+
rows.append(line(f"{title} {index} name", name))
|
|
203
|
+
rows.append(line(f"{title} {index} count", count))
|
|
204
|
+
return rows
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _task_lines(index: int, task: dict) -> list[str]:
|
|
208
|
+
fields = (
|
|
209
|
+
("key", "taskKey"), ("type", "taskType"), ("category", "workCategory"),
|
|
210
|
+
("status", "workStatus"), ("phase", "currentPhase"),
|
|
211
|
+
("phase state", "currentPhaseState"), ("latest run status", "latestRunStatus"),
|
|
212
|
+
("updated at", "updatedAt"), ("runs", "runCount"),
|
|
213
|
+
("CPU sum ms", "cpuSumMs"), ("wall clock ms", "wallClockMs"),
|
|
214
|
+
("errors", "errorCount"), ("report path", "reportPath"),
|
|
215
|
+
)
|
|
216
|
+
rows = [line(f"Task {index} {label}", task.get(key)) for label, key in fields]
|
|
217
|
+
next_phase = task.get("nextRecommendedPhase")
|
|
218
|
+
values = next_phase if isinstance(next_phase, dict) else {}
|
|
219
|
+
for label, key in (("next phase", "phase"), ("next phase status", "status"),
|
|
220
|
+
("next phase rationale", "rationale")):
|
|
221
|
+
rows.append(line(f"Task {index} {label}", values.get(key)))
|
|
222
|
+
return rows
|
|
223
|
+
|
|
224
|
+
|
|
164
225
|
if __name__ == "__main__":
|
|
165
226
|
raise SystemExit(main(sys.argv[1:]))
|