@jjlabsio/claude-crew 0.1.39 → 0.1.41
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 +1 -1
- package/package.json +1 -1
- package/scripts/crew-agent-runner.mjs +66 -0
- package/scripts/lib/artifacts.mjs +89 -0
- package/scripts/lib/dispatch.mjs +24 -0
- package/scripts/lib/render.mjs +23 -7
- package/skills/crew-interview/SKILL.md +7 -4
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"name": "claude-crew",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"description": "오케스트레이터 + PM, 플래너, 개발, QA, 마케팅 에이전트 팀으로 단일 제품의 개발과 마케팅을 통합 관리",
|
|
14
|
-
"version": "0.1.
|
|
14
|
+
"version": "0.1.41",
|
|
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.
|
|
31
|
+
"version": "0.1.41"
|
|
32
32
|
}
|
package/package.json
CHANGED
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
loadUserConfig
|
|
10
10
|
} from "./lib/config.mjs";
|
|
11
11
|
import { parseArgv } from "./lib/cli.mjs";
|
|
12
|
+
import {
|
|
13
|
+
persistCrewArtifact,
|
|
14
|
+
ArtifactPersistError
|
|
15
|
+
} from "./lib/artifacts.mjs";
|
|
12
16
|
import {
|
|
13
17
|
dispatch,
|
|
14
18
|
DispatchError,
|
|
@@ -42,6 +46,10 @@ async function main(argv) {
|
|
|
42
46
|
return dispatchCommand(flags);
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
if (command === "persist-artifact") {
|
|
50
|
+
return persistArtifactCommand(flags);
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
if (command === "render-followup") {
|
|
46
54
|
return renderFollowupCommand(flags);
|
|
47
55
|
}
|
|
@@ -224,6 +232,61 @@ function renderFollowupCommand(flags) {
|
|
|
224
232
|
}
|
|
225
233
|
}
|
|
226
234
|
|
|
235
|
+
async function persistArtifactCommand(flags) {
|
|
236
|
+
if (typeof flags.role !== "string" || flags.role.length === 0) {
|
|
237
|
+
console.error("Missing required --role <name>");
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (
|
|
242
|
+
typeof flags["result-file"] !== "string" ||
|
|
243
|
+
flags["result-file"].length === 0
|
|
244
|
+
) {
|
|
245
|
+
console.error("Missing required --result-file <path>");
|
|
246
|
+
return 1;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (
|
|
250
|
+
typeof flags["request-file"] !== "string" ||
|
|
251
|
+
flags["request-file"].length === 0
|
|
252
|
+
) {
|
|
253
|
+
console.error("Missing required --request-file <path>");
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const contracts = loadContracts();
|
|
259
|
+
const contract = findContract(flags.role, contracts);
|
|
260
|
+
if (!contract) {
|
|
261
|
+
throw new Error(`Unknown role: ${flags.role}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const agentResult = JSON.parse(readFileSync(flags["result-file"], "utf8"));
|
|
265
|
+
const request = JSON.parse(readFileSync(flags["request-file"], "utf8"));
|
|
266
|
+
|
|
267
|
+
const savedPath = await persistCrewArtifact({
|
|
268
|
+
workspaceRoot: process.cwd(),
|
|
269
|
+
contract,
|
|
270
|
+
request,
|
|
271
|
+
agentResult
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (savedPath) {
|
|
275
|
+
process.stdout.write(`${JSON.stringify({ artifact_path: savedPath })}\n`);
|
|
276
|
+
} else {
|
|
277
|
+
process.stdout.write(`${JSON.stringify({ artifact_path: null })}\n`);
|
|
278
|
+
}
|
|
279
|
+
return 0;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (error instanceof ArtifactPersistError) {
|
|
282
|
+
console.error(error.message);
|
|
283
|
+
return 1;
|
|
284
|
+
}
|
|
285
|
+
console.error(error.message);
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
227
290
|
async function dispatchCommand(flags) {
|
|
228
291
|
if (typeof flags.role !== "string" || flags.role.length === 0) {
|
|
229
292
|
console.error("Missing required --role <name>");
|
|
@@ -376,6 +439,9 @@ function usage() {
|
|
|
376
439
|
console.error(
|
|
377
440
|
" crew-agent-runner dispatch --role <name> --request-file <path> [--json] [--resume-handle <thread-id>]"
|
|
378
441
|
);
|
|
442
|
+
console.error(
|
|
443
|
+
" crew-agent-runner persist-artifact --role <name> --result-file <path> --request-file <path>"
|
|
444
|
+
);
|
|
379
445
|
}
|
|
380
446
|
|
|
381
447
|
const exitCode = await main(process.argv.slice(2));
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, normalize, resolve, relative } from "node:path";
|
|
3
|
+
|
|
4
|
+
export class ArtifactPersistError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ArtifactPersistError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function persistCrewArtifact({ workspaceRoot, contract, request, agentResult }) {
|
|
12
|
+
if (!shouldPersist(contract, agentResult)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const target = findArtifactTarget(contract);
|
|
17
|
+
if (!target) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const resolvedTarget = replaceTaskId(target, request?.taskId);
|
|
22
|
+
const absolutePath = validateTargetPath(workspaceRoot, resolvedTarget);
|
|
23
|
+
|
|
24
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
25
|
+
await writeFile(absolutePath, agentResult.artifact, "utf8");
|
|
26
|
+
|
|
27
|
+
return absolutePath;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function shouldPersist(contract, agentResult) {
|
|
31
|
+
if (contract?.capabilities?.workspaceAccess !== "read-only") {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (agentResult?.status !== "complete") {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof agentResult.artifact !== "string" || agentResult.artifact.length === 0) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findArtifactTarget(contract) {
|
|
47
|
+
const outputs = Array.isArray(contract?.outputs) ? contract.outputs : [];
|
|
48
|
+
for (const output of outputs) {
|
|
49
|
+
if (
|
|
50
|
+
output?.type === "artifact" &&
|
|
51
|
+
typeof output.target === "string" &&
|
|
52
|
+
output.target.startsWith(".crew/")
|
|
53
|
+
) {
|
|
54
|
+
return output.target;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function replaceTaskId(target, taskId) {
|
|
61
|
+
if (typeof taskId === "string" && taskId.length > 0) {
|
|
62
|
+
return target.replace(/\{task-id\}/g, taskId);
|
|
63
|
+
}
|
|
64
|
+
return target;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateTargetPath(workspaceRoot, target) {
|
|
68
|
+
if (!workspaceRoot || typeof workspaceRoot !== "string") {
|
|
69
|
+
throw new ArtifactPersistError("workspaceRoot is required.");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const normalized = normalize(target);
|
|
73
|
+
|
|
74
|
+
if (normalized.startsWith("/") || normalized.startsWith("\\")) {
|
|
75
|
+
throw new ArtifactPersistError(`Absolute path rejected: ${target}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const crewBase = resolve(workspaceRoot, ".crew");
|
|
79
|
+
const absolutePath = resolve(workspaceRoot, normalized);
|
|
80
|
+
const rel = relative(crewBase, absolutePath);
|
|
81
|
+
|
|
82
|
+
if (rel.startsWith("..") || rel === "") {
|
|
83
|
+
throw new ArtifactPersistError(
|
|
84
|
+
`Target path escapes .crew/ directory: ${target}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return absolutePath;
|
|
89
|
+
}
|
package/scripts/lib/dispatch.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
|
+
import { persistCrewArtifact, ArtifactPersistError } from "./artifacts.mjs";
|
|
7
8
|
import { renderPrompt } from "./render.mjs";
|
|
8
9
|
|
|
9
10
|
const DEFAULT_COMPANION = fileURLToPath(
|
|
@@ -84,6 +85,11 @@ export async function dispatch(input) {
|
|
|
84
85
|
});
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
const artifactPath = await persistArtifactSafe(input, agentResult);
|
|
89
|
+
if (artifactPath) {
|
|
90
|
+
return { ...agentResult, artifact_path: artifactPath };
|
|
91
|
+
}
|
|
92
|
+
|
|
87
93
|
return agentResult;
|
|
88
94
|
} finally {
|
|
89
95
|
await rm(tmpDir, { recursive: true, force: true });
|
|
@@ -235,6 +241,24 @@ function resolveCompanion(input = {}) {
|
|
|
235
241
|
};
|
|
236
242
|
}
|
|
237
243
|
|
|
244
|
+
async function persistArtifactSafe(input, agentResult) {
|
|
245
|
+
try {
|
|
246
|
+
return await persistCrewArtifact({
|
|
247
|
+
workspaceRoot: process.cwd(),
|
|
248
|
+
contract: input.contract,
|
|
249
|
+
request: input.request,
|
|
250
|
+
agentResult
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error instanceof ArtifactPersistError) {
|
|
254
|
+
throw new DispatchError(`Artifact persist failed: ${error.message}`, {
|
|
255
|
+
agentResult
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
238
262
|
function isNodeScript(value) {
|
|
239
263
|
const name = basename(String(value));
|
|
240
264
|
return name.endsWith(".mjs") || name.endsWith(".js");
|
package/scripts/lib/render.mjs
CHANGED
|
@@ -8,7 +8,7 @@ export function renderPrompt(input) {
|
|
|
8
8
|
section("Instructions", request.instruction, { required: true }),
|
|
9
9
|
section("Success Gate", request.successGate),
|
|
10
10
|
section("Failure Handling", request.failureHandling),
|
|
11
|
-
section("AgentResult Contract", renderAgentResultContract())
|
|
11
|
+
section("AgentResult Contract", renderAgentResultContract(contract))
|
|
12
12
|
].filter(Boolean);
|
|
13
13
|
|
|
14
14
|
return `${parts.join("\n\n")}\n`;
|
|
@@ -41,7 +41,7 @@ function renderCapability(contract) {
|
|
|
41
41
|
`canAskUser: ${String(tools.includes("AskUserQuestion"))}`,
|
|
42
42
|
`canRequestAgent: ${String(tools.includes("Agent"))}`,
|
|
43
43
|
`canUseShell: ${String(tools.includes("Bash"))}`,
|
|
44
|
-
`
|
|
44
|
+
`canReturnCrewArtifact: ${String(canReturnCrewArtifact(outputs, contract.capabilities?.workspaceAccess))}`
|
|
45
45
|
].join("\n");
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -73,15 +73,19 @@ function renderJson(value) {
|
|
|
73
73
|
return JSON.stringify(value, null, 2);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function renderAgentResultContract() {
|
|
77
|
-
|
|
76
|
+
function renderAgentResultContract(contract = {}) {
|
|
77
|
+
const outputs = Array.isArray(contract.outputs) ? contract.outputs : [];
|
|
78
|
+
const workspaceAccess = contract.capabilities?.workspaceAccess;
|
|
79
|
+
const returnArtifact = canReturnCrewArtifact(outputs, workspaceAccess);
|
|
80
|
+
|
|
81
|
+
const lines = [
|
|
78
82
|
"Return exactly one final AgentResult JSON object wrapped in these tags:",
|
|
79
83
|
"",
|
|
80
84
|
"```text",
|
|
81
85
|
"<crew-agent-result>",
|
|
82
86
|
"{",
|
|
83
87
|
' "status": "complete | blocked_on_user | needs_agent | needs_tool | failed",',
|
|
84
|
-
|
|
88
|
+
` "artifact": ${returnArtifact ? '"full Markdown content of the artifact"' : "null"},`,
|
|
85
89
|
' "questions": [],',
|
|
86
90
|
' "requests": [],',
|
|
87
91
|
' "summary": "short summary",',
|
|
@@ -97,7 +101,15 @@ function renderAgentResultContract() {
|
|
|
97
101
|
"- Use blocked_on_user only with a non-empty questions array.",
|
|
98
102
|
"- Use needs_agent or needs_tool only with a non-empty requests array.",
|
|
99
103
|
"- Use failed with an error string when the task cannot continue."
|
|
100
|
-
]
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
if (returnArtifact) {
|
|
107
|
+
lines.push(
|
|
108
|
+
"- Do NOT write the artifact file yourself. Instead, put the full Markdown content into the artifact field as a string. The runner will validate and save it to the target path."
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return lines.join("\n");
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
function normalizeBody(body) {
|
|
@@ -127,7 +139,11 @@ function titleCase(value) {
|
|
|
127
139
|
.join("-");
|
|
128
140
|
}
|
|
129
141
|
|
|
130
|
-
function
|
|
142
|
+
export function canReturnCrewArtifact(outputs, workspaceAccess) {
|
|
143
|
+
if (workspaceAccess !== "read-only") {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
131
147
|
return outputs.some((output) => {
|
|
132
148
|
return (
|
|
133
149
|
output?.type === "artifact" &&
|
|
@@ -20,6 +20,7 @@ description: 유저 요구사항을 인터뷰하여 개발 가능한 수준의 s
|
|
|
20
20
|
- 유저 승인 없이 crew-plan으로 전환하지 않는다.
|
|
21
21
|
- 추측으로 비즈니스 결정을 채우지 않는다.
|
|
22
22
|
- `git worktree add`를 직접 실행하지 않는다. 워크트리는 `EnterWorktree` 도구만 사용한다.
|
|
23
|
+
- `EnterWorktree` 호출 전에 프로젝트 디렉토리에 파일이나 디렉토리를 생성하지 않는다. 워크트리 진입 전 request-file이 필요하면 OS 임시 디렉토리(`$TMPDIR`)를 사용한다.
|
|
23
24
|
|
|
24
25
|
---
|
|
25
26
|
|
|
@@ -68,11 +69,11 @@ output:
|
|
|
68
69
|
- 최초 요구사항 체크리스트 평가
|
|
69
70
|
|
|
70
71
|
role instructions:
|
|
71
|
-
-
|
|
72
|
-
-
|
|
72
|
+
- 오케스트레이터가 요청 내용에서 키워드를 추출하여 kebab-case task-id를 직접 생성한다 (에이전트 호출 불필요).
|
|
73
|
+
- 오케스트레이터가 `EnterWorktree(name: "crew/{task-id}")`를 호출하여 Claude 워크트리를 생성하고 진입한다. 이후 모든 파일 작업은 워크트리 안에서 수행된다.
|
|
73
74
|
- 오케스트레이터가 유저 원본 요청을 `.crew/plans/{task-id}/brief.md`에 저장한다.
|
|
74
75
|
- Explorer는 프로젝트 구조를 병렬로 파악하고, 결과를 파일로 저장하지 않고 인터뷰 컨텍스트로만 제공한다.
|
|
75
|
-
- PM은 유저 요청과 Explorer 결과를 기반으로 체크리스트 5개 항목을 첫
|
|
76
|
+
- PM은 유저 요청과 Explorer 결과를 기반으로 체크리스트 5개 항목을 첫 평가한다 (워크트리 내부에서 실행).
|
|
76
77
|
|
|
77
78
|
success gate:
|
|
78
79
|
- Claude 워크트리 `crew/{task-id}`에 진입했다.
|
|
@@ -86,10 +87,12 @@ failure handling:
|
|
|
86
87
|
|
|
87
88
|
**1b. task-id 생성 + 워크트리 진입 + brief.md 작성**
|
|
88
89
|
|
|
89
|
-
1.
|
|
90
|
+
1. 오케스트레이터가 요청 내용에서 키워드를 추출하여 kebab-case task-id를 직접 생성한다. 에이전트 호출 없이 수행한다.
|
|
90
91
|
2. `EnterWorktree(name: "crew/{task-id}")`를 호출하여 Claude 워크트리를 생성하고 진입한다. 이미 해당 워크트리 안이면 이 단계를 건너뛴다.
|
|
91
92
|
3. 유저 요청 원문을 `.crew/plans/{task-id}/brief.md`에 저장한다.
|
|
92
93
|
|
|
94
|
+
주의: 1~2단계에서 에이전트를 호출하거나 파일을 프로젝트 디렉토리에 생성하지 않는다. 모든 에이전트 호출(PM, Explorer)과 파일 작업은 워크트리 진입 후에 수행한다.
|
|
95
|
+
|
|
93
96
|
**1c. Explorer 호출 (병렬, 워크트리 진입 후)**
|
|
94
97
|
|
|
95
98
|
Explorer는 중앙 runner를 통해 병렬 실행되어 프로젝트 구조를 파악한다.
|