okstra 0.172.0 → 0.174.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 (123) hide show
  1. package/README.md +8 -6
  2. package/docs/architecture/storage-model.md +24 -3
  3. package/docs/architecture.md +21 -35
  4. package/docs/cli.md +39 -7
  5. package/docs/container.md +1 -1
  6. package/docs/contributor-change-matrix.md +1 -1
  7. package/docs/performance-improvement-plan-v2.md +6 -5
  8. package/docs/project-structure-overview.md +33 -25
  9. package/docs/task-process/README.md +6 -4
  10. package/docs/task-process/error-analysis.md +2 -2
  11. package/docs/task-process/final-verification.md +2 -2
  12. package/docs/task-process/implementation-option-selection.md +70 -0
  13. package/docs/task-process/implementation-planning.md +24 -16
  14. package/docs/task-process/requirements-discovery.md +2 -2
  15. package/package.json +1 -1
  16. package/runtime/BUILD.json +2 -2
  17. package/runtime/agents/workers/claude-worker.md +1 -1
  18. package/runtime/agents/workers/report-writer-worker.md +30 -6
  19. package/runtime/bin/lib/okstra/cli.sh +5 -1
  20. package/runtime/bin/lib/okstra/globals.sh +2 -1
  21. package/runtime/bin/lib/okstra/usage.sh +3 -0
  22. package/runtime/bin/okstra-provider-exec.py +29 -12
  23. package/runtime/bin/okstra-trace-cleanup.sh +58 -129
  24. package/runtime/bin/okstra.sh +2 -0
  25. package/runtime/prompts/duties/direction-selection-worker.md +44 -0
  26. package/runtime/prompts/duties/planning-worker.md +12 -4
  27. package/runtime/prompts/lead/adapters/cmux.md +2 -0
  28. package/runtime/prompts/lead/context-loader.md +1 -1
  29. package/runtime/prompts/lead/convergence.md +5 -5
  30. package/runtime/prompts/lead/okstra-lead-contract.md +7 -6
  31. package/runtime/prompts/lead/plan-body-verification.md +23 -6
  32. package/runtime/prompts/lead/report-writer.md +33 -11
  33. package/runtime/prompts/profiles/_common-contract.md +3 -3
  34. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  35. package/runtime/prompts/profiles/_implementation-executor.md +2 -0
  36. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  37. package/runtime/prompts/profiles/error-analysis.md +4 -4
  38. package/runtime/prompts/profiles/final-verification.md +3 -3
  39. package/runtime/prompts/profiles/forbidden-actions.json +7 -0
  40. package/runtime/prompts/profiles/implementation-option-selection.md +35 -0
  41. package/runtime/prompts/profiles/implementation-planning.md +61 -46
  42. package/runtime/prompts/profiles/implementation.md +4 -2
  43. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  44. package/runtime/prompts/profiles/release-handoff.md +1 -1
  45. package/runtime/prompts/profiles/requirements-discovery.md +3 -3
  46. package/runtime/prompts/wizard/prompts.ko.json +9 -1
  47. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
  48. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
  49. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -0
  50. package/runtime/python/okstra_ctl/agent_invocation.py +1 -0
  51. package/runtime/python/okstra_ctl/analysis_packet.py +6 -0
  52. package/runtime/python/okstra_ctl/conformance.py +68 -0
  53. package/runtime/python/okstra_ctl/dispatch_core.py +89 -39
  54. package/runtime/python/okstra_ctl/dispatch_state.py +142 -14
  55. package/runtime/python/okstra_ctl/doctor.py +2 -2
  56. package/runtime/python/okstra_ctl/domain/worker_exec.py +5 -0
  57. package/runtime/python/okstra_ctl/exact_coverage.py +128 -0
  58. package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
  59. package/runtime/python/okstra_ctl/fix_cycles.py +3 -1
  60. package/runtime/python/okstra_ctl/implementation_direction.py +836 -0
  61. package/runtime/python/okstra_ctl/implementation_options.py +479 -0
  62. package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
  63. package/runtime/python/okstra_ctl/plan_items.py +51 -3
  64. package/runtime/python/okstra_ctl/render.py +1 -0
  65. package/runtime/python/okstra_ctl/render_final_report.py +16 -19
  66. package/runtime/python/okstra_ctl/report_contract.py +45 -14
  67. package/runtime/python/okstra_ctl/report_finalize.py +68 -9
  68. package/runtime/python/okstra_ctl/report_html/render.py +4 -2
  69. package/runtime/python/okstra_ctl/report_html/router.py +4 -0
  70. package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +32 -0
  71. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +25 -10
  72. package/runtime/python/okstra_ctl/report_views.py +148 -12
  73. package/runtime/python/okstra_ctl/run.py +393 -4
  74. package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
  75. package/runtime/python/okstra_ctl/scope_provenance.py +16 -10
  76. package/runtime/python/okstra_ctl/session.py +69 -12
  77. package/runtime/python/okstra_ctl/team.py +51 -25
  78. package/runtime/python/okstra_ctl/tmux.py +19 -149
  79. package/runtime/python/okstra_ctl/user_response.py +75 -0
  80. package/runtime/python/okstra_ctl/wizard.py +144 -0
  81. package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
  82. package/runtime/python/okstra_ctl/worker_request.py +2 -0
  83. package/runtime/python/okstra_ctl/workflow.py +29 -7
  84. package/runtime/python/okstra_ctl/worktree.py +69 -3
  85. package/runtime/python/okstra_token_usage/cli.py +1 -1
  86. package/runtime/python/okstra_token_usage/collect.py +66 -6
  87. package/runtime/schemas/final-report-v2.0.schema.json +1428 -137
  88. package/runtime/skills/okstra-setup/references/project-config.md +11 -0
  89. package/runtime/templates/reports/final-report-v2.template.md +4 -0
  90. package/runtime/templates/reports/final-verification-input.template.md +1 -1
  91. package/runtime/templates/reports/html/base.template.html +3 -2
  92. package/runtime/templates/reports/html/i18n/en.json +21 -1
  93. package/runtime/templates/reports/html/i18n/ko.json +21 -1
  94. package/runtime/templates/reports/html/macros/forms.html +21 -2
  95. package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +49 -0
  96. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +36 -2
  97. package/runtime/templates/reports/i18n/en.json +13 -0
  98. package/runtime/templates/reports/implementation-input.template.md +4 -2
  99. package/runtime/templates/reports/implementation-planning-input.template.md +18 -4
  100. package/runtime/templates/reports/improvement-discovery-input.template.md +1 -1
  101. package/runtime/templates/reports/md/tasks/implementation-option-selection.template.md +13 -0
  102. package/runtime/templates/reports/md/tasks/implementation-planning.template.md +17 -0
  103. package/runtime/templates/reports/report.js +111 -4
  104. package/runtime/templates/reports/settings.template.json +0 -24
  105. package/runtime/templates/reports/task-brief.template.md +9 -3
  106. package/runtime/templates/reports/user-response.template.md +25 -4
  107. package/runtime/templates/worker-prompt-preamble.md +8 -0
  108. package/runtime/validators/lib/fixtures.sh +49 -17
  109. package/runtime/validators/validate-implementation-plan-stages.py +169 -4
  110. package/runtime/validators/validate-report-views.py +2 -2
  111. package/runtime/validators/validate-run.py +149 -498
  112. package/runtime/validators/validate_improvement_report.py +5 -1
  113. package/runtime/validators/validate_session_conformance.py +1 -1
  114. package/src/cli-registry.mjs +8 -1
  115. package/src/commands/execute/codex-run.mjs +1 -0
  116. package/src/commands/execute/render-bundle.mjs +1 -0
  117. package/src/commands/execute/team.mjs +3 -3
  118. package/src/commands/execute/worktree-status.mjs +109 -0
  119. package/src/commands/lifecycle/install.mjs +0 -2
  120. package/src/commands/report/finalize.mjs +13 -6
  121. package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
  122. package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
  123. package/runtime/templates/reports/final-report.template.md +0 -1258
@@ -111,6 +111,8 @@ class RunMeta:
111
111
  task_type: str
112
112
  seq: str
113
113
  source_report: str # relative path of the .md the HTML is derived from
114
+ source_data: str = ""
115
+ source_data_sha256: str = ""
114
116
 
115
117
 
116
118
  @dataclass(frozen=True)
@@ -154,7 +156,9 @@ def _infer_run_meta_from_body(text: str) -> dict:
154
156
 
155
157
  def infer_run_meta(report_path: Path, *, task_key: Optional[str] = None,
156
158
  task_type: Optional[str] = None, seq: Optional[str] = None,
157
- source_report: Optional[str] = None) -> RunMeta:
159
+ source_report: Optional[str] = None,
160
+ source_data: Optional[str] = None,
161
+ source_data_sha256: Optional[str] = None) -> RunMeta:
158
162
  """Derive a ``RunMeta`` from a final-report path/body, honouring any
159
163
  explicit override.
160
164
 
@@ -166,11 +170,22 @@ def infer_run_meta(report_path: Path, *, task_key: Optional[str] = None,
166
170
  """
167
171
  text = report_path.read_text(encoding="utf-8")
168
172
  inferred = {**_infer_run_meta_from_path(report_path), **_infer_run_meta_from_body(text)}
173
+ resolved_source_report = source_report or report_path.name
174
+ data_path = final_report_data_path(report_path)
175
+ inferred_source_data = ""
176
+ inferred_source_data_sha256 = ""
177
+ if data_path.is_file():
178
+ inferred_source_data = final_report_data_path(
179
+ Path(resolved_source_report)
180
+ ).as_posix()
181
+ inferred_source_data_sha256 = hashlib.sha256(data_path.read_bytes()).hexdigest()
169
182
  return RunMeta(
170
183
  task_key=task_key or inferred.get("task_key") or "unknown",
171
184
  task_type=task_type or inferred.get("task_type") or "unknown",
172
185
  seq=seq or inferred.get("seq") or "000",
173
- source_report=source_report or report_path.name,
186
+ source_report=resolved_source_report,
187
+ source_data=source_data or inferred_source_data,
188
+ source_data_sha256=source_data_sha256 or inferred_source_data_sha256,
174
189
  )
175
190
 
176
191
 
@@ -271,6 +286,32 @@ def render_report_view_model(
271
286
  sidecar_name = html.escape(model.sidecar_name)
272
287
  sidecar_dir = html.escape(model.sidecar_dir)
273
288
  run_meta = model.run_meta
289
+ source_data_json = (
290
+ f'"source-data":"{html.escape(run_meta.source_data)}",'
291
+ if run_meta.source_data else ""
292
+ )
293
+ source_data_sha256_json = (
294
+ f'"source-data-sha256":"{html.escape(run_meta.source_data_sha256)}",'
295
+ if run_meta.source_data_sha256 else ""
296
+ )
297
+ reentry_only = bool(
298
+ model.approval_ctx and model.approval_ctx.reentry_command
299
+ )
300
+ export_header = (
301
+ ' <button type="button" data-action="export-user-response">Export user response</button>\n'
302
+ if not reentry_only else ""
303
+ )
304
+ export_footer = (
305
+ f'<footer class="report-footer">\n'
306
+ f' <button type="button" data-action="export-user-response">Export user response</button>\n'
307
+ f' <p class="user-response-hint">Export 클릭 시 <code>{sidecar_name}</code> 가 다운로드됩니다 — '
308
+ f'<code>{sidecar_dir}</code> 에 저장하면 resume-clarification 이 자동으로 첨부합니다.</p>\n'
309
+ f' <pre id="user-response-output" aria-live="polite"></pre>\n'
310
+ f' <button type="button" data-action="copy-user-response">Copy</button>\n'
311
+ f' <button type="button" data-action="dismiss-user-response" class="user-response-dismiss" aria-label="출력 닫기" title="닫기" hidden>×</button>\n'
312
+ f'</footer>\n'
313
+ if not reentry_only else ""
314
+ )
274
315
  return (
275
316
  f"<!DOCTYPE html>\n"
276
317
  f"<html lang=\"ko\">\n"
@@ -283,24 +324,19 @@ def render_report_view_model(
283
324
  f"<body>\n"
284
325
  f"<header class=\"report-header\">\n"
285
326
  f" <div>{title}</div>\n"
286
- f" <button type=\"button\" data-action=\"export-user-response\">Export user response</button>\n"
327
+ f"{export_header}"
287
328
  f"</header>\n"
288
329
  f"<main>{_reader_dashboard(model.reader_ctx)}{model.body_html}</main>\n"
289
330
  f"{_plan_approval_section(model.approval_ctx, run_meta) if model.approval_ctx else ''}"
290
331
  f"{_analysis_review_section(model.analysis_review_ctx) if model.analysis_review_ctx else ''}"
291
- f"<footer class=\"report-footer\">\n"
292
- f" <button type=\"button\" data-action=\"export-user-response\">Export user response</button>\n"
293
- f" <p class=\"user-response-hint\">Export 클릭 시 <code>{sidecar_name}</code> 가 다운로드됩니다 — "
294
- f"<code>{sidecar_dir}</code> 에 저장하면 resume-clarification 이 자동으로 첨부합니다.</p>\n"
295
- f" <pre id=\"user-response-output\" aria-live=\"polite\"></pre>\n"
296
- f" <button type=\"button\" data-action=\"copy-user-response\">Copy</button>\n"
297
- f" <button type=\"button\" data-action=\"dismiss-user-response\" class=\"user-response-dismiss\" aria-label=\"출력 닫기\" title=\"닫기\" hidden>×</button>\n"
298
- f"</footer>\n"
332
+ f"{export_footer}"
299
333
  f"<script id=\"run-meta\" type=\"application/json\">"
300
334
  f"{{\"task-key\":\"{html.escape(run_meta.task_key)}\","
301
335
  f"\"task-type\":\"{html.escape(run_meta.task_type)}\","
302
336
  f"\"seq\":\"{html.escape(run_meta.seq)}\","
303
337
  f"\"source-report\":\"{html.escape(run_meta.source_report)}\","
338
+ f"{source_data_json}"
339
+ f"{source_data_sha256_json}"
304
340
  f"\"source-sha256\":\"{model.source_digest}\","
305
341
  f"\"response-ids\":{response_ids_json}}}"
306
342
  f"</script>\n"
@@ -1062,6 +1098,15 @@ class UserPlanDecision:
1062
1098
  reason: str = ""
1063
1099
 
1064
1100
 
1101
+ @dataclass(frozen=True)
1102
+ class UserDirectionSelection:
1103
+ option_id: str
1104
+ option_name: str
1105
+ confirmed: bool
1106
+ selection_note: str = ""
1107
+ constraints: str = ""
1108
+
1109
+
1065
1110
  @dataclass(frozen=True)
1066
1111
  class UserResponseAnalysisReview:
1067
1112
  status: str
@@ -1084,6 +1129,44 @@ _PLAN_DECISION_STATUSES = frozenset({
1084
1129
  "rejected",
1085
1130
  })
1086
1131
 
1132
+ _DIRECTION_IDENTITY_WHITESPACE = frozenset(
1133
+ "\u0009\u000a\u000b\u000c\u000d"
1134
+ "\u001c\u001d\u001e\u001f\u0020\u0085\u00a0\u1680"
1135
+ "\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a"
1136
+ "\u2028\u2029\u202f\u205f\u3000\ufeff"
1137
+ )
1138
+ _DIRECTION_IDENTITY_ERROR = (
1139
+ "DIRECTION SELECTION Option-ID and Option-Name must be "
1140
+ "non-empty single-line values"
1141
+ )
1142
+
1143
+
1144
+ def _trim_direction_identity(value: str) -> str:
1145
+ start = 0
1146
+ end = len(value)
1147
+ while start < end and value[start] in _DIRECTION_IDENTITY_WHITESPACE:
1148
+ start += 1
1149
+ while end > start and value[end - 1] in _DIRECTION_IDENTITY_WHITESPACE:
1150
+ end -= 1
1151
+ return value[start:end]
1152
+
1153
+
1154
+ def normalize_direction_selection_identity(
1155
+ option_id: str, option_name: str,
1156
+ ) -> tuple[str, str]:
1157
+ if (
1158
+ "\r" in option_id
1159
+ or "\n" in option_id
1160
+ or "\r" in option_name
1161
+ or "\n" in option_name
1162
+ ):
1163
+ raise ValueError(_DIRECTION_IDENTITY_ERROR)
1164
+ normalized_id = _trim_direction_identity(option_id)
1165
+ normalized_name = _trim_direction_identity(option_name)
1166
+ if not normalized_id or not normalized_name:
1167
+ raise ValueError(_DIRECTION_IDENTITY_ERROR)
1168
+ return normalized_id, normalized_name
1169
+
1087
1170
 
1088
1171
  def _quoted_sidecar_field(label: str, value: str) -> str:
1089
1172
  cleaned = value.strip()
@@ -1125,6 +1208,23 @@ def _serialize_plan_decision(decision: UserPlanDecision) -> str:
1125
1208
  return chunk
1126
1209
 
1127
1210
 
1211
+ def _serialize_direction_selection(selection: UserDirectionSelection) -> str:
1212
+ option_id, option_name = normalize_direction_selection_identity(
1213
+ selection.option_id, selection.option_name
1214
+ )
1215
+ if not selection.confirmed:
1216
+ raise ValueError("DIRECTION SELECTION requires Confirmed: true")
1217
+ return (
1218
+ "\n## DIRECTION SELECTION\n"
1219
+ "- Status: selected\n"
1220
+ f"- Option-ID: {option_id}\n"
1221
+ f"- Option-Name: {option_name}\n"
1222
+ "- Confirmed: true\n"
1223
+ f"{_quoted_sidecar_field('Selection-Note', selection.selection_note)}"
1224
+ f"{_quoted_sidecar_field('Constraints', selection.constraints)}"
1225
+ )
1226
+
1227
+
1128
1228
  def serialize_user_response(
1129
1229
  *,
1130
1230
  run_meta: RunMeta,
@@ -1132,18 +1232,27 @@ def serialize_user_response(
1132
1232
  created_at: str,
1133
1233
  plan_decision: UserPlanDecision | None = None,
1134
1234
  analysis_review: UserResponseAnalysisReview | None = None,
1235
+ direction_selection: UserDirectionSelection | None = None,
1135
1236
  ) -> str:
1136
1237
  """Return the canonical markdown text the HTML 'Export user
1137
1238
  response' button must produce. Used by validators to confirm that
1138
1239
  pasted sidecar files conform to the schema and that the JS
1139
1240
  serialiser stayed in sync.
1140
1241
  """
1242
+ source_data_lines = ""
1243
+ if run_meta.source_data:
1244
+ source_data_lines += f"source-data: {run_meta.source_data}\n"
1245
+ if run_meta.source_data_sha256:
1246
+ source_data_lines += (
1247
+ f"source-data-sha256: {run_meta.source_data_sha256}\n"
1248
+ )
1141
1249
  head = (
1142
1250
  "---\n"
1143
1251
  f"task-key: {run_meta.task_key}\n"
1144
1252
  f"task-type: {run_meta.task_type}\n"
1145
1253
  f"seq: {run_meta.seq}\n"
1146
1254
  f"source-report: {run_meta.source_report}\n"
1255
+ f"{source_data_lines}"
1147
1256
  "created-by: user\n"
1148
1257
  f"created-at: {created_at}\n"
1149
1258
  "---\n"
@@ -1152,6 +1261,7 @@ def serialize_user_response(
1152
1261
  )
1153
1262
  has_plan_decision = plan_decision is not None
1154
1263
  has_analysis_review = analysis_review is not None
1264
+ has_direction_selection = direction_selection is not None
1155
1265
  body_chunks: list[str] = []
1156
1266
  for e in entries:
1157
1267
  chunk = f"\n## {e.response_id}\n- Kind: {e.kind}\n"
@@ -1162,12 +1272,19 @@ def serialize_user_response(
1162
1272
  if e.rationale:
1163
1273
  chunk += f"- Rationale: {e.rationale.strip()}\n"
1164
1274
  body_chunks.append(chunk)
1165
- if not entries and not has_plan_decision and not has_analysis_review:
1275
+ if (
1276
+ not entries
1277
+ and not has_plan_decision
1278
+ and not has_analysis_review
1279
+ and not has_direction_selection
1280
+ ):
1166
1281
  body_chunks.append("\n_(No user responses recorded.)_\n")
1167
1282
  if plan_decision is not None:
1168
1283
  body_chunks.append(_serialize_plan_decision(plan_decision))
1169
1284
  if analysis_review is not None:
1170
1285
  body_chunks.append(_serialize_analysis_review(analysis_review))
1286
+ if direction_selection is not None:
1287
+ body_chunks.append(_serialize_direction_selection(direction_selection))
1171
1288
  return head + "".join(body_chunks)
1172
1289
 
1173
1290
 
@@ -1263,6 +1380,8 @@ def plan_approval_context(src_md_path: Path) -> PlanApprovalContext | None:
1263
1380
  recommended_option=state.recommended_option,
1264
1381
  disabled_reason=reason,
1265
1382
  blocker_ids=blocker_ids,
1383
+ show_option_selector=state.show_option_selector,
1384
+ reentry_command=state.reentry_command,
1266
1385
  )
1267
1386
 
1268
1387
 
@@ -1352,6 +1471,16 @@ def _approval_blocked_guidance(ctx: PlanApprovalContext, run_meta: RunMeta) -> s
1352
1471
  """disabled 사유를 화면에서 바로 따라할 수 있는 단계별 안내로 렌더한다.
1353
1472
  핵심: 이 화면에서 답을 입력하는 것만으로는 승인이 풀리지 않으며,
1354
1473
  Export → 저장 → resume-clarification → 재렌더 라운드트립이 필요하다는 것."""
1474
+ if ctx.reentry_command:
1475
+ command = (
1476
+ f"/okstra-run task-key={run_meta.task_key} "
1477
+ "task-type=implementation-option-selection"
1478
+ )
1479
+ return (
1480
+ '<p class="approval-disabled-reason">선택 방향이 무효화되어 이 계획은 '
1481
+ "승인할 수 없습니다. 구현 방향 선택 단계에 다시 진입하세요: "
1482
+ f"<code>{html.escape(command)}</code></p>"
1483
+ )
1355
1484
  if not ctx.blocker_ids: # unreadable 사유 — 재렌더 외에 따라할 단계가 없다.
1356
1485
  return f'<p class="approval-disabled-reason">{html.escape(ctx.disabled_reason)}</p>'
1357
1486
  sidecar_dir = f"runs/{run_meta.task_type}/user-responses/"
@@ -1379,6 +1508,13 @@ def _approval_blocked_guidance(ctx: PlanApprovalContext, run_meta: RunMeta) -> s
1379
1508
 
1380
1509
 
1381
1510
  def _plan_approval_section(ctx: PlanApprovalContext, run_meta: RunMeta) -> str:
1511
+ if ctx.reentry_command:
1512
+ return (
1513
+ '<section id="plan-approval">\n'
1514
+ " <h2>선택 방향 무효화</h2>\n"
1515
+ f" {_approval_blocked_guidance(ctx, run_meta)}\n"
1516
+ "</section>\n"
1517
+ )
1382
1518
  disabled = " disabled" if ctx.disabled_reason else ""
1383
1519
  opts: list[str] = []
1384
1520
  for name in ctx.option_names: