okstra 0.201.3 → 0.202.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/dist/commands/lifecycle/setup.mjs +15 -0
- package/dist/commands/lifecycle/setup.mjs.map +1 -1
- package/dist/lib/citation-guidance.d.mts +21 -0
- package/dist/lib/citation-guidance.mjs +79 -0
- package/dist/lib/citation-guidance.mjs.map +1 -0
- package/docs/architecture/storage-model.md +4 -0
- package/docs/architecture.md +1 -1
- package/docs/cli.md +2 -2
- package/docs/for-ai/skills/okstra-manager.md +21 -4
- package/docs/for-ai/skills/okstra-setup.md +9 -0
- package/docs/project-structure-overview.md +4 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +2 -0
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +4 -6
- package/runtime/python/okstra_ctl/convergence_provenance.py +75 -18
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +21 -21
- package/runtime/python/okstra_ctl/manager_cli.py +85 -12
- package/runtime/python/okstra_ctl/manager_launch.py +40 -18
- package/runtime/python/okstra_ctl/manager_paths.py +8 -0
- package/runtime/python/okstra_ctl/manager_split.py +474 -0
- package/runtime/python/okstra_ctl/manager_store.py +121 -18
- package/runtime/python/okstra_ctl/manager_sync.py +33 -15
- package/runtime/python/okstra_ctl/manager_view.py +216 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +6 -1
- package/runtime/python/okstra_ctl/qa_commands.py +15 -0
- package/runtime/python/okstra_ctl/report_finalize.py +13 -6
- package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +2 -21
- package/runtime/python/okstra_ctl/report_html/visualizations.py +0 -5
- package/runtime/python/okstra_ctl/verification_target.py +13 -2
- package/runtime/skills/okstra-manager/SKILL.md +53 -4
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-setup/SKILL.md +9 -0
- package/runtime/templates/manager/view.template.html +108 -0
- package/runtime/templates/reports/html/i18n/en.json +2 -0
- package/runtime/templates/reports/html/i18n/ko.json +2 -0
- package/runtime/templates/reports/html/tasks/final-verification.template.html +2 -2
- package/runtime/validators/validate-brief.py +7 -2
|
@@ -122,6 +122,7 @@ class MutationAuditResult:
|
|
|
122
122
|
changed_artifact_paths: tuple[str, ...] = ()
|
|
123
123
|
declared_out_of_plan_paths: tuple[str, ...] = ()
|
|
124
124
|
unobserved_artifact_paths: tuple[str, ...] = ()
|
|
125
|
+
external_workspace_changes: tuple[str, ...] = ()
|
|
125
126
|
|
|
126
127
|
def change_summary(self) -> dict[str, Any]:
|
|
127
128
|
return {
|
|
@@ -138,6 +139,7 @@ class MutationAuditResult:
|
|
|
138
139
|
"changedArtifactPaths": list(self.changed_artifact_paths),
|
|
139
140
|
"declaredOutOfPlanPaths": list(self.declared_out_of_plan_paths),
|
|
140
141
|
"unobservedArtifactPaths": list(self.unobserved_artifact_paths),
|
|
142
|
+
"externalWorkspaceChanges": list(self.external_workspace_changes),
|
|
141
143
|
"beforeDigest": self.before_digest,
|
|
142
144
|
"afterDigest": self.after_digest,
|
|
143
145
|
}
|
|
@@ -240,13 +242,13 @@ class ExecutionMutationAudit:
|
|
|
240
242
|
rows,
|
|
241
243
|
source_changes,
|
|
242
244
|
)
|
|
243
|
-
artifact_failures, untracked_artifact_changes, switched = (
|
|
245
|
+
artifact_failures, untracked_artifact_changes, switched, external = (
|
|
244
246
|
_artifact_policy_failures(before, rows, artifact_changed, after=after)
|
|
245
247
|
)
|
|
246
248
|
violations.extend(artifact_failures)
|
|
247
249
|
worker_artifact_changes = {
|
|
248
250
|
path for path in artifact_changed
|
|
249
|
-
if path not in untracked_artifact_changes and path not in switched
|
|
251
|
+
if path not in untracked_artifact_changes and path not in switched and path not in external
|
|
250
252
|
and not any(_is_relative_to(before.artifact_root / path, Path(item))
|
|
251
253
|
for item in before.orchestrator_paths)
|
|
252
254
|
}
|
|
@@ -276,11 +278,14 @@ class ExecutionMutationAudit:
|
|
|
276
278
|
untracked_artifact_paths=tuple(sorted(untracked_artifact_changes)),
|
|
277
279
|
warnings=_audit_warnings(untracked_artifact_changes)
|
|
278
280
|
+ _branch_switch_warnings(before, after, switched)
|
|
281
|
+
+ tuple(f"unattributed change outside assigned worktree and artifact paths: {path}"
|
|
282
|
+
for path in sorted(external))
|
|
279
283
|
+ _plan_change_warnings(rows, source_changes | untracked_changes,
|
|
280
284
|
artifact_changed, out_of_plan_edits, unobserved),
|
|
281
|
-
changed_artifact_paths=tuple(sorted(artifact_changed)),
|
|
285
|
+
changed_artifact_paths=tuple(sorted(artifact_changed - external)),
|
|
282
286
|
declared_out_of_plan_paths=tuple(out_of_plan_edits),
|
|
283
287
|
unobserved_artifact_paths=tuple(sorted(unobserved)),
|
|
288
|
+
external_workspace_changes=tuple(sorted(external)),
|
|
284
289
|
)
|
|
285
290
|
|
|
286
291
|
|
|
@@ -753,22 +758,12 @@ def _artifact_policy_failures(
|
|
|
753
758
|
changed: set[str],
|
|
754
759
|
*,
|
|
755
760
|
after: MutationSnapshot | None = None,
|
|
756
|
-
) -> tuple[list[str], set[str], set[str]]:
|
|
757
|
-
"""
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
`_split_tracked` 와 같은 이유로 기록만 한다. artifact root 가 git 레포가
|
|
763
|
-
아니면 추적 여부를 알 수 없으므로 종전대로 전부 위반으로 본다.
|
|
764
|
-
|
|
765
|
-
추적 파일의 변경 중 **artifact root 의 HEAD 가 실행 창 안에서 옮겨졌고 그
|
|
766
|
-
두 커밋 사이에서 실제로 달라지는 경로**는 워커의 쓰기가 아니라 사람의 브랜치
|
|
767
|
-
전환이다. 실측(2026-09-09, `fontsninja-v3-site` dev-10627 reverify r1b): 워커가
|
|
768
|
-
워크트리에서 읽기만 하는 8분 동안 프로젝트 루트에서 `checkout preprod →
|
|
769
|
-
rebase` 가 있었고, run 브랜치에만 있는 `CardHero.{tsx,styled.ts}` 가 사라져
|
|
770
|
-
완주한 결과가 `contract-failed-unattributed` 로 폐기됐다. 그 경로는 위반에서
|
|
771
|
-
빼고 경고로 남긴다. 전환으로 설명되지 않는 추적 파일 변경은 그대로 위반이다.
|
|
761
|
+
) -> tuple[list[str], set[str], set[str], set[str]]:
|
|
762
|
+
"""위반·비추적 파일·브랜치 전환·외부 작업공간 관측을 분리한다.
|
|
763
|
+
|
|
764
|
+
작업트리와 기본 체크아웃이 다르면 기본 체크아웃 소스의 변경 주체는
|
|
765
|
+
전후 비교로 판단할 수 없다. 배정된 산출물 밖의 변경은 관측으로 남기고,
|
|
766
|
+
`.okstra` 내부의 허용 밖 쓰기와 배정된 소스 정책은 별도로 검사한다.
|
|
772
767
|
"""
|
|
773
768
|
allowed = _allowed_artifact_paths(policies)
|
|
774
769
|
orchestrator = {
|
|
@@ -799,9 +794,14 @@ def _artifact_policy_failures(
|
|
|
799
794
|
if path in unauthorized and not _is_within(path, _OKSTRA_ARTIFACT_SUBTREE)
|
|
800
795
|
}
|
|
801
796
|
untracked_outside -= switched
|
|
802
|
-
|
|
797
|
+
external = {
|
|
798
|
+
path for path in unauthorized - untracked_outside - switched
|
|
799
|
+
if snapshot.artifact_root != snapshot.root
|
|
800
|
+
and not _is_within(path, _OKSTRA_ARTIFACT_SUBTREE)
|
|
801
|
+
}
|
|
802
|
+
violating = unauthorized - untracked_outside - switched - external
|
|
803
803
|
failures = ["artifact-root change exceeds batch policy union"] if violating else []
|
|
804
|
-
return failures, untracked_outside, switched
|
|
804
|
+
return failures, untracked_outside, switched, external
|
|
805
805
|
|
|
806
806
|
|
|
807
807
|
def _branch_switch_paths(
|
|
@@ -10,6 +10,7 @@ from okstra_project.dirs import okstra_home
|
|
|
10
10
|
|
|
11
11
|
from .manager_launch import build_launch_packet
|
|
12
12
|
from .fixed_text import line, value_lines
|
|
13
|
+
from .manager_paths import ManagerPathError
|
|
13
14
|
from .manager_store import (
|
|
14
15
|
ManagerError,
|
|
15
16
|
append_directive,
|
|
@@ -19,20 +20,40 @@ from .manager_store import (
|
|
|
19
20
|
discover_projects,
|
|
20
21
|
init_manager,
|
|
21
22
|
link_project,
|
|
23
|
+
list_managers,
|
|
24
|
+
list_projects,
|
|
25
|
+
list_tasks,
|
|
22
26
|
)
|
|
27
|
+
from .manager_split import split_task
|
|
23
28
|
from .manager_sync import status_task, sync_task
|
|
29
|
+
from .manager_view import write_view
|
|
30
|
+
|
|
31
|
+
# list 명령마다 목록 한 줄에 싣는 (라벨, 키). 라벨은 "<접두어> <번호> <라벨>" 로 찍힌다.
|
|
32
|
+
_LIST_ROWS = {
|
|
33
|
+
"discover-projects": ("Project", (("ID", "projectId"), ("root", "projectRoot"),
|
|
34
|
+
("run count", "runCount"), ("active count", "activeCount"),
|
|
35
|
+
("last run at", "lastRunAt"))),
|
|
36
|
+
"list-managers": ("Manager", (("ID", "managerId"), ("created at", "createdAt"),
|
|
37
|
+
("project count", "projectCount"), ("task count", "taskCount"))),
|
|
38
|
+
"list-projects": ("Project", (("ID", "projectId"), ("root", "projectRoot"), ("role", "role"),
|
|
39
|
+
("tags", "tags"), ("linked at", "linkedAt"))),
|
|
40
|
+
"list-tasks": ("Task", (("group", "taskGroup"), ("ID", "taskId"), ("objective", "objective"),
|
|
41
|
+
("progress mode", "progressMode"), ("child count", "childCount"),
|
|
42
|
+
("created at", "createdAt"))),
|
|
43
|
+
}
|
|
24
44
|
|
|
25
45
|
|
|
26
46
|
def render_manager_text(command: str, payload: dict | list[dict]) -> str:
|
|
27
47
|
"""manager 모델 표면에 승인된 식별자와 상태만 투영한다."""
|
|
28
48
|
allowed = {"init", "discover-projects", "new-project", "new-task-group", "new-task",
|
|
29
|
-
"task-assign", "task-note", "task-sync", "task-status", "task-run"
|
|
49
|
+
"task-assign", "task-note", "task-sync", "task-status", "task-run", "task-split",
|
|
50
|
+
"list-managers", "list-projects", "list-tasks", "view"}
|
|
30
51
|
if command not in allowed:
|
|
31
52
|
raise ValueError("unknown manager text purpose")
|
|
32
53
|
rows = [f"Okstra manager {command}\n", line("Status", "ready")]
|
|
33
54
|
if isinstance(payload, list):
|
|
34
55
|
rows.append(line("Result count", len(payload)))
|
|
35
|
-
rows.extend(
|
|
56
|
+
rows.extend(_render_list(command, payload))
|
|
36
57
|
return "".join(rows)
|
|
37
58
|
if command == "task-status":
|
|
38
59
|
rows.extend(_render_manager_status(payload))
|
|
@@ -44,7 +65,9 @@ def render_manager_text(command: str, payload: dict | list[dict]) -> str:
|
|
|
44
65
|
("Work status", "workStatus"), ("Run command", "command"),
|
|
45
66
|
("Backend", "backend"),
|
|
46
67
|
("Worker dispatch backend", "workerDispatchBackend"),
|
|
47
|
-
("Project root", "projectRoot"), ("Context path", "contextPath")
|
|
68
|
+
("Project root", "projectRoot"), ("Context path", "contextPath"),
|
|
69
|
+
("Shell command", "shellCommand"), ("View path", "viewPath"),
|
|
70
|
+
("View URL", "viewUrl")):
|
|
48
71
|
if key in payload:
|
|
49
72
|
rows.append(line(label, payload.get(key)))
|
|
50
73
|
run_args = payload.get("runArgs")
|
|
@@ -64,20 +87,27 @@ def render_manager_text(command: str, payload: dict | list[dict]) -> str:
|
|
|
64
87
|
("Directive body", "body"), ("Created at", "createdAt"),
|
|
65
88
|
("Source", "source")):
|
|
66
89
|
rows.append(line(label, payload.get(key)))
|
|
90
|
+
if command == "task-split":
|
|
91
|
+
briefs = payload.get("briefs") if isinstance(payload.get("briefs"), list) else []
|
|
92
|
+
rows.append(line("Brief count", len(briefs)))
|
|
93
|
+
for index, brief in enumerate(briefs, 1):
|
|
94
|
+
for label, key in (("task key", "taskKey"), ("project ID", "projectId"), ("ticket", "ticketId"),
|
|
95
|
+
("path", "briefPath"), ("status", "briefStatus")):
|
|
96
|
+
rows.append(line(f"Brief {index} {label}", brief.get(key)))
|
|
67
97
|
if command == "task-sync" and "syncedAt" in payload:
|
|
68
98
|
rows.append(line("Synced at", payload.get("syncedAt")))
|
|
69
99
|
return "".join(rows)
|
|
70
100
|
|
|
71
101
|
|
|
72
|
-
def
|
|
102
|
+
def _render_list(command: str, items: list[dict]) -> list[str]:
|
|
103
|
+
prefix, fields = _LIST_ROWS[command]
|
|
73
104
|
rows: list[str] = []
|
|
74
|
-
for index,
|
|
75
|
-
if not isinstance(
|
|
105
|
+
for index, item in enumerate(items, 1):
|
|
106
|
+
if not isinstance(item, dict):
|
|
76
107
|
continue
|
|
77
|
-
for label, key in
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
rows.append(line(f"Project {index} {label}", project.get(key)))
|
|
108
|
+
for label, key in fields:
|
|
109
|
+
value = item.get(key)
|
|
110
|
+
rows.append(line(f"{prefix} {index} {label}", ", ".join(value) if isinstance(value, list) else value))
|
|
81
111
|
return rows
|
|
82
112
|
|
|
83
113
|
|
|
@@ -96,6 +126,7 @@ def _render_manager_status(payload: dict) -> list[str]:
|
|
|
96
126
|
continue
|
|
97
127
|
for label, key in (("task key", "taskKey"), ("run status", "latestRunStatus"),
|
|
98
128
|
("current phase", "currentPhase"), ("work status", "workStatus"),
|
|
129
|
+
("summary", "summaryBucket"), ("ticket", "ticketId"), ("brief path", "briefPath"),
|
|
99
130
|
("report path", "latestReportRecordPath"), ("error", "error")):
|
|
100
131
|
if key in child:
|
|
101
132
|
rows.append(line(f"Child {index} {label}", child.get(key)))
|
|
@@ -156,6 +187,11 @@ _CLI_EPILOG = r"""Usage:
|
|
|
156
187
|
okstra manager task assign --manager-id <id> --task-group <task-group> --task-id <task-id> --project-id <project-id>
|
|
157
188
|
okstra manager task note --manager-id <id> --task-group <task-group> --task-id <task-id> --scope <shared|project> --body <text>
|
|
158
189
|
okstra manager task run --manager-id <id> --project-id <project-id> --task-group <task-group> --task-id <task-id>
|
|
190
|
+
okstra manager task split --manager-id <id> --task-group <task-group> --task-id <task-id> --plan <split-plan.json> [--overwrite]
|
|
191
|
+
okstra manager list managers
|
|
192
|
+
okstra manager list projects --manager-id <id>
|
|
193
|
+
okstra manager list tasks --manager-id <id>
|
|
194
|
+
okstra manager view --manager-id <id>
|
|
159
195
|
|
|
160
196
|
--workspace-root is owned by this command.
|
|
161
197
|
"""
|
|
@@ -198,7 +234,7 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
198
234
|
task_new.add_argument("--task", action="append", default=[])
|
|
199
235
|
task_new.add_argument("--objective", default="")
|
|
200
236
|
task_new.add_argument("--common-brief", default="")
|
|
201
|
-
task_new.add_argument("--progress-mode", choices=["manual", "auto"], default=
|
|
237
|
+
task_new.add_argument("--progress-mode", choices=["manual", "auto"], default=None)
|
|
202
238
|
|
|
203
239
|
task = sub.add_parser("task")
|
|
204
240
|
task_sub = task.add_subparsers(dest="task_command", required=True)
|
|
@@ -233,6 +269,22 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
233
269
|
run.add_argument("--task-group", required=True)
|
|
234
270
|
run.add_argument("--task-id", required=True)
|
|
235
271
|
run.add_argument("--child-task-id", default=None)
|
|
272
|
+
|
|
273
|
+
split = task_sub.add_parser("split")
|
|
274
|
+
split.add_argument("--manager-id", required=True)
|
|
275
|
+
split.add_argument("--task-group", required=True)
|
|
276
|
+
split.add_argument("--task-id", required=True)
|
|
277
|
+
split.add_argument("--plan", required=True)
|
|
278
|
+
split.add_argument("--overwrite", action="store_true")
|
|
279
|
+
|
|
280
|
+
listing = sub.add_parser("list")
|
|
281
|
+
list_sub = listing.add_subparsers(dest="list_command", required=True)
|
|
282
|
+
list_sub.add_parser("managers")
|
|
283
|
+
for name in ("projects", "tasks"):
|
|
284
|
+
list_sub.add_parser(name).add_argument("--manager-id", required=True)
|
|
285
|
+
|
|
286
|
+
view = sub.add_parser("view")
|
|
287
|
+
view.add_argument("--manager-id", required=True)
|
|
236
288
|
return parser
|
|
237
289
|
|
|
238
290
|
|
|
@@ -322,11 +374,32 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
322
374
|
),
|
|
323
375
|
args.json,
|
|
324
376
|
)
|
|
377
|
+
elif args.command == "task" and args.task_command == "split":
|
|
378
|
+
_emit(
|
|
379
|
+
"task-split",
|
|
380
|
+
split_task(
|
|
381
|
+
home,
|
|
382
|
+
args.manager_id,
|
|
383
|
+
args.task_group,
|
|
384
|
+
args.task_id,
|
|
385
|
+
Path(args.plan),
|
|
386
|
+
overwrite=args.overwrite,
|
|
387
|
+
),
|
|
388
|
+
args.json,
|
|
389
|
+
)
|
|
390
|
+
elif args.command == "list" and args.list_command == "managers":
|
|
391
|
+
_emit("list-managers", list_managers(home), args.json)
|
|
392
|
+
elif args.command == "list" and args.list_command == "projects":
|
|
393
|
+
_emit("list-projects", list_projects(home, args.manager_id), args.json)
|
|
394
|
+
elif args.command == "list" and args.list_command == "tasks":
|
|
395
|
+
_emit("list-tasks", list_tasks(home, args.manager_id), args.json)
|
|
396
|
+
elif args.command == "view":
|
|
397
|
+
_emit("view", write_view(home, args.manager_id), args.json)
|
|
325
398
|
else:
|
|
326
399
|
parser.print_help()
|
|
327
400
|
return 2
|
|
328
401
|
return 0
|
|
329
|
-
except ManagerError as exc:
|
|
402
|
+
except (ManagerError, ManagerPathError) as exc:
|
|
330
403
|
print(f"okstra manager: {exc}", file=sys.stderr)
|
|
331
404
|
return 2
|
|
332
405
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Build manager child launch packets and context documents."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import shlex
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from okstra_ctl.jsonl import append_jsonl, read_jsonl
|
|
@@ -15,12 +16,16 @@ from .manager_paths import (
|
|
|
15
16
|
task_manifest_path,
|
|
16
17
|
)
|
|
17
18
|
from .manager_store import ManagerError, _now_iso, _read_json, _read_json_default, _write_json
|
|
19
|
+
from .manager_sync import _resolve_task_root_read_only
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
WORKER_DISPATCH_BACKEND = "subagent"
|
|
21
|
-
# child lead 는
|
|
22
|
-
#
|
|
23
|
-
|
|
23
|
+
# child lead 는 설치본 런처(`okstra.sh`)가 새 host 프로세스로 띄운다. `okstra run`
|
|
24
|
+
# 은 lead host 에 `--launch-only` 를 붙여 task 인자와 `--directive` 를 버리므로
|
|
25
|
+
# child 맥락이 전달되지 않는다(run.mts 의 launch-lead 분기). 런처는 prepare 까지
|
|
26
|
+
# 거친 뒤 `launch-plan --entry-mode spawn-process` 로 lead 를 띄운다.
|
|
27
|
+
CHILD_LEAD_BACKEND = "spawn-process"
|
|
28
|
+
LAUNCHER_RELATIVE = Path("bin") / "okstra.sh"
|
|
24
29
|
LAUNCH_STATUS_PREPARED = "prepared"
|
|
25
30
|
|
|
26
31
|
|
|
@@ -110,9 +115,14 @@ def _render_context(
|
|
|
110
115
|
"",
|
|
111
116
|
child.get("assignment") or "",
|
|
112
117
|
"",
|
|
113
|
-
"## Manager Directives",
|
|
114
|
-
"",
|
|
115
118
|
]
|
|
119
|
+
if child.get("scope"):
|
|
120
|
+
# `task split` 이 만든 하위 태스크. 이 프로젝트에서 할 범위만 적는다.
|
|
121
|
+
lines.extend(["## Project Scope", "", f"- ticket: {child.get('ticketId') or ''}",
|
|
122
|
+
f"- brief: {child.get('briefPath') or ''}", ""])
|
|
123
|
+
lines.extend(f"- in scope: {item}" for item in child["scope"])
|
|
124
|
+
lines.append("")
|
|
125
|
+
lines.extend(["## Manager Directives", ""])
|
|
116
126
|
for row in directives:
|
|
117
127
|
lines.append(f"- [{row.get('scope')}] {row.get('body')}")
|
|
118
128
|
if not directives:
|
|
@@ -214,6 +224,28 @@ def build_launch_packet(
|
|
|
214
224
|
),
|
|
215
225
|
encoding="utf-8",
|
|
216
226
|
)
|
|
227
|
+
command = str(Path(home) / LAUNCHER_RELATIVE)
|
|
228
|
+
run_args = [
|
|
229
|
+
"--project-root",
|
|
230
|
+
project_root,
|
|
231
|
+
"--project-id",
|
|
232
|
+
project_id,
|
|
233
|
+
"--task-group",
|
|
234
|
+
str(child.get("taskGroup") or task_group),
|
|
235
|
+
"--task-id",
|
|
236
|
+
child_task_id_value,
|
|
237
|
+
]
|
|
238
|
+
# `task split` 이 기록한 브리프·권장 phase 를 넘겨 런처가 첫 실행에서 다시 묻지 않게 한다.
|
|
239
|
+
# phase 는 하위 태스크가 아직 없을 때만 넘긴다. 이미 있으면 런처가 manifest 에서 다음
|
|
240
|
+
# phase 를 채워야 하는데, 두 값이 다 주어지면 그 채우기를 건너뛴다(autofill_from_manifest).
|
|
241
|
+
if child.get("recommendedPhase") and _resolve_task_root_read_only(Path(project_root), target_task_key) is None:
|
|
242
|
+
run_args.extend(["--task-type", str(child["recommendedPhase"])])
|
|
243
|
+
if child.get("briefPath"):
|
|
244
|
+
run_args.extend(["--task-brief", str(child["briefPath"])])
|
|
245
|
+
run_args += [
|
|
246
|
+
"--directive",
|
|
247
|
+
f"Read manager child context: {context_path}",
|
|
248
|
+
]
|
|
217
249
|
packet = {
|
|
218
250
|
"managerId": manager_id,
|
|
219
251
|
"taskGroup": task_group,
|
|
@@ -224,19 +256,9 @@ def build_launch_packet(
|
|
|
224
256
|
"workerDispatchBackend": WORKER_DISPATCH_BACKEND,
|
|
225
257
|
"projectRoot": project_root,
|
|
226
258
|
"contextPath": str(context_path),
|
|
227
|
-
"
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
project_root,
|
|
231
|
-
"--project-id",
|
|
232
|
-
project_id,
|
|
233
|
-
"--task-group",
|
|
234
|
-
str(child.get("taskGroup") or task_group),
|
|
235
|
-
"--task-id",
|
|
236
|
-
child_task_id_value,
|
|
237
|
-
"--directive",
|
|
238
|
-
f"Read manager child context: {context_path}",
|
|
239
|
-
],
|
|
259
|
+
"command": command,
|
|
260
|
+
"runArgs": run_args,
|
|
261
|
+
"shellCommand": shlex.join([command, *run_args]),
|
|
240
262
|
"createdAt": created_at,
|
|
241
263
|
}
|
|
242
264
|
_record_launch_prepared(
|
|
@@ -84,6 +84,10 @@ def snapshots_json_path(home: Path, manager_id: str, task_group: str, task_id: s
|
|
|
84
84
|
return task_root(home, manager_id, task_group, task_id) / "snapshots.json"
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
def split_plan_path(home: Path, manager_id: str, task_group: str, task_id: str) -> Path:
|
|
88
|
+
return task_root(home, manager_id, task_group, task_id) / "split-plan.json"
|
|
89
|
+
|
|
90
|
+
|
|
87
91
|
def events_jsonl_path(home: Path, manager_id: str, task_group: str, task_id: str) -> Path:
|
|
88
92
|
return task_root(home, manager_id, task_group, task_id) / "events.jsonl"
|
|
89
93
|
|
|
@@ -99,3 +103,7 @@ def child_context_path(
|
|
|
99
103
|
safe_project = _slug_segment(project_id, "project-id")
|
|
100
104
|
safe_task = _slug_segment(child_task_id, "task-id")
|
|
101
105
|
return task_root(home, manager_id, task_group, task_id) / "child-context" / f"{safe_project}-{safe_task}.md"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def view_html_path(home: Path, manager_id: str) -> Path:
|
|
109
|
+
return manager_root(home, manager_id) / "view" / "index.html"
|