@bojackduy/opencode-loopd 1.9.1 → 1.9.3
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/server.js +312 -16
- package/dist/tui.js +422 -253
- package/package.json +4 -4
package/dist/server.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __esm = (fn, res, err) => () => {
|
|
3
|
+
if (fn)
|
|
4
|
+
try {
|
|
5
|
+
res = fn(fn = 0);
|
|
6
|
+
} catch (e) {
|
|
7
|
+
err = [e];
|
|
8
|
+
}
|
|
9
|
+
if (err)
|
|
10
|
+
throw err[0];
|
|
11
|
+
return res;
|
|
12
|
+
};
|
|
13
|
+
|
|
2
14
|
// src/domain/runtime.ts
|
|
3
15
|
function createRuntimeState(goalID) {
|
|
4
16
|
const now = new Date().toISOString();
|
|
@@ -34,6 +46,7 @@ function acquireLease(rt, timeoutMs) {
|
|
|
34
46
|
idleConfirmFailedAt: undefined,
|
|
35
47
|
idleConfirmFailedGeneration: undefined,
|
|
36
48
|
idleStuckNotifiedGeneration: undefined,
|
|
49
|
+
workerAbortedAt: undefined,
|
|
37
50
|
activePromptObservedAt: undefined,
|
|
38
51
|
activeAssistantMessageID: undefined,
|
|
39
52
|
activeAssistantCompletedAt: undefined,
|
|
@@ -112,14 +125,10 @@ function removeToolCall(rt, callID) {
|
|
|
112
125
|
}
|
|
113
126
|
var PARENT_NOTIFY_DEDUPE_MS = 60000;
|
|
114
127
|
|
|
115
|
-
// src/application/control-worker.ts
|
|
116
|
-
import { randomUUID } from "crypto";
|
|
117
|
-
|
|
118
128
|
// src/infrastructure/state-repository.ts
|
|
119
129
|
import { promises as fs } from "fs";
|
|
120
130
|
import path from "path";
|
|
121
131
|
import os from "os";
|
|
122
|
-
var CURRENT_VERSION = 6;
|
|
123
132
|
function emptyState() {
|
|
124
133
|
return { version: CURRENT_VERSION, revision: 0, goals: [], runtimes: [], commandLedger: [] };
|
|
125
134
|
}
|
|
@@ -139,7 +148,6 @@ function lockDir(directory) {
|
|
|
139
148
|
function lockFile(directory, key) {
|
|
140
149
|
return path.join(lockDir(directory), `${key}.lock`);
|
|
141
150
|
}
|
|
142
|
-
var LOCK_STALE_MS = 1e4;
|
|
143
151
|
async function acquireLock(directory, key, operation) {
|
|
144
152
|
const dir = lockDir(directory);
|
|
145
153
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -475,6 +483,15 @@ async function peekGoalInbox(directory, goalID) {
|
|
|
475
483
|
function delay(ms) {
|
|
476
484
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
477
485
|
}
|
|
486
|
+
var CURRENT_VERSION = 6, LOCK_STALE_MS = 1e4;
|
|
487
|
+
var init_state_repository = () => {};
|
|
488
|
+
|
|
489
|
+
// src/server/plugin.ts
|
|
490
|
+
import { define } from "@opencode-ai/plugin/v2/promise";
|
|
491
|
+
|
|
492
|
+
// src/application/control-worker.ts
|
|
493
|
+
init_state_repository();
|
|
494
|
+
import { randomUUID } from "crypto";
|
|
478
495
|
|
|
479
496
|
// src/application/goal-policy.ts
|
|
480
497
|
function resolveGoalCreationConfig(input) {
|
|
@@ -595,12 +612,17 @@ function createControlWorker(options) {
|
|
|
595
612
|
if (running)
|
|
596
613
|
return;
|
|
597
614
|
running = true;
|
|
598
|
-
processPending()
|
|
615
|
+
processPending().catch((error) => {
|
|
616
|
+
logServerEvent(directory, "control.worker.error", { detail: describeError(error) }).catch(() => {});
|
|
617
|
+
});
|
|
599
618
|
pollTimer = setInterval(() => {
|
|
600
619
|
if (running && lastProcessDone) {
|
|
601
620
|
lastProcessDone = false;
|
|
602
621
|
processPending().then(() => {
|
|
603
622
|
lastProcessDone = true;
|
|
623
|
+
}, (error) => {
|
|
624
|
+
lastProcessDone = true;
|
|
625
|
+
logServerEvent(directory, "control.worker.error", { detail: describeError(error) }).catch(() => {});
|
|
604
626
|
});
|
|
605
627
|
}
|
|
606
628
|
}, pollMs);
|
|
@@ -749,6 +771,21 @@ function createControlWorker(options) {
|
|
|
749
771
|
};
|
|
750
772
|
break;
|
|
751
773
|
}
|
|
774
|
+
case "nudge": {
|
|
775
|
+
if (!request.goalID) {
|
|
776
|
+
response = { ...base, ok: false, message: "goalID is required", errorCode: "bad_request" };
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
const result = await goalSvc.nudge(directory, request.goalID);
|
|
780
|
+
const state = await readState(directory);
|
|
781
|
+
response = {
|
|
782
|
+
...base,
|
|
783
|
+
ok: result.ok,
|
|
784
|
+
message: result.message,
|
|
785
|
+
stateRevision: state.revision
|
|
786
|
+
};
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
752
789
|
case "clear": {
|
|
753
790
|
await goalSvc.clear(directory, request.goalID);
|
|
754
791
|
const state = await readState(directory);
|
|
@@ -770,12 +807,27 @@ function createControlWorker(options) {
|
|
|
770
807
|
response = { ...base, ok: false, message: "goalID is required", errorCode: "bad_request" };
|
|
771
808
|
break;
|
|
772
809
|
}
|
|
773
|
-
await
|
|
810
|
+
const sent = await goalSvc.sendUserMessage(directory, request.goalID, text);
|
|
774
811
|
const state = await readState(directory);
|
|
775
|
-
const goal = state.goals.find((g) => g.id === request.goalID);
|
|
776
812
|
response = {
|
|
777
813
|
...base,
|
|
778
|
-
|
|
814
|
+
ok: sent.ok,
|
|
815
|
+
message: sent.message,
|
|
816
|
+
stateRevision: state.revision
|
|
817
|
+
};
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
case "abort_worker": {
|
|
821
|
+
if (!request.goalID) {
|
|
822
|
+
response = { ...base, ok: false, message: "goalID is required", errorCode: "bad_request" };
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
const result = await goalSvc.abortWorker(directory, request.goalID);
|
|
826
|
+
const state = await readState(directory);
|
|
827
|
+
response = {
|
|
828
|
+
...base,
|
|
829
|
+
ok: result.ok,
|
|
830
|
+
message: result.message,
|
|
779
831
|
stateRevision: state.revision
|
|
780
832
|
};
|
|
781
833
|
break;
|
|
@@ -896,6 +948,7 @@ function createControlWorker(options) {
|
|
|
896
948
|
}
|
|
897
949
|
|
|
898
950
|
// src/application/loop-engine.ts
|
|
951
|
+
init_state_repository();
|
|
899
952
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
900
953
|
|
|
901
954
|
// src/domain/goal.ts
|
|
@@ -1835,6 +1888,7 @@ function createLoopEngine(options) {
|
|
|
1835
1888
|
|
|
1836
1889
|
// src/application/goal-service.ts
|
|
1837
1890
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1891
|
+
init_state_repository();
|
|
1838
1892
|
import * as path2 from "path";
|
|
1839
1893
|
import { promises as fs2 } from "fs";
|
|
1840
1894
|
|
|
@@ -2053,6 +2107,16 @@ function createWorkerManager(host) {
|
|
|
2053
2107
|
});
|
|
2054
2108
|
return result;
|
|
2055
2109
|
},
|
|
2110
|
+
async sendBare(worker, goal, runtime, text) {
|
|
2111
|
+
const result = await host.promptWorker({
|
|
2112
|
+
sessionID: worker.workerSessionID,
|
|
2113
|
+
prompt: text,
|
|
2114
|
+
messageID: runtime.activePromptMessageID,
|
|
2115
|
+
agent: goal.config.agent,
|
|
2116
|
+
model: parseModelRef(goal.config.model)
|
|
2117
|
+
});
|
|
2118
|
+
return result;
|
|
2119
|
+
},
|
|
2056
2120
|
async isIdle(workerSessionID) {
|
|
2057
2121
|
const status = await host.sessionStatus(workerSessionID);
|
|
2058
2122
|
return status === "idle";
|
|
@@ -2511,6 +2575,20 @@ function createGoalService(host) {
|
|
|
2511
2575
|
timestamp: new Date().toISOString(),
|
|
2512
2576
|
revision: state.revision
|
|
2513
2577
|
});
|
|
2578
|
+
if (opts?.bare) {
|
|
2579
|
+
const bareWords = await drainGoalInbox(directory, goalID);
|
|
2580
|
+
const bareText = bareWords.join(`
|
|
2581
|
+
`).trim();
|
|
2582
|
+
if (bareText) {
|
|
2583
|
+
try {
|
|
2584
|
+
await workers.sendBare(session, freshGoal, freshRuntime, bareText);
|
|
2585
|
+
} catch (error) {
|
|
2586
|
+
await recordPromptFailure(directory, goalID, error);
|
|
2587
|
+
throw error;
|
|
2588
|
+
}
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2514
2592
|
const inboxMessages = await drainGoalInbox(directory, goalID);
|
|
2515
2593
|
const allEvents = await readEvents(directory, 200);
|
|
2516
2594
|
const progressHistory = allEvents.filter((e) => e.goalID === goalID && e.type === "goal.progress").map((e) => ({
|
|
@@ -2818,6 +2896,40 @@ function createGoalService(host) {
|
|
|
2818
2896
|
await continueTurnUnlocked(directory, goalID, { force: true });
|
|
2819
2897
|
return { ok: true, message: `Re-prompted worker for "${freshGoal.name}".` };
|
|
2820
2898
|
}
|
|
2899
|
+
async function sendUnlocked(directory, goalID, text) {
|
|
2900
|
+
const trimmed = text.trim();
|
|
2901
|
+
if (!trimmed)
|
|
2902
|
+
return { ok: false, message: "Nothing to send." };
|
|
2903
|
+
const preState = await readState(directory);
|
|
2904
|
+
const goal = preState.goals.find((g) => g.id === goalID);
|
|
2905
|
+
if (!goal)
|
|
2906
|
+
return { ok: false, message: "Goal not found." };
|
|
2907
|
+
await appendGoalInbox(directory, goalID, "user", trimmed);
|
|
2908
|
+
if (goal.status !== "active") {
|
|
2909
|
+
return { ok: true, message: `Queued for "${goal.name}" (goal is ${goal.status}; delivers on the next active turn).` };
|
|
2910
|
+
}
|
|
2911
|
+
const cleared = await mutateState(directory, `goal.send:${goalID}`, async (s) => {
|
|
2912
|
+
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
2913
|
+
if (!rt)
|
|
2914
|
+
return s;
|
|
2915
|
+
rt.phase = "idle";
|
|
2916
|
+
rt.activeRunID = undefined;
|
|
2917
|
+
rt.idleCandidateAt = undefined;
|
|
2918
|
+
rt.activePromptMessageID = undefined;
|
|
2919
|
+
rt.activeToolCallIDs = [];
|
|
2920
|
+
rt.updatedAt = new Date().toISOString();
|
|
2921
|
+
return s;
|
|
2922
|
+
});
|
|
2923
|
+
const freshGoal = cleared.goals.find((g) => g.id === goalID);
|
|
2924
|
+
if (!freshGoal || !freshGoal.workerSessionID) {
|
|
2925
|
+
return { ok: true, message: `Queued for "${goal.name}" (no worker session yet; delivers on the next turn).` };
|
|
2926
|
+
}
|
|
2927
|
+
await continueTurnUnlocked(directory, goalID, { force: true, bare: true });
|
|
2928
|
+
return { ok: true, message: `Sent to "${freshGoal.name}" as its own turn.` };
|
|
2929
|
+
}
|
|
2930
|
+
function sendUserMessage(directory, goalID, text) {
|
|
2931
|
+
return withGoalOperation(goalID, () => sendUnlocked(directory, goalID, text));
|
|
2932
|
+
}
|
|
2821
2933
|
function continueTurn(directory, goalID, opts) {
|
|
2822
2934
|
return withGoalOperation(goalID, () => continueTurnUnlocked(directory, goalID, opts));
|
|
2823
2935
|
}
|
|
@@ -2836,13 +2948,52 @@ function createGoalService(host) {
|
|
|
2836
2948
|
function nudge(directory, goalID) {
|
|
2837
2949
|
return withGoalOperation(goalID, () => nudgeUnlocked(directory, goalID));
|
|
2838
2950
|
}
|
|
2951
|
+
async function abortWorkerUnlocked(directory, goalID) {
|
|
2952
|
+
const preState = await readState(directory);
|
|
2953
|
+
const goal = preState.goals.find((g) => g.id === goalID);
|
|
2954
|
+
if (!goal)
|
|
2955
|
+
return { ok: false, message: "Goal not found." };
|
|
2956
|
+
const workerID = sessions.get(goalID)?.workerSessionID || goal.workerSessionID;
|
|
2957
|
+
if (!workerID)
|
|
2958
|
+
return { ok: false, message: `Goal "${goal.name}" has no worker session to abort.` };
|
|
2959
|
+
try {
|
|
2960
|
+
await workers.abortWorker(workerID);
|
|
2961
|
+
} catch {}
|
|
2962
|
+
sessions.delete(goalID);
|
|
2963
|
+
await mutateState(directory, `goal.abort-worker:${goalID}`, async (s) => {
|
|
2964
|
+
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
2965
|
+
if (rt) {
|
|
2966
|
+
Object.assign(rt, releaseLease(rt));
|
|
2967
|
+
rt.activeRunID = undefined;
|
|
2968
|
+
rt.activePromptMessageID = undefined;
|
|
2969
|
+
rt.activeToolCallIDs = [];
|
|
2970
|
+
rt.idleCandidateAt = undefined;
|
|
2971
|
+
rt.idleCandidateGeneration = undefined;
|
|
2972
|
+
rt.workerAbortedAt = new Date().toISOString();
|
|
2973
|
+
rt.updatedAt = new Date().toISOString();
|
|
2974
|
+
}
|
|
2975
|
+
const g = s.goals.find((item) => item.id === goalID);
|
|
2976
|
+
if (g)
|
|
2977
|
+
g.updatedAt = new Date().toISOString();
|
|
2978
|
+
return s;
|
|
2979
|
+
});
|
|
2980
|
+
await logServerEvent(directory, "worker.aborted-manual", { goalID, workerSessionID: workerID });
|
|
2981
|
+
return {
|
|
2982
|
+
ok: true,
|
|
2983
|
+
message: `Worker run for "${goal.name}" aborted, session kept for inspection (status unchanged: ${goal.status}).` + (goal.status === "active" ? " Engine continues the same session next turn." : "")
|
|
2984
|
+
};
|
|
2985
|
+
}
|
|
2986
|
+
function abortWorker(directory, goalID) {
|
|
2987
|
+
return withGoalOperation(goalID, () => abortWorkerUnlocked(directory, goalID));
|
|
2988
|
+
}
|
|
2839
2989
|
function accountUsage(directory, goalID) {
|
|
2840
2990
|
return withGoalOperation(goalID, () => accountUsageUnlocked(directory, goalID));
|
|
2841
2991
|
}
|
|
2842
|
-
return { start, continueTurn, nudge, pause, resume, retry, clear, getWorker, getActiveWorkers, reconcile, accountUsage };
|
|
2992
|
+
return { start, continueTurn, nudge, pause, resume, retry, clear, getWorker, getActiveWorkers, reconcile, accountUsage, abortWorker, sendUserMessage };
|
|
2843
2993
|
}
|
|
2844
2994
|
|
|
2845
2995
|
// src/application/schedule-worker.ts
|
|
2996
|
+
init_state_repository();
|
|
2846
2997
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2847
2998
|
function createScheduleWorker(options) {
|
|
2848
2999
|
const { directory, goalService } = options;
|
|
@@ -2957,6 +3108,7 @@ function createScheduleWorker(options) {
|
|
|
2957
3108
|
}
|
|
2958
3109
|
|
|
2959
3110
|
// src/server/goal-tools.ts
|
|
3111
|
+
init_state_repository();
|
|
2960
3112
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
2961
3113
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
2962
3114
|
// src/domain/verification.ts
|
|
@@ -3534,6 +3686,7 @@ async function runCompletionChecks(checks, cwd) {
|
|
|
3534
3686
|
}
|
|
3535
3687
|
|
|
3536
3688
|
// src/server/owner-tools.ts
|
|
3689
|
+
init_state_repository();
|
|
3537
3690
|
import { tool as tool2 } from "@opencode-ai/plugin/tool";
|
|
3538
3691
|
import { promises as fs3 } from "fs";
|
|
3539
3692
|
function withTimeout2(promise, ms) {
|
|
@@ -3698,6 +3851,7 @@ function ownerTools(options) {
|
|
|
3698
3851
|
unknownStatusCount: runtime.unknownStatusCount,
|
|
3699
3852
|
lastUnknownStatusAt: runtime.lastUnknownStatusAt,
|
|
3700
3853
|
workerUnreachableNotifiedAt: runtime.workerUnreachableNotifiedAt,
|
|
3854
|
+
workerAbortedAt: runtime.workerAbortedAt,
|
|
3701
3855
|
retryAfter: runtime.retryAfter,
|
|
3702
3856
|
forceFinishRequested: runtime.forceFinishRequested,
|
|
3703
3857
|
scheduleRunCount: runtime.scheduleRunCount,
|
|
@@ -3760,7 +3914,7 @@ function ownerTools(options) {
|
|
|
3760
3914
|
}
|
|
3761
3915
|
}),
|
|
3762
3916
|
send_goal_input: tool2({
|
|
3763
|
-
description: "Send
|
|
3917
|
+
description: "Send bare words to the worker as their own turn (no steering wrapper). Delivers immediately if the goal is active, otherwise queues for the next active turn. Use to answer `question`, redirect, or nudge with a short message.",
|
|
3764
3918
|
args: {
|
|
3765
3919
|
goal_id: tool2.schema.string().optional().describe("Goal ID. Omit to target the first active goal."),
|
|
3766
3920
|
message: tool2.schema.string().describe("Message to send to the worker.")
|
|
@@ -3775,14 +3929,14 @@ function ownerTools(options) {
|
|
|
3775
3929
|
output: JSON.stringify({ ok: false, message: "No matching active goal for this session." })
|
|
3776
3930
|
};
|
|
3777
3931
|
}
|
|
3778
|
-
await
|
|
3932
|
+
const result = await goalService.sendUserMessage(directory, goal.id, args.message);
|
|
3779
3933
|
return {
|
|
3780
|
-
title: "Message sent",
|
|
3934
|
+
title: result.ok ? "Message sent" : "Send failed",
|
|
3781
3935
|
output: JSON.stringify({
|
|
3782
|
-
ok:
|
|
3936
|
+
ok: result.ok,
|
|
3783
3937
|
goalID: goal.id,
|
|
3784
3938
|
goalName: goal.name,
|
|
3785
|
-
message:
|
|
3939
|
+
message: result.message
|
|
3786
3940
|
})
|
|
3787
3941
|
};
|
|
3788
3942
|
}
|
|
@@ -3915,6 +4069,141 @@ function ownerTools(options) {
|
|
|
3915
4069
|
}
|
|
3916
4070
|
}
|
|
3917
4071
|
}),
|
|
4072
|
+
abort_goal_worker: tool2({
|
|
4073
|
+
description: "Abort the worker session only (N / :abort in TUI). Keeps goal+transcript+session browsable, clears the run lease. Use for compaction-spin or stuck runs. Status unchanged; active goals resume next turn in the same session.",
|
|
4074
|
+
args: {
|
|
4075
|
+
goal_id: tool2.schema.string().optional().describe("Goal ID. Omit to abort the first active goal.")
|
|
4076
|
+
},
|
|
4077
|
+
execute: async (args, context) => {
|
|
4078
|
+
const state = await readState(directory);
|
|
4079
|
+
const ownerID = context?.sessionID;
|
|
4080
|
+
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id && g.ownerSessionID === ownerID) : state.goals.find((g) => g.ownerSessionID === ownerID && g.status !== "complete");
|
|
4081
|
+
if (!goal) {
|
|
4082
|
+
return {
|
|
4083
|
+
title: "No goal found",
|
|
4084
|
+
output: JSON.stringify({ ok: false, message: "No matching active goal for this session." })
|
|
4085
|
+
};
|
|
4086
|
+
}
|
|
4087
|
+
try {
|
|
4088
|
+
const result = await goalService.abortWorker(directory, goal.id);
|
|
4089
|
+
return {
|
|
4090
|
+
title: result.ok ? "Worker aborted" : "Abort failed",
|
|
4091
|
+
output: JSON.stringify({ ...result, goalID: goal.id, goalName: goal.name })
|
|
4092
|
+
};
|
|
4093
|
+
} catch (error) {
|
|
4094
|
+
return {
|
|
4095
|
+
title: "Abort failed",
|
|
4096
|
+
output: JSON.stringify({
|
|
4097
|
+
ok: false,
|
|
4098
|
+
goalName: goal.name,
|
|
4099
|
+
message: error instanceof Error ? error.message : String(error)
|
|
4100
|
+
})
|
|
4101
|
+
};
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
}),
|
|
4105
|
+
force_complete_goal: tool2({
|
|
4106
|
+
description: "Force-complete a goal with summary/evidence, bypassing checks (:force in TUI). Use when the worker produced the right artifact but checks are stale or you have verified manually.",
|
|
4107
|
+
args: {
|
|
4108
|
+
goal_id: tool2.schema.string().optional().describe("Goal ID. Omit to target the first non-complete goal."),
|
|
4109
|
+
summary: tool2.schema.string().describe("What was completed."),
|
|
4110
|
+
evidence: tool2.schema.string().describe("Concrete evidence of completion.")
|
|
4111
|
+
},
|
|
4112
|
+
execute: async (args, context) => {
|
|
4113
|
+
const state = await readState(directory);
|
|
4114
|
+
const ownerID = context?.sessionID;
|
|
4115
|
+
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id && g.ownerSessionID === ownerID) : state.goals.find((g) => g.ownerSessionID === ownerID && g.status !== "complete");
|
|
4116
|
+
if (!goal) {
|
|
4117
|
+
return {
|
|
4118
|
+
title: "No goal found",
|
|
4119
|
+
output: JSON.stringify({ ok: false, message: "No matching active goal for this session." })
|
|
4120
|
+
};
|
|
4121
|
+
}
|
|
4122
|
+
await Promise.resolve().then(() => init_state_repository());
|
|
4123
|
+
await Promise.resolve();
|
|
4124
|
+
const { randomUUID } = await import("crypto");
|
|
4125
|
+
const st = await readState(directory);
|
|
4126
|
+
const g = st.goals.find((x) => x.id === goal.id);
|
|
4127
|
+
if (!g)
|
|
4128
|
+
return { title: "No goal", output: JSON.stringify({ ok: false, message: "Goal not found." }) };
|
|
4129
|
+
if (g.status === "complete") {
|
|
4130
|
+
return { title: "Already complete", output: JSON.stringify({ ok: true, message: `Goal "${g.name}" already complete.` }) };
|
|
4131
|
+
}
|
|
4132
|
+
g.status = "complete";
|
|
4133
|
+
g.updatedAt = new Date().toISOString();
|
|
4134
|
+
g.completionEvidence = { summary: args.summary, evidence: args.evidence, at: new Date().toISOString() };
|
|
4135
|
+
const rt = st.runtimes.find((r) => r.goalID === goal.id);
|
|
4136
|
+
if (rt) {
|
|
4137
|
+
Object.assign(rt, releaseLease(rt));
|
|
4138
|
+
rt.activeRunID = undefined;
|
|
4139
|
+
rt.lastError = undefined;
|
|
4140
|
+
rt.updatedAt = new Date().toISOString();
|
|
4141
|
+
}
|
|
4142
|
+
await writeState(directory, st);
|
|
4143
|
+
await appendEvent(directory, {
|
|
4144
|
+
version: 1,
|
|
4145
|
+
eventID: randomUUID(),
|
|
4146
|
+
goalID: goal.id,
|
|
4147
|
+
type: "goal.completed",
|
|
4148
|
+
summary: args.summary,
|
|
4149
|
+
evidence: args.evidence,
|
|
4150
|
+
timestamp: new Date().toISOString(),
|
|
4151
|
+
revision: st.revision
|
|
4152
|
+
});
|
|
4153
|
+
return { title: "Goal force-completed", output: JSON.stringify({ ok: true, goalID: goal.id, goalName: g.name }) };
|
|
4154
|
+
}
|
|
4155
|
+
}),
|
|
4156
|
+
force_block_goal: tool2({
|
|
4157
|
+
description: "Force-block a goal with reason/needed (:block in TUI). Use when the goal is stuck on an external blocker and should stop retrying.",
|
|
4158
|
+
args: {
|
|
4159
|
+
goal_id: tool2.schema.string().optional().describe("Goal ID. Omit to target the first non-complete goal."),
|
|
4160
|
+
reason: tool2.schema.string().describe("Why the goal is blocked."),
|
|
4161
|
+
needed: tool2.schema.string().describe("What is needed to unblock.")
|
|
4162
|
+
},
|
|
4163
|
+
execute: async (args, context) => {
|
|
4164
|
+
const state = await readState(directory);
|
|
4165
|
+
const ownerID = context?.sessionID;
|
|
4166
|
+
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id && g.ownerSessionID === ownerID) : state.goals.find((g) => g.ownerSessionID === ownerID && g.status !== "complete");
|
|
4167
|
+
if (!goal) {
|
|
4168
|
+
return {
|
|
4169
|
+
title: "No goal found",
|
|
4170
|
+
output: JSON.stringify({ ok: false, message: "No matching active goal for this session." })
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
await Promise.resolve().then(() => init_state_repository());
|
|
4174
|
+
await Promise.resolve();
|
|
4175
|
+
const { randomUUID } = await import("crypto");
|
|
4176
|
+
const st = await readState(directory);
|
|
4177
|
+
const g = st.goals.find((x) => x.id === goal.id);
|
|
4178
|
+
if (!g)
|
|
4179
|
+
return { title: "No goal", output: JSON.stringify({ ok: false, message: "Goal not found." }) };
|
|
4180
|
+
if (g.status === "blocked") {
|
|
4181
|
+
return { title: "Already blocked", output: JSON.stringify({ ok: true, message: `Goal "${g.name}" already blocked.` }) };
|
|
4182
|
+
}
|
|
4183
|
+
g.status = "blocked";
|
|
4184
|
+
g.updatedAt = new Date().toISOString();
|
|
4185
|
+
g.blocker = { reason: args.reason, needed: args.needed, at: new Date().toISOString() };
|
|
4186
|
+
const rt = st.runtimes.find((r) => r.goalID === goal.id);
|
|
4187
|
+
if (rt) {
|
|
4188
|
+
Object.assign(rt, releaseLease(rt));
|
|
4189
|
+
rt.activeRunID = undefined;
|
|
4190
|
+
rt.lastError = undefined;
|
|
4191
|
+
rt.updatedAt = new Date().toISOString();
|
|
4192
|
+
}
|
|
4193
|
+
await writeState(directory, st);
|
|
4194
|
+
await appendEvent(directory, {
|
|
4195
|
+
version: 1,
|
|
4196
|
+
eventID: randomUUID(),
|
|
4197
|
+
goalID: goal.id,
|
|
4198
|
+
type: "goal.blocked",
|
|
4199
|
+
reason: args.reason,
|
|
4200
|
+
needed: args.needed,
|
|
4201
|
+
timestamp: new Date().toISOString(),
|
|
4202
|
+
revision: st.revision
|
|
4203
|
+
});
|
|
4204
|
+
return { title: "Goal blocked", output: JSON.stringify({ ok: true, goalID: goal.id, goalName: g.name }) };
|
|
4205
|
+
}
|
|
4206
|
+
}),
|
|
3918
4207
|
clear_goal: tool2({
|
|
3919
4208
|
description: "Clear a goal: aborts worker and removes goal+runtime+ledger (cannot be undone). Use when the goal is no longer needed or to free a stuck writer slot after inspection.",
|
|
3920
4209
|
args: {
|
|
@@ -3955,7 +4244,9 @@ function ownerTools(options) {
|
|
|
3955
4244
|
})
|
|
3956
4245
|
};
|
|
3957
4246
|
}
|
|
4247
|
+
|
|
3958
4248
|
// src/server/plugin.ts
|
|
4249
|
+
init_state_repository();
|
|
3959
4250
|
var PLUGIN_ID = "opencode-loopd.server";
|
|
3960
4251
|
var server = async ({ client, directory }, pluginOptions) => {
|
|
3961
4252
|
const defaults = parsePluginDefaults(pluginOptions);
|
|
@@ -4099,9 +4390,14 @@ function parsePluginDefaults(options) {
|
|
|
4099
4390
|
defaultChecks: checks.length > 0 ? checks : undefined
|
|
4100
4391
|
};
|
|
4101
4392
|
}
|
|
4393
|
+
var v2 = define({
|
|
4394
|
+
id: PLUGIN_ID,
|
|
4395
|
+
async setup() {}
|
|
4396
|
+
});
|
|
4102
4397
|
var plugin_default = {
|
|
4103
4398
|
id: PLUGIN_ID,
|
|
4104
|
-
server
|
|
4399
|
+
server,
|
|
4400
|
+
setup: v2.setup
|
|
4105
4401
|
};
|
|
4106
4402
|
export {
|
|
4107
4403
|
plugin_default as default
|