okstra 0.97.1 → 0.98.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 (74) hide show
  1. package/README.kr.md +4 -3
  2. package/README.md +4 -3
  3. package/docs/kr/architecture.md +2 -2
  4. package/docs/kr/cli.md +2 -0
  5. package/docs/kr/container.md +122 -0
  6. package/docs/project-structure-overview.md +12 -2
  7. package/docs/superpowers/plans/2026-06-21-okstra-container-local-user-test.md +714 -0
  8. package/docs/superpowers/specs/2026-06-21-okstra-container-local-user-test-design.md +125 -0
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/agents/workers/antigravity-worker.md +2 -18
  12. package/runtime/agents/workers/claude-worker.md +10 -33
  13. package/runtime/agents/workers/codex-worker.md +2 -18
  14. package/runtime/agents/workers/report-writer-worker.md +2 -10
  15. package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +5 -2
  16. package/runtime/prompts/profiles/_clarification-recommendation.md +1 -0
  17. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  18. package/runtime/prompts/profiles/_common-contract.md +3 -3
  19. package/runtime/prompts/profiles/_coverage-critic.md +17 -0
  20. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  21. package/runtime/prompts/profiles/_implementation-executor.md +2 -2
  22. package/runtime/prompts/profiles/_implementation-self-check.md +2 -1
  23. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  24. package/runtime/prompts/profiles/_stage-discipline.md +5 -8
  25. package/runtime/prompts/profiles/error-analysis.md +5 -5
  26. package/runtime/prompts/profiles/final-verification.md +4 -4
  27. package/runtime/prompts/profiles/implementation-planning.md +7 -7
  28. package/runtime/prompts/profiles/implementation.md +5 -5
  29. package/runtime/prompts/profiles/improvement-discovery.md +6 -6
  30. package/runtime/prompts/profiles/release-handoff.md +3 -3
  31. package/runtime/prompts/profiles/requirements-discovery.md +4 -4
  32. package/runtime/prompts/wizard/prompts.ko.json +0 -1
  33. package/runtime/python/okstra_ctl/__init__.py +4 -1
  34. package/runtime/python/okstra_ctl/container.py +970 -0
  35. package/runtime/python/okstra_ctl/container_registry.py +93 -0
  36. package/runtime/python/okstra_ctl/error_zip.py +4 -4
  37. package/runtime/python/okstra_ctl/handoff.py +7 -1
  38. package/runtime/python/okstra_ctl/ids.py +40 -1
  39. package/runtime/python/okstra_ctl/listing.py +3 -5
  40. package/runtime/python/okstra_ctl/log_report.py +102 -0
  41. package/runtime/python/okstra_ctl/pane_reclaim.py +5 -4
  42. package/runtime/python/okstra_ctl/paths.py +40 -0
  43. package/runtime/python/okstra_ctl/plan_run_root.py +78 -0
  44. package/runtime/python/okstra_ctl/reconcile.py +4 -2
  45. package/runtime/python/okstra_ctl/resolve_task_key.py +54 -0
  46. package/runtime/python/okstra_ctl/run.py +48 -30
  47. package/runtime/python/okstra_ctl/stage_integrate.py +8 -2
  48. package/runtime/python/okstra_ctl/stage_targets.py +79 -0
  49. package/runtime/python/okstra_ctl/time_report.py +200 -0
  50. package/runtime/python/okstra_ctl/tmux.py +67 -0
  51. package/runtime/python/okstra_ctl/wizard.py +35 -20
  52. package/runtime/python/okstra_project/__init__.py +2 -0
  53. package/runtime/python/okstra_project/state.py +50 -2
  54. package/runtime/skills/okstra-brief/SKILL.md +17 -7
  55. package/runtime/skills/okstra-container/SKILL.md +169 -0
  56. package/runtime/skills/okstra-inspect/SKILL.md +64 -178
  57. package/runtime/skills/okstra-memory/SKILL.md +8 -6
  58. package/runtime/skills/okstra-run/SKILL.md +4 -4
  59. package/runtime/skills/okstra-schedule/SKILL.md +9 -16
  60. package/runtime/skills/okstra-setup/SKILL.md +10 -9
  61. package/runtime/templates/reports/brief.template.md +1 -1
  62. package/runtime/templates/reports/final-report.template.md +1 -1
  63. package/runtime/templates/reports/settings.template.json +3 -1
  64. package/runtime/validators/validate-implementation-plan-stages.py +11 -3
  65. package/src/cli-registry.mjs +28 -0
  66. package/src/commands/inspect/container.mjs +27 -0
  67. package/src/commands/inspect/log-report.mjs +26 -0
  68. package/src/commands/inspect/resolve-task-key.mjs +26 -0
  69. package/src/commands/inspect/task-list.mjs +26 -15
  70. package/src/commands/inspect/time-report.mjs +26 -0
  71. package/src/commands/lifecycle/check-project.mjs +7 -1
  72. package/src/commands/lifecycle/doctor.mjs +16 -0
  73. package/src/lib/skill-catalog.mjs +1 -0
  74. package/runtime/agents/TODO.md +0 -226
@@ -24,7 +24,9 @@
24
24
  "Bash(agy)",
25
25
  "Bash(agy:*)",
26
26
  "Bash(claude)",
27
- "Bash(claude:*)"
27
+ "Bash(claude:*)",
28
+ "Bash(docker)",
29
+ "Bash(docker:*)"
28
30
  ]
29
31
  },
30
32
  "hooks": {
@@ -78,9 +78,17 @@ def _parse_stage_map(text: str) -> Tuple[List[StageMeta], List[ValidationError]]
78
78
  except ValueError:
79
79
  continue
80
80
  depends_raw = cells[2].strip()
81
- depends = [] if depends_raw in ("(none)", "") else [
82
- int(x.strip()) for x in depends_raw.split(",") if x.strip()
83
- ]
81
+ if depends_raw in ("(none)", ""):
82
+ depends = []
83
+ else:
84
+ try:
85
+ depends = [int(x.strip()) for x in depends_raw.split(",") if x.strip()]
86
+ except ValueError:
87
+ # 비정수 depends_on 셀 → 행 skip (비정수 stage_number 와 동일). raw
88
+ # ValueError 를 흘리면 _stage_map_reject_detail·handoff 의 except
89
+ # PrepareError 를 우회해 traceback 으로 죽으므로, 누락된 행을 하류
90
+ # S2(비단조) 검사가 잡게 한다.
91
+ continue
84
92
  try:
85
93
  step_count = int(cells[3])
86
94
  except ValueError:
@@ -110,6 +110,27 @@ export const COMMAND_REGISTRY = [
110
110
  category: "introspection",
111
111
  summary: ["Estimate file/read context cost for a task bundle"],
112
112
  },
113
+ {
114
+ name: "resolve-task-key",
115
+ module: "./commands/inspect/resolve-task-key.mjs",
116
+ export: "run",
117
+ category: "introspection",
118
+ summary: ["Resolve a bare task-id to candidate task-keys from the catalog"],
119
+ },
120
+ {
121
+ name: "time-report",
122
+ module: "./commands/inspect/time-report.mjs",
123
+ export: "run",
124
+ category: "introspection",
125
+ summary: ["Aggregate elapsed work time across a task's runs (raw ms)"],
126
+ },
127
+ {
128
+ name: "log-report",
129
+ module: "./commands/inspect/log-report.mjs",
130
+ export: "run",
131
+ category: "introspection",
132
+ summary: ["Inventory wrapper sidecar logs by size and task (read-only)"],
133
+ },
113
134
  {
114
135
  name: "error-report",
115
136
  module: "./commands/inspect/error-report.mjs",
@@ -131,6 +152,13 @@ export const COMMAND_REGISTRY = [
131
152
  category: "introspection",
132
153
  summary: ["Assemble a task's run-to-run phase recap or append a recap log entry"],
133
154
  },
155
+ {
156
+ name: "container",
157
+ module: "./commands/inspect/container.mjs",
158
+ export: "run",
159
+ category: "introspection",
160
+ summary: ["Manage the okstra container runtime (up/status/logs/stop-watcher/down)"],
161
+ },
134
162
  {
135
163
  name: "worktree-lookup",
136
164
  module: "./commands/execute/worktree-lookup.mjs",
@@ -0,0 +1,27 @@
1
+ import { runPythonModule } from "../../lib/python-helper.mjs";
2
+
3
+ const USAGE = `okstra container — manage the okstra container runtime
4
+
5
+ Usage:
6
+ okstra container up [options]
7
+ okstra container status [options]
8
+ okstra container logs [options]
9
+ okstra container stop-watcher [options]
10
+ okstra container down [options]
11
+
12
+ Each sub-command delegates to the okstra_ctl.container python entrypoint.
13
+ `;
14
+
15
+ export async function run(args) {
16
+ if (args.includes("--help") || args.includes("-h")) {
17
+ process.stdout.write(USAGE);
18
+ return 0;
19
+ }
20
+
21
+ const result = await runPythonModule({
22
+ module: "okstra_ctl.container",
23
+ args,
24
+ stdio: "inherit-stdout",
25
+ });
26
+ return result.code;
27
+ }
@@ -0,0 +1,26 @@
1
+ import { runPythonModule } from "../../lib/python-helper.mjs";
2
+
3
+ const USAGE = `okstra log-report — inventory wrapper sidecar logs by size and task
4
+
5
+ Usage:
6
+ okstra log-report [--project-root <dir>] [--cwd <dir>] [--top <N>] [--json]
7
+
8
+ Output: JSON { ok, projectRoot, logsRoot, topLargest[], perTask[], totals }.
9
+ Scans <project-root>/.okstra/tasks/**/runs/*/prompts/*.log. Sizes are raw bytes
10
+ and mtimes are epoch seconds — the caller formats KB/MB and ages. Read-only: it
11
+ never deletes logs (cleanup commands stay in the skill).
12
+ `;
13
+
14
+ export async function run(args) {
15
+ if (args.includes("--help") || args.includes("-h")) {
16
+ process.stdout.write(USAGE);
17
+ return 0;
18
+ }
19
+
20
+ const result = await runPythonModule({
21
+ module: "okstra_ctl.log_report",
22
+ args,
23
+ stdio: "inherit-stdout",
24
+ });
25
+ return result.code;
26
+ }
@@ -0,0 +1,26 @@
1
+ import { runPythonModule } from "../../lib/python-helper.mjs";
2
+
3
+ const USAGE = `okstra resolve-task-key — resolve a bare task-id to catalog task-keys
4
+
5
+ Usage:
6
+ okstra resolve-task-key <task-id> [--task-group <g>] [--project-root <dir>] [--cwd <dir>] [--json]
7
+
8
+ Output: JSON { ok, projectRoot, matches: [ { taskKey, taskGroup, taskId,
9
+ taskGroupPathSegment, taskIdPathSegment, taskManifestPath, ... } ] }.
10
+ matches.length 0 = not found, 1 = unambiguous, N = ambiguous (caller asks).
11
+ This is read-only; it never mutates task state.
12
+ `;
13
+
14
+ export async function run(args) {
15
+ if (args.includes("--help") || args.includes("-h")) {
16
+ process.stdout.write(USAGE);
17
+ return 0;
18
+ }
19
+
20
+ const result = await runPythonModule({
21
+ module: "okstra_ctl.resolve_task_key",
22
+ args,
23
+ stdio: "inherit-stdout",
24
+ });
25
+ return result.code;
26
+ }
@@ -3,29 +3,34 @@ import { runPythonSnippet, emitJsonError } from "../../lib/python-helper.mjs";
3
3
  const USAGE = `okstra task-list — list okstra tasks registered in this project
4
4
 
5
5
  Usage:
6
- okstra task-list Resolve PROJECT_ROOT from cwd, list tasks
7
- okstra task-list --cwd <dir> Start resolution from <dir> instead of cwd
8
- okstra task-list --project <dir> Use <dir> directly as PROJECT_ROOT
6
+ okstra task-list Resolve PROJECT_ROOT from cwd, list tasks
7
+ okstra task-list --cwd <dir> Start resolution from <dir> instead of cwd
8
+ okstra task-list --project <dir> Use <dir> directly as PROJECT_ROOT
9
+ okstra task-list --task-type <type> Filter by taskType
10
+ okstra task-list --latest-run-status <s> Filter by latestRunStatus
11
+ okstra task-list --task-group <group> Filter by taskGroup
12
+ okstra task-list --limit <N> Keep the first N (catalog is updatedAt desc)
9
13
 
10
14
  Output: JSON { ok, projectRoot, tasks: [...], latest: {...}|null }.
15
+ Filters are AND-combined and all optional.
11
16
 
12
17
  Replaces the common pattern of loading okstra_project and calling
13
18
  list_project_tasks + read_latest_task from a python heredoc.
14
19
  `;
15
20
 
16
21
  function parseArgs(args) {
17
- const opts = { cwd: process.cwd(), projectRoot: "" };
22
+ const opts = { cwd: process.cwd(), projectRoot: "", taskType: "", latestRunStatus: "", taskGroup: "", limit: "" };
23
+ const valued = {
24
+ "--cwd": "cwd", "--project": "projectRoot", "--task-type": "taskType",
25
+ "--latest-run-status": "latestRunStatus", "--task-group": "taskGroup", "--limit": "limit",
26
+ };
18
27
  for (let i = 0; i < args.length; i++) {
19
28
  const a = args[i];
20
- if (a === "--cwd") {
21
- opts.cwd = args[++i];
22
- if (!opts.cwd) throw new Error("--cwd requires a path");
23
- } else if (a === "--project") {
24
- opts.projectRoot = args[++i];
25
- if (!opts.projectRoot) throw new Error("--project requires a path");
26
- } else {
27
- throw new Error(`unknown argument '${a}'`);
28
- }
29
+ const key = valued[a];
30
+ if (!key) throw new Error(`unknown argument '${a}'`);
31
+ const v = args[++i];
32
+ if (v === undefined || v === "") throw new Error(`${a} requires a value`);
33
+ opts[key] = v;
29
34
  }
30
35
  return opts;
31
36
  }
@@ -40,6 +45,10 @@ from okstra_project import (
40
45
 
41
46
  explicit = sys.argv[1]
42
47
  cwd = sys.argv[2]
48
+ task_type = sys.argv[3] or None
49
+ latest_run_status = sys.argv[4] or None
50
+ task_group = sys.argv[5] or None
51
+ limit = int(sys.argv[6]) if sys.argv[6] else None
43
52
  try:
44
53
  pr = resolve_project_root(explicit_root=explicit, cwd=cwd)
45
54
  except ResolverError as e:
@@ -50,7 +59,9 @@ pr_path = Path(pr)
50
59
  print(json.dumps({
51
60
  "ok": True,
52
61
  "projectRoot": str(pr_path),
53
- "tasks": list_project_tasks(pr_path),
62
+ "tasks": list_project_tasks(
63
+ pr_path, task_type=task_type, latest_run_status=latest_run_status,
64
+ task_group=task_group, limit=limit),
54
65
  "latest": read_latest_task(pr_path),
55
66
  }, default=str))
56
67
  `;
@@ -70,7 +81,7 @@ export async function run(args) {
70
81
 
71
82
  const result = await runPythonSnippet({
72
83
  script: SCRIPT,
73
- args: [opts.projectRoot, opts.cwd],
84
+ args: [opts.projectRoot, opts.cwd, opts.taskType, opts.latestRunStatus, opts.taskGroup, opts.limit],
74
85
  });
75
86
 
76
87
  if (result.code !== 0 && !result.stdout.trim()) {
@@ -0,0 +1,26 @@
1
+ import { runPythonModule } from "../../lib/python-helper.mjs";
2
+
3
+ const USAGE = `okstra time-report — aggregate elapsed work time across a task's runs
4
+
5
+ Usage:
6
+ okstra time-report <task-key> [--project-root <dir>] [--cwd <dir>] [--json]
7
+ okstra time-report <task-root-path> [--json]
8
+
9
+ Output: JSON { ok, taskKey, byTaskType[], grandTotal, perWorker{}, perRunWallClock[],
10
+ phaseTimelines[], unavailable[] }. All durations are raw milliseconds — the caller
11
+ formats HH:MM:SS and renders tables. This is read-only; it never mutates task state.
12
+ `;
13
+
14
+ export async function run(args) {
15
+ if (args.includes("--help") || args.includes("-h")) {
16
+ process.stdout.write(USAGE);
17
+ return 0;
18
+ }
19
+
20
+ const result = await runPythonModule({
21
+ module: "okstra_ctl.time_report",
22
+ args,
23
+ stdio: "inherit-stdout",
24
+ });
25
+ return result.code;
26
+ }
@@ -56,7 +56,13 @@ export async function run(args) {
56
56
  return 0;
57
57
  }
58
58
 
59
- const opts = parseArgs(args);
59
+ let opts;
60
+ try {
61
+ opts = parseArgs(args);
62
+ } catch (err) {
63
+ process.stderr.write(`error: ${err.message}\n\n${USAGE}`);
64
+ return 2;
65
+ }
60
66
  const paths = await resolvePaths();
61
67
 
62
68
  const probe = await runProcess(
@@ -125,6 +125,12 @@ async function checkBashEntry(binDir, name) {
125
125
  return { ok: true, detail: `${name} OK` };
126
126
  }
127
127
 
128
+ async function dockerAvailabilityWarning() {
129
+ const r = await runProcess("docker", ["compose", "version"]);
130
+ if (r.code === 0) return null;
131
+ return "docker compose not available — `okstra container` needs Docker; all other commands work without it.";
132
+ }
133
+
128
134
  async function check(name, fn) {
129
135
  try {
130
136
  const result = await fn();
@@ -202,6 +208,12 @@ export async function run(args) {
202
208
  // (requiredSkillNamesForRuntime → []); pass --runtime to include them.
203
209
  const resolvedRuntime = runtimeResolution.ok ? runtimeResolution.resolvedRuntime : null;
204
210
 
211
+ // docker is OPTIONAL for okstra (only the container runtime needs it). Like
212
+ // runtimeResolution, a missing docker must NOT flip allOk → exit 1, so it lives
213
+ // in its own variable and is rendered as a single [WARN] line, never pushed into
214
+ // `results`. Absent → string note; present → null.
215
+ const dockerWarning = await dockerAvailabilityWarning();
216
+
205
217
  const results = [
206
218
  await check("python3", checkPython3),
207
219
  await check("okstra_project import", () => checkPythonImport("okstra_project", paths.pythonpath)),
@@ -254,6 +266,7 @@ export async function run(args) {
254
266
  phase: opts.phase,
255
267
  paths,
256
268
  phaseDiagnostics,
269
+ dockerWarning,
257
270
  checks: results,
258
271
  }, null, 2)}\n`,
259
272
  );
@@ -265,6 +278,9 @@ export async function run(args) {
265
278
  ` [WARN] runtime host: ${runtimeResolution.reason} skill checks skipped — pass --runtime <claude-code|codex|external> to include them.\n`,
266
279
  );
267
280
  }
281
+ if (dockerWarning) {
282
+ process.stdout.write(` [WARN] docker: ${dockerWarning}\n`);
283
+ }
268
284
  for (const r of results) {
269
285
  const mark = r.ok ? "OK " : "FAIL";
270
286
  process.stdout.write(` [${mark}] ${r.name}: ${r.detail}\n`);
@@ -11,6 +11,7 @@ export const USER_SKILL_NAMES = Object.freeze([
11
11
  "okstra-memory",
12
12
  "okstra-inspect",
13
13
  "okstra-schedule",
14
+ "okstra-container",
14
15
  ]);
15
16
 
16
17
  // Names okstra used to install as skills before lead/internal contracts moved
@@ -1,226 +0,0 @@
1
- # okstra (okstra) — 후속 개선 TODO
2
-
3
- 이 파일은 okstra 워크플로우의 워커 종료 / Phase 4 대기 시간 개선 작업, 그리고 워커 설정 일관성 감사(2026-05-03)에서 도출됐지만 단일 라운드에서 안전하게 처리할 수 없어 별도 라운드로 미룬 항목을 기록한다.
4
-
5
- > Historical note: entries before 2026-06-15 may reference the former internal-skill layout; current lead contracts live under `prompts/lead/` and current coding-preflight resources live under `prompts/coding-preflight/`.
6
-
7
- ---
8
-
9
- ## 항목 C2 — Worker-results 경로 표준 결정 [해결됨, 2026-05-03]
10
-
11
- ### 결정
12
-
13
- 런타임이 도달한 layout을 canonical로 채택했다(`scripts/okstra.sh:1495-1523` 기준):
14
-
15
- ```
16
- runs/<task-type>/
17
- ├── manifests/ (run-manifest-<task-type>-<seq>.json)
18
- ├── state/ (team-state-<task-type>-<seq>.json, convergence-<task-type>-<seq>.json)
19
- ├── prompts/ (claude-execution-prompt-<task-type>-<seq>.md, <role>-worker-prompt-<task-type>-<seq>.md)
20
- ├── reports/ (final-report-<task-type>-<seq>.md)
21
- ├── status/ (final-<task-type>-<seq>.status)
22
- ├── sessions/ (claude-resume-<task-type>-<seq>.sh)
23
- ├── logs/ (errors-<task-type>-<seq>.jsonl, optional)
24
- └── worker-results/
25
- ├── claude-worker-<task-type>-<seq>.md
26
- ├── codex-worker-<task-type>-<seq>.md
27
- ├── gemini-worker-<task-type>-<seq>.md
28
- └── report-writer-worker-<task-type>-<seq>.md
29
- ```
30
-
31
- 규칙:
32
- - `<seq>` = 3-digit zero-padded sequence number (`001`, `002`, …) scoped **per-category directory** (i.e., scanned independently per `manifests/`, `prompts/`, `reports/`, `status/`, `state/`, `sessions/`, `worker-results/`). 파일명 infix이며 디렉토리 세그먼트가 아님.
33
- - 카테고리별 카운터이므로 같은 run에서도 디렉토리마다 `<seq>` 값이 다를 수 있다(이전 run이 일부 카테고리만 기록한 경우). 한 run의 cross-category 식별자는 manifest의 `runDateTimeSegment`(ISO timestamp) 필드이며, 파일명 infix가 아니다.
34
- - `runs/<date>/...` 옛 패턴(date-first)은 폐기. `<date>` 디렉토리 세그먼트는 더 이상 사용하지 않음.
35
- - 같은 task-type 재실행 시 같은 디렉토리를 재사용하되, 모든 run-level artifact는 `<task-type>-<seq>` infix로 분리되므로 덮어쓰지 않는다.
36
-
37
- ### 수정 내역
38
-
39
- - `scripts/okstra.sh` 의 `CLAUDE_WORKER_RESULT_FILE` 외 4종 파일명에 `$RUN_FILE_SUFFIX` 추가 (이전엔 suffix가 없어 재실행 시 덮어쓰기됨).
40
- - 옛 `runs/<task-type>/<task-type>-<seq>/...` 및 `runs/<date>/<task-type>/...` 표기를 모두 정리해 reality에 맞춤. 영향 파일:
41
- - `agents/SKILL.md`
42
- - `agents/workers/{claude,codex,gemini,report-writer}-worker.md`
43
- - `skills/okstra-context-loader/SKILL.md` (canonical 트리 갱신)
44
- - `skills/okstra-team-contract/SKILL.md`
45
- - `skills/okstra-convergence/SKILL.md`
46
- - `skills/okstra-report-writer/SKILL.md`
47
- - `OKSTRA_USAGE_MANUAL.md`
48
-
49
- ---
50
-
51
- ## 항목 F4 — `implementation.md` 프로필의 워커 이름 매핑 누락 [부분 진행 — Executor 선택 CLI 만 처리됨, 2026-05-12]
52
-
53
- ### 부분 진행 메모 (2026-05-12)
54
-
55
- `--executor <claude|codex|gemini>` 플래그가 추가되어 Executor provider 를 run-prep 시점에 선택할 수 있게 됐고, profile 텍스트도 "Claude executor" → 일반화된 "Executor" 로 재작성됨. 즉 **CLI / 매니페스트 / 프로필 표현 레이어는 정리됨**.
56
-
57
- 그러나 아래는 여전히 미해결:
58
-
59
- - `claude-executor.md` / `claude-verifier.md` / `codex-verifier.md` / `gemini-verifier.md` 4종 subagent 정식 등록은 안 됨. 현재 lead 는 dispatch 할 때 기존 `claude-worker` / `codex-worker` / `gemini-worker` 를 그대로 재사용하며, executor vs verifier 의 도구 화이트리스트 차이는 프롬프트 레벨에서만 강제됨.
60
- - run-manifest 의 `teamContract.executor.workerAgent` 가 가리키는 subagent 도 위 기존 `*-worker` 이름이라 도구-레벨 차단은 동작하지 않음.
61
-
62
- 본 항목의 원래 의도(도구-레벨 read-only 강제)는 4종 subagent 등록이 머지되어야 완성됨.
63
-
64
- ### 문제 (원본)
65
-
66
- > 2026-05-12 주: 아래 본문은 F4 가 처음 기록된 시점의 분석. "다음 워커 이름을 사용한다" 의 명단(특히 `Claude executor`)은 그 후 프로필 일반화로 `Executor` 단일 role 로 통합됐고 provider 는 `--executor` 로 선택하게 됐다. 핵심 미해결인 *도구-레벨 read-only 강제* 부분은 그대로 유효.
67
-
68
- [prompts/profiles/implementation.md](../../../prompts/profiles/implementation.md) 프로필은 다른 4개 프로필과 달리 다음 워커 이름을 사용한다:
69
-
70
- - `Claude executor` (유일하게 Edit/Write/state-mutating Bash 허용)
71
- - `Claude verifier`
72
- - `Codex verifier`
73
- - `Gemini verifier`
74
-
75
- 그런데 **이 4개 역할 이름 중 어느 것도 [agents/workers/](agents/) 의 `*.md` 파일로 등록돼 있지 않음**. 등록된 워커 정의는 `claude-worker.md`, `codex-worker.md`, `gemini-worker.md`, `report-writer-worker.md` 4종.
76
-
77
- ### 결과로 발생 가능한 모호성
78
-
79
- Lead가 implementation.md 프로필을 따라 Phase 4 dispatch를 시도할 때:
80
-
81
- - `subagent_type: "claude-executor"` 같은 값이 실제로는 등록 안 돼 있어 dispatch 실패.
82
- - 또는 Lead가 임의로 `subagent_type: "claude-worker"` 로 매핑해 dispatch — 그러면 implementation 프로필의 핵심 안전장치(`Claude executor`만 Edit/Write 허용) 가 작동하지 않음. `claude-worker.md` 정의는 read-only를 강제하지 않음.
83
-
84
- ### 결정해야 할 것
85
-
86
- 1. **별도 워커 정의 파일 신설**: `agents/claude-executor.md`, `agents/claude-verifier.md`, `agents/codex-verifier.md`, `agents/gemini-verifier.md` 4종을 만들어 등록 — 각 정의에 read-only / write-allowed 제약을 직접 인코딩.
87
- 2. **기존 워커 재사용 + 프롬프트 레벨 제약**: `claude-worker` 등을 그대로 쓰되 lead의 dispatch 프롬프트에서 "이번 dispatch에서는 Edit/Write 금지" 같은 룰을 주입. 단점: 프롬프트만으로 강제되며 도구 레벨 차단이 아님.
88
- 3. **implementation.md 프로필 자체 폐기/리네임**: 다른 프로필이 `claude-worker` 등 표준 이름을 쓰므로 implementation도 같은 이름 체계로 재작성.
89
-
90
- ### 권장
91
-
92
- 옵션 1이 가장 안전(`tools` 화이트리스트로 도구 레벨 차단 가능). 다만 신규 워커 정의 4개 작성은 작업량이 있어 별도 라운드에서 다룬다.
93
-
94
- ### 추가로 점검할 것
95
-
96
- - okstra-team-contract / SKILL.md 에 implementation 워크플로우의 워커 명단이 따로 등재돼 있는지(현재 Role Definitions 표는 4종만 등재).
97
- - okstra runtime이 `subagent_type` 미등록 시 어떻게 동작하는지(silent fallback vs 명시적 에러).
98
-
99
- ---
100
-
101
- ## 수정 B — Leader-side 워커 soft timeout 도입 [해결됨, 2026-06-10]
102
-
103
- ### 해결 메모 (2026-06-10)
104
-
105
- 본 항목이 요구한 per-worker soft-timeout 안전장치는 self-scheduled polling 작업으로 구현됐다. lead 가 백그라운드 폴링 루프를 띄울 때 거는 `deadline=$((SECONDS + <per-worker-deadline-seconds>))` 가드([skills/okstra-team-contract/SKILL.md:134](../skills/okstra-team-contract/SKILL.md:134))가 워커별 마감을 강제하고, 초과 시 폴링이 `POLL_TIMEOUT` 으로 종료된다([skills/okstra-team-contract/SKILL.md:136](../skills/okstra-team-contract/SKILL.md:136)). 규약은 "Worker-completion detection (self-scheduled polling)" 섹션([skills/okstra-team-contract/SKILL.md:124](../skills/okstra-team-contract/SKILL.md:124))에 정본으로 명세돼 있다. 설계 근거는 [docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md](../docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md) 참고. 아래 배경/초안 분석은 당시 기록으로 보존한다.
106
-
107
- ### 배경
108
-
109
- okstra는 Phase 4에서 Claude / Codex / Gemini 워커 3종을 동시에 spawn한다. 현재 leader는:
110
-
111
- - Agent 호출을 foreground(blocking)로 발사하므로 가장 느린 워커가 phase 전체를 잡아둔다.
112
- - `terminal status: timeout` 카테고리는 [`okstra-team-contract` SKILL.md](skills/okstra-team-contract/SKILL.md) 에 정의돼 있지만, **누가 어떤 조건으로 timeout을 트리거하는지에 대한 룰이 없다**.
113
- - 결과적으로 Claude worker의 ack가 늦으면 leader는 무한정 기다리고, convergence(Phase 5.5)가 지연된다.
114
-
115
- 수정 A(Claude worker Stop Condition)는 워커 측 자율 종료를 강제하지만, 워커가 규칙을 따르지 않거나 in-process subagent runtime이 멈춘 경우의 안전장치는 여전히 없다. 수정 B는 그 안전장치를 leader 쪽에 추가한다.
116
-
117
- ### 변경 대상 파일
118
-
119
- - [SKILL.md](SKILL.md) Phase 4 섹션 (대략 380~410 라인 부근, "Phase 4: Preferred Agent Teams execution")
120
- - [skills/okstra-team-contract/SKILL.md](skills/okstra-team-contract/SKILL.md) — terminal status 표 보강 (필요 시)
121
-
122
- ### 추가할 룰 초안
123
-
124
- Phase 4 실행 섹션에 아래 블록을 추가한다.
125
-
126
- ```markdown
127
- ### Per-worker soft timeout
128
-
129
- Lead must establish an internal expected-duration window per worker based on
130
- task type:
131
-
132
- | Task type | Expected per-worker duration |
133
- | ------------------------ | ----------------------------- |
134
- | requirements-discovery | 10 minutes |
135
- | error-analysis | 15 minutes |
136
- | implementation-planning | 20 minutes |
137
- | final-verification | 10 minutes |
138
-
139
- If a worker has not returned within **2× the expected duration** AND the
140
- worker-results file already exists on disk with a valid standardized header,
141
- Lead MUST:
142
-
143
- 1. Record terminal status `timeout` in team-state for that worker, with
144
- `reason: "worker-overran-after-write"` and the wall-clock duration.
145
- 2. Append a `cli-failure` (Codex/Gemini) or `tool-failure` (Claude worker)
146
- event via `okstra-error-log.py append-observed`.
147
- 3. Proceed with Phase 5.5 using the on-disk worker-results file as the
148
- canonical worker output. Do NOT invent results — if the file does not exist
149
- or fails header validation, record `not-run` instead of `timeout` and skip
150
- that worker's contribution to convergence.
151
-
152
- Lead does NOT have a runtime-level "cancel Agent call" capability. The Agent
153
- call may continue to run in the background and eventually return; Lead simply
154
- ignores the late return if a `timeout` status was already recorded.
155
-
156
- If the worker-results file does NOT exist when the soft timeout fires, Lead
157
- must wait for either the file to appear or the Agent call to return — do not
158
- fabricate a `timeout` status against a missing artifact.
159
- ```
160
-
161
- ### 검증 체크리스트
162
-
163
- 수정 B를 머지하기 전에 다음을 확인한다.
164
-
165
- - [ ] `okstra-team-contract` 의 terminal status 표(`completed | timeout | error | not-run`)와 새 룰의 의미가 정확히 일치하는가
166
- - [ ] `okstra-error-log.py append-observed` 의 `--error-type cli-failure` / `tool-failure` 파라미터가 새 룰에서 요구하는 형태와 일치하는가 (`scripts/okstra-error-log.py` 시그니처 점검)
167
- - [ ] team-state JSON 스키마에 `reason`, `durationSeconds` 필드가 이미 존재하는가, 없다면 스키마 마이그레이션 필요
168
- - [ ] Phase 5.5 (`okstra-convergence`) 가 일부 워커 결과 누락 상태에서도 정상 동작하는가 — 적어도 1개 워커 결과만으로는 convergence 진행을 거부해야 하는가, 아니면 partial 모드를 허용하는가 정책 결정
169
- - [ ] 이 룰을 Phase 5 (sequential/background fallback)에도 동일하게 적용할지 결정
170
-
171
- ### 의존 / 선행 작업
172
-
173
- - 수정 A (Claude worker Stop Condition) 가 먼저 머지되어 일정 기간 사용 데이터를 수집해야, 위 표의 expected duration 값을 실측 기반으로 조정할 수 있다. 초안 값은 보수적으로 잡았으므로 1~2주 사용 후 조정 권장.
174
-
175
- ### 참고 — 적용 범위 밖 (수정 C 후보) [해결됨, 2026-06-10]
176
-
177
- 다음은 수정 B와 별개의 더 큰 설계 변경이며, 별도 브랜치에서 다룬다.
178
-
179
- > 해결 메모 (2026-06-10): 아래 첫 두 항목(background 전환 + worker-results 파일 폴링 루프)은 동일한 self-scheduled polling 프로토콜로 구현됐다 — "Worker-completion detection (self-scheduled polling)" 섹션([skills/okstra-team-contract/SKILL.md:124](../skills/okstra-team-contract/SKILL.md:124)), 설계는 [docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md](../docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md). 진짜 cancellation 통합은 여전히 미해결.
180
-
181
- - Phase 4를 foreground multi-Agent에서 `run_in_background: true` + leader polling 방식으로 전환
182
- - Lead 가 worker-results 파일 존재 + 헤더 검증으로 완료를 판단하는 폴링 루프 추가
183
- - 진짜 cancellation (Agent 강제 종료) 가 가능해지면 그 시점에 통합
184
-
185
- ---
186
-
187
- ## 항목 X1 — cross-repo carry packet 소비 자동화 (okstra-brief) [미착수, 2026-06-17]
188
-
189
- ### 배경
190
-
191
- cross-repo 작업은 구조적으로 repo 별 별도 okstra run 이 필수다(worktree 단일 repo 바인딩 · run-index/registry 단일 project 키 · 편집 allowlist 가 해당 트리로 제한). **생산자 계약은 이미 구현됨** — implementation-planning 계획서가 `## Cross-Repo Carry — <repo>` 부록을 세 하위 섹션(Preconditions / Work for B / How to start)으로 방출한다([prompts/profiles/implementation-planning.md](../prompts/profiles/implementation-planning.md), 커밋 `cfd7c2c`).
192
-
193
- **소비자 쪽은 현재 수동**이다: B repo 에서 `okstra-brief` → variant `Reporter input` → source `File` 로 A 계획서 절대경로를 붙여넣으면 **파일 전체가 verbatim 으로** Source Material 에 들어가([skills/okstra-brief/SKILL.md:147](../skills/okstra-brief/SKILL.md), [SKILL.md:162-164](../skills/okstra-brief/SKILL.md)), B 의 planning 이 A 자신의 stage·decision 노이즈에서 "B 가 할 일 / precondition" 을 직접 골라내야 한다.
194
-
195
- ### 목표
196
-
197
- B 의 `okstra-brief` 가 A 계획서에서 **`## Cross-Repo Carry — <repo>` 섹션만 추출**해 구조화 시드: `Work for B` → B 의 requirements(R-ID), `Preconditions` → B 의 외부 의존 + 관측 확인 신호(구현 대상 아님), recognition caveat 동반.
198
-
199
- ### 변경 대상 파일
200
-
201
- - [skills/okstra-brief/SKILL.md](../skills/okstra-brief/SKILL.md) — 신규 brief variant(또는 `File` 소스가 `## Cross-Repo Carry` 섹션을 감지하면 자동 분기) + 섹션 추출 단계.
202
- - (선택) `## Cross-Repo Carry — <repo>` 파싱 헬퍼 — 순수 마크다운 파싱. 스킬·CLI 가 같이 쓰면 단일 참조점으로 `scripts/okstra_ctl/` Python 모듈에 둔다.
203
-
204
- ### 결정해야 할 것
205
-
206
- 1. 신규 brief variant 로 노출할지 vs `File` 소스 자동 감지로 처리할지.
207
- 2. A 계획서를 수동 경로 입력 vs 최근 run 자동 추천(`okstra task-list`)으로 찾을지.
208
- 3. `Preconditions` 를 B brief 의 어느 섹션(Open Questions / 외부 의존 전용 섹션)으로 매핑할지.
209
-
210
- ### 불변 제약 (자동화해도 반드시 유지)
211
-
212
- - B 는 자기 권한·worktree 로 setup/brief/run 을 실행 — 자동화가 "A 가 B 에 쓰기" 를 허용하면 안 됨.
213
- - 경계를 넘는 읽기는 B 가 A 경로를 **명시 인용**하는 형태만(artifact-home, [SKILL.md:90-92](../skills/okstra-brief/SKILL.md)).
214
- - A 의 done-state 는 B 게이트가 자동 인정하지 않음 — 자동화는 carry 를 구조화할 뿐 B 의 독립성은 그대로.
215
-
216
- ### 결정
217
-
218
- 2026-06-17 — 사용자가 당분간 수동 흐름 유지를 선택. 자동화는 본 항목으로 보류. 진행 전 brainstorming 으로 위 3개 결정 사항을 확정한다.
219
-
220
- ---
221
-
222
- ## 변경 이력
223
-
224
- - 2026-05-03 — 작성. 수정 A(Claude worker Stop Condition) 동시 머지에 따른 후속 항목 기록.
225
- - 2026-06-10 — 수정 B(Leader-side 워커 soft timeout) 및 수정 C 후보(Phase 4 background polling / worker-results 파일 폴링 루프)를 self-scheduled polling 작업으로 해소 표기. 정본: okstra-team-contract "Worker-completion detection (self-scheduled polling)" 섹션, 설계: docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md.
226
- - 2026-06-17 — 항목 X1(cross-repo carry packet 소비 자동화) 기록. 생산자 계약은 커밋 `cfd7c2c` 로 구현됨, 소비자(okstra-brief 자동 추출)는 사용자 선택으로 보류.