@schoolai/shipyard-mcp 0.1.3 → 0.2.0-next.478
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/apps/hook/dist/index.js +1201 -873
- package/apps/server/.env.example +9 -0
- package/apps/server/dist/{chunk-LTC26IRQ.js → chunk-76JWRTPI.js} +58 -2
- package/apps/server/dist/{chunk-N44DCU4J.js → chunk-BWP37ADP.js} +2 -4
- package/apps/server/dist/{chunk-7GPZDCWI.js → chunk-E5DWX2WU.js} +23 -1
- package/apps/server/dist/{dist-LQBXUHLR.js → dist-ORKL4P3L.js} +7 -1
- package/apps/server/dist/index.js +296 -96
- package/apps/server/dist/{input-request-manager-MVKPYLFW.js → input-request-manager-73GSTOIB.js} +2 -2
- package/apps/server/dist/{server-identity-KUXYHULN.js → server-identity-6PHKR2FY.js} +3 -1
- package/package.json +13 -12
|
@@ -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-
|
|
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-
|
|
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-
|
|
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
|
|
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}/${
|
|
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("
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
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: ${
|
|
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 =
|
|
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 '
|
|
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
|
|
4150
|
-
|
|
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":"$
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
echo "
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
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
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
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 '
|
|
4911
|
+
- script: Bash script that polls registry server and exits when status becomes 'in_progress' (approved) or 'changes_requested' (needs work)
|
|
4771
4912
|
|
|
4772
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4778
|
-
|
|
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:
|
|
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:${
|
|
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-
|
|
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-
|
|
5112
|
-
const { getGitHubUsername: getGitHubUsername2 } = await import("./server-identity-
|
|
5113
|
-
const { 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 =
|
|
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-
|
|
5135
|
-
const { getGitHubUsername: getGitHubUsername2 } = await import("./server-identity-
|
|
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:
|
|
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:
|
|
5486
|
+
description: "Timeout in seconds (default: 1800, min: 10, max: 14400)"
|
|
5287
5487
|
},
|
|
5288
5488
|
planId: {
|
|
5289
5489
|
type: "string",
|