@jjlabsio/claude-crew 0.1.33 → 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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +8 -7
- package/README.md +22 -0
- package/agents/code-reviewer.md +7 -0
- package/agents/dev.md +7 -0
- package/agents/explorer.md +7 -0
- package/agents/plan-evaluator.md +7 -0
- package/agents/planner.md +7 -0
- package/agents/pm.md +7 -0
- package/agents/qa.md +8 -1
- package/agents/researcher.md +7 -0
- package/agents/techlead.md +7 -0
- package/data/agent-contracts.json +350 -0
- package/data/agent-instructions/code-reviewer.md +47 -0
- package/data/agent-instructions/dev.md +48 -0
- package/data/agent-instructions/explorer.md +14 -0
- package/data/agent-instructions/plan-evaluator.md +68 -0
- package/data/agent-instructions/planner.md +73 -0
- package/data/agent-instructions/pm.md +47 -0
- package/data/agent-instructions/qa.md +65 -0
- package/data/agent-instructions/researcher.md +15 -0
- package/data/agent-instructions/techlead.md +66 -0
- package/hooks/enforce-delegation.mjs +51 -0
- package/package.json +8 -3
- package/scripts/crew-agent-runner.mjs +382 -0
- package/scripts/lib/build.mjs +213 -0
- package/scripts/lib/cli.mjs +30 -0
- package/scripts/lib/config.mjs +33 -0
- package/scripts/lib/contracts.mjs +146 -0
- package/scripts/lib/dispatch.mjs +241 -0
- package/scripts/lib/installHooks.mjs +136 -0
- package/scripts/lib/pluginRoot.mjs +10 -0
- package/scripts/lib/prepare.mjs +37 -0
- package/scripts/lib/render.mjs +138 -0
- package/scripts/lib/renderFollowup.mjs +51 -0
- package/scripts/lib/resolve.mjs +72 -0
- package/scripts/lib/skillDispatchContract.mjs +93 -0
- package/scripts/lib/validate.mjs +104 -0
- package/skills/crew-agent-runner/SKILL.md +113 -0
- package/skills/crew-dev/SKILL.md +171 -776
- package/skills/crew-interview/SKILL.md +137 -57
- package/skills/crew-plan/SKILL.md +224 -460
- package/skills/crew-setup/SKILL.md +32 -19
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { fenceBlock } from "./render.mjs";
|
|
2
|
+
|
|
3
|
+
export function renderFollowup({ previousResult, newInput } = {}) {
|
|
4
|
+
return [
|
|
5
|
+
"## 이전 결과",
|
|
6
|
+
`status: ${normalizeInline(previousResult?.status)}`,
|
|
7
|
+
`summary: ${normalizeInline(previousResult?.summary)}`,
|
|
8
|
+
"artifact:",
|
|
9
|
+
fenceBlock(serializeArtifact(previousResult?.artifact)),
|
|
10
|
+
"",
|
|
11
|
+
"## 추가 입력",
|
|
12
|
+
normalizeBlock(newInput),
|
|
13
|
+
"",
|
|
14
|
+
"## 지시",
|
|
15
|
+
"계속 진행해라."
|
|
16
|
+
].join("\n") + "\n";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function serializeArtifact(artifact) {
|
|
20
|
+
if (artifact === undefined || artifact === null) {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof artifact === "string") {
|
|
25
|
+
return artifact;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return JSON.stringify(artifact, null, 2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeInline(value) {
|
|
32
|
+
if (value === undefined || value === null) {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return String(value)
|
|
37
|
+
.replace(/^\uFEFF/, "")
|
|
38
|
+
.replace(/\r\n?/g, "\n")
|
|
39
|
+
.replace(/\n/g, " ");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeBlock(value) {
|
|
43
|
+
if (value === undefined || value === null) {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return String(value)
|
|
48
|
+
.replace(/^\uFEFF/, "")
|
|
49
|
+
.replace(/\r\n?/g, "\n")
|
|
50
|
+
.replace(/\n+$/g, "");
|
|
51
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const PROVIDERS = new Set(["claude", "codex"]);
|
|
2
|
+
|
|
3
|
+
export function resolveRole(input) {
|
|
4
|
+
const { role, catalog, userConfig = {}, projectConfig = {}, contracts } = input;
|
|
5
|
+
const contract = findContract(role, contracts);
|
|
6
|
+
|
|
7
|
+
if (!contract) {
|
|
8
|
+
throw new Error(`Unknown role: ${role}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const defaults = cascadeRoleConfig(role, catalog, userConfig, projectConfig);
|
|
12
|
+
const provider = defaults.provider;
|
|
13
|
+
if (!PROVIDERS.has(provider)) {
|
|
14
|
+
throw new Error(`Unknown provider for role ${role}: ${provider}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const model = defaults.model;
|
|
18
|
+
if (typeof model !== "string" || model.length === 0) {
|
|
19
|
+
throw new Error(`Missing model for role ${role}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const codexSandbox = catalog?.agent_runtime?.[role]?.codex_sandbox;
|
|
23
|
+
if (!["read-only", "workspace-write"].includes(codexSandbox)) {
|
|
24
|
+
throw new Error(`Missing codex_sandbox for role ${role}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const warnings = [];
|
|
28
|
+
const workspaceAccess = contract?.capabilities?.workspaceAccess;
|
|
29
|
+
if (codexSandbox !== workspaceAccess) {
|
|
30
|
+
warnings.push(
|
|
31
|
+
`${role}: codex_sandbox ${codexSandbox} does not match contract capabilities.workspaceAccess ${workspaceAccess}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
role,
|
|
37
|
+
provider,
|
|
38
|
+
model,
|
|
39
|
+
reasoning: provider === "codex" ? defaults.reasoning ?? null : null,
|
|
40
|
+
codex_sandbox: codexSandbox,
|
|
41
|
+
contract,
|
|
42
|
+
dispatch: {
|
|
43
|
+
path: provider === "codex" ? "codex" : "claude",
|
|
44
|
+
write: codexSandbox === "workspace-write"
|
|
45
|
+
},
|
|
46
|
+
warnings
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function cascadeRoleConfig(role, ...configs) {
|
|
51
|
+
return configs.reduce((merged, config) => {
|
|
52
|
+
return {
|
|
53
|
+
...merged,
|
|
54
|
+
...roleConfig(config, role)
|
|
55
|
+
};
|
|
56
|
+
}, {});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function roleConfig(config, role) {
|
|
60
|
+
return {
|
|
61
|
+
...(config?.agent_defaults?.[role] ?? {}),
|
|
62
|
+
...(config?.providers?.[role] ?? {})
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function findContract(role, contracts) {
|
|
67
|
+
if (Array.isArray(contracts?.roles)) {
|
|
68
|
+
return contracts.roles.find((contract) => contract.role === role);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return contracts?.[role];
|
|
72
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { deriveBuildOutput, resolveBuildInputs } from "./build.mjs";
|
|
5
|
+
import { loadContracts } from "./contracts.mjs";
|
|
6
|
+
import { validateWorkflowSkillDispatchContracts } from "./skillDispatchContract.mjs";
|
|
7
|
+
|
|
8
|
+
export async function validate({ root = process.cwd() } = {}) {
|
|
9
|
+
const projectRoot = resolve(root);
|
|
10
|
+
const inputs = resolveBuildInputs(projectRoot);
|
|
11
|
+
const errors = [];
|
|
12
|
+
|
|
13
|
+
const contracts = loadContracts(inputs.contractsPath);
|
|
14
|
+
const catalog = JSON.parse(await readFile(inputs.catalogPath, "utf8"));
|
|
15
|
+
const derived = await deriveBuildOutput({
|
|
16
|
+
root: projectRoot,
|
|
17
|
+
contracts,
|
|
18
|
+
instructionsDir: inputs.instructionsDir,
|
|
19
|
+
pluginPath: inputs.pluginPath
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
errors.push(
|
|
23
|
+
...(await compareDerived({
|
|
24
|
+
root: projectRoot,
|
|
25
|
+
pluginPath: inputs.pluginPath,
|
|
26
|
+
derived
|
|
27
|
+
}))
|
|
28
|
+
);
|
|
29
|
+
errors.push(...compareSandboxConsistency({ contracts, catalog }));
|
|
30
|
+
errors.push(
|
|
31
|
+
...(await validateWorkflowSkillDispatchContracts({ root: projectRoot }))
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return { ok: errors.length === 0, errors };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function compareDerived({ root, pluginPath, derived }) {
|
|
38
|
+
const errors = [];
|
|
39
|
+
|
|
40
|
+
for (const [role, expected] of derived.agents.entries()) {
|
|
41
|
+
const relPath = `agents/${role}.md`;
|
|
42
|
+
const actual = await readUtf8OrNull(join(root, relPath));
|
|
43
|
+
if (actual !== expected) {
|
|
44
|
+
errors.push(`drift: ${relPath}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const actualPlugin = await readUtf8OrNull(pluginPath);
|
|
49
|
+
if (actualPlugin !== derived.pluginJson) {
|
|
50
|
+
errors.push(await pluginDriftMessage({ root, pluginPath, actualPlugin }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return errors;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function compareSandboxConsistency({ contracts, catalog }) {
|
|
57
|
+
const errors = [];
|
|
58
|
+
|
|
59
|
+
for (const contract of contracts.roles) {
|
|
60
|
+
const role = contract.role;
|
|
61
|
+
const workspaceAccess = contract.capabilities?.workspaceAccess;
|
|
62
|
+
const codexSandbox = catalog.agent_runtime?.[role]?.codex_sandbox;
|
|
63
|
+
if (workspaceAccess !== codexSandbox) {
|
|
64
|
+
errors.push(
|
|
65
|
+
`mismatch: ${role} workspaceAccess=${workspaceAccess} but codex_sandbox=${codexSandbox}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return errors;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function pluginDriftMessage({ root, pluginPath, actualPlugin }) {
|
|
74
|
+
const label = pluginLabel(root, pluginPath);
|
|
75
|
+
if (actualPlugin === null) {
|
|
76
|
+
return `drift: ${label}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const plugin = JSON.parse(actualPlugin);
|
|
81
|
+
if (!Array.isArray(plugin.agents)) {
|
|
82
|
+
return `drift: ${label} agents`;
|
|
83
|
+
}
|
|
84
|
+
return `drift: ${label} agents`;
|
|
85
|
+
} catch {
|
|
86
|
+
return `drift: ${label}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function pluginLabel(root, pluginPath) {
|
|
91
|
+
const relPath = relative(root, pluginPath);
|
|
92
|
+
return relPath === ".claude-plugin/plugin.json" ? "plugin.json" : relPath;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function readUtf8OrNull(path) {
|
|
96
|
+
try {
|
|
97
|
+
return await readFile(path, "utf8");
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error?.code === "ENOENT") {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crew-agent-runner
|
|
3
|
+
description: 모든 crew 에이전트 dispatch의 중앙 규약 — provider별 호출법 캡슐화
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# crew-agent-runner
|
|
7
|
+
|
|
8
|
+
crew 업무 스킬은 에이전트 provider별 호출 세부사항을 직접 구현하지 않고 이 중앙 규약을 따른다. 본 스킬은 prepare, resolve, dispatch, resume, followup 주입, retry/fallback/escalate 판단의 공통 표면을 정의한다.
|
|
9
|
+
|
|
10
|
+
설치 후 drift 차단용 pre-commit hook은 `node scripts/crew-agent-runner.mjs install-hooks`로 설치한다.
|
|
11
|
+
(plugin 개발자 전용 — 사용자는 호출하지 않습니다. build/validate는 plugin source repo의 drift 차단 도구입니다.)
|
|
12
|
+
|
|
13
|
+
## Dispatch 절차
|
|
14
|
+
|
|
15
|
+
업무 스킬(crew-plan/crew-interview/crew-dev)이 role을 실행해야 할 때 본 절차를 따른다.
|
|
16
|
+
|
|
17
|
+
### 1. request 객체 작성
|
|
18
|
+
|
|
19
|
+
`{ role, inputs (path+content), instruction, successGate, failureHandling, taskId }` 형태의 임시 JSON 파일을 작성한다.
|
|
20
|
+
|
|
21
|
+
### 2. prepare
|
|
22
|
+
|
|
23
|
+
오케스트레이터는 `node "$CLAUDE_PLUGIN_ROOT/scripts/crew-agent-runner.mjs" prepare --role <role> --request-file <request-file> --json`을 실행한다.
|
|
24
|
+
prepare는 provider/model/contract를 해석하고 다음 action 중 하나를 반환한다.
|
|
25
|
+
|
|
26
|
+
### 3a. dispatch action
|
|
27
|
+
|
|
28
|
+
`action == dispatch`이면 prepare가 반환한 command를 실행한다. 이 경로는 Codex provider role에만 사용하며 AgentResult JSON을 즉시 반환한다.
|
|
29
|
+
|
|
30
|
+
### 3b. agent action
|
|
31
|
+
|
|
32
|
+
`action == agent`이면 prepare가 반환한 `subagent_type`, `model`, `prompt`로 메인 오케스트레이터가 Claude provider 역할을 실행하고, sub-agent 결과를 AgentResult JSON 형식으로 정규화한다.
|
|
33
|
+
|
|
34
|
+
주의: 업무 스킬은 직접 provider를 분기하거나 직접 하위 에이전트를 호출하지 않는다. 항상 prepare 결과의 action만 수행한다.
|
|
35
|
+
|
|
36
|
+
## AgentResult 상태 처리
|
|
37
|
+
|
|
38
|
+
AgentResult 5상태:
|
|
39
|
+
|
|
40
|
+
### complete
|
|
41
|
+
|
|
42
|
+
`artifact`를 `outputs.target`에 저장한다. 이후 다음 phase로 진행한다.
|
|
43
|
+
|
|
44
|
+
### blocked_on_user
|
|
45
|
+
|
|
46
|
+
`questions`를 메인 오케스트레이터의 사용자 질문 도구로 사용자에게 전달한다. 답변 수신 후 followup을 주입한다. 절차는 아래 Resume 섹션을 따른다.
|
|
47
|
+
|
|
48
|
+
### needs_agent
|
|
49
|
+
|
|
50
|
+
`requests`의 `role`을 새 dispatch 사이클로 실행한다. 결과를 followup으로 원래 에이전트에 주입한다.
|
|
51
|
+
|
|
52
|
+
### needs_tool
|
|
53
|
+
|
|
54
|
+
capability를 넘어선 도구 요청이다. 오케스트레이터가 `contract.policy`에 따라 직접 도구 실행 후 결과를 주입하거나, 실행할 수 없으면 `failed`로 escalate한다.
|
|
55
|
+
|
|
56
|
+
### failed
|
|
57
|
+
|
|
58
|
+
`contract.policy`의 `maxAttempts`, `fallbackProvider`, `escalateAfterAttempts`, `consecutiveSameFailureLimit`에 따라 처리한다.
|
|
59
|
+
|
|
60
|
+
- retry: `maxAttempts` 미만이면 재시도한다.
|
|
61
|
+
- fallbackProvider 전환: fallback provider가 있으면 provider를 전환한다.
|
|
62
|
+
- 사용자 escalate: 한도에 도달했거나 같은 사유가 반복되면 사용자에게 에스컬레이션한다.
|
|
63
|
+
|
|
64
|
+
## Resume
|
|
65
|
+
|
|
66
|
+
`needs_agent` / `blocked_on_user` 상태에서 같은 sub-agent context를 이어가는 표준 절차.
|
|
67
|
+
|
|
68
|
+
### Codex 경로
|
|
69
|
+
|
|
70
|
+
1. `node scripts/crew-agent-runner.mjs render-followup --previous-result <file> --new-input <file>` 실행 → followup prompt 문자열 → 임시 파일에 저장.
|
|
71
|
+
2. `node scripts/crew-agent-runner.mjs dispatch --role <role> --request-file <new-request-with-followup-prompt> --resume-handle <agent_handle> --json` 실행.
|
|
72
|
+
- 내부적으로 runner가 `crew-codex-companion.mjs task-resume-candidate`로 thread 일치 검증 후 `task --resume-last`를 호출하고 AgentResult를 정규화한다.
|
|
73
|
+
3. AgentResult JSON을 받아 다음 상태 처리.
|
|
74
|
+
|
|
75
|
+
주의: 직접 `crew-codex-companion.mjs task --resume-last`를 호출하지 말 것. runner의 candidate guard와 AgentResult 정규화를 건너뛴다.
|
|
76
|
+
|
|
77
|
+
### Claude 경로
|
|
78
|
+
|
|
79
|
+
1. `Agent` spawn으로 받은 sub-agent handle을 보존한다.
|
|
80
|
+
2. `render-followup`을 사용하여 followup prompt를 생성한다.
|
|
81
|
+
3. 메인 conversation에서 `SendMessage(to: <agent-handle>, message: <followup-prompt>)`로 같은 sub-agent에 후속 turn을 전달한다.
|
|
82
|
+
|
|
83
|
+
## Followup 주입
|
|
84
|
+
|
|
85
|
+
Followup prompt 형식은 양 provider에서 동일하다.
|
|
86
|
+
|
|
87
|
+
```markdown
|
|
88
|
+
## 이전 결과
|
|
89
|
+
status: <status>
|
|
90
|
+
summary: <summary>
|
|
91
|
+
artifact:
|
|
92
|
+
---
|
|
93
|
+
<artifact>
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 추가 입력
|
|
97
|
+
<new-input>
|
|
98
|
+
|
|
99
|
+
## 지시
|
|
100
|
+
계속 진행해라.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
이 형식은 `runner.mjs render-followup`이 결정론적으로 생성한다.
|
|
104
|
+
artifact가 객체/배열인 경우 runner가 JSON으로 직렬화하여 fence 블록 안에 삽입한다 (정보 손실 방지).
|
|
105
|
+
|
|
106
|
+
## Retry / Fallback / Escalate
|
|
107
|
+
|
|
108
|
+
`contract.policy`:
|
|
109
|
+
|
|
110
|
+
- `maxAttempts`: retry 횟수 한도
|
|
111
|
+
- `fallbackProvider`: `claude`/`codex` 전환
|
|
112
|
+
- `escalateAfterAttempts`: 한도 도달 시 사용자 escalate
|
|
113
|
+
- `consecutiveSameFailureLimit`: 같은 사유 연속 fail 한도
|