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.
Files changed (37) hide show
  1. package/dist/cli-registry.mjs +9 -0
  2. package/dist/cli-registry.mjs.map +1 -1
  3. package/docs/cli.md +4 -2
  4. package/docs/project-structure-overview.md +1 -0
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/prompts/lead/convergence.md +19 -4
  8. package/runtime/prompts/lead/report-writer.md +1 -1
  9. package/runtime/prompts/lead/team-contract.md +4 -3
  10. package/runtime/prompts/profiles/implementation-option-selection.md +4 -0
  11. package/runtime/prompts/wizard/prompts.ko.json +6 -2
  12. package/runtime/python/okstra_ctl/conformance.py +26 -5
  13. package/runtime/python/okstra_ctl/convergence.py +58 -3
  14. package/runtime/python/okstra_ctl/convergence_engine.py +71 -0
  15. package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +14 -3
  16. package/runtime/python/okstra_ctl/convergence_store.py +36 -0
  17. package/runtime/python/okstra_ctl/dispatch_core.py +10 -3
  18. package/runtime/python/okstra_ctl/dispatch_state.py +30 -4
  19. package/runtime/python/okstra_ctl/group_context.py +96 -3
  20. package/runtime/python/okstra_ctl/implementation_options.py +123 -0
  21. package/runtime/python/okstra_ctl/option_votes.py +194 -0
  22. package/runtime/python/okstra_ctl/report_assembly.py +10 -0
  23. package/runtime/python/okstra_ctl/run.py +23 -0
  24. package/runtime/python/okstra_ctl/set_work_status.py +30 -1
  25. package/runtime/python/okstra_ctl/verdict_blocks.py +27 -0
  26. package/runtime/python/okstra_ctl/wizard/engine.py +4 -0
  27. package/runtime/python/okstra_ctl/wizard/ids.py +1 -0
  28. package/runtime/python/okstra_ctl/wizard/registry.py +8 -0
  29. package/runtime/python/okstra_ctl/wizard/steps_options.py +37 -1
  30. package/runtime/python/okstra_ctl/worker_audit_check.py +38 -16
  31. package/runtime/python/okstra_ctl/worker_liveness.py +48 -2
  32. package/runtime/python/okstra_ctl/workflow.py +1 -1
  33. package/runtime/skills/okstra-brief-gen/SKILL.md +24 -7
  34. package/runtime/skills/okstra-inspect/facets/recap.md +17 -1
  35. package/runtime/skills/okstra-run/SKILL.md +1 -0
  36. package/runtime/templates/reports/group-context.template.md +1 -1
  37. 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. If the file
1028
- exists, leave it untouched and mention its path in the hand-off block. The
1029
- file's trailing `## Task Memory` region (between `<!-- okstra:task-memory:begin -->`
1030
- and `end`) is okstra's: `report-finalize` rewrites it after every run with the
1031
- group's start order and each task's latest conclusion, and a group whose
1032
- runs finished before anyone created the file already has one holding that
1033
- region alone — `init` then inserts the human sections above it.
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 only the `recap-log.jsonl` append and the `notes/` note authoring (recap.5); it never mutates `task-manifest.json` / catalog / timeline / `group-context.md`.
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 rewrites this region at every report-finalize. Edit the sections above it, not this one. -->
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") != row.get("promptDigest"),
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
- actual_script = _normalize_conformance_script(str(actual_entry.get("script") or ""))
2034
- if actual_script != declaration.get("script"):
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)