okstra 0.200.0 → 0.201.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 +4 -2
- package/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/docs/cli.md +14 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +7 -3
- package/runtime/bin/okstra-spawn-followups.py +2 -2
- package/runtime/prompts/duties/technical-verification-worker.md +44 -0
- package/runtime/prompts/launch.template.md +7 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +7 -2
- package/runtime/prompts/lead/plan-body-verification.md +3 -1
- package/runtime/prompts/lead/report-writer.md +11 -5
- package/runtime/prompts/lead/team-contract.md +6 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +7 -1
- package/runtime/prompts/profiles/final-verification.md +5 -0
- package/runtime/prompts/profiles/forbidden-actions.json +6 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +7 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/prompts/profiles/technical-verification.md +53 -0
- package/runtime/prompts/wizard/prompts.ko.json +2 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -4
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +36 -5
- package/runtime/python/okstra_ctl/agent/invocation.py +14 -6
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +4 -3
- package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +83 -22
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +44 -2
- package/runtime/python/okstra_ctl/conformance.py +2 -20
- package/runtime/python/okstra_ctl/dispatch_core.py +25 -5
- package/runtime/python/okstra_ctl/dispatch_state.py +2 -0
- package/runtime/python/okstra_ctl/domain/provider.py +0 -1
- package/runtime/python/okstra_ctl/domain/role.py +1 -0
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -1
- package/runtime/python/okstra_ctl/implementation_direction.py +64 -7
- package/runtime/python/okstra_ctl/implementation_options.py +58 -45
- package/runtime/python/okstra_ctl/model_pool.py +2 -5
- package/runtime/python/okstra_ctl/next_phase.py +3 -0
- package/runtime/python/okstra_ctl/plan_items.py +15 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +9 -3
- package/runtime/python/okstra_ctl/qa_commands.py +30 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +11 -8
- package/runtime/python/okstra_ctl/render.py +3 -0
- package/runtime/python/okstra_ctl/render_final_report.py +1 -0
- package/runtime/python/okstra_ctl/report_assembly.py +8 -2
- package/runtime/python/okstra_ctl/report_contract.py +3 -0
- package/runtime/python/okstra_ctl/report_corrections.py +209 -93
- package/runtime/python/okstra_ctl/report_finalize.py +25 -8
- package/runtime/python/okstra_ctl/report_html/router.py +2 -0
- package/runtime/python/okstra_ctl/report_html/view_models/technical_verification.py +21 -0
- package/runtime/python/okstra_ctl/report_projections.py +4 -3
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +181 -47
- package/runtime/python/okstra_ctl/run.py +82 -0
- package/runtime/python/okstra_ctl/team.py +4 -1
- package/runtime/python/okstra_ctl/technical_verification.py +195 -0
- package/runtime/python/okstra_ctl/usage_identity.py +54 -0
- package/runtime/python/okstra_ctl/usage_report.py +22 -8
- package/runtime/python/okstra_ctl/verification_target.py +74 -0
- package/runtime/python/okstra_ctl/wizard/__init__.py +1 -1
- package/runtime/python/okstra_ctl/wizard/cli.py +2 -1
- package/runtime/python/okstra_ctl/wizard/confirmation.py +38 -2
- package/runtime/python/okstra_ctl/wizard/engine.py +3 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -0
- package/runtime/python/okstra_ctl/wizard/outcome.py +63 -0
- package/runtime/python/okstra_ctl/wizard/picker_navigation.py +2 -2
- package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
- package/runtime/python/okstra_ctl/wizard/render.py +8 -55
- package/runtime/python/okstra_ctl/wizard/roles.py +11 -7
- package/runtime/python/okstra_ctl/wizard/sources.py +28 -2
- package/runtime/python/okstra_ctl/wizard/state.py +13 -6
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +8 -0
- package/runtime/python/okstra_ctl/worker_liveness.py +52 -39
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
- package/runtime/python/okstra_ctl/workflow.py +8 -0
- package/runtime/python/okstra_ctl/write_policy.py +23 -0
- package/runtime/python/okstra_token_usage/blocks.py +50 -1
- package/runtime/python/okstra_token_usage/claude.py +42 -21
- package/runtime/python/okstra_token_usage/codex.py +17 -0
- package/runtime/python/okstra_token_usage/collect.py +299 -162
- package/runtime/python/okstra_token_usage/cursor.py +2 -3
- package/runtime/python/okstra_token_usage/report.py +35 -30
- package/runtime/python/okstra_token_usage/task_totals.py +3 -12
- package/runtime/schemas/final-report-v2.0.schema.json +298 -7
- package/runtime/schemas/final-report-v3.0.schema.json +298 -7
- package/runtime/schemas/report-narrative-v3.0.schema.json +1 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +1 -1
- package/runtime/schemas/report-writer-corrections-v1.0.schema.json +30 -3
- package/runtime/skills/okstra-run/SKILL.md +10 -2
- package/runtime/skills/okstra-setup/SKILL.md +42 -7
- package/runtime/templates/report-writer-prompt-preamble.md +7 -3
- package/runtime/templates/reports/html/i18n/en.json +11 -0
- package/runtime/templates/reports/html/i18n/ko.json +11 -0
- package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +7 -3
- package/runtime/templates/reports/html/tasks/technical-verification.template.html +35 -0
- package/runtime/templates/reports/md/tasks/technical-verification.template.md +5 -0
- package/runtime/validators/validate-run.py +9 -4
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import copy
|
|
5
|
-
import hashlib
|
|
6
5
|
import json
|
|
7
6
|
import os
|
|
8
7
|
import re
|
|
9
8
|
import tempfile
|
|
9
|
+
from collections import Counter
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any, Mapping
|
|
13
13
|
|
|
14
|
+
from okstra_token_usage.blocks import accounting_workers
|
|
15
|
+
|
|
14
16
|
from .analysis_packet import reference_source_extracts
|
|
15
17
|
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
16
18
|
from .implementation_options import (
|
|
@@ -54,7 +56,6 @@ class ReportSynthesisSource:
|
|
|
54
56
|
label: str
|
|
55
57
|
owner: str
|
|
56
58
|
path: str
|
|
57
|
-
digest: str
|
|
58
59
|
content: str
|
|
59
60
|
|
|
60
61
|
def to_dict(self) -> dict[str, str]:
|
|
@@ -62,7 +63,6 @@ class ReportSynthesisSource:
|
|
|
62
63
|
"label": self.label,
|
|
63
64
|
"owner": self.owner,
|
|
64
65
|
"path": self.path,
|
|
65
|
-
"digest": self.digest,
|
|
66
66
|
"content": self.content,
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -164,6 +164,17 @@ class ReportSynthesisPacket:
|
|
|
164
164
|
return ["carried-stages-unchanged"] if self.incremental_decision else []
|
|
165
165
|
|
|
166
166
|
def _task_instructions(self) -> list[str]:
|
|
167
|
+
if self.task_type == "technical-verification":
|
|
168
|
+
return [
|
|
169
|
+
"Copy sourceReport, sourceDataSha256, scope and every fact identity "
|
|
170
|
+
"from the frozen technical input embedded in the Analysis packet.",
|
|
171
|
+
"Inspect the run-local plans and command logs cited by settled worker "
|
|
172
|
+
"results. These cited experiment artifacts are part of this task's "
|
|
173
|
+
"evidence scope. Record one check per fact and retain limitations.",
|
|
174
|
+
"Route only to implementation-option-selection with one matching "
|
|
175
|
+
"phase-continuation. Evidence does not approve adoption or replace "
|
|
176
|
+
"independent candidate feasibility votes.",
|
|
177
|
+
]
|
|
167
178
|
if self.task_type == "implementation-planning":
|
|
168
179
|
return self._planning_requirement_instructions()
|
|
169
180
|
if self.task_type != "implementation-option-selection":
|
|
@@ -206,6 +217,16 @@ class ReportSynthesisPacket:
|
|
|
206
217
|
"supplied one feasibility vote, at least "
|
|
207
218
|
f"{MIN_FEASIBLE_VOTES} votes are `feasible`, and both `safetyBlockers` "
|
|
208
219
|
"and `unresolvedFeasibilityFacts` are empty.",
|
|
220
|
+
("Classify each `unresolvedFeasibilityFacts` entry with `resolutionKind`: "
|
|
221
|
+
"`user-decision` requires nonempty `clarificationRefs` linking that fact "
|
|
222
|
+
"to actual C-NNN records, including answered records; `technical-verification` "
|
|
223
|
+
"requires no user question. Preserve the fact, whyItMatters, and evidence. "
|
|
224
|
+
"Historical unclassified facts remain readable but require classification "
|
|
225
|
+
"before blocked report reassembly. Saving does not lift blocked routing."),
|
|
226
|
+
("After a linked decision is answered, preserve its disposition and selected "
|
|
227
|
+
"value. Update humanSummary.actions, verdictCard.nextStep, and selection "
|
|
228
|
+
"guidance to name remaining technical verification separately from unanswered "
|
|
229
|
+
"user decisions; never reopen an answered question to permit saving."),
|
|
209
230
|
"Each `feasibilityVotes` row states that analyser's own verdict, "
|
|
210
231
|
"rationale, and strongest counterevidence as its result gives them. "
|
|
211
232
|
"Two non-`uncertain` votes with identical rationale and "
|
|
@@ -282,7 +303,6 @@ class ReportSynthesisPacket:
|
|
|
282
303
|
"user response carry-in",
|
|
283
304
|
],
|
|
284
305
|
"validationRules": [
|
|
285
|
-
"source-digest-match",
|
|
286
306
|
"writer-owned-fields-only",
|
|
287
307
|
"all-defects-collected",
|
|
288
308
|
*self._carry_validation_rules(),
|
|
@@ -307,7 +327,7 @@ class ReportSynthesisPacket:
|
|
|
307
327
|
"- Runtime-owned values: session identifiers, token usage, estimated cost, "
|
|
308
328
|
"user response carry-in",
|
|
309
329
|
"- Accounting details stay in the JSON snapshot; assembly supplies them.",
|
|
310
|
-
"- Validation:
|
|
330
|
+
"- Validation: writer-owned fields, all defects collected",
|
|
311
331
|
]
|
|
312
332
|
# 작성자가 실제로 읽는 것은 마크다운이다. JSON 정본에만 실으면 지시가
|
|
313
333
|
# 도달하지 않는다.
|
|
@@ -315,9 +335,14 @@ class ReportSynthesisPacket:
|
|
|
315
335
|
lines.extend(f"- {text}" for text in self._schema_instructions())
|
|
316
336
|
lines.extend(f"- {text}" for text in self._task_instructions())
|
|
317
337
|
lines.extend(f"- {text}" for text in self._carry_instructions())
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
338
|
+
sections = [("Authoring Contract", "\n".join(lines[1:]).rstrip() + "\n")]
|
|
339
|
+
sections.extend(
|
|
340
|
+
(source.label, "\n".join(_source_markdown(
|
|
341
|
+
source, self.task_type, self.sources,
|
|
342
|
+
)).rstrip() + "\n")
|
|
343
|
+
for source in self.sources
|
|
344
|
+
)
|
|
345
|
+
return _indexed_markdown(lines[0], sections)
|
|
321
346
|
|
|
322
347
|
|
|
323
348
|
_SOURCE_FIELDS = (
|
|
@@ -333,12 +358,156 @@ _SOURCE_FIELDS = (
|
|
|
333
358
|
)
|
|
334
359
|
|
|
335
360
|
|
|
361
|
+
_READ_CHUNK_BYTES = 16_000
|
|
362
|
+
_SHARED_TEXT_MIN_BYTES = 256
|
|
363
|
+
_ANALYSER_OPERATION_SECTIONS = frozenset({
|
|
364
|
+
"Required workers", "Optional workers", "Team contract",
|
|
365
|
+
"Worker interaction model", "Tooling — read-only MCP availability",
|
|
366
|
+
"Run-scoped worker-resource lifecycle",
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _indexed_markdown(title: str, sections: list[tuple[str, str]]) -> str:
|
|
371
|
+
"""긴 한 줄과 다중 바이트 문자도 손실 없이 나눌 읽기 위치를 싣는다."""
|
|
372
|
+
ranges: list[tuple[str, int, int]] = []
|
|
373
|
+
offset = 0
|
|
374
|
+
for label, content in sections:
|
|
375
|
+
raw = content.encode("utf-8")
|
|
376
|
+
start = 0
|
|
377
|
+
while start < len(raw):
|
|
378
|
+
end = min(start + _READ_CHUNK_BYTES, len(raw))
|
|
379
|
+
while end < len(raw) and raw[end] & 0xC0 == 0x80:
|
|
380
|
+
end -= 1
|
|
381
|
+
ranges.append((label, offset + start, end - start))
|
|
382
|
+
start = end
|
|
383
|
+
offset += len(raw)
|
|
384
|
+
prefix = f"{title}\n\n## Read Index\n\n"
|
|
385
|
+
prefix += (
|
|
386
|
+
"Read every range once, in order. Offsets are zero-based UTF-8 bytes "
|
|
387
|
+
"in this Markdown file; each range is at most 16000 bytes. Use "
|
|
388
|
+
"`dd if='<packet-path>' bs=1 skip=<start> count=<bytes> 2>/dev/null`. "
|
|
389
|
+
"Continue with the next range after a successful read; do not reread "
|
|
390
|
+
"the beginning. Source paths identify originals; the sibling "
|
|
391
|
+
"`.data.json` retains every frozen source verbatim.\n\n"
|
|
392
|
+
"| Source | Start byte | Bytes |\n|---|---:|---:|\n"
|
|
393
|
+
)
|
|
394
|
+
prefix_size = 0
|
|
395
|
+
while True:
|
|
396
|
+
index = prefix + "".join(
|
|
397
|
+
f"| {label.replace('|', '/')} | {start + prefix_size} | {size} |\n"
|
|
398
|
+
for label, start, size in ranges
|
|
399
|
+
) + "\n<!-- END READ INDEX -->\n"
|
|
400
|
+
size = len(index.encode("utf-8"))
|
|
401
|
+
if size == prefix_size:
|
|
402
|
+
break
|
|
403
|
+
prefix_size = size
|
|
404
|
+
return index + "".join(content for _, content in sections)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _writer_profile_view(content: str) -> str:
|
|
408
|
+
"""저작 규칙과 미지 절을 유지하고 분석자 배치 절만 참조로 바꾼다."""
|
|
409
|
+
lines: list[str] = []
|
|
410
|
+
skipping = False
|
|
411
|
+
fence = ""
|
|
412
|
+
for number, line in enumerate(content.splitlines(), 1):
|
|
413
|
+
marker = re.match(r"^\s*(`{3,}|~{3,})", line)
|
|
414
|
+
if marker:
|
|
415
|
+
token = marker.group(1)
|
|
416
|
+
if not fence:
|
|
417
|
+
fence = token
|
|
418
|
+
elif token[0] == fence[0] and len(token) >= len(fence):
|
|
419
|
+
fence = ""
|
|
420
|
+
if not fence and line.startswith("- "):
|
|
421
|
+
label = line[2:].split(":", 1)[0].split(" (", 1)[0].strip("*")
|
|
422
|
+
skipping = label in _ANALYSER_OPERATION_SECTIONS
|
|
423
|
+
if skipping:
|
|
424
|
+
lines.append(f"- {label}: analyser operation; original source line {number}.")
|
|
425
|
+
elif not fence and line.startswith("#"):
|
|
426
|
+
skipping = False
|
|
427
|
+
if not skipping:
|
|
428
|
+
lines.append(line)
|
|
429
|
+
return "\n".join(lines)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _replace_shared_text(value: Any, references: Mapping[str, str]) -> Any:
|
|
433
|
+
if isinstance(value, str):
|
|
434
|
+
return {"$sharedText": references[value]} if value in references else value
|
|
435
|
+
if isinstance(value, list):
|
|
436
|
+
return [_replace_shared_text(item, references) for item in value]
|
|
437
|
+
if isinstance(value, dict):
|
|
438
|
+
return {key: _replace_shared_text(item, references) for key, item in value.items()}
|
|
439
|
+
return value
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _convergence_view(content: str) -> str:
|
|
443
|
+
"""반복된 근거 문자열만 공유하고 모든 회차·표·미지 필드는 유지한다."""
|
|
444
|
+
try:
|
|
445
|
+
state = json.loads(content)
|
|
446
|
+
except json.JSONDecodeError:
|
|
447
|
+
return content
|
|
448
|
+
counts: Counter[str] = Counter()
|
|
449
|
+
pending = [state]
|
|
450
|
+
while pending:
|
|
451
|
+
value = pending.pop()
|
|
452
|
+
if isinstance(value, dict):
|
|
453
|
+
# 원자료의 같은 모양과 새 참조를 혼동하지 않는다.
|
|
454
|
+
if "$sharedText" in value:
|
|
455
|
+
return content
|
|
456
|
+
pending.extend(value.values())
|
|
457
|
+
elif isinstance(value, list):
|
|
458
|
+
pending.extend(value)
|
|
459
|
+
elif isinstance(value, str) and len(value.encode("utf-8")) >= _SHARED_TEXT_MIN_BYTES:
|
|
460
|
+
counts[value] += 1
|
|
461
|
+
repeated = sorted(text for text, count in counts.items() if count > 1)
|
|
462
|
+
if not repeated:
|
|
463
|
+
return content
|
|
464
|
+
references = {text: f"T{index}" for index, text in enumerate(repeated, 1)}
|
|
465
|
+
view = {
|
|
466
|
+
"sharedText": {ref: text for text, ref in references.items()},
|
|
467
|
+
"state": _replace_shared_text(state, references),
|
|
468
|
+
}
|
|
469
|
+
return json.dumps(view, ensure_ascii=False, indent=1)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _writer_schema_view(content: str, task_type: str) -> tuple[str, list[str]]:
|
|
473
|
+
try:
|
|
474
|
+
schema = json.loads(content)
|
|
475
|
+
except json.JSONDecodeError:
|
|
476
|
+
return content, ["- View: schema is not JSON; the original text is retained."]
|
|
477
|
+
if not isinstance(schema, dict) or not schema.get("properties"):
|
|
478
|
+
return content, []
|
|
479
|
+
owned = writer_owned_schema(schema)
|
|
480
|
+
# 부분 서사 검증에서 빠지는 루트 조건도 최종 조립의 저작 지침에는 남긴다.
|
|
481
|
+
owned["allOf"] = schema.get("allOf", [])
|
|
482
|
+
owned["required"] = [
|
|
483
|
+
key for key in schema.get("required", []) if key in owned["properties"]
|
|
484
|
+
]
|
|
485
|
+
excerpt = build_schema_excerpt(owned, task_type)
|
|
486
|
+
return json.dumps(excerpt, ensure_ascii=False, indent=1), [
|
|
487
|
+
"- View: writer-owned schema excerpt. Task-specific requirements are listed above.",
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
|
|
336
491
|
def _source_markdown(
|
|
337
492
|
source: ReportSynthesisSource, task_type: str,
|
|
338
493
|
sources: tuple[ReportSynthesisSource, ...],
|
|
339
494
|
) -> list[str]:
|
|
340
495
|
content = source.content.rstrip("\n")
|
|
341
496
|
view = []
|
|
497
|
+
if source.label == "Analysis profile":
|
|
498
|
+
content = _writer_profile_view(content)
|
|
499
|
+
if source.label == "Analysis material":
|
|
500
|
+
brief = next((item.content.strip() for item in sources if item.label == "Task brief"), "")
|
|
501
|
+
if len(brief.encode("utf-8")) >= _SHARED_TEXT_MIN_BYTES:
|
|
502
|
+
content = content.replace(brief, 'Read the complete "Source: Task brief" above.')
|
|
503
|
+
if source.label == "Convergence state":
|
|
504
|
+
content = _convergence_view(content)
|
|
505
|
+
if content != source.content.rstrip("\n"):
|
|
506
|
+
view = [
|
|
507
|
+
"- View: lossless shared-text references. Replace each "
|
|
508
|
+
'`{"$sharedText":"Tn"}` with `sharedText.Tn` when reading `state`. '
|
|
509
|
+
"Every finding, vote, round, condition, dissent, and unknown field remains.",
|
|
510
|
+
]
|
|
342
511
|
if source.label == "Analysis packet":
|
|
343
512
|
texts = {item.label: item.content for item in sources}
|
|
344
513
|
content = reference_source_extracts(
|
|
@@ -351,32 +520,10 @@ def _source_markdown(
|
|
|
351
520
|
if content != source.content.rstrip("\n"):
|
|
352
521
|
view = [
|
|
353
522
|
"- View: repeated extracts refer to frozen sources below; "
|
|
354
|
-
"the original line index is omitted.
|
|
355
|
-
"the frozen original.",
|
|
523
|
+
"the original line index is omitted.",
|
|
356
524
|
]
|
|
357
525
|
if source.label == "Final report schema":
|
|
358
|
-
|
|
359
|
-
schema = json.loads(source.content)
|
|
360
|
-
except json.JSONDecodeError:
|
|
361
|
-
schema = {}
|
|
362
|
-
view = ["- View: schema is not JSON; the original text is retained."]
|
|
363
|
-
if isinstance(schema, dict) and schema.get("properties"):
|
|
364
|
-
# 원본과 해시는 JSON 보관본에 남긴다. 저작용 읽기에는 검증기와
|
|
365
|
-
# 같은 소유권 투영을 쓰고, 도달하지 않는 정의만 기존 발췌기로 뺀다.
|
|
366
|
-
owned = writer_owned_schema(schema)
|
|
367
|
-
# 부분 서사 검증에서 빠지는 루트 조건도 작성 지침에는 남긴다.
|
|
368
|
-
# 판정별 금지 필드와 후속 작업 조건은 최종 조립에 적용된다.
|
|
369
|
-
owned["allOf"] = schema.get("allOf", [])
|
|
370
|
-
owned["required"] = [
|
|
371
|
-
key for key in schema.get("required", [])
|
|
372
|
-
if key in owned["properties"]
|
|
373
|
-
]
|
|
374
|
-
excerpt = build_schema_excerpt(owned, task_type)
|
|
375
|
-
content = json.dumps(excerpt, ensure_ascii=False, indent=2)
|
|
376
|
-
view = [
|
|
377
|
-
"- View: writer-owned schema excerpt; the digest identifies the "
|
|
378
|
-
"frozen original. Task-specific requirements are listed above.",
|
|
379
|
-
]
|
|
526
|
+
content, view = _writer_schema_view(content, task_type)
|
|
380
527
|
longest = max((len(run) for run in re.findall(r"`+", content)), default=0)
|
|
381
528
|
fence = "`" * max(3, longest + 1)
|
|
382
529
|
return [
|
|
@@ -385,7 +532,6 @@ def _source_markdown(
|
|
|
385
532
|
"",
|
|
386
533
|
f"- Owner: `{source.owner}`",
|
|
387
534
|
f"- Path: `{source.path}`",
|
|
388
|
-
f"- Digest: `{source.digest}`",
|
|
389
535
|
*view,
|
|
390
536
|
"",
|
|
391
537
|
f"{fence}text",
|
|
@@ -628,7 +774,7 @@ def _attempt_result_specs(
|
|
|
628
774
|
|
|
629
775
|
|
|
630
776
|
def _accounting_snapshot(team_state: Mapping[str, Any]) -> dict[str, Any]:
|
|
631
|
-
workers = team_state
|
|
777
|
+
workers = accounting_workers(team_state)
|
|
632
778
|
worker_usage = []
|
|
633
779
|
if isinstance(workers, list):
|
|
634
780
|
worker_usage = [
|
|
@@ -700,13 +846,11 @@ def _read_sources(
|
|
|
700
846
|
except (OSError, UnicodeError) as exc:
|
|
701
847
|
issues.append(ReportSynthesisPacketIssue(owner, label, path, str(exc)))
|
|
702
848
|
continue
|
|
703
|
-
digest = "sha256:" + hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
704
849
|
sources.append(
|
|
705
850
|
ReportSynthesisSource(
|
|
706
851
|
label=label,
|
|
707
852
|
owner=owner,
|
|
708
853
|
path=_relative(project_root, path),
|
|
709
|
-
digest=digest,
|
|
710
854
|
content=content,
|
|
711
855
|
)
|
|
712
856
|
)
|
|
@@ -929,18 +1073,8 @@ def verify_report_synthesis_packet_sources(
|
|
|
929
1073
|
)
|
|
930
1074
|
continue
|
|
931
1075
|
try:
|
|
932
|
-
|
|
1076
|
+
path.read_text(encoding="utf-8")
|
|
933
1077
|
except (OSError, UnicodeError) as exc:
|
|
934
1078
|
issues.append(ReportSynthesisPacketIssue(owner, label, path, str(exc)))
|
|
935
1079
|
continue
|
|
936
|
-
digest = "sha256:" + hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
937
|
-
if digest != source.get("digest"):
|
|
938
|
-
issues.append(
|
|
939
|
-
ReportSynthesisPacketIssue(
|
|
940
|
-
owner,
|
|
941
|
-
label,
|
|
942
|
-
path,
|
|
943
|
-
"source value changed after packet materialization",
|
|
944
|
-
)
|
|
945
|
-
)
|
|
946
1080
|
return tuple(issues)
|
|
@@ -14,6 +14,11 @@ state passing, and are read once at the start.
|
|
|
14
14
|
"""
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
from .technical_verification import (
|
|
18
|
+
resolve_technical_verification_input,
|
|
19
|
+
write_technical_verification_input,
|
|
20
|
+
)
|
|
21
|
+
|
|
17
22
|
import hashlib
|
|
18
23
|
from argparse import (
|
|
19
24
|
Action,
|
|
@@ -529,6 +534,7 @@ class PrepareInputs:
|
|
|
529
534
|
brief_path: Path # absolute, already resolved
|
|
530
535
|
analysis_target: str = ""
|
|
531
536
|
evidence_inputs_raw: str = ""
|
|
537
|
+
user_authorization_json: str = ""
|
|
532
538
|
directive: str = ""
|
|
533
539
|
workers_override: str = ""
|
|
534
540
|
role_counts_raw: tuple[str, ...] = ()
|
|
@@ -1175,6 +1181,7 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
|
|
|
1175
1181
|
),
|
|
1176
1182
|
("--critic", inp.critic or legacy_fallbacks.get("CRITIC_CHOICE", "")),
|
|
1177
1183
|
("--related-tasks", inp.related_tasks_raw),
|
|
1184
|
+
("--user-authorization-json", inp.user_authorization_json),
|
|
1178
1185
|
("--work-category", inp.work_category),
|
|
1179
1186
|
]
|
|
1180
1187
|
argv: list[str] = []
|
|
@@ -3819,6 +3826,18 @@ def _write_instruction_set_sources(
|
|
|
3819
3826
|
json.loads(ctx.get("RELATED_TASKS_JSON", "[]")),
|
|
3820
3827
|
),
|
|
3821
3828
|
)
|
|
3829
|
+
if inp.task_type == "technical-verification":
|
|
3830
|
+
verification_path = write_technical_verification_input(
|
|
3831
|
+
json.loads(ctx["TECHNICAL_VERIFICATION_INPUT_JSON"]),
|
|
3832
|
+
Path(ctx["RUN_DIR"]), ctx["RUN_MANIFESTS_SEQ"],
|
|
3833
|
+
)
|
|
3834
|
+
packet += (
|
|
3835
|
+
"\n## Technical Verification Input\n\n"
|
|
3836
|
+
f"- Frozen input: `{verification_path}`\n"
|
|
3837
|
+
f"- Experiment copies only: `{ctx['RUN_DIR']}/experiments/{ctx['RUN_MANIFESTS_SEQ']}/<worker-id>/`\n"
|
|
3838
|
+
"- Read the input before planning experiments; copy each fact identity verbatim into results.\n"
|
|
3839
|
+
+ "\n```json\n" + ctx["TECHNICAL_VERIFICATION_INPUT_JSON"] + "\n```\n"
|
|
3840
|
+
)
|
|
3822
3841
|
if inp.task_type in ANALYSIS_TASK_TYPES:
|
|
3823
3842
|
packet += (
|
|
3824
3843
|
"\n## Analysis Evidence\n\n"
|
|
@@ -4662,8 +4681,54 @@ def _title_lead_pane(inp: PrepareInputs) -> None:
|
|
|
4662
4681
|
print(f"okstra: lead pane title not applied — {reason}", file=sys.stderr)
|
|
4663
4682
|
|
|
4664
4683
|
|
|
4684
|
+
def _prepare_user_authorization(inp: PrepareInputs) -> dict:
|
|
4685
|
+
"""승인 기록은 해당 실행 입력과 일치할 때만 전달하며 호스트 권한을 부여하지 않는다."""
|
|
4686
|
+
if not inp.user_authorization_json:
|
|
4687
|
+
return {}
|
|
4688
|
+
try:
|
|
4689
|
+
record = json.loads(inp.user_authorization_json)
|
|
4690
|
+
except (TypeError, ValueError) as exc:
|
|
4691
|
+
raise PrepareError("invalid user authorization JSON") from exc
|
|
4692
|
+
if not isinstance(record, dict) or (
|
|
4693
|
+
record.get("schemaVersion") != "1.0"
|
|
4694
|
+
or record.get("source") != "wizard-confirmation"
|
|
4695
|
+
or record.get("response") != "proceed"
|
|
4696
|
+
or not isinstance(record.get("prompt"), str)
|
|
4697
|
+
or not record["prompt"].strip()
|
|
4698
|
+
or not isinstance(record.get("scope"), dict)
|
|
4699
|
+
):
|
|
4700
|
+
raise PrepareError("user authorization requires the displayed prompt, proceed response, and scope")
|
|
4701
|
+
if inp.task_type == "implementation":
|
|
4702
|
+
stages = record.get("stageScope") or [record["scope"].get("stage")]
|
|
4703
|
+
if not isinstance(stages, list) or inp.stage not in stages:
|
|
4704
|
+
raise PrepareError("user authorization scope does not include --stage; confirm the changed scope")
|
|
4705
|
+
expected = {
|
|
4706
|
+
"project-root": str(inp.project_root), "project-id": inp.project_id,
|
|
4707
|
+
"task-group": inp.task_group, "task-id": inp.task_id, "task-type": inp.task_type,
|
|
4708
|
+
"role-model": list(inp.role_models_raw), "role-count": list(inp.role_counts_raw),
|
|
4709
|
+
"directive": inp.directive, "related-tasks": inp.related_tasks_raw,
|
|
4710
|
+
"analysis-target": inp.analysis_target, "evidence-inputs": inp.evidence_inputs_raw,
|
|
4711
|
+
}
|
|
4712
|
+
for key, value in expected.items():
|
|
4713
|
+
if record["scope"].get(key) != value:
|
|
4714
|
+
raise PrepareError(f"user authorization scope does not match --{key}; confirm the changed scope")
|
|
4715
|
+
for key, path in (
|
|
4716
|
+
("task-brief", inp.brief_path if inp.task_type != "release-handoff" else ""),
|
|
4717
|
+
("approved-plan", inp.approved_plan_path),
|
|
4718
|
+
("clarification-response", inp.clarification_response_path),
|
|
4719
|
+
("selected-direction", inp.selected_direction_path),
|
|
4720
|
+
):
|
|
4721
|
+
recorded = record["scope"].get(key, "")
|
|
4722
|
+
if not isinstance(recorded, str) or bool(recorded) != bool(path) or (
|
|
4723
|
+
recorded and (inp.project_root / recorded).resolve() != (inp.project_root / path).resolve()
|
|
4724
|
+
):
|
|
4725
|
+
raise PrepareError(f"user authorization scope does not match --{key}; confirm the changed scope")
|
|
4726
|
+
return record
|
|
4727
|
+
|
|
4728
|
+
|
|
4665
4729
|
def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
4666
4730
|
"""Produce a complete okstra task bundle on disk. See module docstring."""
|
|
4731
|
+
user_authorization = _prepare_user_authorization(inp)
|
|
4667
4732
|
workspace_root = Path(inp.workspace_root)
|
|
4668
4733
|
project_root = Path(inp.project_root)
|
|
4669
4734
|
lead_runtime = _normalize_lead_runtime(inp.lead_runtime)
|
|
@@ -4709,6 +4774,19 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4709
4774
|
)
|
|
4710
4775
|
_validate_group_context_preflight(project_root, inp.task_group)
|
|
4711
4776
|
selected_direction = _resolve_planning_direction(inp)
|
|
4777
|
+
technical_input = None
|
|
4778
|
+
from .paths import task_dir
|
|
4779
|
+
if inp.task_type == "technical-verification":
|
|
4780
|
+
if not inp.clarification_response_path:
|
|
4781
|
+
raise PrepareError("technical-verification requires --clarification-response pointing to this task's option-selection record")
|
|
4782
|
+
try:
|
|
4783
|
+
technical_input = resolve_technical_verification_input(
|
|
4784
|
+
Path(inp.clarification_response_path), project_root,
|
|
4785
|
+
task_dir(project_root, inp.task_group, inp.task_id),
|
|
4786
|
+
f"{inp.project_id}:{inp.task_group}:{inp.task_id}",
|
|
4787
|
+
)
|
|
4788
|
+
except ValueError as exc:
|
|
4789
|
+
raise PrepareError(str(exc)) from exc
|
|
4712
4790
|
if inp.task_type == "implementation":
|
|
4713
4791
|
ctx_stage_map = _prepare_implementation_approved_plan(inp)
|
|
4714
4792
|
|
|
@@ -4871,6 +4949,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4871
4949
|
relative_to_project_root(Path(inp.clarification_response_path), project_root)
|
|
4872
4950
|
if inp.clarification_response_path else ""
|
|
4873
4951
|
)
|
|
4952
|
+
ctx["TECHNICAL_VERIFICATION_INPUT_JSON"] = json.dumps(technical_input) if technical_input else ""
|
|
4874
4953
|
selected_direction_relative = (
|
|
4875
4954
|
relative_to_project_root(Path(inp.selected_direction_path), project_root)
|
|
4876
4955
|
if inp.selected_direction_path else ""
|
|
@@ -4888,6 +4967,7 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4888
4967
|
"RECOMMENDED_ANALYSERS": selected_reviewers,
|
|
4889
4968
|
"HOST_RUNTIME": lead_runtime,
|
|
4890
4969
|
"LEAD_RUNTIME": lead_runtime,
|
|
4970
|
+
"USER_AUTHORIZATION_JSON": json.dumps(user_authorization, ensure_ascii=False),
|
|
4891
4971
|
"LEAD_RUNTIME_REQUEST": lead_runtime_request,
|
|
4892
4972
|
"RUNTIME_RESOLUTION_JSON": runtime_resolution_json or "{}",
|
|
4893
4973
|
"PR_TEMPLATE_PATH": pr_template_path_str,
|
|
@@ -5107,6 +5187,7 @@ def build_prepare_argument_parser():
|
|
|
5107
5187
|
),
|
|
5108
5188
|
)
|
|
5109
5189
|
p.add_argument("--directive", default="")
|
|
5190
|
+
p.add_argument("--user-authorization-json", default="")
|
|
5110
5191
|
p.add_argument("--analysis-target", default="", dest="analysis_target")
|
|
5111
5192
|
p.add_argument("--evidence-inputs", default="", dest="evidence_inputs_raw")
|
|
5112
5193
|
p.add_argument(
|
|
@@ -5392,6 +5473,7 @@ def main(argv: list[str]) -> int:
|
|
|
5392
5473
|
analysis_target=args.analysis_target,
|
|
5393
5474
|
evidence_inputs_raw=args.evidence_inputs_raw,
|
|
5394
5475
|
directive=args.directive,
|
|
5476
|
+
user_authorization_json=args.user_authorization_json,
|
|
5395
5477
|
workers_override=args.workers_override,
|
|
5396
5478
|
role_counts_raw=tuple(args.role_counts_raw),
|
|
5397
5479
|
role_models_raw=tuple(args.role_models_raw),
|
|
@@ -125,7 +125,10 @@ def _add_await_parser(sub) -> None:
|
|
|
125
125
|
_add_run_args(parser)
|
|
126
126
|
parser.add_argument("--poll-interval-seconds", type=int, default=5)
|
|
127
127
|
parser.add_argument("--timeout-seconds", type=int, default=None)
|
|
128
|
-
parser.add_argument(
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--heartbeat-seconds", type=int, default=30,
|
|
130
|
+
help="minimum interval between changed pending-attempt summaries; 0 disables output",
|
|
131
|
+
)
|
|
129
132
|
parser.add_argument("--json", action="store_true")
|
|
130
133
|
|
|
131
134
|
|