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
|
@@ -32,6 +32,7 @@ from okstra_project import project_json_path, upsert_project_json
|
|
|
32
32
|
from okstra_project.state import slugify
|
|
33
33
|
from . import fix_cycles
|
|
34
34
|
from .analysis_packet import build_analysis_packet
|
|
35
|
+
from .stage_ledger import build_stage_ledger, render_stage_ledger
|
|
35
36
|
from .analysis_inputs import (
|
|
36
37
|
ANALYSIS_TASK_TYPES,
|
|
37
38
|
AnalysisInputError,
|
|
@@ -66,7 +67,11 @@ from .material import (
|
|
|
66
67
|
resolve_related_tasks,
|
|
67
68
|
)
|
|
68
69
|
from .final_report_schema import load_schema_version
|
|
69
|
-
from .final_report_paths import
|
|
70
|
+
from .final_report_paths import (
|
|
71
|
+
final_report_data_path as _final_report_data_path,
|
|
72
|
+
final_report_markdown_path as _final_report_markdown_path,
|
|
73
|
+
require_approved_plan_record as _require_approved_plan_record_path,
|
|
74
|
+
)
|
|
70
75
|
from .lead_events import LeadEvent, append_lead_event
|
|
71
76
|
from .assignment_environment import load_assignment_context
|
|
72
77
|
from .assignment_resolver import (
|
|
@@ -197,9 +202,9 @@ from .scope_provenance import brief_end_state_id_sequence
|
|
|
197
202
|
# Frontmatter approval-flag matcher.
|
|
198
203
|
#
|
|
199
204
|
# Final-report 의 YAML frontmatter 안에서 `approved: true` / `approved: false`
|
|
200
|
-
# 한 줄만 식별한다.
|
|
201
|
-
#
|
|
202
|
-
#
|
|
205
|
+
# 한 줄만 식별한다. schema-v1 에서는 이 줄이 정본이고, schema-v2 에서는
|
|
206
|
+
# 정본 `frontmatter.approved` 의 표시다. 본문(body) 의 다른 `approved:` 등장과
|
|
207
|
+
# 충돌하지 않도록 호출자는 frontmatter 블록을 먼저 추출
|
|
203
208
|
# (`_extract_frontmatter_block`) 한 뒤 이 패턴을 적용한다.
|
|
204
209
|
APPROVED_FRONTMATTER_PATTERN = re.compile(
|
|
205
210
|
r"^approved:[ \t]+(true|false)[ \t]*$",
|
|
@@ -216,16 +221,14 @@ BLOCKING_PLAN_BODY_GATES = {"blocked-by-disagreement", "aborted-non-result"}
|
|
|
216
221
|
#
|
|
217
222
|
# `approved:` 바로 아래 줄의 `implementation-option:` 한 줄을 식별한다.
|
|
218
223
|
# report-writer 가 빈 값으로 항상 emit 하므로 라인은 존재하되 값은 비어 있을
|
|
219
|
-
# 수 있다. `--implementation-option <name>` CLI 가 이 줄의
|
|
220
|
-
#
|
|
224
|
+
# 수 있다. `--implementation-option <name>` CLI 가 schema-v1 에서 이 줄의
|
|
225
|
+
# 값만 치환한다. schema-v2 는 정본 `frontmatter.implementationOption` 을 쓰고
|
|
226
|
+
# 열람본을 다시 렌더한다. 값이 비면 implementation 은 plan 의
|
|
227
|
+
# `Recommended Option` 으로 폴백한다.
|
|
221
228
|
IMPLEMENTATION_OPTION_FRONTMATTER_PATTERN = re.compile(
|
|
222
229
|
r"^implementation-option:[ \t]*(.*)$",
|
|
223
230
|
re.MULTILINE,
|
|
224
231
|
)
|
|
225
|
-
SELECTED_DIRECTION_FRONTMATTER_PATTERN = re.compile(
|
|
226
|
-
r"^selected-direction-ref:[ \t]*(.*)$",
|
|
227
|
-
re.MULTILINE,
|
|
228
|
-
)
|
|
229
232
|
|
|
230
233
|
# validators/validate-run.py:_FRONTMATTER_BLOCK_RE 의 미러 — 선행 BOM/빈 줄을
|
|
231
234
|
# 허용해 두 모듈이 같은 리포트의 frontmatter 게이트를 동일하게 판정하게 한다.
|
|
@@ -267,40 +270,11 @@ def _data_json_gate_result(data: dict) -> str:
|
|
|
267
270
|
return str(verification.get("gateResult") or "").strip().lower()
|
|
268
271
|
|
|
269
272
|
|
|
270
|
-
def
|
|
271
|
-
|
|
272
|
-
if gate_match:
|
|
273
|
-
gate_value = gate_match.group("value").strip().lower()
|
|
274
|
-
if gate_value in BLOCKING_PLAN_BODY_GATES:
|
|
275
|
-
raise PrepareError(
|
|
276
|
-
f"{action} rejected because approved plan Gate result is "
|
|
277
|
-
f"`{gate_value}`: {path}\n"
|
|
278
|
-
" resolve the plan-body verification disagreement and regenerate "
|
|
279
|
-
"the implementation-planning report before approval."
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
loaded = _load_final_report_data_if_present(path)
|
|
283
|
-
if loaded is None:
|
|
284
|
-
return
|
|
285
|
-
data_path, data = loaded
|
|
286
|
-
data_gate = _data_json_gate_result(data)
|
|
287
|
-
if data_gate in BLOCKING_PLAN_BODY_GATES:
|
|
288
|
-
raise PrepareError(
|
|
289
|
-
f"{action} rejected because approved plan data.json Gate result is "
|
|
290
|
-
f"`{data_gate}`: {data_path}\n"
|
|
291
|
-
" data.json is the final-report source of truth; regenerate the "
|
|
292
|
-
"report after resolving the verification disagreement."
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def _validate_data_json_approval_consistency(
|
|
297
|
-
path: Path,
|
|
298
|
-
*,
|
|
299
|
-
markdown_approved: bool,
|
|
300
|
-
) -> None:
|
|
273
|
+
def _record_approved_flag(path: Path) -> bool | None:
|
|
274
|
+
"""정본 `frontmatter.approved`. 정본이 없으면(schema-v1) None."""
|
|
301
275
|
loaded = _load_final_report_data_if_present(path)
|
|
302
276
|
if loaded is None:
|
|
303
|
-
return
|
|
277
|
+
return None
|
|
304
278
|
data_path, data = loaded
|
|
305
279
|
frontmatter = data.get("frontmatter")
|
|
306
280
|
if not isinstance(frontmatter, dict) or "approved" not in frontmatter:
|
|
@@ -312,15 +286,42 @@ def _validate_data_json_approval_consistency(
|
|
|
312
286
|
raise PrepareError(
|
|
313
287
|
f"approved plan data.json frontmatter.approved is not boolean: {data_path}"
|
|
314
288
|
)
|
|
315
|
-
|
|
289
|
+
return data_approved
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _unapproved_plan_message(path: str, displayed: str) -> str:
|
|
293
|
+
return (
|
|
294
|
+
f"approved plan is not yet approved (frontmatter `approved: {displayed}`): "
|
|
295
|
+
f"{path}\n"
|
|
296
|
+
" re-run okstra with `--approve`, or confirm approval in the "
|
|
297
|
+
"in-session wizard.\n"
|
|
298
|
+
" resolve any `Blocks=approval` rows in `## 1. Clarification Items` first."
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _reject_blocking_plan_body_gate(path: Path, body: str, *, action: str) -> None:
|
|
303
|
+
loaded = _load_final_report_data_if_present(path)
|
|
304
|
+
if loaded is not None:
|
|
305
|
+
data_path, data = loaded
|
|
306
|
+
data_gate = _data_json_gate_result(data)
|
|
307
|
+
if data_gate in BLOCKING_PLAN_BODY_GATES:
|
|
308
|
+
raise PrepareError(
|
|
309
|
+
f"{action} rejected because approved plan data.json Gate result is "
|
|
310
|
+
f"`{data_gate}`: {data_path}\n"
|
|
311
|
+
" resolve the plan-body verification disagreement and regenerate "
|
|
312
|
+
"the implementation-planning report before approval."
|
|
313
|
+
)
|
|
314
|
+
return
|
|
315
|
+
gate_match = PLAN_BODY_GATE_PATTERN.search(body)
|
|
316
|
+
if not gate_match:
|
|
317
|
+
return
|
|
318
|
+
gate_value = gate_match.group("value").strip().lower()
|
|
319
|
+
if gate_value in BLOCKING_PLAN_BODY_GATES:
|
|
316
320
|
raise PrepareError(
|
|
317
|
-
"
|
|
318
|
-
f"`
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
f" data.json: {data_path}\n"
|
|
322
|
-
" regenerate the report from data.json or use `okstra --approve` "
|
|
323
|
-
"so the source of truth is updated first."
|
|
321
|
+
f"{action} rejected because approved plan Gate result is "
|
|
322
|
+
f"`{gate_value}`: {path}\n"
|
|
323
|
+
" resolve the plan-body verification disagreement and regenerate "
|
|
324
|
+
"the implementation-planning report before approval."
|
|
324
325
|
)
|
|
325
326
|
|
|
326
327
|
|
|
@@ -361,24 +362,10 @@ def _validate_approved_plan_conformance(path: Path) -> None:
|
|
|
361
362
|
)
|
|
362
363
|
|
|
363
364
|
|
|
364
|
-
def
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
data_path, data = loaded
|
|
369
|
-
data_gate = _data_json_gate_result(data)
|
|
370
|
-
if data_gate in BLOCKING_PLAN_BODY_GATES:
|
|
371
|
-
raise PrepareError(
|
|
372
|
-
f"--approve rejected because approved plan data.json Gate result is "
|
|
373
|
-
f"`{data_gate}`: {data_path}"
|
|
374
|
-
)
|
|
375
|
-
frontmatter = data.setdefault("frontmatter", {})
|
|
376
|
-
if not isinstance(frontmatter, dict):
|
|
377
|
-
raise PrepareError(
|
|
378
|
-
f"approved plan data.json frontmatter must be an object: {data_path}"
|
|
379
|
-
)
|
|
380
|
-
frontmatter["approved"] = True
|
|
381
|
-
|
|
365
|
+
def _commit_data_json_and_rerender(
|
|
366
|
+
record_path: Path, data: dict, *, action: str
|
|
367
|
+
) -> None:
|
|
368
|
+
"""정본을 쓰고 전문 열람본을 그 정본에서 다시 렌더한다."""
|
|
382
369
|
try:
|
|
383
370
|
from .render_final_report import (
|
|
384
371
|
FinalReportRenderError,
|
|
@@ -392,11 +379,12 @@ def _set_data_json_approved_true_if_present(path: Path) -> bool:
|
|
|
392
379
|
)
|
|
393
380
|
except FinalReportRenderError as exc:
|
|
394
381
|
raise PrepareError(
|
|
395
|
-
f"
|
|
382
|
+
f"{action} could not re-render approved plan from data.json: {exc}"
|
|
396
383
|
) from exc
|
|
397
384
|
|
|
398
|
-
|
|
399
|
-
|
|
385
|
+
md_path = _final_report_markdown_path(record_path)
|
|
386
|
+
data_tmp = record_path.with_suffix(record_path.suffix + f".tmp.{os.getpid()}")
|
|
387
|
+
md_tmp = md_path.with_suffix(md_path.suffix + f".tmp.{os.getpid()}")
|
|
400
388
|
data_tmp.write_text(
|
|
401
389
|
json.dumps(data, ensure_ascii=False, indent=2) + "\n",
|
|
402
390
|
encoding="utf-8",
|
|
@@ -404,16 +392,34 @@ def _set_data_json_approved_true_if_present(path: Path) -> bool:
|
|
|
404
392
|
md_tmp.write_text(rendered, encoding="utf-8")
|
|
405
393
|
# 두 파일을 같이 atomic 하게 commit 할 수 없으므로, 첫 replace 직전의
|
|
406
394
|
# data.json 바이트를 보관했다가 markdown replace 가 실패하면 data.json 을
|
|
407
|
-
# 되돌린다.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
data_prev = data_path.read_bytes()
|
|
411
|
-
data_tmp.replace(data_path)
|
|
395
|
+
# 되돌린다. 남는 불일치 창은 두 replace 사이의 hard process kill 뿐이다.
|
|
396
|
+
data_prev = record_path.read_bytes()
|
|
397
|
+
data_tmp.replace(record_path)
|
|
412
398
|
try:
|
|
413
|
-
md_tmp.replace(
|
|
399
|
+
md_tmp.replace(md_path)
|
|
414
400
|
except OSError:
|
|
415
|
-
|
|
401
|
+
record_path.write_bytes(data_prev)
|
|
416
402
|
raise
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _set_data_json_approved_true_if_present(path: Path) -> bool:
|
|
406
|
+
loaded = _load_final_report_data_if_present(path)
|
|
407
|
+
if loaded is None:
|
|
408
|
+
return False
|
|
409
|
+
data_path, data = loaded
|
|
410
|
+
data_gate = _data_json_gate_result(data)
|
|
411
|
+
if data_gate in BLOCKING_PLAN_BODY_GATES:
|
|
412
|
+
raise PrepareError(
|
|
413
|
+
f"--approve rejected because approved plan data.json Gate result is "
|
|
414
|
+
f"`{data_gate}`: {data_path}"
|
|
415
|
+
)
|
|
416
|
+
frontmatter = data.setdefault("frontmatter", {})
|
|
417
|
+
if not isinstance(frontmatter, dict):
|
|
418
|
+
raise PrepareError(
|
|
419
|
+
f"approved plan data.json frontmatter must be an object: {data_path}"
|
|
420
|
+
)
|
|
421
|
+
frontmatter["approved"] = True
|
|
422
|
+
_commit_data_json_and_rerender(data_path, data, action="--approve")
|
|
417
423
|
return True
|
|
418
424
|
|
|
419
425
|
|
|
@@ -521,35 +527,19 @@ def _default(name: str, fallback: str) -> str:
|
|
|
521
527
|
return os.environ.get(name, "") or fallback
|
|
522
528
|
|
|
523
529
|
|
|
530
|
+
def _approved_plan_record(path: str) -> Path:
|
|
531
|
+
try:
|
|
532
|
+
return _require_approved_plan_record_path(Path(path))
|
|
533
|
+
except ValueError as exc:
|
|
534
|
+
raise PrepareError(str(exc)) from exc
|
|
535
|
+
|
|
536
|
+
|
|
524
537
|
def _validate_approved_plan(path: str) -> None:
|
|
525
|
-
p =
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if frontmatter is None:
|
|
531
|
-
raise PrepareError(
|
|
532
|
-
f"approved plan has no YAML frontmatter block: {path}\n"
|
|
533
|
-
" expected the report to begin with `---\\n...\\n---\\n`. "
|
|
534
|
-
"report-writer worker emits this header on every run; "
|
|
535
|
-
"regenerate the report if the header is missing."
|
|
536
|
-
)
|
|
537
|
-
m = APPROVED_FRONTMATTER_PATTERN.search(frontmatter)
|
|
538
|
-
if not m:
|
|
539
|
-
raise PrepareError(
|
|
540
|
-
f"approved plan frontmatter has no `approved:` field: {path}\n"
|
|
541
|
-
" expected a single line of the form `approved: true` "
|
|
542
|
-
"(or `approved: false` for the unflipped state)."
|
|
543
|
-
)
|
|
544
|
-
if m.group(1).lower() != "true":
|
|
545
|
-
raise PrepareError(
|
|
546
|
-
f"approved plan is not yet approved (frontmatter `approved: {m.group(1)}`): {path}\n"
|
|
547
|
-
" open the report and change the frontmatter line to `approved: true`, "
|
|
548
|
-
"or re-run okstra with `--approve` to flip it from the CLI.\n"
|
|
549
|
-
" resolve any `Blocks=approval` rows in `## 1. Clarification Items` first."
|
|
550
|
-
)
|
|
551
|
-
_reject_blocking_plan_body_gate(p, body, action="approved plan validation")
|
|
552
|
-
_validate_data_json_approval_consistency(p, markdown_approved=True)
|
|
538
|
+
p = _approved_plan_record(path)
|
|
539
|
+
record_approved = _record_approved_flag(p)
|
|
540
|
+
if record_approved is not True:
|
|
541
|
+
raise PrepareError(_unapproved_plan_message(str(p), "false"))
|
|
542
|
+
_reject_blocking_plan_body_gate(p, "", action="approved plan validation")
|
|
553
543
|
_validate_approved_plan_conformance(p)
|
|
554
544
|
# frontmatter approved == true 상태. §1 Clarification Items 의
|
|
555
545
|
# Blocks=approval 행이 아직 open/answered 면 승인을 무효화한다.
|
|
@@ -607,129 +597,37 @@ def _parse_stage_map_into_ctx(plan_path: str) -> list:
|
|
|
607
597
|
) from exc
|
|
608
598
|
|
|
609
599
|
|
|
610
|
-
def _apply_cli_approval(path: str) ->
|
|
611
|
-
"""`--approve` 가 지정된 경우
|
|
600
|
+
def _apply_cli_approval(path: str) -> None:
|
|
601
|
+
"""`--approve` 가 지정된 경우 정본 `frontmatter.approved` 를 true 로 토글.
|
|
612
602
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
audit 라인을 append → `"frontmatter-flipped"` 반환.
|
|
616
|
-
- 이미 `approved: true` 면 한 번도 기록되지 않은 audit 라인만 append →
|
|
617
|
-
`"already-approved-audit-appended"`, 이미 동일 audit 가 있으면
|
|
618
|
-
`"already-approved"` 로 no-op.
|
|
619
|
-
- frontmatter 가 없거나 `approved:` 라인이 없으면 PrepareError.
|
|
620
|
-
|
|
621
|
-
Idempotent: 동일 audit 라인은 한 번만 기록된다.
|
|
603
|
+
정본을 쓰고 열람본을 다시 렌더한다. 이미 true 면 아무 것도 쓰지 않는다.
|
|
604
|
+
schema-v1 계획은 정본이 없어 `--approved-plan` 으로 받을 수 없다.
|
|
622
605
|
"""
|
|
623
|
-
p =
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if frontmatter is None:
|
|
629
|
-
raise PrepareError(
|
|
630
|
-
f"--approve was given but the approved-plan file has no YAML frontmatter: {path}\n"
|
|
631
|
-
" expected the report to begin with `---\\n...\\n---\\n`."
|
|
632
|
-
)
|
|
633
|
-
m = APPROVED_FRONTMATTER_PATTERN.search(frontmatter)
|
|
634
|
-
if not m:
|
|
606
|
+
p = _approved_plan_record(path)
|
|
607
|
+
_reject_blocking_plan_body_gate(p, "", action="--approve")
|
|
608
|
+
if _record_approved_flag(p) is True:
|
|
609
|
+
return
|
|
610
|
+
if not _set_data_json_approved_true_if_present(p):
|
|
635
611
|
raise PrepareError(
|
|
636
|
-
f"--approve
|
|
637
|
-
" expected a single line of the form `approved: false` "
|
|
638
|
-
"(report-writer worker emits this by default)."
|
|
612
|
+
f"--approve found a report record but could not update it: {p}"
|
|
639
613
|
)
|
|
640
|
-
_reject_blocking_plan_body_gate(p, body, action="--approve")
|
|
641
|
-
|
|
642
|
-
audit_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
643
|
-
audit_line = (
|
|
644
|
-
f"- 승인 일시 (CLI ack): {audit_iso} — recorded by `okstra --approve` "
|
|
645
|
-
"(user CLI invocation treated as approval signal)"
|
|
646
|
-
)
|
|
647
|
-
|
|
648
|
-
rendered_from_data = False
|
|
649
|
-
if m.group(1).lower() != "true":
|
|
650
|
-
rendered_from_data = _set_data_json_approved_true_if_present(p)
|
|
651
|
-
if rendered_from_data:
|
|
652
|
-
body = p.read_text(encoding="utf-8", errors="replace")
|
|
653
|
-
frontmatter = _extract_frontmatter_block(body) or ""
|
|
654
|
-
m = APPROVED_FRONTMATTER_PATTERN.search(frontmatter)
|
|
655
|
-
if not m:
|
|
656
|
-
raise PrepareError(
|
|
657
|
-
f"--approve re-rendered the approved-plan but the markdown "
|
|
658
|
-
f"frontmatter has no `approved:` field: {path}"
|
|
659
|
-
)
|
|
660
|
-
|
|
661
|
-
if m.group(1).lower() == "true":
|
|
662
|
-
if audit_line.split(" — ")[1] in body:
|
|
663
|
-
return "already-approved"
|
|
664
|
-
new_body = body.rstrip("\n") + "\n" + audit_line + "\n"
|
|
665
|
-
p.write_text(new_body, encoding="utf-8")
|
|
666
|
-
if rendered_from_data:
|
|
667
|
-
return "frontmatter-flipped"
|
|
668
|
-
return "already-approved-audit-appended"
|
|
669
|
-
|
|
670
|
-
flipped_frontmatter = APPROVED_FRONTMATTER_PATTERN.sub(
|
|
671
|
-
"approved: true", frontmatter, count=1,
|
|
672
|
-
)
|
|
673
|
-
new_body = body.replace(frontmatter, flipped_frontmatter, 1)
|
|
674
|
-
new_body = new_body.rstrip("\n") + "\n" + audit_line + "\n"
|
|
675
|
-
p.write_text(new_body, encoding="utf-8")
|
|
676
|
-
return "frontmatter-flipped"
|
|
677
|
-
|
|
678
614
|
|
|
679
|
-
def _apply_cli_implementation_option(path: str, option_name: str) -> str:
|
|
680
|
-
"""`--implementation-option <name>` 이 지정된 경우 approved-plan frontmatter 의
|
|
681
|
-
`implementation-option:` 라인 값을 `option_name` 으로 치환한다.
|
|
682
615
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
(
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
"""
|
|
693
|
-
p = Path(path)
|
|
694
|
-
if not p.is_file():
|
|
695
|
-
raise PrepareError(f"approved plan file not found: {path}")
|
|
696
|
-
body = p.read_text(encoding="utf-8", errors="replace")
|
|
697
|
-
frontmatter = _extract_frontmatter_block(body)
|
|
698
|
-
if frontmatter is None:
|
|
699
|
-
raise PrepareError(
|
|
700
|
-
f"--implementation-option was given but the approved-plan file has no YAML frontmatter: {path}\n"
|
|
701
|
-
" expected the report to begin with `---\\n...\\n---\\n`."
|
|
702
|
-
)
|
|
703
|
-
if not IMPLEMENTATION_OPTION_FRONTMATTER_PATTERN.search(frontmatter):
|
|
616
|
+
def _apply_cli_implementation_option(path: str, option_name: str) -> None:
|
|
617
|
+
"""`--implementation-option <name>` 을 정본에 쓰고 열람본을 다시 렌더한다."""
|
|
618
|
+
p = _approved_plan_record(path)
|
|
619
|
+
loaded = _load_final_report_data_if_present(p)
|
|
620
|
+
if loaded is None:
|
|
621
|
+
raise PrepareError(f"approved plan data.json disappeared: {p}")
|
|
622
|
+
data_path, data = loaded
|
|
623
|
+
frontmatter = data.get("frontmatter")
|
|
624
|
+
if not isinstance(frontmatter, dict) or "implementationOption" not in frontmatter:
|
|
704
625
|
raise PrepareError(
|
|
705
|
-
f"--implementation-option was given but the approved-plan
|
|
706
|
-
f"
|
|
707
|
-
" expected a single line of the form `implementation-option:` "
|
|
708
|
-
"(report-writer worker emits this empty by default)."
|
|
626
|
+
f"--implementation-option was given but the approved-plan "
|
|
627
|
+
f"data.json has no frontmatter.implementationOption field: {data_path}"
|
|
709
628
|
)
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
audit_line = (
|
|
713
|
-
f"- 선택 옵션 (CLI): {audit_iso} — `{option_name}` recorded by "
|
|
714
|
-
"`okstra --implementation-option` (user-selected Option Candidate)"
|
|
715
|
-
)
|
|
716
|
-
|
|
717
|
-
# 렌더 경로(`final-report.template.md` 의 `yaml_scalar`)와 동일하게 값을
|
|
718
|
-
# YAML 따옴표 처리한다. 옵션명은 보통 `: `(콜론+공백)를 포함하므로 따옴표
|
|
719
|
-
# 없이 쓰면 frontmatter 가 invalid YAML 이 되고 CLI/렌더 출력이 어긋난다.
|
|
720
|
-
# 함수 치환을 써서 quoted 값 안의 백슬래시/그룹 참조가 re 치환 문법으로
|
|
721
|
-
# 재해석되는 것까지 막는다 (리터럴 치환).
|
|
722
|
-
from .render_final_report import _yaml_scalar
|
|
723
|
-
|
|
724
|
-
quoted = _yaml_scalar(option_name)
|
|
725
|
-
set_frontmatter = IMPLEMENTATION_OPTION_FRONTMATTER_PATTERN.sub(
|
|
726
|
-
lambda _m: f"implementation-option: {quoted}", frontmatter, count=1,
|
|
727
|
-
)
|
|
728
|
-
new_body = body.replace(frontmatter, set_frontmatter, 1)
|
|
729
|
-
if audit_line.split(" — ")[1] not in new_body:
|
|
730
|
-
new_body = new_body.rstrip("\n") + "\n" + audit_line + "\n"
|
|
731
|
-
p.write_text(new_body, encoding="utf-8")
|
|
732
|
-
return "frontmatter-set"
|
|
629
|
+
frontmatter["implementationOption"] = option_name
|
|
630
|
+
_commit_data_json_and_rerender(data_path, data, action="--implementation-option")
|
|
733
631
|
|
|
734
632
|
|
|
735
633
|
def _ensure_task_directories(ctx: dict) -> None:
|
|
@@ -802,7 +700,7 @@ def _record_start(
|
|
|
802
700
|
workers=[w for w in ctx.get("RECOMMENDED_ANALYSERS", "").split(",") if w],
|
|
803
701
|
lead_model=ctx.get("LEAD_MODEL", ""),
|
|
804
702
|
run_dir_rel=ctx.get("RUN_DIR_RELATIVE_PATH", ""),
|
|
805
|
-
|
|
703
|
+
final_report_record_rel=ctx.get("FINAL_REPORT_RECORD_RELATIVE_PATH", ""),
|
|
806
704
|
final_status_rel=ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
|
|
807
705
|
argv=canonical_argv,
|
|
808
706
|
cwd=cwd,
|
|
@@ -1204,7 +1102,7 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
|
|
|
1204
1102
|
|
|
1205
1103
|
|
|
1206
1104
|
_PLANNING_REPORT_RE = re.compile(
|
|
1207
|
-
r"^final-report-implementation-planning-\d{3,}\.md$"
|
|
1105
|
+
r"^final-report-implementation-planning-\d{3,}\.(?:md|data\.json)$"
|
|
1208
1106
|
)
|
|
1209
1107
|
|
|
1210
1108
|
|
|
@@ -1343,37 +1241,11 @@ def _validate_reverify_scope(inp: PrepareInputs) -> None:
|
|
|
1343
1241
|
def _implementation_plan_contract(
|
|
1344
1242
|
inp: PrepareInputs,
|
|
1345
1243
|
) -> tuple[str, Path, dict | None]:
|
|
1346
|
-
"""Classify an implementation plan from its
|
|
1347
|
-
plan_path =
|
|
1348
|
-
body = plan_path.read_text(encoding="utf-8", errors="replace")
|
|
1349
|
-
frontmatter = _extract_frontmatter_block(body) or ""
|
|
1350
|
-
markdown_ref_match = SELECTED_DIRECTION_FRONTMATTER_PATTERN.search(frontmatter)
|
|
1351
|
-
markdown_ref: str | None = None
|
|
1352
|
-
if markdown_ref_match is not None:
|
|
1353
|
-
markdown_ref = markdown_ref_match.group(1).strip()
|
|
1354
|
-
if markdown_ref.startswith('"'):
|
|
1355
|
-
try:
|
|
1356
|
-
markdown_ref = json.loads(markdown_ref)
|
|
1357
|
-
except json.JSONDecodeError as exc:
|
|
1358
|
-
raise PrepareError(
|
|
1359
|
-
"selected-direction plan markdown `selected-direction-ref` is "
|
|
1360
|
-
"not a valid quoted string"
|
|
1361
|
-
) from exc
|
|
1362
|
-
elif len(markdown_ref) >= 2 and markdown_ref[0] == markdown_ref[-1] == "'":
|
|
1363
|
-
markdown_ref = markdown_ref[1:-1].replace("''", "'")
|
|
1364
|
-
if not isinstance(markdown_ref, str) or not markdown_ref.strip():
|
|
1365
|
-
raise PrepareError(
|
|
1366
|
-
"selected-direction plan markdown `selected-direction-ref` must "
|
|
1367
|
-
"be a non-empty string"
|
|
1368
|
-
)
|
|
1244
|
+
"""Classify an implementation plan from its report record."""
|
|
1245
|
+
plan_path = _approved_plan_record(inp.approved_plan_path)
|
|
1369
1246
|
loaded = _load_final_report_data_if_present(plan_path)
|
|
1370
1247
|
if loaded is None:
|
|
1371
|
-
|
|
1372
|
-
raise PrepareError(
|
|
1373
|
-
"selected-direction plan requires its sibling data.json before "
|
|
1374
|
-
f"implementation entry: {_final_report_data_path(plan_path)}"
|
|
1375
|
-
)
|
|
1376
|
-
return "legacy", plan_path, None
|
|
1248
|
+
raise PrepareError(f"approved plan record could not be read: {plan_path}")
|
|
1377
1249
|
_, data = loaded
|
|
1378
1250
|
planning = data.get("implementationPlanning")
|
|
1379
1251
|
if not isinstance(planning, dict):
|
|
@@ -1392,11 +1264,10 @@ def _implementation_plan_contract(
|
|
|
1392
1264
|
)
|
|
1393
1265
|
)
|
|
1394
1266
|
if contract is None:
|
|
1395
|
-
if
|
|
1267
|
+
if selected_payload:
|
|
1396
1268
|
raise PrepareError(
|
|
1397
1269
|
"selected-direction markers require "
|
|
1398
|
-
"implementationPlanning.planningContract=`selected-direction
|
|
1399
|
-
"the markdown `selected-direction-ref` and data contract disagree"
|
|
1270
|
+
"implementationPlanning.planningContract=`selected-direction`"
|
|
1400
1271
|
)
|
|
1401
1272
|
return "legacy", plan_path, data
|
|
1402
1273
|
if contract != "selected-direction":
|
|
@@ -1404,27 +1275,6 @@ def _implementation_plan_contract(
|
|
|
1404
1275
|
"approved plan sibling data.json has unsupported planningContract "
|
|
1405
1276
|
f"{contract!r}"
|
|
1406
1277
|
)
|
|
1407
|
-
if markdown_ref_match is None:
|
|
1408
|
-
raise PrepareError(
|
|
1409
|
-
"selected-direction plan markdown frontmatter must contain "
|
|
1410
|
-
"`selected-direction-ref:`"
|
|
1411
|
-
)
|
|
1412
|
-
selected_ref = planning.get("selectedDirectionRef")
|
|
1413
|
-
snapshot_ref = (
|
|
1414
|
-
selected_ref.get("snapshotPath")
|
|
1415
|
-
if isinstance(selected_ref, dict)
|
|
1416
|
-
else None
|
|
1417
|
-
)
|
|
1418
|
-
if not isinstance(markdown_ref, str) or markdown_ref != snapshot_ref:
|
|
1419
|
-
raise PrepareError(
|
|
1420
|
-
"selected-direction plan markdown `selected-direction-ref` must "
|
|
1421
|
-
"exactly match implementationPlanning.selectedDirectionRef.snapshotPath"
|
|
1422
|
-
)
|
|
1423
|
-
if IMPLEMENTATION_OPTION_FRONTMATTER_PATTERN.search(frontmatter):
|
|
1424
|
-
raise PrepareError(
|
|
1425
|
-
"selected-direction plan must not contain an `implementation-option:` "
|
|
1426
|
-
"frontmatter field"
|
|
1427
|
-
)
|
|
1428
1278
|
return "selected-direction", plan_path, data
|
|
1429
1279
|
|
|
1430
1280
|
|
|
@@ -3361,6 +3211,11 @@ def _write_instruction_set_sources(
|
|
|
3361
3211
|
instruction_set_relative_path=ctx["INSTRUCTION_SET_RELATIVE_PATH"],
|
|
3362
3212
|
fix_history_text=fix_cycles.packet_summary(
|
|
3363
3213
|
fix_cycles.read_rows(Path(ctx["TASK_MANIFEST_PATH"]).parent)),
|
|
3214
|
+
stage_ledger_json=(
|
|
3215
|
+
render_stage_ledger(
|
|
3216
|
+
build_stage_ledger(Path(ctx["TASK_MANIFEST_PATH"]).parent))
|
|
3217
|
+
if inp.task_type == "implementation-planning" else ""
|
|
3218
|
+
),
|
|
3364
3219
|
)
|
|
3365
3220
|
if inp.task_type in ANALYSIS_TASK_TYPES:
|
|
3366
3221
|
packet += (
|
|
@@ -3620,7 +3475,7 @@ def _maybe_open_fix_cycle(inp: PrepareInputs, task_root: Path, existing: dict,
|
|
|
3620
3475
|
"(workflow.lastCompletedPhase 확인)")
|
|
3621
3476
|
brief_text = Path(inp.brief_path).read_text(encoding="utf-8")
|
|
3622
3477
|
fix_cycles.append_opened(
|
|
3623
|
-
task_root, target_report=str(existing.get("
|
|
3478
|
+
task_root, target_report=str(existing.get("latestReportRecordPath", "")),
|
|
3624
3479
|
symptom=fix_cycles.derive_symptom(brief_text), opened_at=now)
|
|
3625
3480
|
return fix_cycles.open_cycle(fix_cycles.read_rows(task_root))
|
|
3626
3481
|
|
|
@@ -3650,7 +3505,7 @@ def _record_fix_cycle_events(inp: PrepareInputs, ctx: dict) -> str:
|
|
|
3650
3505
|
if handoff_attached:
|
|
3651
3506
|
fix_cycles.append_closed(
|
|
3652
3507
|
task_root, cycle=open_c["cycle"], closed_by="release-handoff",
|
|
3653
|
-
report=str(existing.get("
|
|
3508
|
+
report=str(existing.get("latestReportRecordPath", "")), closed_at=now)
|
|
3654
3509
|
open_c = None
|
|
3655
3510
|
|
|
3656
3511
|
if inp.fix_cycle == "yes" and open_c is None:
|
|
@@ -3680,7 +3535,7 @@ def _finalize_status_and_render_manifests(
|
|
|
3680
3535
|
ctx["CURRENT_TASK_STATUS"] = "lead-session-started"
|
|
3681
3536
|
ctx["CURRENT_RUN_STATUS"] = "in-progress"
|
|
3682
3537
|
ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_PATH"]
|
|
3683
|
-
ctx["
|
|
3538
|
+
ctx["LATEST_REPORT_RECORD_RELATIVE_PATH"] = ctx["FINAL_REPORT_RECORD_RELATIVE_PATH"]
|
|
3684
3539
|
ctx.update(compute_workflow_state(
|
|
3685
3540
|
task_type=inp.task_type,
|
|
3686
3541
|
current_run_status=ctx["CURRENT_RUN_STATUS"],
|
|
@@ -4330,7 +4185,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4330
4185
|
"VALIDATION_UPDATED_AT": "",
|
|
4331
4186
|
"VALIDATION_FAILURES_JSON": "[]",
|
|
4332
4187
|
"LATEST_REPORT_PATH": "",
|
|
4333
|
-
"
|
|
4188
|
+
"LATEST_REPORT_RECORD_RELATIVE_PATH": "",
|
|
4334
4189
|
"RENDER_ONLY": "true" if inp.render_only else "false",
|
|
4335
4190
|
"OKSTRA_VERSION": installed_version(),
|
|
4336
4191
|
"EXECUTION_IDENTITY_JSON": json.dumps(
|
|
@@ -4516,8 +4371,8 @@ def build_prepare_argument_parser():
|
|
|
4516
4371
|
dest="approve_plan_ack",
|
|
4517
4372
|
help=(
|
|
4518
4373
|
"Treat the CLI invocation itself as the plan approval signal. "
|
|
4519
|
-
"
|
|
4520
|
-
"
|
|
4374
|
+
"Sets the report record `frontmatter.approved` to true and refreshes "
|
|
4375
|
+
"the full reading copy."
|
|
4521
4376
|
),
|
|
4522
4377
|
)
|
|
4523
4378
|
p.add_argument(
|
|
@@ -4527,8 +4382,8 @@ def build_prepare_argument_parser():
|
|
|
4527
4382
|
help=(
|
|
4528
4383
|
"implementation task only. Name of the Option Candidate the user "
|
|
4529
4384
|
"chose from the implementation-planning final-report. Written into "
|
|
4530
|
-
"the
|
|
4531
|
-
"line. When omitted, implementation falls back to the plan's "
|
|
4385
|
+
"the report record `frontmatter.implementationOption` (schema-v1: "
|
|
4386
|
+
"the markdown `implementation-option:` line). When omitted, implementation falls back to the plan's "
|
|
4532
4387
|
"`Recommended Option`."
|
|
4533
4388
|
),
|
|
4534
4389
|
)
|
|
@@ -93,10 +93,10 @@ def _latest_manifest(run_dir: Path) -> tuple[dict, Path | None]:
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
def _resolve_report(run_dir: Path, project_root: str, expected: str) -> Path | None:
|
|
96
|
-
"""`
|
|
96
|
+
"""`expectedReportRecordPath` 를 실제 파일로 푼다. 루트 밖 이탈이면 `None`.
|
|
97
97
|
|
|
98
98
|
이 값은 project_root 기준 상대경로다 — `render.py` 가
|
|
99
|
-
`
|
|
99
|
+
`FINAL_REPORT_RECORD_RELATIVE_PATH` 에서 채우고 그 값은 `paths.py` 의
|
|
100
100
|
`_rel(project_root, final_report)` 이며, 다른 소비자도
|
|
101
101
|
`dispatch_state.resolve_required_path(project_root, ...)` 로 푼다.
|
|
102
102
|
|
|
@@ -116,7 +116,7 @@ def _resolve_report(run_dir: Path, project_root: str, expected: str) -> Path | N
|
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
def _report_data_path(report_path: Path) -> Path:
|
|
119
|
-
"""구조화 본문이 있는 파일. 정본 `
|
|
119
|
+
"""구조화 본문이 있는 파일. 정본 `expectedReportRecordPath` 는 마크다운이고
|
|
120
120
|
본문은 `.data.json` 형제다. 이미 data.json 을 가리키면 그대로 쓴다 —
|
|
121
121
|
`final_report_data_path` 는 `.md` 가 아닌 이름에 `with_suffix` 를 걸어
|
|
122
122
|
`x.data.json` 을 `x.data.data.json` 으로 바꿔 놓는다."""
|
|
@@ -205,7 +205,7 @@ def _never_ran(row: dict) -> bool:
|
|
|
205
205
|
|
|
206
206
|
def _check_report(run_dir: Path, manifest: dict, manifest_path: Path,
|
|
207
207
|
project_root: str, row: dict) -> list[dict]:
|
|
208
|
-
expected = str(manifest.get("
|
|
208
|
+
expected = str(manifest.get("expectedReportRecordPath", ""))
|
|
209
209
|
if not expected:
|
|
210
210
|
return []
|
|
211
211
|
report_path = _resolve_report(run_dir, project_root, expected)
|
|
@@ -313,7 +313,7 @@ def _check_roster(manifest: dict, manifest_path: Path, project_root: str,
|
|
|
313
313
|
`workerResultsDirectoryPath` 는 run_dir 이 아니라 **project_root** 기준
|
|
314
314
|
상대경로다(`render.py` 가 `WORKER_RESULTS_RELATIVE_PATH` 에서 채운다). 실측
|
|
315
315
|
manifest 139건 전부 그랬고, run_dir 기준으로 풀면 한 건도 디렉터리에 닿지
|
|
316
|
-
않아 감사가 통째로 빈손이 된다. `
|
|
316
|
+
않아 감사가 통째로 빈손이 된다. `expectedReportRecordPath` 와 같은 이유로
|
|
317
317
|
`resolve_under_root` 를 거쳐 루트 밖 이탈도 함께 막는다."""
|
|
318
318
|
raw_workers = manifest.get("recommendedWorkers")
|
|
319
319
|
# 문자열이 들어오면 문자 단위로 순회해 `no result from c, l, a, u, d, e` 가
|
|
@@ -25,7 +25,7 @@ def build_run_index_row(*, project_id: str, project_root: str,
|
|
|
25
25
|
run_seq: int, status: str, started_at: str,
|
|
26
26
|
finished_at: Optional[str], workers: list, lead_model: str,
|
|
27
27
|
validation: str, run_dir_rel: str,
|
|
28
|
-
|
|
28
|
+
final_report_record_rel: str, final_status_rel: str,
|
|
29
29
|
execution_manifest_path: str = "",
|
|
30
30
|
role_execution_refs: list | None = None) -> dict:
|
|
31
31
|
"""slim run-index row(14필드)를 만든다. 파생 4필드는 포함하지 않는다."""
|
|
@@ -42,7 +42,7 @@ def build_run_index_row(*, project_id: str, project_root: str,
|
|
|
42
42
|
"workers": workers,
|
|
43
43
|
"leadModel": lead_model,
|
|
44
44
|
"validation": validation,
|
|
45
|
-
"
|
|
45
|
+
"finalReportRecordRel": final_report_record_rel,
|
|
46
46
|
"finalStatusRel": final_status_rel,
|
|
47
47
|
"runDirRel": run_dir_rel,
|
|
48
48
|
**({
|