okstra 0.214.1 → 0.216.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/cli-registry.d.mts +14 -0
- package/dist/cli-registry.mjs +12 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/chat/chat.mjs +2 -56
- package/dist/commands/chat/chat.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +5 -2
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/wizard.mjs +6 -0
- package/dist/commands/execute/wizard.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +57 -6
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/file-lock.d.mts +1 -0
- package/dist/lib/file-lock.mjs +60 -0
- package/dist/lib/file-lock.mjs.map +1 -0
- package/dist/lib/python-command.mjs +11 -1
- package/dist/lib/python-command.mjs.map +1 -1
- package/dist/lib/skill-catalog.mjs +1 -0
- package/dist/lib/skill-catalog.mjs.map +1 -1
- package/dist/lib/types.d.mts +1 -1
- package/docs/architecture.md +2 -2
- package/docs/cli.md +36 -14
- package/docs/project-structure-overview.md +6 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +9 -2
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/interactive.sh +8 -2
- package/runtime/bin/lib/okstra/usage.sh +3 -1
- package/runtime/bin/okstra.sh +7 -1
- package/runtime/prompts/duties/discussion-worker.json +32 -0
- package/runtime/prompts/lead/convergence.md +58 -136
- package/runtime/prompts/lead/okstra-lead-contract.md +45 -12
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +14 -1
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +4 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +4 -11
- package/runtime/python/okstra_ctl/agent/invocation.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +2 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +4 -4
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +2 -2
- package/runtime/python/okstra_ctl/analysis_packet.py +3 -0
- package/runtime/python/okstra_ctl/consumers.py +8 -2
- package/runtime/python/okstra_ctl/convergence.py +143 -29
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +57 -0
- package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +106 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +140 -95
- package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +3 -27
- package/runtime/python/okstra_ctl/convergence_store.py +11 -4
- package/runtime/python/okstra_ctl/dispatch_core.py +4 -1
- package/runtime/python/okstra_ctl/domain/role.py +1 -0
- package/runtime/python/okstra_ctl/manager_cli.py +27 -1
- package/runtime/python/okstra_ctl/manager_launch.py +44 -4
- package/runtime/python/okstra_ctl/manager_sync.py +14 -1
- package/runtime/python/okstra_ctl/manager_view.py +53 -1
- package/runtime/python/okstra_ctl/next_phase.py +1 -0
- package/runtime/python/okstra_ctl/phases/catalog.py +2 -1
- package/runtime/python/okstra_ctl/phases/discussion/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/discussion/boundary.json +11 -0
- package/runtime/python/okstra_ctl/phases/discussion/cli.py +512 -0
- package/runtime/python/okstra_ctl/phases/discussion/entry.py +116 -0
- package/runtime/python/okstra_ctl/phases/discussion/followup.py +876 -0
- package/runtime/python/okstra_ctl/phases/discussion/memory.py +294 -0
- package/runtime/python/okstra_ctl/phases/discussion/overview.py +85 -0
- package/runtime/python/okstra_ctl/phases/discussion/profile.json +31 -0
- package/runtime/python/okstra_ctl/phases/discussion/profile.md +48 -0
- package/runtime/python/okstra_ctl/phases/discussion/report.py +73 -0
- package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.html +15 -0
- package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.md +6 -0
- package/runtime/python/okstra_ctl/phases/discussion/spec.md +197 -0
- package/runtime/python/okstra_ctl/phases/discussion/store.py +1143 -0
- package/runtime/python/okstra_ctl/phases/discussion/validation.py +111 -0
- package/runtime/python/okstra_ctl/phases/discussion/wizard.py +90 -0
- package/runtime/python/okstra_ctl/phases/discussion/workflow.py +667 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/host-rules.md +4 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +1 -1
- package/runtime/python/okstra_ctl/plan_run_root.py +8 -0
- package/runtime/python/okstra_ctl/render.py +7 -20
- package/runtime/python/okstra_ctl/render_final_report.py +2 -1
- package/runtime/python/okstra_ctl/report_assembly.py +22 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -0
- package/runtime/python/okstra_ctl/report_narrative.py +13 -4
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +10 -0
- package/runtime/python/okstra_ctl/run.py +40 -7
- package/runtime/python/okstra_ctl/stage_close.py +46 -11
- package/runtime/python/okstra_ctl/stage_reopen.py +151 -0
- package/runtime/python/okstra_ctl/verdict_blocks.py +11 -35
- package/runtime/python/okstra_ctl/wizard/cli.py +8 -0
- package/runtime/python/okstra_ctl/wizard/confirmation.py +4 -1
- package/runtime/python/okstra_ctl/wizard/engine.py +8 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +12 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +35 -3
- package/runtime/python/okstra_ctl/wizard/render.py +4 -1
- package/runtime/python/okstra_ctl/wizard/sources.py +2 -2
- package/runtime/python/okstra_ctl/wizard/state.py +11 -1
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +4 -4
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +24 -6
- package/runtime/python/okstra_token_usage/pricing.py +2 -0
- 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-v3.0.schema.json +90 -1
- package/runtime/skills/_fragments/wizard-bash-invocation-rule.md +1 -0
- package/runtime/skills/_fragments/wizard-confirm-step.md +7 -0
- package/runtime/skills/_fragments/wizard-lead-takeover.md +8 -0
- package/runtime/skills/_fragments/wizard-preflight.md +75 -0
- package/runtime/skills/_fragments/wizard-prompt-loop.md +22 -0
- package/runtime/skills/_fragments/wizard-relay-protocol.md +40 -0
- package/runtime/skills/_fragments/wizard-render-bundle.md +74 -0
- package/runtime/skills/okstra-discussion/SKILL.md +347 -0
- package/runtime/skills/okstra-pr-gen/SKILL.md +7 -0
- package/runtime/skills/okstra-run/SKILL.md +22 -2
- package/runtime/templates/manager/view.template.html +3 -0
- package/runtime/templates/reports/html/i18n/en.json +5 -0
- package/runtime/templates/reports/html/i18n/ko.json +5 -0
- package/runtime/validators/validate-brief.py +3 -2
- package/runtime/validators/validate-run.py +4 -6
|
@@ -24,9 +24,11 @@ from .convergence_engine import (
|
|
|
24
24
|
validate_final_state,
|
|
25
25
|
validate_working_state,
|
|
26
26
|
)
|
|
27
|
-
from .worker_prompt_policy import critic_assignment_ref
|
|
27
|
+
from .worker_prompt_policy import critic_assignment_ref, critic_duty_for_task_type
|
|
28
28
|
from .convergence_critic_verify_prompt import (
|
|
29
29
|
CriticVerifyPromptError,
|
|
30
|
+
acceptance_candidates,
|
|
31
|
+
acceptance_verify_prompt_body,
|
|
30
32
|
critic_result_paths,
|
|
31
33
|
critic_verify_gaps,
|
|
32
34
|
critic_verify_prompt_body,
|
|
@@ -37,7 +39,9 @@ from .convergence_reverify_prompt import (
|
|
|
37
39
|
reverify_prompt_body,
|
|
38
40
|
)
|
|
39
41
|
from .convergence_critic_prompt import (
|
|
42
|
+
acceptance_critic_prompt_body,
|
|
40
43
|
analyser_results,
|
|
44
|
+
analysis_roster,
|
|
41
45
|
covered_index,
|
|
42
46
|
critic_prompt_body,
|
|
43
47
|
)
|
|
@@ -73,6 +77,7 @@ from .execution_identity import ExecutionManifest
|
|
|
73
77
|
from .execution_manifest import read_execution_manifest_view
|
|
74
78
|
from .final_report_schema import load_named_schema, validate as validate_schema
|
|
75
79
|
from .paths import RunRef
|
|
80
|
+
from .plan_items import CRITIC_WORKER_ID
|
|
76
81
|
from .report_finalize import task_manifest_path
|
|
77
82
|
from .verdict_blocks import FindingVote, VerdictBlockError, parse_finding_votes
|
|
78
83
|
from .fixed_text import line
|
|
@@ -181,10 +186,10 @@ _EXAMPLES: dict[str, dict[str, Any]] = {
|
|
|
181
186
|
"taskKey": "example/group/task",
|
|
182
187
|
"config": {
|
|
183
188
|
"enabled": True,
|
|
184
|
-
"adversarial":
|
|
189
|
+
"adversarial": True,
|
|
185
190
|
"maxRounds": 2,
|
|
186
191
|
"effectiveMaxRounds": 2,
|
|
187
|
-
"verificationMode": "
|
|
192
|
+
"verificationMode": "full-reanalysis",
|
|
188
193
|
},
|
|
189
194
|
"workers": [
|
|
190
195
|
{"workerId": "claude-worker", "audience": "analysis"},
|
|
@@ -412,10 +417,6 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
412
417
|
help="read one round's worker responses into the apply-round input",
|
|
413
418
|
)
|
|
414
419
|
collect.add_argument("--plan", type=Path, required=True)
|
|
415
|
-
collect.add_argument("--mode", choices=("adversarial", "collaborative"),
|
|
416
|
-
required=True,
|
|
417
|
-
help="which reverify prompt this round used; the two "
|
|
418
|
-
"carry different verdict vocabularies")
|
|
419
420
|
collect.add_argument("--result", action="append", default=[],
|
|
420
421
|
metavar="<worker>=<path>",
|
|
421
422
|
help="one worker's reverify result file (repeatable)")
|
|
@@ -452,9 +453,8 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
452
453
|
description=(
|
|
453
454
|
"Print the reverify instruction body for one `dispatches[]` row of "
|
|
454
455
|
"a round plan. The lead writes it verbatim to the file the prompt "
|
|
455
|
-
"materializer's `--instruction` takes. It carries the
|
|
456
|
-
"mandate
|
|
457
|
-
"config), every planned finding with its summary, origin worker, "
|
|
456
|
+
"materializer's `--instruction` takes. It carries the adversarial "
|
|
457
|
+
"mandate, every planned finding with its summary, origin worker, "
|
|
458
458
|
"cited-evidence line, the origin worker's result file and item id, "
|
|
459
459
|
"and the origin audit sidecar the verifier may open, plus the "
|
|
460
460
|
"response format the collector parses. Nothing here is hand-written."
|
|
@@ -485,7 +485,9 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
485
485
|
critic_verify_prompt.add_argument("--run-manifest", type=Path, required=True)
|
|
486
486
|
critic_verify_prompt.add_argument(
|
|
487
487
|
"--gaps", type=Path, required=True,
|
|
488
|
-
help="
|
|
488
|
+
help="critic batch JSON: mode coverage with gaps[], or mode "
|
|
489
|
+
"acceptance-devils-advocate with candidates[] (every analyser "
|
|
490
|
+
"verifies every candidate)",
|
|
489
491
|
)
|
|
490
492
|
critic_verify_prompt.add_argument("--worker", required=True)
|
|
491
493
|
|
|
@@ -556,6 +558,28 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
556
558
|
apply_acceptance.add_argument("--work-state", type=Path, required=True)
|
|
557
559
|
apply_acceptance.add_argument("--results", type=Path, required=True)
|
|
558
560
|
|
|
561
|
+
collect_acceptance = subparsers.add_parser(
|
|
562
|
+
"collect-acceptance-votes",
|
|
563
|
+
help="derive confirmed/downgraded from the analysers' acceptance verdicts",
|
|
564
|
+
description=(
|
|
565
|
+
"Read every analyser's `critic-verify` response to the acceptance "
|
|
566
|
+
"candidates and write the `apply-acceptance-critic --results` batch: "
|
|
567
|
+
"a candidate any analyser answers SURVIVES or SURVIVES-WITH-CAVEAT is "
|
|
568
|
+
"confirmed, every other candidate is downgraded to a Residual Risk. "
|
|
569
|
+
"Each analyser must answer every candidate."
|
|
570
|
+
),
|
|
571
|
+
)
|
|
572
|
+
collect_acceptance.add_argument("--run-manifest", type=Path, required=True)
|
|
573
|
+
collect_acceptance.add_argument(
|
|
574
|
+
"--candidates", type=Path, required=True,
|
|
575
|
+
help="the acceptance batch given to critic-verify-prompt (no verdicts yet)",
|
|
576
|
+
)
|
|
577
|
+
collect_acceptance.add_argument(
|
|
578
|
+
"--result", action="append", default=[], metavar="<worker>=<path>",
|
|
579
|
+
help="one analyser's critic-verify result file (repeatable)",
|
|
580
|
+
)
|
|
581
|
+
collect_acceptance.add_argument("--output", type=Path, required=True)
|
|
582
|
+
|
|
559
583
|
finalize = subparsers.add_parser("finalize", help="write the public final state")
|
|
560
584
|
finalize.add_argument("--work-state", type=Path, required=True)
|
|
561
585
|
finalize.add_argument("--output", type=Path, required=True)
|
|
@@ -1371,7 +1395,7 @@ def _planned_finding_ids(plan: dict[str, Any]) -> dict[str, list[str]]:
|
|
|
1371
1395
|
|
|
1372
1396
|
|
|
1373
1397
|
def _worker_votes(
|
|
1374
|
-
raw_results: list[str], planned: dict[str, list[str]]
|
|
1398
|
+
raw_results: list[str], planned: dict[str, list[str]]
|
|
1375
1399
|
) -> dict[str, dict[str, FindingVote]]:
|
|
1376
1400
|
votes: dict[str, dict[str, FindingVote]] = {}
|
|
1377
1401
|
for raw in raw_results:
|
|
@@ -1380,9 +1404,7 @@ def _worker_votes(
|
|
|
1380
1404
|
raise ConvergenceContractError(
|
|
1381
1405
|
f"`{worker}` returned a result but the round plan did not dispatch it"
|
|
1382
1406
|
)
|
|
1383
|
-
parsed = parse_finding_votes(
|
|
1384
|
-
Path(path).read_text(encoding="utf-8"), adversarial=adversarial
|
|
1385
|
-
)
|
|
1407
|
+
parsed = parse_finding_votes(Path(path).read_text(encoding="utf-8"))
|
|
1386
1408
|
assigned = set(planned[worker])
|
|
1387
1409
|
missing = sorted(assigned - set(parsed))
|
|
1388
1410
|
if missing:
|
|
@@ -1411,7 +1433,7 @@ def _collect_results(args: argparse.Namespace) -> tuple[str, Path]:
|
|
|
1411
1433
|
"a legacy v1 roster has no attempt ledger to read"
|
|
1412
1434
|
)
|
|
1413
1435
|
dispatches = _dispatch_rows_from_manifest(plan, manifest, args.run_manifest)
|
|
1414
|
-
votes = _worker_votes(args.result, planned
|
|
1436
|
+
votes = _worker_votes(args.result, planned)
|
|
1415
1437
|
returned_without_completion = sorted(
|
|
1416
1438
|
worker for worker in votes
|
|
1417
1439
|
if next((row["status"] for row in dispatches if row["worker"] == worker), None)
|
|
@@ -1606,15 +1628,26 @@ def _group_worker(manifest: Mapping[str, Any], worker_id: str) -> dict[str, str]
|
|
|
1606
1628
|
"sourceRoleExecutionRef": str(execution["roleExecutionRef"])}
|
|
1607
1629
|
|
|
1608
1630
|
|
|
1631
|
+
def _critic_tie_breaker(manifest: Mapping[str, Any]) -> list[dict[str, str]]:
|
|
1632
|
+
"""로스터에 크리틱이 있으면 동률 finding 을 판정할 `critic` 워커 행 하나."""
|
|
1633
|
+
executions = manifest.get("roleExecutions") if isinstance(manifest.get("roleExecutions"), list) else []
|
|
1634
|
+
critics = [row for row in executions
|
|
1635
|
+
if isinstance(row, Mapping) and row.get("role") == "critic"]
|
|
1636
|
+
if not critics:
|
|
1637
|
+
return []
|
|
1638
|
+
if len(critics) != 1 or not critics[0].get("participantRef") or not critics[0].get("roleExecutionRef"):
|
|
1639
|
+
raise ConvergenceContractError("no single canonical critic role execution")
|
|
1640
|
+
return [{"workerId": CRITIC_WORKER_ID, "audience": "critic",
|
|
1641
|
+
"participantRef": str(critics[0]["participantRef"]),
|
|
1642
|
+
"sourceRoleExecutionRef": str(critics[0]["roleExecutionRef"])}]
|
|
1643
|
+
|
|
1644
|
+
|
|
1609
1645
|
def _convergence_config(authority: RunArtifactAuthority) -> Mapping[str, Any]:
|
|
1610
1646
|
"""convergence 설정의 정본은 task-manifest.json 이다.
|
|
1611
1647
|
|
|
1612
1648
|
`render.render_task_manifest` 가 `_build_convergence_block` 결과를 거기에만
|
|
1613
|
-
쓴다. run manifest 에는 `convergenceStatePath`
|
|
1614
|
-
|
|
1615
|
-
조용히 떨어졌다. 그 강등은 무증상이다 — `convergence_engine._parse_groups` 가
|
|
1616
|
-
적대 모드가 꺼진 다중 출처 그룹을 큐에 넣지 않고 즉시 `full-consensus` 로
|
|
1617
|
-
확정하므로, 세 워커의 처방이 서로 달라도 합의로 기록된다.
|
|
1649
|
+
쓴다. run manifest 에는 `convergenceStatePath` 뿐이라 거기서는 라운드 상한을
|
|
1650
|
+
읽을 수 없다.
|
|
1618
1651
|
|
|
1619
1652
|
설정이 없으면 기본값으로 떨어지지 않고 실패한다. 여기서 조용히 넘어가면
|
|
1620
1653
|
검증 자체가 사라지고 산출물은 정상으로 읽힌다.
|
|
@@ -1641,6 +1674,18 @@ def _critic_prompt(args: argparse.Namespace) -> str:
|
|
|
1641
1674
|
f"`okstra convergence prepare-groups` first: {groups_path}"
|
|
1642
1675
|
)
|
|
1643
1676
|
groups = load_owned_json_object(groups_path)
|
|
1677
|
+
if critic_duty_for_task_type(authority.task_type) == "acceptance-critic":
|
|
1678
|
+
return acceptance_critic_prompt_body(
|
|
1679
|
+
groups,
|
|
1680
|
+
analysis_packet_path=_manifest_authority_string(
|
|
1681
|
+
authority.payload, "analysisPacketPath"
|
|
1682
|
+
),
|
|
1683
|
+
results=analyser_results(
|
|
1684
|
+
groups,
|
|
1685
|
+
project_root=authority.project_root,
|
|
1686
|
+
run_dir=authority.run_dir,
|
|
1687
|
+
),
|
|
1688
|
+
)
|
|
1644
1689
|
return critic_prompt_body(
|
|
1645
1690
|
groups,
|
|
1646
1691
|
analysis_packet_path=_manifest_authority_string(
|
|
@@ -1667,16 +1712,17 @@ def _prepare_groups(args: argparse.Namespace) -> tuple[str, Path]:
|
|
|
1667
1712
|
)
|
|
1668
1713
|
config = _convergence_config(authority)
|
|
1669
1714
|
workers = [_group_worker(manifest, worker) for worker in worker_ids]
|
|
1715
|
+
workers.extend(_critic_tie_breaker(manifest))
|
|
1670
1716
|
source_refs = [worker["sourceRoleExecutionRef"] for worker in workers]
|
|
1671
1717
|
if len(source_refs) != len(set(source_refs)):
|
|
1672
1718
|
raise ConvergenceContractError("duplicate sourceRoleExecutionRef")
|
|
1673
1719
|
payload = {"schemaVersion": "2.0", "executionIdentityVersion": 2,
|
|
1674
1720
|
"taskKey": manifest.get("taskKey"), "runManifestPath": manifest.get("runManifestPath"),
|
|
1675
1721
|
"config": {"enabled": bool(config.get("enabled", True)),
|
|
1676
|
-
"adversarial":
|
|
1722
|
+
"adversarial": True,
|
|
1677
1723
|
"maxRounds": int(config.get("maxRounds", 2)),
|
|
1678
1724
|
"effectiveMaxRounds": int(config.get("effectiveMaxRounds", config.get("maxRounds", 2))),
|
|
1679
|
-
"verificationMode":
|
|
1725
|
+
"verificationMode": "full-reanalysis"},
|
|
1680
1726
|
"workers": workers, "groups": groups}
|
|
1681
1727
|
schema_errors = validate_schema(
|
|
1682
1728
|
payload, load_named_schema("convergence-groups-v2.0.schema.json")
|
|
@@ -1705,7 +1751,6 @@ def _reverify_prompt(args: argparse.Namespace) -> str:
|
|
|
1705
1751
|
)
|
|
1706
1752
|
groups = load_owned_json_object(groups_path)
|
|
1707
1753
|
plan = load_owned_json_object(args.plan)
|
|
1708
|
-
config = groups.get("config") if isinstance(groups.get("config"), Mapping) else {}
|
|
1709
1754
|
round_number = plan.get("round")
|
|
1710
1755
|
if not isinstance(round_number, int) or round_number < 1:
|
|
1711
1756
|
raise ConvergenceContractError("round plan has no positive `round`")
|
|
@@ -1717,7 +1762,6 @@ def _reverify_prompt(args: argparse.Namespace) -> str:
|
|
|
1717
1762
|
return reverify_prompt_body(
|
|
1718
1763
|
task_key=_manifest_authority_string(authority.payload, "taskKey"),
|
|
1719
1764
|
round_number=round_number,
|
|
1720
|
-
adversarial=bool(config.get("adversarial", False)),
|
|
1721
1765
|
findings=reverify_findings(
|
|
1722
1766
|
groups, plan, args.worker,
|
|
1723
1767
|
project_root=authority.project_root, run_dir=authority.run_dir,
|
|
@@ -1740,11 +1784,11 @@ def _critic_verify_prompt(args: argparse.Namespace) -> str:
|
|
|
1740
1784
|
batch = load_owned_json_object(args.gaps)
|
|
1741
1785
|
task_key = _manifest_authority_string(authority.payload, "taskKey")
|
|
1742
1786
|
if batch.get("taskKey") != task_key:
|
|
1743
|
-
raise ConvergenceContractError("
|
|
1744
|
-
|
|
1787
|
+
raise ConvergenceContractError("critic batch taskKey does not match the run manifest")
|
|
1788
|
+
mode = batch.get("mode")
|
|
1789
|
+
if mode not in {"coverage", "acceptance-devils-advocate"}:
|
|
1745
1790
|
raise ConvergenceContractError(
|
|
1746
|
-
"critic
|
|
1747
|
-
"use confirm-or-downgrade"
|
|
1791
|
+
f"critic batch mode must be coverage or acceptance-devils-advocate, got {mode!r}"
|
|
1748
1792
|
)
|
|
1749
1793
|
critic_ref = critic_assignment_ref(authority.task_type)
|
|
1750
1794
|
assignments = authority.payload.get("invocationAssignments")
|
|
@@ -1759,6 +1803,14 @@ def _critic_verify_prompt(args: argparse.Namespace) -> str:
|
|
|
1759
1803
|
project_root=authority.project_root, run_dir=authority.run_dir,
|
|
1760
1804
|
recorded_result=_settled_critic_result(authority.payload),
|
|
1761
1805
|
)
|
|
1806
|
+
if mode == "acceptance-devils-advocate":
|
|
1807
|
+
return acceptance_verify_prompt_body(
|
|
1808
|
+
task_key=task_key,
|
|
1809
|
+
critic_worker=f"{provider}-critic",
|
|
1810
|
+
critic_result_path=result_path,
|
|
1811
|
+
critic_audit_path=audit_path,
|
|
1812
|
+
candidates=acceptance_candidates(batch, groups, args.worker),
|
|
1813
|
+
)
|
|
1762
1814
|
return critic_verify_prompt_body(
|
|
1763
1815
|
task_key=task_key,
|
|
1764
1816
|
critic_worker=f"{provider}-critic",
|
|
@@ -1768,6 +1820,67 @@ def _critic_verify_prompt(args: argparse.Namespace) -> str:
|
|
|
1768
1820
|
)
|
|
1769
1821
|
|
|
1770
1822
|
|
|
1823
|
+
def _collect_acceptance_votes(args: argparse.Namespace) -> tuple[str, Path]:
|
|
1824
|
+
"""분석자 응답에서 후보마다 confirmed / downgraded 를 정해 반영 배치를 쓴다.
|
|
1825
|
+
|
|
1826
|
+
한 분석자라도 SURVIVES(또는 SURVIVES-WITH-CAVEAT)면 확인된 blocker 다
|
|
1827
|
+
(convergence.md §"Verification — confirm-or-downgrade"). 아무도 살리지
|
|
1828
|
+
않으면 Residual Risk 로 내린다 — 후보는 사라지지 않는다.
|
|
1829
|
+
"""
|
|
1830
|
+
authority = validated_run_authority(args.run_manifest)
|
|
1831
|
+
groups_path = authority.run_dir / "state" / _canonical_run_artifact_name(
|
|
1832
|
+
"convergence-groups", authority.task_type, authority.state_sequence
|
|
1833
|
+
)
|
|
1834
|
+
groups = load_owned_json_object(groups_path)
|
|
1835
|
+
batch = load_owned_json_object(args.candidates)
|
|
1836
|
+
if batch.get("taskKey") != _manifest_authority_string(authority.payload, "taskKey"):
|
|
1837
|
+
raise ConvergenceContractError("acceptance batch taskKey does not match the run manifest")
|
|
1838
|
+
if batch.get("mode") != "acceptance-devils-advocate":
|
|
1839
|
+
raise ConvergenceContractError("acceptance batch mode must be acceptance-devils-advocate")
|
|
1840
|
+
if not args.result:
|
|
1841
|
+
raise ConvergenceContractError("pass at least one analyser --result")
|
|
1842
|
+
roster = analysis_roster(groups)
|
|
1843
|
+
votes: dict[str, dict[str, FindingVote]] = {}
|
|
1844
|
+
for raw in args.result:
|
|
1845
|
+
worker, path = _split_pair(raw, "--result")
|
|
1846
|
+
if worker.removesuffix("-worker") in roster and worker not in roster:
|
|
1847
|
+
worker = worker.removesuffix("-worker")
|
|
1848
|
+
if worker not in roster:
|
|
1849
|
+
raise ConvergenceContractError(
|
|
1850
|
+
f"`{worker}` is not an analysis worker of this run; roster: {', '.join(roster)}"
|
|
1851
|
+
)
|
|
1852
|
+
votes[worker] = parse_finding_votes(Path(path).read_text(encoding="utf-8"))
|
|
1853
|
+
candidates: list[dict[str, Any]] = []
|
|
1854
|
+
for candidate in acceptance_candidates(batch, groups, next(iter(votes))):
|
|
1855
|
+
row: dict[str, Any] = {}
|
|
1856
|
+
for worker, parsed in votes.items():
|
|
1857
|
+
vote = parsed.get(candidate.candidate_id)
|
|
1858
|
+
if vote is None:
|
|
1859
|
+
raise ConvergenceContractError(
|
|
1860
|
+
f"`{worker}` returned no verdict for {candidate.candidate_id}"
|
|
1861
|
+
)
|
|
1862
|
+
row[worker] = {"verdict": vote.verdict, "explanation": vote.explanation}
|
|
1863
|
+
confirmed = any(v["verdict"] in {"agree", "supplement"} for v in row.values())
|
|
1864
|
+
candidates.append({
|
|
1865
|
+
"candidateId": candidate.candidate_id,
|
|
1866
|
+
"statement": candidate.statement,
|
|
1867
|
+
**({"severity": candidate.severity} if candidate.severity else {}),
|
|
1868
|
+
"originEvidence": candidate.origin_evidence,
|
|
1869
|
+
"verdict": "confirmed" if confirmed else "downgraded",
|
|
1870
|
+
"votes": row,
|
|
1871
|
+
})
|
|
1872
|
+
write_json_atomic(args.output, {
|
|
1873
|
+
"schemaVersion": "1.0",
|
|
1874
|
+
"taskKey": batch["taskKey"],
|
|
1875
|
+
"mode": "acceptance-devils-advocate",
|
|
1876
|
+
"provider": batch.get("provider"),
|
|
1877
|
+
"modelExecutionValue": batch.get("modelExecutionValue"),
|
|
1878
|
+
"candidates": candidates,
|
|
1879
|
+
})
|
|
1880
|
+
load_acceptance_critic_batch(args.output)
|
|
1881
|
+
return "collected", args.output
|
|
1882
|
+
|
|
1883
|
+
|
|
1771
1884
|
def _worker_paths(raw_pairs: list[str], label: str) -> dict[str, Path]:
|
|
1772
1885
|
paths: dict[str, Path] = {}
|
|
1773
1886
|
for raw in raw_pairs:
|
|
@@ -1880,6 +1993,7 @@ def _execute(args: argparse.Namespace) -> tuple[Any, ...]:
|
|
|
1880
1993
|
"apply-round": _apply_round,
|
|
1881
1994
|
"apply-critic-gaps": _apply_critic_gaps,
|
|
1882
1995
|
"apply-acceptance-critic": _apply_acceptance_critic,
|
|
1996
|
+
"collect-acceptance-votes": _collect_acceptance_votes,
|
|
1883
1997
|
"finalize": _finalize,
|
|
1884
1998
|
"skip": _skip,
|
|
1885
1999
|
"validate": _validate,
|
|
@@ -288,6 +288,63 @@ def critic_prompt_body(
|
|
|
288
288
|
return "".join(rows)
|
|
289
289
|
|
|
290
290
|
|
|
291
|
+
_ACCEPTANCE_MANDATE = """
|
|
292
|
+
The delivered work is about to be judged for acceptance. Your ONLY job is to find
|
|
293
|
+
reasons it should NOT be accepted — surface candidate acceptance BLOCKERS the
|
|
294
|
+
verifiers may have missed:
|
|
295
|
+
- requirements / acceptance points with no covering evidence,
|
|
296
|
+
- DB / IO / SQL changes lacking real-execution evidence,
|
|
297
|
+
- regressions or broken error paths,
|
|
298
|
+
- scope / contract violations.
|
|
299
|
+
A finding that would not stop the release is not a blocker — say so, and it is
|
|
300
|
+
recorded as a conditional-acceptance condition instead. Do NOT restate an existing
|
|
301
|
+
Acceptance Blocker. If you find none, write `No candidate blockers.`
|
|
302
|
+
|
|
303
|
+
Write each candidate as its own section, numbered from AC-001:
|
|
304
|
+
|
|
305
|
+
### [AC-001] <one-line statement>
|
|
306
|
+
**Severity**: critical | major
|
|
307
|
+
**Evidence**: <file:line / log / test output you executed or read>
|
|
308
|
+
""".strip()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def acceptance_critic_prompt_body(
|
|
312
|
+
groups: Mapping[str, Any],
|
|
313
|
+
*,
|
|
314
|
+
analysis_packet_path: str,
|
|
315
|
+
results: Sequence[AnalyserResult],
|
|
316
|
+
) -> str:
|
|
317
|
+
"""final-verification acceptance critic 지시문 본문. 같은 입력이면 같은 바이트.
|
|
318
|
+
|
|
319
|
+
final-verification 프롬프트 본문은 96줄 상한을 받으므로 Round 0 finding 목록은
|
|
320
|
+
싣지 않는다 — 분석자 결과 파일 경로가 그 자리를 대신한다.
|
|
321
|
+
"""
|
|
322
|
+
task_key = _nonempty_string(groups.get("taskKey"))
|
|
323
|
+
if not task_key:
|
|
324
|
+
raise CriticPromptError("convergence groups carry no taskKey")
|
|
325
|
+
packet = _nonempty_string(analysis_packet_path)
|
|
326
|
+
if not packet.endswith("analysis-packet.md"):
|
|
327
|
+
raise CriticPromptError(
|
|
328
|
+
"run manifest analysisPacketPath must name analysis-packet.md; "
|
|
329
|
+
f"found `{analysis_packet_path}`"
|
|
330
|
+
)
|
|
331
|
+
rows: list[str] = [
|
|
332
|
+
f"**Prompt Delivery Mode:** {PROMPT_DELIVERY_MODE}\n",
|
|
333
|
+
"\n",
|
|
334
|
+
f"# Acceptance critic — {task_key}\n",
|
|
335
|
+
"\n",
|
|
336
|
+
"## Inputs\n",
|
|
337
|
+
f"- Primary analysis packet: `{packet}`\n",
|
|
338
|
+
"\n",
|
|
339
|
+
"## Mandate\n",
|
|
340
|
+
"\n",
|
|
341
|
+
_ACCEPTANCE_MANDATE,
|
|
342
|
+
"\n\n",
|
|
343
|
+
]
|
|
344
|
+
rows.extend(_analyser_rows(results))
|
|
345
|
+
return "".join(rows)
|
|
346
|
+
|
|
347
|
+
|
|
291
348
|
def _round0_rows(groups: Mapping[str, Any]) -> list[str]:
|
|
292
349
|
findings = [
|
|
293
350
|
group for group in groups.get("groups") or [] if isinstance(group, Mapping)
|
|
@@ -237,3 +237,109 @@ def critic_verify_prompt_body(
|
|
|
237
237
|
if len(gaps) > 1:
|
|
238
238
|
rows.append(f"\n### {gaps[1].gap_id}\n**Verdict**: ...\n")
|
|
239
239
|
return "".join(rows)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@dataclass(frozen=True)
|
|
243
|
+
class AcceptanceCandidate:
|
|
244
|
+
"""acceptance critic 이 낸 후보 blocker 하나."""
|
|
245
|
+
|
|
246
|
+
candidate_id: str
|
|
247
|
+
statement: str
|
|
248
|
+
severity: str
|
|
249
|
+
origin_evidence: str
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def acceptance_candidates(
|
|
253
|
+
batch: Mapping[str, Any],
|
|
254
|
+
groups: Mapping[str, Any],
|
|
255
|
+
worker_id: str,
|
|
256
|
+
) -> list[AcceptanceCandidate]:
|
|
257
|
+
"""분석자 한 명이 검증할 후보 — 계약상 모든 분석자가 모든 후보를 본다."""
|
|
258
|
+
roster = analysis_roster(groups)
|
|
259
|
+
if worker_id not in roster and worker_id.removesuffix("-worker") in roster:
|
|
260
|
+
worker_id = worker_id.removesuffix("-worker")
|
|
261
|
+
if worker_id not in roster:
|
|
262
|
+
raise CriticVerifyPromptError(
|
|
263
|
+
f"`{worker_id}` is not an analysis worker of this run; roster: "
|
|
264
|
+
f"{', '.join(roster) or 'none'}"
|
|
265
|
+
)
|
|
266
|
+
raw = batch.get("candidates")
|
|
267
|
+
if not isinstance(raw, list) or not raw:
|
|
268
|
+
raise CriticVerifyPromptError("acceptance batch has no candidates array")
|
|
269
|
+
candidates: list[AcceptanceCandidate] = []
|
|
270
|
+
seen: set[str] = set()
|
|
271
|
+
for row in raw:
|
|
272
|
+
if not isinstance(row, Mapping):
|
|
273
|
+
raise CriticVerifyPromptError("acceptance candidate must be an object")
|
|
274
|
+
candidate_id = _nonempty_string(row.get("candidateId"))
|
|
275
|
+
if not candidate_id:
|
|
276
|
+
raise CriticVerifyPromptError("acceptance candidate has no candidateId")
|
|
277
|
+
if candidate_id in seen:
|
|
278
|
+
raise CriticVerifyPromptError(f"duplicate candidateId: {candidate_id}")
|
|
279
|
+
seen.add(candidate_id)
|
|
280
|
+
candidates.append(AcceptanceCandidate(
|
|
281
|
+
candidate_id=candidate_id,
|
|
282
|
+
statement=_nonempty_string(row.get("statement")),
|
|
283
|
+
severity=_nonempty_string(row.get("severity")),
|
|
284
|
+
origin_evidence=_nonempty_string(row.get("originEvidence")),
|
|
285
|
+
))
|
|
286
|
+
return candidates
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
_ACCEPTANCE_EVIDENCE_ACCESS = """Each candidate claims the delivered work should NOT be accepted. The complete
|
|
290
|
+
claim is the critic's own item: before judging, open the `**Origin item**` file at
|
|
291
|
+
the section whose heading names the candidate id, and the `**Origin audit sidecar**`, and
|
|
292
|
+
re-run or re-read what they cite. SURVIVES means you reproduced the problem or
|
|
293
|
+
found evidence that supports it; the candidate then becomes an Acceptance Blocker.
|
|
294
|
+
A candidate no analyser lets survive is downgraded to a Residual Risk, never
|
|
295
|
+
dropped."""
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def acceptance_verify_prompt_body(
|
|
299
|
+
*,
|
|
300
|
+
task_key: str,
|
|
301
|
+
critic_worker: str,
|
|
302
|
+
critic_result_path: str,
|
|
303
|
+
critic_audit_path: str,
|
|
304
|
+
candidates: Sequence[AcceptanceCandidate],
|
|
305
|
+
) -> str:
|
|
306
|
+
"""acceptance 후보 확인·강등 지시문 본문. 같은 입력이면 같은 바이트를 낸다."""
|
|
307
|
+
if not _nonempty_string(task_key):
|
|
308
|
+
raise CriticVerifyPromptError("run manifest carries no taskKey")
|
|
309
|
+
if not candidates:
|
|
310
|
+
raise CriticVerifyPromptError("no acceptance candidates to verify")
|
|
311
|
+
rows = [
|
|
312
|
+
"## Instructions\n\n",
|
|
313
|
+
f"{RENDERED_BY_LINE}\n\n",
|
|
314
|
+
f"Perform ADVERSARIAL acceptance-candidate verification for {task_key} "
|
|
315
|
+
f"(dispatch kind `{CRITIC_VERIFY_DISPATCH_KIND}`, one round).\n\n",
|
|
316
|
+
ADVERSARIAL_MANDATE, "\n\n",
|
|
317
|
+
_ACCEPTANCE_EVIDENCE_ACCESS, "\n\n",
|
|
318
|
+
"## Findings to verify\n",
|
|
319
|
+
]
|
|
320
|
+
for candidate in candidates:
|
|
321
|
+
rows.append(
|
|
322
|
+
f"\n### {candidate.candidate_id}: {candidate.statement or '(no statement)'}\n"
|
|
323
|
+
)
|
|
324
|
+
rows.append(f"**Origin**: {critic_worker}\n")
|
|
325
|
+
rows.append(f"**Severity**: {candidate.severity or '(none recorded)'}\n")
|
|
326
|
+
rows.append(
|
|
327
|
+
f"**Cited evidence**: {candidate.origin_evidence or '(none recorded)'}\n"
|
|
328
|
+
)
|
|
329
|
+
# 헤딩 모양은 critic 이 정한다(`### [AC-001]`, `### AC-001 — …`); id 만 가리킨다.
|
|
330
|
+
rows.append(
|
|
331
|
+
f"**Origin item**: `{critic_result_path}` — the section headed "
|
|
332
|
+
f"`{candidate.candidate_id}`\n"
|
|
333
|
+
)
|
|
334
|
+
rows.append(f"**Origin audit sidecar**: `{critic_audit_path}`\n")
|
|
335
|
+
rows.append("\n## Response format\n\n")
|
|
336
|
+
rows.append(
|
|
337
|
+
"One block per candidate, headed by the candidate id at exactly three hashes. "
|
|
338
|
+
"Field labels are bold with the colon outside (`**Verdict**: …`); the "
|
|
339
|
+
"collector also reads `**Verdict:** …` and `- Verdict: …` as the same field.\n\n"
|
|
340
|
+
)
|
|
341
|
+
rows.append(ADVERSARIAL_RESPONSE.replace("<finding-id>", candidates[0].candidate_id))
|
|
342
|
+
rows.append("\n")
|
|
343
|
+
if len(candidates) > 1:
|
|
344
|
+
rows.append(f"\n### {candidates[1].candidate_id}\n**Verdict**: ...\n")
|
|
345
|
+
return "".join(rows)
|