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
|
@@ -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(
|
|
@@ -219,6 +219,49 @@ def exempt_stage_surface_conflicts(
|
|
|
219
219
|
return conflicts
|
|
220
220
|
|
|
221
221
|
|
|
222
|
+
def declared_stage_surface_gaps(
|
|
223
|
+
data: object, patterns: object = None,
|
|
224
|
+
) -> list[dict[str, object]]:
|
|
225
|
+
"""`Conformance tests:` 의 `requires` 가 계획된 경로의 표면을 다 덮지 못한 stage.
|
|
226
|
+
|
|
227
|
+
구현 게이트는 두 가지를 동시에 요구한다 — diff 가 건드린 표면을 `requires`
|
|
228
|
+
가 덮을 것(`_validate_conformance_surfaces`), 그리고 선언된 stage 의
|
|
229
|
+
`requires` 가 계획 선언과 같을 것(`_declared_conformance_errors`). 계획이
|
|
230
|
+
`requires` 를 좁게 적었는데 그 stage 가 계획한 경로가 다른 표면을 건드리면
|
|
231
|
+
구현은 둘 중 하나에 반드시 걸린다. 실측(2026-09-22, fontradar-v2-api dev-10860
|
|
232
|
+
Stage 1~3): `requires=[io]` 로 선언하고 `catalog.controller.ts`(http)를
|
|
233
|
+
계획해, 구현 run 이 "undeclared surface" 와 "requires mismatch" 를 번갈아 냈다.
|
|
234
|
+
면제 stage 와 같은 판별을 선언 stage 에도 승인 전에 한다.
|
|
235
|
+
|
|
236
|
+
반환 행: `{"stage": <int>, "surfaces": [빠진 표면..], "paths": [..],
|
|
237
|
+
"requires": [선언된 것..]}`.
|
|
238
|
+
"""
|
|
239
|
+
planning = data.get("implementationPlanning") if isinstance(data, dict) else None
|
|
240
|
+
stages = planning.get("stages") if isinstance(planning, dict) else None
|
|
241
|
+
gaps: list[dict[str, object]] = []
|
|
242
|
+
for stage in stages if isinstance(stages, list) else []:
|
|
243
|
+
if not isinstance(stage, dict):
|
|
244
|
+
continue
|
|
245
|
+
parsed = parse_conformance_tests(stage.get("conformanceTests"))
|
|
246
|
+
number = stage.get("stage")
|
|
247
|
+
if parsed is None or not isinstance(number, int) or isinstance(number, bool):
|
|
248
|
+
continue
|
|
249
|
+
_script, requires = parsed
|
|
250
|
+
touching = sorted(
|
|
251
|
+
path for path in _stage_planned_paths(stage)
|
|
252
|
+
if detect_surfaces([path], patterns) - requires
|
|
253
|
+
)
|
|
254
|
+
missing = detect_surfaces(touching, patterns) - requires
|
|
255
|
+
if missing:
|
|
256
|
+
gaps.append({
|
|
257
|
+
"stage": number,
|
|
258
|
+
"surfaces": sorted(missing),
|
|
259
|
+
"paths": touching,
|
|
260
|
+
"requires": sorted(requires),
|
|
261
|
+
})
|
|
262
|
+
return gaps
|
|
263
|
+
|
|
264
|
+
|
|
222
265
|
def _stage_planned_paths(stage: dict) -> list[str]:
|
|
223
266
|
"""stage 의 step 들이 계획한 경로 — 경로 모양인 문자열만.
|
|
224
267
|
|
|
@@ -244,15 +244,18 @@ def append_verified(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
|
244
244
|
|
|
245
245
|
|
|
246
246
|
def append_pr(plan_run_root: Path, *, impl_task_key: str, stages: List[int],
|
|
247
|
-
branch: str, url: str) -> None:
|
|
248
|
-
"""handoff 의 PR 생성/재사용 기록. 같은 branch 의 pr 행이 이미 있으면 멱등.
|
|
247
|
+
branch: str, url: str, base: str = "") -> None:
|
|
248
|
+
"""handoff 의 PR 생성/재사용 기록. 같은 branch 의 pr 행이 이미 있으면 멱등.
|
|
249
|
+
|
|
250
|
+
`base` 는 그 PR 이 올라앉은 브랜치다. stage PR 은 스택이라 base 가 릴리스
|
|
251
|
+
브랜치가 아닐 수 있고, 어느 순서로 머지해야 하는지는 이 값에만 남는다."""
|
|
249
252
|
with consumers_mutex(plan_run_root):
|
|
250
253
|
for row in read_consumers(plan_run_root):
|
|
251
254
|
if row.get("status") == "pr" and row.get("branch") == branch:
|
|
252
255
|
return
|
|
253
256
|
_append_row(plan_run_root, {
|
|
254
257
|
"impl_task_key": impl_task_key, "stages": sorted(stages),
|
|
255
|
-
"status": "pr", "branch": branch, "url": url,
|
|
258
|
+
"status": "pr", "branch": branch, "base": base, "url": url,
|
|
256
259
|
})
|
|
257
260
|
|
|
258
261
|
|
|
@@ -213,8 +213,18 @@ def _compose_ps(project_name: str, cwd: str, include_stopped: bool = False) -> l
|
|
|
213
213
|
return rows
|
|
214
214
|
|
|
215
215
|
|
|
216
|
-
def _docker_run(argv: list[str], cwd: str | None = None) -> str:
|
|
216
|
+
def _docker_run(argv: list[str], cwd: str | None = None, *, check: bool = False) -> str:
|
|
217
|
+
"""docker 명령을 돌려 stdout 을 돌려준다.
|
|
218
|
+
|
|
219
|
+
`check=True` 면 0 이 아닌 종료를 PrepareError 로 올린다. status 가 빈 목록을,
|
|
220
|
+
down 이 성공을 보고하는데 실제로는 docker 가 실패한 경우를 막는다. config 계열
|
|
221
|
+
조회는 실패해도 진행하도록 설계돼 있어 기본값은 그대로 둔다."""
|
|
217
222
|
result = subprocess.run(argv, capture_output=True, text=True, check=False, cwd=cwd)
|
|
223
|
+
if check and result.returncode != 0:
|
|
224
|
+
detail = (result.stderr or result.stdout or "").strip().splitlines()
|
|
225
|
+
raise PrepareError(
|
|
226
|
+
f"`{' '.join(argv[:4])}` 가 exit {result.returncode} 로 실패했습니다: "
|
|
227
|
+
f"{detail[-1] if detail else '출력 없음'}")
|
|
218
228
|
return result.stdout or ""
|
|
219
229
|
|
|
220
230
|
|
|
@@ -222,7 +232,7 @@ def query_containers_by_label(project_name: str) -> list[dict]:
|
|
|
222
232
|
"""라벨 SSOT(spec §11): `okstra.project-name` 으로 컨테이너를 질의한다.
|
|
223
233
|
|
|
224
234
|
registry 가 아니라 docker 라벨이 컨테이너 존재의 기준값이다."""
|
|
225
|
-
out = _docker_run([
|
|
235
|
+
out = _docker_run(check=True, argv=[
|
|
226
236
|
"docker", "ps", "-a", "--filter",
|
|
227
237
|
f"label=okstra.project-name={project_name}",
|
|
228
238
|
"--format", (
|
|
@@ -431,7 +441,7 @@ def provision_container_group(
|
|
|
431
441
|
raise PrepareError(
|
|
432
442
|
f"container up: `up -d` 후 기동된 컨테이너가 없습니다(`docker compose -p "
|
|
433
443
|
f"{project_name} ps -a` 로 확인). compose 정의·활성 profile 을 점검한 뒤 "
|
|
434
|
-
f"`
|
|
444
|
+
f"`{_down_hint(project_root, task_key)}` 로 정리하고 다시 시도하세요.")
|
|
435
445
|
failed = poll_healthcheck(
|
|
436
446
|
project_name=project_name, cwd=str(worktree), services=started,
|
|
437
447
|
healthcheck_timeout_seconds=healthcheck_timeout_seconds,
|
|
@@ -445,8 +455,8 @@ def provision_container_group(
|
|
|
445
455
|
raise PrepareError(
|
|
446
456
|
f"container up: 다음 서비스가 시간 내 기동하지 못했습니다: {', '.join(failed)} "
|
|
447
457
|
f"(timeout={healthcheck_timeout_seconds}s). `docker compose -p "
|
|
448
|
-
f"{project_name} logs` 로 원인을 확인한 뒤
|
|
449
|
-
f"
|
|
458
|
+
f"{project_name} logs` 로 원인을 확인한 뒤 "
|
|
459
|
+
f"`{_down_hint(project_root, task_key)}` 로 정리하세요.")
|
|
450
460
|
# deploy-state 는 실제 기동된 started 만 기록한다 — full services 를 쓰면
|
|
451
461
|
# profiles 로 안 뜬 서비스가 `down --all` 스윕의 대상으로 남는다.
|
|
452
462
|
_write_deploy_state(cp, project_name=project_name, task_key=task_key,
|
|
@@ -528,12 +538,18 @@ def status_container_group(*, project_root, task_key: str) -> dict:
|
|
|
528
538
|
"containers": query_containers_by_label(project_name)}
|
|
529
539
|
|
|
530
540
|
|
|
541
|
+
def _down_hint(project_root, task_key: str) -> str:
|
|
542
|
+
"""실패 메시지가 안내하는 정리 명령. 파서가 요구하는 인자를 모두 싣는다."""
|
|
543
|
+
return f"okstra container down --project-root {project_root} --task-key {task_key}"
|
|
544
|
+
|
|
545
|
+
|
|
531
546
|
def down_container_group(*, project_root, task_key: str = "", all_groups: bool = False) -> dict:
|
|
532
|
-
"""
|
|
547
|
+
"""compose 프로젝트 이름으로 task 의 컨테이너 그룹을 down 한다."""
|
|
533
548
|
from .ids import compose_project_name
|
|
534
549
|
|
|
535
550
|
project_root = Path(project_root)
|
|
536
551
|
results = []
|
|
552
|
+
failures: list[str] = []
|
|
537
553
|
for project_id, group, task_id in _down_targets(project_root, task_key, all_groups):
|
|
538
554
|
project_name = compose_project_name(project_id, group, task_id)
|
|
539
555
|
# `docker rm -f` 는 컨테이너만 지우고 compose 가 만든 네트워크를 누수시킨다.
|
|
@@ -541,9 +557,16 @@ def down_container_group(*, project_root, task_key: str = "", all_groups: bool =
|
|
|
541
557
|
# 단위 컨테이너+네트워크를 회수한다. `-v` 는 의도적으로 빼서 compose 에
|
|
542
558
|
# 선언된 named volume(예: DB 데이터)을 보존한다 — down 한 번에 사용자
|
|
543
559
|
# 영속 데이터를 파괴하지 않는다.
|
|
544
|
-
|
|
545
|
-
|
|
560
|
+
try:
|
|
561
|
+
_docker_run(["docker", "compose", "-p", project_name, "down",
|
|
562
|
+
"--remove-orphans"], check=True)
|
|
563
|
+
except PrepareError as exc:
|
|
564
|
+
failures.append(f"{project_name}: {exc}")
|
|
565
|
+
continue
|
|
546
566
|
results.append({"projectName": project_name})
|
|
567
|
+
if failures:
|
|
568
|
+
# 나머지 대상은 끝까지 내리고, 실패한 것만 모아 알린다.
|
|
569
|
+
raise PrepareError("container down 실패 — " + "; ".join(failures))
|
|
547
570
|
return {"downed": results}
|
|
548
571
|
|
|
549
572
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The estimator answers a narrow operational question: how much file/context
|
|
4
4
|
surface does the current task bundle ask the lead, analysis workers, and
|
|
5
|
-
report-writer to absorb? It
|
|
5
|
+
report-writer to absorb? It writes no report or run artifact; resolving the task
|
|
6
|
+
may record a finished implementation outcome (see `find_task_root`).
|
|
6
7
|
"""
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
@@ -38,11 +39,9 @@ HOT_PATH_LEAD_RESOURCES = (
|
|
|
38
39
|
"convergence",
|
|
39
40
|
"report-writer",
|
|
40
41
|
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"translator-worker.md",
|
|
45
|
-
)
|
|
42
|
+
# 모든 디스패치 프롬프트 앞머리에 실리는 계약. 종전에는 `~/.claude/agents` 의
|
|
43
|
+
# 에이전트 정의를 셌는데, 그 정의는 더 이상 설치되지 않는다(ADR-0017).
|
|
44
|
+
HOT_PATH_AGENT_CONTRACTS = ("agents/common.json",)
|
|
46
45
|
TIMESTAMPED_ARTIFACT_RE = re.compile(r"\d{4}-\d{2}-\d{2}[_T]\d{2}-\d{2}-\d{2}")
|
|
47
46
|
|
|
48
47
|
|
|
@@ -136,7 +135,7 @@ def _duty_contract_files(
|
|
|
136
135
|
if not isinstance(duty_root_value, str) or not duty_root_value:
|
|
137
136
|
return []
|
|
138
137
|
duty_root = project_root / duty_root_value
|
|
139
|
-
files = [duty_root / "common.
|
|
138
|
+
files = [duty_root / "common.json", duty_root / f"{audience}.json"]
|
|
140
139
|
return [path for path in files if path.is_file()]
|
|
141
140
|
|
|
142
141
|
|
|
@@ -239,19 +238,15 @@ def _skill_assets_metric() -> dict:
|
|
|
239
238
|
instruction footprint and are the prompt-diet ranking input."""
|
|
240
239
|
entries = []
|
|
241
240
|
okstra_home = _okstra_asset_home()
|
|
242
|
-
claude_home = Path.home() / ".claude"
|
|
243
241
|
for name in HOT_PATH_LEAD_RESOURCES:
|
|
244
242
|
path = _installed_or_dev(
|
|
245
243
|
okstra_home / "prompts" / "lead" / f"{name}.md",
|
|
246
244
|
f"prompts/lead/{name}.md",
|
|
247
245
|
)
|
|
248
246
|
entries.append((f"resource:{name}", path))
|
|
249
|
-
for
|
|
250
|
-
path = _installed_or_dev(
|
|
251
|
-
|
|
252
|
-
f"runtime/agents/workers/{fname}",
|
|
253
|
-
)
|
|
254
|
-
entries.append((f"agent:{fname}", path))
|
|
247
|
+
for relative in HOT_PATH_AGENT_CONTRACTS:
|
|
248
|
+
path = _installed_or_dev(okstra_home / relative, relative)
|
|
249
|
+
entries.append((f"contract:{relative}", path))
|
|
255
250
|
|
|
256
251
|
files = []
|
|
257
252
|
for label, path in entries:
|
|
@@ -512,7 +507,8 @@ _CLI_EPILOG = r"""Usage:
|
|
|
512
507
|
|
|
513
508
|
Output: JSON with task file counts, current-run counts, lead Phase 1 read
|
|
514
509
|
surface, analysis-worker initial read surface, and report-writer synthesis
|
|
515
|
-
surface.
|
|
510
|
+
surface. Resolving a task may record a finished implementation outcome in its task
|
|
511
|
+
manifest, the task catalog, and consumers.jsonl; nothing else is written.
|
|
516
512
|
"""
|
|
517
513
|
|
|
518
514
|
|