okstra 0.220.0 → 0.220.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.
@@ -3,7 +3,7 @@ import { join } from "node:path";
3
3
  import { existsSync } from "node:fs";
4
4
  import { getPackageRoot, getPackageVersion } from "./version.mjs";
5
5
  function isStaleTmpHome(p) {
6
- // tests/conftest.py creates the isolated home as `<tmp_path>/.okstra-isolated`
6
+ // tests/_okstra_isolation.py creates the isolated home as `<tmp_path>/.okstra-isolated`
7
7
  // under the system temp dir. Only a deleted one (env leaked, dir gone) heals;
8
8
  // a soon-to-be-created `.okstra` temp home or a custom home is left untouched.
9
9
  if (!p.endsWith("/.okstra-isolated"))
@@ -1 +1 @@
1
- {"version":3,"file":"paths.mjs","sourceRoot":"","sources":["../../src/lib/paths.mts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"paths.mjs","sourceRoot":"","sources":["../../src/lib/paths.mts"],"names":[],"mappings":"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"}
package/docs/cli.md CHANGED
@@ -869,7 +869,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
869
869
  | `okstra chat <rooms\|create\|join\|members\|send\|unread\|inbox\|log\|ack>` | Global rooms under `~/.okstra/chat`. Join takes an explicit `--name`. Send takes exactly one of `--to` (`all` or a member; may equal `--as`) or `--reply-to` (an id in that room), and exactly one of `--body` or `--body-file`; a body may span several lines. A reply inherits `to` from the parent (`all` stays `all`, otherwise the parent's `from`). Unread is the inbox after the read cursor minus messages the participant sent; it does not move the cursor. `inbox` and `log` keep those arrivals, including after ack. Rows are `id @from YYYY-MM-DD HH:MM body`; a reply inserts `↑parentId` after the time; the second and later lines of a body follow on rows indented by two spaces. `ack --through` rejects an id behind the current cursor. Room and display names `.` and `..` are reserved. A `.lock` left by a dead process (pid recorded in the file) or older than 5 s is reclaimed. CLI basis of the `okstra-chat` skill. |
870
870
  | `okstra model-io active-context-input --project-root <dir> --run-manifest <path>` | Resolve the prior implementation-planning active context only through the current project and run authority, then emit fixed fields such as the executor base ref. Caller-supplied active-context paths are not accepted. |
871
871
  | `okstra convergence prepare-groups --run-manifest <path> --input <grouping.md>` | Parse fixed grouping Markdown, validate distinct per-worker evidence and group semantics against the run authority, then publish the schema-valid groups artifact at the run-owned path. |
872
- | `okstra manager <init\|discover-projects\|new\|task\|list\|remove\|view> [--json]` | Public CLI for grouping cross-project okstra tasks into manager-owned context. The default is purpose-specific fixed text for model use; `--json` preserves the machine contract and exit codes. `new project --project-root` accepts only existing directories and performs setup-equivalent registration only if `.okstra/project.json` is absent. `list managers`, `list projects --manager-id <id>` and `list tasks --manager-id <id>` enumerate manager state; `view --manager-id <id>` writes `~/.okstra/managers/<id>/view/index.html` and prints its path and `file://` URL. `task split --plan <json>` writes one validated brief with a `## Project Scope` section into each project an issue is assigned to and registers those children; `task run` then passes `--task-brief`. For a child whose task type is `discussion`, `task sync` records a `discussion` block read from the discussion record without writing to it — state (`draft`, `completed`, or `unknown` when the record cannot be read), phase, pending questions, revisions with their change reasons, and each follow-up's pinned revision and impact-review state, re-read from the follow-up's brief — and `task status` counts the child under `discussion`, never under `done`. `task run` for a child without a registered brief takes the brief that `okstra discussion follow-up-brief` wrote for it, found in the last-synced links of a discussion child of the same manager task. `list task-groups` lists groups including empty ones; `task update` edits a task's objective, common brief or progress mode; `remove project`, `remove task`, `remove task-group` and `task remove-child` print their targets and delete manager files only with `--confirm`, and `remove project` keeps the project's children marked unlinked. |
872
+ | `okstra manager <init\|discover-projects\|new\|task\|list\|remove\|view> [--json]` | Public CLI for grouping cross-project okstra tasks into manager-owned context. The default is purpose-specific fixed text for model use; `--json` preserves the machine contract and exit codes. `new project --project-root` accepts only existing directories and performs setup-equivalent registration only if `.okstra/project.json` is absent. `list managers`, `list projects --manager-id <id>` and `list tasks --manager-id <id>` enumerate manager state; `view --manager-id <id>` writes `~/.okstra/managers/<id>/view/index.html` and prints its path and `file://` URL. `task split --plan <json>` writes one validated brief with a `## Project Scope` section into each project an issue is assigned to and registers those children; `task run` then passes `--task-brief`. `task run` re-reads the target child's state from its project and refuses a child that is already done there, naming its last phase and report, unless `--allow-completed` is given. For a child whose task type is `discussion`, `task sync` records a `discussion` block read from the discussion record without writing to it — state (`draft`, `completed`, or `unknown` when the record cannot be read), phase, pending questions, revisions with their change reasons, and each follow-up's pinned revision and impact-review state, re-read from the follow-up's brief — and `task status` counts the child under `discussion`, never under `done`. `task run` for a child without a registered brief takes the brief that `okstra discussion follow-up-brief` wrote for it, found in the last-synced links of a discussion child of the same manager task. `list task-groups` lists groups including empty ones; `task update` edits a task's objective, common brief or progress mode; `remove project`, `remove task`, `remove task-group` and `task remove-child` print their targets and delete manager files only with `--confirm`, and `remove project` keeps the project's children marked unlinked. |
873
873
  | `okstra rollup [--task-group <group>] [--project-root <dir>] [--cwd <dir>] [--text\|--json]` | Read-only backend for the okstra-rollup skill. `--text` emits ordered fixed labels for model use. The default and `--json` preserve the full machine JSON contract and exit codes. Omitting `--task-group` targets the whole project catalog. |
874
874
  | `okstra usage-report [--days <positive-int>] [--project-root <dir>] [--cwd <dir>] [--text\|--json]` | Read-only backend for the okstra-usage skill. `--text` emits ordered fixed labels for model use. The default and `--json` preserve the full machine JSON contract and exit codes. Defaults to the current project's last 30 days. |
875
875
  | `okstra worker-state transition --team-state <path> --worker <id> --status <in-progress\|completed\|timeout\|error\|not-run> [--reason <text>] [--model <execution-value>]` | Atomically update one persisted worker row. `in-progress` records the authoritative `startedAt` and clears `endedAt`; terminal states record `endedAt`; `timeout`, `error`, and `not-run` require a reason. Dispatch adapters use this same transition path, so CLI-backed and in-process orchestration share the status timestamp contract |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.220.0",
3
+ "version": "0.220.1",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.220.0",
3
- "builtAt": "2026-10-17T11:04:20.410Z",
2
+ "package": "0.220.1",
3
+ "builtAt": "2026-10-18T05:15:37.569Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -260,7 +260,7 @@ Follow the launch prompt's operation-level progress guidance: name the actual pr
260
260
 
261
261
  A single okstra run frequently spans 30–120 minutes with multi-minute silent windows while workers run; without progress signals the user cannot distinguish "still working" from "hung". Lead MUST emit a single short progress line at each checkpoint below — plain user-facing text in a separate brief message (not buried inside a tool call), one line per checkpoint, format: `PROGRESS: <phase-id> <verb-phrase>`. Emit the line raw — the literal `PROGRESS:` token must begin the line. Do NOT wrap it in inline-code backticks (`` `PROGRESS: ...` ``) or a ```` ``` ```` code fence; markdown wrapping is what the post-hoc conformance validator scrapes around, and raw emit keeps the signal unambiguous.
262
262
 
263
- Record each checkpoint with `okstra lead-progress append --project-root <dir> --run-manifest <path> --phase <phase-id>`, then emit the `progressLine` it prints as the user-facing line. The command resolves `leadEventsPath` from the run manifest and writes the checkpoint there — on a host whose adapter declares `sessionAccounting: artifact-only` that ledger is the only place the post-hoc validator can read it, so a conversation line alone leaves no trace and the run is reported as missing the checkpoint. Pass `--worker <role>` on the per-worker checkpoints (it rewrites a phase-specific functional label into the roster role team-state records), `--field NAME=VALUE` for the remaining `key=value` tokens in the order the line carries them, and `--detail <text>` where the line ends in a verb phrase; the wording listed below is the default for the fixed-prose checkpoints.
263
+ Record each checkpoint with `okstra lead-progress append --project-root <dir> --run-manifest <path> --phase <phase-id>`, then emit the `progressLine` it prints as the user-facing line. The command resolves `leadEventsPath` from the run manifest and writes the checkpoint there — on a host whose adapter declares `sessionAccounting: artifact-only` that ledger is the only place the post-hoc validator can read it, so a conversation line alone leaves no trace and the run is reported as missing the checkpoint. Pass `--worker <role>` on the per-worker checkpoints (it rewrites a phase-specific functional label into the roster role team-state records), `--field NAME=VALUE` for the remaining `key=value` tokens in the order the line carries them, and `--detail <text>` where the line ends in a verb phrase; the wording listed below is the default for the fixed-prose checkpoints. The default follows what happened: a second `phase-1-intake` call in the same run renders `complete`, and `phase-6-synthesis` in a task type whose profile has no report-writer role renders `authoring final report`. **Enforced:** `_default_detail` in `scripts/okstra_ctl/lead_progress.py`, `tests/contract/test_lead_progress_cli.py`.
264
264
 
265
265
  Some checkpoints are recorded by the command that performs the step, because that command holds the fact the line states:
266
266
 
@@ -288,7 +288,7 @@ Use the exact CLI projection: `id`, `agent`, quoted `summary`, comma-joined `ite
288
288
  Required checkpoints:
289
289
 
290
290
  - `PROGRESS: phase-1-intake reading task bundle` — at the start of Phase 1, before issuing parallel Read calls.
291
- - `PROGRESS: phase-1-intake complete` — after all intake reads return.
291
+ - `PROGRESS: phase-1-intake complete` — after all intake reads return. Call `lead-progress append --phase phase-1-intake` again; the second call in a run renders this line.
292
292
  - `PROGRESS: phase-2-prompts preparing <N> worker prompts` — at the start of Phase 2, before any `Write` to the assigned prompt paths.
293
293
  - `PROGRESS: phase-3-team-create <adapter-specific-status>` — after selected-adapter setup is recorded in team-state. The stable phase id is retained for artifact compatibility. When the first dispatch writes the implicit-team marker, that dispatch command records the line.
294
294
  - `PROGRESS: phase-4-dispatch worker=<role> model=<model>` — once per worker, immediately before `dispatch_worker`. `<role>` is the **roster** role, exactly as team-state's `workers[].role` records it (`Claude worker`, `Codex worker`) — the checkpoint is matched against that entry, so a phase-specific functional label (`Claude verifier`, `Codex executor`) names no roster worker and fails the check. Only `claude-worker`-style hyphenation of the same roster role is also accepted.
@@ -299,7 +299,7 @@ Required checkpoints:
299
299
  - `PROGRESS: phase-5.5-convergence round=<N> queue=<count>` — at the start of each convergence round (Phase 5.5). `<N>` and `<count>` are the `round` and `inputQueueSize` that `okstra convergence plan-round` prints for that round, copied verbatim.
300
300
  - `PROGRESS: phase-5.6-critic provider=<provider> gaps=<n>` — after the critic result is collected (Phase 5.6, opt-in; the critic dispatch itself fires concurrently with the first 5.5 reverify round). Omitted when `convergence.critic.enabled == false`.
301
301
  - `PROGRESS: phase-batch-cleanup panes=<n>` — immediately after cleaning up the previous batch's panes, at each batch boundary (① just before the first `phase-5.5-convergence` round ② just before the `phase-6-synthesis` report-writer dispatch). `<n>` is the number of panes closed at that boundary — the panes of dispatches this run recorded and that have since finished — counted by `okstra team reclaim --project-root <dir> --run-manifest <path>` as the panes that pass closed, never estimated; no `--dry-run` pass is needed to learn it. A pane the harness opened for its own teammate carries no recorded id, so it is not counted and not closed. Expose only the counts and NEVER expose a raw `paneId` or worker handle. Just before the first batch (analysis-worker dispatch) there is nothing to clean up, so it is a no-op and the marker is omitted.
302
- - `PROGRESS: phase-6-synthesis dispatching report-writer-worker` — at the start of Phase 6. `team dispatch` (cmux-pane run only) and `worker-dispatch` record it on the first report-writer dispatch; on any other backend a `team dispatch` run appends it with `lead-progress append`.
302
+ - `PROGRESS: phase-6-synthesis dispatching report-writer-worker` — at the start of Phase 6. `team dispatch` (cmux-pane run only) and `worker-dispatch` record it on the first report-writer dispatch; on any other backend a `team dispatch` run appends it with `lead-progress append`. A lead-only task type (no report-writer role in its profile, e.g. `release-handoff`) dispatches no report writer; its line reads `phase-6-synthesis authoring final report`.
303
303
  - `PROGRESS: phase-5.5.9-plan-verify round=<N> items=<count>` — immediately before dispatching each plan-body verification round (`implementation-planning` only; see `plan-body-verification` (the absolute path in **Okstra Runtime Resources**) §"Round protocol"). Each round is a worker batch like any other, so round 2 and later MUST be preceded by a `phase-batch-cleanup` line reclaiming the previous round's verifiers. The numbering keeps this line sorted where the work happens — after Phase 6, because the round verifies the drafted plan body.
304
304
  - `PROGRESS: user-confirm <C-NNN> <the question, one line>` — immediately before asking the user about anything that would otherwise become an open `Blocks=approval` row (see "User confirmation before an approval blocker" below). Not tied to a phase: it fires wherever the blocker surfaces. `<C-NNN>` is the id the row will carry, so the answer and the row can be matched afterwards.
305
305
  - `PROGRESS: phase-7-persist updating manifests` — at the start of Phase 7. `okstra report-finalize` records it.
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import hashlib
4
4
  import json
5
+ import re
5
6
  from dataclasses import asdict, replace
6
7
  from datetime import datetime, timezone
7
8
  from pathlib import Path
@@ -197,10 +198,15 @@ def _accept_return(call: PreparedCall, returned: bytes) -> str:
197
198
  ).returned_body
198
199
 
199
200
 
201
+ _FENCED_JSON = re.compile(r"^```json\n(.*?)\n```$", re.MULTILINE | re.DOTALL)
202
+
203
+
200
204
  def _parse_result(body: str, path: Path) -> BusinessResult:
201
205
  text = body.strip()
202
- if text.startswith("```json\n") and text.endswith("\n```"):
203
- text = text[8:-4]
206
+ # 조사자는 "JSON만 반환" 지시에도 객체를 짧은 서문·맺음말과 함께 펜스로 감싸 돌려준다.
207
+ fenced = _FENCED_JSON.findall(text)
208
+ if len(fenced) == 1:
209
+ text = fenced[0]
204
210
  try:
205
211
  result = json.loads(text)
206
212
  except json.JSONDecodeError as exc:
@@ -379,6 +385,8 @@ def _instructions(
379
385
  "Return only one JSON object matching the schema below.",
380
386
  "Read the listed source roots and source artifacts only. Do not edit files, execute tests, start services, deploy or migrate.",
381
387
  "Inspect inbound AND outbound dependencies across all candidates, including indirect call/event/queue/shared-data/file/batch relationships. Name every uninspected candidate or missing branch in gaps.",
388
+ "In relationships, from and to must each be a projectId listed under projects in the Source Material; put modules, files and planned work in data, not in from/to.",
389
+ "Cite evidence only from files Git tracks or would add in a candidate root. Git-ignored files (for example .project-docs/) are outside the captured source inventory and are rejected; name what you learned from them in gaps instead.",
382
390
  "Explain the product business from start to finish for a new developer: purpose, rules, input/output, states, failure, retry and recovery. Link each step and relationship to factKeys.",
383
391
  "Use stable keys in business/subject/attribute form. Facts are source-backed assertions; expected changes remain expected. Cite inspected evidence with one-based line/endLine; each excerpt must copy the whole line..endLine range verbatim, including indentation, with no elision or omitted lines. Tests are static evidence unless a supplied existing verification record supports the execution claim.",
384
392
  "For post mode, use the supplied project-specific baseline. Never invent missing before-state. Describe the actual delta and distinguish confirmed side effects from risks.",
@@ -18,12 +18,14 @@ import argparse
18
18
  import json
19
19
  import sys
20
20
  from collections.abc import Mapping, Sequence
21
+ from dataclasses import replace
21
22
  from datetime import datetime, timezone
22
23
  from pathlib import Path
23
24
  from typing import Any
24
25
 
25
26
  from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
26
- from okstra_ctl.lead_events import LeadEvent, append_lead_event
27
+ from okstra_ctl.lead_events import LeadEvent, append_lead_event, read_lead_events
28
+ from okstra_ctl.phases import catalog
27
29
 
28
30
 
29
31
  class ProgressRecordError(ValueError):
@@ -64,6 +66,11 @@ _DEFAULT_DETAIL: Mapping[str, str] = {
64
66
  "phase-7-teardown": "shutting-down-workers",
65
67
  }
66
68
 
69
+ # phase 하나에 문구가 둘인 체크포인트. 고정 표만으로는 두 번째 호출이 첫 문구를
70
+ # 다시 내므로, 원장과 task type 을 보고 고른다.
71
+ _INTAKE_COMPLETE_DETAIL = "complete"
72
+ _LEAD_ONLY_SYNTHESIS_DETAIL = "authoring final report"
73
+
67
74
 
68
75
  def _utc_now() -> str:
69
76
  return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@@ -165,6 +172,39 @@ def _parse_field(raw: str) -> tuple[str, str]:
165
172
  return name.strip(), field_value.strip()
166
173
 
167
174
 
175
+ def _intake_already_started(events_path: Path, event: LeadEvent) -> bool:
176
+ return any(
177
+ prior.event_type == "progress"
178
+ and prior.details.get("phase") == "phase-1-intake"
179
+ and (prior.task_key, prior.task_type, prior.run_seq)
180
+ == (event.task_key, event.task_type, event.run_seq)
181
+ for prior in read_lead_events(events_path)
182
+ )
183
+
184
+
185
+ def _task_type_dispatches_report_writer(task_type: str) -> bool:
186
+ if task_type not in catalog.task_types():
187
+ return True
188
+ package = catalog.phase_record(task_type).package_name
189
+ profile = _read_json_object(
190
+ Path(catalog.__file__).parent / package / "profile.json", "phase profile"
191
+ )
192
+ return any(
193
+ isinstance(row, Mapping) and row.get("roleId") == "report-writer"
194
+ for row in profile.get("roles") or []
195
+ )
196
+
197
+
198
+ def _default_detail(phase: str, events_path: Path, event: LeadEvent) -> str | None:
199
+ if phase == "phase-1-intake" and _intake_already_started(events_path, event):
200
+ return _INTAKE_COMPLETE_DETAIL
201
+ if phase == "phase-6-synthesis" and not _task_type_dispatches_report_writer(
202
+ event.task_type
203
+ ):
204
+ return _LEAD_ONLY_SYNTHESIS_DETAIL
205
+ return _DEFAULT_DETAIL.get(phase)
206
+
207
+
168
208
  def render_progress_line(
169
209
  phase: str,
170
210
  fields: Sequence[tuple[str, str]] = (),
@@ -199,7 +239,9 @@ def record_progress(
199
239
  )
200
240
  ordered.append(("worker", role))
201
241
  ordered.extend(fields)
202
- line = render_progress_line(phase, ordered, detail)
242
+ events_path = _resolve_project_path(
243
+ project_root, _require_string(manifest, "leadEventsPath")
244
+ )
203
245
  event = LeadEvent(
204
246
  event_type="progress",
205
247
  lead_runtime=_require_string(manifest, "leadRuntime"),
@@ -207,15 +249,15 @@ def record_progress(
207
249
  task_type=_require_string(manifest, "taskType"),
208
250
  run_seq=_manifest_run_seq(manifest),
209
251
  timestamp=timestamp or _utc_now(),
210
- details={
211
- "phase": phase,
212
- "line": line,
213
- **({name: field_value for name, field_value in ordered}),
214
- },
215
- )
216
- events_path = _resolve_project_path(
217
- project_root, _require_string(manifest, "leadEventsPath")
218
252
  )
253
+ if detail is None:
254
+ detail = _default_detail(phase, events_path, event)
255
+ line = render_progress_line(phase, ordered, detail)
256
+ event = replace(event, details={
257
+ "phase": phase,
258
+ "line": line,
259
+ **({name: field_value for name, field_value in ordered}),
260
+ })
219
261
  append_lead_event(events_path, event)
220
262
  return event, warning
221
263
 
@@ -306,7 +348,10 @@ def _parser() -> argparse.ArgumentParser:
306
348
  help="extra `NAME=VALUE` token, in the order given")
307
349
  append_parser.add_argument(
308
350
  "--detail",
309
- help="verb phrase after the fields; contract wording is the default")
351
+ help="verb phrase after the fields; contract wording is the default "
352
+ "(a second phase-1-intake renders `complete`, and phase-6-synthesis "
353
+ "in a task type without a report writer renders "
354
+ "`authoring final report`)")
310
355
  return parser
311
356
 
312
357
 
@@ -332,6 +332,7 @@ def _parser() -> argparse.ArgumentParser:
332
332
  run.add_argument("--task-group", required=True)
333
333
  run.add_argument("--task-id", required=True)
334
334
  run.add_argument("--child-task-id", default=None)
335
+ run.add_argument("--allow-completed", action="store_true")
335
336
 
336
337
  split = task_sub.add_parser("split")
337
338
  split.add_argument("--manager-id", required=True)
@@ -466,6 +467,7 @@ def main(argv: list[str] | None = None) -> int:
466
467
  args.task_id,
467
468
  args.project_id,
468
469
  child_task_id=args.child_task_id,
470
+ allow_completed=args.allow_completed,
469
471
  ),
470
472
  args.json,
471
473
  )
@@ -20,7 +20,7 @@ from .manager_paths import (
20
20
  task_manifest_path,
21
21
  )
22
22
  from .manager_store import ManagerError, _now_iso, _read_json, _read_json_default, _write_json
23
- from .manager_sync import _resolve_task_root_read_only
23
+ from .manager_sync import _snapshot_child, summary_bucket
24
24
 
25
25
 
26
26
  WORKER_DISPATCH_BACKEND = "subagent"
@@ -222,6 +222,25 @@ def _record_launch_prepared(
222
222
  )
223
223
 
224
224
 
225
+ def _current_child_state(project_root: str, child: dict, target_task_key: str, *, allow_completed: bool) -> dict:
226
+ """대상 하위 task 를 마지막 sync 가 아니라 지금 프로젝트에서 다시 읽는다.
227
+
228
+ 스냅샷은 sync 시점 값이라, 그 뒤 프로젝트에서 끝난 task 에 런치 패킷을 만들면 같은 일을 두 번 시킨다.
229
+ """
230
+ current = _snapshot_child(Path(project_root), {**child, "taskKey": target_task_key})
231
+ if current.get("error"):
232
+ raise ManagerError(f"{target_task_key}: cannot read child task state: {current['error']}")
233
+ if summary_bucket(current) == "done" and not allow_completed:
234
+ report = current.get("latestReportRecordPath") or ""
235
+ raise ManagerError(
236
+ f"{target_task_key}: already done in its project "
237
+ f"(last phase {current.get('lastCompletedPhase') or current.get('currentPhase') or 'unknown'}, "
238
+ f"report {Path(project_root) / report if report else 'none'}); nothing to launch. "
239
+ "Run `okstra manager task sync` to refresh the manager, or pass --allow-completed to prepare a launch anyway"
240
+ )
241
+ return current
242
+
243
+
225
244
  def build_launch_packet(
226
245
  home: Path,
227
246
  manager_id: str,
@@ -230,6 +249,7 @@ def build_launch_packet(
230
249
  project_id: str,
231
250
  *,
232
251
  child_task_id: str | None = None,
252
+ allow_completed: bool = False,
233
253
  now: str | None = None,
234
254
  ) -> dict:
235
255
  manifest = _read_json(task_manifest_path(home, manager_id, task_group, task_id))
@@ -244,6 +264,7 @@ def build_launch_packet(
244
264
  target_task_key = _target_task_key(project_id, task_group, child_task_id_value)
245
265
  _validate_child_task_key(child, target_task_key)
246
266
  project_root = _validated_project_root(project)
267
+ current = _current_child_state(project_root, child, target_task_key, allow_completed=allow_completed)
247
268
  created_at = now or _now_iso()
248
269
  brief_path = (Path(str(child["briefPath"])) if child.get("briefPath")
249
270
  else _discussion_follow_up_brief(snapshot, target_task_key))
@@ -277,7 +298,7 @@ def build_launch_packet(
277
298
  # phase 를 채워야 하는데, 두 값이 다 주어지면 그 채우기를 건너뛴다(autofill_from_manifest).
278
299
  # 토론 후속 브리프는 권장 phase 를 따로 적지 않아도 requirements-discovery 에서 시작한다.
279
300
  recommended = child.get("recommendedPhase") or (DISCUSSION_FOLLOW_UP_PHASE if discussion_ref else "")
280
- if recommended and _resolve_task_root_read_only(Path(project_root), target_task_key) is None:
301
+ if recommended and not current["exists"]:
281
302
  run_args.extend(["--task-type", str(recommended)])
282
303
  if brief_path is not None:
283
304
  run_args.extend(["--task-brief", str(brief_path)])
@@ -1,7 +1,7 @@
1
1
  {
2
- "allowed": " - entering this phase only when the cited final-verification report's verdict is release-ready — `Verdict Token` exactly `accepted`, or exactly `conditional-accept` with every Conditional Acceptance Condition row declaring `blocksReleaseHandoff: false` (`okstra_ctl.release_gate.release_handoff_allowed`)\n - asking the user (via `AskUserQuestion` / interactive prompt) which delivery action to take: `local checkout`, `push + PR`, or `skip` (end the run)\n - asking the user to pick a PR base branch from `staging` | `preprod` | `prod` | `main` | `dev` | a user-supplied branch name\n - drafting PR title and PR body **inline as the Okstra lead** (no drafter worker and no delegated dispatch); the lead reviews its own draft with the user before any mutating git / gh command runs\n - read-only git inspection: `git status`, `git diff`, `git log`, `git rev-parse`\n - pushing the current feature branch to its origin remote via `git push -u origin <current-branch>` (the feature branch only — NEVER the base branch)\n - creating a pull request via `gh pr create --base <chosen-base> --head <current-branch>`; if a PR with the same head already exists, surface its URL and skip creation\n - the lead writes the final report directly (no `Report writer worker` dispatch); the report still conforms to the standard final-report template",
2
+ "allowed": " - entering this phase only when the cited final-verification report's verdict is release-ready — `Verdict Token` exactly `accepted`, or exactly `conditional-accept` with every Conditional Acceptance Condition row declaring `blocksReleaseHandoff: false` (`okstra_ctl.release_gate.release_handoff_allowed`)\n - asking the user (via `AskUserQuestion` / interactive prompt) which delivery action to take: `local checkout`, `push + PR`, or `skip` (end the run)\n - asking the user to pick a PR base branch from `staging` | `preprod` | `prod` | `main` | `dev` | a user-supplied branch name\n - drafting PR title and PR body **inline as the Okstra lead** (no drafter worker and no delegated dispatch); the lead reviews its own draft with the user before any mutating git / gh command runs\n - read-only git inspection: `git status`, `git diff`, `git log`, `git rev-parse`\n - pushing the current feature branch to its origin remote via `git push -u origin <current-branch>` (the feature branch only — NEVER the base branch)\n - creating a pull request via `gh pr create --base <chosen-base> --head <current-branch>`; if a PR with the same head already exists, surface its URL and skip creation\n - the lead writes the report narrative at the run manifest's `reportNarrativePath` directly (no `Report writer worker` dispatch); `okstra report-finalize` assembles the final report from it",
3
3
  "forbidden": [
4
- "entering this phase when the cited final-verification `Verdict Token` is `conditional-accept` or `blocked`, or when no final-verification report is cited",
4
+ "entering this phase when a cited final-verification verdict is not release-ready — `Verdict Token` `blocked` or any token other than `accepted` / `conditional-accept`, a `conditional-accept` with no Conditional Acceptance Condition row or with any row whose `blocksReleaseHandoff` is not `false` (`okstra_ctl.release_gate.release_handoff_allowed`) — or when no final-verification report is cited",
5
5
  "local commit commands of any kind (`git add`, `git commit`, `git restore --staged`, `git stash`), and any direct `git merge` / `git rebase` / `git rebase --onto` / `git cherry-pick` / `git commit --amend` run by the lead. Runtime exceptions are dependency-ordered rebases by `okstra_ctl.stage_rebase` (which invalidate verification and stop PR delivery), and merge commits on a dedicated multi-predecessor `merge-base` branch. Never merge a moving predecessor or origin branch into a downstream stage to update its base.",
6
6
  "any git push variant that rewrites remote history, regardless of intent or whether the user said \"force it\": `git push --force`, `git push --force-with-lease`, `git push -f`, `git push +<refspec>`, or any other history-rewriting invocation",
7
7
  "pushing directly to a release base branch — i.e. `git push origin <branch>` where `<branch>` is `main`, `master`, `prod`, `preprod`, `staging`, `dev`, or the branch the user chose as the release base in this run. The only permitted push targets are the branches `okstra handoff pr-plan` listed for this run (each stage `head_branch`, and any `merge-base` branch).",
@@ -4,12 +4,12 @@
4
4
  the `releaseBase` the user picked. The report is the only place a reader learns which stages
5
5
  shipped and in what order their PRs must be merged — the branch names do not say.
6
6
  - Purpose: take a release-ready single-stage final-verification verdict for each already-committed implementation stage and turn it into a delivered push and/or pull request, with explicit user selection at every delivery step. **One stage is one PR.** The PR head is that stage's stack branch (`<prefix>-<task-id>-s<N>`); nothing is squashed or collected into a bundle branch. Runtime dependency rebases happen before delivery and invalidate the prior verification.
7
- - **Execution model: single-lead, no worker dispatch.** This phase is a thin orchestrator over `git` / `gh`; it does NOT dispatch teammates, does NOT dispatch analysis or drafter sub-agents, and does NOT run convergence. The host-native Okstra lead performs every step inline (drafting PR text, asking the user, running git / gh, writing the final report) — see "Lead-only contract" below.
7
+ - **Execution model: single-lead, no worker dispatch.** This phase is a thin orchestrator over `git` / `gh`; it does NOT dispatch teammates, does NOT dispatch analysis or drafter sub-agents, and does NOT run convergence. The host-native Okstra lead performs every step inline (drafting PR text, asking the user, running git / gh, writing the report narrative) — see "Lead-only contract" below.
8
8
  - Worker roster: none — `profile.json` declares no roles; the run is executed entirely by the Okstra lead.
9
9
  - Lead-only contract (replaces the shared team contract for this phase):
10
10
  - The host-native Okstra lead is the sole agent for this run. No worker dispatch, no teammates, no parallel sub-agents, no convergence loop. Do NOT run `okstra convergence` in any form: prepare already wrote this run's convergence state as "not run — fewer than two analysers", which is the record report assembly reads. **Enforced:** `okstra_ctl.render._initialize_lead_only_convergence` writes it when the run has no analysers, and leaves an existing state alone.
11
11
  - The lead drafts each stage's PR title and PR body **inline** by reading the run brief, that stage's cited final-verification report, `git log --oneline <stage base>..<stage head>`, and `git diff <stage base>..<stage head> --stat`. No drafter worker is dispatched.
12
- - The lead authors the final-report file directly (no `Report writer worker` dispatch). The report still conforms to the standard `templates/reports/final-report-v2.template.md` structure, including the `## 5.6 Release Handoff Deliverables` section.
12
+ - The lead authors the report narrative directly (no `Report writer worker` dispatch) and never writes `final-report-*.data.json`. Write the narrative to the path the run manifest names in `reportNarrativePath`. It carries only the writer-owned fields (`humanSummary`, `summary`, `verdictCard`, `finalVerdict`, `rationale`, `recommendedNextSteps`, `followUpTasks`, and `releaseHandoff` with its `userNarrative`) in the narrative line grammar of `report_narrative.NARRATIVE_GRAMMAR_INSTRUCTIONS`: the first line is `# OKSTRA Report Narrative`, followed only by `- **Field Name**`, `- Item <N>` and `> value` lines, each value indented two spaces deeper than its label. `okstra report-finalize` assembles `data.json` from this narrative plus the approval, activity, team-state and convergence ledgers prepare already created. A missing or unparseable narrative fails the `project-activity` step with `owner=lead`. **Enforced:** `report_inputs._LEAD_AUTHORED_NARRATIVE_TASK_TYPES`, read by `report_assembly.assemble_report`.
13
13
  - The shared anti-escalation rule from the common contract still applies: do not start any other lifecycle phase from inside this run.
14
14
  - The shared "authority & permissions assumption" rule from the common contract still applies: assume the user holds every permission needed; do not block on hypothetical approvals.
15
15
  - Pre-handoff entry gate (mandatory — refuse to start if any item fails):
@@ -33,7 +33,7 @@ When a predecessor advances, `okstra_ctl.stage_rebase.synchronize_stage_stack` r
33
33
 
34
34
  Because the PRs are a stack, they must be merged in dependency order with a merge commit or a rebase-merge. A squash replaces the commits the next PR's base points at, so the stack breaks — every PR body states this, and the lead never merges.
35
35
 
36
- This phase has no worker dispatch. It does not use a provider or report-writer roster; the host-native Okstra lead performs git/gh inspection, user questions, the PR draft, and the final report inline.
36
+ This phase has no worker dispatch. It does not use a provider or report-writer roster; the host-native Okstra lead performs git/gh inspection, user questions, the PR draft, and the report narrative inline; `okstra report-finalize` assembles the final report from that narrative.
37
37
 
38
38
  ## 2. okstra-run wizard flow
39
39
 
@@ -762,7 +762,7 @@ def _attach_machine_planning(
762
762
  _sync_human_summary_with_plan_body_gate(data, projection)
763
763
 
764
764
 
765
- def _schema_owner(error: str) -> str:
765
+ def _schema_owner(error: str, narrative_owner: str) -> str:
766
766
  field = error.split(" ", 1)[0]
767
767
  owners = {
768
768
  "executionStatus": "team-state",
@@ -781,8 +781,8 @@ def _schema_owner(error: str) -> str:
781
781
  if "planBodyVerification" in error:
782
782
  return "convergence"
783
783
  if "selectedDirectionRef" in error:
784
- return "report-writer"
785
- return owners.get(field.split(".", 1)[0], "report-writer")
784
+ return narrative_owner
785
+ return owners.get(field.split(".", 1)[0], narrative_owner)
786
786
 
787
787
 
788
788
  def _publish(
@@ -864,7 +864,7 @@ def assemble_report(
864
864
  path_errors = planned_path_declaration_errors(data)
865
865
  if path_errors:
866
866
  raise ReportAssemblyError(tuple(
867
- AssemblyIssue("report-writer", str(inputs["narrative"].path), error.split(":", 1)[0], error)
867
+ AssemblyIssue(inputs["narrative"].owner, str(inputs["narrative"].path), error.split(":", 1)[0], error)
868
868
  for error in path_errors
869
869
  ))
870
870
  direction_errors = selected_direction_plan_errors(
@@ -888,7 +888,7 @@ def assemble_report(
888
888
  raise ReportAssemblyError(
889
889
  tuple(
890
890
  AssemblyIssue(
891
- _schema_owner(error),
891
+ _schema_owner(error, inputs["narrative"].owner),
892
892
  str(target),
893
893
  error.split(" ", 1)[0],
894
894
  error,
@@ -28,6 +28,10 @@ _INPUT_FIELDS = (
28
28
  ("convergence", "convergence", "convergenceStatePath"),
29
29
  )
30
30
 
31
+ # 워커 없이 리드가 리포트까지 쓰는 phase. 서사의 저자가 리드이므로, 서사가
32
+ # 없을 때 조립이 report-writer 를 지목하면 띄울 수 없는 워커를 찾게 된다.
33
+ _LEAD_AUTHORED_NARRATIVE_TASK_TYPES = frozenset({"release-handoff"})
34
+
31
35
  _PLANNING_INPUT_FIELDS = (
32
36
  ("design-preparation", "design-surface-detector", "designPreparationPath"),
33
37
  ("plan-body-verification", "convergence", "planBodyVerificationPath"),
@@ -63,8 +67,13 @@ def report_input_paths(
63
67
  if manifest.get("taskType") == "implementation-planning"
64
68
  else ()
65
69
  )
70
+ lead_authored = manifest.get("taskType") in _LEAD_AUTHORED_NARRATIVE_TASK_TYPES
66
71
  rows = tuple(
67
- ReportInputPath(key, owner, _project_path(project_root, manifest.get(field), field))
72
+ ReportInputPath(
73
+ key,
74
+ "lead" if lead_authored and key == "narrative" else owner,
75
+ _project_path(project_root, manifest.get(field), field),
76
+ )
68
77
  for key, owner, field in fields
69
78
  )
70
79
  if len({row.key for row in rows}) != len(rows):
@@ -96,7 +96,7 @@ def _build_group_prompt(state: WizardState, group_id: str) -> Prompt:
96
96
 
97
97
 
98
98
  def _is_lone_choice_pick(prompt: Prompt) -> bool:
99
- """고를 것이 한 줄뿐인 단일 선택 픽인가.
99
+ """고를 것이 한 줄뿐인 픽인가 — 단일 선택이든 체크박스든.
100
100
 
101
101
  호스트 선택기는 옵션 2개 미만을 받지 않아(`nativeLimits` `minOptions`) 이런
102
102
  화면은 번호 1개짜리 목록으로 내려간다 — 사용자는 고를 것이 없는 목록에서
@@ -106,7 +106,6 @@ def _is_lone_choice_pick(prompt: Prompt) -> bool:
106
106
  """
107
107
  return (
108
108
  prompt.kind == "pick"
109
- and not prompt.multi
110
109
  and len(prompt.options) == 1
111
110
  and prompt.options[0].value != _ABORT_OPTION
112
111
  )
@@ -29,7 +29,7 @@ okstra manager task status --manager-id <manager-id> --task-group <task-group> -
29
29
  okstra manager task sync --manager-id <manager-id> --task-group <task-group> --task-id <task-id>
30
30
  okstra manager task assign --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --project-id <project-id> [--child-task-id <child-task-id>] [--role <role>] [--tag <tag>] [--assignment <text>]
31
31
  okstra manager task note --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --scope <shared|project> [--project-id <project-id>] --body <text>
32
- okstra manager task run --manager-id <manager-id> --project-id <project-id> --task-group <task-group> --task-id <task-id> [--child-task-id <child-task-id>]
32
+ okstra manager task run --manager-id <manager-id> --project-id <project-id> --task-group <task-group> --task-id <task-id> [--child-task-id <child-task-id>] [--allow-completed]
33
33
  okstra manager task split --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --plan <split-plan.json> [--overwrite]
34
34
  okstra manager list managers
35
35
  okstra manager list projects --manager-id <manager-id>
@@ -104,7 +104,7 @@ When the user wants to see a manager at a glance, run `okstra manager task sync
104
104
  For launching child work:
105
105
 
106
106
  1. Run `okstra manager task sync ...` if the manager snapshot must be refreshed first.
107
- 2. Run `okstra manager task run ...`.
107
+ 2. Run `okstra manager task run ...`. It re-reads the child's state from its project, not from the snapshot. When the child is already done there (terminal next phase, or work status `done`), it refuses with the last phase and report path; tell the user that and do not add `--allow-completed` unless the user asks to run the child again.
108
108
  3. Read the returned fixed fields `Backend`, `Worker dispatch backend`, `Project root`, `Context path`, `Run command`, every numbered `Run arg N`, and `Shell command`.
109
109
  4. Give the user the `Shell command` line verbatim and tell them to run it in a new terminal. It starts the installed launcher in the child project: the launcher fills the task type and brief from the child task manifest or asks for them, prepares the run with the manager context directive, and starts the child lead as a separate host process (`Backend` `spawn-process`).
110
110