okstra 0.127.0 → 0.129.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/storage-model.md +23 -3
- package/docs/architecture.md +28 -1
- package/docs/cli.md +9 -0
- package/docs/project-structure-overview.md +17 -7
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +3 -3
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/codex-worker.md +3 -3
- package/runtime/agents/workers/report-writer-worker.md +5 -5
- package/runtime/prompts/lead/adapters/claude-code.md +2 -1
- package/runtime/prompts/lead/adapters/codex.md +1 -0
- package/runtime/prompts/lead/adapters/external.md +1 -0
- package/runtime/prompts/lead/convergence.md +45 -83
- package/runtime/prompts/lead/okstra-lead-contract.md +12 -8
- package/runtime/prompts/lead/report-writer.md +3 -2
- package/runtime/prompts/lead/team-contract.md +19 -9
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +10 -6
- package/runtime/prompts/profiles/implementation-planning.md +5 -0
- package/runtime/prompts/profiles/improvement-discovery.md +11 -1
- package/runtime/prompts/profiles/requirements-discovery.md +8 -1
- package/runtime/python/okstra_ctl/analysis_packet.py +14 -15
- package/runtime/python/okstra_ctl/codex_dispatch.py +51 -80
- package/runtime/python/okstra_ctl/context_cost.py +82 -7
- package/runtime/python/okstra_ctl/convergence.py +223 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +1152 -0
- package/runtime/python/okstra_ctl/convergence_migration.py +184 -0
- package/runtime/python/okstra_ctl/convergence_store.py +85 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +60 -1
- package/runtime/python/okstra_ctl/improvement_assignment.py +61 -0
- package/runtime/python/okstra_ctl/log_report.py +44 -5
- package/runtime/python/okstra_ctl/path_hints.py +28 -1
- package/runtime/python/okstra_ctl/paths.py +10 -1
- package/runtime/python/okstra_ctl/render.py +78 -11
- package/runtime/python/okstra_ctl/run.py +66 -2
- package/runtime/python/okstra_ctl/wizard.py +15 -4
- package/runtime/python/okstra_ctl/work_categories.py +37 -0
- package/runtime/python/okstra_ctl/worker_prompt_body.py +107 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +316 -0
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +109 -10
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +158 -0
- package/runtime/templates/implementation-worker-preamble.md +51 -0
- package/runtime/templates/operating-standard.md +4 -4
- package/runtime/templates/report-writer-prompt-preamble.md +37 -0
- package/runtime/templates/worker-error-contract.md +46 -0
- package/runtime/templates/worker-prompt-preamble.md +28 -227
- package/runtime/validators/lib/fixtures.sh +27 -12
- package/runtime/validators/validate-run.py +228 -45
- package/src/cli-registry.mjs +7 -0
- package/src/commands/execute/convergence.mjs +34 -0
- package/src/commands/inspect/log-report.mjs +5 -3
|
@@ -46,6 +46,9 @@ from okstra_ctl.md_table import ( # noqa: E402
|
|
|
46
46
|
split_pipe_row as _split_pipe_row,
|
|
47
47
|
)
|
|
48
48
|
from okstra_ctl.final_report_paths import final_report_data_path as _data_path_for # noqa: E402
|
|
49
|
+
from okstra_ctl.improvement_assignment import ( # noqa: E402
|
|
50
|
+
validate_primary_lens_assignments,
|
|
51
|
+
)
|
|
49
52
|
from okstra_ctl.design_prep import ( # noqa: E402
|
|
50
53
|
DesignPrepError,
|
|
51
54
|
_planning_seq as _design_prep_planning_seq,
|
|
@@ -58,6 +61,11 @@ from okstra_ctl.design_surfaces import ( # noqa: E402
|
|
|
58
61
|
detect_design_surfaces,
|
|
59
62
|
expected_prep_plan_item_id,
|
|
60
63
|
)
|
|
64
|
+
from okstra_ctl.worker_prompt_contract import ( # noqa: E402
|
|
65
|
+
PromptRecord,
|
|
66
|
+
validate_initial_prompt_records,
|
|
67
|
+
)
|
|
68
|
+
from okstra_ctl.convergence_engine import validate_final_state # noqa: E402
|
|
61
69
|
|
|
62
70
|
TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
|
|
63
71
|
ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
|
|
@@ -367,6 +375,201 @@ def effective_run_task_type(run_manifest: dict, task_manifest: dict) -> str:
|
|
|
367
375
|
).strip()
|
|
368
376
|
|
|
369
377
|
|
|
378
|
+
def _resolve_prompt_record_path(project_root: Path, prompt_value: str) -> Path:
|
|
379
|
+
prompt_path = Path(prompt_value)
|
|
380
|
+
return prompt_path if prompt_path.is_absolute() else project_root / prompt_path
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _fallback_dispatch_kind(prompt_value: str) -> str:
|
|
384
|
+
match = re.search(r"-reverify-(r\d+)", Path(prompt_value).name)
|
|
385
|
+
return f"reverify-{match.group(1)}" if match else "initial"
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _persisted_prompt_records(
|
|
389
|
+
*,
|
|
390
|
+
project_root: Path,
|
|
391
|
+
selected_ids: set[str],
|
|
392
|
+
team_state: dict,
|
|
393
|
+
) -> list[PromptRecord]:
|
|
394
|
+
dispatches = team_state.get("workerDispatches")
|
|
395
|
+
source = dispatches if isinstance(dispatches, list) and dispatches else (
|
|
396
|
+
team_state.get("workers") or []
|
|
397
|
+
)
|
|
398
|
+
records: list[PromptRecord] = []
|
|
399
|
+
seen: set[tuple[str, str, str]] = set()
|
|
400
|
+
for worker in source:
|
|
401
|
+
if not isinstance(worker, dict):
|
|
402
|
+
continue
|
|
403
|
+
worker_id = str(worker.get("workerId") or "").strip()
|
|
404
|
+
prompt_value = str(worker.get("promptPath") or "").strip()
|
|
405
|
+
if worker_id not in selected_ids or not prompt_value:
|
|
406
|
+
continue
|
|
407
|
+
dispatch_kind = str(worker.get("kind") or "").strip()
|
|
408
|
+
if not dispatch_kind:
|
|
409
|
+
dispatch_kind = _fallback_dispatch_kind(prompt_value)
|
|
410
|
+
identity = (worker_id, dispatch_kind, prompt_value)
|
|
411
|
+
if identity in seen:
|
|
412
|
+
continue
|
|
413
|
+
seen.add(identity)
|
|
414
|
+
records.append(
|
|
415
|
+
PromptRecord(
|
|
416
|
+
worker_id=worker_id,
|
|
417
|
+
dispatch_kind=dispatch_kind,
|
|
418
|
+
path=_resolve_prompt_record_path(project_root, prompt_value),
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
return records
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _markdown_section(text: str, heading: str) -> str:
|
|
425
|
+
pattern = rf"(?ms)^##\s+{re.escape(heading)}\s*$\n(.*?)(?=^##\s|\Z)"
|
|
426
|
+
match = re.search(pattern, text)
|
|
427
|
+
return match.group(1) if match else ""
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _parse_resolved_lenses(grilling_text: str) -> list[str]:
|
|
431
|
+
section = _markdown_section(grilling_text, "Resolved lenses")
|
|
432
|
+
return [
|
|
433
|
+
match.group(1).strip().strip("`")
|
|
434
|
+
for line in section.splitlines()
|
|
435
|
+
if (match := re.match(r"^\s*-\s+(.+?)\s*$", line))
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _parse_primary_assignments(
|
|
440
|
+
grilling_text: str,
|
|
441
|
+
) -> tuple[dict[str, str], list[str]]:
|
|
442
|
+
section = _markdown_section(grilling_text, "Primary Pass Assignments")
|
|
443
|
+
rows = [
|
|
444
|
+
(line.strip(), _split_pipe_row(line.strip()))
|
|
445
|
+
for line in section.splitlines()
|
|
446
|
+
if line.strip().startswith("|") and line.strip().endswith("|")
|
|
447
|
+
]
|
|
448
|
+
errors: list[str] = []
|
|
449
|
+
if not rows or rows[0][1] != ["Worker ID", "Primary lens"]:
|
|
450
|
+
return {}, [
|
|
451
|
+
"Primary Pass Assignments table must start with "
|
|
452
|
+
"`| Worker ID | Primary lens |`"
|
|
453
|
+
]
|
|
454
|
+
assignments: dict[str, str] = {}
|
|
455
|
+
for raw_line, row in rows[1:]:
|
|
456
|
+
if _is_markdown_separator(raw_line):
|
|
457
|
+
continue
|
|
458
|
+
if len(row) != 2 or not all(cell.strip() for cell in row):
|
|
459
|
+
errors.append("Primary Pass Assignments rows must contain two values")
|
|
460
|
+
continue
|
|
461
|
+
worker_id, lens = (cell.strip().strip("`") for cell in row)
|
|
462
|
+
if worker_id in assignments:
|
|
463
|
+
errors.append(f"duplicate worker assignment row: {worker_id}")
|
|
464
|
+
continue
|
|
465
|
+
assignments[worker_id] = lens
|
|
466
|
+
return assignments, errors
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _grilling_log_path_from_prompts(
|
|
470
|
+
*,
|
|
471
|
+
project_root: Path,
|
|
472
|
+
worker_ids: list[str],
|
|
473
|
+
records: list[PromptRecord],
|
|
474
|
+
) -> tuple[Path | None, list[str]]:
|
|
475
|
+
header = "**Phase 1.5 Grilling Log:**"
|
|
476
|
+
values: set[str] = set()
|
|
477
|
+
errors: list[str] = []
|
|
478
|
+
for record in records:
|
|
479
|
+
if record.worker_id not in worker_ids or record.dispatch_kind != "initial":
|
|
480
|
+
continue
|
|
481
|
+
try:
|
|
482
|
+
text = record.path.read_text(encoding="utf-8")
|
|
483
|
+
except OSError:
|
|
484
|
+
continue
|
|
485
|
+
matches = [
|
|
486
|
+
line.strip()[len(header):].strip().strip("`")
|
|
487
|
+
for line in text.splitlines()
|
|
488
|
+
if line.strip().startswith(header)
|
|
489
|
+
]
|
|
490
|
+
if len(matches) == 1 and matches[0]:
|
|
491
|
+
values.add(matches[0])
|
|
492
|
+
if not values:
|
|
493
|
+
return None, ["initial analyser prompts have no grilling-log path"]
|
|
494
|
+
if len(values) != 1:
|
|
495
|
+
return None, ["initial analyser prompts reference different grilling logs"]
|
|
496
|
+
return _resolve_prompt_record_path(project_root, values.pop()), errors
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _validate_improvement_primary_assignments(
|
|
500
|
+
*,
|
|
501
|
+
project_root: Path,
|
|
502
|
+
worker_ids: list[str],
|
|
503
|
+
records: list[PromptRecord],
|
|
504
|
+
) -> list[str]:
|
|
505
|
+
grilling_path, errors = _grilling_log_path_from_prompts(
|
|
506
|
+
project_root=project_root,
|
|
507
|
+
worker_ids=worker_ids,
|
|
508
|
+
records=records,
|
|
509
|
+
)
|
|
510
|
+
if grilling_path is None:
|
|
511
|
+
return errors
|
|
512
|
+
try:
|
|
513
|
+
grilling_text = grilling_path.read_text(encoding="utf-8")
|
|
514
|
+
except OSError as exc:
|
|
515
|
+
return [*errors, f"cannot read grilling log {grilling_path}: {exc}"]
|
|
516
|
+
lenses = _parse_resolved_lenses(grilling_text)
|
|
517
|
+
assignments, parse_errors = _parse_primary_assignments(grilling_text)
|
|
518
|
+
return [
|
|
519
|
+
*errors,
|
|
520
|
+
*parse_errors,
|
|
521
|
+
*validate_primary_lens_assignments(assignments, worker_ids, lenses),
|
|
522
|
+
]
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _validate_initial_analysis_prompts(
|
|
526
|
+
data: dict,
|
|
527
|
+
failures: list[str],
|
|
528
|
+
) -> None:
|
|
529
|
+
"""Validate persisted prompts using their functional audience contract."""
|
|
530
|
+
task_type = str(data.get("taskType") or "").strip()
|
|
531
|
+
if not task_type:
|
|
532
|
+
return
|
|
533
|
+
run_manifest = data.get("runManifest") or {}
|
|
534
|
+
team_contract = run_manifest.get("teamContract") or {}
|
|
535
|
+
selected_workers = team_contract.get("requiredWorkerRoles") or []
|
|
536
|
+
selected_worker_ids = [
|
|
537
|
+
str(worker.get("workerId") or "").strip()
|
|
538
|
+
for worker in selected_workers
|
|
539
|
+
if isinstance(worker, dict)
|
|
540
|
+
and str(worker.get("workerId") or "").strip()
|
|
541
|
+
]
|
|
542
|
+
selected_ids = set(selected_worker_ids)
|
|
543
|
+
project_root = Path(data["projectRoot"])
|
|
544
|
+
team_state = data.get("teamState") or {}
|
|
545
|
+
manifest = dict(run_manifest)
|
|
546
|
+
manifest["taskType"] = task_type
|
|
547
|
+
records = _persisted_prompt_records(
|
|
548
|
+
project_root=project_root,
|
|
549
|
+
selected_ids=selected_ids,
|
|
550
|
+
team_state=team_state,
|
|
551
|
+
)
|
|
552
|
+
errors = validate_initial_prompt_records(
|
|
553
|
+
manifest=manifest,
|
|
554
|
+
records=records,
|
|
555
|
+
)
|
|
556
|
+
if task_type == "improvement-discovery":
|
|
557
|
+
errors.extend(
|
|
558
|
+
_validate_improvement_primary_assignments(
|
|
559
|
+
project_root=project_root,
|
|
560
|
+
worker_ids=[
|
|
561
|
+
worker_id
|
|
562
|
+
for worker_id in selected_worker_ids
|
|
563
|
+
if worker_id != "report-writer"
|
|
564
|
+
],
|
|
565
|
+
records=records,
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
failures.extend(
|
|
569
|
+
f"{task_type} prompt contract: {error}" for error in errors
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
|
|
370
573
|
def _is_legal_concurrent_run_skip(
|
|
371
574
|
team_create: object, concurrent_run_authorized: bool
|
|
372
575
|
) -> bool:
|
|
@@ -3090,63 +3293,34 @@ def _validate_forbidden_actions(
|
|
|
3090
3293
|
failures.extend(f"forbidden-action: {v}" for v in violations)
|
|
3091
3294
|
|
|
3092
3295
|
|
|
3093
|
-
|
|
3094
|
-
"
|
|
3095
|
-
"
|
|
3096
|
-
"
|
|
3097
|
-
|
|
3098
|
-
}
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
def classification_counts_from_findings(findings) -> dict:
|
|
3102
|
-
"""Recompute `finalClassificationCounts` from `findings[].classification`.
|
|
3103
|
-
|
|
3104
|
-
Shared with `tests/contract/test_convergence_state_contract.py` so the
|
|
3105
|
-
invariant has one definition — it used to live only in that test, which
|
|
3106
|
-
meant it was checked against four hand-written fixtures and never against
|
|
3107
|
-
the state a lead actually writes.
|
|
3108
|
-
"""
|
|
3109
|
-
counts = {value: 0 for value in CLASSIFICATION_COUNT_KEYS.values()}
|
|
3110
|
-
for finding in findings or []:
|
|
3111
|
-
if not isinstance(finding, dict):
|
|
3112
|
-
continue
|
|
3113
|
-
key = CLASSIFICATION_COUNT_KEYS.get(finding.get("classification"))
|
|
3114
|
-
if key:
|
|
3115
|
-
counts[key] += 1
|
|
3116
|
-
return counts
|
|
3117
|
-
|
|
3296
|
+
_CONVERGENCE_INTERMEDIATE_PREFIXES = (
|
|
3297
|
+
"convergence-groups-",
|
|
3298
|
+
"convergence-work-",
|
|
3299
|
+
"convergence-round-",
|
|
3300
|
+
)
|
|
3118
3301
|
|
|
3119
|
-
def _validate_convergence_state_counts(run_dir, failures) -> None:
|
|
3120
|
-
"""A run's convergence state must agree with its own findings list.
|
|
3121
3302
|
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
artifact, so a miscount survived into the report as two different truths
|
|
3125
|
-
(observed on dev-6901 planning 009: declared partial 6 / worker-unique 4
|
|
3126
|
-
against an actual 5 / 5). Downstream readers pick one arbitrarily.
|
|
3127
|
-
"""
|
|
3303
|
+
def _validate_convergence_states(run_dir, failures) -> None:
|
|
3304
|
+
"""Replay shared engine invariants for every public final state artifact."""
|
|
3128
3305
|
from pathlib import Path as _Path
|
|
3129
3306
|
|
|
3130
3307
|
state_dir = _Path(run_dir) / "state"
|
|
3131
3308
|
if not state_dir.is_dir():
|
|
3132
3309
|
return
|
|
3133
3310
|
for state_path in sorted(state_dir.glob("convergence-*.json")):
|
|
3311
|
+
if state_path.name.startswith(_CONVERGENCE_INTERMEDIATE_PREFIXES):
|
|
3312
|
+
continue
|
|
3134
3313
|
try:
|
|
3135
3314
|
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
3136
3315
|
except (OSError, json.JSONDecodeError) as exc:
|
|
3137
|
-
failures.append(f"convergence state {state_path.name} unreadable: {exc}")
|
|
3138
|
-
continue
|
|
3139
|
-
declared = state.get("finalClassificationCounts")
|
|
3140
|
-
if not isinstance(declared, dict):
|
|
3141
|
-
continue
|
|
3142
|
-
actual = classification_counts_from_findings(state.get("findings"))
|
|
3143
|
-
if declared != actual:
|
|
3144
3316
|
failures.append(
|
|
3145
|
-
f"convergence state {state_path.name}:
|
|
3146
|
-
f"{declared} does not match findings[].classification {actual}. "
|
|
3147
|
-
"The counts are a summary of the findings list — a mismatch means "
|
|
3148
|
-
"the run reports two different classifications for the same work."
|
|
3317
|
+
f"convergence state {state_path.name}: unreadable JSON: {exc}"
|
|
3149
3318
|
)
|
|
3319
|
+
continue
|
|
3320
|
+
failures.extend(
|
|
3321
|
+
f"convergence state {state_path.name}: {error}"
|
|
3322
|
+
for error in validate_final_state(state)
|
|
3323
|
+
)
|
|
3150
3324
|
|
|
3151
3325
|
|
|
3152
3326
|
def _validate_requirements_discovery_fanout(run_dir, failures) -> None:
|
|
@@ -3524,6 +3698,15 @@ def main() -> int:
|
|
|
3524
3698
|
# contain every required section. Substring checks below are a
|
|
3525
3699
|
# safety net for hand-edited or pre-v1.0 reports.
|
|
3526
3700
|
task_type = effective_run_task_type(run_manifest, task_manifest)
|
|
3701
|
+
_validate_initial_analysis_prompts(
|
|
3702
|
+
{
|
|
3703
|
+
"taskType": task_type,
|
|
3704
|
+
"projectRoot": project_root,
|
|
3705
|
+
"runManifest": run_manifest,
|
|
3706
|
+
"teamState": team_state,
|
|
3707
|
+
},
|
|
3708
|
+
failures,
|
|
3709
|
+
)
|
|
3527
3710
|
report_contracts = _normalize_report_contracts(
|
|
3528
3711
|
run_manifest.get("reportContracts")
|
|
3529
3712
|
)
|
|
@@ -3587,7 +3770,7 @@ def main() -> int:
|
|
|
3587
3770
|
run_dir = report_path.parent.parent
|
|
3588
3771
|
_validate_requirements_discovery_fanout(run_dir, failures)
|
|
3589
3772
|
# Phase-agnostic: convergence runs in every finding-producing phase.
|
|
3590
|
-
|
|
3773
|
+
_validate_convergence_states(report_path.parent.parent, failures)
|
|
3591
3774
|
validate_report_views(report_path, failures)
|
|
3592
3775
|
|
|
3593
3776
|
validation_status = "passed" if not failures else "failed"
|
package/src/cli-registry.mjs
CHANGED
|
@@ -66,6 +66,13 @@ export const COMMAND_REGISTRY = [
|
|
|
66
66
|
category: "admin",
|
|
67
67
|
summary: ["Move legacy .project-docs/okstra/ to .okstra/ (one-shot)"],
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
name: "convergence",
|
|
71
|
+
module: "./commands/execute/convergence.mjs",
|
|
72
|
+
export: "run",
|
|
73
|
+
category: "admin",
|
|
74
|
+
summary: ["Advance and validate deterministic re-verification state"],
|
|
75
|
+
},
|
|
69
76
|
{
|
|
70
77
|
name: "git-reconcile",
|
|
71
78
|
module: "./commands/execute/git-reconcile.mjs",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra convergence — advance deterministic re-verification state
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
okstra convergence seed --groups <path> --work-state <path> \\
|
|
7
|
+
--final-state <path> --migration-dir <path> [--restart-from-round0]
|
|
8
|
+
okstra convergence plan-round --work-state <path> --plan <path>
|
|
9
|
+
okstra convergence apply-round --work-state <path> --plan <path> \\
|
|
10
|
+
--results <path>
|
|
11
|
+
okstra convergence finalize --work-state <path> --output <path>
|
|
12
|
+
okstra convergence validate --state <path> --kind <working|final>
|
|
13
|
+
|
|
14
|
+
This internal admin command is consumed by the Phase 5.5 lead contract. It
|
|
15
|
+
does not install or revive the obsolete okstra-convergence skill.
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export async function run(args) {
|
|
19
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
20
|
+
process.stdout.write(USAGE);
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
if (args.length === 0) {
|
|
24
|
+
process.stdout.write(USAGE);
|
|
25
|
+
return 2;
|
|
26
|
+
}
|
|
27
|
+
const result = await runPythonModule({
|
|
28
|
+
module: "okstra_ctl.convergence",
|
|
29
|
+
args,
|
|
30
|
+
stdio: "inherit-stdout",
|
|
31
|
+
});
|
|
32
|
+
return result.code;
|
|
33
|
+
}
|
|
34
|
+
|
|
@@ -6,9 +6,11 @@ Usage:
|
|
|
6
6
|
okstra log-report [--project-root <dir>] [--cwd <dir>] [--top <N>] [--json]
|
|
7
7
|
|
|
8
8
|
Output: JSON { ok, projectRoot, logsRoot, topLargest[], perTask[], totals }.
|
|
9
|
-
Scans <project-root>/.okstra/tasks/**/runs/*/prompts/*.log.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
Scans <project-root>/.okstra/tasks/**/runs/*/prompts/*.log. Each entry reports
|
|
10
|
+
the wrapper transcript as transcriptBytes and its sibling prompt Markdown as
|
|
11
|
+
promptBytes; sizeBytes remains the transcript-size compatibility field. Sizes
|
|
12
|
+
are raw bytes and mtimes are epoch seconds — the caller formats KB/MB and ages.
|
|
13
|
+
Read-only: it never deletes logs (cleanup commands stay in the skill).
|
|
12
14
|
`;
|
|
13
15
|
|
|
14
16
|
export async function run(args) {
|