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
|
@@ -14,6 +14,7 @@ from dataclasses import dataclass
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Dict, List, Optional, Tuple
|
|
16
16
|
|
|
17
|
+
from .fixed_text import line
|
|
17
18
|
from .worktree import _resolve_commit_sha
|
|
18
19
|
|
|
19
20
|
|
|
@@ -202,7 +203,7 @@ def guidance(*, plan_run_root: Path, project_id: str, task_group: str,
|
|
|
202
203
|
f"--task-id {task_id} --work-category {work_category}")
|
|
203
204
|
return ("Recorded stage SHAs no longer match the git history "
|
|
204
205
|
"(external rebase/squash/amend?). Inspect and reconcile:\n"
|
|
205
|
-
f" {base} --check --
|
|
206
|
+
f" {base} --check --text\n"
|
|
206
207
|
f" {base} --apply --stage <N> --use-ref <branch|sha>")
|
|
207
208
|
|
|
208
209
|
|
|
@@ -272,9 +273,8 @@ def _items_as_json(items: List[StaleItem]) -> list:
|
|
|
272
273
|
"suggested_commit": i.suggested_commit} for i in items]
|
|
273
274
|
|
|
274
275
|
|
|
275
|
-
def
|
|
276
|
+
def _parse_args(argv: Optional[list]):
|
|
276
277
|
import argparse
|
|
277
|
-
import json as _json
|
|
278
278
|
|
|
279
279
|
p = argparse.ArgumentParser(
|
|
280
280
|
prog="okstra git-reconcile",
|
|
@@ -294,11 +294,44 @@ def main(argv: Optional[list] = None) -> int:
|
|
|
294
294
|
p.add_argument("--use-ref", default="")
|
|
295
295
|
p.add_argument("--reset-anchor", default="")
|
|
296
296
|
p.add_argument("--json", action="store_true")
|
|
297
|
+
p.add_argument("--text", action="store_true")
|
|
297
298
|
a = p.parse_args(argv)
|
|
298
299
|
if a.check and a.apply:
|
|
299
300
|
p.error("--check and --apply are mutually exclusive")
|
|
300
301
|
if a.apply and a.stage is not None and not a.use_ref:
|
|
301
302
|
p.error("--stage requires --use-ref (confirm 항목은 명시 ref 로만 보정된다)")
|
|
303
|
+
return a
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _emit_result(payload: dict, *, as_json: bool, as_text: bool) -> None:
|
|
307
|
+
import json as _json
|
|
308
|
+
|
|
309
|
+
print(
|
|
310
|
+
render_git_reconcile_text(payload)
|
|
311
|
+
if as_text
|
|
312
|
+
else _json.dumps(payload, ensure_ascii=False, indent=None if as_json else 2)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def render_git_reconcile_text(payload: dict) -> str:
|
|
317
|
+
"""reconcile 결과의 승인된 분류만 고정 순서로 투영한다."""
|
|
318
|
+
rows = ["Okstra git reconcile\n"]
|
|
319
|
+
rows.append(line("Status", "ready" if payload.get("ok", True) else "error"))
|
|
320
|
+
items = payload.get("items") if isinstance(payload.get("items"), list) else []
|
|
321
|
+
rows.append(line("Item count", len(items)))
|
|
322
|
+
for index, item in enumerate(items, 1):
|
|
323
|
+
if not isinstance(item, dict):
|
|
324
|
+
continue
|
|
325
|
+
rows.append(line(f"Item {index} kind", item.get("kind")))
|
|
326
|
+
rows.append(line(f"Item {index} classification", item.get("classification")))
|
|
327
|
+
rows.append(line(f"Item {index} reason", item.get("reason")))
|
|
328
|
+
if "error" in payload:
|
|
329
|
+
rows.append(line("Failure reason", payload.get("error")))
|
|
330
|
+
return "".join(rows)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def main(argv: Optional[list] = None) -> int:
|
|
334
|
+
a = _parse_args(argv)
|
|
302
335
|
|
|
303
336
|
kw = dict(project_root=Path(a.project_root).resolve(),
|
|
304
337
|
plan_run_root=Path(a.plan_run_root).resolve(),
|
|
@@ -310,17 +343,16 @@ def main(argv: Optional[list] = None) -> int:
|
|
|
310
343
|
reset_anchor=a.reset_anchor)
|
|
311
344
|
out = {"applied": result["applied"],
|
|
312
345
|
"remaining_confirm": _items_as_json(result["remaining_confirm"])}
|
|
313
|
-
|
|
314
|
-
indent=None if a.json else 2))
|
|
346
|
+
_emit_result(out, as_json=a.json, as_text=a.text)
|
|
315
347
|
return 2 if result["remaining_confirm"] else 0
|
|
316
348
|
items = classify_task(**kw)
|
|
317
349
|
stale = [i for i in items if i.classification != "ok"]
|
|
318
|
-
|
|
319
|
-
|
|
350
|
+
out = {"items": _items_as_json(stale)}
|
|
351
|
+
_emit_result(out, as_json=a.json, as_text=a.text)
|
|
320
352
|
return 2 if any(i.classification == "confirm" for i in stale) else 0
|
|
321
353
|
except ReconcileError as exc:
|
|
322
|
-
|
|
323
|
-
|
|
354
|
+
out = {"ok": False, "error": str(exc)}
|
|
355
|
+
_emit_result(out, as_json=a.json, as_text=a.text)
|
|
324
356
|
return 1
|
|
325
357
|
|
|
326
358
|
|
|
@@ -15,6 +15,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
|
15
15
|
from . import consumers, stage_targets, worktree_registry
|
|
16
16
|
from .final_report_paths import final_report_markdown_path
|
|
17
17
|
from .paths import RunRef
|
|
18
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
18
19
|
from .release_gate import (
|
|
19
20
|
blocking_condition_ids,
|
|
20
21
|
release_handoff_allowed,
|
|
@@ -75,8 +76,8 @@ def latest_whole_task_fv_release_ready(project_root, project_id: str,
|
|
|
75
76
|
for dj in sorted(reports_dir.glob("final-report-*.data.json"),
|
|
76
77
|
reverse=True):
|
|
77
78
|
try:
|
|
78
|
-
data =
|
|
79
|
-
except
|
|
79
|
+
data = load_owned_object(dj, artifact="final verification report")
|
|
80
|
+
except JsonBoundaryError:
|
|
80
81
|
continue
|
|
81
82
|
if ((data.get("header") or {}).get("taskType") == "final-verification"
|
|
82
83
|
and data.get("verificationScope") == "whole-task"
|
|
@@ -372,8 +373,8 @@ def record_verified(*, plan_run_root, stage: int, report_path: str,
|
|
|
372
373
|
막는다. 통과 조건은 whole-task 와 같고(`release_gate`), 기록되는 verdict 는
|
|
373
374
|
보고서가 실제로 실은 토큰이다."""
|
|
374
375
|
try:
|
|
375
|
-
data =
|
|
376
|
-
except
|
|
376
|
+
data = load_owned_object(Path(data_json), artifact="final verification report")
|
|
377
|
+
except JsonBoundaryError as exc:
|
|
377
378
|
raise HandoffError(f"cannot read final-report data.json: {exc}")
|
|
378
379
|
if (data.get("header") or {}).get("taskType") != "final-verification":
|
|
379
380
|
raise HandoffError("data.json is not a final-verification report")
|
|
@@ -11,6 +11,8 @@ import os
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any, Callable
|
|
13
13
|
|
|
14
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
15
|
+
|
|
14
16
|
SUPPORTED_LANGS = ("en", "ko")
|
|
15
17
|
DICTIONARY_REL = ("templates", "reports", "i18n")
|
|
16
18
|
# The human HTML has its own fixed strings — table headings, empty states,
|
|
@@ -48,8 +50,8 @@ def load_dictionary(lang: str, rel: tuple[str, ...] = DICTIONARY_REL) -> dict[st
|
|
|
48
50
|
)
|
|
49
51
|
path = _i18n_dir(rel) / f"{lang}.json"
|
|
50
52
|
try:
|
|
51
|
-
return
|
|
52
|
-
except
|
|
53
|
+
return load_owned_object(path, artifact="i18n dictionary")
|
|
54
|
+
except JsonBoundaryError as exc:
|
|
53
55
|
raise I18nError(f"failed to load {path}: {exc}") from exc
|
|
54
56
|
|
|
55
57
|
|
|
@@ -20,6 +20,11 @@ from .final_report_schema import (
|
|
|
20
20
|
load_schema_for_data,
|
|
21
21
|
validate as validate_schema,
|
|
22
22
|
)
|
|
23
|
+
from .json_boundary import (
|
|
24
|
+
JsonBoundaryError,
|
|
25
|
+
load_owned_object_snapshot,
|
|
26
|
+
write_owned_object_atomic,
|
|
27
|
+
)
|
|
23
28
|
from .report_views import normalize_direction_selection_identity
|
|
24
29
|
from .scope_provenance import brief_end_state_id_sequence
|
|
25
30
|
from .user_response import UserResponseError, parse_direction_selection
|
|
@@ -139,14 +144,14 @@ def _sibling_data_path(report: Path) -> Path:
|
|
|
139
144
|
return data_path
|
|
140
145
|
|
|
141
146
|
|
|
142
|
-
def _parse_data(data_path: Path
|
|
147
|
+
def _parse_data(data_path: Path) -> tuple[dict[str, Any], bytes]:
|
|
143
148
|
try:
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
snapshot = load_owned_object_snapshot(
|
|
150
|
+
data_path, artifact="implementation option data"
|
|
151
|
+
)
|
|
152
|
+
except JsonBoundaryError as exc:
|
|
146
153
|
raise DirectionSelectionError(f"data JSON is invalid: {data_path}") from exc
|
|
147
|
-
|
|
148
|
-
raise DirectionSelectionError("data JSON must contain an object")
|
|
149
|
-
return data
|
|
154
|
+
return snapshot.value, snapshot.raw_bytes
|
|
150
155
|
|
|
151
156
|
|
|
152
157
|
def _validate_report_identity(data: Mapping[str, Any], expected_task_key: str) -> None:
|
|
@@ -342,8 +347,7 @@ def resolve_selected_direction(
|
|
|
342
347
|
task_root, seq = _selection_layout(report)
|
|
343
348
|
report = validate_task_artifact_path(report, task_root, "report")
|
|
344
349
|
data_path = validate_task_artifact_path(data_path, task_root, "data")
|
|
345
|
-
data_bytes = data_path
|
|
346
|
-
data = _parse_data(data_path, data_bytes)
|
|
350
|
+
data, data_bytes = _parse_data(data_path)
|
|
347
351
|
_validate_report_identity(data, expected_task_key)
|
|
348
352
|
_validate_schema(data)
|
|
349
353
|
selection = data.get("implementationOptionSelection")
|
|
@@ -406,9 +410,10 @@ def write_selected_direction_snapshot(
|
|
|
406
410
|
"requirementCoverage": list(direction.requirement_coverage),
|
|
407
411
|
"planningInvariants": list(direction.planning_invariants),
|
|
408
412
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
413
|
+
write_owned_object_atomic(
|
|
414
|
+
destination,
|
|
415
|
+
payload,
|
|
416
|
+
artifact="selected implementation direction",
|
|
412
417
|
)
|
|
413
418
|
return destination
|
|
414
419
|
|
|
@@ -447,13 +452,16 @@ def _load_selected_direction_snapshot(
|
|
|
447
452
|
task_root = path.parent.parent
|
|
448
453
|
try:
|
|
449
454
|
path = validate_task_artifact_path(path, task_root, "selected direction snapshot")
|
|
450
|
-
|
|
451
|
-
|
|
455
|
+
loaded = load_owned_object_snapshot(
|
|
456
|
+
path, artifact="selected direction snapshot"
|
|
457
|
+
)
|
|
458
|
+
content = loaded.raw_bytes
|
|
459
|
+
data = loaded.value
|
|
452
460
|
except DirectionSelectionError as exc:
|
|
453
461
|
return None, [str(exc)]
|
|
454
462
|
except OSError as exc:
|
|
455
463
|
return None, [f"selected direction snapshot is unreadable: {exc}"]
|
|
456
|
-
except (UnicodeError, json.JSONDecodeError) as exc:
|
|
464
|
+
except (UnicodeError, json.JSONDecodeError, JsonBoundaryError) as exc:
|
|
457
465
|
return None, [f"selected direction snapshot JSON is invalid: {exc}"]
|
|
458
466
|
if not isinstance(data, Mapping):
|
|
459
467
|
return None, ["selected direction snapshot JSON must contain an object"]
|
|
@@ -15,6 +15,7 @@ from . import next_phase
|
|
|
15
15
|
from .paths import RunRef, task_manifest_file
|
|
16
16
|
from .stage_map import StageMapError, load_task_stage_map
|
|
17
17
|
from .workflow import PHASE_SEQUENCE
|
|
18
|
+
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
@dataclass(frozen=True)
|
|
@@ -114,8 +115,8 @@ def _load_json(path: Path) -> dict[str, Any]:
|
|
|
114
115
|
if not path.is_file():
|
|
115
116
|
return {}
|
|
116
117
|
try:
|
|
117
|
-
data =
|
|
118
|
-
except
|
|
118
|
+
data = load_owned_object(path, artifact="implementation outcome")
|
|
119
|
+
except JsonBoundaryError:
|
|
119
120
|
return {}
|
|
120
121
|
return data if isinstance(data, dict) else {}
|
|
121
122
|
|
|
@@ -224,8 +225,4 @@ def _update_task_catalog(project_root: Path, manifest: dict[str, Any]) -> bool:
|
|
|
224
225
|
|
|
225
226
|
|
|
226
227
|
def _write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
227
|
-
path
|
|
228
|
-
path.write_text(
|
|
229
|
-
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
|
230
|
-
encoding="utf-8",
|
|
231
|
-
)
|
|
228
|
+
write_owned_object_atomic(path, payload, artifact="implementation outcome")
|
|
@@ -12,6 +12,7 @@ from typing import Any, Mapping
|
|
|
12
12
|
from .convergence_store import write_json_atomic
|
|
13
13
|
from .final_report_schema import load_schema_version
|
|
14
14
|
from .report_narrative import parse_narrative, writer_owned_data
|
|
15
|
+
from .json_boundary import load_owned_object
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
PREP_VERDICT_RE = re.compile(r"^P-Prep-S([1-9][0-9]*)-")
|
|
@@ -384,7 +385,7 @@ def _load_data(path: str, *, option: str) -> dict:
|
|
|
384
385
|
the documented `carry refused:` refusal it knows how to fall back from.
|
|
385
386
|
"""
|
|
386
387
|
try:
|
|
387
|
-
data =
|
|
388
|
+
data = load_owned_object(Path(path), artifact="incremental carry report")
|
|
388
389
|
except (OSError, ValueError) as exc:
|
|
389
390
|
raise CarryError(f"{option} could not be read: {exc}") from exc
|
|
390
391
|
if not isinstance(data, dict):
|
|
@@ -18,6 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
from okstra_ctl.final_report_paths import final_report_data_path
|
|
19
19
|
from okstra_ctl.stage_citations import cited_stage_numbers
|
|
20
20
|
from okstra_ctl.stage_targets import downstream_stage_closure
|
|
21
|
+
from okstra_ctl.json_boundary import load_owned_object
|
|
21
22
|
|
|
22
23
|
CUTOFF_RATIO = 0.5
|
|
23
24
|
|
|
@@ -275,7 +276,7 @@ def preview_link_availability_for_report(
|
|
|
275
276
|
"reason": f"prior report has no data sibling at {data_path.name}",
|
|
276
277
|
}
|
|
277
278
|
try:
|
|
278
|
-
data =
|
|
279
|
+
data = load_owned_object(data_path, artifact="incremental scope report")
|
|
279
280
|
except (OSError, ValueError) as exc:
|
|
280
281
|
return {
|
|
281
282
|
"wouldForceFull": True,
|
|
@@ -332,7 +333,9 @@ def _preview_result(args) -> dict:
|
|
|
332
333
|
answer when the input cannot be read.
|
|
333
334
|
"""
|
|
334
335
|
try:
|
|
335
|
-
data =
|
|
336
|
+
data = load_owned_object(
|
|
337
|
+
Path(args.prev_data), artifact="incremental scope report"
|
|
338
|
+
)
|
|
336
339
|
answered = {
|
|
337
340
|
token.strip()
|
|
338
341
|
for token in args.answered_clarifications.split(",")
|
|
@@ -392,7 +395,9 @@ def main(argv: list[str]) -> int:
|
|
|
392
395
|
return 0
|
|
393
396
|
|
|
394
397
|
try:
|
|
395
|
-
data =
|
|
398
|
+
data = load_owned_object(
|
|
399
|
+
Path(args.prev_data), artifact="incremental scope report"
|
|
400
|
+
)
|
|
396
401
|
stages = parse_stage_graph(data)
|
|
397
402
|
impacted = {int(t.strip()) for t in args.impacted.split(",") if t.strip()}
|
|
398
403
|
prep_ids = {t.strip() for t in args.prep_items.split(",") if t.strip()}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
"""active/projects 인덱스 행 치환·기록."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import json
|
|
5
4
|
import os
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
8
7
|
from .ids import build_run_id
|
|
9
8
|
from .invocation import save_invocation
|
|
10
|
-
from .jsonl import
|
|
9
|
+
from .jsonl import (
|
|
10
|
+
append_jsonl, read_jsonl, remove_jsonl_row, rewrite_jsonl,
|
|
11
|
+
rotate_recent_if_needed,
|
|
12
|
+
)
|
|
11
13
|
from .project_meta import upsert_project_meta
|
|
14
|
+
from .json_boundary import load_owned_object, write_owned_object_atomic
|
|
12
15
|
from .run_index_row import build_run_index_row, slim_run_row
|
|
13
16
|
|
|
14
17
|
|
|
@@ -21,11 +24,7 @@ def _replace_or_append_row(path: Path, run_id: str, row: dict) -> None:
|
|
|
21
24
|
if r.get("runId") == run_id:
|
|
22
25
|
rows[i] = slim
|
|
23
26
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
-
|
|
25
|
-
with tmp.open("w") as f:
|
|
26
|
-
for rr in rows:
|
|
27
|
-
f.write(json.dumps(rr, separators=(",", ":")) + "\n")
|
|
28
|
-
os.replace(tmp, path)
|
|
27
|
+
rewrite_jsonl(path, rows)
|
|
29
28
|
return
|
|
30
29
|
append_jsonl(path, slim)
|
|
31
30
|
|
|
@@ -156,9 +155,7 @@ def remove_reservation(home: Path, *, project_id: str, task_group: str,
|
|
|
156
155
|
target = home / "projects" / project_id / "meta.json"
|
|
157
156
|
if not target.is_file():
|
|
158
157
|
return
|
|
159
|
-
meta =
|
|
158
|
+
meta = load_owned_object(target, artifact="project metadata")
|
|
160
159
|
meta["runCount"] = max(0, int(meta.get("runCount", 0)) - 1)
|
|
161
160
|
meta["activeCount"] = max(0, int(meta.get("activeCount", 0)) - 1)
|
|
162
|
-
|
|
163
|
-
tmp.write_text(json.dumps(meta, indent=2) + "\n")
|
|
164
|
-
os.replace(tmp, target)
|
|
161
|
+
write_owned_object_atomic(target, meta, artifact="project metadata")
|
|
@@ -24,6 +24,13 @@ from .agent_invocation import (
|
|
|
24
24
|
)
|
|
25
25
|
from .path_hints import hydrate_active_run_context
|
|
26
26
|
from .report_language import resolve_report_language
|
|
27
|
+
from .report_inputs import report_narrative_path, uses_report_contract_v3
|
|
28
|
+
from .report_synthesis_packet import (
|
|
29
|
+
ReportSynthesisPacketError,
|
|
30
|
+
materialize_report_synthesis_packet,
|
|
31
|
+
report_synthesis_packet_paths,
|
|
32
|
+
)
|
|
33
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
27
34
|
from .worker_prompt_body import (
|
|
28
35
|
REPORT_WRITER_WORKER_ID,
|
|
29
36
|
analysis_prompt_body,
|
|
@@ -444,7 +451,17 @@ def _render_prompt_parts(
|
|
|
444
451
|
f"**Prompt Delivery Mode:** {context.request.delivery_mode.value}"
|
|
445
452
|
)
|
|
446
453
|
anchors.extend(_implementation_anchor_lines(context, item.plan))
|
|
447
|
-
|
|
454
|
+
synthesis_packet_path = _materialize_report_writer_packet(
|
|
455
|
+
context,
|
|
456
|
+
item.plan,
|
|
457
|
+
)
|
|
458
|
+
body = _prompt_body(
|
|
459
|
+
context,
|
|
460
|
+
state,
|
|
461
|
+
worker,
|
|
462
|
+
item.plan,
|
|
463
|
+
synthesis_packet_path,
|
|
464
|
+
)
|
|
448
465
|
body.extend(_resource_lines(context, item))
|
|
449
466
|
except InitialPromptMaterializationError:
|
|
450
467
|
raise
|
|
@@ -503,7 +520,7 @@ def _agent_invocation_request(
|
|
|
503
520
|
f"requested model does not match {assignment_ref}",
|
|
504
521
|
)
|
|
505
522
|
anchors, body = _render_prompt_parts(context, item)
|
|
506
|
-
source_paths =
|
|
523
|
+
source_paths = [
|
|
507
524
|
AgentInstructionSource(
|
|
508
525
|
"project",
|
|
509
526
|
_project_relative(context.project_root, context.request.run_manifest_path),
|
|
@@ -514,7 +531,10 @@ def _agent_invocation_request(
|
|
|
514
531
|
AgentInstructionSource(
|
|
515
532
|
"project", _required_string(context.manifest, "teamStatePath")
|
|
516
533
|
),
|
|
517
|
-
|
|
534
|
+
]
|
|
535
|
+
packet_source = _report_writer_packet_source(context, item.plan)
|
|
536
|
+
if packet_source is not None:
|
|
537
|
+
source_paths.append(packet_source)
|
|
518
538
|
invocation_id = _worker_invocation_id(context.manifest, item.worker.worker_id)
|
|
519
539
|
return AgentInvocationRequest(
|
|
520
540
|
invocation_id=invocation_id,
|
|
@@ -526,7 +546,7 @@ def _agent_invocation_request(
|
|
|
526
546
|
instruction=AgentInstruction(
|
|
527
547
|
anchor_lines=tuple(anchors),
|
|
528
548
|
body="\n".join(body).rstrip() + "\n",
|
|
529
|
-
source_paths=source_paths,
|
|
549
|
+
source_paths=tuple(source_paths),
|
|
530
550
|
),
|
|
531
551
|
project_root=context.project_root,
|
|
532
552
|
run_manifest_path=context.request.run_manifest_path,
|
|
@@ -549,6 +569,7 @@ def _prompt_body(
|
|
|
549
569
|
state: Mapping[str, Any],
|
|
550
570
|
worker: InitialPromptWorkerRequest,
|
|
551
571
|
plan: PromptPlan,
|
|
572
|
+
synthesis_packet_path: str = "",
|
|
552
573
|
) -> list[str]:
|
|
553
574
|
if plan.audience == "report-writer":
|
|
554
575
|
return report_writer_prompt_body(
|
|
@@ -561,6 +582,7 @@ def _prompt_body(
|
|
|
561
582
|
context.manifest,
|
|
562
583
|
context.active_context,
|
|
563
584
|
),
|
|
585
|
+
synthesis_packet_path,
|
|
564
586
|
)
|
|
565
587
|
return analysis_prompt_body(
|
|
566
588
|
context.manifest,
|
|
@@ -571,6 +593,56 @@ def _prompt_body(
|
|
|
571
593
|
)
|
|
572
594
|
|
|
573
595
|
|
|
596
|
+
def _report_writer_narrative_path(context: _MaterializationContext) -> Path:
|
|
597
|
+
return report_narrative_path(context.project_root, context.manifest)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _report_writer_packet_source(
|
|
601
|
+
context: _MaterializationContext,
|
|
602
|
+
plan: PromptPlan,
|
|
603
|
+
) -> AgentInstructionSource | None:
|
|
604
|
+
if (
|
|
605
|
+
plan.audience != "report-writer"
|
|
606
|
+
or not uses_report_contract_v3(context.manifest)
|
|
607
|
+
):
|
|
608
|
+
return None
|
|
609
|
+
_, markdown_path = report_synthesis_packet_paths(
|
|
610
|
+
_report_writer_narrative_path(context)
|
|
611
|
+
)
|
|
612
|
+
return AgentInstructionSource(
|
|
613
|
+
"project",
|
|
614
|
+
_project_relative(context.project_root, markdown_path),
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _materialize_report_writer_packet(
|
|
619
|
+
context: _MaterializationContext,
|
|
620
|
+
plan: PromptPlan,
|
|
621
|
+
) -> str:
|
|
622
|
+
packet_source = _report_writer_packet_source(context, plan)
|
|
623
|
+
if packet_source is None:
|
|
624
|
+
return ""
|
|
625
|
+
try:
|
|
626
|
+
_, markdown_path = materialize_report_synthesis_packet(
|
|
627
|
+
project_root=context.project_root,
|
|
628
|
+
manifest=context.manifest,
|
|
629
|
+
active_context=context.active_context,
|
|
630
|
+
team_state=context.team_state,
|
|
631
|
+
narrative_path=_report_writer_narrative_path(context),
|
|
632
|
+
)
|
|
633
|
+
except ReportSynthesisPacketError as exc:
|
|
634
|
+
defects = "; ".join(
|
|
635
|
+
f"owner={issue.owner} source={issue.label} path={issue.path} "
|
|
636
|
+
f"reason={issue.reason}"
|
|
637
|
+
for issue in exc.issues
|
|
638
|
+
)
|
|
639
|
+
raise InitialPromptMaterializationError(
|
|
640
|
+
"required_input_missing",
|
|
641
|
+
f"report synthesis packet contract defects: {defects}",
|
|
642
|
+
) from exc
|
|
643
|
+
return _project_relative(context.project_root, markdown_path)
|
|
644
|
+
|
|
645
|
+
|
|
574
646
|
def _resource_lines(
|
|
575
647
|
context: _MaterializationContext,
|
|
576
648
|
item: _PromptItem,
|
|
@@ -1325,12 +1397,12 @@ def _load_referenced_json(
|
|
|
1325
1397
|
|
|
1326
1398
|
def _load_json_object(path: Path, label: str) -> Mapping[str, Any]:
|
|
1327
1399
|
try:
|
|
1328
|
-
payload =
|
|
1329
|
-
except
|
|
1400
|
+
payload = load_owned_object(path, artifact=label)
|
|
1401
|
+
except JsonBoundaryError as exc:
|
|
1330
1402
|
raise InitialPromptMaterializationError(
|
|
1331
1403
|
"required_input_missing",
|
|
1332
1404
|
f"cannot load {label}: {path}: {exc}",
|
|
1333
|
-
) from exc
|
|
1405
|
+
) from (exc.__cause__ or exc)
|
|
1334
1406
|
if not isinstance(payload, dict):
|
|
1335
1407
|
raise InitialPromptMaterializationError(
|
|
1336
1408
|
"required_input_missing",
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Optional
|
|
8
8
|
|
|
9
9
|
from .ids import _safe_fs_segment
|
|
10
|
+
from .json_boundary import load_owned_object, write_owned_object_atomic
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def invocation_path(home: Path, project_id: str,
|
|
@@ -32,11 +33,7 @@ def save_invocation(home: Path, project_id: str,
|
|
|
32
33
|
run_seq: int, payload: dict) -> None:
|
|
33
34
|
"""invocation 을 원자적으로 저장(임시파일 + os.replace)."""
|
|
34
35
|
target = invocation_path(home, project_id, task_group, task_id, task_type, run_seq)
|
|
35
|
-
target
|
|
36
|
-
tmp = target.with_suffix(".json.tmp")
|
|
37
|
-
tmp.write_text(json.dumps(payload, indent=2) + "\n")
|
|
38
|
-
os.chmod(tmp, 0o600)
|
|
39
|
-
os.replace(tmp, target)
|
|
36
|
+
write_owned_object_atomic(target, payload, artifact="invocation metadata")
|
|
40
37
|
|
|
41
38
|
|
|
42
39
|
def load_invocation(home: Path, project_id: str,
|
|
@@ -46,4 +43,4 @@ def load_invocation(home: Path, project_id: str,
|
|
|
46
43
|
target = invocation_path(home, project_id, task_group, task_id, task_type, run_seq)
|
|
47
44
|
if not target.is_file():
|
|
48
45
|
return None
|
|
49
|
-
return
|
|
46
|
+
return load_owned_object(target, artifact="invocation metadata")
|