opencode-empirical-plan 0.3.1 → 0.3.2
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/package.json +1 -1
- package/src/hooks/skill-reminder.ts +66 -0
- package/src/hooks/system-transform.ts +21 -0
- package/src/index.ts +15 -0
- package/src/tools/lifecycle-start.ts +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const KEY = Symbol.for("opencode-empirical-plan.skillReminder");
|
|
2
|
+
|
|
3
|
+
interface SkillReminderStore {
|
|
4
|
+
armedSessions: Set<string>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (!(globalThis as Record<symbol, unknown>)[KEY]) {
|
|
8
|
+
(globalThis as Record<symbol, unknown>)[KEY] = {
|
|
9
|
+
armedSessions: new Set<string>(),
|
|
10
|
+
} satisfies SkillReminderStore;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const store = (globalThis as Record<symbol, unknown>)[KEY] as SkillReminderStore;
|
|
14
|
+
|
|
15
|
+
const ENGLISH_KEYWORD_REGEX = /\b(plan|planning)\b/i;
|
|
16
|
+
const CHINESE_KEYWORDS = ["计划", "规划", "制定计划", "做计划"];
|
|
17
|
+
|
|
18
|
+
interface TextPartLike {
|
|
19
|
+
type?: string;
|
|
20
|
+
text?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function containsPlanningKeywords(text: string): boolean {
|
|
24
|
+
if (ENGLISH_KEYWORD_REGEX.test(text)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return CHINESE_KEYWORDS.some((keyword) => text.includes(keyword));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function extractText(parts: unknown[]): string {
|
|
32
|
+
const texts: string[] = [];
|
|
33
|
+
for (const part of parts) {
|
|
34
|
+
const candidate = part as TextPartLike;
|
|
35
|
+
if (candidate.type === "text" && typeof candidate.text === "string") {
|
|
36
|
+
texts.push(candidate.text);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return texts.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function handleSkillReminderChatMessage(
|
|
43
|
+
sessionID: string,
|
|
44
|
+
parts: unknown[],
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
try {
|
|
47
|
+
const text = extractText(parts);
|
|
48
|
+
if (!text) return;
|
|
49
|
+
if (!containsPlanningKeywords(text)) return;
|
|
50
|
+
store.armedSessions.add(sessionID);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error("[empirical-plan] skill-reminder chat.message error:", err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function consumeSkillReminderArmed(sessionID: string): boolean {
|
|
57
|
+
if (!store.armedSessions.has(sessionID)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
store.armedSessions.delete(sessionID);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function cleanupSkillReminderSession(sessionID: string): void {
|
|
65
|
+
store.armedSessions.delete(sessionID);
|
|
66
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { consumeSkillReminderArmed } from "./skill-reminder.ts";
|
|
2
|
+
|
|
3
|
+
const SKILL_REMINDER_TEXT = `<skill-reminder>
|
|
4
|
+
User message contains planning-related keywords. BEFORE responding:
|
|
5
|
+
1. Call skill(name="empirical-plan") to load the full planning workflow
|
|
6
|
+
2. Only skip if: this is a clarification question (not a task), or empirical-plan is already loaded this session
|
|
7
|
+
</skill-reminder>`;
|
|
8
|
+
|
|
9
|
+
export async function handleSystemTransform(
|
|
10
|
+
sessionID: string | undefined,
|
|
11
|
+
system: string[],
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
try {
|
|
14
|
+
if (!sessionID) return;
|
|
15
|
+
const armed = consumeSkillReminderArmed(sessionID);
|
|
16
|
+
if (!armed) return;
|
|
17
|
+
system.push(SKILL_REMINDER_TEXT);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error("[empirical-plan] system-transform error:", err);
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { handleToolAfter } from "./hooks/tool-after.ts";
|
|
|
6
6
|
import { recordCallStart } from "./hooks/tool-before.ts";
|
|
7
7
|
import { handleSessionIdle } from "./hooks/session-idle.ts";
|
|
8
8
|
import { cleanup, cleanupChild } from "./state/active-lifecycles.ts";
|
|
9
|
+
import { handleSkillReminderChatMessage, cleanupSkillReminderSession } from "./hooks/skill-reminder.ts";
|
|
10
|
+
import { handleSystemTransform } from "./hooks/system-transform.ts";
|
|
9
11
|
|
|
10
12
|
export const EmpiricalPlanPlugin: Plugin = async ({ client }) => {
|
|
11
13
|
return {
|
|
@@ -15,6 +17,18 @@ export const EmpiricalPlanPlugin: Plugin = async ({ client }) => {
|
|
|
15
17
|
lifecycle_record_reflection: lifecycleRecordReflectionTool,
|
|
16
18
|
},
|
|
17
19
|
|
|
20
|
+
"chat.message": async (input, output) => {
|
|
21
|
+
try {
|
|
22
|
+
await handleSkillReminderChatMessage(input.sessionID, output.parts);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error("[empirical-plan] chat.message error:", err);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
29
|
+
await handleSystemTransform(input.sessionID, output.system);
|
|
30
|
+
},
|
|
31
|
+
|
|
18
32
|
"tool.execute.before": async (input) => {
|
|
19
33
|
try {
|
|
20
34
|
recordCallStart(input.callID);
|
|
@@ -49,6 +63,7 @@ export const EmpiricalPlanPlugin: Plugin = async ({ client }) => {
|
|
|
49
63
|
if (sessionID) {
|
|
50
64
|
cleanup(sessionID);
|
|
51
65
|
cleanupChild(sessionID);
|
|
66
|
+
cleanupSkillReminderSession(sessionID);
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
69
|
} catch (err) {
|
|
@@ -58,7 +58,7 @@ advances lifecycle-state.json to the next phase (plan→execute→reflect→done
|
|
|
58
58
|
next_action: "Wait for the current lifecycle to complete before starting a new one.",
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
await fs.mkdir(runDir, { recursive: true });
|