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.
- package/dist/lib/paths.mjs +1 -1
- package/dist/lib/paths.mjs.map +1 -1
- package/docs/cli.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +3 -3
- package/runtime/python/okstra_ctl/business_flow/engine.py +10 -2
- package/runtime/python/okstra_ctl/lead_progress.py +56 -11
- package/runtime/python/okstra_ctl/manager_cli.py +2 -0
- package/runtime/python/okstra_ctl/manager_launch.py +23 -2
- package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +2 -2
- package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +2 -2
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +1 -1
- package/runtime/python/okstra_ctl/report_assembly.py +5 -5
- package/runtime/python/okstra_ctl/report_inputs.py +10 -1
- package/runtime/python/okstra_ctl/wizard/engine.py +1 -2
- package/runtime/skills/okstra-manager/SKILL.md +2 -2
package/dist/lib/paths.mjs
CHANGED
|
@@ -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/
|
|
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"))
|
package/dist/lib/paths.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.mjs","sourceRoot":"","sources":["../../src/lib/paths.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGlE,SAAS,cAAc,CAAC,CAAS;IAC/B
|
|
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
package/runtime/BUILD.json
CHANGED
|
@@ -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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
785
|
-
return owners.get(field.split(".", 1)[0],
|
|
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("
|
|
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(
|
|
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
|
|