okstra 0.201.3 → 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/lifecycle/setup.mjs +15 -0
- package/dist/commands/lifecycle/setup.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +41 -8
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/citation-guidance.d.mts +21 -0
- package/dist/lib/citation-guidance.mjs +79 -0
- package/dist/lib/citation-guidance.mjs.map +1 -0
- 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 +17 -10
- package/docs/architecture.md +26 -20
- package/docs/cli.md +16 -13
- package/docs/contributor-change-matrix.md +3 -2
- package/docs/performance-improvement-plan-v2.md +2 -3
- package/docs/project-structure-overview.md +38 -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 +3 -2
- 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 +115 -6
- 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 +3 -3
- 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 +6 -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_critic_prompt.py +4 -6
- package/runtime/python/okstra_ctl/convergence_provenance.py +81 -18
- 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/execution_mutation_audit.py +21 -21
- 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 +175 -14
- package/runtime/python/okstra_ctl/manager_launch.py +41 -19
- package/runtime/python/okstra_ctl/manager_paths.py +22 -3
- package/runtime/python/okstra_ctl/manager_split.py +474 -0
- package/runtime/python/okstra_ctl/manager_store.py +331 -21
- package/runtime/python/okstra_ctl/manager_sync.py +37 -16
- package/runtime/python/okstra_ctl/manager_view.py +217 -0
- 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/plan_items_cli.py +6 -1
- package/runtime/python/okstra_ctl/profile_show.py +42 -1
- package/runtime/python/okstra_ctl/qa_commands.py +15 -0
- 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_finalize.py +13 -6
- package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +2 -21
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
- package/runtime/python/okstra_ctl/report_html/visualizations.py +0 -5
- 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/verification_target.py +13 -2
- 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 +70 -5
- 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 +32 -13
- package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
- package/runtime/skills/okstra-setup/SKILL.md +21 -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 +109 -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 +7 -4
- package/runtime/templates/reports/html/i18n/ko.json +7 -4
- package/runtime/templates/reports/html/tasks/final-verification.template.html +2 -2
- 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 +77 -2
- 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 -69
- 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 -158
- 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
|
@@ -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():
|
|
@@ -58,56 +58,7 @@ def _validate_run_identity(
|
|
|
58
58
|
).duty_audience
|
|
59
59
|
except ValueError as exc:
|
|
60
60
|
raise AgentPromptCliError(str(exc)) from exc
|
|
61
|
-
if audience != expected and _manifest_issued_role_execution(
|
|
62
|
-
manifest, audience=audience, assignment_ref=assignment_ref
|
|
63
|
-
):
|
|
64
|
-
# The prompt plan names a worker's role by worker id, so on an
|
|
65
|
-
# implementation run the executor's provider resolves to the
|
|
66
|
-
# executor role no matter which role execution is being targeted.
|
|
67
|
-
# But the manifest issues one role execution per role, and a
|
|
68
|
-
# provider serving as both executor and verifier gets two — a
|
|
69
|
-
# normal roster, since the verifier contract accepts reusing the
|
|
70
|
-
# executor's model behind its own session. Refusing the second
|
|
71
|
-
# audience made a role execution okstra had itself issued
|
|
72
|
-
# undispatchable, which costs the run an independent verifier.
|
|
73
|
-
expected = audience
|
|
74
61
|
if audience != expected:
|
|
75
62
|
raise AgentPromptCliError(
|
|
76
63
|
f"audience {audience!r} does not match required audience {expected!r}"
|
|
77
64
|
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def _manifest_issued_role_execution(
|
|
81
|
-
manifest: Mapping[str, Any],
|
|
82
|
-
*,
|
|
83
|
-
audience: str,
|
|
84
|
-
assignment_ref: str,
|
|
85
|
-
) -> bool:
|
|
86
|
-
"""Whether this run issued a role execution for that audience's role.
|
|
87
|
-
|
|
88
|
-
Keyed on the manifest's own rows, not on what the prompt plan infers from
|
|
89
|
-
a worker id: the question is whether okstra created the execution being
|
|
90
|
-
targeted, and only the manifest answers that.
|
|
91
|
-
"""
|
|
92
|
-
from ...domain.role import RoleCatalogError, role_for_duty
|
|
93
|
-
|
|
94
|
-
try:
|
|
95
|
-
role = role_for_duty(audience)
|
|
96
|
-
except RoleCatalogError:
|
|
97
|
-
return False
|
|
98
|
-
assignments = manifest.get("invocationAssignments")
|
|
99
|
-
assignment = (
|
|
100
|
-
assignments.get(assignment_ref) if isinstance(assignments, Mapping) else None
|
|
101
|
-
)
|
|
102
|
-
provider = (
|
|
103
|
-
assignment.get("provider") if isinstance(assignment, Mapping) else None
|
|
104
|
-
)
|
|
105
|
-
if not provider:
|
|
106
|
-
return False
|
|
107
|
-
executions = manifest.get("roleExecutions")
|
|
108
|
-
return any(
|
|
109
|
-
isinstance(row, Mapping)
|
|
110
|
-
and row.get("role") == role
|
|
111
|
-
and row.get("provider") == provider
|
|
112
|
-
for row in (executions if isinstance(executions, list) else [])
|
|
113
|
-
)
|
|
@@ -500,7 +500,10 @@ def _stage_ledger_block(stage_ledger_json: str) -> list[str]:
|
|
|
500
500
|
"in it is taken. Never reuse or renumber one: a new stage takes the",
|
|
501
501
|
"next number after the highest listed here, and reworking a completed",
|
|
502
502
|
"stage means cancelling it and adding a new number, never editing it",
|
|
503
|
-
"in place.
|
|
503
|
+
"in place. The new plan declares every stage listed here as well as the",
|
|
504
|
+
"ones it adds — its rows run 1..N with no gap — because a plan whose",
|
|
505
|
+
"rows start above 1 is refused by every consumer that parses a Stage",
|
|
506
|
+
"Map. `sourcePlan` is the plan the completed stages were built",
|
|
504
507
|
"against; `latestPlan` is the plan this list came from. When the two",
|
|
505
508
|
"differ, the completed work followed the former and the numbering",
|
|
506
509
|
"authority is the latter.",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
from collections.abc import Callable, Sequence
|
|
5
|
+
from dataclasses import replace
|
|
5
6
|
|
|
6
7
|
from ..domain.worker_runtime import (
|
|
7
8
|
EnvironmentBlocked,
|
|
@@ -19,11 +20,15 @@ def open_worker(
|
|
|
19
20
|
last_unavailable: SurfaceUnavailable | None = None
|
|
20
21
|
for port in chain:
|
|
21
22
|
try:
|
|
22
|
-
|
|
23
|
+
handle = port.spawn(request_for(port.surface))
|
|
23
24
|
except EnvironmentBlocked:
|
|
24
25
|
raise
|
|
25
26
|
except SurfaceUnavailable as exc:
|
|
26
27
|
last_unavailable = exc
|
|
28
|
+
continue
|
|
29
|
+
if last_unavailable is None:
|
|
30
|
+
return handle
|
|
31
|
+
return replace(handle, fallback_reason=str(last_unavailable))
|
|
27
32
|
if last_unavailable is None:
|
|
28
33
|
raise SurfaceUnavailable("runtime chain is empty")
|
|
29
34
|
raise last_unavailable
|
|
@@ -30,7 +30,7 @@ from .ports.host_model import (
|
|
|
30
30
|
HostModelBindingPort,
|
|
31
31
|
HostModelBindingRequest,
|
|
32
32
|
)
|
|
33
|
-
from .role_requirements import RoleProfile, RoleRequirement
|
|
33
|
+
from .role_requirements import RoleProfile, RoleRequirement, StaticRoleRequirement
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
TerminalBackend = Literal[
|
|
@@ -239,8 +239,17 @@ def resolve_model_assignment(
|
|
|
239
239
|
host: HostSessionContext,
|
|
240
240
|
environment: AssignmentEnvironment,
|
|
241
241
|
entry_mode: Literal["current-session", "new-session"] = "new-session",
|
|
242
|
+
require_selectable: bool = True,
|
|
242
243
|
) -> ResolvedAssignment:
|
|
243
|
-
"""Resolve one canonical model through the selected host execution path.
|
|
244
|
+
"""Resolve one canonical model through the selected host execution path.
|
|
245
|
+
|
|
246
|
+
`require_selectable=False` re-resolves a binding a run already recorded.
|
|
247
|
+
Selectability decides what a user may pick now; a run that picked a model
|
|
248
|
+
before it left the picker still has to finish. 실측 2026-09-23
|
|
249
|
+
(fontradar-v2-api dev-10860 implementation-planning-002): `gpt-5.6-sol` 이
|
|
250
|
+
카탈로그에서 목록 제외로 바뀌자, 그 모델로 이미 세 라운드를 돈 run 의 다음
|
|
251
|
+
디스패치가 `model binding changed: ... not-selectable` 로 막혔다.
|
|
252
|
+
"""
|
|
244
253
|
_validate_environment(host, environment)
|
|
245
254
|
model = pool.resolve(model_ref)
|
|
246
255
|
availability = pool.availability(
|
|
@@ -249,7 +258,9 @@ def resolve_model_assignment(
|
|
|
249
258
|
host.host_id,
|
|
250
259
|
entry_mode,
|
|
251
260
|
)
|
|
252
|
-
if not availability.available
|
|
261
|
+
if not availability.available and (
|
|
262
|
+
require_selectable or availability.reason != "not-selectable"
|
|
263
|
+
):
|
|
253
264
|
raise AssignmentResolutionError(
|
|
254
265
|
f"model {model_ref!r} is unavailable: {availability.reason}"
|
|
255
266
|
)
|
|
@@ -326,8 +337,8 @@ def resolve_model_selection(
|
|
|
326
337
|
return selected
|
|
327
338
|
|
|
328
339
|
|
|
329
|
-
def _static_requirements(profile: RoleProfile) -> tuple[
|
|
330
|
-
leader =
|
|
340
|
+
def _static_requirements(profile: RoleProfile) -> tuple[StaticRoleRequirement, ...]:
|
|
341
|
+
leader = StaticRoleRequirement("leader", 1, 1, 1, "lead")
|
|
331
342
|
return (leader, *(row for row in profile.roles if not row.dynamic))
|
|
332
343
|
|
|
333
344
|
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
|
+
import re
|
|
10
11
|
import shlex
|
|
11
12
|
import shutil
|
|
12
13
|
import socket
|
|
@@ -72,6 +73,10 @@ SOCKET_UNREACHABLE = "unreachable"
|
|
|
72
73
|
|
|
73
74
|
SOCKET_PROBE_TIMEOUT_SECONDS = 2
|
|
74
75
|
|
|
76
|
+
# cmux 가 돌려주는 식별자가 UUID 인지 ref 인지 가리는 자리. ref 는 번호가 다시
|
|
77
|
+
# 매겨지므로 원장에 남길 수 없다.
|
|
78
|
+
_UUID_PATTERN = re.compile(r"[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}")
|
|
79
|
+
|
|
75
80
|
|
|
76
81
|
def cmux_cli_path() -> str:
|
|
77
82
|
"""Absolute path to the cmux CLI, or "" when cmux is not installed.
|
|
@@ -455,7 +460,11 @@ def spawn_worker_surface(
|
|
|
455
460
|
owned_surface_ids=owned_surface_ids,
|
|
456
461
|
)
|
|
457
462
|
target = _pane_by_id(panes, placement.pane_id)
|
|
458
|
-
|
|
463
|
+
try:
|
|
464
|
+
surface_uuid = _create_surface(workspace, placement, target)
|
|
465
|
+
except (RuntimeError, OSError, subprocess.SubprocessError):
|
|
466
|
+
_restore_lead_after_failed_spawn(workspace, owned_surface_ids)
|
|
467
|
+
raise
|
|
459
468
|
try:
|
|
460
469
|
renamed = rename_surface(surface_uuid, title)
|
|
461
470
|
if renamed.returncode != 0:
|
|
@@ -475,10 +484,35 @@ def spawn_worker_surface(
|
|
|
475
484
|
raise RuntimeError(
|
|
476
485
|
f"{exc}; cleanup failed for cmux surface {surface_uuid}: {cleanup_error}"
|
|
477
486
|
) from exc
|
|
487
|
+
finally:
|
|
488
|
+
_restore_lead_after_failed_spawn(workspace, owned_surface_ids)
|
|
478
489
|
raise
|
|
479
490
|
return surface_uuid
|
|
480
491
|
|
|
481
492
|
|
|
493
|
+
def _restore_lead_after_failed_spawn(
|
|
494
|
+
workspace: str, owned_surface_ids: Collection[str]
|
|
495
|
+
) -> None:
|
|
496
|
+
"""실패한 분할이 리드에서 떼어 간 폭을 돌려준다.
|
|
497
|
+
|
|
498
|
+
폭을 되돌리는 자리는 창이 열린 뒤의 `_size_lead_pane` 하나뿐이었다. 실패한
|
|
499
|
+
시도는 리드에서 폭만 떼어 가고 끝났고, 실패가 반복되면 축소가 누적된다 —
|
|
500
|
+
실측 2026-09-21, jobs dev-10860: 강등 7건 뒤 리드가 64칼럼에서 14칼럼.
|
|
501
|
+
|
|
502
|
+
닫지 못한 창의 폭까지 되찾지는 않는다. 그 창은 `lead_target_width` 에서
|
|
503
|
+
남의 창으로 셈해져 usable 에서 빠지고, 리드는 남은 공간에서 제 몫을 가져
|
|
504
|
+
간다. 되찾으려면 okstra 가 자기 것이라 증명하지 못한 창의 경계를 미는
|
|
505
|
+
셈이라 그쪽이 더 나쁘다.
|
|
506
|
+
|
|
507
|
+
크기 조정이 실패해도 삼킨다: 호출자는 창을 열지 못한 사유를 들고 올라가는
|
|
508
|
+
중이고, 원장에 남아야 할 문장은 그쪽이다.
|
|
509
|
+
"""
|
|
510
|
+
try:
|
|
511
|
+
_size_lead_pane(workspace, owned_surface_ids)
|
|
512
|
+
except (RuntimeError, OSError, subprocess.SubprocessError):
|
|
513
|
+
return
|
|
514
|
+
|
|
515
|
+
|
|
482
516
|
def rename_surface(surface: str, title: str) -> subprocess.CompletedProcess[str]:
|
|
483
517
|
"""Set the tab title cmux shows for one surface.
|
|
484
518
|
|
|
@@ -686,27 +720,14 @@ def rpc(method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
|
686
720
|
return payload if isinstance(payload, dict) else {}
|
|
687
721
|
|
|
688
722
|
|
|
689
|
-
def _open_worker_surface(
|
|
690
|
-
workspace: str, placement: Placement, target: PaneGeometry
|
|
691
|
-
) -> str:
|
|
692
|
-
before = open_surface_ids(workspace)
|
|
693
|
-
_create_surface(workspace, placement, target)
|
|
694
|
-
new_ids = open_surface_ids(workspace) - before
|
|
695
|
-
if len(new_ids) != 1:
|
|
696
|
-
raise RuntimeError(
|
|
697
|
-
f"cmux opened {len(new_ids)} surfaces where exactly one was expected"
|
|
698
|
-
)
|
|
699
|
-
return new_ids.pop()
|
|
700
|
-
|
|
701
|
-
|
|
702
723
|
def open_surface_ids(workspace: str) -> set[str]:
|
|
703
724
|
"""Every surface UUID the workspace currently holds.
|
|
704
725
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
726
|
+
Teardown intersects its recorded ids with this set to tell a surface that is
|
|
727
|
+
still open from one that closed earlier in the run. It is teardown's only
|
|
728
|
+
caller: dispatch used to diff this set across a create to name the surface
|
|
729
|
+
it had just opened, and that is now `new-split --id-format uuids`, which
|
|
730
|
+
says the uuid outright.
|
|
710
731
|
"""
|
|
711
732
|
return {
|
|
712
733
|
surface_id
|
|
@@ -717,9 +738,22 @@ def open_surface_ids(workspace: str) -> set[str]:
|
|
|
717
738
|
|
|
718
739
|
def _create_surface(
|
|
719
740
|
workspace: str, placement: Placement, target: PaneGeometry
|
|
720
|
-
) ->
|
|
741
|
+
) -> str:
|
|
742
|
+
"""새 창을 열고 그 표면의 UUID 를 준다.
|
|
743
|
+
|
|
744
|
+
UUID 는 cmux 가 스스로 말해 준다. `--id-format uuids` 를 주면 이 명령의 출력이
|
|
745
|
+
`OK <surface-uuid> <workspace-uuid>` 가 되고, 그 첫 UUID 가 새 창의 표면이다
|
|
746
|
+
(실측 2026-09-21, 폐기용 워크스페이스 1회 분할: echo 의 UUID 가 직후
|
|
747
|
+
`pane.list` 가 보여 준 새 창의 `surface_ids[0]` 과 같았다).
|
|
748
|
+
|
|
749
|
+
앞서는 분할 전후의 표면 목록 차집합으로 알아냈는데, 그 차집합이 실제 run 에서
|
|
750
|
+
여덟 번 중 여덟 번 비어 워커가 창에 붙지 못하고 빈 창만 남았다. 자기가 만든 것의
|
|
751
|
+
이름을 만든 쪽에 묻는 편이 두 번 읽어 비교하는 것보다 틀릴 자리가 적다.
|
|
752
|
+
"""
|
|
721
753
|
created = run_cmux(
|
|
722
754
|
[
|
|
755
|
+
"--id-format",
|
|
756
|
+
"uuids",
|
|
723
757
|
"new-split",
|
|
724
758
|
placement.direction,
|
|
725
759
|
"--workspace",
|
|
@@ -730,6 +764,21 @@ def _create_surface(
|
|
|
730
764
|
)
|
|
731
765
|
if created.returncode != 0:
|
|
732
766
|
raise RuntimeError(created.stderr.strip() or "cmux could not open a pane")
|
|
767
|
+
return _surface_from_split_echo(created.stdout)
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def _surface_from_split_echo(stdout: str) -> str:
|
|
771
|
+
"""`new-split` 의 출력에서 새 표면 UUID 만 꺼낸다.
|
|
772
|
+
|
|
773
|
+
ref(`surface:2`)는 받지 않는다. okstra 가 기록하고 나중에 닫는 이름이라, cmux 가
|
|
774
|
+
창을 여닫을 때마다 번호를 다시 매기는 ref 로는 엉뚱한 창을 닫게 된다.
|
|
775
|
+
"""
|
|
776
|
+
parts = stdout.split()
|
|
777
|
+
if len(parts) >= 2 and parts[0] == "OK" and _UUID_PATTERN.fullmatch(parts[1]):
|
|
778
|
+
return parts[1]
|
|
779
|
+
raise RuntimeError(
|
|
780
|
+
f"cmux did not name the pane it opened: {stdout.strip()!r}"
|
|
781
|
+
)
|
|
733
782
|
|
|
734
783
|
|
|
735
784
|
def _exec_worker(surface_uuid: str, *, cwd: Path, command: Sequence[str]) -> None:
|
|
@@ -99,13 +99,13 @@ def _branch_snapshot(repo: Path, branch: str, base: str, review_date: str) -> di
|
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
_CLI_EPILOG = r"""Usage:
|
|
102
|
-
okstra code-review target --task-key <k> --stage <N> [--project-root <dir>] [--cwd <dir>] --json
|
|
102
|
+
okstra code-review target --task-key <k> --stage <N> [--base <ref>] [--project-root <dir>] [--cwd <dir>] --json
|
|
103
103
|
okstra code-review target --branch <name> [--base <ref>] [--date <YYYY-MM-DD>] [--project-root <dir>] --json
|
|
104
104
|
|
|
105
105
|
Output: JSON { ok, mode, worktreePath, branch, baseCommit, headCommit,
|
|
106
|
-
reviewPath, round }.
|
|
107
|
-
|
|
108
|
-
branch. An empty worktreePath means the review reads the branch ref
|
|
106
|
+
reviewPath, round }. `--base` sets the base in either mode. Without it, stage
|
|
107
|
+
mode reads the base commit from the stage registry row and branch mode falls
|
|
108
|
+
back to the merge-base with the default branch. An empty worktreePath means the review reads the branch ref
|
|
109
109
|
instead of a checked-out tree. This is read-only — it never creates
|
|
110
110
|
directories or files.
|
|
111
111
|
"""
|
|
@@ -124,7 +124,10 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
124
124
|
parser.add_argument("--task-key", default="", help="project-id:task-group:task-id")
|
|
125
125
|
parser.add_argument("--stage", type=int, default=0, help="stage number to review")
|
|
126
126
|
parser.add_argument("--branch", default="", help="branch to review")
|
|
127
|
-
parser.add_argument(
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"--base", default="",
|
|
129
|
+
help="explicit base ref; overrides the stage registry base or the branch merge-base",
|
|
130
|
+
)
|
|
128
131
|
parser.add_argument("--date", default="", help="YYYY-MM-DD for branch result naming")
|
|
129
132
|
parser.add_argument("--project-root", default="", help="explicit project root")
|
|
130
133
|
parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
|
|
@@ -138,7 +141,9 @@ def _resolve(args: argparse.Namespace, project_root: Path) -> dict:
|
|
|
138
141
|
if args.task_key:
|
|
139
142
|
if args.stage < 1:
|
|
140
143
|
raise StateError("--task-key needs --stage <N>, N >= 1", stage="args")
|
|
141
|
-
return code_review_target_snapshot(
|
|
144
|
+
return code_review_target_snapshot(
|
|
145
|
+
project_root, args.task_key, args.stage, base=args.base
|
|
146
|
+
)
|
|
142
147
|
if args.branch:
|
|
143
148
|
return _branch_snapshot(
|
|
144
149
|
project_root, args.branch, args.base, _review_date(args.date)
|
|
@@ -204,7 +209,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
204
209
|
# 것으로 합쳐진다.
|
|
205
210
|
|
|
206
211
|
def code_review_target_snapshot(
|
|
207
|
-
project_root: Path, task_key: str, stage: int
|
|
212
|
+
project_root: Path, task_key: str, stage: int, *, base: str = ""
|
|
208
213
|
) -> dict:
|
|
209
214
|
"""stage 코드 리뷰가 무엇을 읽고 결과를 어디에 쓰는지 해소한다.
|
|
210
215
|
|
|
@@ -218,6 +223,9 @@ def code_review_target_snapshot(
|
|
|
218
223
|
표류한다: 다중의존 stage 는 오늘의 task-key worktree HEAD 를 받게 되어
|
|
219
224
|
whole-task final-verification 이 stage 들을 병합한 뒤에는 base..head 구간이
|
|
220
225
|
비거나 뒤집히고, 단일의존 stage 는 선행 stage 가 재실행되면 어긋난다.
|
|
226
|
+
|
|
227
|
+
`base` 가 주어지면 그 값을 쓴다 — 스킬이 기록된 base 를 보여 주고 사용자가
|
|
228
|
+
다른 ref 를 고른 경우다. 무시하면 사용자가 거부한 범위로 리뷰가 진행된다.
|
|
221
229
|
"""
|
|
222
230
|
identity = resolve_task_identity(project_root, task_key)
|
|
223
231
|
task_root = Path(identity["taskRoot"])
|
|
@@ -241,7 +249,7 @@ def code_review_target_snapshot(
|
|
|
241
249
|
coords = (identity["projectId"], identity["taskGroup"], identity["taskId"])
|
|
242
250
|
stage_row = worktree_registry.get_stage_row(*coords, stage) or {}
|
|
243
251
|
worktree_view = _stage_worktree_view(project_root, stage_row, stage)
|
|
244
|
-
base_commit = stage_row.get("base_ref") or _legacy_stage_base_commit(
|
|
252
|
+
base_commit = base or stage_row.get("base_ref") or _legacy_stage_base_commit(
|
|
245
253
|
identity, stages, selected
|
|
246
254
|
)
|
|
247
255
|
review_path, round_no = code_review_paths.next_stage_review(
|