okstra 0.179.2 → 0.183.2
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 +1 -1
- package/dist/cli-registry.mjs +30 -2
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/incremental-carry.mjs +9 -8
- package/dist/commands/execute/incremental-carry.mjs.map +1 -1
- package/dist/commands/execute/plan-verify.mjs +3 -1
- package/dist/commands/execute/plan-verify.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.d.mts +4 -2
- package/dist/commands/execute/render-bundle.mjs +46 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +11 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/inspect/model-io.d.mts +1 -0
- package/dist/commands/inspect/model-io.mjs +25 -0
- package/dist/commands/inspect/model-io.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.mjs +29 -8
- package/dist/commands/inspect/stage-map.mjs.map +1 -1
- package/dist/commands/inspect/task-list.mjs +52 -6
- package/dist/commands/inspect/task-list.mjs.map +1 -1
- package/dist/commands/inspect/user-response.mjs +14 -4
- package/dist/commands/inspect/user-response.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.d.mts +1 -0
- package/dist/commands/lifecycle/check-project.mjs +69 -50
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/contract-check.d.mts +1 -0
- package/dist/commands/lifecycle/contract-check.mjs +18 -0
- package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.mjs +154 -51
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/pr/pr.d.mts +1 -0
- package/dist/commands/pr/pr.mjs +19 -1
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/dist/commands/report/agent-activity.mjs +2 -2
- package/dist/commands/report/approval-decision.d.mts +1 -0
- package/dist/commands/report/approval-decision.mjs +21 -0
- package/dist/commands/report/approval-decision.mjs.map +1 -0
- package/dist/commands/report/design-snapshot.d.mts +1 -0
- package/dist/commands/report/design-snapshot.mjs +19 -0
- package/dist/commands/report/design-snapshot.mjs.map +1 -0
- package/dist/commands/report/translate.mjs +3 -0
- package/dist/commands/report/translate.mjs.map +1 -1
- package/dist/lib/host-registry-client.mjs +13 -9
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +21 -10
- package/docs/cli.md +36 -20
- package/docs/container.md +6 -4
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
- package/docs/for-ai/skills/okstra-code-review.md +4 -4
- package/docs/for-ai/skills/okstra-container-build.md +20 -17
- package/docs/for-ai/skills/okstra-inspect.md +20 -23
- package/docs/for-ai/skills/okstra-manager.md +19 -18
- package/docs/for-ai/skills/okstra-memory.md +2 -2
- package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
- package/docs/for-ai/skills/okstra-rollup.md +14 -13
- package/docs/for-ai/skills/okstra-run.md +7 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
- package/docs/for-ai/skills/okstra-setup.md +7 -7
- package/docs/for-ai/skills/okstra-usage.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +50 -32
- package/docs/project-structure-overview.md +43 -31
- package/docs/task-process/implementation-planning.md +2 -2
- package/docs/task-process/implementation.md +7 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/report-writer-worker.md +15 -164
- package/runtime/agents/workers/translator-worker.md +5 -13
- package/runtime/bin/okstra-error-log.py +51 -11
- package/runtime/bin/okstra-report-translate.py +210 -23
- package/runtime/prompts/host-orchestration/implementation.md +1 -1
- package/runtime/prompts/launch.template.md +9 -12
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/context-loader.md +41 -141
- package/runtime/prompts/lead/convergence.md +10 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +40 -49
- package/runtime/prompts/lead/plan-body-verification.md +217 -23
- package/runtime/prompts/lead/report-writer.md +81 -421
- package/runtime/prompts/lead/team-contract.md +9 -54
- package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/feature-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +20 -12
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/project-analysis.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +0 -1
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +8 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
- package/runtime/python/okstra_ctl/agent_activity.py +141 -38
- package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +70 -23
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +124 -3
- package/runtime/python/okstra_ctl/approval_decisions.py +328 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
- package/runtime/python/okstra_ctl/backfill.py +13 -10
- package/runtime/python/okstra_ctl/batch.py +2 -4
- package/runtime/python/okstra_ctl/build_tools.py +6 -3
- package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
- package/runtime/python/okstra_ctl/clarification_items.py +27 -13
- package/runtime/python/okstra_ctl/cmux.py +130 -52
- package/runtime/python/okstra_ctl/code_review_target.py +34 -8
- package/runtime/python/okstra_ctl/consumers.py +5 -4
- package/runtime/python/okstra_ctl/container.py +103 -8
- package/runtime/python/okstra_ctl/context_cost.py +2 -1
- package/runtime/python/okstra_ctl/contract_graph.py +497 -0
- package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
- package/runtime/python/okstra_ctl/convergence.py +338 -17
- package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
- package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
- package/runtime/python/okstra_ctl/convergence_store.py +55 -34
- package/runtime/python/okstra_ctl/design_prep.py +7 -4
- package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +97 -69
- package/runtime/python/okstra_ctl/dispatch_state.py +163 -63
- package/runtime/python/okstra_ctl/doctor.py +6 -3
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
- package/runtime/python/okstra_ctl/error_log_write.py +35 -30
- package/runtime/python/okstra_ctl/error_report.py +26 -1
- package/runtime/python/okstra_ctl/error_zip.py +27 -5
- package/runtime/python/okstra_ctl/execution_identity.py +3 -2
- package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +26 -17
- package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
- package/runtime/python/okstra_ctl/fixed_text.py +39 -0
- package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
- package/runtime/python/okstra_ctl/handoff.py +5 -4
- package/runtime/python/okstra_ctl/i18n.py +4 -2
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
- package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
- package/runtime/python/okstra_ctl/incremental_carry.py +130 -17
- package/runtime/python/okstra_ctl/incremental_scope.py +12 -4
- package/runtime/python/okstra_ctl/index.py +8 -11
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
- package/runtime/python/okstra_ctl/invocation.py +3 -6
- package/runtime/python/okstra_ctl/json_boundary.py +366 -0
- package/runtime/python/okstra_ctl/json_registry.py +10 -12
- package/runtime/python/okstra_ctl/jsonl.py +19 -2
- package/runtime/python/okstra_ctl/lead_events.py +33 -1
- package/runtime/python/okstra_ctl/listing.py +3 -3
- package/runtime/python/okstra_ctl/log_report.py +24 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -7
- package/runtime/python/okstra_ctl/manager_store.py +12 -10
- package/runtime/python/okstra_ctl/material.py +5 -1
- package/runtime/python/okstra_ctl/migrate.py +29 -25
- package/runtime/python/okstra_ctl/model_cli.py +3 -15
- package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
- package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
- package/runtime/python/okstra_ctl/path_hints.py +12 -0
- package/runtime/python/okstra_ctl/paths.py +21 -0
- package/runtime/python/okstra_ctl/plan_items.py +115 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +626 -34
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
- package/runtime/python/okstra_ctl/pr_template.py +3 -2
- package/runtime/python/okstra_ctl/project_meta.py +5 -7
- package/runtime/python/okstra_ctl/recap.py +5 -4
- package/runtime/python/okstra_ctl/reconcile.py +21 -27
- package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
- package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
- package/runtime/python/okstra_ctl/render.py +74 -16
- package/runtime/python/okstra_ctl/render_final_report.py +10 -8
- package/runtime/python/okstra_ctl/report_assembly.py +509 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -1
- package/runtime/python/okstra_ctl/report_finalize.py +67 -32
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_inputs.py +72 -0
- package/runtime/python/okstra_ctl/report_language.py +3 -2
- package/runtime/python/okstra_ctl/report_markdown.py +81 -8
- package/runtime/python/okstra_ctl/report_narrative.py +351 -0
- package/runtime/python/okstra_ctl/report_projections.py +265 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
- package/runtime/python/okstra_ctl/report_views.py +3 -2
- package/runtime/python/okstra_ctl/rollup.py +65 -4
- package/runtime/python/okstra_ctl/run.py +184 -65
- package/runtime/python/okstra_ctl/run_audit.py +3 -2
- package/runtime/python/okstra_ctl/run_context.py +6 -9
- package/runtime/python/okstra_ctl/run_index_row.py +2 -8
- package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +15 -8
- package/runtime/python/okstra_ctl/session_transcript.py +27 -1
- package/runtime/python/okstra_ctl/set_work_status.py +64 -38
- package/runtime/python/okstra_ctl/stage_fix_carry.py +8 -6
- package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
- package/runtime/python/okstra_ctl/stage_map.py +96 -28
- package/runtime/python/okstra_ctl/stage_targets.py +3 -4
- package/runtime/python/okstra_ctl/team.py +3 -2
- package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
- package/runtime/python/okstra_ctl/time_report.py +51 -4
- package/runtime/python/okstra_ctl/usage_identity.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +58 -4
- package/runtime/python/okstra_ctl/user_response.py +1431 -66
- package/runtime/python/okstra_ctl/wizard.py +11 -44
- package/runtime/python/okstra_ctl/work_categories.py +3 -2
- package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +49 -3
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
- package/runtime/python/okstra_ctl/worker_runner.py +14 -12
- package/runtime/python/okstra_ctl/workflow.py +2 -1
- package/runtime/python/okstra_ctl/worktree.py +3 -2
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
- package/runtime/python/okstra_ctl/write_policy.py +4 -2
- package/runtime/python/okstra_token_usage/antigravity.py +39 -12
- package/runtime/python/okstra_token_usage/collect.py +90 -38
- package/runtime/python/okstra_token_usage/grok.py +127 -0
- package/runtime/schemas/final-report-v2.0.schema.json +9 -0
- package/runtime/schemas/final-report-v3.0.schema.json +10219 -0
- package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
- package/runtime/skills/okstra-code-review/SKILL.md +21 -11
- package/runtime/skills/okstra-container-build/SKILL.md +18 -18
- package/runtime/skills/okstra-inspect/SKILL.md +12 -11
- package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
- package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
- package/runtime/skills/okstra-inspect/facets/history.md +9 -14
- package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
- package/runtime/skills/okstra-inspect/facets/report.md +6 -10
- package/runtime/skills/okstra-inspect/facets/status.md +9 -8
- package/runtime/skills/okstra-inspect/facets/time.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +16 -14
- package/runtime/skills/okstra-memory/SKILL.md +3 -3
- package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
- package/runtime/skills/okstra-rollup/SKILL.md +6 -16
- package/runtime/skills/okstra-run/SKILL.md +7 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
- package/runtime/skills/okstra-setup/SKILL.md +21 -13
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/skills/okstra-usage/SKILL.md +10 -10
- package/runtime/skills/okstra-user-response/SKILL.md +78 -107
- package/runtime/templates/report-writer-prompt-preamble.md +29 -19
- package/runtime/templates/reports/html/macros/forms.html +6 -4
- package/runtime/templates/reports/schedule.template.md +4 -4
- package/runtime/templates/worker-error-contract.md +17 -29
- package/runtime/validators/validate-run.py +636 -72
- package/runtime/validators/validate_session_conformance.py +43 -7
|
@@ -0,0 +1,1051 @@
|
|
|
1
|
+
"""목적별 Okstra 소유 JSON에서 고정 Markdown 입력을 렌더한다."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Mapping
|
|
9
|
+
|
|
10
|
+
from okstra_project import (
|
|
11
|
+
StateError,
|
|
12
|
+
parse_task_key,
|
|
13
|
+
slugify,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .convergence import (
|
|
17
|
+
ConvergenceContractError,
|
|
18
|
+
canonical_run_state_artifact,
|
|
19
|
+
validated_run_authority,
|
|
20
|
+
)
|
|
21
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
22
|
+
from .error_zip import last_output_path
|
|
23
|
+
from .fixed_text import line as _line, scalar as _value
|
|
24
|
+
from .paths import okstra_home
|
|
25
|
+
from .recap import assemble_recap
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _mapping(payload: Mapping[str, Any], key: str) -> Mapping[str, Any]:
|
|
29
|
+
value = payload.get(key)
|
|
30
|
+
return value if isinstance(value, Mapping) else {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _project_policy_lines(project: Mapping[str, Any]) -> str:
|
|
34
|
+
packs = project.get("reviewRulePacks")
|
|
35
|
+
pack_lines = (
|
|
36
|
+
[
|
|
37
|
+
_line("Review rule pack", path)
|
|
38
|
+
for path in packs
|
|
39
|
+
if isinstance(path, str)
|
|
40
|
+
]
|
|
41
|
+
if isinstance(packs, list)
|
|
42
|
+
else []
|
|
43
|
+
)
|
|
44
|
+
qa_lines: list[str] = []
|
|
45
|
+
qa_commands = project.get("qaCommands")
|
|
46
|
+
if isinstance(qa_commands, Mapping):
|
|
47
|
+
for category, entries in sorted(qa_commands.items()):
|
|
48
|
+
if not isinstance(entries, list):
|
|
49
|
+
continue
|
|
50
|
+
for entry in entries:
|
|
51
|
+
if not isinstance(entry, Mapping):
|
|
52
|
+
continue
|
|
53
|
+
qa_lines.append(
|
|
54
|
+
f"- QA `{_value(category)}` / `{_value(entry.get('label'))}`: "
|
|
55
|
+
f"`{_value(entry.get('cmd'))}`\n"
|
|
56
|
+
)
|
|
57
|
+
return (
|
|
58
|
+
"\n## Project Review Rule Packs\n\n"
|
|
59
|
+
+ ("".join(pack_lines) or "- None\n")
|
|
60
|
+
+ "\n## Project QA Commands\n\n"
|
|
61
|
+
+ ("".join(qa_lines) or "- None\n")
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _assignment_line(prefix: str, assignment: Mapping[str, Any], identifier: object = None) -> str:
|
|
66
|
+
label = prefix if identifier is None else f"{prefix} `{_value(identifier)}`"
|
|
67
|
+
return (
|
|
68
|
+
f"- {label}: role `{_value(assignment.get('role'))}`; "
|
|
69
|
+
f"provider `{_value(assignment.get('provider'))}`; "
|
|
70
|
+
f"model `{_value(assignment.get('model'))}`\n"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _worker_assignment_lines(run: Mapping[str, Any]) -> str:
|
|
75
|
+
assignments = run.get("workerAssignments")
|
|
76
|
+
if not isinstance(assignments, list):
|
|
77
|
+
return "- None\n"
|
|
78
|
+
lines = [
|
|
79
|
+
_assignment_line("Worker", assignment, assignment.get("workerId"))
|
|
80
|
+
for assignment in assignments
|
|
81
|
+
if isinstance(assignment, Mapping)
|
|
82
|
+
]
|
|
83
|
+
return "".join(lines) or "- None\n"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _worker_prompt_lines(run: Mapping[str, Any]) -> str:
|
|
87
|
+
paths = run.get("workerPromptPathByWorkerId")
|
|
88
|
+
if not isinstance(paths, Mapping):
|
|
89
|
+
return "- None\n"
|
|
90
|
+
lines = [
|
|
91
|
+
f"- Worker prompt `{_value(worker_id)}`: `{_value(path)}`\n"
|
|
92
|
+
for worker_id, path in sorted(paths.items(), key=lambda item: _value(item[0]))
|
|
93
|
+
]
|
|
94
|
+
return "".join(lines) or "- None\n"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _fixed_owned_artifact_path(
|
|
98
|
+
project_root: Path, relative_path: Path, artifact: str
|
|
99
|
+
) -> Path:
|
|
100
|
+
resolved_project_root = project_root.resolve()
|
|
101
|
+
okstra_root = resolved_project_root / ".okstra"
|
|
102
|
+
if okstra_root.is_symlink():
|
|
103
|
+
raise JsonBoundaryError(okstra_root, artifact, "Okstra root is not a fixed directory")
|
|
104
|
+
resolved_okstra_root = okstra_root.resolve()
|
|
105
|
+
try:
|
|
106
|
+
resolved_okstra_root.relative_to(resolved_project_root)
|
|
107
|
+
except ValueError as exc:
|
|
108
|
+
raise JsonBoundaryError(okstra_root, artifact, "outside this project") from exc
|
|
109
|
+
path = okstra_root / relative_path
|
|
110
|
+
expected_path = resolved_okstra_root / relative_path
|
|
111
|
+
if path.resolve() != expected_path:
|
|
112
|
+
raise JsonBoundaryError(path, artifact, "not at its fixed Okstra path")
|
|
113
|
+
return path
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _owned_tasks_root(project_root: Path, artifact: str) -> Path:
|
|
117
|
+
path = _fixed_owned_artifact_path(project_root, Path("tasks"), artifact)
|
|
118
|
+
if not path.is_dir():
|
|
119
|
+
raise JsonBoundaryError(path, artifact, "tasks root is not a directory")
|
|
120
|
+
return path
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _load_project(project_root: Path) -> dict[str, Any]:
|
|
124
|
+
path = _fixed_owned_artifact_path(
|
|
125
|
+
project_root, Path("project.json"), "project metadata"
|
|
126
|
+
)
|
|
127
|
+
return load_owned_object(path, artifact="project metadata")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _latest_task_pointer(project_root: Path) -> Mapping[str, Any]:
|
|
131
|
+
path = _fixed_owned_artifact_path(
|
|
132
|
+
project_root, Path("discovery") / "latest-task.json", "latest task pointer"
|
|
133
|
+
)
|
|
134
|
+
if not path.is_file():
|
|
135
|
+
return {}
|
|
136
|
+
pointer = load_owned_object(path, artifact="latest task pointer")
|
|
137
|
+
manifest_path = _owned_task_manifest_path(
|
|
138
|
+
project_root, pointer.get("taskManifestPath"), "latest task pointer"
|
|
139
|
+
)
|
|
140
|
+
return _task_pointer_from_manifest(
|
|
141
|
+
project_root,
|
|
142
|
+
manifest_path,
|
|
143
|
+
expected_task_key=pointer.get("taskKey"),
|
|
144
|
+
pointer_run_manifest=pointer.get("latestRunManifestPath"),
|
|
145
|
+
artifact="latest task pointer",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _project_relative(project_root: Path, path: Path) -> str:
|
|
150
|
+
try:
|
|
151
|
+
return str(path.resolve().relative_to(project_root.resolve()))
|
|
152
|
+
except ValueError:
|
|
153
|
+
return str(path)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _owned_project_path(project_root: Path, value: str, artifact: str) -> Path:
|
|
157
|
+
path = Path(value)
|
|
158
|
+
path = path if path.is_absolute() else project_root / path
|
|
159
|
+
try:
|
|
160
|
+
path.resolve().relative_to(project_root.resolve())
|
|
161
|
+
except ValueError as exc:
|
|
162
|
+
raise JsonBoundaryError(path, artifact, "outside this project") from exc
|
|
163
|
+
return path
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _task_reference_error(project_root: Path, reason: str) -> JsonBoundaryError:
|
|
167
|
+
return JsonBoundaryError(project_root, "task reference", reason)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _catalog_tasks(project_root: Path) -> list[Mapping[str, Any]]:
|
|
171
|
+
path = _fixed_owned_artifact_path(
|
|
172
|
+
project_root, Path("discovery") / "task-catalog.json", "task catalog"
|
|
173
|
+
)
|
|
174
|
+
if not path.is_file():
|
|
175
|
+
return []
|
|
176
|
+
catalog = load_owned_object(path, artifact="task catalog")
|
|
177
|
+
tasks = catalog.get("tasks")
|
|
178
|
+
return [task for task in tasks if isinstance(task, Mapping)] if isinstance(tasks, list) else []
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _catalog_task_root(project_root: Path, task: Mapping[str, Any]) -> Path | None:
|
|
182
|
+
value = task.get("taskRootPath") or task.get("taskRoot")
|
|
183
|
+
if not isinstance(value, str) or not value:
|
|
184
|
+
return None
|
|
185
|
+
path = Path(value)
|
|
186
|
+
path = path if path.is_absolute() else project_root / path
|
|
187
|
+
return path if path.is_dir() else None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _normal_task_root(project_root: Path, task_key: str) -> Path | None:
|
|
191
|
+
_, task_group, task_id = parse_task_key(task_key)
|
|
192
|
+
path = _owned_tasks_root(project_root, "task reference") / slugify(task_group) / slugify(task_id)
|
|
193
|
+
return path if path.is_dir() else None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _catalog_task_reference(
|
|
197
|
+
project_root: Path, reference: str
|
|
198
|
+
) -> tuple[Path | None, str | None]:
|
|
199
|
+
tasks = _catalog_tasks(project_root)
|
|
200
|
+
if ":" in reference:
|
|
201
|
+
parse_task_key(reference)
|
|
202
|
+
matches = [
|
|
203
|
+
task for task in tasks
|
|
204
|
+
if isinstance(task.get("taskKey"), str)
|
|
205
|
+
and task["taskKey"].lower() == reference.lower()
|
|
206
|
+
]
|
|
207
|
+
task_key = matches[0]["taskKey"] if matches else reference
|
|
208
|
+
task_root = _catalog_task_root(project_root, matches[0]) if matches else None
|
|
209
|
+
return task_root or _normal_task_root(project_root, task_key), task_key
|
|
210
|
+
|
|
211
|
+
matches = [
|
|
212
|
+
task for task in tasks
|
|
213
|
+
if isinstance(task.get("taskId"), str)
|
|
214
|
+
and task["taskId"].lower() == reference.lower()
|
|
215
|
+
]
|
|
216
|
+
if len(matches) != 1:
|
|
217
|
+
raise _task_reference_error(
|
|
218
|
+
project_root, "task reference must resolve to exactly one task"
|
|
219
|
+
)
|
|
220
|
+
task_key = matches[0].get("taskKey")
|
|
221
|
+
if not isinstance(task_key, str):
|
|
222
|
+
return None, None
|
|
223
|
+
return _catalog_task_root(project_root, matches[0]) or _normal_task_root(
|
|
224
|
+
project_root, task_key
|
|
225
|
+
), task_key
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _owned_task_manifest_path(
|
|
229
|
+
project_root: Path, value: object, artifact: str
|
|
230
|
+
) -> Path:
|
|
231
|
+
if not isinstance(value, str) or not value:
|
|
232
|
+
raise JsonBoundaryError(project_root, artifact, "task manifest path is missing")
|
|
233
|
+
path = Path(value)
|
|
234
|
+
path = path if path.is_absolute() else project_root / path
|
|
235
|
+
if path.name != "task-manifest.json":
|
|
236
|
+
raise JsonBoundaryError(path, artifact, "task manifest path is invalid")
|
|
237
|
+
tasks_root = _owned_tasks_root(project_root, artifact)
|
|
238
|
+
try:
|
|
239
|
+
path.resolve().relative_to(tasks_root.resolve())
|
|
240
|
+
except ValueError as exc:
|
|
241
|
+
raise JsonBoundaryError(path, artifact, "task manifest is outside this project") from exc
|
|
242
|
+
return path
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _selected_run_manifest_path(
|
|
246
|
+
project_root: Path, task_root: Path, value: str
|
|
247
|
+
) -> Path:
|
|
248
|
+
path = Path(value)
|
|
249
|
+
path = path if path.is_absolute() else project_root / path
|
|
250
|
+
tasks_root = _owned_tasks_root(project_root, "run manifest")
|
|
251
|
+
resolved_task_root = task_root.resolve()
|
|
252
|
+
resolved_runs_root = (task_root / "runs").resolve()
|
|
253
|
+
try:
|
|
254
|
+
resolved_task_root.relative_to(tasks_root.resolve())
|
|
255
|
+
resolved_runs_root.relative_to(resolved_task_root)
|
|
256
|
+
path.resolve().relative_to(resolved_runs_root)
|
|
257
|
+
path.resolve().relative_to(resolved_task_root)
|
|
258
|
+
except ValueError as exc:
|
|
259
|
+
raise JsonBoundaryError(path, "run manifest", "outside selected task") from exc
|
|
260
|
+
if not path.name.startswith("run-manifest-") or path.suffix != ".json":
|
|
261
|
+
raise JsonBoundaryError(path, "run manifest", "not a normal run manifest path")
|
|
262
|
+
return path
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _owned_run_manifest_path(run_manifest: Path) -> Path:
|
|
266
|
+
for runs_root in run_manifest.parents:
|
|
267
|
+
if runs_root.name != "runs":
|
|
268
|
+
continue
|
|
269
|
+
task_root = runs_root.parent
|
|
270
|
+
tasks_root = task_root.parent.parent
|
|
271
|
+
if tasks_root.name != "tasks" or tasks_root.parent.name != ".okstra":
|
|
272
|
+
continue
|
|
273
|
+
project_root = tasks_root.parent.parent
|
|
274
|
+
return _selected_run_manifest_path(project_root, task_root, str(run_manifest))
|
|
275
|
+
raise JsonBoundaryError(
|
|
276
|
+
run_manifest, "run manifest", "outside owned task runs"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _task_manifest_from_reference(project_root: Path, task_ref: str) -> tuple[Path, str | None]:
|
|
281
|
+
reference = task_ref.strip()
|
|
282
|
+
candidate = Path(reference).expanduser()
|
|
283
|
+
candidate = candidate if candidate.is_absolute() else project_root / candidate
|
|
284
|
+
if candidate.is_file():
|
|
285
|
+
try:
|
|
286
|
+
return (
|
|
287
|
+
_owned_task_manifest_path(project_root, str(candidate), "task reference"),
|
|
288
|
+
None,
|
|
289
|
+
)
|
|
290
|
+
except JsonBoundaryError as exc:
|
|
291
|
+
raise _task_reference_error(project_root, exc.reason) from exc
|
|
292
|
+
try:
|
|
293
|
+
task_root, task_key = _catalog_task_reference(project_root, reference)
|
|
294
|
+
except StateError as exc:
|
|
295
|
+
raise _task_reference_error(project_root, str(exc)) from exc
|
|
296
|
+
if task_root is None:
|
|
297
|
+
raise _task_reference_error(project_root, "task not found")
|
|
298
|
+
try:
|
|
299
|
+
manifest_path = _owned_task_manifest_path(
|
|
300
|
+
project_root, str(task_root / "task-manifest.json"), "task reference"
|
|
301
|
+
)
|
|
302
|
+
except JsonBoundaryError as exc:
|
|
303
|
+
raise _task_reference_error(project_root, exc.reason) from exc
|
|
304
|
+
return manifest_path, task_key if isinstance(task_key, str) else None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _task_pointer_from_manifest(
|
|
308
|
+
project_root: Path,
|
|
309
|
+
manifest_path: Path,
|
|
310
|
+
*,
|
|
311
|
+
expected_task_key: object = None,
|
|
312
|
+
pointer_run_manifest: object = None,
|
|
313
|
+
artifact: str,
|
|
314
|
+
) -> Mapping[str, Any]:
|
|
315
|
+
manifest = load_owned_object(manifest_path, artifact="task manifest")
|
|
316
|
+
task_key = manifest.get("taskKey")
|
|
317
|
+
if not isinstance(task_key, str):
|
|
318
|
+
raise JsonBoundaryError(manifest_path, artifact, "task key is invalid")
|
|
319
|
+
try:
|
|
320
|
+
_, task_group, task_id = parse_task_key(task_key)
|
|
321
|
+
except StateError as exc:
|
|
322
|
+
raise JsonBoundaryError(manifest_path, artifact, "task key is invalid") from exc
|
|
323
|
+
normal_manifest_path = (
|
|
324
|
+
_owned_tasks_root(project_root, artifact)
|
|
325
|
+
/ slugify(task_group)
|
|
326
|
+
/ slugify(task_id)
|
|
327
|
+
/ "task-manifest.json"
|
|
328
|
+
)
|
|
329
|
+
if manifest_path.resolve() != normal_manifest_path.resolve():
|
|
330
|
+
raise JsonBoundaryError(
|
|
331
|
+
manifest_path, artifact, "task key does not match task manifest path"
|
|
332
|
+
)
|
|
333
|
+
if expected_task_key is not None and task_key != expected_task_key:
|
|
334
|
+
raise JsonBoundaryError(manifest_path, artifact, "task key does not match task manifest")
|
|
335
|
+
task_root = manifest_path.parent
|
|
336
|
+
expected_timeline_path = task_root / "history" / "timeline.json"
|
|
337
|
+
try:
|
|
338
|
+
expected_timeline_path.resolve().relative_to(task_root.resolve())
|
|
339
|
+
except ValueError as exc:
|
|
340
|
+
raise JsonBoundaryError(
|
|
341
|
+
expected_timeline_path, "task timeline", "outside selected task"
|
|
342
|
+
) from exc
|
|
343
|
+
timeline_value = manifest.get("historyTimelinePath")
|
|
344
|
+
if isinstance(timeline_value, str) and timeline_value:
|
|
345
|
+
timeline_path = _owned_project_path(
|
|
346
|
+
project_root, timeline_value, "task timeline"
|
|
347
|
+
)
|
|
348
|
+
if timeline_path.resolve() != expected_timeline_path.resolve():
|
|
349
|
+
raise JsonBoundaryError(
|
|
350
|
+
timeline_path, "task timeline", "outside selected task"
|
|
351
|
+
)
|
|
352
|
+
else:
|
|
353
|
+
timeline_path = expected_timeline_path
|
|
354
|
+
latest_run_manifest: object = None
|
|
355
|
+
latest_run_path: Path | None = None
|
|
356
|
+
if timeline_path.is_file():
|
|
357
|
+
timeline = load_owned_object(timeline_path, artifact="task timeline")
|
|
358
|
+
runs = timeline.get("runs")
|
|
359
|
+
if isinstance(runs, list):
|
|
360
|
+
for run in reversed(runs):
|
|
361
|
+
if isinstance(run, Mapping) and isinstance(run.get("runManifestPath"), str):
|
|
362
|
+
latest_run_path = _selected_run_manifest_path(
|
|
363
|
+
project_root, task_root, run["runManifestPath"]
|
|
364
|
+
)
|
|
365
|
+
latest_run_manifest = _project_relative(project_root, latest_run_path)
|
|
366
|
+
break
|
|
367
|
+
if pointer_run_manifest is not None:
|
|
368
|
+
if not isinstance(pointer_run_manifest, str):
|
|
369
|
+
raise JsonBoundaryError(manifest_path, artifact, "run manifest path is invalid")
|
|
370
|
+
pointer_run_path = _selected_run_manifest_path(
|
|
371
|
+
project_root, task_root, pointer_run_manifest
|
|
372
|
+
)
|
|
373
|
+
if latest_run_path is None or pointer_run_path.resolve() != latest_run_path.resolve():
|
|
374
|
+
raise JsonBoundaryError(
|
|
375
|
+
manifest_path, artifact, "run manifest does not match task timeline"
|
|
376
|
+
)
|
|
377
|
+
return {
|
|
378
|
+
"taskKey": task_key,
|
|
379
|
+
"taskManifestPath": _project_relative(project_root, manifest_path),
|
|
380
|
+
"latestRunManifestPath": latest_run_manifest,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _selected_task_pointer(project_root: Path, task_ref: str) -> Mapping[str, Any]:
|
|
385
|
+
manifest_path, expected_task_key = _task_manifest_from_reference(project_root, task_ref)
|
|
386
|
+
return _task_pointer_from_manifest(
|
|
387
|
+
project_root,
|
|
388
|
+
manifest_path,
|
|
389
|
+
expected_task_key=expected_task_key,
|
|
390
|
+
artifact="task reference",
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def render_project_context(project_root: Path, task_ref: str = "") -> str:
|
|
395
|
+
"""모델의 프로젝트 문맥에 필요한 안정 필드를 렌더한다."""
|
|
396
|
+
project = _load_project(project_root)
|
|
397
|
+
architecture = _mapping(project, "architecture")
|
|
398
|
+
current_task = (
|
|
399
|
+
_selected_task_pointer(project_root, task_ref)
|
|
400
|
+
if task_ref
|
|
401
|
+
else _latest_task_pointer(project_root)
|
|
402
|
+
)
|
|
403
|
+
return (
|
|
404
|
+
"# Okstra Project Context\n\n"
|
|
405
|
+
+ _line("Project ID", project.get("projectId"))
|
|
406
|
+
+ _line("Project root", str(project_root))
|
|
407
|
+
+ _line("Report language", project.get("reportLanguage"))
|
|
408
|
+
+ _line("Architecture style", architecture.get("style"))
|
|
409
|
+
+ _project_policy_lines(project)
|
|
410
|
+
+ "\n## Current Task Pointer\n\n"
|
|
411
|
+
+ _line("Task key", current_task.get("taskKey"))
|
|
412
|
+
+ _line("Task manifest", current_task.get("taskManifestPath"))
|
|
413
|
+
+ _line("Latest run manifest", current_task.get("latestRunManifestPath"))
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def render_run_input(run_manifest: Path) -> str:
|
|
418
|
+
"""런 매니페스트의 간결한 비밀 없는 실행 식별자를 렌더한다."""
|
|
419
|
+
run_manifest = _owned_run_manifest_path(run_manifest)
|
|
420
|
+
run = load_owned_object(run_manifest, artifact="run manifest")
|
|
421
|
+
scope = _mapping(run, "analysisScopeConfirmation")
|
|
422
|
+
workflow = _mapping(run, "workflowSnapshot")
|
|
423
|
+
lead = _mapping(run, "leadAssignment")
|
|
424
|
+
adapter = _mapping(run, "leadAdapter")
|
|
425
|
+
return (
|
|
426
|
+
"# Okstra Run Input\n\n"
|
|
427
|
+
"## Run Context\n\n"
|
|
428
|
+
+ _line("Task key", run.get("taskKey"))
|
|
429
|
+
+ _line("Task type", run.get("taskType"))
|
|
430
|
+
+ _line("Run ID", run.get("runDateTimeSegment"))
|
|
431
|
+
+ _line("Work category", run.get("workCategory"))
|
|
432
|
+
+ _line("Lead runtime", run.get("leadRuntime"))
|
|
433
|
+
+ _line("Lead adapter name", adapter.get("name"))
|
|
434
|
+
+ _line("Lead dispatch mode", adapter.get("dispatchMode"))
|
|
435
|
+
+ _line("Lead session accounting", adapter.get("sessionAccounting"))
|
|
436
|
+
+ _line("Current phase", workflow.get("currentPhase"))
|
|
437
|
+
+ _line("Current phase state", workflow.get("currentPhaseState"))
|
|
438
|
+
+ _line("Scope confirmation", scope.get("status"))
|
|
439
|
+
+ "\n## Worker Roster\n\n"
|
|
440
|
+
+ _assignment_line("Lead", lead)
|
|
441
|
+
+ _worker_assignment_lines(run)
|
|
442
|
+
+ "\n## Artifact Paths\n\n"
|
|
443
|
+
+ _line("Worker prompts directory", run.get("workerPromptsDirectoryPath"))
|
|
444
|
+
+ _worker_prompt_lines(run)
|
|
445
|
+
+ _line("Worker results directory", run.get("workerResultsDirectoryPath"))
|
|
446
|
+
+ _line("Expected report", run.get("expectedReportRecordPath"))
|
|
447
|
+
+ _line("Expected status", run.get("expectedStatusPath"))
|
|
448
|
+
+ _line("Validator", run.get("validatorScriptPath"))
|
|
449
|
+
+ _line("Resume command", run.get("resumeCommandPath"))
|
|
450
|
+
+ "\n## Configuration References\n\n"
|
|
451
|
+
+ _line("Task manifest", run.get("taskManifestPath"))
|
|
452
|
+
+ _line("Instruction set", run.get("instructionSetPath"))
|
|
453
|
+
+ _line("Reference expectations", run.get("referenceExpectationsPath"))
|
|
454
|
+
+ _line("Report template", run.get("reportTemplatePath"))
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def render_active_context_input(project_root: Path, run_manifest: Path) -> str:
|
|
459
|
+
"""이전 run의 executor base ref만 목적별 텍스트로 투영한다."""
|
|
460
|
+
root = Path(project_root)
|
|
461
|
+
if not root.is_absolute() or root.absolute() != root.resolve():
|
|
462
|
+
raise JsonBoundaryError(root, "active run context", "project root is not canonical")
|
|
463
|
+
try:
|
|
464
|
+
authority = validated_run_authority(run_manifest)
|
|
465
|
+
if authority.project_root != root:
|
|
466
|
+
raise ConvergenceContractError(
|
|
467
|
+
"active run context project root does not match run authority"
|
|
468
|
+
)
|
|
469
|
+
path = canonical_run_state_artifact(
|
|
470
|
+
authority,
|
|
471
|
+
manifest_field="activeRunContextPath",
|
|
472
|
+
prefix="active-run-context",
|
|
473
|
+
label="active run context path",
|
|
474
|
+
)
|
|
475
|
+
except ConvergenceContractError as exc:
|
|
476
|
+
raise JsonBoundaryError(
|
|
477
|
+
run_manifest, "active run context", str(exc)
|
|
478
|
+
) from exc
|
|
479
|
+
payload = load_owned_object(path, artifact="active run context")
|
|
480
|
+
executor = _mapping(payload, "executorWorktree")
|
|
481
|
+
return "# Okstra Active Context\n\n" + _line("Executor base ref", executor.get("baseRef"))
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _group_segment(value: str) -> str:
|
|
485
|
+
return "".join(character for character in value.lower() if character.isalnum())
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _catalog_task_path(project_root: Path, task: Mapping[str, Any]) -> tuple[Path, str]:
|
|
489
|
+
task_key = task.get("taskKey")
|
|
490
|
+
if not isinstance(task_key, str):
|
|
491
|
+
raise JsonBoundaryError(project_root, "task catalog", "task key is invalid")
|
|
492
|
+
try:
|
|
493
|
+
_, task_group, task_id = parse_task_key(task_key)
|
|
494
|
+
except StateError as exc:
|
|
495
|
+
raise JsonBoundaryError(project_root, "task catalog", "task key is invalid") from exc
|
|
496
|
+
expected_group_segment = slugify(task_group)
|
|
497
|
+
expected_task_segment = slugify(task_id)
|
|
498
|
+
group_segment = task.get("taskGroupPathSegment", expected_group_segment)
|
|
499
|
+
task_segment = task.get("taskIdPathSegment", expected_task_segment)
|
|
500
|
+
if group_segment != expected_group_segment or task_segment != expected_task_segment:
|
|
501
|
+
raise JsonBoundaryError(
|
|
502
|
+
project_root, "task catalog", "task path segment does not match task key"
|
|
503
|
+
)
|
|
504
|
+
path = (
|
|
505
|
+
_owned_tasks_root(project_root, "task catalog")
|
|
506
|
+
/ expected_group_segment
|
|
507
|
+
/ expected_task_segment
|
|
508
|
+
/ "task-manifest.json"
|
|
509
|
+
)
|
|
510
|
+
return _owned_task_manifest_path(project_root, str(path), "task catalog"), task_key
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def render_schedule_input(project_root: Path, task_group: str) -> str:
|
|
514
|
+
"""한 그룹의 카탈로그 작업에 대한 일정용 메타데이터를 렌더한다."""
|
|
515
|
+
project = _load_project(project_root)
|
|
516
|
+
catalog_path = _fixed_owned_artifact_path(
|
|
517
|
+
project_root, Path("discovery") / "task-catalog.json", "task catalog"
|
|
518
|
+
)
|
|
519
|
+
catalog = load_owned_object(catalog_path, artifact="task catalog")
|
|
520
|
+
target_group = _group_segment(task_group)
|
|
521
|
+
task_blocks: list[str] = []
|
|
522
|
+
catalog_tasks = catalog.get("tasks")
|
|
523
|
+
if isinstance(catalog_tasks, list):
|
|
524
|
+
for entry in catalog_tasks:
|
|
525
|
+
if not isinstance(entry, Mapping):
|
|
526
|
+
continue
|
|
527
|
+
task_key = entry.get("taskKey")
|
|
528
|
+
if not isinstance(task_key, str):
|
|
529
|
+
continue
|
|
530
|
+
try:
|
|
531
|
+
_, entry_group, _ = parse_task_key(task_key)
|
|
532
|
+
except StateError:
|
|
533
|
+
continue
|
|
534
|
+
if _group_segment(entry_group) != target_group:
|
|
535
|
+
continue
|
|
536
|
+
manifest_path, expected_task_key = _catalog_task_path(project_root, entry)
|
|
537
|
+
_task_pointer_from_manifest(
|
|
538
|
+
project_root,
|
|
539
|
+
manifest_path,
|
|
540
|
+
expected_task_key=expected_task_key,
|
|
541
|
+
artifact="task catalog",
|
|
542
|
+
)
|
|
543
|
+
manifest = load_owned_object(manifest_path, artifact="task manifest")
|
|
544
|
+
workflow = _mapping(manifest, "workflow")
|
|
545
|
+
task_blocks.append(
|
|
546
|
+
"### " + _value(manifest.get("taskId")) + "\n\n"
|
|
547
|
+
+ _line("Task key", manifest.get("taskKey"))
|
|
548
|
+
+ _line("Work status", manifest.get("workStatus"))
|
|
549
|
+
+ _line("Work category", manifest.get("workCategory"))
|
|
550
|
+
+ _line("Task type", manifest.get("taskType"))
|
|
551
|
+
+ _line("Current phase", workflow.get("currentPhase"))
|
|
552
|
+
)
|
|
553
|
+
tasks = "".join(task_blocks) or "- None\n"
|
|
554
|
+
return (
|
|
555
|
+
"# Okstra Schedule Input\n\n"
|
|
556
|
+
+ _line("Project ID", project.get("projectId"))
|
|
557
|
+
+ _line("Task group", task_group)
|
|
558
|
+
+ "\n## Tasks\n\n"
|
|
559
|
+
+ tasks
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def render_code_review_input(project_root: Path, base: str, head: str) -> str:
|
|
564
|
+
"""리뷰에 필요한 고정 프로젝트 식별자와 git 범위를 렌더한다."""
|
|
565
|
+
project = _load_project(project_root)
|
|
566
|
+
return (
|
|
567
|
+
"# Okstra Code Review Input\n\n"
|
|
568
|
+
+ _line("Project ID", project.get("projectId"))
|
|
569
|
+
+ _line("Project root", str(project_root))
|
|
570
|
+
+ _line("Base ref", base)
|
|
571
|
+
+ _line("Head ref", head)
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _selected_task_data(
|
|
576
|
+
project_root: Path, task_ref: str
|
|
577
|
+
) -> tuple[Mapping[str, Any], list[Mapping[str, Any]]]:
|
|
578
|
+
manifest_path, expected_key = _task_manifest_from_reference(project_root, task_ref)
|
|
579
|
+
_task_pointer_from_manifest(
|
|
580
|
+
project_root, manifest_path, expected_task_key=expected_key, artifact="task reference"
|
|
581
|
+
)
|
|
582
|
+
manifest = load_owned_object(manifest_path, artifact="task manifest")
|
|
583
|
+
timeline_path = manifest_path.parent / "history" / "timeline.json"
|
|
584
|
+
timeline = (
|
|
585
|
+
load_owned_object(timeline_path, artifact="task timeline")
|
|
586
|
+
if timeline_path.is_file()
|
|
587
|
+
else {}
|
|
588
|
+
)
|
|
589
|
+
runs = timeline.get("runs")
|
|
590
|
+
return manifest, [run for run in runs if isinstance(run, Mapping)] if isinstance(runs, list) else []
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _next_phase_lines(workflow: Mapping[str, Any]) -> str:
|
|
594
|
+
pointer = _mapping(workflow, "nextRecommendedPhase")
|
|
595
|
+
return (
|
|
596
|
+
_line("Next phase", pointer.get("phase"))
|
|
597
|
+
+ _line("Next phase status", pointer.get("status"))
|
|
598
|
+
+ _line("Next phase rationale", pointer.get("rationale"))
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _latest_timeline_run(manifest_path: Path) -> Mapping[str, Any]:
|
|
603
|
+
timeline_path = manifest_path.parent / "history" / "timeline.json"
|
|
604
|
+
if not timeline_path.is_file():
|
|
605
|
+
return {}
|
|
606
|
+
timeline = load_owned_object(timeline_path, artifact="task timeline")
|
|
607
|
+
runs = timeline.get("runs")
|
|
608
|
+
if not isinstance(runs, list):
|
|
609
|
+
return {}
|
|
610
|
+
return next(
|
|
611
|
+
(run for run in reversed(runs) if isinstance(run, Mapping)),
|
|
612
|
+
{},
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _overview_rows(
|
|
617
|
+
project_root: Path,
|
|
618
|
+
*,
|
|
619
|
+
task_type: str = "",
|
|
620
|
+
latest_run_status: str = "",
|
|
621
|
+
task_group: str = "",
|
|
622
|
+
) -> list[dict[str, object]]:
|
|
623
|
+
rows: list[dict[str, object]] = []
|
|
624
|
+
for task in _catalog_tasks(project_root):
|
|
625
|
+
manifest_path, task_key = _catalog_task_path(project_root, task)
|
|
626
|
+
pointer = _task_pointer_from_manifest(
|
|
627
|
+
project_root, manifest_path, expected_task_key=task_key, artifact="task catalog"
|
|
628
|
+
)
|
|
629
|
+
manifest = load_owned_object(manifest_path, artifact="task manifest")
|
|
630
|
+
latest_run = _latest_timeline_run(manifest_path)
|
|
631
|
+
_, group, _ = parse_task_key(task_key)
|
|
632
|
+
workflow = _mapping(manifest, "workflow")
|
|
633
|
+
next_phase = _mapping(workflow, "nextRecommendedPhase")
|
|
634
|
+
row = {
|
|
635
|
+
"taskKey": task_key,
|
|
636
|
+
"taskGroup": manifest.get("taskGroup") or task.get("taskGroup") or group,
|
|
637
|
+
"taskType": manifest.get("taskType") or latest_run.get("taskType"),
|
|
638
|
+
"latestRunStatus": latest_run.get("status") or manifest.get("latestRunStatus"),
|
|
639
|
+
"updatedAt": manifest.get("updatedAt") or task.get("updatedAt") or latest_run.get("runTimestamp"),
|
|
640
|
+
"latestRunManifestPath": pointer.get("latestRunManifestPath"),
|
|
641
|
+
"workCategory": manifest.get("workCategory"),
|
|
642
|
+
"currentStatus": manifest.get("currentStatus"),
|
|
643
|
+
"currentPhase": workflow.get("currentPhase"),
|
|
644
|
+
"currentPhaseState": workflow.get("currentPhaseState"),
|
|
645
|
+
"nextPhase": next_phase.get("phase"),
|
|
646
|
+
"nextPhaseStatus": next_phase.get("status"),
|
|
647
|
+
"nextPhaseRationale": next_phase.get("rationale"),
|
|
648
|
+
"awaitingApproval": workflow.get("awaitingApproval"),
|
|
649
|
+
"latestReportRecordPath": latest_run.get("reportRecordPath") or manifest.get("latestReportRecordPath"),
|
|
650
|
+
"latestResumeCommandPath": latest_run.get("resumeCommandPath") or manifest.get("latestResumeCommandPath"),
|
|
651
|
+
"workStatus": manifest.get("workStatus"),
|
|
652
|
+
"lastRun": latest_run.get("runTimestamp"),
|
|
653
|
+
}
|
|
654
|
+
if task_type and row["taskType"] != task_type:
|
|
655
|
+
continue
|
|
656
|
+
if latest_run_status and row["latestRunStatus"] != latest_run_status:
|
|
657
|
+
continue
|
|
658
|
+
if task_group and row["taskGroup"] != task_group:
|
|
659
|
+
continue
|
|
660
|
+
rows.append(row)
|
|
661
|
+
rows.sort(key=lambda row: _value(row.get("taskKey")))
|
|
662
|
+
rows.sort(key=lambda row: _value(row.get("updatedAt")), reverse=True)
|
|
663
|
+
return rows
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _status_overview_text(rows: list[dict[str, object]]) -> str:
|
|
667
|
+
blocks = [_line("Task count", len(rows))]
|
|
668
|
+
for row in rows:
|
|
669
|
+
blocks.extend((
|
|
670
|
+
"\n## Task\n\n",
|
|
671
|
+
_line("Task key", row.get("taskKey")),
|
|
672
|
+
_line("Task group", row.get("taskGroup")),
|
|
673
|
+
_line("Task type", row.get("taskType")),
|
|
674
|
+
_line("Latest run status", row.get("latestRunStatus")),
|
|
675
|
+
_line("Updated at", row.get("updatedAt")),
|
|
676
|
+
_line("Latest run manifest", row.get("latestRunManifestPath")),
|
|
677
|
+
_line("Work category", row.get("workCategory")),
|
|
678
|
+
_line("Current status", row.get("currentStatus")),
|
|
679
|
+
_line("Current phase", row.get("currentPhase")),
|
|
680
|
+
_line("Current phase state", row.get("currentPhaseState")),
|
|
681
|
+
_line("Next phase", row.get("nextPhase")),
|
|
682
|
+
_line("Next phase status", row.get("nextPhaseStatus")),
|
|
683
|
+
_line("Next phase rationale", row.get("nextPhaseRationale")),
|
|
684
|
+
_line("Awaiting approval", row.get("awaitingApproval")),
|
|
685
|
+
_line("Latest report", row.get("latestReportRecordPath")),
|
|
686
|
+
_line("Latest resume command", row.get("latestResumeCommandPath")),
|
|
687
|
+
_line("Work status", row.get("workStatus")),
|
|
688
|
+
))
|
|
689
|
+
return "# Okstra Status Input\n\n" + "".join(blocks)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _history_overview_text(rows: list[dict[str, object]], limit: int) -> str:
|
|
693
|
+
shown = rows[:limit]
|
|
694
|
+
blocks = [
|
|
695
|
+
_line("Task count", len(rows)),
|
|
696
|
+
_line("Shown task count", len(shown)),
|
|
697
|
+
_line("Remaining task count", max(0, len(rows) - len(shown))),
|
|
698
|
+
]
|
|
699
|
+
for row in shown:
|
|
700
|
+
blocks.extend((
|
|
701
|
+
"\n## Task\n\n", _line("Task key", row.get("taskKey")),
|
|
702
|
+
_line("Task group", row.get("taskGroup")),
|
|
703
|
+
_line("Task type", row.get("taskType")),
|
|
704
|
+
_line("Current status", row.get("currentStatus")),
|
|
705
|
+
_line("Latest run status", row.get("latestRunStatus")),
|
|
706
|
+
_line("Last run", row.get("lastRun")),
|
|
707
|
+
_line("Latest report", row.get("latestReportRecordPath")),
|
|
708
|
+
_line("Updated at", row.get("updatedAt")),
|
|
709
|
+
_line("Latest run manifest", row.get("latestRunManifestPath")),
|
|
710
|
+
))
|
|
711
|
+
return "# Okstra History Input\n\n" + "".join(blocks)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def render_status_input(
|
|
715
|
+
project_root: Path,
|
|
716
|
+
task_ref: str,
|
|
717
|
+
*,
|
|
718
|
+
task_type: str = "",
|
|
719
|
+
latest_run_status: str = "",
|
|
720
|
+
task_group: str = "",
|
|
721
|
+
) -> str:
|
|
722
|
+
if not task_ref:
|
|
723
|
+
return _status_overview_text(
|
|
724
|
+
_overview_rows(
|
|
725
|
+
project_root,
|
|
726
|
+
task_type=task_type,
|
|
727
|
+
latest_run_status=latest_run_status,
|
|
728
|
+
task_group=task_group,
|
|
729
|
+
),
|
|
730
|
+
)
|
|
731
|
+
manifest, _ = _selected_task_data(project_root, task_ref)
|
|
732
|
+
workflow = _mapping(manifest, "workflow")
|
|
733
|
+
checkpoint = _mapping(workflow, "lastSafeCheckpoint")
|
|
734
|
+
phases = _mapping(workflow, "phaseStates")
|
|
735
|
+
phase_text = ", ".join(f"{key}={_value(value)}" for key, value in sorted(phases.items()))
|
|
736
|
+
return (
|
|
737
|
+
"# Okstra Status Input\n\n"
|
|
738
|
+
+ _line("Task key", manifest.get("taskKey"))
|
|
739
|
+
+ _line("Task type", manifest.get("taskType"))
|
|
740
|
+
+ _line("Work category", manifest.get("workCategory"))
|
|
741
|
+
+ _line("Current status", manifest.get("currentStatus"))
|
|
742
|
+
+ _line("Latest run status", manifest.get("latestRunStatus"))
|
|
743
|
+
+ _line("Current phase", workflow.get("currentPhase"))
|
|
744
|
+
+ _line("Current phase state", workflow.get("currentPhaseState"))
|
|
745
|
+
+ _line("Phase states", phase_text)
|
|
746
|
+
+ _line("Last completed phase", workflow.get("lastCompletedPhase"))
|
|
747
|
+
+ _next_phase_lines(workflow)
|
|
748
|
+
+ _line("Awaiting approval", workflow.get("awaitingApproval"))
|
|
749
|
+
+ _line("Work status", manifest.get("workStatus"))
|
|
750
|
+
+ _line("Work status updated at", manifest.get("workStatusUpdatedAt"))
|
|
751
|
+
+ _line("Work status note", manifest.get("workStatusNote"))
|
|
752
|
+
+ _line("Latest report", manifest.get("latestReportRecordPath"))
|
|
753
|
+
+ _line("Latest resume command", manifest.get("latestResumeCommandPath"))
|
|
754
|
+
+ _line("History timeline", manifest.get("historyTimelinePath"))
|
|
755
|
+
+ _line("Safe checkpoint label", checkpoint.get("label"))
|
|
756
|
+
+ _line("Safe checkpoint run manifest", checkpoint.get("runManifestPath"))
|
|
757
|
+
+ _line("Safe checkpoint team state", checkpoint.get("teamStatePath"))
|
|
758
|
+
+ _line("Safe checkpoint report", checkpoint.get("reportRecordPath"))
|
|
759
|
+
+ _line("Safe checkpoint resume command", checkpoint.get("resumeCommandPath"))
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def render_history_input(
|
|
764
|
+
project_root: Path,
|
|
765
|
+
task_ref: str,
|
|
766
|
+
*,
|
|
767
|
+
task_type: str = "",
|
|
768
|
+
latest_run_status: str = "",
|
|
769
|
+
task_group: str = "",
|
|
770
|
+
limit: int = 20,
|
|
771
|
+
) -> str:
|
|
772
|
+
if not task_ref:
|
|
773
|
+
return _history_overview_text(
|
|
774
|
+
_overview_rows(
|
|
775
|
+
project_root,
|
|
776
|
+
task_type=task_type,
|
|
777
|
+
latest_run_status=latest_run_status,
|
|
778
|
+
task_group=task_group,
|
|
779
|
+
), limit,
|
|
780
|
+
)
|
|
781
|
+
manifest, runs = _selected_task_data(project_root, task_ref)
|
|
782
|
+
blocks: list[str] = []
|
|
783
|
+
for index, run in enumerate(runs, 1):
|
|
784
|
+
blocks.append(
|
|
785
|
+
f"\n## Run {index}\n\n"
|
|
786
|
+
+ _line("Run timestamp", run.get("runTimestamp"))
|
|
787
|
+
+ _line("Run date-time segment", run.get("runDateTimeSegment"))
|
|
788
|
+
+ _line("Task type", run.get("taskType"))
|
|
789
|
+
+ _line("Status", run.get("status"))
|
|
790
|
+
+ _line("Run manifest", run.get("runManifestPath"))
|
|
791
|
+
+ _line("Report", run.get("reportRecordPath"))
|
|
792
|
+
+ _line("Resume command", run.get("resumeCommandPath"))
|
|
793
|
+
+ _line("Related tasks", ",".join(run.get("relatedTasks", [])) if isinstance(run.get("relatedTasks"), list) else None)
|
|
794
|
+
)
|
|
795
|
+
return "# Okstra History Input\n\n" + _line("Task key", manifest.get("taskKey")) + "".join(blocks)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def render_recap_input(project_root: Path, task_ref: str) -> str:
|
|
799
|
+
manifest_path, _ = _task_manifest_from_reference(project_root, task_ref)
|
|
800
|
+
recap = assemble_recap(manifest_path.parent, project_root)
|
|
801
|
+
blocks = [
|
|
802
|
+
"# Okstra Recap Input\n\n",
|
|
803
|
+
_line("Task key", recap.get("taskKey")),
|
|
804
|
+
_line("Run count", recap.get("runCount")),
|
|
805
|
+
]
|
|
806
|
+
transitions = recap.get("transitions")
|
|
807
|
+
for transition in transitions if isinstance(transitions, list) else []:
|
|
808
|
+
if not isinstance(transition, Mapping):
|
|
809
|
+
continue
|
|
810
|
+
pointer = _mapping(transition, "nextRecommendedPhase")
|
|
811
|
+
blocks.extend((
|
|
812
|
+
"\n## Transition\n\n",
|
|
813
|
+
_line("Run timestamp", transition.get("runTimestamp")),
|
|
814
|
+
_line("Task type", transition.get("taskType")),
|
|
815
|
+
_line("From phase", transition.get("fromPhase")),
|
|
816
|
+
_line("To phase", transition.get("toPhase")),
|
|
817
|
+
_line("Status", transition.get("status")),
|
|
818
|
+
_line("Last completed phase", transition.get("lastCompletedPhase")),
|
|
819
|
+
_line("Next phase", pointer.get("phase")),
|
|
820
|
+
_line("Next phase status", pointer.get("status")),
|
|
821
|
+
_line("Next phase rationale", pointer.get("rationale")),
|
|
822
|
+
_line("Report", transition.get("reportRecordPath")),
|
|
823
|
+
))
|
|
824
|
+
return "".join(blocks)
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def render_report_input(project_root: Path, task_ref: str) -> str:
|
|
828
|
+
manifest, _ = _selected_task_data(project_root, task_ref)
|
|
829
|
+
workflow = _mapping(manifest, "workflow")
|
|
830
|
+
return (
|
|
831
|
+
"# Okstra Report Input\n\n"
|
|
832
|
+
+ _line("Task key", manifest.get("taskKey"))
|
|
833
|
+
+ _line("Task type", manifest.get("taskType"))
|
|
834
|
+
+ _line("Current status", manifest.get("currentStatus"))
|
|
835
|
+
+ _line("Work status", manifest.get("workStatus"))
|
|
836
|
+
+ _line("Latest report", manifest.get("latestReportRecordPath"))
|
|
837
|
+
+ _next_phase_lines(workflow)
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _csv(value: object) -> str:
|
|
842
|
+
return ",".join(str(item) for item in value) if isinstance(value, list) else ""
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _provider_models(run: Mapping[str, Any]) -> dict[str, str]:
|
|
846
|
+
values: dict[str, set[str]] = {}
|
|
847
|
+
assignments = run.get("workerAssignments")
|
|
848
|
+
if isinstance(assignments, list):
|
|
849
|
+
for assignment in assignments:
|
|
850
|
+
if not isinstance(assignment, Mapping):
|
|
851
|
+
continue
|
|
852
|
+
provider = assignment.get("provider")
|
|
853
|
+
model = assignment.get("model")
|
|
854
|
+
if isinstance(provider, str) and isinstance(model, str) and model:
|
|
855
|
+
values.setdefault(provider, set()).add(model)
|
|
856
|
+
for provider, models in values.items():
|
|
857
|
+
if len(models) > 1:
|
|
858
|
+
raise JsonBoundaryError(
|
|
859
|
+
Path("workerAssignments"), "run manifest", f"conflicting models for provider {provider}"
|
|
860
|
+
)
|
|
861
|
+
return {provider: next(iter(models)) for provider, models in values.items()}
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def render_rerun_input(run_manifest: Path) -> str:
|
|
865
|
+
run_manifest = _owned_run_manifest_path(run_manifest)
|
|
866
|
+
run = load_owned_object(run_manifest, artifact="run manifest")
|
|
867
|
+
models = _provider_models(run)
|
|
868
|
+
team = _mapping(run, "teamContract")
|
|
869
|
+
executor = _mapping(team, "executor")
|
|
870
|
+
executor_provider = executor.get("provider")
|
|
871
|
+
executor_model = executor.get("model")
|
|
872
|
+
if (
|
|
873
|
+
isinstance(executor_provider, str)
|
|
874
|
+
and isinstance(executor_model, str)
|
|
875
|
+
and executor_model
|
|
876
|
+
and models.get(executor_provider)
|
|
877
|
+
and models[executor_provider] != executor_model
|
|
878
|
+
):
|
|
879
|
+
raise JsonBoundaryError(
|
|
880
|
+
run_manifest,
|
|
881
|
+
"run manifest",
|
|
882
|
+
f"executor model conflicts with provider {executor_provider}",
|
|
883
|
+
)
|
|
884
|
+
return (
|
|
885
|
+
"# Okstra Rerun Input\n\n"
|
|
886
|
+
+ _line("Project ID", run.get("projectId"))
|
|
887
|
+
+ _line("Task group", run.get("taskGroup"))
|
|
888
|
+
+ _line("Task ID", run.get("taskId"))
|
|
889
|
+
+ _line("Task type", run.get("taskType"))
|
|
890
|
+
+ _line("Task brief", run.get("taskBriefPath"))
|
|
891
|
+
+ _line("Recommended workers", _csv(run.get("recommendedWorkers")))
|
|
892
|
+
+ _line("Related tasks", _csv(run.get("relatedTasks")))
|
|
893
|
+
+ _line("Claude model", models.get("claude"))
|
|
894
|
+
+ _line("Codex model", models.get("codex"))
|
|
895
|
+
+ _line("Antigravity model", models.get("antigravity"))
|
|
896
|
+
+ _line("Executor provider", executor.get("provider"))
|
|
897
|
+
+ _line("Executor model", executor.get("model"))
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _symlink_ancestor(path: Path) -> Path | None:
|
|
902
|
+
lexical = Path(os.path.abspath(path))
|
|
903
|
+
current = Path(lexical.anchor)
|
|
904
|
+
for part in lexical.parts[1:-1]:
|
|
905
|
+
current /= part
|
|
906
|
+
if current.is_symlink():
|
|
907
|
+
return current
|
|
908
|
+
return None
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def render_error_zip_input() -> str:
|
|
912
|
+
home = okstra_home()
|
|
913
|
+
linked_ancestor = _symlink_ancestor(home)
|
|
914
|
+
if linked_ancestor is not None:
|
|
915
|
+
raise JsonBoundaryError(
|
|
916
|
+
home,
|
|
917
|
+
"error zip configuration",
|
|
918
|
+
f"Okstra home contains a symbolic link ({linked_ancestor})",
|
|
919
|
+
)
|
|
920
|
+
if home.is_symlink():
|
|
921
|
+
raise JsonBoundaryError(home, "error zip configuration", "Okstra home is not a fixed directory")
|
|
922
|
+
configuration = home / "error-zip.json"
|
|
923
|
+
if configuration.is_symlink():
|
|
924
|
+
raise JsonBoundaryError(
|
|
925
|
+
configuration,
|
|
926
|
+
"error zip configuration",
|
|
927
|
+
"configuration is not at its fixed path",
|
|
928
|
+
)
|
|
929
|
+
return "# Okstra Error Zip Input\n\n" + _line(
|
|
930
|
+
"Previous output path", last_output_path(home.resolve())
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def render_task_selection_input(project_root: Path, task_ref: str = "") -> str:
|
|
935
|
+
tasks = _catalog_tasks(project_root)
|
|
936
|
+
token = task_ref.casefold()
|
|
937
|
+
matches: list[tuple[Mapping[str, Any], str]] = []
|
|
938
|
+
for task in tasks:
|
|
939
|
+
task_id = str(task.get("taskId") or "")
|
|
940
|
+
task_group = str(task.get("taskGroup") or "")
|
|
941
|
+
via = "taskId" if token and task_id.casefold() == token else ""
|
|
942
|
+
via = "taskGroup" if token and task_group.casefold() == token else via
|
|
943
|
+
if not token or via:
|
|
944
|
+
matches.append((task, via or "catalog"))
|
|
945
|
+
matches.sort(key=lambda item: (_value(item[0].get("updatedAt")), _value(item[0].get("taskKey"))), reverse=True)
|
|
946
|
+
lines = ["# Okstra Task Selection Input\n\n", _line("Match count", len(matches))]
|
|
947
|
+
for task, via in matches[:10]:
|
|
948
|
+
lines.extend(
|
|
949
|
+
(
|
|
950
|
+
_line("Task", task.get("taskKey")),
|
|
951
|
+
_line("Updated at", task.get("updatedAt")),
|
|
952
|
+
_line("Matched via", via),
|
|
953
|
+
)
|
|
954
|
+
)
|
|
955
|
+
return "".join(lines)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
959
|
+
parser = argparse.ArgumentParser(
|
|
960
|
+
prog="okstra model-io",
|
|
961
|
+
description="Render fixed Markdown views of Okstra-owned JSON.",
|
|
962
|
+
)
|
|
963
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
964
|
+
project = commands.add_parser("project-context")
|
|
965
|
+
project.add_argument("--project-root", required=True)
|
|
966
|
+
project.add_argument("--task-ref", default="")
|
|
967
|
+
run = commands.add_parser("run-input")
|
|
968
|
+
run.add_argument("--run-manifest", required=True)
|
|
969
|
+
active = commands.add_parser("active-context-input")
|
|
970
|
+
active.add_argument("--project-root", required=True)
|
|
971
|
+
active.add_argument("--run-manifest", required=True)
|
|
972
|
+
schedule = commands.add_parser("schedule-input")
|
|
973
|
+
schedule.add_argument("--project-root", required=True)
|
|
974
|
+
schedule.add_argument("--task-group", required=True)
|
|
975
|
+
review = commands.add_parser("code-review-input")
|
|
976
|
+
review.add_argument("--project-root", required=True)
|
|
977
|
+
review.add_argument("--base", required=True)
|
|
978
|
+
review.add_argument("--head", required=True)
|
|
979
|
+
for name in ("status-input", "history-input", "report-input"):
|
|
980
|
+
inspect = commands.add_parser(name)
|
|
981
|
+
inspect.add_argument("--project-root", required=True)
|
|
982
|
+
inspect.add_argument("--task-ref", required=name == "report-input", default="")
|
|
983
|
+
if name != "report-input":
|
|
984
|
+
inspect.add_argument("--task-type", default="")
|
|
985
|
+
inspect.add_argument("--latest-run-status", default="")
|
|
986
|
+
inspect.add_argument("--task-group", default="")
|
|
987
|
+
if name == "history-input":
|
|
988
|
+
inspect.add_argument("--limit", type=int, default=20)
|
|
989
|
+
recap = commands.add_parser("recap-input")
|
|
990
|
+
recap.add_argument("--project-root", required=True)
|
|
991
|
+
recap.add_argument("--task-ref", required=True)
|
|
992
|
+
rerun = commands.add_parser("rerun-input")
|
|
993
|
+
rerun.add_argument("--run-manifest", required=True)
|
|
994
|
+
commands.add_parser("error-zip-input")
|
|
995
|
+
selection = commands.add_parser("task-selection-input")
|
|
996
|
+
selection.add_argument("--project-root", required=True)
|
|
997
|
+
selection.add_argument("--task-ref", default="")
|
|
998
|
+
return parser
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def main(argv: list[str] | None = None) -> int:
|
|
1002
|
+
args = build_parser().parse_args(argv)
|
|
1003
|
+
try:
|
|
1004
|
+
if args.command == "project-context":
|
|
1005
|
+
text = render_project_context(Path(args.project_root), args.task_ref)
|
|
1006
|
+
elif args.command == "run-input":
|
|
1007
|
+
text = render_run_input(Path(args.run_manifest))
|
|
1008
|
+
elif args.command == "active-context-input":
|
|
1009
|
+
text = render_active_context_input(
|
|
1010
|
+
Path(args.project_root), Path(args.run_manifest)
|
|
1011
|
+
)
|
|
1012
|
+
elif args.command == "schedule-input":
|
|
1013
|
+
text = render_schedule_input(Path(args.project_root), args.task_group)
|
|
1014
|
+
elif args.command == "code-review-input":
|
|
1015
|
+
text = render_code_review_input(Path(args.project_root), args.base, args.head)
|
|
1016
|
+
elif args.command == "status-input":
|
|
1017
|
+
text = render_status_input(
|
|
1018
|
+
Path(args.project_root), args.task_ref,
|
|
1019
|
+
task_type=args.task_type,
|
|
1020
|
+
latest_run_status=args.latest_run_status,
|
|
1021
|
+
task_group=args.task_group,
|
|
1022
|
+
)
|
|
1023
|
+
elif args.command == "history-input":
|
|
1024
|
+
text = render_history_input(
|
|
1025
|
+
Path(args.project_root), args.task_ref,
|
|
1026
|
+
task_type=args.task_type,
|
|
1027
|
+
latest_run_status=args.latest_run_status,
|
|
1028
|
+
task_group=args.task_group,
|
|
1029
|
+
limit=max(0, args.limit),
|
|
1030
|
+
)
|
|
1031
|
+
elif args.command == "recap-input":
|
|
1032
|
+
text = render_recap_input(Path(args.project_root), args.task_ref)
|
|
1033
|
+
elif args.command == "report-input":
|
|
1034
|
+
text = render_report_input(Path(args.project_root), args.task_ref)
|
|
1035
|
+
elif args.command == "rerun-input":
|
|
1036
|
+
text = render_rerun_input(Path(args.run_manifest))
|
|
1037
|
+
elif args.command == "error-zip-input":
|
|
1038
|
+
text = render_error_zip_input()
|
|
1039
|
+
elif args.command == "task-selection-input":
|
|
1040
|
+
text = render_task_selection_input(Path(args.project_root), args.task_ref)
|
|
1041
|
+
else: # argparse가 도달 불가로 만들지만 명시적 종료 경계를 유지한다.
|
|
1042
|
+
raise ValueError(f"unsupported command: {args.command}")
|
|
1043
|
+
except JsonBoundaryError as exc:
|
|
1044
|
+
print(f"model-io: {exc}", file=sys.stderr)
|
|
1045
|
+
return 2
|
|
1046
|
+
print(text, end="")
|
|
1047
|
+
return 0
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
if __name__ == "__main__":
|
|
1051
|
+
raise SystemExit(main(sys.argv[1:]))
|