okstra 0.202.0 → 0.204.0
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 +3 -3
- package/dist/cli-registry.mjs +7 -7
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +50 -124
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +41 -8
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/install-assets.mjs +3 -0
- package/dist/lib/install-assets.mjs.map +1 -1
- package/dist/lib/runtime-manifest.mjs +2 -1
- package/dist/lib/runtime-manifest.mjs.map +1 -1
- package/dist/lib/types.d.mts +2 -1
- package/docs/architecture/storage-model.md +14 -11
- package/docs/architecture.md +25 -19
- package/docs/cli.md +15 -12
- package/docs/contributor-change-matrix.md +3 -2
- package/docs/performance-improvement-plan-v2.md +2 -3
- package/docs/project-structure-overview.md +35 -9
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/common-flow.md +1 -1
- package/docs/task-process/final-verification.md +3 -1
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +36 -39
- package/package.json +1 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/common.json +28 -0
- package/runtime/agents/operations/code-review.json +6 -0
- package/runtime/agents/operations/report-translation.json +6 -0
- package/runtime/agents/operations/schedule-verification.json +6 -0
- package/runtime/agents/roles/analyser.json +18 -0
- package/runtime/agents/roles/critic.json +18 -0
- package/runtime/agents/roles/designer.json +18 -0
- package/runtime/agents/roles/implementer.json +20 -0
- package/runtime/agents/roles/leader.json +20 -0
- package/runtime/agents/roles/planner.json +18 -0
- package/runtime/agents/roles/report-writer.json +19 -0
- package/runtime/agents/roles/translator.json +19 -0
- package/runtime/agents/roles/verifier.json +18 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/prompts/duties/acceptance-critic.json +32 -0
- package/runtime/prompts/duties/acceptance-verifier.json +32 -0
- package/runtime/prompts/duties/analysis-worker.json +32 -0
- package/runtime/prompts/duties/code-reviewer.json +32 -0
- package/runtime/prompts/duties/diagnosis-worker.json +32 -0
- package/runtime/prompts/duties/direction-selection-worker.json +32 -0
- package/runtime/prompts/duties/discovery-worker.json +32 -0
- package/runtime/prompts/duties/implementation-executor.json +32 -0
- package/runtime/prompts/duties/implementation-verifier.json +32 -0
- package/runtime/prompts/duties/lead.json +32 -0
- package/runtime/prompts/duties/planning-worker.json +36 -0
- package/runtime/prompts/duties/report-writer.json +32 -0
- package/runtime/prompts/duties/reverification-worker.json +32 -0
- package/runtime/prompts/duties/schedule-verifier.json +32 -0
- package/runtime/prompts/duties/scope-critic.json +32 -0
- package/runtime/prompts/duties/technical-verification-worker.json +32 -0
- package/runtime/prompts/duties/translator.json +32 -0
- package/runtime/prompts/launch.template.md +2 -1
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/convergence.md +4 -4
- package/runtime/prompts/lead/okstra-lead-contract.md +113 -4
- package/runtime/prompts/lead/plan-body-verification.md +6 -6
- package/runtime/prompts/lead/report-writer.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +4 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/change-impact-analysis.json +31 -0
- package/runtime/prompts/profiles/change-impact-analysis.md +0 -20
- package/runtime/prompts/profiles/error-analysis.json +39 -0
- package/runtime/prompts/profiles/error-analysis.md +0 -25
- package/runtime/prompts/profiles/feature-analysis.json +31 -0
- package/runtime/prompts/profiles/feature-analysis.md +0 -20
- package/runtime/prompts/profiles/final-verification.json +30 -0
- package/runtime/prompts/profiles/final-verification.md +3 -22
- package/runtime/prompts/profiles/forbidden-actions.json +4 -3
- package/runtime/prompts/profiles/implementation-option-selection.json +31 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +0 -20
- package/runtime/prompts/profiles/implementation-planning.json +40 -0
- package/runtime/prompts/profiles/implementation-planning.md +4 -29
- package/runtime/prompts/profiles/implementation.json +30 -0
- package/runtime/prompts/profiles/implementation.md +1 -20
- package/runtime/prompts/profiles/improvement-discovery.json +31 -0
- package/runtime/prompts/profiles/improvement-discovery.md +0 -20
- package/runtime/prompts/profiles/project-analysis.json +31 -0
- package/runtime/prompts/profiles/project-analysis.md +0 -20
- package/runtime/prompts/profiles/release-handoff.json +5 -0
- package/runtime/prompts/profiles/release-handoff.md +71 -73
- package/runtime/prompts/profiles/requirements-discovery.json +39 -0
- package/runtime/prompts/profiles/requirements-discovery.md +0 -25
- package/runtime/prompts/profiles/technical-verification.json +39 -0
- package/runtime/prompts/profiles/technical-verification.md +0 -25
- package/runtime/prompts/wizard/prompts.ko.json +12 -17
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +3 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -3
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +108 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +8 -1
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +8 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +23 -6
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -2
- package/runtime/python/okstra_ctl/agent/invocation.py +168 -113
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +120 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +107 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -49
- package/runtime/python/okstra_ctl/analysis_packet.py +4 -1
- package/runtime/python/okstra_ctl/application/open_worker.py +6 -1
- package/runtime/python/okstra_ctl/assignment_resolver.py +16 -5
- package/runtime/python/okstra_ctl/cmux.py +69 -20
- package/runtime/python/okstra_ctl/code_review_target.py +16 -8
- package/runtime/python/okstra_ctl/conformance.py +43 -0
- package/runtime/python/okstra_ctl/consumers.py +6 -3
- package/runtime/python/okstra_ctl/container.py +31 -8
- package/runtime/python/okstra_ctl/context_cost.py +11 -15
- package/runtime/python/okstra_ctl/contract_refreeze.py +156 -0
- package/runtime/python/okstra_ctl/convergence_provenance.py +7 -1
- package/runtime/python/okstra_ctl/design_prep.py +34 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +53 -27
- package/runtime/python/okstra_ctl/domain/host.py +5 -0
- package/runtime/python/okstra_ctl/domain/worker_runtime.py +10 -0
- package/runtime/python/okstra_ctl/error_report.py +4 -3
- package/runtime/python/okstra_ctl/execution_manifest.py +71 -18
- package/runtime/python/okstra_ctl/handoff.py +167 -277
- package/runtime/python/okstra_ctl/implementation_stage.py +9 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +113 -0
- package/runtime/python/okstra_ctl/lead_progress.py +1 -1
- package/runtime/python/okstra_ctl/legacy_model_selection.py +2 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -4
- package/runtime/python/okstra_ctl/manager_launch.py +1 -1
- package/runtime/python/okstra_ctl/manager_paths.py +14 -3
- package/runtime/python/okstra_ctl/manager_store.py +210 -3
- package/runtime/python/okstra_ctl/manager_sync.py +4 -1
- package/runtime/python/okstra_ctl/manager_view.py +2 -1
- package/runtime/python/okstra_ctl/model_discovery.py +30 -0
- package/runtime/python/okstra_ctl/model_io/lines.py +14 -1
- package/runtime/python/okstra_ctl/model_io/renderers.py +4 -3
- package/runtime/python/okstra_ctl/models.py +1 -1
- package/runtime/python/okstra_ctl/next_phase.py +16 -6
- package/runtime/python/okstra_ctl/operation_invocation.py +86 -0
- package/runtime/python/okstra_ctl/option_comparison.py +168 -0
- package/runtime/python/okstra_ctl/path_hints.py +9 -0
- package/runtime/python/okstra_ctl/paths.py +3 -0
- package/runtime/python/okstra_ctl/profile_show.py +42 -1
- package/runtime/python/okstra_ctl/registry/host_discovery.py +20 -12
- package/runtime/python/okstra_ctl/registry/host_registry.py +11 -0
- package/runtime/python/okstra_ctl/render.py +50 -0
- package/runtime/python/okstra_ctl/report_contract.py +1 -1
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +177 -17
- package/runtime/python/okstra_ctl/report_translation.py +2 -1
- package/runtime/python/okstra_ctl/report_translation_dispatch.py +69 -9
- package/runtime/python/okstra_ctl/role_requirements.py +142 -129
- package/runtime/python/okstra_ctl/rollup.py +3 -1
- package/runtime/python/okstra_ctl/run.py +76 -29
- package/runtime/python/okstra_ctl/schedule_semantics.py +17 -6
- package/runtime/python/okstra_ctl/stage_fix_carry.py +23 -4
- package/runtime/python/okstra_ctl/stage_integrate.py +178 -18
- package/runtime/python/okstra_ctl/stage_map.py +16 -2
- package/runtime/python/okstra_ctl/stage_targets.py +209 -43
- package/runtime/python/okstra_ctl/team.py +22 -13
- package/runtime/python/okstra_ctl/time_report.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +3 -1
- package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -9
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -1
- package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
- package/runtime/python/okstra_ctl/wizard/state.py +3 -5
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +3 -23
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +5 -1
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +35 -7
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +66 -48
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/python/okstra_ctl/worktree/__init__.py +3 -1
- package/runtime/python/okstra_ctl/worktree/naming.py +9 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +38 -9
- package/runtime/python/okstra_token_usage/pricing.py +6 -4
- package/runtime/schemas/agent-common-v1.schema.json +34 -0
- package/runtime/schemas/agent-duty-v1.schema.json +38 -0
- package/runtime/schemas/agent-operation-v1.schema.json +11 -0
- package/runtime/schemas/agent-profile-v1.schema.json +46 -0
- package/runtime/schemas/agent-role-v1.schema.json +29 -0
- package/runtime/schemas/final-report-v2.0.schema.json +118 -97
- package/runtime/schemas/final-report-v3.0.schema.json +118 -97
- package/runtime/skills/okstra-brief-gen/SKILL.md +84 -4
- package/runtime/skills/okstra-chat/SKILL.md +2 -2
- package/runtime/skills/okstra-code-review/SKILL.md +23 -9
- package/runtime/skills/okstra-container-build/SKILL.md +10 -10
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-inspect/facets/cost.md +1 -1
- package/runtime/skills/okstra-inspect/facets/error-zip.md +9 -9
- package/runtime/skills/okstra-inspect/facets/errors.md +16 -16
- package/runtime/skills/okstra-inspect/facets/logs.md +7 -7
- package/runtime/skills/okstra-inspect/facets/recap.md +2 -2
- package/runtime/skills/okstra-inspect/facets/report.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +4 -3
- package/runtime/skills/okstra-inspect/facets/time.md +11 -10
- package/runtime/skills/okstra-manager/SKILL.md +18 -2
- package/runtime/skills/okstra-pr-gen/SKILL.md +6 -5
- package/runtime/skills/okstra-rollup/SKILL.md +5 -5
- package/runtime/skills/okstra-run/SKILL.md +31 -12
- package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
- package/runtime/skills/okstra-setup/SKILL.md +12 -10
- package/runtime/skills/okstra-setup/references/project-config.md +7 -6
- package/runtime/skills/okstra-usage/SKILL.md +1 -1
- package/runtime/skills/okstra-user-response/SKILL.md +1 -1
- package/runtime/templates/manager/view.template.html +1 -0
- package/runtime/templates/report-writer-prompt-preamble.md +8 -0
- package/runtime/templates/reports/brief.template.md +14 -4
- package/runtime/templates/reports/html/i18n/en.json +5 -4
- package/runtime/templates/reports/html/i18n/ko.json +5 -4
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +8 -5
- package/runtime/templates/reports/i18n/en.json +1 -1
- package/runtime/templates/reports/md/tasks/release-handoff.template.md +1 -1
- package/runtime/templates/reports/release-handoff-input.template.md +6 -4
- package/runtime/templates/translator-prompt-preamble.md +36 -0
- package/runtime/validators/checks/validate-assets-01.py +7 -8
- package/runtime/validators/validate-brief.py +70 -0
- package/runtime/validators/validate-implementation-plan-stages.py +2 -1
- package/runtime/validators/validate-run.py +59 -9
- package/runtime/validators/validate-schedule.py +9 -0
- package/docs/for-ai/README.md +0 -68
- package/docs/for-ai/skills/okstra-brief-gen.md +0 -262
- package/docs/for-ai/skills/okstra-chat.md +0 -34
- package/docs/for-ai/skills/okstra-code-review.md +0 -57
- package/docs/for-ai/skills/okstra-container-build.md +0 -129
- package/docs/for-ai/skills/okstra-inspect.md +0 -262
- package/docs/for-ai/skills/okstra-manager.md +0 -86
- package/docs/for-ai/skills/okstra-memory.md +0 -126
- package/docs/for-ai/skills/okstra-pr-gen.md +0 -49
- package/docs/for-ai/skills/okstra-rollup.md +0 -114
- package/docs/for-ai/skills/okstra-run.md +0 -250
- package/docs/for-ai/skills/okstra-schedule-gen.md +0 -240
- package/docs/for-ai/skills/okstra-setup.md +0 -167
- package/docs/for-ai/skills/okstra-usage.md +0 -29
- package/docs/for-ai/skills/okstra-user-response.md +0 -72
- package/runtime/agents/workers/claude-worker.md +0 -128
- package/runtime/agents/workers/report-writer-worker.md +0 -37
- package/runtime/agents/workers/translator-worker.md +0 -63
- package/runtime/prompts/duties/acceptance-critic.md +0 -44
- package/runtime/prompts/duties/acceptance-verifier.md +0 -44
- package/runtime/prompts/duties/analysis-worker.md +0 -44
- package/runtime/prompts/duties/code-reviewer.md +0 -44
- package/runtime/prompts/duties/common.md +0 -39
- package/runtime/prompts/duties/diagnosis-worker.md +0 -44
- package/runtime/prompts/duties/direction-selection-worker.md +0 -44
- package/runtime/prompts/duties/discovery-worker.md +0 -44
- package/runtime/prompts/duties/implementation-executor.md +0 -44
- package/runtime/prompts/duties/implementation-verifier.md +0 -44
- package/runtime/prompts/duties/lead.md +0 -44
- package/runtime/prompts/duties/planning-worker.md +0 -52
- package/runtime/prompts/duties/report-writer.md +0 -44
- package/runtime/prompts/duties/reverification-worker.md +0 -44
- package/runtime/prompts/duties/schedule-verifier.md +0 -44
- package/runtime/prompts/duties/scope-critic.md +0 -44
- package/runtime/prompts/duties/technical-verification-worker.md +0 -44
- package/runtime/prompts/duties/translator.md +0 -44
- package/runtime/python/okstra_ctl/pane_title.py +0 -154
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""진행 중인 run 의 동결 계약을 현재 형식으로 다시 동결한다.
|
|
2
|
+
|
|
3
|
+
run 은 시작할 때 직무·역할·공통 계약을 자기 폴더로 동결하고, 그 다이제스트를
|
|
4
|
+
매니페스트에 적는다. 계약 형식이 바뀐 릴리스를 설치하면 그 run 의 다이제스트는
|
|
5
|
+
새 규칙으로 계산한 값과 영영 다르고, 프롬프트를 하나도 더 만들 수 없다 — 실측
|
|
6
|
+
2026-09-23(fontradar-v2-api dev-10860 implementation-planning-002): 같은 run 에서
|
|
7
|
+
8회 성공한 뒤 설치가 한 번 일어나자 그다음 materialize 가
|
|
8
|
+
`run duty snapshot catalog digest does not match` 로 멈췄다.
|
|
9
|
+
|
|
10
|
+
이 모듈은 그 run 을 현재 형식으로 다시 동결하고 매니페스트의 다이제스트와 형식
|
|
11
|
+
표식을 갱신한다. 계약 본문은 런타임이 소유하므로 새로 쓰는 것이 원래 값이고,
|
|
12
|
+
run 의 증거(프롬프트·결과·원장)는 건드리지 않는다. 사용자가 명시적으로 부를 때만
|
|
13
|
+
도는 복구 경로다.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import tempfile
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Mapping
|
|
23
|
+
|
|
24
|
+
from .agent.invocation import digest_duty_catalog
|
|
25
|
+
from .json_boundary import (
|
|
26
|
+
JsonBoundaryError,
|
|
27
|
+
load_owned_object,
|
|
28
|
+
write_owned_object_atomic,
|
|
29
|
+
)
|
|
30
|
+
from .run import (
|
|
31
|
+
CONTRACT_FORMAT_VERSION,
|
|
32
|
+
PrepareError,
|
|
33
|
+
_common_contract_source,
|
|
34
|
+
_duty_catalog_source,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ContractRefreezeError(RuntimeError):
|
|
39
|
+
"""다시 동결할 수 없는 run. 메시지가 그 자리를 이름한다."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class RefreezeResult:
|
|
44
|
+
duty_root: str
|
|
45
|
+
previous_digest: str
|
|
46
|
+
digest: str
|
|
47
|
+
previous_format_version: int
|
|
48
|
+
format_version: int
|
|
49
|
+
changed: bool
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def frozen_format_version(contract: Mapping[str, Any]) -> int:
|
|
53
|
+
"""이 run 이 동결한 계약 형식. 표식이 없는 run 은 1(Markdown 직무)이다."""
|
|
54
|
+
value = contract.get("contractFormatVersion")
|
|
55
|
+
return value if isinstance(value, int) and not isinstance(value, bool) else 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def refreeze_message(duty_root: str) -> str:
|
|
59
|
+
"""형식이 어긋난 run 이 받아야 할 문장 — 원인과 다음 명령."""
|
|
60
|
+
return (
|
|
61
|
+
"run froze an older duty contract format and this install cannot read "
|
|
62
|
+
f"it: {duty_root}. Re-freeze this run's contracts with "
|
|
63
|
+
"`okstra agent-prompt refreeze-contracts --project-root <dir> "
|
|
64
|
+
"--run-manifest <path>`, then repeat the command that failed."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def refreeze_run_contracts(
|
|
69
|
+
*, project_root: Path, run_manifest_path: Path, workspace_root: Path,
|
|
70
|
+
) -> RefreezeResult:
|
|
71
|
+
"""이 run 의 계약 폴더를 현재 런타임 카탈로그로 갈아 끼운다."""
|
|
72
|
+
root = project_root.resolve()
|
|
73
|
+
manifest_path = _resolve(root, run_manifest_path)
|
|
74
|
+
manifest = _load(manifest_path, "run manifest")
|
|
75
|
+
contract = manifest.get("agentContract")
|
|
76
|
+
if not isinstance(contract, dict):
|
|
77
|
+
raise ContractRefreezeError(f"run manifest has no agentContract: {manifest_path}")
|
|
78
|
+
duty_root_rel = str(contract.get("dutyRootPath") or "")
|
|
79
|
+
if not duty_root_rel:
|
|
80
|
+
raise ContractRefreezeError(f"agentContract has no dutyRootPath: {manifest_path}")
|
|
81
|
+
duty_root = _resolve(root, Path(duty_root_rel))
|
|
82
|
+
previous_digest = str(contract.get("catalogDigest") or "")
|
|
83
|
+
previous_format = frozen_format_version(contract)
|
|
84
|
+
try:
|
|
85
|
+
source = _duty_catalog_source(workspace_root)
|
|
86
|
+
common_source = _common_contract_source(workspace_root)
|
|
87
|
+
except PrepareError as exc:
|
|
88
|
+
raise ContractRefreezeError(str(exc)) from exc
|
|
89
|
+
digest = _replace_snapshot(duty_root, source, common_source)
|
|
90
|
+
changed = (
|
|
91
|
+
digest != previous_digest or previous_format != CONTRACT_FORMAT_VERSION
|
|
92
|
+
)
|
|
93
|
+
if changed:
|
|
94
|
+
contract["catalogDigest"] = digest
|
|
95
|
+
contract["contractFormatVersion"] = CONTRACT_FORMAT_VERSION
|
|
96
|
+
_write_manifest(manifest_path, manifest)
|
|
97
|
+
return RefreezeResult(
|
|
98
|
+
duty_root=str(duty_root),
|
|
99
|
+
previous_digest=previous_digest,
|
|
100
|
+
digest=digest,
|
|
101
|
+
previous_format_version=previous_format,
|
|
102
|
+
format_version=CONTRACT_FORMAT_VERSION,
|
|
103
|
+
changed=changed,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _replace_snapshot(duty_root: Path, source: Path, common_source: Path) -> str:
|
|
108
|
+
"""계약 폴더를 통째로 새로 쓴다. 옛 형식 파일은 남기지 않는다.
|
|
109
|
+
|
|
110
|
+
옛 파일을 남기면 두 형식이 한 폴더에 섞이고, 다이제스트가 세는 대상과 로더가
|
|
111
|
+
읽는 대상이 다시 갈린다 — 이 결함이 처음 생긴 자리가 그 갈림이다.
|
|
112
|
+
"""
|
|
113
|
+
duty_root.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
staged = Path(tempfile.mkdtemp(
|
|
115
|
+
dir=duty_root.parent, prefix=f".{duty_root.name}.", suffix=".refreeze",
|
|
116
|
+
))
|
|
117
|
+
retired = duty_root.with_name(f".{duty_root.name}.retired")
|
|
118
|
+
try:
|
|
119
|
+
shutil.copytree(source, staged, dirs_exist_ok=True)
|
|
120
|
+
shutil.copyfile(common_source, staged / "common.json")
|
|
121
|
+
shutil.copytree(
|
|
122
|
+
common_source.parent / "roles", staged / "roles", dirs_exist_ok=True
|
|
123
|
+
)
|
|
124
|
+
digest = digest_duty_catalog(staged)
|
|
125
|
+
if duty_root.exists():
|
|
126
|
+
shutil.rmtree(retired, ignore_errors=True)
|
|
127
|
+
os.rename(duty_root, retired)
|
|
128
|
+
os.rename(staged, duty_root)
|
|
129
|
+
except OSError as exc:
|
|
130
|
+
raise ContractRefreezeError(f"cannot re-freeze duty contracts: {exc}") from exc
|
|
131
|
+
finally:
|
|
132
|
+
shutil.rmtree(staged, ignore_errors=True)
|
|
133
|
+
shutil.rmtree(retired, ignore_errors=True)
|
|
134
|
+
return digest
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _resolve(project_root: Path, value: Path) -> Path:
|
|
138
|
+
path = Path(value).expanduser()
|
|
139
|
+
return path if path.is_absolute() else project_root / path
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _load(path: Path, label: str) -> dict[str, Any]:
|
|
143
|
+
try:
|
|
144
|
+
payload = load_owned_object(path, artifact=label)
|
|
145
|
+
except (OSError, JsonBoundaryError) as exc:
|
|
146
|
+
raise ContractRefreezeError(f"cannot read {label} {path}: {exc}") from exc
|
|
147
|
+
if not isinstance(payload, dict):
|
|
148
|
+
raise ContractRefreezeError(f"{label} is not an object: {path}")
|
|
149
|
+
return payload
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _write_manifest(path: Path, manifest: Mapping[str, Any]) -> None:
|
|
153
|
+
try:
|
|
154
|
+
write_owned_object_atomic(path, manifest, artifact="run manifest")
|
|
155
|
+
except JsonBoundaryError as exc:
|
|
156
|
+
raise ContractRefreezeError(f"cannot write run manifest {path}: {exc}") from exc
|
|
@@ -154,7 +154,13 @@ def worker_result_paths(
|
|
|
154
154
|
if candidate.resolve().parent != results_dir.resolve():
|
|
155
155
|
raise ValueError(f"accepted resultPath is outside worker-results: {candidate}")
|
|
156
156
|
if not candidate.is_file():
|
|
157
|
-
|
|
157
|
+
# 이 모듈의 계약은 "없는 워커 결과는 조용히 건너뛴다" 이고, 그 판정은
|
|
158
|
+
# 아래 `read_canonical_worker_result` 가 None 을 돌려주는 것으로
|
|
159
|
+
# 이루어진다. 여기서 던지면 판정이 아니라 run 이 죽는다 — 실측
|
|
160
|
+
# 2026-09-23(jobs dev-10860 stage 1): 옛 run 의 원장이 ok 로 적은
|
|
161
|
+
# 결과 파일이 디스크에 없어 `report-finalize --only validate-run` 이
|
|
162
|
+
# 예외로 멈췄다. 파일명 규칙 경로를 그대로 두고 넘어간다.
|
|
163
|
+
continue
|
|
158
164
|
paths[row["workerId"]] = candidate
|
|
159
165
|
return paths
|
|
160
166
|
|
|
@@ -20,6 +20,7 @@ from fnmatch import fnmatchcase
|
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
from typing import Any, Iterator
|
|
22
22
|
|
|
23
|
+
from .consumers import read_stage_consumer_state
|
|
23
24
|
from .final_report_schema import load_schema, validate as validate_schema
|
|
24
25
|
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
25
26
|
from okstra_project import ResolverError, resolve_project_root
|
|
@@ -466,6 +467,7 @@ def materialize_design_prep_requests(plan_data_path: Path) -> list[Path]:
|
|
|
466
467
|
# 충돌은 한 항목씩 알리지 않는다. 요청 12개가 모두 옛 리포트의 파일과 부딪히면
|
|
467
468
|
# 첫 건에서 멈추는 구현은 사용자에게 12회 왕복을 시키고, 그때마다 이미 쓴
|
|
468
469
|
# 앞쪽 파일이 남아 상태가 절반만 진행된다. 전부 렌더해 전부 검사한 뒤 쓴다.
|
|
470
|
+
done = _done_stages(data_path)
|
|
469
471
|
rendered = [
|
|
470
472
|
_render_request(
|
|
471
473
|
data_path=data_path,
|
|
@@ -474,7 +476,7 @@ def materialize_design_prep_requests(plan_data_path: Path) -> list[Path]:
|
|
|
474
476
|
item=item,
|
|
475
477
|
)
|
|
476
478
|
for item in items
|
|
477
|
-
if item.get("status") in _REQUEST_STATUSES
|
|
479
|
+
if item.get("status") in _REQUEST_STATUSES and not _is_implemented(item, done)
|
|
478
480
|
]
|
|
479
481
|
conflicts = [
|
|
480
482
|
reason
|
|
@@ -494,6 +496,37 @@ def materialize_design_prep_requests(plan_data_path: Path) -> list[Path]:
|
|
|
494
496
|
return written
|
|
495
497
|
|
|
496
498
|
|
|
499
|
+
def _done_stages(data_path: Path) -> frozenset[int]:
|
|
500
|
+
"""이 태스크에서 이미 구현이 끝난 stage.
|
|
501
|
+
|
|
502
|
+
원장을 못 읽으면 빈 집합이다 — 그때의 동작은 이 판정이 없던 때와 같고,
|
|
503
|
+
"모르니까 건너뛴다" 는 반대 방향의 실패보다 안전하다.
|
|
504
|
+
"""
|
|
505
|
+
try:
|
|
506
|
+
state = read_stage_consumer_state(data_path.parent.parent)
|
|
507
|
+
except (OSError, UnicodeError, ValueError):
|
|
508
|
+
return frozenset()
|
|
509
|
+
return frozenset(state.done_stages)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _is_implemented(item: Mapping[str, Any], done: frozenset[int]) -> bool:
|
|
513
|
+
"""이 항목이 가리키는 stage 가 전부 구현 완료인가.
|
|
514
|
+
|
|
515
|
+
구현이 끝난 stage 의 계획은 역사이지 계획이 아니다. 그런데 탐지기는 매
|
|
516
|
+
계획 run 마다 이월된 stage 본문을 다시 훑고, 그 사이 선택 방향의
|
|
517
|
+
`fileStructure` 가 바뀌면 같은 PREP 항목이 다른 증거로 렌더된다. 요청
|
|
518
|
+
파일은 태스크 단위 이름이라 불변 파일과 충돌하고, report-finalize 는
|
|
519
|
+
빠져나갈 길 없이 멈춘다(2026-09-24, dev-10860: 9건). 이미 지어진 것에
|
|
520
|
+
설계 준비를 다시 요구하는 것이므로, 답도 쓸모가 없다.
|
|
521
|
+
"""
|
|
522
|
+
stages = [
|
|
523
|
+
stage
|
|
524
|
+
for stage in item.get("stageRefs") or ()
|
|
525
|
+
if isinstance(stage, int) and not isinstance(stage, bool)
|
|
526
|
+
]
|
|
527
|
+
return bool(stages) and all(stage in done for stage in stages)
|
|
528
|
+
|
|
529
|
+
|
|
497
530
|
def load_design_prep_items(report_path: Path) -> tuple[dict[str, Any], ...]:
|
|
498
531
|
"""Return validated snapshot items without exposing mutable report data."""
|
|
499
532
|
data_path = _canonical_data_path(report_path)
|
|
@@ -96,6 +96,7 @@ from .initial_prompt_materialization import (
|
|
|
96
96
|
InitialPromptMaterializationRequest,
|
|
97
97
|
InitialPromptWorkerRequest,
|
|
98
98
|
PromptDeliveryMode,
|
|
99
|
+
declared_delivery_mode,
|
|
99
100
|
materialize_initial_prompts,
|
|
100
101
|
)
|
|
101
102
|
from .agent.invocation import AgentInvocationError, materialize_retry_invocation
|
|
@@ -183,6 +184,9 @@ def revalidate_role_execution(
|
|
|
183
184
|
interaction_surface="role-execution-dispatch",
|
|
184
185
|
),
|
|
185
186
|
environment=host,
|
|
187
|
+
# 이 run 은 이 모델을 이미 골랐다. 지금 고를 수 있는지가 아니라 그
|
|
188
|
+
# 배정이 그대로 실행 가능한지를 묻는 자리다.
|
|
189
|
+
require_selectable=False,
|
|
186
190
|
)
|
|
187
191
|
binding = assignment.binding
|
|
188
192
|
if binding is None:
|
|
@@ -261,6 +265,7 @@ class WorkerHandle:
|
|
|
261
265
|
completed_process: subprocess.CompletedProcess[str] | None
|
|
262
266
|
status_sidecar_path: Path | None
|
|
263
267
|
degraded_from: str
|
|
268
|
+
degraded_reason: str = ""
|
|
264
269
|
running_process: subprocess.Popen[str] | None = None
|
|
265
270
|
mutation_snapshot: MutationSnapshot | None = None
|
|
266
271
|
mutation_policies: tuple[WritePolicy, ...] = ()
|
|
@@ -1439,31 +1444,11 @@ def _invocation_job_fields(
|
|
|
1439
1444
|
def _initial_prompt_delivery_mode(
|
|
1440
1445
|
manifest: Mapping[str, Any],
|
|
1441
1446
|
) -> PromptDeliveryMode:
|
|
1442
|
-
"""이 run 의
|
|
1443
|
-
|
|
1444
|
-
선언은 `leadAdapter.initialPromptDeliveryMode` 에 있고, 그 값은 render 가
|
|
1445
|
-
호스트 descriptor(어댑터가 워커 디스패치를 가져갔으면 그 어댑터)에서
|
|
1446
|
-
옮겨 적은 것이다. 선언이 없는 매니페스트는 이 필드가 생기기 전에 준비된
|
|
1447
|
-
run 이므로 그때의 동작인 `eager-include` 로 간다. 아는 값이 아니면 멈춘다 —
|
|
1448
|
-
프롬프트를 발행하고 나면 그 프롬프트는 불변이라, 잘못 고른 배송 방식은
|
|
1449
|
-
나중에 고칠 수 없다.
|
|
1450
|
-
"""
|
|
1451
|
-
adapter = manifest.get("leadAdapter")
|
|
1452
|
-
declared = (
|
|
1453
|
-
_string_value(adapter.get("initialPromptDeliveryMode"))
|
|
1454
|
-
if isinstance(adapter, Mapping)
|
|
1455
|
-
else ""
|
|
1456
|
-
)
|
|
1457
|
-
if not declared:
|
|
1458
|
-
return PromptDeliveryMode.EAGER_INCLUDE
|
|
1447
|
+
"""이 run 의 초기 프롬프트 배송 방식. 판정은 `declared_delivery_mode` 가 한다."""
|
|
1459
1448
|
try:
|
|
1460
|
-
return
|
|
1461
|
-
except
|
|
1462
|
-
|
|
1463
|
-
raise DispatchError(
|
|
1464
|
-
"leadAdapter.initialPromptDeliveryMode is not a delivery mode: "
|
|
1465
|
-
f"{declared!r} (allowed: {allowed})"
|
|
1466
|
-
) from None
|
|
1449
|
+
return declared_delivery_mode(manifest)
|
|
1450
|
+
except InitialPromptMaterializationError as exc:
|
|
1451
|
+
raise DispatchError(str(exc)) from None
|
|
1467
1452
|
|
|
1468
1453
|
|
|
1469
1454
|
def _materialize_roster_prompts(
|
|
@@ -2034,8 +2019,20 @@ def _start_job(plan: DispatchPlan, job: WorkerJob) -> WorkerHandle:
|
|
|
2034
2019
|
raise DispatchError(str(exc)) from exc
|
|
2035
2020
|
degraded_from = job.backend if runtime.surface != job.backend else ""
|
|
2036
2021
|
settled = replace(job, backend=runtime.surface) if degraded_from else job
|
|
2022
|
+
if degraded_from:
|
|
2023
|
+
print(
|
|
2024
|
+
f"DEGRADED {job.role}: {degraded_from} -> {runtime.surface} "
|
|
2025
|
+
f"({runtime.fallback_reason or 'no reason reported'})",
|
|
2026
|
+
flush=True,
|
|
2027
|
+
)
|
|
2037
2028
|
completed = None
|
|
2038
|
-
|
|
2029
|
+
# 강등된 job 은 여기서 기다리지 않는다. `dispatch_plan` 은 wait=True 에서
|
|
2030
|
+
# pane job 을 거절하므로(같은 함수의 pane_backends 검사) 강등은 비차단 배치
|
|
2031
|
+
# 에서만 일어난다. 그 배치에서 기다리면 라운드가 직렬화되고 — 실측 2026-09-21,
|
|
2032
|
+
# 워커 3명이 병렬 대신 37분 — dispatch 행이 워커가 끝난 뒤에야 써져
|
|
2033
|
+
# `okstra worker-liveness` 가 실행 중인 워커를 모른다. 정산은 상태 사이드카를
|
|
2034
|
+
# 읽는 `await_dispatches` 가 맡는다.
|
|
2035
|
+
if runtime.waitable and not degraded_from:
|
|
2039
2036
|
from .adapters.runtime.assembly import port_for
|
|
2040
2037
|
returncode = port_for(plan.runtime_chain, runtime.surface).wait(runtime)
|
|
2041
2038
|
completed = subprocess.CompletedProcess(list(settled.command), returncode)
|
|
@@ -2046,6 +2043,7 @@ def _start_job(plan: DispatchPlan, job: WorkerJob) -> WorkerHandle:
|
|
|
2046
2043
|
completed,
|
|
2047
2044
|
status_path_for_prompt(settled.prompt_path),
|
|
2048
2045
|
degraded_from,
|
|
2046
|
+
degraded_reason=runtime.fallback_reason,
|
|
2049
2047
|
runtime_handle=runtime,
|
|
2050
2048
|
started_at=started_at,
|
|
2051
2049
|
)
|
|
@@ -2237,9 +2235,28 @@ def _retry_from_record(
|
|
|
2237
2235
|
# 사유 한 줄에 실을 경로 개수. 전부 실으면 team-state 행이 부풀고, 하나도 안
|
|
2238
2236
|
# 실으면 읽는 쪽이 다른 파일을 뒤져야 한다.
|
|
2239
2237
|
_MUTATION_REASON_PATH_LIMIT = 5
|
|
2238
|
+
# 사유는 team-state 의 한 행이다. 래퍼 실패 문장은 길 수 있으므로 자른다.
|
|
2239
|
+
_MUTATION_REASON_FAILURE_LIMIT = 200
|
|
2240
|
+
|
|
2241
|
+
|
|
2242
|
+
def _wrapper_failure_text(outcome: WorkerOutcome | None) -> str:
|
|
2243
|
+
"""워커 래퍼가 남긴 실패 문장. 없으면 빈 문자열.
|
|
2244
|
+
|
|
2245
|
+
`mutation-present-unresolved` 는 "시도가 실패했는데 소스가 바뀌어 있다" 는
|
|
2246
|
+
뜻이고, 왜 실패했는지는 상태 사이드카에만 있었다. 읽는 쪽은 상태 이름만 보고
|
|
2247
|
+
소스를 뒤져야 했다 — 실측 2026-09-23: 두 세션이 같은 자리에서 그렇게
|
|
2248
|
+
진단했고, 원인은 카탈로그에 없는 served 모델이었다.
|
|
2249
|
+
"""
|
|
2250
|
+
if outcome is None or outcome.status_sidecar_path is None:
|
|
2251
|
+
return ""
|
|
2252
|
+
status = read_wrapper_status(outcome.status_sidecar_path)
|
|
2253
|
+
if status is None:
|
|
2254
|
+
return ""
|
|
2255
|
+
failure = _string_value(status.raw.get("failure"))
|
|
2256
|
+
return failure[:_MUTATION_REASON_FAILURE_LIMIT]
|
|
2240
2257
|
|
|
2241
2258
|
|
|
2242
|
-
def _mutation_failure_reason(mutation) -> str:
|
|
2259
|
+
def _mutation_failure_reason(mutation, outcome: WorkerOutcome | None = None) -> str:
|
|
2243
2260
|
"""왜 실패했는지와 무엇이 바뀌었는지를 한 줄에 담는다.
|
|
2244
2261
|
|
|
2245
2262
|
종전에는 디스패치 행에 `readonly source changed` 만 남고 바뀐 경로는
|
|
@@ -2249,6 +2266,9 @@ def _mutation_failure_reason(mutation) -> str:
|
|
|
2249
2266
|
사유 옆에 놓는다.
|
|
2250
2267
|
"""
|
|
2251
2268
|
reason = "; ".join(mutation.violations) or mutation.status
|
|
2269
|
+
failure = _wrapper_failure_text(outcome)
|
|
2270
|
+
if failure:
|
|
2271
|
+
reason = f"{reason}: {failure}"
|
|
2252
2272
|
paths = list(mutation.changed_paths)
|
|
2253
2273
|
if not paths:
|
|
2254
2274
|
return reason
|
|
@@ -2273,7 +2293,7 @@ def _finish_attempt(
|
|
|
2273
2293
|
"contract-failed-unattributed",
|
|
2274
2294
|
"mutation-present-unresolved",
|
|
2275
2295
|
}:
|
|
2276
|
-
reason = _mutation_failure_reason(mutation)
|
|
2296
|
+
reason = _mutation_failure_reason(mutation, outcome)
|
|
2277
2297
|
_update_dispatch_status(
|
|
2278
2298
|
plan.team_state_path, job, attempt, "error", reason
|
|
2279
2299
|
)
|
|
@@ -2913,6 +2933,7 @@ def _record_dispatch(
|
|
|
2913
2933
|
handle.pane_id,
|
|
2914
2934
|
handle.degraded_from,
|
|
2915
2935
|
reason,
|
|
2936
|
+
degraded_reason=handle.degraded_reason,
|
|
2916
2937
|
mutation_snapshot_path=(
|
|
2917
2938
|
_mutation_snapshot_path(handle.job)
|
|
2918
2939
|
if handle.mutation_snapshot is not None
|
|
@@ -2932,6 +2953,7 @@ def _dispatch_record(
|
|
|
2932
2953
|
degraded_from: str,
|
|
2933
2954
|
reason: str = "",
|
|
2934
2955
|
*,
|
|
2956
|
+
degraded_reason: str = "",
|
|
2935
2957
|
mutation_snapshot_path: Path | None = None,
|
|
2936
2958
|
write_policy_digest: str = "",
|
|
2937
2959
|
write_enforcement: WriteEnforcement | None = None,
|
|
@@ -2958,6 +2980,9 @@ def _dispatch_record(
|
|
|
2958
2980
|
),
|
|
2959
2981
|
"statusSidecarPath": str(status_path_for_prompt(job.prompt_path)),
|
|
2960
2982
|
"degradedFrom": degraded_from,
|
|
2983
|
+
# `reason` 은 이 행의 정산 사유라 마감과 teardown 이 덮어쓴다. 강등 사유는
|
|
2984
|
+
# 디스패치 시점의 사실이므로 자기 칸을 쓴다.
|
|
2985
|
+
"degradedReason": degraded_reason,
|
|
2961
2986
|
"reason": reason,
|
|
2962
2987
|
"modelExecutionValue": job.model_execution_value,
|
|
2963
2988
|
"wrapperPath": str(job.wrapper_path),
|
|
@@ -3186,6 +3211,7 @@ def _event_details(job: WorkerJob, handle: WorkerHandle) -> dict[str, Any]:
|
|
|
3186
3211
|
"role": job.role,
|
|
3187
3212
|
"paneId": handle.pane_id,
|
|
3188
3213
|
"degradedFrom": handle.degraded_from,
|
|
3214
|
+
"degradedReason": handle.degraded_reason,
|
|
3189
3215
|
}
|
|
3190
3216
|
return details
|
|
3191
3217
|
|
|
@@ -51,6 +51,11 @@ class HostDescriptor:
|
|
|
51
51
|
session_accounting: str
|
|
52
52
|
has_claude_session: bool
|
|
53
53
|
relay_contract: str
|
|
54
|
+
# 이 호스트 세션 안에서 도는 워커(runner=native-session)가 읽어야 하는
|
|
55
|
+
# 계약 파일의 절대 경로. 호스트가 그런 계약을 선언하지 않으면 빈 문자열이다.
|
|
56
|
+
# 호스트 전역 발견 경로에 설치되지 않는다 — 디스패치 프롬프트가 경로로
|
|
57
|
+
# 전달하고, 워커가 프리앰블과 같은 방식으로 연다.
|
|
58
|
+
worker_session_contract: str = ""
|
|
54
59
|
# 이 호스트가 초기 워커 프롬프트를 어떻게 싣겠다고 선언하는가. 값은
|
|
55
60
|
# `relay.md` 의 `initialPromptDeliveryMode` 행과 같아야 한다 — 그 표를
|
|
56
61
|
# 읽고 dispatch 를 판단하는 것은 리드이고, 실제로 프롬프트를 만드는 것은
|
|
@@ -31,9 +31,19 @@ class WorkerSpawnRequest:
|
|
|
31
31
|
|
|
32
32
|
@dataclass(frozen=True)
|
|
33
33
|
class RuntimeHandle:
|
|
34
|
+
"""An opened surface, and why it is not the one that was planned.
|
|
35
|
+
|
|
36
|
+
`fallback_reason` is empty when the planned surface opened. It carries the
|
|
37
|
+
last `SurfaceUnavailable` message when an earlier surface in the chain was
|
|
38
|
+
skipped, because that message exists nowhere else: the chain swallows the
|
|
39
|
+
exception it recovers from, and a degraded dispatch used to be recorded
|
|
40
|
+
with an empty reason.
|
|
41
|
+
"""
|
|
42
|
+
|
|
34
43
|
surface: str
|
|
35
44
|
identifier: str
|
|
36
45
|
waitable: bool
|
|
46
|
+
fallback_reason: str = ""
|
|
37
47
|
|
|
38
48
|
|
|
39
49
|
@dataclass(frozen=True)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Read-side aggregator for okstra-run error logs.
|
|
2
2
|
|
|
3
3
|
task 의 모든 run errors-<type>-<seq>.jsonl 을 모아 phase / worker / errorType
|
|
4
|
-
별로 집계하고 timestamp 누적 .md 리포트로 렌더한다.
|
|
5
|
-
|
|
4
|
+
별로 집계하고 timestamp 누적 .md 리포트로 렌더한다. 리포트 외에 쓰는 것은
|
|
5
|
+
task 해석 중 `find_task_root` 가 끝난 implementation 결과를 기록하는 경우뿐이다.
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
@@ -236,7 +236,8 @@ _CLI_EPILOG = r"""Usage:
|
|
|
236
236
|
|
|
237
237
|
Writes a timestamped markdown report under <task-root>/error-reports/ and
|
|
238
238
|
prints a JSON summary (counts by errorType / source / phase / agent) to stdout.
|
|
239
|
-
|
|
239
|
+
Resolving a task may record a finished implementation outcome in its task
|
|
240
|
+
manifest, the task catalog, and consumers.jsonl; nothing else is written.
|
|
240
241
|
"""
|
|
241
242
|
|
|
242
243
|
|
|
@@ -213,7 +213,9 @@ def reserve_derived_role_invocation(
|
|
|
213
213
|
raise ExecutionManifestError(
|
|
214
214
|
f"source role execution is unknown: {source_role_execution_ref}"
|
|
215
215
|
)
|
|
216
|
-
derived, created = _derived_role_execution(
|
|
216
|
+
derived, created, derived_participant = _derived_role_execution(
|
|
217
|
+
manifest, source, role
|
|
218
|
+
)
|
|
217
219
|
invocation = invocation_factory(derived)
|
|
218
220
|
if not isinstance(invocation, Invocation):
|
|
219
221
|
raise ExecutionManifestError("invocation factory must return Invocation")
|
|
@@ -244,14 +246,22 @@ def reserve_derived_role_invocation(
|
|
|
244
246
|
_write_json_atomic(task_manifest_path, recovered_task_payload)
|
|
245
247
|
return derived, existing_invocation
|
|
246
248
|
|
|
249
|
+
base = manifest
|
|
250
|
+
if created and derived_participant is not None:
|
|
251
|
+
base = _replace_manifest(
|
|
252
|
+
manifest,
|
|
253
|
+
participant_assignments=(
|
|
254
|
+
*manifest.participant_assignments,
|
|
255
|
+
derived_participant,
|
|
256
|
+
),
|
|
257
|
+
)
|
|
247
258
|
roles = (
|
|
248
|
-
(*
|
|
249
|
-
if created
|
|
250
|
-
else manifest.role_executions
|
|
259
|
+
(*base.role_executions, derived) if created else base.role_executions
|
|
251
260
|
)
|
|
252
|
-
with_role = _replace_manifest(
|
|
261
|
+
with_role = _replace_manifest(base, role_executions=roles)
|
|
253
262
|
if created:
|
|
254
|
-
|
|
263
|
+
# 새 참가자가 실린 뒤라야 참가자 참조 검사가 성립한다.
|
|
264
|
+
_validate_next_role_execution(base, derived)
|
|
255
265
|
_validate_next_invocation(with_role, invocation)
|
|
256
266
|
updated = _replace_manifest(
|
|
257
267
|
with_role,
|
|
@@ -540,11 +550,13 @@ def _validate_roles(
|
|
|
540
550
|
"sourceRoleExecutionRef must reference an earlier different "
|
|
541
551
|
"role execution"
|
|
542
552
|
)
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
553
|
+
# 참가자는 더 이상 공유하지 않는다(ADR-0017). 파생을 파생이게 하는
|
|
554
|
+
# 것은 같은 모델로 같은 일을 다시 읽는다는 것이므로, 거기를 검사한다.
|
|
555
|
+
for key in ("provider", "modelRef", "modelId"):
|
|
556
|
+
if source_role.get(key) != row.get(key):
|
|
557
|
+
raise ExecutionManifestError(
|
|
558
|
+
f"derived role execution {key} must match its source"
|
|
559
|
+
)
|
|
548
560
|
earlier_roles[role_ref] = row
|
|
549
561
|
for role, values in ordinals.items():
|
|
550
562
|
if sorted(values) != list(range(1, len(values) + 1)):
|
|
@@ -815,20 +827,29 @@ def _derived_role_execution(
|
|
|
815
827
|
manifest: ExecutionManifest,
|
|
816
828
|
source: RoleExecution,
|
|
817
829
|
role: str,
|
|
818
|
-
) -> tuple[RoleExecution, bool]:
|
|
830
|
+
) -> tuple[RoleExecution, bool, ParticipantAssignment | None]:
|
|
831
|
+
"""The dynamic role slot derived from *source*, creating it when absent.
|
|
832
|
+
|
|
833
|
+
A created slot is its own participant. It copies the source's provider,
|
|
834
|
+
model and binding — that is what makes it the same model re-reading the
|
|
835
|
+
same work — but not the source's identity or session. Sharing the
|
|
836
|
+
participant made the derived slot indistinguishable from its source
|
|
837
|
+
wherever a participant is what gets attributed or resumed.
|
|
838
|
+
"""
|
|
819
839
|
if source.role == role:
|
|
820
|
-
return source, False
|
|
840
|
+
return source, False, None
|
|
821
841
|
existing = [
|
|
822
842
|
item
|
|
823
843
|
for item in manifest.role_executions
|
|
824
|
-
if item.
|
|
844
|
+
if item.source_role_execution_ref == source.role_execution_ref
|
|
845
|
+
and item.role == role
|
|
825
846
|
]
|
|
826
847
|
if len(existing) > 1:
|
|
827
848
|
raise ExecutionManifestError(
|
|
828
|
-
f"
|
|
849
|
+
f"source has multiple {role} role executions: {source.role_execution_ref}"
|
|
829
850
|
)
|
|
830
851
|
if existing:
|
|
831
|
-
return existing[0], False
|
|
852
|
+
return existing[0], False, None
|
|
832
853
|
ordinal = max(
|
|
833
854
|
(item.ordinal for item in manifest.role_executions if item.role == role),
|
|
834
855
|
default=0,
|
|
@@ -840,9 +861,10 @@ def _derived_role_execution(
|
|
|
840
861
|
raise ExecutionManifestError(
|
|
841
862
|
f"derived roleExecutionRef is already reserved: {role_ref}"
|
|
842
863
|
)
|
|
864
|
+
participant = _derived_participant(manifest, source)
|
|
843
865
|
return RoleExecution(
|
|
844
866
|
role_execution_ref=role_ref,
|
|
845
|
-
participant_ref=
|
|
867
|
+
participant_ref=participant.participant_ref,
|
|
846
868
|
source_role_execution_ref=source.role_execution_ref,
|
|
847
869
|
role=role,
|
|
848
870
|
provider=source.provider,
|
|
@@ -859,7 +881,38 @@ def _derived_role_execution(
|
|
|
859
881
|
binding=source.binding,
|
|
860
882
|
served_model_attestation=ServedModelAttestation.unknown(),
|
|
861
883
|
status="prepared",
|
|
862
|
-
), True
|
|
884
|
+
), True, participant
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _derived_participant(
|
|
888
|
+
manifest: ExecutionManifest,
|
|
889
|
+
source: RoleExecution,
|
|
890
|
+
) -> ParticipantAssignment:
|
|
891
|
+
"""A fresh participant carrying the source's model but none of its session."""
|
|
892
|
+
origin = next(
|
|
893
|
+
(
|
|
894
|
+
item
|
|
895
|
+
for item in manifest.participant_assignments
|
|
896
|
+
if item.participant_ref == source.participant_ref
|
|
897
|
+
),
|
|
898
|
+
None,
|
|
899
|
+
)
|
|
900
|
+
if origin is None:
|
|
901
|
+
raise ExecutionManifestError(
|
|
902
|
+
f"source role execution references unknown participant: "
|
|
903
|
+
f"{source.participant_ref}"
|
|
904
|
+
)
|
|
905
|
+
used = {item.participant_ref for item in manifest.participant_assignments}
|
|
906
|
+
ordinal = len(used) + 1
|
|
907
|
+
while f"participant-{ordinal:03d}" in used:
|
|
908
|
+
ordinal += 1
|
|
909
|
+
return replace(
|
|
910
|
+
origin,
|
|
911
|
+
participant_ref=f"participant-{ordinal:03d}",
|
|
912
|
+
session_ref=None,
|
|
913
|
+
window_ref=None,
|
|
914
|
+
status="prepared",
|
|
915
|
+
)
|
|
863
916
|
|
|
864
917
|
|
|
865
918
|
def _validate_next_invocation(manifest: ExecutionManifest, row: Invocation) -> None:
|