@pencil-agent/nano-pencil 1.13.7 → 1.13.9
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/build-meta.json +3 -3
- package/dist/builtin-extensions.js +10 -0
- package/dist/core/config/settings-manager.d.ts +7 -0
- package/dist/core/sub-agent/index.d.ts +1 -1
- package/dist/core/sub-agent/sub-agent-backend.js +117 -13
- package/dist/core/sub-agent/sub-agent-types.d.ts +48 -1
- package/dist/core/sub-agent/sub-agent-types.js +1 -1
- package/dist/extensions/defaults/AGENT.md +2 -2
- package/dist/extensions/defaults/CLAUDE.md +5 -4
- package/dist/extensions/defaults/grub/grub-controller.d.ts +2 -0
- package/dist/extensions/defaults/grub/grub-controller.js +79 -24
- package/dist/extensions/defaults/grub/grub-i18n.d.ts +128 -0
- package/dist/extensions/defaults/grub/grub-i18n.js +167 -0
- package/dist/extensions/defaults/grub/grub-parser.d.ts +2 -1
- package/dist/extensions/defaults/grub/grub-parser.js +5 -3
- package/dist/extensions/defaults/grub/grub-types.d.ts +3 -0
- package/dist/extensions/defaults/grub/index.js +133 -78
- package/dist/extensions/defaults/idle-think/curiosity.d.ts +46 -0
- package/dist/extensions/defaults/idle-think/curiosity.js +137 -0
- package/dist/extensions/defaults/idle-think/index.d.ts +15 -0
- package/dist/extensions/defaults/idle-think/index.js +169 -0
- package/dist/extensions/defaults/idle-think/insights.d.ts +40 -0
- package/dist/extensions/defaults/idle-think/insights.js +123 -0
- package/dist/extensions/defaults/idle-think/thinker.d.ts +26 -0
- package/dist/extensions/defaults/idle-think/thinker.js +208 -0
- package/dist/extensions/defaults/presence/index.d.ts +1 -0
- package/dist/extensions/defaults/presence/index.js +83 -10
- package/dist/extensions/defaults/sal/README.md +5 -2
- package/dist/extensions/defaults/sal/eval/insforge-sink.d.ts +4 -1
- package/dist/extensions/defaults/sal/eval/insforge-sink.js +93 -23
- package/dist/extensions/defaults/sal/index.d.ts +4 -3
- package/dist/extensions/defaults/sal/index.js +45 -8
- package/dist/extensions/defaults/team/CLAUDE.md +15 -5
- package/dist/extensions/defaults/team/index.d.ts +8 -3
- package/dist/extensions/defaults/team/index.js +225 -5
- package/dist/extensions/defaults/team/team-dashboard.d.ts +9 -0
- package/dist/extensions/defaults/team/team-dashboard.js +109 -0
- package/dist/extensions/defaults/team/team-harness.d.ts +35 -0
- package/dist/extensions/defaults/team/team-harness.js +351 -0
- package/dist/extensions/defaults/team/team-parser.d.ts +14 -4
- package/dist/extensions/defaults/team/team-parser.js +60 -10
- package/dist/extensions/defaults/team/team-presets.d.ts +45 -0
- package/dist/extensions/defaults/team/team-presets.js +203 -0
- package/dist/extensions/defaults/team/team-psyche.d.ts +14 -0
- package/dist/extensions/defaults/team/team-psyche.js +130 -0
- package/dist/extensions/defaults/team/team-runtime.d.ts +20 -2
- package/dist/extensions/defaults/team/team-runtime.js +132 -3
- package/dist/extensions/defaults/team/team-types.d.ts +62 -2
- package/dist/extensions/defaults/team/team-types.js +1 -1
- package/dist/modes/interactive/components/pencil-loader.d.ts +0 -2
- package/dist/modes/interactive/components/pencil-loader.js +1 -14
- package/dist/modes/interactive/interactive-mode.js +4 -0
- package/dist/node_modules/@pencil-agent/ai/cli.js +0 -0
- package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +23 -17
- package/dist/node_modules/@pencil-agent/ai/models.generated.js +26 -25
- package/docs/SAL/345/256/236/351/252/214/350/257/204/344/274/260/346/226/271/345/274/217/357/274/210/344/273/243/347/240/201/345/257/271/346/257/224/344/270/216/345/244/232worktree/357/274/211.md +2 -2
- package/docs/SAL/346/200/273/344/275/223/350/267/257/347/272/277/344/270/216/345/256/236/351/252/214/345/244/247/347/272/262.md +2 -2
- package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +251 -0
- package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +123 -0
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +1222 -0
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/256/236/347/216/260/346/212/245/345/221/212.md" +158 -0
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/257/271/346/257/224/345/210/206/346/236/220.md" +128 -0
- package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +321 -0
- package/docs/loop-usage-examples.md +215 -0
- package/docs/planmode.md +1987 -0
- package/package.json +1 -1
package/dist/build-meta.json
CHANGED
|
@@ -24,6 +24,7 @@ const BUNDLED_SAL_EXTENSION = join(__dirname, "extensions", "defaults", "sal", "
|
|
|
24
24
|
const BUNDLED_GRUB_EXTENSION = join(__dirname, "extensions", "defaults", "grub", "index.js");
|
|
25
25
|
const BUNDLED_SUBAGENT_EXTENSION = join(__dirname, "extensions", "defaults", "subagent", "index.js");
|
|
26
26
|
const BUNDLED_TEAM_EXTENSION = join(__dirname, "extensions", "defaults", "team", "index.js");
|
|
27
|
+
const BUNDLED_IDLE_THINK_EXTENSION = join(__dirname, "extensions", "defaults", "idle-think", "index.js");
|
|
27
28
|
const BUNDLED_BTW_EXTENSION = join(__dirname, "extensions", "defaults", "btw", "index.js");
|
|
28
29
|
const BUNDLED_DEBUG_EXTENSION = join(__dirname, "extensions", "defaults", "debug", "index.js");
|
|
29
30
|
const BUNDLED_MCP_EXTENSION = join(__dirname, "extensions", "defaults", "mcp", "index.js");
|
|
@@ -211,6 +212,15 @@ export function getBuiltinExtensionPaths() {
|
|
|
211
212
|
if (existsSync(teamTs))
|
|
212
213
|
paths.push(teamTs);
|
|
213
214
|
}
|
|
215
|
+
// === IdleThink extension (background code exploration during idle) ===
|
|
216
|
+
if (existsSync(BUNDLED_IDLE_THINK_EXTENSION)) {
|
|
217
|
+
paths.push(BUNDLED_IDLE_THINK_EXTENSION);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const idleThinkTs = join(__dirname, "extensions", "defaults", "idle-think", "index.ts");
|
|
221
|
+
if (existsSync(idleThinkTs))
|
|
222
|
+
paths.push(idleThinkTs);
|
|
223
|
+
}
|
|
214
224
|
// === BTW extension (quick side question without interrupting) ===
|
|
215
225
|
if (existsSync(BUNDLED_BTW_EXTENSION)) {
|
|
216
226
|
paths.push(BUNDLED_BTW_EXTENSION);
|
|
@@ -105,6 +105,13 @@ export interface Settings {
|
|
|
105
105
|
presence?: {
|
|
106
106
|
enabled?: boolean;
|
|
107
107
|
};
|
|
108
|
+
/** IdleThink extension settings - background code exploration during idle */
|
|
109
|
+
idleThink?: {
|
|
110
|
+
enabled?: boolean;
|
|
111
|
+
idleMinutes?: number;
|
|
112
|
+
dailyBudget?: number;
|
|
113
|
+
maxDurationMinutes?: number;
|
|
114
|
+
};
|
|
108
115
|
/** Auto-update setting: 'always' = auto-update on startup, 'prompt' = ask user (default), 'never' = never check */
|
|
109
116
|
autoUpdate?: "always" | "prompt" | "never";
|
|
110
117
|
/** Last skipped version for update prompts */
|
|
@@ -8,4 +8,4 @@ export { SubAgentRuntime, subAgentRuntime } from "./sub-agent-runtime.js";
|
|
|
8
8
|
export { InProcessSubAgentBackend } from "./sub-agent-backend.js";
|
|
9
9
|
export { SubprocessSubAgentBackend } from "./subprocess-backend.js";
|
|
10
10
|
export type { SubprocessBackendOptions } from "./subprocess-backend.js";
|
|
11
|
-
export type { SubAgentSpec, SubAgentResult, SubAgentHandle, SubAgentBackend, } from "./sub-agent-types.js";
|
|
11
|
+
export type { SubAgentSpec, SubAgentEvent, SubAgentResult, SubAgentHandle, SubAgentBackend, } from "./sub-agent-types.js";
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* [HERE]: core/sub-agent/sub-agent-backend.ts - in-process SubAgent implementation
|
|
6
6
|
*/
|
|
7
7
|
import { createAgentSession } from "../runtime/sdk.js";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { isAbsolute, resolve } from "node:path";
|
|
8
10
|
/**
|
|
9
11
|
* In-process SubAgent backend.
|
|
10
12
|
* Wraps createAgentSession() to run SubAgent in the same process.
|
|
@@ -12,6 +14,7 @@ import { createAgentSession } from "../runtime/sdk.js";
|
|
|
12
14
|
export class InProcessSubAgentBackend {
|
|
13
15
|
async spawn(spec) {
|
|
14
16
|
const id = crypto.randomUUID();
|
|
17
|
+
const prompt = await buildPromptWithContextFiles(spec);
|
|
15
18
|
// Create an internal AbortController that can be triggered by external signal or timeout
|
|
16
19
|
const internalAbortController = new AbortController();
|
|
17
20
|
// Forward external signal abort to internal controller
|
|
@@ -29,6 +32,12 @@ export class InProcessSubAgentBackend {
|
|
|
29
32
|
model: spec.model,
|
|
30
33
|
};
|
|
31
34
|
const { session } = await createAgentSession(options);
|
|
35
|
+
const unsubscribe = session.subscribe((event) => {
|
|
36
|
+
const subAgentEvent = toSubAgentEvent(id, event);
|
|
37
|
+
if (subAgentEvent) {
|
|
38
|
+
spec.onEvent?.(subAgentEvent);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
32
41
|
const timeoutMs = spec.timeoutMs;
|
|
33
42
|
let status = "running";
|
|
34
43
|
let result;
|
|
@@ -41,22 +50,11 @@ export class InProcessSubAgentBackend {
|
|
|
41
50
|
}
|
|
42
51
|
}, timeoutMs);
|
|
43
52
|
}
|
|
44
|
-
// Extract text from assistant message content
|
|
45
|
-
const extractTextFromContent = (content) => {
|
|
46
|
-
if (typeof content === "string")
|
|
47
|
-
return content;
|
|
48
|
-
if (Array.isArray(content)) {
|
|
49
|
-
return content
|
|
50
|
-
.filter((part) => typeof part === "object" && part !== null && "type" in part && part.type === "text" && typeof part.text === "string")
|
|
51
|
-
.map((part) => part.text)
|
|
52
|
-
.join("\n");
|
|
53
|
-
}
|
|
54
|
-
return "";
|
|
55
|
-
};
|
|
56
53
|
// Start the prompt
|
|
57
54
|
const promptPromise = (async () => {
|
|
58
55
|
try {
|
|
59
|
-
|
|
56
|
+
spec.onEvent?.({ type: "agent_start", subAgentId: id, timestamp: Date.now() });
|
|
57
|
+
await session.prompt(prompt, {
|
|
60
58
|
images: spec.images,
|
|
61
59
|
});
|
|
62
60
|
status = "done";
|
|
@@ -87,11 +85,31 @@ export class InProcessSubAgentBackend {
|
|
|
87
85
|
}
|
|
88
86
|
}
|
|
89
87
|
finally {
|
|
88
|
+
if (spec.exitHook && result) {
|
|
89
|
+
try {
|
|
90
|
+
await spec.exitHook(result);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
status = "error";
|
|
94
|
+
result = {
|
|
95
|
+
success: false,
|
|
96
|
+
error: `exitHook failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
90
100
|
if (timeoutId !== undefined) {
|
|
91
101
|
clearTimeout(timeoutId);
|
|
92
102
|
}
|
|
93
103
|
// Clean up signal handler
|
|
94
104
|
spec.signal.removeEventListener("abort", signalHandler);
|
|
105
|
+
unsubscribe();
|
|
106
|
+
spec.onEvent?.({
|
|
107
|
+
type: "agent_end",
|
|
108
|
+
subAgentId: id,
|
|
109
|
+
timestamp: Date.now(),
|
|
110
|
+
success: result?.success ?? false,
|
|
111
|
+
error: result?.error,
|
|
112
|
+
});
|
|
95
113
|
}
|
|
96
114
|
})();
|
|
97
115
|
return {
|
|
@@ -117,3 +135,89 @@ export class InProcessSubAgentBackend {
|
|
|
117
135
|
};
|
|
118
136
|
}
|
|
119
137
|
}
|
|
138
|
+
function toSubAgentEvent(subAgentId, event) {
|
|
139
|
+
const timestamp = Date.now();
|
|
140
|
+
switch (event.type) {
|
|
141
|
+
case "message_update":
|
|
142
|
+
return {
|
|
143
|
+
type: "message_update",
|
|
144
|
+
subAgentId,
|
|
145
|
+
timestamp,
|
|
146
|
+
text: extractMessageText(event.message),
|
|
147
|
+
deltaType: event.assistantMessageEvent.type,
|
|
148
|
+
};
|
|
149
|
+
case "message_end":
|
|
150
|
+
return {
|
|
151
|
+
type: "message_end",
|
|
152
|
+
subAgentId,
|
|
153
|
+
timestamp,
|
|
154
|
+
text: extractMessageText(event.message),
|
|
155
|
+
};
|
|
156
|
+
case "tool_execution_start":
|
|
157
|
+
return {
|
|
158
|
+
type: "tool_start",
|
|
159
|
+
subAgentId,
|
|
160
|
+
timestamp,
|
|
161
|
+
toolName: event.toolName,
|
|
162
|
+
args: event.args,
|
|
163
|
+
};
|
|
164
|
+
case "tool_execution_update":
|
|
165
|
+
return {
|
|
166
|
+
type: "tool_update",
|
|
167
|
+
subAgentId,
|
|
168
|
+
timestamp,
|
|
169
|
+
toolName: event.toolName,
|
|
170
|
+
partialResult: event.partialResult,
|
|
171
|
+
};
|
|
172
|
+
case "tool_execution_end":
|
|
173
|
+
return {
|
|
174
|
+
type: "tool_end",
|
|
175
|
+
subAgentId,
|
|
176
|
+
timestamp,
|
|
177
|
+
toolName: event.toolName,
|
|
178
|
+
isError: event.isError,
|
|
179
|
+
};
|
|
180
|
+
default:
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function extractTextFromContent(content) {
|
|
185
|
+
if (typeof content === "string")
|
|
186
|
+
return content;
|
|
187
|
+
if (Array.isArray(content)) {
|
|
188
|
+
return content
|
|
189
|
+
.filter((part) => typeof part === "object" && part !== null && "type" in part && part.type === "text" && typeof part.text === "string")
|
|
190
|
+
.map((part) => part.text)
|
|
191
|
+
.join("\n");
|
|
192
|
+
}
|
|
193
|
+
return "";
|
|
194
|
+
}
|
|
195
|
+
function extractMessageText(message) {
|
|
196
|
+
if (typeof message !== "object" || message === null || !("content" in message)) {
|
|
197
|
+
return "";
|
|
198
|
+
}
|
|
199
|
+
return extractTextFromContent(message.content);
|
|
200
|
+
}
|
|
201
|
+
async function buildPromptWithContextFiles(spec) {
|
|
202
|
+
if (!spec.contextFiles?.length) {
|
|
203
|
+
return spec.prompt;
|
|
204
|
+
}
|
|
205
|
+
const chunks = [];
|
|
206
|
+
for (const filePath of spec.contextFiles) {
|
|
207
|
+
const absolutePath = isAbsolute(filePath) ? filePath : resolve(spec.cwd, filePath);
|
|
208
|
+
try {
|
|
209
|
+
const content = await readFile(absolutePath, "utf8");
|
|
210
|
+
chunks.push(`### ${filePath}\n\`\`\`\n${content}\n\`\`\``);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
chunks.push(`### ${filePath}\n(unavailable: ${error instanceof Error ? error.message : String(error)})`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return [
|
|
217
|
+
"The following files are injected as current task context. Treat them as read-only context unless the task instructions explicitly allow updates.",
|
|
218
|
+
"",
|
|
219
|
+
...chunks,
|
|
220
|
+
"",
|
|
221
|
+
spec.prompt,
|
|
222
|
+
].join("\n");
|
|
223
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* [WHO]: SubAgent types - SubAgentSpec, SubAgentHandle, SubAgentBackend, SubAgentResult
|
|
2
|
+
* [WHO]: SubAgent types - SubAgentSpec, SubAgentEvent, SubAgentHandle, SubAgentBackend, SubAgentResult
|
|
3
3
|
* [FROM]: Depends on @pencil-agent/agent-core, @pencil-agent/ai, core/tools
|
|
4
4
|
* [TO]: Consumed by ./sub-agent-runtime, ./sub-agent-backend, ./index.ts, extensions/defaults/subagent/*, extensions/defaults/team/*
|
|
5
5
|
* [HERE]: core/sub-agent/sub-agent-types.ts - SubAgent type definitions
|
|
@@ -7,6 +7,47 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { ImageContent, Model } from "@pencil-agent/ai";
|
|
9
9
|
import type { Tool } from "../tools/index.js";
|
|
10
|
+
/** Realtime lifecycle event emitted by a running SubAgent. */
|
|
11
|
+
export type SubAgentEvent = {
|
|
12
|
+
type: "agent_start";
|
|
13
|
+
subAgentId: string;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
} | {
|
|
16
|
+
type: "message_update";
|
|
17
|
+
subAgentId: string;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
text: string;
|
|
20
|
+
deltaType?: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: "message_end";
|
|
23
|
+
subAgentId: string;
|
|
24
|
+
timestamp: number;
|
|
25
|
+
text: string;
|
|
26
|
+
} | {
|
|
27
|
+
type: "tool_start";
|
|
28
|
+
subAgentId: string;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
toolName: string;
|
|
31
|
+
args: unknown;
|
|
32
|
+
} | {
|
|
33
|
+
type: "tool_update";
|
|
34
|
+
subAgentId: string;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
toolName: string;
|
|
37
|
+
partialResult: unknown;
|
|
38
|
+
} | {
|
|
39
|
+
type: "tool_end";
|
|
40
|
+
subAgentId: string;
|
|
41
|
+
timestamp: number;
|
|
42
|
+
toolName: string;
|
|
43
|
+
isError: boolean;
|
|
44
|
+
} | {
|
|
45
|
+
type: "agent_end";
|
|
46
|
+
subAgentId: string;
|
|
47
|
+
timestamp: number;
|
|
48
|
+
success: boolean;
|
|
49
|
+
error?: string;
|
|
50
|
+
};
|
|
10
51
|
/**
|
|
11
52
|
* Specification for spawning a SubAgent.
|
|
12
53
|
*/
|
|
@@ -25,6 +66,12 @@ export interface SubAgentSpec {
|
|
|
25
66
|
images?: ImageContent[];
|
|
26
67
|
/** Model to use (reuses main session's model and auth) */
|
|
27
68
|
model?: Model<any>;
|
|
69
|
+
/** Files to inject into the initial prompt as read-only context */
|
|
70
|
+
contextFiles?: string[];
|
|
71
|
+
/** Optional callback invoked after the run result is available */
|
|
72
|
+
exitHook?: (result: SubAgentResult) => Promise<void> | void;
|
|
73
|
+
/** Optional realtime observer for TUI/status integrations */
|
|
74
|
+
onEvent?: (event: SubAgentEvent) => void;
|
|
28
75
|
}
|
|
29
76
|
/**
|
|
30
77
|
* Result from a completed SubAgent run.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* [WHO]: SubAgent types - SubAgentSpec, SubAgentHandle, SubAgentBackend, SubAgentResult
|
|
2
|
+
* [WHO]: SubAgent types - SubAgentSpec, SubAgentEvent, SubAgentHandle, SubAgentBackend, SubAgentResult
|
|
3
3
|
* [FROM]: Depends on @pencil-agent/agent-core, @pencil-agent/ai, core/tools
|
|
4
4
|
* [TO]: Consumed by ./sub-agent-runtime, ./sub-agent-backend, ./index.ts, extensions/defaults/subagent/*, extensions/defaults/team/*
|
|
5
5
|
* [HERE]: core/sub-agent/sub-agent-types.ts - SubAgent type definitions
|
|
@@ -37,14 +37,14 @@ loop/scheduler-controller.ts: SchedulerController - in-memory recurring task sto
|
|
|
37
37
|
loop/scheduler-parser.ts: Loop command parsing with flags/subcommands, parseSchedulerCommand/parseDurationSpec/buildSchedulerHelp, --name/--max/--quiet
|
|
38
38
|
loop/scheduler-types.ts: Scheduled loop types, LoopPayloadKind/ScheduledLoopTask/LoopStartSpec/ParsedSchedulerCommand
|
|
39
39
|
loop/README.md: Loop extension documentation - recurring scheduler usage and flags
|
|
40
|
-
sal/index.ts: SAL extension entry, enabled by default, registers --nosal/--sal-rebuild-terrain flags, /sal:coverage /sal:status /sal:setup commands, before_agent_start/tool_execution_start/agent_end hooks; /sal:setup writes ~/.memory-experiments/credentials.json with adapter inference (insforge/jsonl/noop); publishes structuralAnchor via core/runtime/turn-context (no SAL-specific globals); emits run_start/turn_anchor/run_end eval events through pluggable EvalSink; runtime no-op when --nosal is set
|
|
40
|
+
sal/index.ts: SAL extension entry, enabled by default, registers --nosal/--sal-ab/--sal-rebuild-terrain flags, /sal:coverage /sal:status /sal:setup commands, before_agent_start/tool_execution_start/agent_end hooks; /sal:setup writes ~/.memory-experiments/credentials.json with adapter inference (insforge/jsonl/noop); publishes structuralAnchor via core/runtime/turn-context (no SAL-specific globals); emits run_start/turn_anchor/run_end eval events through pluggable EvalSink with best-effort shutdown flushing; writes local .memory-experiments sidecar anchors only when --sal-ab or NANOPENCIL_SAL_AB=1 is enabled; runtime no-op when --nosal is set
|
|
41
41
|
sal/terrain.ts: TerrainSnapshot/TerrainNode/TerrainEdge model, buildTerrainIndex(), checkDipCoverage(), isSnapshotStale(), moduleIdForPath(), parses P2 AGENT.md and P3 file headers
|
|
42
42
|
sal/anchors.ts: StructuralAnchor/AnchorResolution model, locateTask(), locateAction(), evidence-driven scoring with tunable SalWeights, CJK bigram tokenization
|
|
43
43
|
sal/weights.ts: SalWeights interface, SAL_DEFAULT_WEIGHTS, loadSalWeights() reads sal-config.json from workspace or .memory-experiments/sal/
|
|
44
44
|
sal/eval/index.ts: createEvalSink() factory + barrel re-exports; adapter selection via options.adapter or endpoint scheme inference (http(s)→insforge, file://|/|./|../→jsonl, missing→noop); ONLY entry point SAL imports from
|
|
45
45
|
sal/eval/types.ts: EvalSink interface, EvalEventEnvelope/EvalEventType (run_start/run_end/turn_anchor), EvalAdapterId ("insforge"|"jsonl"|"noop"), CreateEvalSinkOptions, createEvalEvent factory; zero-dependency type surface
|
|
46
46
|
sal/eval/noop-sink.ts: noopSink — silent EvalSink used when eval disabled or no adapter configured
|
|
47
|
-
sal/eval/insforge-sink.ts: InsForgeEvalSink — PostgREST adapter, routes run_start→eval_runs INSERT (merge-duplicates), turn_anchor
|
|
47
|
+
sal/eval/insforge-sink.ts: InsForgeEvalSink — PostgREST adapter, routes run_start→eval_runs INSERT (merge-duplicates) with legacy-schema fallback, writes turn_anchor/tool_trace/memory_recalls/run_end only after parent run confirmation, tool_trace→eval_tool_traces with PGRST204 fallback, memory_recalls→eval_memory_recalls batch INSERT, run_end→eval_runs PATCH; allowSelfSigned TLS option logs only in development runtime, batching with default 2000ms interval
|
|
48
48
|
sal/eval/jsonl-sink.ts: JsonlEvalSink — append-only filesystem adapter, one JSON object per line, accepts file:// URLs or plain paths, auto-creates parent dir, batched writes
|
|
49
49
|
sal/README.md: SAL extension usage, sidecar output layout, weights override, pluggability contract
|
|
50
50
|
team/index.ts: AgentTeam extension entry, /team:/team:spawn/:send/:status/:stop/:terminate/:approve/:mode commands, TEAM_MESSAGE_TYPE renderer
|
|
@@ -17,10 +17,11 @@ security-audit/engine/interceptor.ts: Request/response interception, Interceptor
|
|
|
17
17
|
security-audit/engine/logger.ts: Security event logging, JSON file audit trail
|
|
18
18
|
security-audit/engine/detector.ts: Vulnerability detection, pattern matching for dangerous commands
|
|
19
19
|
soul/index.ts: AI personality evolution extension, persistent personality across sessions
|
|
20
|
-
grub/index.ts: Grub extension entry - long-running autonomous harness, dual-phase system prompts (initializer/coding), /grub command (start/status/resume/stop) + grub renderer, session_start auto-adopt, git harness commit, pruneStale cleanup
|
|
21
|
-
grub/grub-controller.ts: GrubController - state machine for /grub iterations, durable persistState on every transition, adoptResumedTask for cross-session resume, validateCompletion downgrades premature complete when feature-list still has pending entries
|
|
22
|
-
grub/grub-parser.ts: Grub command parsing - parseGrubCommand/buildGrubHelp with resume subcommand, status --json, --max-iter/--max-fail flags
|
|
23
|
-
grub/grub-types.ts: Grub types - GrubStatus/GrubDecisionStatus/GrubDecision/GrubPhase/GrubTaskState/GrubTaskSnapshot/ParsedGrubCommand + FeatureItem/FeatureList (version 1 schema) + PersistedGrubState envelope
|
|
20
|
+
grub/index.ts: Grub extension entry - long-running autonomous harness, locale-aware dual-phase system prompts (initializer/coding), /grub command (start/status/resume/stop) + grub renderer, session_start auto-adopt, git harness commit, pruneStale cleanup
|
|
21
|
+
grub/grub-controller.ts: GrubController - state machine for /grub iterations, locale-persisted prompt generation, durable persistState on every transition, adoptResumedTask for cross-session resume, validateCompletion downgrades premature complete when feature-list still has pending entries
|
|
22
|
+
grub/grub-parser.ts: Grub command parsing - parseGrubCommand/buildGrubHelp with localized help, resume subcommand, status --json, --max-iter/--max-fail flags
|
|
23
|
+
grub/grub-types.ts: Grub types - GrubStatus/GrubDecisionStatus/GrubDecision/GrubPhase/GrubLocale/GrubTaskState/GrubTaskSnapshot/ParsedGrubCommand + FeatureItem/FeatureList (version 1 schema) + PersistedGrubState envelope
|
|
24
|
+
grub/grub-i18n.ts: Grub localization helper - detectGrubLocale(), grubText(), languageName(), English/Chinese TUI strings
|
|
24
25
|
grub/grub-feature-list.ts: feature-list.json IO - readFeatureList/writeFeatureList atomic write, validateFeatureListDiff enforces passes/evidence-only mutations, createInitialFeatureList placeholder, migrateChecklistToFeatureList legacy converter, countPassing/allPassing/firstPending helpers
|
|
25
26
|
grub/grub-persistence.ts: Cross-session persistence - persistState atomic JSON write to .grub/<id>/state.json, loadState shape-validated read, discoverActiveTasks scans .grub/ for running records, pruneStale removes terminal harnesses older than 30 days by default
|
|
26
27
|
grub/README.md: Grub extension documentation - long-running harness contract, feature-list.json schema, completion guard, cross-session resume, legacy migration
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* [TO]: Consumed by extension entry point (./index.ts)
|
|
5
5
|
* [HERE]: extensions/defaults/grub/grub-controller.ts - state machine for /grub iterations with cross-session persistence and feature-list-gated completion
|
|
6
6
|
*/
|
|
7
|
+
import { type GrubLocale } from "./grub-i18n.js";
|
|
7
8
|
import type { GrubControllerState, GrubDecision, GrubTaskSnapshot, GrubTaskState } from "./grub-types.js";
|
|
8
9
|
export interface GrubStartOptions {
|
|
9
10
|
maxIterations?: number;
|
|
10
11
|
maxConsecutiveFailures?: number;
|
|
12
|
+
locale?: GrubLocale;
|
|
11
13
|
}
|
|
12
14
|
export declare class GrubController {
|
|
13
15
|
private activeTask?;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { allPassing, firstPending, readFeatureList } from "./grub-feature-list.js";
|
|
10
|
+
import { languageName, grubText } from "./grub-i18n.js";
|
|
10
11
|
import { persistState, stateFilePathFor } from "./grub-persistence.js";
|
|
11
12
|
const DEFAULT_MAX_ITERATIONS = 25;
|
|
12
13
|
const DEFAULT_MAX_CONSECUTIVE_FAILURES = 3;
|
|
@@ -39,6 +40,7 @@ export class GrubController {
|
|
|
39
40
|
const task = {
|
|
40
41
|
id,
|
|
41
42
|
goal: trimmedGoal,
|
|
43
|
+
locale: options.locale ?? "en",
|
|
42
44
|
status: "running",
|
|
43
45
|
phase: "initializer",
|
|
44
46
|
startedAt: now,
|
|
@@ -67,7 +69,7 @@ export class GrubController {
|
|
|
67
69
|
if (this.activeTask && this.activeTask.id !== task.id) {
|
|
68
70
|
throw new Error(`Cannot adopt task ${task.id}; ${this.activeTask.id} is already active.`);
|
|
69
71
|
}
|
|
70
|
-
const resumed = { ...task, awaitingTurn: false, updatedAt: Date.now() };
|
|
72
|
+
const resumed = { ...task, locale: task.locale ?? "en", awaitingTurn: false, updatedAt: Date.now() };
|
|
71
73
|
this.activeTask = resumed;
|
|
72
74
|
this.safePersist(resumed);
|
|
73
75
|
return resumed;
|
|
@@ -82,6 +84,7 @@ export class GrubController {
|
|
|
82
84
|
const snapshot = {
|
|
83
85
|
id: finalTask.id,
|
|
84
86
|
goal: finalTask.goal,
|
|
87
|
+
locale: finalTask.locale,
|
|
85
88
|
status,
|
|
86
89
|
phase: finalTask.phase,
|
|
87
90
|
startedAt: finalTask.startedAt,
|
|
@@ -109,36 +112,68 @@ export class GrubController {
|
|
|
109
112
|
throw new Error("No active grub task.");
|
|
110
113
|
}
|
|
111
114
|
const task = this.activeTask;
|
|
115
|
+
const text = grubText(task.locale);
|
|
112
116
|
const sections = [
|
|
113
117
|
`${this.getPromptPrefix(task.id)}${task.currentIteration}]`,
|
|
114
118
|
"",
|
|
115
|
-
"Autonomous grub goal:",
|
|
119
|
+
task.locale === "zh" ? "自主 Grub 目标:" : "Autonomous grub goal:",
|
|
116
120
|
task.goal,
|
|
117
121
|
"",
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
task.locale === "zh"
|
|
123
|
+
? "你正在一个受控的 grub harness 中工作。请围绕同一个目标持续推进具体进展。"
|
|
124
|
+
: "You are inside a managed grub harness. Keep making concrete progress on the same goal.",
|
|
125
|
+
task.locale === "zh"
|
|
126
|
+
? "按需使用工具、编辑文件、运行检查并验证结果。所有面向用户的总结、进度和说明都必须使用中文。"
|
|
127
|
+
: "Use tools, edit files, run checks, and verify results as needed.",
|
|
128
|
+
`User language: ${languageName(task.locale)}.`,
|
|
120
129
|
"",
|
|
121
|
-
"Harness files (must stay up to date every iteration):",
|
|
122
|
-
`-
|
|
123
|
-
`-
|
|
124
|
-
`-
|
|
130
|
+
task.locale === "zh" ? "Harness 文件(每轮都必须保持最新):" : "Harness files (must stay up to date every iteration):",
|
|
131
|
+
`- ${text.featureList}: ${task.featureListPath}`,
|
|
132
|
+
`- ${text.progressLog}: ${task.progressLogPath}`,
|
|
133
|
+
`- ${text.initScript}: ${task.initScriptPath}`,
|
|
125
134
|
];
|
|
126
135
|
if (task.phase === "initializer") {
|
|
127
|
-
sections.push("",
|
|
136
|
+
sections.push("", task.locale === "zh" ? "初始化阶段要求:" : "Initializer phase requirements:", task.locale === "zh"
|
|
137
|
+
? "1. 将 feature-list.json 的占位内容替换为 15-40 个具体、可测试的切片。每项必须保持 {id, category, description, steps[], passes:false}。"
|
|
138
|
+
: "1. Replace the placeholder feature-list.json with 15-40 concrete, testable slices. Every entry MUST keep the schema {id, category, description, steps[], passes:false}.", task.locale === "zh"
|
|
139
|
+
? "2. 确保 init.sh 包含可靠的启动检查,并设置为可执行。"
|
|
140
|
+
: "2. Ensure init.sh contains reliable startup checks and make it executable.", task.locale === "zh"
|
|
141
|
+
? "3. 在 progress-log.md 中追加清晰的初始化总结。"
|
|
142
|
+
: "3. Append a clear initialization summary in progress-log.md.", task.locale === "zh"
|
|
143
|
+
? "4. 先建立强 harness,不要开始大范围实现。"
|
|
144
|
+
: "4. Do not attempt broad implementation yet; prepare a strong harness first.", task.locale === "zh"
|
|
145
|
+
? "5. 除非目标已经完成或阻塞,否则本轮以 loop-state status=continue 结束。"
|
|
146
|
+
: "5. End this turn with loop-state status=continue unless the goal is already complete/blocked.");
|
|
128
147
|
}
|
|
129
148
|
else {
|
|
130
|
-
sections.push("",
|
|
149
|
+
sections.push("", task.locale === "zh" ? "执行阶段要求:" : "Execution phase requirements:", task.locale === "zh"
|
|
150
|
+
? "1. 先运行 init.sh,再读取 feature-list.json 和 progress-log.md。"
|
|
151
|
+
: "1. Start by running the init script, then read feature-list.json and progress-log.md.", task.locale === "zh"
|
|
152
|
+
? "2. 只选择一个 passes:false 的 feature,并端到端完成它。"
|
|
153
|
+
: "2. Pick exactly one feature with passes:false and execute it end-to-end.", task.locale === "zh"
|
|
154
|
+
? "3. 运行相关验证(测试、烟测或运行时检查)。"
|
|
155
|
+
: "3. Run relevant verification (tests, smoke checks, or runtime checks).", task.locale === "zh"
|
|
156
|
+
? "4. 只能修改该 feature 的 passes/evidence 字段;其他字段不可变。"
|
|
157
|
+
: "4. Flip ONLY the passes/evidence fields for that feature; other fields are immutable.", task.locale === "zh"
|
|
158
|
+
? "5. 本轮结束前追加进度日志并 git commit。"
|
|
159
|
+
: "5. Append progress log and git-commit before finishing the turn.", task.locale === "zh"
|
|
160
|
+
? "6. 每轮都保持增量、安全、可回退。"
|
|
161
|
+
: "6. Keep each iteration incremental and production-safe.");
|
|
131
162
|
}
|
|
132
163
|
if (task.lastDecision?.summary) {
|
|
133
|
-
sections.push("", "Previous summary:", task.lastDecision.summary);
|
|
164
|
+
sections.push("", task.locale === "zh" ? "上次总结:" : "Previous summary:", task.lastDecision.summary);
|
|
134
165
|
}
|
|
135
166
|
if (task.lastDecision?.nextStep) {
|
|
136
|
-
sections.push("", "Previous planned next step:", task.lastDecision.nextStep);
|
|
167
|
+
sections.push("", task.locale === "zh" ? "上次计划的下一步:" : "Previous planned next step:", task.lastDecision.nextStep);
|
|
137
168
|
}
|
|
138
169
|
if (task.lastError) {
|
|
139
|
-
sections.push("", "Recovery note:", task.lastError);
|
|
170
|
+
sections.push("", task.locale === "zh" ? "恢复提示:" : "Recovery note:", task.lastError);
|
|
140
171
|
}
|
|
141
|
-
sections.push("",
|
|
172
|
+
sections.push("", task.locale === "zh"
|
|
173
|
+
? "不要因为一次查询结束就停止。只有 feature-list.json 中每个 feature 都 passes:true 时,才可以决定 `complete`。"
|
|
174
|
+
: "Do not stop just because one query finished. Only decide `complete` when every feature in feature-list.json has passes:true.", task.locale === "zh"
|
|
175
|
+
? "如果还需要下一轮自主推进,请以有效的 <loop-state> 块结束,让系统自动继续。"
|
|
176
|
+
: "If you need another autonomous pass, end with a valid <loop-state> block so the system can continue automatically.");
|
|
142
177
|
return sections.join("\n");
|
|
143
178
|
}
|
|
144
179
|
markDispatched() {
|
|
@@ -165,12 +200,14 @@ export class GrubController {
|
|
|
165
200
|
const rewritten = {
|
|
166
201
|
status: "continue",
|
|
167
202
|
summary: decision.summary,
|
|
168
|
-
nextStep:
|
|
203
|
+
nextStep: this.activeTask.locale === "zh"
|
|
204
|
+
? "feature-list.json 缺失或无效;初始化阶段必须先生成它,不能直接声明完成。"
|
|
205
|
+
: "feature-list.json is missing or invalid; the initializer must produce it before claiming complete.",
|
|
169
206
|
};
|
|
170
207
|
return {
|
|
171
208
|
decision: rewritten,
|
|
172
209
|
downgraded: true,
|
|
173
|
-
reason: "feature-list.json missing or invalid",
|
|
210
|
+
reason: this.activeTask.locale === "zh" ? "feature-list.json 缺失或无效" : "feature-list.json missing or invalid",
|
|
174
211
|
};
|
|
175
212
|
}
|
|
176
213
|
if (allPassing(list)) {
|
|
@@ -181,13 +218,19 @@ export class GrubController {
|
|
|
181
218
|
status: "continue",
|
|
182
219
|
summary: decision.summary,
|
|
183
220
|
nextStep: pending
|
|
184
|
-
?
|
|
185
|
-
|
|
221
|
+
? this.activeTask.locale === "zh"
|
|
222
|
+
? `完成待处理 feature:${pending.id}(${pending.description})`
|
|
223
|
+
: `Complete pending feature: ${pending.id} (${pending.description})`
|
|
224
|
+
: this.activeTask.locale === "zh"
|
|
225
|
+
? "先完成剩余待处理 feature,再声明完成。"
|
|
226
|
+
: "Complete the remaining pending features before declaring done.",
|
|
186
227
|
};
|
|
187
228
|
return {
|
|
188
229
|
decision: rewritten,
|
|
189
230
|
downgraded: true,
|
|
190
|
-
reason:
|
|
231
|
+
reason: this.activeTask.locale === "zh"
|
|
232
|
+
? `feature-list 仍有 ${list.features.length - list.features.filter((f) => f.passes).length} 个待处理条目`
|
|
233
|
+
: `feature-list still has ${list.features.length - list.features.filter((f) => f.passes).length} pending entries`,
|
|
191
234
|
};
|
|
192
235
|
}
|
|
193
236
|
finishTurn(decision) {
|
|
@@ -204,15 +247,23 @@ export class GrubController {
|
|
|
204
247
|
task.phase = "execution";
|
|
205
248
|
}
|
|
206
249
|
if (decision.status === "complete") {
|
|
207
|
-
return {
|
|
250
|
+
return {
|
|
251
|
+
action: "stop",
|
|
252
|
+
snapshot: this.stop(task.locale === "zh" ? "Grub 目标已完成。" : "Grub goal completed.", "complete"),
|
|
253
|
+
};
|
|
208
254
|
}
|
|
209
255
|
if (decision.status === "blocked") {
|
|
210
|
-
return {
|
|
256
|
+
return {
|
|
257
|
+
action: "stop",
|
|
258
|
+
snapshot: this.stop(task.locale === "zh" ? "Grub 报告任务被阻塞。" : "Grub reported it is blocked.", "blocked"),
|
|
259
|
+
};
|
|
211
260
|
}
|
|
212
261
|
if (task.currentIteration >= task.maxIterations) {
|
|
213
262
|
return {
|
|
214
263
|
action: "stop",
|
|
215
|
-
snapshot: this.stop(
|
|
264
|
+
snapshot: this.stop(task.locale === "zh"
|
|
265
|
+
? `Grub 达到轮次上限(${task.maxIterations})。`
|
|
266
|
+
: `Grub hit the iteration limit (${task.maxIterations}).`, "failed"),
|
|
216
267
|
};
|
|
217
268
|
}
|
|
218
269
|
task.currentIteration += 1;
|
|
@@ -231,13 +282,17 @@ export class GrubController {
|
|
|
231
282
|
if (task.consecutiveFailures >= task.maxConsecutiveFailures) {
|
|
232
283
|
return {
|
|
233
284
|
action: "stop",
|
|
234
|
-
snapshot: this.stop(
|
|
285
|
+
snapshot: this.stop(task.locale === "zh"
|
|
286
|
+
? `Grub 连续失败 ${task.consecutiveFailures} 次后停止。最近错误:${message}`
|
|
287
|
+
: `Grub stopped after ${task.consecutiveFailures} consecutive failures. Last error: ${message}`, "failed"),
|
|
235
288
|
};
|
|
236
289
|
}
|
|
237
290
|
if (task.currentIteration >= task.maxIterations) {
|
|
238
291
|
return {
|
|
239
292
|
action: "stop",
|
|
240
|
-
snapshot: this.stop(
|
|
293
|
+
snapshot: this.stop(task.locale === "zh"
|
|
294
|
+
? `Grub 达到轮次上限(${task.maxIterations})。`
|
|
295
|
+
: `Grub hit the iteration limit (${task.maxIterations}).`, "failed"),
|
|
241
296
|
};
|
|
242
297
|
}
|
|
243
298
|
task.currentIteration += 1;
|