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.
- package/README.md +2 -1
- package/dist/cli-registry.d.mts +14 -0
- package/dist/cli-registry.mjs +12 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/chat/chat.mjs +2 -56
- package/dist/commands/chat/chat.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +5 -2
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/wizard.mjs +6 -0
- package/dist/commands/execute/wizard.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +57 -6
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/file-lock.d.mts +1 -0
- package/dist/lib/file-lock.mjs +60 -0
- package/dist/lib/file-lock.mjs.map +1 -0
- package/dist/lib/python-command.mjs +11 -1
- package/dist/lib/python-command.mjs.map +1 -1
- package/dist/lib/skill-catalog.mjs +1 -0
- package/dist/lib/skill-catalog.mjs.map +1 -1
- package/dist/lib/types.d.mts +1 -1
- package/docs/architecture.md +2 -2
- package/docs/cli.md +36 -14
- package/docs/project-structure-overview.md +6 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +9 -2
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/interactive.sh +8 -2
- package/runtime/bin/lib/okstra/usage.sh +3 -1
- package/runtime/bin/okstra.sh +7 -1
- package/runtime/prompts/duties/discussion-worker.json +32 -0
- package/runtime/prompts/lead/convergence.md +58 -136
- package/runtime/prompts/lead/okstra-lead-contract.md +45 -12
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +14 -1
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +4 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +4 -11
- package/runtime/python/okstra_ctl/agent/invocation.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +2 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +4 -4
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +2 -2
- package/runtime/python/okstra_ctl/analysis_packet.py +3 -0
- package/runtime/python/okstra_ctl/consumers.py +8 -2
- package/runtime/python/okstra_ctl/convergence.py +143 -29
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +57 -0
- package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +106 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +140 -95
- package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +3 -27
- package/runtime/python/okstra_ctl/convergence_store.py +11 -4
- package/runtime/python/okstra_ctl/dispatch_core.py +4 -1
- package/runtime/python/okstra_ctl/domain/role.py +1 -0
- package/runtime/python/okstra_ctl/manager_cli.py +27 -1
- package/runtime/python/okstra_ctl/manager_launch.py +44 -4
- package/runtime/python/okstra_ctl/manager_sync.py +14 -1
- package/runtime/python/okstra_ctl/manager_view.py +53 -1
- package/runtime/python/okstra_ctl/next_phase.py +1 -0
- package/runtime/python/okstra_ctl/phases/catalog.py +2 -1
- package/runtime/python/okstra_ctl/phases/discussion/__init__.py +1 -0
- package/runtime/python/okstra_ctl/phases/discussion/boundary.json +11 -0
- package/runtime/python/okstra_ctl/phases/discussion/cli.py +512 -0
- package/runtime/python/okstra_ctl/phases/discussion/entry.py +116 -0
- package/runtime/python/okstra_ctl/phases/discussion/followup.py +876 -0
- package/runtime/python/okstra_ctl/phases/discussion/memory.py +294 -0
- package/runtime/python/okstra_ctl/phases/discussion/overview.py +85 -0
- package/runtime/python/okstra_ctl/phases/discussion/profile.json +31 -0
- package/runtime/python/okstra_ctl/phases/discussion/profile.md +48 -0
- package/runtime/python/okstra_ctl/phases/discussion/report.py +73 -0
- package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.html +15 -0
- package/runtime/python/okstra_ctl/phases/discussion/report_assets/discussion.template.md +6 -0
- package/runtime/python/okstra_ctl/phases/discussion/spec.md +197 -0
- package/runtime/python/okstra_ctl/phases/discussion/store.py +1143 -0
- package/runtime/python/okstra_ctl/phases/discussion/validation.py +111 -0
- package/runtime/python/okstra_ctl/phases/discussion/wizard.py +90 -0
- package/runtime/python/okstra_ctl/phases/discussion/workflow.py +667 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/host-rules.md +4 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +1 -1
- package/runtime/python/okstra_ctl/plan_run_root.py +8 -0
- package/runtime/python/okstra_ctl/render.py +7 -20
- package/runtime/python/okstra_ctl/render_final_report.py +2 -1
- package/runtime/python/okstra_ctl/report_assembly.py +22 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -0
- package/runtime/python/okstra_ctl/report_narrative.py +13 -4
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +10 -0
- package/runtime/python/okstra_ctl/run.py +40 -7
- package/runtime/python/okstra_ctl/stage_close.py +46 -11
- package/runtime/python/okstra_ctl/stage_reopen.py +151 -0
- package/runtime/python/okstra_ctl/verdict_blocks.py +11 -35
- package/runtime/python/okstra_ctl/wizard/cli.py +8 -0
- package/runtime/python/okstra_ctl/wizard/confirmation.py +4 -1
- package/runtime/python/okstra_ctl/wizard/engine.py +8 -0
- package/runtime/python/okstra_ctl/wizard/ids.py +12 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +35 -3
- package/runtime/python/okstra_ctl/wizard/render.py +4 -1
- package/runtime/python/okstra_ctl/wizard/sources.py +2 -2
- package/runtime/python/okstra_ctl/wizard/state.py +11 -1
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +4 -4
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +24 -6
- package/runtime/python/okstra_token_usage/pricing.py +2 -0
- package/runtime/schemas/convergence-groups-v1.0.schema.json +1 -1
- package/runtime/schemas/convergence-groups-v2.0.schema.json +1 -1
- package/runtime/schemas/final-report-v3.0.schema.json +90 -1
- package/runtime/skills/_fragments/wizard-bash-invocation-rule.md +1 -0
- package/runtime/skills/_fragments/wizard-confirm-step.md +7 -0
- package/runtime/skills/_fragments/wizard-lead-takeover.md +8 -0
- package/runtime/skills/_fragments/wizard-preflight.md +75 -0
- package/runtime/skills/_fragments/wizard-prompt-loop.md +22 -0
- package/runtime/skills/_fragments/wizard-relay-protocol.md +40 -0
- package/runtime/skills/_fragments/wizard-render-bundle.md +74 -0
- package/runtime/skills/okstra-discussion/SKILL.md +347 -0
- package/runtime/skills/okstra-pr-gen/SKILL.md +7 -0
- package/runtime/skills/okstra-run/SKILL.md +22 -2
- package/runtime/templates/manager/view.template.html +3 -0
- package/runtime/templates/reports/html/i18n/en.json +5 -0
- package/runtime/templates/reports/html/i18n/ko.json +5 -0
- package/runtime/validators/validate-brief.py +3 -2
- 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
|
-
|
|
243
|
-
|
|
244
|
-
if
|
|
245
|
-
run_args.extend(["--task-
|
|
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
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
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
|
+
}
|