okstra 0.103.0 → 0.104.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.103.0",
3
+ "version": "0.104.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.103.0",
3
- "builtAt": "2026-06-27T12:06:27.647Z",
2
+ "package": "0.104.0",
3
+ "builtAt": "2026-06-27T12:43:11.068Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -4,17 +4,22 @@ from __future__ import annotations
4
4
  from pathlib import Path
5
5
  from typing import Mapping
6
6
 
7
- from okstra_ctl.jsonl import read_jsonl
7
+ from okstra_ctl.jsonl import append_jsonl, read_jsonl
8
8
 
9
9
  from .manager_paths import (
10
10
  child_context_path,
11
11
  children_json_path,
12
12
  directives_jsonl_path,
13
+ events_jsonl_path,
13
14
  projects_json_path,
14
15
  snapshots_json_path,
15
16
  task_manifest_path,
16
17
  )
17
- from .manager_store import ManagerError, _now_iso, _read_json, _read_json_default
18
+ from .manager_store import ManagerError, _now_iso, _read_json, _read_json_default, _write_json
19
+
20
+
21
+ WORKER_DISPATCH_BACKEND = "subagent"
22
+ LAUNCH_STATUS_PREPARED = "prepared"
18
23
 
19
24
 
20
25
  def select_child_lead_backend(env: Mapping[str, str]) -> str:
@@ -135,6 +140,47 @@ def _render_context(
135
140
  return "\n".join(lines)
136
141
 
137
142
 
143
+ def _record_launch_prepared(
144
+ *,
145
+ home: Path,
146
+ manager_id: str,
147
+ task_group: str,
148
+ task_id: str,
149
+ children: dict,
150
+ child: dict,
151
+ packet: dict,
152
+ created_at: str,
153
+ ) -> None:
154
+ launch = child.get("launch")
155
+ if not isinstance(launch, dict):
156
+ launch = {}
157
+ launch.update(
158
+ {
159
+ "status": LAUNCH_STATUS_PREPARED,
160
+ "childLeadBackend": packet["backend"],
161
+ "workerDispatchBackend": WORKER_DISPATCH_BACKEND,
162
+ "taskKey": packet["taskKey"],
163
+ "contextPath": packet["contextPath"],
164
+ "preparedAt": created_at,
165
+ }
166
+ )
167
+ child["launch"] = launch
168
+ _write_json(children_json_path(home, manager_id, task_group, task_id), children)
169
+ append_jsonl(
170
+ events_jsonl_path(home, manager_id, task_group, task_id),
171
+ {
172
+ "event": "child-launch-prepared",
173
+ "createdAt": created_at,
174
+ "projectId": packet["projectId"],
175
+ "taskKey": packet["taskKey"],
176
+ "backend": packet["backend"],
177
+ "workerDispatchBackend": WORKER_DISPATCH_BACKEND,
178
+ "contextPath": packet["contextPath"],
179
+ "runArgs": packet["runArgs"],
180
+ },
181
+ )
182
+
183
+
138
184
  def build_launch_packet(
139
185
  home: Path,
140
186
  manager_id: str,
@@ -159,6 +205,7 @@ def build_launch_packet(
159
205
  _validate_child_task_key(child, target_task_key)
160
206
  project_root = _validated_project_root(project)
161
207
  backend = select_child_lead_backend(env or {})
208
+ created_at = now or _now_iso()
162
209
  context_path = child_context_path(home, manager_id, task_group, task_id, project_id, child_task_id_value)
163
210
  context_path.parent.mkdir(parents=True, exist_ok=True)
164
211
  context_path.write_text(
@@ -171,14 +218,14 @@ def build_launch_packet(
171
218
  ),
172
219
  encoding="utf-8",
173
220
  )
174
- return {
221
+ packet = {
175
222
  "managerId": manager_id,
176
223
  "taskGroup": task_group,
177
224
  "taskId": task_id,
178
225
  "projectId": project_id,
179
226
  "taskKey": target_task_key,
180
227
  "backend": backend,
181
- "workerDispatchBackend": "subagent",
228
+ "workerDispatchBackend": WORKER_DISPATCH_BACKEND,
182
229
  "projectRoot": project_root,
183
230
  "contextPath": str(context_path),
184
231
  "runArgs": [
@@ -194,5 +241,16 @@ def build_launch_packet(
194
241
  "--directive",
195
242
  f"Read manager child context: {context_path}",
196
243
  ],
197
- "createdAt": now or _now_iso(),
244
+ "createdAt": created_at,
198
245
  }
246
+ _record_launch_prepared(
247
+ home=home,
248
+ manager_id=manager_id,
249
+ task_group=task_group,
250
+ task_id=task_id,
251
+ children=children,
252
+ child=child,
253
+ packet=packet,
254
+ created_at=created_at,
255
+ )
256
+ return packet
@@ -1,6 +1,7 @@
1
1
  """Pure path computation for okstra manager state."""
2
2
  from __future__ import annotations
3
3
 
4
+ import hashlib
4
5
  from pathlib import Path
5
6
  import os
6
7
 
@@ -21,9 +22,11 @@ def _require_id(value: str, label: str) -> str:
21
22
 
22
23
 
23
24
  def _slug_segment(value: str, label: str) -> str:
24
- segment = slugify(_require_id(value, label))
25
+ cleaned = _require_id(value, label)
26
+ segment = slugify(cleaned)
25
27
  if not segment:
26
- raise ManagerPathError(f"{label} must contain at least one ASCII alphanumeric character")
28
+ digest = hashlib.sha1(cleaned.encode("utf-8")).hexdigest()[:12]
29
+ return f"u-{digest}"
27
30
  return segment
28
31
 
29
32
 
@@ -93,6 +96,6 @@ def child_context_path(
93
96
  project_id: str,
94
97
  child_task_id: str,
95
98
  ) -> Path:
96
- safe_project = slugify(_require_id(project_id, "project-id"))
97
- safe_task = slugify(_require_id(child_task_id, "task-id"))
99
+ safe_project = _slug_segment(project_id, "project-id")
100
+ safe_task = _slug_segment(child_task_id, "task-id")
98
101
  return task_root(home, manager_id, task_group, task_id) / "child-context" / f"{safe_project}-{safe_task}.md"
@@ -126,9 +126,11 @@ def link_project(
126
126
  role: str,
127
127
  tags: list[str],
128
128
  now: str | None = None,
129
- ) -> dict:
129
+ ) -> dict:
130
130
  load_manager(home, manager_id)
131
131
  root = Path(project_root).resolve()
132
+ if not root.is_dir():
133
+ raise ManagerError(f"projectRoot is not a directory: {root}")
132
134
  project_json = project_json_path(root)
133
135
  if project_json.is_file():
134
136
  try:
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  from pathlib import Path
5
5
 
6
- from okstra_project import TASKS_RELATIVE, parse_task_key, read_task_catalog, read_task_manifest, slugify
6
+ from okstra_project import TASKS_RELATIVE, StateError, parse_task_key, read_task_catalog, read_task_manifest, slugify
7
7
 
8
8
  from .manager_paths import children_json_path, projects_json_path, snapshots_json_path, task_manifest_path
9
9
  from .manager_store import _now_iso, _read_json, _read_json_default, _write_json
@@ -52,9 +52,9 @@ def _resolve_task_root_read_only(project_root: Path, task_key: str) -> Path | No
52
52
  return None
53
53
 
54
54
 
55
- def _snapshot_child(project_root: Path, child: dict) -> dict:
55
+ def _base_child_snapshot(child: dict) -> dict:
56
56
  task_key = str(child.get("taskKey") or "")
57
- base = {
57
+ return {
58
58
  "projectId": str(child.get("projectId") or ""),
59
59
  "taskGroup": str(child.get("taskGroup") or ""),
60
60
  "taskId": str(child.get("taskId") or ""),
@@ -62,10 +62,18 @@ def _snapshot_child(project_root: Path, child: dict) -> dict:
62
62
  "exists": False,
63
63
  "taskRoot": "",
64
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 {}
65
+
66
+
67
+ def _snapshot_child(project_root: Path, child: dict) -> dict:
68
+ base = _base_child_snapshot(child)
69
+ task_key = base["taskKey"]
70
+ try:
71
+ task_dir = _resolve_task_root_read_only(project_root, task_key)
72
+ if task_dir is None:
73
+ return base
74
+ manifest = read_task_manifest(task_dir) or {}
75
+ except StateError as exc:
76
+ return {**base, "error": str(exc)}
69
77
  workflow = _workflow(manifest)
70
78
  outcome = _phase_outcome(manifest)
71
79
  return {