okstra 0.20.1 → 0.21.1
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 +2 -2
- package/README.md +2 -2
- package/docs/kr/architecture.md +1 -0
- package/docs/kr/cli.md +1 -1
- package/docs/kr/performance-improvement-plan-v2.md +330 -0
- package/docs/kr/performance-improvement-plan.md +125 -0
- package/docs/project-structure-overview.md +388 -0
- package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +1568 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +7 -1
- package/runtime/agents/workers/claude-worker.md +3 -1
- package/runtime/agents/workers/report-writer-worker.md +4 -0
- package/runtime/bin/okstra-codex-exec.sh +42 -0
- package/runtime/bin/okstra-gemini-exec.sh +7 -0
- package/runtime/bin/okstra-trace-cleanup.sh +42 -0
- package/runtime/prompts/profiles/final-verification.md +8 -2
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +26 -28
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/python/okstra_ctl/render.py +78 -4
- package/runtime/python/okstra_ctl/run_context.py +5 -0
- package/runtime/python/okstra_ctl/workflow.py +8 -7
- package/runtime/python/okstra_ctl/worktree.py +155 -12
- package/runtime/skills/okstra-brief/SKILL.md +523 -0
- package/runtime/skills/okstra-convergence/SKILL.md +149 -37
- package/runtime/skills/okstra-report-writer/SKILL.md +8 -6
- package/runtime/templates/prd/brief.template.md +12 -0
- package/runtime/templates/project-docs/task-index.template.md +12 -0
- package/runtime/templates/reports/error-analysis-input.template.md +12 -0
- package/runtime/templates/reports/final-report.template.md +39 -12
- package/runtime/templates/reports/final-verification-input.template.md +22 -0
- package/runtime/templates/reports/implementation-input.template.md +12 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +12 -0
- package/runtime/templates/reports/quick-input.template.md +12 -0
- package/runtime/templates/reports/release-handoff-input.template.md +23 -10
- package/runtime/templates/reports/schedule.template.md +12 -0
- package/runtime/templates/reports/settings.template.json +92 -30
- package/runtime/templates/reports/task-brief.template.md +12 -0
- package/src/install.mjs +1 -0
- package/src/uninstall.mjs +1 -0
|
@@ -34,6 +34,8 @@ from __future__ import annotations
|
|
|
34
34
|
|
|
35
35
|
import json
|
|
36
36
|
import os
|
|
37
|
+
import shutil
|
|
38
|
+
import stat
|
|
37
39
|
import subprocess
|
|
38
40
|
from dataclasses import dataclass
|
|
39
41
|
from pathlib import Path
|
|
@@ -68,6 +70,35 @@ DEFAULT_WORKTREE_SYNC_DIRS: tuple[str, ...] = (
|
|
|
68
70
|
)
|
|
69
71
|
|
|
70
72
|
|
|
73
|
+
# Project-root-relative FILES (not dirs) symlinked from MAIN → task worktree
|
|
74
|
+
# at provision time. Same symlink semantics as `DEFAULT_WORKTREE_SYNC_DIRS`:
|
|
75
|
+
# every task sees the live shared file. The split exists because the original
|
|
76
|
+
# `_link_sync_dirs` helper only walked directories — a `.env` (or any
|
|
77
|
+
# top-level file outside `.git`'s tracking) would otherwise be lost on every
|
|
78
|
+
# new worktree, blocking verifier dispatches that depend on environment-
|
|
79
|
+
# resolved secrets.
|
|
80
|
+
#
|
|
81
|
+
# Override precedence mirrors `DEFAULT_WORKTREE_SYNC_DIRS`:
|
|
82
|
+
# 1. `OKSTRA_WORKTREE_SYNC_FILES` env var (colon-separated, REPLACES).
|
|
83
|
+
# 2. `worktreeSyncFiles` array in `.project-docs/okstra/project.json`.
|
|
84
|
+
# 3. The built-in `DEFAULT_WORKTREE_SYNC_FILES` below.
|
|
85
|
+
DEFAULT_WORKTREE_SYNC_FILES: tuple[str, ...] = (
|
|
86
|
+
".env",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Project-root-relative files COPIED (not symlinked) from MAIN → task worktree
|
|
91
|
+
# at provision time, then `chmod 0o444` so the task cannot mutate the
|
|
92
|
+
# snapshot. Used for live-mutating fixtures (e.g. `classifications.db`)
|
|
93
|
+
# where the verifier needs to reproduce accuracy SQL against the same rows
|
|
94
|
+
# the executor saw, without sharing the writable handle that would corrupt
|
|
95
|
+
# the main worktree's copy. Default is empty — okstra has no opinion about
|
|
96
|
+
# which fixtures any given project relies on; opt in per-project via
|
|
97
|
+
# `worktreeSnapshotFiles` in `project.json` (or `OKSTRA_WORKTREE_SNAPSHOT_FILES`
|
|
98
|
+
# for one-off operator override).
|
|
99
|
+
DEFAULT_WORKTREE_SNAPSHOT_FILES: tuple[str, ...] = ()
|
|
100
|
+
|
|
101
|
+
|
|
71
102
|
# Work-category → short branch prefix. Mirrors the values accepted by
|
|
72
103
|
# `--work-category` (bugfix / feature / refactor / ops / improvement) and
|
|
73
104
|
# falls back to `task` when the category is unset or unrecognised.
|
|
@@ -187,14 +218,14 @@ def _main_worktree_path(project_root: Path) -> Path:
|
|
|
187
218
|
return project_root
|
|
188
219
|
|
|
189
220
|
|
|
190
|
-
def
|
|
191
|
-
"""Read
|
|
221
|
+
def _read_project_json_field(project_root: Path, field: str) -> Optional[tuple[str, ...]]:
|
|
222
|
+
"""Read a string-array field from `.project-docs/okstra/project.json`.
|
|
192
223
|
|
|
193
224
|
Returns None if the field is absent or the file cannot be parsed (so
|
|
194
225
|
the caller falls back to defaults). Returns an empty tuple if the
|
|
195
226
|
field is explicitly an empty array (caller treats this as "disable").
|
|
196
227
|
A non-list value is treated as missing — we do not raise here because
|
|
197
|
-
|
|
228
|
+
field resolution must never block worktree provisioning.
|
|
198
229
|
"""
|
|
199
230
|
target = project_root / ".project-docs" / "okstra" / "project.json"
|
|
200
231
|
if not target.is_file():
|
|
@@ -205,7 +236,7 @@ def _read_project_json_sync_dirs(project_root: Path) -> Optional[tuple[str, ...]
|
|
|
205
236
|
return None
|
|
206
237
|
if not isinstance(data, dict):
|
|
207
238
|
return None
|
|
208
|
-
value = data.get(
|
|
239
|
+
value = data.get(field)
|
|
209
240
|
if not isinstance(value, list):
|
|
210
241
|
return None
|
|
211
242
|
cleaned = tuple(
|
|
@@ -215,22 +246,68 @@ def _read_project_json_sync_dirs(project_root: Path) -> Optional[tuple[str, ...]
|
|
|
215
246
|
return cleaned
|
|
216
247
|
|
|
217
248
|
|
|
218
|
-
def
|
|
219
|
-
"""
|
|
220
|
-
|
|
221
|
-
|
|
249
|
+
def _read_project_json_sync_dirs(project_root: Path) -> Optional[tuple[str, ...]]:
|
|
250
|
+
"""Back-compat shim — preserves the old name used by external callers."""
|
|
251
|
+
return _read_project_json_field(project_root, "worktreeSyncDirs")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _resolve_entries(
|
|
255
|
+
*,
|
|
256
|
+
env_var: str,
|
|
257
|
+
project_field: str,
|
|
258
|
+
default: tuple[str, ...],
|
|
259
|
+
project_root: Optional[Path],
|
|
260
|
+
) -> tuple[str, ...]:
|
|
261
|
+
"""Generic resolver shared by sync-dirs / sync-files / snapshot-files.
|
|
262
|
+
|
|
263
|
+
Precedence: env var (colon-separated, REPLACES) → project.json field →
|
|
264
|
+
built-in default. An empty env value or empty JSON array disables the
|
|
265
|
+
feature (returns `()`).
|
|
222
266
|
"""
|
|
223
|
-
raw = os.environ.get(
|
|
267
|
+
raw = os.environ.get(env_var)
|
|
224
268
|
if raw is not None:
|
|
225
269
|
raw = raw.strip()
|
|
226
270
|
if not raw:
|
|
227
271
|
return ()
|
|
228
272
|
return tuple(part for part in (p.strip() for p in raw.split(":")) if part)
|
|
229
273
|
if project_root is not None:
|
|
230
|
-
from_project =
|
|
274
|
+
from_project = _read_project_json_field(project_root, project_field)
|
|
231
275
|
if from_project is not None:
|
|
232
276
|
return from_project
|
|
233
|
-
return
|
|
277
|
+
return default
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _resolve_sync_dirs(project_root: Optional[Path] = None) -> tuple[str, ...]:
|
|
281
|
+
"""Return the list of project-root-relative dirs to symlink into the
|
|
282
|
+
new worktree. Precedence: env var → project.json → built-in default.
|
|
283
|
+
See the comment above `DEFAULT_WORKTREE_SYNC_DIRS` for full semantics.
|
|
284
|
+
"""
|
|
285
|
+
return _resolve_entries(
|
|
286
|
+
env_var="OKSTRA_WORKTREE_SYNC_DIRS",
|
|
287
|
+
project_field="worktreeSyncDirs",
|
|
288
|
+
default=DEFAULT_WORKTREE_SYNC_DIRS,
|
|
289
|
+
project_root=project_root,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _resolve_sync_files(project_root: Optional[Path] = None) -> tuple[str, ...]:
|
|
294
|
+
"""File-level counterpart to `_resolve_sync_dirs` (FU-V2)."""
|
|
295
|
+
return _resolve_entries(
|
|
296
|
+
env_var="OKSTRA_WORKTREE_SYNC_FILES",
|
|
297
|
+
project_field="worktreeSyncFiles",
|
|
298
|
+
default=DEFAULT_WORKTREE_SYNC_FILES,
|
|
299
|
+
project_root=project_root,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _resolve_snapshot_files(project_root: Optional[Path] = None) -> tuple[str, ...]:
|
|
304
|
+
"""Read-only snapshot file list (FU-V3)."""
|
|
305
|
+
return _resolve_entries(
|
|
306
|
+
env_var="OKSTRA_WORKTREE_SNAPSHOT_FILES",
|
|
307
|
+
project_field="worktreeSnapshotFiles",
|
|
308
|
+
default=DEFAULT_WORKTREE_SNAPSHOT_FILES,
|
|
309
|
+
project_root=project_root,
|
|
310
|
+
)
|
|
234
311
|
|
|
235
312
|
|
|
236
313
|
def _link_sync_dirs(source_root: Path, worktree_path: Path) -> list[str]:
|
|
@@ -261,6 +338,63 @@ def _link_sync_dirs(source_root: Path, worktree_path: Path) -> list[str]:
|
|
|
261
338
|
return notes
|
|
262
339
|
|
|
263
340
|
|
|
341
|
+
def _link_sync_files(source_root: Path, worktree_path: Path) -> list[str]:
|
|
342
|
+
"""File-level counterpart to `_link_sync_dirs` (FU-V2).
|
|
343
|
+
|
|
344
|
+
Same skip rules: missing source → skipped silently; pre-existing dst
|
|
345
|
+
(tracked content checked out by `git worktree add`) → skipped to avoid
|
|
346
|
+
clobbering. Symlinks (not copies) keep secrets out of duplicate files —
|
|
347
|
+
the link points back to the MAIN worktree's `.env` so rotating the file
|
|
348
|
+
there is reflected in every active task without re-provisioning.
|
|
349
|
+
"""
|
|
350
|
+
notes: list[str] = []
|
|
351
|
+
for rel in _resolve_sync_files(source_root):
|
|
352
|
+
src = (source_root / rel).resolve()
|
|
353
|
+
if not src.exists() or not src.is_file():
|
|
354
|
+
continue
|
|
355
|
+
dst = worktree_path / rel
|
|
356
|
+
if dst.exists() or dst.is_symlink():
|
|
357
|
+
continue
|
|
358
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
359
|
+
os.symlink(src, dst)
|
|
360
|
+
notes.append(rel)
|
|
361
|
+
return notes
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _copy_snapshot_files(source_root: Path, worktree_path: Path) -> list[str]:
|
|
365
|
+
"""Copy fixture files from MAIN → task worktree as read-only snapshots
|
|
366
|
+
(FU-V3).
|
|
367
|
+
|
|
368
|
+
Unlike `_link_sync_files`, this materialises a fresh on-disk copy and
|
|
369
|
+
chmods it to `0o444` so the verifier can read the same rows the
|
|
370
|
+
executor saw without sharing a writable handle that would corrupt the
|
|
371
|
+
main worktree's copy. Skip rules mirror the symlink helpers (missing
|
|
372
|
+
source → skipped silently; pre-existing dst → skipped to avoid
|
|
373
|
+
clobbering tracked content).
|
|
374
|
+
"""
|
|
375
|
+
notes: list[str] = []
|
|
376
|
+
for rel in _resolve_snapshot_files(source_root):
|
|
377
|
+
src = (source_root / rel).resolve()
|
|
378
|
+
if not src.exists() or not src.is_file():
|
|
379
|
+
continue
|
|
380
|
+
dst = worktree_path / rel
|
|
381
|
+
if dst.exists() or dst.is_symlink():
|
|
382
|
+
continue
|
|
383
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
384
|
+
shutil.copy2(src, dst)
|
|
385
|
+
try:
|
|
386
|
+
os.chmod(dst, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
|
|
387
|
+
except OSError:
|
|
388
|
+
# chmod failure (exotic filesystems, root-squashed mounts) does
|
|
389
|
+
# not invalidate the snapshot itself — it just means the read-
|
|
390
|
+
# only contract is best-effort. Surface in notes so the operator
|
|
391
|
+
# can audit if a verifier later mutates the file.
|
|
392
|
+
notes.append(f"{rel} (rw-fallback)")
|
|
393
|
+
continue
|
|
394
|
+
notes.append(rel)
|
|
395
|
+
return notes
|
|
396
|
+
|
|
397
|
+
|
|
264
398
|
def compute_worktree_path(
|
|
265
399
|
*,
|
|
266
400
|
project_id: str,
|
|
@@ -424,7 +558,16 @@ def provision_task_worktree(
|
|
|
424
558
|
# Sync dirs sourced from the MAIN worktree so every task sees the
|
|
425
559
|
# same shared state regardless of which checkout invoked okstra.
|
|
426
560
|
linked = _link_sync_dirs(main_root, worktree_path)
|
|
427
|
-
|
|
561
|
+
linked_files = _link_sync_files(main_root, worktree_path)
|
|
562
|
+
snapshot_files = _copy_snapshot_files(main_root, worktree_path)
|
|
563
|
+
linked_parts: list[str] = []
|
|
564
|
+
if linked:
|
|
565
|
+
linked_parts.append(f"linked {', '.join(linked)}")
|
|
566
|
+
if linked_files:
|
|
567
|
+
linked_parts.append(f"linked-files {', '.join(linked_files)}")
|
|
568
|
+
if snapshot_files:
|
|
569
|
+
linked_parts.append(f"snapshot {', '.join(snapshot_files)}")
|
|
570
|
+
linked_suffix = ("; " + "; ".join(linked_parts)) if linked_parts else ""
|
|
428
571
|
|
|
429
572
|
try:
|
|
430
573
|
worktree_registry.reserve(
|