okstra 0.102.3 → 0.103.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.kr.md +3 -2
- package/README.md +3 -2
- package/docs/for-ai/skills/okstra-brief.md +18 -2
- package/docs/for-ai/skills/okstra-run.md +9 -1
- package/docs/kr/architecture/storage-model.md +7 -2
- package/docs/kr/architecture.md +5 -2
- package/docs/kr/cli.md +1 -0
- package/docs/project-structure-overview.md +9 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/interactive.sh +12 -0
- package/runtime/prompts/profiles/_common-contract.md +4 -0
- package/runtime/prompts/profiles/error-analysis.md +2 -0
- package/runtime/prompts/profiles/improvement-discovery.md +2 -0
- package/runtime/prompts/profiles/requirements-discovery.md +4 -0
- package/runtime/prompts/wizard/prompts.ko.json +1 -1
- package/runtime/python/okstra_ctl/codex_dispatch.py +2 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +4 -1
- package/runtime/python/okstra_ctl/implementation_outcome.py +271 -0
- package/runtime/python/okstra_ctl/manager_cli.py +231 -0
- package/runtime/python/okstra_ctl/manager_launch.py +198 -0
- package/runtime/python/okstra_ctl/manager_paths.py +98 -0
- package/runtime/python/okstra_ctl/manager_store.py +318 -0
- package/runtime/python/okstra_ctl/manager_sync.py +144 -0
- package/runtime/python/okstra_ctl/path_hints.py +665 -0
- package/runtime/python/okstra_ctl/render.py +4 -3
- package/runtime/python/okstra_ctl/run_context.py +4 -2
- package/runtime/python/okstra_ctl/wizard.py +118 -16
- package/runtime/python/okstra_project/state.py +40 -2
- package/runtime/skills/okstra-brief/SKILL.md +32 -7
- package/runtime/skills/okstra-manager/SKILL.md +44 -0
- package/runtime/skills/okstra-run/SKILL.md +2 -2
- package/runtime/templates/reports/brief.template.md +25 -0
- package/runtime/validators/validate-brief.py +128 -0
- package/src/cli-registry.mjs +7 -0
- package/src/commands/manager.mjs +57 -0
- package/src/lib/skill-catalog.mjs +1 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Artifact-derived implementation phase outcome reconciliation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .consumers import read_stage_consumer_state
|
|
10
|
+
from .workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ImplementationOutcome:
|
|
15
|
+
completed: bool
|
|
16
|
+
stages: list[int] = field(default_factory=list)
|
|
17
|
+
head_commit: str = ""
|
|
18
|
+
report_path: str = ""
|
|
19
|
+
next_recommended_phase: str = ""
|
|
20
|
+
reason: str = ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def derive_implementation_outcome(task_root: Path) -> ImplementationOutcome:
|
|
24
|
+
task_root = Path(task_root)
|
|
25
|
+
manifest = _load_json(task_root / "task-manifest.json")
|
|
26
|
+
workflow = manifest.get("workflow") if isinstance(manifest.get("workflow"), dict) else {}
|
|
27
|
+
if workflow.get("currentPhase") != "implementation":
|
|
28
|
+
return ImplementationOutcome(completed=False, reason="current phase is not implementation")
|
|
29
|
+
|
|
30
|
+
plan_run_root = task_root / "runs" / "implementation-planning"
|
|
31
|
+
if not plan_run_root.is_dir():
|
|
32
|
+
return ImplementationOutcome(completed=False, reason="implementation-planning run root missing")
|
|
33
|
+
|
|
34
|
+
stage_map = _load_stage_map(task_root, manifest)
|
|
35
|
+
if not stage_map:
|
|
36
|
+
return ImplementationOutcome(completed=False, reason="stage map unavailable")
|
|
37
|
+
|
|
38
|
+
consumers = read_stage_consumer_state(plan_run_root, recover_from_carry=True)
|
|
39
|
+
required_stages = {stage["stage_number"] for stage in stage_map}
|
|
40
|
+
if not required_stages or not required_stages.issubset(consumers.done_stages):
|
|
41
|
+
return ImplementationOutcome(completed=False, reason="not all stages are done")
|
|
42
|
+
|
|
43
|
+
carries = [_load_carry(task_root, stage) for stage in sorted(required_stages)]
|
|
44
|
+
if any(carry is None for carry in carries):
|
|
45
|
+
return ImplementationOutcome(completed=False, reason="carry sidecar missing")
|
|
46
|
+
if any(not _carry_passed(carry) for carry in carries if carry is not None):
|
|
47
|
+
return ImplementationOutcome(completed=False, reason="carry sidecar is not pass-grade")
|
|
48
|
+
|
|
49
|
+
latest_stage = max(required_stages)
|
|
50
|
+
latest_done = consumers.done_by_stage.get(latest_stage) or {}
|
|
51
|
+
latest_carry = carries[-1] or {}
|
|
52
|
+
return ImplementationOutcome(
|
|
53
|
+
completed=True,
|
|
54
|
+
stages=sorted(required_stages),
|
|
55
|
+
head_commit=str(latest_done.get("head_commit") or "") or _carry_head_commit(latest_carry),
|
|
56
|
+
report_path=_latest_implementation_report(task_root),
|
|
57
|
+
next_recommended_phase=DEFAULT_NEXT_PHASE["implementation"],
|
|
58
|
+
reason="all implementation stages have pass-grade carry and done rows",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def reconcile_implementation_outcome(project_root: Path, task_root: Path) -> bool:
|
|
63
|
+
project_root = Path(project_root)
|
|
64
|
+
task_root = Path(task_root)
|
|
65
|
+
outcome = derive_implementation_outcome(task_root)
|
|
66
|
+
if not outcome.completed:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
manifest_path = task_root / "task-manifest.json"
|
|
70
|
+
manifest = _load_json(manifest_path)
|
|
71
|
+
workflow = manifest.get("workflow")
|
|
72
|
+
if not isinstance(workflow, dict):
|
|
73
|
+
workflow = {}
|
|
74
|
+
manifest["workflow"] = workflow
|
|
75
|
+
|
|
76
|
+
changed = _promote_workflow(workflow, outcome)
|
|
77
|
+
phase_outcome = manifest.get("phaseOutcome")
|
|
78
|
+
if not isinstance(phase_outcome, dict):
|
|
79
|
+
phase_outcome = {}
|
|
80
|
+
manifest["phaseOutcome"] = phase_outcome
|
|
81
|
+
|
|
82
|
+
contract = manifest.get("contractValidation")
|
|
83
|
+
contract_status = contract.get("status", "") if isinstance(contract, dict) else ""
|
|
84
|
+
implementation = {
|
|
85
|
+
"state": "completed",
|
|
86
|
+
"source": "implementation-carry",
|
|
87
|
+
"stages": outcome.stages,
|
|
88
|
+
"headCommit": outcome.head_commit,
|
|
89
|
+
"reportPath": outcome.report_path,
|
|
90
|
+
"contractValidationStatus": contract_status,
|
|
91
|
+
"reason": outcome.reason,
|
|
92
|
+
}
|
|
93
|
+
if phase_outcome.get("implementation") != implementation:
|
|
94
|
+
phase_outcome["implementation"] = implementation
|
|
95
|
+
changed = True
|
|
96
|
+
|
|
97
|
+
if changed:
|
|
98
|
+
_write_json(manifest_path, manifest)
|
|
99
|
+
return changed or _update_task_catalog(project_root, manifest)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _load_json(path: Path) -> dict[str, Any]:
|
|
103
|
+
if not path.is_file():
|
|
104
|
+
return {}
|
|
105
|
+
try:
|
|
106
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
107
|
+
except json.JSONDecodeError:
|
|
108
|
+
return {}
|
|
109
|
+
return data if isinstance(data, dict) else {}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _load_stage_map(task_root: Path, manifest: dict[str, Any]) -> list[dict[str, Any]]:
|
|
113
|
+
plan_path = _source_plan_path(task_root, manifest)
|
|
114
|
+
if not plan_path.is_file():
|
|
115
|
+
return []
|
|
116
|
+
from .run import PrepareError, _load_parsed_stage_map, _stage_map_reject_detail
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
stages, errs = _load_parsed_stage_map(plan_path.read_text(encoding="utf-8"))
|
|
120
|
+
except PrepareError:
|
|
121
|
+
return []
|
|
122
|
+
if _stage_map_reject_detail(stages, errs) is not None:
|
|
123
|
+
return []
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
"stage_number": stage.stage_number,
|
|
127
|
+
"title": stage.title,
|
|
128
|
+
"depends_on": list(stage.depends_on),
|
|
129
|
+
"step_count": stage.step_count,
|
|
130
|
+
}
|
|
131
|
+
for stage in stages
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _source_plan_path(task_root: Path, manifest: dict[str, Any]) -> Path:
|
|
136
|
+
carry_dir = task_root / "runs" / "implementation" / "carry"
|
|
137
|
+
for carry_path in sorted(carry_dir.glob("stage-*.json")):
|
|
138
|
+
value = _load_json(carry_path).get("sourcePlanPath")
|
|
139
|
+
if isinstance(value, str) and value:
|
|
140
|
+
return _resolve_relative(task_root, value)
|
|
141
|
+
|
|
142
|
+
latest = manifest.get("latestReportPath")
|
|
143
|
+
if isinstance(latest, str) and "implementation-planning" in latest:
|
|
144
|
+
return _resolve_relative(task_root, latest)
|
|
145
|
+
|
|
146
|
+
reports = task_root / "runs" / "implementation-planning" / "reports"
|
|
147
|
+
candidates = sorted(reports.glob("final-report-implementation-planning-*.md"))
|
|
148
|
+
return candidates[-1] if candidates else Path()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _resolve_relative(task_root: Path, value: str) -> Path:
|
|
152
|
+
path = Path(value)
|
|
153
|
+
if path.is_absolute():
|
|
154
|
+
return path
|
|
155
|
+
project_root = _project_root_from_task_root(task_root)
|
|
156
|
+
project_relative = project_root / path
|
|
157
|
+
if project_relative.exists():
|
|
158
|
+
return project_relative
|
|
159
|
+
task_relative = task_root / path
|
|
160
|
+
if task_relative.exists():
|
|
161
|
+
return task_relative
|
|
162
|
+
return project_relative
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _project_root_from_task_root(task_root: Path) -> Path:
|
|
166
|
+
parts = task_root.resolve().parts
|
|
167
|
+
if ".okstra" not in parts:
|
|
168
|
+
return task_root.resolve()
|
|
169
|
+
return Path(*parts[: parts.index(".okstra")])
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _load_carry(task_root: Path, stage: int) -> dict[str, Any] | None:
|
|
173
|
+
data = _load_json(task_root / "runs" / "implementation" / "carry" / f"stage-{stage}.json")
|
|
174
|
+
return data or None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _carry_passed(carry: dict[str, Any]) -> bool:
|
|
178
|
+
status = str(carry.get("status") or "").lower()
|
|
179
|
+
if status in {"fail", "failed", "blocked", "error", "aborted"}:
|
|
180
|
+
return False
|
|
181
|
+
conformance = carry.get("conformance")
|
|
182
|
+
if isinstance(conformance, dict):
|
|
183
|
+
overall = str(conformance.get("overall") or "").upper()
|
|
184
|
+
if overall and overall != "PASS":
|
|
185
|
+
return False
|
|
186
|
+
unverified = carry.get("unverified")
|
|
187
|
+
return not (isinstance(unverified, list) and unverified)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _carry_head_commit(carry: dict[str, Any]) -> str:
|
|
191
|
+
stage_range = carry.get("stageCommitRange")
|
|
192
|
+
if isinstance(stage_range, dict) and stage_range.get("head"):
|
|
193
|
+
return str(stage_range["head"])
|
|
194
|
+
for key in ("head_sha", "head_commit", "head"):
|
|
195
|
+
value = carry.get(key)
|
|
196
|
+
if value:
|
|
197
|
+
return str(value)
|
|
198
|
+
commits = carry.get("commits")
|
|
199
|
+
if isinstance(commits, list) and commits:
|
|
200
|
+
last = commits[-1]
|
|
201
|
+
if isinstance(last, dict) and last.get("sha"):
|
|
202
|
+
return str(last["sha"])
|
|
203
|
+
return ""
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _latest_implementation_report(task_root: Path) -> str:
|
|
207
|
+
reports = sorted(
|
|
208
|
+
(task_root / "runs" / "implementation").glob(
|
|
209
|
+
"stage-*/reports/final-report-implementation-*.md"
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
if not reports:
|
|
213
|
+
return ""
|
|
214
|
+
project_root = _project_root_from_task_root(task_root)
|
|
215
|
+
try:
|
|
216
|
+
return str(reports[-1].relative_to(project_root))
|
|
217
|
+
except ValueError:
|
|
218
|
+
return str(reports[-1])
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _promote_workflow(workflow: dict[str, Any], outcome: ImplementationOutcome) -> bool:
|
|
222
|
+
before = json.dumps(workflow, sort_keys=True, ensure_ascii=False)
|
|
223
|
+
phase_states = workflow.get("phaseStates")
|
|
224
|
+
if not isinstance(phase_states, dict):
|
|
225
|
+
phase_states = {}
|
|
226
|
+
workflow["phaseStates"] = phase_states
|
|
227
|
+
for phase in PHASE_SEQUENCE:
|
|
228
|
+
phase_states.setdefault(phase, "not-started")
|
|
229
|
+
workflow["currentPhase"] = "implementation"
|
|
230
|
+
workflow["currentPhaseState"] = "completed"
|
|
231
|
+
phase_states["implementation"] = "completed"
|
|
232
|
+
workflow["lastCompletedPhase"] = "implementation"
|
|
233
|
+
workflow["nextRecommendedPhase"] = outcome.next_recommended_phase
|
|
234
|
+
after = json.dumps(workflow, sort_keys=True, ensure_ascii=False)
|
|
235
|
+
return before != after
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _update_task_catalog(project_root: Path, manifest: dict[str, Any]) -> bool:
|
|
239
|
+
catalog_path = project_root / ".okstra" / "discovery" / "task-catalog.json"
|
|
240
|
+
catalog = _load_json(catalog_path)
|
|
241
|
+
tasks = catalog.get("tasks")
|
|
242
|
+
if not isinstance(tasks, list):
|
|
243
|
+
return False
|
|
244
|
+
task_key = manifest.get("taskKey")
|
|
245
|
+
workflow = manifest.get("workflow") if isinstance(manifest.get("workflow"), dict) else {}
|
|
246
|
+
outcome = manifest.get("phaseOutcome") if isinstance(manifest.get("phaseOutcome"), dict) else {}
|
|
247
|
+
for entry in tasks:
|
|
248
|
+
if not isinstance(entry, dict) or entry.get("taskKey") != task_key:
|
|
249
|
+
continue
|
|
250
|
+
before = json.dumps(entry, sort_keys=True, ensure_ascii=False)
|
|
251
|
+
entry["currentPhase"] = workflow.get("currentPhase", "")
|
|
252
|
+
entry["currentPhaseState"] = workflow.get("currentPhaseState", "")
|
|
253
|
+
entry["lastCompletedPhase"] = workflow.get("lastCompletedPhase", "")
|
|
254
|
+
entry["nextRecommendedPhase"] = workflow.get("nextRecommendedPhase", "")
|
|
255
|
+
entry["latestReportPath"] = manifest.get("latestReportPath", "")
|
|
256
|
+
entry["latestRunStatus"] = manifest.get("latestRunStatus", entry.get("latestRunStatus", ""))
|
|
257
|
+
entry["currentStatus"] = manifest.get("currentStatus", entry.get("currentStatus", ""))
|
|
258
|
+
entry["phaseOutcome"] = outcome
|
|
259
|
+
if before == json.dumps(entry, sort_keys=True, ensure_ascii=False):
|
|
260
|
+
return False
|
|
261
|
+
_write_json(catalog_path, catalog)
|
|
262
|
+
return True
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
267
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
268
|
+
path.write_text(
|
|
269
|
+
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
|
270
|
+
encoding="utf-8",
|
|
271
|
+
)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""CLI entrypoint for okstra manager."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from okstra_project.dirs import okstra_home
|
|
11
|
+
|
|
12
|
+
from .manager_launch import build_launch_packet
|
|
13
|
+
from .manager_store import (
|
|
14
|
+
ManagerError,
|
|
15
|
+
append_directive,
|
|
16
|
+
assign_child,
|
|
17
|
+
create_task,
|
|
18
|
+
create_task_group,
|
|
19
|
+
discover_projects,
|
|
20
|
+
init_manager,
|
|
21
|
+
link_project,
|
|
22
|
+
)
|
|
23
|
+
from .manager_sync import status_task, sync_task
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _emit(payload: dict | list[dict], as_json: bool) -> None:
|
|
27
|
+
if as_json:
|
|
28
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
29
|
+
return
|
|
30
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _normalize_global_flags(argv: list[str] | None) -> list[str]:
|
|
34
|
+
values = list(argv or [])
|
|
35
|
+
out: list[str] = []
|
|
36
|
+
json_seen = False
|
|
37
|
+
for value in values:
|
|
38
|
+
if value == "--json":
|
|
39
|
+
json_seen = True
|
|
40
|
+
continue
|
|
41
|
+
out.append(value)
|
|
42
|
+
return (["--json"] if json_seen else []) + out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_child_specs(values: list[str], task_group: str, default_task_id: str) -> list[dict]:
|
|
46
|
+
specs: list[dict] = []
|
|
47
|
+
for value in values:
|
|
48
|
+
parts = value.split(":")
|
|
49
|
+
if len(parts) == 1:
|
|
50
|
+
specs.append({"projectId": parts[0], "taskId": default_task_id})
|
|
51
|
+
continue
|
|
52
|
+
if len(parts) == 2:
|
|
53
|
+
specs.append({"projectId": parts[0], "taskId": parts[1]})
|
|
54
|
+
continue
|
|
55
|
+
if len(parts) == 3:
|
|
56
|
+
project_id, embedded_task_group, child_task_id = parts
|
|
57
|
+
if embedded_task_group != task_group:
|
|
58
|
+
raise ManagerError(
|
|
59
|
+
f"task-group mismatch for --task {value!r}: expected {task_group!r}, got {embedded_task_group!r}"
|
|
60
|
+
)
|
|
61
|
+
specs.append({"projectId": project_id, "taskId": child_task_id})
|
|
62
|
+
continue
|
|
63
|
+
raise ManagerError(
|
|
64
|
+
f"invalid --task {value!r}: expected project-id, project-id:task-id, or project-id:task-group:task-id"
|
|
65
|
+
)
|
|
66
|
+
return specs
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _parser() -> argparse.ArgumentParser:
|
|
70
|
+
parser = argparse.ArgumentParser(prog="okstra manager")
|
|
71
|
+
parser.add_argument("--json", action="store_true")
|
|
72
|
+
parser.add_argument("--workspace-root", default="")
|
|
73
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
74
|
+
|
|
75
|
+
init = sub.add_parser("init")
|
|
76
|
+
init.add_argument("--manager-id", required=True)
|
|
77
|
+
|
|
78
|
+
sub.add_parser("discover-projects")
|
|
79
|
+
|
|
80
|
+
new = sub.add_parser("new")
|
|
81
|
+
new_sub = new.add_subparsers(dest="new_command", required=True)
|
|
82
|
+
|
|
83
|
+
project = new_sub.add_parser("project")
|
|
84
|
+
project.add_argument("--manager-id", required=True)
|
|
85
|
+
project.add_argument("--project-id", required=True)
|
|
86
|
+
project.add_argument("--project-root", required=True)
|
|
87
|
+
project.add_argument("--role", default="")
|
|
88
|
+
project.add_argument("--tag", action="append", default=[])
|
|
89
|
+
|
|
90
|
+
group = new_sub.add_parser("task-group")
|
|
91
|
+
group.add_argument("--manager-id", required=True)
|
|
92
|
+
group.add_argument("--task-group", required=True)
|
|
93
|
+
|
|
94
|
+
task_new = new_sub.add_parser("task")
|
|
95
|
+
task_new.add_argument("--manager-id", required=True)
|
|
96
|
+
task_new.add_argument("--task-group", required=True)
|
|
97
|
+
task_new.add_argument("--task-id", required=True)
|
|
98
|
+
task_new.add_argument("--task", action="append", default=[])
|
|
99
|
+
task_new.add_argument("--objective", default="")
|
|
100
|
+
task_new.add_argument("--common-brief", default="")
|
|
101
|
+
task_new.add_argument("--progress-mode", choices=["manual", "auto"], default="manual")
|
|
102
|
+
|
|
103
|
+
task = sub.add_parser("task")
|
|
104
|
+
task_sub = task.add_subparsers(dest="task_command", required=True)
|
|
105
|
+
|
|
106
|
+
for name in ("status", "sync"):
|
|
107
|
+
command = task_sub.add_parser(name)
|
|
108
|
+
command.add_argument("--manager-id", required=True)
|
|
109
|
+
command.add_argument("--task-group", required=True)
|
|
110
|
+
command.add_argument("--task-id", required=True)
|
|
111
|
+
|
|
112
|
+
assign = task_sub.add_parser("assign")
|
|
113
|
+
assign.add_argument("--manager-id", required=True)
|
|
114
|
+
assign.add_argument("--task-group", required=True)
|
|
115
|
+
assign.add_argument("--task-id", required=True)
|
|
116
|
+
assign.add_argument("--project-id", required=True)
|
|
117
|
+
assign.add_argument("--child-task-id", default=None)
|
|
118
|
+
assign.add_argument("--role", default="")
|
|
119
|
+
assign.add_argument("--tag", action="append", default=[])
|
|
120
|
+
assign.add_argument("--assignment", default="")
|
|
121
|
+
|
|
122
|
+
note = task_sub.add_parser("note")
|
|
123
|
+
note.add_argument("--manager-id", required=True)
|
|
124
|
+
note.add_argument("--task-group", required=True)
|
|
125
|
+
note.add_argument("--task-id", required=True)
|
|
126
|
+
note.add_argument("--scope", choices=["shared", "project"], required=True)
|
|
127
|
+
note.add_argument("--project-id", default="")
|
|
128
|
+
note.add_argument("--body", required=True)
|
|
129
|
+
|
|
130
|
+
run = task_sub.add_parser("run")
|
|
131
|
+
run.add_argument("--manager-id", required=True)
|
|
132
|
+
run.add_argument("--project-id", required=True)
|
|
133
|
+
run.add_argument("--task-group", required=True)
|
|
134
|
+
run.add_argument("--task-id", required=True)
|
|
135
|
+
run.add_argument("--child-task-id", default=None)
|
|
136
|
+
return parser
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def main(argv: list[str] | None = None) -> int:
|
|
140
|
+
parser = _parser()
|
|
141
|
+
args = parser.parse_args(_normalize_global_flags(argv if argv is not None else sys.argv[1:]))
|
|
142
|
+
home = okstra_home()
|
|
143
|
+
try:
|
|
144
|
+
if args.command == "init":
|
|
145
|
+
_emit(init_manager(home, args.manager_id), args.json)
|
|
146
|
+
elif args.command == "discover-projects":
|
|
147
|
+
_emit(discover_projects(home), args.json)
|
|
148
|
+
elif args.command == "new" and args.new_command == "project":
|
|
149
|
+
_emit(
|
|
150
|
+
link_project(
|
|
151
|
+
home,
|
|
152
|
+
args.manager_id,
|
|
153
|
+
args.project_id,
|
|
154
|
+
Path(args.project_root),
|
|
155
|
+
role=args.role,
|
|
156
|
+
tags=args.tag,
|
|
157
|
+
),
|
|
158
|
+
args.json,
|
|
159
|
+
)
|
|
160
|
+
elif args.command == "new" and args.new_command == "task-group":
|
|
161
|
+
_emit(create_task_group(home, args.manager_id, args.task_group), args.json)
|
|
162
|
+
elif args.command == "new" and args.new_command == "task":
|
|
163
|
+
_emit(
|
|
164
|
+
create_task(
|
|
165
|
+
home,
|
|
166
|
+
args.manager_id,
|
|
167
|
+
args.task_group,
|
|
168
|
+
args.task_id,
|
|
169
|
+
_parse_child_specs(args.task, args.task_group, args.task_id),
|
|
170
|
+
objective=args.objective,
|
|
171
|
+
common_brief_path=args.common_brief,
|
|
172
|
+
progress_mode=args.progress_mode,
|
|
173
|
+
),
|
|
174
|
+
args.json,
|
|
175
|
+
)
|
|
176
|
+
elif args.command == "task" and args.task_command == "assign":
|
|
177
|
+
_emit(
|
|
178
|
+
assign_child(
|
|
179
|
+
home,
|
|
180
|
+
args.manager_id,
|
|
181
|
+
args.task_group,
|
|
182
|
+
args.task_id,
|
|
183
|
+
args.project_id,
|
|
184
|
+
role=args.role,
|
|
185
|
+
tags=args.tag,
|
|
186
|
+
assignment=args.assignment,
|
|
187
|
+
child_task_id=args.child_task_id,
|
|
188
|
+
),
|
|
189
|
+
args.json,
|
|
190
|
+
)
|
|
191
|
+
elif args.command == "task" and args.task_command == "note":
|
|
192
|
+
_emit(
|
|
193
|
+
append_directive(
|
|
194
|
+
home,
|
|
195
|
+
args.manager_id,
|
|
196
|
+
args.task_group,
|
|
197
|
+
args.task_id,
|
|
198
|
+
scope=args.scope,
|
|
199
|
+
project_id=args.project_id,
|
|
200
|
+
body=args.body,
|
|
201
|
+
),
|
|
202
|
+
args.json,
|
|
203
|
+
)
|
|
204
|
+
elif args.command == "task" and args.task_command == "sync":
|
|
205
|
+
_emit(sync_task(home, args.manager_id, args.task_group, args.task_id), args.json)
|
|
206
|
+
elif args.command == "task" and args.task_command == "status":
|
|
207
|
+
_emit(status_task(home, args.manager_id, args.task_group, args.task_id), args.json)
|
|
208
|
+
elif args.command == "task" and args.task_command == "run":
|
|
209
|
+
_emit(
|
|
210
|
+
build_launch_packet(
|
|
211
|
+
home,
|
|
212
|
+
args.manager_id,
|
|
213
|
+
args.task_group,
|
|
214
|
+
args.task_id,
|
|
215
|
+
args.project_id,
|
|
216
|
+
child_task_id=args.child_task_id,
|
|
217
|
+
env=os.environ,
|
|
218
|
+
),
|
|
219
|
+
args.json,
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
parser.print_help()
|
|
223
|
+
return 2
|
|
224
|
+
return 0
|
|
225
|
+
except ManagerError as exc:
|
|
226
|
+
print(f"okstra manager: {exc}", file=sys.stderr)
|
|
227
|
+
return 2
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
raise SystemExit(main(sys.argv[1:]))
|