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.
Files changed (167) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
  3. package/template/bin/dev.sh +7 -1
  4. package/template/bin/setup.js +53 -9
  5. package/template/bin/sync-images.js +53 -0
  6. package/template/builder-journal.md +17 -0
  7. package/template/claude-task-manager/api-prompts.js +98 -13
  8. package/template/claude-task-manager/api-reviews.js +82 -5
  9. package/template/claude-task-manager/db.js +32 -5
  10. package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
  11. package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
  12. package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
  13. package/template/claude-task-manager/lib/session-capture.js +421 -0
  14. package/template/claude-task-manager/lib/session-history.js +135 -15
  15. package/template/claude-task-manager/lib/session-jobs.js +10 -5
  16. package/template/claude-task-manager/lib/session-stream.js +87 -19
  17. package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
  18. package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
  19. package/template/claude-task-manager/lib/walle-session-context.js +61 -0
  20. package/template/claude-task-manager/lib/walle-transcript.js +176 -0
  21. package/template/claude-task-manager/public/css/setup.css +35 -8
  22. package/template/claude-task-manager/public/css/walle-session.css +56 -0
  23. package/template/claude-task-manager/public/css/walle.css +120 -0
  24. package/template/claude-task-manager/public/index.html +814 -181
  25. package/template/claude-task-manager/public/js/message-renderer.js +148 -19
  26. package/template/claude-task-manager/public/js/reviews.js +120 -62
  27. package/template/claude-task-manager/public/js/setup.js +75 -31
  28. package/template/claude-task-manager/public/js/stream-view.js +115 -55
  29. package/template/claude-task-manager/public/js/walle-session.js +84 -2
  30. package/template/claude-task-manager/public/js/walle.js +308 -54
  31. package/template/claude-task-manager/server.js +1092 -146
  32. package/template/claude-task-manager/session-integrity.js +181 -54
  33. package/template/claude-task-manager/session-utils.js +123 -41
  34. package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
  35. package/template/package.json +1 -1
  36. package/template/wall-e/adapters/ctm.js +39 -18
  37. package/template/wall-e/agent-runners/contract.js +17 -0
  38. package/template/wall-e/agent-runners/index.js +22 -0
  39. package/template/wall-e/agent-runtime/harness.js +212 -0
  40. package/template/wall-e/agent-runtime/index.js +8 -0
  41. package/template/wall-e/agent-runtime/registry.js +67 -0
  42. package/template/wall-e/agent-runtime/session-store.js +179 -0
  43. package/template/wall-e/agent-runtime/spawn.js +208 -0
  44. package/template/wall-e/api-walle.js +174 -7
  45. package/template/wall-e/brain.js +266 -28
  46. package/template/wall-e/channels/policy.js +88 -0
  47. package/template/wall-e/channels/registry.js +15 -1
  48. package/template/wall-e/channels/reply-dispatcher.js +70 -0
  49. package/template/wall-e/channels/session-bindings.js +51 -0
  50. package/template/wall-e/chat/code-review-context.js +29 -0
  51. package/template/wall-e/chat.js +188 -42
  52. package/template/wall-e/coding/acp-adapter.js +188 -0
  53. package/template/wall-e/coding/agent-catalog.js +129 -0
  54. package/template/wall-e/coding/compaction-service.js +247 -0
  55. package/template/wall-e/coding/execution-trace.js +3 -0
  56. package/template/wall-e/coding/instruction-service.js +224 -0
  57. package/template/wall-e/coding/model-message.js +67 -0
  58. package/template/wall-e/coding/permission-rules-store.js +111 -0
  59. package/template/wall-e/coding/permission-service.js +266 -0
  60. package/template/wall-e/coding/prompt-bundle.js +67 -0
  61. package/template/wall-e/coding/prompt-runtime.js +243 -0
  62. package/template/wall-e/coding/provider-transform.js +188 -0
  63. package/template/wall-e/coding/runtime-mode.js +132 -0
  64. package/template/wall-e/coding/snapshot-service.js +155 -0
  65. package/template/wall-e/coding/stream-processor.js +268 -0
  66. package/template/wall-e/coding/task-tool.js +255 -0
  67. package/template/wall-e/coding/tool-registry.js +361 -0
  68. package/template/wall-e/coding/transcript-writer.js +143 -0
  69. package/template/wall-e/coding/workspace-replay.js +324 -0
  70. package/template/wall-e/coding-context.js +4 -22
  71. package/template/wall-e/coding-orchestrator.js +307 -18
  72. package/template/wall-e/coding-prompts.js +44 -3
  73. package/template/wall-e/context/context-builder.js +43 -1
  74. package/template/wall-e/context/topic-matcher.js +1 -1
  75. package/template/wall-e/eval/agent-runner.js +59 -13
  76. package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
  77. package/template/wall-e/eval/benchmarks.js +100 -16
  78. package/template/wall-e/eval/eval-orchestrator.js +218 -8
  79. package/template/wall-e/eval/harvester.js +62 -5
  80. package/template/wall-e/eval/head-to-head.js +23 -2
  81. package/template/wall-e/eval/humaneval-adapter.js +30 -5
  82. package/template/wall-e/eval/livecodebench-adapter.js +29 -5
  83. package/template/wall-e/eval/manifest.js +186 -0
  84. package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
  85. package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
  86. package/template/wall-e/eval/session-transcripts.js +57 -4
  87. package/template/wall-e/eval/swebench-adapter.js +109 -3
  88. package/template/wall-e/evaluation/agent-router.js +53 -1
  89. package/template/wall-e/evaluation/coding-quorum.js +48 -1
  90. package/template/wall-e/evaluation/router.js +4 -2
  91. package/template/wall-e/evaluation/tier-selector.js +11 -1
  92. package/template/wall-e/extraction/contradiction.js +2 -2
  93. package/template/wall-e/extraction/indexer.js +2 -1
  94. package/template/wall-e/extraction/knowledge-extractor.js +2 -2
  95. package/template/wall-e/hooks/cli.js +92 -0
  96. package/template/wall-e/hooks/discovery.js +119 -0
  97. package/template/wall-e/hooks/index.js +7 -0
  98. package/template/wall-e/hooks/manifest.js +55 -0
  99. package/template/wall-e/hooks/runtime.js +84 -0
  100. package/template/wall-e/hooks/session-memory.js +225 -0
  101. package/template/wall-e/http/auth.js +6 -2
  102. package/template/wall-e/http/chat-api.js +54 -8
  103. package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
  104. package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
  105. package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
  106. package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
  107. package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
  108. package/template/wall-e/listening/calendar.js +3 -1
  109. package/template/wall-e/llm/client.js +64 -10
  110. package/template/wall-e/llm/google.js +39 -5
  111. package/template/wall-e/llm/ollama.js +1 -1
  112. package/template/wall-e/llm/ollama.plugin.json +1 -1
  113. package/template/wall-e/llm/provider-availability.js +10 -0
  114. package/template/wall-e/llm/provider-error.js +269 -0
  115. package/template/wall-e/llm/tool-adapter.js +48 -12
  116. package/template/wall-e/loops/boot.js +2 -1
  117. package/template/wall-e/loops/initiative.js +2 -2
  118. package/template/wall-e/loops/tasks.js +8 -47
  119. package/template/wall-e/loops/workspace-prompts.js +20 -0
  120. package/template/wall-e/mcp-server.js +442 -1
  121. package/template/wall-e/memory/session-ingest-service.js +159 -0
  122. package/template/wall-e/memory/source-indexer.js +289 -0
  123. package/template/wall-e/plugins/discovery.js +83 -0
  124. package/template/wall-e/plugins/manifest-loader.js +50 -10
  125. package/template/wall-e/plugins/manifest-schema.js +69 -0
  126. package/template/wall-e/plugins/model-catalog.js +55 -0
  127. package/template/wall-e/prompts/coding/base.txt +2 -0
  128. package/template/wall-e/prompts/coding/deepseek.txt +1 -0
  129. package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
  130. package/template/wall-e/prompts/coding/plan.txt +1 -0
  131. package/template/wall-e/runtime/execution-trace.js +220 -0
  132. package/template/wall-e/security/audit.js +266 -0
  133. package/template/wall-e/security/ssrf.js +236 -0
  134. package/template/wall-e/session-files.js +303 -0
  135. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
  136. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
  137. package/template/wall-e/skills/internal-skill-registry.js +2 -2
  138. package/template/wall-e/skills/script-skill-runner.js +143 -0
  139. package/template/wall-e/skills/skill-executor.js +5 -6
  140. package/template/wall-e/skills/skill-fallback.js +3 -1
  141. package/template/wall-e/skills/skill-harness-registry.js +7 -8
  142. package/template/wall-e/skills/skill-planner.js +52 -4
  143. package/template/wall-e/skills/slack-ingest.js +11 -3
  144. package/template/wall-e/sources/base.js +90 -0
  145. package/template/wall-e/sources/builtin.js +33 -0
  146. package/template/wall-e/sources/claude-code-jsonl.js +78 -0
  147. package/template/wall-e/sources/codex-jsonl.js +125 -0
  148. package/template/wall-e/sources/coding-session-utils.js +117 -0
  149. package/template/wall-e/sources/contract-suite.js +59 -0
  150. package/template/wall-e/sources/gemini-jsonl.js +85 -0
  151. package/template/wall-e/sources/index.js +9 -0
  152. package/template/wall-e/sources/jsonl-utils.js +181 -0
  153. package/template/wall-e/sources/record-types.js +252 -0
  154. package/template/wall-e/sources/registry.js +92 -0
  155. package/template/wall-e/sources/transforms.js +100 -0
  156. package/template/wall-e/sources/walle-jsonl.js +108 -0
  157. package/template/wall-e/tools/coding-middleware.js +31 -1
  158. package/template/wall-e/tools/file-tracker.js +25 -1
  159. package/template/wall-e/tools/local-tools.js +75 -47
  160. package/template/wall-e/tools/session-sharing.js +68 -1
  161. package/template/wall-e/tools/shell-analyzer.js +1 -1
  162. package/template/wall-e/tools/shell-policy.js +47 -0
  163. package/template/wall-e/tools/snapshot.js +42 -0
  164. package/template/wall-e/training/harvester.js +62 -5
  165. package/template/wall-e/utils/repair.js +253 -1
  166. package/template/website/index.html +3 -3
  167. 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
- return {
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
- if (entry.avg_score >= 0.7 && entry.total_evals >= 10) {
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 || 'claude-haiku-4-5-20251001',
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,7 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ ...require('./discovery'),
5
+ ...require('./manifest'),
6
+ ...require('./runtime'),
7
+ };
@@ -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
+ };