okstra 0.156.0 → 0.157.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.
Files changed (38) hide show
  1. package/docs/architecture.md +3 -1
  2. package/docs/for-ai/skills/okstra-inspect.md +1 -1
  3. package/docs/for-ai/skills/okstra-schedule-gen.md +5 -4
  4. package/docs/project-structure-overview.md +7 -1
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/prompts/profiles/_common-contract.md +1 -1
  8. package/runtime/python/okstra_ctl/clarification_items.py +3 -3
  9. package/runtime/python/okstra_ctl/model_discovery.py +8 -2
  10. package/runtime/python/okstra_ctl/render_final_report.py +31 -44
  11. package/runtime/python/okstra_ctl/report_contract.py +15 -0
  12. package/runtime/python/okstra_ctl/report_markdown.py +441 -0
  13. package/runtime/python/okstra_ctl/schedule_semantics.py +186 -91
  14. package/runtime/python/okstra_ctl/stage_map.py +111 -6
  15. package/runtime/python/okstra_ctl/wizard.py +1 -11
  16. package/runtime/python/okstra_project/state.py +14 -2
  17. package/runtime/skills/okstra-inspect/SKILL.md +2 -1
  18. package/runtime/skills/okstra-schedule-gen/SKILL.md +43 -18
  19. package/runtime/templates/reports/final-report-v2.template.md +74 -10
  20. package/runtime/templates/reports/md/macros/sections.md +19 -0
  21. package/runtime/templates/reports/md/tasks/change-impact-analysis.template.md +18 -0
  22. package/runtime/templates/reports/md/tasks/error-analysis.template.md +13 -0
  23. package/runtime/templates/reports/md/tasks/feature-analysis.template.md +13 -0
  24. package/runtime/templates/reports/md/tasks/final-verification.template.md +13 -0
  25. package/runtime/templates/reports/md/tasks/implementation-planning.template.md +15 -0
  26. package/runtime/templates/reports/md/tasks/implementation.template.md +15 -0
  27. package/runtime/templates/reports/md/tasks/improvement-discovery.template.md +10 -0
  28. package/runtime/templates/reports/md/tasks/project-analysis.template.md +15 -0
  29. package/runtime/templates/reports/md/tasks/release-handoff.template.md +13 -0
  30. package/runtime/templates/reports/md/tasks/requirements-discovery.template.md +15 -0
  31. package/runtime/templates/reports/schedule.template.md +166 -63
  32. package/runtime/validators/validate-run.py +19 -6
  33. package/runtime/validators/validate-schedule.py +94 -65
  34. package/src/commands/inspect/stage-map.mjs +6 -1
  35. package/src/commands/inspect/worker-liveness.mjs +15 -3
  36. package/src/commands/lifecycle/install.mjs +69 -4
  37. package/src/commands/lifecycle/uninstall.mjs +21 -35
  38. package/src/lib/install-assets.mjs +37 -0
@@ -737,7 +737,9 @@ Resume decision rules:
737
737
 
738
738
  New task bundles use `schemas/final-report-v2.0.schema.json` as the final-report data contract. The report writer authors one data.json source of truth, including the required `humanSummary` and exactly one task-type deliverable. Two renderers consume it independently:
739
739
 
740
- - `templates/reports/final-report-v2.template.md` produces compact AI handoff Markdown in a fixed order: handoff summary, clarifications, evidence ledger, one task deliverable, cross-verification audit, execution audit, and token/cost audit. Human narrative fields are excluded from this artifact.
740
+ - `templates/reports/final-report-v2.template.md` produces the AI handoff Markdown spine in a fixed order: handoff summary, decision context, next-task contract, clarifications, evidence ledger, one task deliverable, cross-verification audit, execution audit, and token/cost audit. The task deliverable body comes from `templates/reports/md/tasks/<task-type>.template.md`, the Markdown sibling of the HTML task template below. Human narrative fields are excluded from this artifact.
741
+ - Section bodies are rendered as Markdown — headings, tables for uniform row sets, prose for narrative fields — by `scripts/okstra_ctl/report_markdown.py`, never serialized as JSON. Field order within a section comes from `schemas/final-report-v2.0.schema.json`, whose properties are authored in reading order (a decision draft reads `context → decision → consequences`).
742
+ - Each task template names the sections a reading agent should meet first and closes with a `md_rest()` sweep, so a field added to the schema reaches the Markdown without a template edit. **Enforced:** `tests/contract/test_ai_markdown_rendering.py`.
741
743
  - `templates/reports/html/tasks/<task-type>.template.html` produces the task-specific human-facing HTML. It leads with a plain-language decision summary and presents the selected task's findings, diagrams, tables, evidence, and actions. Worker execution and convergence detail stays in a visually subordinate audit section.
742
744
 
743
745
  The legacy schema v1 contract remains supported by `schemas/final-report-v1.0.schema.json` and `templates/reports/final-report.template.md`. Its Markdown structure is:
@@ -236,7 +236,7 @@ Enter code mode only when the user explicitly requests it, such as "including th
236
236
 
237
237
  ## Output rules
238
238
 
239
- - Answer concisely in Korean.
239
+ - Answer in Korean, spelling out task ids, phase names, and status values on first mention.
240
240
  - Prefer project-relative paths.
241
241
  - Show disk field values as-is without normalizing.
242
242
  - Clearly indicate an awaiting-approval state.
@@ -191,11 +191,11 @@ An unrepresentable half-day allocation is a validation error. Do not substitute
191
191
  Render a plain fenced relative-day Gantt when the selected stages have finite day ranges. Every forward row is identified by stage and repeats its Work Breakdown range:
192
192
 
193
193
  ```text
194
- DEV-1/S2 ████ days=2.0~3.0
195
- DEV-1/S3 ████ days=1.0~2.0
194
+ DEV-1 Stage 2 ████ days=2.0~3.0
195
+ DEV-1 Stage 3 ████ days=1.0~2.0
196
196
  ```
197
197
 
198
- The identifier format is `<TASK-ID>/S<stage-number>` and the annotation is `days=<lower>~<upper>`. Do not emit a row for a completed, unselected, missing, or unknown stage. Bar length is illustrative; `days=` is the validated duration contract.
198
+ A row is labelled `Stage <n>` when exactly one task is scheduled and `<TASK-ID> Stage <n>` when more than one is; the annotation is `days=<lower>~<upper>`. Spell the stage out — `S1` is an opaque code that costs the reader a lookup and saves five characters. Do not emit a row for a completed, unselected, missing, or unknown stage. Bar length is arithmetic: one column is half a day, so a bar runs `lower / 0.5` filled cells `█` then `(upper - lower) / 0.5` open cells `░`.
199
199
 
200
200
  Skip the chart only when no forward task has a finite day signal, and state the concrete reason. Do not use calendar dates, Mermaid, PlantUML, Graphviz, or another graph language.
201
201
 
@@ -233,7 +233,8 @@ After both gates pass:
233
233
  - Guessing a planning report after `stage-map` reports a structured error.
234
234
  - Treating `workStatus` as the detailed schedule status.
235
235
  - Scheduling completed or non-selected stages.
236
- - Publishing a task-level Gantt row without the `/S<stage-number>` suffix and `days=` range.
236
+ - Publishing a Gantt row without its `Stage <n>` label and `days=` range, or abbreviating that label to `S<n>`.
237
+ - Drawing stage order in `## Task Dependency Graph` — that graph carries cross-task edges only; stage order lives in the Work Breakdown's `Depends On` column.
237
238
  - Dispatching narrative validation before deterministic `--selection-json` validation.
238
239
  - Re-rendering after validation instead of promoting the same draft.
239
240
  - Deleting the selection contract before final validation.
@@ -261,6 +261,7 @@ Important modules:
261
261
  | `conformance.py` | validates task-level Tier 3 manifests, parses `QA-RESULT`, detects diff capability surfaces, and reduces results to PASS/ADVISORY/BLOCKING; DB/HTTP/external non-PASS is user-owned advisory while local IO and contract defects remain blocking, enforced by `scripts/okstra_ctl/conformance.py::decide_conformance_gate` and `validators/validate-run.py::_validate_conformance` |
262
262
  | `pr_template.py` | PR body template resolution for release-handoff |
263
263
  | `report_views.py`, `render_final_report.py`, `final_report_schema.py` | Versioned final-report contract: schema v2 data independently produces AI handoff Markdown and human HTML; schema v1 keeps the legacy Markdown/view pipeline |
264
+ | `report_markdown.py` | Schema-ordered Markdown serialisation of a data.json subtree for the AI handoff report — headings, tables for uniform row sets, prose for narrative fields; field order read from the schema, not from the mapping |
264
265
  | `final_report_paths.py`, `report_view_artifacts.py` | Path-helper SSOT for the final-report markdown/data.json pair and the generated view artifacts (HTML view, user-responses directory) |
265
266
  | `wizard.py` | `okstra-run` prompt state machine; user-facing Korean strings live in `prompts/wizard/prompts.ko.json` |
266
267
  | `wizard_stage_intent.py` | stage-related intent projection of the `okstra-run` wizard output — normalizes whole-task (`__whole_task__`) vs single/multi stage selection into render-args (`resolve_wizard_stage_intent`) |
@@ -352,7 +353,8 @@ Token/cost accounting:
352
353
  | Path | Role |
353
354
  |---|---|
354
355
  | `templates/reports/final-report.template.md` | Schema v1 compatibility Markdown template |
355
- | `templates/reports/final-report-v2.template.md` | Compact schema v2 AI handoff Markdown template |
356
+ | `templates/reports/final-report-v2.template.md` | Schema v2 AI handoff Markdown spine |
357
+ | `templates/reports/md/tasks/*.template.md`, `md/macros/sections.md` | Ten dedicated task bodies for the AI handoff Markdown, sibling of `html/tasks/`; shared section macro |
356
358
  | `templates/reports/html/base.template.html`, `html/tasks/*.template.html` | Shared HTML shell plus ten dedicated task templates for human reports; task bodies are not shared |
357
359
  | `templates/reports/report.css`, `report.js` | Inline assets for self-contained HTML report views |
358
360
  | `templates/reports/*.template.md` | Inputs, schedule, user-response, settings templates |
@@ -431,6 +433,10 @@ The neutral lead lifecycle contract lives at `prompts/lead/okstra-lead-contract.
431
433
 
432
434
  - `tests/`: pytest modules, layered into per-domain subfolders — `run/` (prepare/dispatch/run-index core), `contract/` (validator, repo/docs contract, phase rules, profile), `report/` (render, convergence, language, template), `inspect/` (recap, error, context-cost, token-usage), `worktree/` (worktree, stage isolation, reconcile, reclaim), `wizard/`, `handoff/`. The shared path SSOT is `tests/_paths.py` (`REPO_ROOT`/`TESTS_DIR`/`FIXTURES`), and the setting that puts `tests/` on the import path is the repo-root `pytest.ini` (`pythonpath = tests`). Fixtures live in `tests/fixtures/`.
433
435
  - `tests-e2e/`: `scenario-<id>-<name>.sh` shell scenarios (record-start/reconcile, rerun, task lock, agent install, report view, etc.).
436
+ - Each behavior branch has one owning test at the lowest practical layer.
437
+ - An end-to-end scenario must cross the public CLI or installed-runtime boundary.
438
+ - Tests replace operating-system resources and wall-clock waits with controlled doubles.
439
+ - A higher-layer test keeps only the minimum positive flow needed to detect wiring failures.
434
440
 
435
441
  ### 4.13 `tools/korean-sources/`
436
442
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.156.0",
3
+ "version": "0.157.1",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.156.0",
3
- "builtAt": "2026-08-07T06:39:03.548Z",
2
+ "package": "0.157.1",
3
+ "builtAt": "2026-08-07T07:18:35.043Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -59,7 +59,7 @@ profile document.
59
59
  - Any decision in this run that contradicts the brief's `Source Material` must be raised back to the reporter via a `Clarification Items` row; it must NOT be silently overridden. Disagreement with the reporter is allowed only after the row is resolved.
60
60
  - This contract is the single authority on brief consumption. Phase-specific addenda may *tighten* these rules but may not relax them.
61
61
  - Clarification request policy (shared — applies whenever a profile uses `## 1. Clarification Items`):
62
- - Schema-v2 final reports author `clarificationItems[]` in data.json; task-specific HTML renders the question and response controls directly from those IDs, and AI handoff Markdown serializes the same array for the next agent. The remaining table-layout rules describe schema-v1 compatibility and analysis-worker result tables only.
62
+ - Schema-v2 final reports author `clarificationItems[]` in data.json; task-specific HTML renders the question and response controls directly from those IDs, and AI handoff Markdown renders the same array as one headed section per row for the next agent. The remaining table-layout rules describe schema-v1 compatibility and analysis-worker result tables only.
63
63
  - **Legacy canonical column schema (must match `templates/reports/final-report.template.md` §1 exactly):** every `## 1. Clarification Items` table has exactly these 4 columns, in this order:
64
64
  `| <record-meta> | Statement | Expected form | User input |` (the first header is the i18n `columns.recordMeta` label — `Record`).
65
65
  The five short fields (ID, Ticket ID, Kind, Blocks, Status) are stacked inside the single record-meta cell, one per line separated by `<br>`, in this fixed order (mirrors the §2.1 Primary-Evidence meta column):
@@ -12,9 +12,9 @@ read functions take a report **path**, not its text:
12
12
  * schema-v1 — the ``## 1. Clarification Items`` markdown table (introduced
13
13
  when §4.5.9 / §5.1 / §5.2 collapsed into a single section).
14
14
  * schema-v2 — ``clarificationItems[]`` in the ``.data.json`` sibling. Its AI
15
- handoff markdown serialises them as a JSON block under
16
- ``## Clarification and User Decisions``, so the §1 table walk finds nothing
17
- there by construction.
15
+ handoff markdown renders them as one section per row under
16
+ ``## Clarification and User Decisions``, not as a §1 table, so the §1 table
17
+ walk finds nothing there by construction.
18
18
 
19
19
  Every gate goes through ``scan_approval_gate`` / ``scan_open_user_input`` so
20
20
  run-prep (``_validate_approved_plan``), the wizard, and the user-response CLI
@@ -53,7 +53,12 @@ def _dispatch_effort(execution: str, role: str) -> str:
53
53
 
54
54
  @lru_cache(maxsize=1)
55
55
  def agy_models(agy_bin: str = "agy") -> tuple[str, ...]:
56
- """Live `agy models` list, or () when agy is unavailable (non-blocking)."""
56
+ """Live `agy models` ids, or () when agy is unavailable (non-blocking).
57
+
58
+ agy prints one model per line as `<id>\\t<display label>`; only the id is
59
+ comparable to a catalog execution value, so the label column is dropped.
60
+ A line with no tab is already bare and passes through unchanged.
61
+ """
57
62
  try:
58
63
  proc = subprocess.run(
59
64
  [agy_bin, "models"],
@@ -63,7 +68,8 @@ def agy_models(agy_bin: str = "agy") -> tuple[str, ...]:
63
68
  return ()
64
69
  if proc.returncode != 0:
65
70
  return ()
66
- return tuple(line.strip() for line in proc.stdout.splitlines() if line.strip())
71
+ ids = (line.split("\t", 1)[0].strip() for line in proc.stdout.splitlines())
72
+ return tuple(model_id for model_id in ids if model_id)
67
73
 
68
74
 
69
75
  def _codex_config_path() -> Path:
@@ -52,7 +52,8 @@ from okstra_ctl.final_report_schema import (
52
52
  from okstra_ctl.i18n import I18nError, SUPPORTED_LANGS, load_dictionary, make_jinja_global
53
53
  from okstra_ctl.md_table import UNESCAPED_PIPE_RE, to_cell_text
54
54
  from okstra_ctl.models import UnknownModelError, resolve_model_metadata
55
- from okstra_ctl.report_contract import TASK_TYPE_DATA_PROPERTY
55
+ from okstra_ctl.report_contract import TASK_TYPE_DATA_PROPERTY, markdown_template_for
56
+ from okstra_ctl.report_markdown import ReportSections
56
57
  from okstra_ctl.schema_excerpt import excerpt_cut_from_version
57
58
  from okstra_ctl.seeding import installed_version
58
59
 
@@ -474,9 +475,12 @@ def inject_index_into_file(md_path: Path) -> int:
474
475
  return len(injected.encode("utf-8"))
475
476
 
476
477
 
477
- def _enforce_schema(data: dict) -> None:
478
+ def _enforce_schema(data: dict) -> dict | None:
478
479
  """렌더 전에 data.json 을 스키마에 대해 검증하는 seam.
479
480
 
481
+ 검증에 쓴 스키마를 그대로 돌려준다 — v2 마크다운 렌더러가 필드 순서를
482
+ 이 스키마에서 읽기 때문에, 같은 파일을 두 번 찾지 않는다.
483
+
480
484
  스키마 파일을 찾지 못하는 경우(손상된 설치 환경)는 경고만 출력하고 계속
481
485
  진행한다 — validate-run 과 install 경고가 이미 해당 상황을 표면화하므로
482
486
  Phase 7 재렌더를 hard-fail 시키는 것은 과도하다.
@@ -488,13 +492,14 @@ def _enforce_schema(data: dict) -> None:
488
492
  f"render-final-report: schema not locatable; skipping schema enforcement ({exc})",
489
493
  file=sys.stderr,
490
494
  )
491
- return
495
+ return None
492
496
  errors = schema_validate(data, schema)
493
497
  if errors:
494
498
  raise FinalReportRenderError(
495
499
  f"final-report data.json fails schema validation ({len(errors)} error(s)): "
496
500
  + "; ".join(errors[:5])
497
501
  )
502
+ return schema
498
503
 
499
504
 
500
505
  # 일반 alias('opus'/'sonnet'/'haiku')가 런타임에 해소되는, okstra 가 아는 최신
@@ -556,7 +561,6 @@ def _build_environment(template_dir: Path) -> Environment:
556
561
  env.filters["yaml_scalar"] = _yaml_scalar
557
562
  env.filters["yaml_inline_list"] = _yaml_inline_list
558
563
  env.filters["model_detail"] = _model_detail
559
- env.filters["json_block"] = _json_markdown_block
560
564
  # `mdcell` neutralises the two things in worker prose that can break a
561
565
  # markdown table row: a literal `|` (splits the row) and a newline
562
566
  # (truncates it, dropping every later column). Table-cell interpolations
@@ -571,50 +575,15 @@ def _build_environment(template_dir: Path) -> Environment:
571
575
  return env
572
576
 
573
577
 
574
- def _json_markdown_block(value: Any) -> str:
575
- serialized = json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True)
576
- return "\n".join(f" {line}" for line in serialized.splitlines())
577
-
578
-
579
- def _without_human_narrative(value: Any) -> Any:
580
- if isinstance(value, dict):
581
- return {
582
- key: _without_human_narrative(item)
583
- for key, item in value.items()
584
- if key != "userNarrative"
585
- }
586
- if isinstance(value, list):
587
- return [_without_human_narrative(item) for item in value]
588
- return value
589
-
590
-
591
- def _ai_markdown_context(data: dict) -> dict:
578
+ def _ai_markdown_context(data: dict, schema: dict | None) -> dict:
592
579
  context = _with_optional_defaults(data)
593
580
  header = data.get("header") if isinstance(data.get("header"), dict) else {}
594
581
  task_type = header.get("taskType", "")
595
- property_name = TASK_TYPE_DATA_PROPERTY.get(task_type, "")
596
582
  context["aiTaskDeliverableTitle"] = TASK_DELIVERABLE_TITLES.get(
597
583
  task_type, task_type
598
584
  )
599
- context["aiTaskDeliverable"] = _without_human_narrative(
600
- data.get(property_name, {})
601
- )
602
- context["aiEvidenceLedger"] = {
603
- "evidence": data.get("evidence", {}),
604
- "missingInformation": data.get("missingInformation", []),
605
- "endStateCoverage": data.get("endStateCoverage", []),
606
- "analysisCommon": data.get("analysisCommon"),
607
- }
608
- context["aiRoutingContract"] = {
609
- "recommendedNextSteps": data.get("recommendedNextSteps", []),
610
- "followUpTasks": data.get("followUpTasks", []),
611
- }
612
- context["aiDecisionContext"] = {
613
- "rationale": data.get("rationale", {}),
614
- "summary": data.get("summary", []),
615
- "ticketCoverage": data.get("ticketCoverage"),
616
- "finalVerdict": data.get("finalVerdict", {}),
617
- }
585
+ context["aiTaskProperty"] = TASK_TYPE_DATA_PROPERTY.get(task_type, "")
586
+ context["aiTaskTemplate"] = _markdown_task_template(task_type)
618
587
  context["aiBlockingIds"] = [
619
588
  row.get("id")
620
589
  for row in data.get("clarificationItems", [])
@@ -623,9 +592,27 @@ def _ai_markdown_context(data: dict) -> dict:
623
592
  and row.get("blocks") in {"approval", "next-phase"}
624
593
  and isinstance(row.get("id"), str)
625
594
  ]
595
+ sections = ReportSections(data, schema or {})
596
+ context["md"] = sections.section
597
+ context["md_rest"] = sections.rest
598
+ context["md_has"] = sections.has
599
+ context["md_claim"] = sections.mark_rendered
626
600
  return context
627
601
 
628
602
 
603
+ def _markdown_task_template(task_type: str) -> str:
604
+ """The task body this report includes, or '' for an unknown task type.
605
+
606
+ An unknown type still renders the shared spine plus the generic sweep, so a
607
+ task type that reaches the renderer before its template exists produces a
608
+ complete report rather than a crash.
609
+ """
610
+ try:
611
+ return markdown_template_for(task_type)
612
+ except ValueError:
613
+ return ""
614
+
615
+
629
616
  # The Markdown is the AI handoff sibling of the data.json — same content, same
630
617
  # audience, so it renders in the SSOT's language and nothing else. The field
631
618
  # still has to be well-formed here because it decides whether Phase 7 pays for
@@ -700,7 +687,7 @@ def render(
700
687
  if not template_path.is_file():
701
688
  raise FinalReportRenderError(f"template not found: {template_path}")
702
689
 
703
- _enforce_schema(data)
690
+ schema = _enforce_schema(data)
704
691
 
705
692
  lang = validate_report_language(data)
706
693
  try:
@@ -714,7 +701,7 @@ def render(
714
701
  try:
715
702
  template = env.get_template(template_path.name)
716
703
  context = (
717
- _ai_markdown_context(data)
704
+ _ai_markdown_context(data, schema)
718
705
  if data.get("schemaVersion") == "2.0"
719
706
  else _with_optional_defaults(data)
720
707
  )
@@ -36,6 +36,14 @@ TASK_TYPE_HTML_TEMPLATE = {
36
36
  for task_type in PUBLIC_REPORT_TASK_TYPES
37
37
  }
38
38
 
39
+ # The AI handoff Markdown's task body, sibling of the human HTML template above.
40
+ # Both are keyed off the same task-type list so a new task type cannot ship one
41
+ # audience's view without the other.
42
+ TASK_TYPE_MARKDOWN_TEMPLATE = {
43
+ task_type: f"md/tasks/{task_type}.template.md"
44
+ for task_type in PUBLIC_REPORT_TASK_TYPES
45
+ }
46
+
39
47
  TASK_TYPE_REQUIRED_HUMAN_FIELDS = {
40
48
  "requirements-discovery": (
41
49
  "requirementsDiscovery.requestVerbatim",
@@ -122,3 +130,10 @@ def html_template_for(task_type: str) -> str:
122
130
  return TASK_TYPE_HTML_TEMPLATE[task_type]
123
131
  except KeyError as exc:
124
132
  raise ValueError(f"no task-specific html template for: {task_type}") from exc
133
+
134
+
135
+ def markdown_template_for(task_type: str) -> str:
136
+ try:
137
+ return TASK_TYPE_MARKDOWN_TEMPLATE[task_type]
138
+ except KeyError as exc:
139
+ raise ValueError(f"no task-specific markdown template for: {task_type}") from exc