okstra 0.178.0 → 0.179.1
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 +3 -3
- package/runtime/prompts/lead/okstra-lead-contract.md +5 -5
- 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/prompts/wizard/prompts.ko.json +1 -0
- 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 +77 -6
- 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/convergence_store.py +13 -2
- package/runtime/python/okstra_ctl/dispatch_core.py +85 -14
- package/runtime/python/okstra_ctl/dispatch_state.py +34 -20
- 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_manifest.py +22 -3
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +35 -1
- 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 +24 -15
- 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 +170 -294
- 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 +102 -78
- 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 +51 -9
- 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
- package/runtime/validators/validate_session_conformance.py +44 -8
|
@@ -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
|
-
|
|
273
|
+
def _record_approved_flag(path: Path) -> bool | None:
|
|
274
|
+
"""정본 `frontmatter.approved`. 정본이 없으면(schema-v1) None."""
|
|
282
275
|
loaded = _load_final_report_data_if_present(path)
|
|
283
276
|
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:
|
|
301
|
-
loaded = _load_final_report_data_if_present(path)
|
|
302
|
-
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
614
|
|
|
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
615
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
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
|
-
|
|
683
|
-
동작 요약:
|
|
684
|
-
- frontmatter 의 `implementation-option:` 라인을
|
|
685
|
-
`implementation-option: <option_name>` 으로 교체 → `"frontmatter-set"`.
|
|
686
|
-
- audit 라인을 한 번 append (idempotent: 동일 audit 라인은 한 번만).
|
|
687
|
-
- frontmatter 가 없거나 `implementation-option:` 라인이 없으면 PrepareError
|
|
688
|
-
(report-writer 가 빈 값으로 이 라인을 항상 emit 하므로 라인은 존재해야 한다).
|
|
689
|
-
|
|
690
|
-
`--approve` (`_apply_cli_approval`) 의 frontmatter-치환 + audit-append 패턴을
|
|
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
|
|
|
@@ -3252,6 +3102,23 @@ def _write_prior_run_error_digest(ctx: dict, instruction_set: Path) -> None:
|
|
|
3252
3102
|
(instruction_set / "prior-run-errors.md").write_text(digest, encoding="utf-8")
|
|
3253
3103
|
|
|
3254
3104
|
|
|
3105
|
+
def _stage_or_clear(path: Path, body: str) -> None:
|
|
3106
|
+
"""이 런의 값을 쓰거나, 값이 없으면 이전 런이 남긴 파일을 지운다.
|
|
3107
|
+
|
|
3108
|
+
instruction-set 은 run 단위가 아니라 **task 단위** 디렉터리다
|
|
3109
|
+
(`paths.py`: `task_root / "instruction-set"`). 그래서 값이 있을 때만 쓰고
|
|
3110
|
+
없을 때 아무것도 안 하면, 이전 런의 파일이 그대로 남아 다음 런의 입력이 된다.
|
|
3111
|
+
directive 를 비우고 준비한 런에서 워커들이 예전 directive 를 읽고, 그 내용이
|
|
3112
|
+
현재 기록과 어긋나 반박에 시간을 쓴 사례가 보고됐다. carry-in 답변
|
|
3113
|
+
(`clarification-response.md`)은 같은 형태에 결과가 더 나쁘다 — 남은 파일이
|
|
3114
|
+
analysis packet 에 그대로 실린다.
|
|
3115
|
+
"""
|
|
3116
|
+
if body:
|
|
3117
|
+
path.write_text(body, encoding="utf-8")
|
|
3118
|
+
return
|
|
3119
|
+
path.unlink(missing_ok=True)
|
|
3120
|
+
|
|
3121
|
+
|
|
3255
3122
|
def _write_instruction_set_sources(
|
|
3256
3123
|
inp: PrepareInputs,
|
|
3257
3124
|
ctx: dict,
|
|
@@ -3328,22 +3195,26 @@ def _write_instruction_set_sources(
|
|
|
3328
3195
|
)
|
|
3329
3196
|
ctx["HOST_ORCHESTRATION_RULES_RELATIVE_PATH"] = host_rules_relative
|
|
3330
3197
|
if inp.clarification_response_path:
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
encoding="utf-8",
|
|
3198
|
+
clarification_body = clarification_response_with_sidecars(
|
|
3199
|
+
Path(inp.clarification_response_path)
|
|
3334
3200
|
)
|
|
3335
3201
|
elif inp.task_type == "implementation" and inp.approved_plan_path:
|
|
3336
3202
|
# implementation carry-in: the approved plan reaches this run by path
|
|
3337
3203
|
# (executor re-reads it), so attach ONLY the planning HTML form answers
|
|
3338
3204
|
# (`runs/implementation-planning/.../user-responses/`) — without these
|
|
3339
3205
|
# the user's clarification answers never reach the implementation run.
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3206
|
+
clarification_body = attached_user_responses_section(
|
|
3207
|
+
Path(inp.approved_plan_path)
|
|
3208
|
+
)
|
|
3209
|
+
else:
|
|
3210
|
+
clarification_body = ""
|
|
3211
|
+
_stage_or_clear(
|
|
3212
|
+
instruction_set / "clarification-response.md", clarification_body
|
|
3213
|
+
)
|
|
3214
|
+
_stage_or_clear(
|
|
3215
|
+
instruction_set / "directive.txt",
|
|
3216
|
+
inp.directive + "\n" if inp.directive else "",
|
|
3217
|
+
)
|
|
3347
3218
|
render_reference_expectations(
|
|
3348
3219
|
str(inp.brief_path), str(instruction_set / "reference-expectations.md"), ctx,
|
|
3349
3220
|
)
|
|
@@ -3361,6 +3232,11 @@ def _write_instruction_set_sources(
|
|
|
3361
3232
|
instruction_set_relative_path=ctx["INSTRUCTION_SET_RELATIVE_PATH"],
|
|
3362
3233
|
fix_history_text=fix_cycles.packet_summary(
|
|
3363
3234
|
fix_cycles.read_rows(Path(ctx["TASK_MANIFEST_PATH"]).parent)),
|
|
3235
|
+
stage_ledger_json=(
|
|
3236
|
+
render_stage_ledger(
|
|
3237
|
+
build_stage_ledger(Path(ctx["TASK_MANIFEST_PATH"]).parent))
|
|
3238
|
+
if inp.task_type == "implementation-planning" else ""
|
|
3239
|
+
),
|
|
3364
3240
|
)
|
|
3365
3241
|
if inp.task_type in ANALYSIS_TASK_TYPES:
|
|
3366
3242
|
packet += (
|
|
@@ -3620,7 +3496,7 @@ def _maybe_open_fix_cycle(inp: PrepareInputs, task_root: Path, existing: dict,
|
|
|
3620
3496
|
"(workflow.lastCompletedPhase 확인)")
|
|
3621
3497
|
brief_text = Path(inp.brief_path).read_text(encoding="utf-8")
|
|
3622
3498
|
fix_cycles.append_opened(
|
|
3623
|
-
task_root, target_report=str(existing.get("
|
|
3499
|
+
task_root, target_report=str(existing.get("latestReportRecordPath", "")),
|
|
3624
3500
|
symptom=fix_cycles.derive_symptom(brief_text), opened_at=now)
|
|
3625
3501
|
return fix_cycles.open_cycle(fix_cycles.read_rows(task_root))
|
|
3626
3502
|
|
|
@@ -3650,7 +3526,7 @@ def _record_fix_cycle_events(inp: PrepareInputs, ctx: dict) -> str:
|
|
|
3650
3526
|
if handoff_attached:
|
|
3651
3527
|
fix_cycles.append_closed(
|
|
3652
3528
|
task_root, cycle=open_c["cycle"], closed_by="release-handoff",
|
|
3653
|
-
report=str(existing.get("
|
|
3529
|
+
report=str(existing.get("latestReportRecordPath", "")), closed_at=now)
|
|
3654
3530
|
open_c = None
|
|
3655
3531
|
|
|
3656
3532
|
if inp.fix_cycle == "yes" and open_c is None:
|
|
@@ -3680,7 +3556,7 @@ def _finalize_status_and_render_manifests(
|
|
|
3680
3556
|
ctx["CURRENT_TASK_STATUS"] = "lead-session-started"
|
|
3681
3557
|
ctx["CURRENT_RUN_STATUS"] = "in-progress"
|
|
3682
3558
|
ctx["LATEST_REPORT_PATH"] = ctx["FINAL_REPORT_PATH"]
|
|
3683
|
-
ctx["
|
|
3559
|
+
ctx["LATEST_REPORT_RECORD_RELATIVE_PATH"] = ctx["FINAL_REPORT_RECORD_RELATIVE_PATH"]
|
|
3684
3560
|
ctx.update(compute_workflow_state(
|
|
3685
3561
|
task_type=inp.task_type,
|
|
3686
3562
|
current_run_status=ctx["CURRENT_RUN_STATUS"],
|
|
@@ -4330,7 +4206,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4330
4206
|
"VALIDATION_UPDATED_AT": "",
|
|
4331
4207
|
"VALIDATION_FAILURES_JSON": "[]",
|
|
4332
4208
|
"LATEST_REPORT_PATH": "",
|
|
4333
|
-
"
|
|
4209
|
+
"LATEST_REPORT_RECORD_RELATIVE_PATH": "",
|
|
4334
4210
|
"RENDER_ONLY": "true" if inp.render_only else "false",
|
|
4335
4211
|
"OKSTRA_VERSION": installed_version(),
|
|
4336
4212
|
"EXECUTION_IDENTITY_JSON": json.dumps(
|
|
@@ -4516,8 +4392,8 @@ def build_prepare_argument_parser():
|
|
|
4516
4392
|
dest="approve_plan_ack",
|
|
4517
4393
|
help=(
|
|
4518
4394
|
"Treat the CLI invocation itself as the plan approval signal. "
|
|
4519
|
-
"
|
|
4520
|
-
"
|
|
4395
|
+
"Sets the report record `frontmatter.approved` to true and refreshes "
|
|
4396
|
+
"the full reading copy."
|
|
4521
4397
|
),
|
|
4522
4398
|
)
|
|
4523
4399
|
p.add_argument(
|
|
@@ -4527,8 +4403,8 @@ def build_prepare_argument_parser():
|
|
|
4527
4403
|
help=(
|
|
4528
4404
|
"implementation task only. Name of the Option Candidate the user "
|
|
4529
4405
|
"chose from the implementation-planning final-report. Written into "
|
|
4530
|
-
"the
|
|
4531
|
-
"line. When omitted, implementation falls back to the plan's "
|
|
4406
|
+
"the report record `frontmatter.implementationOption` (schema-v1: "
|
|
4407
|
+
"the markdown `implementation-option:` line). When omitted, implementation falls back to the plan's "
|
|
4532
4408
|
"`Recommended Option`."
|
|
4533
4409
|
),
|
|
4534
4410
|
)
|