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
|
@@ -32,6 +32,10 @@ class MutationSnapshot:
|
|
|
32
32
|
scratch_digests: Mapping[str, str]
|
|
33
33
|
git_projection: Mapping[str, Any]
|
|
34
34
|
digest: str
|
|
35
|
+
# 오케스트레이터가 쓰는 경로. 파일 하나를 싣거나 디렉터리를 실어 그 트리
|
|
36
|
+
# 전체를 덮을 수 있다 — 한 배치가 무엇을 쓸지 파일 단위로 미리 셀 수 없기
|
|
37
|
+
# 때문이다(다른 라운드의 재시도 프롬프트, 로그, 상태 사이드카가 그 창에
|
|
38
|
+
# 들어온다).
|
|
35
39
|
orchestrator_paths: tuple[str, ...] = ()
|
|
36
40
|
|
|
37
41
|
def to_payload(self) -> dict[str, Any]:
|
|
@@ -445,7 +449,7 @@ def _source_changes(
|
|
|
445
449
|
snapshot.artifact_root == snapshot.root
|
|
446
450
|
and any(_is_within(path, item) for item in artifacts)
|
|
447
451
|
)
|
|
448
|
-
and path
|
|
452
|
+
and not any(_is_within(path, item) for item in orchestrator)
|
|
449
453
|
and not _excluded(Path(path), generated)
|
|
450
454
|
}
|
|
451
455
|
|
|
@@ -464,7 +468,7 @@ def _artifact_policy_failures(
|
|
|
464
468
|
unauthorized = {
|
|
465
469
|
path for path in changed
|
|
466
470
|
if not any(_is_within(path, item) for item in allowed)
|
|
467
|
-
and path
|
|
471
|
+
and not any(_is_within(path, item) for item in orchestrator)
|
|
468
472
|
}
|
|
469
473
|
if snapshot.artifact_root == snapshot.root:
|
|
470
474
|
return []
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""Mini JSON Schema validator for
|
|
1
|
+
"""Mini JSON Schema validator for versioned final-report data.json records.
|
|
2
2
|
|
|
3
3
|
This is a deliberately narrow JSON Schema implementation that supports
|
|
4
4
|
exactly the keywords used by ``schemas/final-report-v2.0.schema.json``:
|
|
5
5
|
|
|
6
6
|
type, required, properties, additionalProperties, enum, const, pattern,
|
|
7
7
|
minLength, minItems, maxItems, uniqueItems, items, minimum, maximum, $ref ($defs),
|
|
8
|
-
oneOf, allOf, if/then/else, contains, not
|
|
8
|
+
oneOf, allOf, if/then/else, contains, minContains, maxContains, not
|
|
9
9
|
|
|
10
10
|
We do NOT depend on the ``jsonschema`` PyPI package because its
|
|
11
11
|
dependency tree (``referencing``, ``rpds-py``) includes a Rust C
|
|
@@ -13,8 +13,8 @@ extension which we would have to vendor or ship pre-built per platform.
|
|
|
13
13
|
The ~250 lines below cover everything the final-report schema needs;
|
|
14
14
|
strict spec compliance is not a goal — strict validation of OUR schema is.
|
|
15
15
|
|
|
16
|
-
Error messages include the JSON pointer of the failing field so
|
|
17
|
-
|
|
16
|
+
Error messages include the JSON pointer of the failing field so report
|
|
17
|
+
assembly can return the defect to the input owner.
|
|
18
18
|
"""
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
@@ -23,9 +23,6 @@ import re
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import Any
|
|
25
25
|
|
|
26
|
-
from okstra_ctl.report_contract import CURRENT_REPORT_SCHEMA_VERSION
|
|
27
|
-
|
|
28
|
-
|
|
29
26
|
class SchemaError(ValueError):
|
|
30
27
|
"""Raised when a schema itself is malformed (e.g. a $ref points
|
|
31
28
|
nowhere). Distinct from a data validation failure.
|
|
@@ -204,10 +201,20 @@ class _Validator:
|
|
|
204
201
|
for i, value in enumerate(instance):
|
|
205
202
|
self.validate(value, items, path + (i,))
|
|
206
203
|
contains = schema.get("contains")
|
|
207
|
-
if contains is not None
|
|
208
|
-
self._matches(value, contains) for value in instance
|
|
209
|
-
|
|
210
|
-
|
|
204
|
+
if contains is not None:
|
|
205
|
+
matches = sum(self._matches(value, contains) for value in instance)
|
|
206
|
+
minimum = schema.get("minContains", 1)
|
|
207
|
+
maximum = schema.get("maxContains")
|
|
208
|
+
if matches < minimum:
|
|
209
|
+
self._err(
|
|
210
|
+
path,
|
|
211
|
+
f"array contains {matches} matching item(s), below minContains {minimum}",
|
|
212
|
+
)
|
|
213
|
+
if maximum is not None and matches > maximum:
|
|
214
|
+
self._err(
|
|
215
|
+
path,
|
|
216
|
+
f"array contains {matches} matching item(s), above maxContains {maximum}",
|
|
217
|
+
)
|
|
211
218
|
|
|
212
219
|
def _validate_conditional(self, instance: Any, schema: dict, path: tuple[str | int, ...]) -> None:
|
|
213
220
|
if_schema = schema.get("if")
|
|
@@ -263,6 +270,7 @@ def validate(data: Any, schema: dict) -> list[str]:
|
|
|
263
270
|
|
|
264
271
|
SCHEMA_FILENAMES = {
|
|
265
272
|
"2.0": "final-report-v2.0.schema.json",
|
|
273
|
+
"3.0": "final-report-v3.0.schema.json",
|
|
266
274
|
}
|
|
267
275
|
|
|
268
276
|
|
|
@@ -295,10 +303,11 @@ def load_schema_for_data(data: dict, start: Path | None = None) -> dict:
|
|
|
295
303
|
|
|
296
304
|
|
|
297
305
|
def load_schema(schema_path: Path | None = None) -> dict:
|
|
298
|
-
"""Load the
|
|
299
|
-
|
|
300
|
-
|
|
306
|
+
"""Load the compatibility schema used by historical direct callers.
|
|
307
|
+
|
|
308
|
+
New write paths select contract 3.0 explicitly. Readers select from the
|
|
309
|
+
record's ``schemaVersion`` through :func:`load_schema_for_data`.
|
|
301
310
|
"""
|
|
302
311
|
if schema_path is not None:
|
|
303
312
|
return json.loads(Path(schema_path).read_text(encoding="utf-8"))
|
|
304
|
-
return load_schema_version(
|
|
313
|
+
return load_schema_version("2.0")
|
|
@@ -7,9 +7,15 @@ import json
|
|
|
7
7
|
import re
|
|
8
8
|
import sys
|
|
9
9
|
from pathlib import Path
|
|
10
|
+
from typing import Any, Mapping
|
|
11
|
+
|
|
12
|
+
from .convergence_store import write_json_atomic
|
|
13
|
+
from .final_report_schema import load_schema_version
|
|
14
|
+
from .report_narrative import parse_narrative, writer_owned_data
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
PREP_VERDICT_RE = re.compile(r"^P-Prep-S([1-9][0-9]*)-")
|
|
18
|
+
STEP_VERDICT_RE = re.compile(r"^P-Step-([1-9][0-9]*)\.")
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
class CarryError(Exception):
|
|
@@ -290,6 +296,78 @@ def merge_carried_forward(
|
|
|
290
296
|
return cur
|
|
291
297
|
|
|
292
298
|
|
|
299
|
+
def _plan_item_stage(item: Mapping[str, Any]) -> int | None:
|
|
300
|
+
item_id = str(item.get("id") or "")
|
|
301
|
+
for pattern in (PREP_VERDICT_RE, STEP_VERDICT_RE):
|
|
302
|
+
match = pattern.match(item_id)
|
|
303
|
+
if match:
|
|
304
|
+
return int(match.group(1))
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _writer_stage_rows(data: Mapping[str, Any], *, snapshot: str) -> dict[int, dict]:
|
|
309
|
+
planning = _planning(dict(writer_owned_data(data)))
|
|
310
|
+
return _stage_rows(planning, snapshot=snapshot)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def merge_v3_plan_state(
|
|
314
|
+
prev: dict[str, Any],
|
|
315
|
+
narrative: dict[str, Any],
|
|
316
|
+
state: dict[str, Any],
|
|
317
|
+
*,
|
|
318
|
+
prev_seq: str,
|
|
319
|
+
carry_stages: set[int],
|
|
320
|
+
reverify_stages: set[int],
|
|
321
|
+
) -> dict[str, Any]:
|
|
322
|
+
"""이전 판정 중 이월 스테이지 소유분만 v3 수렴 상태에 복사한다."""
|
|
323
|
+
prev_stages = _writer_stage_rows(prev, snapshot="prior")
|
|
324
|
+
cur_stages = _stage_rows(_planning(narrative), snapshot="current")
|
|
325
|
+
_validate_stage_sets(prev_stages, cur_stages, carry_stages, reverify_stages)
|
|
326
|
+
for stage in sorted(carry_stages):
|
|
327
|
+
current = cur_stages.get(stage)
|
|
328
|
+
if current is None:
|
|
329
|
+
raise CarryError(f"carry stage {stage} is missing from current narrative")
|
|
330
|
+
if _canonical(prev_stages[stage]) != _canonical(current):
|
|
331
|
+
raise CarryError(f"carry stage {stage} changed in current narrative")
|
|
332
|
+
|
|
333
|
+
prev_items = _indexed_rows(
|
|
334
|
+
_planning(prev).get("planBodyVerification", {}).get("planItems", []),
|
|
335
|
+
key="id",
|
|
336
|
+
label="prior planItems",
|
|
337
|
+
)
|
|
338
|
+
state_items = _indexed_rows(
|
|
339
|
+
state.get("planBodyVerification", {}).get("planItems", []),
|
|
340
|
+
key="id",
|
|
341
|
+
label="current state planItems",
|
|
342
|
+
)
|
|
343
|
+
for item_id, current in state_items.items():
|
|
344
|
+
stage = _plan_item_stage(current)
|
|
345
|
+
if stage not in carry_stages:
|
|
346
|
+
continue
|
|
347
|
+
previous = prev_items.get(item_id)
|
|
348
|
+
if previous is None:
|
|
349
|
+
raise CarryError(f"carry-owned plan item {item_id} is missing from prior report")
|
|
350
|
+
for field in ("subject", "sourceSection"):
|
|
351
|
+
if current.get(field) != previous.get(field):
|
|
352
|
+
raise CarryError(f"carry-owned plan item {item_id} changed {field}")
|
|
353
|
+
carried = copy.deepcopy(previous)
|
|
354
|
+
carried["carriedForwardFromSeq"] = str(prev_seq)
|
|
355
|
+
state_items[item_id] = carried
|
|
356
|
+
|
|
357
|
+
prior_carried_ids = {
|
|
358
|
+
item_id
|
|
359
|
+
for item_id, item in prev_items.items()
|
|
360
|
+
if _plan_item_stage(item) in carry_stages
|
|
361
|
+
}
|
|
362
|
+
missing = sorted(prior_carried_ids - set(state_items))
|
|
363
|
+
if missing:
|
|
364
|
+
raise CarryError(f"current state omitted carry-owned plan items: {missing}")
|
|
365
|
+
state["planBodyVerification"]["planItems"] = [
|
|
366
|
+
state_items[item_id] for item_id in sorted(state_items)
|
|
367
|
+
]
|
|
368
|
+
return state
|
|
369
|
+
|
|
370
|
+
|
|
293
371
|
def _parse_stage_csv(value: str | None, *, option: str) -> set[int] | None:
|
|
294
372
|
if value is None:
|
|
295
373
|
return None
|
|
@@ -317,33 +395,67 @@ def _load_data(path: str, *, option: str) -> dict:
|
|
|
317
395
|
def main(argv: list[str]) -> int:
|
|
318
396
|
ap = argparse.ArgumentParser(prog="okstra incremental-carry")
|
|
319
397
|
ap.add_argument("--prev-data", required=True, help="prior run final-report data.json")
|
|
320
|
-
ap.
|
|
398
|
+
current = ap.add_mutually_exclusive_group(required=True)
|
|
399
|
+
current.add_argument("--cur-data", help="historical v2 current data.json")
|
|
400
|
+
current.add_argument("--cur-narrative", help="contract v3 current narrative Markdown")
|
|
401
|
+
ap.add_argument("--state", help="contract v3 convergence-owned plan state")
|
|
321
402
|
ap.add_argument("--prev-seq", required=True, help="prior run seq, tagged onto carried items")
|
|
322
403
|
ap.add_argument("--carry-stages", default=None, help="comma-separated carried stage numbers")
|
|
323
404
|
ap.add_argument("--reverify-stages", default=None, help="comma-separated reverified stage numbers")
|
|
324
|
-
ap.add_argument("--out",
|
|
405
|
+
ap.add_argument("--out", help="historical v2 merged data.json path")
|
|
406
|
+
ap.add_argument("--out-state", help="contract v3 merged plan state path")
|
|
325
407
|
args = ap.parse_args(argv)
|
|
326
408
|
|
|
327
409
|
try:
|
|
328
410
|
prev = _load_data(args.prev_data, option="--prev-data")
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
cur,
|
|
333
|
-
prev_seq=args.prev_seq,
|
|
334
|
-
carry_stages=_parse_stage_csv(args.carry_stages, option="--carry-stages"),
|
|
335
|
-
reverify_stages=_parse_stage_csv(
|
|
336
|
-
args.reverify_stages, option="--reverify-stages",
|
|
337
|
-
),
|
|
411
|
+
carry_stages = _parse_stage_csv(args.carry_stages, option="--carry-stages")
|
|
412
|
+
reverify_stages = _parse_stage_csv(
|
|
413
|
+
args.reverify_stages, option="--reverify-stages",
|
|
338
414
|
)
|
|
339
|
-
|
|
415
|
+
if args.cur_narrative:
|
|
416
|
+
if not args.state or not args.out_state or args.out:
|
|
417
|
+
raise CarryError(
|
|
418
|
+
"--cur-narrative requires --state and --out-state, and forbids --out"
|
|
419
|
+
)
|
|
420
|
+
if carry_stages is None or reverify_stages is None:
|
|
421
|
+
raise CarryError("contract v3 carry requires both stage sets")
|
|
422
|
+
try:
|
|
423
|
+
narrative = parse_narrative(
|
|
424
|
+
Path(args.cur_narrative).read_text(encoding="utf-8"),
|
|
425
|
+
load_schema_version("3.0"),
|
|
426
|
+
)
|
|
427
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
428
|
+
raise CarryError(f"--cur-narrative could not be read: {exc}") from exc
|
|
429
|
+
state = _load_data(args.state, option="--state")
|
|
430
|
+
merged = merge_v3_plan_state(
|
|
431
|
+
prev,
|
|
432
|
+
narrative,
|
|
433
|
+
state,
|
|
434
|
+
prev_seq=args.prev_seq,
|
|
435
|
+
carry_stages=carry_stages,
|
|
436
|
+
reverify_stages=reverify_stages,
|
|
437
|
+
)
|
|
438
|
+
output = Path(args.out_state)
|
|
439
|
+
else:
|
|
440
|
+
if not args.out or args.out_state or args.state:
|
|
441
|
+
raise CarryError(
|
|
442
|
+
"--cur-data requires --out, and forbids --state and --out-state"
|
|
443
|
+
)
|
|
444
|
+
cur = _load_data(args.cur_data, option="--cur-data")
|
|
445
|
+
merged = merge_carried_forward(
|
|
446
|
+
prev,
|
|
447
|
+
cur,
|
|
448
|
+
prev_seq=args.prev_seq,
|
|
449
|
+
carry_stages=carry_stages,
|
|
450
|
+
reverify_stages=reverify_stages,
|
|
451
|
+
)
|
|
452
|
+
output = Path(args.out)
|
|
453
|
+
except (CarryError, KeyError, TypeError) as exc:
|
|
340
454
|
print(f"carry refused: {exc}", file=sys.stderr)
|
|
341
455
|
return 1
|
|
342
456
|
|
|
343
457
|
try:
|
|
344
|
-
|
|
345
|
-
json.dumps(merged, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
|
346
|
-
)
|
|
458
|
+
write_json_atomic(output, merged)
|
|
347
459
|
except OSError as exc:
|
|
348
460
|
print(f"carry refused: --out could not be written: {exc}", file=sys.stderr)
|
|
349
461
|
return 1
|
|
@@ -354,7 +466,7 @@ def main(argv: list[str]) -> int:
|
|
|
354
466
|
.get("planItems", [])
|
|
355
467
|
if item.get("carriedForwardFromSeq") == str(args.prev_seq)
|
|
356
468
|
)
|
|
357
|
-
print(f"merged {carried} carried-forward plan item(s) from seq {args.prev_seq} -> {
|
|
469
|
+
print(f"merged {carried} carried-forward plan item(s) from seq {args.prev_seq} -> {output}")
|
|
358
470
|
return 0
|
|
359
471
|
|
|
360
472
|
|
|
@@ -207,7 +207,10 @@ def stages_for_clarification(data: dict, clarification_id: str) -> set[int]:
|
|
|
207
207
|
)
|
|
208
208
|
stages = _stages_blocked_on(planning.get("requirementCoverage"), clarification_id)
|
|
209
209
|
for item in plan_items if isinstance(plan_items, list) else []:
|
|
210
|
-
if isinstance(item, dict) and
|
|
210
|
+
if isinstance(item, dict) and (
|
|
211
|
+
item.get("clarificationId") == clarification_id
|
|
212
|
+
or clarification_id in (item.get("clarificationRefs") or [])
|
|
213
|
+
):
|
|
211
214
|
stages |= _plan_item_stages(item)
|
|
212
215
|
return stages
|
|
213
216
|
|
|
@@ -439,6 +439,10 @@ def _run_files(
|
|
|
439
439
|
"codex_worker_result": worker_results / f"codex-worker{suffixes['worker_results']}.md",
|
|
440
440
|
"antigravity_worker_result": worker_results / f"antigravity-worker{suffixes['worker_results']}.md",
|
|
441
441
|
"report_writer_worker_result": worker_results / f"report-writer-worker{suffixes['worker_results']}.md",
|
|
442
|
+
"report_writer_narrative": worker_results / f"report-writer-narrative{suffixes['worker_results']}.md",
|
|
443
|
+
"approval_decisions": run_state / f"approval-decisions-{task_type_segment}-{sequences['state']}.json",
|
|
444
|
+
"design_preparation": run_state / f"design-preparation-{task_type_segment}-{sequences['state']}.json",
|
|
445
|
+
"plan_body_verification": run_state / f"plan-body-verification-{task_type_segment}-{sequences['state']}.json",
|
|
442
446
|
"run_errors_log": run_logs / f"errors-{task_type_segment}-{sequences['state']}.jsonl",
|
|
443
447
|
"claude_worker_errors_sidecar": worker_results / f"claude-worker-errors{suffixes['worker_results']}.json",
|
|
444
448
|
"codex_worker_errors_sidecar": worker_results / f"codex-worker-errors{suffixes['worker_results']}.json",
|
|
@@ -570,6 +574,10 @@ def _absolute_worker_fields(paths: Mapping[str, Any]) -> dict[str, str]:
|
|
|
570
574
|
"CODEX_WORKER_RESULT_FILE": str(paths["codex_worker_result"]),
|
|
571
575
|
"ANTIGRAVITY_WORKER_RESULT_FILE": str(paths["antigravity_worker_result"]),
|
|
572
576
|
"REPORT_WRITER_WORKER_RESULT_FILE": str(paths["report_writer_worker_result"]),
|
|
577
|
+
"REPORT_WRITER_NARRATIVE_FILE": str(paths["report_writer_narrative"]),
|
|
578
|
+
"APPROVAL_DECISIONS_PATH": str(paths["approval_decisions"]),
|
|
579
|
+
"DESIGN_PREPARATION_PATH": str(paths["design_preparation"]),
|
|
580
|
+
"PLAN_BODY_VERIFICATION_STATE_PATH": str(paths["plan_body_verification"]),
|
|
573
581
|
"CLAUDE_WORKER_ERRORS_SIDECAR_PATH": str(paths["claude_worker_errors_sidecar"]),
|
|
574
582
|
"CODEX_WORKER_ERRORS_SIDECAR_PATH": str(paths["codex_worker_errors_sidecar"]),
|
|
575
583
|
"ANTIGRAVITY_WORKER_ERRORS_SIDECAR_PATH": str(paths["antigravity_worker_errors_sidecar"]),
|
|
@@ -670,6 +678,10 @@ def _relative_file_fields(project_root: Path, paths: Mapping[str, Any]) -> dict[
|
|
|
670
678
|
"CODEX_WORKER_RESULT_RELATIVE_PATH": _rel(project_root, paths["codex_worker_result"]),
|
|
671
679
|
"ANTIGRAVITY_WORKER_RESULT_RELATIVE_PATH": _rel(project_root, paths["antigravity_worker_result"]),
|
|
672
680
|
"REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH": _rel(project_root, paths["report_writer_worker_result"]),
|
|
681
|
+
"REPORT_WRITER_NARRATIVE_RELATIVE_PATH": _rel(project_root, paths["report_writer_narrative"]),
|
|
682
|
+
"APPROVAL_DECISIONS_RELATIVE_PATH": _rel(project_root, paths["approval_decisions"]),
|
|
683
|
+
"DESIGN_PREPARATION_RELATIVE_PATH": _rel(project_root, paths["design_preparation"]),
|
|
684
|
+
"PLAN_BODY_VERIFICATION_STATE_RELATIVE_PATH": _rel(project_root, paths["plan_body_verification"]),
|
|
673
685
|
"RUN_ERRORS_LOG_RELATIVE_PATH": _rel(project_root, paths["run_errors_log"]),
|
|
674
686
|
"CLAUDE_WORKER_ERRORS_SIDECAR_RELATIVE_PATH": _rel(project_root, paths["claude_worker_errors_sidecar"]),
|
|
675
687
|
"CODEX_WORKER_ERRORS_SIDECAR_RELATIVE_PATH": _rel(project_root, paths["codex_worker_errors_sidecar"]),
|
|
@@ -562,6 +562,10 @@ def compute_run_paths(
|
|
|
562
562
|
codex_worker_result = worker_results / f"codex-worker{suffixes['worker_results']}.md"
|
|
563
563
|
antigravity_worker_result = worker_results / f"antigravity-worker{suffixes['worker_results']}.md"
|
|
564
564
|
report_writer_worker_result = worker_results / f"report-writer-worker{suffixes['worker_results']}.md"
|
|
565
|
+
report_writer_narrative = worker_results / f"report-writer-narrative{suffixes['worker_results']}.md"
|
|
566
|
+
approval_decisions = run_state / f"approval-decisions-{task_type_segment}-{seqs['state']}.json"
|
|
567
|
+
design_preparation = run_state / f"design-preparation-{task_type_segment}-{seqs['state']}.json"
|
|
568
|
+
plan_body_verification = run_state / f"plan-body-verification-{task_type_segment}-{seqs['state']}.json"
|
|
565
569
|
|
|
566
570
|
run_errors_log = run_logs / f"errors-{task_type_segment}-{seqs['state']}.jsonl"
|
|
567
571
|
runtime_home = okstra_home()
|
|
@@ -667,6 +671,10 @@ def compute_run_paths(
|
|
|
667
671
|
"CODEX_WORKER_RESULT_FILE": str(codex_worker_result),
|
|
668
672
|
"ANTIGRAVITY_WORKER_RESULT_FILE": str(antigravity_worker_result),
|
|
669
673
|
"REPORT_WRITER_WORKER_RESULT_FILE": str(report_writer_worker_result),
|
|
674
|
+
"REPORT_WRITER_NARRATIVE_FILE": str(report_writer_narrative),
|
|
675
|
+
"APPROVAL_DECISIONS_PATH": str(approval_decisions),
|
|
676
|
+
"DESIGN_PREPARATION_PATH": str(design_preparation),
|
|
677
|
+
"PLAN_BODY_VERIFICATION_STATE_PATH": str(plan_body_verification),
|
|
670
678
|
"RUN_ERRORS_LOG_PATH": str(run_errors_log),
|
|
671
679
|
"CLAUDE_WORKER_ERRORS_SIDECAR_PATH": str(claude_worker_errors_sidecar),
|
|
672
680
|
"CODEX_WORKER_ERRORS_SIDECAR_PATH": str(codex_worker_errors_sidecar),
|
|
@@ -737,6 +745,10 @@ def compute_run_paths(
|
|
|
737
745
|
("CODEX_WORKER_RESULT_RELATIVE_PATH", codex_worker_result),
|
|
738
746
|
("ANTIGRAVITY_WORKER_RESULT_RELATIVE_PATH", antigravity_worker_result),
|
|
739
747
|
("REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH", report_writer_worker_result),
|
|
748
|
+
("REPORT_WRITER_NARRATIVE_RELATIVE_PATH", report_writer_narrative),
|
|
749
|
+
("APPROVAL_DECISIONS_RELATIVE_PATH", approval_decisions),
|
|
750
|
+
("DESIGN_PREPARATION_RELATIVE_PATH", design_preparation),
|
|
751
|
+
("PLAN_BODY_VERIFICATION_STATE_RELATIVE_PATH", plan_body_verification),
|
|
740
752
|
("RUN_ERRORS_LOG_RELATIVE_PATH", run_errors_log),
|
|
741
753
|
("CLAUDE_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", claude_worker_errors_sidecar),
|
|
742
754
|
("CODEX_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", codex_worker_errors_sidecar),
|
|
@@ -16,6 +16,8 @@ from typing import Any
|
|
|
16
16
|
from .convergence_store import write_json_atomic
|
|
17
17
|
from .plan_derivations import extract_tokens, find_derivations
|
|
18
18
|
from .plan_items import PlanItemContractError, extract_plan_items
|
|
19
|
+
from .final_report_schema import load_schema_version
|
|
20
|
+
from .report_narrative import parse_narrative
|
|
19
21
|
from .user_response import parse_user_response_entries
|
|
20
22
|
from .verdict_blocks import (
|
|
21
23
|
PLAN_ITEM_VERDICTS,
|
|
@@ -57,10 +59,10 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
57
59
|
)
|
|
58
60
|
commands = parser.add_subparsers(dest="command", required=True)
|
|
59
61
|
extract = commands.add_parser("extract")
|
|
60
|
-
extract
|
|
62
|
+
_add_plan_source(extract)
|
|
61
63
|
extract.add_argument("--output", type=Path, required=True)
|
|
62
64
|
validate = commands.add_parser("validate")
|
|
63
|
-
validate
|
|
65
|
+
_add_plan_source(validate)
|
|
64
66
|
validate.add_argument("--items", type=Path, required=True)
|
|
65
67
|
collect = commands.add_parser(
|
|
66
68
|
"collect-verdicts",
|
|
@@ -84,12 +86,15 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
84
86
|
"seed",
|
|
85
87
|
help="create the planBodyVerification.planItems[] rows a round lands in",
|
|
86
88
|
)
|
|
87
|
-
seed
|
|
89
|
+
_add_plan_source(seed)
|
|
90
|
+
seed.add_argument("--state", type=Path)
|
|
88
91
|
apply_verdicts = commands.add_parser(
|
|
89
92
|
"apply-verdicts",
|
|
90
|
-
help="overwrite planBodyVerification.planItems[].verdicts in
|
|
93
|
+
help="overwrite planBodyVerification.planItems[].verdicts in owned state",
|
|
91
94
|
)
|
|
92
|
-
apply_verdicts.
|
|
95
|
+
target = apply_verdicts.add_mutually_exclusive_group(required=True)
|
|
96
|
+
target.add_argument("--data", type=Path)
|
|
97
|
+
target.add_argument("--state", type=Path)
|
|
93
98
|
apply_verdicts.add_argument("--verdicts", type=Path, required=True)
|
|
94
99
|
apply_verdicts.add_argument(
|
|
95
100
|
"--round", type=int, required=True, dest="round_number",
|
|
@@ -99,14 +104,35 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
99
104
|
return parser
|
|
100
105
|
|
|
101
106
|
|
|
107
|
+
def _add_plan_source(parser: argparse.ArgumentParser) -> None:
|
|
108
|
+
source = parser.add_mutually_exclusive_group(required=True)
|
|
109
|
+
source.add_argument("--data", type=Path)
|
|
110
|
+
source.add_argument("--narrative", type=Path)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _plan_source(args: argparse.Namespace) -> dict[str, Any]:
|
|
114
|
+
if args.data is not None:
|
|
115
|
+
return _load_json_object(args.data)
|
|
116
|
+
try:
|
|
117
|
+
markdown = args.narrative.read_text(encoding="utf-8")
|
|
118
|
+
except (OSError, UnicodeError) as exc:
|
|
119
|
+
raise PlanItemContractError(
|
|
120
|
+
f"cannot read report narrative {args.narrative}: {exc}"
|
|
121
|
+
) from exc
|
|
122
|
+
try:
|
|
123
|
+
return parse_narrative(markdown, load_schema_version("3.0"))
|
|
124
|
+
except ValueError as exc:
|
|
125
|
+
raise PlanItemContractError(f"invalid report narrative: {exc}") from exc
|
|
126
|
+
|
|
127
|
+
|
|
102
128
|
def _extract(args: argparse.Namespace) -> dict[str, Any]:
|
|
103
|
-
envelope = _envelope(
|
|
129
|
+
envelope = _envelope(_plan_source(args))
|
|
104
130
|
write_json_atomic(args.output, envelope)
|
|
105
131
|
return {"ok": True, "operation": "extract", "path": str(args.output)}
|
|
106
132
|
|
|
107
133
|
|
|
108
134
|
def _validate(args: argparse.Namespace) -> dict[str, Any]:
|
|
109
|
-
expected = _envelope(
|
|
135
|
+
expected = _envelope(_plan_source(args))
|
|
110
136
|
actual = _load_json_object(args.items)
|
|
111
137
|
if actual != expected:
|
|
112
138
|
raise PlanItemContractError("plan items envelope does not match deterministic extraction")
|
|
@@ -229,6 +255,45 @@ def _plan_body_items(data: dict[str, Any], data_path: Path) -> list[dict[str, An
|
|
|
229
255
|
return items
|
|
230
256
|
|
|
231
257
|
|
|
258
|
+
def _state_plan_body_items(
|
|
259
|
+
state: dict[str, Any], state_path: Path,
|
|
260
|
+
) -> list[dict[str, Any]]:
|
|
261
|
+
if state.get("owner") != "convergence":
|
|
262
|
+
raise PlanItemContractError(
|
|
263
|
+
f"plan-body state owner must be convergence: {state_path}"
|
|
264
|
+
)
|
|
265
|
+
verification = state.get("planBodyVerification")
|
|
266
|
+
if not isinstance(verification, Mapping):
|
|
267
|
+
raise PlanItemContractError(
|
|
268
|
+
f"planBodyVerification must be an object: {state_path}"
|
|
269
|
+
)
|
|
270
|
+
items = verification.get("planItems")
|
|
271
|
+
if not isinstance(items, list):
|
|
272
|
+
raise PlanItemContractError(
|
|
273
|
+
f"planBodyVerification.planItems must be an array: {state_path}"
|
|
274
|
+
)
|
|
275
|
+
return items
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _new_v3_state() -> dict[str, Any]:
|
|
279
|
+
return {
|
|
280
|
+
"schemaVersion": "1.1",
|
|
281
|
+
"owner": "convergence",
|
|
282
|
+
"planItems": [],
|
|
283
|
+
"roundHistory": [],
|
|
284
|
+
"selfFixRoundsApplied": 0,
|
|
285
|
+
"planBodyVerification": {
|
|
286
|
+
"roundCount": 0,
|
|
287
|
+
"gateResult": "passed",
|
|
288
|
+
"gateBlockedBy": [],
|
|
289
|
+
"selfFixRoundsApplied": 0,
|
|
290
|
+
"selfFixStopReason": "not-attempted",
|
|
291
|
+
"planItems": [],
|
|
292
|
+
"dissentLog": [],
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
232
297
|
def _derivations(args: argparse.Namespace) -> dict[str, Any]:
|
|
233
298
|
"""Candidate statements each answered clarification may have falsified.
|
|
234
299
|
|
|
@@ -283,9 +348,17 @@ def _seed(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
283
348
|
already applied, `carriedForwardFromSeq`, `selfFixNote` — because a re-seed
|
|
284
349
|
between rounds must not erase the round before it.
|
|
285
350
|
"""
|
|
286
|
-
extracted = _envelope(
|
|
287
|
-
|
|
288
|
-
|
|
351
|
+
extracted = _envelope(_plan_source(args))["items"]
|
|
352
|
+
if args.state is not None:
|
|
353
|
+
data = _load_json_object(args.state) if args.state.is_file() else _new_v3_state()
|
|
354
|
+
recorded = _state_plan_body_items(data, args.state)
|
|
355
|
+
target = args.state
|
|
356
|
+
else:
|
|
357
|
+
if args.data is None:
|
|
358
|
+
raise PlanItemContractError("--narrative requires --state")
|
|
359
|
+
data = _load_json_object(args.data)
|
|
360
|
+
recorded = _plan_body_items(data, args.data)
|
|
361
|
+
target = args.data
|
|
289
362
|
known = {
|
|
290
363
|
item.get("id")
|
|
291
364
|
for item in recorded
|
|
@@ -306,18 +379,38 @@ def _seed(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
306
379
|
if item["id"] not in known
|
|
307
380
|
]
|
|
308
381
|
recorded.extend(added)
|
|
309
|
-
|
|
382
|
+
if args.state is not None:
|
|
383
|
+
audit_rows = data.get("planItems")
|
|
384
|
+
if not isinstance(audit_rows, list):
|
|
385
|
+
raise PlanItemContractError("state planItems must be an array")
|
|
386
|
+
audited = {
|
|
387
|
+
item.get("id") for item in audit_rows if isinstance(item, Mapping)
|
|
388
|
+
}
|
|
389
|
+
audit_rows.extend(
|
|
390
|
+
{
|
|
391
|
+
"id": item["id"],
|
|
392
|
+
"subject": item["subject"],
|
|
393
|
+
"sourceSection": item["sourceSection"],
|
|
394
|
+
"ticketId": item.get("ticketId") or "unknown",
|
|
395
|
+
"rounds": [],
|
|
396
|
+
"clarificationId": None,
|
|
397
|
+
}
|
|
398
|
+
for item in extracted
|
|
399
|
+
if item["id"] not in audited
|
|
400
|
+
)
|
|
401
|
+
write_json_atomic(target, data)
|
|
310
402
|
return {
|
|
311
403
|
"ok": True,
|
|
312
404
|
"operation": "seed",
|
|
313
|
-
"path": str(
|
|
405
|
+
"path": str(target),
|
|
314
406
|
"seeded": len(added),
|
|
315
407
|
"existing": len(known),
|
|
316
408
|
}
|
|
317
409
|
|
|
318
410
|
|
|
319
411
|
def _apply_verdicts(args: argparse.Namespace) -> dict[str, Any]:
|
|
320
|
-
|
|
412
|
+
target = args.state or args.data
|
|
413
|
+
data = _load_json_object(target)
|
|
321
414
|
incoming = _load_json_object(args.verdicts).get("planItems")
|
|
322
415
|
if not isinstance(incoming, list):
|
|
323
416
|
raise PlanItemContractError("verdicts envelope has no `planItems` array")
|
|
@@ -326,7 +419,11 @@ def _apply_verdicts(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
326
419
|
for item in incoming
|
|
327
420
|
if isinstance(item, Mapping) and isinstance(item.get("id"), str)
|
|
328
421
|
}
|
|
329
|
-
recorded =
|
|
422
|
+
recorded = (
|
|
423
|
+
_state_plan_body_items(data, target)
|
|
424
|
+
if args.state is not None
|
|
425
|
+
else _plan_body_items(data, target)
|
|
426
|
+
)
|
|
330
427
|
known = {item.get("id") for item in recorded if isinstance(item, Mapping)}
|
|
331
428
|
missing = sorted(item_id for item_id in rows if item_id not in known)
|
|
332
429
|
if missing:
|
|
@@ -351,8 +448,8 @@ def _apply_verdicts(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
351
448
|
item["verdicts"] = [
|
|
352
449
|
{**row, "round": args.round_number} for row in rows[item["id"]]
|
|
353
450
|
]
|
|
354
|
-
write_json_atomic(
|
|
355
|
-
return {"ok": True, "operation": "apply-verdicts", "path": str(
|
|
451
|
+
write_json_atomic(target, data)
|
|
452
|
+
return {"ok": True, "operation": "apply-verdicts", "path": str(target)}
|
|
356
453
|
|
|
357
454
|
|
|
358
455
|
_HANDLERS = {
|
|
@@ -15,7 +15,8 @@ class WorkerDispatchRequest:
|
|
|
15
15
|
workspace_root: Path | None = None
|
|
16
16
|
okstra_bin: Path | None = None
|
|
17
17
|
requested_workers: tuple[str, ...] = ()
|
|
18
|
-
|
|
18
|
+
# 기본은 "정하지 않음" 이다. 그래야 엔트리포인트가 역할의 예산을 쓴다.
|
|
19
|
+
idle_timeout_seconds: int | None = None
|
|
19
20
|
dispatch_kind: str = "initial"
|
|
20
21
|
jobs_file: Path | None = None
|
|
21
22
|
|