@schoolai/shipyard-mcp 0.1.3 → 0.2.0-next.477

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.
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ getEnvironmentContext,
3
4
  getGitHubUsername,
4
5
  getRepositoryFullName,
5
6
  githubConfig
6
- } from "./chunk-7GPZDCWI.js";
7
+ } from "./chunk-E5DWX2WU.js";
7
8
  import {
8
9
  assertNever,
9
10
  getSessionIdByPlanId,
@@ -18,7 +19,7 @@ import {
18
19
  } from "./chunk-EBNL5ZX7.js";
19
20
  import {
20
21
  InputRequestManager
21
- } from "./chunk-N44DCU4J.js";
22
+ } from "./chunk-BWP37ADP.js";
22
23
  import {
23
24
  ArtifactSchema,
24
25
  DeliverableSchema,
@@ -62,12 +63,13 @@ import {
62
63
  logPlanEvent,
63
64
  parseClaudeCodeOrigin,
64
65
  parseThreads,
66
+ resetPlanToDraft,
65
67
  setAgentPresence,
66
68
  setPlanIndexEntry,
67
69
  setPlanMetadata,
68
70
  touchPlanIndexEntry,
69
71
  transitionPlanStatus
70
- } from "./chunk-LTC26IRQ.js";
72
+ } from "./chunk-76JWRTPI.js";
71
73
  import {
72
74
  loadEnv,
73
75
  logger
@@ -500,14 +502,14 @@ async function uploadArtifact(params) {
500
502
  }
501
503
  const { repo, planId, filename, content } = params;
502
504
  const { owner, repoName } = parseRepoString(repo);
503
- const path = `plans/${planId}/${filename}`;
505
+ const path2 = `plans/${planId}/${filename}`;
504
506
  await ensureArtifactsBranch(repo);
505
507
  let existingSha;
506
508
  try {
507
509
  const { data } = await octokit.repos.getContent({
508
510
  owner,
509
511
  repo: repoName,
510
- path,
512
+ path: path2,
511
513
  ref: ARTIFACTS_BRANCH
512
514
  });
513
515
  if (!Array.isArray(data) && data.type === "file") {
@@ -521,14 +523,14 @@ async function uploadArtifact(params) {
521
523
  await octokit.repos.createOrUpdateFileContents({
522
524
  owner,
523
525
  repo: repoName,
524
- path,
526
+ path: path2,
525
527
  message: `Add artifact: ${filename}`,
526
528
  content,
527
529
  branch: ARTIFACTS_BRANCH,
528
530
  sha: existingSha
529
531
  });
530
- const url = `https://raw.githubusercontent.com/${repo}/${ARTIFACTS_BRANCH}/${path}`;
531
- logger.info({ repo, path, url }, "Artifact uploaded");
532
+ const url = `https://raw.githubusercontent.com/${repo}/${ARTIFACTS_BRANCH}/${path2}`;
533
+ logger.info({ repo, path: path2, url }, "Artifact uploaded");
532
534
  return url;
533
535
  });
534
536
  }
@@ -558,7 +560,7 @@ import open from "open";
558
560
  // src/config/env/web.ts
559
561
  import { z as z2 } from "zod";
560
562
  var schema2 = z2.object({
561
- SHIPYARD_WEB_URL: z2.string().url().default("http://localhost:5173")
563
+ SHIPYARD_WEB_URL: z2.string().url().default("https://schoolai.github.io/shipyard")
562
564
  });
563
565
  var webConfig = loadEnv(schema2);
564
566
 
@@ -1027,16 +1029,14 @@ async function waitForApprovalHandler(planId, _reviewRequestIdParam, ctx) {
1027
1029
  if (!reviewedBy) {
1028
1030
  throw new Error(`Invalid session state transition: missing reviewedBy for changes_requested`);
1029
1031
  }
1030
- if (!syncedFields) {
1031
- throw new Error(
1032
- `Invalid session state transition: changes_requested requires synced fields (contentHash, sessionToken)`
1033
- );
1034
- }
1035
1032
  const deliverables = extraData.deliverables || (isSessionStateApproved(session) || isSessionStateReviewed(session) || isSessionStateApprovedAwaitingToken(session) ? session.deliverables : []);
1033
+ const webUrl = webConfig.SHIPYARD_WEB_URL;
1036
1034
  setSessionState(sessionId, {
1037
1035
  lifecycle: "reviewed",
1038
1036
  ...baseState,
1039
- ...syncedFields,
1037
+ contentHash: syncedFields?.contentHash ?? "",
1038
+ sessionToken: syncedFields?.sessionToken ?? "",
1039
+ url: syncedFields?.url ?? `${webUrl}/plan/${baseState.planId}`,
1040
1040
  deliverables,
1041
1041
  reviewComment: reviewComment || "",
1042
1042
  reviewedBy,
@@ -1685,6 +1685,10 @@ function handleResolvedComments(doc, planId, threads, prev, actor) {
1685
1685
  }
1686
1686
 
1687
1687
  // src/registry-server.ts
1688
+ function getParam(value) {
1689
+ if (Array.isArray(value)) return value[0];
1690
+ return value;
1691
+ }
1688
1692
  var PERSISTENCE_DIR = join3(homedir3(), ".shipyard", "plans");
1689
1693
  var HUB_LOCK_FILE = join3(homedir3(), ".shipyard", "hub.lock");
1690
1694
  var SHIPYARD_DIR = join3(homedir3(), ".shipyard");
@@ -1988,7 +1992,8 @@ async function handleHealthCheck(_req, res) {
1988
1992
  res.json({ status: "ok" });
1989
1993
  }
1990
1994
  async function handleGetPRDiff(req, res) {
1991
- const { id: planId, prNumber } = req.params;
1995
+ const planId = getParam(req.params.id);
1996
+ const prNumber = getParam(req.params.prNumber);
1992
1997
  if (!planId || !prNumber) {
1993
1998
  res.status(400).json({ error: "Missing plan ID or PR number" });
1994
1999
  return;
@@ -2024,7 +2029,8 @@ async function handleGetPRDiff(req, res) {
2024
2029
  }
2025
2030
  }
2026
2031
  async function handleGetPRFiles(req, res) {
2027
- const { id: planId, prNumber } = req.params;
2032
+ const planId = getParam(req.params.id);
2033
+ const prNumber = getParam(req.params.prNumber);
2028
2034
  if (!planId || !prNumber) {
2029
2035
  res.status(400).json({ error: "Missing plan ID or PR number" });
2030
2036
  return;
@@ -2065,7 +2071,7 @@ async function handleGetPRFiles(req, res) {
2065
2071
  }
2066
2072
  }
2067
2073
  async function handleGetTranscript(req, res) {
2068
- const planId = req.params.id;
2074
+ const planId = getParam(req.params.id);
2069
2075
  if (!planId) {
2070
2076
  res.status(400).json({ error: "Missing plan ID" });
2071
2077
  return;
@@ -2155,8 +2161,8 @@ function createApp() {
2155
2161
  app.get("/api/plans/:id/pr-diff/:prNumber", handleGetPRDiff);
2156
2162
  app.get("/api/plans/:id/pr-files/:prNumber", handleGetPRFiles);
2157
2163
  app.get("/artifacts/:planId/:filename", async (req, res) => {
2158
- const planId = req.params.planId;
2159
- const filename = req.params.filename;
2164
+ const planId = getParam(req.params.planId);
2165
+ const filename = getParam(req.params.filename);
2160
2166
  if (!planId || !filename) {
2161
2167
  res.status(400).json({ error: "Missing planId or filename" });
2162
2168
  return;
@@ -2291,29 +2297,28 @@ async function createWebRtcProvider(ydoc, planId) {
2291
2297
  }
2292
2298
  }
2293
2299
  });
2294
- let username;
2295
- try {
2296
- username = await getGitHubUsername();
2297
- const awarenessState = {
2298
- user: {
2299
- id: `mcp-${username}`,
2300
- name: `Claude Code (${username})`,
2301
- color: "#0066cc"
2302
- },
2303
- platform: "claude-code",
2304
- status: "approved",
2305
- isOwner: true,
2306
- webrtcPeerId: crypto.randomUUID()
2307
- };
2308
- provider.awareness.setLocalStateField("planStatus", awarenessState);
2309
- logger.info({ username, platform: "claude-code" }, "MCP awareness state set");
2310
- sendApprovalStateToSignaling(provider, planId, username);
2311
- } catch (error) {
2312
- logger.warn(
2313
- { error },
2314
- "Could not set MCP awareness (GitHub not authenticated - run: gh auth login)"
2315
- );
2316
- }
2300
+ const username = await getGitHubUsername().catch(() => void 0);
2301
+ const fallbackId = `mcp-anon-${crypto.randomUUID().slice(0, 8)}`;
2302
+ const userId = username ? `mcp-${username}` : fallbackId;
2303
+ const displayName = username ? `Claude Code (${username})` : "Claude Code";
2304
+ const awarenessState = {
2305
+ user: {
2306
+ id: userId,
2307
+ name: displayName,
2308
+ color: "#0066cc"
2309
+ },
2310
+ platform: "claude-code",
2311
+ status: "approved",
2312
+ isOwner: true,
2313
+ webrtcPeerId: crypto.randomUUID(),
2314
+ context: getEnvironmentContext()
2315
+ };
2316
+ provider.awareness.setLocalStateField("planStatus", awarenessState);
2317
+ logger.info(
2318
+ { planId, username: username ?? fallbackId, platform: "claude-code", hasContext: true },
2319
+ "MCP awareness state set"
2320
+ );
2321
+ sendApprovalStateToSignaling(provider, planId, username ?? fallbackId);
2317
2322
  setupProviderListeners(provider, planId);
2318
2323
  logger.info(
2319
2324
  {
@@ -2461,12 +2466,17 @@ async function hasActiveConnections2(planId) {
2461
2466
  }
2462
2467
 
2463
2468
  // src/tools/execute-code.ts
2469
+ import * as child_process from "child_process";
2470
+ import * as fs from "fs";
2471
+ import * as os from "os";
2472
+ import * as path from "path";
2464
2473
  import * as vm from "vm";
2474
+ import ffmpegInstaller from "@ffmpeg-installer/ffmpeg";
2465
2475
  import { z as z12 } from "zod";
2466
2476
 
2467
2477
  // src/tools/add-artifact.ts
2468
2478
  import { execSync } from "child_process";
2469
- import { readFile as readFile3 } from "fs/promises";
2479
+ import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2470
2480
  import { ServerBlockNoteEditor as ServerBlockNoteEditor2 } from "@blocknote/server-util";
2471
2481
  import { nanoid as nanoid4 } from "nanoid";
2472
2482
  import { z as z3 } from "zod";
@@ -2847,6 +2857,7 @@ Linked to deliverable: ${input.deliverableId}` : "";
2847
2857
  const fragment = doc.getXmlFragment("document");
2848
2858
  const blocks = editor.yXmlFragmentToBlocks(fragment);
2849
2859
  const artifacts = getArtifacts(doc);
2860
+ const hasLocalArtifacts = artifacts.some((a) => a.storage === "local");
2850
2861
  const completionSnapshot = createPlanSnapshot(
2851
2862
  doc,
2852
2863
  "Task completed - all deliverables fulfilled",
@@ -2898,6 +2909,14 @@ Linked to deliverable: ${input.deliverableId}` : "";
2898
2909
  logger.warn({ planId }, "Cannot update plan index: missing ownerId");
2899
2910
  }
2900
2911
  logger.info({ planId, snapshotUrl }, "Task auto-completed");
2912
+ const { homedir: homedir5 } = await import("os");
2913
+ const { join: join6 } = await import("path");
2914
+ const { mkdir: mkdir3 } = await import("fs/promises");
2915
+ const snapshotsDir = join6(homedir5(), ".shipyard", "snapshots");
2916
+ await mkdir3(snapshotsDir, { recursive: true });
2917
+ const snapshotFile = join6(snapshotsDir, `${planId}.txt`);
2918
+ await writeFile4(snapshotFile, snapshotUrl, "utf-8");
2919
+ logger.info({ planId, snapshotFile }, "Snapshot URL written to file");
2901
2920
  let prText = "";
2902
2921
  if (linkedPR) {
2903
2922
  prText = `
@@ -2921,13 +2940,15 @@ Type: ${type}
2921
2940
  Filename: ${filename}
2922
2941
  URL: ${artifactUrl}${linkedText}
2923
2942
 
2924
- \u{1F389} ALL DELIVERABLES COMPLETE! Task auto-completed.
2943
+ \u{1F389} ALL DELIVERABLES COMPLETE! Task auto-completed.${prText}
2925
2944
 
2926
- Snapshot URL: ${snapshotUrl}${prText}
2927
-
2928
- Embed this snapshot URL in your PR description as proof of completed work.`
2945
+ Snapshot URL saved to: ${snapshotFile}
2946
+ (Note: Very long URL - recommend not reading directly. Use file path to attach to PR or access later.)${hasLocalArtifacts ? "\n\n\u26A0\uFE0F WARNING: This plan contains local artifacts that will not be visible to remote viewers. For full remote access, configure GITHUB_TOKEN to upload artifacts to GitHub." : ""}`
2929
2947
  }
2930
- ]
2948
+ ],
2949
+ // Keep structured data for execute_code wrapper
2950
+ snapshotUrl,
2951
+ allDeliverablesComplete: true
2931
2952
  };
2932
2953
  }
2933
2954
  const statusText = statusChanged ? "\nStatus: draft \u2192 in_progress (auto-updated)" : "";
@@ -3227,6 +3248,7 @@ RETURNS:
3227
3248
  };
3228
3249
  }
3229
3250
  const artifacts = getArtifacts(ydoc);
3251
+ const hasLocalArtifacts = artifacts.some((a) => a.storage === "local");
3230
3252
  if (artifacts.length === 0) {
3231
3253
  return {
3232
3254
  content: [
@@ -3346,6 +3368,9 @@ Generated with [Shipyard](https://github.com/SchoolAI/shipyard)"
3346
3368
  linkPR({ planId, sessionToken, prNumber: 42 })
3347
3369
  \`\`\``;
3348
3370
  }
3371
+ if (hasLocalArtifacts) {
3372
+ responseText += "\n\n\u26A0\uFE0F WARNING: This plan contains local artifacts that will not be visible to remote viewers. For full remote access, configure GITHUB_TOKEN to upload artifacts to GitHub.";
3373
+ }
3349
3374
  return {
3350
3375
  content: [{ type: "text", text: responseText }]
3351
3376
  };
@@ -3602,7 +3627,7 @@ Bad deliverables (not provable):
3602
3627
  deleted: false
3603
3628
  });
3604
3629
  logger.info({ planId }, "Plan index updated");
3605
- const url = `http://localhost:5173/plan/${planId}`;
3630
+ const url = `${webConfig.SHIPYARD_WEB_URL}/plan/${planId}`;
3606
3631
  await openPlanInBrowser(planId, url);
3607
3632
  const repoInfo = repo ? `Repo: ${repo}${!input.repo ? " (auto-detected)" : ""}` : "Repo: Not set (provide repo and prNumber for artifact uploads)";
3608
3633
  return {
@@ -4128,7 +4153,9 @@ USAGE (for non-hook agents):
4128
4153
  1. Call this tool to get monitoring script
4129
4154
  2. Run script in background: bash <script> &
4130
4155
  3. Script polls registry server for status changes
4131
- 4. Exits when status becomes 'approved' or 'changes_requested'`,
4156
+ 4. Exits when status becomes 'in_progress' (approved) or 'changes_requested' (needs work)
4157
+
4158
+ REQUIREMENTS: The script requires 'jq' for URL encoding. Install with: brew install jq (macOS) or apt install jq (Linux)`,
4132
4159
  inputSchema: {
4133
4160
  type: "object",
4134
4161
  properties: {
@@ -4146,25 +4173,64 @@ USAGE (for non-hook agents):
4146
4173
  const { planId, pollIntervalSeconds = 30 } = input;
4147
4174
  const registryPort2 = registryConfig.REGISTRY_PORT[0];
4148
4175
  const trpcUrl = `http://localhost:${registryPort2}/trpc`;
4149
- const script = `# Subscribe to status and comment changes via tRPC
4150
- CLIENT_ID=$(curl -sf -X POST "${trpcUrl}/subscription.create" \\
4176
+ const statusInProgress = PlanStatusValues.find((s) => s === "in_progress");
4177
+ const statusChangesRequested = PlanStatusValues.find((s) => s === "changes_requested");
4178
+ if (!statusInProgress || !statusChangesRequested) {
4179
+ throw new Error("Required status values not found in PlanStatusValues");
4180
+ }
4181
+ const script = `#!/bin/bash
4182
+ # Monitor plan "${planId}" for approval status changes
4183
+ # Polls the Shipyard registry server and exits when approved/rejected
4184
+
4185
+ # Check for required dependency
4186
+ if ! command -v jq &> /dev/null; then
4187
+ echo "Error: jq is required but not installed."
4188
+ echo "Install with: brew install jq (macOS) or apt install jq (Linux)"
4189
+ exit 1
4190
+ fi
4191
+
4192
+ TRPC_URL="${trpcUrl}"
4193
+ PLAN_ID="${planId}"
4194
+ POLL_INTERVAL=${pollIntervalSeconds}
4195
+
4196
+ # Subscribe to status changes via tRPC mutation
4197
+ echo "Subscribing to plan changes..."
4198
+ RESPONSE=$(curl -sf -X POST "$TRPC_URL/subscription.create" \\
4151
4199
  -H "Content-Type: application/json" \\
4152
- -d '{"planId":"${planId}","subscribe":["status","comments"],"windowMs":5000,"threshold":1}' \\
4153
- | grep -o '"clientId":"[^"]*"' | cut -d'"' -f4)
4154
-
4155
- echo "Subscribed. Monitoring plan..."
4156
-
4157
- # Poll for changes via tRPC
4158
- while sleep ${pollIntervalSeconds}; do
4159
- result=$(curl -sf -X POST "${trpcUrl}/subscription.getChanges" \\
4160
- -H "Content-Type: application/json" \\
4161
- -d '{"planId":"${planId}","clientId":"'"$CLIENT_ID"'"}' 2>/dev/null)
4162
- ready=$(echo "$result" | grep -o '"ready":true')
4163
- if [ -n "$ready" ]; then
4164
- changes=$(echo "$result" | grep -o '"changes":"[^"]*"' | cut -d'"' -f4)
4165
- echo "Changes: $changes"
4166
- # Exit on status change to approved/changes_requested
4167
- echo "$changes" | grep -qE "Status:.*(approved|changes_requested)" && exit 0
4200
+ -d '{"planId":"'"$PLAN_ID"'","subscribe":["status","comments"],"windowMs":5000,"threshold":1}')
4201
+
4202
+ # Extract clientId from response: {"result":{"data":{"clientId":"..."}}}
4203
+ CLIENT_ID=$(echo "$RESPONSE" | sed -n 's/.*"clientId":"\\([^"]*\\)".*/\\1/p')
4204
+
4205
+ if [ -z "$CLIENT_ID" ]; then
4206
+ echo "Failed to subscribe. Is the Shipyard registry server running?"
4207
+ echo "Response: $RESPONSE"
4208
+ exit 1
4209
+ fi
4210
+
4211
+ echo "Subscribed with clientId: $CLIENT_ID"
4212
+ echo "Polling every $POLL_INTERVAL seconds..."
4213
+
4214
+ # Poll for changes via tRPC query (GET with url-encoded input)
4215
+ while true; do
4216
+ sleep $POLL_INTERVAL
4217
+
4218
+ # URL-encode the input JSON for GET request
4219
+ INPUT='{"planId":"'"$PLAN_ID"'","clientId":"'"$CLIENT_ID"'"}'
4220
+ ENCODED_INPUT=$(printf '%s' "$INPUT" | jq -sRr @uri)
4221
+
4222
+ RESULT=$(curl -sf "$TRPC_URL/subscription.getChanges?input=$ENCODED_INPUT" 2>/dev/null)
4223
+
4224
+ # Check if changes are ready: {"result":{"data":{"ready":true,"changes":"..."}}}
4225
+ if echo "$RESULT" | grep -q '"ready":true'; then
4226
+ CHANGES=$(echo "$RESULT" | sed -n 's/.*"changes":"\\([^"]*\\)".*/\\1/p')
4227
+ echo "Changes detected: $CHANGES"
4228
+
4229
+ # Exit on status change to in_progress (approved) or changes_requested (needs work)
4230
+ if echo "$CHANGES" | grep -qE "Status:.*(${statusInProgress}|${statusChangesRequested})"; then
4231
+ echo "Plan status changed. Exiting."
4232
+ exit 0
4233
+ fi
4168
4234
  fi
4169
4235
  done`;
4170
4236
  return {
@@ -4177,10 +4243,13 @@ done`;
4177
4243
  ${script}
4178
4244
  \`\`\`
4179
4245
 
4180
- > Subscribes to status and comment changes with server-side batching.
4181
- > Batching: 5s window or 1 change threshold (whichever comes first).
4182
- > Exits when status becomes approved/changes_requested.
4183
- > Most agent environments support background bash notifications.`
4246
+ **Usage:** Save to a file and run in background: \`bash script.sh &\`
4247
+
4248
+ The script:
4249
+ - Subscribes to status/comment changes via tRPC
4250
+ - Polls every ${pollIntervalSeconds} seconds
4251
+ - Exits when status becomes in_progress (approved) or changes_requested (needs work)
4252
+ - Requires \`jq\` for URL encoding (install: brew install jq)`
4184
4253
  }
4185
4254
  ]
4186
4255
  };
@@ -4474,7 +4543,40 @@ async function applyOperation(blocks, operation, editor) {
4474
4543
 
4475
4544
  // src/tools/update-plan.ts
4476
4545
  import { ServerBlockNoteEditor as ServerBlockNoteEditor7 } from "@blocknote/server-util";
4546
+ import { nanoid as nanoid7 } from "nanoid";
4477
4547
  import { z as z11 } from "zod";
4548
+ function buildStatusTransition(targetStatus, actorName) {
4549
+ const now = Date.now();
4550
+ switch (targetStatus) {
4551
+ case "pending_review":
4552
+ return {
4553
+ status: "pending_review",
4554
+ reviewRequestId: nanoid7()
4555
+ };
4556
+ case "changes_requested":
4557
+ return {
4558
+ status: "changes_requested",
4559
+ reviewedAt: now,
4560
+ reviewedBy: actorName
4561
+ };
4562
+ case "in_progress":
4563
+ return {
4564
+ status: "in_progress",
4565
+ reviewedAt: now,
4566
+ reviewedBy: actorName
4567
+ };
4568
+ case "completed":
4569
+ return {
4570
+ status: "completed",
4571
+ completedAt: now,
4572
+ completedBy: actorName
4573
+ };
4574
+ case "draft":
4575
+ return null;
4576
+ default:
4577
+ return null;
4578
+ }
4579
+ }
4478
4580
  var UpdatePlanInput = z11.object({
4479
4581
  planId: z11.string().describe("The plan ID to update"),
4480
4582
  sessionToken: z11.string().describe("Session token from create_plan"),
@@ -4524,6 +4626,7 @@ STATUSES:
4524
4626
  required: ["planId", "sessionToken"]
4525
4627
  }
4526
4628
  },
4629
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: tool handler orchestrates validation, snapshots, and state transitions
4527
4630
  handler: async (args) => {
4528
4631
  const input = UpdatePlanInput.parse(args);
4529
4632
  const doc = await getOrCreateDoc3(input.planId);
@@ -4551,12 +4654,6 @@ STATUSES:
4551
4654
  isError: true
4552
4655
  };
4553
4656
  }
4554
- const updates = {
4555
- updatedAt: Date.now()
4556
- };
4557
- if (input.title) updates.title = input.title;
4558
- if (input.status) updates.status = input.status;
4559
- if (input.tags !== void 0) updates.tags = input.tags;
4560
4657
  const statusChanged = input.status && input.status !== existingMetadata.status;
4561
4658
  if (statusChanged && input.status) {
4562
4659
  const editor = ServerBlockNoteEditor7.create();
@@ -4565,8 +4662,52 @@ STATUSES:
4565
4662
  const reason = `Status changed to ${input.status}`;
4566
4663
  const snapshot = createPlanSnapshot(doc, reason, actorName, input.status, blocks);
4567
4664
  addSnapshot(doc, snapshot);
4665
+ if (input.status === "draft") {
4666
+ const resetResult = resetPlanToDraft(doc, actorName);
4667
+ if (!resetResult.success) {
4668
+ return {
4669
+ content: [
4670
+ {
4671
+ type: "text",
4672
+ text: `Failed to reset plan to draft: ${resetResult.error}`
4673
+ }
4674
+ ],
4675
+ isError: true
4676
+ };
4677
+ }
4678
+ } else {
4679
+ const transition = buildStatusTransition(input.status, actorName);
4680
+ if (!transition) {
4681
+ return {
4682
+ content: [
4683
+ {
4684
+ type: "text",
4685
+ text: `Invalid status: ${input.status}`
4686
+ }
4687
+ ],
4688
+ isError: true
4689
+ };
4690
+ }
4691
+ const transitionResult = transitionPlanStatus(doc, transition, actorName);
4692
+ if (!transitionResult.success) {
4693
+ return {
4694
+ content: [
4695
+ {
4696
+ type: "text",
4697
+ text: `Failed to transition status: ${transitionResult.error}`
4698
+ }
4699
+ ],
4700
+ isError: true
4701
+ };
4702
+ }
4703
+ }
4704
+ }
4705
+ const updates = {};
4706
+ if (input.title) updates.title = input.title;
4707
+ if (input.tags !== void 0) updates.tags = input.tags;
4708
+ if (Object.keys(updates).length > 0) {
4709
+ setPlanMetadata(doc, updates, actorName);
4568
4710
  }
4569
- setPlanMetadata(doc, updates, actorName);
4570
4711
  const indexDoc = await getOrCreateDoc3(PLAN_INDEX_DOC_NAME);
4571
4712
  if (existingMetadata.ownerId) {
4572
4713
  setPlanIndexEntry(indexDoc, {
@@ -4767,17 +4908,36 @@ Parameters:
4767
4908
  - pollIntervalSeconds (number, optional): Polling interval (default: 30)
4768
4909
 
4769
4910
  Returns:
4770
- - script: Bash script that polls registry server and exits when status becomes 'changes_requested' or 'in_progress'
4911
+ - script: Bash script that polls registry server and exits when status becomes 'in_progress' (approved) or 'changes_requested' (needs work)
4771
4912
 
4772
- Use this for agents WITHOUT hook support (Cursor, Devin, etc). The script can be run in background.
4913
+ **IMPORTANT:** This is ONLY for agents WITHOUT hook support (Cursor, Devin, Windsurf, etc).
4914
+ Claude Code users have automatic blocking via the shipyard hook - you don't need this.
4773
4915
 
4774
- Example:
4916
+ **Complete workflow for non-hook agents (example user code):**
4775
4917
  \`\`\`typescript
4918
+ // 1. Create plan and get the monitoring script in ONE code block
4919
+ const plan = await createPlan({
4920
+ title: "My Feature Implementation",
4921
+ content: "- [ ] Screenshot of working feature {#deliverable}"
4922
+ });
4923
+
4924
+ // 2. Get the polling script (returns bash script as string)
4776
4925
  const { script } = await setupReviewNotification(plan.planId, 15);
4777
- // Agent runs this script in background to wait for approval
4778
- console.log(script);
4926
+
4927
+ // 3. Return both so the agent can run the script
4928
+ return {
4929
+ planId: plan.planId,
4930
+ sessionToken: plan.sessionToken,
4931
+ monitoringScript: script,
4932
+ instructions: "Run the monitoring script in background: bash <script> &"
4933
+ };
4779
4934
  \`\`\`
4780
4935
 
4936
+ The agent then runs the returned bash script in the background. The script will:
4937
+ - Poll the registry server every N seconds
4938
+ - Print status changes to stdout
4939
+ - Exit with code 0 when the plan is approved/rejected
4940
+
4781
4941
  ---
4782
4942
 
4783
4943
  ### requestUserInput(opts): Promise<{ success, response?, status, reason? }>
@@ -4789,7 +4949,7 @@ Parameters:
4789
4949
  - options (string[], optional): For 'choice' type - available options (required for choice)
4790
4950
  - multiSelect (boolean, optional): For 'choice' type - allow selecting multiple options (uses checkboxes instead of radio buttons)
4791
4951
  - defaultValue (string, optional): Pre-filled value for text/multiline inputs
4792
- - timeout (number, optional): Timeout in seconds (default: 300, min: 10, max: 600)
4952
+ - timeout (number, optional): Timeout in seconds (default: 1800, min: 10, max: 14400)
4793
4953
  - planId (string, optional): Optional metadata to link request to plan (for activity log filtering)
4794
4954
 
4795
4955
  Returns:
@@ -5004,7 +5164,7 @@ async function addArtifact2(opts) {
5004
5164
  const metadata = getPlanMetadata(ydoc);
5005
5165
  let artifactUrl = "";
5006
5166
  if (addedArtifact) {
5007
- artifactUrl = addedArtifact.storage === "github" ? addedArtifact.url : `http://localhost:${process.env.REGISTRY_PORT || 3e3}/artifacts/${addedArtifact.localArtifactId}`;
5167
+ artifactUrl = addedArtifact.storage === "github" ? addedArtifact.url : `http://localhost:${registryConfig.REGISTRY_PORT[0]}/artifacts/${addedArtifact.localArtifactId}`;
5008
5168
  }
5009
5169
  return {
5010
5170
  artifactId: addedArtifact?.id || "",
@@ -5064,7 +5224,7 @@ async function setupReviewNotification(planId, pollIntervalSeconds) {
5064
5224
  return { script, fullResponse: text };
5065
5225
  }
5066
5226
  async function requestUserInput(opts) {
5067
- const { InputRequestManager: InputRequestManager2 } = await import("./input-request-manager-MVKPYLFW.js");
5227
+ const { InputRequestManager: InputRequestManager2 } = await import("./input-request-manager-73GSTOIB.js");
5068
5228
  const ydoc = await getOrCreateDoc3(PLAN_INDEX_DOC_NAME);
5069
5229
  const manager = new InputRequestManager2();
5070
5230
  const params = opts.type === "choice" ? {
@@ -5108,12 +5268,12 @@ async function requestUserInput(opts) {
5108
5268
  };
5109
5269
  }
5110
5270
  async function postActivityUpdate(opts) {
5111
- const { logPlanEvent: logPlanEvent2 } = await import("./dist-LQBXUHLR.js");
5112
- const { getGitHubUsername: getGitHubUsername2 } = await import("./server-identity-KUXYHULN.js");
5113
- const { nanoid: nanoid7 } = await import("nanoid");
5271
+ const { logPlanEvent: logPlanEvent2 } = await import("./dist-ORKL4P3L.js");
5272
+ const { getGitHubUsername: getGitHubUsername2 } = await import("./server-identity-6PHKR2FY.js");
5273
+ const { nanoid: nanoid8 } = await import("nanoid");
5114
5274
  const doc = await getOrCreateDoc3(opts.planId);
5115
5275
  const actorName = await getGitHubUsername2();
5116
- const requestId = nanoid7();
5276
+ const requestId = nanoid8();
5117
5277
  const eventId = logPlanEvent2(
5118
5278
  doc,
5119
5279
  "agent_activity",
@@ -5131,8 +5291,8 @@ async function postActivityUpdate(opts) {
5131
5291
  return { success: true, eventId, requestId };
5132
5292
  }
5133
5293
  async function resolveActivityRequest(opts) {
5134
- const { logPlanEvent: logPlanEvent2, getPlanEvents } = await import("./dist-LQBXUHLR.js");
5135
- const { getGitHubUsername: getGitHubUsername2 } = await import("./server-identity-KUXYHULN.js");
5294
+ const { logPlanEvent: logPlanEvent2, getPlanEvents } = await import("./dist-ORKL4P3L.js");
5295
+ const { getGitHubUsername: getGitHubUsername2 } = await import("./server-identity-6PHKR2FY.js");
5136
5296
  const doc = await getOrCreateDoc3(opts.planId);
5137
5297
  const actorName = await getGitHubUsername2();
5138
5298
  const events = getPlanEvents(doc);
@@ -5176,7 +5336,38 @@ var executeCodeTool = {
5176
5336
  const { code } = ExecuteCodeInput.parse(args);
5177
5337
  logger.info({ codeLength: code.length }, "Executing code");
5178
5338
  try {
5339
+ async function encodeVideo(opts) {
5340
+ const fps = opts.fps || 6;
5341
+ const outputPath = opts.outputPath || path.join(os.tmpdir(), `video-${Date.now()}.mp4`);
5342
+ const { spawnSync } = child_process;
5343
+ const result2 = spawnSync(
5344
+ ffmpegInstaller.path,
5345
+ [
5346
+ "-y",
5347
+ "-framerate",
5348
+ String(fps),
5349
+ "-i",
5350
+ path.join(opts.framesDir, "frame-%06d.jpg"),
5351
+ "-vf",
5352
+ "scale=trunc(iw/2)*2:trunc(ih/2)*2",
5353
+ "-c:v",
5354
+ "libx264",
5355
+ "-pix_fmt",
5356
+ "yuv420p",
5357
+ "-preset",
5358
+ "fast",
5359
+ outputPath
5360
+ ],
5361
+ { encoding: "utf-8", timeout: 6e4 }
5362
+ );
5363
+ if (result2.status !== 0) {
5364
+ throw new Error(`FFmpeg encoding failed: ${result2.stderr?.slice(-300)}`);
5365
+ }
5366
+ fs.rmSync(opts.framesDir, { recursive: true, force: true });
5367
+ return outputPath;
5368
+ }
5179
5369
  const sandbox = {
5370
+ // Shipyard API functions
5180
5371
  createPlan,
5181
5372
  readPlan,
5182
5373
  updatePlan,
@@ -5189,6 +5380,15 @@ var executeCodeTool = {
5189
5380
  requestUserInput,
5190
5381
  postActivityUpdate,
5191
5382
  resolveActivityRequest,
5383
+ // Video encoding helper (uses bundled FFmpeg)
5384
+ encodeVideo,
5385
+ // Node.js modules for advanced workflows (file ops, process spawning)
5386
+ child_process,
5387
+ fs,
5388
+ path,
5389
+ os,
5390
+ // FFmpeg bundled with server - no installation required
5391
+ ffmpegPath: ffmpegInstaller.path,
5192
5392
  console: {
5193
5393
  log: (...logArgs) => logger.info({ output: logArgs }, "console.log"),
5194
5394
  error: (...logArgs) => logger.error({ output: logArgs }, "console.error")
@@ -5226,7 +5426,7 @@ var RequestUserInputInput = z13.object({
5226
5426
  options: z13.array(z13.string()).optional().describe("For 'choice' type - available options (required for choice)"),
5227
5427
  multiSelect: z13.boolean().optional().describe("For 'choice' type - allow selecting multiple options"),
5228
5428
  defaultValue: z13.string().optional().describe("Pre-filled value for text/multiline inputs"),
5229
- timeout: z13.number().optional().describe("Timeout in seconds (default: 300, min: 10, max: 900)"),
5429
+ timeout: z13.number().optional().describe("Timeout in seconds (default: 1800, min: 10, max: 14400)"),
5230
5430
  planId: z13.string().optional().describe("Optional metadata to link request to plan (for activity log filtering)")
5231
5431
  });
5232
5432
  var requestUserInputTool = {
@@ -5283,7 +5483,7 @@ NOTE: This is also available as requestUserInput() inside execute_code for multi
5283
5483
  },
5284
5484
  timeout: {
5285
5485
  type: "number",
5286
- description: "Timeout in seconds (default: 300, min: 10, max: 900)"
5486
+ description: "Timeout in seconds (default: 1800, min: 10, max: 14400)"
5287
5487
  },
5288
5488
  planId: {
5289
5489
  type: "string",