@pencil-agent/nano-pencil 1.13.8 → 1.13.10

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.
Files changed (83) hide show
  1. package/dist/build-meta.json +3 -3
  2. package/dist/builtin-extensions.js +22 -0
  3. package/dist/core/config/settings-manager.d.ts +7 -0
  4. package/dist/core/runtime/pencil-agent.d.ts +28 -1
  5. package/dist/core/runtime/pencil-agent.js +56 -0
  6. package/dist/core/sub-agent/index.d.ts +1 -1
  7. package/dist/core/sub-agent/sub-agent-backend.js +78 -12
  8. package/dist/core/sub-agent/sub-agent-types.d.ts +44 -1
  9. package/dist/core/sub-agent/sub-agent-types.js +1 -1
  10. package/dist/extensions/defaults/AGENT.md +7 -2
  11. package/dist/extensions/defaults/CLAUDE.md +5 -4
  12. package/dist/extensions/defaults/diagnostics/diagnostic-buffer.d.ts +19 -0
  13. package/dist/extensions/defaults/diagnostics/diagnostic-buffer.js +125 -0
  14. package/dist/extensions/defaults/diagnostics/index.d.ts +8 -0
  15. package/dist/extensions/defaults/diagnostics/index.js +101 -0
  16. package/dist/extensions/defaults/diagnostics/redaction.d.ts +8 -0
  17. package/dist/extensions/defaults/diagnostics/redaction.js +45 -0
  18. package/dist/extensions/defaults/diagnostics/reporter.d.ts +17 -0
  19. package/dist/extensions/defaults/diagnostics/reporter.js +203 -0
  20. package/dist/extensions/defaults/diagnostics/types.d.ts +61 -0
  21. package/dist/extensions/defaults/diagnostics/types.js +7 -0
  22. package/dist/extensions/defaults/grub/grub-controller.d.ts +2 -0
  23. package/dist/extensions/defaults/grub/grub-controller.js +79 -24
  24. package/dist/extensions/defaults/grub/grub-i18n.d.ts +128 -0
  25. package/dist/extensions/defaults/grub/grub-i18n.js +167 -0
  26. package/dist/extensions/defaults/grub/grub-parser.d.ts +2 -1
  27. package/dist/extensions/defaults/grub/grub-parser.js +5 -3
  28. package/dist/extensions/defaults/grub/grub-types.d.ts +3 -0
  29. package/dist/extensions/defaults/grub/index.js +133 -78
  30. package/dist/extensions/defaults/idle-think/curiosity.d.ts +46 -0
  31. package/dist/extensions/defaults/idle-think/curiosity.js +137 -0
  32. package/dist/extensions/defaults/idle-think/index.d.ts +15 -0
  33. package/dist/extensions/defaults/idle-think/index.js +169 -0
  34. package/dist/extensions/defaults/idle-think/insights.d.ts +40 -0
  35. package/dist/extensions/defaults/idle-think/insights.js +123 -0
  36. package/dist/extensions/defaults/idle-think/thinker.d.ts +26 -0
  37. package/dist/extensions/defaults/idle-think/thinker.js +208 -0
  38. package/dist/extensions/defaults/presence/index.d.ts +1 -0
  39. package/dist/extensions/defaults/presence/index.js +83 -10
  40. package/dist/extensions/defaults/sal/eval/index.js +22 -3
  41. package/dist/extensions/defaults/sal/eval/insforge-sink.d.ts +3 -1
  42. package/dist/extensions/defaults/sal/eval/insforge-sink.js +46 -25
  43. package/dist/extensions/defaults/sal/eval/jsonl-sink.d.ts +2 -0
  44. package/dist/extensions/defaults/sal/eval/jsonl-sink.js +15 -2
  45. package/dist/extensions/defaults/sal/eval/types.d.ts +10 -0
  46. package/dist/extensions/defaults/sal/index.js +74 -18
  47. package/dist/extensions/defaults/team/CLAUDE.md +7 -6
  48. package/dist/extensions/defaults/team/index.d.ts +1 -0
  49. package/dist/extensions/defaults/team/index.js +92 -3
  50. package/dist/extensions/defaults/team/team-dashboard.js +6 -0
  51. package/dist/extensions/defaults/team/team-parser.d.ts +1 -1
  52. package/dist/extensions/defaults/team/team-parser.js +3 -2
  53. package/dist/extensions/defaults/team/team-presets.d.ts +14 -2
  54. package/dist/extensions/defaults/team/team-presets.js +124 -4
  55. package/dist/extensions/defaults/team/team-runtime.d.ts +15 -2
  56. package/dist/extensions/defaults/team/team-runtime.js +62 -1
  57. package/dist/extensions/defaults/team/team-types.d.ts +9 -0
  58. package/dist/modes/interactive/components/pencil-loader.d.ts +0 -2
  59. package/dist/modes/interactive/components/pencil-loader.js +1 -14
  60. package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +68 -17
  61. package/dist/node_modules/@pencil-agent/ai/models.generated.js +81 -30
  62. package/dist/packages/mem-core/consolidation.js +14 -1
  63. package/dist/packages/mem-core/diagnostics.d.ts +24 -0
  64. package/dist/packages/mem-core/diagnostics.js +62 -0
  65. package/dist/packages/mem-core/extension.js +48 -28
  66. package/dist/packages/mem-core/extraction.js +14 -1
  67. package/dist/packages/soul-core/diagnostics.d.ts +24 -0
  68. package/dist/packages/soul-core/diagnostics.js +62 -0
  69. package/dist/packages/soul-core/manager.js +13 -2
  70. package/dist/packages/soul-core/src/diagnostics.d.ts +23 -0
  71. package/dist/packages/soul-core/src/diagnostics.js +61 -0
  72. package/dist/packages/soul-core/src/manager.js +13 -2
  73. package/dist/utils/diagnostics.d.ts +38 -0
  74. package/dist/utils/diagnostics.js +89 -0
  75. package/package.json +1 -1
  76. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +0 -251
  77. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +0 -123
  78. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +0 -1222
  79. 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" +0 -158
  80. 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" +0 -128
  81. package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +0 -321
  82. package/docs/loop-usage-examples.md +0 -215
  83. package/docs/planmode.md +0 -1987
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.13.8",
3
- "commitHash": "66ecf7f",
2
+ "version": "1.13.10",
3
+ "commitHash": "027d681",
4
4
  "branch": "main",
5
- "builtAt": "2026-04-26T15:41:55.811Z"
5
+ "builtAt": "2026-04-27T10:15:14.235Z"
6
6
  }
@@ -20,10 +20,12 @@ const BUNDLED_PRESENCE_EXTENSION = join(__dirname, "extensions", "defaults", "pr
20
20
  const BUNDLED_INTERVIEW_EXTENSION = join(__dirname, "extensions", "defaults", "interview", "index.js");
21
21
  const BUNDLED_LOOP_EXTENSION = join(__dirname, "extensions", "defaults", "loop", "index.js");
22
22
  const BUNDLED_PLAN_EXTENSION = join(__dirname, "extensions", "defaults", "plan", "index.js");
23
+ const BUNDLED_DIAGNOSTICS_EXTENSION = join(__dirname, "extensions", "defaults", "diagnostics", "index.js");
23
24
  const BUNDLED_SAL_EXTENSION = join(__dirname, "extensions", "defaults", "sal", "index.js");
24
25
  const BUNDLED_GRUB_EXTENSION = join(__dirname, "extensions", "defaults", "grub", "index.js");
25
26
  const BUNDLED_SUBAGENT_EXTENSION = join(__dirname, "extensions", "defaults", "subagent", "index.js");
26
27
  const BUNDLED_TEAM_EXTENSION = join(__dirname, "extensions", "defaults", "team", "index.js");
28
+ const BUNDLED_IDLE_THINK_EXTENSION = join(__dirname, "extensions", "defaults", "idle-think", "index.js");
27
29
  const BUNDLED_BTW_EXTENSION = join(__dirname, "extensions", "defaults", "btw", "index.js");
28
30
  const BUNDLED_DEBUG_EXTENSION = join(__dirname, "extensions", "defaults", "debug", "index.js");
29
31
  const BUNDLED_MCP_EXTENSION = join(__dirname, "extensions", "defaults", "mcp", "index.js");
@@ -67,6 +69,17 @@ function findPackageRoot(startDir) {
67
69
  */
68
70
  export function getBuiltinExtensionPaths() {
69
71
  const paths = [];
72
+ // === Diagnostics extension (extension-owned issue buffer and reporting) ===
73
+ // Loaded first so it can subscribe to diagnostic:event before producer
74
+ // extensions such as SAL publish background failures.
75
+ if (existsSync(BUNDLED_DIAGNOSTICS_EXTENSION)) {
76
+ paths.push(BUNDLED_DIAGNOSTICS_EXTENSION);
77
+ }
78
+ else {
79
+ const diagnosticsTs = join(__dirname, "extensions", "defaults", "diagnostics", "index.ts");
80
+ if (existsSync(diagnosticsTs))
81
+ paths.push(diagnosticsTs);
82
+ }
70
83
  // === SAL extension (Structural Anchor Localization, default-on, experimental) ===
71
84
  // Loaded ahead of NanoMem because turn-context producers must publish before
72
85
  // turn-context consumers read. SAL is a producer of structuralAnchor; NanoMem
@@ -211,6 +224,15 @@ export function getBuiltinExtensionPaths() {
211
224
  if (existsSync(teamTs))
212
225
  paths.push(teamTs);
213
226
  }
227
+ // === IdleThink extension (background code exploration during idle) ===
228
+ if (existsSync(BUNDLED_IDLE_THINK_EXTENSION)) {
229
+ paths.push(BUNDLED_IDLE_THINK_EXTENSION);
230
+ }
231
+ else {
232
+ const idleThinkTs = join(__dirname, "extensions", "defaults", "idle-think", "index.ts");
233
+ if (existsSync(idleThinkTs))
234
+ paths.push(idleThinkTs);
235
+ }
214
236
  // === BTW extension (quick side question without interrupting) ===
215
237
  if (existsSync(BUNDLED_BTW_EXTENSION)) {
216
238
  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 */
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { type SDKLogger } from "./sdk.js";
8
8
  import { type AgentSessionEvent } from "./agent-session.js";
9
+ import type { Api } from "@pencil-agent/ai";
9
10
  import type { ThinkingLevel } from "@pencil-agent/agent-core";
10
11
  /**
11
12
  * Simplified options for PencilAgent wrapper.
@@ -13,10 +14,21 @@ import type { ThinkingLevel } from "@pencil-agent/agent-core";
13
14
  export interface PencilAgentOptions {
14
15
  /** API key for the provider. If omitted, uses environment variable. */
15
16
  apiKey?: string;
16
- /** Provider name: 'anthropic', 'openai', 'google', etc. */
17
+ /** Provider name: 'anthropic', 'openai', 'google', or any custom provider in models.json. */
17
18
  provider?: string;
18
19
  /** Model ID: 'claude-4-5-20250920', 'gpt-4o', etc. */
19
20
  model?: string;
21
+ /**
22
+ * Optional base URL when registering a custom provider on the fly.
23
+ * Required when `provider` + `model` is not already defined in
24
+ * ~/.nanopencil/agent/models.json. Ignored when the model is found.
25
+ */
26
+ baseUrl?: string;
27
+ /**
28
+ * Optional API protocol for the dynamically-registered provider.
29
+ * Defaults to "openai-completions". Ignored when the model is found.
30
+ */
31
+ api?: Api;
20
32
  /** Thinking level: 'off' | 'low' | 'medium' | 'high' */
21
33
  thinkingLevel?: ThinkingLevel;
22
34
  /** Working directory. Default: process.cwd() */
@@ -69,6 +81,21 @@ export declare class PencilAgent {
69
81
  * Must be called before run/chat.
70
82
  */
71
83
  init(): Promise<void>;
84
+ /**
85
+ * Resolve constructor-provided provider/model into a Model<any>.
86
+ *
87
+ * Lookup order:
88
+ * 1. Existing entry in modelRegistry (e.g. ~/.nanopencil/agent/models.json
89
+ * already declares this provider/model — common case for users who ran
90
+ * /sal:setup or hand-edited models.json).
91
+ * 2. Dynamic registration when caller supplied baseUrl + apiKey — lets a
92
+ * one-line constructor call wire up a brand-new OpenAI-compatible
93
+ * endpoint without touching disk.
94
+ * 3. Otherwise return undefined and let createAgentSession fall back to
95
+ * findInitialModel() (built-in default). The logger surfaces a warning
96
+ * in this case so the caller knows their args were not honoured.
97
+ */
98
+ private resolveRequestedModel;
72
99
  /**
73
100
  * Handle session events - collects text for run()
74
101
  */
@@ -63,6 +63,10 @@ export class PencilAgent {
63
63
  key: this.options.apiKey,
64
64
  });
65
65
  }
66
+ // Resolve user-specified provider/model into a Model<any> for createAgentSession.
67
+ // Without this, createAgentSession falls back to findInitialModel() which
68
+ // picks the first available built-in — silently ignoring constructor args.
69
+ const resolvedModel = this.resolveRequestedModel(modelRegistry);
66
70
  // Resolve tools
67
71
  let tools = undefined;
68
72
  if (this.options.tools && this.options.tools.length > 0) {
@@ -73,6 +77,7 @@ export class PencilAgent {
73
77
  // Create session
74
78
  this.sessionResult = await createAgentSession({
75
79
  cwd: this.cwd,
80
+ model: resolvedModel,
76
81
  thinkingLevel: this.options.thinkingLevel,
77
82
  tools,
78
83
  authStorage,
@@ -94,6 +99,57 @@ export class PencilAgent {
94
99
  this.session.subscribe(this.handleEvent.bind(this));
95
100
  this.initialized = true;
96
101
  }
102
+ /**
103
+ * Resolve constructor-provided provider/model into a Model<any>.
104
+ *
105
+ * Lookup order:
106
+ * 1. Existing entry in modelRegistry (e.g. ~/.nanopencil/agent/models.json
107
+ * already declares this provider/model — common case for users who ran
108
+ * /sal:setup or hand-edited models.json).
109
+ * 2. Dynamic registration when caller supplied baseUrl + apiKey — lets a
110
+ * one-line constructor call wire up a brand-new OpenAI-compatible
111
+ * endpoint without touching disk.
112
+ * 3. Otherwise return undefined and let createAgentSession fall back to
113
+ * findInitialModel() (built-in default). The logger surfaces a warning
114
+ * in this case so the caller knows their args were not honoured.
115
+ */
116
+ resolveRequestedModel(registry) {
117
+ const provider = this.options.provider;
118
+ const modelId = this.options.model;
119
+ if (!provider || !modelId)
120
+ return undefined;
121
+ const existing = registry.find(provider, modelId);
122
+ if (existing)
123
+ return existing;
124
+ if (this.options.baseUrl) {
125
+ try {
126
+ registry.registerProvider(provider, {
127
+ api: this.options.api ?? "openai-completions",
128
+ baseUrl: this.options.baseUrl,
129
+ apiKey: this.options.apiKey,
130
+ models: [{
131
+ id: modelId,
132
+ name: modelId,
133
+ reasoning: false,
134
+ input: ["text"],
135
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
136
+ contextWindow: 128_000,
137
+ maxTokens: 8192,
138
+ }],
139
+ });
140
+ const registered = registry.find(provider, modelId);
141
+ if (registered)
142
+ return registered;
143
+ }
144
+ catch (err) {
145
+ this.logger.warn(`[PencilAgent] dynamic provider registration failed for ${provider}/${modelId}: ${err.message}`);
146
+ }
147
+ }
148
+ this.logger.warn(`[PencilAgent] model ${provider}/${modelId} not found in registry. ` +
149
+ `Either add it to ~/.nanopencil/agent/models.json or pass { baseUrl, apiKey } to register it dynamically. ` +
150
+ `Falling back to default model selection.`);
151
+ return undefined;
152
+ }
97
153
  /**
98
154
  * Handle session events - collects text for run()
99
155
  */
@@ -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";
@@ -32,6 +32,12 @@ export class InProcessSubAgentBackend {
32
32
  model: spec.model,
33
33
  };
34
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
+ });
35
41
  const timeoutMs = spec.timeoutMs;
36
42
  let status = "running";
37
43
  let result;
@@ -44,21 +50,10 @@ export class InProcessSubAgentBackend {
44
50
  }
45
51
  }, timeoutMs);
46
52
  }
47
- // Extract text from assistant message content
48
- const extractTextFromContent = (content) => {
49
- if (typeof content === "string")
50
- return content;
51
- if (Array.isArray(content)) {
52
- return content
53
- .filter((part) => typeof part === "object" && part !== null && "type" in part && part.type === "text" && typeof part.text === "string")
54
- .map((part) => part.text)
55
- .join("\n");
56
- }
57
- return "";
58
- };
59
53
  // Start the prompt
60
54
  const promptPromise = (async () => {
61
55
  try {
56
+ spec.onEvent?.({ type: "agent_start", subAgentId: id, timestamp: Date.now() });
62
57
  await session.prompt(prompt, {
63
58
  images: spec.images,
64
59
  });
@@ -107,6 +102,14 @@ export class InProcessSubAgentBackend {
107
102
  }
108
103
  // Clean up signal handler
109
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
+ });
110
113
  }
111
114
  })();
112
115
  return {
@@ -132,6 +135,69 @@ export class InProcessSubAgentBackend {
132
135
  };
133
136
  }
134
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
+ }
135
201
  async function buildPromptWithContextFiles(spec) {
136
202
  if (!spec.contextFiles?.length) {
137
203
  return spec.prompt;
@@ -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
  */
@@ -29,6 +70,8 @@ export interface SubAgentSpec {
29
70
  contextFiles?: string[];
30
71
  /** Optional callback invoked after the run result is available */
31
72
  exitHook?: (result: SubAgentResult) => Promise<void> | void;
73
+ /** Optional realtime observer for TUI/status integrations */
74
+ onEvent?: (event: SubAgentEvent) => void;
32
75
  }
33
76
  /**
34
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
@@ -3,6 +3,11 @@
3
3
  > P2 | Parent: ../AGENT.md
4
4
 
5
5
  Member List
6
+ diagnostics/index.ts: Diagnostics extension entry, subscribes to diagnostic:event, buffers session-local diagnostic records, prompts only after threshold at agent_end, registers /report-issue
7
+ diagnostics/types.ts: Diagnostic event/report type contract and diagnostic:event channel name
8
+ diagnostics/diagnostic-buffer.ts: DiagnosticBuffer, event coercion, fingerprint dedupe, prompt gating
9
+ diagnostics/reporter.ts: User-approved InsForge pencil_issue_events reporter, configured via NANOPENCIL_ISSUE_* env vars
10
+ diagnostics/redaction.ts: Diagnostic message normalization and secret/path redaction helpers
6
11
  link-world/index.ts: Internet access extension, provides internet-search Skill after setup
7
12
  mcp/index.ts: MCP protocol integration extension, MCP guidance resources
8
13
  presence/index.ts: AI-driven opening + idle presence lines, uses NanoMemEngine episodes/preferences/lessons + git/cwd snapshot, injects latest line into agent systemPrompt every turn for main-conversation perception, 30s debounce + idle in-flight lock, configurable via settings.presence.enabled, PRESENCE_MESSAGE_TYPE renderer
@@ -37,12 +42,12 @@ loop/scheduler-controller.ts: SchedulerController - in-memory recurring task sto
37
42
  loop/scheduler-parser.ts: Loop command parsing with flags/subcommands, parseSchedulerCommand/parseDurationSpec/buildSchedulerHelp, --name/--max/--quiet
38
43
  loop/scheduler-types.ts: Scheduled loop types, LoopPayloadKind/ScheduledLoopTask/LoopStartSpec/ParsedSchedulerCommand
39
44
  loop/README.md: Loop extension documentation - recurring scheduler usage and flags
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
45
+ 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; publishes SAL eval background failures to diagnostic:event; 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
46
  sal/terrain.ts: TerrainSnapshot/TerrainNode/TerrainEdge model, buildTerrainIndex(), checkDipCoverage(), isSnapshotStale(), moduleIdForPath(), parses P2 AGENT.md and P3 file headers
42
47
  sal/anchors.ts: StructuralAnchor/AnchorResolution model, locateTask(), locateAction(), evidence-driven scoring with tunable SalWeights, CJK bigram tokenization
43
48
  sal/weights.ts: SalWeights interface, SAL_DEFAULT_WEIGHTS, loadSalWeights() reads sal-config.json from workspace or .memory-experiments/sal/
44
49
  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
- 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
50
+ sal/eval/types.ts: EvalSink interface, EvalEventEnvelope/EvalEventType (run_start/run_end/turn_anchor), EvalAdapterId ("insforge"|"jsonl"|"noop"), CreateEvalSinkOptions with optional onDiagnostic callback, createEvalEvent factory; zero-dependency type surface
46
51
  sal/eval/noop-sink.ts: noopSink — silent EvalSink used when eval disabled or no adapter configured
47
52
  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
53
  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
@@ -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
@@ -0,0 +1,19 @@
1
+ /**
2
+ * [WHO]: DiagnosticBuffer, coerceDiagnosticEvent()
3
+ * [FROM]: Depends on ./types.js and ./redaction.js for event schema and privacy normalization
4
+ * [TO]: Consumed by extensions/defaults/diagnostics/index.ts
5
+ * [HERE]: extensions/defaults/diagnostics/diagnostic-buffer.ts - session-local dedupe and prompt gating state
6
+ */
7
+ import { type DiagnosticEvent, type DiagnosticRecord } from "./types.js";
8
+ export declare class DiagnosticBuffer {
9
+ private records;
10
+ add(event: DiagnosticEvent): DiagnosticRecord;
11
+ all(): DiagnosticRecord[];
12
+ last(): DiagnosticRecord | undefined;
13
+ findPromptCandidate(): DiagnosticRecord | undefined;
14
+ findUnreported(): DiagnosticRecord[];
15
+ markPrompted(fingerprint: string): void;
16
+ markReported(fingerprint: string): void;
17
+ private trim;
18
+ }
19
+ export declare function coerceDiagnosticEvent(value: unknown): DiagnosticEvent | undefined;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * [WHO]: DiagnosticBuffer, coerceDiagnosticEvent()
3
+ * [FROM]: Depends on ./types.js and ./redaction.js for event schema and privacy normalization
4
+ * [TO]: Consumed by extensions/defaults/diagnostics/index.ts
5
+ * [HERE]: extensions/defaults/diagnostics/diagnostic-buffer.ts - session-local dedupe and prompt gating state
6
+ */
7
+ import { normalizeDiagnosticMessage, sanitizeDiagnosticValue } from "./redaction.js";
8
+ const MAX_RECORDS = 100;
9
+ export class DiagnosticBuffer {
10
+ records = new Map();
11
+ add(event) {
12
+ const now = event.created_at ?? new Date().toISOString();
13
+ const sanitized = {
14
+ source: event.source,
15
+ severity: event.severity,
16
+ category: event.category,
17
+ message: normalizeDiagnosticMessage(event.message),
18
+ detail: sanitizeDiagnosticValue(event.detail),
19
+ context: sanitizeDiagnosticValue(event.context),
20
+ created_at: now,
21
+ };
22
+ const fingerprint = event.fingerprint ?? buildFingerprint(sanitized);
23
+ const existing = this.records.get(fingerprint);
24
+ if (existing) {
25
+ existing.last_seen_at = now;
26
+ existing.occurrence_count += 1;
27
+ existing.severity = maxSeverity(existing.severity, sanitized.severity);
28
+ existing.detail = sanitized.detail;
29
+ existing.context = { ...(existing.context ?? {}), ...(sanitized.context ?? {}) };
30
+ // New occurrences invalidate a prior auto-report so the next
31
+ // agent_end batch uploads the updated count.
32
+ existing.reported = false;
33
+ return existing;
34
+ }
35
+ const record = {
36
+ ...sanitized,
37
+ fingerprint,
38
+ first_seen_at: now,
39
+ last_seen_at: now,
40
+ occurrence_count: 1,
41
+ prompted: false,
42
+ reported: false,
43
+ };
44
+ this.records.set(fingerprint, record);
45
+ this.trim();
46
+ return record;
47
+ }
48
+ all() {
49
+ return Array.from(this.records.values()).sort((a, b) => b.last_seen_at.localeCompare(a.last_seen_at));
50
+ }
51
+ last() {
52
+ return this.all()[0];
53
+ }
54
+ findPromptCandidate() {
55
+ return this.all().find((record) => !record.prompted && shouldPrompt(record));
56
+ }
57
+ findUnreported() {
58
+ return this.all().filter((record) => !record.reported);
59
+ }
60
+ markPrompted(fingerprint) {
61
+ const record = this.records.get(fingerprint);
62
+ if (record)
63
+ record.prompted = true;
64
+ }
65
+ markReported(fingerprint) {
66
+ const record = this.records.get(fingerprint);
67
+ if (record)
68
+ record.reported = true;
69
+ }
70
+ trim() {
71
+ if (this.records.size <= MAX_RECORDS)
72
+ return;
73
+ const sorted = this.all();
74
+ for (const record of sorted.slice(MAX_RECORDS)) {
75
+ this.records.delete(record.fingerprint);
76
+ }
77
+ }
78
+ }
79
+ export function coerceDiagnosticEvent(value) {
80
+ if (!value || typeof value !== "object")
81
+ return undefined;
82
+ const input = value;
83
+ const source = typeof input.source === "string" ? input.source : undefined;
84
+ const severity = isSeverity(input.severity) ? input.severity : undefined;
85
+ const category = isCategory(input.category) ? input.category : "unknown";
86
+ const message = typeof input.message === "string" ? input.message : undefined;
87
+ if (!source || !severity || !message)
88
+ return undefined;
89
+ return {
90
+ source,
91
+ severity,
92
+ category,
93
+ message,
94
+ detail: input.detail,
95
+ fingerprint: typeof input.fingerprint === "string" ? input.fingerprint : undefined,
96
+ context: input.context && typeof input.context === "object" ? input.context : undefined,
97
+ created_at: typeof input.created_at === "string" ? input.created_at : undefined,
98
+ };
99
+ }
100
+ function shouldPrompt(record) {
101
+ if (record.severity === "error")
102
+ return record.occurrence_count >= 3;
103
+ if (record.severity === "warning")
104
+ return record.occurrence_count >= 5 || record.category === "fallback";
105
+ return false;
106
+ }
107
+ function buildFingerprint(event) {
108
+ return `${event.source}:${event.category}:${normalizeDiagnosticMessage(event.message).toLowerCase()}`;
109
+ }
110
+ function isSeverity(value) {
111
+ return value === "debug" || value === "info" || value === "warning" || value === "error";
112
+ }
113
+ function isCategory(value) {
114
+ return (value === "network" ||
115
+ value === "fallback" ||
116
+ value === "persistence" ||
117
+ value === "config" ||
118
+ value === "extension_timeout" ||
119
+ value === "schema" ||
120
+ value === "unknown");
121
+ }
122
+ function maxSeverity(a, b) {
123
+ const rank = { debug: 0, info: 1, warning: 2, error: 3 };
124
+ return rank[b] > rank[a] ? b : a;
125
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * [WHO]: diagnosticsExtension - diagnostic:event listener, /report-issue command, silent auto-upload on agent_end
3
+ * [FROM]: Depends on core/extensions/types, @pencil-agent/tui, ./diagnostic-buffer, ./reporter, ./types
4
+ * [TO]: Auto-loaded by builtin-extensions.ts as a default extension before diagnostic producers
5
+ * [HERE]: extensions/defaults/diagnostics/index.ts - extension-owned diagnostic buffer; background failures auto-upload silently at agent_end, /report-issue stays for explicit user-initiated bundles
6
+ */
7
+ import type { ExtensionAPI } from "../../../core/extensions/types.js";
8
+ export default function diagnosticsExtension(api: ExtensionAPI): Promise<void>;