okstra 0.38.0 → 0.38.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 +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +4 -4
- package/runtime/bin/okstra-render-report-views.py +4 -1
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/python/okstra_ctl/paths.py +3 -0
- package/runtime/python/okstra_ctl/report_views.py +27 -3
- package/runtime/python/okstra_ctl/run.py +22 -0
- package/runtime/python/okstra_ctl/schema_excerpt.py +116 -0
- package/runtime/skills/okstra-convergence/SKILL.md +2 -16
- package/runtime/skills/okstra-report-writer/SKILL.md +11 -8
- package/runtime/skills/okstra-team-contract/SKILL.md +5 -13
- package/runtime/templates/reports/final-report.template.md +9 -0
- package/runtime/templates/reports/i18n/en.json +3 -1
- package/runtime/templates/reports/i18n/ko.json +3 -1
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-report-views.py +9 -2
- package/runtime/validators/validate-run.py +3 -1
- package/src/install.mjs +21 -0
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -35,7 +35,7 @@ You also write an audit sidecar at the path the lead registers as `**Worker Resu
|
|
|
35
35
|
1. The canonical data.json path you wrote (project-relative).
|
|
36
36
|
2. The rendered markdown path produced by the renderer (project-relative).
|
|
37
37
|
3. Inputs reconciled (analysis-worker result files + convergence-state file).
|
|
38
|
-
4. Any structural deviations from
|
|
38
|
+
4. Any structural deviations from the `<instruction-set>/final-report-schema.json` excerpt and the reason.
|
|
39
39
|
|
|
40
40
|
Do NOT duplicate the data.json contents here — the data.json is the canonical artifact; this sidecar is the validator-required pointer / audit record.
|
|
41
41
|
|
|
@@ -67,8 +67,8 @@ Before writing the data.json, you MUST:
|
|
|
67
67
|
|
|
68
68
|
For the report writer specifically, the `## Inputs` list always includes:
|
|
69
69
|
|
|
70
|
-
- `schemas/final-report-v1.0.schema.json`
|
|
71
|
-
-
|
|
70
|
+
- `<instruction-set>/final-report-schema.json` — the **per-task-type excerpt** of the data.json schema (scoped to this run's task-type at prep time: other task-types' deliverable blocks and their unreachable `$defs` are stripped). This is the shape you must author. Read this, NOT the full `schemas/final-report-v1.0.schema.json` (it is not in the task bundle and its `schemas/...` path is not resolvable here). Validation still runs against the full schema post-hoc, so the excerpt never relaxes the contract.
|
|
71
|
+
- `<instruction-set>/final-report-template.md` — the **phase-stripped** Jinja2 template the renderer uses (only this run's §4.x deliverable block remains). Read it to understand which data.json fields appear where in the rendered markdown; do NOT edit it, and do NOT pull the full `templates/reports/final-report.template.md` source.
|
|
72
72
|
- `templates/reports/i18n/en.json` and `templates/reports/i18n/ko.json`.
|
|
73
73
|
- Every analysis worker's result file under `worker-results/`.
|
|
74
74
|
- `state/convergence-<task-type>-<seq>.json` (if present). When present, reproduce its `roundHistory[]`, `round2SkippedReason`, and `finalClassificationCounts` verbatim into the final report's Section 1 Round History sub-table — do not recompute from worker results.
|
|
@@ -79,7 +79,7 @@ Write a Reading Confirmation block to your **audit sidecar** at `runs/<task-type
|
|
|
79
79
|
|
|
80
80
|
## Authoring Contract
|
|
81
81
|
|
|
82
|
-
You author the final-report data.json (the JSON SSOT).
|
|
82
|
+
You author the final-report data.json (the JSON SSOT). You author it against the `<instruction-set>/final-report-schema.json` excerpt — its `$defs` enumerate every row shape, enum value, and cross-field constraint that applies to this run's task-type. The validator and renderer both consume the **full** `schemas/final-report-v1.0.schema.json` (the excerpt is a faithful task-type-scoped subset of it), so a data.json that satisfies the excerpt is a data.json that validates and renders correctly.
|
|
83
83
|
|
|
84
84
|
The rendered markdown (`final-report-<task-type>-<seq>.md`) is produced by `scripts/okstra-render-final-report.py` immediately after you write the data.json. The HTML view (`*.html`) is produced from the markdown by Phase 7 step 1.5 (`scripts/okstra-render-report-views.py`). The data.json is the only file you write; the rest are derived.
|
|
85
85
|
|
|
@@ -128,7 +128,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
128
128
|
css, js = _load_assets()
|
|
129
129
|
meta = RunMeta(task_key=task_key, task_type=task_type, seq=seq, source_report=source_report)
|
|
130
130
|
html_path = render_html_view(report_path, run_meta=meta, css=css, js=js)
|
|
131
|
-
|
|
131
|
+
if html_path is None:
|
|
132
|
+
print("html: skipped (no §5 clarification rows — html view carries no interactive forms for this report)")
|
|
133
|
+
else:
|
|
134
|
+
print(f"html: {html_path}")
|
|
132
135
|
return 0
|
|
133
136
|
|
|
134
137
|
|
|
@@ -81,7 +81,7 @@ Emit one `PROGRESS: <phase-id> <verb-phrase>` line as plain user-facing text at
|
|
|
81
81
|
## Available MCP Servers
|
|
82
82
|
|
|
83
83
|
{{AVAILABLE_MCP_SERVERS}}
|
|
84
|
-
- The full usage policy and per-phase rules live in the task brief's `## Available MCP Servers` section. Read them there before dispatching workers and
|
|
84
|
+
- The full usage policy and per-phase rules live in the task brief's `## Available MCP Servers` section. Read them there before dispatching workers and inject only the one-line pointer below into each worker prompt (the brief is already in every worker's [Required reading], so verbatim copy is redundant): `**MCP servers:** follow the task brief's "## Available MCP Servers" section (already in your Required reading).`
|
|
85
85
|
- **Invocation rule (forward to every worker prompt)**: MCP tools are addressed by their tool name through the host's tool interface — **never via `Bash`**. Claude-side workers call the tool directly (e.g. `mcp__<server>__<tool>`). Codex/Gemini workers call through their CLI's own MCP transport (e.g. `codex mcp call ...`). Running the tool name as a shell command is a contract violation and will always fail regardless of permission grants.
|
|
86
86
|
- Codex worker and Gemini worker run external CLIs; they can only use these MCP servers if their own CLI configs mirror them. If not, instruct the worker to record `MCP not available in this CLI` in its `Missing Information or Assumptions` block rather than guessing or shell-falling-back.
|
|
87
87
|
- MCP queries are evidence-grade. Cite server, table, and the SELECT used in worker output. MCP must NOT be used as a write path in any phase, including `implementation`.
|
|
@@ -144,6 +144,7 @@ def compute_run_paths(
|
|
|
144
144
|
final_status = run_status / f"final{suffixes['status']}.status"
|
|
145
145
|
team_state = run_state / f"team-state{suffixes['state']}.json"
|
|
146
146
|
final_report_template = instruction_set / "final-report-template.md"
|
|
147
|
+
final_report_schema = instruction_set / "final-report-schema.json"
|
|
147
148
|
reference_expectations = instruction_set / "reference-expectations.md"
|
|
148
149
|
claude_resume_command = run_sessions / f"claude-resume{suffixes['sessions']}.sh"
|
|
149
150
|
latest_task_file = discovery_dir / "latest-task.json"
|
|
@@ -205,6 +206,7 @@ def compute_run_paths(
|
|
|
205
206
|
"FINAL_STATUS_PATH": str(final_status),
|
|
206
207
|
"TEAM_STATE_PATH": str(team_state),
|
|
207
208
|
"FINAL_REPORT_TEMPLATE_PATH": str(final_report_template),
|
|
209
|
+
"FINAL_REPORT_SCHEMA_PATH": str(final_report_schema),
|
|
208
210
|
"REFERENCE_EXPECTATIONS_FILE": str(reference_expectations),
|
|
209
211
|
"CLAUDE_RESUME_COMMAND_PATH": str(claude_resume_command),
|
|
210
212
|
"OKSTRA_LATEST_TASK_FILE": str(latest_task_file),
|
|
@@ -264,6 +266,7 @@ def compute_run_paths(
|
|
|
264
266
|
("WORKER_RESULTS_RELATIVE_PATH", worker_results),
|
|
265
267
|
("RUN_CARRY_RELATIVE_PATH", run_carry),
|
|
266
268
|
("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", final_report_template),
|
|
269
|
+
("FINAL_REPORT_SCHEMA_RELATIVE_PATH", final_report_schema),
|
|
267
270
|
("REFERENCE_EXPECTATIONS_RELATIVE_PATH", reference_expectations),
|
|
268
271
|
("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", claude_resume_command),
|
|
269
272
|
("RUN_VALIDATOR_RELATIVE_PATH", run_validator_script),
|
|
@@ -664,17 +664,41 @@ def serialize_user_response(
|
|
|
664
664
|
# Convenience entrypoint for tests + CLI.
|
|
665
665
|
# --------------------------------------------------------------------------- #
|
|
666
666
|
|
|
667
|
+
def report_has_clarification_items(src_md: str) -> bool:
|
|
668
|
+
"""True when the final-report MD has at least one §5 ``C-*``
|
|
669
|
+
clarification row. This is the single predicate that gates HTML-view
|
|
670
|
+
generation: the self-contained html's only value over the markdown is
|
|
671
|
+
the embedded ``<form>`` widgets for those rows, so a clarification-free
|
|
672
|
+
report does not get an html sibling. The renderer, the CLI, and
|
|
673
|
+
``validators/validate-report-views.py`` all key off this same function
|
|
674
|
+
so generation and validation never disagree."""
|
|
675
|
+
return any(
|
|
676
|
+
re.fullmatch(r"C-\d+", item.row_id)
|
|
677
|
+
for item in (parse_clarification_items(src_md) or [])
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
|
|
667
681
|
def render_html_view(
|
|
668
682
|
src_md_path: Path,
|
|
669
683
|
*,
|
|
670
684
|
run_meta: RunMeta,
|
|
671
685
|
css: str,
|
|
672
686
|
js: str,
|
|
673
|
-
) -> Path:
|
|
674
|
-
"""Write ``<stem>.html`` next to ``src_md_path`` and return its
|
|
675
|
-
|
|
687
|
+
) -> Path | None:
|
|
688
|
+
"""Write ``<stem>.html`` next to ``src_md_path`` and return its path,
|
|
689
|
+
or return ``None`` when generation is skipped because the report has
|
|
690
|
+
no §5 clarification rows (see ``report_has_clarification_items``).
|
|
691
|
+
Idempotent — overwrites an existing html sibling, and removes a stale
|
|
692
|
+
one when a previously-clarification-bearing report no longer has rows."""
|
|
676
693
|
src_text = src_md_path.read_text(encoding="utf-8")
|
|
677
694
|
html_path = src_md_path.with_name(src_md_path.stem + ".html")
|
|
695
|
+
if not report_has_clarification_items(src_text):
|
|
696
|
+
# Conditional generation: no interactive forms to render. Drop any
|
|
697
|
+
# stale html left over from a prior clarification-bearing run so the
|
|
698
|
+
# validator's "no rows → no html" branch stays consistent.
|
|
699
|
+
if html_path.is_file():
|
|
700
|
+
html_path.unlink()
|
|
701
|
+
return None
|
|
678
702
|
html_text = render_html(src_text, run_meta=run_meta, css=css, js=js)
|
|
679
703
|
html_path.write_text(html_text, encoding="utf-8")
|
|
680
704
|
return html_path
|
|
@@ -35,7 +35,9 @@ from .material import (
|
|
|
35
35
|
related_tasks_inline,
|
|
36
36
|
resolve_related_tasks,
|
|
37
37
|
)
|
|
38
|
+
from .final_report_schema import load_schema
|
|
38
39
|
from .models import resolve_model_metadata
|
|
40
|
+
from .schema_excerpt import build_schema_excerpt
|
|
39
41
|
from .path_resolve import relative_to_project_root, resolve_user_file
|
|
40
42
|
from .render import (
|
|
41
43
|
apply_lead_prompt_defaults,
|
|
@@ -932,6 +934,26 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
932
934
|
render_template_with_ctx(
|
|
933
935
|
str(final_report_template), ctx["FINAL_REPORT_TEMPLATE_PATH"], ctx,
|
|
934
936
|
)
|
|
937
|
+
# Per-task-type schema excerpt for the report-writer worker. The full
|
|
938
|
+
# schema validates the data.json post-hoc (load_schema); the worker only
|
|
939
|
+
# needs the common structure + this run's task-type block, so we write a
|
|
940
|
+
# scoped excerpt into the instruction-set rather than make the worker read
|
|
941
|
+
# the whole 44 KB / all-task-types schema (whose repo `schemas/...` path is
|
|
942
|
+
# not resolvable from a consumer project's task bundle anyway).
|
|
943
|
+
#
|
|
944
|
+
# Guarded: a missing/unreadable schema must NOT break bundle preparation.
|
|
945
|
+
# If the excerpt cannot be produced (e.g. an older install that predates
|
|
946
|
+
# the schemas/ copy step), prep proceeds without it — the report-writer
|
|
947
|
+
# still has the phase-stripped template + skill structure guide, and
|
|
948
|
+
# validation runs against the full schema regardless.
|
|
949
|
+
try:
|
|
950
|
+
_excerpt = build_schema_excerpt(load_schema(), inp.task_type)
|
|
951
|
+
Path(ctx["FINAL_REPORT_SCHEMA_PATH"]).write_text(
|
|
952
|
+
json.dumps(_excerpt, indent=2, ensure_ascii=False) + "\n",
|
|
953
|
+
encoding="utf-8",
|
|
954
|
+
)
|
|
955
|
+
except Exception: # noqa: BLE001 — advisory artifact; never fail prep over it
|
|
956
|
+
pass
|
|
935
957
|
render_template_with_ctx(
|
|
936
958
|
str(prompt_template), str(instruction_set / "claude-execution-prompt.md"), ctx,
|
|
937
959
|
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Build a task-type-scoped excerpt of the final-report schema.
|
|
2
|
+
|
|
3
|
+
The full schema (``schemas/final-report-v1.0.schema.json``) carries the
|
|
4
|
+
deliverable property blocks for ALL task-types (``implementationPlanning``,
|
|
5
|
+
``releaseHandoff``, ``implementation``, ``finalVerification``) plus a
|
|
6
|
+
``$defs`` library (~38% of the file) shared across them. A single run only
|
|
7
|
+
authors ONE task-type's data.json, so the report-writer worker only needs
|
|
8
|
+
the common structure + its own task-type's block + the ``$defs`` those
|
|
9
|
+
reach.
|
|
10
|
+
|
|
11
|
+
This module produces that scoped excerpt, written into the run's
|
|
12
|
+
instruction-set at prep time so the worker reads a smaller, path-local
|
|
13
|
+
file (`instruction-set/final-report-schema.json`) instead of the full
|
|
14
|
+
repo/installed schema (whose `schemas/...` path is not even resolvable
|
|
15
|
+
from inside a consumer project's task bundle).
|
|
16
|
+
|
|
17
|
+
The excerpt is ADVISORY — a reading aid for the author. Validation always
|
|
18
|
+
runs against the FULL schema via ``final_report_schema.load_schema()``, so
|
|
19
|
+
the excerpt never gates correctness; it only trims what the worker reads.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
|
|
26
|
+
# task-type → the per-type deliverable property key it owns. task-types
|
|
27
|
+
# absent from this map (requirements-discovery, error-analysis,
|
|
28
|
+
# improvement-discovery) have no per-type block; their excerpt keeps only
|
|
29
|
+
# the common properties.
|
|
30
|
+
_TASK_TYPE_PROPERTY = {
|
|
31
|
+
"implementation-planning": "implementationPlanning",
|
|
32
|
+
"release-handoff": "releaseHandoff",
|
|
33
|
+
"implementation": "implementation",
|
|
34
|
+
"final-verification": "finalVerification",
|
|
35
|
+
}
|
|
36
|
+
_ALL_PER_TYPE_PROPERTIES = frozenset(_TASK_TYPE_PROPERTY.values())
|
|
37
|
+
|
|
38
|
+
_REF_RE = re.compile(r'"\$ref"\s*:\s*"#/\$defs/([^"]+)"')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _refs_in(obj) -> set[str]:
|
|
42
|
+
"""Every ``#/$defs/<name>`` referenced anywhere inside ``obj``."""
|
|
43
|
+
return set(_REF_RE.findall(json.dumps(obj)))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _conditional_applies(entry: dict, task_type: str) -> bool:
|
|
47
|
+
"""True when an ``allOf`` if/then entry is relevant to *task_type*.
|
|
48
|
+
|
|
49
|
+
Universal entries (no ``header.taskType`` constraint) always apply;
|
|
50
|
+
``const`` entries apply on exact match; ``enum`` entries apply when
|
|
51
|
+
*task_type* is a member.
|
|
52
|
+
"""
|
|
53
|
+
constraint = (
|
|
54
|
+
entry.get("if", {})
|
|
55
|
+
.get("properties", {})
|
|
56
|
+
.get("header", {})
|
|
57
|
+
.get("properties", {})
|
|
58
|
+
.get("taskType", {})
|
|
59
|
+
)
|
|
60
|
+
const = constraint.get("const")
|
|
61
|
+
enum = constraint.get("enum")
|
|
62
|
+
if const is None and enum is None:
|
|
63
|
+
return True
|
|
64
|
+
if const is not None:
|
|
65
|
+
return const == task_type
|
|
66
|
+
return task_type in (enum or [])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_schema_excerpt(schema: dict, task_type: str) -> dict:
|
|
70
|
+
"""Return a task-type-scoped copy of *schema*.
|
|
71
|
+
|
|
72
|
+
Drops the per-type property blocks that do not belong to *task_type*,
|
|
73
|
+
the ``allOf`` conditionals that cannot fire for it, and the ``$defs``
|
|
74
|
+
that become unreachable as a result (transitive closure preserves any
|
|
75
|
+
def reachable from a kept property or conditional). Top-level metadata
|
|
76
|
+
and ``required`` are preserved (``required`` never lists per-type
|
|
77
|
+
blocks, but it is filtered defensively).
|
|
78
|
+
"""
|
|
79
|
+
keep_per_type = _TASK_TYPE_PROPERTY.get(task_type)
|
|
80
|
+
drop_props = _ALL_PER_TYPE_PROPERTIES - ({keep_per_type} if keep_per_type else set())
|
|
81
|
+
|
|
82
|
+
props = {
|
|
83
|
+
k: v for k, v in schema.get("properties", {}).items() if k not in drop_props
|
|
84
|
+
}
|
|
85
|
+
all_of = [e for e in schema.get("allOf", []) if _conditional_applies(e, task_type)]
|
|
86
|
+
|
|
87
|
+
# Reachable $defs = transitive closure of $ref from kept props + allOf.
|
|
88
|
+
defs = schema.get("$defs", {})
|
|
89
|
+
reachable: set[str] = set()
|
|
90
|
+
work = list(_refs_in(props) | _refs_in(all_of))
|
|
91
|
+
while work:
|
|
92
|
+
name = work.pop()
|
|
93
|
+
if name in reachable or name not in defs:
|
|
94
|
+
continue
|
|
95
|
+
reachable.add(name)
|
|
96
|
+
work.extend(_refs_in(defs[name]))
|
|
97
|
+
|
|
98
|
+
excerpt = {
|
|
99
|
+
k: v
|
|
100
|
+
for k, v in schema.items()
|
|
101
|
+
if k not in ("properties", "allOf", "$defs", "required", "description")
|
|
102
|
+
}
|
|
103
|
+
excerpt["description"] = (
|
|
104
|
+
f"Per-task-type excerpt of the okstra final-report schema, scoped to "
|
|
105
|
+
f"`{task_type}`. Reading aid for the report-writer worker — validation "
|
|
106
|
+
f"runs against the full schema (schemas/final-report-v1.0.schema.json)."
|
|
107
|
+
)
|
|
108
|
+
excerpt["properties"] = props
|
|
109
|
+
excerpt["required"] = [
|
|
110
|
+
r for r in schema.get("required", []) if r not in drop_props
|
|
111
|
+
]
|
|
112
|
+
if all_of:
|
|
113
|
+
excerpt["allOf"] = all_of
|
|
114
|
+
if reachable:
|
|
115
|
+
excerpt["$defs"] = {k: v for k, v in defs.items() if k in reachable}
|
|
116
|
+
return excerpt
|
|
@@ -322,8 +322,6 @@ For each finding:
|
|
|
322
322
|
|
|
323
323
|
Save it to `runs/<task-type>/state/convergence-<task-type>-<seq>.json`.
|
|
324
324
|
|
|
325
|
-
Schema version `1.1` extends `1.0` (legacy fields kept as aliases for backward-compat with already-shipped reports):
|
|
326
|
-
|
|
327
325
|
```json
|
|
328
326
|
{
|
|
329
327
|
"schemaVersion": "1.1",
|
|
@@ -368,12 +366,7 @@ Schema version `1.1` extends `1.0` (legacy fields kept as aliases for backward-c
|
|
|
368
366
|
],
|
|
369
367
|
"skippedWorkers": [
|
|
370
368
|
{ "worker": "claude-worker", "reason": "no items to verify" }
|
|
371
|
-
]
|
|
372
|
-
"verificationsRequested": 2,
|
|
373
|
-
"verificationsCompleted": 2,
|
|
374
|
-
"newConsensus": 3,
|
|
375
|
-
"remainingInQueue": 0,
|
|
376
|
-
"earlyExit": true
|
|
369
|
+
]
|
|
377
370
|
}
|
|
378
371
|
],
|
|
379
372
|
"round2SkippedReason": "queue-empty",
|
|
@@ -384,12 +377,6 @@ Schema version `1.1` extends `1.0` (legacy fields kept as aliases for backward-c
|
|
|
384
377
|
"partialConsensus": 1,
|
|
385
378
|
"contested": 0,
|
|
386
379
|
"workerUnique": 1
|
|
387
|
-
},
|
|
388
|
-
"summary": {
|
|
389
|
-
"fullConsensus": 5,
|
|
390
|
-
"partialConsensus": 1,
|
|
391
|
-
"contested": 0,
|
|
392
|
-
"workerUnique": 1
|
|
393
380
|
}
|
|
394
381
|
}
|
|
395
382
|
```
|
|
@@ -408,9 +395,8 @@ Schema rules:
|
|
|
408
395
|
- `roundHistory[].carriedForwardCount`: queue size at the END of this round — the single definition. In-round insertions into the queue are forbidden, so this always equals `inputQueueSize - resolvedCount`. The pseudocode's per-item `carriedForwardCount += 1` accumulator is a counting convenience that lands on the same value; persist the post-round queue length, not the loop accumulator, if the two ever diverge.
|
|
409
396
|
- `roundHistory[].dispatches[]`: one entry per worker that was actually dispatched in this round. Each entry is `{worker, status, durationMs}`. `status ∈ {completed, timeout, error, not-run}`. `durationMs` is integer milliseconds and is always present, even for terminal-non-result dispatches (use the elapsed time before the wrapper gave up).
|
|
410
397
|
- `roundHistory[].skippedWorkers[]`: per-worker `{worker, reason}` for workers with no items to verify OR with a non-result dispatch.
|
|
411
|
-
- `roundHistory[].verificationsRequested|verificationsCompleted|newConsensus|remainingInQueue|earlyExit`: legacy v1.0 aliases. New runs SHOULD populate them so existing parsers keep working: `verificationsRequested == len(dispatches)`, `verificationsCompleted == len(d for d in dispatches if d.status == "completed")`, `newConsensus == resolvedCount`, `remainingInQueue == carriedForwardCount`, `earlyExit == (round < effectiveMaxRounds AND carriedForwardCount == 0)`.
|
|
412
398
|
- `round2SkippedReason`: literal enum `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped`. Always present. Use `"not-skipped"` when Round 2 actually ran. Use `"max-rounds-1"` when `effectiveMaxRounds == 1` (Round 2 was never attempted). Use `"queue-empty"` when Round 1 fully drained the queue. Use `"all-reverify-non-result"` when all Round 1 dispatches terminated as non-result.
|
|
413
|
-
- `finalClassificationCounts`: post-loop counts.
|
|
399
|
+
- `finalClassificationCounts`: post-loop counts. Required field with keys `fullConsensus`, `partialConsensus`, `contested`, `workerUnique`.
|
|
414
400
|
- `finalState ∈ {converged, max-rounds-reached, aborted-non-result}`. Assigned by the lead at WHILE-loop exit: `converged` when the queue is empty at the end of any round; `max-rounds-reached` when the loop exits because `roundIndex == effectiveMaxRounds` with the queue still non-empty; `aborted-non-result` when the loop exits via the Worker-failure BREAK (Task 3's "Worker failure handling in reverify" rule 4). `aborted-non-result` is the new v1.1 value.
|
|
415
401
|
- `totalRounds`: count of rounds actually executed (not `effectiveMaxRounds`). May be `0` when Round 0 produced no queue items (all findings reached consensus during grouping).
|
|
416
402
|
|
|
@@ -46,9 +46,11 @@ The prompt MUST include, in this order at the top:
|
|
|
46
46
|
4. `**Worker Result Path:** runs/<task-type>/worker-results/report-writer-worker-<task-type>-<seq>.md` — mandatory validator-checked worker-results audit file
|
|
47
47
|
5. `Assigned worker prompt history path: <absolute-path>`
|
|
48
48
|
6. `**Model:** Report writer worker, <modelExecutionValue>` (resolved per Phase 5.5 anchor-header rules)
|
|
49
|
-
7. The full `[Required reading]` clause (see [okstra-team-contract](../okstra-team-contract/SKILL.md))
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
7. The full `[Required reading]` clause (see [okstra-team-contract](../okstra-team-contract/SKILL.md)) — for Phase 6 it adds two **per-task-type, instruction-set-local** read-only files, both scoped to this run's task-type by `okstra-ctl` at prep time:
|
|
50
|
+
- `<instruction-set>/final-report-schema.json` — a task-type excerpt of the data.json schema (the other task-types' deliverable blocks and their unreachable `$defs` are stripped; ~38% of the full schema is `$defs` alone). This is your authoring contract for the data.json shape. Do **NOT** pull the full `schemas/final-report-v1.0.schema.json` — it carries all task-types and its `schemas/...` path is not part of the task bundle. (Validation still runs against the full schema post-hoc via the renderer, so the excerpt never relaxes the contract.)
|
|
51
|
+
- `<instruction-set>/final-report-template.md` — the **phase-stripped** template (every other task-type's §4.x deliverable block removed by `render.py`'s `_strip_phase_blocks`, leaving only your run's §4.x). Do **NOT** also pull the full `templates/reports/final-report.template.md` source (it re-adds ~330 lines of other phases' deliverables and is not in the task bundle).
|
|
52
|
+
8. A one-line MCP pointer instead of the verbatim block — `**MCP servers:** follow the task brief's "## Available MCP Servers" section (already in your Required reading).` The brief is already in the report-writer's Required reading (item 7), so the verbatim block is redundant.
|
|
53
|
+
9. The convergence classifications (Full/Partial/Contested/Worker-Unique), the round history data (`roundHistory[]`), the `round2SkippedReason` value, and pointers to all worker result files under `worker-results/`. The report-writer worker populates `crossVerification.roundHistory` in the data.json so Section 1 can show which rounds executed, queue sizes, and why Round 2 was (or was not) skipped. The renderer prints the full per-round table only when more than one round ran; single-round or zero-round histories are auto-collapsed to a one-line summary.
|
|
52
54
|
10. `**Report Language:** <en|ko>` — must be either `en` or `ko`; `auto`
|
|
53
55
|
has been resolved by the lead from project.json / global config
|
|
54
56
|
before the dispatch is constructed. The worker copies this verbatim
|
|
@@ -89,7 +91,7 @@ The four steps below MUST execute in this exact order. Reordering them is the re
|
|
|
89
91
|
```
|
|
90
92
|
|
|
91
93
|
The data.json paths populated: `tokenUsage.lead.{totalTokens,billableTokens,costUsd}`, the `worker` / `grand` rows, `tokenUsage.cli.costUsd`, and each `executionStatus[].{totalTokens,billableTokens,costUsd,durationMs,cliTotalTokens,cliCostUsd}` for rows whose role matches a team-state worker. The data.json MUST already exist (Phase 6 output).
|
|
92
|
-
3. **Phase 7 step 1.5 — Render report views** (BLOCKING).
|
|
94
|
+
3. **Phase 7 step 1.5 — Render report views** (BLOCKING, conditional output). Always invoke the renderer; it decides whether an html sibling is warranted:
|
|
93
95
|
|
|
94
96
|
```bash
|
|
95
97
|
python3 scripts/okstra-render-report-views.py \
|
|
@@ -97,9 +99,10 @@ The four steps below MUST execute in this exact order. Reordering them is the re
|
|
|
97
99
|
```
|
|
98
100
|
|
|
99
101
|
Output (idempotent — re-running overwrites):
|
|
100
|
-
- `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — single-file self-contained human view
|
|
102
|
+
- `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — single-file self-contained human view, **generated only when the report has at least one §5 `C-*` clarification row**. Those rows with `Status` ∈ {`open`, `answered`} embed form widgets (`<select>` for enum-style decisions, `<input>` for material / data-point kinds, `<textarea>` fallback); an `Export user response` button serialises form values to a markdown sidecar (schema in [`templates/reports/user-response.template.md`](../../templates/reports/user-response.template.md)) that the user pastes to `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md`. The original final-report MD is **never** mutated by user input — the sidecar is the single write target.
|
|
103
|
+
- When the report has **no** `C-*` clarification rows, the html carries no interactive forms (it would only duplicate the MD), so the renderer prints `html: skipped (...)` and writes nothing. This is the expected state for clarification-free runs — `validators/validate-report-views.py` treats "no C-* rows + no html" as a pass, not a missing artifact.
|
|
101
104
|
|
|
102
|
-
Must run AFTER step 1 (so token placeholders are substituted in
|
|
105
|
+
Must run AFTER step 1 (so token placeholders are substituted in any rendered html) and BEFORE step 2 (so the html artifact, when generated, exists for the validator step that checks it).
|
|
103
106
|
4. **Phase 7 step 2 — Follow-up task spawner** (BLOCKING when Section 7 is non-empty). Turns the report's `## 7. Follow-up Tasks (후속 작업)` rows into `tasks/<task-group>/<new-task-id>/` stubs.
|
|
104
107
|
|
|
105
108
|
```bash
|
|
@@ -255,7 +258,7 @@ Skipping this file because "the real report is in `reports/`" is wrong. Both fil
|
|
|
255
258
|
|
|
256
259
|
### Main Body Section
|
|
257
260
|
|
|
258
|
-
Section numbering follows `templates/reports/final-report.template.md` exactly — that file is the
|
|
261
|
+
Section numbering follows `templates/reports/final-report.template.md` exactly — that file is the documentation SSOT for section names and ordering. For full body structure at authoring time, consult your run's **phase-stripped** `final-report-template.md` (the instruction-set copy of the same template, with other task-types' §4.x deliverable blocks removed); the "copy that block verbatim" references below mean the §-block as it appears in that stripped copy, not a re-read of the full source.
|
|
259
262
|
|
|
260
263
|
**Verdict Card (top-of-report, mandatory).** Render `## Verdict Card` between the report header and the (conditional) Approval block. Its `Verdict Token` / `Direction` / `Next Step` cells MUST byte-match the corresponding cells in `## 2. Final Verdict` and the first item of `## 6.`. Divergence is `contract-violated`.
|
|
261
264
|
|
|
@@ -313,7 +316,7 @@ Persistence steps that must be performed in Phase 7:
|
|
|
313
316
|
- [ ] 6. **Generate final status file**: `runs/<task-type>/status/final-<task-type>-<seq>.status` (if necessary)
|
|
314
317
|
- [ ] 7. **Save convergence state**: `runs/<task-type>/state/convergence-<task-type>-<seq>.json` (when convergence is enabled)
|
|
315
318
|
- [ ] 8. **Spawn follow-up task stubs**: run `scripts/okstra-spawn-followups.py` against the final-report per the canonical spawn rule defined in "Phase 7 follow-up task spawner" above. Do not restate the trigger condition here — that section is the single source of truth. The script is idempotent across reruns.
|
|
316
|
-
- [ ] 9. **Human HTML report
|
|
319
|
+
- [ ] 9. **Human HTML report** (conditional): `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — produced by Phase 7 step 1.5 **only when the report has ≥1 §5 `C-*` clarification row** (self-contained, embeds `Export user response` button). Clarification-free reports legitimately have no html sibling; do not treat its absence as a missing artifact.
|
|
317
320
|
|
|
318
321
|
### Response after Persistence
|
|
319
322
|
|
|
@@ -80,7 +80,7 @@ The body must include: role name, task type, task key, required bundle paths, as
|
|
|
80
80
|
|
|
81
81
|
When a worker reads any project-relative path from the prompt, it MUST resolve it against `Project Root` (e.g. `<Project Root>/<Result Path>`) — never use bare relative paths that depend on cwd.
|
|
82
82
|
|
|
83
|
-
If the task brief contains an `## Available MCP Servers` section,
|
|
83
|
+
If the task brief contains an `## Available MCP Servers` section, inject only the one-line pointer into every analysis worker's prompt (and into the report-writer prompt when it is dispatched in Phase 6) — the brief is already in every worker's [Required reading], so verbatim copy is redundant: `**MCP servers:** follow the task brief's "## Available MCP Servers" section (already in your Required reading).` Codex/Gemini workers run external CLIs whose MCP availability is governed by their own CLI configs; they can record `MCP not available in this CLI` cleanly after reading that section in the brief.
|
|
84
84
|
|
|
85
85
|
Before dispatching any required worker, lead persists the exact worker prompt to the assigned current-run prompt history path under `runs/<task-type>/prompts/`. Do not use `/tmp/*prompt*.txt` as the canonical artifact path.
|
|
86
86
|
|
|
@@ -100,7 +100,7 @@ Audience-scoped file enumeration (BLOCKING — performance optimization):
|
|
|
100
100
|
| Recipient | Files the lead lists under `## Inputs` |
|
|
101
101
|
|---|---|
|
|
102
102
|
| Claude / Codex / Gemini analysis workers | task-brief, analysis-profile, analysis-material (if present), reference-expectations, clarification-response (if carry-in) |
|
|
103
|
-
| Report writer worker (Phase 6) | all of the above **plus** `final-report-template.md` |
|
|
103
|
+
| Report writer worker (Phase 6) | all of the above **plus** the instruction-set-local `final-report-template.md` (phase-stripped) and `final-report-schema.json` (per-task-type excerpt) — NOT the full `templates/reports/...` / `schemas/...` sources |
|
|
104
104
|
| Reverify dispatches | none — the lead provides only the items to reverify |
|
|
105
105
|
|
|
106
106
|
Asymmetry note: `claude-worker` runs in-process and the Agent SDK auto-loads its agent definition; lead's dispatch prompt body for claude-worker can therefore be shorter than for codex/gemini. The Worker Preamble pointer is still emitted for all three so the contract source is identical regardless of dispatch path.
|
|
@@ -318,41 +318,33 @@ Every worker result file under `worker-results/` must begin with a standardized
|
|
|
318
318
|
```markdown
|
|
319
319
|
# <Role> Analysis — <task-key>
|
|
320
320
|
|
|
321
|
-
**Task:** <task-type>
|
|
322
321
|
**Target:** <path or scope> <!-- OPTIONAL: include when the run is scoped to a specific file/module -->
|
|
323
|
-
**Date:** <YYYY-MM-DD>
|
|
324
322
|
**Model:** <Role>, <AI model>
|
|
325
323
|
```
|
|
326
324
|
|
|
327
|
-
The `Target:` line is optional
|
|
325
|
+
Task-type and date are **not** repeated in this human header — they already live in the YAML frontmatter (`taskType`, `date`), which is the copy Obsidian indexes. Restating them here added a third, un-indexed, machine-unparsed copy with no value; the frontmatter is the single source for both. The `Target:` line is optional — include it when the run is scoped to a specific path or module, omit it for whole-project runs; when present, place it between the title and the `Model:` line.
|
|
328
326
|
|
|
329
327
|
Examples:
|
|
330
328
|
|
|
331
329
|
```markdown
|
|
332
330
|
# Claude Worker Analysis — jobs:tasks:8852
|
|
333
331
|
|
|
334
|
-
**Task:** error-analysis
|
|
335
332
|
**Target:** server/auth.ts
|
|
336
|
-
**
|
|
337
|
-
**Model:** Claude worker, sonnet
|
|
333
|
+
**Model:** Claude worker, opus
|
|
338
334
|
```
|
|
339
335
|
|
|
340
336
|
```markdown
|
|
341
337
|
# Codex Worker Analysis — jobs:tasks:8852
|
|
342
338
|
|
|
343
|
-
**Task:** error-analysis
|
|
344
339
|
**Target:** server/auth.ts
|
|
345
|
-
**Date:** 2026-04-06
|
|
346
340
|
**Model:** Codex worker, <codex-model-id>
|
|
347
341
|
```
|
|
348
342
|
|
|
349
343
|
```markdown
|
|
350
344
|
# Report Writer Worker Analysis — jobs:tasks:8852
|
|
351
345
|
|
|
352
|
-
**Task:** error-analysis
|
|
353
346
|
**Target:** server/auth.ts
|
|
354
|
-
**
|
|
355
|
-
**Model:** Report writer worker, opus-4-6
|
|
347
|
+
**Model:** Report writer worker, opus
|
|
356
348
|
```
|
|
357
349
|
|
|
358
350
|
Use the actual model identifier recorded in team-state (never invent a model ID — read it from `resultContract.requiredWorkerRoles[*].modelExecutionValue` or the tool response metadata).
|
|
@@ -96,7 +96,9 @@ approved: {{ frontmatter.approved | yaml_scalar }}
|
|
|
96
96
|
| {{ t("tokenSummary.rowLead") }} | `{{ tokenUsage.lead.totalTokens | format_int }}` | `{{ tokenUsage.lead.billableTokens | format_int }}` | `{{ tokenUsage.lead.costUsd | format_usd }}` |
|
|
97
97
|
| {{ t("tokenSummary.rowWorkerTotal") }} | `{{ tokenUsage.worker.totalTokens | format_int }}` | `{{ tokenUsage.worker.billableTokens | format_int }}` | `{{ tokenUsage.worker.costUsd | format_usd }}` |
|
|
98
98
|
| {{ t("tokenSummary.rowGrandTotal") }} | **`{{ tokenUsage.grand.totalTokens | format_int }}`** | **`{{ tokenUsage.grand.billableTokens | format_int }}`** | **`{{ tokenUsage.grand.costUsd | format_usd }}`** |
|
|
99
|
+
{% if tokenUsage.cli and tokenUsage.cli.costUsd is not none and tokenUsage.cli.costUsd > 0 -%}
|
|
99
100
|
| {{ t("tokenSummary.rowCliExtra") }} | | | `{{ tokenUsage.cli.costUsd | format_usd }}` |
|
|
101
|
+
{% endif %}
|
|
100
102
|
|
|
101
103
|
{# At Phase 6 numeric cells are null and render as `--`.
|
|
102
104
|
Phase 7's okstra-token-usage.py populates tokenUsage in data.json then
|
|
@@ -107,6 +109,7 @@ approved: {{ frontmatter.approved | yaml_scalar }}
|
|
|
107
109
|
{% if crossVerification.roundHistory and not crossVerification.roundHistory.disabled -%}
|
|
108
110
|
### 1.0 Round History
|
|
109
111
|
|
|
112
|
+
{% if crossVerification.roundHistory.rounds | length > 1 -%}
|
|
110
113
|
| Round | inputQueueSize | resolvedCount | carriedForwardCount | dispatches (worker:status:durationMs) | skippedWorkers (worker:reason) |
|
|
111
114
|
|-------|----------------|---------------|----------------------|----------------------------------------|---------------------------------|
|
|
112
115
|
{% for row in crossVerification.roundHistory.rounds -%}
|
|
@@ -114,6 +117,12 @@ approved: {{ frontmatter.approved | yaml_scalar }}
|
|
|
114
117
|
{% endfor %}
|
|
115
118
|
|
|
116
119
|
- `round2SkippedReason`: `{{ crossVerification.roundHistory.round2SkippedReason }}` ← {{ t("roundHistory.round2SkippedReasonNote") }}
|
|
120
|
+
{% elif crossVerification.roundHistory.rounds | length == 1 -%}
|
|
121
|
+
{% set r = crossVerification.roundHistory.rounds[0] -%}
|
|
122
|
+
- {{ t("roundHistory.singleRoundPrefix") }} resolved={{ r.resolvedCount }}, carriedForward={{ r.carriedForwardCount }}, round2SkippedReason=`{{ crossVerification.roundHistory.round2SkippedReason }}`
|
|
123
|
+
{% else -%}
|
|
124
|
+
- {{ t("roundHistory.noRoundsNote") }} round2SkippedReason=`{{ crossVerification.roundHistory.round2SkippedReason }}`
|
|
125
|
+
{% endif %}
|
|
117
126
|
|
|
118
127
|
{% endif %}
|
|
119
128
|
### 1.1 Consensus
|
|
@@ -75,7 +75,9 @@
|
|
|
75
75
|
"sourceItemsColumnNote": "`Source items` column rule is the same as §1.1."
|
|
76
76
|
},
|
|
77
77
|
"roundHistory": {
|
|
78
|
-
"round2SkippedReasonNote": "value is one of `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | convergence-disabled | single-analyser-only`"
|
|
78
|
+
"round2SkippedReasonNote": "value is one of `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | convergence-disabled | single-analyser-only`",
|
|
79
|
+
"singleRoundPrefix": "Single round —",
|
|
80
|
+
"noRoundsNote": "No reverify rounds executed (all findings reached consensus at grouping)."
|
|
79
81
|
},
|
|
80
82
|
"implementationPlanning": {
|
|
81
83
|
"optionInterfacesLabel": "Affected interfaces / public contracts / downstream consumers",
|
|
@@ -75,7 +75,9 @@
|
|
|
75
75
|
"sourceItemsColumnNote": "`Source items` 컬럼 규칙은 §1.1 과 동일."
|
|
76
76
|
},
|
|
77
77
|
"roundHistory": {
|
|
78
|
-
"round2SkippedReasonNote": "값은 `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | convergence-disabled | single-analyser-only` 중 하나."
|
|
78
|
+
"round2SkippedReasonNote": "값은 `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | convergence-disabled | single-analyser-only` 중 하나.",
|
|
79
|
+
"singleRoundPrefix": "단일 라운드 —",
|
|
80
|
+
"noRoundsNote": "재검증 라운드 미실행 (그룹핑 단계에서 전부 합의)."
|
|
79
81
|
},
|
|
80
82
|
"implementationPlanning": {
|
|
81
83
|
"optionInterfacesLabel": "영향 인터페이스 / 공개 계약 / 다운스트림 소비자",
|
|
@@ -17,7 +17,7 @@ Different recipients need different files. Do NOT include `final-report-template
|
|
|
17
17
|
| Recipient | Files included in `[Required reading]` |
|
|
18
18
|
|---|---|
|
|
19
19
|
| Claude / Codex / Gemini analysis workers | task-brief, analysis-profile, analysis-material (if present), reference-expectations, clarification-response (if carry-in) |
|
|
20
|
-
| Report writer worker (Phase 6) | all of the above **plus** `final-report-template.md` |
|
|
20
|
+
| Report writer worker (Phase 6) | all of the above **plus** the instruction-set-local `final-report-template.md` (phase-stripped) and `final-report-schema.json` (per-task-type excerpt) — NOT the full `templates/reports/...` / `schemas/...` sources |
|
|
21
21
|
| Reverify dispatches (Phase 5.5, lightweight mode) | **do NOT inject `[Required reading]` at all** — see [okstra-convergence](../skills/okstra-convergence/SKILL.md) "Reverify prompt: required-reading suppression". |
|
|
22
22
|
|
|
23
23
|
### Reading rules
|
|
@@ -80,9 +80,16 @@ def validate(report_path: Path) -> list[str]:
|
|
|
80
80
|
|
|
81
81
|
md = report_path.read_text(encoding="utf-8")
|
|
82
82
|
html_path = report_path.with_name(report_path.stem + ".html")
|
|
83
|
+
md_ids = _md_response_ids(md)
|
|
83
84
|
|
|
84
|
-
# (1) sibling artifact exists
|
|
85
|
+
# (1) sibling artifact exists — conditional on §5 clarification rows.
|
|
86
|
+
# The html view's sole value over the MD is its embedded form widgets
|
|
87
|
+
# for §5 C-* rows, so a clarification-free report intentionally has no
|
|
88
|
+
# html sibling (see report_views.report_has_clarification_items). When
|
|
89
|
+
# there are no C-* rows and no html, that is the expected skip state.
|
|
85
90
|
if not html_path.is_file():
|
|
91
|
+
if not md_ids:
|
|
92
|
+
return []
|
|
86
93
|
return [f"missing html artifact: {html_path}"]
|
|
87
94
|
|
|
88
95
|
html_text = html_path.read_text(encoding="utf-8")
|
|
@@ -119,7 +126,7 @@ def validate(report_path: Path) -> list[str]:
|
|
|
119
126
|
# (5) Response ID parity: HTML form rows ↔ §5 C-* rows in MD.
|
|
120
127
|
# Bidirectional — catches both "MD has C-* the HTML lost" AND
|
|
121
128
|
# "HTML has stale C-* that the current MD no longer declares".
|
|
122
|
-
md_ids
|
|
129
|
+
# (md_ids computed once at the top.)
|
|
123
130
|
html_ids = sorted(set(_RESPONSE_ID_ATTR_RE.findall(html_text)))
|
|
124
131
|
if md_ids != html_ids:
|
|
125
132
|
failures.append(
|
|
@@ -1540,9 +1540,11 @@ def _rerender_report_views_after_autofix(report_path: Path) -> str:
|
|
|
1540
1540
|
source_report=report_path.name,
|
|
1541
1541
|
)
|
|
1542
1542
|
try:
|
|
1543
|
-
render_html_view(report_path, run_meta=meta, css=css, js=js)
|
|
1543
|
+
rendered = render_html_view(report_path, run_meta=meta, css=css, js=js)
|
|
1544
1544
|
except Exception as exc: # noqa: BLE001
|
|
1545
1545
|
return f"report-views re-render failed: {exc}"
|
|
1546
|
+
if rendered is None:
|
|
1547
|
+
return "report-views skipped (no §5 clarification rows)"
|
|
1546
1548
|
return "report-views re-rendered"
|
|
1547
1549
|
|
|
1548
1550
|
|
package/src/install.mjs
CHANGED
|
@@ -589,11 +589,24 @@ export async function runInstall(args) {
|
|
|
589
589
|
join(paths.home, "templates"),
|
|
590
590
|
{ refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o644 },
|
|
591
591
|
);
|
|
592
|
+
// schemas/ tree — final-report-v1.0.schema.json is loaded at runtime by
|
|
593
|
+
// okstra_ctl.final_report_schema.load_schema() (parent-walk from the
|
|
594
|
+
// installed okstra_ctl package finds ~/.okstra/schemas/). It is needed by
|
|
595
|
+
// (a) prepare_task_bundle to write the per-task-type schema excerpt into
|
|
596
|
+
// each run's instruction-set, and (b) validate-run's data.json schema
|
|
597
|
+
// check. Without this step copy-mode installs leave load_schema() unable
|
|
598
|
+
// to locate the schema. Link mode resolves it through the repo symlink.
|
|
599
|
+
const schemasResult = await copyTreeIfChanged(
|
|
600
|
+
join(runtimeRoot, "schemas"),
|
|
601
|
+
join(paths.home, "schemas"),
|
|
602
|
+
{ refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o644 },
|
|
603
|
+
);
|
|
592
604
|
|
|
593
605
|
if (!opts.quiet) {
|
|
594
606
|
summarise("python", pythonResult, paths.pythonpath);
|
|
595
607
|
summarise("bin", binResult, paths.bin);
|
|
596
608
|
summarise("templates", templatesResult, join(paths.home, "templates"));
|
|
609
|
+
summarise("schemas", schemasResult, join(paths.home, "schemas"));
|
|
597
610
|
}
|
|
598
611
|
|
|
599
612
|
if (pythonResult.missingSource && binResult.missingSource) {
|
|
@@ -606,6 +619,11 @@ export async function runInstall(args) {
|
|
|
606
619
|
"warning: runtime/templates is empty. report.css / report.js will be missing — re-run the build step.\n",
|
|
607
620
|
);
|
|
608
621
|
}
|
|
622
|
+
if (schemasResult.missingSource) {
|
|
623
|
+
process.stderr.write(
|
|
624
|
+
"warning: runtime/schemas is empty. final-report schema validation + excerpt generation will be unavailable — re-run the build step.\n",
|
|
625
|
+
);
|
|
626
|
+
}
|
|
609
627
|
|
|
610
628
|
const skillResult = await installSkillsCopy(runtimeRoot, opts);
|
|
611
629
|
await writeSkillsManifest(paths.home, skillResult.installed, { dryRun: opts.dryRun });
|
|
@@ -682,6 +700,9 @@ export async function runEnsureInstalled(args) {
|
|
|
682
700
|
}
|
|
683
701
|
if (!(await dirExists(paths.pythonpath))) reasons.push(`missing ${paths.pythonpath}`);
|
|
684
702
|
if (!(await dirExists(paths.agents))) reasons.push(`missing agents dir ${paths.agents}`);
|
|
703
|
+
if (!(await fileExists(join(paths.home, "schemas", "final-report-v1.0.schema.json")))) {
|
|
704
|
+
reasons.push(`missing ${join(paths.home, "schemas", "final-report-v1.0.schema.json")}`);
|
|
705
|
+
}
|
|
685
706
|
if (!(await fileExists(join(CLAUDE_SKILLS_DIR, "okstra-setup", "SKILL.md")))) {
|
|
686
707
|
reasons.push(`missing ${CLAUDE_SKILLS_DIR}/okstra-setup/SKILL.md`);
|
|
687
708
|
}
|