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
|
@@ -15,7 +15,9 @@ from datetime import datetime, timezone
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
17
|
from okstra_ctl.ids import slugify_task_segment
|
|
18
|
+
from okstra_ctl.fixed_text import line, scalar
|
|
18
19
|
from okstra_ctl.paths import task_dir, task_manifest_file
|
|
20
|
+
from okstra_ctl.json_boundary import load_owned_object, write_owned_object_atomic
|
|
19
21
|
from okstra_project import (
|
|
20
22
|
ResolverError,
|
|
21
23
|
StateError,
|
|
@@ -26,8 +28,23 @@ from okstra_project import (
|
|
|
26
28
|
ALLOWED_WORK_STATUSES = ("todo", "in-progress", "blocked", "done")
|
|
27
29
|
|
|
28
30
|
|
|
29
|
-
def _emit(payload: dict) -> None:
|
|
30
|
-
|
|
31
|
+
def _emit(payload: dict, *, text: bool = False) -> None:
|
|
32
|
+
if not text:
|
|
33
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
34
|
+
return
|
|
35
|
+
lines = ["# Okstra Work Status Result", ""]
|
|
36
|
+
for key, label in (
|
|
37
|
+
("ok", "OK"), ("stage", "Stage"), ("reason", "Reason"),
|
|
38
|
+
("taskKey", "Task key"), ("previousWorkStatus", "Previous work status"),
|
|
39
|
+
("workStatus", "Work status"), ("workStatusUpdatedAt", "Updated at"),
|
|
40
|
+
("workStatusNote", "Note"), ("taskManifestPath", "Task manifest"),
|
|
41
|
+
):
|
|
42
|
+
if key in payload:
|
|
43
|
+
lines.append(line(label, payload.get(key)).rstrip("\n"))
|
|
44
|
+
for match in payload.get("matches", []) if isinstance(payload.get("matches"), list) else []:
|
|
45
|
+
if isinstance(match, dict):
|
|
46
|
+
lines.append(f"- Match: `{scalar(match.get('taskKey'))}`")
|
|
47
|
+
print("\n".join(lines))
|
|
31
48
|
|
|
32
49
|
|
|
33
50
|
def _manifest_path(project_root: Path, entry: dict) -> Path:
|
|
@@ -44,7 +61,7 @@ def _manifest_path(project_root: Path, entry: dict) -> Path:
|
|
|
44
61
|
return task_manifest_file(task_dir(project_root, group_seg, id_seg))
|
|
45
62
|
|
|
46
63
|
|
|
47
|
-
def
|
|
64
|
+
def _parse_args(argv: list[str] | None):
|
|
48
65
|
parser = argparse.ArgumentParser(
|
|
49
66
|
description="Set a task's user-managed workStatus in task-manifest.json."
|
|
50
67
|
)
|
|
@@ -64,13 +81,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
64
81
|
parser.add_argument("--project-root", default="", help="project root for catalog lookup")
|
|
65
82
|
parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
|
|
66
83
|
parser.add_argument("--json", action="store_true", help="emit JSON (always on)")
|
|
67
|
-
|
|
84
|
+
parser.add_argument("--text", action="store_true", help="emit fixed text fields")
|
|
85
|
+
return parser.parse_args(argv)
|
|
86
|
+
|
|
68
87
|
|
|
88
|
+
def _resolve_entry(args):
|
|
69
89
|
try:
|
|
70
90
|
project_root = resolve_project_root(explicit_root=args.project_root, cwd=args.cwd)
|
|
71
91
|
except ResolverError as exc:
|
|
72
|
-
_emit({"ok": False, "stage": "resolve", "reason": str(exc)})
|
|
73
|
-
return 2
|
|
92
|
+
_emit({"ok": False, "stage": "resolve", "reason": str(exc)}, text=args.text)
|
|
93
|
+
return None, None, 2
|
|
74
94
|
|
|
75
95
|
token = args.token
|
|
76
96
|
task_group = args.task_group
|
|
@@ -84,17 +104,42 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
84
104
|
project_root, token, task_group=task_group or None
|
|
85
105
|
)
|
|
86
106
|
except StateError as exc:
|
|
87
|
-
_emit({"ok": False, "stage": "catalog", "reason": str(exc)})
|
|
88
|
-
return 2
|
|
107
|
+
_emit({"ok": False, "stage": "catalog", "reason": str(exc)}, text=args.text)
|
|
108
|
+
return None, None, 2
|
|
89
109
|
|
|
90
110
|
if not matches:
|
|
91
|
-
_emit({"ok": False, "stage": "not-found", "token": args.token})
|
|
92
|
-
return 1
|
|
111
|
+
_emit({"ok": False, "stage": "not-found", "token": args.token}, text=args.text)
|
|
112
|
+
return None, None, 1
|
|
93
113
|
if len(matches) > 1:
|
|
94
|
-
_emit({"ok": False, "stage": "ambiguous", "matches": matches})
|
|
95
|
-
return 2
|
|
114
|
+
_emit({"ok": False, "stage": "ambiguous", "matches": matches}, text=args.text)
|
|
115
|
+
return None, None, 2
|
|
116
|
+
return project_root, matches[0], 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _updated_payload(manifest_path: Path, manifest: dict, entry: dict, args) -> dict:
|
|
120
|
+
previous = manifest.get("workStatus", "")
|
|
121
|
+
manifest["workStatus"] = args.status
|
|
122
|
+
manifest["workStatusUpdatedAt"] = datetime.now(timezone.utc).strftime(
|
|
123
|
+
"%Y-%m-%dT%H:%M:%SZ"
|
|
124
|
+
)
|
|
125
|
+
if args.note is not None:
|
|
126
|
+
manifest["workStatusNote"] = args.note
|
|
127
|
+
write_owned_object_atomic(manifest_path, manifest, artifact="task manifest")
|
|
128
|
+
return {
|
|
129
|
+
"ok": True, "taskKey": entry.get("taskKey", ""),
|
|
130
|
+
"previousWorkStatus": previous, "workStatus": args.status,
|
|
131
|
+
"workStatusUpdatedAt": manifest["workStatusUpdatedAt"],
|
|
132
|
+
"workStatusNote": manifest.get("workStatusNote", ""),
|
|
133
|
+
"taskManifestPath": str(manifest_path),
|
|
134
|
+
}
|
|
96
135
|
|
|
97
|
-
|
|
136
|
+
|
|
137
|
+
def main(argv: list[str] | None = None) -> int:
|
|
138
|
+
args = _parse_args(argv)
|
|
139
|
+
project_root, entry, resolution_exit = _resolve_entry(args)
|
|
140
|
+
if resolution_exit:
|
|
141
|
+
return resolution_exit
|
|
142
|
+
assert project_root is not None and entry is not None
|
|
98
143
|
manifest_path = _manifest_path(project_root, entry)
|
|
99
144
|
if not manifest_path.exists():
|
|
100
145
|
_emit(
|
|
@@ -103,11 +148,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
103
148
|
"stage": "manifest-missing",
|
|
104
149
|
"taskKey": entry.get("taskKey", ""),
|
|
105
150
|
"taskManifestPath": str(manifest_path),
|
|
106
|
-
}
|
|
151
|
+
},
|
|
152
|
+
text=args.text,
|
|
107
153
|
)
|
|
108
154
|
return 1
|
|
109
155
|
try:
|
|
110
|
-
manifest =
|
|
156
|
+
manifest = load_owned_object(manifest_path, artifact="task manifest")
|
|
111
157
|
except ValueError as exc:
|
|
112
158
|
_emit(
|
|
113
159
|
{
|
|
@@ -115,32 +161,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
115
161
|
"stage": "manifest-invalid",
|
|
116
162
|
"taskManifestPath": str(manifest_path),
|
|
117
163
|
"reason": str(exc),
|
|
118
|
-
}
|
|
164
|
+
},
|
|
165
|
+
text=args.text,
|
|
119
166
|
)
|
|
120
167
|
return 1
|
|
121
168
|
|
|
122
|
-
|
|
123
|
-
manifest["workStatus"] = args.status
|
|
124
|
-
manifest["workStatusUpdatedAt"] = datetime.now(timezone.utc).strftime(
|
|
125
|
-
"%Y-%m-%dT%H:%M:%SZ"
|
|
126
|
-
)
|
|
127
|
-
if args.note is not None:
|
|
128
|
-
manifest["workStatusNote"] = args.note
|
|
129
|
-
manifest_path.write_text(
|
|
130
|
-
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
_emit(
|
|
134
|
-
{
|
|
135
|
-
"ok": True,
|
|
136
|
-
"taskKey": entry.get("taskKey", ""),
|
|
137
|
-
"previousWorkStatus": previous,
|
|
138
|
-
"workStatus": args.status,
|
|
139
|
-
"workStatusUpdatedAt": manifest["workStatusUpdatedAt"],
|
|
140
|
-
"workStatusNote": manifest.get("workStatusNote", ""),
|
|
141
|
-
"taskManifestPath": str(manifest_path),
|
|
142
|
-
}
|
|
143
|
-
)
|
|
169
|
+
_emit(_updated_payload(manifest_path, manifest, entry, args), text=args.text)
|
|
144
170
|
return 0
|
|
145
171
|
|
|
146
172
|
|
|
@@ -14,6 +14,8 @@ import subprocess
|
|
|
14
14
|
from dataclasses import dataclass
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
18
|
+
|
|
17
19
|
_REPORT_RE = re.compile(r"final-report-implementation-(\d+)\.data\.json$")
|
|
18
20
|
|
|
19
21
|
|
|
@@ -60,8 +62,8 @@ def derive_stage_fix_carry(
|
|
|
60
62
|
if report is None:
|
|
61
63
|
return None
|
|
62
64
|
try:
|
|
63
|
-
data =
|
|
64
|
-
except (OSError,
|
|
65
|
+
data = load_owned_object(report, artifact="implementation final report")
|
|
66
|
+
except (OSError, JsonBoundaryError):
|
|
65
67
|
return None
|
|
66
68
|
impl = data.get("implementation")
|
|
67
69
|
if not isinstance(impl, dict):
|
|
@@ -8,6 +8,8 @@ from pathlib import Path
|
|
|
8
8
|
from typing import Any, Iterable
|
|
9
9
|
|
|
10
10
|
from .final_report_paths import final_report_data_path, is_full_reading_copy_path
|
|
11
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
12
|
+
from .fixed_text import line, value_lines
|
|
11
13
|
from .md_table import is_separator_row, split_pipe_row
|
|
12
14
|
from .paths import RunRef
|
|
13
15
|
from .plan_run_root import list_implementation_planning_reports
|
|
@@ -48,6 +50,24 @@ class StageMapSnapshot:
|
|
|
48
50
|
stages: list[dict[str, Any]]
|
|
49
51
|
|
|
50
52
|
|
|
53
|
+
def render_stage_map_text(payload: dict[str, Any]) -> str:
|
|
54
|
+
"""모델 소비용 Stage Map 스냅샷을 고정 텍스트로 투영한다."""
|
|
55
|
+
rows = ["Okstra stage map\n"]
|
|
56
|
+
rows.append(line("Status", "ready" if payload.get("ok") else "error"))
|
|
57
|
+
for label, key in (
|
|
58
|
+
("Task key", "taskKey"), ("Task root", "taskRoot"),
|
|
59
|
+
("State", "state"), ("Source plan path", "sourcePlanPath"),
|
|
60
|
+
):
|
|
61
|
+
rows.append(line(label, payload.get(key)))
|
|
62
|
+
for label, key in (("Stages", "stages"), ("Done stages", "doneStages"),
|
|
63
|
+
("Planning", "planning")):
|
|
64
|
+
rows.extend(value_lines(label, payload.get(key)))
|
|
65
|
+
if not payload.get("ok"):
|
|
66
|
+
rows.append(line("Failure stage", payload.get("stage")))
|
|
67
|
+
rows.append(line("Failure reason", payload.get("reason")))
|
|
68
|
+
return "".join(rows)
|
|
69
|
+
|
|
70
|
+
|
|
51
71
|
# `stepwiseExecution` is what the stage DOES; the other three are what becomes
|
|
52
72
|
# true when it is finished. A schedule carrying only the latter reads as an
|
|
53
73
|
# analysis of a plan rather than the plan itself.
|
|
@@ -354,8 +374,8 @@ def parse_stage_map_file(plan_path: Path) -> list[StageMapStage]:
|
|
|
354
374
|
resolved = Path(plan_path).resolve()
|
|
355
375
|
if is_report_record_path(resolved):
|
|
356
376
|
try:
|
|
357
|
-
data =
|
|
358
|
-
except (OSError, UnicodeError,
|
|
377
|
+
data = load_owned_object(resolved, artifact="planning final report")
|
|
378
|
+
except (OSError, UnicodeError, JsonBoundaryError) as exc:
|
|
359
379
|
raise StageMapError("stage_map", str(exc), str(resolved)) from exc
|
|
360
380
|
if not isinstance(data, dict):
|
|
361
381
|
raise StageMapError(
|
|
@@ -379,8 +399,8 @@ def structured_report(plan_path: Path) -> dict[str, Any]:
|
|
|
379
399
|
if not is_report_record_path(resolved) or not resolved.exists():
|
|
380
400
|
return {}
|
|
381
401
|
try:
|
|
382
|
-
data =
|
|
383
|
-
except (OSError, UnicodeError,
|
|
402
|
+
data = load_owned_object(resolved, artifact="planning final report")
|
|
403
|
+
except (OSError, UnicodeError, JsonBoundaryError) as exc:
|
|
384
404
|
raise StageMapError("stage_map", str(exc), str(resolved)) from exc
|
|
385
405
|
if not isinstance(data, dict) or data.get("schemaVersion") not in {"2.0", "3.0"}:
|
|
386
406
|
return {}
|
|
@@ -518,8 +538,8 @@ def _unique_carry_source_paths(task_root: Path) -> list[Path]:
|
|
|
518
538
|
paths: set[Path] = set()
|
|
519
539
|
for carry_path in sorted(carry_dir.glob("stage-*.json")):
|
|
520
540
|
try:
|
|
521
|
-
carry =
|
|
522
|
-
except (OSError, UnicodeError,
|
|
541
|
+
carry = load_owned_object(carry_path, artifact="implementation stage carry")
|
|
542
|
+
except (OSError, UnicodeError, JsonBoundaryError):
|
|
523
543
|
continue
|
|
524
544
|
value = carry.get("sourcePlanPath") if isinstance(carry, dict) else None
|
|
525
545
|
if isinstance(value, str) and value:
|
|
@@ -853,18 +853,17 @@ def integrate_and_teardown_whole_task(
|
|
|
853
853
|
stage_map 은 done 행에서 만든다. 정리 대상은 완료된 stage 뿐이고, 계획에만 있고
|
|
854
854
|
완료되지 않은 stage 는 Phase A 가 어차피 건너뛰기 때문이다.
|
|
855
855
|
"""
|
|
856
|
-
from json import loads as _loads
|
|
857
|
-
|
|
858
856
|
from okstra_project.dirs import okstra_home, project_json_path
|
|
859
857
|
|
|
860
858
|
from . import consumers, worktree_registry
|
|
859
|
+
from .json_boundary import load_owned_object
|
|
861
860
|
from .locks import worktree_provision_mutex
|
|
862
861
|
from .paths import task_runs_dir
|
|
863
862
|
from .stage_integrate import integrate_stages
|
|
864
863
|
|
|
865
864
|
try:
|
|
866
|
-
project_id =
|
|
867
|
-
project_json_path(project_root)
|
|
865
|
+
project_id = load_owned_object(
|
|
866
|
+
project_json_path(project_root), artifact="project config"
|
|
868
867
|
).get("projectId", "")
|
|
869
868
|
except (OSError, ValueError):
|
|
870
869
|
project_id = ""
|
|
@@ -39,6 +39,7 @@ from .ports.worker_dispatch import WorkerDispatchRequest
|
|
|
39
39
|
from .registry.host_registry import default_host_registry
|
|
40
40
|
from .registry.provider_registry import default_provider_registry
|
|
41
41
|
from .session import observe_lead_session
|
|
42
|
+
from .json_boundary import load_owned_object
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
_SUPPORTED_WRAPPERS = provider_worker_wrappers(default_provider_registry())
|
|
@@ -417,7 +418,7 @@ def _load_manifest(project_root: str, run_manifest: str) -> dict[str, Any]:
|
|
|
417
418
|
def _load_json(path: Path, label: str) -> dict[str, Any]:
|
|
418
419
|
if not path.is_file():
|
|
419
420
|
raise DispatchError(f"{label} not found: {path}")
|
|
420
|
-
payload =
|
|
421
|
+
payload = load_owned_object(path, artifact=label)
|
|
421
422
|
if not isinstance(payload, dict):
|
|
422
423
|
raise DispatchError(f"{label} must be a JSON object: {path}")
|
|
423
424
|
return payload
|
|
@@ -36,6 +36,11 @@ import tempfile
|
|
|
36
36
|
from pathlib import Path
|
|
37
37
|
|
|
38
38
|
from okstra_ctl.session import resolve_inproc_lead_session_id
|
|
39
|
+
from okstra_ctl.json_boundary import (
|
|
40
|
+
JsonBoundaryError,
|
|
41
|
+
external_claude_team_json_source,
|
|
42
|
+
load_external_json,
|
|
43
|
+
)
|
|
39
44
|
|
|
40
45
|
|
|
41
46
|
def live_pane_ids() -> tuple[bool, set[str]]:
|
|
@@ -148,8 +153,12 @@ def _reconcile_config(cfg_path: Path, dry_run: bool, print_members: bool) -> int
|
|
|
148
153
|
line per non-lead member so the lead knows whom to ``shutdown_request``.
|
|
149
154
|
"""
|
|
150
155
|
try:
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
# 외부 입력: Claude Code가 소유하는 team roster 설정이다.
|
|
157
|
+
config = load_external_json(
|
|
158
|
+
external_claude_team_json_source(cfg_path),
|
|
159
|
+
artifact="Claude team configuration",
|
|
160
|
+
)
|
|
161
|
+
except JsonBoundaryError as exc:
|
|
153
162
|
print(f"team-reconcile: cannot read team config at {cfg_path}: {exc}",
|
|
154
163
|
file=sys.stderr)
|
|
155
164
|
return 1
|
|
@@ -14,7 +14,10 @@ import json
|
|
|
14
14
|
import sys
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
+
from .fixed_text import line as _text_line
|
|
18
|
+
|
|
17
19
|
from okstra_ctl.paths import resolve_under_root, task_timeline_file
|
|
20
|
+
from okstra_ctl.json_boundary import load_owned_object
|
|
18
21
|
from okstra_project import read_task_key
|
|
19
22
|
from okstra_ctl.task_target import resolve_task_root
|
|
20
23
|
|
|
@@ -25,7 +28,7 @@ def time_rows_with_identity(run_root: Path) -> list[dict]:
|
|
|
25
28
|
|
|
26
29
|
state_path = run_root / "state" / "team-state.json"
|
|
27
30
|
try:
|
|
28
|
-
state =
|
|
31
|
+
state = load_owned_object(state_path, artifact="team state")
|
|
29
32
|
except (OSError, ValueError):
|
|
30
33
|
return []
|
|
31
34
|
return identity_rows_from_state(state if isinstance(state, dict) else {})
|
|
@@ -34,7 +37,7 @@ def time_rows_with_identity(run_root: Path) -> list[dict]:
|
|
|
34
37
|
def load_runs(task_root: Path) -> tuple[list[dict], bool]:
|
|
35
38
|
path = task_timeline_file(task_root)
|
|
36
39
|
try:
|
|
37
|
-
data =
|
|
40
|
+
data = load_owned_object(path, artifact="task timeline")
|
|
38
41
|
except (OSError, ValueError):
|
|
39
42
|
return [], True
|
|
40
43
|
runs = data.get("runs", []) if isinstance(data, dict) else []
|
|
@@ -96,7 +99,7 @@ def _read_state(project_root: Path, rel: str) -> dict | None:
|
|
|
96
99
|
if target is None:
|
|
97
100
|
return None
|
|
98
101
|
try:
|
|
99
|
-
state =
|
|
102
|
+
state = load_owned_object(target, artifact="team state")
|
|
100
103
|
except (OSError, ValueError):
|
|
101
104
|
return None
|
|
102
105
|
return state if isinstance(state, dict) else None
|
|
@@ -211,6 +214,48 @@ def aggregate_time(runs: list[dict], project_root: Path) -> dict:
|
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
|
|
217
|
+
def _worker_text(result: dict) -> str:
|
|
218
|
+
parts: list[str] = []
|
|
219
|
+
workers = result.get("perWorker")
|
|
220
|
+
if not isinstance(workers, dict):
|
|
221
|
+
return ""
|
|
222
|
+
for task_type, rows in workers.items():
|
|
223
|
+
for row in rows if isinstance(rows, list) else []:
|
|
224
|
+
if isinstance(row, dict):
|
|
225
|
+
parts.extend(("\n## Worker\n\n", _text_line("Task type", task_type), _text_line("Worker ID", row.get("workerId")), _text_line("Agents", ",".join(row.get("agents", [])) if isinstance(row.get("agents"), list) else None), _text_line("Runs", row.get("runs")), _text_line("Total ms", row.get("totalMs")), _text_line("Average ms", row.get("avgMs"))))
|
|
226
|
+
return "".join(parts)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _timeline_text(result: dict) -> str:
|
|
230
|
+
parts: list[str] = []
|
|
231
|
+
for row in result.get("perRunWallClock", []):
|
|
232
|
+
if isinstance(row, dict):
|
|
233
|
+
parts.extend(("\n## Run wall clock\n\n", _text_line("Run timestamp", row.get("runTimestamp")), _text_line("Task type", row.get("taskType")), _text_line("Wall clock ms", row.get("wallClockMs"))))
|
|
234
|
+
for timeline in result.get("phaseTimelines", []):
|
|
235
|
+
if not isinstance(timeline, dict):
|
|
236
|
+
continue
|
|
237
|
+
for phase in timeline.get("phases", []):
|
|
238
|
+
if isinstance(phase, dict):
|
|
239
|
+
parts.extend(("\n## Phase\n\n", _text_line("Run timestamp", timeline.get("runTimestamp")), _text_line("Task type", timeline.get("taskType")), _text_line("Phase", phase.get("phase")), _text_line("First at", phase.get("firstAt")), _text_line("Wall ms to next", phase.get("wallMsToNext"))))
|
|
240
|
+
return "".join(parts)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def render_result_text(result: dict) -> str:
|
|
244
|
+
parts = ["# Okstra Time Input\n\n", _text_line("Task key", result.get("taskKey"))]
|
|
245
|
+
total = result.get("grandTotal") if isinstance(result.get("grandTotal"), dict) else {}
|
|
246
|
+
for key, label in (("runs", "Total runs"), ("leadMs", "Total lead ms"), ("workersMs", "Total workers ms"), ("cpuSumMs", "Total CPU sum ms")):
|
|
247
|
+
parts.append(_text_line(label, total.get(key)))
|
|
248
|
+
for row in result.get("byTaskType", []):
|
|
249
|
+
if not isinstance(row, dict):
|
|
250
|
+
continue
|
|
251
|
+
parts.extend(("\n## Task type\n\n", _text_line("Task type", row.get("taskType")), _text_line("Runs", row.get("runs")), _text_line("Lead ms", row.get("leadMs")), _text_line("Workers ms", row.get("workersMs")), _text_line("CPU sum ms", row.get("cpuSumMs"))))
|
|
252
|
+
parts.extend((_worker_text(result), _timeline_text(result)))
|
|
253
|
+
for row in result.get("unavailable", []):
|
|
254
|
+
if isinstance(row, dict):
|
|
255
|
+
parts.extend(("\n## Unavailable run\n\n", _text_line("Run timestamp", row.get("runTimestamp")), _text_line("Task type", row.get("taskType")), _text_line("Reason", row.get("reason"))))
|
|
256
|
+
return "".join(parts)
|
|
257
|
+
|
|
258
|
+
|
|
214
259
|
def main(argv: list[str] | None = None) -> int:
|
|
215
260
|
parser = argparse.ArgumentParser(
|
|
216
261
|
prog="okstra time-report",
|
|
@@ -219,13 +264,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
219
264
|
parser.add_argument("--project-root", default="")
|
|
220
265
|
parser.add_argument("--cwd", default=".")
|
|
221
266
|
parser.add_argument("--json", action="store_true", help="emit JSON (always on)")
|
|
267
|
+
parser.add_argument("--text", action="store_true", help="emit fixed text fields")
|
|
222
268
|
args = parser.parse_args(argv)
|
|
223
269
|
|
|
224
270
|
task_root, project_root = resolve_task_root(args.target, args.project_root, args.cwd)
|
|
225
271
|
runs, _ = load_runs(task_root)
|
|
226
272
|
result = {"ok": True, "taskKey": read_task_key(task_root)}
|
|
227
273
|
result.update(aggregate_time(runs, project_root))
|
|
228
|
-
|
|
274
|
+
output = render_result_text(result) if args.text else json.dumps(result, ensure_ascii=False, indent=2)
|
|
275
|
+
print(output, end="" if args.text else "\n")
|
|
229
276
|
return 0
|
|
230
277
|
|
|
231
278
|
|
|
@@ -13,6 +13,7 @@ from .execution_identity import (
|
|
|
13
13
|
stored_identity,
|
|
14
14
|
)
|
|
15
15
|
from .execution_manifest import read_execution_manifest
|
|
16
|
+
from .json_boundary import load_owned_object
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def run_execution_manifest(run_root: Path) -> ExecutionManifest:
|
|
@@ -83,4 +84,4 @@ def _manifest_path(run_root: Path) -> Path:
|
|
|
83
84
|
|
|
84
85
|
|
|
85
86
|
def _load_json(path: Path) -> dict[str, Any]:
|
|
86
|
-
return
|
|
87
|
+
return load_owned_object(path, artifact="usage identity source")
|
|
@@ -14,6 +14,8 @@ from okstra_ctl.time_report import (
|
|
|
14
14
|
wall_clock_ms,
|
|
15
15
|
load_runs,
|
|
16
16
|
)
|
|
17
|
+
from okstra_ctl.json_boundary import load_owned_object
|
|
18
|
+
from okstra_ctl.fixed_text import line
|
|
17
19
|
from okstra_project import ResolverError, list_project_tasks, resolve_project_root
|
|
18
20
|
|
|
19
21
|
UTC = dt.timezone.utc
|
|
@@ -43,8 +45,8 @@ def _parse_timestamp(value: Any) -> dt.datetime | None:
|
|
|
43
45
|
|
|
44
46
|
def _project_id(project_root: Path) -> str:
|
|
45
47
|
try:
|
|
46
|
-
data =
|
|
47
|
-
|
|
48
|
+
data = load_owned_object(
|
|
49
|
+
project_root / ".okstra" / "project.json", artifact="project config"
|
|
48
50
|
)
|
|
49
51
|
except (OSError, ValueError):
|
|
50
52
|
return ""
|
|
@@ -214,6 +216,9 @@ def build_usage_report(project_root: Path, days: int, now: dt.datetime) -> dict:
|
|
|
214
216
|
rows = sorted(buckets.values(), key=lambda row: row["firstRunAt"])
|
|
215
217
|
for row in rows:
|
|
216
218
|
row["costUsd"] = round(_float_value(row["costUsd"]), 4)
|
|
219
|
+
row["collectionRate"] = (
|
|
220
|
+
row["collectedRuns"] / row["runs"] if row["runs"] else 0.0
|
|
221
|
+
)
|
|
217
222
|
totals = {
|
|
218
223
|
"runs": total_runs,
|
|
219
224
|
"collectedRuns": collected_runs,
|
|
@@ -264,18 +269,67 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
264
269
|
parser.add_argument("--project-root", default="")
|
|
265
270
|
parser.add_argument("--cwd", default=".")
|
|
266
271
|
parser.add_argument("--json", action="store_true", help="emit JSON (always on)")
|
|
272
|
+
parser.add_argument("--text", action="store_true", help="emit fixed text fields")
|
|
267
273
|
args = parser.parse_args(argv)
|
|
268
274
|
try:
|
|
269
275
|
project_root = Path(
|
|
270
276
|
resolve_project_root(explicit_root=args.project_root, cwd=args.cwd)
|
|
271
277
|
).resolve()
|
|
272
278
|
except ResolverError as exc:
|
|
273
|
-
|
|
279
|
+
payload = {"ok": False, "stage": "resolve", "reason": str(exc)}
|
|
280
|
+
print(render_usage_text(payload) if args.text else json.dumps(payload))
|
|
274
281
|
return 2
|
|
275
282
|
result = build_usage_report(project_root, args.days, utc_now())
|
|
276
|
-
print(
|
|
283
|
+
print(
|
|
284
|
+
render_usage_text(result)
|
|
285
|
+
if args.text
|
|
286
|
+
else json.dumps(result, ensure_ascii=False, indent=2)
|
|
287
|
+
)
|
|
277
288
|
return 0
|
|
278
289
|
|
|
279
290
|
|
|
291
|
+
def render_usage_text(payload: dict) -> str:
|
|
292
|
+
"""usage 모델 표면에 승인된 집계만 고정 순서로 투영한다."""
|
|
293
|
+
rows = ["Okstra usage\n"]
|
|
294
|
+
rows.append(line("Status", "ready" if payload.get("ok") else "error"))
|
|
295
|
+
rows.append(line("Project ID", payload.get("projectId")))
|
|
296
|
+
period = payload.get("range") if isinstance(payload.get("range"), dict) else {}
|
|
297
|
+
rows.append(line("Range days", period.get("days")))
|
|
298
|
+
runs = payload.get("runs") if isinstance(payload.get("runs"), dict) else {}
|
|
299
|
+
rows.append(line("Runs total", runs.get("total")))
|
|
300
|
+
rows.append(line("Runs collected", runs.get("collected")))
|
|
301
|
+
rows.append(line("Runs collection rate", runs.get("collectionRate")))
|
|
302
|
+
task_types = payload.get("byTaskType") if isinstance(payload.get("byTaskType"), list) else []
|
|
303
|
+
for index, item in enumerate(task_types, 1):
|
|
304
|
+
if not isinstance(item, dict):
|
|
305
|
+
continue
|
|
306
|
+
rows.append(line(f"Task type {index} name", item.get("taskType")))
|
|
307
|
+
rows.append(line(f"Task type {index} runs", item.get("runs")))
|
|
308
|
+
rows.append(line(f"Task type {index} collected runs", item.get("collectedRuns")))
|
|
309
|
+
rows.append(line(f"Task type {index} collection rate", item.get("collectionRate")))
|
|
310
|
+
rows.append(line(f"Task type {index} raw tokens", item.get("rawTokens")))
|
|
311
|
+
rows.append(line(f"Task type {index} billable tokens", item.get("billableTokens")))
|
|
312
|
+
rows.append(line(f"Task type {index} cost USD", item.get("costUsd")))
|
|
313
|
+
rows.append(line(f"Task type {index} CPU sum ms", item.get("cpuSumMs")))
|
|
314
|
+
rows.append(line(f"Task type {index} wall clock ms", item.get("wallClockMs")))
|
|
315
|
+
totals = payload.get("totals") if isinstance(payload.get("totals"), dict) else {}
|
|
316
|
+
for label, key in (("Raw tokens", "rawTokens"), ("Billable tokens", "billableTokens"),
|
|
317
|
+
("Cost USD", "costUsd"), ("CPU sum ms", "cpuSumMs"),
|
|
318
|
+
("Wall clock ms", "wallClockMs")):
|
|
319
|
+
rows.append(line(label, totals.get(key)))
|
|
320
|
+
for prefix, key in (("Unavailable", "unavailable"), ("Warning", "warnings")):
|
|
321
|
+
values = payload.get(key) if isinstance(payload.get(key), dict) else {}
|
|
322
|
+
for index, (name, count) in enumerate(sorted(values.items()), 1):
|
|
323
|
+
rows.append(line(f"{prefix} {index} reason", name))
|
|
324
|
+
rows.append(line(f"{prefix} {index} count", count))
|
|
325
|
+
models = payload.get("unmatchedModels") if isinstance(payload.get("unmatchedModels"), list) else []
|
|
326
|
+
for index, model in enumerate(models, 1):
|
|
327
|
+
rows.append(line(f"Unmatched model {index} name", model))
|
|
328
|
+
if not payload.get("ok"):
|
|
329
|
+
rows.append(line("Failure stage", payload.get("stage")))
|
|
330
|
+
rows.append(line("Failure reason", payload.get("reason")))
|
|
331
|
+
return "".join(rows)
|
|
332
|
+
|
|
333
|
+
|
|
280
334
|
if __name__ == "__main__":
|
|
281
335
|
raise SystemExit(main(sys.argv[1:]))
|