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.
Files changed (37) hide show
  1. package/README.kr.md +3 -2
  2. package/README.md +3 -2
  3. package/docs/for-ai/skills/okstra-brief.md +18 -2
  4. package/docs/for-ai/skills/okstra-run.md +9 -1
  5. package/docs/kr/architecture/storage-model.md +7 -2
  6. package/docs/kr/architecture.md +5 -2
  7. package/docs/kr/cli.md +1 -0
  8. package/docs/project-structure-overview.md +9 -4
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/bin/lib/okstra/interactive.sh +12 -0
  12. package/runtime/prompts/profiles/_common-contract.md +4 -0
  13. package/runtime/prompts/profiles/error-analysis.md +2 -0
  14. package/runtime/prompts/profiles/improvement-discovery.md +2 -0
  15. package/runtime/prompts/profiles/requirements-discovery.md +4 -0
  16. package/runtime/prompts/wizard/prompts.ko.json +1 -1
  17. package/runtime/python/okstra_ctl/codex_dispatch.py +2 -1
  18. package/runtime/python/okstra_ctl/dispatch_core.py +4 -1
  19. package/runtime/python/okstra_ctl/implementation_outcome.py +271 -0
  20. package/runtime/python/okstra_ctl/manager_cli.py +231 -0
  21. package/runtime/python/okstra_ctl/manager_launch.py +198 -0
  22. package/runtime/python/okstra_ctl/manager_paths.py +98 -0
  23. package/runtime/python/okstra_ctl/manager_store.py +318 -0
  24. package/runtime/python/okstra_ctl/manager_sync.py +144 -0
  25. package/runtime/python/okstra_ctl/path_hints.py +665 -0
  26. package/runtime/python/okstra_ctl/render.py +4 -3
  27. package/runtime/python/okstra_ctl/run_context.py +4 -2
  28. package/runtime/python/okstra_ctl/wizard.py +118 -16
  29. package/runtime/python/okstra_project/state.py +40 -2
  30. package/runtime/skills/okstra-brief/SKILL.md +32 -7
  31. package/runtime/skills/okstra-manager/SKILL.md +44 -0
  32. package/runtime/skills/okstra-run/SKILL.md +2 -2
  33. package/runtime/templates/reports/brief.template.md +25 -0
  34. package/runtime/validators/validate-brief.py +128 -0
  35. package/src/cli-registry.mjs +7 -0
  36. package/src/commands/manager.mjs +57 -0
  37. 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}