okstra 0.197.0 → 0.198.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.
- package/dist/cli-registry.mjs +9 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/docs/cli.md +4 -2
- package/docs/project-structure-overview.md +1 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/convergence.md +19 -4
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/lead/team-contract.md +4 -3
- package/runtime/prompts/profiles/implementation-option-selection.md +4 -0
- package/runtime/prompts/wizard/prompts.ko.json +6 -2
- package/runtime/python/okstra_ctl/conformance.py +26 -5
- package/runtime/python/okstra_ctl/convergence.py +58 -3
- package/runtime/python/okstra_ctl/convergence_engine.py +71 -0
- package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +14 -3
- package/runtime/python/okstra_ctl/convergence_store.py +36 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +10 -3
- package/runtime/python/okstra_ctl/dispatch_state.py +30 -4
- package/runtime/python/okstra_ctl/group_context.py +96 -3
- package/runtime/python/okstra_ctl/implementation_options.py +123 -0
- package/runtime/python/okstra_ctl/option_votes.py +194 -0
- package/runtime/python/okstra_ctl/report_assembly.py +10 -0
- package/runtime/python/okstra_ctl/run.py +23 -0
- package/runtime/python/okstra_ctl/set_work_status.py +30 -1
- package/runtime/python/okstra_ctl/verdict_blocks.py +27 -0
- package/runtime/python/okstra_ctl/wizard/engine.py +4 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +8 -0
- package/runtime/python/okstra_ctl/wizard/steps_options.py +37 -1
- package/runtime/python/okstra_ctl/worker_audit_check.py +38 -16
- package/runtime/python/okstra_ctl/worker_liveness.py +48 -2
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/skills/okstra-brief-gen/SKILL.md +24 -7
- package/runtime/skills/okstra-inspect/facets/recap.md +17 -1
- package/runtime/skills/okstra-run/SKILL.md +1 -0
- package/runtime/templates/reports/group-context.template.md +1 -1
- package/runtime/validators/validate-run.py +77 -3
|
@@ -1024,13 +1024,30 @@ per-page ratio instead of origin load). If the file is absent, ask once via
|
|
|
1024
1024
|
- `Skip — this group needs no shared context`.
|
|
1025
1025
|
|
|
1026
1026
|
Never fill the skeleton yourself from the tickets: a ticket summary decides
|
|
1027
|
-
nothing, and the sections ask for what the tickets do not say.
|
|
1028
|
-
|
|
1029
|
-
file
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1027
|
+
nothing, and the sections ask for what the tickets do not say.
|
|
1028
|
+
|
|
1029
|
+
If the file exists, what you do with it depends on what you are writing:
|
|
1030
|
+
|
|
1031
|
+
- **A new brief** leaves it untouched. Mention its path in the hand-off block.
|
|
1032
|
+
- **A rewrite or correction of an existing brief** reconciles it in the same
|
|
1033
|
+
response, whenever the change supersedes something the group document
|
|
1034
|
+
states — a Definition of Better figure, a success signal, a Group-Wide
|
|
1035
|
+
Constraint, a Ticket Relation, or what it says is executable from the
|
|
1036
|
+
project root. okstra copies this file into every run's
|
|
1037
|
+
`instruction-set/task-group-context.md` and carries it in the analysis
|
|
1038
|
+
packet **ahead of the brief extract**, so a sentence you corrected in the
|
|
1039
|
+
brief is still outranked by the group document's stale copy of it. Qualify a
|
|
1040
|
+
diverged figure with the date and method of the measurement that contradicts
|
|
1041
|
+
it rather than deleting it, and name the brief the correction came from.
|
|
1042
|
+
|
|
1043
|
+
Either way, edit only the authored sections. The file's trailing `## Task Memory`
|
|
1044
|
+
region (between `<!-- okstra:task-memory:begin -->` and `end`) is okstra's: it
|
|
1045
|
+
holds the group's start order and each task's latest conclusion, and okstra
|
|
1046
|
+
redraws it after every run's `report-finalize`, at `okstra set-work-status`, and
|
|
1047
|
+
before each run copies the file into its instruction set. Anything written there
|
|
1048
|
+
by hand is overwritten without warning. A group whose runs finished before
|
|
1049
|
+
anyone created the file already has one holding that region alone — `init` then
|
|
1050
|
+
inserts the human sections above it.
|
|
1034
1051
|
|
|
1035
1052
|
Then stop. Do not invoke `okstra-run` directly — the user chooses when to
|
|
1036
1053
|
proceed, and they may want to edit the brief externally first. In the
|
|
@@ -6,7 +6,7 @@ Loaded lazily by the dispatch table in `SKILL.md` (core). Shared rules — Step
|
|
|
6
6
|
|
|
7
7
|
Trigger phrases: "okstra recap", "recap", "work summary", "summarize this task", "before/after summary", "explain this work", "task question".
|
|
8
8
|
|
|
9
|
-
On top of the `.okstra` artifacts accumulated for a single task-id — or for every task of one task-group — (a) produce a before/after summary and (b) answer free-form questions about that work. By default it reads only the `.okstra/` subtree (artifact mode). It expands to code mode only when the user explicitly asks to look at the code changes too. This sub-command performs
|
|
9
|
+
On top of the `.okstra` artifacts accumulated for a single task-id — or for every task of one task-group — (a) produce a before/after summary and (b) answer free-form questions about that work. By default it reads only the `.okstra/` subtree (artifact mode). It expands to code mode only when the user explicitly asks to look at the code changes too. This sub-command performs the `recap-log.jsonl` append, the `notes/` note authoring (recap.5), and the group-context reconciliation that note triggers (recap.6); it never mutates `task-manifest.json` / catalog / timeline, and it touches `group-context.md` only in the authored sections above the `<!-- okstra:task-memory:begin -->` marker.
|
|
10
10
|
|
|
11
11
|
### recap.1 — Resolve target
|
|
12
12
|
|
|
@@ -144,3 +144,19 @@ Write the body to the scratchpad as markdown first, then pass it with `--body-fi
|
|
|
144
144
|
4. **`notes/` is inert to okstra** — no run reads it automatically. After writing, relay the `clarificationResponseArg` the CLI printed (e.g. `--clarification-response <notePath>`) to the user verbatim, telling them it only takes effect when the next run is executed with that argument.
|
|
145
145
|
|
|
146
146
|
**Guardrail:** `.okstra/` is gitignored — treat `notes/` as local scratch and never `git add` it. Creating a new note is easy to undo (delete the file), so always prefer it over editing a generated or user-owned file.
|
|
147
|
+
|
|
148
|
+
### recap.6 — Reconcile the task-group context (required whenever recap.5 writes a note)
|
|
149
|
+
|
|
150
|
+
A group's shared context does not stay true while its tasks run. Figures get re-measured, a success signal turns out to be satisfiable without a fix, a constraint names the wrong place. `group-context.md` states in its own header that okstra copies it into each run's `instruction-set/task-group-context.md` and carries it in the analysis packet's `## Task-Group Context` section, **ahead of the brief extract** — so a stale sentence there outranks a corrected brief for every task in the group.
|
|
151
|
+
|
|
152
|
+
**After writing a note, read `<PROJECT_ROOT>/.okstra/briefs/<task-group>/group-context.md` if it exists.** If anything the note establishes touches its Definition of Better figures, success signals, Group-Wide Constraints, Ticket Relations, or its statement of what is executable from the project root, update those sections **in the same response that wrote the note**. A note written without that check is an incomplete deliverable, the same way a `.project-docs/` document without its index row is.
|
|
153
|
+
|
|
154
|
+
Three rules for the edit:
|
|
155
|
+
|
|
156
|
+
1. **Only above the marker.** Edit the authored sections above `<!-- okstra:task-memory:begin -->`. The region below is okstra's projection — start order, per-task status, recorded runs — and it is redrawn from the task manifests at every `report-finalize`, at `okstra set-work-status`, and before each run copies the file. Anything you write there is overwritten without warning.
|
|
157
|
+
2. **Qualify a diverged figure; do not delete it.** An audit number is the baseline for the window it was measured in, not a current reading. Where a direct measurement contradicts it, say so with the measurement's date and method, and keep both. Deleting the old figure destroys the reason the group exists.
|
|
158
|
+
3. **Say which task and note the correction came from.** The next reader needs to get from the sentence back to its evidence.
|
|
159
|
+
|
|
160
|
+
If the group has no `group-context.md`, do not create one here — `okstra group-context init` (via okstra-brief-gen) owns that skeleton, and creating an empty one pre-empts the sections a human meant to author.
|
|
161
|
+
|
|
162
|
+
The same exposure applies to any other edit that supersedes group-wide facts, a brief rewrite most of all. Treat this check as belonging to the fact, not to this sub-command.
|
|
@@ -191,6 +191,7 @@ Repeat until `next.kind == "done"` (or `"aborted"` — terminal cancel, see "How
|
|
|
191
191
|
|
|
192
192
|
That is the entire interactive flow. The wizard handles:
|
|
193
193
|
|
|
194
|
+
- project report language before task selection: when `.okstra/project.json` has no `reportLanguage` value, ask for a language tag and save the answer to that file. An existing value skips this question. Prompt rendering and progress estimation do not save a language,
|
|
194
195
|
- new-vs-existing task split (remaining work — `workStatus != done` — top-3 newest recommendations + Enter directly), task-group / task-id slug validation (task-group offers the top-3 newest candidates combining recent task use + recent `.okstra/briefs/<group>/` creation activity + Enter directly; task-id offers the top-3 recent candidates from the same group + Enter directly),
|
|
195
196
|
- task-type pick (3 options + Enter directly; Enter directly is validated against the full task-type whitelist in a follow-up `text` step). **For a brand-new task the brief is asked first** and the three options are entry phases only (`requirements-discovery` / `improvement-discovery` / `project-analysis` / `feature-analysis` / `change-impact-analysis` / `error-analysis`) — a new task-key has no approved plan, no implementation and no commits, so `implementation`, `final-verification` and `release-handoff` cannot be entered there; the recommended slot comes from the selected brief's `Recommended next phase:` line and falls back to `requirements-discovery` when the brief carries none. For an existing task-key what fills those three depends on `workflow.nextRecommendedPhase` — an object `{phase, status, rationale}`, not a phase-name string. `status: ready` gives the classic trio: its `phase` marked recommended, re-run the current phase, the lifecycle's next step. Any other status contributes nothing at all, since both the recommended slot and the next-step slot derive from that phase — re-run the current phase becomes the first option and the remaining slots fill unlabelled from recently used task-types and then the whitelist. Never recover a phase name from a non-`ready` pointer and propose it yourself: prepare deliberately lowers the pointer to `pending` while its run is unfinished, and the missing recommendation is that signal,
|
|
196
197
|
- brief path — **for a new task it is asked right after task-group, before the task-type** (it is the only input that says what the task is, and the task-type recommendation reads it); for an existing task it is **asked only for entry task-types (requirements-discovery / error-analysis / improvement-discovery / project-analysis / feature-analysis / change-impact-analysis)** (same-group `.okstra/briefs/<task-group>/**/*.md` candidates first, sorted by the newer of file-created/modified time and latest task-catalog use; direct input last; `Keep / Change` for existing entry tasks). `project-analysis`, `feature-analysis`, and `change-impact-analysis` are brief entry task types. On an existing task a downstream lifecycle task-type auto carries in the manifest's brief, and when no registered brief exists a `brief_carry` 3-option prompt appears (recommend switching to entry / Enter directly / Abort). `release-handoff` has no brief step of its own — a new-task run answers the brief before the type is known, and the brief is dropped at render time, so prepare always generates the input document that cites the verification report,
|
|
@@ -31,7 +31,7 @@ generator: okstra-brief-gen
|
|
|
31
31
|
<Which tickets are causes, which are observation means, which depend on which. Write `_(none)_` when the briefs' Related Task Graph already says it all.>
|
|
32
32
|
|
|
33
33
|
<!-- okstra:task-memory:begin -->
|
|
34
|
-
<!-- okstra
|
|
34
|
+
<!-- okstra redraws this region after every report-finalize, at `okstra set-work-status`, and before each run copies this file. Edit the sections above it, not this one. -->
|
|
35
35
|
## Task Memory
|
|
36
36
|
|
|
37
37
|
_(no runs recorded yet)_
|
|
@@ -115,6 +115,7 @@ from okstra_ctl.final_report_paths import ( # noqa: E402
|
|
|
115
115
|
)
|
|
116
116
|
from okstra_token_usage.report import _match_worker_index # noqa: E402
|
|
117
117
|
from okstra_ctl.implementation_options import ( # noqa: E402
|
|
118
|
+
validate_blocked_answer_channel,
|
|
118
119
|
validate_implementation_option_selection,
|
|
119
120
|
)
|
|
120
121
|
from okstra_ctl.implementation_direction import ( # noqa: E402
|
|
@@ -149,6 +150,7 @@ from okstra_ctl.agent.invocation import ( # noqa: E402
|
|
|
149
150
|
AgentInvocationError,
|
|
150
151
|
agent_model_assignment_from_payload,
|
|
151
152
|
invocation_execution_identity_from_manifest,
|
|
153
|
+
invocation_input_digest,
|
|
152
154
|
verify_agent_invocation,
|
|
153
155
|
)
|
|
154
156
|
from okstra_ctl.execution_identity import ExecutionManifestError # noqa: E402
|
|
@@ -253,6 +255,39 @@ def _result_link_attempt_status_failure(
|
|
|
253
255
|
)
|
|
254
256
|
|
|
255
257
|
|
|
258
|
+
def _dispatch_input_digest(
|
|
259
|
+
project_root: Path, row: Mapping[str, Any]
|
|
260
|
+
) -> tuple[str | None, str]:
|
|
261
|
+
"""이 디스패치의 예약 입력 해시, 예약이 계산한 것과 같은 방법으로.
|
|
262
|
+
|
|
263
|
+
`inputDigest` 와 `promptDigest` 는 서로 다른 대상이다. 전달 계약
|
|
264
|
+
`execution-identity-v1` 부터 예약은 **논리 작업**을 해시한다 — 시도별 전달값과
|
|
265
|
+
prompt history 경로를 뺀 본문(`agent_prompt_task_bytes`) — 반면 `promptDigest`
|
|
266
|
+
는 프롬프트 파일 전체 바이트다. 둘을 동등 비교하면 그 계약을 쓰는 run 은
|
|
267
|
+
통과할 수 있는 값이 하나도 없다(2026-09-08 f56ec08 이후 전부, 2026-09-10
|
|
268
|
+
dev-10642-15 final-verification 001 에서 7/7 디스패치가 이 규칙에 걸렸다).
|
|
269
|
+
|
|
270
|
+
그래서 재구현하지 않고 예약이 쓰는 함수를 그대로 부른다. 메타데이터 경로가
|
|
271
|
+
없는 구형 행만 `promptDigest` 로 돌아간다 — 그 계약에서는 두 값이 같다.
|
|
272
|
+
"""
|
|
273
|
+
metadata_value = row.get("promptMetadataPath")
|
|
274
|
+
if not isinstance(metadata_value, str) or not metadata_value.strip():
|
|
275
|
+
return row.get("promptDigest"), ""
|
|
276
|
+
try:
|
|
277
|
+
metadata = json.loads(
|
|
278
|
+
_resolve_prompt_record_path(project_root, metadata_value)
|
|
279
|
+
.read_text(encoding="utf-8")
|
|
280
|
+
)
|
|
281
|
+
except (OSError, json.JSONDecodeError):
|
|
282
|
+
return None, "prompt metadata is missing or invalid"
|
|
283
|
+
if not isinstance(metadata, Mapping):
|
|
284
|
+
return None, "prompt metadata is not an object"
|
|
285
|
+
try:
|
|
286
|
+
return invocation_input_digest(metadata, project_root), ""
|
|
287
|
+
except (AgentInvocationError, KeyError, OSError, ValueError) as exc:
|
|
288
|
+
return None, f"input digest cannot be recomputed: {exc}"
|
|
289
|
+
|
|
290
|
+
|
|
256
291
|
def _validate_agent_dispatch_contract(
|
|
257
292
|
*,
|
|
258
293
|
project_root: Path,
|
|
@@ -399,12 +434,16 @@ def _validate_agent_dispatch_contract(
|
|
|
399
434
|
)
|
|
400
435
|
continue
|
|
401
436
|
invocation = canonical_invocations.get(row.get("invocationRef"))
|
|
437
|
+
input_digest, digest_error = _dispatch_input_digest(project_root, row)
|
|
438
|
+
if digest_error:
|
|
439
|
+
failures.append(f"agent dispatch {dispatch_id}: {digest_error}")
|
|
440
|
+
continue
|
|
402
441
|
if not isinstance(invocation, Mapping) or any((
|
|
403
442
|
invocation.get("participantRef") != row.get("participantRef"),
|
|
404
443
|
invocation.get("roleExecutionRef") != row.get("roleExecutionRef"),
|
|
405
444
|
invocation.get("dutyId") != row.get("dutyId"),
|
|
406
445
|
invocation.get("dispatchKind") != dispatch_kind,
|
|
407
|
-
invocation.get("inputDigest") !=
|
|
446
|
+
invocation.get("inputDigest") != input_digest,
|
|
408
447
|
)):
|
|
409
448
|
failures.append(
|
|
410
449
|
f"agent dispatch {dispatch_id}: does not match canonical invocation"
|
|
@@ -2002,10 +2041,32 @@ def _approved_plan_conformance_manifest(
|
|
|
2002
2041
|
}
|
|
2003
2042
|
|
|
2004
2043
|
|
|
2044
|
+
def _conformance_script_matches(actual: str, declared: str) -> bool:
|
|
2045
|
+
"""두 script 표기가 같은 파일을 가리키는지 판정한다.
|
|
2046
|
+
|
|
2047
|
+
1순위는 task-root 상대형끼리의 일치다. task_root 아래로 접히지 않는
|
|
2048
|
+
절대경로(예: 워크트리 경로로 적힌 선언)가 남으면, 남은 쪽이 다른 쪽을
|
|
2049
|
+
경로 경계에서 후행 일치하는지까지 본다 — `/…/wt/qa/scripts/s.ts` 와
|
|
2050
|
+
`qa/scripts/s.ts` 는 같은 파일이다. 후행 일치는 경계(`/`)를 요구하므로
|
|
2051
|
+
`renamed-stage-1.ts` 같은 다른 파일은 걸리지 않는다.
|
|
2052
|
+
"""
|
|
2053
|
+
if actual == declared:
|
|
2054
|
+
return True
|
|
2055
|
+
if not actual or not declared:
|
|
2056
|
+
return False
|
|
2057
|
+
if actual.startswith("/") != declared.startswith("/"):
|
|
2058
|
+
longer, shorter = (
|
|
2059
|
+
(actual, declared) if actual.startswith("/") else (declared, actual)
|
|
2060
|
+
)
|
|
2061
|
+
return longer.endswith("/" + shorter)
|
|
2062
|
+
return False
|
|
2063
|
+
|
|
2064
|
+
|
|
2005
2065
|
def _declared_conformance_errors(
|
|
2006
2066
|
declared_manifest: dict,
|
|
2007
2067
|
actual_manifest: dict,
|
|
2008
2068
|
stage_name: str | None,
|
|
2069
|
+
task_root: Path,
|
|
2009
2070
|
) -> list[str]:
|
|
2010
2071
|
"""Compare scoped plan declarations with their one actual manifest entry."""
|
|
2011
2072
|
declared = _scope_manifest_entries(declared_manifest, stage_name).get("entries", [])
|
|
@@ -2030,8 +2091,15 @@ def _declared_conformance_errors(
|
|
|
2030
2091
|
errors.append(f"stage {stage_number} has multiple matching entries")
|
|
2031
2092
|
continue
|
|
2032
2093
|
actual_entry = matches[0]
|
|
2033
|
-
|
|
2034
|
-
|
|
2094
|
+
# 양쪽을 같은 task-root 상대형으로 접은 뒤 대조한다 — 계획이 절대경로를,
|
|
2095
|
+
# 실행자가 상대형을 쓰면 같은 파일이 문자열로는 영영 안 맞는다.
|
|
2096
|
+
actual_script = _normalize_conformance_script(
|
|
2097
|
+
str(actual_entry.get("script") or ""), task_root
|
|
2098
|
+
)
|
|
2099
|
+
declared_script = _normalize_conformance_script(
|
|
2100
|
+
str(declaration.get("script") or ""), task_root
|
|
2101
|
+
)
|
|
2102
|
+
if not _conformance_script_matches(actual_script, declared_script):
|
|
2035
2103
|
errors.append(f"stage {stage_number} script mismatch")
|
|
2036
2104
|
actual_requires = actual_entry.get("requires")
|
|
2037
2105
|
actual_capabilities = (
|
|
@@ -2231,6 +2299,7 @@ def _validate_conformance(
|
|
|
2231
2299
|
declared_manifest,
|
|
2232
2300
|
empty_scoped_manifest,
|
|
2233
2301
|
stage_name,
|
|
2302
|
+
task_root,
|
|
2234
2303
|
):
|
|
2235
2304
|
failures.append(
|
|
2236
2305
|
f"conformance gate BLOCKING: approved plan {error}; "
|
|
@@ -2257,6 +2326,7 @@ def _validate_conformance(
|
|
|
2257
2326
|
declared_manifest,
|
|
2258
2327
|
manifest,
|
|
2259
2328
|
stage_name,
|
|
2329
|
+
task_root,
|
|
2260
2330
|
):
|
|
2261
2331
|
failures.append(
|
|
2262
2332
|
f"conformance gate BLOCKING: approved plan {error} "
|
|
@@ -3242,6 +3312,10 @@ def validate_final_report_data(
|
|
|
3242
3312
|
participating_analysers,
|
|
3243
3313
|
)
|
|
3244
3314
|
)
|
|
3315
|
+
failures.extend(
|
|
3316
|
+
f"implementation-option-selection: {error}"
|
|
3317
|
+
for error in validate_blocked_answer_channel(data)
|
|
3318
|
+
)
|
|
3245
3319
|
elif task_type == "implementation":
|
|
3246
3320
|
_validate_stage_carry_sidecar_exists(data, report_path, failures)
|
|
3247
3321
|
_validate_lead_authored_report(data, report_path, failures)
|