todo-enforcer 1.0.0
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/LICENSE +21 -0
- package/README.md +216 -0
- package/package.json +53 -0
- package/src/conditions.ts +79 -0
- package/src/config.ts +592 -0
- package/src/external-caller.ts +219 -0
- package/src/index.ts +1022 -0
- package/src/lib/hooks-manager.ts +207 -0
- package/src/lib/plugin-logger.ts +155 -0
- package/src/lib/types.ts +59 -0
- package/src/message-stall.ts +188 -0
- package/src/session-state.ts +395 -0
- package/src/todo-snapshot.ts +288 -0
- package/src/type-guards.ts +105 -0
- package/todo-enforcer.example.json +52 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* type-guards — Runtime type guards for todo-enforcer
|
|
3
|
+
*
|
|
4
|
+
* Replaces `as X` type assertions with safe runtime checks.
|
|
5
|
+
* Each guard narrows `unknown` to a specific type.
|
|
6
|
+
*/
|
|
7
|
+
// @ts-nocheck
|
|
8
|
+
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// ─── Primitive guards ────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if a value is a plain object (not null, not array).
|
|
16
|
+
*/
|
|
17
|
+
export function isRecord(v: unknown): v is Record<string, unknown> {
|
|
18
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a value is a string.
|
|
23
|
+
*/
|
|
24
|
+
export function isString(v: unknown): v is string {
|
|
25
|
+
return typeof v === "string";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Session entry guards ─────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if a value has a `message` property (session entry shape).
|
|
32
|
+
*/
|
|
33
|
+
export function hasMessage(
|
|
34
|
+
v: unknown,
|
|
35
|
+
): v is { message?: { customType?: string; role?: string; toolName?: string; content?: unknown } } {
|
|
36
|
+
if (!isRecord(v)) return false;
|
|
37
|
+
return "message" in v;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a value has a `role` property (session message shape).
|
|
42
|
+
*/
|
|
43
|
+
export function hasRole(v: unknown): v is { role?: string; [key: string]: unknown } {
|
|
44
|
+
return isRecord(v) && "role" in v;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a value has a `stopReason` property.
|
|
49
|
+
*/
|
|
50
|
+
export function hasStopReason(v: unknown): v is { stopReason?: string; [key: string]: unknown } {
|
|
51
|
+
return isRecord(v) && "stopReason" in v;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Safe extractors ──────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Safely extract string content from unknown value.
|
|
58
|
+
* Returns the string if it's a string, empty string otherwise.
|
|
59
|
+
*/
|
|
60
|
+
export function getStringContent(content: unknown): string {
|
|
61
|
+
return isString(content) ? content : "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Safely extract `role` from an unknown value.
|
|
66
|
+
* Returns the role string if present, undefined otherwise.
|
|
67
|
+
*/
|
|
68
|
+
export function extractRole(m: unknown): string | undefined {
|
|
69
|
+
if (!hasRole(m)) return undefined;
|
|
70
|
+
return typeof m.role === "string" ? m.role : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Safely extract `stopReason` from an unknown value.
|
|
75
|
+
* Returns the stopReason string if present, undefined otherwise.
|
|
76
|
+
*/
|
|
77
|
+
export function extractStopReason(m: unknown): string | undefined {
|
|
78
|
+
if (!hasStopReason(m)) return undefined;
|
|
79
|
+
return typeof m.stopReason === "string" ? m.stopReason : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Array guards ─────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if a value is an array of session entries.
|
|
86
|
+
* Each entry must be an object with a `type` property (string).
|
|
87
|
+
*/
|
|
88
|
+
export function isSessionEntryArray(v: unknown): v is Array<{ type: string; message?: unknown }> {
|
|
89
|
+
if (!Array.isArray(v)) return false;
|
|
90
|
+
return v.every(
|
|
91
|
+
(entry) => isRecord(entry) && typeof entry.type === "string",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Config guards ────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a value looks like a partial TodoEnforcerConfig.
|
|
99
|
+
* At minimum it must be a non-null plain object.
|
|
100
|
+
*/
|
|
101
|
+
export function isPartialTodoConfig(
|
|
102
|
+
v: unknown,
|
|
103
|
+
): v is Partial<import("./config").TodoEnforcerConfig> {
|
|
104
|
+
return isRecord(v);
|
|
105
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./todo-enforcer/config.schema.json",
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"maxInjections": 5,
|
|
5
|
+
"cooldownMs": 60000,
|
|
6
|
+
"completionSummary": false,
|
|
7
|
+
"detectStagnation": true,
|
|
8
|
+
"stagnationThreshold": 3,
|
|
9
|
+
"messageDelivery": {
|
|
10
|
+
"customType": "todo-enforcer",
|
|
11
|
+
"display": true,
|
|
12
|
+
"triggerTurn": true,
|
|
13
|
+
"deliverAs": "followUp"
|
|
14
|
+
},
|
|
15
|
+
"contextFeed": {
|
|
16
|
+
"userMode": "latest",
|
|
17
|
+
"assistantMode": "allSinceLatestUser",
|
|
18
|
+
"includeSessionMetadata": true,
|
|
19
|
+
"excludePreviousEnforcerMessages": true
|
|
20
|
+
},
|
|
21
|
+
"rules": [
|
|
22
|
+
{
|
|
23
|
+
"name": "incomplete-tasks-remain",
|
|
24
|
+
"condition": "has_incomplete",
|
|
25
|
+
"action": "prompt",
|
|
26
|
+
"prompt": "You have incomplete tasks. Continue working on them.\n\n[Status: {{completed_count}}/{{total_count}} completed, {{incomplete_count}} remaining]\n\nRemaining tasks:\n{{incomplete_list}}\n\nLatest user message:\n{{latest_user_message}}\n\nRecent assistant messages:\n{{assistant_messages}}\n\nPick up where you left off. Do NOT stop until all tasks are completed or explicitly blocked."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "all-complete-celebration",
|
|
30
|
+
"condition": "all_complete",
|
|
31
|
+
"action": "prompt",
|
|
32
|
+
"prompt": "All {{total_count}} tasks are complete. Great work.\n\nCompleted tasks:\n{{completed_list}}\n\nYou may now summarize the results or ask the user for next steps."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "remote-followup",
|
|
36
|
+
"condition": "has_incomplete",
|
|
37
|
+
"action": "external",
|
|
38
|
+
"external": {
|
|
39
|
+
"silent": true,
|
|
40
|
+
"errorFallback": "default_prompt",
|
|
41
|
+
"http": {
|
|
42
|
+
"url": "https://example.com/todo-enforcer",
|
|
43
|
+
"method": "POST",
|
|
44
|
+
"headers": {
|
|
45
|
+
"authorization": "Bearer <token>"
|
|
46
|
+
},
|
|
47
|
+
"timeoutMs": 15000
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|