pulse-coder-engine 0.0.1-alpha.4 → 0.0.1-alpha.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.
@@ -233,10 +233,410 @@ var builtInSkillsPlugin = {
233
233
  }
234
234
  };
235
235
 
236
+ // src/built-in/plan-mode-plugin/index.ts
237
+ var PLANNING_POLICY = {
238
+ mode: "planning",
239
+ allowedCategories: ["read", "search", "other"],
240
+ disallowedCategories: ["write", "execute"],
241
+ notes: "Planning mode is prompt-constrained only. Disallowed tool attempts are observed and logged, not hard-blocked."
242
+ };
243
+ var EXECUTING_POLICY = {
244
+ mode: "executing",
245
+ allowedCategories: ["read", "search", "write", "execute", "other"],
246
+ disallowedCategories: []
247
+ };
248
+ var EXECUTE_PATTERNS = [
249
+ /开始执行/i,
250
+ /按这个计划做/i,
251
+ /可以改代码了/i,
252
+ /直接实现/i,
253
+ /go\s+ahead/i,
254
+ /proceed/i,
255
+ /implement\s+it/i,
256
+ /start\s+implement/i,
257
+ /start\s+coding/i
258
+ ];
259
+ var NEGATIVE_PATTERNS = [
260
+ /先不(要)?执行/i,
261
+ /先别执行/i,
262
+ /不要执行/i,
263
+ /暂时不要执行/i,
264
+ /先别改代码/i,
265
+ /先不要改代码/i,
266
+ /先不要实现/i,
267
+ /not\s+now/i,
268
+ /hold\s+off/i,
269
+ /do\s+not\s+(start|execute|implement|proceed)/i,
270
+ /don't\s+(start|execute|implement|proceed)/i
271
+ ];
272
+ var PLAN_PATTERNS = [/先计划/i, /先分析/i, /先出方案/i, /plan\s+first/i, /analysis\s+first/i];
273
+ function appendSystemPrompt(base, append) {
274
+ if (!append.trim()) {
275
+ return base ?? { append: "" };
276
+ }
277
+ if (!base) {
278
+ return { append };
279
+ }
280
+ if (typeof base === "string") {
281
+ return `${base}
282
+
283
+ ${append}`;
284
+ }
285
+ if (typeof base === "function") {
286
+ return () => `${base()}
287
+
288
+ ${append}`;
289
+ }
290
+ const currentAppend = base.append.trim();
291
+ return {
292
+ append: currentAppend ? `${currentAppend}
293
+
294
+ ${append}` : append
295
+ };
296
+ }
297
+ var KNOWN_TOOL_META = {
298
+ read: {
299
+ category: "read",
300
+ risk: "low",
301
+ description: "Read file contents from the workspace."
302
+ },
303
+ ls: {
304
+ category: "read",
305
+ risk: "low",
306
+ description: "List files and directories."
307
+ },
308
+ grep: {
309
+ category: "search",
310
+ risk: "low",
311
+ description: "Search text content across files."
312
+ },
313
+ tavily: {
314
+ category: "search",
315
+ risk: "low",
316
+ description: "Search web results from external sources."
317
+ },
318
+ skill: {
319
+ category: "search",
320
+ risk: "low",
321
+ description: "Load procedural guidance from installed skills."
322
+ },
323
+ write: {
324
+ category: "write",
325
+ risk: "high",
326
+ description: "Create or overwrite file content."
327
+ },
328
+ edit: {
329
+ category: "write",
330
+ risk: "high",
331
+ description: "Modify existing files using exact replacements."
332
+ },
333
+ bash: {
334
+ category: "execute",
335
+ risk: "high",
336
+ description: "Execute shell commands."
337
+ },
338
+ clarify: {
339
+ category: "other",
340
+ risk: "low",
341
+ description: "Ask the user a targeted clarification question."
342
+ }
343
+ };
344
+ var BuiltInPlanModeService = class {
345
+ constructor(logger, eventEmitter, initialMode = "executing") {
346
+ this.logger = logger;
347
+ this.eventEmitter = eventEmitter;
348
+ this.mode = initialMode;
349
+ this.emitEvent("mode_entered", { reason: "initialize" });
350
+ }
351
+ mode;
352
+ events = [];
353
+ getMode() {
354
+ return this.mode;
355
+ }
356
+ setMode(mode, reason = "manual") {
357
+ if (this.mode === mode) {
358
+ return;
359
+ }
360
+ this.mode = mode;
361
+ this.emitEvent("mode_entered", { reason });
362
+ }
363
+ detectIntent(input) {
364
+ const text = input.trim();
365
+ if (!text) {
366
+ return "UNCLEAR";
367
+ }
368
+ if (NEGATIVE_PATTERNS.some((pattern) => pattern.test(text))) {
369
+ return "PLAN_ONLY";
370
+ }
371
+ if (EXECUTE_PATTERNS.some((pattern) => pattern.test(text))) {
372
+ return "EXECUTE_NOW";
373
+ }
374
+ if (PLAN_PATTERNS.some((pattern) => pattern.test(text))) {
375
+ return "PLAN_ONLY";
376
+ }
377
+ return "UNCLEAR";
378
+ }
379
+ processContextMessages(messages) {
380
+ const userInput = this.getLatestUserText(messages);
381
+ const modeBefore = this.mode;
382
+ if (!userInput) {
383
+ return {
384
+ modeBefore,
385
+ modeAfter: this.mode,
386
+ switched: false,
387
+ intent: "UNCLEAR",
388
+ userInput: ""
389
+ };
390
+ }
391
+ const intent = this.detectIntent(userInput);
392
+ if (intent === "EXECUTE_NOW") {
393
+ this.emitEvent("execution_intent_detected", { intent, userInput });
394
+ if (this.mode === "planning") {
395
+ this.mode = "executing";
396
+ this.emitEvent("mode_switched_by_intent", {
397
+ from: "planning",
398
+ to: "executing",
399
+ userInput
400
+ });
401
+ this.emitEvent("mode_entered", {
402
+ reason: "intent",
403
+ from: "planning"
404
+ });
405
+ }
406
+ }
407
+ return {
408
+ modeBefore,
409
+ modeAfter: this.mode,
410
+ switched: modeBefore !== this.mode,
411
+ intent,
412
+ userInput
413
+ };
414
+ }
415
+ getModePolicy(mode = this.mode) {
416
+ return mode === "planning" ? PLANNING_POLICY : EXECUTING_POLICY;
417
+ }
418
+ getToolMetadata(toolNames) {
419
+ return Array.from(new Set(toolNames)).sort().map((name) => this.inferToolMeta(name));
420
+ }
421
+ buildPromptAppend(toolNames, transition) {
422
+ const mode = this.mode;
423
+ const policy = this.getModePolicy(mode);
424
+ const toolMeta = this.getToolMetadata(toolNames);
425
+ const shownTools = toolMeta.slice(0, 40);
426
+ const omittedCount = toolMeta.length - shownTools.length;
427
+ const lines = [
428
+ "## Plan Mode Policy (Built-in Plugin)",
429
+ `Current mode: ${mode.toUpperCase()}`,
430
+ `Allowed tool categories: ${policy.allowedCategories.join(", ")}`,
431
+ `Disallowed tool categories: ${policy.disallowedCategories.join(", ") || "none"}`,
432
+ policy.notes ? `Policy notes: ${policy.notes}` : ""
433
+ ].filter(Boolean);
434
+ if (mode === "planning") {
435
+ lines.push(
436
+ "Planning objective: prioritize reading, analysis, and plan generation.",
437
+ "In planning mode, do not intentionally perform write/edit/execute actions.",
438
+ "If implementation is requested but intent is not explicit, ask for execution authorization.",
439
+ "Before each tool call in planning mode, self-check category compliance.",
440
+ "Plan format must include: goals, assumptions, steps, risks, validation approach."
441
+ );
442
+ } else {
443
+ lines.push(
444
+ "Executing objective: follow the agreed plan before broad exploration.",
445
+ "Make targeted edits and keep changes scoped.",
446
+ "Report what changed and how it was validated."
447
+ );
448
+ }
449
+ if (transition?.switched && transition.modeAfter === "executing") {
450
+ lines.push(
451
+ "Mode transition: the latest user message explicitly authorized execution.",
452
+ "In your next reply, acknowledge switching to EXECUTING mode before implementation details."
453
+ );
454
+ }
455
+ lines.push("Tool metadata (prompt-level policy reference):");
456
+ for (const meta of shownTools) {
457
+ lines.push(`- ${meta.name}: category=${meta.category}, risk=${meta.risk}, ${meta.description}`);
458
+ }
459
+ if (omittedCount > 0) {
460
+ lines.push(`- ... ${omittedCount} additional tool(s) omitted for brevity.`);
461
+ }
462
+ return lines.join("\n");
463
+ }
464
+ applyHooks(baseHooks) {
465
+ return {
466
+ onBeforeToolCall: async (name, input) => {
467
+ this.observePotentialPolicyViolation(name, input);
468
+ if (baseHooks?.onBeforeToolCall) {
469
+ const nextInput = await baseHooks.onBeforeToolCall(name, input);
470
+ return nextInput ?? input;
471
+ }
472
+ return input;
473
+ },
474
+ onAfterToolCall: async (name, input, output) => {
475
+ if (baseHooks?.onAfterToolCall) {
476
+ const nextOutput = await baseHooks.onAfterToolCall(name, input, output);
477
+ return nextOutput ?? output;
478
+ }
479
+ return output;
480
+ }
481
+ };
482
+ }
483
+ getEvents(limit = 50) {
484
+ return this.events.slice(-Math.max(0, limit));
485
+ }
486
+ observePotentialPolicyViolation(toolName, input) {
487
+ if (this.mode !== "planning") {
488
+ return;
489
+ }
490
+ const meta = this.inferToolMeta(toolName);
491
+ const policy = this.getModePolicy("planning");
492
+ if (!policy.disallowedCategories.includes(meta.category)) {
493
+ return;
494
+ }
495
+ this.emitEvent("disallowed_tool_attempt_in_planning", {
496
+ toolName,
497
+ category: meta.category,
498
+ risk: meta.risk,
499
+ input
500
+ });
501
+ }
502
+ inferToolMeta(name) {
503
+ const knownMeta = KNOWN_TOOL_META[name];
504
+ if (knownMeta) {
505
+ return {
506
+ name,
507
+ ...knownMeta
508
+ };
509
+ }
510
+ if (name.startsWith("mcp_")) {
511
+ return {
512
+ name,
513
+ category: "execute",
514
+ risk: "medium",
515
+ description: "MCP external tool invocation."
516
+ };
517
+ }
518
+ if (name.endsWith("_agent")) {
519
+ return {
520
+ name,
521
+ category: "execute",
522
+ risk: "high",
523
+ description: "Sub-agent task execution tool."
524
+ };
525
+ }
526
+ if (/read|list|cat/i.test(name)) {
527
+ return {
528
+ name,
529
+ category: "read",
530
+ risk: "low",
531
+ description: "Likely a read-only inspection tool inferred by name."
532
+ };
533
+ }
534
+ if (/search|find|query|grep/i.test(name)) {
535
+ return {
536
+ name,
537
+ category: "search",
538
+ risk: "low",
539
+ description: "Likely a search tool inferred by name."
540
+ };
541
+ }
542
+ if (/write|edit|patch|update|create|delete|remove/i.test(name)) {
543
+ return {
544
+ name,
545
+ category: "write",
546
+ risk: "high",
547
+ description: "Likely a file-modifying tool inferred by name."
548
+ };
549
+ }
550
+ if (/bash|exec|run|shell|deploy|build|test/i.test(name)) {
551
+ return {
552
+ name,
553
+ category: "execute",
554
+ risk: "high",
555
+ description: "Likely a command execution tool inferred by name."
556
+ };
557
+ }
558
+ return {
559
+ name,
560
+ category: "other",
561
+ risk: "low",
562
+ description: "Tool category could not be inferred with high confidence."
563
+ };
564
+ }
565
+ getLatestUserText(messages) {
566
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
567
+ const message = messages[i];
568
+ if (message.role !== "user") {
569
+ continue;
570
+ }
571
+ return this.messageContentToText(message.content);
572
+ }
573
+ return "";
574
+ }
575
+ messageContentToText(content) {
576
+ if (typeof content === "string") {
577
+ return content;
578
+ }
579
+ if (!Array.isArray(content)) {
580
+ return "";
581
+ }
582
+ const textParts = [];
583
+ for (const part of content) {
584
+ if (typeof part === "string") {
585
+ textParts.push(part);
586
+ continue;
587
+ }
588
+ if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
589
+ textParts.push(part.text);
590
+ }
591
+ }
592
+ return textParts.join("\n");
593
+ }
594
+ emitEvent(name, payload) {
595
+ const event = {
596
+ name,
597
+ mode: this.mode,
598
+ timestamp: Date.now(),
599
+ payload
600
+ };
601
+ this.events.push(event);
602
+ if (this.events.length > 500) {
603
+ this.events.shift();
604
+ }
605
+ this.eventEmitter.emit(name, event);
606
+ this.eventEmitter.emit("plan_mode_event", event);
607
+ if (name === "disallowed_tool_attempt_in_planning") {
608
+ this.logger.warn("[PlanMode] Soft violation detected in planning mode", payload);
609
+ return;
610
+ }
611
+ this.logger.info(`[PlanMode] ${name}`, payload);
612
+ }
613
+ };
614
+ var builtInPlanModePlugin = {
615
+ name: "pulse-coder-engine/built-in-plan-mode",
616
+ version: "1.0.0",
617
+ async initialize(context) {
618
+ const service = new BuiltInPlanModeService(context.logger, context.events, "executing");
619
+ context.registerRunHook("plan-mode", ({ context: runContext, tools, systemPrompt, hooks }) => {
620
+ const transition = service.processContextMessages(runContext.messages);
621
+ const append = service.buildPromptAppend(Object.keys(tools), transition);
622
+ const finalSystemPrompt = appendSystemPrompt(systemPrompt, append);
623
+ return {
624
+ systemPrompt: finalSystemPrompt,
625
+ hooks: service.applyHooks(hooks)
626
+ };
627
+ });
628
+ context.registerService("planMode", service);
629
+ context.registerService("planModeService", service);
630
+ context.logger.info("[PlanMode] Built-in plan mode plugin initialized", {
631
+ mode: service.getMode()
632
+ });
633
+ }
634
+ };
635
+
236
636
  // src/built-in/sub-agent-plugin/index.ts
237
637
  import { z as z10 } from "zod";
238
- import { promises as fs } from "fs";
239
- import path2 from "path";
638
+ import { promises as fs2 } from "fs";
639
+ import path3 from "path";
240
640
 
241
641
  // src/ai/index.ts
242
642
  import { generateText, streamText } from "ai";
@@ -268,8 +668,9 @@ var CLARIFICATION_TIMEOUT = Number(process.env.CLARIFICATION_TIMEOUT ?? 3e5);
268
668
  var CLARIFICATION_ENABLED = process.env.CLARIFICATION_ENABLED !== "false";
269
669
 
270
670
  // src/prompt/system.ts
271
- var generateSystemPrompt = () => {
272
- const basePrompt = `
671
+ import fs from "fs";
672
+ import path2 from "path";
673
+ var DEFAULT_PROMPT = `
273
674
  You are Pulse Coder, the best coding agent on the planet.
274
675
 
275
676
  You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
@@ -349,7 +750,7 @@ Use the 'clarify' tool when you genuinely need information from the user to proc
349
750
  - Explain briefly what would change based on the answer
350
751
 
351
752
  Example usage: Call clarify with a question, optional context, and optional default answer. The tool will pause and wait for the user's response.
352
- - For substantial work, summarize clearly; follow final\u2011answer formatting.
753
+ - For substantial work, summarize clearly; follow final-answer formatting.
353
754
  - Skip heavy formatting for simple confirmations.
354
755
  - Don't dump large files you've written; reference paths only.
355
756
  - No "save/copy this file" - User is on the same machine.
@@ -363,19 +764,19 @@ Example usage: Call clarify with a question, optional context, and optional defa
363
764
  ## Final answer structure and style guidelines
364
765
 
365
766
  - Plain text; CLI handles styling. Use structure only when it helps scanability.
366
- - Headers: optional; short Title Case (1-3 words) wrapped in **\u2026**; no blank line before the first bullet; add only if they truly help.
367
- - Bullets: use - ; merge related points; keep to one line when possible; 4\u20136 per list ordered by importance; keep phrasing consistent.
767
+ - Headers: optional; short Title Case (1-3 words) wrapped in **...**; no blank line before the first bullet; add only if they truly help.
768
+ - Bullets: use - ; merge related points; keep to one line when possible; 4-6 per list ordered by importance; keep phrasing consistent.
368
769
  - Monospace: backticks for commands/paths/env vars/code ids and inline examples; use for literal keyword bullets; never combine with **.
369
770
  - Code samples or multi-line snippets should be wrapped in fenced code blocks; include an info string as often as possible.
370
- - Structure: group related bullets; order sections general \u2192 specific \u2192 supporting; for subsections, start with a bolded keyword bullet, then items; match complexity to the task.
371
- - Tone: collaborative, concise, factual; present tense, active voice; self\u2011contained; no "above/below"; parallel wording.
372
- - Don'ts: no nested bullets/hierarchies; no ANSI codes; don't cram unrelated keywords; keep keyword lists short\u2014wrap/reformat if long; avoid naming formatting styles in answers.
373
- - Adaptation: code explanations \u2192 precise, structured with code refs; simple tasks \u2192 lead with outcome; big changes \u2192 logical walkthrough + rationale + next actions; casual one-offs \u2192 plain sentences, no headers/bullets.
771
+ - Structure: group related bullets; order sections general -> specific -> supporting; for subsections, start with a bolded keyword bullet, then items; match complexity to the task.
772
+ - Tone: collaborative, concise, factual; present tense, active voice; self-contained; no "above/below"; parallel wording.
773
+ - Don'ts: no nested bullets/hierarchies; no ANSI codes; don't cram unrelated keywords; keep keyword lists short-wrap/reformat if long; avoid naming formatting styles in answers.
774
+ - Adaptation: code explanations -> precise, structured with code refs; simple tasks -> lead with outcome; big changes -> logical walkthrough + rationale + next actions; casual one-offs -> plain sentences, no headers/bullets.
374
775
  - File References: When referencing files in your response follow the below rules:
375
776
  * Use inline code to make file paths clickable.
376
777
  * Each reference should have a stand alone path. Even if it's the same file.
377
- * Accepted: absolute, workspace\u2011relative, a/ or b/ diff prefixes, or bare filename/suffix.
378
- * Optionally include line/column (1\u2011based): :line[:column] or #Lline[Ccolumn] (column defaults to 1).
778
+ * Accepted: absolute, workspace-relative, a/ or b/ diff prefixes, or bare filename/suffix.
779
+ * Optionally include line/column (1-based): :line[:column] or #Lline[Ccolumn] (column defaults to 1).
379
780
  * Do not use URIs like file://, vscode://, or https://.
380
781
  * Do not provide range of lines
381
782
  * Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\repoprojectmain.rs:12:5
@@ -389,7 +790,24 @@ Here is some useful information about the environment you are running in:
389
790
  <files>
390
791
 
391
792
  </files>`;
392
- return basePrompt;
793
+ var AGENTS_FILE_REGEX = /^agents\.md$/i;
794
+ var loadAgentsPrompt = () => {
795
+ try {
796
+ const cwd = process.cwd();
797
+ const entries = fs.readdirSync(cwd, { withFileTypes: true });
798
+ const target = entries.find((entry) => entry.isFile() && AGENTS_FILE_REGEX.test(entry.name));
799
+ if (!target) {
800
+ return null;
801
+ }
802
+ const filePath = path2.join(cwd, target.name);
803
+ const content = fs.readFileSync(filePath, "utf8").trim();
804
+ return content.length > 0 ? content : null;
805
+ } catch {
806
+ return null;
807
+ }
808
+ };
809
+ var generateSystemPrompt = () => {
810
+ return loadAgentsPrompt() ?? DEFAULT_PROMPT;
393
811
  };
394
812
 
395
813
  // src/ai/index.ts
@@ -864,7 +1282,7 @@ var GrepTool = {
864
1282
  }),
865
1283
  execute: async ({
866
1284
  pattern,
867
- path: path3 = ".",
1285
+ path: path4 = ".",
868
1286
  glob,
869
1287
  type,
870
1288
  outputMode = "files_with_matches",
@@ -899,11 +1317,11 @@ var GrepTool = {
899
1317
  if (type) {
900
1318
  args.push("--type", type);
901
1319
  }
902
- if (path3 && path3 !== ".") {
903
- if (!existsSync5(path3)) {
904
- throw new Error(`Path does not exist: ${path3}`);
1320
+ if (path4 && path4 !== ".") {
1321
+ if (!existsSync5(path4)) {
1322
+ throw new Error(`Path does not exist: ${path4}`);
905
1323
  }
906
- args.push(path3);
1324
+ args.push(path4);
907
1325
  }
908
1326
  let command = args.map((arg) => {
909
1327
  if (arg.includes(" ") || arg.includes("$") || arg.includes("*")) {
@@ -958,8 +1376,8 @@ var LsTool = {
958
1376
  inputSchema: z6.object({
959
1377
  path: z6.string().optional().describe("The path to list files from (defaults to current directory)")
960
1378
  }),
961
- execute: async ({ path: path3 = "." }) => {
962
- const files = readdirSync(path3);
1379
+ execute: async ({ path: path4 = "." }) => {
1380
+ const files = readdirSync(path4);
963
1381
  return { files };
964
1382
  }
965
1383
  };
@@ -1130,8 +1548,8 @@ var ConfigLoader = class {
1130
1548
  const fileInfos = [];
1131
1549
  for (let configDir of configDirs) {
1132
1550
  try {
1133
- await fs.access(configDir);
1134
- const files = await fs.readdir(configDir);
1551
+ await fs2.access(configDir);
1552
+ const files = await fs2.readdir(configDir);
1135
1553
  fileInfos.push({ files, configDir });
1136
1554
  } catch {
1137
1555
  continue;
@@ -1148,7 +1566,7 @@ var ConfigLoader = class {
1148
1566
  const files = fileInfo.files;
1149
1567
  for (const file of files) {
1150
1568
  if (file.endsWith(".md")) {
1151
- const config = await this.parseConfig(path2.join(fileInfo.configDir, file));
1569
+ const config = await this.parseConfig(path3.join(fileInfo.configDir, file));
1152
1570
  if (config) configs.push(config);
1153
1571
  }
1154
1572
  }
@@ -1160,7 +1578,7 @@ var ConfigLoader = class {
1160
1578
  }
1161
1579
  async parseConfig(filePath) {
1162
1580
  try {
1163
- const content = await fs.readFile(filePath, "utf-8");
1581
+ const content = await fs2.readFile(filePath, "utf-8");
1164
1582
  const lines = content.split("\n");
1165
1583
  let name = "";
1166
1584
  let description = "";
@@ -1189,7 +1607,7 @@ var ConfigLoader = class {
1189
1607
  }
1190
1608
  }
1191
1609
  if (!name) {
1192
- name = path2.basename(filePath, ".md");
1610
+ name = path3.basename(filePath, ".md");
1193
1611
  }
1194
1612
  return {
1195
1613
  name: name.trim(),
@@ -1272,13 +1690,16 @@ var SubAgentPlugin = class {
1272
1690
  var builtInPlugins = [
1273
1691
  builtInMCPPlugin,
1274
1692
  builtInSkillsPlugin,
1693
+ builtInPlanModePlugin,
1275
1694
  new SubAgentPlugin()
1276
1695
  ];
1277
1696
  var built_in_default = builtInPlugins;
1278
1697
  export {
1698
+ BuiltInPlanModeService,
1279
1699
  BuiltInSkillRegistry,
1280
1700
  SubAgentPlugin,
1281
1701
  builtInMCPPlugin,
1702
+ builtInPlanModePlugin,
1282
1703
  builtInPlugins,
1283
1704
  builtInSkillsPlugin,
1284
1705
  built_in_default as default