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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""manager 상태를 한 장짜리 HTML 로 그린다.
|
|
2
|
+
|
|
3
|
+
manager 가 이미 저장한 파일(projects / task manifest / children / snapshots /
|
|
4
|
+
directives / events)만 읽는다. 하위 프로젝트는 읽지 않는다 — 최신 상태가 필요하면
|
|
5
|
+
먼저 `okstra manager task sync` 를 돌리고, 페이지는 각 task 의 마지막 sync 시각을
|
|
6
|
+
보여 준다.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import html
|
|
11
|
+
import re
|
|
12
|
+
import shlex
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from okstra_ctl.jsonl import read_jsonl
|
|
17
|
+
|
|
18
|
+
from .manager_paths import directives_jsonl_path, events_jsonl_path, view_html_path
|
|
19
|
+
from .manager_store import ManagerError, list_projects, list_tasks, load_manager
|
|
20
|
+
from .manager_sync import status_task
|
|
21
|
+
from .paths import find_asset_root
|
|
22
|
+
|
|
23
|
+
TEMPLATE_RELATIVE = ("templates", "manager", "view.template.html")
|
|
24
|
+
_MARKER = re.compile(r"\{\{([A-Z]+)\}\}")
|
|
25
|
+
BUCKETS = ("planned", "missing", "running", "done", "blocked")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _now_iso() -> str:
|
|
29
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _e(value: object) -> str:
|
|
33
|
+
return html.escape("" if value is None else str(value))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _chip(bucket: str) -> str:
|
|
37
|
+
return f'<span class="chip {_e(bucket)}">{_e(bucket)}</span>'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _table(headers: tuple[str, ...], rows: list[list[str]], empty: str) -> str:
|
|
41
|
+
if not rows:
|
|
42
|
+
return f'<p class="empty">{_e(empty)}</p>'
|
|
43
|
+
head = "".join(f"<th>{_e(header)}</th>" for header in headers)
|
|
44
|
+
body = "".join("<tr>" + "".join(f"<td>{cell}</td>" for cell in row) + "</tr>" for row in rows)
|
|
45
|
+
return f'<div class="scroll"><table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table></div>'
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _file_link(project_root: str, path: str) -> str:
|
|
49
|
+
"""하위 프로젝트 기준 상대 경로를 절대 file 링크로 바꾼다. 빈 값은 `-`."""
|
|
50
|
+
if not path:
|
|
51
|
+
return "-"
|
|
52
|
+
target = Path(path)
|
|
53
|
+
if not target.is_absolute() and project_root:
|
|
54
|
+
target = Path(project_root) / target
|
|
55
|
+
if not target.is_absolute():
|
|
56
|
+
return f"<code>{_e(path)}</code>"
|
|
57
|
+
return f'<a href="{_e(target.as_uri())}"><code>{_e(path)}</code></a>'
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _command(*args: str) -> str:
|
|
61
|
+
return shlex.join(["okstra", "manager", *args])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _summary_tiles(totals: dict[str, int]) -> str:
|
|
65
|
+
tiles = "".join(
|
|
66
|
+
f'<div class="tile"><div class="count">{totals[bucket]}</div>'
|
|
67
|
+
f'<div class="label">{_e(bucket)}</div></div>'
|
|
68
|
+
for bucket in BUCKETS
|
|
69
|
+
)
|
|
70
|
+
return f'<div class="tiles">{tiles}</div>'
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _projects_table(projects: list[dict]) -> str:
|
|
74
|
+
rows = [
|
|
75
|
+
[
|
|
76
|
+
f"<code class=\"id\">{_e(project.get('projectId'))}</code>",
|
|
77
|
+
f"<code>{_e(project.get('projectRoot'))}</code>",
|
|
78
|
+
_e(project.get("role") or "-"),
|
|
79
|
+
_e(", ".join(project.get("tags") or []) or "-"),
|
|
80
|
+
_e(project.get("linkedAt") or "-"),
|
|
81
|
+
]
|
|
82
|
+
for project in projects
|
|
83
|
+
]
|
|
84
|
+
return _table(("Project", "Root", "Role", "Tags", "Linked at"), rows, "No projects registered.")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _child_rows(children: list[dict], roots: dict[str, str]) -> list[list[str]]:
|
|
88
|
+
rows = []
|
|
89
|
+
for child in children:
|
|
90
|
+
launch = child.get("launch") if isinstance(child.get("launch"), dict) else {}
|
|
91
|
+
phase = child.get("currentPhase") or ""
|
|
92
|
+
phase_state = child.get("currentPhaseState") or ""
|
|
93
|
+
assignment = " — ".join(part for part in (child.get("role"), child.get("assignment")) if part)
|
|
94
|
+
# `task split` 하위 태스크는 assignment 대신 범위 목록을 가진다.
|
|
95
|
+
assignment = assignment or "; ".join(child.get("scope") or [])
|
|
96
|
+
rows.append(
|
|
97
|
+
[
|
|
98
|
+
f"<code>{_e(child.get('taskKey'))}</code>"
|
|
99
|
+
+ ("" if child.get("projectLinked", True) else " " + _chip("unlinked")),
|
|
100
|
+
_e(child.get("ticketId") or "-"),
|
|
101
|
+
_chip(str(child.get("summaryBucket") or "planned")),
|
|
102
|
+
_e(child.get("workStatus") or "-"),
|
|
103
|
+
_e(f"{phase} ({phase_state})" if phase and phase_state else phase or "-"),
|
|
104
|
+
_e(child.get("finalVerdict") or "-"),
|
|
105
|
+
_e(launch.get("status") or "-"),
|
|
106
|
+
f'<span class="wrap">{_e(assignment or "-")}</span>',
|
|
107
|
+
_e(child.get("error") or "-"),
|
|
108
|
+
_file_link("", str(child.get("briefPath") or "")),
|
|
109
|
+
_file_link(
|
|
110
|
+
roots.get(str(child.get("projectId") or ""), ""),
|
|
111
|
+
str(child.get("latestReportRecordPath") or ""),
|
|
112
|
+
),
|
|
113
|
+
]
|
|
114
|
+
)
|
|
115
|
+
return rows
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _directive_list(directives: list[dict]) -> str:
|
|
119
|
+
if not directives:
|
|
120
|
+
return '<p class="empty">No directives.</p>'
|
|
121
|
+
items = "".join(
|
|
122
|
+
f"<li><strong>{_e(row.get('scope'))}"
|
|
123
|
+
f"{' · ' + _e(row.get('projectId')) if row.get('projectId') else ''}</strong>: "
|
|
124
|
+
f"{_e(row.get('body'))} <span class=\"meta\">{_e(row.get('createdAt'))}</span></li>"
|
|
125
|
+
for row in directives
|
|
126
|
+
)
|
|
127
|
+
return f'<ul class="plain">{items}</ul>'
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _event_list(events: list[dict]) -> str:
|
|
131
|
+
if not events:
|
|
132
|
+
return '<p class="empty">No events.</p>'
|
|
133
|
+
items = "".join(
|
|
134
|
+
f"<li><code>{_e(row.get('createdAt'))}</code> {_e(row.get('event'))}"
|
|
135
|
+
f"{' · ' + _e(row.get('taskKey')) if row.get('taskKey') else ''}</li>"
|
|
136
|
+
for row in events
|
|
137
|
+
)
|
|
138
|
+
return f'<ul class="plain">{items}</ul>'
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _task_panel(home: Path, manager_id: str, task: dict, status: dict, roots: dict[str, str]) -> str:
|
|
142
|
+
group, task_id = task["taskGroup"], task["taskId"]
|
|
143
|
+
synced_at = status["lastSnapshot"].get("syncedAt") or ""
|
|
144
|
+
sync_command = _command(
|
|
145
|
+
"task", "sync", "--manager-id", manager_id, "--task-group", group, "--task-id", task_id
|
|
146
|
+
)
|
|
147
|
+
counts = "".join(
|
|
148
|
+
f"{_chip(bucket)} {status['summary'][bucket]}"
|
|
149
|
+
for bucket in BUCKETS
|
|
150
|
+
if status["summary"][bucket]
|
|
151
|
+
)
|
|
152
|
+
head = (
|
|
153
|
+
f'<div class="task-head"><h3><code>{_e(group)} / {_e(task_id)}</code></h3>'
|
|
154
|
+
f'<div class="counts">{counts or "<span class=empty>no children</span>"}</div></div>'
|
|
155
|
+
)
|
|
156
|
+
meta = (
|
|
157
|
+
f'<div class="meta">Objective: {_e(task["objective"] or "-")} · '
|
|
158
|
+
f'Common brief: {_e(status["manifest"].get("commonBriefPath") or "-")} · '
|
|
159
|
+
f'Progress mode: {_e(task["progressMode"] or "-")}</div>'
|
|
160
|
+
f'<p class="hint">Last sync: {_e(synced_at or "never")} — refresh with '
|
|
161
|
+
f"<code>{_e(sync_command)}</code>, then re-run the view command.</p>"
|
|
162
|
+
)
|
|
163
|
+
children = _table(
|
|
164
|
+
("Child", "Ticket", "Summary", "Work status", "Phase", "Verdict", "Launch", "Assignment", "Error",
|
|
165
|
+
"Brief", "Latest report"),
|
|
166
|
+
_child_rows(status["children"], roots),
|
|
167
|
+
"No child tasks.",
|
|
168
|
+
)
|
|
169
|
+
directives = read_jsonl(directives_jsonl_path(home, manager_id, group, task_id))
|
|
170
|
+
events = read_jsonl(events_jsonl_path(home, manager_id, group, task_id))
|
|
171
|
+
details = (
|
|
172
|
+
f"<details><summary>Directives ({len(directives)})</summary>{_directive_list(directives)}</details>"
|
|
173
|
+
f"<details><summary>Events ({len(events)})</summary>{_event_list(events)}</details>"
|
|
174
|
+
)
|
|
175
|
+
return f'<div class="panel">{head}{meta}{children}{details}</div>'
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def render_view(home: Path, manager_id: str, *, now: str | None = None) -> str:
|
|
179
|
+
"""manager 한 개의 HTML 페이지 본문을 돌려준다."""
|
|
180
|
+
manager = load_manager(home, manager_id)
|
|
181
|
+
root = find_asset_root(TEMPLATE_RELATIVE)
|
|
182
|
+
if root is None:
|
|
183
|
+
raise ManagerError("manager view template not found: " + "/".join(TEMPLATE_RELATIVE))
|
|
184
|
+
template = root.joinpath(*TEMPLATE_RELATIVE).read_text(encoding="utf-8")
|
|
185
|
+
projects = list_projects(home, manager_id)
|
|
186
|
+
roots = {str(project.get("projectId")): str(project.get("projectRoot") or "") for project in projects}
|
|
187
|
+
tasks = list_tasks(home, manager_id)
|
|
188
|
+
totals = dict.fromkeys(BUCKETS, 0)
|
|
189
|
+
panels = []
|
|
190
|
+
for task in tasks:
|
|
191
|
+
status = status_task(home, manager_id, task["taskGroup"], task["taskId"])
|
|
192
|
+
for bucket in BUCKETS:
|
|
193
|
+
totals[bucket] += status["summary"][bucket]
|
|
194
|
+
panels.append(_task_panel(home, manager_id, task, status, roots))
|
|
195
|
+
meta = (
|
|
196
|
+
f"Created {_e(manager.get('createdAt') or '-')} · Generated {_e(now or _now_iso())} · "
|
|
197
|
+
f"{len(projects)} projects · {len(tasks)} manager tasks"
|
|
198
|
+
)
|
|
199
|
+
replacements = {
|
|
200
|
+
"TITLE": _e(f"Okstra manager · {manager.get('managerId') or manager_id}"),
|
|
201
|
+
"META": meta,
|
|
202
|
+
"SUMMARY": _summary_tiles(totals),
|
|
203
|
+
"PROJECTS": _projects_table(projects),
|
|
204
|
+
"TASKS": "".join(panels) or '<p class="empty">No manager tasks.</p>',
|
|
205
|
+
}
|
|
206
|
+
# 한 번에 치환한다. 차례로 바꾸면 앞에서 넣은 사용자 값 속 `{{TASKS}}` 같은
|
|
207
|
+
# 문자열이 뒤 치환에 다시 걸린다.
|
|
208
|
+
return _MARKER.sub(lambda match: replacements[match.group(1)], template)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def write_view(home: Path, manager_id: str, *, now: str | None = None) -> dict:
|
|
212
|
+
"""페이지를 `managers/<id>/view/index.html` 에 쓰고 경로를 돌려준다."""
|
|
213
|
+
page = render_view(home, manager_id, now=now)
|
|
214
|
+
path = view_html_path(home, manager_id)
|
|
215
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
216
|
+
path.write_text(page, encoding="utf-8")
|
|
217
|
+
return {"managerId": manager_id, "viewPath": str(path), "viewUrl": path.as_uri()}
|
|
@@ -73,6 +73,25 @@ def _codex_home() -> Path:
|
|
|
73
73
|
return Path.home() / ".codex"
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
@lru_cache(maxsize=1)
|
|
77
|
+
def codex_cli_version(codex_bin: str = "codex") -> str:
|
|
78
|
+
"""디스패치에 쓸 codex CLI 의 버전, 알 수 없으면 "".
|
|
79
|
+
|
|
80
|
+
`codex --version` 은 `codex-cli 0.155.1` 처럼 마지막 마디에 버전을 싣는다.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
proc = subprocess.run(
|
|
84
|
+
[codex_bin, "--version"],
|
|
85
|
+
capture_output=True, text=True, timeout=20,
|
|
86
|
+
)
|
|
87
|
+
except (FileNotFoundError, OSError, subprocess.SubprocessError):
|
|
88
|
+
return ""
|
|
89
|
+
if proc.returncode != 0:
|
|
90
|
+
return ""
|
|
91
|
+
parts = proc.stdout.split()
|
|
92
|
+
return parts[-1].strip() if parts else ""
|
|
93
|
+
|
|
94
|
+
|
|
76
95
|
def codex_availability(codex_home: Path | None = None) -> tuple[str, ...]:
|
|
77
96
|
"""Model slugs the codex CLI last fetched from the provider, or ().
|
|
78
97
|
|
|
@@ -100,6 +119,17 @@ def codex_availability(codex_home: Path | None = None) -> tuple[str, ...]:
|
|
|
100
119
|
models = payload.get("models") if isinstance(payload, dict) else None
|
|
101
120
|
if not isinstance(models, list):
|
|
102
121
|
return ()
|
|
122
|
+
# 이 캐시는 codex 홈 하나를 쓰는 모든 클라이언트가 공유한다 — CLI 뿐 아니라
|
|
123
|
+
# ChatGPT 데스크톱 앱과 VS Code 확장도 자기 번들 버전으로 덮어쓴다. 그런데
|
|
124
|
+
# 카탈로그는 클라이언트 버전마다 다르다: 실측 2026-09-23, 같은 계정에서
|
|
125
|
+
# 0.155.1 은 gpt-6-sol·gpt-6-luna 를 내놓고 0.154.0·0.153.4 는 내놓지 않으며,
|
|
126
|
+
# 세 버전이 10분 사이 번갈아 이 파일을 썼다. 다른 클라이언트가 쓴 목록은
|
|
127
|
+
# okstra 가 실행할 CLI 의 카탈로그에 대한 근거가 아니므로 판정을 끈다 —
|
|
128
|
+
# 탐색 실패와 같은 처리이고, 정말 없는 모델이면 CLI 자신이 거절한다.
|
|
129
|
+
writer = payload.get("client_version")
|
|
130
|
+
running = codex_cli_version()
|
|
131
|
+
if running and isinstance(writer, str) and writer.strip() and writer.strip() != running:
|
|
132
|
+
return ()
|
|
103
133
|
out: list[str] = []
|
|
104
134
|
for entry in models:
|
|
105
135
|
slug = entry.get("slug") if isinstance(entry, dict) else None
|
|
@@ -10,6 +10,8 @@ from __future__ import annotations
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Mapping
|
|
12
12
|
|
|
13
|
+
from okstra_project.phase_pointer import promote
|
|
14
|
+
|
|
13
15
|
from ..fixed_text import line as _line, scalar as _value
|
|
14
16
|
from ..json_boundary import JsonBoundaryError
|
|
15
17
|
|
|
@@ -19,6 +21,17 @@ def _mapping(payload: Mapping[str, Any], key: str) -> Mapping[str, Any]:
|
|
|
19
21
|
return value if isinstance(value, Mapping) else {}
|
|
20
22
|
|
|
21
23
|
|
|
24
|
+
def next_phase_pointer(payload: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
25
|
+
"""`nextRecommendedPhase` 를 포인터로 읽는다. 구형 문자열도 승격한다.
|
|
26
|
+
|
|
27
|
+
`_mapping` 으로 읽으면 구형 문자열 포인터가 빈 매핑이 되어 `Next phase: -`
|
|
28
|
+
로 사라진다(ADR-0004 의 승격 예외가 이 읽기 경로에는 없었다). 값이 아예
|
|
29
|
+
없으면 빈 매핑 그대로 둔다 — 없는 포인터를 `pending` 으로 지어내지 않는다.
|
|
30
|
+
"""
|
|
31
|
+
value = payload.get("nextRecommendedPhase")
|
|
32
|
+
return {} if value is None else promote(value)
|
|
33
|
+
|
|
34
|
+
|
|
22
35
|
def _project_policy_lines(project: Mapping[str, Any]) -> str:
|
|
23
36
|
packs = project.get("reviewRulePacks")
|
|
24
37
|
pack_lines = (
|
|
@@ -84,7 +97,7 @@ def _worker_prompt_lines(run: Mapping[str, Any]) -> str:
|
|
|
84
97
|
|
|
85
98
|
|
|
86
99
|
def _next_phase_lines(workflow: Mapping[str, Any]) -> str:
|
|
87
|
-
pointer =
|
|
100
|
+
pointer = next_phase_pointer(workflow)
|
|
88
101
|
return (
|
|
89
102
|
_line("Next phase", pointer.get("phase"))
|
|
90
103
|
+ _line("Next phase status", pointer.get("status"))
|
|
@@ -29,6 +29,7 @@ from .lines import (
|
|
|
29
29
|
_mapping,
|
|
30
30
|
_next_phase_lines,
|
|
31
31
|
_project_policy_lines,
|
|
32
|
+
next_phase_pointer,
|
|
32
33
|
_provider_models,
|
|
33
34
|
_status_overview_text,
|
|
34
35
|
_worker_assignment_lines,
|
|
@@ -84,7 +85,7 @@ def _overview_rows(
|
|
|
84
85
|
latest_run = _latest_timeline_run(project_root, manifest_path)
|
|
85
86
|
_, group, _ = parse_task_key(task_key)
|
|
86
87
|
workflow = _mapping(manifest, "workflow")
|
|
87
|
-
next_phase =
|
|
88
|
+
next_phase = next_phase_pointer(workflow)
|
|
88
89
|
# task 단위 현재 사실은 task-manifest 가 권위다(validate-run 이 run 종료
|
|
89
90
|
# 시 거기에 쓴다). timeline 의 마지막 run 은 task-manifest 에 그 필드가
|
|
90
91
|
# 없을 때의 폴백이고, 그 값도 run-manifest 로 덮어쓴 현재 사실이다.
|
|
@@ -372,7 +373,7 @@ def render_recap_input(project_root: Path, task_ref: str) -> str:
|
|
|
372
373
|
for transition in transitions if isinstance(transitions, list) else []:
|
|
373
374
|
if not isinstance(transition, Mapping):
|
|
374
375
|
continue
|
|
375
|
-
pointer =
|
|
376
|
+
pointer = next_phase_pointer(transition)
|
|
376
377
|
blocks.extend((
|
|
377
378
|
"\n## Transition\n\n",
|
|
378
379
|
_line("Run timestamp", transition.get("runTimestamp")),
|
|
@@ -420,7 +421,7 @@ def render_group_recap_input(project_root: Path, task_group: str) -> str:
|
|
|
420
421
|
for task in tasks if isinstance(tasks, list) else []:
|
|
421
422
|
if not isinstance(task, Mapping):
|
|
422
423
|
continue
|
|
423
|
-
pointer =
|
|
424
|
+
pointer = next_phase_pointer(task)
|
|
424
425
|
memory = _mapping(task, "memory")
|
|
425
426
|
blocks.extend((
|
|
426
427
|
"\n## Task\n\n",
|
|
@@ -35,7 +35,7 @@ IMPLEMENTATION_ROLES = frozenset({"executor", "verifier"})
|
|
|
35
35
|
# Legacy role-keyed compatibility defaults. Canonical assignments resolve a
|
|
36
36
|
# provider first and then use ProviderSpec.default_models for that role.
|
|
37
37
|
ROLE_DEFAULTS = {
|
|
38
|
-
"lead": "opus", "claude": "opus", "codex": "gpt-
|
|
38
|
+
"lead": "opus", "claude": "opus", "codex": "gpt-6-sol",
|
|
39
39
|
"antigravity": "gemini-3.1-pro", "report-writer": "sonnet",
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -235,8 +235,9 @@ def _from_option_selection(report_data: Mapping[str, Any]) -> dict[str, str]:
|
|
|
235
235
|
# release-handoff 는 `routingRecommendation`.
|
|
236
236
|
return make(status=STATUS_BLOCKED, rationale=_selection_guidance(selection))
|
|
237
237
|
if routing == "pending-direction-selection":
|
|
238
|
-
# 이 상태는 **성공**이다 — 비교가 끝났고, 방향은
|
|
239
|
-
#
|
|
238
|
+
# 이 상태는 **성공**이다 — 비교가 끝났고, 방향은 HTML 열람본의
|
|
239
|
+
# `DIRECTION SELECTION` Export 가 고르며 계획 위저드의
|
|
240
|
+
# `selected_direction_pick` 은 그 답이 딸린 리포트를 고른다. 그러니 다음 phase 는 지금 바로
|
|
240
241
|
# 시작할 수 있는 `implementation-planning` 이고 status 는 `ready` 다.
|
|
241
242
|
# 종전에는 phase 없는 `pending` 이었다: task 선택 화면이 `next: --
|
|
242
243
|
# (pending)` 을 찍어 목적지를 지웠고, task-type 화면은 추천 없이 방금
|
|
@@ -276,11 +277,20 @@ def _direction_selection_reason(selection: Mapping[str, Any]) -> str:
|
|
|
276
277
|
recommended = _text(selection.get("recommendedOptionId"))
|
|
277
278
|
head = f"비교가 끝났습니다. 후보 {listed} 중 하나를 방향으로 고르세요"
|
|
278
279
|
head += f" (권장 `{recommended}`)." if recommended else "."
|
|
280
|
+
# 위저드의 `selected_direction_pick` 은 후보가 아니라 **리포트**를 고른다
|
|
281
|
+
# (`wizard/steps_plan.py` `_build_selected_direction_pick`). 방향은 그 리포트
|
|
282
|
+
# 옆 `user-responses/` 의 `DIRECTION SELECTION` 사이드카가 정한다
|
|
283
|
+
# (`implementation_direction.py` `_comparison_selection`). 종전 문장은
|
|
284
|
+
# "위저드에 후보가 선택지로 나온다" 고 해서 사이드카 없이 계획을 열게 했다.
|
|
279
285
|
return (
|
|
280
|
-
f"{head} 고르는 자리는
|
|
281
|
-
"
|
|
282
|
-
"
|
|
283
|
-
"
|
|
286
|
+
f"{head} 고르는 자리는 이 리포트의 HTML 열람본입니다 — 방향 선택 칸에서 "
|
|
287
|
+
"후보를 고르고 Export 한 파일을 "
|
|
288
|
+
"`runs/implementation-option-selection/user-responses/"
|
|
289
|
+
"user-response-implementation-option-selection-<이 리포트 번호>.md` 로 "
|
|
290
|
+
"저장하세요. 그다음 `/okstra-run` 으로 `implementation-planning` 을 "
|
|
291
|
+
"시작하고 위저드의 방향 보고서 단계에서 이 리포트를 고르면 저장한 방향이 "
|
|
292
|
+
"계획에 전달됩니다. 이 phase 를 다시 돌리지 마세요: 비교는 이미 끝났고, "
|
|
293
|
+
"재실행은 같은 비교를 처음부터 다시 합니다."
|
|
284
294
|
)
|
|
285
295
|
|
|
286
296
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""okstra-run 밖에서 도는 Okstra 소유 LLM 작업의 준비.
|
|
2
|
+
|
|
3
|
+
작업 계약(`agents/operations/<id>.json`)이 직무와 워커 수를 소유하고, 역할은 그
|
|
4
|
+
직무의 `roleId` 에서 파생된다. 기술 문서는 작업 이름만 전달한다 — 역할·공급자·
|
|
5
|
+
모델을 기술이 조립하면 같은 역할이 호출 표면마다 다른 규칙으로 배정된다
|
|
6
|
+
(ADR-0017).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .contract_graph import (
|
|
15
|
+
ContractGraphError,
|
|
16
|
+
ContractGraphRoot,
|
|
17
|
+
ContractGraphValidator,
|
|
18
|
+
ContractSelection,
|
|
19
|
+
)
|
|
20
|
+
from .model_pool import ModelPool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OperationPreparationError(ValueError):
|
|
24
|
+
"""작업을 디스패치 전에 준비할 수 없다. 메시지가 그 자리를 이름한다."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class OperationSlot:
|
|
29
|
+
"""이 작업이 열 워커 한 칸. 모델은 칸마다 다르다."""
|
|
30
|
+
|
|
31
|
+
operation_id: str
|
|
32
|
+
duty_id: str
|
|
33
|
+
role_id: str
|
|
34
|
+
ordinal: int
|
|
35
|
+
model_ref: str
|
|
36
|
+
provider_id: str
|
|
37
|
+
display_name: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_operation_slots(
|
|
41
|
+
operation_id: str,
|
|
42
|
+
*,
|
|
43
|
+
contract_root: Path,
|
|
44
|
+
pool: ModelPool,
|
|
45
|
+
) -> Sequence[OperationSlot]:
|
|
46
|
+
"""작업 계약이 요구하는 칸과 각 칸의 모델.
|
|
47
|
+
|
|
48
|
+
모델은 런과 같은 기본값 사슬에서 고른다. 서로 다른 모델 참조를 요구한
|
|
49
|
+
수만큼 채우지 못하면 여기서 멈춘다 — 같은 모델을 반복하거나 워커 수를
|
|
50
|
+
줄이면 그 작업이 요구한 교차검증이 조용히 사라진다.
|
|
51
|
+
"""
|
|
52
|
+
graph_root = ContractGraphRoot.from_base(contract_root)
|
|
53
|
+
try:
|
|
54
|
+
graph = ContractGraphValidator().validate(
|
|
55
|
+
graph_root, selected=ContractSelection(operation_id=operation_id)
|
|
56
|
+
)
|
|
57
|
+
except ContractGraphError as exc:
|
|
58
|
+
raise OperationPreparationError(str(exc)) from exc
|
|
59
|
+
operation = graph.operations.get(operation_id)
|
|
60
|
+
if operation is None:
|
|
61
|
+
known = ", ".join(sorted(graph.operations)) or "(none)"
|
|
62
|
+
raise OperationPreparationError(
|
|
63
|
+
f"unknown operation: {operation_id} (known: {known})"
|
|
64
|
+
)
|
|
65
|
+
duty_id = str(operation.payload["dutyId"])
|
|
66
|
+
count = int(operation.payload["count"])
|
|
67
|
+
role_id = graph.duties[duty_id].role_id
|
|
68
|
+
|
|
69
|
+
candidates = pool.default_candidates(role_id)
|
|
70
|
+
if len(candidates) < count:
|
|
71
|
+
raise OperationPreparationError(
|
|
72
|
+
f"operation {operation_id!r} requires {count} distinct models for role "
|
|
73
|
+
f"{role_id!r}, available {len(candidates)}"
|
|
74
|
+
)
|
|
75
|
+
slots = []
|
|
76
|
+
for ordinal, model in enumerate(candidates[:count], start=1):
|
|
77
|
+
slots.append(OperationSlot(
|
|
78
|
+
operation_id=operation_id,
|
|
79
|
+
duty_id=duty_id,
|
|
80
|
+
role_id=role_id,
|
|
81
|
+
ordinal=ordinal,
|
|
82
|
+
model_ref=str(model.model_ref),
|
|
83
|
+
provider_id=model.provider_id,
|
|
84
|
+
display_name=model.display_name,
|
|
85
|
+
))
|
|
86
|
+
return tuple(slots)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""구현 방향 비교 자료 — 선택 리포트 기록 하나를 채팅용 사실 묶음으로 투영한다.
|
|
2
|
+
|
|
3
|
+
사용자가 방향을 고르기 전에 비교 자료를 청하면 리드는 점수표·투표표·충족
|
|
4
|
+
요약을 보여 준다. 그 숫자는 전부 `implementationOptionSelection` 기록에 있고
|
|
5
|
+
`weightedScore` 는 발행 시점에 재계산 값과 대조된다
|
|
6
|
+
(`implementation_options.py` `_validate_option_scores`). 리드가 JSON 을 손으로
|
|
7
|
+
읽고 가중 점수를 다시 계산하던 자리를 이 명령이 대신한다. 서술(도입 문장,
|
|
8
|
+
방향별 요지)은 리드가 `narrative` 와 각 방향의 근거 필드로 쓴다.
|
|
9
|
+
|
|
10
|
+
읽기 전용이다. 어떤 파일도 쓰지 않는다.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from collections.abc import Mapping, Sequence
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from okstra_ctl import next_phase
|
|
22
|
+
from okstra_ctl.final_report_paths import final_report_data_path
|
|
23
|
+
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OptionComparisonError(ValueError):
|
|
27
|
+
"""비교 자료를 만들 수 없는 기록."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _rows(value: object) -> list[Mapping[str, Any]]:
|
|
31
|
+
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
|
|
32
|
+
return []
|
|
33
|
+
return [row for row in value if isinstance(row, Mapping)]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _text(value: object) -> str:
|
|
37
|
+
return value.strip() if isinstance(value, str) else ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _count(value: object) -> int:
|
|
41
|
+
return len(value) if isinstance(value, Sequence) and not isinstance(value, str) else 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _option(
|
|
45
|
+
option: Mapping[str, Any], recommended_id: str
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
coverage = option.get("coverageSummary")
|
|
48
|
+
coverage = coverage if isinstance(coverage, Mapping) else {}
|
|
49
|
+
scores = _rows(option.get("criterionScores"))
|
|
50
|
+
return {
|
|
51
|
+
"id": _text(option.get("id")),
|
|
52
|
+
"name": _text(option.get("name")),
|
|
53
|
+
"recommended": _text(option.get("id")) == recommended_id,
|
|
54
|
+
"goal": _text(option.get("goal")),
|
|
55
|
+
"coreMechanism": _text(option.get("coreMechanism")),
|
|
56
|
+
# 발행 검증이 재계산 값과 같음을 보장한 기록값이다. 다시 계산하지 않는다.
|
|
57
|
+
"weightedScore": option.get("weightedScore"),
|
|
58
|
+
"scores": {_text(row.get("criterion")): row.get("score") for row in scores},
|
|
59
|
+
"scoreRationales": {
|
|
60
|
+
_text(row.get("criterion")): _text(row.get("rationale")) for row in scores
|
|
61
|
+
},
|
|
62
|
+
"coverage": {
|
|
63
|
+
"verdict": _text(coverage.get("coverageVerdict")),
|
|
64
|
+
"coveredCount": coverage.get("coveredCount"),
|
|
65
|
+
"totalCount": coverage.get("totalCount"),
|
|
66
|
+
"requirementIds": [
|
|
67
|
+
_text(row.get("requirementId"))
|
|
68
|
+
for row in _rows(option.get("requirementCoverage"))
|
|
69
|
+
if _text(row.get("requirementId"))
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
"safetyBlockers": _count(option.get("safetyBlockers")),
|
|
73
|
+
"unresolvedFeasibilityFacts": _count(option.get("unresolvedFeasibilityFacts")),
|
|
74
|
+
"votes": {
|
|
75
|
+
_text(vote.get("worker")): {
|
|
76
|
+
"verdict": _text(vote.get("verdict")),
|
|
77
|
+
"rationale": _text(vote.get("rationale")),
|
|
78
|
+
"counterevidence": _text(vote.get("counterevidence")),
|
|
79
|
+
}
|
|
80
|
+
for vote in _rows(option.get("feasibilityVotes"))
|
|
81
|
+
if _text(vote.get("worker"))
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_comparison(report_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
87
|
+
"""선택 리포트 기록 → 비교 자료 사실 묶음."""
|
|
88
|
+
selection = report_data.get("implementationOptionSelection")
|
|
89
|
+
if not isinstance(selection, Mapping):
|
|
90
|
+
raise OptionComparisonError(
|
|
91
|
+
"report has no implementationOptionSelection block; "
|
|
92
|
+
"pass an implementation-option-selection report record"
|
|
93
|
+
)
|
|
94
|
+
recommended_id = _text(selection.get("recommendedOptionId"))
|
|
95
|
+
options = [
|
|
96
|
+
_option(row, recommended_id) for row in _rows(selection.get("rankedOptions"))
|
|
97
|
+
]
|
|
98
|
+
if not options:
|
|
99
|
+
raise OptionComparisonError("report ranks no direction; there is nothing to compare")
|
|
100
|
+
# 투표 열 순서는 처음 등장한 순서다 — 방향마다 같은 분석자 집합이 투표한다.
|
|
101
|
+
workers: list[str] = []
|
|
102
|
+
for option in options:
|
|
103
|
+
for worker in option["votes"]:
|
|
104
|
+
if worker not in workers:
|
|
105
|
+
workers.append(worker)
|
|
106
|
+
narrative = selection.get("userNarrative")
|
|
107
|
+
narrative = narrative if isinstance(narrative, Mapping) else {}
|
|
108
|
+
return {
|
|
109
|
+
"mode": _text(selection.get("mode")),
|
|
110
|
+
"routing": _text(selection.get("routing")),
|
|
111
|
+
"recommendedOptionId": recommended_id,
|
|
112
|
+
"criteria": [
|
|
113
|
+
{"criterion": _text(row.get("criterion")), "weight": row.get("weight")}
|
|
114
|
+
for row in _rows(selection.get("evaluationCriteria"))
|
|
115
|
+
],
|
|
116
|
+
"workers": workers,
|
|
117
|
+
"options": options,
|
|
118
|
+
"narrative": {
|
|
119
|
+
key: _text(block.get("text"))
|
|
120
|
+
for key, block in narrative.items()
|
|
121
|
+
if isinstance(block, Mapping) and _text(block.get("text"))
|
|
122
|
+
},
|
|
123
|
+
# "남은 절차" 문단의 근거. 다음 phase 포인터와 같은 문장이다.
|
|
124
|
+
"nextStep": next_phase.project(report_data)["rationale"],
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _load(report: Path) -> Mapping[str, Any]:
|
|
129
|
+
data_path = final_report_data_path(report)
|
|
130
|
+
try:
|
|
131
|
+
data = load_owned_object(data_path, artifact="implementation option data")
|
|
132
|
+
except (OSError, JsonBoundaryError) as exc:
|
|
133
|
+
raise OptionComparisonError(f"cannot read report record {data_path}: {exc}") from exc
|
|
134
|
+
return data
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
138
|
+
parser = argparse.ArgumentParser(
|
|
139
|
+
prog="okstra option-comparison",
|
|
140
|
+
description=(
|
|
141
|
+
"Print the comparison facts of an implementation-option-selection "
|
|
142
|
+
"report as JSON: criteria and weights, per-direction scores and "
|
|
143
|
+
"recorded weighted score, requirement coverage, blocker counts, "
|
|
144
|
+
"per-worker feasibility votes, the report narrative, and the next step."
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
"--report",
|
|
149
|
+
required=True,
|
|
150
|
+
help="final-report-implementation-option-selection-<seq>.data.json (a .md sibling is accepted)",
|
|
151
|
+
)
|
|
152
|
+
return parser
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main(argv: list[str] | None = None) -> int:
|
|
156
|
+
args = build_parser().parse_args(argv)
|
|
157
|
+
try:
|
|
158
|
+
payload = build_comparison(_load(Path(args.report)))
|
|
159
|
+
except OptionComparisonError as exc:
|
|
160
|
+
print(f"option-comparison: {exc}", file=sys.stderr)
|
|
161
|
+
return 2
|
|
162
|
+
json.dump(payload, sys.stdout, ensure_ascii=False, indent=2)
|
|
163
|
+
sys.stdout.write("\n")
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
sys.exit(main())
|
|
@@ -64,6 +64,7 @@ def compact_active_run_context(
|
|
|
64
64
|
"run": {
|
|
65
65
|
"stage": ctx.get("RUN_STAGE", ""),
|
|
66
66
|
"fixRunCarry": ctx.get("FIX_RUN_CONTEXT", ""),
|
|
67
|
+
"hostSessionContract": ctx.get("HOST_SESSION_CONTRACT_PATH", ""),
|
|
67
68
|
},
|
|
68
69
|
"agentContract": dict(_mapping(payload.get("agentContract"))),
|
|
69
70
|
"inputs": _compact_active_inputs(payload),
|
|
@@ -118,8 +119,13 @@ def hydrate_active_run_context(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
118
119
|
"IMPLEMENTATION_WORKER_PREAMBLE_PATH", ""
|
|
119
120
|
),
|
|
120
121
|
"report-writer": ctx.get("REPORT_WRITER_PREAMBLE_PATH", ""),
|
|
122
|
+
"translator": ctx.get("TRANSLATOR_PREAMBLE_PATH", ""),
|
|
121
123
|
},
|
|
122
124
|
"workerErrorContractPath": ctx.get("WORKER_ERROR_CONTRACT_PATH", ""),
|
|
125
|
+
"hostSessionContractPath": (
|
|
126
|
+
str(_mapping(payload.get("run")).get("hostSessionContract", ""))
|
|
127
|
+
or ctx.get("HOST_SESSION_CONTRACT_PATH", "")
|
|
128
|
+
),
|
|
123
129
|
},
|
|
124
130
|
"executorWorktree": dict(_mapping(payload.get("executorWorktree"))),
|
|
125
131
|
"verificationTarget": dict(_mapping(payload.get("verificationTarget"))),
|
|
@@ -509,6 +515,9 @@ def _runtime_fields(paths: Mapping[str, Any]) -> dict[str, str]:
|
|
|
509
515
|
"REPORT_WRITER_PREAMBLE_PATH": str(
|
|
510
516
|
runtime_home / "templates" / "report-writer-prompt-preamble.md"
|
|
511
517
|
),
|
|
518
|
+
"TRANSLATOR_PREAMBLE_PATH": str(
|
|
519
|
+
runtime_home / "templates" / "translator-prompt-preamble.md"
|
|
520
|
+
),
|
|
512
521
|
"WORKER_ERROR_CONTRACT_PATH": str(
|
|
513
522
|
runtime_home / "templates" / "worker-error-contract.md"
|
|
514
523
|
),
|