wispy-cli 1.1.2 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/engine.mjs CHANGED
@@ -23,6 +23,7 @@ import { MemoryManager } from "./memory.mjs";
23
23
  import { SubAgentManager } from "./subagents.mjs";
24
24
  import { PermissionManager } from "./permissions.mjs";
25
25
  import { AuditLog, EVENT_TYPES } from "./audit.mjs";
26
+ import { Harness } from "./harness.mjs";
26
27
 
27
28
  const MAX_TOOL_ROUNDS = 10;
28
29
  const MAX_CONTEXT_CHARS = 40_000;
@@ -38,7 +39,11 @@ export class WispyEngine {
38
39
  this.subagents = new SubAgentManager(this, this.sessions);
39
40
  this.permissions = new PermissionManager(config.permissions ?? {});
40
41
  this.audit = new AuditLog(WISPY_DIR);
42
+ this.harness = new Harness(this.tools, this.permissions, this.audit, config);
41
43
  this._initialized = false;
44
+ this._workMdContent = null;
45
+ this._workMdLoaded = false;
46
+ this._workMdPath = null;
42
47
  this._activeWorkstream = config.workstream
43
48
  ?? process.env.WISPY_WORKSTREAM
44
49
  ?? process.argv.find((a, i) => (process.argv[i-1] === "-w" || process.argv[i-1] === "--workstream"))
@@ -67,9 +72,15 @@ export class WispyEngine {
67
72
  // Register memory tools
68
73
  this._registerMemoryTools();
69
74
 
75
+ // Register work.md tool
76
+ this._registerWorkContextTool();
77
+
70
78
  // Register node tools
71
79
  this._registerNodeTools();
72
80
 
81
+ // Re-wire harness after tools are registered
82
+ this.harness = new Harness(this.tools, this.permissions, this.audit, this.config);
83
+
73
84
  // Initialize MCP
74
85
  if (!opts.skipMcp) {
75
86
  await ensureDefaultMcpConfig(this.mcpManager.configPath);
@@ -289,68 +300,33 @@ export class WispyEngine {
289
300
  }
290
301
 
291
302
  /**
292
- * Execute a tool, handling engine-level tools (spawn_agent, etc.) specially.
303
+ * Execute a tool, routing through the harness for permissions/audit/receipts.
304
+ * Engine-level tools that need conversation context are dispatched via executeToolFn.
293
305
  */
294
306
  async _executeTool(name, args, messages, session, opts) {
295
- // ── Permission check ─────────────────────────────────────────────────────
296
- const permResult = await this.permissions.check(name, args, { session });
297
- const permLevel = permResult.level ?? "auto";
298
-
299
- if (!permResult.allowed) {
300
- const reason = permResult.reason ?? "Permission denied";
301
- this.audit.log({
302
- type: EVENT_TYPES.APPROVAL_DENIED,
303
- sessionId: session?.id,
304
- tool: name,
305
- args,
306
- }).catch(() => {});
307
- return { success: false, error: reason, denied: true };
308
- }
309
-
310
- if (permResult.needsApproval && permResult.approved) {
311
- this.audit.log({
312
- type: EVENT_TYPES.APPROVAL_GRANTED,
313
- sessionId: session?.id,
314
- tool: name,
315
- args,
316
- }).catch(() => {});
317
- }
318
-
319
- // ── Audit: log tool call ──────────────────────────────────────────────────
320
- const callStart = Date.now();
321
- this.audit.log({
322
- type: EVENT_TYPES.TOOL_CALL,
307
+ const engineTools = new Set([
308
+ "spawn_agent", "list_agents", "get_agent_result", "update_plan",
309
+ "pipeline", "spawn_async_agent", "ralph_loop",
310
+ "memory_save", "memory_search", "memory_list", "memory_get",
311
+ "memory_append", "memory_delete",
312
+ "spawn_subagent", "list_subagents", "get_subagent_result",
313
+ "kill_subagent", "steer_subagent",
314
+ "node_list", "node_status", "node_execute",
315
+ "update_work_context",
316
+ ]);
317
+
318
+ const harnessResult = await this.harness.execute(name, args, {
323
319
  sessionId: session?.id,
324
- tool: name,
325
- args,
326
- permissionLevel: permLevel,
327
- }).catch(() => {});
328
-
329
- // ── Execute ───────────────────────────────────────────────────────────────
330
- let result;
331
- try {
332
- result = await this._executeToolInner(name, args, messages, session, opts);
333
- } catch (err) {
334
- this.audit.log({
335
- type: EVENT_TYPES.ERROR,
336
- sessionId: session?.id,
337
- tool: name,
338
- message: err.message,
339
- duration: Date.now() - callStart,
340
- }).catch(() => {});
341
- throw err;
342
- }
343
-
344
- // ── Audit: log tool result ────────────────────────────────────────────────
345
- this.audit.log({
346
- type: EVENT_TYPES.TOOL_RESULT,
347
- sessionId: session?.id,
348
- tool: name,
349
- result: JSON.stringify(result).slice(0, 500),
350
- duration: Date.now() - callStart,
351
- }).catch(() => {});
320
+ channel: opts?.channel,
321
+ dryRun: opts?.dryRun,
322
+ // Engine-level tools use inner dispatch
323
+ executeToolFn: engineTools.has(name)
324
+ ? (n, a) => this._executeToolInner(n, a, messages, session, opts)
325
+ : null,
326
+ });
352
327
 
353
- return result;
328
+ if (opts?.onReceipt) opts.onReceipt(harnessResult.receipt);
329
+ return harnessResult.result;
354
330
  }
355
331
 
356
332
  /**
@@ -365,6 +341,8 @@ export class WispyEngine {
365
341
  return this._toolListAgents();
366
342
  case "get_agent_result":
367
343
  return this._toolGetAgentResult(args);
344
+ case "update_work_context":
345
+ return this._toolUpdateWorkContext(args);
368
346
  case "update_plan":
369
347
  return this._toolUpdatePlan(args);
370
348
  case "pipeline":
@@ -759,6 +737,12 @@ export class WispyEngine {
759
737
  parts.push("## Project Context (WISPY.md)", wispyMd, "");
760
738
  }
761
739
 
740
+ // Load work.md context for active workstream
741
+ const workMd = await this._loadWorkMd();
742
+ if (workMd) {
743
+ parts.push("## Project context from work.md", workMd, "");
744
+ }
745
+
762
746
  // Load memories via MemoryManager
763
747
  try {
764
748
  const memories = await this.memory.getContextForPrompt(lastUserMessage);
@@ -791,6 +775,76 @@ export class WispyEngine {
791
775
  return null;
792
776
  }
793
777
 
778
+ async _loadWorkMd() {
779
+ if (this._workMdLoaded) return this._workMdContent;
780
+ this._workMdLoaded = true;
781
+
782
+ const ws = this._activeWorkstream;
783
+ const { default: osModule } = await import("node:os");
784
+ const homedir = osModule.homedir();
785
+ const candidates = [
786
+ path.resolve("work.md"),
787
+ path.resolve(ws, "work.md"),
788
+ path.join(homedir, ".wispy", "workstreams", ws, "work.md"),
789
+ ];
790
+
791
+ for (const p of candidates) {
792
+ try {
793
+ const content = await readFile(p, "utf8");
794
+ if (content?.trim()) {
795
+ this._workMdContent = content.trim().slice(0, 8000);
796
+ this._workMdPath = p;
797
+ return this._workMdContent;
798
+ }
799
+ } catch { /* not found */ }
800
+ }
801
+ this._workMdContent = null;
802
+ return null;
803
+ }
804
+
805
+ // ── work.md tool ─────────────────────────────────────────────────────────────
806
+
807
+ _registerWorkContextTool() {
808
+ this.tools._definitions.set("update_work_context", {
809
+ name: "update_work_context",
810
+ description: "Update the work.md context file for the current workstream with project notes, decisions, and context.",
811
+ parameters: {
812
+ type: "object",
813
+ properties: {
814
+ content: { type: "string", description: "New content for work.md" },
815
+ append: { type: "boolean", description: "If true, append instead of replace" },
816
+ },
817
+ required: ["content"],
818
+ },
819
+ });
820
+ }
821
+
822
+ async _toolUpdateWorkContext(args) {
823
+ const { mkdir: mkdirFn, writeFile: writeFn, appendFile: appendFn } = await import("node:fs/promises");
824
+ const { default: osModule } = await import("node:os");
825
+ const ws = this._activeWorkstream;
826
+
827
+ let targetPath = this._workMdPath;
828
+ if (!targetPath) {
829
+ targetPath = path.join(osModule.homedir(), ".wispy", "workstreams", ws, "work.md");
830
+ }
831
+
832
+ await mkdirFn(path.dirname(targetPath), { recursive: true });
833
+
834
+ if (args.append) {
835
+ await appendFn(targetPath, "\n" + args.content, "utf8");
836
+ } else {
837
+ await writeFn(targetPath, args.content, "utf8");
838
+ }
839
+
840
+ // Invalidate cache
841
+ this._workMdLoaded = false;
842
+ this._workMdContent = null;
843
+ this._workMdPath = targetPath;
844
+
845
+ return { success: true, message: `work.md updated at ${targetPath}`, path: targetPath };
846
+ }
847
+
794
848
  async _loadMemories() {
795
849
  const types = ["user", "feedback", "project", "references"];
796
850
  const sections = [];