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,144 @@
|
|
|
1
|
+
"""Read project-local okstra state into manager-owned snapshots."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from okstra_project import TASKS_RELATIVE, parse_task_key, read_task_catalog, read_task_manifest, slugify
|
|
7
|
+
|
|
8
|
+
from .manager_paths import children_json_path, projects_json_path, snapshots_json_path, task_manifest_path
|
|
9
|
+
from .manager_store import _now_iso, _read_json, _read_json_default, _write_json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _project_map(home: Path, manager_id: str) -> dict[str, dict]:
|
|
13
|
+
projects = _read_json_default(projects_json_path(home, manager_id), {"projects": []})
|
|
14
|
+
return {
|
|
15
|
+
str(project.get("projectId")): project
|
|
16
|
+
for project in projects.get("projects", [])
|
|
17
|
+
if isinstance(project, dict) and project.get("projectId")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _phase_outcome(manifest: dict) -> dict:
|
|
22
|
+
value = manifest.get("phaseOutcome")
|
|
23
|
+
return value if isinstance(value, dict) else {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _workflow(manifest: dict) -> dict:
|
|
27
|
+
value = manifest.get("workflow")
|
|
28
|
+
return value if isinstance(value, dict) else {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _list_field(payload: dict, field: str) -> list:
|
|
32
|
+
value = payload.get(field)
|
|
33
|
+
return list(value) if isinstance(value, list) else []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _resolve_task_root_read_only(project_root: Path, task_key: str) -> Path | None:
|
|
37
|
+
_, task_group, task_id = parse_task_key(task_key)
|
|
38
|
+
requested_key = task_key.lower()
|
|
39
|
+
for entry in read_task_catalog(project_root):
|
|
40
|
+
entry_key = entry.get("taskKey") or ""
|
|
41
|
+
if not isinstance(entry_key, str) or entry_key.lower() != requested_key:
|
|
42
|
+
continue
|
|
43
|
+
relative_path = entry.get("taskRootPath") or entry.get("taskRoot") or ""
|
|
44
|
+
if not isinstance(relative_path, str) or not relative_path:
|
|
45
|
+
continue
|
|
46
|
+
candidate = project_root / relative_path if not Path(relative_path).is_absolute() else Path(relative_path)
|
|
47
|
+
if candidate.is_dir():
|
|
48
|
+
return candidate
|
|
49
|
+
slug_path = project_root / TASKS_RELATIVE / slugify(task_group) / slugify(task_id)
|
|
50
|
+
if slug_path.is_dir():
|
|
51
|
+
return slug_path
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _snapshot_child(project_root: Path, child: dict) -> dict:
|
|
56
|
+
task_key = str(child.get("taskKey") or "")
|
|
57
|
+
base = {
|
|
58
|
+
"projectId": str(child.get("projectId") or ""),
|
|
59
|
+
"taskGroup": str(child.get("taskGroup") or ""),
|
|
60
|
+
"taskId": str(child.get("taskId") or ""),
|
|
61
|
+
"taskKey": task_key,
|
|
62
|
+
"exists": False,
|
|
63
|
+
"taskRoot": "",
|
|
64
|
+
}
|
|
65
|
+
task_dir = _resolve_task_root_read_only(project_root, task_key)
|
|
66
|
+
if task_dir is None:
|
|
67
|
+
return base
|
|
68
|
+
manifest = read_task_manifest(task_dir) or {}
|
|
69
|
+
workflow = _workflow(manifest)
|
|
70
|
+
outcome = _phase_outcome(manifest)
|
|
71
|
+
return {
|
|
72
|
+
**base,
|
|
73
|
+
"exists": True,
|
|
74
|
+
"taskRoot": str(task_dir),
|
|
75
|
+
"taskType": str(manifest.get("taskType") or ""),
|
|
76
|
+
"currentPhase": str(workflow.get("currentPhase") or ""),
|
|
77
|
+
"currentPhaseState": str(workflow.get("currentPhaseState") or ""),
|
|
78
|
+
"lastCompletedPhase": str(workflow.get("lastCompletedPhase") or ""),
|
|
79
|
+
"nextRecommendedPhase": str(workflow.get("nextRecommendedPhase") or ""),
|
|
80
|
+
"latestRunStatus": str(manifest.get("latestRunStatus") or manifest.get("currentStatus") or ""),
|
|
81
|
+
"latestReportPath": str(manifest.get("latestReportPath") or ""),
|
|
82
|
+
"finalVerdict": str(outcome.get("finalVerdict") or ""),
|
|
83
|
+
"crossProjectDependencies": _list_field(outcome, "crossProjectDependencies"),
|
|
84
|
+
"recommendedNextSteps": _list_field(outcome, "recommendedNextSteps"),
|
|
85
|
+
"openClarifications": _list_field(outcome, "openClarifications"),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def sync_task(home: Path, manager_id: str, task_group: str, task_id: str, *, now: str | None = None) -> dict:
|
|
90
|
+
_read_json(task_manifest_path(home, manager_id, task_group, task_id))
|
|
91
|
+
children = _read_json_default(children_json_path(home, manager_id, task_group, task_id), {"children": []})
|
|
92
|
+
projects = _project_map(home, manager_id)
|
|
93
|
+
rows = []
|
|
94
|
+
for child in children.get("children", []):
|
|
95
|
+
if not isinstance(child, dict):
|
|
96
|
+
continue
|
|
97
|
+
project = projects.get(str(child.get("projectId") or ""))
|
|
98
|
+
if project is None:
|
|
99
|
+
rows.append({**child, "exists": False, "error": "unknown project membership"})
|
|
100
|
+
continue
|
|
101
|
+
root = Path(str(project.get("projectRoot") or ""))
|
|
102
|
+
if not root.is_dir():
|
|
103
|
+
rows.append({**child, "exists": False, "error": f"missing project root: {root}"})
|
|
104
|
+
continue
|
|
105
|
+
rows.append(_snapshot_child(root, child))
|
|
106
|
+
payload = {
|
|
107
|
+
"managerId": manager_id,
|
|
108
|
+
"taskGroup": task_group,
|
|
109
|
+
"taskId": task_id,
|
|
110
|
+
"syncedAt": now or _now_iso(),
|
|
111
|
+
"children": rows,
|
|
112
|
+
}
|
|
113
|
+
_write_json(snapshots_json_path(home, manager_id, task_group, task_id), payload)
|
|
114
|
+
return payload
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def status_task(home: Path, manager_id: str, task_group: str, task_id: str) -> dict:
|
|
118
|
+
manifest = _read_json(task_manifest_path(home, manager_id, task_group, task_id))
|
|
119
|
+
children = _read_json_default(children_json_path(home, manager_id, task_group, task_id), {"children": []})
|
|
120
|
+
snapshot = _read_json_default(snapshots_json_path(home, manager_id, task_group, task_id), {"children": []})
|
|
121
|
+
by_key = {row.get("taskKey"): row for row in snapshot.get("children", []) if isinstance(row, dict)}
|
|
122
|
+
rows = []
|
|
123
|
+
summary = {"planned": 0, "missing": 0, "running": 0, "done": 0, "blocked": 0}
|
|
124
|
+
for child in children.get("children", []):
|
|
125
|
+
if not isinstance(child, dict):
|
|
126
|
+
continue
|
|
127
|
+
key = child.get("taskKey")
|
|
128
|
+
snapshot_row = by_key.get(key)
|
|
129
|
+
row = {**child, **(snapshot_row or {})}
|
|
130
|
+
status = str(row.get("latestRunStatus") or row.get("launch", {}).get("status") or "planned")
|
|
131
|
+
if snapshot_row is None:
|
|
132
|
+
summary["planned"] += 1
|
|
133
|
+
elif not row.get("exists", False):
|
|
134
|
+
summary["missing"] += 1
|
|
135
|
+
elif status == "done":
|
|
136
|
+
summary["done"] += 1
|
|
137
|
+
elif status in {"running", "started", "in-progress"}:
|
|
138
|
+
summary["running"] += 1
|
|
139
|
+
elif status == "blocked":
|
|
140
|
+
summary["blocked"] += 1
|
|
141
|
+
else:
|
|
142
|
+
summary["planned"] += 1
|
|
143
|
+
rows.append(row)
|
|
144
|
+
return {"manifest": manifest, "summary": summary, "children": rows, "lastSnapshot": snapshot}
|