okstra 0.156.0 → 0.157.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.
- package/docs/architecture.md +3 -1
- package/docs/for-ai/skills/okstra-schedule-gen.md +5 -4
- package/docs/project-structure-overview.md +7 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/python/okstra_ctl/clarification_items.py +3 -3
- package/runtime/python/okstra_ctl/render_final_report.py +31 -44
- package/runtime/python/okstra_ctl/report_contract.py +15 -0
- package/runtime/python/okstra_ctl/report_markdown.py +441 -0
- package/runtime/python/okstra_ctl/schedule_semantics.py +186 -91
- package/runtime/python/okstra_ctl/stage_map.py +111 -6
- package/runtime/python/okstra_ctl/wizard.py +1 -11
- package/runtime/python/okstra_project/state.py +14 -2
- package/runtime/skills/okstra-schedule-gen/SKILL.md +43 -18
- package/runtime/templates/reports/final-report-v2.template.md +74 -10
- package/runtime/templates/reports/md/macros/sections.md +19 -0
- package/runtime/templates/reports/md/tasks/change-impact-analysis.template.md +18 -0
- package/runtime/templates/reports/md/tasks/error-analysis.template.md +13 -0
- package/runtime/templates/reports/md/tasks/feature-analysis.template.md +13 -0
- package/runtime/templates/reports/md/tasks/final-verification.template.md +13 -0
- package/runtime/templates/reports/md/tasks/implementation-planning.template.md +15 -0
- package/runtime/templates/reports/md/tasks/implementation.template.md +15 -0
- package/runtime/templates/reports/md/tasks/improvement-discovery.template.md +10 -0
- package/runtime/templates/reports/md/tasks/project-analysis.template.md +15 -0
- package/runtime/templates/reports/md/tasks/release-handoff.template.md +13 -0
- package/runtime/templates/reports/md/tasks/requirements-discovery.template.md +15 -0
- package/runtime/templates/reports/schedule.template.md +166 -63
- package/runtime/validators/validate-run.py +19 -6
- package/runtime/validators/validate-schedule.py +94 -65
- package/src/commands/inspect/stage-map.mjs +6 -1
- package/src/commands/inspect/worker-liveness.mjs +15 -3
- package/src/commands/lifecycle/install.mjs +69 -4
- package/src/commands/lifecycle/uninstall.mjs +21 -35
- package/src/lib/install-assets.mjs +37 -0
package/docs/architecture.md
CHANGED
|
@@ -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
|
|
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:
|
|
@@ -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
|
|
195
|
-
DEV-1
|
|
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
|
-
|
|
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
|
|
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` |
|
|
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
package/runtime/BUILD.json
CHANGED
|
@@ -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
|
|
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
|
|
16
|
-
``## Clarification and User Decisions``,
|
|
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
|
|
@@ -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
|
|
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["
|
|
600
|
-
|
|
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
|