okstra 0.214.1 → 0.216.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +2 -1
  2. package/dist/cli-registry.d.mts +14 -0
  3. package/dist/cli-registry.mjs +12 -0
  4. package/dist/cli-registry.mjs.map +1 -1
  5. package/dist/commands/chat/chat.mjs +2 -56
  6. package/dist/commands/chat/chat.mjs.map +1 -1
  7. package/dist/commands/execute/render-bundle.mjs +5 -2
  8. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  9. package/dist/commands/execute/wizard.mjs +6 -0
  10. package/dist/commands/execute/wizard.mjs.map +1 -1
  11. package/dist/commands/memory/memory.mjs +57 -6
  12. package/dist/commands/memory/memory.mjs.map +1 -1
  13. package/dist/lib/file-lock.d.mts +1 -0
  14. package/dist/lib/file-lock.mjs +60 -0
  15. package/dist/lib/file-lock.mjs.map +1 -0
  16. package/dist/lib/python-command.mjs +11 -1
  17. package/dist/lib/python-command.mjs.map +1 -1
  18. package/dist/lib/skill-catalog.mjs +1 -0
  19. package/dist/lib/skill-catalog.mjs.map +1 -1
  20. package/dist/lib/types.d.mts +1 -1
  21. package/docs/architecture.md +2 -2
  22. package/docs/cli.md +36 -14
  23. package/docs/project-structure-overview.md +6 -3
  24. package/package.json +1 -1
  25. package/runtime/BUILD.json +2 -2
  26. package/runtime/bin/lib/okstra/cli.sh +9 -2
  27. package/runtime/bin/lib/okstra/globals.sh +2 -0
  28. package/runtime/bin/lib/okstra/interactive.sh +8 -2
  29. package/runtime/bin/lib/okstra/usage.sh +3 -1
  30. package/runtime/bin/okstra.sh +7 -1
  31. package/runtime/prompts/duties/discussion-worker.json +32 -0
  32. package/runtime/prompts/lead/convergence.md +58 -136
  33. package/runtime/prompts/lead/okstra-lead-contract.md +45 -12
  34. package/runtime/prompts/profiles/_common-contract.md +1 -1
  35. package/runtime/prompts/wizard/prompts.ko.json +14 -1
  36. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +4 -0
  37. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +4 -11
  38. package/runtime/python/okstra_ctl/agent/invocation.py +1 -0
  39. package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +2 -2
  40. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +4 -4
  41. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +2 -2
  42. package/runtime/python/okstra_ctl/analysis_packet.py +3 -0
  43. package/runtime/python/okstra_ctl/consumers.py +8 -2
  44. package/runtime/python/okstra_ctl/convergence.py +143 -29
  45. package/runtime/python/okstra_ctl/convergence_critic_prompt.py +57 -0
  46. package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +106 -0
  47. package/runtime/python/okstra_ctl/convergence_engine.py +140 -95
  48. package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +3 -27
  49. package/runtime/python/okstra_ctl/convergence_store.py +11 -4
  50. package/runtime/python/okstra_ctl/dispatch_core.py +4 -1
  51. package/runtime/python/okstra_ctl/domain/role.py +1 -0
  52. package/runtime/python/okstra_ctl/manager_cli.py +27 -1
  53. package/runtime/python/okstra_ctl/manager_launch.py +44 -4
  54. package/runtime/python/okstra_ctl/manager_sync.py +14 -1
  55. package/runtime/python/okstra_ctl/manager_view.py +53 -1
  56. package/runtime/python/okstra_ctl/next_phase.py +1 -0
  57. package/runtime/python/okstra_ctl/phases/catalog.py +2 -1
  58. package/runtime/python/okstra_ctl/phases/discussion/__init__.py +1 -0
  59. package/runtime/python/okstra_ctl/phases/discussion/boundary.json +11 -0
  60. package/runtime/python/okstra_ctl/phases/discussion/cli.py +512 -0
  61. package/runtime/python/okstra_ctl/phases/discussion/entry.py +116 -0
  62. package/runtime/python/okstra_ctl/phases/discussion/followup.py +876 -0
  63. package/runtime/python/okstra_ctl/phases/discussion/memory.py +294 -0
  64. package/runtime/python/okstra_ctl/phases/discussion/overview.py +85 -0
  65. package/runtime/python/okstra_ctl/phases/discussion/profile.json +31 -0
  66. package/runtime/python/okstra_ctl/phases/discussion/profile.md +48 -0
  67. package/runtime/python/okstra_ctl/phases/discussion/report.py +73 -0
  68. package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.html +15 -0
  69. package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.md +6 -0
  70. package/runtime/python/okstra_ctl/phases/discussion/spec.md +197 -0
  71. package/runtime/python/okstra_ctl/phases/discussion/store.py +1143 -0
  72. package/runtime/python/okstra_ctl/phases/discussion/validation.py +111 -0
  73. package/runtime/python/okstra_ctl/phases/discussion/wizard.py +90 -0
  74. package/runtime/python/okstra_ctl/phases/discussion/workflow.py +667 -0
  75. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +1 -1
  76. package/runtime/python/okstra_ctl/phases/implementation_planning/host-rules.md +4 -1
  77. package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +1 -1
  78. package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +1 -1
  79. package/runtime/python/okstra_ctl/plan_run_root.py +8 -0
  80. package/runtime/python/okstra_ctl/render.py +7 -20
  81. package/runtime/python/okstra_ctl/render_final_report.py +2 -1
  82. package/runtime/python/okstra_ctl/report_assembly.py +22 -0
  83. package/runtime/python/okstra_ctl/report_contract.py +2 -0
  84. package/runtime/python/okstra_ctl/report_narrative.py +13 -4
  85. package/runtime/python/okstra_ctl/report_synthesis_packet.py +10 -0
  86. package/runtime/python/okstra_ctl/run.py +40 -7
  87. package/runtime/python/okstra_ctl/stage_close.py +46 -11
  88. package/runtime/python/okstra_ctl/stage_reopen.py +151 -0
  89. package/runtime/python/okstra_ctl/verdict_blocks.py +11 -35
  90. package/runtime/python/okstra_ctl/wizard/cli.py +8 -0
  91. package/runtime/python/okstra_ctl/wizard/confirmation.py +4 -1
  92. package/runtime/python/okstra_ctl/wizard/engine.py +8 -0
  93. package/runtime/python/okstra_ctl/wizard/ids.py +12 -0
  94. package/runtime/python/okstra_ctl/wizard/registry.py +35 -3
  95. package/runtime/python/okstra_ctl/wizard/render.py +4 -1
  96. package/runtime/python/okstra_ctl/wizard/sources.py +2 -2
  97. package/runtime/python/okstra_ctl/wizard/state.py +11 -1
  98. package/runtime/python/okstra_ctl/wizard/steps_identity.py +4 -4
  99. package/runtime/python/okstra_ctl/worker_prompt_policy.py +24 -6
  100. package/runtime/python/okstra_token_usage/pricing.py +2 -0
  101. package/runtime/schemas/convergence-groups-v1.0.schema.json +1 -1
  102. package/runtime/schemas/convergence-groups-v2.0.schema.json +1 -1
  103. package/runtime/schemas/final-report-v3.0.schema.json +90 -1
  104. package/runtime/skills/_fragments/wizard-bash-invocation-rule.md +1 -0
  105. package/runtime/skills/_fragments/wizard-confirm-step.md +7 -0
  106. package/runtime/skills/_fragments/wizard-lead-takeover.md +8 -0
  107. package/runtime/skills/_fragments/wizard-preflight.md +75 -0
  108. package/runtime/skills/_fragments/wizard-prompt-loop.md +22 -0
  109. package/runtime/skills/_fragments/wizard-relay-protocol.md +40 -0
  110. package/runtime/skills/_fragments/wizard-render-bundle.md +74 -0
  111. package/runtime/skills/okstra-discussion/SKILL.md +347 -0
  112. package/runtime/skills/okstra-pr-gen/SKILL.md +7 -0
  113. package/runtime/skills/okstra-run/SKILL.md +22 -2
  114. package/runtime/templates/manager/view.template.html +3 -0
  115. package/runtime/templates/reports/html/i18n/en.json +5 -0
  116. package/runtime/templates/reports/html/i18n/ko.json +5 -0
  117. package/runtime/validators/validate-brief.py +3 -2
  118. package/runtime/validators/validate-run.py +4 -6
@@ -5,6 +5,9 @@ import shlex
5
5
  from pathlib import Path
6
6
 
7
7
  from okstra_ctl.jsonl import append_jsonl, read_jsonl
8
+ from okstra_ctl.phases.discussion.followup import RECOMMENDED_PHASE as DISCUSSION_FOLLOW_UP_PHASE
9
+ from okstra_ctl.phases.discussion.followup import brief_revision_ref
10
+ from okstra_ctl.phases.discussion.store import same_task
8
11
 
9
12
  from .manager_paths import (
10
13
  MANAGER_CONTEXT_DIRECTIVE_PREFIX,
@@ -91,6 +94,24 @@ def _sibling_snapshots(snapshot: dict, target_task_key: str) -> list[dict]:
91
94
  ]
92
95
 
93
96
 
97
+ def _discussion_follow_up_brief(snapshot: dict, target_task_key: str) -> Path | None:
98
+ """`okstra discussion follow-up-brief` 가 이 하위 task 에 쓴 브리프.
99
+
100
+ 그 명령은 manager 상태에 쓰지 않는다(관리자 없는 토론도 있다). 같은 manager task 에 묶인
101
+ 토론의 마지막 sync 가 남긴 연결에서 위치만 찾고, 고정한 확정본은 실행 시점에 브리프에서 다시 읽는다.
102
+ """
103
+ for row in snapshot.get("children", []):
104
+ discussion = row.get("discussion") if isinstance(row, dict) else None
105
+ follow_ups = discussion.get("followUps") if isinstance(discussion, dict) else None
106
+ for link in (follow_ups or {}).get("items") or []:
107
+ if not same_task(str(link.get("targetTaskKey") or ""), target_task_key):
108
+ continue
109
+ candidate = Path(str(link.get("targetProjectRoot") or "")) / str(link.get("briefPath") or "")
110
+ if candidate.is_file() and brief_revision_ref(candidate) is not None:
111
+ return candidate
112
+ return None
113
+
114
+
94
115
  def _render_context(
95
116
  *,
96
117
  manifest: dict,
@@ -98,6 +119,7 @@ def _render_context(
98
119
  child_task_key: str,
99
120
  directives: list[dict],
100
121
  siblings: list[dict],
122
+ discussion_ref: dict | None,
101
123
  ) -> str:
102
124
  lines = [
103
125
  "# Okstra Manager Child Context",
@@ -123,6 +145,14 @@ def _render_context(
123
145
  f"- brief: {child.get('briefPath') or ''}", ""])
124
146
  lines.extend(f"- in scope: {item}" for item in child["scope"])
125
147
  lines.append("")
148
+ if discussion_ref:
149
+ lines.extend(["## Discussion Revision", "",
150
+ f"- discussion: {discussion_ref['discussionTaskKey']}",
151
+ f"- revision: {discussion_ref['revisionId']} ({discussion_ref['contentDigest']})",
152
+ f"- follow-up: {discussion_ref['followUp']}",
153
+ f"- requirements: {', '.join(discussion_ref['requirementIds'])}",
154
+ "- The brief pins this revision; a newer revision applies only when the user adopts it.",
155
+ ""])
126
156
  lines.extend(["## Manager Directives", ""])
127
157
  for row in directives:
128
158
  lines.append(f"- [{row.get('scope')}] {row.get('body')}")
@@ -186,6 +216,8 @@ def _record_launch_prepared(
186
216
  "workerDispatchBackend": WORKER_DISPATCH_BACKEND,
187
217
  "contextPath": packet["contextPath"],
188
218
  "runArgs": packet["runArgs"],
219
+ **({"discussionRevisionRef": packet["discussionRevisionRef"]}
220
+ if "discussionRevisionRef" in packet else {}),
189
221
  },
190
222
  )
191
223
 
@@ -213,6 +245,9 @@ def build_launch_packet(
213
245
  _validate_child_task_key(child, target_task_key)
214
246
  project_root = _validated_project_root(project)
215
247
  created_at = now or _now_iso()
248
+ brief_path = (Path(str(child["briefPath"])) if child.get("briefPath")
249
+ else _discussion_follow_up_brief(snapshot, target_task_key))
250
+ discussion_ref = brief_revision_ref(brief_path) if brief_path is not None and brief_path.is_file() else None
216
251
  context_path = child_context_path(home, manager_id, task_group, task_id, project_id, child_task_id_value)
217
252
  context_path.parent.mkdir(parents=True, exist_ok=True)
218
253
  context_path.write_text(
@@ -222,6 +257,7 @@ def build_launch_packet(
222
257
  child_task_key=target_task_key,
223
258
  directives=_directives_for_project(directives, project_id),
224
259
  siblings=_sibling_snapshots(snapshot, target_task_key),
260
+ discussion_ref=discussion_ref,
225
261
  ),
226
262
  encoding="utf-8",
227
263
  )
@@ -239,10 +275,12 @@ def build_launch_packet(
239
275
  # `task split` 이 기록한 브리프·권장 phase 를 넘겨 런처가 첫 실행에서 다시 묻지 않게 한다.
240
276
  # phase 는 하위 태스크가 아직 없을 때만 넘긴다. 이미 있으면 런처가 manifest 에서 다음
241
277
  # phase 를 채워야 하는데, 두 값이 다 주어지면 그 채우기를 건너뛴다(autofill_from_manifest).
242
- if child.get("recommendedPhase") and _resolve_task_root_read_only(Path(project_root), target_task_key) is None:
243
- run_args.extend(["--task-type", str(child["recommendedPhase"])])
244
- if child.get("briefPath"):
245
- run_args.extend(["--task-brief", str(child["briefPath"])])
278
+ # 토론 후속 브리프는 권장 phase 를 따로 적지 않아도 requirements-discovery 에서 시작한다.
279
+ recommended = child.get("recommendedPhase") or (DISCUSSION_FOLLOW_UP_PHASE if discussion_ref else "")
280
+ if recommended and _resolve_task_root_read_only(Path(project_root), target_task_key) is None:
281
+ run_args.extend(["--task-type", str(recommended)])
282
+ if brief_path is not None:
283
+ run_args.extend(["--task-brief", str(brief_path)])
246
284
  run_args += [
247
285
  "--directive",
248
286
  f"{MANAGER_CONTEXT_DIRECTIVE_PREFIX}{context_path}",
@@ -262,6 +300,8 @@ def build_launch_packet(
262
300
  "shellCommand": shlex.join([command, *run_args]),
263
301
  "createdAt": created_at,
264
302
  }
303
+ if discussion_ref:
304
+ packet["discussionRevisionRef"] = discussion_ref
265
305
  _record_launch_prepared(
266
306
  home=home,
267
307
  manager_id=manager_id,
@@ -7,6 +7,8 @@ from okstra_project import TASKS_RELATIVE, StateError, parse_task_key, read_task
7
7
 
8
8
  from . import next_phase
9
9
  from .next_phase import STATUS_BLOCKED, STATUS_TERMINAL
10
+ from .phases.discussion.overview import discussion_overview
11
+ from .phases.discussion.store import DiscussionStore
10
12
  from .manager_paths import children_json_path, projects_json_path, snapshots_json_path, task_manifest_path
11
13
  from .manager_store import _now_iso, _read_json, _read_json_default, _write_json
12
14
 
@@ -54,6 +56,9 @@ def _resolve_task_root_read_only(project_root: Path, task_key: str) -> Path | No
54
56
  return None
55
57
 
56
58
 
59
+ DISCUSSION_TASK_TYPE = "discussion"
60
+
61
+
57
62
  def _base_child_snapshot(child: dict) -> dict:
58
63
  task_key = str(child.get("taskKey") or "")
59
64
  return {
@@ -78,6 +83,10 @@ def _snapshot_child(project_root: Path, child: dict) -> dict:
78
83
  return {**base, "error": str(exc)}
79
84
  workflow = _workflow(manifest)
80
85
  outcome = _phase_outcome(manifest)
86
+ discussion = (
87
+ {"discussion": discussion_overview(DiscussionStore(project_root, task_key))}
88
+ if manifest.get("taskType") == DISCUSSION_TASK_TYPE else {}
89
+ )
81
90
  return {
82
91
  **base,
83
92
  "exists": True,
@@ -96,6 +105,7 @@ def _snapshot_child(project_root: Path, child: dict) -> dict:
96
105
  "crossProjectDependencies": _list_field(outcome, "crossProjectDependencies"),
97
106
  "recommendedNextSteps": _list_field(outcome, "recommendedNextSteps"),
98
107
  "openClarifications": _list_field(outcome, "openClarifications"),
108
+ **discussion,
99
109
  }
100
110
 
101
111
 
@@ -147,6 +157,9 @@ def summary_bucket(row: dict) -> str:
147
157
  """스냅샷이 있는 하위 task 한 줄을 요약 칸 하나로 분류한다."""
148
158
  if not row.get("exists", False):
149
159
  return "missing"
160
+ # 토론 완료는 후속 작업이나 프로젝트의 완료가 아니다 — 토론 task 는 따로 센다.
161
+ if row.get("taskType") == DISCUSSION_TASK_TYPE:
162
+ return "discussion"
150
163
  work_status = str(row.get("workStatus") or "")
151
164
  if work_status in _WORK_STATUS_BUCKETS:
152
165
  return _WORK_STATUS_BUCKETS[work_status]
@@ -162,7 +175,7 @@ def status_task(home: Path, manager_id: str, task_group: str, task_id: str) -> d
162
175
  by_key = {row.get("taskKey"): row for row in snapshot.get("children", []) if isinstance(row, dict)}
163
176
  linked = _project_map(home, manager_id)
164
177
  rows = []
165
- summary = {"planned": 0, "missing": 0, "running": 0, "done": 0, "blocked": 0}
178
+ summary = {"planned": 0, "missing": 0, "running": 0, "done": 0, "blocked": 0, "discussion": 0}
166
179
  for child in children.get("children", []):
167
180
  if not isinstance(child, dict):
168
181
  continue
@@ -22,7 +22,7 @@ from .paths import find_asset_root
22
22
 
23
23
  TEMPLATE_RELATIVE = ("templates", "manager", "view.template.html")
24
24
  _MARKER = re.compile(r"\{\{([A-Z]+)\}\}")
25
- BUCKETS = ("planned", "missing", "running", "done", "blocked")
25
+ BUCKETS = ("planned", "missing", "running", "done", "blocked", "discussion")
26
26
 
27
27
 
28
28
  def _now_iso() -> str:
@@ -90,6 +90,54 @@ def _projects_table(projects: list[dict]) -> str:
90
90
  return f'<div class="projects">{table}</div>'
91
91
 
92
92
 
93
+ def _discussion_fields(discussion: dict) -> tuple[tuple[str, str], ...]:
94
+ if discussion.get("state") == "unknown":
95
+ return (("Discussion", "unknown"),)
96
+ questions = discussion.get("pendingQuestions") or {}
97
+ state = discussion.get("completedRevision") if discussion.get("state") == "completed" else (
98
+ f"draft ({discussion.get('phase') or '-'})")
99
+ return (
100
+ ("Discussion", state or "-"),
101
+ ("Pending questions", f"{len(questions.get('forUser') or [])} for the user · "
102
+ f"{len(questions.get('forParticipants') or [])} for participants"),
103
+ )
104
+
105
+
106
+ def _discussion_detail(discussion: dict) -> str:
107
+ """토론 원본의 식별자·상태만 보여 준다. 마지막 sync 시점의 값이다."""
108
+ if discussion.get("state") == "unknown":
109
+ return f'<p class="child-error"><strong>Discussion unknown:</strong> {_e(discussion.get("reason"))}</p>'
110
+ questions = discussion.get("pendingQuestions") or {}
111
+ pending = [f"{_e(row.get('recordId'))} → user: {_e(row.get('text'))}" for row in questions.get("forUser") or []]
112
+ pending += [f"{_e(row.get('recordId'))} → {_e(row.get('to'))}: {_e(row.get('text'))}"
113
+ for row in questions.get("forParticipants") or []]
114
+ revisions = [
115
+ f"<code>{_e(row.get('revisionId'))}</code> · "
116
+ + (f"unknown — {_e(row.get('reason'))}" if row.get("state") == "unknown"
117
+ else f"change reason: {_e(row.get('changeReason'))}" if row.get("changeReason") else "first revision")
118
+ for row in discussion.get("revisions") or []
119
+ ]
120
+ reopened = discussion.get("reopenedFrom")
121
+ if reopened:
122
+ revisions.append(f"re-discussing <code>{_e(reopened.get('revisionId'))}</code>: {_e(reopened.get('reason'))}")
123
+ links = discussion.get("followUps") or {}
124
+ follow_ups = [
125
+ f"<code>{_e(row.get('targetTaskKey'))}</code> ({_e(row.get('followUp'))}) · {_e(row.get('state'))} · "
126
+ f"uses {_e(row.get('pinnedRevision') or '-')}"
127
+ + (f" · {_e(row.get('reason'))}" if row.get("reason") else "")
128
+ for row in links.get("items") or []
129
+ ]
130
+ follow_ups += [f"{_e(label)} · not linked" for label in links.get("unlinked") or []]
131
+ if links.get("state") == "unknown":
132
+ follow_ups = [f"unknown — {_e(links.get('reason'))}"]
133
+ sections = (("Pending questions", pending), ("Revisions", revisions), ("Follow-ups", follow_ups))
134
+ body = "".join(
135
+ f"<dt>{_e(label)}</dt><dd>" + ("<br>".join(items) if items else "-") + "</dd>"
136
+ for label, items in sections
137
+ )
138
+ return f'<dl class="child-status">{body}</dl>'
139
+
140
+
93
141
  def _child_rows(children: list[dict], roots: dict[str, str]) -> list[list[str]]:
94
142
  rows = []
95
143
  for child in children:
@@ -105,6 +153,9 @@ def _child_rows(children: list[dict], roots: dict[str, str]) -> list[list[str]]:
105
153
  ("Verdict", child.get("finalVerdict") or "-"),
106
154
  ("Launch", launch.get("status") or "-"),
107
155
  )
156
+ discussion = child.get("discussion") if isinstance(child.get("discussion"), dict) else None
157
+ if discussion is not None:
158
+ status_fields += _discussion_fields(discussion)
108
159
  status = "".join(f"<dt>{_e(label)}</dt><dd>{_e(value)}</dd>" for label, value in status_fields)
109
160
  links = "".join(
110
161
  _file_link(root, str(path), label)
@@ -122,6 +173,7 @@ def _child_rows(children: list[dict], roots: dict[str, str]) -> list[list[str]]:
122
173
  + ("" if child.get("projectLinked", True) else " " + _chip("unlinked")),
123
174
  _chip(str(child.get("summaryBucket") or "planned")) + f'<dl class="child-status">{status}</dl>',
124
175
  f'<div class="child-assignment">{_e(assignment or "-")}</div>'
176
+ + (_discussion_detail(discussion) if discussion is not None else "")
125
177
  + error + (f'<div class="child-links">{links}</div>' if links else ""),
126
178
  ]
127
179
  )
@@ -45,6 +45,7 @@ _SIDETRACK_TASK_TYPES = frozenset(
45
45
  "project-analysis",
46
46
  "feature-analysis",
47
47
  "change-impact-analysis",
48
+ "discussion",
48
49
  }
49
50
  )
50
51
 
@@ -1,4 +1,4 @@
1
- """12개 작업 유형과 단계 디렉터리의 고정 대응.
1
+ """13개 작업 유형과 단계 디렉터리의 고정 대응.
2
2
 
3
3
  이 모듈을 import 해도 단계 실행 모듈은 적재되지 않는다. 경로는 문자열로만
4
4
  계산한다. 외부 설정에서 모듈 경로를 받아 실행하지 않는다.
@@ -68,6 +68,7 @@ PHASES: tuple[PhaseRecord, ...] = (
68
68
  _record("implementation", "implementation", True, "build_implementation_view"),
69
69
  _record("final-verification", "final_verification", True, "build_final_verification_view"),
70
70
  _record("release-handoff", "release_handoff", True, "build_release_handoff_view"),
71
+ _record("discussion", "discussion", True, "build_discussion_view"),
71
72
  )
72
73
 
73
74
  _BY_TASK_TYPE = {row.task_type: row for row in PHASES}
@@ -0,0 +1 @@
1
+ """토론 단계의 역할 구성과 출력 경계."""
@@ -0,0 +1,11 @@
1
+ {
2
+ "allowed": " - research of the discussion subject with evidence, assumptions and alternative directions\n - adversarial cross-checking of factual claims between participants\n - questions put to the user one at a time, and the user's answers and decisions recorded as user decisions\n - remaining disagreements, risks and unverified scope presented for the user's choice\n - confirmed requirements, scope, exclusions and implementation direction once the user confirms completion",
3
+ "forbidden": [
4
+ "source code edits of any kind",
5
+ "detailed implementation plans, file-change specifications or stage maps — those belong to a follow-up task",
6
+ "executing builds, migrations, deployments, or any state-mutating command",
7
+ "treating agent consensus, a convergence stop or a finished worker batch as the user's completion confirmation",
8
+ "deciding the user's intent or priority from a fact check",
9
+ "starting a follow-up task's run or any other lifecycle phase inside this run, or editing a follow-up task's plan or source"
10
+ ]
11
+ }