@jjlabsio/claude-crew 0.1.40 → 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.
@@ -11,7 +11,7 @@
11
11
  "name": "claude-crew",
12
12
  "source": "./",
13
13
  "description": "오케스트레이터 + PM, 플래너, 개발, QA, 마케팅 에이전트 팀으로 단일 제품의 개발과 마케팅을 통합 관리",
14
- "version": "0.1.40",
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.40"
31
+ "version": "0.1.41"
32
32
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-crew",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "description": "1인 SaaS 개발자를 위한 멀티 에이전트 오케스트레이션 — 개발, 마케팅, 일정을 한 대화에서 통합 관리",
5
5
  "author": {
6
6
  "name": "Jaejin Song",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlabsio/claude-crew",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "description": "1인 SaaS 개발자를 위한 멀티 에이전트 오케스트레이션 — 개발, 마케팅, 일정을 한 대화에서 통합 관리",
5
5
  "author": "Jaejin Song <wowlxx28@gmail.com>",
6
6
  "license": "MIT",
@@ -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
+ }
@@ -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");
@@ -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
- `canWriteCrewFiles: ${String(canWriteCrewFiles(outputs))}`
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
- return [
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
- ' "artifact": null,',
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
- ].join("\n");
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 canWriteCrewFiles(outputs) {
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" &&