@encoredigitalgroup/opencode 0.2.5 → 0.2.6
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/README.md +15 -20
- package/dist/config/ConfigTypes.d.ts +4 -0
- package/dist/server/index.js +20 -57
- package/dist/tui/index.js +20 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,9 +32,8 @@ The plugin resolves the mapped skills through OpenCode, so those skills can live
|
|
|
32
32
|
## Configuration
|
|
33
33
|
|
|
34
34
|
Set `planDirectory` and `skillMappings` in the object's `options` field. `planDirectory` is a non-empty path relative to the workspace. `skillMappings` can override
|
|
35
|
-
any supported command's installed skill name. `enforceTodoTracker` defaults to `true`; set it to `false` to disable tracker-refresh enforcement.
|
|
36
|
-
|
|
37
|
-
and no custom tool classifications.
|
|
35
|
+
any supported command's installed skill name. `enforceTodoTracker` defaults to `true`; set it to `false` to disable tracker-refresh enforcement. Unspecified values
|
|
36
|
+
use `docs/_planning` and the command's existing skill name.
|
|
38
37
|
|
|
39
38
|
```json
|
|
40
39
|
{
|
|
@@ -42,16 +41,14 @@ and no custom tool classifications.
|
|
|
42
41
|
"plugins": [
|
|
43
42
|
{
|
|
44
43
|
"package": "@encoredigitalgroup/opencode",
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
"writeCapableTools": ["deploy"]
|
|
54
|
-
}
|
|
44
|
+
"options": {
|
|
45
|
+
"planDirectory": "plans",
|
|
46
|
+
"enforceTodoTracker": false,
|
|
47
|
+
"skillMappings": {
|
|
48
|
+
"plan-review": "custom-plan-review",
|
|
49
|
+
"plan-execute": "custom-plan-execute"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
55
52
|
}
|
|
56
53
|
]
|
|
57
54
|
}
|
|
@@ -73,14 +70,12 @@ Each todo requires:
|
|
|
73
70
|
The checklist renders only in the OpenCode terminal TUI sidebar. After a restart, it appears only when the completed `todotracker` result is in the host's loaded
|
|
74
71
|
message cache. That cache starts with the newest 20 messages, so older results are not shown until the host has loaded them.
|
|
75
72
|
|
|
76
|
-
After a session's first successful `todotracker` call, the plugin requires another
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
`writeCapableTools` extend these lists for custom tools. If a tool appears in both lists, the write-capable classification takes precedence. The plugin also supports
|
|
80
|
-
the `apply_patch` and `bash` compatibility aliases as write-capable tools. Enforcement is disabled for sessions that have never used `todotracker`, and can be
|
|
81
|
-
disabled for all sessions with `"enforceTodoTracker": false`.
|
|
73
|
+
After a session's first successful `todotracker` call, the plugin requires another update at least once per minute. When an update is due, it blocks every other
|
|
74
|
+
tool until the tracker is updated. A tracked subagent completing also requires its parent session to update the tracker before continuing. Enforcement is disabled
|
|
75
|
+
for sessions that have never used `todotracker`, and can be disabled for all sessions with `"enforceTodoTracker": false`.
|
|
82
76
|
|
|
83
|
-
When a session is idle, the sidebar renders
|
|
77
|
+
When a root session is idle, the sidebar renders the final in-progress todo as completed only when every earlier todo is completed. It restores the original status
|
|
78
|
+
when work resumes.
|
|
84
79
|
|
|
85
80
|
## Development
|
|
86
81
|
|
|
@@ -4,15 +4,19 @@ export type SkillMappings = Record<SkillCommand, string>;
|
|
|
4
4
|
export type PluginConfig = {
|
|
5
5
|
enforceTodoTracker: boolean;
|
|
6
6
|
planDirectory: string;
|
|
7
|
+
/** @deprecated Tool classifications no longer affect todo tracker enforcement. */
|
|
7
8
|
readOnlyTools: string[];
|
|
8
9
|
skillMappings: SkillMappings;
|
|
10
|
+
/** @deprecated Tool classifications no longer affect todo tracker enforcement. */
|
|
9
11
|
writeCapableTools: string[];
|
|
10
12
|
};
|
|
11
13
|
export type UserPluginConfig = {
|
|
12
14
|
enforceTodoTracker?: boolean;
|
|
13
15
|
planDirectory?: string;
|
|
16
|
+
/** @deprecated Tool classifications no longer affect todo tracker enforcement. */
|
|
14
17
|
readOnlyTools?: string[];
|
|
15
18
|
skillMappings?: Partial<SkillMappings>;
|
|
19
|
+
/** @deprecated Tool classifications no longer affect todo tracker enforcement. */
|
|
16
20
|
writeCapableTools?: string[];
|
|
17
21
|
};
|
|
18
22
|
/** Configuration type utilities. */
|
package/dist/server/index.js
CHANGED
|
@@ -251,35 +251,17 @@ function todosFromMessages(messages) {
|
|
|
251
251
|
}
|
|
252
252
|
return [];
|
|
253
253
|
}
|
|
254
|
-
var
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
var readToolsBeforeRefresh = 10;
|
|
258
|
-
function refreshedTrackerState() {
|
|
259
|
-
return { kind: "tracked", readToolsSinceRefresh: 0, requiresRefreshBeforeWrite: false };
|
|
254
|
+
var trackerRefreshIntervalMs = 60000;
|
|
255
|
+
function refreshedTrackerState(refreshedAt = Date.now()) {
|
|
256
|
+
return { kind: "tracked", refreshedAt, refreshRequired: false };
|
|
260
257
|
}
|
|
261
|
-
function
|
|
262
|
-
return
|
|
258
|
+
function trackerRefreshDue(state, now = Date.now()) {
|
|
259
|
+
return state.kind === "tracked" && (state.refreshRequired || now - state.refreshedAt >= trackerRefreshIntervalMs);
|
|
263
260
|
}
|
|
264
|
-
function
|
|
265
|
-
return
|
|
261
|
+
function requiresTrackerRefresh(state, tool, now = Date.now()) {
|
|
262
|
+
return tool !== todoTrackerName && trackerRefreshDue(state, now);
|
|
266
263
|
}
|
|
267
|
-
function
|
|
268
|
-
if (state.kind === "untracked" || tool === todoTrackerName) {
|
|
269
|
-
return false;
|
|
270
|
-
}
|
|
271
|
-
if (isWriteCapableTool(tool, config)) {
|
|
272
|
-
return state.requiresRefreshBeforeWrite;
|
|
273
|
-
}
|
|
274
|
-
if (isReadOnlyTool(tool, config)) {
|
|
275
|
-
return state.readToolsSinceRefresh >= readToolsBeforeRefresh;
|
|
276
|
-
}
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
function requiresTrackerRefreshBeforeMoreWork(state) {
|
|
280
|
-
return state.kind === "tracked" && (state.requiresRefreshBeforeWrite || state.readToolsSinceRefresh >= readToolsBeforeRefresh);
|
|
281
|
-
}
|
|
282
|
-
function trackerStateFromMessages(messages, config) {
|
|
264
|
+
function trackerStateFromMessages(messages) {
|
|
283
265
|
let state = { kind: "untracked" };
|
|
284
266
|
for (const message of messages) {
|
|
285
267
|
if (message.type !== "assistant") {
|
|
@@ -290,13 +272,7 @@ function trackerStateFromMessages(messages, config) {
|
|
|
290
272
|
continue;
|
|
291
273
|
}
|
|
292
274
|
if (part.name === todoTrackerName) {
|
|
293
|
-
state = refreshedTrackerState();
|
|
294
|
-
} else if (state.kind === "tracked") {
|
|
295
|
-
if (isWriteCapableTool(part.name, config)) {
|
|
296
|
-
state.requiresRefreshBeforeWrite = true;
|
|
297
|
-
} else if (isReadOnlyTool(part.name, config)) {
|
|
298
|
-
state.readToolsSinceRefresh += 1;
|
|
299
|
-
}
|
|
275
|
+
state = refreshedTrackerState(part.time?.completed ?? part.time?.ran ?? part.time?.created ?? message.time.completed ?? message.time.created);
|
|
300
276
|
}
|
|
301
277
|
}
|
|
302
278
|
}
|
|
@@ -305,25 +281,23 @@ function trackerStateFromMessages(messages, config) {
|
|
|
305
281
|
|
|
306
282
|
class TodoTrackerEnforcement {
|
|
307
283
|
context;
|
|
308
|
-
config;
|
|
309
284
|
states = new Map;
|
|
310
|
-
constructor(context
|
|
285
|
+
constructor(context) {
|
|
311
286
|
this.context = context;
|
|
312
|
-
this.config = config;
|
|
313
287
|
}
|
|
314
288
|
async stateFor(sessionID) {
|
|
315
289
|
const existing = this.states.get(sessionID);
|
|
316
290
|
if (existing !== undefined) {
|
|
317
291
|
return existing;
|
|
318
292
|
}
|
|
319
|
-
const state = trackerStateFromMessages(await this.context.session.context({ sessionID })
|
|
293
|
+
const state = trackerStateFromMessages(await this.context.session.context({ sessionID }));
|
|
320
294
|
this.states.set(sessionID, state);
|
|
321
295
|
return state;
|
|
322
296
|
}
|
|
323
|
-
async
|
|
297
|
+
async requireTrackerRefresh(sessionID) {
|
|
324
298
|
const state = await this.stateFor(sessionID);
|
|
325
299
|
if (state.kind === "tracked") {
|
|
326
|
-
state.
|
|
300
|
+
state.refreshRequired = true;
|
|
327
301
|
}
|
|
328
302
|
}
|
|
329
303
|
async register() {
|
|
@@ -333,16 +307,9 @@ class TodoTrackerEnforcement {
|
|
|
333
307
|
try {
|
|
334
308
|
before = await this.context.tool.hook("execute.before", async (event) => {
|
|
335
309
|
const state = await this.stateFor(event.sessionID);
|
|
336
|
-
if (requiresTrackerRefresh(state, event.tool
|
|
310
|
+
if (requiresTrackerRefresh(state, event.tool)) {
|
|
337
311
|
throw new Error(trackerRefreshMessage);
|
|
338
312
|
}
|
|
339
|
-
if (state.kind === "tracked") {
|
|
340
|
-
if (isWriteCapableTool(event.tool, this.config)) {
|
|
341
|
-
state.requiresRefreshBeforeWrite = true;
|
|
342
|
-
} else if (isReadOnlyTool(event.tool, this.config)) {
|
|
343
|
-
state.readToolsSinceRefresh += 1;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
313
|
});
|
|
347
314
|
after = await this.context.tool.hook("execute.after", async (event) => {
|
|
348
315
|
if (event.status !== "completed") {
|
|
@@ -350,16 +317,11 @@ class TodoTrackerEnforcement {
|
|
|
350
317
|
}
|
|
351
318
|
if (event.tool === todoTrackerName) {
|
|
352
319
|
this.states.set(event.sessionID, refreshedTrackerState());
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
const state = await this.stateFor(event.sessionID);
|
|
356
|
-
if (state.kind !== "tracked") {
|
|
357
|
-
return;
|
|
358
320
|
}
|
|
359
321
|
});
|
|
360
322
|
context = await this.context.session.hook("context", async (event) => {
|
|
361
323
|
const state = await this.stateFor(event.sessionID);
|
|
362
|
-
if (
|
|
324
|
+
if (trackerRefreshDue(state)) {
|
|
363
325
|
event.system.push({ type: "text", text: trackerRefreshMessage });
|
|
364
326
|
}
|
|
365
327
|
});
|
|
@@ -392,7 +354,7 @@ class TodoTrackerEnforcement {
|
|
|
392
354
|
}
|
|
393
355
|
const session = await this.context.session.get({ sessionID: event.data.sessionID });
|
|
394
356
|
if (session.parentID !== undefined) {
|
|
395
|
-
await this.
|
|
357
|
+
await this.requireTrackerRefresh(session.parentID);
|
|
396
358
|
}
|
|
397
359
|
}
|
|
398
360
|
} catch (error) {
|
|
@@ -418,10 +380,11 @@ class TodoTracker {
|
|
|
418
380
|
}
|
|
419
381
|
static sidebar(messages, completed = false) {
|
|
420
382
|
const todos = todosFromMessages(messages);
|
|
421
|
-
|
|
383
|
+
const finalTodo = todos.at(-1);
|
|
384
|
+
if (!completed || finalTodo?.status !== "in_progress" || todos.slice(0, -1).some((todo) => todo.status !== "completed")) {
|
|
422
385
|
return todos;
|
|
423
386
|
}
|
|
424
|
-
return todos.
|
|
387
|
+
return [...todos.slice(0, -1), { ...finalTodo, status: "completed" }];
|
|
425
388
|
}
|
|
426
389
|
static async register(context, config) {
|
|
427
390
|
const tool = await context.tool.transform((tools) => {
|
|
@@ -442,7 +405,7 @@ class TodoTracker {
|
|
|
442
405
|
return tool;
|
|
443
406
|
}
|
|
444
407
|
try {
|
|
445
|
-
const enforcement = await new TodoTrackerEnforcement(context
|
|
408
|
+
const enforcement = await new TodoTrackerEnforcement(context).register();
|
|
446
409
|
return {
|
|
447
410
|
dispose: async () => {
|
|
448
411
|
try {
|
package/dist/tui/index.js
CHANGED
|
@@ -1036,35 +1036,17 @@ function todosFromMessages(messages) {
|
|
|
1036
1036
|
}
|
|
1037
1037
|
return [];
|
|
1038
1038
|
}
|
|
1039
|
-
var
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
var readToolsBeforeRefresh = 10;
|
|
1043
|
-
function refreshedTrackerState() {
|
|
1044
|
-
return { kind: "tracked", readToolsSinceRefresh: 0, requiresRefreshBeforeWrite: false };
|
|
1039
|
+
var trackerRefreshIntervalMs = 60000;
|
|
1040
|
+
function refreshedTrackerState(refreshedAt = Date.now()) {
|
|
1041
|
+
return { kind: "tracked", refreshedAt, refreshRequired: false };
|
|
1045
1042
|
}
|
|
1046
|
-
function
|
|
1047
|
-
return
|
|
1043
|
+
function trackerRefreshDue(state, now = Date.now()) {
|
|
1044
|
+
return state.kind === "tracked" && (state.refreshRequired || now - state.refreshedAt >= trackerRefreshIntervalMs);
|
|
1048
1045
|
}
|
|
1049
|
-
function
|
|
1050
|
-
return
|
|
1046
|
+
function requiresTrackerRefresh(state, tool, now = Date.now()) {
|
|
1047
|
+
return tool !== todoTrackerName && trackerRefreshDue(state, now);
|
|
1051
1048
|
}
|
|
1052
|
-
function
|
|
1053
|
-
if (state.kind === "untracked" || tool === todoTrackerName) {
|
|
1054
|
-
return false;
|
|
1055
|
-
}
|
|
1056
|
-
if (isWriteCapableTool(tool, config)) {
|
|
1057
|
-
return state.requiresRefreshBeforeWrite;
|
|
1058
|
-
}
|
|
1059
|
-
if (isReadOnlyTool(tool, config)) {
|
|
1060
|
-
return state.readToolsSinceRefresh >= readToolsBeforeRefresh;
|
|
1061
|
-
}
|
|
1062
|
-
return false;
|
|
1063
|
-
}
|
|
1064
|
-
function requiresTrackerRefreshBeforeMoreWork(state) {
|
|
1065
|
-
return state.kind === "tracked" && (state.requiresRefreshBeforeWrite || state.readToolsSinceRefresh >= readToolsBeforeRefresh);
|
|
1066
|
-
}
|
|
1067
|
-
function trackerStateFromMessages(messages, config) {
|
|
1049
|
+
function trackerStateFromMessages(messages) {
|
|
1068
1050
|
let state = { kind: "untracked" };
|
|
1069
1051
|
for (const message of messages) {
|
|
1070
1052
|
if (message.type !== "assistant") {
|
|
@@ -1075,13 +1057,7 @@ function trackerStateFromMessages(messages, config) {
|
|
|
1075
1057
|
continue;
|
|
1076
1058
|
}
|
|
1077
1059
|
if (part.name === todoTrackerName) {
|
|
1078
|
-
state = refreshedTrackerState();
|
|
1079
|
-
} else if (state.kind === "tracked") {
|
|
1080
|
-
if (isWriteCapableTool(part.name, config)) {
|
|
1081
|
-
state.requiresRefreshBeforeWrite = true;
|
|
1082
|
-
} else if (isReadOnlyTool(part.name, config)) {
|
|
1083
|
-
state.readToolsSinceRefresh += 1;
|
|
1084
|
-
}
|
|
1060
|
+
state = refreshedTrackerState(part.time?.completed ?? part.time?.ran ?? part.time?.created ?? message.time.completed ?? message.time.created);
|
|
1085
1061
|
}
|
|
1086
1062
|
}
|
|
1087
1063
|
}
|
|
@@ -1090,25 +1066,23 @@ function trackerStateFromMessages(messages, config) {
|
|
|
1090
1066
|
|
|
1091
1067
|
class TodoTrackerEnforcement {
|
|
1092
1068
|
context;
|
|
1093
|
-
config;
|
|
1094
1069
|
states = new Map;
|
|
1095
|
-
constructor(context
|
|
1070
|
+
constructor(context) {
|
|
1096
1071
|
this.context = context;
|
|
1097
|
-
this.config = config;
|
|
1098
1072
|
}
|
|
1099
1073
|
async stateFor(sessionID) {
|
|
1100
1074
|
const existing = this.states.get(sessionID);
|
|
1101
1075
|
if (existing !== undefined) {
|
|
1102
1076
|
return existing;
|
|
1103
1077
|
}
|
|
1104
|
-
const state = trackerStateFromMessages(await this.context.session.context({ sessionID })
|
|
1078
|
+
const state = trackerStateFromMessages(await this.context.session.context({ sessionID }));
|
|
1105
1079
|
this.states.set(sessionID, state);
|
|
1106
1080
|
return state;
|
|
1107
1081
|
}
|
|
1108
|
-
async
|
|
1082
|
+
async requireTrackerRefresh(sessionID) {
|
|
1109
1083
|
const state = await this.stateFor(sessionID);
|
|
1110
1084
|
if (state.kind === "tracked") {
|
|
1111
|
-
state.
|
|
1085
|
+
state.refreshRequired = true;
|
|
1112
1086
|
}
|
|
1113
1087
|
}
|
|
1114
1088
|
async register() {
|
|
@@ -1118,16 +1092,9 @@ class TodoTrackerEnforcement {
|
|
|
1118
1092
|
try {
|
|
1119
1093
|
before = await this.context.tool.hook("execute.before", async (event) => {
|
|
1120
1094
|
const state = await this.stateFor(event.sessionID);
|
|
1121
|
-
if (requiresTrackerRefresh(state, event.tool
|
|
1095
|
+
if (requiresTrackerRefresh(state, event.tool)) {
|
|
1122
1096
|
throw new Error(trackerRefreshMessage);
|
|
1123
1097
|
}
|
|
1124
|
-
if (state.kind === "tracked") {
|
|
1125
|
-
if (isWriteCapableTool(event.tool, this.config)) {
|
|
1126
|
-
state.requiresRefreshBeforeWrite = true;
|
|
1127
|
-
} else if (isReadOnlyTool(event.tool, this.config)) {
|
|
1128
|
-
state.readToolsSinceRefresh += 1;
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
1098
|
});
|
|
1132
1099
|
after = await this.context.tool.hook("execute.after", async (event) => {
|
|
1133
1100
|
if (event.status !== "completed") {
|
|
@@ -1135,16 +1102,11 @@ class TodoTrackerEnforcement {
|
|
|
1135
1102
|
}
|
|
1136
1103
|
if (event.tool === todoTrackerName) {
|
|
1137
1104
|
this.states.set(event.sessionID, refreshedTrackerState());
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
const state = await this.stateFor(event.sessionID);
|
|
1141
|
-
if (state.kind !== "tracked") {
|
|
1142
|
-
return;
|
|
1143
1105
|
}
|
|
1144
1106
|
});
|
|
1145
1107
|
context = await this.context.session.hook("context", async (event) => {
|
|
1146
1108
|
const state = await this.stateFor(event.sessionID);
|
|
1147
|
-
if (
|
|
1109
|
+
if (trackerRefreshDue(state)) {
|
|
1148
1110
|
event.system.push({ type: "text", text: trackerRefreshMessage });
|
|
1149
1111
|
}
|
|
1150
1112
|
});
|
|
@@ -1177,7 +1139,7 @@ class TodoTrackerEnforcement {
|
|
|
1177
1139
|
}
|
|
1178
1140
|
const session = await this.context.session.get({ sessionID: event.data.sessionID });
|
|
1179
1141
|
if (session.parentID !== undefined) {
|
|
1180
|
-
await this.
|
|
1142
|
+
await this.requireTrackerRefresh(session.parentID);
|
|
1181
1143
|
}
|
|
1182
1144
|
}
|
|
1183
1145
|
} catch (error) {
|
|
@@ -1203,10 +1165,11 @@ class TodoTracker {
|
|
|
1203
1165
|
}
|
|
1204
1166
|
static sidebar(messages, completed = false) {
|
|
1205
1167
|
const todos = todosFromMessages(messages);
|
|
1206
|
-
|
|
1168
|
+
const finalTodo = todos.at(-1);
|
|
1169
|
+
if (!completed || finalTodo?.status !== "in_progress" || todos.slice(0, -1).some((todo) => todo.status !== "completed")) {
|
|
1207
1170
|
return todos;
|
|
1208
1171
|
}
|
|
1209
|
-
return todos.
|
|
1172
|
+
return [...todos.slice(0, -1), { ...finalTodo, status: "completed" }];
|
|
1210
1173
|
}
|
|
1211
1174
|
static async register(context, config) {
|
|
1212
1175
|
const tool = await context.tool.transform((tools) => {
|
|
@@ -1227,7 +1190,7 @@ class TodoTracker {
|
|
|
1227
1190
|
return tool;
|
|
1228
1191
|
}
|
|
1229
1192
|
try {
|
|
1230
|
-
const enforcement = await new TodoTrackerEnforcement(context
|
|
1193
|
+
const enforcement = await new TodoTrackerEnforcement(context).register();
|
|
1231
1194
|
return {
|
|
1232
1195
|
dispose: async () => {
|
|
1233
1196
|
try {
|