@schoolai/shipyard 0.6.0 → 0.8.0-rc.20260221
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/dist/{chunk-CQBP5B4G.js → chunk-OTIQVES4.js} +19 -1
- package/dist/{chunk-CQBP5B4G.js.map → chunk-OTIQVES4.js.map} +1 -1
- package/dist/index.js +319 -46
- package/dist/index.js.map +1 -1
- package/dist/{login-625HP2EN.js → login-6TA7RIBE.js} +2 -2
- package/package.json +1 -1
- /package/dist/{login-625HP2EN.js.map → login-6TA7RIBE.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
PersonalRoomConnection,
|
|
13
13
|
ROUTES,
|
|
14
14
|
ValidationErrorResponseSchema
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-OTIQVES4.js";
|
|
16
16
|
import {
|
|
17
17
|
__export,
|
|
18
18
|
getShipyardHome,
|
|
@@ -11331,13 +11331,16 @@ var TaskDocumentSchema = Shape.doc({
|
|
|
11331
11331
|
updatedAt: Shape.plain.number()
|
|
11332
11332
|
}),
|
|
11333
11333
|
conversation: Shape.list(MessageShape),
|
|
11334
|
+
pendingFollowUps: Shape.list(MessageShape),
|
|
11334
11335
|
sessions: Shape.list(SessionEntryShape),
|
|
11335
11336
|
diffState: DiffStateShape,
|
|
11336
11337
|
plans: Shape.list(PlanVersionShape),
|
|
11337
11338
|
planEditorDocs: Shape.record(Shape.any()),
|
|
11338
11339
|
diffComments: Shape.record(DiffCommentShape),
|
|
11339
|
-
planComments: Shape.record(PlanCommentShape)
|
|
11340
|
+
planComments: Shape.record(PlanCommentShape),
|
|
11341
|
+
deliveredCommentIds: Shape.list(Shape.plain.string())
|
|
11340
11342
|
});
|
|
11343
|
+
var TERMINAL_TASK_STATES = ["completed", "failed", "canceled"];
|
|
11341
11344
|
var TOOL_RISK_LEVELS = ["low", "medium", "high"];
|
|
11342
11345
|
var PERMISSION_DECISIONS = ["approved", "denied"];
|
|
11343
11346
|
var PermissionRequestEphemeral = Shape.plain.struct({
|
|
@@ -11392,7 +11395,10 @@ var WorktreeScriptShape = Shape.plain.struct({
|
|
|
11392
11395
|
script: Shape.plain.string()
|
|
11393
11396
|
});
|
|
11394
11397
|
var UserSettingsShape = Shape.struct({
|
|
11395
|
-
worktreeScripts: Shape.record(WorktreeScriptShape)
|
|
11398
|
+
worktreeScripts: Shape.record(WorktreeScriptShape),
|
|
11399
|
+
composerModel: Shape.plain.string().nullable(),
|
|
11400
|
+
composerReasoning: Shape.plain.string(...REASONING_EFFORTS).nullable(),
|
|
11401
|
+
composerPermission: Shape.plain.string(...PERMISSION_MODES).nullable()
|
|
11396
11402
|
});
|
|
11397
11403
|
var TaskIndexDocumentSchema = Shape.doc({
|
|
11398
11404
|
taskIndex: Shape.record(TaskIndexEntryShape),
|
|
@@ -12759,13 +12765,17 @@ function recoverOrphanedTask(taskDoc, log) {
|
|
|
12759
12765
|
|
|
12760
12766
|
// src/peer-manager.ts
|
|
12761
12767
|
var ICE_SERVERS = [{ urls: "stun:stun.l.google.com:19302" }];
|
|
12768
|
+
var MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
|
|
12762
12769
|
function machineIdToPeerId(machineId) {
|
|
12763
12770
|
return machineId;
|
|
12764
12771
|
}
|
|
12765
12772
|
async function loadDefaultFactory() {
|
|
12766
12773
|
const { RTCPeerConnection } = await import("node-datachannel/polyfill");
|
|
12767
12774
|
return () => {
|
|
12768
|
-
const pc = new RTCPeerConnection({
|
|
12775
|
+
const pc = new RTCPeerConnection({
|
|
12776
|
+
iceServers: ICE_SERVERS,
|
|
12777
|
+
maxMessageSize: MAX_MESSAGE_SIZE
|
|
12778
|
+
});
|
|
12769
12779
|
return pc;
|
|
12770
12780
|
};
|
|
12771
12781
|
}
|
|
@@ -12909,6 +12919,40 @@ function createPeerManager(config2) {
|
|
|
12909
12919
|
};
|
|
12910
12920
|
}
|
|
12911
12921
|
|
|
12922
|
+
// src/plan-editor/format-diff-feedback.ts
|
|
12923
|
+
function formatDiffFeedbackForClaudeCode(comments, generalFeedback) {
|
|
12924
|
+
const sections = [];
|
|
12925
|
+
if (generalFeedback) {
|
|
12926
|
+
sections.push("## General Feedback\n");
|
|
12927
|
+
sections.push(generalFeedback);
|
|
12928
|
+
sections.push("");
|
|
12929
|
+
}
|
|
12930
|
+
if (comments.length > 0) {
|
|
12931
|
+
appendCommentSection(sections, comments);
|
|
12932
|
+
}
|
|
12933
|
+
return sections.join("\n").trim();
|
|
12934
|
+
}
|
|
12935
|
+
function appendCommentSection(sections, comments) {
|
|
12936
|
+
sections.push("## Inline Comments on Code Changes\n");
|
|
12937
|
+
sections.push("The user left the following comments on the diff:\n");
|
|
12938
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
12939
|
+
for (const c of comments) {
|
|
12940
|
+
const existing = byFile.get(c.filePath) ?? [];
|
|
12941
|
+
existing.push(c);
|
|
12942
|
+
byFile.set(c.filePath, existing);
|
|
12943
|
+
}
|
|
12944
|
+
for (const [filePath, fileComments] of byFile) {
|
|
12945
|
+
sections.push(`### ${filePath}
|
|
12946
|
+
`);
|
|
12947
|
+
const sorted = [...fileComments].sort((a, b) => a.lineNumber - b.lineNumber);
|
|
12948
|
+
for (const comment2 of sorted) {
|
|
12949
|
+
const sideLabel = comment2.side === "old" ? "before change" : "after change";
|
|
12950
|
+
sections.push(`> Line ${comment2.lineNumber} (${sideLabel}): ${comment2.body}`);
|
|
12951
|
+
}
|
|
12952
|
+
sections.push("");
|
|
12953
|
+
}
|
|
12954
|
+
}
|
|
12955
|
+
|
|
12912
12956
|
// src/plan-editor/format-plan-feedback.ts
|
|
12913
12957
|
function formatPlanFeedbackForClaudeCode(original, edited, comments, generalFeedback) {
|
|
12914
12958
|
const sections = [];
|
|
@@ -12918,7 +12962,7 @@ function formatPlanFeedbackForClaudeCode(original, edited, comments, generalFeed
|
|
|
12918
12962
|
sections.push("");
|
|
12919
12963
|
}
|
|
12920
12964
|
if (comments.length > 0) {
|
|
12921
|
-
|
|
12965
|
+
appendCommentSection2(sections, comments, edited);
|
|
12922
12966
|
}
|
|
12923
12967
|
if (original !== edited) {
|
|
12924
12968
|
sections.push("## Edits Made\n");
|
|
@@ -12930,7 +12974,7 @@ function formatPlanFeedbackForClaudeCode(original, edited, comments, generalFeed
|
|
|
12930
12974
|
}
|
|
12931
12975
|
return sections.join("\n").trim();
|
|
12932
12976
|
}
|
|
12933
|
-
function
|
|
12977
|
+
function appendCommentSection2(sections, comments, edited) {
|
|
12934
12978
|
sections.push("## Inline Comments\n");
|
|
12935
12979
|
sections.push("The user left the following comments on specific parts of the plan:\n");
|
|
12936
12980
|
const editedLines = edited.split("\n");
|
|
@@ -41672,7 +41716,8 @@ var SessionManager = class {
|
|
|
41672
41716
|
* Determine whether to resume an existing session or start fresh.
|
|
41673
41717
|
*
|
|
41674
41718
|
* Walks backwards through sessions to find the most recent one with a
|
|
41675
|
-
* non-empty agentSessionId that has not failed.
|
|
41719
|
+
* non-empty agentSessionId that has not genuinely failed. Interrupted
|
|
41720
|
+
* (user-cancelled) sessions are resumable. If found, returns
|
|
41676
41721
|
* { resume: true, sessionId } so the caller can pass it to resumeSession().
|
|
41677
41722
|
*/
|
|
41678
41723
|
shouldResume() {
|
|
@@ -41743,7 +41788,7 @@ var SessionManager = class {
|
|
|
41743
41788
|
});
|
|
41744
41789
|
this.#inputController = controller;
|
|
41745
41790
|
this.#activeQuery = response;
|
|
41746
|
-
return this.#processMessages(response, sessionId);
|
|
41791
|
+
return this.#processMessages(response, sessionId, opts.abortController);
|
|
41747
41792
|
}
|
|
41748
41793
|
/**
|
|
41749
41794
|
* Send a follow-up message to the active streaming session.
|
|
@@ -41778,6 +41823,9 @@ var SessionManager = class {
|
|
|
41778
41823
|
await this.#activeQuery?.setModel(model);
|
|
41779
41824
|
this.#currentModel = model;
|
|
41780
41825
|
}
|
|
41826
|
+
async setPermissionMode(mode) {
|
|
41827
|
+
await this.#activeQuery?.setPermissionMode(mode);
|
|
41828
|
+
}
|
|
41781
41829
|
/**
|
|
41782
41830
|
* Resume an existing Claude Code session using streaming input mode.
|
|
41783
41831
|
* Looks up the agentSessionId from the task doc and passes it as `resume`.
|
|
@@ -41839,9 +41887,9 @@ var SessionManager = class {
|
|
|
41839
41887
|
});
|
|
41840
41888
|
this.#inputController = controller;
|
|
41841
41889
|
this.#activeQuery = response;
|
|
41842
|
-
return this.#processMessages(response, newSessionId);
|
|
41890
|
+
return this.#processMessages(response, newSessionId, opts?.abortController);
|
|
41843
41891
|
}
|
|
41844
|
-
async #processMessages(response, sessionId) {
|
|
41892
|
+
async #processMessages(response, sessionId, abortController) {
|
|
41845
41893
|
let agentSessionId = "";
|
|
41846
41894
|
let lastMessageAt = Date.now();
|
|
41847
41895
|
let idleTimedOut = false;
|
|
@@ -41868,14 +41916,13 @@ var SessionManager = class {
|
|
|
41868
41916
|
}
|
|
41869
41917
|
}
|
|
41870
41918
|
} catch (error2) {
|
|
41871
|
-
|
|
41872
|
-
|
|
41873
|
-
return {
|
|
41919
|
+
return this.#handleProcessError(
|
|
41920
|
+
error2,
|
|
41874
41921
|
sessionId,
|
|
41875
41922
|
agentSessionId,
|
|
41876
|
-
|
|
41877
|
-
|
|
41878
|
-
|
|
41923
|
+
idleTimedOut,
|
|
41924
|
+
abortController
|
|
41925
|
+
);
|
|
41879
41926
|
} finally {
|
|
41880
41927
|
clearInterval(idleTimer);
|
|
41881
41928
|
this.#inputController = null;
|
|
@@ -42113,6 +42160,28 @@ var SessionManager = class {
|
|
|
42113
42160
|
error: !isSuccess ? errorText ?? message.subtype : void 0
|
|
42114
42161
|
};
|
|
42115
42162
|
}
|
|
42163
|
+
#handleProcessError(error2, sessionId, agentSessionId, idleTimedOut, abortController) {
|
|
42164
|
+
if (abortController?.signal.aborted) {
|
|
42165
|
+
this.#markInterrupted(sessionId);
|
|
42166
|
+
return { sessionId, agentSessionId, status: "interrupted" };
|
|
42167
|
+
}
|
|
42168
|
+
const errorMsg = idleTimedOut ? "Session idle timeout exceeded" : error2 instanceof Error ? error2.message : String(error2);
|
|
42169
|
+
this.#markFailed(sessionId, errorMsg);
|
|
42170
|
+
return { sessionId, agentSessionId, status: "failed", error: errorMsg };
|
|
42171
|
+
}
|
|
42172
|
+
#markInterrupted(sessionId) {
|
|
42173
|
+
const idx = this.#findSessionIndex(sessionId);
|
|
42174
|
+
change(this.#taskDoc, (draft) => {
|
|
42175
|
+
const session = idx >= 0 ? draft.sessions.get(idx) : void 0;
|
|
42176
|
+
if (session) {
|
|
42177
|
+
session.status = "interrupted";
|
|
42178
|
+
session.completedAt = Date.now();
|
|
42179
|
+
}
|
|
42180
|
+
draft.meta.status = "canceled";
|
|
42181
|
+
draft.meta.updatedAt = Date.now();
|
|
42182
|
+
});
|
|
42183
|
+
this.#notifyStatusChange("canceled");
|
|
42184
|
+
}
|
|
42116
42185
|
#markFailed(sessionId, errorMsg) {
|
|
42117
42186
|
const idx = this.#findSessionIndex(sessionId);
|
|
42118
42187
|
change(this.#taskDoc, (draft) => {
|
|
@@ -42488,6 +42557,34 @@ var TaskEphemeralDeclarations = {
|
|
|
42488
42557
|
permReqs: PermissionRequestEphemeral,
|
|
42489
42558
|
permResps: PermissionResponseEphemeral
|
|
42490
42559
|
};
|
|
42560
|
+
var TERMINAL_STATUSES = new Set(TERMINAL_TASK_STATES);
|
|
42561
|
+
async function rehydrateTaskDocuments(roomHandle, roomDoc, repo, log) {
|
|
42562
|
+
try {
|
|
42563
|
+
await roomHandle.waitForSync({ kind: "storage", timeout: 5e3 });
|
|
42564
|
+
} catch {
|
|
42565
|
+
log.warn(
|
|
42566
|
+
"Room doc storage sync timed out during rehydration \u2014 task rehydration may be incomplete"
|
|
42567
|
+
);
|
|
42568
|
+
}
|
|
42569
|
+
const roomJson = roomDoc.toJSON();
|
|
42570
|
+
const taskEntries = Object.entries(roomJson.taskIndex ?? {});
|
|
42571
|
+
log.info({ count: taskEntries.length }, "Rehydrating task documents from storage");
|
|
42572
|
+
for (const [taskId, entry] of taskEntries) {
|
|
42573
|
+
if (TERMINAL_STATUSES.has(entry.status)) continue;
|
|
42574
|
+
try {
|
|
42575
|
+
const taskDocId = buildDocumentId("task", taskId, DEFAULT_EPOCH);
|
|
42576
|
+
const taskHandle = repo.get(taskDocId, TaskDocumentSchema, TaskEphemeralDeclarations);
|
|
42577
|
+
await taskHandle.waitForSync({ kind: "storage", timeout: 5e3 });
|
|
42578
|
+
if (recoverOrphanedTask(taskHandle.doc, createChildLogger({ mode: "rehydrate", taskId }))) {
|
|
42579
|
+
updateTaskInIndex(roomDoc, taskId, { status: "failed", updatedAt: Date.now() });
|
|
42580
|
+
}
|
|
42581
|
+
log.debug({ taskId, taskDocId }, "Task document rehydrated");
|
|
42582
|
+
} catch (err) {
|
|
42583
|
+
log.warn({ taskId, err }, "Failed to rehydrate task document");
|
|
42584
|
+
}
|
|
42585
|
+
}
|
|
42586
|
+
log.info("Task document rehydration complete");
|
|
42587
|
+
}
|
|
42491
42588
|
async function serve(env) {
|
|
42492
42589
|
if (!env.SHIPYARD_SIGNALING_URL) {
|
|
42493
42590
|
logger.error("SHIPYARD_SIGNALING_URL is required for serve mode");
|
|
@@ -42520,6 +42617,7 @@ async function serve(env) {
|
|
|
42520
42617
|
const activeTasks = /* @__PURE__ */ new Map();
|
|
42521
42618
|
const watchedTasks = /* @__PURE__ */ new Map();
|
|
42522
42619
|
const taskHandles = /* @__PURE__ */ new Map();
|
|
42620
|
+
const lastProcessedConvLen = /* @__PURE__ */ new Map();
|
|
42523
42621
|
const devSuffix = env.SHIPYARD_DEV ? "-dev" : "";
|
|
42524
42622
|
const machineId = env.SHIPYARD_MACHINE_ID ?? `${hostname2()}${devSuffix}`;
|
|
42525
42623
|
const dataDir = resolve(env.SHIPYARD_DATA_DIR.replace("~", homedir2()));
|
|
@@ -42733,6 +42831,7 @@ async function serve(env) {
|
|
|
42733
42831
|
const typedRoomHandle = roomHandle;
|
|
42734
42832
|
const typedRoomDoc = roomHandle.doc;
|
|
42735
42833
|
cleanupStaleSetupEntries(typedRoomDoc, machineId, log);
|
|
42834
|
+
await rehydrateTaskDocuments(roomHandle, typedRoomDoc, repo, log);
|
|
42736
42835
|
typedRoomHandle.enhancePromptReqs.subscribe(({ key: requestId, value, source }) => {
|
|
42737
42836
|
if (source !== "remote") return;
|
|
42738
42837
|
if (!value) return;
|
|
@@ -42904,6 +43003,7 @@ async function serve(env) {
|
|
|
42904
43003
|
activeTasks,
|
|
42905
43004
|
watchedTasks,
|
|
42906
43005
|
taskHandles,
|
|
43006
|
+
lastProcessedConvLen,
|
|
42907
43007
|
peerManager,
|
|
42908
43008
|
env,
|
|
42909
43009
|
machineId,
|
|
@@ -42935,6 +43035,7 @@ async function serve(env) {
|
|
|
42935
43035
|
}
|
|
42936
43036
|
watchedTasks.clear();
|
|
42937
43037
|
taskHandles.clear();
|
|
43038
|
+
lastProcessedConvLen.clear();
|
|
42938
43039
|
for (const timer of pendingCleanupTimers) clearTimeout(timer);
|
|
42939
43040
|
pendingCleanupTimers.clear();
|
|
42940
43041
|
for (const [id, ptyMgr] of terminalPtys) {
|
|
@@ -43109,6 +43210,12 @@ function handleMessage(msg, ctx) {
|
|
|
43109
43210
|
case "worktree-create-error":
|
|
43110
43211
|
ctx.log.debug({ type: msg.type }, "Worktree create echo");
|
|
43111
43212
|
break;
|
|
43213
|
+
case "cancel-task":
|
|
43214
|
+
handleCancelTask(msg, ctx);
|
|
43215
|
+
break;
|
|
43216
|
+
case "control-ack":
|
|
43217
|
+
ctx.log.debug({ type: msg.type }, "Control ack echo");
|
|
43218
|
+
break;
|
|
43112
43219
|
case "authenticated":
|
|
43113
43220
|
case "agent-joined":
|
|
43114
43221
|
case "agent-left":
|
|
@@ -43160,6 +43267,32 @@ function handleNotifyTask(msg, ctx) {
|
|
|
43160
43267
|
taskLog.error({ err: errMsg }, "Failed to start watching task document");
|
|
43161
43268
|
});
|
|
43162
43269
|
}
|
|
43270
|
+
function handleCancelTask(msg, ctx) {
|
|
43271
|
+
const { taskId, requestId } = msg;
|
|
43272
|
+
const taskLog = createChildLogger({ mode: "serve", taskId });
|
|
43273
|
+
const activeTask = ctx.activeTasks.get(taskId);
|
|
43274
|
+
if (!activeTask) {
|
|
43275
|
+
taskLog.warn("Cancel requested but no active task found");
|
|
43276
|
+
ctx.connection.send({
|
|
43277
|
+
type: "control-ack",
|
|
43278
|
+
requestId,
|
|
43279
|
+
taskId,
|
|
43280
|
+
action: "cancel",
|
|
43281
|
+
accepted: false,
|
|
43282
|
+
error: "No active agent running for this task"
|
|
43283
|
+
});
|
|
43284
|
+
return;
|
|
43285
|
+
}
|
|
43286
|
+
taskLog.info("Canceling active task");
|
|
43287
|
+
activeTask.abortController.abort();
|
|
43288
|
+
ctx.connection.send({
|
|
43289
|
+
type: "control-ack",
|
|
43290
|
+
requestId,
|
|
43291
|
+
taskId,
|
|
43292
|
+
action: "cancel",
|
|
43293
|
+
accepted: true
|
|
43294
|
+
});
|
|
43295
|
+
}
|
|
43163
43296
|
var ENHANCE_PROMPT_TIMEOUT_MS = 3e4;
|
|
43164
43297
|
var ENHANCE_SYSTEM_PROMPT = `You are a prompt rewriter that transforms rough user requests into clear, specific instructions for an AI coding assistant (Claude Code). The enhanced prompt will be sent AS the user's message to that assistant.
|
|
43165
43298
|
|
|
@@ -43586,12 +43719,15 @@ async function watchTaskDocument(taskId, taskLog, ctx) {
|
|
|
43586
43719
|
try {
|
|
43587
43720
|
await taskHandle.waitForSync({ kind: "storage", timeout: 5e3 });
|
|
43588
43721
|
} catch {
|
|
43589
|
-
taskLog.
|
|
43722
|
+
taskLog.info({ taskDocId }, "No existing task data in storage");
|
|
43590
43723
|
}
|
|
43591
43724
|
try {
|
|
43592
43725
|
await taskHandle.waitForSync({ kind: "network", timeout: 3e3 });
|
|
43593
43726
|
} catch {
|
|
43594
|
-
taskLog.
|
|
43727
|
+
taskLog.warn(
|
|
43728
|
+
{ taskDocId, timeoutMs: 3e3 },
|
|
43729
|
+
"Network sync timed out (browser may not be connected yet)"
|
|
43730
|
+
);
|
|
43595
43731
|
}
|
|
43596
43732
|
if (recoverOrphanedTask(taskHandle.doc, taskLog)) {
|
|
43597
43733
|
updateTaskInIndex(ctx.roomDoc, taskId, { status: "failed", updatedAt: Date.now() });
|
|
@@ -43622,28 +43758,50 @@ async function watchTaskDocument(taskId, taskLog, ctx) {
|
|
|
43622
43758
|
onTaskDocChanged(taskId, taskHandle, taskLog, ctx);
|
|
43623
43759
|
}
|
|
43624
43760
|
}
|
|
43625
|
-
function
|
|
43761
|
+
function promotePendingFollowUps(taskHandle, activeTask, taskLog) {
|
|
43626
43762
|
if (!activeTask) return;
|
|
43627
|
-
if (
|
|
43628
|
-
taskLog.debug(
|
|
43629
|
-
{ conversationLen, lastDispatched: activeTask.lastDispatchedConvLen },
|
|
43630
|
-
"Conversation unchanged since last dispatch, skipping duplicate"
|
|
43631
|
-
);
|
|
43763
|
+
if (activeTask.abortController.signal.aborted) {
|
|
43764
|
+
taskLog.debug("Task is being aborted, skipping pending follow-up promotion");
|
|
43632
43765
|
return;
|
|
43633
43766
|
}
|
|
43767
|
+
const json = taskHandle.doc.toJSON();
|
|
43768
|
+
const pending = json.pendingFollowUps ?? [];
|
|
43769
|
+
if (pending.length === 0) return;
|
|
43770
|
+
change(taskHandle.doc, (draft) => {
|
|
43771
|
+
const items = draft.pendingFollowUps.toArray();
|
|
43772
|
+
for (const msg of items) {
|
|
43773
|
+
draft.conversation.push(msg);
|
|
43774
|
+
}
|
|
43775
|
+
if (draft.pendingFollowUps.length > 0) {
|
|
43776
|
+
draft.pendingFollowUps.delete(0, draft.pendingFollowUps.length);
|
|
43777
|
+
}
|
|
43778
|
+
});
|
|
43779
|
+
taskLog.info({ pendingCount: pending.length }, "Promoted pending follow-ups to conversation");
|
|
43634
43780
|
if (!activeTask.sessionManager.isStreaming) {
|
|
43635
|
-
taskLog.debug("Task
|
|
43781
|
+
taskLog.debug("Task not streaming, skipping follow-up dispatch");
|
|
43636
43782
|
return;
|
|
43637
43783
|
}
|
|
43638
|
-
const
|
|
43639
|
-
|
|
43784
|
+
const allContentBlocks = pending.flatMap(
|
|
43785
|
+
(msg) => msg.content.filter((block2) => block2.type === "text" || block2.type === "image")
|
|
43786
|
+
);
|
|
43787
|
+
if (allContentBlocks.length === 0) return;
|
|
43788
|
+
const dispatchFollowUp = () => {
|
|
43640
43789
|
try {
|
|
43641
|
-
|
|
43642
|
-
activeTask.
|
|
43643
|
-
activeTask.sessionManager.sendFollowUp(contentBlocks);
|
|
43790
|
+
activeTask.lastDispatchedConvLen = json.conversation.length + pending.length;
|
|
43791
|
+
activeTask.sessionManager.sendFollowUp(allContentBlocks);
|
|
43644
43792
|
} catch (err) {
|
|
43645
|
-
taskLog.warn({ err }, "Failed to send follow-up
|
|
43793
|
+
taskLog.warn({ err }, "Failed to send promoted follow-up");
|
|
43646
43794
|
}
|
|
43795
|
+
};
|
|
43796
|
+
const lastPending = pending[pending.length - 1];
|
|
43797
|
+
const mappedMode = lastPending?.permissionMode ? mapPermissionMode(lastPending.permissionMode) : void 0;
|
|
43798
|
+
if (mappedMode) {
|
|
43799
|
+
activeTask.sessionManager.setPermissionMode(mappedMode).then(dispatchFollowUp).catch((err) => {
|
|
43800
|
+
taskLog.warn({ err }, "Failed to update permission mode from queued message");
|
|
43801
|
+
dispatchFollowUp();
|
|
43802
|
+
});
|
|
43803
|
+
} else {
|
|
43804
|
+
dispatchFollowUp();
|
|
43647
43805
|
}
|
|
43648
43806
|
}
|
|
43649
43807
|
function onTaskDocChanged(taskId, taskHandle, taskLog, ctx) {
|
|
@@ -43659,22 +43817,35 @@ function onTaskDocChanged(taskId, taskHandle, taskLog, ctx) {
|
|
|
43659
43817
|
"onTaskDocChanged evaluation"
|
|
43660
43818
|
);
|
|
43661
43819
|
if (ctx.activeTasks.has(taskId)) {
|
|
43662
|
-
const
|
|
43663
|
-
|
|
43664
|
-
|
|
43665
|
-
handleFollowUp(ctx.activeTasks.get(taskId), conversation2.length, taskLog);
|
|
43820
|
+
const pendingFollowUps2 = json.pendingFollowUps ?? [];
|
|
43821
|
+
if (pendingFollowUps2.length > 0) {
|
|
43822
|
+
promotePendingFollowUps(taskHandle, ctx.activeTasks.get(taskId), taskLog);
|
|
43666
43823
|
}
|
|
43824
|
+
const conversation2 = json.conversation;
|
|
43667
43825
|
const activeLastUserMsg = [...conversation2].reverse().find((m) => m.role === "user");
|
|
43668
43826
|
const activeCwd = activeLastUserMsg?.cwd ?? process.cwd();
|
|
43669
43827
|
debouncedDiffCapture(taskId, activeCwd, taskHandle, taskLog);
|
|
43670
43828
|
debouncedBranchDiffCapture(taskId, activeCwd, taskHandle, taskLog);
|
|
43671
43829
|
return;
|
|
43672
43830
|
}
|
|
43673
|
-
|
|
43674
|
-
|
|
43675
|
-
|
|
43831
|
+
const pendingFollowUps = json.pendingFollowUps ?? [];
|
|
43832
|
+
if (pendingFollowUps.length > 0) {
|
|
43833
|
+
taskLog.info(
|
|
43834
|
+
{ pendingCount: pendingFollowUps.length },
|
|
43835
|
+
"Promoting orphaned pending follow-ups"
|
|
43836
|
+
);
|
|
43837
|
+
change(taskHandle.doc, (draft) => {
|
|
43838
|
+
const items = draft.pendingFollowUps.toArray();
|
|
43839
|
+
for (const msg of items) {
|
|
43840
|
+
draft.conversation.push(msg);
|
|
43841
|
+
}
|
|
43842
|
+
if (draft.pendingFollowUps.length > 0) {
|
|
43843
|
+
draft.pendingFollowUps.delete(0, draft.pendingFollowUps.length);
|
|
43844
|
+
}
|
|
43845
|
+
});
|
|
43676
43846
|
}
|
|
43677
|
-
const
|
|
43847
|
+
const freshJson = taskHandle.doc.toJSON();
|
|
43848
|
+
const conversation = freshJson.conversation;
|
|
43678
43849
|
if (conversation.length === 0) {
|
|
43679
43850
|
taskLog.debug("No conversation messages, skipping");
|
|
43680
43851
|
return;
|
|
@@ -43683,7 +43854,18 @@ function onTaskDocChanged(taskId, taskHandle, taskLog, ctx) {
|
|
|
43683
43854
|
if (!lastMessage || lastMessage.role !== "user") {
|
|
43684
43855
|
return;
|
|
43685
43856
|
}
|
|
43686
|
-
|
|
43857
|
+
const prevLen = ctx.lastProcessedConvLen.get(taskId) ?? 0;
|
|
43858
|
+
if (conversation.length <= prevLen) {
|
|
43859
|
+
taskLog.debug(
|
|
43860
|
+
{ conversationLen: conversation.length, lastProcessed: prevLen },
|
|
43861
|
+
"No new messages since last run, skipping"
|
|
43862
|
+
);
|
|
43863
|
+
return;
|
|
43864
|
+
}
|
|
43865
|
+
taskLog.info(
|
|
43866
|
+
{ prevLen, newLen: conversation.length },
|
|
43867
|
+
"New user message detected, starting agent"
|
|
43868
|
+
);
|
|
43687
43869
|
const cwd = lastMessage.cwd ?? process.cwd();
|
|
43688
43870
|
const model = lastMessage.model ?? void 0;
|
|
43689
43871
|
const permissionMode = mapPermissionMode(lastMessage.permissionMode);
|
|
@@ -43700,6 +43882,7 @@ function onTaskDocChanged(taskId, taskHandle, taskLog, ctx) {
|
|
|
43700
43882
|
lastDispatchedConvLen: conversation.length
|
|
43701
43883
|
};
|
|
43702
43884
|
ctx.activeTasks.set(taskId, activeTask);
|
|
43885
|
+
ctx.lastProcessedConvLen.set(taskId, conversation.length);
|
|
43703
43886
|
ctx.signaling.updateStatus("running", taskId);
|
|
43704
43887
|
const turnStartRefPromise = captureTreeSnapshot(cwd);
|
|
43705
43888
|
turnStartRefPromise.then((ref) => taskLog.debug({ turnStartRef: ref }, "Captured turn start snapshot")).catch((err) => taskLog.warn({ err }, "Failed to capture turn start snapshot"));
|
|
@@ -43746,6 +43929,7 @@ async function cleanupTaskRun(opts) {
|
|
|
43746
43929
|
const { taskId, cwd, taskHandle, taskLog, turnStartRefPromise, abortController, ctx } = opts;
|
|
43747
43930
|
const activeTask = ctx.activeTasks.get(taskId);
|
|
43748
43931
|
activeTask?.sessionManager.closeSession();
|
|
43932
|
+
abortController.abort();
|
|
43749
43933
|
clearDebouncedTimer(diffDebounceTimers, taskId);
|
|
43750
43934
|
clearDebouncedTimer(branchDiffTimers, taskId);
|
|
43751
43935
|
try {
|
|
@@ -43764,7 +43948,6 @@ async function cleanupTaskRun(opts) {
|
|
|
43764
43948
|
} catch (err) {
|
|
43765
43949
|
taskLog.warn({ err }, "Failed to capture turn diff");
|
|
43766
43950
|
}
|
|
43767
|
-
abortController.abort();
|
|
43768
43951
|
for (const [key] of taskHandle.permReqs.getAll()) {
|
|
43769
43952
|
taskHandle.permReqs.delete(key);
|
|
43770
43953
|
}
|
|
@@ -43773,6 +43956,7 @@ async function cleanupTaskRun(opts) {
|
|
|
43773
43956
|
}
|
|
43774
43957
|
ctx.activeTasks.delete(taskId);
|
|
43775
43958
|
ctx.signaling.updateStatus("idle");
|
|
43959
|
+
onTaskDocChanged(taskId, taskHandle, taskLog, ctx);
|
|
43776
43960
|
}
|
|
43777
43961
|
function clearDebouncedTimer(timers, taskId) {
|
|
43778
43962
|
const timer = timers.get(taskId);
|
|
@@ -43846,6 +44030,19 @@ function resolveExitPlanMode(taskHandle, taskLog, toolUseID, value) {
|
|
|
43846
44030
|
"Updated plan reviewStatus in CRDT with rich feedback"
|
|
43847
44031
|
);
|
|
43848
44032
|
}
|
|
44033
|
+
function resolveAskUserQuestion(taskLog, toolUseID, input, value) {
|
|
44034
|
+
if (value.decision !== "approved" || !value.message) return input;
|
|
44035
|
+
try {
|
|
44036
|
+
const parsed = JSON.parse(value.message);
|
|
44037
|
+
if ("answers" in parsed && parsed.answers && typeof parsed.answers === "object" && !Array.isArray(parsed.answers)) {
|
|
44038
|
+
taskLog.info({ toolUseID }, "Merged AskUserQuestion answers into input");
|
|
44039
|
+
return { ...input, answers: parsed.answers };
|
|
44040
|
+
}
|
|
44041
|
+
} catch {
|
|
44042
|
+
taskLog.warn({ toolUseID }, "Failed to parse AskUserQuestion answers from message");
|
|
44043
|
+
}
|
|
44044
|
+
return input;
|
|
44045
|
+
}
|
|
43849
44046
|
function resolvePermissionResponse(ctx) {
|
|
43850
44047
|
const { taskHandle, roomDoc, taskId, taskLog, toolName, toolUseID, input, suggestions, value } = ctx;
|
|
43851
44048
|
taskHandle.permReqs.delete(toolUseID);
|
|
@@ -43858,6 +44055,10 @@ function resolvePermissionResponse(ctx) {
|
|
|
43858
44055
|
if (toolName === "ExitPlanMode") {
|
|
43859
44056
|
resolveExitPlanMode(taskHandle, taskLog, toolUseID, value);
|
|
43860
44057
|
}
|
|
44058
|
+
let resolvedInput = input;
|
|
44059
|
+
if (toolName === "AskUserQuestion") {
|
|
44060
|
+
resolvedInput = resolveAskUserQuestion(taskLog, toolUseID, input, value);
|
|
44061
|
+
}
|
|
43861
44062
|
taskLog.info(
|
|
43862
44063
|
{
|
|
43863
44064
|
toolName,
|
|
@@ -43869,7 +44070,8 @@ function resolvePermissionResponse(ctx) {
|
|
|
43869
44070
|
"Permission response received"
|
|
43870
44071
|
);
|
|
43871
44072
|
const decision = value.decision === "approved" ? "approved" : "denied";
|
|
43872
|
-
|
|
44073
|
+
const resultMessage = toolName === "AskUserQuestion" && decision === "approved" ? null : value.message;
|
|
44074
|
+
return toPermissionResult(decision, resolvedInput, suggestions, resultMessage);
|
|
43873
44075
|
}
|
|
43874
44076
|
function buildCanUseTool(taskHandle, taskLog, roomDoc, taskId) {
|
|
43875
44077
|
return async (toolName, input, options) => {
|
|
@@ -43950,6 +44152,76 @@ function buildCanUseTool(taskHandle, taskLog, roomDoc, taskId) {
|
|
|
43950
44152
|
});
|
|
43951
44153
|
};
|
|
43952
44154
|
}
|
|
44155
|
+
function collectPlanFeedback(unresolvedPlan, plans, loroDoc) {
|
|
44156
|
+
const parts = [];
|
|
44157
|
+
const byPlanId = /* @__PURE__ */ new Map();
|
|
44158
|
+
for (const c of unresolvedPlan) {
|
|
44159
|
+
const existing = byPlanId.get(c.planId) ?? [];
|
|
44160
|
+
existing.push(c);
|
|
44161
|
+
byPlanId.set(c.planId, existing);
|
|
44162
|
+
}
|
|
44163
|
+
for (const [planId, comments] of byPlanId) {
|
|
44164
|
+
const plan = plans.find((p) => p.planId === planId);
|
|
44165
|
+
const originalMarkdown = plan?.markdown ?? "";
|
|
44166
|
+
const editedMarkdown = serializePlanEditorDoc(loroDoc, planId) || originalMarkdown;
|
|
44167
|
+
const feedback = formatPlanFeedbackForClaudeCode(
|
|
44168
|
+
originalMarkdown,
|
|
44169
|
+
editedMarkdown,
|
|
44170
|
+
comments,
|
|
44171
|
+
null
|
|
44172
|
+
);
|
|
44173
|
+
if (feedback) {
|
|
44174
|
+
parts.push(feedback);
|
|
44175
|
+
}
|
|
44176
|
+
}
|
|
44177
|
+
return parts;
|
|
44178
|
+
}
|
|
44179
|
+
function harvestUndeliveredComments(taskHandle, contentBlocks, log) {
|
|
44180
|
+
const json = taskHandle.doc.toJSON();
|
|
44181
|
+
const deliveredSet = new Set(json.deliveredCommentIds ?? []);
|
|
44182
|
+
const unresolvedDiff = Object.values(json.diffComments).filter(
|
|
44183
|
+
(c) => c.resolvedAt === null && !deliveredSet.has(c.commentId)
|
|
44184
|
+
);
|
|
44185
|
+
const unresolvedPlan = Object.values(json.planComments).filter(
|
|
44186
|
+
(c) => c.resolvedAt === null && !deliveredSet.has(c.commentId)
|
|
44187
|
+
);
|
|
44188
|
+
if (unresolvedDiff.length === 0 && unresolvedPlan.length === 0) {
|
|
44189
|
+
return;
|
|
44190
|
+
}
|
|
44191
|
+
const feedbackParts = [];
|
|
44192
|
+
if (unresolvedDiff.length > 0) {
|
|
44193
|
+
const diffFeedback = formatDiffFeedbackForClaudeCode(unresolvedDiff, null);
|
|
44194
|
+
if (diffFeedback) {
|
|
44195
|
+
feedbackParts.push(diffFeedback);
|
|
44196
|
+
}
|
|
44197
|
+
}
|
|
44198
|
+
if (unresolvedPlan.length > 0) {
|
|
44199
|
+
feedbackParts.push(...collectPlanFeedback(unresolvedPlan, json.plans, taskHandle.loroDoc));
|
|
44200
|
+
}
|
|
44201
|
+
if (feedbackParts.length === 0) {
|
|
44202
|
+
return;
|
|
44203
|
+
}
|
|
44204
|
+
const feedbackText = `
|
|
44205
|
+
|
|
44206
|
+
---
|
|
44207
|
+
**User feedback on your changes (comments from code review):**
|
|
44208
|
+
|
|
44209
|
+
${feedbackParts.join("\n\n")}`;
|
|
44210
|
+
contentBlocks.push({ type: "text", text: feedbackText });
|
|
44211
|
+
const harvestedIds = [
|
|
44212
|
+
...unresolvedDiff.map((c) => c.commentId),
|
|
44213
|
+
...unresolvedPlan.map((c) => c.commentId)
|
|
44214
|
+
];
|
|
44215
|
+
change(taskHandle.doc, (draft) => {
|
|
44216
|
+
for (const id of harvestedIds) {
|
|
44217
|
+
draft.deliveredCommentIds.push(id);
|
|
44218
|
+
}
|
|
44219
|
+
});
|
|
44220
|
+
log.info(
|
|
44221
|
+
{ diffCommentCount: unresolvedDiff.length, planCommentCount: unresolvedPlan.length },
|
|
44222
|
+
"Harvested undelivered comments as safety net"
|
|
44223
|
+
);
|
|
44224
|
+
}
|
|
43953
44225
|
async function runTask(opts) {
|
|
43954
44226
|
const {
|
|
43955
44227
|
sessionManager: manager,
|
|
@@ -43968,13 +44240,14 @@ async function runTask(opts) {
|
|
|
43968
44240
|
if (!contentBlocks || contentBlocks.length === 0) {
|
|
43969
44241
|
throw new Error(`No user message found in task ${taskId}`);
|
|
43970
44242
|
}
|
|
44243
|
+
harvestUndeliveredComments(taskHandle, contentBlocks, log);
|
|
43971
44244
|
const textPreview = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join(" ").slice(0, 100);
|
|
43972
44245
|
const imageCount = contentBlocks.filter((b) => b.type === "image").length;
|
|
43973
44246
|
log.info(
|
|
43974
44247
|
{ prompt: textPreview || "(images only)", imageCount },
|
|
43975
44248
|
"Running task with prompt from CRDT"
|
|
43976
44249
|
);
|
|
43977
|
-
const canUseTool =
|
|
44250
|
+
const canUseTool = buildCanUseTool(taskHandle, log, roomDoc, taskId);
|
|
43978
44251
|
const stderr = (data) => {
|
|
43979
44252
|
const trimmed = data.trim();
|
|
43980
44253
|
if (!trimmed) return;
|
|
@@ -43995,7 +44268,7 @@ async function runTask(opts) {
|
|
|
43995
44268
|
effort,
|
|
43996
44269
|
canUseTool,
|
|
43997
44270
|
stderr,
|
|
43998
|
-
allowDangerouslySkipPermissions:
|
|
44271
|
+
allowDangerouslySkipPermissions: true
|
|
43999
44272
|
});
|
|
44000
44273
|
}
|
|
44001
44274
|
return manager.createSession({
|
|
@@ -44008,7 +44281,7 @@ async function runTask(opts) {
|
|
|
44008
44281
|
abortController,
|
|
44009
44282
|
canUseTool,
|
|
44010
44283
|
stderr,
|
|
44011
|
-
allowDangerouslySkipPermissions:
|
|
44284
|
+
allowDangerouslySkipPermissions: true
|
|
44012
44285
|
});
|
|
44013
44286
|
}
|
|
44014
44287
|
|
|
@@ -44154,7 +44427,7 @@ function handleResult(log, result, startTime) {
|
|
|
44154
44427
|
async function handleSubcommand() {
|
|
44155
44428
|
const subcommand = process.argv[2];
|
|
44156
44429
|
if (subcommand === "login") {
|
|
44157
|
-
const { loginCommand } = await import("./login-
|
|
44430
|
+
const { loginCommand } = await import("./login-6TA7RIBE.js");
|
|
44158
44431
|
const hasCheck = process.argv.includes("--check");
|
|
44159
44432
|
await loginCommand({ check: hasCheck });
|
|
44160
44433
|
return true;
|