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
|
@@ -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,
|
|
@@ -310,7 +305,7 @@ class DispatchPlan:
|
|
|
310
305
|
@dataclass(frozen=True)
|
|
311
306
|
class _BuildOptions:
|
|
312
307
|
okstra_bin: Path | None
|
|
313
|
-
idle_timeout_seconds: int
|
|
308
|
+
idle_timeout_seconds: int | None
|
|
314
309
|
default_backend: str
|
|
315
310
|
supported_worker_wrappers: Mapping[str, str]
|
|
316
311
|
unsupported_worker_label: str
|
|
@@ -335,7 +330,7 @@ def build_dispatch_plan(
|
|
|
335
330
|
workspace_root: Path,
|
|
336
331
|
okstra_bin: Path | None = None,
|
|
337
332
|
requested_workers: Sequence[str] = (),
|
|
338
|
-
idle_timeout_seconds: int =
|
|
333
|
+
idle_timeout_seconds: int | None = None,
|
|
339
334
|
required_lead_runtime: str | None = None,
|
|
340
335
|
default_backend: str = BACKEND_CLI_WRAPPER,
|
|
341
336
|
supported_worker_wrappers: Mapping[str, str],
|
|
@@ -432,6 +427,43 @@ def dispatch_plan(plan: DispatchPlan, *, wait: bool = True) -> int:
|
|
|
432
427
|
return 0
|
|
433
428
|
|
|
434
429
|
|
|
430
|
+
def _orchestrator_artifact_roots(plan: DispatchPlan) -> tuple[Path, ...]:
|
|
431
|
+
"""okstra 자신이 이 run 동안 쓰는 산출물 경로.
|
|
432
|
+
|
|
433
|
+
`_round_artifact_paths` 는 이번 배치의 job 만 알기 때문에 원리상 완결될 수
|
|
434
|
+
없다. 감사 창에 들어오는 것은 형제 job 의 산출물만이 아니라 같은 run 의 다른
|
|
435
|
+
라운드가 남기는 것들이다 — 재시도 프롬프트와 그 `.meta.json` / `.publish.lock`,
|
|
436
|
+
이전 라운드 워커의 `.log` / `.status.json` / `.mutation-audit.json`, critic 의
|
|
437
|
+
결과, design-prep 요청. 실측 한 건에서 그렇게 빠져나간 경로가 16개였고 그
|
|
438
|
+
배치의 디스패치 5건이 전부 `readonly source changed` 로 떨어졌다.
|
|
439
|
+
|
|
440
|
+
그래서 파일을 더 세는 대신 트리를 싣는다. 이 디렉터리 아래는 정의상 okstra 의
|
|
441
|
+
산출물이고 프로젝트 소스가 아니다 — 소스가 바뀌었는지를 묻는 질문의 답이 될 수
|
|
442
|
+
없다.
|
|
443
|
+
"""
|
|
444
|
+
# 실행 매니페스트는 `<run_dir>/manifests/` 아래 놓인다.
|
|
445
|
+
roots = [plan.manifest_path.resolve().parents[1]]
|
|
446
|
+
# task 매니페스트와 discovery 카탈로그는 run 디렉터리 위에 있고, prep 과
|
|
447
|
+
# 완료 기록이 실행 중에 갱신한다. 경로는 매니페스트가 이미 들고 있으므로
|
|
448
|
+
# 디렉터리 깊이를 세지 않는다 — implementation 은 stage 층이 하나 더 있어
|
|
449
|
+
# 고정 인덱스로는 task root 에 닿지 못한다.
|
|
450
|
+
for key in ("taskManifestPath", "taskCatalogPath"):
|
|
451
|
+
value = plan.manifest.get(key)
|
|
452
|
+
if isinstance(value, str) and value:
|
|
453
|
+
candidate = Path(value)
|
|
454
|
+
roots.append(
|
|
455
|
+
candidate if candidate.is_absolute()
|
|
456
|
+
else (plan.project_root / candidate)
|
|
457
|
+
)
|
|
458
|
+
return tuple(roots)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
# 이 모듈이 정산하는 모든 디스패치의 집행 방식. 상수인 이유는
|
|
462
|
+
# `dispatch_state._AGENT_ENFORCEMENT_MODES` 의 다른 값(`host-native-spec-link-gate`)
|
|
463
|
+
# 이 호스트 원시 호출 전용이고 이 경로로 오지 않기 때문이다.
|
|
464
|
+
CLI_DISPATCH_ENFORCEMENT_MODE = "core-pre-dispatch"
|
|
465
|
+
|
|
466
|
+
|
|
435
467
|
def _round_artifact_paths(plan: DispatchPlan) -> tuple[Path, ...]:
|
|
436
468
|
"""Every artifact this round's workers are entitled to write.
|
|
437
469
|
|
|
@@ -1214,7 +1246,7 @@ def _invocation_job_fields(
|
|
|
1214
1246
|
"instruction_digest": _require_string(digests, "instructionDigest"),
|
|
1215
1247
|
"prompt_digest": _require_string(digests, "promptDigest"),
|
|
1216
1248
|
"host_model_value": host_model_value,
|
|
1217
|
-
"enforcement_mode":
|
|
1249
|
+
"enforcement_mode": CLI_DISPATCH_ENFORCEMENT_MODE,
|
|
1218
1250
|
}
|
|
1219
1251
|
schema = metadata.get("schemaVersion")
|
|
1220
1252
|
identity_version = metadata.get("executionIdentityVersion")
|
|
@@ -1488,6 +1520,7 @@ def _mutation_snapshot(
|
|
|
1488
1520
|
orchestrator_paths=(
|
|
1489
1521
|
*round_artifact_paths,
|
|
1490
1522
|
*_run_errors_log_path(plan),
|
|
1523
|
+
*_orchestrator_artifact_roots(plan),
|
|
1491
1524
|
plan.manifest_path,
|
|
1492
1525
|
plan.team_state_path,
|
|
1493
1526
|
Path(f"{plan.team_state_path}.lock"),
|
|
@@ -1840,6 +1873,31 @@ def _retry_from_record(
|
|
|
1840
1873
|
_spawn_job(plan, _job_for_next_attempt(job), attempt + 1)
|
|
1841
1874
|
|
|
1842
1875
|
|
|
1876
|
+
# 사유 한 줄에 실을 경로 개수. 전부 실으면 team-state 행이 부풀고, 하나도 안
|
|
1877
|
+
# 실으면 읽는 쪽이 다른 파일을 뒤져야 한다.
|
|
1878
|
+
_MUTATION_REASON_PATH_LIMIT = 5
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
def _mutation_failure_reason(mutation) -> str:
|
|
1882
|
+
"""왜 실패했는지와 무엇이 바뀌었는지를 한 줄에 담는다.
|
|
1883
|
+
|
|
1884
|
+
종전에는 디스패치 행에 `readonly source changed` 만 남고 바뀐 경로는
|
|
1885
|
+
lead-events 에만 있었다. 읽는 쪽이 그 파일의 존재를 알아야 원인에 닿을 수
|
|
1886
|
+
있었고, 실제로 두 번의 진단이 같은 자리에서 빗나갔다 — 한 번은 감사를 손으로
|
|
1887
|
+
재구현해서, 한 번은 사용자 편집을 의심해서. 감사는 답을 이미 계산해 두었으니
|
|
1888
|
+
사유 옆에 놓는다.
|
|
1889
|
+
"""
|
|
1890
|
+
reason = "; ".join(mutation.violations) or mutation.status
|
|
1891
|
+
paths = list(mutation.changed_paths)
|
|
1892
|
+
if not paths:
|
|
1893
|
+
return reason
|
|
1894
|
+
shown = ", ".join(paths[:_MUTATION_REASON_PATH_LIMIT])
|
|
1895
|
+
remaining = len(paths) - _MUTATION_REASON_PATH_LIMIT
|
|
1896
|
+
if remaining > 0:
|
|
1897
|
+
shown += f", +{remaining} more"
|
|
1898
|
+
return f"{reason} [changed: {shown}]"
|
|
1899
|
+
|
|
1900
|
+
|
|
1843
1901
|
def _finish_attempt(
|
|
1844
1902
|
plan: DispatchPlan,
|
|
1845
1903
|
job: WorkerJob,
|
|
@@ -1854,7 +1912,7 @@ def _finish_attempt(
|
|
|
1854
1912
|
"contract-failed-unattributed",
|
|
1855
1913
|
"mutation-present-unresolved",
|
|
1856
1914
|
}:
|
|
1857
|
-
reason =
|
|
1915
|
+
reason = _mutation_failure_reason(mutation)
|
|
1858
1916
|
_transition_job_status(plan, job, "error", reason)
|
|
1859
1917
|
_update_dispatch_status(
|
|
1860
1918
|
plan.team_state_path, job, attempt, "error", reason
|
|
@@ -1867,28 +1925,6 @@ def _finish_attempt(
|
|
|
1867
1925
|
settlement = _settle(plan, job, attempt, outcome)
|
|
1868
1926
|
if settlement.completed:
|
|
1869
1927
|
result_link = _link_result(plan, job, attempt)
|
|
1870
|
-
post_process = _post_process_report_writer_result(plan, job)
|
|
1871
|
-
if not post_process["ok"]:
|
|
1872
|
-
reason = _require_string(post_process, "reason")
|
|
1873
|
-
_transition_job_status(plan, job, "error", reason)
|
|
1874
|
-
_update_dispatch_status(
|
|
1875
|
-
plan.team_state_path,
|
|
1876
|
-
job,
|
|
1877
|
-
attempt,
|
|
1878
|
-
"error",
|
|
1879
|
-
reason,
|
|
1880
|
-
)
|
|
1881
|
-
details = _failure_details(
|
|
1882
|
-
job,
|
|
1883
|
-
attempt,
|
|
1884
|
-
outcome,
|
|
1885
|
-
reason,
|
|
1886
|
-
exit_code=1,
|
|
1887
|
-
)
|
|
1888
|
-
details["postProcessing"] = post_process["steps"]
|
|
1889
|
-
_append_event(plan, "worker-failed", details)
|
|
1890
|
-
_finish_manifest_attempt(plan, job, attempt, mutation, None)
|
|
1891
|
-
return False
|
|
1892
1928
|
_transition_job_status(plan, job, "completed", "")
|
|
1893
1929
|
_update_dispatch_status(
|
|
1894
1930
|
plan.team_state_path,
|
|
@@ -1898,7 +1934,6 @@ def _finish_attempt(
|
|
|
1898
1934
|
"; ".join(part for part in (settlement.note, result_link["reason"]) if part),
|
|
1899
1935
|
)
|
|
1900
1936
|
details = _result_details(job, attempt, outcome)
|
|
1901
|
-
details["postProcessing"] = post_process["steps"]
|
|
1902
1937
|
details["resultLink"] = result_link
|
|
1903
1938
|
if settlement.error_log_append is not None:
|
|
1904
1939
|
details["errorLogAppend"] = settlement.error_log_append
|
|
@@ -1950,7 +1985,9 @@ def _audit_attempt(
|
|
|
1950
1985
|
return ExecutionMutationAudit().compare(
|
|
1951
1986
|
snapshot,
|
|
1952
1987
|
policies,
|
|
1953
|
-
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
|
+
),
|
|
1954
1991
|
attempt_succeeded=succeeded,
|
|
1955
1992
|
result_present=result_present,
|
|
1956
1993
|
)
|
|
@@ -1987,14 +2024,26 @@ def _declared_out_of_plan_paths(result_path: Path) -> tuple[str, ...]:
|
|
|
1987
2024
|
return tuple(paths)
|
|
1988
2025
|
|
|
1989
2026
|
|
|
1990
|
-
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, ...]:
|
|
1991
2030
|
if result_path.suffix != ".json":
|
|
1992
2031
|
return _declared_out_of_plan_paths(result_path)
|
|
1993
2032
|
if not result_path.is_file():
|
|
1994
2033
|
return ()
|
|
2034
|
+
if project_root is None:
|
|
2035
|
+
raise ValueError("JSON worker result requires project_root")
|
|
1995
2036
|
try:
|
|
1996
|
-
|
|
1997
|
-
|
|
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:
|
|
1998
2047
|
return ()
|
|
1999
2048
|
implementation = payload.get("implementation") if isinstance(payload, Mapping) else None
|
|
2000
2049
|
rows = implementation.get("outOfPlanEdits") if isinstance(implementation, Mapping) else None
|
|
@@ -2380,36 +2429,6 @@ def _wrapper_log_tail(job: WorkerJob) -> str | None:
|
|
|
2380
2429
|
return tail.decode("utf-8", errors="replace").strip() or None
|
|
2381
2430
|
|
|
2382
2431
|
|
|
2383
|
-
def _post_process_report_writer_result(
|
|
2384
|
-
plan: DispatchPlan,
|
|
2385
|
-
job: WorkerJob,
|
|
2386
|
-
) -> dict[str, Any]:
|
|
2387
|
-
if job.worker_id != REPORT_WRITER_WORKER_ID:
|
|
2388
|
-
return {"ok": True, "reason": "", "steps": []}
|
|
2389
|
-
try:
|
|
2390
|
-
context = FinalizeContext.from_manifest(
|
|
2391
|
-
project_root=plan.project_root,
|
|
2392
|
-
workspace_root=plan.workspace_root,
|
|
2393
|
-
manifest_path=plan.manifest_path,
|
|
2394
|
-
team_state_path=plan.team_state_path,
|
|
2395
|
-
data_path=job.result_path,
|
|
2396
|
-
manifest=plan.manifest,
|
|
2397
|
-
)
|
|
2398
|
-
|
|
2399
|
-
def settle_before(step_name: str) -> None:
|
|
2400
|
-
if step_name == STEP_VALIDATE_RUN:
|
|
2401
|
-
_transition_worker_status(
|
|
2402
|
-
plan.team_state_path,
|
|
2403
|
-
job.worker_id,
|
|
2404
|
-
"completed",
|
|
2405
|
-
"",
|
|
2406
|
-
)
|
|
2407
|
-
|
|
2408
|
-
return run_finalize(context, before_step=settle_before)
|
|
2409
|
-
except FinalizeError as exc:
|
|
2410
|
-
return {"ok": False, "reason": str(exc), "steps": []}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
2432
|
def _projectionless_translator(plan: DispatchPlan, job: WorkerJob) -> bool:
|
|
2414
2433
|
if not (
|
|
2415
2434
|
job.has_execution_identity
|
|
@@ -2866,7 +2885,8 @@ def _job_from_record(project_root: Path, record: Mapping[str, Any]) -> WorkerJob
|
|
|
2866
2885
|
completion_paths=tuple(_record_completion_paths(record)),
|
|
2867
2886
|
worktree_path=_string_value(record.get("worktreePath")),
|
|
2868
2887
|
role=_require_string(record, "role"),
|
|
2869
|
-
|
|
2888
|
+
# 복원된 job 은 다시 실행되지 않으므로 예산을 정하지 않는다.
|
|
2889
|
+
idle_timeout_seconds=None,
|
|
2870
2890
|
dispatch_kind=_require_string(record, "kind"),
|
|
2871
2891
|
# Restored, never re-issued: both callers rebuild a job from its record
|
|
2872
2892
|
# to settle a dispatch that already ran, and a fresh id would settle it
|
|
@@ -2888,7 +2908,15 @@ def _job_from_record(project_root: Path, record: Mapping[str, Any]) -> WorkerJob
|
|
|
2888
2908
|
if isinstance(record.get("hostModelValue"), str)
|
|
2889
2909
|
else None
|
|
2890
2910
|
),
|
|
2891
|
-
|
|
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
|
+
),
|
|
2892
2920
|
**(identity or {}),
|
|
2893
2921
|
)
|
|
2894
2922
|
|
|
@@ -47,6 +47,7 @@ from .execution_manifest import (
|
|
|
47
47
|
)
|
|
48
48
|
from .execution_mutation_audit import ExecutionMutationAudit, MutationSnapshot
|
|
49
49
|
from .final_report_paths import final_report_data_path
|
|
50
|
+
from .report_inputs import report_narrative_path, uses_report_contract_v3
|
|
50
51
|
from .worker_prompt_body import REPORT_WRITER_WORKER_ID
|
|
51
52
|
from .worker_prompt_contract import (
|
|
52
53
|
PromptRecord,
|
|
@@ -67,6 +68,13 @@ from .write_policy import (
|
|
|
67
68
|
write_policy_from_payload,
|
|
68
69
|
)
|
|
69
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
|
+
)
|
|
70
78
|
|
|
71
79
|
BACKEND_CLI_WRAPPER = "cli-wrapper"
|
|
72
80
|
BACKEND_CMUX_PANE = "cmux-pane"
|
|
@@ -152,7 +160,11 @@ class WorkerJob:
|
|
|
152
160
|
completion_paths: tuple[Path, ...]
|
|
153
161
|
worktree_path: str
|
|
154
162
|
role: str
|
|
155
|
-
|
|
163
|
+
# None 은 "이 디스패치가 예산을 정하지 않는다" 는 뜻이고, 그때 예산은
|
|
164
|
+
# 역할이 정한다(`domain/worker_role.role_spec`). 여기에 숫자 기본값을 두면
|
|
165
|
+
# 역할별 예산이 영영 도달 불가능해진다 — 엔트리포인트는 이 자리가 비었을
|
|
166
|
+
# 때만 역할을 보기 때문이다.
|
|
167
|
+
idle_timeout_seconds: int | None
|
|
156
168
|
dispatch_kind: str
|
|
157
169
|
invocation_id: str = ""
|
|
158
170
|
audience: str = ""
|
|
@@ -188,7 +200,7 @@ class WorkerJob:
|
|
|
188
200
|
str(self.prompt_path),
|
|
189
201
|
self.worktree_path,
|
|
190
202
|
self.wrapper_role,
|
|
191
|
-
|
|
203
|
+
self._idle_timeout_argument,
|
|
192
204
|
"--presentation",
|
|
193
205
|
self._presentation(),
|
|
194
206
|
]
|
|
@@ -201,6 +213,18 @@ class WorkerJob:
|
|
|
201
213
|
argv += ["--session-id", self.session_id]
|
|
202
214
|
return argv
|
|
203
215
|
|
|
216
|
+
@property
|
|
217
|
+
def _idle_timeout_argument(self) -> str:
|
|
218
|
+
"""엔트리포인트가 읽는 유휴 예산 자리.
|
|
219
|
+
|
|
220
|
+
비워 두면 엔트리포인트가 역할의 예산을 쓴다. 이 자리를 항상 채우던
|
|
221
|
+
동안에는 `worker_request.idle_timeout` 의 역할 분기가 한 번도 실행되지
|
|
222
|
+
않았고, executor/verifier 의 1500s 도 함께 죽어 있었다.
|
|
223
|
+
"""
|
|
224
|
+
if self.idle_timeout_seconds is None:
|
|
225
|
+
return ""
|
|
226
|
+
return str(self.idle_timeout_seconds)
|
|
227
|
+
|
|
204
228
|
@property
|
|
205
229
|
def wrapper_role(self) -> str:
|
|
206
230
|
"""The canonical role the entrypoint's role positional is read as.
|
|
@@ -352,8 +376,8 @@ def load_json_object(path: Path, label: str) -> dict[str, Any]:
|
|
|
352
376
|
if not path.is_file():
|
|
353
377
|
raise DispatchError(f"{label} not found: {path}")
|
|
354
378
|
try:
|
|
355
|
-
payload =
|
|
356
|
-
except
|
|
379
|
+
payload = load_owned_object(path, artifact=label)
|
|
380
|
+
except JsonBoundaryError as exc:
|
|
357
381
|
raise DispatchError(f"{label} is invalid JSON: {path}: {exc}") from exc
|
|
358
382
|
if not isinstance(payload, dict):
|
|
359
383
|
raise DispatchError(f"{label} must be a JSON object: {path}")
|
|
@@ -364,26 +388,7 @@ def write_json(path: Path, payload: Mapping[str, Any]) -> None:
|
|
|
364
388
|
# Write via temp file + os.replace so a crash mid-write cannot leave a
|
|
365
389
|
# truncated team-state.json that every later load_json_object rejects;
|
|
366
390
|
# os.replace is atomic on POSIX, matching run_context._atomic_write_json.
|
|
367
|
-
path
|
|
368
|
-
temp_path: Path | None = None
|
|
369
|
-
try:
|
|
370
|
-
with tempfile.NamedTemporaryFile(
|
|
371
|
-
mode="w",
|
|
372
|
-
encoding="utf-8",
|
|
373
|
-
dir=path.parent,
|
|
374
|
-
prefix=f".{path.name}.",
|
|
375
|
-
suffix=".tmp",
|
|
376
|
-
delete=False,
|
|
377
|
-
) as handle:
|
|
378
|
-
temp_path = Path(handle.name)
|
|
379
|
-
handle.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
|
380
|
-
handle.flush()
|
|
381
|
-
os.fsync(handle.fileno())
|
|
382
|
-
os.replace(temp_path, path)
|
|
383
|
-
temp_path = None
|
|
384
|
-
finally:
|
|
385
|
-
if temp_path is not None:
|
|
386
|
-
temp_path.unlink(missing_ok=True)
|
|
391
|
+
write_owned_object_atomic(path, payload, artifact="team state")
|
|
387
392
|
|
|
388
393
|
|
|
389
394
|
@contextlib.contextmanager
|
|
@@ -707,6 +712,10 @@ def record_verified_agent_dispatch(
|
|
|
707
712
|
snapshot = ExecutionMutationAudit().snapshot(
|
|
708
713
|
(write_policy,),
|
|
709
714
|
orchestrator_paths=(
|
|
715
|
+
# run 산출물 트리 전체. 같은 run 의 다른 라운드가 감사 창
|
|
716
|
+
# 안에 남기는 프롬프트·로그·사이드카를 파일 단위로 미리 셀
|
|
717
|
+
# 수 없고, 그 아래는 정의상 소스가 아니다.
|
|
718
|
+
Path(run_manifest_path).resolve().parents[1],
|
|
710
719
|
run_manifest_path,
|
|
711
720
|
team_state_path,
|
|
712
721
|
Path(f"{team_state_path}.lock"),
|
|
@@ -1086,6 +1095,33 @@ def _finish_native_agent_attempt(
|
|
|
1086
1095
|
if dispatch.get("writeEnforcement") != invocation.write_enforcement:
|
|
1087
1096
|
raise DispatchError("agent dispatch write enforcement changed")
|
|
1088
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]]:
|
|
1089
1125
|
snapshot_value = string_value(dispatch.get("mutationAuditSnapshotPath"))
|
|
1090
1126
|
if snapshot_value:
|
|
1091
1127
|
snapshot_payload = load_json_object(
|
|
@@ -1099,7 +1135,9 @@ def _finish_native_agent_attempt(
|
|
|
1099
1135
|
result = ExecutionMutationAudit().compare(
|
|
1100
1136
|
snapshot,
|
|
1101
1137
|
(policy,),
|
|
1102
|
-
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
|
+
),
|
|
1103
1141
|
attempt_succeeded=True,
|
|
1104
1142
|
result_present=True,
|
|
1105
1143
|
)
|
|
@@ -1118,18 +1156,7 @@ def _finish_native_agent_attempt(
|
|
|
1118
1156
|
"beforeDigest": "",
|
|
1119
1157
|
"afterDigest": "",
|
|
1120
1158
|
}
|
|
1121
|
-
|
|
1122
|
-
manifest_path,
|
|
1123
|
-
manifest,
|
|
1124
|
-
identity,
|
|
1125
|
-
status,
|
|
1126
|
-
result_relative,
|
|
1127
|
-
summary,
|
|
1128
|
-
)
|
|
1129
|
-
if status != "ok":
|
|
1130
|
-
violations = summary.get("violations")
|
|
1131
|
-
detail = "; ".join(violations) if isinstance(violations, list) else status
|
|
1132
|
-
raise DispatchError(f"agent mutation audit failed: {detail or status}")
|
|
1159
|
+
return status, summary
|
|
1133
1160
|
|
|
1134
1161
|
|
|
1135
1162
|
def _close_native_attempt(
|
|
@@ -1170,12 +1197,22 @@ def _close_native_attempt(
|
|
|
1170
1197
|
)
|
|
1171
1198
|
|
|
1172
1199
|
|
|
1173
|
-
def _native_out_of_plan_paths(
|
|
1200
|
+
def _native_out_of_plan_paths(
|
|
1201
|
+
result_path: Path, *, project_root: Path
|
|
1202
|
+
) -> tuple[str, ...]:
|
|
1174
1203
|
if result_path.suffix != ".json":
|
|
1175
1204
|
return ()
|
|
1176
1205
|
try:
|
|
1177
|
-
|
|
1178
|
-
|
|
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:
|
|
1179
1216
|
return ()
|
|
1180
1217
|
implementation = payload.get("implementation") if isinstance(payload, Mapping) else None
|
|
1181
1218
|
rows = implementation.get("outOfPlanEdits") if isinstance(implementation, Mapping) else None
|
|
@@ -1468,6 +1505,8 @@ def dispatch_result_path(
|
|
|
1468
1505
|
"""
|
|
1469
1506
|
if worker_id != REPORT_WRITER_WORKER_ID:
|
|
1470
1507
|
return worker_result_path
|
|
1508
|
+
if uses_report_contract_v3(manifest):
|
|
1509
|
+
return report_narrative_path(project_root, manifest)
|
|
1471
1510
|
return final_report_data_path(
|
|
1472
1511
|
resolve_required_path(project_root, manifest, "expectedReportRecordPath")
|
|
1473
1512
|
)
|
|
@@ -1487,6 +1526,8 @@ def dispatch_completion_paths(
|
|
|
1487
1526
|
"""
|
|
1488
1527
|
if worker_id != REPORT_WRITER_WORKER_ID:
|
|
1489
1528
|
return (worker_result_path,)
|
|
1529
|
+
if uses_report_contract_v3(manifest):
|
|
1530
|
+
return (report_narrative_path(project_root, manifest), worker_result_path)
|
|
1490
1531
|
data_json = final_report_data_path(
|
|
1491
1532
|
resolve_required_path(project_root, manifest, "expectedReportRecordPath")
|
|
1492
1533
|
)
|
|
@@ -1643,6 +1684,26 @@ def _validate_agent_invocations(
|
|
|
1643
1684
|
raise DispatchError("agent invocation contract: " + "; ".join(errors))
|
|
1644
1685
|
|
|
1645
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
|
+
|
|
1646
1707
|
def _v2_job_authority_errors(
|
|
1647
1708
|
job: WorkerJob,
|
|
1648
1709
|
metadata: Mapping[str, Any],
|
|
@@ -1657,14 +1718,22 @@ def _v2_job_authority_errors(
|
|
|
1657
1718
|
errors: list[str] = []
|
|
1658
1719
|
if job.invocation_ref != identity.invocation_ref:
|
|
1659
1720
|
errors.append("jobs file invocationRef does not match invocation metadata")
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
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
|
+
)
|
|
1668
1737
|
prompt = metadata.get("prompt")
|
|
1669
1738
|
metadata_prompt_value = (
|
|
1670
1739
|
string_value(prompt.get("path")) if isinstance(prompt, Mapping) else ""
|
|
@@ -1709,19 +1778,50 @@ def _v2_job_authority_errors(
|
|
|
1709
1778
|
assignment=parsed_assignment,
|
|
1710
1779
|
)
|
|
1711
1780
|
)
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
+
)
|
|
1725
1825
|
return errors
|
|
1726
1826
|
|
|
1727
1827
|
|
|
@@ -1765,7 +1865,7 @@ def worker_jobs_from_file(
|
|
|
1765
1865
|
manifest: Mapping[str, Any],
|
|
1766
1866
|
active_context: Mapping[str, Any] | None = None,
|
|
1767
1867
|
backend: str,
|
|
1768
|
-
idle_timeout_seconds: int,
|
|
1868
|
+
idle_timeout_seconds: int | None,
|
|
1769
1869
|
default_dispatch_kind: str,
|
|
1770
1870
|
resolve_wrapper: Callable[[str], Path],
|
|
1771
1871
|
default_provider: Callable[[str], str],
|
|
@@ -1804,7 +1904,7 @@ def _worker_job_from_file(
|
|
|
1804
1904
|
manifest: Mapping[str, Any],
|
|
1805
1905
|
active_context: Mapping[str, Any] | None = None,
|
|
1806
1906
|
backend: str,
|
|
1807
|
-
idle_timeout_seconds: int,
|
|
1907
|
+
idle_timeout_seconds: int | None,
|
|
1808
1908
|
dispatch_kind: str,
|
|
1809
1909
|
resolve_wrapper: Callable[[str], Path],
|
|
1810
1910
|
default_provider: Callable[[str], str],
|
|
@@ -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")
|