@jjlabsio/claude-crew 0.1.34 → 0.1.35

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.
@@ -11,7 +11,7 @@
11
11
  "name": "claude-crew",
12
12
  "source": "./",
13
13
  "description": "오케스트레이터 + PM, 플래너, 개발, QA, 마케팅 에이전트 팀으로 단일 제품의 개발과 마케팅을 통합 관리",
14
- "version": "0.1.34",
14
+ "version": "0.1.35",
15
15
  "author": {
16
16
  "name": "Jaejin Song",
17
17
  "email": "wowlxx28@gmail.com"
@@ -28,5 +28,5 @@
28
28
  "category": "workflow"
29
29
  }
30
30
  ],
31
- "version": "0.1.34"
31
+ "version": "0.1.35"
32
32
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-crew",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "1인 SaaS 개발자를 위한 멀티 에이전트 오케스트레이션 — 개발, 마케팅, 일정을 한 대화에서 통합 관리",
5
5
  "author": {
6
6
  "name": "Jaejin Song",
@@ -10,6 +10,10 @@
10
10
  import { readFileSync, readdirSync } from 'node:fs';
11
11
  import { join, dirname } from 'node:path';
12
12
 
13
+ import { loadCatalog, loadProjectConfig, loadUserConfig } from '../scripts/lib/config.mjs';
14
+ import { loadContracts } from '../scripts/lib/contracts.mjs';
15
+ import { resolveRole } from '../scripts/lib/resolve.mjs';
16
+
13
17
  // ---------------------------------------------------------------------------
14
18
  // Read stdin
15
19
  // ---------------------------------------------------------------------------
@@ -52,6 +56,30 @@ function loadAgentDefinitions(pluginRoot) {
52
56
  return defs;
53
57
  }
54
58
 
59
+ function resolveCrewRoleProvider(role) {
60
+ return resolveRole({
61
+ role,
62
+ catalog: loadCatalog(),
63
+ userConfig: loadUserConfig(),
64
+ projectConfig: loadProjectConfig(),
65
+ contracts: loadContracts()
66
+ });
67
+ }
68
+
69
+ function blockCodexAgentCall(role, resolved) {
70
+ return {
71
+ continue: true,
72
+ decision: 'block',
73
+ reason: [
74
+ `Role '${role}' is configured for Codex provider and cannot be called with the Agent tool.`,
75
+ 'Use the central runner prepare/dispatch path instead:',
76
+ `node "$CLAUDE_PLUGIN_ROOT/scripts/crew-agent-runner.mjs" prepare --role ${role} --request-file <request-file> --json`,
77
+ `node "$CLAUDE_PLUGIN_ROOT/scripts/crew-agent-runner.mjs" dispatch --role ${role} --request-file <request-file> --json`,
78
+ `Resolved model: ${resolved.model}${resolved.reasoning ? ` / ${resolved.reasoning}` : ''}`
79
+ ].join('\n')
80
+ };
81
+ }
82
+
55
83
  // ---------------------------------------------------------------------------
56
84
  // Main
57
85
  // ---------------------------------------------------------------------------
@@ -86,6 +114,29 @@ async function main() {
86
114
 
87
115
  // Canonicalize subagent_type (strip plugin prefix if present)
88
116
  const rawType = input.subagent_type.replace(/^claude-crew:/, '');
117
+ let resolved = null;
118
+ try {
119
+ resolved = resolveCrewRoleProvider(rawType);
120
+ } catch {
121
+ resolved = null;
122
+ }
123
+
124
+ if (resolved?.provider === 'codex') {
125
+ console.log(JSON.stringify(blockCodexAgentCall(rawType, resolved)));
126
+ return;
127
+ }
128
+
129
+ if (resolved?.provider === 'claude' && !input.model && resolved.model) {
130
+ console.log(JSON.stringify({
131
+ continue: true,
132
+ modifiedInput: { ...input, model: resolved.model },
133
+ hookSpecificOutput: {
134
+ hookEventName: 'PreToolUse',
135
+ additionalContext: `model 자동 주입: ${rawType} → ${resolved.model}`,
136
+ },
137
+ }));
138
+ return;
139
+ }
89
140
 
90
141
  // Load agent definitions and auto-inject model if missing
91
142
  const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || dirname(import.meta.url.replace('file://', '')).replace('/hooks', '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlabsio/claude-crew",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "1인 SaaS 개발자를 위한 멀티 에이전트 오케스트레이션 — 개발, 마케팅, 일정을 한 대화에서 통합 관리",
5
5
  "author": "Jaejin Song <wowlxx28@gmail.com>",
6
6
  "license": "MIT",
@@ -15,6 +15,7 @@ import {
15
15
  formatDispatchProviderGuardMessage
16
16
  } from "./lib/dispatch.mjs";
17
17
  import { installHooks } from "./lib/installHooks.mjs";
18
+ import { prepareDispatch } from "./lib/prepare.mjs";
18
19
  import { renderFollowup } from "./lib/renderFollowup.mjs";
19
20
  import { renderPrompt } from "./lib/render.mjs";
20
21
  import { resolveRole } from "./lib/resolve.mjs";
@@ -33,6 +34,10 @@ async function main(argv) {
33
34
  return renderCommand(flags);
34
35
  }
35
36
 
37
+ if (command === "prepare") {
38
+ return prepareCommand(flags);
39
+ }
40
+
36
41
  if (command === "dispatch") {
37
42
  return dispatchCommand(flags);
38
43
  }
@@ -84,6 +89,49 @@ async function main(argv) {
84
89
  }
85
90
  }
86
91
 
92
+ function prepareCommand(flags) {
93
+ if (typeof flags.role !== "string" || flags.role.length === 0) {
94
+ console.error("Missing required --role <name>");
95
+ return 1;
96
+ }
97
+
98
+ if (
99
+ typeof flags["request-file"] !== "string" ||
100
+ flags["request-file"].length === 0
101
+ ) {
102
+ console.error("Missing required --request-file <path>");
103
+ return 1;
104
+ }
105
+
106
+ try {
107
+ const contracts = loadContracts();
108
+ const resolved = resolveRole({
109
+ role: flags.role,
110
+ catalog: loadCatalog(),
111
+ userConfig: loadUserConfig(),
112
+ projectConfig: loadProjectConfig(),
113
+ contracts
114
+ });
115
+ const request = JSON.parse(readFileSync(flags["request-file"], "utf8"));
116
+ const prepared = prepareDispatch({
117
+ role: flags.role,
118
+ requestFile: flags["request-file"],
119
+ request,
120
+ resolved
121
+ });
122
+
123
+ if (flags.json) {
124
+ process.stdout.write(`${JSON.stringify(prepared, null, 2)}\n`);
125
+ } else {
126
+ process.stdout.write(formatPrepared(prepared));
127
+ }
128
+ return 0;
129
+ } catch (error) {
130
+ console.error(error.message);
131
+ return 1;
132
+ }
133
+ }
134
+
87
135
  async function installHooksCommand(flags) {
88
136
  if (
89
137
  flags.root !== undefined &&
@@ -292,6 +340,14 @@ function formatTable(value) {
292
340
  .join("\n")}\n`;
293
341
  }
294
342
 
343
+ function formatPrepared(value) {
344
+ if (value.action === "dispatch") {
345
+ return `${value.command.join(" ")}\n`;
346
+ }
347
+
348
+ return value.prompt;
349
+ }
350
+
295
351
  function findContract(role, contracts) {
296
352
  return contracts.roles.find((contract) => contract.role === role);
297
353
  }
@@ -310,6 +366,9 @@ function usage() {
310
366
  console.error(" crew-agent-runner build [--root <path>]");
311
367
  console.error(" crew-agent-runner validate [--root <path>]");
312
368
  console.error(" crew-agent-runner install-hooks [--root <path>]");
369
+ console.error(
370
+ " crew-agent-runner prepare --role <name> --request-file <path> [--json]"
371
+ );
313
372
  console.error(" crew-agent-runner render --role <name> --request-file <path>");
314
373
  console.error(
315
374
  " crew-agent-runner render-followup --previous-result <file> --new-input <file>"
@@ -0,0 +1,37 @@
1
+ import { renderPrompt } from "./render.mjs";
2
+ import { pluginPath } from "./pluginRoot.mjs";
3
+
4
+ export function prepareDispatch({ role, requestFile, request, resolved }) {
5
+ if (resolved.provider === "codex") {
6
+ return {
7
+ role,
8
+ provider: "codex",
9
+ action: "dispatch",
10
+ command: [
11
+ "node",
12
+ pluginPath("scripts", "crew-agent-runner.mjs"),
13
+ "dispatch",
14
+ "--role",
15
+ role,
16
+ "--request-file",
17
+ requestFile,
18
+ "--json"
19
+ ],
20
+ resolved
21
+ };
22
+ }
23
+
24
+ return {
25
+ role,
26
+ provider: "claude",
27
+ action: "agent",
28
+ subagent_type: role,
29
+ model: resolved.model,
30
+ prompt: renderPrompt({
31
+ role,
32
+ request,
33
+ contract: resolved.contract
34
+ }),
35
+ resolved
36
+ };
37
+ }
@@ -7,7 +7,8 @@ export function renderPrompt(input) {
7
7
  section("Outputs", renderJson(contract.outputs)),
8
8
  section("Instructions", request.instruction, { required: true }),
9
9
  section("Success Gate", request.successGate),
10
- section("Failure Handling", request.failureHandling)
10
+ section("Failure Handling", request.failureHandling),
11
+ section("AgentResult Contract", renderAgentResultContract())
11
12
  ].filter(Boolean);
12
13
 
13
14
  return `${parts.join("\n\n")}\n`;
@@ -72,6 +73,33 @@ function renderJson(value) {
72
73
  return JSON.stringify(value, null, 2);
73
74
  }
74
75
 
76
+ function renderAgentResultContract() {
77
+ return [
78
+ "Return exactly one final AgentResult JSON object wrapped in these tags:",
79
+ "",
80
+ "```text",
81
+ "<crew-agent-result>",
82
+ "{",
83
+ ' "status": "complete | blocked_on_user | needs_agent | needs_tool | failed",',
84
+ ' "artifact": null,',
85
+ ' "questions": [],',
86
+ ' "requests": [],',
87
+ ' "summary": "short summary",',
88
+ ' "error": null',
89
+ "}",
90
+ "</crew-agent-result>",
91
+ "```",
92
+ "",
93
+ "Rules:",
94
+ "- The wrapper tags are mandatory.",
95
+ "- The JSON inside the tags must be valid JSON.",
96
+ "- Use complete when the requested artifact is ready.",
97
+ "- Use blocked_on_user only with a non-empty questions array.",
98
+ "- Use needs_agent or needs_tool only with a non-empty requests array.",
99
+ "- Use failed with an error string when the task cannot continue."
100
+ ].join("\n");
101
+ }
102
+
75
103
  function normalizeBody(body) {
76
104
  if (body === undefined || body === null) {
77
105
  return "";
@@ -0,0 +1,93 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ export const RUNNER_DISPATCH_MARKER =
5
+ "중앙 `crew-agent-runner` 스킬의 dispatch 절차로 실행한다.";
6
+
7
+ export const COMMON_AGENT_INTERFACE_HEADING =
8
+ "## 공통 에이전트 실행 인터페이스";
9
+
10
+ export const REQUIRED_INTERFACE_MARKERS = [
11
+ COMMON_AGENT_INTERFACE_HEADING,
12
+ 'node "$CLAUDE_PLUGIN_ROOT/scripts/crew-agent-runner.mjs" prepare --role <role> --request-file <request-file> --json',
13
+ "request-file",
14
+ "action == dispatch",
15
+ "action == agent",
16
+ "직접 하위 에이전트를 호출하지 않는다",
17
+ "AgentResult"
18
+ ];
19
+
20
+ const FORBIDDEN_DIRECT_DISPATCH_PATTERNS = [
21
+ /Agent\(/,
22
+ /Bash\(/,
23
+ new RegExp(["crew", "codex", "companion"].join("-")),
24
+ /runAgent\(/,
25
+ new RegExp(["AskUser", "Question"].join("")),
26
+ new RegExp(`<${["crew", "agent", "result"].join("-")}>`)
27
+ ];
28
+
29
+ export async function validateWorkflowSkillDispatchContracts({
30
+ root = process.cwd()
31
+ } = {}) {
32
+ const skillsDir = join(root, "skills");
33
+ const errors = [];
34
+ let entries = [];
35
+
36
+ try {
37
+ entries = await readdir(skillsDir, { withFileTypes: true });
38
+ } catch (error) {
39
+ if (error?.code === "ENOENT") {
40
+ return errors;
41
+ }
42
+ throw error;
43
+ }
44
+
45
+ for (const entry of entries) {
46
+ if (!entry.isDirectory()) {
47
+ continue;
48
+ }
49
+
50
+ const relPath = `skills/${entry.name}/SKILL.md`;
51
+ const text = await readUtf8OrNull(join(root, relPath));
52
+ if (!text || entry.name === "crew-agent-runner") {
53
+ continue;
54
+ }
55
+
56
+ if (!text.includes(RUNNER_DISPATCH_MARKER)) {
57
+ continue;
58
+ }
59
+
60
+ errors.push(...validateWorkflowSkillDispatchContract(text, relPath));
61
+ }
62
+
63
+ return errors;
64
+ }
65
+
66
+ export function validateWorkflowSkillDispatchContract(text, relPath = "SKILL.md") {
67
+ const errors = [];
68
+
69
+ for (const pattern of FORBIDDEN_DIRECT_DISPATCH_PATTERNS) {
70
+ if (pattern.test(text)) {
71
+ errors.push(`${relPath}: direct agent dispatch is forbidden: ${pattern}`);
72
+ }
73
+ }
74
+
75
+ for (const marker of REQUIRED_INTERFACE_MARKERS) {
76
+ if (!text.includes(marker)) {
77
+ errors.push(`${relPath}: missing runner dispatch interface marker: ${marker}`);
78
+ }
79
+ }
80
+
81
+ return errors;
82
+ }
83
+
84
+ async function readUtf8OrNull(path) {
85
+ try {
86
+ return await readFile(path, "utf8");
87
+ } catch (error) {
88
+ if (error?.code === "ENOENT") {
89
+ return null;
90
+ }
91
+ throw error;
92
+ }
93
+ }
@@ -3,6 +3,7 @@ import { join, relative, resolve } from "node:path";
3
3
 
4
4
  import { deriveBuildOutput, resolveBuildInputs } from "./build.mjs";
5
5
  import { loadContracts } from "./contracts.mjs";
6
+ import { validateWorkflowSkillDispatchContracts } from "./skillDispatchContract.mjs";
6
7
 
7
8
  export async function validate({ root = process.cwd() } = {}) {
8
9
  const projectRoot = resolve(root);
@@ -26,6 +27,9 @@ export async function validate({ root = process.cwd() } = {}) {
26
27
  }))
27
28
  );
28
29
  errors.push(...compareSandboxConsistency({ contracts, catalog }));
30
+ errors.push(
31
+ ...(await validateWorkflowSkillDispatchContracts({ root: projectRoot }))
32
+ );
29
33
 
30
34
  return { ok: errors.length === 0, errors };
31
35
  }
@@ -5,7 +5,7 @@ description: 모든 crew 에이전트 dispatch의 중앙 규약 — provider별
5
5
 
6
6
  # crew-agent-runner
7
7
 
8
- crew 업무 스킬은 에이전트 provider별 호출 세부사항을 직접 구현하지 않고 이 중앙 규약을 따른다. 본 스킬은 resolve, dispatch, resume, followup 주입, retry/fallback/escalate 판단의 공통 표면을 정의한다.
8
+ crew 업무 스킬은 에이전트 provider별 호출 세부사항을 직접 구현하지 않고 이 중앙 규약을 따른다. 본 스킬은 prepare, resolve, dispatch, resume, followup 주입, retry/fallback/escalate 판단의 공통 표면을 정의한다.
9
9
 
10
10
  설치 후 drift 차단용 pre-commit hook은 `node scripts/crew-agent-runner.mjs install-hooks`로 설치한다.
11
11
  (plugin 개발자 전용 — 사용자는 호출하지 않습니다. build/validate는 plugin source repo의 drift 차단 도구입니다.)
@@ -14,26 +14,24 @@ crew 업무 스킬은 에이전트 provider별 호출 세부사항을 직접 구
14
14
 
15
15
  업무 스킬(crew-plan/crew-interview/crew-dev)이 role을 실행해야 할 때 본 절차를 따른다.
16
16
 
17
- ### 1. resolve
17
+ ### 1. request 객체 작성
18
18
 
19
- 오케스트레이터는 먼저 `node scripts/crew-agent-runner.mjs resolve --role <role> --json`을 실행하여 provider/model/contract 통합 표를 받는다.
19
+ `{ role, inputs (path+content), instruction, successGate, failureHandling, taskId }` 형태의 임시 JSON 파일을 작성한다.
20
20
 
21
- ### 2. request 객체 작성
21
+ ### 2. prepare
22
22
 
23
- `{ role, inputs (path+content), instruction, successGate, failureHandling, taskId }` 형태의 임시 JSON 파일을 작성한다.
23
+ 오케스트레이터는 `node "$CLAUDE_PLUGIN_ROOT/scripts/crew-agent-runner.mjs" prepare --role <role> --request-file <request-file> --json`을 실행한다.
24
+ prepare는 provider/model/contract를 해석하고 다음 action 중 하나를 반환한다.
24
25
 
25
- ### 3a. Codex 경로
26
+ ### 3a. dispatch action
26
27
 
27
- `provider == codex`이면 `node scripts/crew-agent-runner.mjs dispatch --role <role> --request-file <path> --json`을 실행한다. 명령은 AgentResult JSON을 즉시 반환한다.
28
- dispatch CLI는 codex provider role에만 사용. claude provider role은 render + Agent tool 경로를 사용.
28
+ `action == dispatch`이면 prepare가 반환한 command를 실행한다. 경로는 Codex provider role에만 사용하며 AgentResult JSON을 즉시 반환한다.
29
29
 
30
- ### 3b. Claude 경로
30
+ ### 3b. agent action
31
31
 
32
- `provider == claude`이면 다음 순서로 실행한다.
32
+ `action == agent`이면 prepare가 반환한 `subagent_type`, `model`, `prompt`로 메인 오케스트레이터가 Claude provider 역할을 실행하고, sub-agent 결과를 AgentResult JSON 형식으로 정규화한다.
33
33
 
34
- 1. `node scripts/crew-agent-runner.mjs render --role <role> --request-file <path>`를 실행하여 prompt 문자열을 받는다.
35
- 2. 메인 오케스트레이터(Claude conversation)가 `Agent(subagent_type=<role>, model=<model>, prompt=<rendered>)`를 호출한다.
36
- 3. sub-agent 결과를 AgentResult JSON 형식으로 정규화한다.
34
+ 주의: 업무 스킬은 직접 provider를 분기하거나 직접 하위 에이전트를 호출하지 않는다. 항상 prepare 결과의 action만 수행한다.
37
35
 
38
36
  ## AgentResult 상태 처리
39
37
 
@@ -23,6 +23,19 @@ description: contract.md를 입력으로 받아 Dev + CodeReviewer + QA 파이
23
23
  에이전트가 사용자 입력이 필요하다고 반환하면 오케스트레이터가 사용자에게 질문한다.
24
24
  에이전트가 추가 역할 실행이나 허용 도구 실행을 요청하면 오케스트레이터가 runner 정책에 따라 처리하고 같은 역할 실행에 후속 입력으로 주입한다.
25
25
 
26
+ ## 공통 에이전트 실행 인터페이스
27
+
28
+ crew-dev의 모든 에이전트 실행은 역할이나 phase와 무관하게 아래 인터페이스만 사용한다.
29
+ 오케스트레이터는 `Dev`, `CodeReviewer`, `QA`, 후속 요청 role을 실행할 때마다 이 순서를 반복한다.
30
+
31
+ 1. `{ role, taskId, inputs, instruction, successGate, failureHandling }` 형태의 `request-file`을 작성한다.
32
+ 2. `node "$CLAUDE_PLUGIN_ROOT/scripts/crew-agent-runner.mjs" prepare --role <role> --request-file <request-file> --json`을 실행한다.
33
+ 3. `action == dispatch`이면 prepare가 반환한 command를 실행하고 AgentResult를 처리한다.
34
+ 4. `action == agent`이면 prepare가 반환한 `subagent_type`, `model`, `prompt`로 runner 계약의 Claude 경로를 실행하고 AgentResult로 정규화한다.
35
+
36
+ 이 순서를 생략하고 직접 하위 에이전트를 호출하지 않는다.
37
+ provider 선택, 런타임 선택, AgentResult 반환 형식, 후속 입력 주입, retry/fallback/escalate 판단은 모두 중앙 runner 계약을 따른다.
38
+
26
39
  ---
27
40
 
28
41
  ## 절대 금지
@@ -36,6 +36,19 @@ description: 유저 요구사항을 인터뷰하여 개발 가능한 수준의 s
36
36
 
37
37
  에이전트 실행은 모두 중앙 `crew-agent-runner` 스킬의 dispatch 절차로 실행한다. 이 문서는 어떤 런타임을 어떻게 호출하는지가 아니라, 각 Phase가 어떤 역할에게 어떤 입력을 주고 어떤 결과를 받아야 하는지만 정의한다.
38
38
 
39
+ ## 공통 에이전트 실행 인터페이스
40
+
41
+ crew-interview의 모든 에이전트 실행은 역할이나 phase와 무관하게 아래 인터페이스만 사용한다.
42
+ 오케스트레이터는 `pm`, `explorer`, `researcher`, 후속 요청 role을 실행할 때마다 이 순서를 반복한다.
43
+
44
+ 1. `{ role, taskId, inputs, instruction, successGate, failureHandling }` 형태의 `request-file`을 작성한다.
45
+ 2. `node "$CLAUDE_PLUGIN_ROOT/scripts/crew-agent-runner.mjs" prepare --role <role> --request-file <request-file> --json`을 실행한다.
46
+ 3. `action == dispatch`이면 prepare가 반환한 command를 실행하고 AgentResult를 처리한다.
47
+ 4. `action == agent`이면 prepare가 반환한 `subagent_type`, `model`, `prompt`로 runner 계약의 Claude 경로를 실행하고 AgentResult로 정규화한다.
48
+
49
+ 이 순서를 생략하고 직접 하위 에이전트를 호출하지 않는다.
50
+ provider 선택, 런타임 선택, AgentResult 반환 형식, 후속 입력 주입, retry/fallback/escalate 판단은 모두 중앙 runner 계약을 따른다.
51
+
39
52
  ### Phase 1 — 초기화
40
53
 
41
54
  중앙 `crew-agent-runner` 스킬의 dispatch 절차로 실행한다.
@@ -42,6 +42,19 @@ crew-interview가 생성한 spec.md를 입력으로 받아 **HOW(어떻게 만
42
42
 
43
43
  각 에이전트 단계는 중앙 `crew-agent-runner` 스킬의 dispatch 절차로 실행한다. 이 문서는 역할, 입력, 기대 산출물, 검증 기준만 정의하며 실행 방식은 runner 계약을 따른다.
44
44
 
45
+ ## 공통 에이전트 실행 인터페이스
46
+
47
+ crew-plan의 모든 에이전트 실행은 역할이나 step과 무관하게 아래 인터페이스만 사용한다.
48
+ 오케스트레이터는 `techlead`, `planner`, `plan-evaluator`, 후속 요청 role을 실행할 때마다 이 순서를 반복한다.
49
+
50
+ 1. `{ role, taskId, inputs, instruction, successGate, failureHandling }` 형태의 `request-file`을 작성한다.
51
+ 2. `node "$CLAUDE_PLUGIN_ROOT/scripts/crew-agent-runner.mjs" prepare --role <role> --request-file <request-file> --json`을 실행한다.
52
+ 3. `action == dispatch`이면 prepare가 반환한 command를 실행하고 AgentResult를 처리한다.
53
+ 4. `action == agent`이면 prepare가 반환한 `subagent_type`, `model`, `prompt`로 runner 계약의 Claude 경로를 실행하고 AgentResult로 정규화한다.
54
+
55
+ 이 순서를 생략하고 직접 하위 에이전트를 호출하지 않는다.
56
+ provider 선택, 런타임 선택, AgentResult 반환 형식, 후속 입력 주입, retry/fallback/escalate 판단은 모두 중앙 runner 계약을 따른다.
57
+
45
58
  ### Step 1 — spec.md 검증
46
59
 
47
60
  role: orchestrator