okstra 0.135.0 → 0.137.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 (44) hide show
  1. package/docs/architecture/storage-model.md +1 -1
  2. package/docs/contributor-change-matrix.md +1 -1
  3. package/docs/project-structure-overview.md +8 -3
  4. package/package.json +2 -3
  5. package/runtime/BUILD.json +2 -2
  6. package/runtime/bin/lib/okstra/interactive.sh +24 -67
  7. package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +1 -1
  8. package/runtime/bin/okstra-antigravity-exec.sh +10 -8
  9. package/runtime/bin/okstra-claude-exec.sh +10 -8
  10. package/runtime/bin/okstra-codex-exec.sh +10 -8
  11. package/runtime/bin/okstra-spawn-followups.py +4 -9
  12. package/runtime/prompts/lead/plan-body-verification.md +2 -2
  13. package/runtime/python/okstra_ctl/backfill.py +2 -1
  14. package/runtime/python/okstra_ctl/codex_dispatch.py +56 -331
  15. package/runtime/python/okstra_ctl/consumers.py +7 -5
  16. package/runtime/python/okstra_ctl/context_cost.py +6 -5
  17. package/runtime/python/okstra_ctl/dispatch_core.py +54 -175
  18. package/runtime/python/okstra_ctl/dispatch_state.py +231 -0
  19. package/runtime/python/okstra_ctl/error_log_core.py +3 -1
  20. package/runtime/python/okstra_ctl/error_report.py +2 -7
  21. package/runtime/python/okstra_ctl/handoff.py +2 -1
  22. package/runtime/python/okstra_ctl/implementation_outcome.py +17 -27
  23. package/runtime/python/okstra_ctl/implementation_stage.py +7 -2
  24. package/runtime/python/okstra_ctl/path_hints.py +5 -3
  25. package/runtime/python/okstra_ctl/paths.py +278 -4
  26. package/runtime/python/okstra_ctl/plan_run_root.py +4 -4
  27. package/runtime/python/okstra_ctl/recap.py +5 -12
  28. package/runtime/python/okstra_ctl/render.py +9 -15
  29. package/runtime/python/okstra_ctl/report_finalize.py +8 -4
  30. package/runtime/python/okstra_ctl/report_language.py +83 -0
  31. package/runtime/python/okstra_ctl/run.py +222 -239
  32. package/runtime/python/okstra_ctl/set_work_status.py +2 -1
  33. package/runtime/python/okstra_ctl/stage_targets.py +98 -55
  34. package/runtime/python/okstra_ctl/time_report.py +4 -9
  35. package/runtime/python/okstra_ctl/wizard.py +50 -50
  36. package/runtime/python/okstra_ctl/work_categories.py +2 -2
  37. package/runtime/python/okstra_ctl/worker_prompt_body.py +82 -0
  38. package/runtime/python/okstra_ctl/worker_prompt_contract.py +0 -13
  39. package/runtime/python/okstra_ctl/worker_prompt_headers.py +7 -0
  40. package/runtime/python/okstra_project/__init__.py +4 -0
  41. package/runtime/python/okstra_project/dirs.py +7 -0
  42. package/runtime/python/okstra_project/state.py +78 -8
  43. package/runtime/validators/validate-run.py +23 -52
  44. package/src/commands/inspect/stage-map.mjs +12 -19
@@ -236,7 +236,7 @@ Main contents:
236
236
  This document is only a quick summary and is not the source of truth.
237
237
  Always use `task-manifest.json` when checking canonical metadata.
238
238
 
239
- The rendered `task-index.md` is an English-literal artifact. The headings, field labels, and `## Notes` footnote of the template (`templates/project-docs/task-index.template.md`) ship as English literals regardless of the `reportLanguage` setting, matching the okstra-schedule-gen schedule contract. The docs-publication pipeline's Hangul check (`npm run docs:validate`) enforces this contract.
239
+ The rendered `task-index.md` is an English-literal artifact. The headings, field labels, and `## Notes` footnote of the template (`templates/project-docs/task-index.template.md`) ship as English literals regardless of the `reportLanguage` setting, matching the okstra-schedule-gen schedule contract. `tests/contract/test_docs_runtime_contract.py` enforces this contract.
240
240
 
241
241
  ## Run manifest contract
242
242
 
@@ -11,6 +11,6 @@ Use this matrix before changing high-risk repo contracts. Update the source file
11
11
  | Add phase | `scripts/okstra_ctl/workflow.py`, `prompts/profiles/`, `validators/`, `tests/` | workflow and validation contract tests |
12
12
  | Change worker roster | `prompts/profiles/*.md`, `scripts/okstra_ctl/workers.py`, `tests/contract/test_repo_contracts.py` | worker roster contract tests |
13
13
  | Change report section | `schemas/final-report-v1.0.schema.json`, `templates/reports/final-report.template.md`, `scripts/okstra_ctl/render_final_report.py`, `validators/validate-run.py` | final-report schema, renderer, and validator tests |
14
- | Change Markdown publication policy | `config/docs-publication.json`, `tools/docs_publication/`, affected public Markdown paths | `tests/docs_publication/`, `tests/contract/test_repo_contracts.py` |
14
+ | Maintain Korean review mirrors | `config/korean-sources.json`, `tools/korean-sources/`, `.agents/skills/sync-korean-sources/`, `.claude/skills/sync-korean-sources/` | `tests-js/korean-sources-*.test.mjs` |
15
15
 
16
16
  `runtime/` is build output. Never edit it directly; change source files and rebuild the runtime payload instead.
@@ -55,6 +55,10 @@ okstra/
55
55
  │ ├── lib/okstra/ Bash helpers for okstra.sh
56
56
  │ └── lib/okstra-ctl/ Bash control-center subcommands
57
57
  ├── skills/ Claude Code skills (13); `_fragments/` holds shared marker blocks
58
+ ├── .agents/skills/ Codex repo-local maintainer skills
59
+ ├── .claude/skills/ Claude Code project-only mirror-sync surface
60
+ ├── .claude/settings.json Claude Code project-only mirror-sync surface
61
+ ├── .codex/hooks.json Codex project lifecycle hooks
58
62
  ├── agents/ lead SKILL.md + worker agent specs
59
63
  ├── prompts/ launch template, phase profiles, wizard prompt JSON
60
64
  ├── schemas/ JSON schema for final-report data.json
@@ -62,8 +66,8 @@ okstra/
62
66
  ├── validators/ run / brief / schedule / view validators
63
67
  ├── tools/build.mjs source → runtime sync
64
68
  ├── tools/sync-skill-fragments.mjs expand shared fragment blocks into skills/*/SKILL.md
65
- ├── tools/docs_publication/ public-manual publication pipeline (build-time only; not shipped)
66
- ├── config/docs-publication.json publication inventory: every tracked Markdown path + classification
69
+ ├── tools/korean-sources/ maintainer-only Korean mirror sync engine
70
+ ├── config/korean-sources.json configured English Markdown source roots
67
71
  ├── runtime/ generated install payload; do not edit directly
68
72
  ├── tests/ pytest unit suite
69
73
  ├── tests-e2e/ Bash end-to-end scenarios
@@ -289,6 +293,7 @@ Important modules:
289
293
  | `team_reconcile.py` | stale team-member reconciliation at run-end teardown |
290
294
  | `worker_prompt_headers.py` | shared rendering of phase-aware worker prompt anchors (`worker_prompt_headers`): coding-preflight only for implementation and compact target identity for final-verification |
291
295
  | `worker_prompt_body.py` | provider-neutral initial analysis body/input renderer shared by Codex and external/team dispatch paths |
296
+ | `report_language.py` | report-writer `**Report Language:**` resolution (`resolve_report_language`) — precedence project config → global config → task-brief inference, shared by both dispatchers so the stamped language does not depend on which dispatch path ran |
292
297
  | `worker_prompt_policy.py` | `PromptPlan` generating SSOT — resolves functional audience, equality group, packet-only input, coding-preflight eligibility, required headers, and size limits without consulting provider/model identity |
293
298
  | `worker_prompt_contract.py` | deterministic cross-task initial-prompt validator and normalized cross-worker equality SSOT, reused by dispatch adapters and the Phase 7 persisted-artifact validator |
294
299
  | `convergence_engine.py` | pure `ConvergenceEngine` reducer — seeds Round 0 working state, plans roster-aware rounds, applies structured outcomes and one critic-gap batch, finalizes schema v1.3, and validates replayable state without dispatch or filesystem ownership |
@@ -558,7 +563,7 @@ When changing code, keep these docs in sync:
558
563
  - New report field/section: update schema, template, report-writer worker, validator tests, this file's report model if user-visible.
559
564
  - New phase/profile behavior: update `prompts/profiles/*`, `docs/architecture.md`, `docs/cli.md`, and `README.md` if user-facing.
560
565
 
561
- `README.md`, `docs/cli.md`, `docs/architecture.md`, `docs/architecture/storage-model.md`, `docs/container.md`, and `docs/performance-improvement-plan-v2.md` are `translated` outputs in `config/docs-publication.json` do **not** hand-edit them. Author the Korean source under the maintainer's Git-ignored `.project-docs/ko-source/`, then run the publication pipeline (`python3 -m tools.docs_publication prepare|validate|apply|record`), which keeps the source/output hashes in `.project-docs/doc-publishing/state.json` in step. Files classified `pending` including this one, `CHANGES.md`, and `CHANGELOG.md` — are edited directly.
566
+ Edit English canonical Markdown sources directly. After changing a path registered in `config/korean-sources.json`, process the local Korean mirror queue with `$sync-korean-sources` or `/sync-korean-sources`. `.project-docs/ko-sources/**` is maintainer-local only: it is neither published nor committed.
562
567
 
563
568
  ---
564
569
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.135.0",
3
+ "version": "0.137.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -35,10 +35,9 @@
35
35
  "scripts": {
36
36
  "build": "node tools/build.mjs",
37
37
  "prepack": "node tools/build.mjs",
38
- "docs:validate": "python3 -m tools.docs_publication validate --repo .",
39
38
  "test:js": "node --test tests-js/*.test.mjs",
40
39
  "test:py": "python3 -m pytest tests/",
41
40
  "test:workflow": "bash validators/validate-workflow.sh",
42
- "check": "npm run docs:validate && npm run build && npm run test:js && npm run test:py && npm run test:workflow"
41
+ "check": "npm run build && npm run test:js && npm run test:py && npm run test:workflow"
43
42
  }
44
43
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.135.0",
3
- "builtAt": "2026-07-25T13:20:54.916Z",
2
+ "package": "0.137.0",
3
+ "builtAt": "2026-07-26T18:44:34.234Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -11,19 +11,6 @@ is_interactive_session() {
11
11
  [[ -t 0 && -t 1 ]]
12
12
  }
13
13
 
14
- file_mtime() {
15
- # Epoch mtime; portable across GNU (stat -c) and macOS/BSD (stat -f).
16
- # Validate that the output is a bare integer before accepting it: GNU
17
- # `stat -f %m` (a BSD-ism) does NOT fail cleanly — it succeeds with
18
- # non-numeric filesystem output, so a plain `a || b` would return that
19
- # garbage and later break `-gt`/`-eq` arithmetic under `set -u`.
20
- local m=""
21
- m="$(stat -c %Y "$1" 2>/dev/null)"
22
- [[ "$m" =~ ^[0-9]+$ ]] || m="$(stat -f %m "$1" 2>/dev/null)"
23
- [[ "$m" =~ ^[0-9]+$ ]] || m="0"
24
- printf '%s' "$m"
25
- }
26
-
27
14
  split_task_key() {
28
15
  local raw_key=""
29
16
  raw_key="$(trim_whitespace "${TASK_KEY_INPUT-}")"
@@ -293,74 +280,44 @@ find_latest_final_report() {
293
280
  # Args: project_root task_group task_id task_type_filter(optional)
294
281
  # Echoes: <abs path to latest final-report-*.md>\t<task-type>
295
282
  # Returns: 1 if not found.
283
+ #
284
+ # 선택 규칙(mtime 최신, 동률이면 basename)과 산출물 위치 지식은
285
+ # okstra_ctl.paths.RunRef 가 정본이다. 과거 이 함수는 그 규칙을 bash 로
286
+ # 재구현해 python 쪽과 손으로 동기화해야 했다.
296
287
  local project_root="$1"
297
288
  local task_group="$2"
298
289
  local task_id="$3"
299
290
  local task_type_filter="$4"
300
291
 
292
+ # task-key 단축키 해소와 오타 진단(실제 task-key 목록 출력)은 이 resolver 가
293
+ # 소유한다. 최신 보고서 선택만 python 정본에 위임한다.
301
294
  local task_root=""
302
295
  task_root="$(resolve_task_root_for_shortcut "$project_root" "$PROJECT_ID" "$task_group" "$task_id")" || {
303
296
  printf 'resume-clarification: unable to resolve task root for the given task-key. See diagnostics above.\n' >&2
304
297
  return 1
305
298
  }
306
299
 
307
- local runs_root="$task_root/runs"
308
- if [[ ! -d "$runs_root" ]]; then
309
- printf 'resume-clarification: no runs/ directory under %s\n' "$task_root" >&2
310
- return 1
311
- fi
312
-
313
- local search_dirs=()
314
- if [[ -n "$task_type_filter" ]]; then
315
- search_dirs+=("$runs_root/$task_type_filter/reports")
316
- else
317
- [[ -d "$runs_root/error-analysis/reports" ]] && search_dirs+=("$runs_root/error-analysis/reports")
318
- [[ -d "$runs_root/requirements-discovery/reports" ]] && search_dirs+=("$runs_root/requirements-discovery/reports")
319
- fi
320
-
321
- if (( ${#search_dirs[@]} == 0 )); then
322
- printf 'resume-clarification: no requirements-discovery or error-analysis reports/ directory under %s/runs/. Run that phase first before invoking --resume-clarification.\n' "$task_root" >&2
323
- return 1
324
- fi
325
-
326
- # Pick the chronologically newest report (mtime first), matching the Python
327
- # resume path (okstra_ctl.wizard's mtime-max). A pure basename sort conflates
328
- # phases: across error-analysis/ and requirements-discovery/ dirs the filename
329
- # embeds the task-type, so a lexicographic max always favors one phase
330
- # regardless of which run is actually newer. Ties (same-second mtime within a
331
- # phase dir) fall back to the greater basename so the highest seq/timestamp
332
- # wins deterministically.
333
- local best_path=""
334
- local best_mtime=-1
335
- local best_basename=""
336
- local candidate=""
337
- local candidate_mtime=""
338
- local candidate_base=""
339
- local d=""
340
- for d in "${search_dirs[@]}"; do
341
- [[ -d "$d" ]] || continue
342
- while IFS= read -r candidate; do
343
- candidate_mtime="$(file_mtime "$candidate")"
344
- candidate_base="$(basename "$candidate")"
345
- if [[ -z "$best_path" \
346
- || "$candidate_mtime" -gt "$best_mtime" \
347
- || ( "$candidate_mtime" -eq "$best_mtime" && "$candidate_base" > "$best_basename" ) ]]; then
348
- best_path="$candidate"
349
- best_mtime="$candidate_mtime"
350
- best_basename="$candidate_base"
351
- fi
352
- done < <(find "$d" -maxdepth 1 -type f -name 'final-report-*.md' 2>/dev/null)
353
- done
300
+ local found=""
301
+ found="$(PYTHONPATH="$WORKSPACE_ROOT/scripts:${PYTHONPATH-}" python3 - \
302
+ "$task_root" "$task_type_filter" <<'PY'
303
+ import sys
304
+ from okstra_ctl.paths import RunRef
305
+
306
+ task_root, task_type_filter = sys.argv[1:3]
307
+ types = ((task_type_filter,) if task_type_filter
308
+ else ("error-analysis", "requirements-discovery"))
309
+ ref = RunRef.latest_under(task_root, types)
310
+ if ref is not None:
311
+ sys.stdout.write(f"{ref.report}\t{ref.task_type}")
312
+ PY
313
+ )" || return 1
354
314
 
355
- if [[ -z "$best_path" ]]; then
356
- printf 'resume-clarification: no final-report-*.md found for task %s:%s under %s\n' \
357
- "$task_group" "$task_id" "$runs_root" >&2
315
+ if [[ -z "$found" ]]; then
316
+ printf 'resume-clarification: no final-report-*.md found for task %s:%s under %s/runs\n' \
317
+ "$task_group" "$task_id" "$task_root" >&2
358
318
  return 1
359
319
  fi
360
-
361
- local resolved_type=""
362
- resolved_type="$(basename "$(dirname "$(dirname "$best_path")")")"
363
- printf '%s\t%s\n' "$best_path" "$resolved_type"
320
+ printf '%s\n' "$found"
364
321
  }
365
322
 
366
323
  run_resume_clarification() {
@@ -151,7 +151,7 @@ batch_reserved = {}
151
151
  for original in targets:
152
152
  row = find_row_by_run_id(home, original)
153
153
  # implementation 은 stage 격리 task-type 이다. 한 run = 런타임에 live
154
- # registry/consumers 상태로 auto-resolve 되는 단일 stage(_resolve_effective_stages)
154
+ # registry/consumers 상태로 auto-resolve 되는 단일 stage(stage_targets.resolve_effective_stages)
155
155
  # 이므로, cmd-rerun 은 spawn 전에 어느 stage-<N> 가 선택될지 알 수 없어
156
156
  # runs/implementation/stage-<N> 경로도 per-stage run_seq 도 예측할 수 없다.
157
157
  # OKSTRA_RUN_SEQ_OVERRIDE(run 전체에 단일 seq) 는 per-stage seq 카운터와
@@ -290,15 +290,17 @@ if (( idle_timeout_secs > 0 )); then
290
290
  while kill -0 "$agy_pid" 2>/dev/null; do
291
291
  sleep "$poll_interval"
292
292
  kill -0 "$agy_pid" 2>/dev/null || exit 0
293
- # GNU `stat -c` 우선 + 숫자 검증. BSD-ism `stat -f %m` 은 Linux 에서 깨끗이
294
- # 실패하지 않고 비숫자 파일시스템 출력으로 성공해 아래 산술을 set -u 에서
295
- # 깨뜨린다file_mtime(lib/okstra/interactive.sh) 동일 가드.
293
+ # Portable mtime probe: GNU (stat -c %Y) first, macOS/BSD (stat -f %m) as
294
+ # the fallback, 0 as the floor. Validate each result as a bare integer
295
+ # before accepting it on Linux the BSD-ism `stat -f %m` does NOT fail
296
+ # cleanly; it succeeds and prints non-numeric filesystem output, which
297
+ # would break the arithmetic below under `set -u`.
296
298
  #
297
- # `|| true` 필수다. macOS 에는 GNU `stat -c` 없어 exit 1 이고,
298
- # `2>/dev/null` stderr 막지 종료코드는 막지 못한다. 워치독은 명시적
299
- # `( ) &` 서브셸이라 위쪽 `set -e` 상속하므로, 실패한 할당 하나가 첫 폴링에서
300
- # 워치독을 통째로 죽인다 조용히, 로그 없이. file_mtime 같은 코드로도
301
- # 멀쩡한 것은 항상 `$( )` 안에서 호출돼 errexit 상속하지 않기 때문이다.
299
+ # `|| true` is mandatory. macOS has no GNU `stat -c`, so that call exits 1,
300
+ # and `2>/dev/null` suppresses stderr but not the exit code. This watchdog
301
+ # is an explicit `( ) &` subshell, so it inherits whatever errexit setting
302
+ # is in force; under `set -e` a single failed assignment kills the whole
303
+ # watchdog on the first poll silently, without a log line.
302
304
  last_mtime="$(stat -c %Y "$log_path" 2>/dev/null || true)"
303
305
  [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime="$(stat -f %m "$log_path" 2>/dev/null || true)"
304
306
  [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime=0
@@ -132,15 +132,17 @@ if (( idle_timeout_secs > 0 )); then
132
132
  while kill -0 "$claude_pid" 2>/dev/null; do
133
133
  sleep "$poll_interval"
134
134
  kill -0 "$claude_pid" 2>/dev/null || exit 0
135
- # GNU `stat -c` 우선 + 숫자 검증. BSD-ism `stat -f %m` 은 Linux 에서 깨끗이
136
- # 실패하지 않고 비숫자 파일시스템 출력으로 성공해 아래 산술을 set -u 에서
137
- # 깨뜨린다file_mtime(lib/okstra/interactive.sh) 동일 가드.
135
+ # Portable mtime probe: GNU (stat -c %Y) first, macOS/BSD (stat -f %m) as
136
+ # the fallback, 0 as the floor. Validate each result as a bare integer
137
+ # before accepting it on Linux the BSD-ism `stat -f %m` does NOT fail
138
+ # cleanly; it succeeds and prints non-numeric filesystem output, which
139
+ # would break the arithmetic below under `set -u`.
138
140
  #
139
- # `|| true` 필수다. macOS 에는 GNU `stat -c` 없어 exit 1 이고,
140
- # `2>/dev/null` stderr 막지 종료코드는 막지 못한다. 워치독은 명시적
141
- # `( ) &` 서브셸이라 위쪽 `set -e` 상속하므로, 실패한 할당 하나가 첫 폴링에서
142
- # 워치독을 통째로 죽인다 조용히, 로그 없이. file_mtime 같은 코드로도
143
- # 멀쩡한 것은 항상 `$( )` 안에서 호출돼 errexit 상속하지 않기 때문이다.
141
+ # `|| true` is mandatory. macOS has no GNU `stat -c`, so that call exits 1,
142
+ # and `2>/dev/null` suppresses stderr but not the exit code. This watchdog
143
+ # is an explicit `( ) &` subshell, so it inherits whatever errexit setting
144
+ # is in force; under `set -e` a single failed assignment kills the whole
145
+ # watchdog on the first poll silently, without a log line.
144
146
  last_mtime="$(stat -c %Y "$log_path" 2>/dev/null || true)"
145
147
  [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime="$(stat -f %m "$log_path" 2>/dev/null || true)"
146
148
  [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime=0
@@ -421,15 +421,17 @@ if (( idle_timeout_secs > 0 )); then
421
421
  while kill -0 "$codex_pid" 2>/dev/null; do
422
422
  sleep "$poll_interval"
423
423
  kill -0 "$codex_pid" 2>/dev/null || exit 0
424
- # GNU `stat -c` 우선 + 숫자 검증. BSD-ism `stat -f %m` 은 Linux 에서 깨끗이
425
- # 실패하지 않고 비숫자 파일시스템 출력으로 성공해 아래 산술을 set -u 에서
426
- # 깨뜨린다file_mtime(lib/okstra/interactive.sh) 동일 가드.
424
+ # Portable mtime probe: GNU (stat -c %Y) first, macOS/BSD (stat -f %m) as
425
+ # the fallback, 0 as the floor. Validate each result as a bare integer
426
+ # before accepting it on Linux the BSD-ism `stat -f %m` does NOT fail
427
+ # cleanly; it succeeds and prints non-numeric filesystem output, which
428
+ # would break the arithmetic below under `set -u`.
427
429
  #
428
- # `|| true` 필수다. macOS 에는 GNU `stat -c` 없어 exit 1 이고,
429
- # `2>/dev/null` stderr 막지 종료코드는 막지 못한다. 워치독은 명시적
430
- # `( ) &` 서브셸이라 위쪽 `set -e` 상속하므로, 실패한 할당 하나가 첫 폴링에서
431
- # 워치독을 통째로 죽인다 조용히, 로그 없이. file_mtime 같은 코드로도
432
- # 멀쩡한 것은 항상 `$( )` 안에서 호출돼 errexit 상속하지 않기 때문이다.
430
+ # `|| true` is mandatory. macOS has no GNU `stat -c`, so that call exits 1,
431
+ # and `2>/dev/null` suppresses stderr but not the exit code. This watchdog
432
+ # is an explicit `( ) &` subshell, so it inherits whatever errexit setting
433
+ # is in force; under `set -e` a single failed assignment kills the whole
434
+ # watchdog on the first poll silently, without a log line.
433
435
  last_mtime="$(stat -c %Y "$log_path" 2>/dev/null || true)"
434
436
  [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime="$(stat -f %m "$log_path" 2>/dev/null || true)"
435
437
  [[ "$last_mtime" =~ ^[0-9]+$ ]] || last_mtime=0
@@ -51,19 +51,14 @@ from pathlib import Path
51
51
  sys.path.insert(0, str(Path(__file__).resolve().parent))
52
52
 
53
53
  from okstra_ctl.final_report_paths import final_report_markdown_path # noqa: E402
54
+ from okstra_ctl.paths import task_manifest_file # noqa: E402
55
+ from okstra_ctl.workflow import PHASE_SEQUENCE # noqa: E402
54
56
  from okstra_project.dirs import tasks_root # noqa: E402
55
57
 
56
58
 
57
59
  SLUG_RE = re.compile(r"[^a-zA-Z0-9-]+")
58
60
 
59
- ALLOWED_TASK_TYPES = {
60
- "requirements-discovery",
61
- "error-analysis",
62
- "implementation-planning",
63
- "implementation",
64
- "final-verification",
65
- "release-handoff",
66
- }
61
+ ALLOWED_TASK_TYPES = set(PHASE_SEQUENCE)
67
62
  ALLOWED_ORIGINS = {
68
63
  "phase-continuation",
69
64
  "out-of-plan",
@@ -193,7 +188,7 @@ def _spawn_one(
193
188
  "routingStatus": "follow-up-spawned",
194
189
  },
195
190
  }
196
- _write_manifest(task_root / "task-manifest.json", manifest_payload)
191
+ _write_manifest(task_manifest_file(task_root), manifest_payload)
197
192
 
198
193
  brief_path = task_root / "instruction-set" / "task-brief.md"
199
194
  brief_path.parent.mkdir(parents=True, exist_ok=True)
@@ -244,7 +244,7 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
244
244
 
245
245
  Required prompt anchor headers are identical to finding convergence (see [convergence](./convergence.md) §"Required reverify-prompt anchor headers"). The prompt body changes from F-* listing to P-* listing:
246
246
 
247
- ```
247
+ ````
248
248
  You are <worker-role> performing plan-body verification for <task-key> (round 1).
249
249
 
250
250
  ## Instructions
@@ -342,7 +342,7 @@ rerun a free-form requirements analysis. Every DISAGREE includes Fixability.
342
342
  **Fixability** (DISAGREE only): planner-fixable | needs-user-input. Fixability is not applicable for `UNVERIFIABLE`.
343
343
  **Note**: <the falsification candidate considered and why it is excluded; required even for AGREE>
344
344
  **Explanation**: <2-3 sentences applying the disposition-specific rule>
345
- ```
345
+ ````
346
346
 
347
347
  When `config.adversarial == true`, the lead prepends the adversarial framing from §"Adversarial plan-body posture" to the `## Instructions` block: the burden of proof is on the plan, the verifier opens and confirms every accessible cited path / command, and evidence that was opened but is insufficient yields the applicable `DISAGREE(<kind>)` rather than `AGREE`. Inability to inspect because of capability, credential, network, or service state yields `UNVERIFIABLE`, not DISAGREE. The verdict tokens, breakage kinds (a–f), classification, and the majority gate threshold are unchanged. This prepended framing supersedes the template's "Judge solely from plan internal consistency" instruction for the adversarial round.
348
348
 
@@ -10,6 +10,7 @@ from typing import List
10
10
  from okstra_project.dirs import tasks_root
11
11
 
12
12
  from .ids import build_run_id
13
+ from .paths import runs_dir_of
13
14
  from .invocation import save_invocation
14
15
  from .jsonl import append_jsonl, read_jsonl, rotate_recent_if_needed
15
16
  from .project_meta import _project_meta_path
@@ -130,7 +131,7 @@ def backfill_project(home: Path, project_id: str, project_root: Path) -> int:
130
131
  manifest_re = _re.compile(r"^run-manifest-(?P<tt>.+)-(?P<seq>\d+)\.json$")
131
132
  for group_dir in sorted(p for p in base.iterdir() if p.is_dir()):
132
133
  for task_dir in sorted(p for p in group_dir.iterdir() if p.is_dir()):
133
- runs = task_dir / "runs"
134
+ runs = runs_dir_of(task_dir)
134
135
  if not runs.is_dir():
135
136
  continue
136
137
  for manifests in _iter_manifest_dirs(runs):