okstra 0.178.0 → 0.179.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/README.md +2 -2
- package/dist/commands/execute/plan-verify.mjs +1 -1
- package/dist/commands/execute/worktree-status.mjs +8 -2
- package/dist/commands/execute/worktree-status.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +1 -1
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/report/render-final-report.mjs +3 -3
- package/docs/architecture/storage-model.md +3 -3
- package/docs/architecture.md +10 -9
- package/docs/cli.md +11 -13
- package/docs/for-ai/skills/okstra-inspect.md +3 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +2 -2
- package/docs/for-ai/skills/okstra-user-response.md +2 -2
- package/docs/project-structure-overview.md +10 -11
- package/docs/task-process/implementation-planning.md +1 -1
- package/docs/task-process/implementation.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +11 -12
- package/runtime/bin/lib/okstra/globals.sh +2 -2
- package/runtime/bin/lib/okstra/interactive.sh +1 -1
- package/runtime/bin/lib/okstra/usage.sh +11 -9
- package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +1 -1
- package/runtime/bin/okstra-central.sh +2 -2
- package/runtime/bin/okstra-render-final-report.py +1 -1
- package/runtime/bin/okstra-token-usage.py +1 -1
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/adapters/cmux.md +6 -1
- package/runtime/prompts/lead/context-loader.md +3 -2
- package/runtime/prompts/lead/convergence.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +4 -4
- package/runtime/prompts/lead/plan-body-verification.md +3 -3
- package/runtime/prompts/lead/report-writer.md +21 -20
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +5 -4
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -0
- package/runtime/prompts/profiles/_implementation-executor.md +2 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +9 -5
- package/runtime/prompts/profiles/implementation.md +4 -4
- package/runtime/prompts/profiles/improvement-discovery.md +2 -2
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +5 -4
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -4
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +71 -9
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +5 -4
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +55 -3
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -3
- package/runtime/python/okstra_ctl/analysis_packet.py +21 -0
- package/runtime/python/okstra_ctl/backfill.py +12 -5
- package/runtime/python/okstra_ctl/consumers.py +70 -3
- package/runtime/python/okstra_ctl/convergence_engine.py +43 -17
- package/runtime/python/okstra_ctl/dispatch_core.py +47 -11
- package/runtime/python/okstra_ctl/dispatch_state.py +20 -19
- package/runtime/python/okstra_ctl/domain/worker_exec.py +13 -34
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +128 -0
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +5 -0
- package/runtime/python/okstra_ctl/final_report_paths.py +77 -1
- package/runtime/python/okstra_ctl/handoff.py +1 -2
- package/runtime/python/okstra_ctl/implementation_outcome.py +1 -1
- package/runtime/python/okstra_ctl/index.py +4 -4
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +26 -12
- package/runtime/python/okstra_ctl/listing.py +4 -2
- package/runtime/python/okstra_ctl/manager_launch.py +1 -1
- package/runtime/python/okstra_ctl/manager_sync.py +1 -1
- package/runtime/python/okstra_ctl/path_hints.py +2 -2
- package/runtime/python/okstra_ctl/paths.py +13 -10
- package/runtime/python/okstra_ctl/plan_run_root.py +9 -5
- package/runtime/python/okstra_ctl/recap.py +3 -2
- package/runtime/python/okstra_ctl/reconcile.py +3 -1
- package/runtime/python/okstra_ctl/render.py +22 -22
- package/runtime/python/okstra_ctl/report_finalize.py +4 -4
- package/runtime/python/okstra_ctl/rollup.py +1 -1
- package/runtime/python/okstra_ctl/run.py +139 -284
- package/runtime/python/okstra_ctl/run_audit.py +5 -5
- package/runtime/python/okstra_ctl/run_index_row.py +2 -2
- package/runtime/python/okstra_ctl/session_transcript.py +89 -0
- package/runtime/python/okstra_ctl/stage_ledger.py +72 -0
- package/runtime/python/okstra_ctl/stage_map.py +28 -29
- package/runtime/python/okstra_ctl/stage_targets.py +61 -0
- package/runtime/python/okstra_ctl/user_response.py +97 -12
- package/runtime/python/okstra_ctl/wizard.py +43 -65
- package/runtime/python/okstra_ctl/worker_prompt_body.py +6 -7
- package/runtime/python/okstra_ctl/worker_runner.py +76 -213
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/python/okstra_ctl/wrapper_status.py +23 -0
- package/runtime/python/okstra_ctl/write_policy.py +45 -6
- package/runtime/python/okstra_project/state.py +2 -2
- package/runtime/python/okstra_token_usage/__init__.py +1 -1
- package/runtime/python/okstra_token_usage/cli.py +3 -3
- package/runtime/python/okstra_token_usage/report.py +7 -24
- package/runtime/schemas/convergence-groups-v1.0.schema.json +1 -1
- package/runtime/schemas/convergence-groups-v2.0.schema.json +1 -1
- package/runtime/schemas/final-report-v2.0.schema.json +11 -1
- package/runtime/skills/okstra-inspect/facets/history.md +3 -3
- package/runtime/skills/okstra-inspect/facets/recap.md +1 -1
- package/runtime/skills/okstra-inspect/facets/report.md +5 -5
- package/runtime/skills/okstra-inspect/facets/status.md +2 -2
- package/runtime/skills/okstra-pr-gen/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-schedule-gen/SKILL.md +1 -1
- package/runtime/skills/okstra-user-response/SKILL.md +3 -3
- package/runtime/templates/project-docs/task-index.template.md +1 -1
- package/runtime/templates/report-writer-prompt-preamble.md +1 -1
- package/runtime/validators/forbidden_actions.py +76 -5
- package/runtime/validators/lib/fixtures.sh +14 -10
- package/runtime/validators/lib/runners.sh +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +3 -0
- package/runtime/validators/validate-report-views.py +1 -1
- package/runtime/validators/validate-run.py +95 -37
|
@@ -131,6 +131,7 @@ def build_invocation_write_contract(
|
|
|
131
131
|
artifact_paths: Sequence[Path],
|
|
132
132
|
maximum_precision: BoundaryPrecision,
|
|
133
133
|
planned_paths: Sequence[str] = (),
|
|
134
|
+
planned_paths_declared: bool = True,
|
|
134
135
|
scratch_roots: Sequence[Path] = (),
|
|
135
136
|
generated_paths: Sequence[str] = (),
|
|
136
137
|
auxiliary_roots: Sequence[Path] = (),
|
|
@@ -146,6 +147,7 @@ def build_invocation_write_contract(
|
|
|
146
147
|
"projectRoot": str(root),
|
|
147
148
|
"artifactPaths": list(relative_artifacts),
|
|
148
149
|
"plannedPaths": list(planned_paths),
|
|
150
|
+
"plannedPathsDeclared": planned_paths_declared,
|
|
149
151
|
"protectedPaths": [".okstra", ".git"],
|
|
150
152
|
"generatedPaths": list(generated_paths),
|
|
151
153
|
"scratchRoots": [str(path) for path in scratch_roots],
|
|
@@ -198,6 +200,14 @@ def planned_paths_from_run_manifest(
|
|
|
198
200
|
return _planned_paths_from_report(Path(approved), int(stage_value))
|
|
199
201
|
|
|
200
202
|
|
|
203
|
+
def planned_paths_declared(project_root: Path, manifest: Mapping[str, Any]) -> bool:
|
|
204
|
+
"""Whether this run's approved plan states its paths in the declared column."""
|
|
205
|
+
try:
|
|
206
|
+
return planned_paths_from_run_manifest(project_root, manifest)[1]
|
|
207
|
+
except WritePolicyError:
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
|
|
201
211
|
def derive_write_enforcement(
|
|
202
212
|
policy: WritePolicy,
|
|
203
213
|
maximum_precision: BoundaryPrecision,
|
|
@@ -466,7 +476,11 @@ def _relative_to_root(path: Path, root: Path, label: str) -> str:
|
|
|
466
476
|
return relative
|
|
467
477
|
|
|
468
478
|
|
|
469
|
-
def _planned_paths_from_report(
|
|
479
|
+
def _planned_paths_from_report(
|
|
480
|
+
report_path: Path, stage: int
|
|
481
|
+
) -> tuple[tuple[str, ...], bool]:
|
|
482
|
+
"""(paths, declared). `declared` is False for a plan written before the
|
|
483
|
+
structured `plannedPaths` column existed."""
|
|
470
484
|
payload = _read_json(report_path.with_suffix(".data.json"), "approved plan data")
|
|
471
485
|
planning = payload.get("implementationPlanning")
|
|
472
486
|
stages = planning.get("stages") if isinstance(planning, Mapping) else None
|
|
@@ -478,15 +492,40 @@ def _planned_paths_from_report(report_path: Path, stage: int) -> tuple[str, ...]
|
|
|
478
492
|
None,
|
|
479
493
|
) if isinstance(stages, list) else None
|
|
480
494
|
steps = selected.get("stepwiseExecution") if isinstance(selected, Mapping) else None
|
|
495
|
+
# `plannedPaths`, not `files`: the latter is prose written for a reader and
|
|
496
|
+
# splitting it on commas produced entries like `.npmrc (template only` and
|
|
497
|
+
# `no literal token)`, while a single cell naming a glob migration counted
|
|
498
|
+
# as one literal path. Every file the plan actually asked for then read as
|
|
499
|
+
# an unauthorized source change, which is a failure the executor cannot
|
|
500
|
+
# avoid by following the plan.
|
|
501
|
+
rows = steps if isinstance(steps, list) else []
|
|
481
502
|
paths = {
|
|
503
|
+
str(path).strip().strip("`")
|
|
504
|
+
for step in rows if isinstance(step, Mapping)
|
|
505
|
+
for path in (step.get("plannedPaths") or [])
|
|
506
|
+
if isinstance(path, str) and path.strip()
|
|
507
|
+
}
|
|
508
|
+
if paths:
|
|
509
|
+
return _relative_paths(tuple(sorted(paths))), True
|
|
510
|
+
# A plan approved before `plannedPaths` existed carries its paths only in
|
|
511
|
+
# the prose `files` cell. Deriving them there is exactly what this change
|
|
512
|
+
# removed — it mis-splits globs and parentheticals — so the derived set is
|
|
513
|
+
# not used to reject anything. It travels with `plannedPathsDeclared:
|
|
514
|
+
# false`, which turns the path comparison off for that stage rather than
|
|
515
|
+
# failing every file the plan asked for. An in-flight task keeps running;
|
|
516
|
+
# the next planning run produces a plan the audit can actually enforce.
|
|
517
|
+
derived = {
|
|
482
518
|
part.strip().strip("`")
|
|
483
|
-
for step in
|
|
519
|
+
for step in rows if isinstance(step, Mapping)
|
|
484
520
|
for part in str(step.get("files") or "").replace("<br>", ",").split(",")
|
|
485
521
|
if part.strip()
|
|
486
|
-
}
|
|
487
|
-
if not
|
|
488
|
-
raise WritePolicyError(
|
|
489
|
-
|
|
522
|
+
}
|
|
523
|
+
if not derived:
|
|
524
|
+
raise WritePolicyError(
|
|
525
|
+
"implementer write policy stage has no planned paths: the approved "
|
|
526
|
+
"plan's stepwise rows carry neither `plannedPaths` nor `files`"
|
|
527
|
+
)
|
|
528
|
+
return _relative_paths(tuple(sorted(derived))), False
|
|
490
529
|
|
|
491
530
|
|
|
492
531
|
def _rooted(project_root: Path, value: str) -> Path:
|
|
@@ -104,7 +104,7 @@ def read_task_manifest(task_root: Path) -> Optional[dict]:
|
|
|
104
104
|
|
|
105
105
|
**디스크는 건드리지 않는다.** 승격한 값을 여기서 되적으면 lock 없는
|
|
106
106
|
read-modify-write 가 되어, 파싱과 쓰기 사이에 다른 프로세스가 적은
|
|
107
|
-
`currentPhase` · `phaseStates` · `
|
|
107
|
+
`currentPhase` · `phaseStates` · `latestReportRecordPath` 를 되돌린다
|
|
108
108
|
(`os.replace` 는 찢어진 읽기만 막고 갱신 유실은 못 막는다). 이 분기는
|
|
109
109
|
디스크 포인터가 아직 문자열일 때 — 즉 업그레이드 직후 진행 중인 task 가
|
|
110
110
|
살아 있을 때 — 만 발화하므로 되돌릴 수 있는 값이 바로 phase 진행 상태다.
|
|
@@ -155,7 +155,7 @@ _DERIVED_STATUS_STRING_FIELDS = (
|
|
|
155
155
|
"workStatus",
|
|
156
156
|
"currentStatus",
|
|
157
157
|
"latestRunStatus",
|
|
158
|
-
"
|
|
158
|
+
"latestReportRecordPath",
|
|
159
159
|
)
|
|
160
160
|
|
|
161
161
|
|
|
@@ -9,7 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from .collect import collect
|
|
10
10
|
from .report import (
|
|
11
11
|
SubstituteRefusedError,
|
|
12
|
-
|
|
12
|
+
populate_token_cells,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
|
|
@@ -130,7 +130,7 @@ def main() -> int:
|
|
|
130
130
|
|
|
131
131
|
if args.substitute_data is not None:
|
|
132
132
|
try:
|
|
133
|
-
cells_changed
|
|
133
|
+
cells_changed = populate_token_cells(
|
|
134
134
|
args.substitute_data,
|
|
135
135
|
updated,
|
|
136
136
|
)
|
|
@@ -142,7 +142,7 @@ def main() -> int:
|
|
|
142
142
|
return 2
|
|
143
143
|
print(
|
|
144
144
|
f"final-report substitution: populated {cells_changed} cell(s) in "
|
|
145
|
-
f"{args.substitute_data}
|
|
145
|
+
f"{args.substitute_data}. The full reading copy is on-demand.",
|
|
146
146
|
file=sys.stderr,
|
|
147
147
|
)
|
|
148
148
|
|
|
@@ -18,11 +18,6 @@ from okstra_ctl.design_prep import ( # noqa: E402
|
|
|
18
18
|
DesignPrepError,
|
|
19
19
|
materialize_design_prep_requests,
|
|
20
20
|
)
|
|
21
|
-
from okstra_ctl.final_report_paths import final_report_markdown_path # noqa: E402
|
|
22
|
-
from okstra_ctl.render_final_report import ( # noqa: E402
|
|
23
|
-
FinalReportRenderError,
|
|
24
|
-
render_to_file,
|
|
25
|
-
)
|
|
26
21
|
|
|
27
22
|
from .task_totals import task_cumulative_usage # noqa: E402
|
|
28
23
|
|
|
@@ -270,28 +265,16 @@ def populate_data_token_cells(data_path: Path, team_state: dict) -> int:
|
|
|
270
265
|
return changes
|
|
271
266
|
|
|
272
267
|
|
|
273
|
-
def
|
|
268
|
+
def populate_token_cells(
|
|
274
269
|
data_path: Path,
|
|
275
270
|
team_state: dict,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
``
|
|
281
|
-
|
|
282
|
-
The caller is normally Phase 7 step 1 (token-usage CLI with the
|
|
283
|
-
``--substitute-data`` flag). The rendered markdown is the canonical
|
|
284
|
-
user-facing artifact; data.json is the SSOT.
|
|
271
|
+
) -> int:
|
|
272
|
+
"""Mutate data.json token cells and materialize design-prep requests.
|
|
273
|
+
|
|
274
|
+
The full reading copy is rendered on demand with
|
|
275
|
+
``okstra render-final-report``.
|
|
285
276
|
"""
|
|
286
277
|
changes = populate_data_token_cells(data_path, team_state)
|
|
287
|
-
|
|
288
|
-
if output_path is None:
|
|
289
|
-
output_path = final_report_markdown_path(data_path)
|
|
290
|
-
|
|
291
|
-
try:
|
|
292
|
-
bytes_written = render_to_file(data_path, output_path)
|
|
293
|
-
except FinalReportRenderError as exc:
|
|
294
|
-
raise SubstituteRefusedError(f"render after substitute failed: {exc}") from exc
|
|
295
278
|
if data_path.name.startswith("final-report-implementation-planning-"):
|
|
296
279
|
try:
|
|
297
280
|
materialize_design_prep_requests(data_path)
|
|
@@ -299,4 +282,4 @@ def populate_and_render(
|
|
|
299
282
|
raise SubstituteRefusedError(
|
|
300
283
|
f"design-prep request materialization failed: {exc}"
|
|
301
284
|
) from exc
|
|
302
|
-
return changes
|
|
285
|
+
return changes
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"additionalProperties": false,
|
|
57
57
|
"properties": {
|
|
58
58
|
"workerId": { "type": "string", "pattern": "\\S" },
|
|
59
|
-
"audience": { "enum": ["analysis", "report-writer"] },
|
|
59
|
+
"audience": { "enum": ["analysis", "lead", "report-writer"] },
|
|
60
60
|
"participantRef": { "type": "string", "pattern": "\\S" },
|
|
61
61
|
"sourceRoleExecutionRef": { "type": "string", "pattern": "\\S" }
|
|
62
62
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://okstra.dev/schemas/final-report-v2.0.json",
|
|
4
4
|
"title": "OKSTRA Final Report Data (v2.0)",
|
|
5
|
-
"description": "JSON SSOT for okstra final reports. Written by report-writer-worker in Phase 6, then consumed independently by the
|
|
5
|
+
"description": "JSON SSOT for okstra final reports. Written by report-writer-worker in Phase 6, then consumed independently by the on-demand full reading copy Markdown renderer and the task-specific human HTML renderer.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": [
|
|
8
8
|
"schemaVersion",
|
|
@@ -8627,6 +8627,7 @@
|
|
|
8627
8627
|
"step",
|
|
8628
8628
|
"action",
|
|
8629
8629
|
"files",
|
|
8630
|
+
"plannedPaths",
|
|
8630
8631
|
"command",
|
|
8631
8632
|
"outcome",
|
|
8632
8633
|
"expectedDetail"
|
|
@@ -8645,6 +8646,15 @@
|
|
|
8645
8646
|
"type": "string",
|
|
8646
8647
|
"minLength": 1
|
|
8647
8648
|
},
|
|
8649
|
+
"plannedPaths": {
|
|
8650
|
+
"description": "Repository-relative paths this step may write, one per entry. `files` is prose for a human reader and cannot serve as a path ledger — it carries globs, exclusions and counts — so the implementer write policy reads this list instead.",
|
|
8651
|
+
"type": "array",
|
|
8652
|
+
"minItems": 1,
|
|
8653
|
+
"items": {
|
|
8654
|
+
"type": "string",
|
|
8655
|
+
"minLength": 1
|
|
8656
|
+
}
|
|
8657
|
+
},
|
|
8648
8658
|
"command": {
|
|
8649
8659
|
"type": "string",
|
|
8650
8660
|
"minLength": 1
|
|
@@ -17,14 +17,14 @@ Trigger phrases: "okstra history", "past runs", "run history", "re-run", "list t
|
|
|
17
17
|
- `--task-group <group>` → keep entries whose `taskGroup` matches.
|
|
18
18
|
3. Sort by `updatedAt` desc.
|
|
19
19
|
4. Page: default `--limit 20`. If truncated, add `... <N> more (pass --limit <N> to see all)`.
|
|
20
|
-
5. Extract: `taskKey`, `taskType`, `currentStatus`, `latestRunStatus`, `latestRunManifestPath`, `updatedAt`, `
|
|
20
|
+
5. Extract: `taskKey`, `taskType`, `currentStatus`, `latestRunStatus`, `latestRunManifestPath`, `updatedAt`, `latestReportRecordPath`, `latestResumeCommandPath`, `historyTimelinePath`.
|
|
21
21
|
|
|
22
22
|
```markdown
|
|
23
23
|
## okstra Task History — <project-id>
|
|
24
24
|
|
|
25
25
|
| # | Task Key | Type | currentStatus | latestRunStatus | Last Run | Report |
|
|
26
26
|
|---|----------|------|---------------|------------------|----------|--------|
|
|
27
|
-
| 1 | proj:group:id | error-analysis | completed | completed | 2026-04-05 22:59 | .project-docs/.../final-report-*.
|
|
27
|
+
| 1 | proj:group:id | error-analysis | completed | completed | 2026-04-05 22:59 | .project-docs/.../final-report-*.data.json |
|
|
28
28
|
| 2 | proj:group:id2 | final-verification | todo | error | 2026-04-04 15:30 | -- |
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -47,7 +47,7 @@ When a user selects a specific task:
|
|
|
47
47
|
|
|
48
48
|
| # | Timestamp | Type | Status | Report |
|
|
49
49
|
|---|-----------|------|--------|--------|
|
|
50
|
-
| 1 | 2026-04-05 22:59 | error-analysis | completed | .../final-report-*.
|
|
50
|
+
| 1 | 2026-04-05 22:59 | error-analysis | completed | .../final-report-*.data.json |
|
|
51
51
|
| 2 | 2026-04-04 15:30 | error-analysis | error | -- |
|
|
52
52
|
```
|
|
53
53
|
|
|
@@ -26,7 +26,7 @@ Parse the stdout JSON (`{taskKey, runCount, transitions[], latestPhaseStates}`)
|
|
|
26
26
|
|
|
27
27
|
`nextRecommendedPhase` in each entry is an **object** `{phase, status, rationale}`, not a phase-name string. Narrate it as `phase` when `phase` is non-empty, and as what `status` says otherwise — `pending` (that run did not settle the route), `blocked` (it stopped on something outside the run), `terminal` (the lifecycle ended there). Printing the object itself puts a raw dict in front of the reader.
|
|
28
28
|
|
|
29
|
-
For a run that has a `reportPath`, read that
|
|
29
|
+
For a run that has a `reportPath`, read that report record (`.data.json`) to enrich the summary only when the user wants a deeper one (do not read them all automatically).
|
|
30
30
|
|
|
31
31
|
### recap.3 — Free-form Q&A loop
|
|
32
32
|
|
|
@@ -14,20 +14,20 @@ task-key format: `<project-id>:<task-group>:<task-id>`.
|
|
|
14
14
|
|
|
15
15
|
Lookup methods (in priority order):
|
|
16
16
|
|
|
17
|
-
A. **`task-catalog.json` (fast):** read `.okstra/discovery/task-catalog.json`, match `taskKey` lowercase. `
|
|
17
|
+
A. **`task-catalog.json` (fast):** read `.okstra/discovery/task-catalog.json`, match `taskKey` lowercase. `latestReportRecordPath` is task-type-agnostic "most recent report".
|
|
18
18
|
|
|
19
|
-
B. **`task-manifest.json` (direct):** if catalog missing, slugify task-group / task-id, read `.okstra/tasks/<group-segment>/<task-id-segment>/task-manifest.json`, use `
|
|
19
|
+
B. **`task-manifest.json` (direct):** if catalog missing, slugify task-group / task-id, read `.okstra/tasks/<group-segment>/<task-id-segment>/task-manifest.json`, use `latestReportRecordPath` (task-type-agnostic).
|
|
20
20
|
|
|
21
21
|
C. **`timeline.json` (specific run):** for a specific date or run, read `.okstra/tasks/<group-segment>/<id-segment>/history/timeline.json`, filter `runs[]` by `runTimestamp` / `status` / `taskType`, use `runs[].reportPath`.
|
|
22
22
|
|
|
23
|
-
D. **Specific task-type (fallback):** `
|
|
23
|
+
D. **Specific task-type (fallback):** `latestReportRecordPath` is task-type-agnostic. For a specific task-type's latest report, look under `.okstra/tasks/<group-segment>/<id-segment>/runs/<task-type-segment>/reports/`, filename pattern `final-report-<task-type-segment>-<NNN>.data.json`. Highest seq is latest. Stage-isolated runs (`implementation`, single-stage `final-verification`) keep their reports one level deeper at `runs/<task-type-segment>/stage-<N>/reports/` — scan those subdirectories too; seq is independent per stage. Cross-verify with `timeline.json`'s `runs[].taskType` filter. If the user wants the full reading copy, render it with `okstra render-final-report <that data.json>`.
|
|
24
24
|
|
|
25
25
|
### report.2 — Confirm existence
|
|
26
26
|
|
|
27
|
-
1. Verify `
|
|
27
|
+
1. Verify `latestReportRecordPath` is non-empty AND the file exists on disk. Either signal indicates report presence (tolerant).
|
|
28
28
|
2. If present, display the path and ask the user whether to read it.
|
|
29
29
|
3. If absent, check `task-manifest.json` signals:
|
|
30
|
-
- `
|
|
30
|
+
- `latestReportRecordPath` empty/missing AND `currentStatus != completed` AND `workStatus != done` AND `workflow.nextRecommendedPhase.status != "terminal"` → `This task is not yet complete (currentStatus: <currentStatus>, workStatus: <workStatus>).`
|
|
31
31
|
- Any signal indicates completion but the file is missing → `Report file does not exist: <path>`
|
|
32
32
|
|
|
33
33
|
`workStatus` enum: `todo | in-progress | blocked | done`. `currentStatus`: `completed` / `contract-violated` etc. `"completed"` string does NOT exist in `workStatus` — do not confuse the two.
|
|
@@ -21,7 +21,7 @@ Read `.okstra/discovery/task-catalog.json`. The catalog is the authoritative sou
|
|
|
21
21
|
| `nextRecommendedPhase` | next-phase pointer — an **object** `{phase, status, rationale}`, never a string. `status` is `ready` / `pending` / `blocked` / `terminal`. The lead writes `phase` only under `ready`, but that is an authoring rule, not a constraint the struct enforces — `prepare` lowers a `ready` pointer to `pending` and keeps its `phase`, so a non-`ready` pointer that still names a phase is a normal state to read. Decide launchability from `status` alone. Render `phase` (or `--` when it is empty) — never the object itself. |
|
|
22
22
|
| `awaitingApproval` | whether awaiting approval |
|
|
23
23
|
| `latestRunStatus` | latest run status |
|
|
24
|
-
| `
|
|
24
|
+
| `latestReportRecordPath` | latest report path |
|
|
25
25
|
| `latestResumeCommandPath` | latest resume command |
|
|
26
26
|
| `workStatus` | user-managed work status (todo / in-progress / blocked / done; default in-progress) |
|
|
27
27
|
| `updatedAt` | last update time |
|
|
@@ -51,7 +51,7 @@ Given a specific `task-key` or `task-group + task-id`:
|
|
|
51
51
|
2. If necessary, read `.okstra/tasks/<task-group>/<task-id>/task-manifest.json` directly.
|
|
52
52
|
3. If you need the latest run information, read `history/timeline.json` along with the latest run manifest.
|
|
53
53
|
|
|
54
|
-
Required fields: `taskKey`, `taskType`, `workCategory`, `currentStatus`, `latestRunStatus`, `workflow.{currentPhase, currentPhaseState, phaseStates, lastCompletedPhase, nextRecommendedPhase, awaitingApproval, lastSafeCheckpoint}`, `workStatus`, `workStatusUpdatedAt`, `workStatusNote`, `
|
|
54
|
+
Required fields: `taskKey`, `taskType`, `workCategory`, `currentStatus`, `latestRunStatus`, `workflow.{currentPhase, currentPhaseState, phaseStates, lastCompletedPhase, nextRecommendedPhase, awaitingApproval, lastSafeCheckpoint}`, `workStatus`, `workStatusUpdatedAt`, `workStatusNote`, `latestReportRecordPath`, `latestResumeCommandPath`, `historyTimelinePath`.
|
|
55
55
|
|
|
56
56
|
`workflow.nextRecommendedPhase` is the `{phase, status, rationale}` object described in `status.1` — read its three fields, do not print it whole. A `task-manifest.json` written before the object existed still holds a bare string there; `scripts/okstra_ctl/next_phase.py::promote` is what turns that string into the object, and every CLI-projected surface you can read instead (`task-catalog.json`, `okstra rollup`, `okstra recap`) has already applied it. Prefer those over a raw manifest rather than promoting a legacy string yourself.
|
|
57
57
|
|
|
@@ -112,7 +112,7 @@ okstra's own artifact identifiers are out of the allowlist — drop them:
|
|
|
112
112
|
|
|
113
113
|
- report item ids — `F-001`, `C-001`, `R-001`, `D-0001`, `PREP-001`
|
|
114
114
|
- run artifact names and their `<task-type>-<seq>` suffixes —
|
|
115
|
-
`final-report-implementation-3.
|
|
115
|
+
`final-report-implementation-3.data.json`, `run-manifest-implementation-3.json`
|
|
116
116
|
- phase / stage / worker labels — `final-verification`, `stage-2`, `codex-worker`
|
|
117
117
|
- any path under `.okstra/` — reports, decisions, glossary, user-response sidecars
|
|
118
118
|
|
|
@@ -384,7 +384,7 @@ Do not read the wizard state file directly. `okstra wizard outcome` exposes any
|
|
|
384
384
|
| `No module named okstra_ctl.wizard` | Install predates wizard module | `npx okstra@latest install` to refresh. |
|
|
385
385
|
| `wizard step` returns `ok: false` repeatedly | User keeps giving invalid answers | Echo the error verbatim and re-prompt the same step — do not advance. |
|
|
386
386
|
| `task root not found for <key>` | catalog entry stale or task-key typo | Restart the wizard (`okstra wizard init`) to refresh the pick list. |
|
|
387
|
-
| `approved plan is not yet approved (frontmatter ...)` | `implementation` without proper approval | Ask the user to
|
|
387
|
+
| `approved plan is not yet approved (frontmatter ...)` | `implementation` without proper approval | Ask the user to re-run with `--approve` or confirm approval in the in-session wizard, or pick a different task-type. Editing the full reading copy does not approve the plan. |
|
|
388
388
|
|
|
389
389
|
## Output Rules
|
|
390
390
|
|
|
@@ -96,7 +96,7 @@ If 0 tasks remain, output `All tasks in this task-group are done. There is no sc
|
|
|
96
96
|
|
|
97
97
|
### Step 3: Per-task stage extraction (Stage Map source)
|
|
98
98
|
|
|
99
|
-
For each in-scope task, the **authoritative source is its `implementation-planning` Stage Map resolved by the CLI**. Do not hand-parse it or use `
|
|
99
|
+
For each in-scope task, the **authoritative source is its `implementation-planning` Stage Map resolved by the CLI**. Do not hand-parse it or use `latestReportRecordPath` to choose its source. Call:
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
102
|
okstra stage-map <task-key> --json
|
|
@@ -75,7 +75,7 @@ Returns `{reportPath, rows: [{id, kind, blocks, status, statement, expectedForm,
|
|
|
75
75
|
|
|
76
76
|
Each entry of `options[]` is `{role, answer, rationale, scopeImpact, addedWork, directionChange, disposition}`. `role` is `recommended` for exactly one entry and `alternative` for the rest; `scopeImpact` is a token list drawn from `in-repo` / `cross-repo` / `new-schema` / `deferrable`; `addedWork` and `directionChange` are one sentence each. Activity-contract v1 approval options also carry the gate action in `disposition`. A legacy schema-v1 report has nowhere to record impact or a gate action, so those fields arrive empty — the CLI reconstructs only the answers from the report's `Expected form` cell.
|
|
77
77
|
|
|
78
|
-
`resolvedRefs` is `[{ref, definition}]` — the CLI has already looked up what each
|
|
78
|
+
`resolvedRefs` is `[{ref, definition}]` — the CLI has already looked up what each record coordinate (`RB-002`, `FU-001`) means in the report record; `definition` is `null` when the record has no such row, when the citation is a section number (`§4.7`, leftover from an older report), or when the citation is a `path:line` pointer.
|
|
79
79
|
|
|
80
80
|
This call is a **data fetch, not a presentation step**. Do not print `rows` at the user, and do not paste a raw `statement` as a question — a `statement` like `Rewrite RB-002 rollback (see §4.7)` is meaningless on its own, which is the whole reason this skill exists. Announce only the count and the plan:
|
|
81
81
|
|
|
@@ -100,7 +100,7 @@ Walk the open rows in report order. **One item per `AskUserQuestion` call** —
|
|
|
100
100
|
Where the background comes from — in this order:
|
|
101
101
|
|
|
102
102
|
- `resolvedRefs[].definition`, which is already resolved and needs no file read;
|
|
103
|
-
- when a `definition` is `null`, or the three blocks would otherwise be empty, **Read** the report
|
|
103
|
+
- when a `definition` is `null`, or the three blocks would otherwise be empty, **Read** the report record (the `.data.json` at `reportPath`) and take the cited row's `statement` / `summary` / `item` from there. Do not open the full reading copy to chase a `§` number — that number is not a record coordinate;
|
|
104
104
|
- **never invent it.** If the report genuinely does not say why, write `The report states nothing beyond the statement itself.` and go straight to the question. Fabricated background corrupts the answer it collects.
|
|
105
105
|
|
|
106
106
|
Close the background with the raw source on one line, so the mapping back to the report stays visible:
|
|
@@ -133,7 +133,7 @@ Record one entry — `{id, kind, value, rationale?, disposition}`, `kind` copied
|
|
|
133
133
|
|
|
134
134
|
A reframe is not an answer, so it does not satisfy the approval gate.
|
|
135
135
|
|
|
136
|
-
If the user replies with a question instead of an answer ("what does this mean?"), record nothing: **Read** the
|
|
136
|
+
If the user replies with a question instead of an answer ("what does this mean?"), record nothing: **Read** the report record for the cited row id from `contextRefs[]`, explain it in plain language, and re-ask the same item with the same options. Explain only — **do not resolve it for them**; the decision goes back to the user.
|
|
137
137
|
|
|
138
138
|
Echo one line per finished item (`[2/5] C-014 → answer: 60s`), then ask the next one. Do not summarize the whole set until Step 4.
|
|
139
139
|
|
|
@@ -24,7 +24,7 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
24
24
|
- Worker Roster: `{{RECOMMENDED_ANALYSERS}}`
|
|
25
25
|
- Lead Model: `{{LEAD_MODEL}}`
|
|
26
26
|
- Latest Run: `{{LATEST_RUN_RELATIVE_PATH}}`
|
|
27
|
-
- Latest Report: `{{
|
|
27
|
+
- Latest Report: `{{LATEST_REPORT_RECORD_RELATIVE_PATH}}`
|
|
28
28
|
- Team State: `{{TEAM_STATE_RELATIVE_PATH}}`
|
|
29
29
|
- Validation Status: `{{VALIDATION_STATUS}}`
|
|
30
30
|
- Resume Command: `{{CLAUDE_RESUME_COMMAND_RELATIVE_PATH}}`
|
|
@@ -14,7 +14,7 @@ Write `- PROGRESS: <stage> <ISO-8601-UTC>` there before reading and at least eve
|
|
|
14
14
|
|
|
15
15
|
- Author the data.json at `**Result Path:**` and the audit file at `**Audit sidecar path:**`.
|
|
16
16
|
- Follow the task-type schema excerpt and Phase 6 report-writer contract. Do not perform independent analysis, edit source code, or load implementation coding-preflight resources.
|
|
17
|
-
-
|
|
17
|
+
- Do not invoke `okstra render-final-report`. The full reading copy is rendered on demand.
|
|
18
18
|
- Preserve source item IDs, convergence classifications, round history, and unresolved dissent; do not recompute them from intuition.
|
|
19
19
|
- Every `clarificationItems[]` row you leave `status: open` with `blocks: approval` carries `origin` and `userConfirmation`. `origin` is `worker-finding` only when an analyser or verifier reached it on its own evidence — when the lead's dispatch prompt told you to raise it, it is `lead-directed`, whatever the workers then agreed. `userConfirmation` is what the lead reports having done about it (`asked-and-answered` / `asked-awaiting` / `deferred-no-interactive-session`); if the dispatch does not say, write `asked-awaiting` and name the gap in your return message rather than guessing. An open approval blocker stops the whole task, so a row that misreports where it came from costs a re-run. **Enforced:** `validators/validate-run.py` `_validate_open_approval_blocker_provenance`.
|
|
20
20
|
|
|
@@ -15,10 +15,12 @@ Bash tool_use 명령을 phase deny-list 와 대조해 위반을 기계적으로
|
|
|
15
15
|
한계 1: claude-code 세션 jsonl 만 스캔한다. codex/antigravity lead·worker 는 동일 형식의
|
|
16
16
|
tool_use transcript 를 ~/.claude/projects 에 남기지 않으므로 이 스캐너의 사정권 밖이며,
|
|
17
17
|
해당 런타임의 금지 행위는 여전히 수동 감사에 의존한다.
|
|
18
|
-
한계 2: deny-list 는 heredoc
|
|
19
|
-
검색하므로, 금지 토큰이 인자/메시지에
|
|
20
|
-
publish ..."`)는 여전히 위반으로 잡힐 수
|
|
21
|
-
head 앵커링은 불가하므로 이 잔여 오탐은
|
|
18
|
+
한계 2: deny-list 는 heredoc 본문과 검색 명령 절을 걷어낸(strip_heredoc_bodies,
|
|
19
|
+
strip_search_clauses) 명령 문자열 전체를 검색하므로, 금지 토큰이 인자/메시지에
|
|
20
|
+
등장하는 경우(예: `git commit -m "... npm publish ..."`)는 여전히 위반으로 잡힐 수
|
|
21
|
+
있다. `cd <path> && <cmd>` 형태 때문에 명령 head 앵커링은 불가하므로 이 잔여 오탐은
|
|
22
|
+
감수한다. 검색 절을 예외로 둔 것은 그 오탐이 우연이 아니라 계약이 시키는 감사에서
|
|
23
|
+
반드시 발생하기 때문이다.
|
|
22
24
|
"""
|
|
23
25
|
from __future__ import annotations
|
|
24
26
|
|
|
@@ -86,6 +88,75 @@ def strip_heredoc_bodies(command: str) -> str:
|
|
|
86
88
|
return "\n".join(kept)
|
|
87
89
|
|
|
88
90
|
|
|
91
|
+
_SEARCH_HEADS = frozenset({"grep", "egrep", "fgrep", "rg", "ag", "ack"})
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _shell_clauses(command: str) -> list[str]:
|
|
95
|
+
"""쉘 구분자로 자른 절 목록. 인용 안의 구분자는 자르지 않는다.
|
|
96
|
+
|
|
97
|
+
검색 패턴은 거의 언제나 `'git push|npm publish'` 처럼 인용된 교대(alternation)
|
|
98
|
+
이므로, 인용을 무시하고 자르면 패턴이 절 경계로 쪼개져 뒷조각이 그대로 명령처럼
|
|
99
|
+
남는다.
|
|
100
|
+
"""
|
|
101
|
+
clauses: list[str] = []
|
|
102
|
+
buffer: list[str] = []
|
|
103
|
+
quote = ""
|
|
104
|
+
index = 0
|
|
105
|
+
while index < len(command):
|
|
106
|
+
char = command[index]
|
|
107
|
+
if quote:
|
|
108
|
+
buffer.append(char)
|
|
109
|
+
if char == quote:
|
|
110
|
+
quote = ""
|
|
111
|
+
index += 1
|
|
112
|
+
continue
|
|
113
|
+
if char in "'\"":
|
|
114
|
+
quote = char
|
|
115
|
+
buffer.append(char)
|
|
116
|
+
index += 1
|
|
117
|
+
continue
|
|
118
|
+
if command.startswith(("&&", "||"), index):
|
|
119
|
+
clauses.append("".join(buffer))
|
|
120
|
+
buffer = []
|
|
121
|
+
index += 2
|
|
122
|
+
continue
|
|
123
|
+
if char in ";|\n":
|
|
124
|
+
clauses.append("".join(buffer))
|
|
125
|
+
buffer = []
|
|
126
|
+
index += 1
|
|
127
|
+
continue
|
|
128
|
+
buffer.append(char)
|
|
129
|
+
index += 1
|
|
130
|
+
clauses.append("".join(buffer))
|
|
131
|
+
return clauses
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def strip_search_clauses(command: str) -> str:
|
|
135
|
+
"""검색 명령 절을 걷어낸 명령 문자열.
|
|
136
|
+
|
|
137
|
+
`grep -E 'git push|npm publish' <logs>` 의 패턴은 실행되는 명령이 아니라
|
|
138
|
+
**찾는 대상**이다. 그리고 그 감사는 우연이 아니라 계약이 시킨다 —
|
|
139
|
+
`_implementation-deliverable.md` 의 자체 리뷰 항목이 리드에게 세션
|
|
140
|
+
트랜스크립트에서 배포 명령을 스캔하라고 요구한다. 걷어내지 않으면 계약을
|
|
141
|
+
이행한 런이 매번 그 이행 때문에 실패했다.
|
|
142
|
+
|
|
143
|
+
절 단위로 자르므로 `cd <path> && grep …` 에서도 grep 절만 빠지고, 같은 줄
|
|
144
|
+
뒤에 이어지는 실제 명령은 계속 스캔된다.
|
|
145
|
+
"""
|
|
146
|
+
kept: list[str] = []
|
|
147
|
+
for clause in _shell_clauses(command):
|
|
148
|
+
tokens = clause.split()
|
|
149
|
+
if not tokens:
|
|
150
|
+
continue
|
|
151
|
+
head = tokens[0].rsplit("/", 1)[-1]
|
|
152
|
+
if head in _SEARCH_HEADS:
|
|
153
|
+
continue
|
|
154
|
+
if head == "git" and len(tokens) > 1 and tokens[1] == "grep":
|
|
155
|
+
continue
|
|
156
|
+
kept.append(clause)
|
|
157
|
+
return "\n".join(kept)
|
|
158
|
+
|
|
159
|
+
|
|
89
160
|
def forbidden_patterns_for(task_type: str) -> list[tuple[str, re.Pattern]]:
|
|
90
161
|
"""task_type(=phase profile) 의 deny-list. 모든 phase 가 _UNIVERSAL 을 받고,
|
|
91
162
|
release-handoff 를 제외한 모든 phase 는 bare `git push` 도 금지한다
|
|
@@ -142,7 +213,7 @@ def _scan_one(
|
|
|
142
213
|
if block.get("type") != "tool_use" or block.get("name") != "Bash":
|
|
143
214
|
continue
|
|
144
215
|
command = (block.get("input") or {}).get("command") or ""
|
|
145
|
-
executed = strip_heredoc_bodies(command)
|
|
216
|
+
executed = strip_search_clauses(strip_heredoc_bodies(command))
|
|
146
217
|
for label, pattern in patterns:
|
|
147
218
|
if pattern.search(executed):
|
|
148
219
|
hits.append((label, command))
|
|
@@ -197,7 +197,7 @@ run_manifest_path = project_root / latest_run["runManifestPath"]
|
|
|
197
197
|
run_manifest = load_json(run_manifest_path)
|
|
198
198
|
team_state_path = project_root / run_manifest["teamStatePath"]
|
|
199
199
|
team_state = load_json(team_state_path)
|
|
200
|
-
report_path = project_root / run_manifest["
|
|
200
|
+
report_path = project_root / run_manifest["expectedReportRecordPath"]
|
|
201
201
|
final_status_path = project_root / run_manifest["expectedStatusPath"]
|
|
202
202
|
|
|
203
203
|
status_by_worker_id = {
|
|
@@ -412,7 +412,17 @@ report_lines.extend(
|
|
|
412
412
|
]
|
|
413
413
|
)
|
|
414
414
|
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
415
|
-
report_path.
|
|
415
|
+
_report_name = report_path.name
|
|
416
|
+
if _report_name.endswith(".data.json"):
|
|
417
|
+
data_path = report_path
|
|
418
|
+
md_path = report_path.with_name(_report_name[: -len(".data.json")] + ".md")
|
|
419
|
+
elif _report_name.endswith(".md"):
|
|
420
|
+
md_path = report_path
|
|
421
|
+
data_path = report_path.with_name(_report_name[: -len(".md")] + ".data.json")
|
|
422
|
+
else:
|
|
423
|
+
md_path = report_path
|
|
424
|
+
data_path = report_path.with_suffix(".data.json")
|
|
425
|
+
md_path.write_text("\n".join(report_lines) + "\n")
|
|
416
426
|
|
|
417
427
|
# Phase 7 step 1.5 (BLOCKING) — render the html sibling artifact next
|
|
418
428
|
# to the final-report so validate-run.py's report-views hook passes.
|
|
@@ -460,12 +470,6 @@ if WORKSPACE_ROOT:
|
|
|
460
470
|
{**_rows[min(i, len(_rows) - 1)], "role": role}
|
|
461
471
|
for i, role in enumerate(_required)
|
|
462
472
|
]
|
|
463
|
-
name = report_path.name
|
|
464
|
-
data_path = (
|
|
465
|
-
report_path.with_name(name[:-3] + ".data.json")
|
|
466
|
-
if name.endswith(".md")
|
|
467
|
-
else report_path.with_suffix(".data.json")
|
|
468
|
-
)
|
|
469
473
|
data_path.write_text(
|
|
470
474
|
json.dumps(sample, indent=2, ensure_ascii=False) + "\n",
|
|
471
475
|
encoding="utf-8",
|
|
@@ -481,7 +485,7 @@ if WORKSPACE_ROOT:
|
|
|
481
485
|
try:
|
|
482
486
|
from okstra_ctl.render_final_report import render_to_file
|
|
483
487
|
|
|
484
|
-
render_to_file(data_path,
|
|
488
|
+
render_to_file(data_path, md_path)
|
|
485
489
|
except Exception as exc: # pragma: no cover — fixture path only
|
|
486
490
|
raise SystemExit(f"failed to render final report in fixture: {exc}")
|
|
487
491
|
# Go through the same CLI the run uses, so the fixture picks the schema's
|
|
@@ -491,7 +495,7 @@ if WORKSPACE_ROOT:
|
|
|
491
495
|
[
|
|
492
496
|
_sys.executable,
|
|
493
497
|
str(Path(WORKSPACE_ROOT) / "scripts" / "okstra-render-report-views.py"),
|
|
494
|
-
str(
|
|
498
|
+
str(data_path),
|
|
495
499
|
"--task-key", str(task_manifest.get("taskKey", "validation/fixture")),
|
|
496
500
|
"--task-type", str(task_manifest.get("taskType", "validation")),
|
|
497
501
|
"--seq", "001",
|
|
@@ -70,7 +70,7 @@ if latest_run is None:
|
|
|
70
70
|
run_manifest_path = project_root / latest_run["runManifestPath"]
|
|
71
71
|
run_manifest = json.loads(run_manifest_path.read_text())
|
|
72
72
|
team_state_path = project_root / run_manifest["teamStatePath"]
|
|
73
|
-
report_path = project_root / run_manifest["
|
|
73
|
+
report_path = project_root / run_manifest["expectedReportRecordPath"]
|
|
74
74
|
final_status_path = project_root / run_manifest["expectedStatusPath"]
|
|
75
75
|
|
|
76
76
|
process = subprocess.run(
|
|
@@ -523,6 +523,9 @@ def _check_data_step_counts(
|
|
|
523
523
|
errs.append(ValidationError("S4", meta.stage_number,
|
|
524
524
|
f"stageMap stepCount {meta.step_count} != "
|
|
525
525
|
f"{actual} stepwiseExecution rows"))
|
|
526
|
+
if actual > HARD_STEP_CAP:
|
|
527
|
+
errs.append(ValidationError("S5", meta.stage_number,
|
|
528
|
+
f"effective step count {actual} exceeds {HARD_STEP_CAP}"))
|
|
526
529
|
for number in sorted(n for n in by_number if n not in {m.stage_number for m in stage_map}):
|
|
527
530
|
errs.append(ValidationError("S3", number,
|
|
528
531
|
"stages[] entry has no matching stageMap row"))
|
|
@@ -188,7 +188,7 @@ def validate(report_path: Path) -> list[str]:
|
|
|
188
188
|
"""Validate the html view of the report named by *report_path*.
|
|
189
189
|
|
|
190
190
|
A schema-v2 report is checked against its data.json and the rendered html;
|
|
191
|
-
the
|
|
191
|
+
the full reading copy markdown is a sibling rendering of the same record and is
|
|
192
192
|
never read here, so it does not have to exist. Schema-v1 reports keep the
|
|
193
193
|
markdown as their source and still require it.
|
|
194
194
|
"""
|