okstra 0.220.0 → 0.221.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 (67) hide show
  1. package/dist/cli-registry.mjs +6 -0
  2. package/dist/cli-registry.mjs.map +1 -1
  3. package/dist/lib/paths.mjs +1 -1
  4. package/dist/lib/paths.mjs.map +1 -1
  5. package/docs/architecture.md +2 -0
  6. package/docs/cli.md +7 -3
  7. package/docs/project-structure-overview.md +1 -1
  8. package/package.json +1 -1
  9. package/runtime/BUILD.json +2 -2
  10. package/runtime/prompts/launch.template.md +1 -1
  11. package/runtime/prompts/lead/okstra-lead-contract.md +12 -8
  12. package/runtime/prompts/lead/team-contract.md +1 -1
  13. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +3 -2
  14. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +3 -3
  15. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +4 -2
  16. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +3 -3
  17. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +27 -0
  18. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +24 -0
  19. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +24 -0
  20. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +25 -0
  21. package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +20 -1
  22. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +3 -0
  23. package/runtime/python/okstra_ctl/analysis_packet.py +14 -0
  24. package/runtime/python/okstra_ctl/business_flow/engine.py +10 -2
  25. package/runtime/python/okstra_ctl/correction_cli.py +44 -5
  26. package/runtime/python/okstra_ctl/correction_round.py +40 -11
  27. package/runtime/python/okstra_ctl/domain/mcp.py +43 -0
  28. package/runtime/python/okstra_ctl/domain/provider.py +2 -0
  29. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +89 -8
  30. package/runtime/python/okstra_ctl/file_size_cap.py +142 -0
  31. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +3 -6
  32. package/runtime/python/okstra_ctl/lead_progress.py +56 -11
  33. package/runtime/python/okstra_ctl/manager_cli.py +13 -1
  34. package/runtime/python/okstra_ctl/manager_launch.py +23 -2
  35. package/runtime/python/okstra_ctl/manager_sync.py +38 -2
  36. package/runtime/python/okstra_ctl/mcp_cli.py +179 -0
  37. package/runtime/python/okstra_ctl/mcp_servers.py +421 -0
  38. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +4 -3
  39. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -0
  40. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-self-check.md +1 -0
  41. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +3 -2
  42. package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +1 -1
  43. package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +19 -0
  44. package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +14 -0
  45. package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +2 -1
  46. package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +2 -2
  47. package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +2 -2
  48. package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +1 -1
  49. package/runtime/python/okstra_ctl/phases/technical_verification/entry.py +37 -0
  50. package/runtime/python/okstra_ctl/phases/technical_verification/validation.py +3 -3
  51. package/runtime/python/okstra_ctl/render.py +7 -48
  52. package/runtime/python/okstra_ctl/report_assembly.py +5 -5
  53. package/runtime/python/okstra_ctl/report_inputs.py +10 -1
  54. package/runtime/python/okstra_ctl/report_language.py +4 -5
  55. package/runtime/python/okstra_ctl/run.py +29 -0
  56. package/runtime/python/okstra_ctl/stage_done.py +37 -3
  57. package/runtime/python/okstra_ctl/wizard/engine.py +1 -2
  58. package/runtime/skills/_fragments/wizard-lead-takeover.md +1 -1
  59. package/runtime/skills/_fragments/wizard-relay-protocol.md +2 -2
  60. package/runtime/skills/okstra-discussion/SKILL.md +3 -3
  61. package/runtime/skills/okstra-manager/SKILL.md +3 -2
  62. package/runtime/skills/okstra-run/SKILL.md +3 -3
  63. package/runtime/skills/okstra-setup/SKILL.md +2 -0
  64. package/runtime/skills/okstra-setup/references/project-config.md +34 -0
  65. package/runtime/templates/report-writer-prompt-preamble.md +1 -1
  66. package/runtime/templates/reports/task-brief.template.md +3 -3
  67. package/runtime/validators/validate-run.py +13 -0
@@ -33,7 +33,7 @@ When a predecessor advances, `okstra_ctl.stage_rebase.synchronize_stage_stack` r
33
33
 
34
34
  Because the PRs are a stack, they must be merged in dependency order with a merge commit or a rebase-merge. A squash replaces the commits the next PR's base points at, so the stack breaks — every PR body states this, and the lead never merges.
35
35
 
36
- This phase has no worker dispatch. It does not use a provider or report-writer roster; the host-native Okstra lead performs git/gh inspection, user questions, the PR draft, and the final report inline.
36
+ This phase has no worker dispatch. It does not use a provider or report-writer roster; the host-native Okstra lead performs git/gh inspection, user questions, the PR draft, and the report narrative inline; `okstra report-finalize` assembles the final report from that narrative.
37
37
 
38
38
  ## 2. okstra-run wizard flow
39
39
 
@@ -98,3 +98,40 @@ def materialize_technical_input_packet(payload_json: str, run_root: Path, seq: s
98
98
  "- Read the input before planning experiments; copy each fact identity verbatim into results.\n"
99
99
  + "\n```json\n" + payload_json + "\n```\n"
100
100
  )
101
+
102
+
103
+ def technical_evidence_packet_block(source: Path, project_root: Path) -> str:
104
+ """carry-in 이 시험 결과 기록이면 후속 비교 워커가 읽을 증거 위치를 렌더한다.
105
+
106
+ clarification carry-in 은 §1 행만 싣고 원문은 `Source report:` 한 줄로만
107
+ 가리키므로, 이 블록이 없으면 워커는 시험 결과와 로그에 닿지 못한다.
108
+ """
109
+ from okstra_ctl.path_resolve import relative_to_project_root
110
+ from okstra_ctl.phases.technical_verification.validation import REPORT_NAME_RE
111
+
112
+ match = REPORT_NAME_RE.fullmatch(source.name)
113
+ if match is None or not source.is_file():
114
+ return ""
115
+ data = load_owned_object_snapshot(
116
+ source, artifact="technical verification result"
117
+ ).value
118
+ if (data.get("header") or {}).get("taskType") != "technical-verification":
119
+ return ""
120
+ experiments = source.parent.parent / "experiments" / match[1]
121
+ lines = [
122
+ "## Technical Verification Evidence",
123
+ "",
124
+ "The clarification response is a technical-verification record. Apply"
125
+ " `Technical evidence loop` in `## Phase Focus Extract`: read each"
126
+ " check's plan, commands and logs below before authoring a fresh"
127
+ " feasibility vote.",
128
+ "",
129
+ f"- Result record: `{relative_to_project_root(source, project_root)}`",
130
+ "- Experiment plans and logs:"
131
+ f" `{relative_to_project_root(experiments, project_root)}/`",
132
+ ]
133
+ for check in (data.get("technicalVerification") or {}).get("checks") or []:
134
+ lines.append(
135
+ f"- `{check.get('id')}` on `{check.get('candidateId')}`: {check.get('status')}"
136
+ )
137
+ return "\n".join(lines)
@@ -10,6 +10,8 @@ from okstra_ctl.implementation_direction import validate_task_artifact_path
10
10
  from okstra_ctl.json_boundary import load_owned_object_snapshot
11
11
  from okstra_ctl.technical_verification_facts import TechnicalVerificationError
12
12
 
13
+ REPORT_NAME_RE = re.compile(r"final-report-technical-verification-(\d{3,})\.data\.json")
14
+
13
15
 
14
16
  def _check_execution_evidence(
15
17
  check: Mapping[str, Any], project_root: Path, run_root: Path, seq: str
@@ -51,9 +53,7 @@ def validate_technical_verification_report(
51
53
  """발행·최종 검증에서 같은 사실 집합과 실제 시험 산출물을 대조한다."""
52
54
  if (data.get("header") or {}).get("taskType") != "technical-verification":
53
55
  return []
54
- match = re.fullmatch(
55
- r"final-report-technical-verification-(\d{3,})\.data\.json", report_path.name
56
- )
56
+ match = REPORT_NAME_RE.fullmatch(report_path.name)
57
57
  if match is None:
58
58
  return ["technical verification report must use its canonical record path"]
59
59
  run_root = report_path.parent.parent
@@ -26,7 +26,7 @@ import sys
26
26
  from collections.abc import Mapping
27
27
  from pathlib import Path
28
28
 
29
- from okstra_project.dirs import TASK_MANIFEST_FILENAME, OKSTRA_DIR_NAME, project_json_path
29
+ from okstra_project.dirs import TASK_MANIFEST_FILENAME
30
30
 
31
31
  # phase 시퀀스 / 기본 next-phase 매핑의 SSOT 는 workflow 모듈이다. 과거
32
32
  # render_task_manifest 가 동일한 리스트/딕셔너리를 로컬에 중복 정의했는데,
@@ -50,6 +50,8 @@ from .registry.provider_registry import default_provider_registry
50
50
  from .path_hints import compact_active_run_context, hydrate_run_context
51
51
  from .paths import task_timeline_file
52
52
  from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
53
+ from .mcp_servers import load_declarations as load_mcp_declarations
54
+ from .mcp_servers import render_block as render_mcp_block
53
55
  from .report_contract import CURRENT_REPORT_SCHEMA_VERSION
54
56
  from .report_language import resolve_report_language
55
57
  from .validation_contract import CURRENT_VALIDATION_CONTRACT_VERSION
@@ -2509,54 +2511,11 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
2509
2511
  # --------------------------------------------------------------------------- #
2510
2512
 
2511
2513
 
2512
- _NO_MCP_SERVERS_LINE = (
2513
- f"- No MCP servers are declared in `{OKSTRA_DIR_NAME}/project.json`'s "
2514
- "`mcpServers` array. Treat MCP tools as unavailable for this run. To enable "
2515
- "them, add entries shaped `{name, description, tools, notes?}` to that array "
2516
- "and re-render the bundle."
2517
- )
2518
-
2519
-
2520
2514
  def build_available_mcp_servers_block(project_root: Path) -> str:
2521
- """Render the `## Available MCP Servers` first bullet from project.json.
2522
-
2523
- The MCP server list used to be hardcoded for one specific environment.
2524
- It now comes from the project's okstra project.json (`mcpServers` array),
2525
- so each user/project declares the MCP surface available to their
2526
- lead+workers. Missing file or empty array yields a generic "none declared"
2527
- fallback.
2528
- """
2529
- config_path = project_json_path(project_root)
2530
- try:
2531
- raw = load_owned_object(config_path, artifact="project config")
2532
- except JsonBoundaryError:
2533
- return _NO_MCP_SERVERS_LINE
2534
- servers = raw.get("mcpServers") if isinstance(raw, dict) else None
2535
- if not isinstance(servers, list) or not servers:
2536
- return _NO_MCP_SERVERS_LINE
2537
- lines: list[str] = []
2538
- for entry in servers:
2539
- if not isinstance(entry, dict):
2540
- continue
2541
- name = str(entry.get("name", "")).strip()
2542
- if not name:
2543
- continue
2544
- description = str(entry.get("description", "")).strip()
2545
- tools = entry.get("tools") or []
2546
- notes = str(entry.get("notes", "")).strip()
2547
- parts = [f"`mcp__{name}`"]
2548
- if description:
2549
- parts.append(description)
2550
- if isinstance(tools, list) and tools:
2551
- tool_names = ", ".join(
2552
- f"`{str(t).strip()}`" for t in tools if str(t).strip()
2553
- )
2554
- if tool_names:
2555
- parts.append(f"Tools: {tool_names}")
2556
- if notes:
2557
- parts.append(notes)
2558
- lines.append("- " + ". ".join(parts) + ".")
2559
- return "\n".join(lines) if lines else _NO_MCP_SERVERS_LINE
2515
+ """provider 판정 없이 렌더한다. prepare 는 판정한 블록을 ctx 로 넘기므로
2516
+ 이 경로는 prepare 를 거치지 않는 렌더 호출자만 쓴다."""
2517
+ declarations, _problems = load_mcp_declarations(project_root)
2518
+ return render_mcp_block(declarations)
2560
2519
 
2561
2520
 
2562
2521
  # --------------------------------------------------------------------------- #
@@ -762,7 +762,7 @@ def _attach_machine_planning(
762
762
  _sync_human_summary_with_plan_body_gate(data, projection)
763
763
 
764
764
 
765
- def _schema_owner(error: str) -> str:
765
+ def _schema_owner(error: str, narrative_owner: str) -> str:
766
766
  field = error.split(" ", 1)[0]
767
767
  owners = {
768
768
  "executionStatus": "team-state",
@@ -781,8 +781,8 @@ def _schema_owner(error: str) -> str:
781
781
  if "planBodyVerification" in error:
782
782
  return "convergence"
783
783
  if "selectedDirectionRef" in error:
784
- return "report-writer"
785
- return owners.get(field.split(".", 1)[0], "report-writer")
784
+ return narrative_owner
785
+ return owners.get(field.split(".", 1)[0], narrative_owner)
786
786
 
787
787
 
788
788
  def _publish(
@@ -864,7 +864,7 @@ def assemble_report(
864
864
  path_errors = planned_path_declaration_errors(data)
865
865
  if path_errors:
866
866
  raise ReportAssemblyError(tuple(
867
- AssemblyIssue("report-writer", str(inputs["narrative"].path), error.split(":", 1)[0], error)
867
+ AssemblyIssue(inputs["narrative"].owner, str(inputs["narrative"].path), error.split(":", 1)[0], error)
868
868
  for error in path_errors
869
869
  ))
870
870
  direction_errors = selected_direction_plan_errors(
@@ -888,7 +888,7 @@ def assemble_report(
888
888
  raise ReportAssemblyError(
889
889
  tuple(
890
890
  AssemblyIssue(
891
- _schema_owner(error),
891
+ _schema_owner(error, inputs["narrative"].owner),
892
892
  str(target),
893
893
  error.split(" ", 1)[0],
894
894
  error,
@@ -28,6 +28,10 @@ _INPUT_FIELDS = (
28
28
  ("convergence", "convergence", "convergenceStatePath"),
29
29
  )
30
30
 
31
+ # 워커 없이 리드가 리포트까지 쓰는 phase. 서사의 저자가 리드이므로, 서사가
32
+ # 없을 때 조립이 report-writer 를 지목하면 띄울 수 없는 워커를 찾게 된다.
33
+ _LEAD_AUTHORED_NARRATIVE_TASK_TYPES = frozenset({"release-handoff"})
34
+
31
35
  _PLANNING_INPUT_FIELDS = (
32
36
  ("design-preparation", "design-surface-detector", "designPreparationPath"),
33
37
  ("plan-body-verification", "convergence", "planBodyVerificationPath"),
@@ -63,8 +67,13 @@ def report_input_paths(
63
67
  if manifest.get("taskType") == "implementation-planning"
64
68
  else ()
65
69
  )
70
+ lead_authored = manifest.get("taskType") in _LEAD_AUTHORED_NARRATIVE_TASK_TYPES
66
71
  rows = tuple(
67
- ReportInputPath(key, owner, _project_path(project_root, manifest.get(field), field))
72
+ ReportInputPath(
73
+ key,
74
+ "lead" if lead_authored and key == "narrative" else owner,
75
+ _project_path(project_root, manifest.get(field), field),
76
+ )
68
77
  for key, owner, field in fields
69
78
  )
70
79
  if len({row.key for row in rows}) != len(rows):
@@ -1,10 +1,9 @@
1
1
  """Report-language resolution — the one place the precedence lives.
2
2
 
3
- Two surfaces need the answer and must agree: the `**Report Language:**` line
4
- both dispatchers stamp into the report-writer prompt, and `reportLanguage` in
5
- the run manifest, which report assembly copies into `meta.reportLanguage` and
6
- the HTML renderer reads. When only the prompt called this, a project could ask
7
- for `ko`, get a Korean-instructed writer, and still ship an English report.
3
+ Prepare calls this once and freezes the answer as `reportLanguage` in the run
4
+ manifest. Both report-writer dispatch paths stamp that manifest value as the
5
+ `**Report Language:**` line, and report assembly copies it into
6
+ `meta.reportLanguage`, so a config edit after prepare cannot split the two.
8
7
 
9
8
  The value is a language tag (`en`, `ko`, `fr`, `pt-BR`, …). The report writer
10
9
  authors the report prose in that language; nothing here knows any particular
@@ -23,6 +23,7 @@ from okstra_ctl.phases.implementation_planning.validation import validate_select
23
23
  from .phases.technical_verification.entry import (
24
24
  prepare_technical_verification_input,
25
25
  materialize_technical_input_packet,
26
+ technical_evidence_packet_block,
26
27
  )
27
28
 
28
29
  from argparse import (
@@ -55,6 +56,7 @@ from .phases.implementation_planning.entry import (
55
56
  )
56
57
  from . import fix_cycles
57
58
  from .analysis_packet import build_analysis_packet
59
+ from .mcp_servers import NO_MCP_SERVERS_LINE, prepare_mcp
58
60
  from .coverage_census import (
59
61
  CensusError,
60
62
  census_filename,
@@ -3051,6 +3053,16 @@ def _write_instruction_set_sources(
3051
3053
  ),
3052
3054
  coverage_census_text=coverage_census_text,
3053
3055
  has_selected_direction=(instruction_set / "selected-direction.json").is_file(),
3056
+ mcp_servers_text=(
3057
+ "" if ctx.get("AVAILABLE_MCP_SERVERS", NO_MCP_SERVERS_LINE) == NO_MCP_SERVERS_LINE
3058
+ else ctx["AVAILABLE_MCP_SERVERS"]
3059
+ ),
3060
+ technical_evidence_text=(
3061
+ technical_evidence_packet_block(
3062
+ Path(inp.clarification_response_path), Path(ctx["PROJECT_ROOT"]),
3063
+ )
3064
+ if inp.clarification_response_path else ""
3065
+ ),
3054
3066
  )
3055
3067
  if inp.task_type == "technical-verification":
3056
3068
  packet += materialize_technical_input_packet(
@@ -4055,6 +4067,20 @@ def _prepare_user_authorization(inp: PrepareInputs) -> dict:
4055
4067
  return record
4056
4068
 
4057
4069
 
4070
+ def _prepare_mcp_block(
4071
+ project_root: Path, plan: AssignmentPlan, brief_path: Path,
4072
+ ) -> str:
4073
+ brief_text = brief_path.read_text(encoding="utf-8") if brief_path.is_file() else ""
4074
+ prepared = prepare_mcp(
4075
+ project_root,
4076
+ (assignment.provider_id for assignment in plan.assignments),
4077
+ brief_text,
4078
+ )
4079
+ for warning in prepared.warnings:
4080
+ print(f"prepare: {warning}", file=sys.stderr)
4081
+ return prepared.block
4082
+
4083
+
4058
4084
  def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
4059
4085
  """Produce a complete okstra task bundle on disk. See module docstring."""
4060
4086
  user_authorization = _prepare_user_authorization(inp)
@@ -4326,6 +4352,9 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
4326
4352
  "DYNAMIC_EXECUTION_ROLES_JSON": json.dumps(
4327
4353
  dynamic_roles, ensure_ascii=False,
4328
4354
  ),
4355
+ "AVAILABLE_MCP_SERVERS": _prepare_mcp_block(
4356
+ project_root, assignment_plan, inp.brief_path,
4357
+ ),
4329
4358
  })
4330
4359
  refresh_run_context_snapshot(ctx)
4331
4360
  if lead_runtime == "codex":
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
  import argparse
13
13
  import json
14
14
  import re
15
+ import subprocess
15
16
  from datetime import datetime, timezone
16
17
  from pathlib import Path
17
18
 
@@ -23,6 +24,7 @@ from .consumers import (
23
24
  carry_head_commit,
24
25
  read_consumers,
25
26
  )
27
+ from .file_size_cap import file_size_violations
26
28
  from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
27
29
  from .paths import RunRef
28
30
  from .stage_reopen import resolve_planned_stage
@@ -49,6 +51,34 @@ def _read_carry(carry_file: Path, stage: int) -> dict:
49
51
  return carry
50
52
 
51
53
 
54
+ def _check_file_size(project_root: Path, carry: dict, head: str) -> str:
55
+ """Refuse to settle a stage whose diff breaks the file-size limit; return what was checked.
56
+
57
+ Verifiers report the limit from `okstra correction file-size`; this re-runs it so a stage
58
+ a verifier did not check still reaches the correction round instead of landing. A range
59
+ git cannot read is reported in the output, not refused: the limit is a code-quality rule
60
+ and must not strand a stage whose verdict already passed.
61
+ """
62
+ rng = carry.get("stageCommitRange")
63
+ base = rng.get("base") if isinstance(rng, dict) else None
64
+ if not base:
65
+ return "not checked: the carry has no stageCommitRange.base"
66
+ try:
67
+ violations = [
68
+ v for v in file_size_violations(project_root, str(base), head) if v.blocking
69
+ ]
70
+ except subprocess.CalledProcessError as exc:
71
+ return f"not checked: git: {exc.stderr.decode(errors='replace').strip()}"
72
+ if violations:
73
+ raise StateError(
74
+ "the stage diff breaks the 400-line file-size limit:\n"
75
+ + "\n".join(v.line() for v in violations)
76
+ + f"\nRun a correction round (`okstra correction prompt ... --base {head} "
77
+ f"--stage-base {base}`) so the executor splits these files, or end the run FAIL "
78
+ "with `okstra stage-fail` when no round is left.", stage="file-size")
79
+ return f"pass: {base}..{head}"
80
+
81
+
52
82
  def _supersede(target: Path, rows: list[dict], stage: int) -> Path:
53
83
  """Move the sidecar of the run that first settled a reopened stage aside, unchanged."""
54
84
  settled = [r for r in rows if r.get("stage") == stage and r.get("status") == "done"]
@@ -76,6 +106,7 @@ def done_stage(
76
106
  identity, plan_run_root = resolve_planned_stage(project_root, task_key, stage)
77
107
  carry = _read_carry(carry_file, stage)
78
108
  head = carry_head_commit(carry)
109
+ file_size_cap = _check_file_size(project_root, carry, head)
79
110
  report = report if report.is_absolute() else project_root / report
80
111
  try:
81
112
  report_rel = report.relative_to(RunRef.from_report_path(report).run_dir).as_posix()
@@ -118,6 +149,7 @@ def done_stage(
118
149
  "supersededCarryPath": str(superseded) if superseded else "",
119
150
  "reportPath": report_rel,
120
151
  "consumersPath": str(plan_run_root / "consumers.jsonl"),
152
+ "fileSizeCap": file_size_cap,
121
153
  }
122
154
 
123
155
 
@@ -131,11 +163,13 @@ canonical sidecar `runs/implementation/carry/stage-<N>.json`, then appends the
131
163
  `done` row with that carry path, the report path relative to its run directory,
132
164
  the carry's head and the completion time. A reopened stage's previous sidecar
133
165
  is moved to `carry/superseded/stage-<N>-from-<task-type>-<seq>.json` first.
134
- Refuses a carry for another stage, a failed carry, and a stage already done at
135
- another head. Re-running with the same carry writes nothing new.
166
+ Refuses a carry for another stage, a failed carry, a stage already done at
167
+ another head, and a carry whose stageCommitRange diff leaves a source or test
168
+ file over the 400-line limit (`okstra correction file-size`). Re-running with the
169
+ same carry writes nothing new.
136
170
 
137
171
  Output: JSON { ok, taskKey, stage, headCommit, carryPath, supersededCarryPath,
138
- reportPath, consumersPath }. Exit 1 on a refusal, 2 when
172
+ reportPath, consumersPath, fileSizeCap }. Exit 1 on a refusal, 2 when
139
173
  PROJECT_ROOT cannot be resolved.
140
174
  """
141
175
 
@@ -96,7 +96,7 @@ def _build_group_prompt(state: WizardState, group_id: str) -> Prompt:
96
96
 
97
97
 
98
98
  def _is_lone_choice_pick(prompt: Prompt) -> bool:
99
- """고를 것이 한 줄뿐인 단일 선택 픽인가.
99
+ """고를 것이 한 줄뿐인 픽인가 — 단일 선택이든 체크박스든.
100
100
 
101
101
  호스트 선택기는 옵션 2개 미만을 받지 않아(`nativeLimits` `minOptions`) 이런
102
102
  화면은 번호 1개짜리 목록으로 내려간다 — 사용자는 고를 것이 없는 목록에서
@@ -106,7 +106,6 @@ def _is_lone_choice_pick(prompt: Prompt) -> bool:
106
106
  """
107
107
  return (
108
108
  prompt.kind == "pick"
109
- and not prompt.multi
110
109
  and len(prompt.options) == 1
111
110
  and prompt.options[0].value != _ABORT_OPTION
112
111
  )
@@ -1,6 +1,6 @@
1
1
  ## Step 6: Take over as Okstra lead
2
2
 
3
- Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it.
3
+ Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it. The run manifest's `reportLanguage` sets the language of okstra report artifacts; a host, project or global instruction file (`CLAUDE.md`, `AGENTS.md`) does not override it, and the lead never tells the report writer to use another language.
4
4
 
5
5
  Then proceed through the phases exactly as the lead prompt directs (Phase 1 context → Phase 2+ worker dispatch → final synthesis → final report).
6
6
 
@@ -20,8 +20,8 @@ On `ok: false`, re-prompt with the same `current.step` using the error message.
20
20
  The wizard tells you which relay operation to use via `next.interaction.kind`. Step 1 loads the current registered host's relay contract. Use the matching entry under its `interactions` object, including the runtime-generated navigation and completion options. If the entry is absent, stop instead of inventing a host function or falling back silently. Use the explicit `Legacy text relay compatibility` mapping only when the preflight response has no `relayContract`.
21
21
 
22
22
  - `native-single` → use the current registered host's relay contract for its native single-select function. Pass every option in its original order and submit the selected `options[].value`.
23
- - `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the selected values as one CSV answer; submit an empty selection as `--answer ""`.
24
- - `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; when a grouped question is multi-select, its step value is the selected option values joined as one CSV string, not a JSON array.
23
+ - `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the host's selected-labels string unchanged (labels may contain `, `; the wizard decodes it against the option list); submit an empty selection as `--answer ""`.
24
+ - `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; a single-select step value is the selected option value, and a multi-select step value is the host's selected-labels string unchanged, not a JSON array.
25
25
  - `numbered-single` → render every option as a 1-based numbered Markdown list and submit the user's next message unchanged.
26
26
  - `numbered-multi` → render every option as a 1-based numbered Markdown list and submit the user's comma-separated reply unchanged.
27
27
  - `sequential-group` → render every question in order, preserve each complete option list, collect every answer, and submit one JSON object keyed by `questions[].step`.
@@ -47,8 +47,8 @@ On `ok: false`, re-prompt with the same `current.step` using the error message.
47
47
  The wizard tells you which relay operation to use via `next.interaction.kind`. Step 1 loads the current registered host's relay contract. Use the matching entry under its `interactions` object, including the runtime-generated navigation and completion options. If the entry is absent, stop instead of inventing a host function or falling back silently. Use the explicit `Legacy text relay compatibility` mapping only when the preflight response has no `relayContract`.
48
48
 
49
49
  - `native-single` → use the current registered host's relay contract for its native single-select function. Pass every option in its original order and submit the selected `options[].value`.
50
- - `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the selected values as one CSV answer; submit an empty selection as `--answer ""`.
51
- - `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; when a grouped question is multi-select, its step value is the selected option values joined as one CSV string, not a JSON array.
50
+ - `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the host's selected-labels string unchanged (labels may contain `, `; the wizard decodes it against the option list); submit an empty selection as `--answer ""`.
51
+ - `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; a single-select step value is the selected option value, and a multi-select step value is the host's selected-labels string unchanged, not a JSON array.
52
52
  - `numbered-single` → render every option as a 1-based numbered Markdown list and submit the user's next message unchanged.
53
53
  - `numbered-multi` → render every option as a 1-based numbered Markdown list and submit the user's comma-separated reply unchanged.
54
54
  - `sequential-group` → render every question in order, preserve each complete option list, collect every answer, and submit one JSON object keyed by `questions[].step`.
@@ -290,7 +290,7 @@ You can delete the literal state-file path after this point — its job is done.
290
290
  <!-- BEGIN FRAGMENT: wizard-lead-takeover -->
291
291
  ## Step 6: Take over as Okstra lead
292
292
 
293
- Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it.
293
+ Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it. The run manifest's `reportLanguage` sets the language of okstra report artifacts; a host, project or global instruction file (`CLAUDE.md`, `AGENTS.md`) does not override it, and the lead never tells the report writer to use another language.
294
294
 
295
295
  Then proceed through the phases exactly as the lead prompt directs (Phase 1 context → Phase 2+ worker dispatch → final synthesis → final report).
296
296
 
@@ -29,7 +29,7 @@ okstra manager task status --manager-id <manager-id> --task-group <task-group> -
29
29
  okstra manager task sync --manager-id <manager-id> --task-group <task-group> --task-id <task-id>
30
30
  okstra manager task assign --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --project-id <project-id> [--child-task-id <child-task-id>] [--role <role>] [--tag <tag>] [--assignment <text>]
31
31
  okstra manager task note --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --scope <shared|project> [--project-id <project-id>] --body <text>
32
- okstra manager task run --manager-id <manager-id> --project-id <project-id> --task-group <task-group> --task-id <task-id> [--child-task-id <child-task-id>]
32
+ okstra manager task run --manager-id <manager-id> --project-id <project-id> --task-group <task-group> --task-id <task-id> [--child-task-id <child-task-id>] [--allow-completed]
33
33
  okstra manager task split --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --plan <split-plan.json> [--overwrite]
34
34
  okstra manager list managers
35
35
  okstra manager list projects --manager-id <manager-id>
@@ -52,6 +52,7 @@ okstra manager view --manager-id <manager-id>
52
52
 
53
53
  `remove project`, `remove task`, `remove task-group` and `task remove-child` change nothing without `--confirm`; they print what would be removed (`Removed: no — dry run`). Run the command without `--confirm` first, show the user the listed project, path, tasks or kept children, and rerun with `--confirm` only after the user approves that exact target. These commands touch manager files only; project `.okstra/` state and briefs written by `task split` stay.
54
54
 
55
+ - `task status` shows the snapshot as of `Synced at` with its `Sync age`; a child marked `changed since sync: yes` has a project task manifest written after that sync, so run `task sync` before reporting its state.
55
56
  - `remove project` unregisters the project and keeps its child tasks. `task status` shows them with `project linked: no`, the view marks them `unlinked`, and `task run` refuses them until the project is registered again with `new project`.
56
57
  - `task update` changes only the fields given and needs no confirmation. `new task` still refuses to change an existing task's objective, common brief or progress mode.
57
58
 
@@ -104,7 +105,7 @@ When the user wants to see a manager at a glance, run `okstra manager task sync
104
105
  For launching child work:
105
106
 
106
107
  1. Run `okstra manager task sync ...` if the manager snapshot must be refreshed first.
107
- 2. Run `okstra manager task run ...`.
108
+ 2. Run `okstra manager task run ...`. It re-reads the child's state from its project, not from the snapshot. When the child is already done there (terminal next phase, or work status `done`), it refuses with the last phase and report path; tell the user that and do not add `--allow-completed` unless the user asks to run the child again.
108
109
  3. Read the returned fixed fields `Backend`, `Worker dispatch backend`, `Project root`, `Context path`, `Run command`, every numbered `Run arg N`, and `Shell command`.
109
110
  4. Give the user the `Shell command` line verbatim and tell them to run it in a new terminal. It starts the installed launcher in the child project: the launcher fills the task type and brief from the child task manifest or asks for them, prepares the run with the manager context directive, and starts the child lead as a separate host process (`Backend` `spawn-process`).
110
111
 
@@ -47,8 +47,8 @@ On `ok: false`, re-prompt with the same `current.step` using the error message.
47
47
  The wizard tells you which relay operation to use via `next.interaction.kind`. Step 1 loads the current registered host's relay contract. Use the matching entry under its `interactions` object, including the runtime-generated navigation and completion options. If the entry is absent, stop instead of inventing a host function or falling back silently. Use the explicit `Legacy text relay compatibility` mapping only when the preflight response has no `relayContract`.
48
48
 
49
49
  - `native-single` → use the current registered host's relay contract for its native single-select function. Pass every option in its original order and submit the selected `options[].value`.
50
- - `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the selected values as one CSV answer; submit an empty selection as `--answer ""`.
51
- - `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; when a grouped question is multi-select, its step value is the selected option values joined as one CSV string, not a JSON array.
50
+ - `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the host's selected-labels string unchanged (labels may contain `, `; the wizard decodes it against the option list); submit an empty selection as `--answer ""`.
51
+ - `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; a single-select step value is the selected option value, and a multi-select step value is the host's selected-labels string unchanged, not a JSON array.
52
52
  - `numbered-single` → render every option as a 1-based numbered Markdown list and submit the user's next message unchanged.
53
53
  - `numbered-multi` → render every option as a 1-based numbered Markdown list and submit the user's comma-separated reply unchanged.
54
54
  - `sequential-group` → render every question in order, preserve each complete option list, collect every answer, and submit one JSON object keyed by `questions[].step`.
@@ -455,7 +455,7 @@ If `render-bundle` raises Step 5's concurrent-run conflict detection (concurrent
455
455
  <!-- BEGIN FRAGMENT: wizard-lead-takeover -->
456
456
  ## Step 6: Take over as Okstra lead
457
457
 
458
- Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it.
458
+ Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it. The run manifest's `reportLanguage` sets the language of okstra report artifacts; a host, project or global instruction file (`CLAUDE.md`, `AGENTS.md`) does not override it, and the lead never tells the report writer to use another language.
459
459
 
460
460
  Then proceed through the phases exactly as the lead prompt directs (Phase 1 context → Phase 2+ worker dispatch → final synthesis → final report).
461
461
 
@@ -174,6 +174,8 @@ matching section:
174
174
  - **F. declared architecture style** (`architecture.style`) — `hexagonal` /
175
175
  `layered` / `none`; hand-added, default `none`. Declaring one makes that
176
176
  architecture's placement rules binding in planning + verification.
177
+ - **H. MCP servers** (`mcpServers`) — `okstra mcp add` declares a server,
178
+ `okstra mcp check` shows which provider CLIs reach it.
177
179
 
178
180
  ## Step 4: Verify
179
181
 
@@ -301,3 +301,37 @@ preserves it:
301
301
  records that it read no rules and the run still passes. Whether a pack that
302
302
  does resolve was actually read and applied stays the phase's own
303
303
  `project-review-rules:` record — no machine check reads a worker's reasoning.
304
+
305
+ ## H. MCP servers (`mcpServers`)
306
+
307
+ `mcpServers` declares the MCP servers a run may use — a Figma file server for a
308
+ visual check, for example. Without a declaration the launch prompt and the
309
+ analysis packet tell every worker that MCP is unavailable, even when a provider
310
+ CLI has the server configured.
311
+
312
+ Register each server with `okstra mcp add`. `--tool` repeats per tool;
313
+ `--alias` repeats per name a provider CLI lists the server under (one provider
314
+ may list Figma as `claude.ai Figma`, another as `figma-desktop`):
315
+
316
+ ```bash
317
+ okstra mcp add --name figma --description "Figma design files" --tool get_screenshot --tool get_metadata --alias "claude.ai Figma" --alias figma-desktop
318
+ ```
319
+
320
+ Then check which providers can reach it:
321
+
322
+ ```bash
323
+ okstra mcp check
324
+ ```
325
+
326
+ Each provider adapter lists its own CLI's MCP servers without an inference
327
+ call. `reachable` means the provider listed the server enabled; `disabled`
328
+ means listed but disabled or awaiting authentication; `missing` means not
329
+ listed; `unknown` means the adapter cannot list. Show the result to the user.
330
+ For a provider that is not `reachable`, tell the user to add the server to that
331
+ provider CLI's own MCP configuration — okstra never edits a provider's
332
+ configuration. `okstra mcp list` and `okstra mcp remove --name <name>` manage
333
+ existing entries.
334
+
335
+ prepare repeats the check for the providers a run selects and prints a warning
336
+ for each gap; it never refuses the run. It also warns when the task brief cites
337
+ a Figma URL and no declared server name or alias contains `figma`.
@@ -30,7 +30,7 @@ The narrative is not free-form Markdown. A narrative file is the line `# OKSTRA
30
30
 
31
31
  Blank lines are ignored. **Every other line is rejected**, including Markdown headings (`#`, `##`, `###`), a pipe table written at column 0, code fences, bare paragraphs, JSON, YAML, and JSON Pointer. A heading or table that belongs in the reader's report is written as the text of a `> ` value, never as a narrative line of its own.
32
32
 
33
- Follow the task-specific schema order. Write every `> ` prose value directly in the language named by `**Report Language:**`; do not draft in English first, because no translation step follows. Field names, `Verdict Token` and other enum or status values, finding and requirement ids, paths, symbols, commands, and model names stay exactly as the schema and sources spell them.
33
+ Follow the task-specific schema order. Write every `> ` prose value directly in the language named by `**Report Language:**`; do not draft in English first, because no translation step follows. A host, project or global instruction file (`CLAUDE.md`, `AGENTS.md`) does not change that language for report artifacts. Field names, `Verdict Token` and other enum or status values, finding and requirement ids, paths, symbols, commands, and model names stay exactly as the schema and sources spell them.
34
34
 
35
35
  ### Top-level fields you may author
36
36
 
@@ -154,13 +154,13 @@ taskType: "{{FM_TASK_TYPE}}"
154
154
 
155
155
  ## Available MCP Servers
156
156
 
157
- The MCP servers available to this run are declared in `.okstra/project.json`'s `mcpServers` array and rendered into the Okstra lead's launch prompt under `## Available MCP Servers`. The lead is responsible for forwarding the rendered list verbatim into worker prompts (Phase 2) so each assignment can determine whether its selected runner exposes the named tools.
157
+ The MCP servers available to this run are declared in `.okstra/project.json`'s `mcpServers` array. Prepare renders them, with the providers that list each server, into the lead's launch prompt and the analysis packet under `## Available MCP Servers`.
158
158
 
159
- To declare servers, add entries shaped `{ "name": "<server>", "description": "...", "tools": ["..."], "notes": "..." }` to that array. If the array is empty or absent, treat MCP as unavailable for this run.
159
+ To declare a server, run `okstra mcp add --name <server> --description <text> --tool <tool>... [--alias <name-as-a-provider-lists-it>...]`, then `okstra mcp check` to see which providers reach it. If nothing is declared, treat MCP as unavailable for this run.
160
160
 
161
161
  How to invoke (worker-by-worker):
162
162
 
163
- - **Native-session assignments**: invoke an MCP tool directly by its tool name (e.g. `mcp__<server>__<tool>`) only when the selected host adapter exposes it. Do not call an MCP tool name through a shell.
163
+ - **Native-session assignments**: invoke an MCP tool directly through the host's tool interface, under the name the host adapter exposes, only when it exposes the server. Do not call an MCP tool name through a shell.
164
164
  - **CLI-wrapper assignments**: use only the MCP or tool configuration explicitly supported and forwarded by that provider wrapper. If the wrapper has no matching MCP configuration, record `MCP not available in this CLI` in `Missing Information or Assumptions`; do not attempt a shell fallback.
165
165
  - All workers: cite the exact server, tool, and SELECT (or `WHERE` filters) used in the result file. Tool-call failures must be logged in the worker's `*-errors.json` (commandKind `mcp_call`) so the lead can decide whether to retry under a different worker.
166
166
 
@@ -2342,6 +2342,15 @@ def _validate_conformance_surfaces(
2342
2342
  )
2343
2343
 
2344
2344
 
2345
+ def _stage_ended_without_a_commit(report_data: dict) -> bool:
2346
+ implementation = report_data.get("implementation")
2347
+ if not isinstance(implementation, dict):
2348
+ return False
2349
+ evidence = implementation.get("stageSidecarEvidence")
2350
+ withheld = isinstance(evidence, dict) and str(evidence.get("withheld") or "").strip()
2351
+ return bool(withheld) and not implementation.get("commitList")
2352
+
2353
+
2345
2354
  def _validate_conformance(
2346
2355
  report_path: Path,
2347
2356
  failures: list[str],
@@ -2389,6 +2398,10 @@ def _validate_conformance(
2389
2398
  )
2390
2399
  if declared_manifest is None:
2391
2400
  return warnings
2401
+ # 스크립트와 manifest 항목은 stage 를 실행한 run 이 쓴다. 커밋 없이 carry 를
2402
+ # 보류한 run(첫 편집 전 정지 후 재계획)은 그것을 쓸 기회가 없었다.
2403
+ if _stage_ended_without_a_commit(report_data):
2404
+ declared_manifest = None
2392
2405
  if declared_manifest is not None:
2393
2406
  scoped_declared = _scope_manifest_entries(declared_manifest, stage_name)
2394
2407
  for error in missing_declared_scripts(