okstra 0.179.2 → 0.180.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 +1 -1
- package/dist/cli-registry.mjs +14 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/incremental-carry.mjs +9 -8
- package/dist/commands/execute/incremental-carry.mjs.map +1 -1
- package/dist/commands/execute/plan-verify.mjs +3 -1
- package/dist/commands/execute/plan-verify.mjs.map +1 -1
- package/dist/commands/report/approval-decision.d.mts +1 -0
- package/dist/commands/report/approval-decision.mjs +21 -0
- package/dist/commands/report/approval-decision.mjs.map +1 -0
- package/dist/commands/report/design-snapshot.d.mts +1 -0
- package/dist/commands/report/design-snapshot.mjs +19 -0
- package/dist/commands/report/design-snapshot.mjs.map +1 -0
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +10 -10
- package/docs/cli.md +11 -8
- package/docs/project-structure-overview.md +15 -6
- package/docs/task-process/implementation-planning.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +15 -164
- package/runtime/prompts/launch.template.md +6 -5
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/convergence.md +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +19 -18
- package/runtime/prompts/lead/plan-body-verification.md +39 -18
- package/runtime/prompts/lead/report-writer.md +64 -423
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/feature-analysis.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +13 -11
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/project-analysis.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +2 -1
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/agent_activity.py +23 -3
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +6 -6
- package/runtime/python/okstra_ctl/analysis_packet.py +43 -2
- package/runtime/python/okstra_ctl/approval_decisions.py +327 -0
- package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +62 -4
- package/runtime/python/okstra_ctl/dispatch_state.py +29 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +24 -15
- package/runtime/python/okstra_ctl/incremental_carry.py +128 -16
- package/runtime/python/okstra_ctl/incremental_scope.py +4 -1
- package/runtime/python/okstra_ctl/path_hints.py +12 -0
- package/runtime/python/okstra_ctl/paths.py +12 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +113 -16
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
- package/runtime/python/okstra_ctl/render.py +48 -1
- package/runtime/python/okstra_ctl/render_final_report.py +7 -6
- package/runtime/python/okstra_ctl/report_assembly.py +354 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -1
- package/runtime/python/okstra_ctl/report_finalize.py +60 -22
- package/runtime/python/okstra_ctl/report_inputs.py +72 -0
- package/runtime/python/okstra_ctl/report_markdown.py +69 -8
- package/runtime/python/okstra_ctl/report_narrative.py +319 -0
- package/runtime/python/okstra_ctl/report_projections.py +265 -0
- package/runtime/python/okstra_ctl/run.py +25 -9
- package/runtime/python/okstra_ctl/schema_excerpt.py +11 -6
- package/runtime/python/okstra_ctl/stage_fix_carry.py +4 -4
- package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
- package/runtime/python/okstra_ctl/stage_map.py +70 -22
- package/runtime/python/okstra_ctl/team.py +1 -1
- package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +35 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
- package/runtime/schemas/final-report-v3.0.schema.json +10210 -0
- package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
- package/runtime/templates/report-writer-prompt-preamble.md +15 -21
- package/runtime/templates/reports/html/macros/forms.html +6 -4
- package/runtime/validators/validate-run.py +258 -10
|
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any, Iterable
|
|
9
9
|
|
|
10
|
+
from .final_report_paths import final_report_data_path, is_full_reading_copy_path
|
|
10
11
|
from .md_table import is_separator_row, split_pipe_row
|
|
11
12
|
from .paths import RunRef
|
|
12
13
|
from .plan_run_root import list_implementation_planning_reports
|
|
@@ -364,8 +365,8 @@ def parse_stage_map_file(plan_path: Path) -> list[StageMapStage]:
|
|
|
364
365
|
return _parse_stage_map_markdown(resolved)
|
|
365
366
|
|
|
366
367
|
|
|
367
|
-
def
|
|
368
|
-
"""
|
|
368
|
+
def structured_report(plan_path: Path) -> dict[str, Any]:
|
|
369
|
+
"""계약 2.0·3.0 구조화 리포트를 반환하고 Markdown에는 빈 값을 반환한다.
|
|
369
370
|
|
|
370
371
|
Public because every caller that must branch on report schema needs it —
|
|
371
372
|
including `validators/validate-implementation-plan-stages.py`, which is a
|
|
@@ -381,14 +382,19 @@ def schema_v2_report(plan_path: Path) -> dict[str, Any]:
|
|
|
381
382
|
data = json.loads(resolved.read_text(encoding="utf-8"))
|
|
382
383
|
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
383
384
|
raise StageMapError("stage_map", str(exc), str(resolved)) from exc
|
|
384
|
-
if not isinstance(data, dict) or data.get("schemaVersion")
|
|
385
|
+
if not isinstance(data, dict) or data.get("schemaVersion") not in {"2.0", "3.0"}:
|
|
385
386
|
return {}
|
|
386
387
|
return data
|
|
387
388
|
|
|
388
389
|
|
|
390
|
+
def schema_v2_report(plan_path: Path) -> dict[str, Any]:
|
|
391
|
+
"""기존 호출자를 위한 구조화 리포트 판독 별칭."""
|
|
392
|
+
return structured_report(plan_path)
|
|
393
|
+
|
|
394
|
+
|
|
389
395
|
def _planning_section(markdown_path: Path) -> dict[str, Any]:
|
|
390
396
|
"""The report's `implementationPlanning` block, `{}` for v1."""
|
|
391
|
-
planning =
|
|
397
|
+
planning = structured_report(markdown_path).get("implementationPlanning")
|
|
392
398
|
return planning if isinstance(planning, dict) else {}
|
|
393
399
|
|
|
394
400
|
|
|
@@ -412,7 +418,7 @@ def _stage_narratives(value: Any) -> dict[int, dict[str, Any]]:
|
|
|
412
418
|
|
|
413
419
|
def load_planning_detail(markdown_path: Path) -> PlanningDetail:
|
|
414
420
|
"""Read one report's narrative rows; empty for a schema-v1 report."""
|
|
415
|
-
report =
|
|
421
|
+
report = structured_report(markdown_path)
|
|
416
422
|
planning = report.get("implementationPlanning")
|
|
417
423
|
if not isinstance(planning, dict) or not planning:
|
|
418
424
|
return PlanningDetail({}, {})
|
|
@@ -456,9 +462,34 @@ def stage_map_records(stages: Iterable[StageMapStage]) -> list[dict[str, Any]]:
|
|
|
456
462
|
]
|
|
457
463
|
|
|
458
464
|
|
|
465
|
+
def _latest_plan_report(task_root: Path) -> Path | None:
|
|
466
|
+
reports_dir = RunRef.from_task_root(
|
|
467
|
+
task_root, "implementation-planning"
|
|
468
|
+
).reports_dir
|
|
469
|
+
reports = list_implementation_planning_reports(reports_dir)
|
|
470
|
+
return reports[0] if reports else None
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _snapshot_for(source: Path | None) -> StageMapSnapshot:
|
|
474
|
+
if source is None:
|
|
475
|
+
return StageMapSnapshot("missing", "", [])
|
|
476
|
+
resolved = source.resolve()
|
|
477
|
+
return StageMapSnapshot(
|
|
478
|
+
"ready",
|
|
479
|
+
str(resolved),
|
|
480
|
+
stage_map_records(parse_stage_map_file(resolved)),
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
459
484
|
def load_task_stage_map(
|
|
460
485
|
task_root: Path, manifest: dict[str, Any],
|
|
461
486
|
) -> StageMapSnapshot:
|
|
487
|
+
"""완료된 stage 를 무엇으로 지었는지 답하는 Stage Map.
|
|
488
|
+
|
|
489
|
+
carry 사이드카가 가리키는 계획을 우선한다 — 그게 실행이 실제로 따른
|
|
490
|
+
문서이기 때문이다. "어떤 stage 번호가 이미 쓰였나" 는 다른 질문이며
|
|
491
|
+
`load_latest_plan_stage_map` 이 답한다.
|
|
492
|
+
"""
|
|
462
493
|
carried = _unique_carry_source_paths(task_root)
|
|
463
494
|
if len(carried) > 1:
|
|
464
495
|
paths = tuple(str(path) for path in carried)
|
|
@@ -467,22 +498,19 @@ def load_task_stage_map(
|
|
|
467
498
|
"implementation carries reference different source plans",
|
|
468
499
|
conflicting_paths=paths,
|
|
469
500
|
)
|
|
470
|
-
if carried
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
str(resolved),
|
|
484
|
-
stage_map_records(parse_stage_map_file(resolved)),
|
|
485
|
-
)
|
|
501
|
+
source = carried[0] if carried else _latest_plan_report(task_root)
|
|
502
|
+
return _snapshot_for(source)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def load_latest_plan_stage_map(task_root: Path) -> StageMapSnapshot:
|
|
506
|
+
"""이 task 의 최신 계획 리포트가 선언한 Stage Map.
|
|
507
|
+
|
|
508
|
+
번호 점유의 판정 기준이다. ADR-0015 는 stage 번호를 append-only 로 두고
|
|
509
|
+
새 stage 를 `max+1` 로 붙이라고 정하는데, 그 `max` 는 최신 계획에서만
|
|
510
|
+
나온다 — 완료된 stage 를 지을 때 쓴 계획 뒤에 stage 를 덧붙인 계획이
|
|
511
|
+
있으면 그 번호들도 이미 점유된 상태다.
|
|
512
|
+
"""
|
|
513
|
+
return _snapshot_for(_latest_plan_report(task_root))
|
|
486
514
|
|
|
487
515
|
|
|
488
516
|
def _unique_carry_source_paths(task_root: Path) -> list[Path]:
|
|
@@ -495,10 +523,30 @@ def _unique_carry_source_paths(task_root: Path) -> list[Path]:
|
|
|
495
523
|
continue
|
|
496
524
|
value = carry.get("sourcePlanPath") if isinstance(carry, dict) else None
|
|
497
525
|
if isinstance(value, str) and value:
|
|
498
|
-
|
|
526
|
+
# carry 의 이 값은 executor 워커가 손으로 적는 자유 문자열이고,
|
|
527
|
+
# `--approved-plan` 이 레코드 전용으로 조여지기 전에 준비된 run 은
|
|
528
|
+
# 읽기본(`.md`) 경로를 남겼다. v2 리포트의 `.md` 에는 Stage Map
|
|
529
|
+
# 섹션이 없으므로 그대로 읽으면 원장이 통째로 사라진다. 같은 legacy
|
|
530
|
+
# 포인터 정규화를 run-index·타임라인 소비자는 이미 하고 있다.
|
|
531
|
+
# 읽는 쪽에서만 낮춘다 — 동결된 carry 사이드카는 고치지 않는다.
|
|
532
|
+
paths.add(_carry_source_path(task_root, value))
|
|
499
533
|
return sorted(paths, key=str)
|
|
500
534
|
|
|
501
535
|
|
|
536
|
+
def _carry_source_path(task_root: Path, value: str) -> Path:
|
|
537
|
+
"""carry 가 가리키는 계획의 읽을 경로.
|
|
538
|
+
|
|
539
|
+
레코드 형제가 실재할 때만 읽기본(`.md`)을 레코드로 낮춘다. schema-v1 계획은
|
|
540
|
+
레코드가 아예 없고 마크다운이 유일한 정본이므로, 무조건 낮추면 v1 태스크의
|
|
541
|
+
Stage Map 을 없는 파일에서 찾게 된다.
|
|
542
|
+
"""
|
|
543
|
+
resolved = _resolve_plan_path(task_root, value).resolve()
|
|
544
|
+
if not is_full_reading_copy_path(resolved):
|
|
545
|
+
return resolved
|
|
546
|
+
record = final_report_data_path(resolved)
|
|
547
|
+
return record if record.is_file() else resolved
|
|
548
|
+
|
|
549
|
+
|
|
502
550
|
def _resolve_plan_path(task_root: Path, value: str) -> Path:
|
|
503
551
|
path = Path(value)
|
|
504
552
|
if path.is_absolute():
|
|
@@ -81,7 +81,7 @@ def _add_dispatch_parser(sub) -> None:
|
|
|
81
81
|
parser.add_argument("--workers", default="")
|
|
82
82
|
parser.add_argument("--jobs-file", default="")
|
|
83
83
|
parser.add_argument("--dry-run", action="store_true")
|
|
84
|
-
parser.add_argument("--idle-timeout-seconds", type=int, default=
|
|
84
|
+
parser.add_argument("--idle-timeout-seconds", type=int, default=None)
|
|
85
85
|
parser.add_argument("--dispatch-kind", default="initial")
|
|
86
86
|
|
|
87
87
|
|
|
@@ -76,7 +76,7 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
76
76
|
parser.add_argument("--okstra-bin", default="")
|
|
77
77
|
parser.add_argument("--workers", default="")
|
|
78
78
|
parser.add_argument("--dry-run", action="store_true")
|
|
79
|
-
parser.add_argument("--idle-timeout-seconds", default="
|
|
79
|
+
parser.add_argument("--idle-timeout-seconds", default="")
|
|
80
80
|
parser.add_argument("--dispatch-kind", default="initial")
|
|
81
81
|
parser.add_argument("--jobs-file", default="")
|
|
82
82
|
return parser
|
|
@@ -86,7 +86,10 @@ def _parse_workers(raw: str) -> list[str]:
|
|
|
86
86
|
return [item.strip() for item in raw.split(",") if item.strip()]
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
def _parse_idle_timeout(raw: str) -> int:
|
|
89
|
+
def _parse_idle_timeout(raw: str) -> int | None:
|
|
90
|
+
"""명시된 예산, 또는 역할이 정하게 두는 ``None``."""
|
|
91
|
+
if not raw:
|
|
92
|
+
return None
|
|
90
93
|
try:
|
|
91
94
|
value = int(raw)
|
|
92
95
|
except ValueError as exc:
|
|
@@ -91,6 +91,10 @@ def report_writer_prompt_body(
|
|
|
91
91
|
report_language: str,
|
|
92
92
|
) -> list[str]:
|
|
93
93
|
"""Render the provider-neutral body for the report-writer audience."""
|
|
94
|
+
if str(manifest.get("reportContractVersion") or "") == "3.0":
|
|
95
|
+
return _report_writer_v3_body(
|
|
96
|
+
manifest, active_context, team_state, model, report_language
|
|
97
|
+
)
|
|
94
98
|
return [
|
|
95
99
|
f"**Model:** Report writer worker, {model}",
|
|
96
100
|
f"**Report Language:** {report_language}",
|
|
@@ -124,6 +128,37 @@ def report_writer_prompt_body(
|
|
|
124
128
|
]
|
|
125
129
|
|
|
126
130
|
|
|
131
|
+
def _report_writer_v3_body(
|
|
132
|
+
manifest: Mapping[str, Any],
|
|
133
|
+
active_context: Mapping[str, Any],
|
|
134
|
+
team_state: Mapping[str, Any],
|
|
135
|
+
model: str,
|
|
136
|
+
report_language: str,
|
|
137
|
+
) -> list[str]:
|
|
138
|
+
return [
|
|
139
|
+
f"**Model:** Report writer worker, {model}",
|
|
140
|
+
f"**Report Language:** {report_language}",
|
|
141
|
+
"",
|
|
142
|
+
"# Report Writer Worker Dispatch",
|
|
143
|
+
"",
|
|
144
|
+
"## Task",
|
|
145
|
+
f"- Task key: `{_require_string(manifest, 'taskKey')}`",
|
|
146
|
+
f"- Task type: `{_require_string(manifest, 'taskType')}`",
|
|
147
|
+
"",
|
|
148
|
+
"## Inputs",
|
|
149
|
+
*report_writer_input_lines(manifest, active_context, team_state),
|
|
150
|
+
"",
|
|
151
|
+
mcp_pointer_line(),
|
|
152
|
+
"",
|
|
153
|
+
"## Output Contract",
|
|
154
|
+
"You are the author of the report narrative Markdown at Result Path.",
|
|
155
|
+
"Write the worker-result pointer at Worker Result Path.",
|
|
156
|
+
"Maintain the separate audit sidecar at Audit sidecar path.",
|
|
157
|
+
"Do not write the final report record or another owner's input ledger.",
|
|
158
|
+
"Do not return the report inline.",
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
|
|
127
162
|
def report_writer_input_lines(
|
|
128
163
|
manifest: Mapping[str, Any],
|
|
129
164
|
active_context: Mapping[str, Any],
|
|
@@ -120,9 +120,7 @@ def resolve_prompt_plan(
|
|
|
120
120
|
# that are *supposed* to differ and fail every critic-enabled run.
|
|
121
121
|
analysis_equality_group = None if dispatch_kind == "critic" else "analysis-core"
|
|
122
122
|
analysis_duty: AgentAudience = (
|
|
123
|
-
|
|
124
|
-
if dispatch_kind == "critic" and task_type == "final-verification"
|
|
125
|
-
else "scope-critic"
|
|
123
|
+
critic_duty_for_task_type(task_type)
|
|
126
124
|
if dispatch_kind == "critic"
|
|
127
125
|
else "acceptance-verifier"
|
|
128
126
|
if task_type == "final-verification"
|
|
@@ -232,6 +230,36 @@ def _worker_facing_headers(
|
|
|
232
230
|
return (*ERRORS_PATH_HEADERS, *required_headers)
|
|
233
231
|
|
|
234
232
|
|
|
233
|
+
# 하나의 run 이 실제로 디스패치할 수 있는 critic 은 한 종류다. 판정은 task type
|
|
234
|
+
# 만의 함수이므로 여기 한 번만 적고, 로스터 등록과 프롬프트 정체성 검사가 같은
|
|
235
|
+
# 값을 읽는다 — 로스터가 실행될 수 없는 critic 행을 하나 더 들고 있으면 provider
|
|
236
|
+
# 로만 만든 role 라벨이 겹쳐 validate-run 의 중복 역할 검사에 걸린다.
|
|
237
|
+
CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT: dict[str, AgentAudience] = {
|
|
238
|
+
"scope": "scope-critic",
|
|
239
|
+
"acceptance": "acceptance-critic",
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def critic_duty_for_task_type(task_type: str) -> AgentAudience:
|
|
244
|
+
"""이 task type 의 critic 직무."""
|
|
245
|
+
return (
|
|
246
|
+
"acceptance-critic"
|
|
247
|
+
if task_type == "final-verification"
|
|
248
|
+
else "scope-critic"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def critic_assignment_ref(task_type: str) -> str:
|
|
253
|
+
"""이 task type 이 등록할 유일한 critic 배정 참조."""
|
|
254
|
+
duty = critic_duty_for_task_type(task_type)
|
|
255
|
+
segment = next(
|
|
256
|
+
key
|
|
257
|
+
for key, value in CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT.items()
|
|
258
|
+
if value == duty
|
|
259
|
+
)
|
|
260
|
+
return f"critic/{segment}"
|
|
261
|
+
|
|
262
|
+
|
|
235
263
|
def _executor_worker_id(manifest: Mapping[str, Any]) -> str | None:
|
|
236
264
|
team_contract = manifest.get("teamContract")
|
|
237
265
|
if not isinstance(team_contract, Mapping):
|