okstra 0.188.1 → 0.189.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.
Files changed (34) hide show
  1. package/dist/cli-registry.mjs +6 -0
  2. package/dist/cli-registry.mjs.map +1 -1
  3. package/docs/architecture/storage-model.md +9 -0
  4. package/docs/cli.md +5 -2
  5. package/docs/for-ai/skills/okstra-brief-gen.md +2 -0
  6. package/docs/project-structure-overview.md +5 -2
  7. package/package.json +1 -1
  8. package/runtime/BUILD.json +2 -2
  9. package/runtime/agents/workers/report-writer-worker.md +2 -0
  10. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  11. package/runtime/prompts/lead/plan-body-verification.md +1 -1
  12. package/runtime/prompts/lead/report-writer.md +11 -8
  13. package/runtime/prompts/profiles/_common-contract.md +1 -0
  14. package/runtime/python/okstra_ctl/agent/activity.py +3 -0
  15. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +51 -1
  16. package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +280 -0
  17. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +191 -0
  18. package/runtime/python/okstra_ctl/analysis_packet.py +38 -0
  19. package/runtime/python/okstra_ctl/group_context.py +223 -0
  20. package/runtime/python/okstra_ctl/model_io/renderers.py +12 -7
  21. package/runtime/python/okstra_ctl/recap.py +7 -1
  22. package/runtime/python/okstra_ctl/report_corrections.py +443 -0
  23. package/runtime/python/okstra_ctl/report_narrative.py +44 -3
  24. package/runtime/python/okstra_ctl/run.py +28 -0
  25. package/runtime/python/okstra_ctl/timeline_runs.py +71 -0
  26. package/runtime/python/okstra_ctl/wizard.py +2 -1
  27. package/runtime/schemas/final-report-v3.0.schema.json +1 -0
  28. package/runtime/schemas/report-writer-corrections-v1.0.schema.json +52 -0
  29. package/runtime/skills/okstra-brief-gen/SKILL.md +29 -1
  30. package/runtime/skills/okstra-inspect/facets/history.md +1 -0
  31. package/runtime/skills/okstra-inspect/facets/recap.md +1 -1
  32. package/runtime/templates/report-writer-prompt-preamble.md +6 -0
  33. package/runtime/templates/reports/group-context.template.md +31 -0
  34. package/runtime/validators/validate-brief.py +9 -0
@@ -0,0 +1,280 @@
1
+ """run 갈래의 교정 원장 대조 — 매니페스트가 정한 스키마·소스로 `check_corrections` 를 돌린다.
2
+
3
+ `materialize --corrections`, `check-corrections`, `apply-corrections` 가 같은 대조
4
+ 함수를 부른다. 스키마는 조립(`report_assembly.assemble_report`)이 쓰는 것과 같은
5
+ 계약 3.0 완성 리포트 스키마이고, task 의미 검증기의 입력(요구 id 순서·참여
6
+ analyser)은 작성자가 받는 합성 묶음에서 가져온다 — 두 소비자가 다른 규칙을 보지
7
+ 않게 하기 위해서다.
8
+
9
+ `apply-corrections` 는 replace·remove 만 있는 원장을 작성자 라운드 없이 서사에
10
+ 쓰고 활동 원장에 `lead-correction-applied` 행을 남긴다. 선례는 implementation-
11
+ planning 의 planner self-fix 기록(`plan_items_cli._record_self_fixes`)이다.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from pathlib import Path
17
+ import tempfile
18
+ from typing import Any, Mapping
19
+
20
+ from ..activity import (
21
+ ActivityProjectionError,
22
+ agent_activity_rows,
23
+ record_activity,
24
+ )
25
+ from ...final_report_schema import load_schema_version
26
+ from ...implementation_options import validate_implementation_option_selection
27
+ from ...report_contract import TASK_TYPE_DATA_PROPERTY
28
+ from ...report_corrections import (
29
+ CorrectionsCheck,
30
+ check_corrections,
31
+ load_corrections,
32
+ render_applied_narrative,
33
+ )
34
+ from ...report_inputs import report_narrative_path, uses_report_contract_v3
35
+ from ...report_synthesis_packet import (
36
+ ReportSynthesisPacketError,
37
+ build_report_synthesis_packet,
38
+ )
39
+ from .inputs import AgentPromptCliError, _authorized_path, _relative
40
+
41
+
42
+ LEAD_CORRECTION_ACTIVITY_KIND = "lead-correction-applied"
43
+ # 활동 원장의 `agent` 허용값 중 리드를 뜻하는 고정 이름(`activity._validate_activity_ownership`).
44
+ _LEAD_AGENT = "okstra-lead"
45
+
46
+
47
+ def run_corrections_check(
48
+ *,
49
+ project_root: Path,
50
+ manifest: Mapping[str, Any],
51
+ active_context: Mapping[str, Any],
52
+ team_state: Mapping[str, Any],
53
+ corrections_path: Path,
54
+ narrative_path: Path,
55
+ ) -> CorrectionsCheck:
56
+ """원장을 이 run 의 기준 서사에 대조한다. 결함은 `CorrectionsCheck.defects` 에 모인다."""
57
+ ledger, load_defects = load_corrections(corrections_path)
58
+ if load_defects:
59
+ return CorrectionsCheck(ledger, (), None, False, {}, tuple(load_defects))
60
+ if not uses_report_contract_v3(manifest):
61
+ return CorrectionsCheck(ledger, (), None, False, {}, (
62
+ "owner=lead correction=ledger path=- reason=corrections require report "
63
+ "contract 3.0 (a Markdown narrative); this run uses an older contract",
64
+ ))
65
+ contract = manifest.get("agentContract")
66
+ authorized = contract.get("authorizedPaths") if isinstance(contract, Mapping) else None
67
+ authorized = authorized if isinstance(authorized, Mapping) else {}
68
+ try:
69
+ base_path = _authorized_path(
70
+ project_root,
71
+ str(ledger.get("baseNarrativePath") or ""),
72
+ authorized.get("resultRoots"),
73
+ "base narrative",
74
+ must_exist=True,
75
+ )
76
+ except AgentPromptCliError as exc:
77
+ return CorrectionsCheck(ledger, (), None, False, {}, (
78
+ f"owner=lead correction=ledger path={ledger.get('baseNarrativePath')} reason={exc}",
79
+ ))
80
+ try:
81
+ packet = build_report_synthesis_packet(
82
+ project_root=project_root,
83
+ manifest=manifest,
84
+ active_context=active_context,
85
+ team_state=team_state,
86
+ narrative_path=narrative_path,
87
+ )
88
+ except ReportSynthesisPacketError as exc:
89
+ return CorrectionsCheck(ledger, (), None, False, {}, tuple(
90
+ f"owner={issue.owner} correction=ledger path={issue.path} "
91
+ f"reason=synthesis packet source defect: {issue.reason}"
92
+ for issue in exc.issues
93
+ ))
94
+ task_type = str(manifest.get("taskType") or "")
95
+ semantic_validator = None
96
+ if task_type == "implementation-option-selection":
97
+ block_key = TASK_TYPE_DATA_PROPERTY[task_type]
98
+ original_ids = packet.original_requirement_ids
99
+ analysers = packet.participating_analysers
100
+
101
+ def semantic_validator(data: dict[str, Any]) -> list[str]:
102
+ block = data.get(block_key)
103
+ return validate_implementation_option_selection(
104
+ block if isinstance(block, Mapping) else {}, original_ids, analysers,
105
+ )
106
+
107
+ return check_corrections(
108
+ ledger=ledger,
109
+ base_narrative=base_path.read_text(encoding="utf-8"),
110
+ schema=load_schema_version("3.0"),
111
+ block_rules=packet.block_rules,
112
+ semantic_validator=semantic_validator,
113
+ )
114
+
115
+
116
+ def corrections_payload(
117
+ check: CorrectionsCheck, *, project_root: Path, corrections_path: Path,
118
+ ) -> dict[str, Any]:
119
+ """`check-corrections --json` 의 출력."""
120
+ return {
121
+ "ok": check.ok,
122
+ "correctionsPath": _relative(project_root, corrections_path),
123
+ "mechanical": check.mechanical,
124
+ "corrections": [
125
+ {
126
+ "id": item.get("id"),
127
+ "kind": item.get("kind"),
128
+ "path": item.get("path"),
129
+ "constraints": list(check.constraints.get(str(item.get("id")), ())),
130
+ }
131
+ for item in check.corrections
132
+ ],
133
+ "defects": list(check.defects),
134
+ }
135
+
136
+
137
+ def run_corrections_apply(
138
+ *,
139
+ project_root: Path,
140
+ manifest: Mapping[str, Any],
141
+ manifest_path: Path,
142
+ active_context: Mapping[str, Any],
143
+ team_state: Mapping[str, Any],
144
+ corrections_path: Path,
145
+ ) -> dict[str, Any]:
146
+ """기계적 원장을 서사에 쓰고 활동 행을 남긴다 — `apply-corrections` 의 본체.
147
+
148
+ 순서: 대조(결함이면 거절) → rewrite 없음 확인 → 활동 계약·중복 적용 확인 →
149
+ 서사 원자적 쓰기 → 활동 행. 활동 계약이 없는 run 은 서사를 쓰기 전에
150
+ 거절한다: 행 없는 기계 수정은 감사에서 작성자의 것으로 읽힌다.
151
+ """
152
+ if not uses_report_contract_v3(manifest):
153
+ raise AgentPromptCliError(
154
+ "apply-corrections requires report contract 3.0 (a Markdown "
155
+ "narrative); this run uses an older contract"
156
+ )
157
+ narrative_path = report_narrative_path(project_root, manifest)
158
+ events_value = manifest.get("leadEventsPath")
159
+ if manifest.get("activityContractVersion") != 1 or not (
160
+ isinstance(events_value, str) and events_value.strip()
161
+ ):
162
+ raise AgentPromptCliError(
163
+ "apply-corrections records a `lead-correction-applied` activity row, "
164
+ "and this run manifest declares no activity contract "
165
+ "(activityContractVersion 1 with leadEventsPath); dispatch the "
166
+ "writer with --corrections instead"
167
+ )
168
+ corrections_rel = _relative(project_root, corrections_path)
169
+ check = run_corrections_check(
170
+ project_root=project_root,
171
+ manifest=manifest,
172
+ active_context=active_context,
173
+ team_state=team_state,
174
+ corrections_path=corrections_path,
175
+ narrative_path=narrative_path,
176
+ )
177
+ if check.defects:
178
+ raise AgentPromptCliError(
179
+ "report-writer corrections defects: " + "; ".join(check.defects)
180
+ )
181
+ if not check.mechanical:
182
+ pending = ", ".join(
183
+ str(item.get("id")) for item in check.corrections
184
+ if item.get("kind") == "rewrite"
185
+ )
186
+ raise AgentPromptCliError(
187
+ f"corrections ledger has rewrite entries ({pending}): a rewrite "
188
+ "needs a writer round — materialize the report-writer prompt with "
189
+ "--corrections instead of applying"
190
+ )
191
+ base_path = project_root / str(check.ledger.get("baseNarrativePath") or "")
192
+ if os.path.normpath(base_path) == os.path.normpath(narrative_path):
193
+ raise AgentPromptCliError(
194
+ "baseNarrativePath is the live narrative "
195
+ f"({_relative(project_root, narrative_path)}); applying would "
196
+ "overwrite the only copy of the attempt being corrected. Copy it "
197
+ "to worker-results/report-writer-narrative-a<N>-<task-type>-<seq>.md "
198
+ "first and point the ledger at that copy"
199
+ )
200
+ previous = _already_applied(project_root, manifest_path, corrections_rel)
201
+ if previous is not None:
202
+ raise AgentPromptCliError(
203
+ f"corrections ledger {corrections_rel} was already applied as "
204
+ f"activity {previous}; a further correction needs a new ledger "
205
+ "whose baseNarrativePath is the narrative that apply wrote"
206
+ )
207
+ applied_ids = [str(item.get("id")) for item in check.corrections]
208
+ narrative_rel = _relative(project_root, narrative_path)
209
+ _write_text_atomic(
210
+ narrative_path, render_applied_narrative(check, load_schema_version("3.0")),
211
+ )
212
+ details = {
213
+ "kind": LEAD_CORRECTION_ACTIVITY_KIND,
214
+ "agent": _LEAD_AGENT,
215
+ "summary": (
216
+ f"Applied {len(applied_ids)} mechanical correction(s) "
217
+ f"({', '.join(applied_ids)}) from {corrections_rel} to the report "
218
+ "narrative without a writer round"
219
+ ),
220
+ "planItemIds": [],
221
+ "resultPath": narrative_rel,
222
+ "commands": [],
223
+ "evidenceRefs": [corrections_rel, *applied_ids],
224
+ "outcome": "completed",
225
+ }
226
+ try:
227
+ event = record_activity(project_root, manifest_path, details)
228
+ except ActivityProjectionError as exc:
229
+ raise AgentPromptCliError(
230
+ f"narrative written to {narrative_rel} but the activity row was "
231
+ f"refused: {exc}; re-run apply-corrections once the cause is fixed "
232
+ "(the same ledger applies again to the same base)"
233
+ ) from exc
234
+ return {
235
+ "ok": True,
236
+ "correctionsPath": corrections_rel,
237
+ "narrativePath": narrative_rel,
238
+ "appliedCorrectionIds": applied_ids,
239
+ "activityId": event.details.get("activityId"),
240
+ "activityLine": (
241
+ f"ACTIVITY: id={event.details.get('activityId')} agent={_LEAD_AGENT} "
242
+ f"kind={LEAD_CORRECTION_ACTIVITY_KIND} corrections={corrections_rel} "
243
+ f"result={narrative_rel}"
244
+ ),
245
+ }
246
+
247
+
248
+ def _already_applied(
249
+ project_root: Path, manifest_path: Path, corrections_rel: str,
250
+ ) -> str | None:
251
+ """이 원장을 이미 적용한 활동 행의 id. 없으면 None."""
252
+ try:
253
+ rows = agent_activity_rows(project_root, manifest_path)
254
+ except ActivityProjectionError as exc:
255
+ raise AgentPromptCliError(f"activity ledger is unreadable: {exc}") from exc
256
+ for row in rows:
257
+ refs = row.get("evidenceRefs")
258
+ if (
259
+ row.get("kind") == LEAD_CORRECTION_ACTIVITY_KIND
260
+ and isinstance(refs, list)
261
+ and corrections_rel in refs
262
+ ):
263
+ return str(row.get("activityId"))
264
+ return None
265
+
266
+
267
+ def _write_text_atomic(path: Path, text: str) -> None:
268
+ path.parent.mkdir(parents=True, exist_ok=True)
269
+ descriptor, temporary = tempfile.mkstemp(
270
+ prefix=f".{path.name}.", suffix=".tmp", dir=path.parent,
271
+ )
272
+ try:
273
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
274
+ handle.write(text)
275
+ handle.flush()
276
+ os.fsync(handle.fileno())
277
+ os.replace(temporary, path)
278
+ finally:
279
+ if os.path.exists(temporary):
280
+ os.unlink(temporary)
@@ -32,13 +32,25 @@ from ...assignment_resolver import AssignmentContext, resolve_dispatch_assignmen
32
32
  from ...path_hints import hydrate_active_run_context
33
33
  from ...worker_prompt_headers import worker_prompt_headers
34
34
  from ...final_report_paths import final_report_data_path
35
+ from ...final_report_schema import load_schema_version
35
36
  from ...report_inputs import report_narrative_path, uses_report_contract_v3
37
+ from ...report_narrative import NarrativeContractError, parse_narrative_structure
36
38
  from ...report_synthesis_packet import (
37
39
  ReportSynthesisPacketError,
38
40
  materialize_report_synthesis_packet,
39
41
  )
40
42
  from ...worker_prompt_body import report_writer_input_lines
41
43
  from ...dispatch_state import detect_terminal_backend
44
+ from ...report_corrections import (
45
+ body_owned_section_conflicts,
46
+ render_corrections_section,
47
+ render_output_section,
48
+ )
49
+ from .corrections import (
50
+ corrections_payload,
51
+ run_corrections_apply,
52
+ run_corrections_check,
53
+ )
42
54
  from .dynamic_verifier import (
43
55
  _dynamic_verifier_source,
44
56
  _reserve_dynamic_verifier_request,
@@ -270,6 +282,26 @@ def _materialize_run(
270
282
  narrative_path=result_path,
271
283
  ),
272
284
  )
285
+ if args.audience == "report-writer" and uses_report_contract_v3(manifest):
286
+ body = _with_report_writer_sections(
287
+ args,
288
+ project_root,
289
+ manifest,
290
+ authorized,
291
+ active_context,
292
+ body,
293
+ narrative_path=result_path,
294
+ )
295
+ elif getattr(args, "corrections", None):
296
+ if args.audience != "report-writer":
297
+ raise AgentPromptCliError(
298
+ "--corrections is a report-writer materialization option; "
299
+ f"audience {args.audience} has no corrections ledger"
300
+ )
301
+ raise AgentPromptCliError(
302
+ "--corrections requires report contract 3.0 (a Markdown narrative); "
303
+ "this run uses an older contract"
304
+ )
273
305
  request = AgentInvocationRequest(
274
306
  invocation_id=args.invocation_id,
275
307
  worker_id=args.worker_id if identity is None else None,
@@ -315,6 +347,165 @@ def _normalized(path: Path) -> Path:
315
347
  return Path(os.path.normpath(path))
316
348
 
317
349
 
350
+ def _with_report_writer_sections(
351
+ args: argparse.Namespace,
352
+ project_root: Path,
353
+ manifest: Mapping[str, Any],
354
+ authorized: Mapping[str, Any],
355
+ active_context: Mapping[str, Any],
356
+ body: str,
357
+ *,
358
+ narrative_path: Path,
359
+ ) -> str:
360
+ """run 갈래 report-writer 프롬프트의 okstra 소유 절을 본문 끝에 렌더한다.
361
+
362
+ `## Output` 은 매 디스패치에 렌더한다 — 세 산출물을 리드가 손으로 열거하다
363
+ 하나를 빠뜨리면 `required worker artifact was not produced` 로 끝났다.
364
+ `--corrections` 가 있으면 원장을 대조해 `## Corrections` 를 그 앞에 둔다.
365
+ 리드가 자유 서술로 적던 교정 지시는 기계가 대조할 수 없어 값 오류가
366
+ 조립에서야 드러났다(2026-09-03 실측: 재실행 6회 중 4회). 그래서 원장 없는
367
+ 교정 디스패치(서사가 이미 있고 파싱되는 경우)는 거절한다. 두 절은 okstra 가
368
+ 렌더하므로 본문에 같은 제목이 있으면 거절한다.
369
+ """
370
+ conflicts = body_owned_section_conflicts(body)
371
+ if conflicts:
372
+ raise AgentPromptCliError(
373
+ f"instruction body must not contain {', '.join(conflicts)}: okstra "
374
+ "renders those sections itself (## Output from the run manifest, "
375
+ "## Corrections from the --corrections ledger)"
376
+ )
377
+ sections: list[str] = []
378
+ if getattr(args, "corrections", None):
379
+ corrections_path = _authorized_path(
380
+ project_root,
381
+ args.corrections,
382
+ authorized.get("instructionRoots"),
383
+ "corrections",
384
+ must_exist=True,
385
+ )
386
+ check = run_corrections_check(
387
+ project_root=project_root,
388
+ manifest=manifest,
389
+ active_context=active_context,
390
+ team_state=_load_team_state(project_root, manifest),
391
+ corrections_path=corrections_path,
392
+ narrative_path=narrative_path,
393
+ )
394
+ if check.defects:
395
+ raise AgentPromptCliError(
396
+ "report-writer corrections defects: " + "; ".join(check.defects)
397
+ )
398
+ sections.extend(render_corrections_section(
399
+ check,
400
+ base_narrative_rel=str(check.ledger.get("baseNarrativePath") or ""),
401
+ corrections_rel=_relative(project_root, corrections_path),
402
+ ))
403
+ sections.append("")
404
+ else:
405
+ _refuse_free_form_correction(project_root, narrative_path)
406
+ sections.extend(render_output_section())
407
+ return body.rstrip("\n") + "\n\n" + "\n".join(sections) + "\n"
408
+
409
+
410
+ def _refuse_free_form_correction(project_root: Path, narrative_path: Path) -> None:
411
+ """서사가 이미 있고 구조가 읽히면 이 디스패치는 교정이다 — 원장 없이는 거절한다.
412
+
413
+ 구조가 읽히지 않는 서사(줄 문법·소유권 결함)는 교정 대상이 아니라 재저작
414
+ 대상이다(원장의 경로가 해소될 자료가 없다). 그 디스패치는 원장 없이
415
+ 통과한다. 값 결함(패턴 밖 id, enum 밖 값)은 구조가 읽히는 서사이고, 그것이
416
+ 원장이 고치는 자리다 — 실측(dev-10626 a3)의 `SC-` id 20곳이 이 경우다.
417
+ """
418
+ if not narrative_path.is_file():
419
+ return
420
+ try:
421
+ parse_narrative_structure(
422
+ narrative_path.read_text(encoding="utf-8"), load_schema_version("3.0"),
423
+ )
424
+ except NarrativeContractError:
425
+ return
426
+ narrative_rel = _relative(project_root, narrative_path)
427
+ raise AgentPromptCliError(
428
+ f"report-writer narrative already exists at {narrative_rel} and parses, "
429
+ "so this is a corrective dispatch, and a free-form correction is refused: "
430
+ "nothing can check it before the writer runs. Write a corrections ledger "
431
+ "(schemas/report-writer-corrections-v1.0.schema.json; baseNarrativePath "
432
+ "names a preserved copy of that attempt), run `okstra agent-prompt "
433
+ "check-corrections --project-root <root> --run-manifest <manifest> "
434
+ "--corrections <ledger>` until it reports no defect, then pass the same "
435
+ "--corrections here. A ledger of only replace/remove entries needs no "
436
+ "writer round: `okstra agent-prompt apply-corrections` writes the "
437
+ "narrative and records the activity row. Only a narrative whose "
438
+ "structure does not parse (line grammar, unknown top-level field) is "
439
+ "re-authored without a ledger; value defects are what the ledger fixes"
440
+ )
441
+
442
+
443
+ def _corrections_context(
444
+ args: argparse.Namespace,
445
+ ) -> tuple[Path, Path, dict[str, Any], Mapping[str, Any], Path]:
446
+ """`check-corrections`·`apply-corrections` 가 공유하는 입력 해소."""
447
+ project_root = _project_root(args.project_root)
448
+ manifest_path = _project_input(project_root, args.run_manifest, "run manifest")
449
+ manifest = _read_json_object(manifest_path, "run manifest")
450
+ contract = _mapping(manifest.get("agentContract"), "run agent contract")
451
+ authorized = _mapping(contract.get("authorizedPaths"), "authorized paths")
452
+ corrections_path = _authorized_path(
453
+ project_root,
454
+ args.corrections,
455
+ authorized.get("instructionRoots"),
456
+ "corrections",
457
+ must_exist=True,
458
+ )
459
+ active_context_path = _project_manifest_path(
460
+ project_root,
461
+ manifest.get("activeRunContextPath"),
462
+ "active run context",
463
+ must_exist=True,
464
+ )
465
+ active_context = hydrate_active_run_context(
466
+ _read_json_object(active_context_path, "active run context")
467
+ )
468
+ return project_root, manifest_path, manifest, active_context, corrections_path
469
+
470
+
471
+ def _check_corrections(args: argparse.Namespace) -> dict[str, Any]:
472
+ """`okstra agent-prompt check-corrections` — 실체화 없이 원장만 대조한다."""
473
+ project_root, _manifest_path, manifest, active_context, corrections_path = (
474
+ _corrections_context(args)
475
+ )
476
+ narrative_path = (
477
+ report_narrative_path(project_root, manifest)
478
+ if uses_report_contract_v3(manifest)
479
+ else project_root
480
+ )
481
+ check = run_corrections_check(
482
+ project_root=project_root,
483
+ manifest=manifest,
484
+ active_context=active_context,
485
+ team_state=_load_team_state(project_root, manifest),
486
+ corrections_path=corrections_path,
487
+ narrative_path=narrative_path,
488
+ )
489
+ return corrections_payload(
490
+ check, project_root=project_root, corrections_path=corrections_path,
491
+ )
492
+
493
+
494
+ def _apply_corrections(args: argparse.Namespace) -> dict[str, Any]:
495
+ """`okstra agent-prompt apply-corrections` — 기계적 원장을 서사에 쓴다."""
496
+ project_root, manifest_path, manifest, active_context, corrections_path = (
497
+ _corrections_context(args)
498
+ )
499
+ return run_corrections_apply(
500
+ project_root=project_root,
501
+ manifest=manifest,
502
+ manifest_path=manifest_path,
503
+ active_context=active_context,
504
+ team_state=_load_team_state(project_root, manifest),
505
+ corrections_path=corrections_path,
506
+ )
507
+
508
+
318
509
  def _validate_report_writer_paths(
319
510
  project_root: Path,
320
511
  manifest: Mapping[str, Any],
@@ -99,6 +99,7 @@ def build_analysis_packet(
99
99
  clarification_response_path: Path | None,
100
100
  directive: str,
101
101
  instruction_set_relative_path: str,
102
+ group_context_path: Path | None = None,
102
103
  fix_history_text: str = "",
103
104
  stage_ledger_json: str = "",
104
105
  stage_ledger_notice: str = "",
@@ -112,8 +113,13 @@ def build_analysis_packet(
112
113
  from disk state this file does not own. Each arrives already narrowed, and
113
114
  an empty string means the caller found nothing to carry — the matching
114
115
  block is then omitted rather than rendered saying "none".
116
+
117
+ `group_context_path` is the task-group context prepare copied into the
118
+ instruction set (`task-group-context.md`); `None` means the group has no
119
+ such document and the packet carries no `## Task-Group Context` section.
115
120
  """
116
121
  brief_text = task_brief_path.read_text(encoding="utf-8")
122
+ group_context_text = _read_optional(group_context_path) if group_context_path else ""
117
123
  profile_text = analysis_profile_path.read_text(encoding="utf-8")
118
124
  reference_text = _read_optional(reference_expectations_path)
119
125
  clarification_text = (
@@ -127,8 +133,12 @@ def build_analysis_packet(
127
133
  task_type,
128
134
  instruction_set_relative_path,
129
135
  bool(clarification_response_path),
136
+ bool(group_context_text),
130
137
  )
131
138
  )
139
+ # 그룹 맥락은 브리프 발췌보다 앞에 둔다: 짧고 "왜" 를 담으며, 46KB 에서 잘리는
140
+ # 읽기(agy view_file)에서도 도착해야 하는 절이다.
141
+ parts.extend(_group_context_block(group_context_text))
132
142
  parts.extend(_brief_block(brief_text))
133
143
  parts.extend(_profile_block(task_type, profile_text))
134
144
  parts.extend(_reference_block(reference_text))
@@ -243,6 +253,7 @@ def _intro_block(
243
253
  task_type: str,
244
254
  instruction_set: str,
245
255
  has_clarification: bool,
256
+ has_group_context: bool = False,
246
257
  ) -> list[str]:
247
258
  lines = [
248
259
  f"# OKSTRA Analysis Packet - {task_key}",
@@ -268,9 +279,36 @@ def _intro_block(
268
279
  lines.append(
269
280
  f"- Clarification response: `{instruction_set}/clarification-response.md`"
270
281
  )
282
+ if has_group_context:
283
+ lines.append(
284
+ f"- Task-group context: `{instruction_set}/task-group-context.md`"
285
+ )
271
286
  return lines
272
287
 
273
288
 
289
+ GROUP_CONTEXT_PREFACE = (
290
+ "Background shared by every task in this task-group, copied from the group's "
291
+ "`group-context.md`. It is not this task's requirement ledger: the "
292
+ "`## Task-Specific Brief Extract` below decides what this run must deliver, "
293
+ "while this section decides how the result is measured and what no task in the "
294
+ "group may break. Where the two conflict, raise a `Clarification Items` row; "
295
+ "never resolve the conflict silently in either direction."
296
+ )
297
+
298
+
299
+ def _group_context_block(group_context_text: str) -> list[str]:
300
+ if not group_context_text.strip():
301
+ return []
302
+ return [
303
+ "",
304
+ "## Task-Group Context",
305
+ "",
306
+ GROUP_CONTEXT_PREFACE,
307
+ "",
308
+ _strip_frontmatter(group_context_text).strip(),
309
+ ]
310
+
311
+
274
312
  def _brief_block(brief_text: str) -> list[str]:
275
313
  return [
276
314
  "",