okstra 0.207.1 → 0.209.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 +3 -2
- package/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +1 -1
- package/dist/commands/lifecycle/doctor.mjs +1 -1
- package/dist/lib/skill-catalog.mjs +1 -0
- package/dist/lib/skill-catalog.mjs.map +1 -1
- package/docs/architecture/storage-model.md +14 -0
- package/docs/architecture.md +30 -9
- package/docs/cli.md +26 -22
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/project-structure-overview.md +15 -8
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/operations/explain-flow.json +6 -0
- package/runtime/bin/lib/okstra/cli.sh +1 -5
- package/runtime/bin/lib/okstra/globals.sh +0 -2
- package/runtime/bin/lib/okstra/usage.sh +5 -3
- package/runtime/bin/okstra.sh +0 -2
- package/runtime/prompts/duties/business-flow-investigator.json +14 -0
- package/runtime/prompts/lead/context-loader.md +1 -1
- package/runtime/prompts/lead/convergence.md +22 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -6
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/lead/team-contract.md +12 -17
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/wizard/prompts.ko.json +0 -91
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +6 -0
- package/runtime/python/okstra_ctl/agent/invocation.py +1 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/batch.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +11 -125
- package/runtime/python/okstra_ctl/agent/standalone.py +183 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +39 -8
- package/runtime/python/okstra_ctl/assignment_resolver.py +7 -1
- package/runtime/python/okstra_ctl/brief_frontmatter.py +10 -0
- package/runtime/python/okstra_ctl/business_flow/__init__.py +4 -0
- package/runtime/python/okstra_ctl/business_flow/cli.py +134 -0
- package/runtime/python/okstra_ctl/business_flow/contracts.py +268 -0
- package/runtime/python/okstra_ctl/business_flow/engine.py +518 -0
- package/runtime/python/okstra_ctl/business_flow/hooks.py +221 -0
- package/runtime/python/okstra_ctl/business_flow/invocation.py +170 -0
- package/runtime/python/okstra_ctl/business_flow/report.py +49 -0
- package/runtime/python/okstra_ctl/business_flow/source.py +206 -0
- package/runtime/python/okstra_ctl/business_flow/store.py +388 -0
- package/runtime/python/okstra_ctl/cmux.py +21 -16
- package/runtime/python/okstra_ctl/convergence.py +173 -2
- package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +18 -0
- package/runtime/python/okstra_ctl/convergence_provenance.py +8 -0
- package/runtime/python/okstra_ctl/coverage_census.py +603 -0
- package/runtime/python/okstra_ctl/design_surfaces.py +4 -0
- package/runtime/python/okstra_ctl/direct_work.py +1 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +7 -3
- package/runtime/python/okstra_ctl/doctor.py +12 -6
- package/runtime/python/okstra_ctl/domain/role.py +1 -0
- package/runtime/python/okstra_ctl/group_context.py +5 -4
- package/runtime/python/okstra_ctl/legacy_model_selection.py +7 -51
- package/runtime/python/okstra_ctl/manager_split.py +4 -1
- package/runtime/python/okstra_ctl/model_io/lines.py +1 -24
- package/runtime/python/okstra_ctl/model_io/renderers.py +54 -41
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/error_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/error_analysis/profile.md +6 -8
- package/runtime/python/okstra_ctl/phases/feature_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/feature_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/final_verification/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -8
- package/runtime/python/okstra_ctl/phases/implementation/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/profile.md +0 -6
- package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +4 -3
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +1 -14
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +5 -8
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +3 -3
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/validation.py +15 -5
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +9 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +4 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +25 -9
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +7 -10
- package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.md +4 -11
- package/runtime/python/okstra_ctl/phases/project_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/project_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +1 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.md +14 -8
- package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +3 -3
- package/runtime/python/okstra_ctl/phases/technical_verification/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/technical_verification/profile.md +0 -4
- package/runtime/python/okstra_ctl/plan_items.py +1 -1
- package/runtime/python/okstra_ctl/render.py +10 -43
- package/runtime/python/okstra_ctl/render_final_report.py +3 -0
- package/runtime/python/okstra_ctl/report_assembly.py +15 -1
- package/runtime/python/okstra_ctl/report_finalize.py +50 -0
- package/runtime/python/okstra_ctl/report_html/render.py +3 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -2
- package/runtime/python/okstra_ctl/run.py +78 -409
- package/runtime/python/okstra_ctl/wizard/__init__.py +2 -24
- package/runtime/python/okstra_ctl/wizard/cli.py +3 -6
- package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -35
- package/runtime/python/okstra_ctl/wizard/engine.py +2 -4
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -88
- package/runtime/python/okstra_ctl/wizard/registry.py +36 -228
- package/runtime/python/okstra_ctl/wizard/render.py +2 -2
- package/runtime/python/okstra_ctl/wizard/roles.py +1 -3
- package/runtime/python/okstra_ctl/wizard/sources.py +9 -40
- package/runtime/python/okstra_ctl/wizard/state.py +36 -145
- package/runtime/python/okstra_ctl/wizard/statefile.py +27 -128
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +22 -10
- package/runtime/python/okstra_ctl/wizard/steps_options.py +5 -4
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +15 -565
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +9 -2
- package/runtime/schemas/business-flow-v1.schema.json +847 -0
- package/runtime/schemas/convergence-groups-v2.0.schema.json +7 -0
- package/runtime/skills/okstra-explain-flow/SKILL.md +42 -0
- package/runtime/skills/okstra-inspect/facets/history.md +5 -5
- package/runtime/skills/okstra-run/SKILL.md +2 -2
- package/runtime/templates/reports/business-flow.template.md +106 -0
- package/runtime/templates/reports/html/base.template.html +14 -1
- package/runtime/templates/reports/html/business-flow.template.html +31 -0
- package/runtime/templates/reports/html/i18n/en.json +1 -0
- package/runtime/templates/reports/html/i18n/ko.json +1 -0
- package/runtime/templates/worker-prompt-preamble.md +11 -2
- package/runtime/validators/checks/validate-prompt-metadata-01.py +10 -10
- package/runtime/validators/validate-run.py +70 -21
- package/runtime/validators/validate_analysis_report.py +21 -21
- package/runtime/python/okstra_ctl/workers.py +0 -133
|
@@ -118,6 +118,7 @@ def _add_materialize_parser(commands: argparse._SubParsersAction) -> None:
|
|
|
118
118
|
"result is not their own worker result",
|
|
119
119
|
)
|
|
120
120
|
materialize.add_argument("--host-runtime", help=standalone_only)
|
|
121
|
+
materialize.add_argument("--execution-runner", choices=("cli-wrapper",), help=standalone_only)
|
|
121
122
|
materialize.add_argument(
|
|
122
123
|
"--terminal-backend",
|
|
123
124
|
choices=(BACKEND_CLI_WRAPPER, BACKEND_CMUX_PANE),
|
|
@@ -13,7 +13,6 @@ import os
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
import shlex
|
|
15
15
|
import shutil
|
|
16
|
-
import tempfile
|
|
17
16
|
from typing import Any, Mapping, get_args
|
|
18
17
|
|
|
19
18
|
from ..invocation import (
|
|
@@ -31,7 +30,7 @@ from ..invocation import (
|
|
|
31
30
|
compose_unbound_run_prompt,
|
|
32
31
|
)
|
|
33
32
|
from ...assignment_environment import load_assignment_context
|
|
34
|
-
from
|
|
33
|
+
from ..standalone import StandaloneInvocationRequest, prepare_standalone_invocation
|
|
35
34
|
from ...path_hints import hydrate_active_run_context
|
|
36
35
|
from ...worker_prompt_headers import worker_prompt_headers
|
|
37
36
|
from ...worker_prompt_contract import (
|
|
@@ -127,6 +126,7 @@ def _materialize_run(
|
|
|
127
126
|
)
|
|
128
127
|
forbidden = {
|
|
129
128
|
"host-runtime": args.host_runtime,
|
|
129
|
+
"execution-runner": args.execution_runner,
|
|
130
130
|
"provider": args.provider,
|
|
131
131
|
"model-role": args.model_role,
|
|
132
132
|
"model": args.model,
|
|
@@ -142,9 +142,11 @@ def _materialize_run(
|
|
|
142
142
|
contract = _mapping(manifest.get("agentContract"), "run agent contract")
|
|
143
143
|
authorized = _mapping(contract.get("authorizedPaths"), "authorized paths")
|
|
144
144
|
|
|
145
|
+
# 교정 전용 디스패치는 본문을 원장에서 렌더한다(`_with_report_writer_sections`).
|
|
146
|
+
# 그때 지시문 출처는 원장 자신이다.
|
|
145
147
|
instruction_path = _authorized_path(
|
|
146
148
|
project_root,
|
|
147
|
-
args.instruction,
|
|
149
|
+
args.instruction or getattr(args, "corrections", None),
|
|
148
150
|
authorized.get("instructionRoots"),
|
|
149
151
|
"instruction",
|
|
150
152
|
must_exist=True,
|
|
@@ -343,7 +345,7 @@ def _materialize_run(
|
|
|
343
345
|
manifest=manifest,
|
|
344
346
|
active_context=active_context,
|
|
345
347
|
))
|
|
346
|
-
body = instruction_path.read_text(encoding="utf-8")
|
|
348
|
+
body = instruction_path.read_text(encoding="utf-8") if args.instruction else ""
|
|
347
349
|
identity_errors = validate_plan_verify_dispatch_identity(
|
|
348
350
|
body, dispatch_kind=args.dispatch_kind, result_name=result_path.name,
|
|
349
351
|
)
|
|
@@ -963,125 +965,9 @@ def _materialize_standalone(
|
|
|
963
965
|
execution_provider_ids=(args.provider,),
|
|
964
966
|
include_native_provider=False,
|
|
965
967
|
)
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
provider=args.provider,
|
|
969
|
-
model_role=args.model_role,
|
|
970
|
-
|
|
971
|
-
)
|
|
972
|
-
duty_root = root / f"{args.invocation_id}.duty-contracts"
|
|
973
|
-
_snapshot_standalone_duties(duty_root)
|
|
974
|
-
return prepare_agent_invocation(AgentInvocationRequest(
|
|
975
|
-
invocation_id=args.invocation_id,
|
|
976
|
-
worker_id=None,
|
|
977
|
-
audience=args.audience,
|
|
978
|
-
assignment_ref=None,
|
|
979
|
-
purpose=args.purpose,
|
|
980
|
-
assignment=assignment,
|
|
981
|
-
instruction=AgentInstruction(
|
|
982
|
-
anchor_lines=(),
|
|
983
|
-
body=instruction_path.read_text(encoding="utf-8"),
|
|
984
|
-
source_paths=(AgentInstructionSource(
|
|
985
|
-
kind="project",
|
|
986
|
-
path=_relative(project_root, instruction_path),
|
|
987
|
-
),),
|
|
988
|
-
),
|
|
989
|
-
project_root=project_root,
|
|
990
|
-
run_manifest_path=None,
|
|
991
|
-
duty_root=duty_root,
|
|
992
|
-
prompt_path=prompt_path,
|
|
993
|
-
metadata_path=prompt_path.with_name(prompt_path.name + ".meta.json"),
|
|
994
|
-
dispatch_kind="standalone",
|
|
995
|
-
participant_ref=None,
|
|
996
|
-
role_execution_ref=None,
|
|
997
|
-
duty_id=None,
|
|
998
|
-
invocation_ref=None,
|
|
999
|
-
attempt=1,
|
|
968
|
+
return prepare_standalone_invocation(StandaloneInvocationRequest(
|
|
969
|
+
project_root=project_root, invocation_id=args.invocation_id, audience=args.audience,
|
|
970
|
+
purpose=args.purpose, context=assignment_context, provider=args.provider,
|
|
971
|
+
model_role=args.model_role, model=args.model, instruction_path=instruction_path,
|
|
972
|
+
prompt_path=prompt_path, requested_runner=args.execution_runner,
|
|
1000
973
|
))
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
def _standalone_assignment(
|
|
1004
|
-
*,
|
|
1005
|
-
context: AssignmentContext,
|
|
1006
|
-
provider: str,
|
|
1007
|
-
model_role: str,
|
|
1008
|
-
model: str,
|
|
1009
|
-
) -> AgentModelAssignment:
|
|
1010
|
-
host_runtime = context.environment.host_descriptor.id
|
|
1011
|
-
resolved = resolve_dispatch_assignment(
|
|
1012
|
-
context=context,
|
|
1013
|
-
host_runtime=host_runtime,
|
|
1014
|
-
role=model_role,
|
|
1015
|
-
duty_id=model_role,
|
|
1016
|
-
provider=provider,
|
|
1017
|
-
model=model,
|
|
1018
|
-
)
|
|
1019
|
-
binding = resolved.binding
|
|
1020
|
-
if binding is None:
|
|
1021
|
-
raise AgentPromptCliError("standalone assignment requires a model binding")
|
|
1022
|
-
return AgentModelAssignment(
|
|
1023
|
-
provider=resolved.provider_id,
|
|
1024
|
-
model=resolved.display_name,
|
|
1025
|
-
model_execution_value=binding.resolved_execution_value,
|
|
1026
|
-
runner=binding.runner,
|
|
1027
|
-
host_runtime=host_runtime,
|
|
1028
|
-
host_model_value=binding.host_model_value,
|
|
1029
|
-
)
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
def _snapshot_standalone_duties(destination: Path) -> None:
|
|
1033
|
-
# 부모 개수를 세면 체크아웃(`<root>/scripts/okstra_ctl/agent/prompt_cli/`)
|
|
1034
|
-
# 에서만 맞는다 — 설치본은 패키지가 `~/.okstra/lib/python/` 이라 prompts 가
|
|
1035
|
-
# 한 단계 위(`~/.okstra/prompts/`)에 있다. duties 를 실제로 가진 루트를
|
|
1036
|
-
# 탐색해서 해소한다.
|
|
1037
|
-
from ...paths import find_asset_root
|
|
1038
|
-
|
|
1039
|
-
duties_relative = ("prompts", "duties")
|
|
1040
|
-
root = find_asset_root(duties_relative, is_present=Path.is_dir)
|
|
1041
|
-
if root is None:
|
|
1042
|
-
raise AgentPromptCliError(
|
|
1043
|
-
"duty contract source not found: no prompts/duties under "
|
|
1044
|
-
"OKSTRA_HOME or this checkout"
|
|
1045
|
-
)
|
|
1046
|
-
source = root.joinpath(*duties_relative)
|
|
1047
|
-
# 공통 계약은 직무가 아니라 `agents/` 가 소유한다(ADR-0017). 동결본은 한
|
|
1048
|
-
# 뿌리여야 하므로 여기서 함께 싣는다.
|
|
1049
|
-
common_root = find_asset_root(("agents", "common.json"), is_present=Path.is_file)
|
|
1050
|
-
if common_root is None:
|
|
1051
|
-
raise AgentPromptCliError(
|
|
1052
|
-
"agent common contract not found: no agents/common.json under "
|
|
1053
|
-
"OKSTRA_HOME or this checkout"
|
|
1054
|
-
)
|
|
1055
|
-
common_source = common_root / "agents" / "common.json"
|
|
1056
|
-
|
|
1057
|
-
def _staged_digest() -> str:
|
|
1058
|
-
staged = Path(tempfile.mkdtemp(prefix=".okstra-standalone-duties."))
|
|
1059
|
-
try:
|
|
1060
|
-
shutil.copytree(source, staged, dirs_exist_ok=True)
|
|
1061
|
-
shutil.copyfile(common_source, staged / "common.json")
|
|
1062
|
-
shutil.copytree(
|
|
1063
|
-
common_source.parent / "roles", staged / "roles", dirs_exist_ok=True
|
|
1064
|
-
)
|
|
1065
|
-
return digest_duty_catalog(staged)
|
|
1066
|
-
finally:
|
|
1067
|
-
shutil.rmtree(staged, ignore_errors=True)
|
|
1068
|
-
|
|
1069
|
-
if destination.exists():
|
|
1070
|
-
if not destination.is_dir() or digest_duty_catalog(destination) != _staged_digest():
|
|
1071
|
-
raise AgentPromptCliError("standalone duty snapshot conflicts with existing files")
|
|
1072
|
-
return
|
|
1073
|
-
temporary = Path(tempfile.mkdtemp(prefix=f".{destination.name}.", dir=destination.parent))
|
|
1074
|
-
try:
|
|
1075
|
-
shutil.copytree(source, temporary, dirs_exist_ok=True)
|
|
1076
|
-
shutil.copyfile(common_source, temporary / "common.json")
|
|
1077
|
-
shutil.copytree(
|
|
1078
|
-
common_source.parent / "roles", temporary / "roles", dirs_exist_ok=True
|
|
1079
|
-
)
|
|
1080
|
-
try:
|
|
1081
|
-
os.rename(temporary, destination)
|
|
1082
|
-
except FileExistsError:
|
|
1083
|
-
if digest_duty_catalog(destination) != _staged_digest():
|
|
1084
|
-
raise AgentPromptCliError("standalone duty snapshot publication conflict")
|
|
1085
|
-
finally:
|
|
1086
|
-
if temporary.exists():
|
|
1087
|
-
shutil.rmtree(temporary)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import tempfile
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..assignment_resolver import AssignmentContext, resolve_dispatch_assignment
|
|
10
|
+
from .invocation import (
|
|
11
|
+
AgentAudience,
|
|
12
|
+
AgentInstruction,
|
|
13
|
+
AgentInstructionSource,
|
|
14
|
+
AgentInvocationError,
|
|
15
|
+
AgentInvocationRequest,
|
|
16
|
+
AgentModelAssignment,
|
|
17
|
+
PreparedAgentInvocation,
|
|
18
|
+
digest_duty_catalog,
|
|
19
|
+
prepare_agent_invocation,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class StandaloneInvocationRequest:
|
|
25
|
+
project_root: Path
|
|
26
|
+
invocation_id: str
|
|
27
|
+
audience: AgentAudience
|
|
28
|
+
purpose: str
|
|
29
|
+
context: AssignmentContext
|
|
30
|
+
provider: str
|
|
31
|
+
model_role: str
|
|
32
|
+
model: str
|
|
33
|
+
instruction_path: Path
|
|
34
|
+
prompt_path: Path
|
|
35
|
+
requested_runner: str | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def prepare_standalone_invocation(
|
|
39
|
+
request: StandaloneInvocationRequest,
|
|
40
|
+
) -> PreparedAgentInvocation:
|
|
41
|
+
root = request.project_root / ".okstra" / "agent-invocations" / request.purpose
|
|
42
|
+
if not root.resolve().is_relative_to(request.project_root.resolve()):
|
|
43
|
+
raise AgentInvocationError("standalone invocation root escapes project")
|
|
44
|
+
duty_root = root / f"{request.invocation_id}.duty-contracts"
|
|
45
|
+
snapshot_standalone_duties(duty_root)
|
|
46
|
+
assignment = standalone_assignment(
|
|
47
|
+
context=request.context,
|
|
48
|
+
provider=request.provider,
|
|
49
|
+
model_role=request.model_role,
|
|
50
|
+
model=request.model,
|
|
51
|
+
requested_runner=request.requested_runner,
|
|
52
|
+
)
|
|
53
|
+
return prepare_agent_invocation(
|
|
54
|
+
AgentInvocationRequest(
|
|
55
|
+
invocation_id=request.invocation_id,
|
|
56
|
+
worker_id=None,
|
|
57
|
+
audience=request.audience,
|
|
58
|
+
assignment_ref=None,
|
|
59
|
+
purpose=request.purpose,
|
|
60
|
+
assignment=assignment,
|
|
61
|
+
instruction=AgentInstruction(
|
|
62
|
+
anchor_lines=(),
|
|
63
|
+
body=request.instruction_path.read_text(encoding="utf-8"),
|
|
64
|
+
source_paths=(
|
|
65
|
+
AgentInstructionSource(
|
|
66
|
+
kind="project",
|
|
67
|
+
path=request.instruction_path.relative_to(
|
|
68
|
+
request.project_root
|
|
69
|
+
).as_posix(),
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
project_root=request.project_root,
|
|
74
|
+
run_manifest_path=None,
|
|
75
|
+
duty_root=duty_root,
|
|
76
|
+
prompt_path=request.prompt_path,
|
|
77
|
+
metadata_path=request.prompt_path.with_name(
|
|
78
|
+
request.prompt_path.name + ".meta.json"
|
|
79
|
+
),
|
|
80
|
+
dispatch_kind="standalone",
|
|
81
|
+
participant_ref=None,
|
|
82
|
+
role_execution_ref=None,
|
|
83
|
+
duty_id=None,
|
|
84
|
+
invocation_ref=None,
|
|
85
|
+
attempt=1,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def standalone_assignment(
|
|
91
|
+
*,
|
|
92
|
+
context: AssignmentContext,
|
|
93
|
+
provider: str,
|
|
94
|
+
model_role: str,
|
|
95
|
+
model: str,
|
|
96
|
+
requested_runner: str | None = None,
|
|
97
|
+
) -> AgentModelAssignment:
|
|
98
|
+
host_runtime = context.environment.host_descriptor.id
|
|
99
|
+
resolved = resolve_dispatch_assignment(
|
|
100
|
+
context=context,
|
|
101
|
+
host_runtime=host_runtime,
|
|
102
|
+
role=model_role,
|
|
103
|
+
duty_id=model_role,
|
|
104
|
+
provider=provider,
|
|
105
|
+
model=model,
|
|
106
|
+
requested_runner=requested_runner,
|
|
107
|
+
)
|
|
108
|
+
binding = resolved.binding
|
|
109
|
+
if binding is None:
|
|
110
|
+
raise AgentInvocationError("standalone assignment requires a model binding")
|
|
111
|
+
return AgentModelAssignment(
|
|
112
|
+
provider=resolved.provider_id,
|
|
113
|
+
model=resolved.display_name,
|
|
114
|
+
model_execution_value=binding.resolved_execution_value,
|
|
115
|
+
runner=binding.runner,
|
|
116
|
+
host_runtime=host_runtime,
|
|
117
|
+
host_model_value=binding.host_model_value,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def snapshot_standalone_duties(destination: Path) -> None:
|
|
122
|
+
# 부모 개수를 세면 체크아웃(`<root>/scripts/okstra_ctl/agent/prompt_cli/`)
|
|
123
|
+
# 에서만 맞는다 — 설치본은 패키지가 `~/.okstra/lib/python/` 이라 prompts 가
|
|
124
|
+
# 한 단계 위(`~/.okstra/prompts/`)에 있다. duties 를 실제로 가진 루트를
|
|
125
|
+
# 탐색해서 해소한다.
|
|
126
|
+
from ..paths import find_asset_root
|
|
127
|
+
|
|
128
|
+
duties_relative = ("prompts", "duties")
|
|
129
|
+
root = find_asset_root(duties_relative, is_present=Path.is_dir)
|
|
130
|
+
if root is None:
|
|
131
|
+
raise AgentInvocationError(
|
|
132
|
+
"duty contract source not found: no prompts/duties under "
|
|
133
|
+
"OKSTRA_HOME or this checkout"
|
|
134
|
+
)
|
|
135
|
+
source = root.joinpath(*duties_relative)
|
|
136
|
+
# 공통 계약은 직무가 아니라 `agents/` 가 소유한다(ADR-0017). 동결본은 한
|
|
137
|
+
# 뿌리여야 하므로 여기서 함께 싣는다.
|
|
138
|
+
common_root = find_asset_root(("agents", "common.json"), is_present=Path.is_file)
|
|
139
|
+
if common_root is None:
|
|
140
|
+
raise AgentInvocationError(
|
|
141
|
+
"agent common contract not found: no agents/common.json under "
|
|
142
|
+
"OKSTRA_HOME or this checkout"
|
|
143
|
+
)
|
|
144
|
+
common_source = common_root / "agents" / "common.json"
|
|
145
|
+
|
|
146
|
+
def _staged_digest() -> str:
|
|
147
|
+
staged = Path(tempfile.mkdtemp(prefix=".okstra-standalone-duties."))
|
|
148
|
+
try:
|
|
149
|
+
_copy_standalone_contracts(source, common_source, staged)
|
|
150
|
+
return digest_duty_catalog(staged)
|
|
151
|
+
finally:
|
|
152
|
+
shutil.rmtree(staged, ignore_errors=True)
|
|
153
|
+
|
|
154
|
+
if destination.exists():
|
|
155
|
+
if (
|
|
156
|
+
not destination.is_dir()
|
|
157
|
+
or digest_duty_catalog(destination) != _staged_digest()
|
|
158
|
+
):
|
|
159
|
+
raise AgentInvocationError(
|
|
160
|
+
"standalone duty snapshot conflicts with existing files"
|
|
161
|
+
)
|
|
162
|
+
return
|
|
163
|
+
temporary = Path(
|
|
164
|
+
tempfile.mkdtemp(prefix=f".{destination.name}.", dir=destination.parent)
|
|
165
|
+
)
|
|
166
|
+
try:
|
|
167
|
+
_copy_standalone_contracts(source, common_source, temporary)
|
|
168
|
+
try:
|
|
169
|
+
os.rename(temporary, destination)
|
|
170
|
+
except FileExistsError:
|
|
171
|
+
if digest_duty_catalog(destination) != _staged_digest():
|
|
172
|
+
raise AgentInvocationError(
|
|
173
|
+
"standalone duty snapshot publication conflict"
|
|
174
|
+
)
|
|
175
|
+
finally:
|
|
176
|
+
if temporary.exists():
|
|
177
|
+
shutil.rmtree(temporary)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _copy_standalone_contracts(source: Path, common: Path, target: Path) -> None:
|
|
181
|
+
shutil.copytree(source, target, dirs_exist_ok=True)
|
|
182
|
+
shutil.copyfile(common, target / "common.json")
|
|
183
|
+
shutil.copytree(common.parent / "roles", target / "roles", dirs_exist_ok=True)
|
|
@@ -25,6 +25,7 @@ BRIEF_SECTIONS = (
|
|
|
25
25
|
CANONICAL_BRIEF_SECTIONS = (
|
|
26
26
|
"Source Material",
|
|
27
27
|
"Context",
|
|
28
|
+
"Project Scope",
|
|
28
29
|
"Problem / Symptom",
|
|
29
30
|
"Desired Outcome",
|
|
30
31
|
"Expected Behavior",
|
|
@@ -107,6 +108,8 @@ def build_analysis_packet(
|
|
|
107
108
|
stage_ledger_notice: str = "",
|
|
108
109
|
prior_planning_summary: str = "",
|
|
109
110
|
direct_work_text: str = "",
|
|
111
|
+
business_knowledge_text: str = "",
|
|
112
|
+
coverage_census_text: str = "",
|
|
110
113
|
) -> str:
|
|
111
114
|
"""Return the primary compact input for Claude/Codex/Antigravity analysers.
|
|
112
115
|
|
|
@@ -120,6 +123,9 @@ def build_analysis_packet(
|
|
|
120
123
|
`group_context_path` is the task-group context prepare copied into the
|
|
121
124
|
instruction set (`task-group-context.md`); `None` means the group has no
|
|
122
125
|
such document and the packet carries no `## Task-Group Context` section.
|
|
126
|
+
|
|
127
|
+
`coverage_census_text` is the `## Coverage Census` block
|
|
128
|
+
`coverage_census.render_census_section` rendered; empty means no census.
|
|
123
129
|
"""
|
|
124
130
|
brief_text = task_brief_path.read_text(encoding="utf-8")
|
|
125
131
|
group_context_text = _read_optional(group_context_path) if group_context_path else ""
|
|
@@ -155,6 +161,10 @@ def build_analysis_packet(
|
|
|
155
161
|
fix_history_text, prior_planning_summary, stage_ledger_json)),
|
|
156
162
|
))
|
|
157
163
|
parts.extend(_profile_block(task_type, profile_text))
|
|
164
|
+
if business_knowledge_text:
|
|
165
|
+
parts.extend(["", business_knowledge_text, ""])
|
|
166
|
+
if coverage_census_text:
|
|
167
|
+
parts.extend(["", coverage_census_text])
|
|
158
168
|
parts.extend(_reference_block(reference_text))
|
|
159
169
|
parts.extend(_fix_history_block(fix_history_text))
|
|
160
170
|
parts.extend(_stage_ledger_block(stage_ledger_json))
|
|
@@ -167,7 +177,7 @@ def build_analysis_packet(
|
|
|
167
177
|
|
|
168
178
|
|
|
169
179
|
_HEADING_RE = re.compile(r"\A#{1,2} \S")
|
|
170
|
-
_FENCE_RE = re.compile(r"\A\s*(
|
|
180
|
+
_FENCE_RE = re.compile(r"\A\s*(?P<marker>`{3,}|~{3,})(?P<info>.*)\Z")
|
|
171
181
|
# 목차 자신이 차지하는 줄 수 중 표제 행을 뺀 나머지. 행 수는 표제 개수로
|
|
172
182
|
# 정해지므로 삽입 전에 전체 이동량을 계산할 수 있다.
|
|
173
183
|
_INDEX_PREAMBLE_LINES = 5
|
|
@@ -273,11 +283,7 @@ def _heading_lines(lines: list[str]) -> list[tuple[int, str]]:
|
|
|
273
283
|
not a section.
|
|
274
284
|
"""
|
|
275
285
|
out: list[tuple[int, str]] = []
|
|
276
|
-
fenced =
|
|
277
|
-
for number, line in enumerate(lines, start=1):
|
|
278
|
-
if _FENCE_RE.match(line):
|
|
279
|
-
fenced = not fenced
|
|
280
|
-
continue
|
|
286
|
+
for number, (line, fenced) in enumerate(zip(lines, _fence_mask(lines)), start=1):
|
|
281
287
|
if fenced or not _HEADING_RE.match(line):
|
|
282
288
|
continue
|
|
283
289
|
if line.startswith("# OKSTRA Analysis Packet"):
|
|
@@ -286,6 +292,30 @@ def _heading_lines(lines: list[str]) -> list[tuple[int, str]]:
|
|
|
286
292
|
return out
|
|
287
293
|
|
|
288
294
|
|
|
295
|
+
def _fence_mask(lines: list[str]) -> list[bool]:
|
|
296
|
+
"""Per line, whether it belongs to a fenced block (fence lines included).
|
|
297
|
+
|
|
298
|
+
A fence closes only on the same character at least as long, with no info
|
|
299
|
+
string (CommonMark). Briefs quote whole documents in ````` ```` ````` fences
|
|
300
|
+
that themselves contain ``` blocks and `## ` headings.
|
|
301
|
+
"""
|
|
302
|
+
mask: list[bool] = []
|
|
303
|
+
opener: str | None = None
|
|
304
|
+
for line in lines:
|
|
305
|
+
match = _FENCE_RE.match(line)
|
|
306
|
+
if opener is None:
|
|
307
|
+
if match:
|
|
308
|
+
opener = match.group("marker")
|
|
309
|
+
mask.append(opener is not None)
|
|
310
|
+
continue
|
|
311
|
+
mask.append(True)
|
|
312
|
+
if (match and not match.group("info").strip()
|
|
313
|
+
and match.group("marker")[0] == opener[0]
|
|
314
|
+
and len(match.group("marker")) >= len(opener)):
|
|
315
|
+
opener = None
|
|
316
|
+
return mask
|
|
317
|
+
|
|
318
|
+
|
|
289
319
|
def _packet_frontmatter(brief_text: str, task_key: str) -> str:
|
|
290
320
|
frontmatter = _extract_frontmatter(brief_text)
|
|
291
321
|
if frontmatter:
|
|
@@ -697,8 +727,9 @@ def _section_map(text: str) -> dict[str, str]:
|
|
|
697
727
|
result: dict[str, list[str]] = {}
|
|
698
728
|
aliases: dict[str, str] = {}
|
|
699
729
|
current = ""
|
|
700
|
-
|
|
701
|
-
|
|
730
|
+
lines = _strip_frontmatter(text).splitlines()
|
|
731
|
+
for line, fenced in zip(lines, _fence_mask(lines)):
|
|
732
|
+
if line.startswith("## ") and not fenced:
|
|
702
733
|
current = line[3:].strip()
|
|
703
734
|
result.setdefault(current, [])
|
|
704
735
|
alias = _heading_alias(current)
|
|
@@ -240,6 +240,7 @@ def resolve_model_assignment(
|
|
|
240
240
|
environment: AssignmentEnvironment,
|
|
241
241
|
entry_mode: Literal["current-session", "new-session"] = "new-session",
|
|
242
242
|
require_selectable: bool = True,
|
|
243
|
+
requested_runner: str | None = None,
|
|
243
244
|
) -> ResolvedAssignment:
|
|
244
245
|
"""Resolve one canonical model through the selected host execution path.
|
|
245
246
|
|
|
@@ -269,6 +270,7 @@ def resolve_model_assignment(
|
|
|
269
270
|
model,
|
|
270
271
|
pool,
|
|
271
272
|
environment,
|
|
273
|
+
requested_runner,
|
|
272
274
|
)
|
|
273
275
|
return _resolved_assignment(instance, model, host, entry_mode, binding)
|
|
274
276
|
|
|
@@ -281,6 +283,7 @@ def resolve_dispatch_assignment(
|
|
|
281
283
|
duty_id: str,
|
|
282
284
|
provider: str,
|
|
283
285
|
model: str = "",
|
|
286
|
+
requested_runner: str | None = None,
|
|
284
287
|
) -> ResolvedAssignment:
|
|
285
288
|
"""Resolve one legacy provider/model input through the shared core."""
|
|
286
289
|
canonical_role = normalize_role(role)
|
|
@@ -298,6 +301,7 @@ def resolve_dispatch_assignment(
|
|
|
298
301
|
pool=pool,
|
|
299
302
|
host=host,
|
|
300
303
|
environment=context.environment,
|
|
304
|
+
requested_runner=requested_runner,
|
|
301
305
|
)
|
|
302
306
|
|
|
303
307
|
|
|
@@ -604,10 +608,12 @@ def _first_supported_binding(
|
|
|
604
608
|
model: ResolvedModel,
|
|
605
609
|
pool: ModelPool,
|
|
606
610
|
environment: AssignmentEnvironment,
|
|
611
|
+
requested_runner: str | None = None,
|
|
607
612
|
) -> HostModelBinding:
|
|
608
613
|
provider = pool.resolve_provider(model.provider_id)
|
|
609
614
|
errors: list[str] = []
|
|
610
|
-
|
|
615
|
+
runners = (requested_runner,) if requested_runner else _runner_candidates(instance, environment, provider)
|
|
616
|
+
for runner in runners:
|
|
611
617
|
try:
|
|
612
618
|
resolve_assignment_runner(
|
|
613
619
|
host=environment.host_descriptor,
|
|
@@ -45,5 +45,15 @@ def read_brief_frontmatter(path: Path) -> dict[str, str]:
|
|
|
45
45
|
return out
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def brief_task_id(frontmatter: Mapping[str, str]) -> str:
|
|
49
|
+
"""The task-id a brief belongs to, unslugified; ``''`` when it names none.
|
|
50
|
+
|
|
51
|
+
okstra-manager splits one ticket into per-project child tasks keyed by the
|
|
52
|
+
ticket alone, so its briefs record that key in ``task-id`` while ``brief-id``
|
|
53
|
+
stays the long file stem. Other briefs carry only ``brief-id``.
|
|
54
|
+
"""
|
|
55
|
+
return frontmatter.get("task-id") or frontmatter.get("brief-id", "")
|
|
56
|
+
|
|
57
|
+
|
|
48
58
|
def has_reporter_confirmation_contract(frontmatter: Mapping[str, str]) -> bool:
|
|
49
59
|
return "reporter-confirmations" in frontmatter
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..json_boundary import JsonBoundaryError
|
|
9
|
+
from ..report_language import resolve_report_language
|
|
10
|
+
from .contracts import BusinessFlowError, FlowRequest
|
|
11
|
+
from .engine import explain_flow, load_execution, rerun_explanation
|
|
12
|
+
from .store import query_knowledge
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
16
|
+
explain = argparse.ArgumentParser(add_help=False)
|
|
17
|
+
explain.add_argument("--question", dest="question_flag", default="")
|
|
18
|
+
explain.add_argument(
|
|
19
|
+
"--mode", choices=("current", "pre", "post"), default="current"
|
|
20
|
+
)
|
|
21
|
+
explain.add_argument("--task-key", default="")
|
|
22
|
+
explain.add_argument("--baseline", default="")
|
|
23
|
+
explain.add_argument("--source-root", type=Path)
|
|
24
|
+
explain.add_argument("--report-language", default="")
|
|
25
|
+
explain.add_argument(
|
|
26
|
+
"--source-artifact",
|
|
27
|
+
action="append",
|
|
28
|
+
default=[],
|
|
29
|
+
help="existing read-only verification record",
|
|
30
|
+
)
|
|
31
|
+
explain.add_argument("--host-runtime", default="claude-code")
|
|
32
|
+
explain.add_argument("--project-root", type=Path, default=Path.cwd())
|
|
33
|
+
explain.add_argument("--json", action="store_true")
|
|
34
|
+
parser = argparse.ArgumentParser(prog="okstra explain-flow", parents=[explain])
|
|
35
|
+
parser.set_defaults(command="explain", question="")
|
|
36
|
+
commands = parser.add_subparsers(dest="command")
|
|
37
|
+
explicit = commands.add_parser(
|
|
38
|
+
"explain", help="investigate a business question", parents=[explain]
|
|
39
|
+
)
|
|
40
|
+
explicit.add_argument("question", nargs="?", default="")
|
|
41
|
+
knowledge = commands.add_parser(
|
|
42
|
+
"knowledge", help="query relevant shared business knowledge"
|
|
43
|
+
)
|
|
44
|
+
knowledge.add_argument("--query", required=True)
|
|
45
|
+
rerun = commands.add_parser(
|
|
46
|
+
"rerun", help="retry only an explanation and retain its history"
|
|
47
|
+
)
|
|
48
|
+
rerun.add_argument("--execution", required=True)
|
|
49
|
+
rerun.add_argument("--source-root", type=Path)
|
|
50
|
+
status = commands.add_parser("status", help="inspect an explanation execution")
|
|
51
|
+
status.add_argument("--execution", required=True)
|
|
52
|
+
for child in (knowledge, rerun, status):
|
|
53
|
+
child.add_argument("--project-root", type=Path, default=Path.cwd())
|
|
54
|
+
child.add_argument("--json", action="store_true")
|
|
55
|
+
return parser
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main(argv: list[str] | None = None) -> int:
|
|
59
|
+
arguments = list(sys.argv[1:] if argv is None else argv)
|
|
60
|
+
if (
|
|
61
|
+
arguments
|
|
62
|
+
and not arguments[0].startswith("-")
|
|
63
|
+
and arguments[0]
|
|
64
|
+
not in {
|
|
65
|
+
"explain",
|
|
66
|
+
"knowledge",
|
|
67
|
+
"rerun",
|
|
68
|
+
"status",
|
|
69
|
+
"-h",
|
|
70
|
+
"--help",
|
|
71
|
+
}
|
|
72
|
+
):
|
|
73
|
+
arguments.insert(0, "explain")
|
|
74
|
+
args = build_parser().parse_args(arguments)
|
|
75
|
+
args.command = args.command or "explain"
|
|
76
|
+
try:
|
|
77
|
+
if args.command == "explain":
|
|
78
|
+
result = explain_flow(
|
|
79
|
+
FlowRequest(
|
|
80
|
+
args.project_root,
|
|
81
|
+
args.question_flag or args.question,
|
|
82
|
+
args.mode,
|
|
83
|
+
args.task_key,
|
|
84
|
+
args.baseline,
|
|
85
|
+
args.source_root,
|
|
86
|
+
args.report_language
|
|
87
|
+
or resolve_report_language(args.project_root, {}, {}),
|
|
88
|
+
tuple(args.source_artifact),
|
|
89
|
+
args.host_runtime,
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
elif args.command == "knowledge":
|
|
93
|
+
result = query_knowledge(args.project_root, args.query)
|
|
94
|
+
elif args.command == "rerun":
|
|
95
|
+
result = rerun_explanation(
|
|
96
|
+
args.project_root, args.execution, source_root=args.source_root
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
result = load_execution(args.project_root, args.execution)
|
|
100
|
+
print(
|
|
101
|
+
json.dumps(result, ensure_ascii=False, indent=2)
|
|
102
|
+
if args.json
|
|
103
|
+
else render_result(result)
|
|
104
|
+
)
|
|
105
|
+
return 1 if result.get("status") == "failed" else 0
|
|
106
|
+
except (OSError, BusinessFlowError, JsonBoundaryError) as exc:
|
|
107
|
+
print(f"explain-flow: {exc}", file=sys.stderr)
|
|
108
|
+
return 1
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def render_result(result: dict) -> str:
|
|
112
|
+
if "claims" in result:
|
|
113
|
+
lines = ["Shared Business Knowledge", ""]
|
|
114
|
+
for row in result["claims"]:
|
|
115
|
+
lines.append(
|
|
116
|
+
f"- {row['value']} ({row['level']}; {row['freshness']}; {row['status']})"
|
|
117
|
+
)
|
|
118
|
+
lines.append(f" - Claim: {row['id']}; owner: {row['ownerRoot']}")
|
|
119
|
+
lines.extend(f"- Read error: {error}" for error in result["errors"])
|
|
120
|
+
return "\n".join(lines)
|
|
121
|
+
lines = [
|
|
122
|
+
"Business Flow Explanation",
|
|
123
|
+
f"Status: {result['status']}",
|
|
124
|
+
f"Execution: {result['id']}",
|
|
125
|
+
]
|
|
126
|
+
lines.extend(f"{kind}: {path}" for kind, path in result.get("reports", {}).items())
|
|
127
|
+
if result.get("error"):
|
|
128
|
+
lines.append(f"Error: {result['error']}")
|
|
129
|
+
lines.append(f"Retry: okstra explain-flow rerun --execution {result['id']}")
|
|
130
|
+
return "\n".join(lines)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
raise SystemExit(main())
|