okstra 0.183.2 → 0.184.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 (33) hide show
  1. package/README.md +1 -1
  2. package/docs/architecture.md +2 -2
  3. package/docs/cli.md +7 -4
  4. package/docs/project-structure-overview.md +1 -1
  5. package/docs/task-process/README.md +1 -1
  6. package/docs/task-process/common-flow.md +2 -3
  7. package/docs/task-process/error-analysis.md +3 -4
  8. package/docs/task-process/final-verification.md +2 -3
  9. package/docs/task-process/implementation-planning.md +2 -3
  10. package/docs/task-process/implementation.md +2 -3
  11. package/docs/task-process/release-handoff.md +3 -4
  12. package/docs/task-process/requirements-discovery.md +3 -4
  13. package/package.json +1 -1
  14. package/runtime/BUILD.json +2 -2
  15. package/runtime/prompts/launch.template.md +8 -7
  16. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  17. package/runtime/prompts/lead/plan-body-verification.md +27 -19
  18. package/runtime/prompts/lead/report-writer.md +3 -3
  19. package/runtime/prompts/profiles/_implementation-executor.md +1 -0
  20. package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
  21. package/runtime/prompts/profiles/implementation-planning.md +9 -11
  22. package/runtime/prompts/wizard/prompts.ko.json +7 -10
  23. package/runtime/python/okstra_ctl/conformance.py +37 -1
  24. package/runtime/python/okstra_ctl/incremental_scope.py +84 -39
  25. package/runtime/python/okstra_ctl/plan_items.py +410 -1
  26. package/runtime/python/okstra_ctl/plan_items_cli.py +346 -31
  27. package/runtime/python/okstra_ctl/render.py +4 -0
  28. package/runtime/python/okstra_ctl/wizard.py +39 -73
  29. package/runtime/schemas/final-report-v2.0.schema.json +12 -0
  30. package/runtime/schemas/final-report-v3.0.schema.json +12 -0
  31. package/runtime/skills/okstra-run/SKILL.md +2 -2
  32. package/runtime/validators/validate-run.py +164 -66
  33. package/runtime/validators/validate_session_conformance.py +24 -5
@@ -1231,17 +1231,6 @@ def _wizard_state_from_json(payload: dict[str, Any]) -> WizardState:
1231
1231
  if version != 2:
1232
1232
  state.execution_identity_version = 2
1233
1233
  _convert_v1_provider_selections(state, payload)
1234
- # v1 은 리더 확인 칸이 없었다. 변환 재개 시 다시 묻지 않는다.
1235
- if (
1236
- state.host_entry_mode == "current-session"
1237
- and _LEADER_SESSION_STEP not in state.answered
1238
- ):
1239
- state.answered.append(_LEADER_SESSION_STEP)
1240
- if (
1241
- state.host_entry_mode == "current-session"
1242
- and _LEADER_SESSION_STEP not in state.role_selection_order
1243
- ):
1244
- state.role_selection_order.insert(0, _LEADER_SESSION_STEP)
1245
1234
  return state
1246
1235
 
1247
1236
 
@@ -1281,13 +1270,8 @@ def _role_model_prompt_id(role: str, ordinal: int) -> str:
1281
1270
  return f"role-model:{role}:{ordinal}"
1282
1271
 
1283
1272
 
1284
- _LEADER_SESSION_STEP = "leader-session"
1285
-
1286
-
1287
1273
  def _is_role_selection_step(step_id: str) -> bool:
1288
- return step_id == _LEADER_SESSION_STEP or step_id.startswith(
1289
- ("role-count:", "role-model:", "role-add:")
1290
- )
1274
+ return step_id.startswith(("role-count:", "role-model:", "role-add:"))
1291
1275
 
1292
1276
 
1293
1277
  def _selected_role_count(
@@ -1320,36 +1304,6 @@ def _selectable_static_requirements(
1320
1304
  return tuple(requirements)
1321
1305
 
1322
1306
 
1323
- def _leader_session_prompt(state: WizardState) -> Prompt:
1324
- """current-session 리더 칸: 모델/effort 표시만 하고 role_models 에 쓰지 않는다."""
1325
- attestation = _host_session_context(state).current_model
1326
- model_ref = (
1327
- attestation.normalized_model_ref
1328
- or attestation.observed_model
1329
- or "current-session"
1330
- )
1331
- steps = _load_wizard_root(state.workspace_root)["steps"]
1332
- raw = steps.get("leader_session") or {}
1333
- effort_suffix = ""
1334
- if attestation.effort:
1335
- effort_template = raw.get("effort_suffix", " · effort {effort}")
1336
- effort_suffix = effort_template.format(effort=attestation.effort)
1337
- prompt = _p(
1338
- state.workspace_root,
1339
- "leader_session",
1340
- model_ref=model_ref,
1341
- effort_suffix=effort_suffix,
1342
- )
1343
- continue_label = prompt["options"].get("continue", "계속")
1344
- return Prompt(
1345
- step=_LEADER_SESSION_STEP,
1346
- kind="pick",
1347
- label=prompt["label"],
1348
- options=[_opt("continue", continue_label)],
1349
- echo_template=prompt["echo_template"],
1350
- )
1351
-
1352
-
1353
1307
  def _count_prompt(
1354
1308
  state: WizardState,
1355
1309
  requirement: RoleRequirement,
@@ -1777,13 +1731,6 @@ def next_role_prompt(state: WizardState) -> Prompt | None:
1777
1731
  if not _role_selection_enabled(state) or not _identity_ready(state):
1778
1732
  return None
1779
1733
  state.use_defaults = False
1780
- # current-session: 리더는 읽기 전용 확인 칸만 먼저 보여 준다.
1781
- if (
1782
- state.host_entry_mode == "current-session"
1783
- and _LEADER_SESSION_STEP not in state.answered
1784
- and _LEADER_SESSION_STEP not in state.role_selection_order
1785
- ):
1786
- return _leader_session_prompt(state)
1787
1734
  profile = _load_role_profile_for_state(state)
1788
1735
  for requirement in profile.roles:
1789
1736
  # 필수 수량: min < max 이고 min > 0 일 때만.
@@ -1884,11 +1831,6 @@ def _validate_submitted_role_model(
1884
1831
 
1885
1832
 
1886
1833
  def _submit_role_prompt(state: WizardState, prompt: Prompt, value: str) -> str:
1887
- if prompt.step == _LEADER_SESSION_STEP:
1888
- if value != "continue":
1889
- raise WizardError("leader session step accepts only 'continue'")
1890
- # role_models["leader"] 에 쓰지 않는다 — 현재 세션 모델을 그대로 쓴다.
1891
- return "leader-session: continue"
1892
1834
  if prompt.step.startswith("role-count:"):
1893
1835
  role = prompt.step.split(":", 1)[1]
1894
1836
  profile = _load_role_profile_for_state(state)
@@ -2041,9 +1983,11 @@ def _p(workspace_root: str, step_id: str, **vars: str) -> dict:
2041
1983
  raise WizardError(f"unknown wizard step_id: {step_id!r}")
2042
1984
  label_template = raw.get("label", "")
2043
1985
  fv_label_template = raw.get("label_final_verification", "")
1986
+ unlinked_template = raw.get("label_unlinked", "")
2044
1987
  try:
2045
1988
  label = label_template.format(**vars)
2046
1989
  label_final_verification = fv_label_template.format(**vars)
1990
+ label_unlinked = unlinked_template.format(**vars)
2047
1991
  except KeyError as exc:
2048
1992
  missing = exc.args[0] if exc.args else "<unknown>"
2049
1993
  raise WizardError(
@@ -2052,6 +1996,7 @@ def _p(workspace_root: str, step_id: str, **vars: str) -> dict:
2052
1996
  return {
2053
1997
  "label": label,
2054
1998
  "label_final_verification": label_final_verification,
1999
+ "label_unlinked": label_unlinked,
2055
2000
  "echo_template": raw.get("echo_template", ""),
2056
2001
  "options": raw.get("options", {}),
2057
2002
  "options_final_verification": raw.get("options_final_verification", {}),
@@ -4191,14 +4136,15 @@ def _reverify_scope_preview(state: WizardState) -> Optional[dict]:
4191
4136
 
4192
4137
 
4193
4138
  def _reverify_scope_pick_required(state: WizardState) -> bool:
4194
- """좁힐 여지가 있는 재실행에서만 범위를 묻는다.
4139
+ """좁힐 여지가 있거나, unlinked id 의 stage 번호를 받아야 할 때 묻는다.
4195
4140
 
4196
- 링크가 끊겨 full 이미 확정된 재실행에서 물으면 어떤 답도 판정을 바꾸지
4197
- 못한다 고를 없는 선택지를 내미는 화면이 하나 느는 것뿐이다. 그 경우
4198
- 확인 블록의 `reverify-scope: full 예상` 줄이 이유까지 같이 알려준다.
4141
+ unlinked full 확정이 아니다. SHA 바뀌었거나 입력을 읽을 없어
4142
+ `wouldForceFull` 경우만 질문이 의미 없다.
4199
4143
  """
4200
4144
  preview = _reverify_scope_preview(state)
4201
- return preview is not None and not preview["wouldForceFull"]
4145
+ return preview is not None and (
4146
+ not preview["wouldForceFull"] or bool(preview["unlinkedIds"])
4147
+ )
4202
4148
 
4203
4149
 
4204
4150
  def _reverify_scope_step_pending(state: WizardState) -> bool:
@@ -4234,13 +4180,20 @@ def _prior_stage_numbers(state: WizardState) -> set[int]:
4234
4180
  def _build_reverify_scope_pick(state: WizardState) -> Prompt:
4235
4181
  t = _p(state.workspace_root, "reverify_scope_pick")
4236
4182
  opts = t["options"]
4183
+ preview = _reverify_scope_preview(state) or {}
4184
+ unlinked = bool(preview.get("unlinkedIds"))
4185
+ options = []
4186
+ if not unlinked:
4187
+ options.append(_opt("auto", opts["auto"]))
4188
+ options.append(_opt("full", opts["full"]))
4189
+ options.append(_opt(PICK_TYPE_CUSTOM, opts[PICK_TYPE_CUSTOM]))
4190
+ else:
4191
+ options.append(_opt(PICK_TYPE_CUSTOM, opts[PICK_TYPE_CUSTOM]))
4192
+ options.append(_opt("full", opts["full"]))
4193
+ label = t["label_unlinked"] if unlinked else t["label"]
4237
4194
  return Prompt(
4238
- step=S_REVERIFY_SCOPE_PICK, kind="pick", label=t["label"],
4239
- options=[
4240
- _opt("auto", opts["auto"]),
4241
- _opt("full", opts["full"]),
4242
- _opt(PICK_TYPE_CUSTOM, opts[PICK_TYPE_CUSTOM]),
4243
- ],
4195
+ step=S_REVERIFY_SCOPE_PICK, kind="pick", label=label,
4196
+ options=options,
4244
4197
  echo_template=t["echo_template"])
4245
4198
 
4246
4199
 
@@ -4255,6 +4208,13 @@ def _submit_reverify_scope_pick(state: WizardState, value: str) -> Optional[str]
4255
4208
  raise WizardError(
4256
4209
  f"expected 'auto' / 'full' / {PICK_TYPE_CUSTOM!r}, got: {value!r}"
4257
4210
  )
4211
+ if picked == "auto":
4212
+ preview = _reverify_scope_preview(state) or {}
4213
+ unlinked = preview.get("unlinkedIds") or []
4214
+ if unlinked:
4215
+ raise WizardError(
4216
+ t["errors"]["unlinked_auto"].format(ids=", ".join(unlinked))
4217
+ )
4258
4218
  state.reverify_scope = picked
4259
4219
  state.reverify_scope_pending_text = False
4260
4220
  return t["echo_suffixes"][picked]
@@ -4271,6 +4231,12 @@ def _submit_reverify_scope_stages(state: WizardState, value: str) -> Optional[st
4271
4231
  t = _p(state.workspace_root, "reverify_scope_stages")
4272
4232
  tokens = [token.strip() for token in value.split(",") if token.strip()]
4273
4233
  if not tokens:
4234
+ preview = _reverify_scope_preview(state) or {}
4235
+ unlinked = preview.get("unlinkedIds") or []
4236
+ if unlinked:
4237
+ raise WizardError(
4238
+ t["errors"]["unlinked_empty"].format(ids=", ".join(unlinked))
4239
+ )
4274
4240
  state.reverify_scope = "auto"
4275
4241
  state.reverify_scope_pending_text = False
4276
4242
  return t["echo_suffixes"]["auto"]
@@ -6341,13 +6307,13 @@ def _reverify_scope_line(state: WizardState) -> Optional[str]:
6341
6307
  if state.reverify_scope and state.reverify_scope != "auto":
6342
6308
  return _msg(state.workspace_root, "confirmation",
6343
6309
  "reverify_scope_user_stages", stages=state.reverify_scope)
6344
- if not preview["wouldForceFull"]:
6345
- return _msg(state.workspace_root, "confirmation",
6346
- "reverify_scope_incremental")
6347
6310
  if preview["unlinkedIds"]:
6348
6311
  return _msg(state.workspace_root, "confirmation",
6349
6312
  "reverify_scope_unlinked",
6350
6313
  ids=", ".join(preview["unlinkedIds"]))
6314
+ if not preview["wouldForceFull"]:
6315
+ return _msg(state.workspace_root, "confirmation",
6316
+ "reverify_scope_incremental")
6351
6317
  return _msg(state.workspace_root, "confirmation", "reverify_scope_full",
6352
6318
  reason=preview["reason"])
6353
6319
 
@@ -9145,6 +9145,12 @@
9145
9145
  "properties": {
9146
9146
  "setAside": {"type": "array", "items": {"type": "object", "required": ["id", "reason"], "additionalProperties": false, "properties": {"id": {"type": "string", "minLength": 1}, "reason": {"type": "string", "enum": ["record", "observed", "deferred"]}}}},
9147
9147
  "stageLedger": {"type": "object", "additionalProperties": {"type": "string", "enum": ["done", "active", "ready", "blocked"]}},
9148
+ "dispatchQueue": {
9149
+ "type": "array",
9150
+ "description": "Plan-item ids sent to verifiers this round. In-scope plus plan-wide; deferred and observed stages are omitted.",
9151
+ "items": {"type": "string", "minLength": 1},
9152
+ "uniqueItems": true
9153
+ },
9148
9154
  "uniformVerifiers": {
9149
9155
  "type": "array",
9150
9156
  "description": "Verifiers whose every vote this round was one verdict. Advisory: a unanimous round is legitimate, but the gate reads as a three-way cross-check unless this sits beside it.",
@@ -9172,6 +9178,10 @@
9172
9178
  }
9173
9179
  }
9174
9180
  },
9181
+ "gating": {
9182
+ "type": "boolean",
9183
+ "description": "If false, plan-body verification is advisory: majority-disagree does not block approval, the self-fix loop does not run, and only one automatic round is allowed. Legal only when designPreparation.mode is no-design-inputs and the Stage Map has exactly one row. Prepare emits true; plan-items prepare/seed flip it after those facts exist."
9184
+ },
9175
9185
  "roundCount": {
9176
9186
  "type": "integer",
9177
9187
  "minimum": 0
@@ -9247,6 +9257,8 @@
9247
9257
  "properties": {
9248
9258
  "block": {"type": "string", "enum": ["execution", "record"]},
9249
9259
  "stageScope": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "integer", "minimum": 1}},
9260
+ "contentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of subject+payload+block+stageScope for the current extract."},
9261
+ "verifiedContentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "contentHash at the last apply-verdicts. Matching hashes keep a pre-self-fix verdict."},
9250
9262
  "id": {
9251
9263
  "type": "string",
9252
9264
  "minLength": 1
@@ -9140,6 +9140,12 @@
9140
9140
  "properties": {
9141
9141
  "setAside": {"type": "array", "items": {"type": "object", "required": ["id", "reason"], "additionalProperties": false, "properties": {"id": {"type": "string", "minLength": 1}, "reason": {"type": "string", "enum": ["record", "observed", "deferred"]}}}},
9142
9142
  "stageLedger": {"type": "object", "additionalProperties": {"type": "string", "enum": ["done", "active", "ready", "blocked"]}},
9143
+ "dispatchQueue": {
9144
+ "type": "array",
9145
+ "description": "Plan-item ids sent to verifiers this round. In-scope plus plan-wide; deferred and observed stages are omitted.",
9146
+ "items": {"type": "string", "minLength": 1},
9147
+ "uniqueItems": true
9148
+ },
9143
9149
  "uniformVerifiers": {
9144
9150
  "type": "array",
9145
9151
  "description": "Verifiers whose every vote this round was one verdict. Advisory: a unanimous round is legitimate, but the gate reads as a three-way cross-check unless this sits beside it.",
@@ -9167,6 +9173,10 @@
9167
9173
  }
9168
9174
  }
9169
9175
  },
9176
+ "gating": {
9177
+ "type": "boolean",
9178
+ "description": "If false, plan-body verification is advisory: majority-disagree does not block approval, the self-fix loop does not run, and only one automatic round is allowed. Legal only when designPreparation.mode is no-design-inputs and the Stage Map has exactly one row. Prepare emits true; plan-items prepare/seed flip it after those facts exist."
9179
+ },
9170
9180
  "roundCount": {
9171
9181
  "type": "integer",
9172
9182
  "minimum": 0
@@ -9242,6 +9252,8 @@
9242
9252
  "properties": {
9243
9253
  "block": {"type": "string", "enum": ["execution", "record"]},
9244
9254
  "stageScope": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "integer", "minimum": 1}},
9255
+ "contentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of subject+payload+block+stageScope for the current extract."},
9256
+ "verifiedContentHash": {"type": "string", "pattern": "^[a-f0-9]{64}$", "description": "contentHash at the last apply-verdicts. Matching hashes keep a pre-self-fix verdict."},
9245
9257
  "id": {
9246
9258
  "type": "string",
9247
9259
  "minLength": 1
@@ -185,9 +185,9 @@ That is the entire interactive flow. The wizard handles:
185
185
  - base-ref pick + git rev-parse validation (skipped when reusing an active worktree),
186
186
  - `implementation`-only sub-flow: approved-plan path (frontmatter `approved: true` check) + stage pick (`auto` = the earliest incomplete stage whose dependencies are satisfied, or a specific stage number). Implementer slots use role-count / role-model like every other role (`executor` is only a compatibility alias for `implementer`). When an approved plan is selected and a `## PLAN DECISION` sidecar carrying `Status: approved`, exported from the report — matching the plan on source-report·seq — is detected in that run's sibling `user-responses/`, the approve-confirm step expands to 3 options (`yes_apply` recommended: approve + apply the option as exported / `yes` approve only / `no` abort) — `yes_apply` validates the option against the plan's `optionCandidates` before applying it via the existing approval·option path,
187
187
  - `release-handoff`-only sub-flow: after the approved plan auto-resolves, a `handoff_stage_pick` multi-select — choose an eligible stage bundle (stage-group) or the whole task (when an accepted whole-task verification report exists); the result goes out as render-args' `stages` key (csv, empty when whole-task),
188
- - launch selection after identity/worktree steps: leader session read-only (current-session) → role-count per static role (`min..max`, omit uses **recommended**, skip when `min == max`) → role-model `provider/model` per slot → min=0 roles only via role-add (default skip). The wizard does not fork on defaults-vs-customize, does not show a provider roster multi-pick, and does not offer a separate implementer-provider pick. Dynamic verifiers are not chosen at launch. `--workers` is compatibility-only, not a launch picker. Repeated `--role-count` / `--role-model` tokens on `renderArgv` are intentional,
188
+ - launch selection after identity/worktree steps: role-count per static role (`min..max`, omit uses **recommended**, skip when `min == max`) → role-model `provider/model` per slot → min=0 roles only via role-add (default skip). current-session lead is this session and is listed on the confirmation summary, not as a wizard step. The wizard does not fork on defaults-vs-customize, does not show a provider roster multi-pick, and does not offer a separate implementer-provider pick. Dynamic verifiers are not chosen at launch. `--workers` is compatibility-only, not a launch picker. Repeated `--role-count` / `--role-model` tokens on `renderArgv` are intentional,
189
189
  - **resume-clarification (in-session equivalent)** — there is no separate mode or flag matching the shell's `okstra.sh --resume-clarification`; two steps of the standard flow carry out its substance. (1) `reuse_previous` (yes/no to reuse the previous run's settings — in `requirements-discovery` / `error-analysis` / `implementation-planning`, only when prior run-inputs exist): YES prefills role-count·role-model·directive·related-tasks at once. (2) `clarification_pick`: if the **task-type's own** previous `final-report` exists it is auto-recommended as the carry-in input (falling back to the newest by mtime across all phases when absent), and the same run's `user-responses/` sidecar (answers the user filled in) is attached alongside. The chosen path is passed to prepare as `--clarification-response` — the user makes the sidecar via the report's `Export user response`, places it in `runs/<task-type>/user-responses/`, and re-runs the same phase,
190
- - **re-verification scope (`reverify_scope_pick`, `implementation-planning` clarification re-runs only)** — asked right before `confirm`, and **only when the re-run is narrowable** (every answered `C-NNN` traces back to a stage in the prior report). 3 options: `auto` (recommended — leave it to the lead's `okstra incremental-scope` decision) / `full` (re-verify every stage) / Enter directly (a stage-number CSV, validated against the prior report's Stage Map). The answer goes out as `--reverify-scope` and reaches the lead prompt as the `REVERIFY_SCOPE_MODE` / `REVERIFY_SCOPE_STAGES` tokens; it shapes that CLI's inputs rather than replacing the decision. When the re-run is not narrowable the step does not appear — full is already fixed, and the confirmation block's `reverify-scope` line says which answered id broke the link,
190
+ - **re-verification scope (`reverify_scope_pick`, `implementation-planning` clarification re-runs only)** — asked right before `confirm` when the re-run is narrowable **or** an answered `C-NNN` traces to no stage. When every answered id traces to a stage: 3 options `auto` (recommended — leave it to the lead's `okstra incremental-scope` decision) / `full` (re-verify every stage) / Enter directly (a stage-number CSV, validated against the prior report's Stage Map). When an id is unlinked, `auto` is omitted and the user names stages or picks `full`; that unlinked id does not freeze the run at full. The answer goes out as `--reverify-scope` and reaches the lead prompt as the `REVERIFY_SCOPE_MODE` / `REVERIFY_SCOPE_STAGES` tokens; it shapes that CLI's inputs rather than replacing the decision. The confirmation block's `reverify-scope` line names unlinked ids as needing stage numbers, not as a forced full re-run,
191
191
  - `release-handoff` PR template override + persist scope,
192
192
  - final `Proceed / Edit` confirmation; on `Edit` the wizard asks which step to rewind to and clears every later answer.
193
193