okstra 0.44.0 → 0.45.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.44.0",
3
+ "version": "0.45.1",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.44.0",
3
- "builtAt": "2026-06-04T05:25:22.876Z",
2
+ "package": "0.45.1",
3
+ "builtAt": "2026-06-04T08:12:55.290Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -229,11 +229,11 @@
229
229
  }
230
230
  },
231
231
  "defaults_or_custom": {
232
- "label": "기본 워커/모델로 진행할까요, 아니면 커스터마이즈할까요?",
232
+ "label": "역할별로 어떤 모델을 쓸지 정하는 단계입니다 (참여 워커 구성을 바꾸는 게 아닙니다).\n· 기본값으로 진행 — lead·실행자/워커·report-writer 를 모두 추천 모델로 두고 바로 진행합니다.\n· 커스터마이즈 — 역할별 모델을 직접 고르고, 추가 directive·관련 task 도 지정합니다.",
233
233
  "echo_template": "customize: {value}",
234
234
  "options": {
235
- "defaults": "Use defaults",
236
- "customize": "Customize"
235
+ "defaults": "기본값으로 진행 (역할별 추천 모델 그대로)",
236
+ "customize": "커스터마이즈 (역할별 모델 직접 선택)"
237
237
  }
238
238
  },
239
239
  "workers_override": {
@@ -300,6 +300,10 @@ def _emit_table(lines: list[str], start: int, section_path: list[str]) -> tuple[
300
300
  rows.append(_split_pipe_row(lines[i]))
301
301
  i += 1
302
302
 
303
+ grouped_spec = _grouped_table_spec(header_cells, section_path)
304
+ if grouped_spec is not None:
305
+ return _emit_grouped_table(header_cells, rows, grouped_spec), i - start
306
+
303
307
  is_clarification_table = (
304
308
  not _section_forbids_form(section_path)
305
309
  and any("Clarification Items" in h for h in section_path)
@@ -383,6 +387,172 @@ def _emit_table(lines: list[str], start: int, section_path: list[str]) -> tuple[
383
387
  return f"<table>{head}{body}</table>", i - start
384
388
 
385
389
 
390
+ @dataclass(frozen=True)
391
+ class _GroupedSpec:
392
+ """Plan for rendering a wide table as a compact grouped layout: the
393
+ short columns (``group_cols``) collapse into one stacked ``key:
394
+ value`` metadata cell led by ``headline_col``; the long columns
395
+ (``wide_cols``) each keep their own min-width column.
396
+
397
+ ``kind == "clarification"`` additionally re-attaches the §5 form
398
+ widget to the ``user_input_col`` cell and the ``data-*`` row attrs."""
399
+ headline_col: int
400
+ group_cols: tuple[int, ...]
401
+ wide_cols: tuple[int, ...]
402
+ kind: str # "plain" | "clarification"
403
+ id_col: int = -1
404
+ kind_col: int = -1
405
+ status_col: int = -1
406
+ statement_col: int = -1
407
+ user_input_col: int = -1
408
+
409
+
410
+ _FOLLOWUP_WIDE_PREFIXES: tuple[str, ...] = ("title", "scope", "reason")
411
+
412
+
413
+ def _grouped_table_spec(
414
+ header_cells: list[str], section_path: list[str]
415
+ ) -> Optional[_GroupedSpec]:
416
+ """Return a ``_GroupedSpec`` for the three wide final-report tables
417
+ that benefit from the compact layout — Execution Status, §5
418
+ Clarification Items, §7 Follow-up Tasks — or ``None`` for every other
419
+ table (which keeps the default per-cell ``td-narrow`` rendering).
420
+
421
+ Each table is identified by stable header tokens (the i18n token/cost
422
+ columns are never used as anchors). ``wide_cols`` lists the long-prose
423
+ columns that must keep a guaranteed min-width; everything else short
424
+ collapses into the leading metadata cell."""
425
+ norm = [h.strip() for h in header_cells]
426
+
427
+ def _spec(headline: int, wide: tuple[int, ...], **kw) -> _GroupedSpec:
428
+ wide_set = set(wide)
429
+ group = tuple(c for c in range(len(norm)) if c != headline and c not in wide_set)
430
+ return _GroupedSpec(headline_col=headline, group_cols=group, wide_cols=wide, **kw)
431
+
432
+ # Execution Status by Agent — Agent … Summary of Key Findings.
433
+ if len(norm) >= 3 and norm[0] == "Agent" and norm[-1] == "Summary of Key Findings":
434
+ return _spec(0, (len(norm) - 1,), kind="plain")
435
+
436
+ # §5 Clarification Items — keep the interactive form, but collapse the
437
+ # short ID/Kind/Status/… columns and widen Statement + User input.
438
+ if (
439
+ any("Clarification Items" in h for h in section_path)
440
+ and not _section_forbids_form(section_path)
441
+ and "ID" in norm
442
+ and "User input" in norm
443
+ and any(h.startswith("Statement") for h in norm)
444
+ ):
445
+ statement_col = next(i for i, h in enumerate(norm) if h.startswith("Statement"))
446
+ user_input_col = norm.index("User input")
447
+ return _spec(
448
+ norm.index("ID"),
449
+ (statement_col, user_input_col),
450
+ kind="clarification",
451
+ id_col=norm.index("ID"),
452
+ kind_col=norm.index("Kind") if "Kind" in norm else -1,
453
+ status_col=norm.index("Status") if "Status" in norm else -1,
454
+ statement_col=statement_col,
455
+ user_input_col=user_input_col,
456
+ )
457
+
458
+ # §7 Follow-up Tasks — widen Title / Scope / Reason, collapse the rest.
459
+ if any("Follow-up Tasks" in h for h in section_path) and "ID" in norm:
460
+ wide = tuple(
461
+ i
462
+ for i, h in enumerate(norm)
463
+ if any(h.lower().startswith(p) for p in _FOLLOWUP_WIDE_PREFIXES)
464
+ )
465
+ if wide:
466
+ return _spec(norm.index("ID"), wide, kind="plain")
467
+
468
+ return None
469
+
470
+
471
+ def _grouped_meta_cell(
472
+ header_cells: list[str], row: list[str], spec: _GroupedSpec
473
+ ) -> str:
474
+ """The leading metadata ``<td>``: a bold headline (``headline_col``)
475
+ above one ``key: value`` line per collapsed short column."""
476
+ headline = row[spec.headline_col] if spec.headline_col < len(row) else ""
477
+ fields = "".join(
478
+ '<div class="grp-field">'
479
+ f'<span class="grp-key">{_inline(header_cells[col])}</span>'
480
+ f'<span class="grp-val">{_inline(row[col] if col < len(row) else "")}</span>'
481
+ "</div>"
482
+ for col in spec.group_cols
483
+ )
484
+ return (
485
+ '<td class="grp-meta">'
486
+ f'<div class="grp-headline">{_inline(headline)}</div>'
487
+ f"{fields}</td>"
488
+ )
489
+
490
+
491
+ def _grouped_clarification_row(
492
+ row: list[str], spec: _GroupedSpec
493
+ ) -> tuple[str, str]:
494
+ """Return ``(tr_attrs, wide_cells_html)`` for one §5 row, re-attaching
495
+ the form widget + ``data-*`` attrs to ``C-\\d+`` rows exactly as the
496
+ non-grouped path does."""
497
+ rid = row[spec.id_col] if 0 <= spec.id_col < len(row) else ""
498
+ is_form_row = bool(re.fullmatch(r"C-\d+", rid)) and spec.user_input_col >= 0
499
+ kind = row[spec.kind_col] if is_form_row and 0 <= spec.kind_col < len(row) else ""
500
+ status = row[spec.status_col] if is_form_row and 0 <= spec.status_col < len(row) else ""
501
+ statement = (
502
+ row[spec.statement_col]
503
+ if is_form_row and 0 <= spec.statement_col < len(row)
504
+ else ""
505
+ )
506
+ tr_attrs = (
507
+ f' data-response-id="{html.escape(rid)}" '
508
+ f'data-kind="{html.escape(kind)}" '
509
+ f'data-status="{html.escape(status)}"'
510
+ if is_form_row
511
+ else ""
512
+ )
513
+ cells: list[str] = []
514
+ for col in spec.wide_cols:
515
+ value = row[col] if col < len(row) else ""
516
+ if is_form_row and col == spec.user_input_col:
517
+ cells.append(
518
+ f'<td class="grp-wide grp-form">'
519
+ f"{_form_control(rid, kind, status, value, statement)}</td>"
520
+ )
521
+ else:
522
+ cells.append(f'<td class="grp-wide">{_inline(value)}</td>')
523
+ return tr_attrs, "".join(cells)
524
+
525
+
526
+ def _emit_grouped_table(
527
+ header_cells: list[str], rows: list[list[str]], spec: _GroupedSpec
528
+ ) -> str:
529
+ """Render a wide table in the compact grouped layout described by
530
+ ``spec`` — one metadata cell plus the min-width long columns."""
531
+ head = (
532
+ "<thead><tr>"
533
+ f"<th>{_inline(header_cells[spec.headline_col])}</th>"
534
+ + "".join(
535
+ f'<th class="grp-wide">{_inline(header_cells[col])}</th>'
536
+ for col in spec.wide_cols
537
+ )
538
+ + "</tr></thead>"
539
+ )
540
+ body_rows: list[str] = []
541
+ for row in rows:
542
+ meta = _grouped_meta_cell(header_cells, row, spec)
543
+ if spec.kind == "clarification":
544
+ tr_attrs, wide_cells = _grouped_clarification_row(row, spec)
545
+ else:
546
+ tr_attrs = ""
547
+ wide_cells = "".join(
548
+ f'<td class="grp-wide">{_inline(row[col] if col < len(row) else "")}</td>'
549
+ for col in spec.wide_cols
550
+ )
551
+ body_rows.append(f"<tr{tr_attrs}>{meta}{wide_cells}</tr>")
552
+ body = "<tbody>" + "".join(body_rows) + "</tbody>"
553
+ return f'<table class="grouped-table">{head}{body}</table>'
554
+
555
+
386
556
  _ENUM_LETTERS = "abcde"
387
557
  _ENUM_CUE_WORDS: tuple[str, ...] = (
388
558
  "권장은", "권장 ", "추천은", "추천 ", "사유:", "사유 ", "근거:",
@@ -1068,23 +1068,22 @@ def _list_implementation_planning_reports(
1068
1068
  """
1069
1069
  if not state.task_group or not state.task_id or not state.project_root:
1070
1070
  return []
1071
- base = (tasks_root(state.project_root)
1072
- / slugify_task_segment(state.task_group)
1073
- / slugify_task_segment(state.task_id)
1074
- / "runs" / "implementation-planning")
1075
- if not base.is_dir():
1071
+ # Run seq lives in the filename, not a per-run subdirectory: every
1072
+ # implementation-planning run writes into the same flat `reports/`
1073
+ # dir (see paths.py — `run_reports = runs/<task-type>/reports`).
1074
+ reports_dir = (tasks_root(state.project_root)
1075
+ / slugify_task_segment(state.task_group)
1076
+ / slugify_task_segment(state.task_id)
1077
+ / "runs" / "implementation-planning" / "reports")
1078
+ if not reports_dir.is_dir():
1076
1079
  return []
1077
1080
  pat = re.compile(r"^final-report-implementation-planning-(\d+)\.md$")
1078
1081
  found: list[tuple[int, Path]] = []
1079
- for run_dir in base.iterdir():
1080
- reports = run_dir / "reports"
1081
- if not reports.is_dir():
1082
+ for child in reports_dir.iterdir():
1083
+ m = pat.match(child.name)
1084
+ if not m:
1082
1085
  continue
1083
- for child in reports.iterdir():
1084
- m = pat.match(child.name)
1085
- if not m:
1086
- continue
1087
- found.append((int(m.group(1)), child))
1086
+ found.append((int(m.group(1)), child))
1088
1087
  found.sort(key=lambda x: -x[0])
1089
1088
  out: list[Path] = []
1090
1089
  for _, p in found[:limit]:
@@ -132,6 +132,38 @@ td.td-narrow, th.td-narrow {
132
132
  width: 5%;
133
133
  white-space: nowrap;
134
134
  }
135
+ /* Compact grouped layout for the wide final-report tables (Execution
136
+ * Status, §5 Clarification Items, §7 Follow-up Tasks): the short
137
+ * columns collapse into one stacked `key: value` metadata cell, while
138
+ * each long-prose column keeps its own guaranteed min-width so it never
139
+ * gets squashed into a one-character ladder. */
140
+ table.grouped-table td.grp-meta {
141
+ width: 24%;
142
+ }
143
+ table.grouped-table th.grp-wide,
144
+ table.grouped-table td.grp-wide {
145
+ min-width: 18ch;
146
+ }
147
+ .grp-meta .grp-headline {
148
+ font-weight: 600;
149
+ margin-bottom: 0.4em;
150
+ }
151
+ .grp-meta .grp-field {
152
+ display: flex;
153
+ gap: 0.45em;
154
+ font-size: 0.88rem;
155
+ line-height: 1.55;
156
+ }
157
+ .grp-meta .grp-key {
158
+ color: GrayText;
159
+ white-space: nowrap;
160
+ }
161
+ .grp-meta .grp-key::after {
162
+ content: ":";
163
+ }
164
+ .grp-meta .grp-val {
165
+ overflow-wrap: anywhere;
166
+ }
135
167
  thead th {
136
168
  position: sticky;
137
169
  top: 3rem;