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.
Files changed (41) hide show
  1. package/README.kr.md +2 -2
  2. package/README.md +2 -2
  3. package/docs/kr/architecture.md +1 -0
  4. package/docs/kr/cli.md +1 -1
  5. package/docs/kr/performance-improvement-plan-v2.md +330 -0
  6. package/docs/kr/performance-improvement-plan.md +125 -0
  7. package/docs/project-structure-overview.md +388 -0
  8. package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +1568 -0
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/agents/SKILL.md +7 -1
  12. package/runtime/agents/workers/claude-worker.md +3 -1
  13. package/runtime/agents/workers/report-writer-worker.md +4 -0
  14. package/runtime/bin/okstra-codex-exec.sh +42 -0
  15. package/runtime/bin/okstra-gemini-exec.sh +7 -0
  16. package/runtime/bin/okstra-trace-cleanup.sh +42 -0
  17. package/runtime/prompts/profiles/final-verification.md +8 -2
  18. package/runtime/prompts/profiles/implementation-planning.md +1 -1
  19. package/runtime/prompts/profiles/release-handoff.md +26 -28
  20. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  21. package/runtime/python/okstra_ctl/render.py +78 -4
  22. package/runtime/python/okstra_ctl/run_context.py +5 -0
  23. package/runtime/python/okstra_ctl/workflow.py +8 -7
  24. package/runtime/python/okstra_ctl/worktree.py +155 -12
  25. package/runtime/skills/okstra-brief/SKILL.md +523 -0
  26. package/runtime/skills/okstra-convergence/SKILL.md +149 -37
  27. package/runtime/skills/okstra-report-writer/SKILL.md +8 -6
  28. package/runtime/templates/prd/brief.template.md +12 -0
  29. package/runtime/templates/project-docs/task-index.template.md +12 -0
  30. package/runtime/templates/reports/error-analysis-input.template.md +12 -0
  31. package/runtime/templates/reports/final-report.template.md +39 -12
  32. package/runtime/templates/reports/final-verification-input.template.md +22 -0
  33. package/runtime/templates/reports/implementation-input.template.md +12 -0
  34. package/runtime/templates/reports/implementation-planning-input.template.md +12 -0
  35. package/runtime/templates/reports/quick-input.template.md +12 -0
  36. package/runtime/templates/reports/release-handoff-input.template.md +23 -10
  37. package/runtime/templates/reports/schedule.template.md +12 -0
  38. package/runtime/templates/reports/settings.template.json +92 -30
  39. package/runtime/templates/reports/task-brief.template.md +12 -0
  40. package/src/install.mjs +1 -0
  41. 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 _read_project_json_sync_dirs(project_root: Path) -> Optional[tuple[str, ...]]:
191
- """Read `worktreeSyncDirs` from `.project-docs/okstra/project.json`.
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
- sync-dir resolution must never block worktree provisioning.
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("worktreeSyncDirs")
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 _resolve_sync_dirs(project_root: Optional[Path] = None) -> tuple[str, ...]:
219
- """Return the list of project-root-relative dirs to symlink into the
220
- new worktree. Precedence: env var → project.json → built-in default.
221
- See the comment above `DEFAULT_WORKTREE_SYNC_DIRS` for full semantics.
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("OKSTRA_WORKTREE_SYNC_DIRS")
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 = _read_project_json_sync_dirs(project_root)
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 DEFAULT_WORKTREE_SYNC_DIRS
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
- linked_suffix = f"; linked {', '.join(linked)}" if linked else ""
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(