okstra 0.127.0 → 0.129.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.
Files changed (53) hide show
  1. package/docs/architecture/storage-model.md +23 -3
  2. package/docs/architecture.md +28 -1
  3. package/docs/cli.md +9 -0
  4. package/docs/project-structure-overview.md +17 -7
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/agents/workers/antigravity-worker.md +3 -3
  8. package/runtime/agents/workers/claude-worker.md +4 -4
  9. package/runtime/agents/workers/codex-worker.md +3 -3
  10. package/runtime/agents/workers/report-writer-worker.md +5 -5
  11. package/runtime/prompts/lead/adapters/claude-code.md +2 -1
  12. package/runtime/prompts/lead/adapters/codex.md +1 -0
  13. package/runtime/prompts/lead/adapters/external.md +1 -0
  14. package/runtime/prompts/lead/convergence.md +45 -83
  15. package/runtime/prompts/lead/okstra-lead-contract.md +12 -8
  16. package/runtime/prompts/lead/report-writer.md +3 -2
  17. package/runtime/prompts/lead/team-contract.md +19 -9
  18. package/runtime/prompts/profiles/_common-contract.md +1 -1
  19. package/runtime/prompts/profiles/error-analysis.md +1 -1
  20. package/runtime/prompts/profiles/final-verification.md +10 -6
  21. package/runtime/prompts/profiles/implementation-planning.md +5 -0
  22. package/runtime/prompts/profiles/improvement-discovery.md +11 -1
  23. package/runtime/prompts/profiles/requirements-discovery.md +8 -1
  24. package/runtime/python/okstra_ctl/analysis_packet.py +14 -15
  25. package/runtime/python/okstra_ctl/codex_dispatch.py +51 -80
  26. package/runtime/python/okstra_ctl/context_cost.py +82 -7
  27. package/runtime/python/okstra_ctl/convergence.py +223 -0
  28. package/runtime/python/okstra_ctl/convergence_engine.py +1152 -0
  29. package/runtime/python/okstra_ctl/convergence_migration.py +184 -0
  30. package/runtime/python/okstra_ctl/convergence_store.py +85 -0
  31. package/runtime/python/okstra_ctl/dispatch_core.py +60 -1
  32. package/runtime/python/okstra_ctl/improvement_assignment.py +61 -0
  33. package/runtime/python/okstra_ctl/log_report.py +44 -5
  34. package/runtime/python/okstra_ctl/path_hints.py +28 -1
  35. package/runtime/python/okstra_ctl/paths.py +10 -1
  36. package/runtime/python/okstra_ctl/render.py +78 -11
  37. package/runtime/python/okstra_ctl/run.py +66 -2
  38. package/runtime/python/okstra_ctl/wizard.py +15 -4
  39. package/runtime/python/okstra_ctl/work_categories.py +37 -0
  40. package/runtime/python/okstra_ctl/worker_prompt_body.py +107 -0
  41. package/runtime/python/okstra_ctl/worker_prompt_contract.py +316 -0
  42. package/runtime/python/okstra_ctl/worker_prompt_headers.py +109 -10
  43. package/runtime/python/okstra_ctl/worker_prompt_policy.py +158 -0
  44. package/runtime/templates/implementation-worker-preamble.md +51 -0
  45. package/runtime/templates/operating-standard.md +4 -4
  46. package/runtime/templates/report-writer-prompt-preamble.md +37 -0
  47. package/runtime/templates/worker-error-contract.md +46 -0
  48. package/runtime/templates/worker-prompt-preamble.md +28 -227
  49. package/runtime/validators/lib/fixtures.sh +27 -12
  50. package/runtime/validators/validate-run.py +228 -45
  51. package/src/cli-registry.mjs +7 -0
  52. package/src/commands/execute/convergence.mjs +34 -0
  53. package/src/commands/inspect/log-report.mjs +5 -3
@@ -354,6 +354,8 @@ def _active_instruction_set(ctx: dict) -> dict:
354
354
  "clarificationResponsePath": clarification,
355
355
  "finalReportTemplatePath": ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
356
356
  "finalReportSchemaPath": ctx.get("FINAL_REPORT_SCHEMA_RELATIVE_PATH", ""),
357
+ "verificationTargetPath": ctx.get("VERIFICATION_TARGET_RELATIVE_PATH", ""),
358
+ "verificationTargetDigest": ctx.get("VERIFICATION_TARGET_DIGEST", ""),
357
359
  }
358
360
 
359
361
 
@@ -374,6 +376,22 @@ def _active_runtime_resources(ctx: dict) -> dict:
374
376
  return {
375
377
  "codingPreflightDir": ctx.get("OKSTRA_CODING_PREFLIGHT_DIR")
376
378
  or str(runtime_home / "prompts" / "coding-preflight"),
379
+ "workerPreamblePathByAudience": {
380
+ "analysis": ctx.get("ANALYSIS_WORKER_PREAMBLE_PATH")
381
+ or str(runtime_home / "templates" / "worker-prompt-preamble.md"),
382
+ "implementation-executor": ctx.get(
383
+ "IMPLEMENTATION_WORKER_PREAMBLE_PATH"
384
+ )
385
+ or str(runtime_home / "templates" / "implementation-worker-preamble.md"),
386
+ "implementation-verifier": ctx.get(
387
+ "IMPLEMENTATION_WORKER_PREAMBLE_PATH"
388
+ )
389
+ or str(runtime_home / "templates" / "implementation-worker-preamble.md"),
390
+ "report-writer": ctx.get("REPORT_WRITER_PREAMBLE_PATH")
391
+ or str(runtime_home / "templates" / "report-writer-prompt-preamble.md"),
392
+ },
393
+ "workerErrorContractPath": ctx.get("WORKER_ERROR_CONTRACT_PATH")
394
+ or str(runtime_home / "templates" / "worker-error-contract.md"),
377
395
  }
378
396
 
379
397
 
@@ -387,6 +405,19 @@ def _active_executor_worktree(ctx: dict) -> dict:
387
405
  }
388
406
 
389
407
 
408
+ def _active_verification_target(ctx: dict) -> dict:
409
+ if ctx.get("TASK_TYPE") != "final-verification":
410
+ return {}
411
+ return {
412
+ "scope": ctx.get("VERIFICATION_SCOPE", ""),
413
+ "worktreePath": ctx.get("VERIFICATION_WORKTREE_PATH", ""),
414
+ "baseRef": ctx.get("VERIFICATION_BASE_REF", ""),
415
+ "headRef": ctx.get("VERIFICATION_HEAD_REF", ""),
416
+ "path": ctx.get("VERIFICATION_TARGET_RELATIVE_PATH", ""),
417
+ "digest": ctx.get("VERIFICATION_TARGET_DIGEST", ""),
418
+ }
419
+
420
+
390
421
  def _active_source_artifacts(ctx: dict) -> dict:
391
422
  return {
392
423
  "taskManifestPath": ctx.get("TASK_MANIFEST_RELATIVE_PATH", ""),
@@ -429,6 +460,7 @@ def render_active_run_context(active_context_path: str, ctx: dict) -> None:
429
460
  "errorLogs": _active_error_logs(ctx),
430
461
  "runtimeResources": _active_runtime_resources(ctx),
431
462
  "executorWorktree": _active_executor_worktree(ctx),
463
+ "verificationTarget": _active_verification_target(ctx),
432
464
  "sourceArtifacts": _active_source_artifacts(ctx),
433
465
  "lazyReadPlan": _active_lazy_read_plan(),
434
466
  }
@@ -1053,6 +1085,10 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1053
1085
  "analysisMaterialPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
1054
1086
  + "/analysis-material.md",
1055
1087
  "analysisPacketPath": ctx.get("ANALYSIS_PACKET_RELATIVE_PATH", ""),
1088
+ "verificationTargetPath": ctx.get(
1089
+ "VERIFICATION_TARGET_RELATIVE_PATH", ""
1090
+ ),
1091
+ "verificationTargetDigest": ctx.get("VERIFICATION_TARGET_DIGEST", ""),
1056
1092
  "taskBriefCopyPath": ctx.get("INSTRUCTION_SET_RELATIVE_PATH", "")
1057
1093
  + "/task-brief.md",
1058
1094
  "referenceExpectationsPath": ctx.get(
@@ -1278,6 +1314,8 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1278
1314
  "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
1279
1315
  "activeRunContextPath": ctx.get("ACTIVE_RUN_CONTEXT_RELATIVE_PATH", ""),
1280
1316
  "analysisPacketPath": ctx.get("ANALYSIS_PACKET_RELATIVE_PATH", ""),
1317
+ "verificationTargetPath": ctx.get("VERIFICATION_TARGET_RELATIVE_PATH", ""),
1318
+ "verificationTargetDigest": ctx.get("VERIFICATION_TARGET_DIGEST", ""),
1281
1319
  "workerResultsDirectoryPath": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
1282
1320
  "reportTemplatePath": ctx.get("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", ""),
1283
1321
  "validatorScriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
@@ -1761,6 +1799,19 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1761
1799
  coding_preflight_dir = ctx.get("OKSTRA_CODING_PREFLIGHT_DIR") or str(
1762
1800
  home_prompts / "coding-preflight"
1763
1801
  )
1802
+ runtime_templates = okstra_home() / "templates"
1803
+ analysis_preamble = ctx.get("ANALYSIS_WORKER_PREAMBLE_PATH") or str(
1804
+ runtime_templates / "worker-prompt-preamble.md"
1805
+ )
1806
+ implementation_preamble = ctx.get("IMPLEMENTATION_WORKER_PREAMBLE_PATH") or str(
1807
+ runtime_templates / "implementation-worker-preamble.md"
1808
+ )
1809
+ report_writer_preamble = ctx.get("REPORT_WRITER_PREAMBLE_PATH") or str(
1810
+ runtime_templates / "report-writer-prompt-preamble.md"
1811
+ )
1812
+ worker_error_contract = ctx.get("WORKER_ERROR_CONTRACT_PATH") or str(
1813
+ runtime_templates / "worker-error-contract.md"
1814
+ )
1764
1815
  okstra_runtime_resources_block = (
1765
1816
  "## Okstra Runtime Resources\n"
1766
1817
  "\n"
@@ -1773,7 +1824,11 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1773
1824
  f"- Convergence contract: `{convergence_path}`\n"
1774
1825
  f"- Plan-body verification contract (implementation-planning Phase 6 sub-step only): `{plan_body_verification_path}`\n"
1775
1826
  f"- Report writer contract: `{report_writer_path}`\n"
1776
- f"- Coding preflight pack: `{coding_preflight_dir}`"
1827
+ f"- Coding preflight pack: `{coding_preflight_dir}`\n"
1828
+ f"- Analysis worker preamble: `{analysis_preamble}`\n"
1829
+ f"- Implementation worker preamble: `{implementation_preamble}`\n"
1830
+ f"- Report writer preamble: `{report_writer_preamble}`\n"
1831
+ f"- Shared worker error contract: `{worker_error_contract}`"
1777
1832
  )
1778
1833
 
1779
1834
  def fmt_assignment(role: str, model: str, execution: str) -> str:
@@ -1893,10 +1948,6 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1893
1948
  "the selected adapter's mapping. Follow the lifecycle core's lazy-read rules "
1894
1949
  "for support resources; do not invoke hidden/internal skills."
1895
1950
  )
1896
- worker_preamble_path = ctx.get(
1897
- "WORKER_PROMPT_PREAMBLE_PATH",
1898
- str(okstra_home() / "templates" / "worker-prompt-preamble.md"),
1899
- )
1900
1951
  worker_dispatch_guidance = (
1901
1952
  "## Worker Dispatch Contract\n"
1902
1953
  "\n"
@@ -1910,10 +1961,14 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1910
1961
  "- Use `await_workers` through the selected adapter and confirm terminal state "
1911
1962
  "plus every required Result Path. File presence alone is never a signal to "
1912
1963
  "skip; worker selection and skipped status come from `team-state`.\n"
1913
- "- Every worker prompt must include the anchor header "
1914
- f"`**Worker Preamble Path:** {worker_preamble_path}`. That file is the canonical "
1915
- "source for Required Reading, Error Reporting, and Output sections; do not "
1916
- "re-inline those blocks into worker prompt bodies."
1964
+ "- Resolve the worker's PromptPlan audience and inject exactly one "
1965
+ "`**Worker Preamble Path:**` from the audience map: "
1966
+ f"analysis=`{analysis_preamble}`, implementation executor/verifier="
1967
+ f"`{implementation_preamble}`, report-writer=`{report_writer_preamble}`.\n"
1968
+ "- Every initial worker prompt also injects "
1969
+ f"`**Worker Error Contract Path:** {worker_error_contract}`. The selected "
1970
+ "preamble owns audience procedure; the shared error contract owns error "
1971
+ "schema and write rules. Do not re-inline either contract."
1917
1972
  )
1918
1973
 
1919
1974
  if lead_runtime == "external":
@@ -1984,12 +2039,24 @@ def apply_lead_prompt_defaults(ctx: dict) -> None:
1984
2039
  ctx.setdefault("STAGE_INTEGRATION", "")
1985
2040
  runtime_home = okstra_home()
1986
2041
  ctx.setdefault(
1987
- "WORKER_PROMPT_PREAMBLE_PATH",
2042
+ "ANALYSIS_WORKER_PREAMBLE_PATH",
1988
2043
  str(runtime_home / "templates" / "worker-prompt-preamble.md"),
1989
2044
  )
2045
+ ctx.setdefault(
2046
+ "IMPLEMENTATION_WORKER_PREAMBLE_PATH",
2047
+ str(runtime_home / "templates" / "implementation-worker-preamble.md"),
2048
+ )
2049
+ ctx.setdefault(
2050
+ "REPORT_WRITER_PREAMBLE_PATH",
2051
+ str(runtime_home / "templates" / "report-writer-prompt-preamble.md"),
2052
+ )
2053
+ ctx.setdefault(
2054
+ "WORKER_ERROR_CONTRACT_PATH",
2055
+ str(runtime_home / "templates" / "worker-error-contract.md"),
2056
+ )
1990
2057
  # Lead resource paths the launch template references directly. paths.py
1991
2058
  # seeds the production values; setdefault backfills callers that render the
1992
- # template without the full path ctx (mirrors WORKER_PROMPT_PREAMBLE_PATH).
2059
+ # template without the full path ctx.
1993
2060
  ctx.setdefault(
1994
2061
  "OKSTRA_LEAD_CONTRACT_PATH",
1995
2062
  str(runtime_home / "prompts" / "lead" / "okstra-lead-contract.md"),
@@ -16,6 +16,7 @@ state passing, and are read once at the start.
16
16
  from __future__ import annotations
17
17
 
18
18
  import importlib.util
19
+ import hashlib
19
20
  import json
20
21
  import os
21
22
  import re
@@ -100,6 +101,7 @@ from . import implementation_stage as _implementation_stage
100
101
  from .stage_reconcile import (
101
102
  auto_reconcile_best_effort as _stage_auto_reconcile_best_effort,
102
103
  )
104
+ from .work_categories import resolve_work_category
103
105
  from .worktree import (
104
106
  WorktreeProvision,
105
107
  provision_task_worktree,
@@ -1578,20 +1580,70 @@ def _reserve_final_verification_target(
1578
1580
 
1579
1581
  diff_stat = _git_out(target.worktree_path, "diff", "--stat",
1580
1582
  f"{target.base}..{target.head}")
1583
+ ctx["VERIFICATION_SCOPE"] = target.scope
1584
+ ctx["VERIFICATION_WORKTREE_PATH"] = target.worktree_path
1585
+ ctx["VERIFICATION_BASE_REF"] = target.base
1586
+ ctx["VERIFICATION_HEAD_REF"] = target.head
1587
+ ctx["VERIFICATION_TARGET"] = _format_verification_target(target, diff_stat)
1588
+
1589
+
1590
+ def _format_verification_target(
1591
+ target: _stage_targets.FinalVerificationTarget,
1592
+ diff_stat: str,
1593
+ ) -> str:
1581
1594
  reports = "\n".join(
1582
1595
  f" - stage {s}: `{rp or '(report_path unrecorded)'}`"
1583
1596
  for s, rp in zip(target.stages, target.reports)
1584
1597
  )
1585
- ctx["VERIFICATION_TARGET"] = (
1598
+ return (
1586
1599
  f"- **Verification scope:** `{target.scope}`\n"
1587
1600
  f"- **Worktree:** `{target.worktree_path}`\n"
1588
1601
  f"- **Verification base ref:** `{target.base}`\n"
1602
+ f"- **Verification head ref:** `{target.head}`\n"
1589
1603
  f"- **Stages under verification:** {target.stages}\n"
1590
1604
  f"- **Source implementation reports:**\n{reports}\n"
1591
1605
  f"- **Verification diff stat:**\n```\n{diff_stat}\n```"
1592
1606
  )
1593
1607
 
1594
1608
 
1609
+ def write_verification_target_snapshot(
1610
+ instruction_set: Path,
1611
+ target_markdown: str,
1612
+ ) -> tuple[Path, str]:
1613
+ """Persist one normalized final-verification target and return its digest."""
1614
+ normalized = target_markdown.replace("\r\n", "\n").replace("\r", "\n")
1615
+ normalized = normalized.rstrip() + "\n"
1616
+ digest = "sha256:" + hashlib.sha256(normalized.encode("utf-8")).hexdigest()
1617
+ path = instruction_set / "verification-target.md"
1618
+ path.write_text(
1619
+ normalized + f"- **Verification target digest:** `{digest}`\n",
1620
+ encoding="utf-8",
1621
+ )
1622
+ return path, digest
1623
+
1624
+
1625
+ def _write_verification_target_artifact(
1626
+ inp: PrepareInputs,
1627
+ ctx: dict,
1628
+ instruction_set: Path,
1629
+ ) -> None:
1630
+ if inp.task_type != "final-verification":
1631
+ return
1632
+ target_markdown = str(ctx.get("VERIFICATION_TARGET") or "")
1633
+ if not target_markdown:
1634
+ raise PrepareError("final-verification target snapshot is missing")
1635
+ target_path, target_digest = write_verification_target_snapshot(
1636
+ instruction_set,
1637
+ target_markdown,
1638
+ )
1639
+ ctx["VERIFICATION_TARGET_PATH"] = str(target_path)
1640
+ ctx["VERIFICATION_TARGET_RELATIVE_PATH"] = (
1641
+ f"{ctx['INSTRUCTION_SET_RELATIVE_PATH']}/verification-target.md"
1642
+ )
1643
+ ctx["VERIFICATION_TARGET_DIGEST"] = target_digest
1644
+ ctx["VERIFICATION_TARGET"] = target_path.read_text(encoding="utf-8")
1645
+
1646
+
1595
1647
  def _write_instruction_set_sources(
1596
1648
  inp: PrepareInputs, ctx: dict, profile_content: str, review_material: str
1597
1649
  ) -> Path:
@@ -1599,6 +1651,7 @@ def _write_instruction_set_sources(
1599
1651
  reference-expectations 를 기록하고 디렉터리 경로를 돌려준다."""
1600
1652
  instruction_set = Path(ctx["INSTRUCTION_SET_PATH"])
1601
1653
  instruction_set.mkdir(parents=True, exist_ok=True)
1654
+ _write_verification_target_artifact(inp, ctx, instruction_set)
1602
1655
  profile_rendered = profile_content
1603
1656
  if inp.task_type == "implementation":
1604
1657
  profile_rendered += "\n\n{{DESIGN_PREP_CONTEXT}}\n\n{{FIX_RUN_CONTEXT}}"
@@ -1950,6 +2003,16 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
1950
2003
  task_id_segment = slugify(inp.task_id)
1951
2004
  task_key = f"{inp.project_id}:{inp.task_group}:{inp.task_id}"
1952
2005
 
2006
+ # Worktree branch namespace, workflow state and every rendered manifest read
2007
+ # the same resolved value, so a phase invoked without --work-category still
2008
+ # lands on the branch namespace the task was classified with.
2009
+ inp.work_category = resolve_work_category(
2010
+ inp.work_category,
2011
+ project_root=project_root,
2012
+ task_group=inp.task_group,
2013
+ task_id=inp.task_id,
2014
+ )
2015
+
1953
2016
  # ---- task worktree provisioning (every phase, every task-type) ----
1954
2017
  # One worktree per task-key: requirements-discovery, error-analysis,
1955
2018
  # implementation-planning and implementation phases of the same task
@@ -2371,7 +2434,8 @@ def main(argv: list[str]) -> int:
2371
2434
  help=(
2372
2435
  "Work-category classification for this task "
2373
2436
  "(bugfix / feature / refactor / ops / improvement). "
2374
- "Falls back to `unknown` when omitted."
2437
+ "When omitted, falls back to the category already recorded in "
2438
+ "task-manifest.json, then to `feature`."
2375
2439
  ),
2376
2440
  )
2377
2441
  p.add_argument(
@@ -85,6 +85,7 @@ from okstra_ctl.worktree import (
85
85
  preview_worktree_decision,
86
86
  )
87
87
  from okstra_ctl.paths import task_dir, task_runs_dir
88
+ from okstra_ctl.work_categories import resolve_work_category
88
89
  from okstra_ctl.run_context import latest_run_inputs
89
90
  from okstra_project.dirs import project_json_path
90
91
  from okstra_project.state import (
@@ -3126,6 +3127,17 @@ def _submit_fix_cycle_confirm(state: WizardState, value: str) -> Optional[str]:
3126
3127
  return f"fix-cycle: {v}"
3127
3128
 
3128
3129
 
3130
+ def _preview_work_category(state: WizardState) -> str:
3131
+ """okstra-run 경로는 --work-category 를 넘기지 않는다. prepare 가 쓰는 것과
3132
+ 같은 resolver 를 태워야 미리보기 브랜치명이 실제 생성 브랜치와 일치한다."""
3133
+ return resolve_work_category(
3134
+ "",
3135
+ project_root=Path(state.project_root),
3136
+ task_group=state.task_group,
3137
+ task_id=state.task_id,
3138
+ )
3139
+
3140
+
3129
3141
  def _worktree_preview_line(state: WizardState) -> Optional[str]:
3130
3142
  """confirm 직전 요약 블록에 넣을 worktree 미리보기 한 줄.
3131
3143
  final-verification 은 stage worktree 를 읽기 전용 재사용하므로 None."""
@@ -3138,9 +3150,7 @@ def _worktree_preview_line(state: WizardState) -> Optional[str]:
3138
3150
  project_id=state.project_id,
3139
3151
  task_group_segment=state.task_group,
3140
3152
  task_id_segment=state.task_id,
3141
- # okstra-run 경로는 --work-category 를 넘기지 않으므로 provision 도 ""(→ "task-" prefix)
3142
- # 브랜치를 만든다. 이 미리보기 브랜치명은 실제 생성 브랜치와 정확히 일치한다.
3143
- work_category="",
3153
+ work_category=_preview_work_category(state),
3144
3154
  base_ref=state.base_ref,
3145
3155
  )
3146
3156
  key = {
@@ -3167,7 +3177,8 @@ def _worktree_preview_line_impl(state: WizardState) -> str:
3167
3177
  path=str(parent_dir))
3168
3178
  decision = preview_stage_worktree_decision(
3169
3179
  project_id=state.project_id, task_group_segment=state.task_group,
3170
- task_id_segment=state.task_id, work_category="",
3180
+ task_id_segment=state.task_id,
3181
+ work_category=_preview_work_category(state),
3171
3182
  stage_number=int(stage),
3172
3183
  )
3173
3184
  key = ("worktree_impl_reuse" if decision.status == "reused"
@@ -8,6 +8,11 @@ profile / validators / wizard 는 이 모듈에서 enum 을 import 해야 한다
8
8
  """
9
9
  from __future__ import annotations
10
10
 
11
+ import json
12
+ from pathlib import Path
13
+
14
+ from .paths import task_dir
15
+
11
16
  WORK_CATEGORIES: tuple[str, ...] = (
12
17
  "bugfix",
13
18
  "feature",
@@ -16,6 +21,38 @@ WORK_CATEGORIES: tuple[str, ...] = (
16
21
  "improvement",
17
22
  )
18
23
 
24
+ DEFAULT_WORK_CATEGORY = "feature"
25
+
19
26
 
20
27
  def is_valid_category(value: str) -> bool:
21
28
  return value in WORK_CATEGORIES
29
+
30
+
31
+ def recorded_work_category(project_root: Path, task_group: str, task_id: str) -> str:
32
+ """task-manifest.json 에 이미 기록된 work-category. 없거나 enum 밖이면 ""."""
33
+ manifest = task_dir(project_root, task_group, task_id) / "task-manifest.json"
34
+ try:
35
+ data = json.loads(manifest.read_text(encoding="utf-8"))
36
+ except (OSError, json.JSONDecodeError):
37
+ return ""
38
+ value = str(data.get("workCategory") or "").strip()
39
+ return value if is_valid_category(value) else ""
40
+
41
+
42
+ def resolve_work_category(
43
+ explicit: str, *, project_root: Path, task_group: str, task_id: str
44
+ ) -> str:
45
+ """이번 run 이 실제로 쓸 work-category: 명시값 → task 에 기록된 분류 →
46
+ DEFAULT_WORK_CATEGORY.
47
+
48
+ requirements-discovery 가 분류를 task-manifest 에 기록해두면 이후 phase 는
49
+ `--work-category` 없이 실행돼도 같은 branch namespace 로 수렴한다. 어느 쪽도
50
+ 없을 때 `unknown`(→ `task/` 접두) 대신 기본 분류로 떨어뜨린다.
51
+ """
52
+ value = (explicit or "").strip()
53
+ if value:
54
+ return value
55
+ return (
56
+ recorded_work_category(project_root, task_group, task_id)
57
+ or DEFAULT_WORK_CATEGORY
58
+ )
@@ -0,0 +1,107 @@
1
+ """Shared initial analysis-worker prompt body rendering."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Mapping, Sequence
5
+
6
+ from .worker_prompt_policy import PromptPlan
7
+
8
+
9
+ ANALYSIS_WORKER_LABELS = {
10
+ "claude": "Claude worker",
11
+ "codex": "Codex worker",
12
+ "antigravity": "Antigravity worker",
13
+ }
14
+
15
+
16
+ def analysis_prompt_body(
17
+ manifest: Mapping[str, Any],
18
+ active_context: Mapping[str, Any],
19
+ worker_id: str,
20
+ model: str,
21
+ role: str,
22
+ plan: PromptPlan,
23
+ ) -> list[str]:
24
+ """Render the provider-neutral body for an initial analysis audience."""
25
+ label = ANALYSIS_WORKER_LABELS.get(worker_id, f"{worker_id} worker")
26
+ return [
27
+ f"**Model:** {label}, {model}",
28
+ f"**Pane role:** {role}",
29
+ "",
30
+ f"# {label} Dispatch",
31
+ "",
32
+ "## Role",
33
+ (
34
+ f"You are the {label} for okstra cross-verification. "
35
+ "Produce an independent worker result."
36
+ ),
37
+ "",
38
+ "## Task",
39
+ f"- Task key: `{_require_string(manifest, 'taskKey')}`",
40
+ f"- Task type: `{_require_string(manifest, 'taskType')}`",
41
+ "",
42
+ "## Inputs",
43
+ *analysis_input_lines(manifest, active_context, plan),
44
+ "",
45
+ mcp_pointer_line(),
46
+ "",
47
+ "## Output Contract",
48
+ "- Read the Worker Preamble Path end-to-end before analysis.",
49
+ "- Write the worker result to Result Path and no other canonical result path.",
50
+ "- Write the audit sidecar and any tool-failure entries as the preamble requires.",
51
+ "- Cite evidence with file paths and line numbers whenever you make a claim.",
52
+ ]
53
+
54
+
55
+ def analysis_input_lines(
56
+ manifest: Mapping[str, Any],
57
+ active_context: Mapping[str, Any],
58
+ plan: PromptPlan,
59
+ ) -> list[str]:
60
+ if plan.packet_only:
61
+ packet_path = instruction_path(manifest, active_context, "analysisPacketPath")
62
+ return existing_input_lines((("Primary analysis packet", packet_path),))
63
+ inputs = [
64
+ ("Primary analysis packet", instruction_path(manifest, active_context, "analysisPacketPath")),
65
+ ("Task brief", instruction_path(manifest, active_context, "taskBriefPath")),
66
+ ("Analysis profile", instruction_path(manifest, active_context, "analysisProfilePath")),
67
+ ("Analysis material", instruction_path(manifest, active_context, "analysisMaterialPath")),
68
+ ("Reference expectations", instruction_path(manifest, active_context, "referenceExpectationsPath")),
69
+ ("Clarification response", instruction_path(manifest, active_context, "clarificationResponsePath")),
70
+ ]
71
+ return existing_input_lines(inputs)
72
+
73
+
74
+ def existing_input_lines(inputs: Sequence[tuple[str, str]]) -> list[str]:
75
+ lines = [f"- {label}: `{path}`" for label, path in inputs if path]
76
+ return lines or ["- No input paths were recorded in active-run-context."]
77
+
78
+
79
+ def instruction_path(
80
+ manifest: Mapping[str, Any],
81
+ active_context: Mapping[str, Any],
82
+ key: str,
83
+ ) -> str:
84
+ instruction_set = active_context.get("instructionSet")
85
+ if isinstance(instruction_set, Mapping):
86
+ value = _string_value(instruction_set.get(key))
87
+ if value:
88
+ return value
89
+ return _string_value(manifest.get(key))
90
+
91
+
92
+ def mcp_pointer_line() -> str:
93
+ return (
94
+ '**MCP servers:** follow the task brief\'s "## Available MCP Servers" '
95
+ "section (already in your Required reading)."
96
+ )
97
+
98
+
99
+ def _require_string(payload: Mapping[str, Any], key: str) -> str:
100
+ value = _string_value(payload.get(key))
101
+ if not value:
102
+ raise ValueError(f"missing required string field: {key}")
103
+ return value
104
+
105
+
106
+ def _string_value(value: Any) -> str:
107
+ return value.strip() if isinstance(value, str) else ""