okstra 0.179.2 → 0.183.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli-registry.mjs +30 -2
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/incremental-carry.mjs +9 -8
- package/dist/commands/execute/incremental-carry.mjs.map +1 -1
- package/dist/commands/execute/plan-verify.mjs +3 -1
- package/dist/commands/execute/plan-verify.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.d.mts +4 -2
- package/dist/commands/execute/render-bundle.mjs +46 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +11 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/inspect/model-io.d.mts +1 -0
- package/dist/commands/inspect/model-io.mjs +25 -0
- package/dist/commands/inspect/model-io.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.mjs +29 -8
- package/dist/commands/inspect/stage-map.mjs.map +1 -1
- package/dist/commands/inspect/task-list.mjs +52 -6
- package/dist/commands/inspect/task-list.mjs.map +1 -1
- package/dist/commands/inspect/user-response.mjs +14 -4
- package/dist/commands/inspect/user-response.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.d.mts +1 -0
- package/dist/commands/lifecycle/check-project.mjs +69 -50
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/contract-check.d.mts +1 -0
- package/dist/commands/lifecycle/contract-check.mjs +18 -0
- package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.mjs +154 -51
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/pr/pr.d.mts +1 -0
- package/dist/commands/pr/pr.mjs +19 -1
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/dist/commands/report/agent-activity.mjs +2 -2
- package/dist/commands/report/approval-decision.d.mts +1 -0
- package/dist/commands/report/approval-decision.mjs +21 -0
- package/dist/commands/report/approval-decision.mjs.map +1 -0
- package/dist/commands/report/design-snapshot.d.mts +1 -0
- package/dist/commands/report/design-snapshot.mjs +19 -0
- package/dist/commands/report/design-snapshot.mjs.map +1 -0
- package/dist/commands/report/translate.mjs +3 -0
- package/dist/commands/report/translate.mjs.map +1 -1
- package/dist/lib/host-registry-client.mjs +13 -9
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +21 -10
- package/docs/cli.md +36 -20
- package/docs/container.md +6 -4
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
- package/docs/for-ai/skills/okstra-code-review.md +4 -4
- package/docs/for-ai/skills/okstra-container-build.md +20 -17
- package/docs/for-ai/skills/okstra-inspect.md +20 -23
- package/docs/for-ai/skills/okstra-manager.md +19 -18
- package/docs/for-ai/skills/okstra-memory.md +2 -2
- package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
- package/docs/for-ai/skills/okstra-rollup.md +14 -13
- package/docs/for-ai/skills/okstra-run.md +7 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
- package/docs/for-ai/skills/okstra-setup.md +7 -7
- package/docs/for-ai/skills/okstra-usage.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +50 -32
- package/docs/project-structure-overview.md +43 -31
- package/docs/task-process/implementation-planning.md +2 -2
- package/docs/task-process/implementation.md +7 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/report-writer-worker.md +15 -164
- package/runtime/agents/workers/translator-worker.md +5 -13
- package/runtime/bin/okstra-error-log.py +51 -11
- package/runtime/bin/okstra-report-translate.py +210 -23
- package/runtime/prompts/host-orchestration/implementation.md +1 -1
- package/runtime/prompts/launch.template.md +9 -12
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/context-loader.md +41 -141
- package/runtime/prompts/lead/convergence.md +10 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +40 -49
- package/runtime/prompts/lead/plan-body-verification.md +217 -23
- package/runtime/prompts/lead/report-writer.md +81 -421
- package/runtime/prompts/lead/team-contract.md +9 -54
- package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/feature-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +20 -12
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/project-analysis.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +0 -1
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +8 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
- package/runtime/python/okstra_ctl/agent_activity.py +141 -38
- package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +70 -23
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +124 -3
- package/runtime/python/okstra_ctl/approval_decisions.py +328 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
- package/runtime/python/okstra_ctl/backfill.py +13 -10
- package/runtime/python/okstra_ctl/batch.py +2 -4
- package/runtime/python/okstra_ctl/build_tools.py +6 -3
- package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
- package/runtime/python/okstra_ctl/clarification_items.py +27 -13
- package/runtime/python/okstra_ctl/cmux.py +130 -52
- package/runtime/python/okstra_ctl/code_review_target.py +34 -8
- package/runtime/python/okstra_ctl/consumers.py +5 -4
- package/runtime/python/okstra_ctl/container.py +103 -8
- package/runtime/python/okstra_ctl/context_cost.py +2 -1
- package/runtime/python/okstra_ctl/contract_graph.py +497 -0
- package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
- package/runtime/python/okstra_ctl/convergence.py +338 -17
- package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
- package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
- package/runtime/python/okstra_ctl/convergence_store.py +55 -34
- package/runtime/python/okstra_ctl/design_prep.py +7 -4
- package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +97 -69
- package/runtime/python/okstra_ctl/dispatch_state.py +163 -63
- package/runtime/python/okstra_ctl/doctor.py +6 -3
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
- package/runtime/python/okstra_ctl/error_log_write.py +35 -30
- package/runtime/python/okstra_ctl/error_report.py +26 -1
- package/runtime/python/okstra_ctl/error_zip.py +27 -5
- package/runtime/python/okstra_ctl/execution_identity.py +3 -2
- package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +26 -17
- package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
- package/runtime/python/okstra_ctl/fixed_text.py +39 -0
- package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
- package/runtime/python/okstra_ctl/handoff.py +5 -4
- package/runtime/python/okstra_ctl/i18n.py +4 -2
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
- package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
- package/runtime/python/okstra_ctl/incremental_carry.py +130 -17
- package/runtime/python/okstra_ctl/incremental_scope.py +12 -4
- package/runtime/python/okstra_ctl/index.py +8 -11
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
- package/runtime/python/okstra_ctl/invocation.py +3 -6
- package/runtime/python/okstra_ctl/json_boundary.py +366 -0
- package/runtime/python/okstra_ctl/json_registry.py +10 -12
- package/runtime/python/okstra_ctl/jsonl.py +19 -2
- package/runtime/python/okstra_ctl/lead_events.py +33 -1
- package/runtime/python/okstra_ctl/listing.py +3 -3
- package/runtime/python/okstra_ctl/log_report.py +24 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -7
- package/runtime/python/okstra_ctl/manager_store.py +12 -10
- package/runtime/python/okstra_ctl/material.py +5 -1
- package/runtime/python/okstra_ctl/migrate.py +29 -25
- package/runtime/python/okstra_ctl/model_cli.py +3 -15
- package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
- package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
- package/runtime/python/okstra_ctl/path_hints.py +12 -0
- package/runtime/python/okstra_ctl/paths.py +21 -0
- package/runtime/python/okstra_ctl/plan_items.py +115 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +626 -34
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
- package/runtime/python/okstra_ctl/pr_template.py +3 -2
- package/runtime/python/okstra_ctl/project_meta.py +5 -7
- package/runtime/python/okstra_ctl/recap.py +5 -4
- package/runtime/python/okstra_ctl/reconcile.py +21 -27
- package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
- package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
- package/runtime/python/okstra_ctl/render.py +74 -16
- package/runtime/python/okstra_ctl/render_final_report.py +10 -8
- package/runtime/python/okstra_ctl/report_assembly.py +509 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -1
- package/runtime/python/okstra_ctl/report_finalize.py +67 -32
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_inputs.py +72 -0
- package/runtime/python/okstra_ctl/report_language.py +3 -2
- package/runtime/python/okstra_ctl/report_markdown.py +81 -8
- package/runtime/python/okstra_ctl/report_narrative.py +351 -0
- package/runtime/python/okstra_ctl/report_projections.py +265 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
- package/runtime/python/okstra_ctl/report_views.py +3 -2
- package/runtime/python/okstra_ctl/rollup.py +65 -4
- package/runtime/python/okstra_ctl/run.py +184 -65
- package/runtime/python/okstra_ctl/run_audit.py +3 -2
- package/runtime/python/okstra_ctl/run_context.py +6 -9
- package/runtime/python/okstra_ctl/run_index_row.py +2 -8
- package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +15 -8
- package/runtime/python/okstra_ctl/session_transcript.py +27 -1
- package/runtime/python/okstra_ctl/set_work_status.py +64 -38
- package/runtime/python/okstra_ctl/stage_fix_carry.py +8 -6
- package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
- package/runtime/python/okstra_ctl/stage_map.py +96 -28
- package/runtime/python/okstra_ctl/stage_targets.py +3 -4
- package/runtime/python/okstra_ctl/team.py +3 -2
- package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
- package/runtime/python/okstra_ctl/time_report.py +51 -4
- package/runtime/python/okstra_ctl/usage_identity.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +58 -4
- package/runtime/python/okstra_ctl/user_response.py +1431 -66
- package/runtime/python/okstra_ctl/wizard.py +11 -44
- package/runtime/python/okstra_ctl/work_categories.py +3 -2
- package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +49 -3
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
- package/runtime/python/okstra_ctl/worker_runner.py +14 -12
- package/runtime/python/okstra_ctl/workflow.py +2 -1
- package/runtime/python/okstra_ctl/worktree.py +3 -2
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
- package/runtime/python/okstra_ctl/write_policy.py +4 -2
- package/runtime/python/okstra_token_usage/antigravity.py +39 -12
- package/runtime/python/okstra_token_usage/collect.py +90 -38
- package/runtime/python/okstra_token_usage/grok.py +127 -0
- package/runtime/schemas/final-report-v2.0.schema.json +9 -0
- package/runtime/schemas/final-report-v3.0.schema.json +10219 -0
- package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
- package/runtime/skills/okstra-code-review/SKILL.md +21 -11
- package/runtime/skills/okstra-container-build/SKILL.md +18 -18
- package/runtime/skills/okstra-inspect/SKILL.md +12 -11
- package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
- package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
- package/runtime/skills/okstra-inspect/facets/history.md +9 -14
- package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
- package/runtime/skills/okstra-inspect/facets/report.md +6 -10
- package/runtime/skills/okstra-inspect/facets/status.md +9 -8
- package/runtime/skills/okstra-inspect/facets/time.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +16 -14
- package/runtime/skills/okstra-memory/SKILL.md +3 -3
- package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
- package/runtime/skills/okstra-rollup/SKILL.md +6 -16
- package/runtime/skills/okstra-run/SKILL.md +7 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
- package/runtime/skills/okstra-setup/SKILL.md +21 -13
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/skills/okstra-usage/SKILL.md +10 -10
- package/runtime/skills/okstra-user-response/SKILL.md +78 -107
- package/runtime/templates/report-writer-prompt-preamble.md +29 -19
- package/runtime/templates/reports/html/macros/forms.html +6 -4
- package/runtime/templates/reports/schedule.template.md +4 -4
- package/runtime/templates/worker-error-contract.md +17 -29
- package/runtime/validators/validate-run.py +636 -72
- package/runtime/validators/validate_session_conformance.py +43 -7
|
@@ -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
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Derive the fix-run carry for an implementation stage whose previous run
|
|
2
2
|
ended with one or more verifier FAIL verdicts.
|
|
3
3
|
|
|
4
|
-
Pure read-side: scans the stage run directory's final-report data.json
|
|
5
|
-
|
|
4
|
+
Pure read-side: scans the stage run directory's assembled final-report data.json
|
|
5
|
+
files and the stage worktree HEAD.
|
|
6
6
|
Returns None when the upcoming run is not a fix run, so callers can treat
|
|
7
7
|
"no carry" and "first run on this stage" identically.
|
|
8
8
|
"""
|
|
@@ -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
|
|
|
@@ -47,8 +49,8 @@ class StageFixCarry:
|
|
|
47
49
|
"`report-writer.md` § Fix-run incremental authoring).",
|
|
48
50
|
"This block is inlined into the executor and verifier prompts by "
|
|
49
51
|
"`initial_prompt_materialization`; do not transcribe it by hand. "
|
|
50
|
-
"Lead duties: pass the previous data.json path above
|
|
51
|
-
"report-writer dispatch so it
|
|
52
|
+
"Lead duties: pass the previous data.json path above as read context "
|
|
53
|
+
"to the report-writer dispatch so it updates only changed narrative blocks.",
|
|
52
54
|
])
|
|
53
55
|
return "\n".join(lines)
|
|
54
56
|
|
|
@@ -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):
|
|
@@ -4,8 +4,17 @@
|
|
|
4
4
|
이 모듈은 그 사실만 모아 준다 — 무엇을 계획해야 하는지는 말하지 않는다.
|
|
5
5
|
|
|
6
6
|
판정은 소유하지 않는다. 상태 어휘와 lifecycle 판정은 stage_targets 가, stage
|
|
7
|
-
map 의 출처 판정은 stage_map
|
|
8
|
-
|
|
7
|
+
map 의 출처 판정은 stage_map 이 소유한다. 여기서는 둘을 잇고 직렬화만 한다.
|
|
8
|
+
|
|
9
|
+
원장은 두 질문에 답하고, 답의 출처가 서로 다르다.
|
|
10
|
+
|
|
11
|
+
- "무엇이 이미 지어졌나" — carry 사이드카가 가리키는 계획이 답한다. 실행이
|
|
12
|
+
실제로 따른 문서이기 때문이다.
|
|
13
|
+
- "어떤 stage 번호가 이미 쓰였나" — 최신 계획이 답한다. ADR-0015 의
|
|
14
|
+
append-only 는 최신 계획의 `max` 로만 판정할 수 있다.
|
|
15
|
+
|
|
16
|
+
한 소스가 둘 다 답하면, 완료 이후 stage 를 덧붙인 계획이 있을 때 그 번호가
|
|
17
|
+
비어 있는 것처럼 보이고 새 stage 가 그 번호를 다시 받는다.
|
|
9
18
|
"""
|
|
10
19
|
from __future__ import annotations
|
|
11
20
|
|
|
@@ -15,25 +24,31 @@ from typing import Any
|
|
|
15
24
|
|
|
16
25
|
from .consumers import read_stage_consumer_state
|
|
17
26
|
from .paths import RunRef
|
|
18
|
-
from .stage_map import
|
|
27
|
+
from .stage_map import (
|
|
28
|
+
StageMapError,
|
|
29
|
+
load_latest_plan_stage_map,
|
|
30
|
+
load_task_stage_map,
|
|
31
|
+
)
|
|
19
32
|
from .stage_targets import stage_lifecycle_snapshot_from_state
|
|
20
33
|
from .task_target import infer_project_root
|
|
21
34
|
|
|
22
35
|
|
|
23
36
|
def build_stage_ledger(task_root: Path) -> dict[str, Any] | None:
|
|
24
|
-
"""이 task 의 Stage 원장.
|
|
37
|
+
"""이 task 의 Stage 원장.
|
|
38
|
+
|
|
39
|
+
세 결과를 구분한다. 셋이 하나로 접히면 소비처가 "계획이 아직 없다" 와
|
|
40
|
+
"계획을 못 읽었다" 를 같은 것으로 읽는다.
|
|
25
41
|
|
|
26
|
-
``None``
|
|
27
|
-
|
|
42
|
+
- ``None`` — 계획 리포트가 아직 없다. 이 task 의 첫 계획 run 이다.
|
|
43
|
+
- ``{"unreadable": <사유>}`` — 계획은 있는데 Stage Map 을 못 읽었다.
|
|
44
|
+
- 그 외 — 사실 기록.
|
|
28
45
|
"""
|
|
29
46
|
task_root = Path(task_root)
|
|
30
47
|
try:
|
|
31
|
-
|
|
32
|
-
except StageMapError:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return None
|
|
36
|
-
if snapshot.state != "ready" or not snapshot.stages:
|
|
48
|
+
latest = load_latest_plan_stage_map(task_root)
|
|
49
|
+
except StageMapError as exc:
|
|
50
|
+
return {"unreadable": _failure_reason(task_root, exc)}
|
|
51
|
+
if latest.state != "ready" or not latest.stages:
|
|
37
52
|
return None
|
|
38
53
|
|
|
39
54
|
plan_run_root = RunRef.from_task_root(
|
|
@@ -43,20 +58,119 @@ def build_stage_ledger(task_root: Path) -> dict[str, Any] | None:
|
|
|
43
58
|
# 같은 멱등 복구이며, 이걸 건너뛰면 크래시 창에 걸린 완료 stage 가 원장에
|
|
44
59
|
# 미완으로 실려 저작 쪽이 이미 구현된 stage 를 다시 계획한다.
|
|
45
60
|
state = read_stage_consumer_state(plan_run_root, recover_from_carry=True)
|
|
46
|
-
lifecycle = stage_lifecycle_snapshot_from_state(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
lifecycle = stage_lifecycle_snapshot_from_state(latest.stages, state)
|
|
62
|
+
records = lifecycle.ledger_records()
|
|
63
|
+
|
|
64
|
+
built_from, divergence = _built_from(task_root, latest)
|
|
65
|
+
divergence += _renumbering_divergence(
|
|
66
|
+
latest, built_from, state.done_stages
|
|
67
|
+
)
|
|
68
|
+
ledger: dict[str, Any] = {
|
|
69
|
+
"sourcePlan": _project_relative(
|
|
70
|
+
task_root, built_from.source_plan_path if built_from else ""
|
|
71
|
+
),
|
|
72
|
+
"latestPlan": _project_relative(task_root, latest.source_plan_path),
|
|
73
|
+
"stages": records,
|
|
50
74
|
}
|
|
75
|
+
if divergence:
|
|
76
|
+
ledger["planDivergence"] = divergence
|
|
77
|
+
return ledger
|
|
51
78
|
|
|
52
79
|
|
|
53
80
|
def render_stage_ledger(ledger: dict[str, Any] | None) -> str:
|
|
54
|
-
"""packet 에 실릴 JSON 본문.
|
|
55
|
-
if not ledger:
|
|
81
|
+
"""packet 에 실릴 JSON 본문. 사실 기록이 없으면 빈 문자열."""
|
|
82
|
+
if not ledger or ledger.get("unreadable"):
|
|
56
83
|
return ""
|
|
57
84
|
return json.dumps(ledger, ensure_ascii=False, indent=2)
|
|
58
85
|
|
|
59
86
|
|
|
87
|
+
def stage_ledger_notice(ledger: dict[str, Any] | None) -> str:
|
|
88
|
+
"""원장을 실을 수 없는 사유. 실을 수 있으면 빈 문자열.
|
|
89
|
+
|
|
90
|
+
평문이다. 실패 통지는 저작 쪽이 파싱할 데이터가 아니라 읽을 판정이고,
|
|
91
|
+
JSON 표면을 하나 더 만들면 그만큼 오독할 키가 늘어난다.
|
|
92
|
+
"""
|
|
93
|
+
if not ledger:
|
|
94
|
+
return ""
|
|
95
|
+
return str(ledger.get("unreadable") or "")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _built_from(task_root: Path, latest: Any):
|
|
99
|
+
"""완료된 stage 가 따른 계획. 못 읽으면 ``(None, [사유])``.
|
|
100
|
+
|
|
101
|
+
이걸 못 읽어도 원장 자체는 낼 수 있다 — 상태는 consumers 원장에서 오고,
|
|
102
|
+
stage 목록은 최신 계획에서 온다. 잃는 것은 번호 재배치 검사뿐이므로,
|
|
103
|
+
실패를 원장 부재로 승격하지 않고 그 사실만 함께 싣는다.
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
return load_task_stage_map(task_root, {}), []
|
|
107
|
+
except StageMapError as exc:
|
|
108
|
+
return None, [
|
|
109
|
+
"could not read the plan the completed stages were built against, "
|
|
110
|
+
"so stage renumbering could not be checked: "
|
|
111
|
+
+ _failure_reason(task_root, exc)
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _renumbering_divergence(
|
|
116
|
+
latest: Any,
|
|
117
|
+
built_from: Any,
|
|
118
|
+
done_stages: set[int],
|
|
119
|
+
) -> list[str]:
|
|
120
|
+
"""완료된 stage 가 두 계획에서 같은 작업을 가리키는지.
|
|
121
|
+
|
|
122
|
+
ADR-0015 는 stage 번호의 재사용과 재배치를 금지하므로, 규칙이 지켜졌다면
|
|
123
|
+
최신 계획 위에 실행 상태를 겹쳐 쓰는 것이 안전하다. 지켜졌는지는 검사할
|
|
124
|
+
사실이지 가정할 사실이 아니다 — 과거 replan 이 번호를 다시 매겼다면 완료
|
|
125
|
+
커밋이 엉뚱한 stage 에 귀속되고, 그 오류는 통합 시점까지 조용하다.
|
|
126
|
+
"""
|
|
127
|
+
if built_from is None or built_from.state != "ready":
|
|
128
|
+
return []
|
|
129
|
+
if built_from.source_plan_path == latest.source_plan_path:
|
|
130
|
+
# 완료된 stage 를 지은 계획이 곧 최신 계획이다. 비교할 두 번째 계획이
|
|
131
|
+
# 없으므로 재배치가 일어날 자리 자체가 없다.
|
|
132
|
+
return []
|
|
133
|
+
latest_titles = {
|
|
134
|
+
int(row["stage_number"]): str(row.get("title") or "")
|
|
135
|
+
for row in latest.stages
|
|
136
|
+
}
|
|
137
|
+
built_titles = {
|
|
138
|
+
int(row["stage_number"]): str(row.get("title") or "")
|
|
139
|
+
for row in built_from.stages
|
|
140
|
+
}
|
|
141
|
+
notes: list[str] = []
|
|
142
|
+
for stage in sorted(done_stages):
|
|
143
|
+
if stage not in built_titles:
|
|
144
|
+
continue
|
|
145
|
+
if stage not in latest_titles:
|
|
146
|
+
notes.append(
|
|
147
|
+
f"stage {stage} is recorded done but the latest plan does not "
|
|
148
|
+
"declare it; a completed stage was dropped rather than cancelled"
|
|
149
|
+
)
|
|
150
|
+
continue
|
|
151
|
+
if built_titles[stage] != latest_titles[stage]:
|
|
152
|
+
notes.append(
|
|
153
|
+
f"stage {stage} names different work in the two plans "
|
|
154
|
+
f"(built: {built_titles[stage]!r}; latest: "
|
|
155
|
+
f"{latest_titles[stage]!r}); its completion may be attributed "
|
|
156
|
+
"to the wrong stage"
|
|
157
|
+
)
|
|
158
|
+
return notes
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _failure_reason(task_root: Path, exc: StageMapError) -> str:
|
|
162
|
+
"""StageMapError 를 프로젝트 상대 경로로 낮춘 한 줄."""
|
|
163
|
+
parts = [exc.reason]
|
|
164
|
+
if exc.source_plan_path:
|
|
165
|
+
parts.append(f"source={_project_relative(task_root, exc.source_plan_path)}")
|
|
166
|
+
if exc.conflicting_paths:
|
|
167
|
+
conflicts = ", ".join(
|
|
168
|
+
_project_relative(task_root, path) for path in exc.conflicting_paths
|
|
169
|
+
)
|
|
170
|
+
parts.append(f"conflicts={conflicts}")
|
|
171
|
+
return "; ".join(parts)
|
|
172
|
+
|
|
173
|
+
|
|
60
174
|
def _project_relative(task_root: Path, source_plan_path: str) -> str:
|
|
61
175
|
"""절대 경로를 프로젝트 상대로 낮춘다.
|
|
62
176
|
|
|
@@ -7,6 +7,9 @@ from dataclasses import dataclass
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any, Iterable
|
|
9
9
|
|
|
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
|
|
10
13
|
from .md_table import is_separator_row, split_pipe_row
|
|
11
14
|
from .paths import RunRef
|
|
12
15
|
from .plan_run_root import list_implementation_planning_reports
|
|
@@ -47,6 +50,24 @@ class StageMapSnapshot:
|
|
|
47
50
|
stages: list[dict[str, Any]]
|
|
48
51
|
|
|
49
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
|
+
|
|
50
71
|
# `stepwiseExecution` is what the stage DOES; the other three are what becomes
|
|
51
72
|
# true when it is finished. A schedule carrying only the latter reads as an
|
|
52
73
|
# analysis of a plan rather than the plan itself.
|
|
@@ -353,8 +374,8 @@ def parse_stage_map_file(plan_path: Path) -> list[StageMapStage]:
|
|
|
353
374
|
resolved = Path(plan_path).resolve()
|
|
354
375
|
if is_report_record_path(resolved):
|
|
355
376
|
try:
|
|
356
|
-
data =
|
|
357
|
-
except (OSError, UnicodeError,
|
|
377
|
+
data = load_owned_object(resolved, artifact="planning final report")
|
|
378
|
+
except (OSError, UnicodeError, JsonBoundaryError) as exc:
|
|
358
379
|
raise StageMapError("stage_map", str(exc), str(resolved)) from exc
|
|
359
380
|
if not isinstance(data, dict):
|
|
360
381
|
raise StageMapError(
|
|
@@ -364,8 +385,8 @@ def parse_stage_map_file(plan_path: Path) -> list[StageMapStage]:
|
|
|
364
385
|
return _parse_stage_map_markdown(resolved)
|
|
365
386
|
|
|
366
387
|
|
|
367
|
-
def
|
|
368
|
-
"""
|
|
388
|
+
def structured_report(plan_path: Path) -> dict[str, Any]:
|
|
389
|
+
"""계약 2.0·3.0 구조화 리포트를 반환하고 Markdown에는 빈 값을 반환한다.
|
|
369
390
|
|
|
370
391
|
Public because every caller that must branch on report schema needs it —
|
|
371
392
|
including `validators/validate-implementation-plan-stages.py`, which is a
|
|
@@ -378,17 +399,22 @@ def schema_v2_report(plan_path: Path) -> dict[str, Any]:
|
|
|
378
399
|
if not is_report_record_path(resolved) or not resolved.exists():
|
|
379
400
|
return {}
|
|
380
401
|
try:
|
|
381
|
-
data =
|
|
382
|
-
except (OSError, UnicodeError,
|
|
402
|
+
data = load_owned_object(resolved, artifact="planning final report")
|
|
403
|
+
except (OSError, UnicodeError, JsonBoundaryError) as exc:
|
|
383
404
|
raise StageMapError("stage_map", str(exc), str(resolved)) from exc
|
|
384
|
-
if not isinstance(data, dict) or data.get("schemaVersion")
|
|
405
|
+
if not isinstance(data, dict) or data.get("schemaVersion") not in {"2.0", "3.0"}:
|
|
385
406
|
return {}
|
|
386
407
|
return data
|
|
387
408
|
|
|
388
409
|
|
|
410
|
+
def schema_v2_report(plan_path: Path) -> dict[str, Any]:
|
|
411
|
+
"""기존 호출자를 위한 구조화 리포트 판독 별칭."""
|
|
412
|
+
return structured_report(plan_path)
|
|
413
|
+
|
|
414
|
+
|
|
389
415
|
def _planning_section(markdown_path: Path) -> dict[str, Any]:
|
|
390
416
|
"""The report's `implementationPlanning` block, `{}` for v1."""
|
|
391
|
-
planning =
|
|
417
|
+
planning = structured_report(markdown_path).get("implementationPlanning")
|
|
392
418
|
return planning if isinstance(planning, dict) else {}
|
|
393
419
|
|
|
394
420
|
|
|
@@ -412,7 +438,7 @@ def _stage_narratives(value: Any) -> dict[int, dict[str, Any]]:
|
|
|
412
438
|
|
|
413
439
|
def load_planning_detail(markdown_path: Path) -> PlanningDetail:
|
|
414
440
|
"""Read one report's narrative rows; empty for a schema-v1 report."""
|
|
415
|
-
report =
|
|
441
|
+
report = structured_report(markdown_path)
|
|
416
442
|
planning = report.get("implementationPlanning")
|
|
417
443
|
if not isinstance(planning, dict) or not planning:
|
|
418
444
|
return PlanningDetail({}, {})
|
|
@@ -456,9 +482,34 @@ def stage_map_records(stages: Iterable[StageMapStage]) -> list[dict[str, Any]]:
|
|
|
456
482
|
]
|
|
457
483
|
|
|
458
484
|
|
|
485
|
+
def _latest_plan_report(task_root: Path) -> Path | None:
|
|
486
|
+
reports_dir = RunRef.from_task_root(
|
|
487
|
+
task_root, "implementation-planning"
|
|
488
|
+
).reports_dir
|
|
489
|
+
reports = list_implementation_planning_reports(reports_dir)
|
|
490
|
+
return reports[0] if reports else None
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _snapshot_for(source: Path | None) -> StageMapSnapshot:
|
|
494
|
+
if source is None:
|
|
495
|
+
return StageMapSnapshot("missing", "", [])
|
|
496
|
+
resolved = source.resolve()
|
|
497
|
+
return StageMapSnapshot(
|
|
498
|
+
"ready",
|
|
499
|
+
str(resolved),
|
|
500
|
+
stage_map_records(parse_stage_map_file(resolved)),
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
459
504
|
def load_task_stage_map(
|
|
460
505
|
task_root: Path, manifest: dict[str, Any],
|
|
461
506
|
) -> StageMapSnapshot:
|
|
507
|
+
"""완료된 stage 를 무엇으로 지었는지 답하는 Stage Map.
|
|
508
|
+
|
|
509
|
+
carry 사이드카가 가리키는 계획을 우선한다 — 그게 실행이 실제로 따른
|
|
510
|
+
문서이기 때문이다. "어떤 stage 번호가 이미 쓰였나" 는 다른 질문이며
|
|
511
|
+
`load_latest_plan_stage_map` 이 답한다.
|
|
512
|
+
"""
|
|
462
513
|
carried = _unique_carry_source_paths(task_root)
|
|
463
514
|
if len(carried) > 1:
|
|
464
515
|
paths = tuple(str(path) for path in carried)
|
|
@@ -467,22 +518,19 @@ def load_task_stage_map(
|
|
|
467
518
|
"implementation carries reference different source plans",
|
|
468
519
|
conflicting_paths=paths,
|
|
469
520
|
)
|
|
470
|
-
if carried
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
str(resolved),
|
|
484
|
-
stage_map_records(parse_stage_map_file(resolved)),
|
|
485
|
-
)
|
|
521
|
+
source = carried[0] if carried else _latest_plan_report(task_root)
|
|
522
|
+
return _snapshot_for(source)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def load_latest_plan_stage_map(task_root: Path) -> StageMapSnapshot:
|
|
526
|
+
"""이 task 의 최신 계획 리포트가 선언한 Stage Map.
|
|
527
|
+
|
|
528
|
+
번호 점유의 판정 기준이다. ADR-0015 는 stage 번호를 append-only 로 두고
|
|
529
|
+
새 stage 를 `max+1` 로 붙이라고 정하는데, 그 `max` 는 최신 계획에서만
|
|
530
|
+
나온다 — 완료된 stage 를 지을 때 쓴 계획 뒤에 stage 를 덧붙인 계획이
|
|
531
|
+
있으면 그 번호들도 이미 점유된 상태다.
|
|
532
|
+
"""
|
|
533
|
+
return _snapshot_for(_latest_plan_report(task_root))
|
|
486
534
|
|
|
487
535
|
|
|
488
536
|
def _unique_carry_source_paths(task_root: Path) -> list[Path]:
|
|
@@ -490,15 +538,35 @@ def _unique_carry_source_paths(task_root: Path) -> list[Path]:
|
|
|
490
538
|
paths: set[Path] = set()
|
|
491
539
|
for carry_path in sorted(carry_dir.glob("stage-*.json")):
|
|
492
540
|
try:
|
|
493
|
-
carry =
|
|
494
|
-
except (OSError, UnicodeError,
|
|
541
|
+
carry = load_owned_object(carry_path, artifact="implementation stage carry")
|
|
542
|
+
except (OSError, UnicodeError, JsonBoundaryError):
|
|
495
543
|
continue
|
|
496
544
|
value = carry.get("sourcePlanPath") if isinstance(carry, dict) else None
|
|
497
545
|
if isinstance(value, str) and value:
|
|
498
|
-
|
|
546
|
+
# carry 의 이 값은 executor 워커가 손으로 적는 자유 문자열이고,
|
|
547
|
+
# `--approved-plan` 이 레코드 전용으로 조여지기 전에 준비된 run 은
|
|
548
|
+
# 읽기본(`.md`) 경로를 남겼다. v2 리포트의 `.md` 에는 Stage Map
|
|
549
|
+
# 섹션이 없으므로 그대로 읽으면 원장이 통째로 사라진다. 같은 legacy
|
|
550
|
+
# 포인터 정규화를 run-index·타임라인 소비자는 이미 하고 있다.
|
|
551
|
+
# 읽는 쪽에서만 낮춘다 — 동결된 carry 사이드카는 고치지 않는다.
|
|
552
|
+
paths.add(_carry_source_path(task_root, value))
|
|
499
553
|
return sorted(paths, key=str)
|
|
500
554
|
|
|
501
555
|
|
|
556
|
+
def _carry_source_path(task_root: Path, value: str) -> Path:
|
|
557
|
+
"""carry 가 가리키는 계획의 읽을 경로.
|
|
558
|
+
|
|
559
|
+
레코드 형제가 실재할 때만 읽기본(`.md`)을 레코드로 낮춘다. schema-v1 계획은
|
|
560
|
+
레코드가 아예 없고 마크다운이 유일한 정본이므로, 무조건 낮추면 v1 태스크의
|
|
561
|
+
Stage Map 을 없는 파일에서 찾게 된다.
|
|
562
|
+
"""
|
|
563
|
+
resolved = _resolve_plan_path(task_root, value).resolve()
|
|
564
|
+
if not is_full_reading_copy_path(resolved):
|
|
565
|
+
return resolved
|
|
566
|
+
record = final_report_data_path(resolved)
|
|
567
|
+
return record if record.is_file() else resolved
|
|
568
|
+
|
|
569
|
+
|
|
502
570
|
def _resolve_plan_path(task_root: Path, value: str) -> Path:
|
|
503
571
|
path = Path(value)
|
|
504
572
|
if path.is_absolute():
|
|
@@ -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())
|
|
@@ -81,7 +82,7 @@ def _add_dispatch_parser(sub) -> None:
|
|
|
81
82
|
parser.add_argument("--workers", default="")
|
|
82
83
|
parser.add_argument("--jobs-file", default="")
|
|
83
84
|
parser.add_argument("--dry-run", action="store_true")
|
|
84
|
-
parser.add_argument("--idle-timeout-seconds", type=int, default=
|
|
85
|
+
parser.add_argument("--idle-timeout-seconds", type=int, default=None)
|
|
85
86
|
parser.add_argument("--dispatch-kind", default="initial")
|
|
86
87
|
|
|
87
88
|
|
|
@@ -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
|