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
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from pathlib import PurePosixPath
|
|
13
13
|
import re
|
|
14
14
|
import tempfile
|
|
15
|
-
from typing import Iterator, Literal, Mapping, get_args
|
|
15
|
+
from typing import Any, Iterator, Literal, Mapping, get_args
|
|
16
16
|
|
|
17
17
|
from ..domain.role import RoleCatalogError, role_for_duty
|
|
18
18
|
from ..json_boundary import (
|
|
@@ -44,34 +44,6 @@ AgentAudience = Literal[
|
|
|
44
44
|
]
|
|
45
45
|
|
|
46
46
|
_SUPPORTED_AUDIENCES = frozenset(get_args(AgentAudience))
|
|
47
|
-
# The section set IS the duty contract's shape: a duty author adding a role file
|
|
48
|
-
# reads these names, and `_validate_duty_sections` refuses a file that misses one.
|
|
49
|
-
# The check is structural — it proves every required section exists and carries
|
|
50
|
-
# text, NOT that the text is a real contract. A one-line placeholder passes it;
|
|
51
|
-
# what keeps a section substantive is review, and the rule that earns a section a
|
|
52
|
-
# place here at all: a sentence that reads the same in another duty file belongs
|
|
53
|
-
# in `common.md` or nowhere.
|
|
54
|
-
COMMON_DUTY_SECTIONS = (
|
|
55
|
-
"Assignment fidelity",
|
|
56
|
-
"Required inputs",
|
|
57
|
-
"Evidence first",
|
|
58
|
-
"Authority and scope",
|
|
59
|
-
"Collaboration and independence",
|
|
60
|
-
"Instruction precedence",
|
|
61
|
-
"Conflict handling",
|
|
62
|
-
"Completion honesty",
|
|
63
|
-
)
|
|
64
|
-
ROLE_DUTY_SECTIONS = (
|
|
65
|
-
"Responsibility",
|
|
66
|
-
"Required conduct",
|
|
67
|
-
"Decision principles",
|
|
68
|
-
"Authority and boundaries",
|
|
69
|
-
"Evidence standard",
|
|
70
|
-
"Collaboration contract",
|
|
71
|
-
"Completion criteria",
|
|
72
|
-
"Forbidden conduct",
|
|
73
|
-
"Blocked-state reporting",
|
|
74
|
-
)
|
|
75
47
|
_SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
76
48
|
_DUTY_SECTION_RE = re.compile(r"(?m)^## ([^\n]+)\s*$")
|
|
77
49
|
_TOP_LEVEL_KEYS = {
|
|
@@ -144,6 +116,8 @@ class DutyContract:
|
|
|
144
116
|
applies_to: AgentAudience | None
|
|
145
117
|
body: str
|
|
146
118
|
source_path: Path
|
|
119
|
+
# 직무가 선언한 정본 역할. 공통 계약과 역할 계약 자신은 갖지 않는다.
|
|
120
|
+
role_id: str = ""
|
|
147
121
|
|
|
148
122
|
|
|
149
123
|
@dataclass(frozen=True)
|
|
@@ -495,21 +469,102 @@ class _MaterializedInvocation:
|
|
|
495
469
|
reservation: dict[str, object] | None
|
|
496
470
|
|
|
497
471
|
|
|
472
|
+
|
|
473
|
+
# 계약 JSON 을 프롬프트에 실을 Markdown 으로 바꾸는 고정 렌더러. 제목과 순서는
|
|
474
|
+
# 스키마 필드 순서에 고정되어 있고, 빈 배열도 섹션을 생략하지 않는다 — 생략하면
|
|
475
|
+
# 같은 계약이 호출마다 다른 다이제스트를 갖는다.
|
|
476
|
+
_COMMON_DUTY_SECTIONS_JSON = (
|
|
477
|
+
("assignmentFidelity", "Assignment fidelity"),
|
|
478
|
+
("requiredInputs", "Required inputs"),
|
|
479
|
+
("evidenceFirst", "Evidence first"),
|
|
480
|
+
("authorityAndScope", "Authority and scope"),
|
|
481
|
+
("collaborationAndIndependence", "Collaboration and independence"),
|
|
482
|
+
("instructionPrecedence", "Instruction precedence"),
|
|
483
|
+
("conflictHandling", "Conflict handling"),
|
|
484
|
+
("completionHonesty", "Completion honesty"),
|
|
485
|
+
)
|
|
486
|
+
_ROLE_CONTRACT_SECTIONS_JSON = (
|
|
487
|
+
("identity", "Identity"),
|
|
488
|
+
("responsibilities", "Responsibility"),
|
|
489
|
+
("requiredCapabilities", "Required capabilities"),
|
|
490
|
+
("prohibitions", "Prohibitions"),
|
|
491
|
+
)
|
|
492
|
+
_ROLE_DUTY_SECTIONS_JSON = (
|
|
493
|
+
("responsibilities", "Responsibility"),
|
|
494
|
+
("requiredConduct", "Required conduct"),
|
|
495
|
+
("decisionPrinciples", "Decision principles"),
|
|
496
|
+
("authorityAndBoundaries", "Authority and boundaries"),
|
|
497
|
+
("evidenceStandards", "Evidence standard"),
|
|
498
|
+
("collaborationContract", "Collaboration contract"),
|
|
499
|
+
("completionCriteria", "Completion criteria"),
|
|
500
|
+
("prohibitions", "Forbidden conduct"),
|
|
501
|
+
("blockedStateReporting", "Blocked-state reporting"),
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _contract_title(contract_id: str) -> str:
|
|
506
|
+
return " ".join(part.capitalize() for part in contract_id.split("-"))
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _render_contract_body(
|
|
510
|
+
payload: Mapping[str, Any],
|
|
511
|
+
sections: tuple[tuple[str, str], ...],
|
|
512
|
+
heading: str,
|
|
513
|
+
path: Path,
|
|
514
|
+
) -> str:
|
|
515
|
+
lines = [f"# {heading}", ""]
|
|
516
|
+
for key, title in sections:
|
|
517
|
+
values = payload.get(key)
|
|
518
|
+
if not isinstance(values, list) or not all(
|
|
519
|
+
isinstance(item, str) and item.strip() for item in values
|
|
520
|
+
):
|
|
521
|
+
raise AgentInvocationError(f"invalid contract field {key!r}: {path}")
|
|
522
|
+
lines.append(f"## {title}")
|
|
523
|
+
lines.append("")
|
|
524
|
+
if values:
|
|
525
|
+
lines.extend(f"- {item}" for item in values)
|
|
526
|
+
else:
|
|
527
|
+
lines.append("- (none)")
|
|
528
|
+
lines.append("")
|
|
529
|
+
return "\n".join(lines).rstrip("\n") + "\n"
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _load_contract_payload(path: Path) -> Mapping[str, Any]:
|
|
533
|
+
if not path.is_file() and path.with_suffix(".md").is_file():
|
|
534
|
+
# 이 run 은 Markdown 직무를 동결했다(contractFormatVersion 1). 그 형식의
|
|
535
|
+
# 파서는 제거됐으므로 여기서 멈추고 원인을 이름한다 — 없는 파일 이름만
|
|
536
|
+
# 알리면 설치가 깨진 것처럼 읽힌다.
|
|
537
|
+
raise AgentInvocationError(
|
|
538
|
+
f"run froze markdown duty contracts and cannot be resumed: {path.parent}"
|
|
539
|
+
)
|
|
540
|
+
try:
|
|
541
|
+
payload = load_owned_object(path, artifact="agent contract")
|
|
542
|
+
except JsonBoundaryError as exc:
|
|
543
|
+
raise AgentInvocationError(f"invalid agent contract: {path}: {exc}") from exc
|
|
544
|
+
except OSError as exc:
|
|
545
|
+
raise AgentInvocationError(f"cannot read agent contract: {path}") from exc
|
|
546
|
+
if payload.get("schemaVersion") != "1.0":
|
|
547
|
+
raise AgentInvocationError(f"unsupported contract schemaVersion: {path}")
|
|
548
|
+
return payload
|
|
549
|
+
|
|
550
|
+
|
|
498
551
|
def load_common_duty_contract(duty_root: Path) -> DutyContract:
|
|
499
552
|
"""Load the common fragment, which is deliberately outside the role catalog."""
|
|
500
|
-
path = duty_root / "common.
|
|
501
|
-
|
|
502
|
-
if
|
|
503
|
-
raise AgentInvocationError(f"invalid common
|
|
504
|
-
if fields["id"] != "common" or fields["kind"] != "common":
|
|
505
|
-
raise AgentInvocationError(f"invalid common duty frontmatter: {path}")
|
|
506
|
-
_validate_duty_sections(body, COMMON_DUTY_SECTIONS, "common", path)
|
|
553
|
+
path = duty_root / "common.json"
|
|
554
|
+
payload = _load_contract_payload(path)
|
|
555
|
+
if payload.get("id") != "common":
|
|
556
|
+
raise AgentInvocationError(f"invalid common contract id: {path}")
|
|
507
557
|
return DutyContract(
|
|
508
558
|
id="common",
|
|
509
|
-
version=
|
|
559
|
+
version=1,
|
|
510
560
|
kind="common",
|
|
511
561
|
applies_to=None,
|
|
512
|
-
body=
|
|
562
|
+
body=_render_contract_body(
|
|
563
|
+
payload,
|
|
564
|
+
_COMMON_DUTY_SECTIONS_JSON,
|
|
565
|
+
"Common Agent Duty Contract",
|
|
566
|
+
path,
|
|
567
|
+
),
|
|
513
568
|
source_path=path,
|
|
514
569
|
)
|
|
515
570
|
|
|
@@ -518,8 +573,8 @@ def load_duty_catalog(duty_root: Path) -> dict[AgentAudience, DutyContract]:
|
|
|
518
573
|
"""Load one role contract for every supported invocation audience."""
|
|
519
574
|
catalog: dict[AgentAudience, DutyContract] = {}
|
|
520
575
|
seen_ids: set[str] = set()
|
|
521
|
-
for path in sorted(duty_root.glob("*.
|
|
522
|
-
if path.name == "common.
|
|
576
|
+
for path in sorted(duty_root.glob("*.json")):
|
|
577
|
+
if path.name == "common.json":
|
|
523
578
|
continue
|
|
524
579
|
duty = _load_role_duty(path)
|
|
525
580
|
if duty.id in seen_ids:
|
|
@@ -538,18 +593,49 @@ def load_role_duty_contract(duty_root: Path, audience: AgentAudience) -> DutyCon
|
|
|
538
593
|
"""현재 호출의 지침만 읽어 이후 추가된 역할을 기존 실행에 요구하지 않는다."""
|
|
539
594
|
if audience not in _SUPPORTED_AUDIENCES:
|
|
540
595
|
raise AgentInvocationError(f"unknown duty audience: {audience}")
|
|
541
|
-
duty = _load_role_duty(duty_root / f"{audience}.
|
|
596
|
+
duty = _load_role_duty(duty_root / f"{audience}.json")
|
|
542
597
|
if duty.applies_to != audience:
|
|
543
598
|
raise AgentInvocationError(f"duty audience does not match requested audience: {audience}")
|
|
544
599
|
return duty
|
|
545
600
|
|
|
546
601
|
|
|
547
602
|
def digest_duty_catalog(duty_root: Path) -> str:
|
|
548
|
-
"""Return the canonical digest of every
|
|
549
|
-
|
|
603
|
+
"""Return the canonical digest of every contract file in a snapshot.
|
|
604
|
+
|
|
605
|
+
역할 계약은 하위 `roles/` 에 둔다 — `report-writer` 와 `translator` 는 직무
|
|
606
|
+
id 이면서 역할 id 라, 한 폴더에 두면 두 계약이 같은 파일 이름을 두고 겹친다.
|
|
607
|
+
"""
|
|
608
|
+
names = [
|
|
609
|
+
path.relative_to(duty_root).as_posix()
|
|
610
|
+
for path in (*duty_root.glob("*.json"), *(duty_root / "roles").glob("*.json"))
|
|
611
|
+
]
|
|
550
612
|
return _digest_framed_files(duty_root, names)
|
|
551
613
|
|
|
552
614
|
|
|
615
|
+
def load_role_contract(duty_root: Path, role_id: str) -> DutyContract:
|
|
616
|
+
"""이 호출의 역할 계약. 직무가 `roleId` 로 이름한 역할 하나만 읽는다."""
|
|
617
|
+
path = duty_root / "roles" / f"{role_id}.json"
|
|
618
|
+
payload = _load_contract_payload(path)
|
|
619
|
+
if payload.get("id") != role_id:
|
|
620
|
+
raise AgentInvocationError(f"role contract id does not match its file: {path}")
|
|
621
|
+
identity = payload.get("identity")
|
|
622
|
+
if not isinstance(identity, str) or not identity.strip():
|
|
623
|
+
raise AgentInvocationError(f"role contract has no identity: {path}")
|
|
624
|
+
return DutyContract(
|
|
625
|
+
id=role_id,
|
|
626
|
+
version=1,
|
|
627
|
+
kind="role",
|
|
628
|
+
applies_to=None,
|
|
629
|
+
body=_render_contract_body(
|
|
630
|
+
{**payload, "identity": [identity]},
|
|
631
|
+
_ROLE_CONTRACT_SECTIONS_JSON,
|
|
632
|
+
f"{_contract_title(role_id)} Role Contract",
|
|
633
|
+
path,
|
|
634
|
+
),
|
|
635
|
+
source_path=path,
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
|
|
553
639
|
def prepare_agent_invocation(
|
|
554
640
|
request: AgentInvocationRequest,
|
|
555
641
|
) -> PreparedAgentInvocation:
|
|
@@ -1314,7 +1400,10 @@ def _render_prompt(
|
|
|
1314
1400
|
if assignment.host_model_value is not None:
|
|
1315
1401
|
header.append(f"**Host model value:** {assignment.host_model_value}")
|
|
1316
1402
|
header.extend(delivery)
|
|
1317
|
-
|
|
1403
|
+
role_contract = load_role_contract(request.duty_root, duty.role_id)
|
|
1404
|
+
duty_body = "\n\n".join(
|
|
1405
|
+
part.rstrip() for part in (common.body, role_contract.body, duty.body)
|
|
1406
|
+
)
|
|
1318
1407
|
return (
|
|
1319
1408
|
"\n".join(header)
|
|
1320
1409
|
+ "\n\n## Duty Contract\n\n"
|
|
@@ -1344,7 +1433,11 @@ def _invocation_digests(
|
|
|
1344
1433
|
_source_payload(source) for source in request.instruction.source_paths
|
|
1345
1434
|
],
|
|
1346
1435
|
}
|
|
1347
|
-
selected_names = [
|
|
1436
|
+
selected_names = [
|
|
1437
|
+
"common.json",
|
|
1438
|
+
f"roles/{duty.role_id}.json",
|
|
1439
|
+
duty.source_path.name,
|
|
1440
|
+
]
|
|
1348
1441
|
return {
|
|
1349
1442
|
"catalogDigest": digest_duty_catalog(request.duty_root),
|
|
1350
1443
|
"assignmentDigest": _sha256(_canonical_json(assignment)),
|
|
@@ -2131,7 +2224,9 @@ def _verify_duty_snapshot(
|
|
|
2131
2224
|
if actual_catalog != expected_catalog or digests["catalogDigest"] != actual_catalog:
|
|
2132
2225
|
errors.append("catalog digest does not match run duty snapshot")
|
|
2133
2226
|
return errors
|
|
2134
|
-
actual_duty = _digest_framed_files(
|
|
2227
|
+
actual_duty = _digest_framed_files(
|
|
2228
|
+
root, ["common.json", f"roles/{duty.role_id}.json", duty.source_path.name]
|
|
2229
|
+
)
|
|
2135
2230
|
if digests["dutyDigest"] != actual_duty:
|
|
2136
2231
|
errors.append("duty digest does not match run duty snapshot")
|
|
2137
2232
|
return errors
|
|
@@ -2163,7 +2258,17 @@ def _verify_prompt_duty_body(
|
|
|
2163
2258
|
rendered_duty, _task = remainder.split("\n\n## Task Instructions\n\n", 1)
|
|
2164
2259
|
except (AgentInvocationError, OSError, UnicodeDecodeError, ValueError):
|
|
2165
2260
|
return ["prompt duty contract does not match duty snapshot"]
|
|
2166
|
-
|
|
2261
|
+
role_contract = load_role_contract(
|
|
2262
|
+
_project_path(
|
|
2263
|
+
project_root,
|
|
2264
|
+
str(metadata["contractSource"]["dutyRootPath"]),
|
|
2265
|
+
must_exist=True,
|
|
2266
|
+
),
|
|
2267
|
+
duty.role_id,
|
|
2268
|
+
)
|
|
2269
|
+
expected = "\n\n".join(
|
|
2270
|
+
part.rstrip() for part in (common.body, role_contract.body, duty.body)
|
|
2271
|
+
)
|
|
2167
2272
|
if rendered_duty != expected:
|
|
2168
2273
|
return ["prompt duty contract does not match duty snapshot"]
|
|
2169
2274
|
return []
|
|
@@ -2407,77 +2512,27 @@ def _deduplicate(errors: list[str]) -> list[str]:
|
|
|
2407
2512
|
|
|
2408
2513
|
|
|
2409
2514
|
def _load_role_duty(path: Path) -> DutyContract:
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
audience = fields["appliesTo"]
|
|
2414
|
-
if audience not in _SUPPORTED_AUDIENCES:
|
|
2515
|
+
payload = _load_contract_payload(path)
|
|
2516
|
+
audience = payload.get("id")
|
|
2517
|
+
if not isinstance(audience, str) or audience not in _SUPPORTED_AUDIENCES:
|
|
2415
2518
|
raise AgentInvocationError(f"unknown duty audience: {audience}")
|
|
2416
|
-
if
|
|
2417
|
-
raise AgentInvocationError(f"
|
|
2418
|
-
|
|
2519
|
+
if audience != path.stem:
|
|
2520
|
+
raise AgentInvocationError(f"duty id does not match its file name: {path}")
|
|
2521
|
+
if not isinstance(payload.get("roleId"), str) or not payload["roleId"]:
|
|
2522
|
+
raise AgentInvocationError(f"duty contract has no roleId: {path}")
|
|
2419
2523
|
return DutyContract(
|
|
2420
|
-
|
|
2421
|
-
|
|
2524
|
+
role_id=payload["roleId"],
|
|
2525
|
+
id=audience,
|
|
2526
|
+
version=1,
|
|
2422
2527
|
kind="role",
|
|
2423
2528
|
applies_to=audience,
|
|
2424
|
-
body=
|
|
2529
|
+
body=_render_contract_body(
|
|
2530
|
+
payload,
|
|
2531
|
+
_ROLE_DUTY_SECTIONS_JSON,
|
|
2532
|
+
f"{_contract_title(audience)} Duty Contract",
|
|
2533
|
+
path,
|
|
2534
|
+
),
|
|
2425
2535
|
source_path=path,
|
|
2426
2536
|
)
|
|
2427
2537
|
|
|
2428
2538
|
|
|
2429
|
-
def _parse_duty_file(path: Path) -> tuple[dict[str, str], str]:
|
|
2430
|
-
try:
|
|
2431
|
-
text = path.read_text(encoding="utf-8")
|
|
2432
|
-
except OSError as exc:
|
|
2433
|
-
raise AgentInvocationError(f"cannot read duty contract: {path}") from exc
|
|
2434
|
-
lines = text.splitlines(keepends=True)
|
|
2435
|
-
if not lines or lines[0].strip() != "---":
|
|
2436
|
-
raise AgentInvocationError(f"missing duty frontmatter: {path}")
|
|
2437
|
-
try:
|
|
2438
|
-
end = next(index for index, line in enumerate(lines[1:], 1) if line.strip() == "---")
|
|
2439
|
-
except StopIteration as exc:
|
|
2440
|
-
raise AgentInvocationError(f"unterminated duty frontmatter: {path}") from exc
|
|
2441
|
-
fields: dict[str, str] = {}
|
|
2442
|
-
for line in lines[1:end]:
|
|
2443
|
-
key, separator, value = line.partition(":")
|
|
2444
|
-
if not separator or not key.strip() or key.strip() in fields:
|
|
2445
|
-
raise AgentInvocationError(f"invalid duty frontmatter: {path}")
|
|
2446
|
-
fields[key.strip()] = value.strip()
|
|
2447
|
-
return fields, "".join(lines[end + 1 :]).lstrip("\n")
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
def _validate_duty_sections(
|
|
2451
|
-
body: str,
|
|
2452
|
-
required: tuple[str, ...],
|
|
2453
|
-
kind: str,
|
|
2454
|
-
path: Path,
|
|
2455
|
-
) -> None:
|
|
2456
|
-
matches = list(_DUTY_SECTION_RE.finditer(body))
|
|
2457
|
-
sections: dict[str, str] = {}
|
|
2458
|
-
for index, match in enumerate(matches):
|
|
2459
|
-
name = match.group(1).strip()
|
|
2460
|
-
if name in sections:
|
|
2461
|
-
raise AgentInvocationError(f"duplicate {kind} duty section {name}: {path}")
|
|
2462
|
-
end = matches[index + 1].start() if index + 1 < len(matches) else len(body)
|
|
2463
|
-
sections[name] = body[match.end() : end].strip()
|
|
2464
|
-
missing = [name for name in required if name not in sections]
|
|
2465
|
-
if missing:
|
|
2466
|
-
raise AgentInvocationError(
|
|
2467
|
-
f"missing {kind} duty sections: {', '.join(missing)}: {path}"
|
|
2468
|
-
)
|
|
2469
|
-
empty = [name for name in required if not sections[name]]
|
|
2470
|
-
if empty:
|
|
2471
|
-
raise AgentInvocationError(
|
|
2472
|
-
f"empty {kind} duty sections: {', '.join(empty)}: {path}"
|
|
2473
|
-
)
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
def _parse_version(value: str, path: Path) -> int:
|
|
2477
|
-
try:
|
|
2478
|
-
version = int(value)
|
|
2479
|
-
except ValueError as exc:
|
|
2480
|
-
raise AgentInvocationError(f"invalid duty version: {path}") from exc
|
|
2481
|
-
if version < 1:
|
|
2482
|
-
raise AgentInvocationError(f"invalid duty version: {path}")
|
|
2483
|
-
return version
|
|
@@ -41,6 +41,7 @@ _CLI_EPILOG = r"""Usage:
|
|
|
41
41
|
okstra agent-prompt verify-completion [options]
|
|
42
42
|
okstra agent-prompt record-dispatch [options]
|
|
43
43
|
okstra agent-prompt link-result [options]
|
|
44
|
+
okstra agent-prompt refreeze-contracts --project-root <dir> --run-manifest <path> [--json]
|
|
44
45
|
"""
|
|
45
46
|
|
|
46
47
|
|
|
@@ -57,6 +58,12 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
57
58
|
_add_materialize_parser(commands)
|
|
58
59
|
_add_completion_parsers(commands)
|
|
59
60
|
_add_dispatch_parsers(commands)
|
|
61
|
+
operation = commands.add_parser(
|
|
62
|
+
"resolve-operation",
|
|
63
|
+
help="resolve the role, worker count and models an operation requires",
|
|
64
|
+
)
|
|
65
|
+
operation.add_argument("--operation", required=True)
|
|
66
|
+
operation.add_argument("--json", action="store_true")
|
|
60
67
|
jobs = commands.add_parser("jobs", help="generate a verified jobs file without dispatching")
|
|
61
68
|
_common_paths(jobs)
|
|
62
69
|
jobs.add_argument("--run-manifest", required=True)
|
|
@@ -210,6 +217,15 @@ def _add_dispatch_parsers(commands: argparse._SubParsersAction) -> None:
|
|
|
210
217
|
abandon.add_argument("--reason", required=True)
|
|
211
218
|
abandon.add_argument("--json", action="store_true")
|
|
212
219
|
|
|
220
|
+
refreeze = commands.add_parser(
|
|
221
|
+
"refreeze-contracts",
|
|
222
|
+
help="re-freeze this run's duty contracts in the installed format "
|
|
223
|
+
"and stamp the manifest digest",
|
|
224
|
+
)
|
|
225
|
+
_common_paths(refreeze)
|
|
226
|
+
refreeze.add_argument("--run-manifest", required=True)
|
|
227
|
+
refreeze.add_argument("--json", action="store_true")
|
|
228
|
+
|
|
213
229
|
link_result = commands.add_parser("link-result")
|
|
214
230
|
_common_paths(link_result)
|
|
215
231
|
link_result.add_argument("--run-manifest", required=True)
|
|
@@ -229,9 +245,99 @@ def _standalone_completion_args(parser: argparse.ArgumentParser) -> None:
|
|
|
229
245
|
parser.add_argument("--json", action="store_true")
|
|
230
246
|
|
|
231
247
|
|
|
248
|
+
def _refreeze_contracts(args: argparse.Namespace) -> dict[str, object]:
|
|
249
|
+
"""이 run 의 계약을 설치본 형식으로 다시 동결한다. 사용자만 부른다."""
|
|
250
|
+
from ...contract_refreeze import (
|
|
251
|
+
ContractRefreezeError,
|
|
252
|
+
refreeze_run_contracts,
|
|
253
|
+
)
|
|
254
|
+
from ...paths import find_asset_root
|
|
255
|
+
|
|
256
|
+
workspace_root = find_asset_root(("prompts", "duties"), is_present=Path.is_dir)
|
|
257
|
+
if workspace_root is None:
|
|
258
|
+
raise AgentPromptCliError(
|
|
259
|
+
"agent duty catalog not found: no prompts/duties under OKSTRA_HOME "
|
|
260
|
+
"or this checkout"
|
|
261
|
+
)
|
|
262
|
+
try:
|
|
263
|
+
result = refreeze_run_contracts(
|
|
264
|
+
project_root=Path(args.project_root),
|
|
265
|
+
run_manifest_path=Path(args.run_manifest),
|
|
266
|
+
workspace_root=workspace_root,
|
|
267
|
+
)
|
|
268
|
+
except ContractRefreezeError as exc:
|
|
269
|
+
raise AgentPromptCliError(str(exc)) from exc
|
|
270
|
+
return {
|
|
271
|
+
"dutyRoot": result.duty_root,
|
|
272
|
+
"digest": result.digest,
|
|
273
|
+
"previousDigest": result.previous_digest,
|
|
274
|
+
"formatVersion": result.format_version,
|
|
275
|
+
"previousFormatVersion": result.previous_format_version,
|
|
276
|
+
"changed": result.changed,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _resolve_operation(operation_id: str) -> dict[str, object]:
|
|
281
|
+
"""이 작업이 열 칸과 각 칸의 모델. 기술은 작업 이름만 알면 된다."""
|
|
282
|
+
from ...model_pool import ModelPool
|
|
283
|
+
from ...operation_invocation import (
|
|
284
|
+
OperationPreparationError,
|
|
285
|
+
resolve_operation_slots,
|
|
286
|
+
)
|
|
287
|
+
from ...paths import find_asset_root
|
|
288
|
+
from ...registry.provider_registry import default_provider_registry
|
|
289
|
+
|
|
290
|
+
root = find_asset_root(("agents", "operations"), is_present=Path.is_dir)
|
|
291
|
+
if root is None:
|
|
292
|
+
raise AgentPromptCliError(
|
|
293
|
+
"operation contracts not found: no agents/operations under "
|
|
294
|
+
"OKSTRA_HOME or this checkout"
|
|
295
|
+
)
|
|
296
|
+
try:
|
|
297
|
+
slots = resolve_operation_slots(
|
|
298
|
+
operation_id,
|
|
299
|
+
contract_root=root,
|
|
300
|
+
pool=ModelPool.from_registry(default_provider_registry()),
|
|
301
|
+
)
|
|
302
|
+
except OperationPreparationError as exc:
|
|
303
|
+
raise AgentPromptCliError(str(exc)) from exc
|
|
304
|
+
return {
|
|
305
|
+
"operation": operation_id,
|
|
306
|
+
"dutyId": slots[0].duty_id,
|
|
307
|
+
"roleId": slots[0].role_id,
|
|
308
|
+
"count": len(slots),
|
|
309
|
+
"slots": [
|
|
310
|
+
{
|
|
311
|
+
"ordinal": slot.ordinal,
|
|
312
|
+
"provider": slot.provider_id,
|
|
313
|
+
"modelRef": slot.model_ref,
|
|
314
|
+
"model": slot.display_name,
|
|
315
|
+
}
|
|
316
|
+
for slot in slots
|
|
317
|
+
],
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
232
321
|
def main(argv: list[str] | None = None) -> int:
|
|
233
322
|
try:
|
|
234
323
|
args = _parser().parse_args(argv)
|
|
324
|
+
if args.command == "resolve-operation":
|
|
325
|
+
payload = _resolve_operation(args.operation)
|
|
326
|
+
if args.json:
|
|
327
|
+
_emit(payload, True)
|
|
328
|
+
else:
|
|
329
|
+
# 기본 출력은 고정 텍스트다. 기술 본문이 모델에게 okstra 소유
|
|
330
|
+
# JSON 을 읽으라고 지시하면 그 해석이 곧 두 번째 파서가 된다.
|
|
331
|
+
print(f"operation: {payload['operation']}")
|
|
332
|
+
print(f"duty: {payload['dutyId']}")
|
|
333
|
+
print(f"role: {payload['roleId']}")
|
|
334
|
+
print(f"workers: {payload['count']}")
|
|
335
|
+
for slot in payload["slots"]:
|
|
336
|
+
print(
|
|
337
|
+
f" slot {slot['ordinal']}: "
|
|
338
|
+
f"provider={slot['provider']} model={slot['modelRef']}"
|
|
339
|
+
)
|
|
340
|
+
return 0
|
|
235
341
|
if args.command == "jobs":
|
|
236
342
|
payload = generate_jobs(args)
|
|
237
343
|
if args.json:
|
|
@@ -277,6 +383,20 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
277
383
|
if args.command == "verify-completion":
|
|
278
384
|
_verify_completion(args)
|
|
279
385
|
return 0
|
|
386
|
+
if args.command == "refreeze-contracts":
|
|
387
|
+
payload = _refreeze_contracts(args)
|
|
388
|
+
if args.json:
|
|
389
|
+
_emit(payload, True)
|
|
390
|
+
else:
|
|
391
|
+
print(f"duty root: {payload['dutyRoot']}")
|
|
392
|
+
print(f"catalog digest: {payload['digest']}")
|
|
393
|
+
print(
|
|
394
|
+
"contract format: "
|
|
395
|
+
f"{payload['previousFormatVersion']} -> {payload['formatVersion']}"
|
|
396
|
+
if payload["changed"]
|
|
397
|
+
else "contract format: already current"
|
|
398
|
+
)
|
|
399
|
+
return 0
|
|
280
400
|
if args.command == "record-dispatch":
|
|
281
401
|
_record_dispatch(args)
|
|
282
402
|
return 0
|
|
@@ -41,6 +41,16 @@ from ...worker_prompt_policy import (
|
|
|
41
41
|
is_verification_dispatch_kind,
|
|
42
42
|
)
|
|
43
43
|
from ...paths import okstra_home
|
|
44
|
+
from ...contract_refreeze import (
|
|
45
|
+
CONTRACT_FORMAT_VERSION,
|
|
46
|
+
frozen_format_version,
|
|
47
|
+
refreeze_message,
|
|
48
|
+
)
|
|
49
|
+
from ...initial_prompt_materialization import (
|
|
50
|
+
InitialPromptMaterializationError,
|
|
51
|
+
implementation_anchor_lines,
|
|
52
|
+
required_resource_block,
|
|
53
|
+
)
|
|
44
54
|
from ...final_report_paths import final_report_data_path
|
|
45
55
|
from ...final_report_schema import load_schema_version
|
|
46
56
|
from ...report_inputs import report_narrative_path, uses_report_contract_v3
|
|
@@ -300,6 +310,12 @@ def _materialize_run(
|
|
|
300
310
|
must_exist=True,
|
|
301
311
|
)
|
|
302
312
|
if contract.get("catalogDigest") != digest_duty_catalog(duty_root):
|
|
313
|
+
# 형식이 바뀐 릴리스를 설치하면 옛 run 의 다이제스트는 영영 맞지 않는다.
|
|
314
|
+
# "다이제스트 불일치" 만 말하면 읽는 쪽은 파일이 변조된 줄 알고 그 자리를
|
|
315
|
+
# 뒤진다 — 실측 2026-09-23: 한 세션이 dispatch-kind 별 분기를 찾다가
|
|
316
|
+
# 멈췄고, 원인은 그 사이에 일어난 설치였다.
|
|
317
|
+
if frozen_format_version(contract) != CONTRACT_FORMAT_VERSION:
|
|
318
|
+
raise AgentPromptCliError(refreeze_message(str(duty_root)))
|
|
303
319
|
raise AgentPromptCliError("run duty snapshot catalog digest does not match")
|
|
304
320
|
active_context_path = _project_manifest_path(
|
|
305
321
|
project_root,
|
|
@@ -357,6 +373,13 @@ def _materialize_run(
|
|
|
357
373
|
"--corrections requires report contract 3.0 (a Markdown narrative); "
|
|
358
374
|
"this run uses an older contract"
|
|
359
375
|
)
|
|
376
|
+
if args.audience in _RESOURCE_AUDIENCES and not is_reverify:
|
|
377
|
+
anchor_lines = anchor_lines + tuple(
|
|
378
|
+
_implementation_anchors(project_root, manifest_path, args.worker_id)
|
|
379
|
+
)
|
|
380
|
+
body = _with_required_resources(
|
|
381
|
+
project_root, manifest_path, args.worker_id, body
|
|
382
|
+
)
|
|
360
383
|
request = AgentInvocationRequest(
|
|
361
384
|
invocation_id=args.invocation_id,
|
|
362
385
|
worker_id=args.worker_id if identity is None else None,
|
|
@@ -410,6 +433,62 @@ def _materialize_run(
|
|
|
410
433
|
return prepare_agent_invocation(request)
|
|
411
434
|
|
|
412
435
|
|
|
436
|
+
# 디스패치 composer 가 필수 자원 블록을 싣는 audience. 이 명령으로 먼저 발행된
|
|
437
|
+
# 프롬프트는 디스패치가 그대로 쓰므로, 블록을 여기서 붙이지 않으면 워커에 닿지 않는다.
|
|
438
|
+
_RESOURCE_AUDIENCES = frozenset({"implementation-executor", "implementation-verifier"})
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _implementation_anchors(
|
|
442
|
+
project_root: Path, manifest_path: Path, worker_id: str,
|
|
443
|
+
) -> list[str]:
|
|
444
|
+
"""디스패치 composer 와 같은 구현 앵커 줄."""
|
|
445
|
+
try:
|
|
446
|
+
return implementation_anchor_lines(
|
|
447
|
+
project_root=project_root,
|
|
448
|
+
run_manifest_path=manifest_path,
|
|
449
|
+
runtime_root=_runtime_root(),
|
|
450
|
+
worker_id=worker_id,
|
|
451
|
+
)
|
|
452
|
+
except InitialPromptMaterializationError as exc:
|
|
453
|
+
raise AgentPromptCliError(f"implementation prompt anchors: {exc}") from exc
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _runtime_root() -> Path:
|
|
457
|
+
from ...paths import find_asset_root
|
|
458
|
+
|
|
459
|
+
root = find_asset_root(("prompts", "profiles"), is_present=Path.is_dir)
|
|
460
|
+
if root is None:
|
|
461
|
+
raise AgentPromptCliError(
|
|
462
|
+
"prompt profiles not found: no prompts/profiles under OKSTRA_HOME "
|
|
463
|
+
"or this checkout"
|
|
464
|
+
)
|
|
465
|
+
return root
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _with_required_resources(
|
|
469
|
+
project_root: Path, manifest_path: Path, worker_id: str, body: str,
|
|
470
|
+
) -> str:
|
|
471
|
+
"""리드의 지시문 뒤에 composer 와 같은 필수 자원 블록을 붙인다.
|
|
472
|
+
|
|
473
|
+
executor 의 BLOCKING 게이트 세 본문, verifier 의 `_implementation-verifier.md`
|
|
474
|
+
(self-mock 검출기 호출문 포함), clarification 권한 블록, fix-run carry 다.
|
|
475
|
+
배송 방식(eager / lazy)은 run 매니페스트의 선언을 따른다.
|
|
476
|
+
"""
|
|
477
|
+
runtime_root = _runtime_root()
|
|
478
|
+
try:
|
|
479
|
+
block = required_resource_block(
|
|
480
|
+
project_root=project_root,
|
|
481
|
+
run_manifest_path=manifest_path,
|
|
482
|
+
runtime_root=runtime_root,
|
|
483
|
+
worker_id=worker_id,
|
|
484
|
+
)
|
|
485
|
+
except InitialPromptMaterializationError as exc:
|
|
486
|
+
raise AgentPromptCliError(f"required prompt resources: {exc}") from exc
|
|
487
|
+
if not block:
|
|
488
|
+
return body
|
|
489
|
+
return body.rstrip() + "\n\n" + block + "\n"
|
|
490
|
+
|
|
491
|
+
|
|
413
492
|
def _complete_run_reverify_body(
|
|
414
493
|
args: argparse.Namespace, manifest: Mapping[str, Any],
|
|
415
494
|
active_context: Mapping[str, Any], assignment: AgentModelAssignment,
|
|
@@ -956,17 +1035,43 @@ def _snapshot_standalone_duties(destination: Path) -> None:
|
|
|
956
1035
|
"OKSTRA_HOME or this checkout"
|
|
957
1036
|
)
|
|
958
1037
|
source = root.joinpath(*duties_relative)
|
|
1038
|
+
# 공통 계약은 직무가 아니라 `agents/` 가 소유한다(ADR-0017). 동결본은 한
|
|
1039
|
+
# 뿌리여야 하므로 여기서 함께 싣는다.
|
|
1040
|
+
common_root = find_asset_root(("agents", "common.json"), is_present=Path.is_file)
|
|
1041
|
+
if common_root is None:
|
|
1042
|
+
raise AgentPromptCliError(
|
|
1043
|
+
"agent common contract not found: no agents/common.json under "
|
|
1044
|
+
"OKSTRA_HOME or this checkout"
|
|
1045
|
+
)
|
|
1046
|
+
common_source = common_root / "agents" / "common.json"
|
|
1047
|
+
|
|
1048
|
+
def _staged_digest() -> str:
|
|
1049
|
+
staged = Path(tempfile.mkdtemp(prefix=".okstra-standalone-duties."))
|
|
1050
|
+
try:
|
|
1051
|
+
shutil.copytree(source, staged, dirs_exist_ok=True)
|
|
1052
|
+
shutil.copyfile(common_source, staged / "common.json")
|
|
1053
|
+
shutil.copytree(
|
|
1054
|
+
common_source.parent / "roles", staged / "roles", dirs_exist_ok=True
|
|
1055
|
+
)
|
|
1056
|
+
return digest_duty_catalog(staged)
|
|
1057
|
+
finally:
|
|
1058
|
+
shutil.rmtree(staged, ignore_errors=True)
|
|
1059
|
+
|
|
959
1060
|
if destination.exists():
|
|
960
|
-
if not destination.is_dir() or digest_duty_catalog(destination) !=
|
|
1061
|
+
if not destination.is_dir() or digest_duty_catalog(destination) != _staged_digest():
|
|
961
1062
|
raise AgentPromptCliError("standalone duty snapshot conflicts with existing files")
|
|
962
1063
|
return
|
|
963
1064
|
temporary = Path(tempfile.mkdtemp(prefix=f".{destination.name}.", dir=destination.parent))
|
|
964
1065
|
try:
|
|
965
1066
|
shutil.copytree(source, temporary, dirs_exist_ok=True)
|
|
1067
|
+
shutil.copyfile(common_source, temporary / "common.json")
|
|
1068
|
+
shutil.copytree(
|
|
1069
|
+
common_source.parent / "roles", temporary / "roles", dirs_exist_ok=True
|
|
1070
|
+
)
|
|
966
1071
|
try:
|
|
967
1072
|
os.rename(temporary, destination)
|
|
968
1073
|
except FileExistsError:
|
|
969
|
-
if digest_duty_catalog(destination) !=
|
|
1074
|
+
if digest_duty_catalog(destination) != _staged_digest():
|
|
970
1075
|
raise AgentPromptCliError("standalone duty snapshot publication conflict")
|
|
971
1076
|
finally:
|
|
972
1077
|
if temporary.exists():
|