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
|
@@ -16,6 +16,7 @@ state passing, and are read once at the start.
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
import hashlib
|
|
19
|
+
from argparse import Action, ArgumentError, ArgumentParser, Namespace
|
|
19
20
|
import json
|
|
20
21
|
import os
|
|
21
22
|
import re
|
|
@@ -144,6 +145,11 @@ from .agent_invocation import (
|
|
|
144
145
|
verify_agent_invocation,
|
|
145
146
|
)
|
|
146
147
|
from .report_contract import CURRENT_REPORT_SCHEMA_VERSION
|
|
148
|
+
from .json_boundary import (
|
|
149
|
+
JsonBoundaryError,
|
|
150
|
+
load_owned_object,
|
|
151
|
+
write_owned_object_atomic,
|
|
152
|
+
)
|
|
147
153
|
from .path_resolve import relative_to_project_root, resolve_user_file
|
|
148
154
|
from .render import (
|
|
149
155
|
apply_lead_prompt_defaults,
|
|
@@ -257,8 +263,8 @@ def _load_final_report_data_if_present(report_path: Path) -> tuple[Path, dict] |
|
|
|
257
263
|
if not data_path.is_file():
|
|
258
264
|
return None
|
|
259
265
|
try:
|
|
260
|
-
data =
|
|
261
|
-
except
|
|
266
|
+
data = load_owned_object(data_path, artifact="approved plan final report")
|
|
267
|
+
except JsonBoundaryError as exc:
|
|
262
268
|
raise PrepareError(
|
|
263
269
|
f"approved plan data.json is invalid JSON: {data_path}: {exc}"
|
|
264
270
|
) from exc
|
|
@@ -671,7 +677,6 @@ def _record_start(
|
|
|
671
677
|
"""record_start hook 호출. okstra-central.sh 의 bash wrapper 와 같은 동작
|
|
672
678
|
이지만 python 직접 호출이라 환경 변수 의존 없음.
|
|
673
679
|
"""
|
|
674
|
-
import json as _json
|
|
675
680
|
from datetime import datetime, timezone
|
|
676
681
|
from okstra_ctl import record_start
|
|
677
682
|
from .locks import central_lock
|
|
@@ -686,11 +691,15 @@ def _record_start(
|
|
|
686
691
|
(home / f).touch(exist_ok=True)
|
|
687
692
|
state_file = home / "state.json"
|
|
688
693
|
if not state_file.exists():
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
+
write_owned_object_atomic(
|
|
695
|
+
state_file,
|
|
696
|
+
{
|
|
697
|
+
"schemaVersion": "1",
|
|
698
|
+
"createdAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
699
|
+
"backfilledAt": None,
|
|
700
|
+
},
|
|
701
|
+
artifact="global state",
|
|
702
|
+
)
|
|
694
703
|
os.chmod(state_file, 0o600)
|
|
695
704
|
# 중앙 인덱스 락은 reconcile.py 와 공유하는 central_lock 단일 기준점을 쓴다.
|
|
696
705
|
# 직접 flock 을 손으로 깔면 락 파일/모드가 두 경로에서 갈라질 수 있다.
|
|
@@ -1533,8 +1542,8 @@ def validate_project_qa_commands(task_type: str, project_root: Path) -> None:
|
|
|
1533
1542
|
if not project_json.is_file():
|
|
1534
1543
|
return
|
|
1535
1544
|
try:
|
|
1536
|
-
project_meta =
|
|
1537
|
-
except (OSError,
|
|
1545
|
+
project_meta = load_owned_object(project_json, artifact="project config")
|
|
1546
|
+
except (OSError, JsonBoundaryError) as exc:
|
|
1538
1547
|
raise PrepareError(
|
|
1539
1548
|
f"project.json read failed at {project_json}: {exc}"
|
|
1540
1549
|
) from exc
|
|
@@ -1569,11 +1578,13 @@ def _apply_qa_waiver_if_requested(inp: "PrepareInputs", project_root: Path) -> N
|
|
|
1569
1578
|
):
|
|
1570
1579
|
if not manifest_path.is_file():
|
|
1571
1580
|
raise PrepareError(f"--qa-waiver: conformance manifest not found at {manifest_path}")
|
|
1572
|
-
manifest =
|
|
1581
|
+
manifest = load_owned_object(manifest_path, artifact="conformance manifest")
|
|
1573
1582
|
when = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
1574
1583
|
if not apply_qa_waiver(manifest, stage_key, reason, at=when):
|
|
1575
1584
|
raise PrepareError(f"--qa-waiver: stageKey {stage_key!r} not in manifest {manifest_path}")
|
|
1576
|
-
|
|
1585
|
+
write_owned_object_atomic(
|
|
1586
|
+
manifest_path, manifest, artifact="conformance manifest"
|
|
1587
|
+
)
|
|
1577
1588
|
|
|
1578
1589
|
|
|
1579
1590
|
def _clear_stale_stage_waiver(inp: "PrepareInputs", project_root: Path, stage: int) -> None:
|
|
@@ -1591,7 +1602,7 @@ def _clear_stale_stage_waiver(inp: "PrepareInputs", project_root: Path, stage: i
|
|
|
1591
1602
|
)
|
|
1592
1603
|
if not manifest_path.is_file():
|
|
1593
1604
|
return
|
|
1594
|
-
manifest =
|
|
1605
|
+
manifest = load_owned_object(manifest_path, artifact="conformance manifest")
|
|
1595
1606
|
entries = manifest.get("entries") if isinstance(manifest, dict) else None
|
|
1596
1607
|
if not isinstance(entries, list):
|
|
1597
1608
|
return
|
|
@@ -1611,8 +1622,8 @@ def _clear_stale_stage_waiver(inp: "PrepareInputs", project_root: Path, stage: i
|
|
|
1611
1622
|
if parsed is not None and parsed[0] == stage_key:
|
|
1612
1623
|
return # user intentionally waived this stage for this run
|
|
1613
1624
|
if clear_qa_waiver(manifest, stage_key):
|
|
1614
|
-
|
|
1615
|
-
|
|
1625
|
+
write_owned_object_atomic(
|
|
1626
|
+
manifest_path, manifest, artifact="conformance manifest"
|
|
1616
1627
|
)
|
|
1617
1628
|
|
|
1618
1629
|
|
|
@@ -2124,8 +2135,8 @@ def _read_model_defaults(path: Path) -> dict[str, tuple[str, ...]]:
|
|
|
2124
2135
|
if not path.is_file():
|
|
2125
2136
|
return {}
|
|
2126
2137
|
try:
|
|
2127
|
-
payload =
|
|
2128
|
-
except (OSError,
|
|
2138
|
+
payload = load_owned_object(path, artifact="model defaults config")
|
|
2139
|
+
except (OSError, JsonBoundaryError) as exc:
|
|
2129
2140
|
raise PrepareError(f"model defaults file is invalid: {path}: {exc}") from exc
|
|
2130
2141
|
raw_defaults = payload.get("modelDefaults", {}) if isinstance(payload, dict) else {}
|
|
2131
2142
|
if not isinstance(raw_defaults, dict):
|
|
@@ -3327,9 +3338,10 @@ def _render_lead_prompt_and_snapshot(
|
|
|
3327
3338
|
inp.task_type,
|
|
3328
3339
|
installed_version(),
|
|
3329
3340
|
)
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3341
|
+
write_owned_object_atomic(
|
|
3342
|
+
Path(ctx["FINAL_REPORT_SCHEMA_PATH"]),
|
|
3343
|
+
_excerpt,
|
|
3344
|
+
artifact="final report schema excerpt",
|
|
3333
3345
|
)
|
|
3334
3346
|
except Exception: # noqa: BLE001 — advisory artifact; never fail prep over it
|
|
3335
3347
|
pass
|
|
@@ -3339,7 +3351,9 @@ def _render_lead_prompt_and_snapshot(
|
|
|
3339
3351
|
json.loads(ctx["INVOCATION_ASSIGNMENTS_JSON"])["lead"]
|
|
3340
3352
|
)
|
|
3341
3353
|
identity = invocation_execution_identity_from_manifest(
|
|
3342
|
-
|
|
3354
|
+
load_owned_object(
|
|
3355
|
+
Path(ctx["RUN_MANIFEST_PATH"]), artifact="run manifest"
|
|
3356
|
+
),
|
|
3343
3357
|
assignment=assignment,
|
|
3344
3358
|
assignment_ref="lead",
|
|
3345
3359
|
duty_id="lead",
|
|
@@ -3492,8 +3506,8 @@ def _read_existing_manifest(manifest_path: Path) -> dict:
|
|
|
3492
3506
|
if not manifest_path.exists():
|
|
3493
3507
|
return {}
|
|
3494
3508
|
try:
|
|
3495
|
-
return
|
|
3496
|
-
except (OSError,
|
|
3509
|
+
return load_owned_object(manifest_path, artifact="run manifest")
|
|
3510
|
+
except (OSError, JsonBoundaryError):
|
|
3497
3511
|
return {}
|
|
3498
3512
|
|
|
3499
3513
|
|
|
@@ -3987,7 +4001,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
3987
4001
|
try:
|
|
3988
4002
|
json.loads(runtime_resolution_json or "{}")
|
|
3989
4003
|
except json.JSONDecodeError as exc:
|
|
3990
|
-
raise PrepareError(f"invalid
|
|
4004
|
+
raise PrepareError(f"invalid runtime resolution data: {exc}") from exc
|
|
3991
4005
|
# Probed once here and reused below, so the gate and the manifest cannot
|
|
3992
4006
|
# disagree about which backend this run is on.
|
|
3993
4007
|
terminal_backend = detect_terminal_backend()
|
|
@@ -4302,11 +4316,77 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4302
4316
|
)
|
|
4303
4317
|
|
|
4304
4318
|
|
|
4319
|
+
class _StoreRuntimeSingleton(Action):
|
|
4320
|
+
def __call__(
|
|
4321
|
+
self,
|
|
4322
|
+
parser: ArgumentParser,
|
|
4323
|
+
namespace: Namespace,
|
|
4324
|
+
values: object,
|
|
4325
|
+
option_string: str | None = None,
|
|
4326
|
+
) -> None:
|
|
4327
|
+
seen = getattr(namespace, "_runtime_singletons_seen", frozenset())
|
|
4328
|
+
if self.dest in seen:
|
|
4329
|
+
raise ArgumentError(
|
|
4330
|
+
self, f"{option_string} may be supplied only once"
|
|
4331
|
+
)
|
|
4332
|
+
setattr(namespace, self.dest, True if self.nargs == 0 else values)
|
|
4333
|
+
setattr(namespace, "_runtime_singletons_seen", seen | {self.dest})
|
|
4334
|
+
|
|
4335
|
+
|
|
4336
|
+
def _add_runtime_resolution_arguments(parser: ArgumentParser) -> None:
|
|
4337
|
+
runtime_ids = (*default_host_registry().ids(), "all")
|
|
4338
|
+
parser.add_argument(
|
|
4339
|
+
"--lead-runtime",
|
|
4340
|
+
action=_StoreRuntimeSingleton,
|
|
4341
|
+
default="claude-code",
|
|
4342
|
+
choices=runtime_ids,
|
|
4343
|
+
dest="lead_runtime",
|
|
4344
|
+
help=(
|
|
4345
|
+
"Lead runtime adapter. Default: claude-code. `codex` renders "
|
|
4346
|
+
"Codex CLI artifact bundles; `antigravity` renders Antigravity CLI "
|
|
4347
|
+
"artifact bundles; `external` renders neutral bootstrap bundles for "
|
|
4348
|
+
"manifest inspection until a dispatch backend is enabled."
|
|
4349
|
+
),
|
|
4350
|
+
)
|
|
4351
|
+
parser.add_argument(
|
|
4352
|
+
"--lead-runtime-request", action=_StoreRuntimeSingleton, default=""
|
|
4353
|
+
)
|
|
4354
|
+
parser.add_argument(
|
|
4355
|
+
"--runtime-resolution-launch-mode",
|
|
4356
|
+
action=_StoreRuntimeSingleton,
|
|
4357
|
+
choices=("lead", "team", "catalog"),
|
|
4358
|
+
default="",
|
|
4359
|
+
)
|
|
4360
|
+
parser.add_argument(
|
|
4361
|
+
"--runtime-resolution-install-target", action="append", default=[]
|
|
4362
|
+
)
|
|
4363
|
+
for flag in (
|
|
4364
|
+
"--runtime-resolution-host",
|
|
4365
|
+
"--runtime-resolution-reason",
|
|
4366
|
+
"--runtime-resolution-fallback-from",
|
|
4367
|
+
):
|
|
4368
|
+
parser.add_argument(flag, action=_StoreRuntimeSingleton, default="")
|
|
4369
|
+
parser.add_argument(
|
|
4370
|
+
"--runtime-resolution-confidence",
|
|
4371
|
+
action=_StoreRuntimeSingleton,
|
|
4372
|
+
choices=("none", "low", "medium", "high"),
|
|
4373
|
+
default="",
|
|
4374
|
+
)
|
|
4375
|
+
parser.add_argument("--runtime-resolution-warning", action="append", default=[])
|
|
4376
|
+
parser.add_argument(
|
|
4377
|
+
"--runtime-resolution-catalog-hosts-present",
|
|
4378
|
+
action=_StoreRuntimeSingleton,
|
|
4379
|
+
nargs=0,
|
|
4380
|
+
default=False,
|
|
4381
|
+
)
|
|
4382
|
+
parser.add_argument(
|
|
4383
|
+
"--runtime-resolution-catalog-host-id", action="append", default=[]
|
|
4384
|
+
)
|
|
4385
|
+
|
|
4386
|
+
|
|
4305
4387
|
def build_prepare_argument_parser():
|
|
4306
4388
|
"""Build the shared flat-argument parser used by public prepare shims."""
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
p = argparse.ArgumentParser()
|
|
4389
|
+
p = ArgumentParser()
|
|
4310
4390
|
p.add_argument("--workspace-root", required=True)
|
|
4311
4391
|
p.add_argument("--project-root", required=True)
|
|
4312
4392
|
p.add_argument("--project-id", required=True)
|
|
@@ -4348,30 +4428,7 @@ def build_prepare_argument_parser():
|
|
|
4348
4428
|
p.add_argument("--worker-model", default="", dest="worker_models_raw")
|
|
4349
4429
|
p.add_argument("--report-writer-provider", default="")
|
|
4350
4430
|
p.add_argument("--report-writer-model", default="")
|
|
4351
|
-
p
|
|
4352
|
-
"--lead-runtime",
|
|
4353
|
-
default="claude-code",
|
|
4354
|
-
choices=list(default_host_registry().ids()),
|
|
4355
|
-
dest="lead_runtime",
|
|
4356
|
-
help=(
|
|
4357
|
-
"Lead runtime adapter. Default: claude-code. `codex` renders "
|
|
4358
|
-
"Codex CLI artifact bundles; `antigravity` renders Antigravity CLI "
|
|
4359
|
-
"artifact bundles; `external` renders neutral bootstrap bundles for "
|
|
4360
|
-
"manifest inspection until a dispatch backend is enabled."
|
|
4361
|
-
),
|
|
4362
|
-
)
|
|
4363
|
-
p.add_argument(
|
|
4364
|
-
"--lead-runtime-request",
|
|
4365
|
-
default="",
|
|
4366
|
-
dest="lead_runtime_request",
|
|
4367
|
-
help="Original lead runtime request before Node-side resolution, e.g. auto.",
|
|
4368
|
-
)
|
|
4369
|
-
p.add_argument(
|
|
4370
|
-
"--runtime-resolution-json",
|
|
4371
|
-
default="",
|
|
4372
|
-
dest="runtime_resolution_json",
|
|
4373
|
-
help="Node-side runtime resolution payload serialized as JSON.",
|
|
4374
|
-
)
|
|
4431
|
+
_add_runtime_resolution_arguments(p)
|
|
4375
4432
|
p.add_argument("--executor", default="")
|
|
4376
4433
|
p.add_argument("--critic", default="")
|
|
4377
4434
|
p.add_argument("--related-tasks", default="", dest="related_tasks_raw")
|
|
@@ -4486,6 +4543,51 @@ def build_prepare_argument_parser():
|
|
|
4486
4543
|
return p
|
|
4487
4544
|
|
|
4488
4545
|
|
|
4546
|
+
def _runtime_resolution_json_from_args(args: Namespace) -> str:
|
|
4547
|
+
values = {
|
|
4548
|
+
"launch mode": args.runtime_resolution_launch_mode,
|
|
4549
|
+
"host": args.runtime_resolution_host,
|
|
4550
|
+
"confidence": args.runtime_resolution_confidence,
|
|
4551
|
+
"reason": args.runtime_resolution_reason,
|
|
4552
|
+
}
|
|
4553
|
+
optional_values = (
|
|
4554
|
+
args.runtime_resolution_install_target,
|
|
4555
|
+
args.runtime_resolution_fallback_from,
|
|
4556
|
+
args.runtime_resolution_warning,
|
|
4557
|
+
args.runtime_resolution_catalog_hosts_present,
|
|
4558
|
+
args.runtime_resolution_catalog_host_id,
|
|
4559
|
+
)
|
|
4560
|
+
if not any(values.values()) and not any(optional_values):
|
|
4561
|
+
return "{}"
|
|
4562
|
+
missing = [name for name, value in values.items() if not value]
|
|
4563
|
+
if missing:
|
|
4564
|
+
raise PrepareError(
|
|
4565
|
+
"incomplete runtime resolution arguments: " + ", ".join(missing)
|
|
4566
|
+
)
|
|
4567
|
+
if (
|
|
4568
|
+
args.runtime_resolution_catalog_host_id
|
|
4569
|
+
and not args.runtime_resolution_catalog_hosts_present
|
|
4570
|
+
):
|
|
4571
|
+
raise PrepareError(
|
|
4572
|
+
"runtime resolution catalog host IDs require the presence marker"
|
|
4573
|
+
)
|
|
4574
|
+
payload: dict[str, object] = {
|
|
4575
|
+
"ok": True,
|
|
4576
|
+
"requestedRuntime": args.lead_runtime_request or args.lead_runtime,
|
|
4577
|
+
"resolvedRuntime": args.lead_runtime,
|
|
4578
|
+
"launchMode": args.runtime_resolution_launch_mode,
|
|
4579
|
+
"installTargets": args.runtime_resolution_install_target,
|
|
4580
|
+
"host": args.runtime_resolution_host,
|
|
4581
|
+
"confidence": args.runtime_resolution_confidence,
|
|
4582
|
+
"reason": args.runtime_resolution_reason,
|
|
4583
|
+
"fallbackFrom": args.runtime_resolution_fallback_from or None,
|
|
4584
|
+
"warnings": args.runtime_resolution_warning,
|
|
4585
|
+
}
|
|
4586
|
+
if args.runtime_resolution_catalog_hosts_present:
|
|
4587
|
+
payload["catalogHostIds"] = args.runtime_resolution_catalog_host_id
|
|
4588
|
+
return json.dumps(payload, ensure_ascii=False)
|
|
4589
|
+
|
|
4590
|
+
|
|
4489
4591
|
def main(argv: list[str]) -> int:
|
|
4490
4592
|
"""Parse one public prepare invocation and emit its rendered bundle."""
|
|
4491
4593
|
args = build_prepare_argument_parser().parse_args(argv)
|
|
@@ -4536,7 +4638,8 @@ def main(argv: list[str]) -> int:
|
|
|
4536
4638
|
host_session_context = deserialize_host_session_context(
|
|
4537
4639
|
args.host_session_context_json
|
|
4538
4640
|
)
|
|
4539
|
-
|
|
4641
|
+
runtime_resolution_json = _runtime_resolution_json_from_args(args)
|
|
4642
|
+
except (ModelSelectionInputError, PrepareError) as exc:
|
|
4540
4643
|
print(str(exc), file=__import__("sys").stderr)
|
|
4541
4644
|
return 1
|
|
4542
4645
|
if not args.host_session_context_json:
|
|
@@ -4576,7 +4679,7 @@ def main(argv: list[str]) -> int:
|
|
|
4576
4679
|
report_writer_model=args.report_writer_model,
|
|
4577
4680
|
lead_runtime=args.lead_runtime,
|
|
4578
4681
|
lead_runtime_request=args.lead_runtime_request,
|
|
4579
|
-
runtime_resolution_json=
|
|
4682
|
+
runtime_resolution_json=runtime_resolution_json,
|
|
4580
4683
|
executor=args.executor,
|
|
4581
4684
|
critic=args.critic,
|
|
4582
4685
|
related_tasks_raw=args.related_tasks_raw,
|
|
@@ -4644,17 +4747,17 @@ def _lead_launch_payload(ctx: dict[str, object]) -> dict[str, object]:
|
|
|
4644
4747
|
project_root = Path(str(ctx["PROJECT_ROOT"]))
|
|
4645
4748
|
run_manifest_path = Path(str(ctx["RUN_MANIFEST_PATH"]))
|
|
4646
4749
|
try:
|
|
4647
|
-
manifest =
|
|
4750
|
+
manifest = load_owned_object(run_manifest_path, artifact="run manifest")
|
|
4648
4751
|
resources = manifest["resources"]
|
|
4649
4752
|
assignment = agent_model_assignment_from_payload(
|
|
4650
4753
|
manifest["invocationAssignments"]["lead"]
|
|
4651
4754
|
)
|
|
4652
4755
|
prompt_file = project_root / resources["leadExecutionPromptPath"]
|
|
4653
4756
|
metadata_path = project_root / resources["leadPromptMetadataPath"]
|
|
4654
|
-
metadata =
|
|
4757
|
+
metadata = load_owned_object(metadata_path, artifact="lead prompt metadata")
|
|
4655
4758
|
if metadata["prompt"]["path"] != resources["leadExecutionPromptPath"]:
|
|
4656
4759
|
raise KeyError("lead prompt resource does not match metadata")
|
|
4657
|
-
except (OSError,
|
|
4760
|
+
except (OSError, JsonBoundaryError, KeyError, TypeError, AgentInvocationError) as exc:
|
|
4658
4761
|
raise PrepareError("lead invocation authority is incomplete") from exc
|
|
4659
4762
|
errors = verify_agent_invocation(
|
|
4660
4763
|
metadata_path,
|
|
@@ -19,6 +19,7 @@ from okstra_ctl.paths import okstra_home, resolve_under_root
|
|
|
19
19
|
from okstra_ctl.reconcile import NON_TERMINAL_RECENT_STATUSES
|
|
20
20
|
from okstra_ctl.task_target import project_rel
|
|
21
21
|
from okstra_ctl.workflow import PHASE_SEQUENCE
|
|
22
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
22
23
|
|
|
23
24
|
# 불변식 이름은 여기서만 정의한다. `INVARIANTS` 와 방출부가 같은 상수를 봐야,
|
|
24
25
|
# 이름을 하나 더할 때 한쪽만 고치고도 스위트가 green 인 상태가 생기지 않는다 —
|
|
@@ -74,8 +75,8 @@ _RECORDED_WORKER_STATUSES = frozenset(
|
|
|
74
75
|
|
|
75
76
|
def _load_json(path: Path) -> dict:
|
|
76
77
|
try:
|
|
77
|
-
loaded =
|
|
78
|
-
except
|
|
78
|
+
loaded = load_owned_object(path, artifact="run audit input")
|
|
79
|
+
except JsonBoundaryError:
|
|
79
80
|
return {}
|
|
80
81
|
return loaded if isinstance(loaded, dict) else {}
|
|
81
82
|
|
|
@@ -26,6 +26,7 @@ from typing import Iterator, Optional
|
|
|
26
26
|
from okstra_project.dirs import okstra_home
|
|
27
27
|
|
|
28
28
|
from .path_hints import compact_run_context, hydrate_run_context
|
|
29
|
+
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
29
30
|
from .paths import compute_run_paths, task_runs_dir
|
|
30
31
|
|
|
31
32
|
|
|
@@ -66,8 +67,8 @@ def latest_run_inputs(
|
|
|
66
67
|
return {}
|
|
67
68
|
candidates.sort(key=lambda x: -x[0])
|
|
68
69
|
try:
|
|
69
|
-
data =
|
|
70
|
-
except
|
|
70
|
+
data = load_owned_object(candidates[0][1], artifact="run context")
|
|
71
|
+
except JsonBoundaryError:
|
|
71
72
|
return {}
|
|
72
73
|
inputs = data.get("inputs")
|
|
73
74
|
return inputs if isinstance(inputs, dict) else {}
|
|
@@ -129,11 +130,7 @@ def consumers_mutex(plan_run_root: Path) -> Iterator[None]:
|
|
|
129
130
|
|
|
130
131
|
|
|
131
132
|
def _atomic_write_json(path: Path, payload: dict) -> None:
|
|
132
|
-
path
|
|
133
|
-
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
134
|
-
tmp.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
|
|
135
|
-
encoding="utf-8")
|
|
136
|
-
os.replace(tmp, path)
|
|
133
|
+
write_owned_object_atomic(path, payload, artifact="run context")
|
|
137
134
|
|
|
138
135
|
|
|
139
136
|
def _run_context_filename(task_type_segment: str, seq: str) -> str:
|
|
@@ -240,7 +237,7 @@ def read_run_context(run_manifests_dir: Path, task_type_segment: str,
|
|
|
240
237
|
path = Path(run_manifests_dir) / _run_context_filename(task_type_segment, seq)
|
|
241
238
|
if not path.is_file():
|
|
242
239
|
return None
|
|
243
|
-
payload =
|
|
240
|
+
payload = load_owned_object(path, artifact="run context")
|
|
244
241
|
context = hydrate_run_context(payload)
|
|
245
242
|
analysis = payload.get("analysis")
|
|
246
243
|
if isinstance(analysis, dict):
|
|
@@ -268,4 +265,4 @@ def read_run_inputs(run_manifests_dir: Path, task_type_segment: str,
|
|
|
268
265
|
path = Path(run_manifests_dir) / _run_inputs_filename(task_type_segment, seq)
|
|
269
266
|
if not path.is_file():
|
|
270
267
|
return None
|
|
271
|
-
return
|
|
268
|
+
return load_owned_object(path, artifact="run inputs")
|
|
@@ -8,14 +8,12 @@ slim row 에 raw 로 보존한다 — taskGroup/taskId 와 동일한 이유다.
|
|
|
8
8
|
"""
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
import json
|
|
12
|
-
import os
|
|
13
11
|
from pathlib import Path
|
|
14
12
|
from typing import Optional
|
|
15
13
|
|
|
16
14
|
from .ids import build_run_id, parse_run_id
|
|
17
15
|
from .invocation import invocation_path
|
|
18
|
-
from .jsonl import read_jsonl
|
|
16
|
+
from .jsonl import read_jsonl, rewrite_jsonl
|
|
19
17
|
|
|
20
18
|
DERIVED_FIELDS = ("taskType", "runSeq", "taskKey", "invocationFile")
|
|
21
19
|
|
|
@@ -103,11 +101,7 @@ def _rewrite_slim(path: Path) -> int:
|
|
|
103
101
|
rows = read_jsonl(path)
|
|
104
102
|
if not rows:
|
|
105
103
|
return 0
|
|
106
|
-
|
|
107
|
-
with tmp.open("w") as f:
|
|
108
|
-
for r in rows:
|
|
109
|
-
f.write(json.dumps(slim_run_row(r), separators=(",", ":")) + "\n")
|
|
110
|
-
os.replace(tmp, path)
|
|
104
|
+
rewrite_jsonl(path, [slim_run_row(row) for row in rows])
|
|
111
105
|
return len(rows)
|
|
112
106
|
|
|
113
107
|
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from .md_table import is_separator_row, split_pipe_row
|
|
12
|
+
from .json_boundary import external_user_json_source, load_external_json
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
_ROOT_FIELDS = {"schemaVersion", "tasks"}
|
|
@@ -404,8 +405,10 @@ def _strict_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
|
404
405
|
|
|
405
406
|
|
|
406
407
|
def load_schedule_selection(path: Path) -> tuple[SelectionTask, ...]:
|
|
407
|
-
|
|
408
|
-
|
|
408
|
+
# 외부 입력: okstra-schedule 사용자가 선택한 작업 집합 문서다.
|
|
409
|
+
payload = load_external_json(
|
|
410
|
+
external_user_json_source(path),
|
|
411
|
+
artifact="schedule selection",
|
|
409
412
|
object_pairs_hook=_strict_json_object,
|
|
410
413
|
)
|
|
411
414
|
root = _require_object(payload, "selection")
|
|
@@ -27,6 +27,8 @@ from collections.abc import Sequence
|
|
|
27
27
|
from dataclasses import dataclass
|
|
28
28
|
from pathlib import Path
|
|
29
29
|
|
|
30
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
31
|
+
|
|
30
32
|
from .final_report_schema import load_schema_version
|
|
31
33
|
from .report_contract import TASK_TYPE_DATA_PROPERTY
|
|
32
34
|
|
|
@@ -160,8 +162,8 @@ def excerpt_contract_skew(
|
|
|
160
162
|
if not installed:
|
|
161
163
|
return None
|
|
162
164
|
try:
|
|
163
|
-
excerpt =
|
|
164
|
-
except
|
|
165
|
+
excerpt = load_owned_object(excerpt_path, artifact="schema excerpt")
|
|
166
|
+
except JsonBoundaryError:
|
|
165
167
|
return None
|
|
166
168
|
if not isinstance(excerpt, dict):
|
|
167
169
|
return None
|
|
@@ -50,13 +50,39 @@ class SessionTranscript:
|
|
|
50
50
|
결론과 okstra 자신의 기록이 그렇다. 잘린 도구 에코는 세부를 잃지만
|
|
51
51
|
잘린 결론은 사후 분석 전체를 잃는다.
|
|
52
52
|
"""
|
|
53
|
+
row = self._row(speaker, line)
|
|
54
|
+
self._show(row)
|
|
55
|
+
self._keep(row, capped=capped)
|
|
56
|
+
|
|
57
|
+
def write_event(
|
|
58
|
+
self, speaker: str, *, screen: list[str], archive: list[str]
|
|
59
|
+
) -> None:
|
|
60
|
+
"""한 이벤트의 두 투영을 각자의 목적지로 보낸다.
|
|
61
|
+
|
|
62
|
+
화면은 요약을 받고 기록은 본문까지 받는다. 둘 중 하나를 골라 양쪽에
|
|
63
|
+
쓰던 동안에는 pane 이 붙은 run 의 기록이 화면 투영으로 대체됐다 —
|
|
64
|
+
도구 결과가 `← done (23 bytes)` 한 줄로만 남고 그 23 바이트가 무엇이
|
|
65
|
+
었는지는 파일 어디에도 없었다. antigravity `view_file` 은 파일 내용이
|
|
66
|
+
아니라 `721 lines, 114306 bytes` 같은 요약을 돌려주므로, 본문이 빠진
|
|
67
|
+
기록에서는 46KB 를 읽은 호출과 잘린 호출이 같은 줄로 보인다.
|
|
68
|
+
"""
|
|
69
|
+
for line in screen:
|
|
70
|
+
self._show(self._row(speaker, line))
|
|
71
|
+
for line in archive:
|
|
72
|
+
self._keep(self._row(speaker, line), capped=True)
|
|
73
|
+
|
|
74
|
+
def _row(self, speaker: str, line: str) -> str:
|
|
53
75
|
# 패딩이 본문 앞 공백을 겸한다. 뒤에 공백을 한 칸 더 넣으면
|
|
54
76
|
# `[worker:grok]`(13칸) 줄이 두 칸이 되고 `[okstra]` 정렬이 깨진다.
|
|
55
77
|
label = f"[{speaker}]".ljust(_SPEAKER_WIDTH)
|
|
56
|
-
|
|
78
|
+
return f"{self._clock()} {label}{line}".rstrip()
|
|
79
|
+
|
|
80
|
+
def _show(self, row: str) -> None:
|
|
57
81
|
# 화면은 상한과 무관하다. 사람이 보고 있는 것을 잘라 낼 이유가 없다.
|
|
58
82
|
if self._live:
|
|
59
83
|
print(row, flush=True)
|
|
84
|
+
|
|
85
|
+
def _keep(self, row: str, *, capped: bool) -> None:
|
|
60
86
|
if not capped:
|
|
61
87
|
self._append(row)
|
|
62
88
|
return
|