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.
- package/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/docs/architecture/storage-model.md +9 -0
- package/docs/cli.md +5 -2
- package/docs/for-ai/skills/okstra-brief-gen.md +2 -0
- package/docs/project-structure-overview.md +5 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +2 -0
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/lead/plan-body-verification.md +1 -1
- package/runtime/prompts/lead/report-writer.md +11 -8
- package/runtime/prompts/profiles/_common-contract.md +1 -0
- package/runtime/python/okstra_ctl/agent/activity.py +3 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +51 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +280 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +191 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +38 -0
- package/runtime/python/okstra_ctl/group_context.py +223 -0
- package/runtime/python/okstra_ctl/model_io/renderers.py +12 -7
- package/runtime/python/okstra_ctl/recap.py +7 -1
- package/runtime/python/okstra_ctl/report_corrections.py +443 -0
- package/runtime/python/okstra_ctl/report_narrative.py +44 -3
- package/runtime/python/okstra_ctl/run.py +28 -0
- package/runtime/python/okstra_ctl/timeline_runs.py +71 -0
- package/runtime/python/okstra_ctl/wizard.py +2 -1
- package/runtime/schemas/final-report-v3.0.schema.json +1 -0
- package/runtime/schemas/report-writer-corrections-v1.0.schema.json +52 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +29 -1
- package/runtime/skills/okstra-inspect/facets/history.md +1 -0
- package/runtime/skills/okstra-inspect/facets/recap.md +1 -1
- package/runtime/templates/report-writer-prompt-preamble.md +6 -0
- package/runtime/templates/reports/group-context.template.md +31 -0
- 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
|
"",
|