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.
- package/docs/architecture/storage-model.md +23 -3
- package/docs/architecture.md +28 -1
- package/docs/cli.md +9 -0
- package/docs/project-structure-overview.md +17 -7
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +3 -3
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/codex-worker.md +3 -3
- package/runtime/agents/workers/report-writer-worker.md +5 -5
- package/runtime/prompts/lead/adapters/claude-code.md +2 -1
- package/runtime/prompts/lead/adapters/codex.md +1 -0
- package/runtime/prompts/lead/adapters/external.md +1 -0
- package/runtime/prompts/lead/convergence.md +45 -83
- package/runtime/prompts/lead/okstra-lead-contract.md +12 -8
- package/runtime/prompts/lead/report-writer.md +3 -2
- package/runtime/prompts/lead/team-contract.md +19 -9
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +10 -6
- package/runtime/prompts/profiles/implementation-planning.md +5 -0
- package/runtime/prompts/profiles/improvement-discovery.md +11 -1
- package/runtime/prompts/profiles/requirements-discovery.md +8 -1
- package/runtime/python/okstra_ctl/analysis_packet.py +14 -15
- package/runtime/python/okstra_ctl/codex_dispatch.py +51 -80
- package/runtime/python/okstra_ctl/context_cost.py +82 -7
- package/runtime/python/okstra_ctl/convergence.py +223 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +1152 -0
- package/runtime/python/okstra_ctl/convergence_migration.py +184 -0
- package/runtime/python/okstra_ctl/convergence_store.py +85 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +60 -1
- package/runtime/python/okstra_ctl/improvement_assignment.py +61 -0
- package/runtime/python/okstra_ctl/log_report.py +44 -5
- package/runtime/python/okstra_ctl/path_hints.py +28 -1
- package/runtime/python/okstra_ctl/paths.py +10 -1
- package/runtime/python/okstra_ctl/render.py +78 -11
- package/runtime/python/okstra_ctl/run.py +66 -2
- package/runtime/python/okstra_ctl/wizard.py +15 -4
- package/runtime/python/okstra_ctl/work_categories.py +37 -0
- package/runtime/python/okstra_ctl/worker_prompt_body.py +107 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +316 -0
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +109 -10
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +158 -0
- package/runtime/templates/implementation-worker-preamble.md +51 -0
- package/runtime/templates/operating-standard.md +4 -4
- package/runtime/templates/report-writer-prompt-preamble.md +37 -0
- package/runtime/templates/worker-error-contract.md +46 -0
- package/runtime/templates/worker-prompt-preamble.md +28 -227
- package/runtime/validators/lib/fixtures.sh +27 -12
- package/runtime/validators/validate-run.py +228 -45
- package/src/cli-registry.mjs +7 -0
- package/src/commands/execute/convergence.mjs +34 -0
- 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
|
-
"-
|
|
1914
|
-
|
|
1915
|
-
"
|
|
1916
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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,
|
|
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 ""
|