wispy-cli 1.1.2 → 1.2.1
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/bin/wispy.mjs +225 -0
- package/core/deploy.mjs +292 -0
- package/core/engine.mjs +112 -58
- package/core/harness.mjs +531 -0
- package/core/index.mjs +2 -0
- package/lib/channels/index.mjs +229 -4
- package/lib/wispy-tui.mjs +430 -30
- package/package.json +2 -2
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,
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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 = [];
|