create-walle 0.9.11 → 0.9.13
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/README.md +3 -3
- package/package.json +2 -2
- package/template/bin/dev.sh +7 -1
- package/template/bin/setup.js +53 -9
- package/template/bin/sync-images.js +53 -0
- package/template/builder-journal.md +17 -0
- package/template/claude-task-manager/api-prompts.js +98 -13
- package/template/claude-task-manager/api-reviews.js +82 -5
- package/template/claude-task-manager/db.js +32 -5
- package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
- package/template/claude-task-manager/lib/session-capture.js +421 -0
- package/template/claude-task-manager/lib/session-history.js +135 -15
- package/template/claude-task-manager/lib/session-jobs.js +10 -5
- package/template/claude-task-manager/lib/session-stream.js +87 -19
- package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
- package/template/claude-task-manager/lib/walle-session-context.js +61 -0
- package/template/claude-task-manager/lib/walle-transcript.js +176 -0
- package/template/claude-task-manager/public/css/setup.css +35 -8
- package/template/claude-task-manager/public/css/walle-session.css +56 -0
- package/template/claude-task-manager/public/css/walle.css +120 -0
- package/template/claude-task-manager/public/index.html +814 -181
- package/template/claude-task-manager/public/js/message-renderer.js +148 -19
- package/template/claude-task-manager/public/js/reviews.js +120 -62
- package/template/claude-task-manager/public/js/setup.js +75 -31
- package/template/claude-task-manager/public/js/stream-view.js +115 -55
- package/template/claude-task-manager/public/js/walle-session.js +84 -2
- package/template/claude-task-manager/public/js/walle.js +308 -54
- package/template/claude-task-manager/server.js +1092 -146
- package/template/claude-task-manager/session-integrity.js +181 -54
- package/template/claude-task-manager/session-utils.js +123 -41
- package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
- package/template/package.json +1 -1
- package/template/wall-e/adapters/ctm.js +39 -18
- package/template/wall-e/agent-runners/contract.js +17 -0
- package/template/wall-e/agent-runners/index.js +22 -0
- package/template/wall-e/agent-runtime/harness.js +212 -0
- package/template/wall-e/agent-runtime/index.js +8 -0
- package/template/wall-e/agent-runtime/registry.js +67 -0
- package/template/wall-e/agent-runtime/session-store.js +179 -0
- package/template/wall-e/agent-runtime/spawn.js +208 -0
- package/template/wall-e/api-walle.js +174 -7
- package/template/wall-e/brain.js +266 -28
- package/template/wall-e/channels/policy.js +88 -0
- package/template/wall-e/channels/registry.js +15 -1
- package/template/wall-e/channels/reply-dispatcher.js +70 -0
- package/template/wall-e/channels/session-bindings.js +51 -0
- package/template/wall-e/chat/code-review-context.js +29 -0
- package/template/wall-e/chat.js +188 -42
- package/template/wall-e/coding/acp-adapter.js +188 -0
- package/template/wall-e/coding/agent-catalog.js +129 -0
- package/template/wall-e/coding/compaction-service.js +247 -0
- package/template/wall-e/coding/execution-trace.js +3 -0
- package/template/wall-e/coding/instruction-service.js +224 -0
- package/template/wall-e/coding/model-message.js +67 -0
- package/template/wall-e/coding/permission-rules-store.js +111 -0
- package/template/wall-e/coding/permission-service.js +266 -0
- package/template/wall-e/coding/prompt-bundle.js +67 -0
- package/template/wall-e/coding/prompt-runtime.js +243 -0
- package/template/wall-e/coding/provider-transform.js +188 -0
- package/template/wall-e/coding/runtime-mode.js +132 -0
- package/template/wall-e/coding/snapshot-service.js +155 -0
- package/template/wall-e/coding/stream-processor.js +268 -0
- package/template/wall-e/coding/task-tool.js +255 -0
- package/template/wall-e/coding/tool-registry.js +361 -0
- package/template/wall-e/coding/transcript-writer.js +143 -0
- package/template/wall-e/coding/workspace-replay.js +324 -0
- package/template/wall-e/coding-context.js +4 -22
- package/template/wall-e/coding-orchestrator.js +307 -18
- package/template/wall-e/coding-prompts.js +44 -3
- package/template/wall-e/context/context-builder.js +43 -1
- package/template/wall-e/context/topic-matcher.js +1 -1
- package/template/wall-e/eval/agent-runner.js +59 -13
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
- package/template/wall-e/eval/benchmarks.js +100 -16
- package/template/wall-e/eval/eval-orchestrator.js +218 -8
- package/template/wall-e/eval/harvester.js +62 -5
- package/template/wall-e/eval/head-to-head.js +23 -2
- package/template/wall-e/eval/humaneval-adapter.js +30 -5
- package/template/wall-e/eval/livecodebench-adapter.js +29 -5
- package/template/wall-e/eval/manifest.js +186 -0
- package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
- package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
- package/template/wall-e/eval/session-transcripts.js +57 -4
- package/template/wall-e/eval/swebench-adapter.js +109 -3
- package/template/wall-e/evaluation/agent-router.js +53 -1
- package/template/wall-e/evaluation/coding-quorum.js +48 -1
- package/template/wall-e/evaluation/router.js +4 -2
- package/template/wall-e/evaluation/tier-selector.js +11 -1
- package/template/wall-e/extraction/contradiction.js +2 -2
- package/template/wall-e/extraction/indexer.js +2 -1
- package/template/wall-e/extraction/knowledge-extractor.js +2 -2
- package/template/wall-e/hooks/cli.js +92 -0
- package/template/wall-e/hooks/discovery.js +119 -0
- package/template/wall-e/hooks/index.js +7 -0
- package/template/wall-e/hooks/manifest.js +55 -0
- package/template/wall-e/hooks/runtime.js +84 -0
- package/template/wall-e/hooks/session-memory.js +225 -0
- package/template/wall-e/http/auth.js +6 -2
- package/template/wall-e/http/chat-api.js +54 -8
- package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
- package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
- package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
- package/template/wall-e/listening/calendar.js +3 -1
- package/template/wall-e/llm/client.js +64 -10
- package/template/wall-e/llm/google.js +39 -5
- package/template/wall-e/llm/ollama.js +1 -1
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/provider-availability.js +10 -0
- package/template/wall-e/llm/provider-error.js +269 -0
- package/template/wall-e/llm/tool-adapter.js +48 -12
- package/template/wall-e/loops/boot.js +2 -1
- package/template/wall-e/loops/initiative.js +2 -2
- package/template/wall-e/loops/tasks.js +8 -47
- package/template/wall-e/loops/workspace-prompts.js +20 -0
- package/template/wall-e/mcp-server.js +442 -1
- package/template/wall-e/memory/session-ingest-service.js +159 -0
- package/template/wall-e/memory/source-indexer.js +289 -0
- package/template/wall-e/plugins/discovery.js +83 -0
- package/template/wall-e/plugins/manifest-loader.js +50 -10
- package/template/wall-e/plugins/manifest-schema.js +69 -0
- package/template/wall-e/plugins/model-catalog.js +55 -0
- package/template/wall-e/prompts/coding/base.txt +2 -0
- package/template/wall-e/prompts/coding/deepseek.txt +1 -0
- package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
- package/template/wall-e/prompts/coding/plan.txt +1 -0
- package/template/wall-e/runtime/execution-trace.js +220 -0
- package/template/wall-e/security/audit.js +266 -0
- package/template/wall-e/security/ssrf.js +236 -0
- package/template/wall-e/session-files.js +303 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
- package/template/wall-e/skills/internal-skill-registry.js +2 -2
- package/template/wall-e/skills/script-skill-runner.js +143 -0
- package/template/wall-e/skills/skill-executor.js +5 -6
- package/template/wall-e/skills/skill-fallback.js +3 -1
- package/template/wall-e/skills/skill-harness-registry.js +7 -8
- package/template/wall-e/skills/skill-planner.js +52 -4
- package/template/wall-e/skills/slack-ingest.js +11 -3
- package/template/wall-e/sources/base.js +90 -0
- package/template/wall-e/sources/builtin.js +33 -0
- package/template/wall-e/sources/claude-code-jsonl.js +78 -0
- package/template/wall-e/sources/codex-jsonl.js +125 -0
- package/template/wall-e/sources/coding-session-utils.js +117 -0
- package/template/wall-e/sources/contract-suite.js +59 -0
- package/template/wall-e/sources/gemini-jsonl.js +85 -0
- package/template/wall-e/sources/index.js +9 -0
- package/template/wall-e/sources/jsonl-utils.js +181 -0
- package/template/wall-e/sources/record-types.js +252 -0
- package/template/wall-e/sources/registry.js +92 -0
- package/template/wall-e/sources/transforms.js +100 -0
- package/template/wall-e/sources/walle-jsonl.js +108 -0
- package/template/wall-e/tools/coding-middleware.js +31 -1
- package/template/wall-e/tools/file-tracker.js +25 -1
- package/template/wall-e/tools/local-tools.js +75 -47
- package/template/wall-e/tools/session-sharing.js +68 -1
- package/template/wall-e/tools/shell-analyzer.js +1 -1
- package/template/wall-e/tools/shell-policy.js +47 -0
- package/template/wall-e/tools/snapshot.js +42 -0
- package/template/wall-e/training/harvester.js +62 -5
- package/template/wall-e/utils/repair.js +253 -1
- package/template/website/index.html +3 -3
- package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +0 -18
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const {
|
|
4
|
+
normalizeHarness,
|
|
5
|
+
scoreHarness,
|
|
6
|
+
selectAgentHarness,
|
|
7
|
+
} = require('../agent-runtime/harness');
|
|
8
|
+
|
|
3
9
|
function normalizeCandidate(candidate = {}) {
|
|
4
10
|
const runnerId = candidate.runnerId || candidate.id;
|
|
5
11
|
return {
|
|
@@ -34,6 +40,14 @@ function scoreAgentRunner(stats = {}, candidate = {}) {
|
|
|
34
40
|
));
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
function applyQuorumRiskScore(baseScore, stats = {}) {
|
|
44
|
+
const passRate = numberOr(stats.pass_rate ?? stats.review_pass_rate, null);
|
|
45
|
+
if (passRate == null) return baseScore;
|
|
46
|
+
if (passRate < 0.4) return Math.max(0, baseScore - 0.20);
|
|
47
|
+
if (passRate >= 0.85) return Math.min(1, baseScore + 0.05);
|
|
48
|
+
return baseScore;
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
function selectBestAgentRunners(taskType, candidates = [], brain = null, opts = {}) {
|
|
38
52
|
const count = Math.max(1, opts.count || 1);
|
|
39
53
|
const days = opts.days || 30;
|
|
@@ -48,7 +62,7 @@ function selectBestAgentRunners(taskType, candidates = [], brain = null, opts =
|
|
|
48
62
|
} catch {
|
|
49
63
|
stats = null;
|
|
50
64
|
}
|
|
51
|
-
const score = scoreAgentRunner(stats || {}, candidate);
|
|
65
|
+
const score = applyQuorumRiskScore(scoreAgentRunner(stats || {}, candidate), stats || {});
|
|
52
66
|
return { candidate, stats, score, index };
|
|
53
67
|
});
|
|
54
68
|
|
|
@@ -68,9 +82,47 @@ function selectBestAgentRunner(taskType, candidates = [], brain = null, opts = {
|
|
|
68
82
|
return selectBestAgentRunners(taskType, candidates, brain, { ...opts, count: 1 })[0] || null;
|
|
69
83
|
}
|
|
70
84
|
|
|
85
|
+
function selectBestAgentHarness(request = {}, candidates = [], brain = null, opts = {}) {
|
|
86
|
+
const taskType = request.taskType || opts.taskType || 'coding:general';
|
|
87
|
+
const normalized = candidates.map((candidate) => {
|
|
88
|
+
const harness = normalizeHarness(candidate);
|
|
89
|
+
let stats = null;
|
|
90
|
+
try {
|
|
91
|
+
stats = brain?.getAgentRunnerScorecard
|
|
92
|
+
? brain.getAgentRunnerScorecard(harness.runnerId, taskType, opts.days || 30)
|
|
93
|
+
: null;
|
|
94
|
+
} catch {
|
|
95
|
+
stats = null;
|
|
96
|
+
}
|
|
97
|
+
const qualityScore = scoreAgentRunner(stats || {}, harness);
|
|
98
|
+
return {
|
|
99
|
+
...harness,
|
|
100
|
+
qualityScore,
|
|
101
|
+
stats,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
const selection = selectAgentHarness(request, normalized, {
|
|
105
|
+
...opts,
|
|
106
|
+
qualityScore: undefined,
|
|
107
|
+
providerAvailability: opts.providerAvailability,
|
|
108
|
+
});
|
|
109
|
+
if (!selection.selected) return null;
|
|
110
|
+
return {
|
|
111
|
+
...selection.selected,
|
|
112
|
+
score: scoreHarness(selection.selected, request, {
|
|
113
|
+
...opts,
|
|
114
|
+
providerAvailability: opts.providerAvailability,
|
|
115
|
+
qualityScore: selection.selected.qualityScore,
|
|
116
|
+
}),
|
|
117
|
+
selection,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
71
121
|
module.exports = {
|
|
122
|
+
applyQuorumRiskScore,
|
|
72
123
|
normalizeCandidate,
|
|
73
124
|
scoreAgentRunner,
|
|
74
125
|
selectBestAgentRunner,
|
|
75
126
|
selectBestAgentRunners,
|
|
127
|
+
selectBestAgentHarness,
|
|
76
128
|
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { randomUUID } = require('node:crypto');
|
|
4
4
|
const codingReview = require('../coding-review');
|
|
5
|
+
const { recordReview } = require('../runtime/execution-trace');
|
|
5
6
|
|
|
6
7
|
const SCORE_KEYS = ['correctness', 'completeness', 'tests', 'maintainability', 'security'];
|
|
7
8
|
const VERDICTS = new Set(['pass', 'revise', 'block']);
|
|
@@ -252,6 +253,42 @@ function aggregateCodingReviews(reviews = []) {
|
|
|
252
253
|
};
|
|
253
254
|
}
|
|
254
255
|
|
|
256
|
+
function qualityFromCodingAggregate(aggregate = {}) {
|
|
257
|
+
const scoreValues = Object.values(aggregate.scores || {}).filter(Number.isFinite);
|
|
258
|
+
const scoreAverage = scoreValues.length > 0
|
|
259
|
+
? scoreValues.reduce((sum, value) => sum + value, 0) / scoreValues.length / 5
|
|
260
|
+
: 0.7;
|
|
261
|
+
const verdictPenalty = aggregate.verdict === 'block' ? 0.55 : aggregate.verdict === 'revise' ? 0.20 : 0;
|
|
262
|
+
const confidence = Number.isFinite(aggregate.confidence) ? aggregate.confidence : 0.7;
|
|
263
|
+
const blended = scoreAverage * 0.75 + confidence * 0.25 - verdictPenalty;
|
|
264
|
+
return Math.max(0, Math.min(1, Math.round(blended * 1000) / 1000));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function recordCodingQuorumLearning(result = {}, options = {}, brain = null) {
|
|
268
|
+
if (!brain?.insertAgentRunnerEvaluation) return null;
|
|
269
|
+
const builder = options.builder || {};
|
|
270
|
+
const runnerId = builder.runnerId || options.workerRunnerId || options.runnerId;
|
|
271
|
+
if (!runnerId) return null;
|
|
272
|
+
const trace = options.trace || null;
|
|
273
|
+
const changedFiles = Array.isArray(options.changedFiles) ? options.changedFiles : [];
|
|
274
|
+
const aggregate = result.aggregate || {};
|
|
275
|
+
return brain.insertAgentRunnerEvaluation({
|
|
276
|
+
runnerId,
|
|
277
|
+
providerType: builder.providerType || options.workerProvider || null,
|
|
278
|
+
modelId: builder.modelId || options.workerModel || null,
|
|
279
|
+
taskType: options.taskType || 'coding:subtask',
|
|
280
|
+
phase: options.phase || 'coding:review-gated',
|
|
281
|
+
qualityScore: qualityFromCodingAggregate(aggregate),
|
|
282
|
+
latencyMs: options.latencyMs ?? trace?.latencyMs ?? null,
|
|
283
|
+
testsPassed: options.testsPassed ?? (result.requiredTests?.length === 0 ? true : null),
|
|
284
|
+
reviewVerdict: result.verdict || aggregate.verdict || null,
|
|
285
|
+
retries: Math.max(0, (trace?.attempts?.length || 1) - 1),
|
|
286
|
+
filesChanged: changedFiles.length,
|
|
287
|
+
costEstimate: options.costEstimate ?? null,
|
|
288
|
+
wasSelected: true,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
255
292
|
function selectCodingReviewers(options = {}) {
|
|
256
293
|
const quorumSize = Math.max(1, options.quorumSize || options.reviewQuorumSize || 2);
|
|
257
294
|
const reviewers = Array.isArray(options.reviewers) ? options.reviewers.filter(Boolean) : [];
|
|
@@ -290,7 +327,7 @@ async function runCodingQuorum(options = {}, deps = {}) {
|
|
|
290
327
|
});
|
|
291
328
|
const aggregate = aggregateCodingReviews(reviews);
|
|
292
329
|
|
|
293
|
-
|
|
330
|
+
const result = {
|
|
294
331
|
quorumId,
|
|
295
332
|
prompt,
|
|
296
333
|
reviewers,
|
|
@@ -314,12 +351,22 @@ async function runCodingQuorum(options = {}, deps = {}) {
|
|
|
314
351
|
requiredTestCount: aggregate.requiredTests.length,
|
|
315
352
|
},
|
|
316
353
|
};
|
|
354
|
+
recordReview(options.trace, result);
|
|
355
|
+
const persistedEvaluationId = recordCodingQuorumLearning(result, options, options.brain || deps.brain);
|
|
356
|
+
if (persistedEvaluationId) {
|
|
357
|
+
result.learningSignals.persistedEvaluationId = persistedEvaluationId;
|
|
358
|
+
result.learningSignals.builderRunnerId = options.builder?.runnerId || options.workerRunnerId || options.runnerId || null;
|
|
359
|
+
result.learningSignals.qualityScore = qualityFromCodingAggregate(aggregate);
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
317
362
|
}
|
|
318
363
|
|
|
319
364
|
module.exports = {
|
|
320
365
|
buildCodingReviewPrompt,
|
|
321
366
|
parseCodingReview,
|
|
322
367
|
aggregateCodingReviews,
|
|
368
|
+
qualityFromCodingAggregate,
|
|
369
|
+
recordCodingQuorumLearning,
|
|
323
370
|
selectCodingReviewers,
|
|
324
371
|
runCodingQuorum,
|
|
325
372
|
};
|
|
@@ -251,7 +251,9 @@ async function routeRequest(options, deps = {}) {
|
|
|
251
251
|
if (brain && brain.getBenchmarkLeaderboard) {
|
|
252
252
|
const benchLb = brain.getBenchmarkLeaderboard({ days: 30 });
|
|
253
253
|
for (const entry of benchLb) {
|
|
254
|
-
|
|
254
|
+
const trustedEvals = Number(entry.trusted_evals) || 0;
|
|
255
|
+
const qualityScore = entry.trusted_avg_score != null ? Number(entry.trusted_avg_score) : null;
|
|
256
|
+
if (qualityScore != null && qualityScore >= 0.7 && trustedEvals >= 10) {
|
|
255
257
|
const modelId = entry.model;
|
|
256
258
|
if (candidates.some(c => c.modelId === modelId)) continue;
|
|
257
259
|
candidates.push({
|
|
@@ -261,7 +263,7 @@ async function routeRequest(options, deps = {}) {
|
|
|
261
263
|
modelId,
|
|
262
264
|
speed_tier: 2,
|
|
263
265
|
cost_per_1m_input: 0,
|
|
264
|
-
_benchScore: entry,
|
|
266
|
+
_benchScore: { ...entry, quality_score: qualityScore },
|
|
265
267
|
});
|
|
266
268
|
}
|
|
267
269
|
}
|
|
@@ -51,7 +51,7 @@ function isCritical(complexityTier) {
|
|
|
51
51
|
function selectModelForTier(modelTier, providerType, opts = {}) {
|
|
52
52
|
// 1. Check env var override
|
|
53
53
|
const envVar = TIER_ENV_VARS[modelTier];
|
|
54
|
-
if (envVar && process.env[envVar]) {
|
|
54
|
+
if (envVar && process.env[envVar] && _modelMatchesProvider(process.env[envVar], providerType)) {
|
|
55
55
|
return process.env[envVar];
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -75,6 +75,16 @@ function selectModelForTier(modelTier, providerType, opts = {}) {
|
|
|
75
75
|
return DEFAULT_TIER_MODELS.anthropic[modelTier];
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function _modelMatchesProvider(model, providerType) {
|
|
79
|
+
try {
|
|
80
|
+
const registry = require('../llm/registry');
|
|
81
|
+
registry.ensureBootstrapped();
|
|
82
|
+
return registry.modelMatchesProvider(model, providerType);
|
|
83
|
+
} catch {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
/**
|
|
79
89
|
* Select the best worker model given complexity assessment and available providers.
|
|
80
90
|
* @param {object} complexityResult - Output from assessComplexity() with .tier
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const brain = require('../brain.js');
|
|
4
|
-
const { getDefaultClient } = require('../llm/client');
|
|
4
|
+
const { getDefaultClient, getDefaultModel, resolveCompatibleModel } = require('../llm/client');
|
|
5
5
|
|
|
6
6
|
const MAX_CONTENT_LENGTH = 2000;
|
|
7
7
|
|
|
@@ -197,8 +197,8 @@ async function detectContradictions(newEntries, ownerName, opts = {}) {
|
|
|
197
197
|
contradictions = opts.detectFn(existing, entries, ownerName);
|
|
198
198
|
} else {
|
|
199
199
|
// Call Claude API
|
|
200
|
-
const model = opts.model || process.env.WALLE_MODEL || 'claude-haiku-4-5-20251001';
|
|
201
200
|
const provider = opts.client || getDefaultClient();
|
|
201
|
+
const model = resolveCompatibleModel(opts.model || process.env.WALLE_MODEL || getDefaultModel(), provider.type);
|
|
202
202
|
const prompt = buildContradictionPrompt(existing, entries, ownerName);
|
|
203
203
|
|
|
204
204
|
const controller = new AbortController();
|
|
@@ -138,8 +138,9 @@ RULES:
|
|
|
138
138
|
- quotes: 2-5 entries. EXACT verbatim from content (15-150 chars each).
|
|
139
139
|
- Output valid JSON only. No code fences. No commentary.`;
|
|
140
140
|
|
|
141
|
+
const { getDefaultModel, resolveCompatibleModel } = require('../llm/client');
|
|
141
142
|
const response = await opts.client.chat({
|
|
142
|
-
model: opts.model ||
|
|
143
|
+
model: resolveCompatibleModel(opts.model || process.env.WALLE_MODEL || getDefaultModel(), opts.client.type),
|
|
143
144
|
maxTokens: 1024,
|
|
144
145
|
messages: [{ role: 'user', content: prompt }],
|
|
145
146
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { getDefaultClient } = require('../llm/client');
|
|
3
|
+
const { getDefaultClient, getDefaultModel, resolveCompatibleModel } = require('../llm/client');
|
|
4
4
|
const { buildClientOptsFromEnv } = require('../llm/anthropic');
|
|
5
5
|
|
|
6
6
|
const VALID_CATEGORIES = ['technical', 'preference', 'relationship', 'work', 'personal', 'world'];
|
|
@@ -131,8 +131,8 @@ async function extractKnowledge(memories, ownerName, opts = {}) {
|
|
|
131
131
|
return empty;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
const model = opts.model || process.env.WALLE_MODEL || 'claude-haiku-4-5-20251001';
|
|
135
134
|
const provider = opts.client || getDefaultClient();
|
|
135
|
+
const model = resolveCompatibleModel(opts.model || process.env.WALLE_MODEL || getDefaultModel(), provider.type);
|
|
136
136
|
|
|
137
137
|
const prompt = buildExtractionPrompt(memories, ownerName);
|
|
138
138
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
handlePrecompact,
|
|
6
|
+
handleSessionStart,
|
|
7
|
+
handleStop,
|
|
8
|
+
runLockedIngest,
|
|
9
|
+
} = require('./session-memory');
|
|
10
|
+
|
|
11
|
+
async function main(argv = process.argv.slice(2), stdin = process.stdin) {
|
|
12
|
+
const command = argv[0] || '';
|
|
13
|
+
const opts = parseArgs(argv.slice(1));
|
|
14
|
+
|
|
15
|
+
if (command === 'ingest-file') {
|
|
16
|
+
const result = await runLockedIngest({
|
|
17
|
+
adapterId: opts.adapter,
|
|
18
|
+
uri: opts.file,
|
|
19
|
+
sourceFile: opts.file,
|
|
20
|
+
sourceId: opts.sourceId || undefined,
|
|
21
|
+
cwd: opts.cwd || '',
|
|
22
|
+
});
|
|
23
|
+
writeJson(result);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const data = await readStdinJson(stdin);
|
|
28
|
+
const harness = opts.harness || 'walle';
|
|
29
|
+
let result;
|
|
30
|
+
if (command === 'session-start') {
|
|
31
|
+
result = await handleSessionStart(data, { harness });
|
|
32
|
+
} else if (command === 'stop') {
|
|
33
|
+
result = await handleStop(data, { harness, background: opts.background !== 'false' });
|
|
34
|
+
} else if (command === 'precompact') {
|
|
35
|
+
result = await handlePrecompact(data, { harness });
|
|
36
|
+
} else {
|
|
37
|
+
throw new Error(`Unknown hook command: ${command}`);
|
|
38
|
+
}
|
|
39
|
+
writeJson(result);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseArgs(argv) {
|
|
44
|
+
const opts = {};
|
|
45
|
+
for (let i = 0; i < argv.length; i++) {
|
|
46
|
+
const arg = argv[i];
|
|
47
|
+
if (!arg.startsWith('--')) continue;
|
|
48
|
+
const key = arg.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
49
|
+
const next = argv[i + 1];
|
|
50
|
+
if (!next || next.startsWith('--')) {
|
|
51
|
+
opts[key] = 'true';
|
|
52
|
+
} else {
|
|
53
|
+
opts[key] = next;
|
|
54
|
+
i += 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return opts;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readStdinJson(stdin) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const chunks = [];
|
|
63
|
+
stdin.on('data', (chunk) => chunks.push(chunk));
|
|
64
|
+
stdin.on('error', reject);
|
|
65
|
+
stdin.on('end', () => {
|
|
66
|
+
const text = Buffer.concat(chunks).toString('utf8').trim();
|
|
67
|
+
if (!text) return resolve({});
|
|
68
|
+
try {
|
|
69
|
+
resolve(JSON.parse(text));
|
|
70
|
+
} catch (err) {
|
|
71
|
+
reject(new Error(`Invalid hook JSON: ${err.message}`));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeJson(result) {
|
|
78
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (require.main === module) {
|
|
82
|
+
main().catch((err) => {
|
|
83
|
+
process.stderr.write(`[wall-e-hook] ${err.message}\n`);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
main,
|
|
90
|
+
parseArgs,
|
|
91
|
+
readStdinJson,
|
|
92
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const {
|
|
6
|
+
HOOK_MANIFEST,
|
|
7
|
+
MAX_HOOK_MANIFEST_BYTES,
|
|
8
|
+
validateHookManifest,
|
|
9
|
+
} = require('./manifest');
|
|
10
|
+
|
|
11
|
+
function discoverHookPackages({ roots = [], enabled = null } = {}) {
|
|
12
|
+
const packages = [];
|
|
13
|
+
const diagnostics = [];
|
|
14
|
+
const enabledSet = enabled ? new Set(enabled) : null;
|
|
15
|
+
|
|
16
|
+
for (const root of roots || []) {
|
|
17
|
+
const rootResult = safeRealpath(root);
|
|
18
|
+
if (!rootResult.ok) {
|
|
19
|
+
diagnostics.push({ level: 'warn', root, reason: rootResult.reason });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const rootReal = rootResult.realpath;
|
|
23
|
+
let entries = [];
|
|
24
|
+
try {
|
|
25
|
+
entries = fs.readdirSync(rootReal, { withFileTypes: true });
|
|
26
|
+
} catch (err) {
|
|
27
|
+
diagnostics.push({ level: 'warn', root: rootReal, reason: err.message });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const candidateDirs = [
|
|
31
|
+
{ name: '.', dir: rootReal },
|
|
32
|
+
...entries.filter(entry => entry.isDirectory()).map(entry => ({ name: entry.name, dir: path.join(rootReal, entry.name) })),
|
|
33
|
+
];
|
|
34
|
+
for (const candidate of candidateDirs) {
|
|
35
|
+
const manifestPath = path.join(candidate.dir, HOOK_MANIFEST);
|
|
36
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
37
|
+
const loaded = loadHookManifest(manifestPath, { rootReal, packageDir: candidate.dir });
|
|
38
|
+
diagnostics.push(...loaded.diagnostics);
|
|
39
|
+
if (!loaded.ok) continue;
|
|
40
|
+
if (enabledSet && !enabledSet.has(loaded.package.manifest.id)) {
|
|
41
|
+
diagnostics.push({ level: 'info', id: loaded.package.manifest.id, reason: 'not enabled' });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
packages.push(loaded.package);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { packages, diagnostics };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadHookManifest(manifestPath, { rootReal, packageDir }) {
|
|
51
|
+
const diagnostics = [];
|
|
52
|
+
let manifestReal;
|
|
53
|
+
let packageReal;
|
|
54
|
+
try {
|
|
55
|
+
manifestReal = fs.realpathSync(manifestPath);
|
|
56
|
+
packageReal = fs.realpathSync(packageDir);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return { ok: false, diagnostics: [{ level: 'warn', path: manifestPath, reason: err.message }] };
|
|
59
|
+
}
|
|
60
|
+
if (!isInside(rootReal, manifestReal) || !isInside(rootReal, packageReal)) {
|
|
61
|
+
return { ok: false, diagnostics: [{ level: 'error', path: manifestPath, reason: 'hook package escapes root' }] };
|
|
62
|
+
}
|
|
63
|
+
const stat = fs.statSync(manifestReal);
|
|
64
|
+
if (stat.size > MAX_HOOK_MANIFEST_BYTES) {
|
|
65
|
+
return { ok: false, diagnostics: [{ level: 'error', path: manifestPath, reason: 'manifest too large' }] };
|
|
66
|
+
}
|
|
67
|
+
if ((fs.statSync(packageReal).mode & 0o002) !== 0) {
|
|
68
|
+
return { ok: false, diagnostics: [{ level: 'error', path: packageReal, reason: 'hook package is world-writable' }] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let raw;
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
raw = fs.readFileSync(manifestReal, 'utf8');
|
|
75
|
+
parsed = JSON.parse(raw);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return { ok: false, diagnostics: [{ level: 'error', path: manifestPath, reason: err.message }] };
|
|
78
|
+
}
|
|
79
|
+
const validation = validateHookManifest(parsed, { packageDir: packageReal });
|
|
80
|
+
if (!validation.ok) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
diagnostics: validation.errors.map(reason => ({ level: 'error', path: manifestPath, reason })),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const entryPath = path.resolve(packageReal, validation.manifest.entry);
|
|
87
|
+
const entryRealResult = safeRealpath(entryPath);
|
|
88
|
+
if (!entryRealResult.ok || !isInside(packageReal, entryRealResult.realpath)) {
|
|
89
|
+
return { ok: false, diagnostics: [{ level: 'error', path: entryPath, reason: 'entry escapes hook package' }] };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
package: {
|
|
94
|
+
manifest: validation.manifest,
|
|
95
|
+
manifestPath: manifestReal,
|
|
96
|
+
entryPath: entryRealResult.realpath,
|
|
97
|
+
},
|
|
98
|
+
diagnostics,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function safeRealpath(target) {
|
|
103
|
+
try {
|
|
104
|
+
return { ok: true, realpath: fs.realpathSync(target) };
|
|
105
|
+
} catch (err) {
|
|
106
|
+
return { ok: false, reason: err.message };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isInside(root, target) {
|
|
111
|
+
const rel = path.relative(root, target);
|
|
112
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
discoverHookPackages,
|
|
117
|
+
isInside,
|
|
118
|
+
loadHookManifest,
|
|
119
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const HOOK_MANIFEST = 'walle-hook.json';
|
|
4
|
+
const MAX_HOOK_MANIFEST_BYTES = 128 * 1024;
|
|
5
|
+
|
|
6
|
+
const HOOK_EVENTS = Object.freeze([
|
|
7
|
+
'before_prompt_build',
|
|
8
|
+
'llm_input',
|
|
9
|
+
'llm_output',
|
|
10
|
+
'agent_end',
|
|
11
|
+
'tool_result',
|
|
12
|
+
'session_patch',
|
|
13
|
+
'message_received',
|
|
14
|
+
'message_sent',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function validateHookManifest(manifest = {}, { packageDir = '' } = {}) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
if (!manifest.id || typeof manifest.id !== 'string') errors.push('id is required');
|
|
20
|
+
if (manifest.id && !/^[a-z0-9][a-z0-9._-]*$/i.test(manifest.id)) errors.push('id has invalid characters');
|
|
21
|
+
if (!manifest.entry || typeof manifest.entry !== 'string') errors.push('entry is required');
|
|
22
|
+
if (manifest.entry && (manifest.entry.includes('..') || manifest.entry.startsWith('/') || manifest.entry.includes('\\'))) {
|
|
23
|
+
errors.push('entry must be a relative path inside the hook package');
|
|
24
|
+
}
|
|
25
|
+
const events = Array.isArray(manifest.events) ? manifest.events : [];
|
|
26
|
+
if (events.length === 0) errors.push('events must include at least one supported event');
|
|
27
|
+
for (const event of events) {
|
|
28
|
+
if (!HOOK_EVENTS.includes(event)) errors.push(`unsupported event: ${event}`);
|
|
29
|
+
}
|
|
30
|
+
const permissions = manifest.permissions || {};
|
|
31
|
+
for (const key of Object.keys(permissions)) {
|
|
32
|
+
if (typeof permissions[key] !== 'boolean') errors.push(`permission ${key} must be boolean`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
ok: errors.length === 0,
|
|
37
|
+
errors,
|
|
38
|
+
manifest: {
|
|
39
|
+
id: manifest.id || '',
|
|
40
|
+
version: manifest.version || '0.0.0',
|
|
41
|
+
events,
|
|
42
|
+
entry: manifest.entry || '',
|
|
43
|
+
permissions,
|
|
44
|
+
packageDir,
|
|
45
|
+
metadata: { ...(manifest.metadata || {}) },
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
HOOK_EVENTS,
|
|
52
|
+
HOOK_MANIFEST,
|
|
53
|
+
MAX_HOOK_MANIFEST_BYTES,
|
|
54
|
+
validateHookManifest,
|
|
55
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { HOOK_EVENTS } = require('./manifest');
|
|
4
|
+
|
|
5
|
+
class HookRuntime {
|
|
6
|
+
constructor({ logger = console } = {}) {
|
|
7
|
+
this.logger = logger;
|
|
8
|
+
this.handlers = new Map();
|
|
9
|
+
this.packages = new Map();
|
|
10
|
+
this.diagnostics = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
registerPackage(hookPackage) {
|
|
14
|
+
const manifest = hookPackage?.manifest;
|
|
15
|
+
if (!manifest?.id) throw new Error('Hook package manifest id is required');
|
|
16
|
+
if (this.packages.has(manifest.id)) throw new Error(`Duplicate hook package: ${manifest.id}`);
|
|
17
|
+
const moduleExports = hookPackage.module || require(hookPackage.entryPath);
|
|
18
|
+
const registered = {
|
|
19
|
+
...hookPackage,
|
|
20
|
+
module: moduleExports,
|
|
21
|
+
};
|
|
22
|
+
this.packages.set(manifest.id, registered);
|
|
23
|
+
for (const event of manifest.events || []) {
|
|
24
|
+
if (!HOOK_EVENTS.includes(event)) continue;
|
|
25
|
+
const list = this.handlers.get(event) || [];
|
|
26
|
+
list.push({ id: manifest.id, event, module: moduleExports, permissions: manifest.permissions || {} });
|
|
27
|
+
this.handlers.set(event, list);
|
|
28
|
+
}
|
|
29
|
+
return registered;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
registerDiscovered(discoveryResult = {}) {
|
|
33
|
+
for (const diagnostic of discoveryResult.diagnostics || []) this.diagnostics.push(diagnostic);
|
|
34
|
+
for (const hookPackage of discoveryResult.packages || []) {
|
|
35
|
+
try {
|
|
36
|
+
this.registerPackage(hookPackage);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
this.diagnostics.push({ level: 'error', id: hookPackage.manifest?.id, reason: err.message });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async trigger(event, payload = {}, context = {}) {
|
|
44
|
+
if (!HOOK_EVENTS.includes(event)) throw new Error(`Unsupported hook event: ${event}`);
|
|
45
|
+
let current = payload;
|
|
46
|
+
const handlers = this.handlers.get(event) || [];
|
|
47
|
+
for (const handler of handlers) {
|
|
48
|
+
try {
|
|
49
|
+
const next = await callHandler(handler, event, current, context);
|
|
50
|
+
if (next !== undefined) current = next;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const diagnostic = { level: 'error', hook: handler.id, event, reason: err.message };
|
|
53
|
+
this.diagnostics.push(diagnostic);
|
|
54
|
+
if (this.logger?.error) this.logger.error(`[hook-runtime] ${handler.id}:${event}: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return current;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
listHooks() {
|
|
61
|
+
return [...this.packages.values()].map(pkg => ({
|
|
62
|
+
id: pkg.manifest.id,
|
|
63
|
+
version: pkg.manifest.version,
|
|
64
|
+
events: pkg.manifest.events,
|
|
65
|
+
permissions: pkg.manifest.permissions,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getDiagnostics() {
|
|
70
|
+
return [...this.diagnostics];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function callHandler(handler, event, payload, context) {
|
|
75
|
+
const exported = handler.module;
|
|
76
|
+
if (typeof exported === 'function') return exported(event, payload, context);
|
|
77
|
+
if (typeof exported?.handle === 'function') return exported.handle(event, payload, context);
|
|
78
|
+
if (typeof exported?.[event] === 'function') return exported[event](payload, context);
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
HookRuntime,
|
|
84
|
+
};
|