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
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
"""Validated JSON loading and atomic persistence for convergence artifacts."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
4
|
from pathlib import Path
|
|
7
|
-
import tempfile
|
|
8
5
|
from typing import Any, Mapping
|
|
9
6
|
|
|
10
7
|
from .convergence_engine import ConvergenceContractError
|
|
@@ -14,6 +11,8 @@ from .execution_manifest import (
|
|
|
14
11
|
read_execution_manifest,
|
|
15
12
|
reserve_derived_role_invocation,
|
|
16
13
|
)
|
|
14
|
+
from .final_report_schema import load_named_schema
|
|
15
|
+
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
17
16
|
from .worker_request import verifier_extra_dirs
|
|
18
17
|
from .write_policy import build_invocation_write_contract
|
|
19
18
|
|
|
@@ -25,44 +24,66 @@ DYNAMIC_VERIFIER_SOURCE_ROLES = frozenset({
|
|
|
25
24
|
"verifier",
|
|
26
25
|
})
|
|
27
26
|
|
|
27
|
+
_CRITIC_BATCH_SCHEMA = {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"required": [
|
|
30
|
+
"schemaVersion", "taskKey", "mode", "provider",
|
|
31
|
+
"modelExecutionValue", "dispatches", "gaps",
|
|
32
|
+
],
|
|
33
|
+
"additionalProperties": False,
|
|
34
|
+
"properties": {
|
|
35
|
+
"schemaVersion": {"const": "1.0"},
|
|
36
|
+
"taskKey": {"type": "string", "pattern": "\\S"},
|
|
37
|
+
"mode": {"const": "coverage"},
|
|
38
|
+
"provider": {"type": "string", "pattern": "\\S"},
|
|
39
|
+
"modelExecutionValue": {"type": "string", "pattern": "\\S"},
|
|
40
|
+
"dispatches": {"type": "array", "items": {"type": "object"}},
|
|
41
|
+
"gaps": {"type": "array", "items": {"type": "object"}},
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_owned_json_object(path: Path) -> dict[str, Any]:
|
|
47
|
+
try:
|
|
48
|
+
return load_owned_object(path, artifact="convergence artifact")
|
|
49
|
+
except JsonBoundaryError as exc:
|
|
50
|
+
raise ConvergenceContractError(str(exc)) from exc
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load_convergence_result(
|
|
54
|
+
path: Path, *, schema: Mapping[str, Any]
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
try:
|
|
57
|
+
return load_owned_object(
|
|
58
|
+
path, artifact="convergence result", schema=schema
|
|
59
|
+
)
|
|
60
|
+
except JsonBoundaryError as exc:
|
|
61
|
+
raise ConvergenceContractError(str(exc)) from exc
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def load_convergence_round_results(path: Path) -> dict[str, Any]:
|
|
65
|
+
return _load_convergence_result(
|
|
66
|
+
path,
|
|
67
|
+
schema=load_named_schema("convergence-round-results-v1.0.schema.json"),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def load_convergence_critic_batch(path: Path) -> dict[str, Any]:
|
|
72
|
+
return _load_convergence_result(path, schema=_CRITIC_BATCH_SCHEMA)
|
|
73
|
+
|
|
28
74
|
|
|
29
75
|
def load_json_object(path: Path) -> dict[str, Any]:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
raise ConvergenceContractError(f"JSON input must be an object: {path}")
|
|
33
|
-
return value
|
|
76
|
+
"""Compatibility alias for convergence artifacts owned by okstra."""
|
|
77
|
+
return load_owned_json_object(path)
|
|
34
78
|
|
|
35
79
|
|
|
36
80
|
def write_json_atomic(path: Path, payload: Mapping[str, Any]) -> None:
|
|
37
81
|
if not isinstance(payload, Mapping):
|
|
38
82
|
raise ConvergenceContractError("JSON output payload must be an object")
|
|
39
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
-
temp_path: Path | None = None
|
|
41
83
|
try:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
dir=path.parent,
|
|
46
|
-
prefix=f".{path.name}.",
|
|
47
|
-
suffix=".tmp",
|
|
48
|
-
delete=False,
|
|
49
|
-
) as handle:
|
|
50
|
-
temp_path = Path(handle.name)
|
|
51
|
-
json.dump(
|
|
52
|
-
payload,
|
|
53
|
-
handle,
|
|
54
|
-
ensure_ascii=False,
|
|
55
|
-
sort_keys=True,
|
|
56
|
-
indent=2,
|
|
57
|
-
)
|
|
58
|
-
handle.write("\n")
|
|
59
|
-
handle.flush()
|
|
60
|
-
os.fsync(handle.fileno())
|
|
61
|
-
os.replace(temp_path, path)
|
|
62
|
-
temp_path = None
|
|
63
|
-
finally:
|
|
64
|
-
if temp_path is not None:
|
|
65
|
-
temp_path.unlink(missing_ok=True)
|
|
84
|
+
write_owned_object_atomic(path, payload, artifact="convergence artifact")
|
|
85
|
+
except JsonBoundaryError as exc:
|
|
86
|
+
raise ConvergenceContractError(str(exc)) from exc
|
|
66
87
|
|
|
67
88
|
|
|
68
89
|
def write_final_state_atomic(
|
|
@@ -111,7 +132,7 @@ def reserve_dynamic_verifier(
|
|
|
111
132
|
source_role_execution_ref,
|
|
112
133
|
duty_id,
|
|
113
134
|
)
|
|
114
|
-
authority =
|
|
135
|
+
authority = load_owned_json_object(manifest_path)
|
|
115
136
|
if authority.get("taskKey") not in (None, task_key):
|
|
116
137
|
raise ExecutionManifestError("dynamic verifier taskKey does not match manifest")
|
|
117
138
|
project_root = _manifest_project_root(authority, manifest_path)
|
|
@@ -21,6 +21,7 @@ from pathlib import Path
|
|
|
21
21
|
from typing import Any, Iterator
|
|
22
22
|
|
|
23
23
|
from .final_report_schema import load_schema, validate as validate_schema
|
|
24
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
24
25
|
from okstra_project import ResolverError, resolve_project_root
|
|
25
26
|
|
|
26
27
|
|
|
@@ -166,8 +167,10 @@ def assessment_fingerprint(item: Mapping[str, Any]) -> str:
|
|
|
166
167
|
|
|
167
168
|
def _load_plan_data(data_path: Path) -> dict[str, Any]:
|
|
168
169
|
try:
|
|
169
|
-
data =
|
|
170
|
-
except
|
|
170
|
+
data = load_owned_object(data_path, artifact="planning report record")
|
|
171
|
+
except JsonBoundaryError as exc:
|
|
172
|
+
if exc.reason == "top-level JSON value must be an object":
|
|
173
|
+
raise DesignPrepError(f"planning data must be an object: {data_path}") from exc
|
|
171
174
|
raise DesignPrepError(f"cannot read planning data {data_path}: {exc}") from exc
|
|
172
175
|
if not isinstance(data, dict):
|
|
173
176
|
raise DesignPrepError(f"planning data must be an object: {data_path}")
|
|
@@ -1045,8 +1048,8 @@ def _read_carry(path: Path, stage: int, warnings: list[str]) -> Mapping[str, Any
|
|
|
1045
1048
|
if not path.is_file():
|
|
1046
1049
|
return None
|
|
1047
1050
|
try:
|
|
1048
|
-
payload =
|
|
1049
|
-
except
|
|
1051
|
+
payload = load_owned_object(path, artifact="design preparation carry")
|
|
1052
|
+
except JsonBoundaryError as exc:
|
|
1050
1053
|
warnings.append(f"malformed carry stage {stage}; ignored: {exc}")
|
|
1051
1054
|
return None
|
|
1052
1055
|
if not isinstance(payload, Mapping):
|
|
@@ -78,6 +78,7 @@ from .execution_mutation_audit import (
|
|
|
78
78
|
assert_compatible_batch,
|
|
79
79
|
)
|
|
80
80
|
from .convergence_store import write_json_atomic
|
|
81
|
+
from .json_boundary import JsonBoundaryError, external_worker_json_source, load_external_json
|
|
81
82
|
from .assignment_environment import load_assignment_context
|
|
82
83
|
from .model_pool import ModelPool
|
|
83
84
|
from .error_log_write import append_observed
|
|
@@ -97,12 +98,6 @@ from .schema_excerpt import (
|
|
|
97
98
|
excerpt_contract_skew,
|
|
98
99
|
)
|
|
99
100
|
from .seeding import installed_version
|
|
100
|
-
from .report_finalize import (
|
|
101
|
-
STEP_VALIDATE_RUN,
|
|
102
|
-
FinalizeContext,
|
|
103
|
-
FinalizeError,
|
|
104
|
-
run_finalize,
|
|
105
|
-
)
|
|
106
101
|
from .worker_audit_ledger import (
|
|
107
102
|
check_worker_results_audit,
|
|
108
103
|
parse_worker_result_name,
|
|
@@ -463,6 +458,12 @@ def _orchestrator_artifact_roots(plan: DispatchPlan) -> tuple[Path, ...]:
|
|
|
463
458
|
return tuple(roots)
|
|
464
459
|
|
|
465
460
|
|
|
461
|
+
# 이 모듈이 정산하는 모든 디스패치의 집행 방식. 상수인 이유는
|
|
462
|
+
# `dispatch_state._AGENT_ENFORCEMENT_MODES` 의 다른 값(`host-native-spec-link-gate`)
|
|
463
|
+
# 이 호스트 원시 호출 전용이고 이 경로로 오지 않기 때문이다.
|
|
464
|
+
CLI_DISPATCH_ENFORCEMENT_MODE = "core-pre-dispatch"
|
|
465
|
+
|
|
466
|
+
|
|
466
467
|
def _round_artifact_paths(plan: DispatchPlan) -> tuple[Path, ...]:
|
|
467
468
|
"""Every artifact this round's workers are entitled to write.
|
|
468
469
|
|
|
@@ -1245,7 +1246,7 @@ def _invocation_job_fields(
|
|
|
1245
1246
|
"instruction_digest": _require_string(digests, "instructionDigest"),
|
|
1246
1247
|
"prompt_digest": _require_string(digests, "promptDigest"),
|
|
1247
1248
|
"host_model_value": host_model_value,
|
|
1248
|
-
"enforcement_mode":
|
|
1249
|
+
"enforcement_mode": CLI_DISPATCH_ENFORCEMENT_MODE,
|
|
1249
1250
|
}
|
|
1250
1251
|
schema = metadata.get("schemaVersion")
|
|
1251
1252
|
identity_version = metadata.get("executionIdentityVersion")
|
|
@@ -1924,28 +1925,6 @@ def _finish_attempt(
|
|
|
1924
1925
|
settlement = _settle(plan, job, attempt, outcome)
|
|
1925
1926
|
if settlement.completed:
|
|
1926
1927
|
result_link = _link_result(plan, job, attempt)
|
|
1927
|
-
post_process = _post_process_report_writer_result(plan, job)
|
|
1928
|
-
if not post_process["ok"]:
|
|
1929
|
-
reason = _require_string(post_process, "reason")
|
|
1930
|
-
_transition_job_status(plan, job, "error", reason)
|
|
1931
|
-
_update_dispatch_status(
|
|
1932
|
-
plan.team_state_path,
|
|
1933
|
-
job,
|
|
1934
|
-
attempt,
|
|
1935
|
-
"error",
|
|
1936
|
-
reason,
|
|
1937
|
-
)
|
|
1938
|
-
details = _failure_details(
|
|
1939
|
-
job,
|
|
1940
|
-
attempt,
|
|
1941
|
-
outcome,
|
|
1942
|
-
reason,
|
|
1943
|
-
exit_code=1,
|
|
1944
|
-
)
|
|
1945
|
-
details["postProcessing"] = post_process["steps"]
|
|
1946
|
-
_append_event(plan, "worker-failed", details)
|
|
1947
|
-
_finish_manifest_attempt(plan, job, attempt, mutation, None)
|
|
1948
|
-
return False
|
|
1949
1928
|
_transition_job_status(plan, job, "completed", "")
|
|
1950
1929
|
_update_dispatch_status(
|
|
1951
1930
|
plan.team_state_path,
|
|
@@ -1955,7 +1934,6 @@ def _finish_attempt(
|
|
|
1955
1934
|
"; ".join(part for part in (settlement.note, result_link["reason"]) if part),
|
|
1956
1935
|
)
|
|
1957
1936
|
details = _result_details(job, attempt, outcome)
|
|
1958
|
-
details["postProcessing"] = post_process["steps"]
|
|
1959
1937
|
details["resultLink"] = result_link
|
|
1960
1938
|
if settlement.error_log_append is not None:
|
|
1961
1939
|
details["errorLogAppend"] = settlement.error_log_append
|
|
@@ -2007,7 +1985,9 @@ def _audit_attempt(
|
|
|
2007
1985
|
return ExecutionMutationAudit().compare(
|
|
2008
1986
|
snapshot,
|
|
2009
1987
|
policies,
|
|
2010
|
-
out_of_plan_edits=_out_of_plan_edit_paths(
|
|
1988
|
+
out_of_plan_edits=_out_of_plan_edit_paths(
|
|
1989
|
+
job.result_path, project_root=job.project_root
|
|
1990
|
+
),
|
|
2011
1991
|
attempt_succeeded=succeeded,
|
|
2012
1992
|
result_present=result_present,
|
|
2013
1993
|
)
|
|
@@ -2044,14 +2024,26 @@ def _declared_out_of_plan_paths(result_path: Path) -> tuple[str, ...]:
|
|
|
2044
2024
|
return tuple(paths)
|
|
2045
2025
|
|
|
2046
2026
|
|
|
2047
|
-
def _out_of_plan_edit_paths(
|
|
2027
|
+
def _out_of_plan_edit_paths(
|
|
2028
|
+
result_path: Path, *, project_root: Path | None = None
|
|
2029
|
+
) -> tuple[str, ...]:
|
|
2048
2030
|
if result_path.suffix != ".json":
|
|
2049
2031
|
return _declared_out_of_plan_paths(result_path)
|
|
2050
2032
|
if not result_path.is_file():
|
|
2051
2033
|
return ()
|
|
2034
|
+
if project_root is None:
|
|
2035
|
+
raise ValueError("JSON worker result requires project_root")
|
|
2052
2036
|
try:
|
|
2053
|
-
|
|
2054
|
-
|
|
2037
|
+
# 외부 입력: 워커 모델이 게시한 결과 봉투를 디스패처가 수락 전에 판독한다.
|
|
2038
|
+
payload = load_external_json(
|
|
2039
|
+
external_worker_json_source(
|
|
2040
|
+
result_path,
|
|
2041
|
+
trusted_root=project_root,
|
|
2042
|
+
lane_root=result_path.parent,
|
|
2043
|
+
),
|
|
2044
|
+
artifact="worker result",
|
|
2045
|
+
)
|
|
2046
|
+
except JsonBoundaryError:
|
|
2055
2047
|
return ()
|
|
2056
2048
|
implementation = payload.get("implementation") if isinstance(payload, Mapping) else None
|
|
2057
2049
|
rows = implementation.get("outOfPlanEdits") if isinstance(implementation, Mapping) else None
|
|
@@ -2437,36 +2429,6 @@ def _wrapper_log_tail(job: WorkerJob) -> str | None:
|
|
|
2437
2429
|
return tail.decode("utf-8", errors="replace").strip() or None
|
|
2438
2430
|
|
|
2439
2431
|
|
|
2440
|
-
def _post_process_report_writer_result(
|
|
2441
|
-
plan: DispatchPlan,
|
|
2442
|
-
job: WorkerJob,
|
|
2443
|
-
) -> dict[str, Any]:
|
|
2444
|
-
if job.worker_id != REPORT_WRITER_WORKER_ID:
|
|
2445
|
-
return {"ok": True, "reason": "", "steps": []}
|
|
2446
|
-
try:
|
|
2447
|
-
context = FinalizeContext.from_manifest(
|
|
2448
|
-
project_root=plan.project_root,
|
|
2449
|
-
workspace_root=plan.workspace_root,
|
|
2450
|
-
manifest_path=plan.manifest_path,
|
|
2451
|
-
team_state_path=plan.team_state_path,
|
|
2452
|
-
data_path=job.result_path,
|
|
2453
|
-
manifest=plan.manifest,
|
|
2454
|
-
)
|
|
2455
|
-
|
|
2456
|
-
def settle_before(step_name: str) -> None:
|
|
2457
|
-
if step_name == STEP_VALIDATE_RUN:
|
|
2458
|
-
_transition_worker_status(
|
|
2459
|
-
plan.team_state_path,
|
|
2460
|
-
job.worker_id,
|
|
2461
|
-
"completed",
|
|
2462
|
-
"",
|
|
2463
|
-
)
|
|
2464
|
-
|
|
2465
|
-
return run_finalize(context, before_step=settle_before)
|
|
2466
|
-
except FinalizeError as exc:
|
|
2467
|
-
return {"ok": False, "reason": str(exc), "steps": []}
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
2432
|
def _projectionless_translator(plan: DispatchPlan, job: WorkerJob) -> bool:
|
|
2471
2433
|
if not (
|
|
2472
2434
|
job.has_execution_identity
|
|
@@ -2946,7 +2908,15 @@ def _job_from_record(project_root: Path, record: Mapping[str, Any]) -> WorkerJob
|
|
|
2946
2908
|
if isinstance(record.get("hostModelValue"), str)
|
|
2947
2909
|
else None
|
|
2948
2910
|
),
|
|
2949
|
-
|
|
2911
|
+
# jobs 파일은 `agent-prompt materialize` 가 쓴 `.meta.json` 을 옮겨 적는
|
|
2912
|
+
# 것이 정상 경로인데, materialize 는 이 값을 기록하지 않는다 — 기록할
|
|
2913
|
+
# 것이 없기 때문이다. worker-dispatch 를 타는 디스패치의 집행 방식은
|
|
2914
|
+
# 하나뿐이고, 다른 하나(`host-native-spec-link-gate`)는 리드가 호스트
|
|
2915
|
+
# 원시 호출을 직접 기록할 때만 쓴다. 그래서 손으로 채우게 하지 않는다.
|
|
2916
|
+
enforcement_mode=(
|
|
2917
|
+
_string_value(record.get("enforcementMode"))
|
|
2918
|
+
or CLI_DISPATCH_ENFORCEMENT_MODE
|
|
2919
|
+
),
|
|
2950
2920
|
**(identity or {}),
|
|
2951
2921
|
)
|
|
2952
2922
|
|
|
@@ -68,6 +68,13 @@ from .write_policy import (
|
|
|
68
68
|
write_policy_from_payload,
|
|
69
69
|
)
|
|
70
70
|
from .convergence_store import write_json_atomic
|
|
71
|
+
from .json_boundary import (
|
|
72
|
+
JsonBoundaryError,
|
|
73
|
+
external_worker_json_source,
|
|
74
|
+
load_external_json,
|
|
75
|
+
load_owned_object,
|
|
76
|
+
write_owned_object_atomic,
|
|
77
|
+
)
|
|
71
78
|
|
|
72
79
|
BACKEND_CLI_WRAPPER = "cli-wrapper"
|
|
73
80
|
BACKEND_CMUX_PANE = "cmux-pane"
|
|
@@ -369,8 +376,8 @@ def load_json_object(path: Path, label: str) -> dict[str, Any]:
|
|
|
369
376
|
if not path.is_file():
|
|
370
377
|
raise DispatchError(f"{label} not found: {path}")
|
|
371
378
|
try:
|
|
372
|
-
payload =
|
|
373
|
-
except
|
|
379
|
+
payload = load_owned_object(path, artifact=label)
|
|
380
|
+
except JsonBoundaryError as exc:
|
|
374
381
|
raise DispatchError(f"{label} is invalid JSON: {path}: {exc}") from exc
|
|
375
382
|
if not isinstance(payload, dict):
|
|
376
383
|
raise DispatchError(f"{label} must be a JSON object: {path}")
|
|
@@ -381,26 +388,7 @@ def write_json(path: Path, payload: Mapping[str, Any]) -> None:
|
|
|
381
388
|
# Write via temp file + os.replace so a crash mid-write cannot leave a
|
|
382
389
|
# truncated team-state.json that every later load_json_object rejects;
|
|
383
390
|
# os.replace is atomic on POSIX, matching run_context._atomic_write_json.
|
|
384
|
-
path
|
|
385
|
-
temp_path: Path | None = None
|
|
386
|
-
try:
|
|
387
|
-
with tempfile.NamedTemporaryFile(
|
|
388
|
-
mode="w",
|
|
389
|
-
encoding="utf-8",
|
|
390
|
-
dir=path.parent,
|
|
391
|
-
prefix=f".{path.name}.",
|
|
392
|
-
suffix=".tmp",
|
|
393
|
-
delete=False,
|
|
394
|
-
) as handle:
|
|
395
|
-
temp_path = Path(handle.name)
|
|
396
|
-
handle.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
|
397
|
-
handle.flush()
|
|
398
|
-
os.fsync(handle.fileno())
|
|
399
|
-
os.replace(temp_path, path)
|
|
400
|
-
temp_path = None
|
|
401
|
-
finally:
|
|
402
|
-
if temp_path is not None:
|
|
403
|
-
temp_path.unlink(missing_ok=True)
|
|
391
|
+
write_owned_object_atomic(path, payload, artifact="team state")
|
|
404
392
|
|
|
405
393
|
|
|
406
394
|
@contextlib.contextmanager
|
|
@@ -1107,6 +1095,33 @@ def _finish_native_agent_attempt(
|
|
|
1107
1095
|
if dispatch.get("writeEnforcement") != invocation.write_enforcement:
|
|
1108
1096
|
raise DispatchError("agent dispatch write enforcement changed")
|
|
1109
1097
|
policy = write_policy_from_payload(invocation.write_policy)
|
|
1098
|
+
status, summary = _native_mutation_result(
|
|
1099
|
+
project_root=project_root,
|
|
1100
|
+
dispatch=dispatch,
|
|
1101
|
+
result_path=result_path,
|
|
1102
|
+
policy=policy,
|
|
1103
|
+
)
|
|
1104
|
+
_close_native_attempt(
|
|
1105
|
+
manifest_path,
|
|
1106
|
+
manifest,
|
|
1107
|
+
identity,
|
|
1108
|
+
status,
|
|
1109
|
+
result_relative,
|
|
1110
|
+
summary,
|
|
1111
|
+
)
|
|
1112
|
+
if status != "ok":
|
|
1113
|
+
violations = summary.get("violations")
|
|
1114
|
+
detail = "; ".join(violations) if isinstance(violations, list) else status
|
|
1115
|
+
raise DispatchError(f"agent mutation audit failed: {detail or status}")
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def _native_mutation_result(
|
|
1119
|
+
*,
|
|
1120
|
+
project_root: Path,
|
|
1121
|
+
dispatch: Mapping[str, Any],
|
|
1122
|
+
result_path: Path,
|
|
1123
|
+
policy: WritePolicy,
|
|
1124
|
+
) -> tuple[str, Mapping[str, Any]]:
|
|
1110
1125
|
snapshot_value = string_value(dispatch.get("mutationAuditSnapshotPath"))
|
|
1111
1126
|
if snapshot_value:
|
|
1112
1127
|
snapshot_payload = load_json_object(
|
|
@@ -1120,7 +1135,9 @@ def _finish_native_agent_attempt(
|
|
|
1120
1135
|
result = ExecutionMutationAudit().compare(
|
|
1121
1136
|
snapshot,
|
|
1122
1137
|
(policy,),
|
|
1123
|
-
out_of_plan_edits=_native_out_of_plan_paths(
|
|
1138
|
+
out_of_plan_edits=_native_out_of_plan_paths(
|
|
1139
|
+
result_path, project_root=project_root
|
|
1140
|
+
),
|
|
1124
1141
|
attempt_succeeded=True,
|
|
1125
1142
|
result_present=True,
|
|
1126
1143
|
)
|
|
@@ -1139,18 +1156,7 @@ def _finish_native_agent_attempt(
|
|
|
1139
1156
|
"beforeDigest": "",
|
|
1140
1157
|
"afterDigest": "",
|
|
1141
1158
|
}
|
|
1142
|
-
|
|
1143
|
-
manifest_path,
|
|
1144
|
-
manifest,
|
|
1145
|
-
identity,
|
|
1146
|
-
status,
|
|
1147
|
-
result_relative,
|
|
1148
|
-
summary,
|
|
1149
|
-
)
|
|
1150
|
-
if status != "ok":
|
|
1151
|
-
violations = summary.get("violations")
|
|
1152
|
-
detail = "; ".join(violations) if isinstance(violations, list) else status
|
|
1153
|
-
raise DispatchError(f"agent mutation audit failed: {detail or status}")
|
|
1159
|
+
return status, summary
|
|
1154
1160
|
|
|
1155
1161
|
|
|
1156
1162
|
def _close_native_attempt(
|
|
@@ -1191,12 +1197,22 @@ def _close_native_attempt(
|
|
|
1191
1197
|
)
|
|
1192
1198
|
|
|
1193
1199
|
|
|
1194
|
-
def _native_out_of_plan_paths(
|
|
1200
|
+
def _native_out_of_plan_paths(
|
|
1201
|
+
result_path: Path, *, project_root: Path
|
|
1202
|
+
) -> tuple[str, ...]:
|
|
1195
1203
|
if result_path.suffix != ".json":
|
|
1196
1204
|
return ()
|
|
1197
1205
|
try:
|
|
1198
|
-
|
|
1199
|
-
|
|
1206
|
+
# 외부 입력: 네이티브 모델 결과에서 선택적 수정 경로만 투영한다.
|
|
1207
|
+
payload = load_external_json(
|
|
1208
|
+
external_worker_json_source(
|
|
1209
|
+
result_path,
|
|
1210
|
+
trusted_root=project_root,
|
|
1211
|
+
lane_root=result_path.parent,
|
|
1212
|
+
),
|
|
1213
|
+
artifact="native worker result",
|
|
1214
|
+
)
|
|
1215
|
+
except JsonBoundaryError:
|
|
1200
1216
|
return ()
|
|
1201
1217
|
implementation = payload.get("implementation") if isinstance(payload, Mapping) else None
|
|
1202
1218
|
rows = implementation.get("outOfPlanEdits") if isinstance(implementation, Mapping) else None
|
|
@@ -1668,6 +1684,26 @@ def _validate_agent_invocations(
|
|
|
1668
1684
|
raise DispatchError("agent invocation contract: " + "; ".join(errors))
|
|
1669
1685
|
|
|
1670
1686
|
|
|
1687
|
+
def divergent_fields(
|
|
1688
|
+
pairs: Sequence[tuple[str, Any, Any]],
|
|
1689
|
+
*,
|
|
1690
|
+
left: str,
|
|
1691
|
+
right: str,
|
|
1692
|
+
) -> list[str]:
|
|
1693
|
+
"""어긋난 필드만 이름과 두 값으로 적는다.
|
|
1694
|
+
|
|
1695
|
+
이 두 검사는 각각 일곱 개와 다섯 개의 조건을 `or` 로 묶어 한 문장으로
|
|
1696
|
+
보고했다. 그래서 재시도 프롬프트의 `dispatchKind` 하나가 틀린 경우와
|
|
1697
|
+
provider 가 틀린 경우가 같은 문장으로 나왔고, 읽는 쪽이 어느 값을 봐야 할지
|
|
1698
|
+
알 수 없었다. 판정에 쓴 값은 이미 손에 있으므로 감출 이유가 없다.
|
|
1699
|
+
"""
|
|
1700
|
+
return [
|
|
1701
|
+
f"{name}: {left}={found!r} {right}={expected!r}"
|
|
1702
|
+
for name, found, expected in pairs
|
|
1703
|
+
if found != expected
|
|
1704
|
+
]
|
|
1705
|
+
|
|
1706
|
+
|
|
1671
1707
|
def _v2_job_authority_errors(
|
|
1672
1708
|
job: WorkerJob,
|
|
1673
1709
|
metadata: Mapping[str, Any],
|
|
@@ -1682,14 +1718,22 @@ def _v2_job_authority_errors(
|
|
|
1682
1718
|
errors: list[str] = []
|
|
1683
1719
|
if job.invocation_ref != identity.invocation_ref:
|
|
1684
1720
|
errors.append("jobs file invocationRef does not match invocation metadata")
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1721
|
+
divergent = divergent_fields(
|
|
1722
|
+
(
|
|
1723
|
+
("participantRef", identity.participant_ref, job.participant_ref),
|
|
1724
|
+
("roleExecutionRef", identity.role_execution_ref, job.role_execution_ref),
|
|
1725
|
+
("executionLabel", identity.execution_label, job.execution_label),
|
|
1726
|
+
("dutyId", identity.duty_id, job.duty_id),
|
|
1727
|
+
("attempt", identity.attempt, job.attempt),
|
|
1728
|
+
),
|
|
1729
|
+
left="metadata",
|
|
1730
|
+
right="job",
|
|
1731
|
+
)
|
|
1732
|
+
if divergent:
|
|
1733
|
+
errors.append(
|
|
1734
|
+
"jobs file v2 identity does not match invocation metadata — "
|
|
1735
|
+
+ "; ".join(divergent)
|
|
1736
|
+
)
|
|
1693
1737
|
prompt = metadata.get("prompt")
|
|
1694
1738
|
metadata_prompt_value = (
|
|
1695
1739
|
string_value(prompt.get("path")) if isinstance(prompt, Mapping) else ""
|
|
@@ -1734,19 +1778,50 @@ def _v2_job_authority_errors(
|
|
|
1734
1778
|
assignment=parsed_assignment,
|
|
1735
1779
|
)
|
|
1736
1780
|
)
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1781
|
+
missing = [
|
|
1782
|
+
name for name, value in (
|
|
1783
|
+
("roleExecution", execution),
|
|
1784
|
+
("participant", participant),
|
|
1785
|
+
("modelAssignment", assignment),
|
|
1786
|
+
)
|
|
1787
|
+
if not isinstance(value, Mapping)
|
|
1788
|
+
]
|
|
1789
|
+
if missing:
|
|
1790
|
+
errors.append(
|
|
1791
|
+
"jobs file v2 identity does not match role execution authority: "
|
|
1792
|
+
f"the run manifest has no {', '.join(missing)} for this job"
|
|
1793
|
+
)
|
|
1794
|
+
return errors
|
|
1795
|
+
# 어긋난 필드를 이름으로 댄다. 일곱 조건을 한 문장으로 접던 동안, dispatchKind
|
|
1796
|
+
# 하나가 틀린 경우와 provider 가 틀린 경우가 같은 문장으로 보고됐다 —
|
|
1797
|
+
# 재시도 프롬프트를 만들다 `initial-retry` 로 적은 리드가 원인을 좁히지
|
|
1798
|
+
# 못했다. 판정에 쓴 값은 여기 다 있으므로 굳이 감출 이유가 없다.
|
|
1799
|
+
divergent = divergent_fields(
|
|
1800
|
+
(
|
|
1801
|
+
("participantRef",
|
|
1802
|
+
execution.get("participantRef"), job.participant_ref),
|
|
1803
|
+
("executionLabel",
|
|
1804
|
+
execution.get("executionLabel"), job.execution_label),
|
|
1805
|
+
("role", execution.get("role"), job.role),
|
|
1806
|
+
("modelExecutionValue",
|
|
1807
|
+
assignment.get("modelExecutionValue"), job.model_execution_value),
|
|
1808
|
+
("dispatchKind",
|
|
1809
|
+
metadata.get("dispatchKind"), job.dispatch_kind),
|
|
1810
|
+
("provider", execution.get("provider"), job.provider),
|
|
1811
|
+
),
|
|
1812
|
+
left="manifest",
|
|
1813
|
+
right="job",
|
|
1814
|
+
)
|
|
1815
|
+
if execution.get("role") != duty_role:
|
|
1816
|
+
divergent.append(
|
|
1817
|
+
f"role: roleExecution={execution.get('role')!r} "
|
|
1818
|
+
f"duty {job.duty_id!r} requires {duty_role!r}"
|
|
1819
|
+
)
|
|
1820
|
+
if divergent:
|
|
1821
|
+
errors.append(
|
|
1822
|
+
"jobs file v2 identity does not match role execution authority — "
|
|
1823
|
+
+ "; ".join(divergent)
|
|
1824
|
+
)
|
|
1750
1825
|
return errors
|
|
1751
1826
|
|
|
1752
1827
|
|
|
@@ -15,6 +15,7 @@ from . import improvement_lenses, worktree_registry
|
|
|
15
15
|
from .models import provider_wrappers
|
|
16
16
|
from .workers import resolve_profile_workers
|
|
17
17
|
from .worktree import is_git_work_tree, main_worktree_path
|
|
18
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
18
19
|
|
|
19
20
|
SUPPORTED_PHASES: tuple[str, ...] = (
|
|
20
21
|
"implementation",
|
|
@@ -156,7 +157,9 @@ def _architecture_declaration_check(project_root: Path) -> DoctorCheck | None:
|
|
|
156
157
|
value is read here: `resolve_architecture()` maps absent and `none` alike.
|
|
157
158
|
"""
|
|
158
159
|
try:
|
|
159
|
-
payload =
|
|
160
|
+
payload = load_owned_object(
|
|
161
|
+
project_json_path(project_root), artifact="project configuration"
|
|
162
|
+
)
|
|
160
163
|
except (OSError, ValueError):
|
|
161
164
|
return None
|
|
162
165
|
architecture = payload.get("architecture") if isinstance(payload, dict) else None
|
|
@@ -383,8 +386,8 @@ def _worktree_registry_check() -> DoctorCheck:
|
|
|
383
386
|
if not path.exists():
|
|
384
387
|
return _ok("worktree registry", f"no registry yet ({path})")
|
|
385
388
|
try:
|
|
386
|
-
data =
|
|
387
|
-
except
|
|
389
|
+
data = load_owned_object(path, artifact="worktree registry")
|
|
390
|
+
except JsonBoundaryError as exc:
|
|
388
391
|
return _fail("worktree registry", f"{path} is unreadable: {exc}")
|
|
389
392
|
if not isinstance(data, dict) or not isinstance(data.get("tasks"), dict):
|
|
390
393
|
return _fail("worktree registry", f"{path} is missing a valid 'tasks' table")
|