338
.agents/skills/ax-agent-optimize/SKILL.md
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
---
|
||||||
|
name: ax-agent-optimize
|
||||||
|
description: This skill helps an LLM generate correct AxAgent tuning and evaluation code using @ax-llm/ax. Use when the user asks about agent.optimize(...), judgeOptions, eval datasets, optimization targets, saved optimizedProgram artifacts, or recursive optimization guidance.
|
||||||
|
version: "19.0.33"
|
||||||
|
---
|
||||||
|
|
||||||
|
# AxAgent Optimize Codegen Rules (@ax-llm/ax)
|
||||||
|
|
||||||
|
Use this skill for `agent.optimize(...)` workflows. Prefer short, modern, copyable patterns. Do not repeat general agent-authoring guidance unless the user needs it.
|
||||||
|
|
||||||
|
Your job is to help the model choose a good optimization setup for the user's actual goal:
|
||||||
|
|
||||||
|
- If the user wants better tool use, prefer action-aware tasks and either a deterministic metric or the built-in judge depending on how objective the scoring is.
|
||||||
|
- If the user wants better wording only, responder optimization may be enough.
|
||||||
|
- If the user wants reusable improvements, include artifact save/load.
|
||||||
|
- If the user wants cost or recursion behavior improved, make the eval tasks expose those tradeoffs explicitly.
|
||||||
|
|
||||||
|
## Use These Defaults
|
||||||
|
|
||||||
|
- Use `agent.optimize(...)` only after the agent is already configured and runnable.
|
||||||
|
- Prefer a deterministic custom `metric` when success is easy to score from the prediction and task record.
|
||||||
|
- Prefer the built-in judge path for open-ended assistant tasks: `judgeAI` plus `judgeOptions`.
|
||||||
|
- Only reach for a plain typed `AxGen` evaluator when the user needs LLM-as-judge behavior outside the built-in `agent.optimize(...)` flow.
|
||||||
|
- Default optimize target is `root.actor`; use `target: 'responder'` or explicit program IDs only when the user clearly asks for that.
|
||||||
|
- Use eval-safe tools or in-memory mocks because optimization replays tasks many times.
|
||||||
|
- Prefer precise tool return schemas such as `f.object(...)` over vague `f.json(...)` whenever the agent must reason about returned fields.
|
||||||
|
- Prefer task wording with canonical entity names like "the Atlas project" instead of ambiguous labels like "Atlas" when ambiguity could trigger pointless clarification.
|
||||||
|
- Save `result.optimizedProgram`, then restore with `new AxOptimizedProgramImpl(...)` and `agent.applyOptimization(...)`.
|
||||||
|
- When recursive behavior matters, keep `mode: 'advanced'` on the agent and tune against realistic `recursionOptions`.
|
||||||
|
|
||||||
|
## Decision Guide
|
||||||
|
|
||||||
|
Pick the optimization shape from the user's need:
|
||||||
|
|
||||||
|
- "Make the agent use tools correctly" -> optimize `root.actor` with `expectedActions` and `forbiddenActions`.
|
||||||
|
- "Make final answers read better" -> consider `target: 'responder'`, but only if the task is not mostly tool-selection or clarification behavior.
|
||||||
|
- "Make the whole agent better" -> use the default actor target first; only broaden target selection when the user clearly wants that extra scope.
|
||||||
|
- "Tune recursive delegation" -> keep `mode: 'advanced'` and use tasks that actually exercise recursion depth, fan-out, and termination choices.
|
||||||
|
- "Compare before and after" -> include a held-out task plus artifact save/load and replay.
|
||||||
|
|
||||||
|
Choose task design carefully:
|
||||||
|
|
||||||
|
- Prefer a small number of realistic tasks over broad but vague datasets.
|
||||||
|
- Prefer concrete criteria over generic "be helpful" language.
|
||||||
|
- Prefer explicit action expectations when correctness depends on tools, recipients, dates, or side effects.
|
||||||
|
- Prefer eval-safe mocks anytime the task touches email, scheduling, external APIs, or persistence.
|
||||||
|
|
||||||
|
## Make Agents Optimizable
|
||||||
|
|
||||||
|
Optimization works much better when the agent and dataset remove avoidable ambiguity:
|
||||||
|
|
||||||
|
- Prefer typed tool outputs over free-form JSON blobs so the actor can rely on exact field names.
|
||||||
|
- Tell the actor the exact tool fields it may use when payload shape matters.
|
||||||
|
- Explicitly ban invented fields if the model has any reason to guess hidden IDs or alternate key names.
|
||||||
|
- If recursive children only see explicit `llmQuery(..., context)` payloads, say that directly in the actor prompt.
|
||||||
|
- For recursive synthesis, tell the agent what the narrowed context should look like before delegation.
|
||||||
|
- Keep `maxSubAgentCalls` small in examples unless the user is explicitly testing broad fan-out behavior.
|
||||||
|
- Use canonical, unambiguous task wording so the model does not burn turns asking for fake clarification.
|
||||||
|
- In JS-runtime agents, require raw runnable JavaScript only. Ban `javascript:` prefixes, mixed prose/code, and multi-snippet turns.
|
||||||
|
|
||||||
|
Good pattern:
|
||||||
|
|
||||||
|
- tool schema says exactly what fields exist
|
||||||
|
- task names the exact entity to look up
|
||||||
|
- actor prompt says which fields to extract before delegation
|
||||||
|
- metric or judge penalizes unnecessary recursion and tool misuse
|
||||||
|
|
||||||
|
Bad pattern:
|
||||||
|
|
||||||
|
- tool returns `json` with an underspecified shape
|
||||||
|
- task uses overloaded names like `Atlas` without clarifying whether that is a project, team, or account
|
||||||
|
- recursive child is expected to infer hidden parent state that was never passed in context
|
||||||
|
- code agent is allowed to mix natural language with JavaScript in the same turn
|
||||||
|
|
||||||
|
## Metric vs Judge
|
||||||
|
|
||||||
|
Choose the scoring path based on how objectively the task can be measured:
|
||||||
|
|
||||||
|
- Use a custom `metric` when you can score success directly from `prediction` and `example`.
|
||||||
|
- Use the built-in agent judge when success depends on a full-run qualitative review across tool choices, clarifications, and final output.
|
||||||
|
- Use `judgeOptions.description` to tell the built-in judge what to value most.
|
||||||
|
- Use helper-based judge code only when the user is not inside `agent.optimize(...)` and still wants LLM judging.
|
||||||
|
|
||||||
|
Quick rules:
|
||||||
|
|
||||||
|
- Tool correctness with exact expected calls or forbidden calls: prefer a deterministic metric first.
|
||||||
|
- Simple extraction or classification with known correct answers: prefer a deterministic metric.
|
||||||
|
- Open-ended assistant quality, nuanced clarification behavior, or broad synthesis quality: prefer the built-in judge.
|
||||||
|
- GEPA or optimizer flows outside agents that still need LLM judging: use a plain typed `AxGen` evaluator.
|
||||||
|
|
||||||
|
Important:
|
||||||
|
|
||||||
|
- A custom `metric` overrides the built-in judge path entirely.
|
||||||
|
- Do not introduce a dedicated judge abstraction in new examples; prefer a plain typed `AxGen`.
|
||||||
|
- Do not add both a custom `metric` and judge guidance unless the user explicitly wants two separate scoring systems and understands only the custom metric drives optimization.
|
||||||
|
- If the user builds a plain `AxGen` judge metric, prefer a numeric `score:number` output over a string tier when possible. It is simpler and less fragile in practice.
|
||||||
|
|
||||||
|
## Canonical Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
AxAIGoogleGeminiModel,
|
||||||
|
AxJSRuntime,
|
||||||
|
AxOptimizedProgramImpl,
|
||||||
|
axDefaultOptimizerLogger,
|
||||||
|
agent,
|
||||||
|
ai,
|
||||||
|
f,
|
||||||
|
fn,
|
||||||
|
} from '@ax-llm/ax';
|
||||||
|
|
||||||
|
const tools = [
|
||||||
|
fn('sendEmail')
|
||||||
|
.namespace('email')
|
||||||
|
.description('Send an email message')
|
||||||
|
.arg('to', f.string('Recipient email address'))
|
||||||
|
.arg('body', f.string('Email body text'))
|
||||||
|
.returns(
|
||||||
|
f.object({
|
||||||
|
sent: f.boolean('Whether the email was sent'),
|
||||||
|
to: f.string('Recipient email address'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.handler(async ({ to }) => ({ sent: true, to }))
|
||||||
|
.build(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const studentAI = ai({
|
||||||
|
name: 'google-gemini',
|
||||||
|
apiKey: process.env.GOOGLE_APIKEY!,
|
||||||
|
config: { model: AxAIGoogleGeminiModel.Gemini25FlashLite, temperature: 0.2 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const judgeAI = ai({
|
||||||
|
name: 'google-gemini',
|
||||||
|
apiKey: process.env.GOOGLE_APIKEY!,
|
||||||
|
config: { model: AxAIGoogleGeminiModel.Gemini3Pro, temperature: 1.0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const assistant = agent('query:string -> answer:string', {
|
||||||
|
ai: studentAI,
|
||||||
|
judgeAI,
|
||||||
|
contextFields: [],
|
||||||
|
runtime: new AxJSRuntime(),
|
||||||
|
functions: { local: tools },
|
||||||
|
contextPolicy: { preset: 'checkpointed', budget: 'balanced' },
|
||||||
|
judgeOptions: {
|
||||||
|
description: 'Prefer correct tool use over polished wording.',
|
||||||
|
model: 'judge-model',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tasks = [
|
||||||
|
{
|
||||||
|
input: { query: 'Send an email to Jim saying good morning.' },
|
||||||
|
criteria: 'Use the email tool and send the message to Jim.',
|
||||||
|
expectedActions: ['email.sendEmail'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await assistant.optimize(tasks, {
|
||||||
|
target: 'actor',
|
||||||
|
maxMetricCalls: 12,
|
||||||
|
verbose: true,
|
||||||
|
optimizerLogger: axDefaultOptimizerLogger,
|
||||||
|
onProgress: (progress) => {
|
||||||
|
console.log(
|
||||||
|
`round ${progress.round}/${progress.totalRounds} current=${progress.currentScore} best=${progress.bestScore}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const saved = JSON.stringify(result.optimizedProgram, null, 2);
|
||||||
|
const restored = new AxOptimizedProgramImpl(JSON.parse(saved));
|
||||||
|
assistant.applyOptimization(restored);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deterministic Metric Pattern
|
||||||
|
|
||||||
|
Use this when the task has crisp correctness and cost/behavior tradeoffs:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await assistant.optimize(tasks, {
|
||||||
|
target: 'actor',
|
||||||
|
metric: ({ prediction, example }) => {
|
||||||
|
if (prediction.completionType !== 'final' || !prediction.output) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
if (prediction.output.answer.includes('Jim')) score += 0.4;
|
||||||
|
|
||||||
|
if (
|
||||||
|
prediction.functionCalls.some(
|
||||||
|
(call) => call.qualifiedName === 'email.sendEmail'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
score += 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((prediction.recursiveStats?.recursiveCallCount ?? 0) === 0) {
|
||||||
|
score += 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this pattern when:
|
||||||
|
|
||||||
|
- the task has a known correct answer or exact action pattern
|
||||||
|
- recursion cost or tool count must be measured explicitly
|
||||||
|
- you want repeatable, low-variance optimization runs
|
||||||
|
|
||||||
|
## Built-In Judge Pattern
|
||||||
|
|
||||||
|
Use this when the agent behavior needs holistic review:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await assistant.optimize(tasks, {
|
||||||
|
judgeAI,
|
||||||
|
judgeOptions: {
|
||||||
|
model: AxAIGoogleGeminiModel.Gemini3Pro,
|
||||||
|
description:
|
||||||
|
'Be strict about unnecessary delegation, weak clarifications, and incorrect tool choices.',
|
||||||
|
},
|
||||||
|
maxMetricCalls: 12,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this pattern when:
|
||||||
|
|
||||||
|
- task quality is open-ended or hard to score exactly
|
||||||
|
- the final answer quality matters together with the action trace
|
||||||
|
- the user wants a judge to consider clarifications, tool errors, and overall completion quality
|
||||||
|
|
||||||
|
## Plain `AxGen` Judge Pattern
|
||||||
|
|
||||||
|
Use this only when the user needs LLM judging outside the built-in `agent.optimize(...)` path:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AxGen, s } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
const judgeGen = new AxGen(
|
||||||
|
s(`
|
||||||
|
taskInput:json "Task input",
|
||||||
|
candidateOutput:json "Candidate output",
|
||||||
|
expectedOutput?:json "Optional reference output"
|
||||||
|
->
|
||||||
|
score:number "Normalized score from 0 to 1"
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
judgeGen.setInstruction(
|
||||||
|
'Score the candidate output from 0 to 1. Reward correctness and task completion. Return only the score field.'
|
||||||
|
);
|
||||||
|
|
||||||
|
const metric = async ({ prediction, example }) => {
|
||||||
|
const result = await judgeGen.forward(judgeAI, {
|
||||||
|
taskInput: example,
|
||||||
|
candidateOutput: prediction,
|
||||||
|
expectedOutput: example.expectedOutput,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Math.max(0, Math.min(1, result.score));
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await optimizer.compile(program, train, metric, {
|
||||||
|
validationExamples: validation,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this pattern when:
|
||||||
|
|
||||||
|
- the user is optimizing an `AxGen`, flow, or another program directly
|
||||||
|
- the user wants LLM judging without the higher-level `agent.optimize(...)` wrapper
|
||||||
|
- the user wants to inspect judge results directly, not just a numeric score
|
||||||
|
|
||||||
|
## Dataset And Judge Rules
|
||||||
|
|
||||||
|
- Pass already-loaded tasks. Do not invent a benchmark loader unless the user asks for one.
|
||||||
|
- Use `expectedActions` and `forbiddenActions` when tool correctness matters.
|
||||||
|
- `judgeOptions` mirrors normal forward options and supports extra judge guidance through `description`.
|
||||||
|
- The built-in judge scores from the full agent run, not just the final reply. It can see completion type, clarification payload, final output, action log, normalized function calls, tool errors, and turn count.
|
||||||
|
- For recursive advanced-mode evals, the built-in judge can also see `recursiveTrace` and `recursiveStats`.
|
||||||
|
- If the user provides a custom `metric`, that overrides the built-in judge path.
|
||||||
|
- If the user provides an LLM-based custom metric, keep the output schema as small as possible and prefer a direct numeric score.
|
||||||
|
|
||||||
|
Decision rules:
|
||||||
|
|
||||||
|
- Prefer a custom metric when the user has deterministic business scoring, exact action expectations, or explicit cost tradeoffs.
|
||||||
|
- Prefer the built-in judge when the user wants practical assistant-quality tuning and does not already have a trusted metric.
|
||||||
|
- Prefer a plain typed `AxGen` evaluator when the user is not calling `agent.optimize(...)` but still wants LLM judging.
|
||||||
|
- Prefer `judgeOptions.description` to steer the judge toward the user's real priority, such as tool correctness, brevity, groundedness, or policy compliance.
|
||||||
|
|
||||||
|
## Eval Semantics
|
||||||
|
|
||||||
|
- `agent.optimize(...)` runs each evaluation rollout from a clean continuation state.
|
||||||
|
- Saved runtime state from `getState()` and `setState(...)` is not used during eval rollouts.
|
||||||
|
- During optimize/eval, `askClarification(...)` is treated as a scored evaluation outcome instead of going through the responder.
|
||||||
|
- For clarification outcomes in custom metrics, expect `prediction.completionType === 'askClarification'`, populated `prediction.clarification`, and absent `prediction.output`.
|
||||||
|
- For final outcomes in custom metrics, expect `prediction.completionType === 'final'` and populated `prediction.output`.
|
||||||
|
- `target: 'responder'` still works, but clarification-heavy tasks are usually low-signal for responder optimization.
|
||||||
|
|
||||||
|
## Recursive Optimization Notes
|
||||||
|
|
||||||
|
- Recursive-slot artifacts require an agent configured for recursive advanced mode.
|
||||||
|
- Keep `mode: 'advanced'` top-level; child recursion behavior still follows `recursionOptions`.
|
||||||
|
- When recursive behavior matters, tune against the same `maxDepth` and tool/discovery structure you expect in production.
|
||||||
|
- Use recursive traces and recursive stats when the user wants to diagnose where token or delegation cost is coming from.
|
||||||
|
- For recursion-efficiency tuning, prefer a deterministic metric unless the user specifically needs a qualitative LLM review of decomposition quality.
|
||||||
|
- Tell the actor that recursive children only see passed context, not parent globals or prior tool results.
|
||||||
|
- For synthesis-style recursive tasks, specify the desired delegation pattern explicitly, for example "use at most one focused delegated child analysis after narrowing the tool output in JS."
|
||||||
|
- Penalize over-decomposition directly in the metric or judge prompt.
|
||||||
|
- If one training task keeps collapsing to zero, inspect that task first instead of adding more optimizer rounds. Most failures come from task ambiguity, weak tool schemas, or vague delegation guidance rather than GEPA itself.
|
||||||
|
|
||||||
|
## Artifacts And Replay
|
||||||
|
|
||||||
|
- Save `result.optimizedProgram` if the user wants portable artifacts.
|
||||||
|
- Restore artifacts with `new AxOptimizedProgramImpl(...)`, then call `agent.applyOptimization(...)`.
|
||||||
|
- For demonstrations, use fresh eval-safe tool state for baseline, optimize, and restored replay so side effects do not leak across phases.
|
||||||
|
- If the user wants to show improvement, run a held-out task before optimization, then replay it on a freshly restored optimized agent.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
- [RLM Agent Optimize](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/rlm-agent-optimize.ts) — Gemini office-assistant tuning with save/load
|
||||||
|
- [RLM Agent Recursive Optimize](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/rlm-agent-recursive-optimize.ts) — recursive-slot optimization artifacts
|
||||||
|
|
||||||
|
## Do Not Generate
|
||||||
|
|
||||||
|
- Do not optimize against production tools with real side effects unless the user explicitly wants that.
|
||||||
|
- Do not recommend responder-only optimization by default for clarification-heavy workflows.
|
||||||
|
- Do not omit artifact save/load steps when the user asks for reusable optimized configurations.
|
||||||
|
- Do not introduce a dedicated judge class or helper abstraction in new agent-optimize examples; prefer the built-in judge path or a plain typed `AxGen`.
|
||||||
|
- Do not rely on vague `json` tool returns when the agent must reason about specific fields across recursive steps.
|
||||||
|
- Do not leave recursive child context implicit. If the child needs a fact, pass it explicitly.
|
||||||
|
- Do not let code-generation agents mix prose and JavaScript if the user is optimizing runtime behavior.
|
||||||
1090
.agents/skills/ax-agent/SKILL.md
Normal file
245
.agents/skills/ax-ai/SKILL.md
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
---
|
||||||
|
name: ax-ai
|
||||||
|
description: This skill helps an LLM generate correct AI provider setup and configuration code using @ax-llm/ax. Use when the user asks about ai(), providers, models, presets, embeddings, extended thinking, context caching, or mentions OpenAI/Anthropic/Google/Azure/Groq/DeepSeek/Mistral/Cohere/Together/Ollama/HuggingFace/Reka/OpenRouter with @ax-llm/ax.
|
||||||
|
version: "19.0.33"
|
||||||
|
---
|
||||||
|
|
||||||
|
# AI Provider Codegen Rules (@ax-llm/ax)
|
||||||
|
|
||||||
|
Use this skill to generate AI provider setup, configuration, and chat code. Prefer short, modern, copyable patterns. Do not write tutorial prose unless the user explicitly asks for explanation.
|
||||||
|
|
||||||
|
## Quick Setup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ai } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
const openai = ai({ name: 'openai', apiKey: 'sk-...' });
|
||||||
|
const Codex = ai({ name: 'anthropic', apiKey: 'sk-ant-...' });
|
||||||
|
const gemini = ai({ name: 'google-gemini', apiKey: 'AIza...' });
|
||||||
|
const azure = ai({ name: 'azure-openai', apiKey: 'your-key', resourceName: 'your-resource', deploymentName: 'gpt-4' });
|
||||||
|
const groq = ai({ name: 'groq', apiKey: 'gsk_...' });
|
||||||
|
const deepseek = ai({ name: 'deepseek', apiKey: 'sk-...' });
|
||||||
|
const mistral = ai({ name: 'mistral', apiKey: 'your-key' });
|
||||||
|
const cohere = ai({ name: 'cohere', apiKey: 'your-key' });
|
||||||
|
const together = ai({ name: 'together', apiKey: 'your-key' });
|
||||||
|
const openrouter = ai({ name: 'openrouter', apiKey: 'your-key' });
|
||||||
|
const ollama = ai({ name: 'ollama', url: 'http://localhost:11434' });
|
||||||
|
const hf = ai({ name: 'huggingface', apiKey: 'hf_...' });
|
||||||
|
const reka = ai({ name: 'reka', apiKey: 'your-key' });
|
||||||
|
const grok = ai({ name: 'x-grok', apiKey: 'your-key' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model Presets
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ai, AxAIGoogleGeminiModel } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
const gemini = ai({
|
||||||
|
name: 'google-gemini',
|
||||||
|
apiKey: process.env.GOOGLE_APIKEY!,
|
||||||
|
config: { model: 'simple' },
|
||||||
|
models: [
|
||||||
|
{ key: 'tiny', model: AxAIGoogleGeminiModel.Gemini20FlashLite, description: 'Fast + cheap', config: { maxTokens: 1024, temperature: 0.3 } },
|
||||||
|
{ key: 'simple', model: AxAIGoogleGeminiModel.Gemini20Flash, description: 'Balanced', config: { temperature: 0.6 } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await gemini.chat({ model: 'tiny', chatPrompt: [{ role: 'user', content: 'Hi' }] });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chat
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const res = await llm.chat({
|
||||||
|
chatPrompt: [
|
||||||
|
{ role: 'system', content: 'You are concise.' },
|
||||||
|
{ role: 'user', content: 'Write a haiku about the ocean.' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
console.log(res.results[0]?.content);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Options
|
||||||
|
|
||||||
|
- `stream` (boolean): enable SSE; true by default
|
||||||
|
- `thinkingTokenBudget`: `'minimal'` | `'low'` | `'medium'` | `'high'` | `'highest'` | `'none'`
|
||||||
|
- `showThoughts`: include thoughts in output
|
||||||
|
- `functionCallMode`: `'auto'` | `'native'` | `'prompt'`
|
||||||
|
- `debug`, `logger`, `tracer`, `rateLimiter`, `timeout`
|
||||||
|
|
||||||
|
## Extended Thinking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ai, AxAIAnthropicModel } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
const Codex = ai({
|
||||||
|
name: 'anthropic',
|
||||||
|
apiKey: process.env.ANTHROPIC_APIKEY!,
|
||||||
|
config: { model: AxAIAnthropicModel.Claude46Opus },
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await Codex.chat(
|
||||||
|
{ chatPrompt: [{ role: 'user', content: 'Solve step by step...' }] },
|
||||||
|
{ thinkingTokenBudget: 'medium', showThoughts: true },
|
||||||
|
);
|
||||||
|
console.log(res.results[0]?.thought);
|
||||||
|
console.log(res.results[0]?.content);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Budget Levels
|
||||||
|
|
||||||
|
| Level | Anthropic (tokens) | Gemini (tokens) |
|
||||||
|
|---|---|---|
|
||||||
|
| `'none'` | disabled | minimal |
|
||||||
|
| `'minimal'` | 1,024 | 200 |
|
||||||
|
| `'low'` | 5,000 | 800 |
|
||||||
|
| `'medium'` | 10,000 | 5,000 |
|
||||||
|
| `'high'` | 20,000 | 10,000 |
|
||||||
|
| `'highest'` | 32,000 | 24,500 |
|
||||||
|
|
||||||
|
### Anthropic Model-Specific Behavior
|
||||||
|
|
||||||
|
- Opus 4.6: adaptive thinking, effort levels
|
||||||
|
- Opus 4.5: budget_tokens + effort levels (capped at `'high'`)
|
||||||
|
- Other thinking models: budget tokens only
|
||||||
|
|
||||||
|
### Custom Thinking Levels
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const Codex = ai({
|
||||||
|
name: 'anthropic',
|
||||||
|
apiKey: '...',
|
||||||
|
config: {
|
||||||
|
model: AxAIAnthropicModel.Claude46Opus,
|
||||||
|
thinkingTokenBudgetLevels: {
|
||||||
|
minimal: 2048,
|
||||||
|
low: 8000,
|
||||||
|
medium: 16000,
|
||||||
|
high: 25000,
|
||||||
|
highest: 40000,
|
||||||
|
},
|
||||||
|
effortLevelMapping: {
|
||||||
|
minimal: 'low',
|
||||||
|
low: 'medium',
|
||||||
|
medium: 'high',
|
||||||
|
high: 'high',
|
||||||
|
highest: 'max',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Embeddings
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { embeddings } = await llm.embed({
|
||||||
|
texts: ['hello', 'world'],
|
||||||
|
embedModel: 'text-embedding-005',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context Caching
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await gen.forward(llm, { code, language }, {
|
||||||
|
mem,
|
||||||
|
sessionId: 'code-review-session',
|
||||||
|
contextCache: {
|
||||||
|
ttlSeconds: 3600,
|
||||||
|
cacheBreakpoint: 'after-examples',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Breakpoint values: `'system'` | `'after-functions'` | `'after-examples'`
|
||||||
|
|
||||||
|
Provider behavior:
|
||||||
|
|
||||||
|
- Google Gemini: explicit caching with cache resource ID, auto TTL refresh
|
||||||
|
- Anthropic: implicit via `cache_control` markers
|
||||||
|
|
||||||
|
### External Registry (serverless)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const registry: AxContextCacheRegistry = {
|
||||||
|
get: async (key) => { /* redis.get */ },
|
||||||
|
set: async (key, entry) => { /* redis.set */ },
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## AWS Bedrock
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AxAIBedrock, AxAIBedrockModel } from '@ax-llm/ax-ai-aws-bedrock';
|
||||||
|
|
||||||
|
const bedrock = new AxAIBedrock({
|
||||||
|
region: 'us-east-2',
|
||||||
|
fallbackRegions: ['us-west-2'],
|
||||||
|
config: { model: AxAIBedrockModel.ClaudeSonnet4 },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vercel AI SDK Integration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ai } from '@ax-llm/ax';
|
||||||
|
import { AxAIProvider } from '@ax-llm/ax-ai-sdk-provider';
|
||||||
|
import { generateText } from 'ai';
|
||||||
|
|
||||||
|
const axAI = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY! });
|
||||||
|
const model = new AxAIProvider(axAI);
|
||||||
|
const result = await generateText({
|
||||||
|
model,
|
||||||
|
messages: [{ role: 'user', content: 'Hello!' }],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP + AxJSRuntime
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AxMCPClient } from '@ax-llm/ax';
|
||||||
|
import { axCreateMCPStdioTransport } from '@ax-llm/ax-tools';
|
||||||
|
|
||||||
|
const transport = axCreateMCPStdioTransport({
|
||||||
|
command: 'npx',
|
||||||
|
args: ['-y', '@anthropic/mcp-server-filesystem'],
|
||||||
|
});
|
||||||
|
const client = new AxMCPClient(transport);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
- Use `ai()` factory for all providers.
|
||||||
|
- Provider names: `'openai'`, `'anthropic'`, `'google-gemini'`, `'azure-openai'`, `'mistral'`, `'groq'`, `'cohere'`, `'together'`, `'deepseek'`, `'ollama'`, `'huggingface'`, `'openrouter'`, `'reka'`, `'x-grok'`
|
||||||
|
- Thinking constraints on Anthropic: `temperature` and `topK` are ignored; `topP` only sent if >= 0.95.
|
||||||
|
- Bedrock uses `new AxAIBedrock()`, not `ai()`.
|
||||||
|
- Vercel AI SDK uses `AxAIProvider` wrapper.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Fetch these for full working code:
|
||||||
|
|
||||||
|
- [Embeddings](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/embed.ts) — embedding generation
|
||||||
|
- [Anthropic Thinking](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/anthropic-thinking-function.ts) — extended thinking with functions
|
||||||
|
- [Anthropic Thinking Separation](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/anthropic-thinking-separation.ts) — thinking separation
|
||||||
|
- [Anthropic Web Search](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/anthropic-web-search.ts) — Anthropic web search
|
||||||
|
- [OpenAI Web Search](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/openai-web-search.ts) — OpenAI web search
|
||||||
|
- [OpenAI Responses](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/openai-responses.ts) — OpenAI responses API
|
||||||
|
- [o3 Reasoning](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/reasoning-o3-example.ts) — o3 reasoning
|
||||||
|
- [Gemini Context Cache](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/gemini-context-cache.ts) — Gemini context caching
|
||||||
|
- [Gemini Files](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/gemini-file-support.ts) — Gemini file handling
|
||||||
|
- [Grok Live Search](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/grok-live-search.ts) — Grok live search
|
||||||
|
- [OpenRouter](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/openrouter.ts) — OpenRouter provider
|
||||||
|
- [Vertex AI Auth](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/vertex-auth-example.ts) — Vertex AI authentication
|
||||||
|
- [MCP Stdio](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/mcp-client-memory.ts) — MCP stdio transport
|
||||||
|
- [MCP HTTP](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/mcp-client-pipedream.ts) — MCP HTTP transport
|
||||||
|
- [Telemetry](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/telemetry.ts) — OpenTelemetry tracing
|
||||||
|
- [Multi-Modal](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/multi-modal.ts) — image handling
|
||||||
|
|
||||||
|
## Do Not Generate
|
||||||
|
|
||||||
|
- Do not use `new AxAIOpenAI(...)` or similar class constructors for standard providers; use `ai()`.
|
||||||
|
- Do not hardcode provider class names when `ai({ name: ... })` covers the provider.
|
||||||
|
- Do not mix `thinkingTokenBudget` with explicit `temperature` on Anthropic thinking models.
|
||||||
|
- Do not use `ai()` for AWS Bedrock; use `new AxAIBedrock()`.
|
||||||
|
- Do not omit `resourceName` and `deploymentName` for Azure OpenAI.
|
||||||
402
.agents/skills/ax-flow/SKILL.md
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
---
|
||||||
|
name: ax-flow
|
||||||
|
description: This skill helps an LLM generate correct AxFlow workflow code using @ax-llm/ax. Use when the user asks about flow(), AxFlow, workflow orchestration, parallel execution, DAG workflows, conditional routing, map/reduce patterns, or multi-node AI pipelines.
|
||||||
|
version: "19.0.33"
|
||||||
|
---
|
||||||
|
|
||||||
|
# AxFlow Codegen Rules (@ax-llm/ax)
|
||||||
|
|
||||||
|
Use this skill to generate `AxFlow` workflow code. Prefer short, modern, copyable patterns. Do not write tutorial prose unless the user explicitly asks for explanation.
|
||||||
|
|
||||||
|
## Use These Defaults
|
||||||
|
|
||||||
|
- Use `flow()` factory, not `new AxFlow()`.
|
||||||
|
- Import: `import { ai, flow, f } from '@ax-llm/ax';`
|
||||||
|
- `autoParallel: true` is the default; independent executes run in parallel automatically.
|
||||||
|
- Node results are stored as `${nodeName}Result` in state.
|
||||||
|
- Always define `.node()` before `.execute()` for that node.
|
||||||
|
- Use `.returns()` (or `.r()`) as the last step to lock the output type.
|
||||||
|
- Use descriptive node names: `documentSummarizer`, not `proc1`.
|
||||||
|
- Use descriptive field names: `userInput`, `responseText`, not `text`, `result`.
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
- Use `flow()` factory syntax for new code.
|
||||||
|
- Node results in state follow the pattern `state.${nodeName}Result.${fieldName}`.
|
||||||
|
- `.execute()` maps current state to node inputs; `.map()` transforms state without AI calls.
|
||||||
|
- `.returns()` maps final state to the flow output type.
|
||||||
|
- Always define nodes before executing them; reversed order throws at runtime.
|
||||||
|
- Keep state flat; avoid deep nesting in `.map()`.
|
||||||
|
- Ensure loop conditions can change to avoid infinite loops.
|
||||||
|
- Structure independent executes to maximize auto-parallelization.
|
||||||
|
- Use `flow<InputType, OutputType>()` for typed flows.
|
||||||
|
- Aliases: `.n()` = `.node()`, `.nx()` = `.nodeExtended()`, `.m()` = `.map()`, `.r()` = `.returns()`.
|
||||||
|
|
||||||
|
## Canonical Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ai, flow } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
const llm = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY! });
|
||||||
|
|
||||||
|
const wf = flow<{ userInput: string }, { responseText: string }>()
|
||||||
|
.node('testNode', 'userInput:string -> responseText:string')
|
||||||
|
.execute('testNode', (state) => ({ userInput: state.userInput }))
|
||||||
|
.returns((state) => ({ responseText: state.testNodeResult.responseText }));
|
||||||
|
|
||||||
|
const result = await wf.forward(llm, { userInput: 'Hello world' });
|
||||||
|
console.log(result.responseText);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Factory Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic
|
||||||
|
const wf = flow();
|
||||||
|
|
||||||
|
// With options
|
||||||
|
const wf = flow({ autoParallel: false });
|
||||||
|
|
||||||
|
// Typed
|
||||||
|
const wf = flow<InputType, OutputType>();
|
||||||
|
|
||||||
|
// Typed with options
|
||||||
|
const wf = flow<InputType, OutputType>({ autoParallel: true, batchSize: 5 });
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Evolution
|
||||||
|
|
||||||
|
State grows with each executed node. Results are stored as `${nodeName}Result`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Initial state: { userInput: 'Hello' }
|
||||||
|
flow.execute('processor', (state) => ({ input: state.userInput }));
|
||||||
|
// State: { userInput: 'Hello', processorResult: { output: '...' } }
|
||||||
|
|
||||||
|
flow.execute('analyzer', (state) => ({ text: state.processorResult.output }));
|
||||||
|
// State: { ..., analyzerResult: { sentiment: '...', confidence: 0.8 } }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node Definition
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// String signature (creates AxGen automatically)
|
||||||
|
flow.node('processor', 'input:string -> output:string');
|
||||||
|
|
||||||
|
// Multiple outputs
|
||||||
|
flow.node('analyzer', 'text:string -> sentiment:string, confidence:number');
|
||||||
|
|
||||||
|
// Array outputs
|
||||||
|
flow.node('extractor', 'documentText:string -> entities:string[]');
|
||||||
|
|
||||||
|
// Short alias
|
||||||
|
flow.n('processor', 'input:string -> output:string');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extended Nodes (nx)
|
||||||
|
|
||||||
|
Add fields to a base signature without rewriting it:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { f, flow } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
// Chain-of-thought reasoning
|
||||||
|
flow.nx('reasoner', 'question:string -> answer:string', {
|
||||||
|
prependOutputs: [
|
||||||
|
{ name: 'reasoning', type: f.internal(f.string('Step-by-step reasoning')) },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add confidence scoring
|
||||||
|
flow.nx('analyzer', 'input:string -> result:string', {
|
||||||
|
appendOutputs: [{ name: 'confidence', type: f.number('Confidence 0-1') }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add optional context input
|
||||||
|
flow.nx('processor', 'query:string -> response:string', {
|
||||||
|
appendInputs: [{ name: 'context', type: f.optional(f.string('Extra context')) }],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Extension options: `prependInputs`, `appendInputs`, `prependOutputs`, `appendOutputs`.
|
||||||
|
|
||||||
|
## Execute With Input Mapping
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
flow.execute('summarizer', (state) => ({ documentText: state.document }));
|
||||||
|
|
||||||
|
// With AI override (use a different model for this node)
|
||||||
|
flow.execute('processor', (state) => ({ input: state.data }), { ai: alternativeAI });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Map (State Transformation)
|
||||||
|
|
||||||
|
Use `map()` for data shaping without AI calls:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Sync
|
||||||
|
flow.map((state) => ({ ...state, upperText: state.rawText.toUpperCase() }));
|
||||||
|
|
||||||
|
// Async
|
||||||
|
flow.map(async (state) => {
|
||||||
|
const data = await fetchFromAPI(state.query);
|
||||||
|
return { ...state, enrichedData: data };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parallel async transforms
|
||||||
|
flow.map([
|
||||||
|
async (state) => ({ ...state, result1: await api1(state.data) }),
|
||||||
|
async (state) => ({ ...state, result2: await api2(state.data) }),
|
||||||
|
], { parallel: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Returns (Final Output)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const wf = flow<{ input: string }>()
|
||||||
|
.map((state) => ({ ...state, upper: state.input.toUpperCase(), len: state.input.length }))
|
||||||
|
.returns((state) => ({ upper: state.upper, isLong: state.len > 20 }));
|
||||||
|
|
||||||
|
// Result is typed as { upper: string; isLong: boolean }
|
||||||
|
const result = await wf.forward(llm, { input: 'test' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sequential Processing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const wf = flow<{ input: string }, { finalResult: string }>()
|
||||||
|
.node('step1', 'input:string -> intermediate:string')
|
||||||
|
.node('step2', 'intermediate:string -> output:string')
|
||||||
|
.execute('step1', (state) => ({ input: state.input }))
|
||||||
|
.execute('step2', (state) => ({ intermediate: state.step1Result.intermediate }))
|
||||||
|
.returns((state) => ({ finalResult: state.step2Result.output }));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auto-Parallel Execution
|
||||||
|
|
||||||
|
Independent executes run in parallel automatically (`autoParallel: true` by default):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const wf = flow<{ text: string }, { combined: string }>()
|
||||||
|
.node('sentimentAnalyzer', 'text:string -> sentiment:string')
|
||||||
|
.node('topicExtractor', 'text:string -> topics:string[]')
|
||||||
|
.node('entityRecognizer', 'text:string -> entities:string[]')
|
||||||
|
// These three run in parallel (all depend only on state.text)
|
||||||
|
.execute('sentimentAnalyzer', (state) => ({ text: state.text }))
|
||||||
|
.execute('topicExtractor', (state) => ({ text: state.text }))
|
||||||
|
.execute('entityRecognizer', (state) => ({ text: state.text }))
|
||||||
|
// This waits for all three
|
||||||
|
.returns((state) => ({
|
||||||
|
combined: JSON.stringify({
|
||||||
|
sentiment: state.sentimentAnalyzerResult.sentiment,
|
||||||
|
topics: state.topicExtractorResult.topics,
|
||||||
|
entities: state.entityRecognizerResult.entities,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Inspect execution plan
|
||||||
|
const plan = wf.getExecutionPlan();
|
||||||
|
console.log(plan.parallelGroups, plan.maxParallelism);
|
||||||
|
```
|
||||||
|
|
||||||
|
Disable auto-parallel:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const wf = flow({ autoParallel: false });
|
||||||
|
// or per execution:
|
||||||
|
await wf.forward(llm, input, { autoParallel: false });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conditional Branching
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const wf = flow<{ query: string; expertMode: boolean }, { response: string }>()
|
||||||
|
.node('simple', 'query:string -> response:string')
|
||||||
|
.node('expert', 'query:string -> response:string')
|
||||||
|
.branch((state) => state.expertMode)
|
||||||
|
.when(true)
|
||||||
|
.execute('expert', (state) => ({ query: state.query }))
|
||||||
|
.when(false)
|
||||||
|
.execute('simple', (state) => ({ query: state.query }))
|
||||||
|
.merge()
|
||||||
|
.returns((state) => ({
|
||||||
|
response: state.expertResult?.response ?? state.simpleResult?.response,
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
After `.merge()`, only the taken branch's result exists; use optional chaining (`?.`) on untaken branch results.
|
||||||
|
|
||||||
|
## While Loops
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const wf = flow<{ content: string }, { finalContent: string }>()
|
||||||
|
.node('processor', 'content:string -> processedContent:string')
|
||||||
|
.node('qualityChecker', 'content:string -> qualityScore:number')
|
||||||
|
.map((state) => ({ currentContent: state.content, iteration: 0, qualityScore: 0 }))
|
||||||
|
.while((state) => state.iteration < 3 && state.qualityScore < 0.8)
|
||||||
|
.map((state) => ({ ...state, iteration: state.iteration + 1 }))
|
||||||
|
.execute('processor', (state) => ({ content: state.currentContent }))
|
||||||
|
.execute('qualityChecker', (state) => ({
|
||||||
|
content: state.processorResult.processedContent,
|
||||||
|
}))
|
||||||
|
.map((state) => ({
|
||||||
|
...state,
|
||||||
|
currentContent: state.processorResult.processedContent,
|
||||||
|
qualityScore: state.qualityCheckerResult.qualityScore,
|
||||||
|
}))
|
||||||
|
.endWhile()
|
||||||
|
.returns((state) => ({ finalContent: state.currentContent }));
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Every `.while()` needs a matching `.endWhile()`.
|
||||||
|
- Ensure the loop condition can change to avoid infinite loops.
|
||||||
|
|
||||||
|
## Feedback Loops (label/feedback)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const wf = flow<{ prompt: string }, { result: string }>()
|
||||||
|
.node('gen', 'prompt:string -> result:string, quality:number')
|
||||||
|
.map((state) => ({ ...state, tries: 0 }))
|
||||||
|
.label('retry')
|
||||||
|
.map((state) => ({ ...state, tries: state.tries + 1 }))
|
||||||
|
.execute('gen', (state) => ({ prompt: state.prompt }))
|
||||||
|
.feedback((state) => state.genResult.quality < 0.9 && state.tries < 3, 'retry')
|
||||||
|
.returns((state) => ({ result: state.genResult.result }));
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Define the label before referencing it in `.feedback()`.
|
||||||
|
- Always include a max-iteration guard to avoid infinite loops.
|
||||||
|
|
||||||
|
## Explicit Parallel Sub-Flows
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
flow
|
||||||
|
.parallel([
|
||||||
|
(sub) => sub.execute('analyzer1', (state) => ({ text: state.input })),
|
||||||
|
(sub) => sub.execute('analyzer2', (state) => ({ text: state.input })),
|
||||||
|
(sub) => sub.execute('analyzer3', (state) => ({ text: state.input })),
|
||||||
|
])
|
||||||
|
.merge('combinedResults', (r1, r2, r3) => ({
|
||||||
|
a1: r1.analyzer1Result.analysis,
|
||||||
|
a2: r2.analyzer2Result.analysis,
|
||||||
|
a3: r3.analyzer3Result.analysis,
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Derive (Batch/Array Processing)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const wf = flow<{ items: string[] }, { processed: string[] }>({ batchSize: 3 })
|
||||||
|
.derive('processed', 'items', (item, index) => `processed-${item}-${index}`, {
|
||||||
|
batchSize: 2,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dynamic AI Context (Multi-Model)
|
||||||
|
|
||||||
|
Route nodes to different AI providers:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const fast = ai({ name: 'groq', apiKey: '...' });
|
||||||
|
const smart = ai({ name: 'anthropic', apiKey: '...' });
|
||||||
|
|
||||||
|
const wf = flow<{ text: string }, { out: string }>()
|
||||||
|
.node('draft', 'text:string -> out:string')
|
||||||
|
.node('refine', 'text:string -> out:string')
|
||||||
|
.execute('draft', (state) => ({ text: state.text }), { ai: fast })
|
||||||
|
.execute('refine', (state) => ({ text: state.draftResult.out }), { ai: smart })
|
||||||
|
.returns((state) => ({ out: state.refineResult.out }));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Description and toFunction
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const wf = flow<{ userQuestion: string }, { responseText: string }>()
|
||||||
|
.node('qa', 'userQuestion:string -> responseText:string')
|
||||||
|
.execute('qa', (state) => ({ userQuestion: state.userQuestion }))
|
||||||
|
.returns((state) => ({ responseText: state.qaResult.responseText }))
|
||||||
|
.description('Question Answerer', 'Answers user questions concisely.');
|
||||||
|
|
||||||
|
const fn = wf.toFunction();
|
||||||
|
// fn.name, fn.parameters (JSON Schema), fn.func
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instrumentation (Tracing)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ai, flow } from '@ax-llm/ax';
|
||||||
|
import { context, trace } from '@opentelemetry/api';
|
||||||
|
|
||||||
|
const tracer = trace.getTracer('axflow');
|
||||||
|
const llm = ai({ name: 'openai', apiKey: '...' });
|
||||||
|
|
||||||
|
const wf = flow<{ userQuestion: string }>()
|
||||||
|
.node('summarizer', 'documentText:string -> summaryText:string')
|
||||||
|
.execute('summarizer', (s) => ({ documentText: s.userQuestion }))
|
||||||
|
.returns((s) => ({ answer: s.summarizerResult.summaryText }));
|
||||||
|
|
||||||
|
const result = await wf.forward(llm, { userQuestion: 'hi' }, {
|
||||||
|
tracer,
|
||||||
|
traceContext: context.active(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Program IDs and Demos
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const wf = flow<{ input: string }>()
|
||||||
|
.node('summarizer', 'text:string -> summary:string')
|
||||||
|
.node('classifier', 'text:string -> category:string');
|
||||||
|
|
||||||
|
// Discover program IDs
|
||||||
|
console.log(wf.namedPrograms());
|
||||||
|
// [{ id: 'root.summarizer', ... }, { id: 'root.classifier', ... }]
|
||||||
|
|
||||||
|
// Set demos (TypeScript catches typos)
|
||||||
|
wf.setDemos([{ programId: 'root.summarizer', traces: [] }]);
|
||||||
|
|
||||||
|
// Apply optimization
|
||||||
|
wf.applyOptimization(optimizedProgram);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
const result = await wf.forward(llm, input);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Flow execution failed:', error);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Common errors:
|
||||||
|
- `"Node 'x' not found"` -- define `.node()` before `.execute()`.
|
||||||
|
- `"endWhile() without matching while()"` -- every `.while()` needs `.endWhile()`.
|
||||||
|
- `"when() without matching branch()"` -- `.when()` must be inside `.branch()`/`.merge()`.
|
||||||
|
- `"merge() without matching branch()"` -- every `.branch()` needs `.merge()`.
|
||||||
|
- `"Label 'x' not found"` -- define `.label()` before `.feedback()` references it.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Fetch these for full working code:
|
||||||
|
|
||||||
|
- [Flow](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow.ts) — complete flow usage
|
||||||
|
- [Auto-Parallel](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow-auto-parallel.ts) — auto-parallelization
|
||||||
|
- [Async Map](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow-async-map.ts) — async map transforms
|
||||||
|
- [Enhanced Demo](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow-enhanced-demo.ts) — instance-based nodes
|
||||||
|
- [Flow as Function](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/ax-flow-to-function.ts) — flow as callable function
|
||||||
|
- [Fluent Builder](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/fluent-flow-example.ts) — fluent builder pattern
|
||||||
|
- [Flow Logging](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/flow-logging-simple.ts) — flow logging
|
||||||
|
- [Load Balancing](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/balancer.ts) — load balancing
|
||||||
|
|
||||||
|
## Do Not Generate
|
||||||
|
|
||||||
|
- Do not use `new AxFlow(...)` for new code.
|
||||||
|
- Do not execute a node before defining it with `.node()`.
|
||||||
|
- Do not use generic field names like `text`, `result`, `data`, `input`, `output`.
|
||||||
|
- Do not create deep-nested state objects in `.map()`.
|
||||||
|
- Do not create loop conditions that can never change.
|
||||||
|
- Do not add unnecessary dependencies between executes (kills auto-parallelism).
|
||||||
|
- Do not forget to use optional chaining on branch results after `.merge()`.
|
||||||
323
.agents/skills/ax-gen/SKILL.md
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
---
|
||||||
|
name: ax-gen
|
||||||
|
description: This skill helps an LLM generate correct AxGen code using @ax-llm/ax. Use when the user asks about ax(), AxGen, generators, forward(), streamingForward(), assertions, field processors, step hooks, self-tuning, or structured outputs.
|
||||||
|
version: "19.0.33"
|
||||||
|
---
|
||||||
|
|
||||||
|
# AxGen Codegen Rules (@ax-llm/ax)
|
||||||
|
|
||||||
|
Use this skill to generate `AxGen` code. Prefer short, modern, copyable patterns. Do not write tutorial prose unless the user explicitly asks for explanation.
|
||||||
|
|
||||||
|
## Use These Defaults
|
||||||
|
|
||||||
|
- Use `ax(...)` factory, not `new AxGen(...)`.
|
||||||
|
- Always pass an AI instance from `ai(...)` as the first argument to `forward()`.
|
||||||
|
- Streaming uses `streamingForward()`, not `forward()` with a stream option.
|
||||||
|
- Assertions auto-retry with error feedback on failure.
|
||||||
|
- Step hook mutations are applied at the next step boundary (pending pattern).
|
||||||
|
- `stopFunction` accepts a string or string[] for multiple stop functions.
|
||||||
|
- Multi-step continues until: all outputs filled, stop function called, or `maxSteps` reached.
|
||||||
|
|
||||||
|
## Canonical Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ai, ax, s } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
const llm = ai({
|
||||||
|
name: 'openai',
|
||||||
|
apiKey: process.env.OPENAI_APIKEY!,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inline signature
|
||||||
|
const gen = ax('input:string -> output:string, reasoning:string');
|
||||||
|
|
||||||
|
// Reusable signature
|
||||||
|
const sig = s('question:string, context:string[] -> answer:string');
|
||||||
|
const gen2 = ax(sig);
|
||||||
|
|
||||||
|
// With options
|
||||||
|
const gen3 = ax('input -> output', {
|
||||||
|
description: 'A helpful assistant',
|
||||||
|
maxRetries: 3,
|
||||||
|
maxSteps: 10,
|
||||||
|
temperature: 0.7,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await gen.forward(llm, { input: 'Hello world' });
|
||||||
|
console.log(result.output);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running AxGen
|
||||||
|
|
||||||
|
### `forward()`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await gen.forward(llm, { input: '...' });
|
||||||
|
|
||||||
|
// With options
|
||||||
|
const result = await gen.forward(llm, { input: '...' }, {
|
||||||
|
maxRetries: 5,
|
||||||
|
model: 'gpt-4.1',
|
||||||
|
modelConfig: { temperature: 0.9, maxTokens: 1000 },
|
||||||
|
debug: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `streamingForward()`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const stream = gen.streamingForward(llm, { input: 'Write a long story' });
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
if (chunk.delta.output) process.stdout.write(chunk.delta.output);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stopping And Cancellation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AxAIServiceAbortedError } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
const timer = setTimeout(() => gen.stop(), 3_000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await gen.forward(llm, { topic: 'Long document' }, {
|
||||||
|
abortSignal: AbortSignal.timeout(10_000),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AxAIServiceAbortedError) console.log('Aborted');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `gen.stop()` gracefully stops multi-step execution at the next step boundary.
|
||||||
|
- `abortSignal` cancels the underlying AI service call immediately.
|
||||||
|
- Catch `AxAIServiceAbortedError` when using either mechanism.
|
||||||
|
|
||||||
|
## Assertions And Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Standard assertion (checked after forward completes)
|
||||||
|
gen.addAssert(
|
||||||
|
(args) => args.output.length > 50,
|
||||||
|
'Output must be at least 50 characters'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Streaming assertion (checked during streaming)
|
||||||
|
gen.addStreamingAssert(
|
||||||
|
'output',
|
||||||
|
(text) => !text.includes('forbidden'),
|
||||||
|
'Output contains forbidden text'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Failed assertions cause an automatic retry with the error message fed back to the LLM.
|
||||||
|
- `addAssert` receives the full output object.
|
||||||
|
- `addStreamingAssert` targets a specific field and receives the partial text so far.
|
||||||
|
|
||||||
|
## Field Processors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Post-processing after generation
|
||||||
|
gen.addFieldProcessor('summary', (value, context) => value.toUpperCase());
|
||||||
|
|
||||||
|
// Streaming field processor (called on each chunk)
|
||||||
|
gen.addStreamingFieldProcessor('content', (partialValue, context) => {
|
||||||
|
console.log(`Received ${partialValue.length} chars`);
|
||||||
|
return partialValue;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `addFieldProcessor` runs once after the field is fully generated.
|
||||||
|
- `addStreamingFieldProcessor` runs on each streaming chunk for the target field.
|
||||||
|
- Both must return the (possibly transformed) value.
|
||||||
|
|
||||||
|
## Function Calling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await gen.forward(llm, { question: '...' }, {
|
||||||
|
functions: tools,
|
||||||
|
functionCallMode: 'auto',
|
||||||
|
stopFunction: 'finalAnswer',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `functionCallMode` can be `'auto'`, `'none'`, or a specific function name to force.
|
||||||
|
- `stopFunction` accepts a string or string[] to halt multi-step on specific function calls.
|
||||||
|
- Multi-step continues until all outputs filled, stop function called, or `maxSteps` reached.
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
### Response Caching
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const gen = ax('question:string -> answer:string', {
|
||||||
|
cachingFunction: async (key, value?) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
await cache.set(key, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return await cache.get(key);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Context Caching
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await gen.forward(llm, { question: '...' }, {
|
||||||
|
contextCache: { cacheBreakpoint: 'after-examples' },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `cachingFunction` acts as a get/set: called with `(key)` to read, `(key, value)` to write.
|
||||||
|
- `contextCache` enables AI provider-level prompt caching for long context.
|
||||||
|
|
||||||
|
## Sampling And Result Picker
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await gen.forward(llm, { question: '...' }, {
|
||||||
|
sampleCount: 3,
|
||||||
|
resultPicker: async (samples) => {
|
||||||
|
// Evaluate each sample and return the index of the best one
|
||||||
|
return bestIndex;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `sampleCount` generates multiple completions in parallel.
|
||||||
|
- `resultPicker` receives all samples and must return the index of the chosen result.
|
||||||
|
|
||||||
|
## Extended Thinking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await gen.forward(llm, { question: '...' }, {
|
||||||
|
thinkingTokenBudget: 'medium',
|
||||||
|
showThoughts: true,
|
||||||
|
});
|
||||||
|
console.log(result.thought);
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `thinkingTokenBudget` can be `'low'`, `'medium'`, `'high'`, or a number.
|
||||||
|
- Set `showThoughts: true` to include the model's reasoning in `result.thought`.
|
||||||
|
|
||||||
|
## Step Hooks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await gen.forward(llm, values, {
|
||||||
|
stepHooks: {
|
||||||
|
beforeStep: (ctx) => {
|
||||||
|
if (ctx.functionsExecuted.has('complexanalysis')) {
|
||||||
|
ctx.setModel('smart');
|
||||||
|
ctx.setThinkingBudget('high');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
afterStep: (ctx) => {
|
||||||
|
console.log(`Usage: ${ctx.usage.totalTokens} tokens`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### AxStepContext Read-Only Properties
|
||||||
|
|
||||||
|
- `stepIndex` - current step number
|
||||||
|
- `maxSteps` - configured maximum steps
|
||||||
|
- `isFirstStep` - whether this is the first step
|
||||||
|
- `functionsExecuted` - `Set<string>` of function names called so far
|
||||||
|
- `lastFunctionCalls` - array of the most recent function call results
|
||||||
|
- `usage` - token usage statistics
|
||||||
|
- `state` - current step state
|
||||||
|
|
||||||
|
### AxStepContext Mutators
|
||||||
|
|
||||||
|
- `setModel(model)` - change the model for the next step
|
||||||
|
- `setThinkingBudget(budget)` - adjust thinking budget
|
||||||
|
- `setTemperature(temp)` - adjust temperature
|
||||||
|
- `setMaxTokens(max)` - adjust max output tokens
|
||||||
|
- `setOptions(opts)` - set arbitrary forward options
|
||||||
|
- `addFunctions(fns)` - add functions for the next step
|
||||||
|
- `removeFunctions(names)` - remove functions by name
|
||||||
|
- `stop()` - stop multi-step execution
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- All mutations are pending and applied at the next step boundary.
|
||||||
|
- `beforeStep` runs before each LLM call; `afterStep` runs after.
|
||||||
|
- Use `afterFunctionExecution` to react to specific function results.
|
||||||
|
|
||||||
|
## Self-Tuning
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Simple: enable all self-tuning
|
||||||
|
const result = await gen.forward(llm, values, { selfTuning: true });
|
||||||
|
|
||||||
|
// Granular: pick what to tune
|
||||||
|
const result = await gen.forward(llm, values, {
|
||||||
|
selfTuning: {
|
||||||
|
model: true,
|
||||||
|
thinkingBudget: true,
|
||||||
|
functions: [searchWeb, calculate],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `selfTuning: true` enables automatic model and parameter selection.
|
||||||
|
- Granular config allows tuning specific aspects independently.
|
||||||
|
- `selfTuning.functions` provides a pool of functions the tuner may add or remove per step.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AxGenerateError } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await gen.forward(llm, { input: '...' });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxGenerateError) {
|
||||||
|
console.log(error.details.model, error.details.signature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `AxGenerateError` includes `details` with `model` and `signature` for debugging.
|
||||||
|
- `AxAIServiceAbortedError` is thrown on cancellation via `stop()` or `abortSignal`.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Fetch these for full working code:
|
||||||
|
|
||||||
|
- [Streaming](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/streaming.ts) — streaming with assertions
|
||||||
|
- [Assertions](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/asserts.ts) — output validation
|
||||||
|
- [Streaming Assertions](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/streaming-asserts.ts) — streaming with assertion checks
|
||||||
|
- [Structured Output](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/structured_output.ts) — fluent API with validation
|
||||||
|
- [Debug Logging](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/debug-logging.ts) — debug mode and step hooks
|
||||||
|
- [Stop Function](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/stop-function.ts) — stop functions
|
||||||
|
- [Fibonacci](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/fibonacci.ts) — streaming with thinking
|
||||||
|
- [Extraction](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/extract.ts) — information extraction
|
||||||
|
- [Multi-Sampling](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/sample-count.ts) — sample count usage
|
||||||
|
|
||||||
|
## Do Not Generate
|
||||||
|
|
||||||
|
- Do not use `new AxGen(...)` for new code unless explicitly required.
|
||||||
|
- Do not pass raw API keys or config objects where an `ai(...)` instance is expected.
|
||||||
|
- Do not use `forward()` for streaming; use `streamingForward()`.
|
||||||
|
- Do not forget that assertions auto-retry; avoid manual retry loops around assertion logic.
|
||||||
|
- Do not mutate step hook context expecting immediate effect; mutations are pending until the next step.
|
||||||
|
- Do not assume multi-step stops after one LLM call; it continues until outputs are filled, a stop function fires, or `maxSteps` is reached.
|
||||||
260
.agents/skills/ax-gepa/SKILL.md
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
---
|
||||||
|
name: ax-gepa
|
||||||
|
description: This skill helps an LLM generate correct AxGEPA optimization code using @ax-llm/ax. Use when the user asks about AxGEPA, GEPA, Pareto optimization, multi-objective prompt tuning, reflective prompt evolution, validationExamples, maxMetricCalls, or optimizing a generator, flow, or agent tree.
|
||||||
|
version: "19.0.33"
|
||||||
|
---
|
||||||
|
|
||||||
|
# AxGEPA Codegen Rules (@ax-llm/ax)
|
||||||
|
|
||||||
|
Use this skill to generate direct `AxGEPA` optimization code. Prefer short, modern, copyable patterns over long explanation.
|
||||||
|
|
||||||
|
## Use These Defaults
|
||||||
|
|
||||||
|
- Use `new AxGEPA({ studentAI, teacherAI, ... })`.
|
||||||
|
- Prefer `ai()`, `ax()`, and `flow()` for new code.
|
||||||
|
- Use a strong `teacherAI` and a cheaper `studentAI`.
|
||||||
|
- Always pass `validationExamples` to `compile()`.
|
||||||
|
- Always set `maxMetricCalls` to bound optimizer cost.
|
||||||
|
- Use scalar metrics for one objective and object metrics for Pareto optimization.
|
||||||
|
- Apply results with `program.applyOptimization(result.optimizedProgram!)`.
|
||||||
|
- For tree-wide runs, expect `optimizedProgram.instructionMap`.
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
- `AxGEPA.compile()` works for a single generator and for tree-aware roots such as flows or agents with registered instruction-bearing descendants.
|
||||||
|
- There is no separate flow-only GEPA optimizer. Use `AxGEPA` for flows too.
|
||||||
|
- The metric may return either `number` or `Record<string, number>`.
|
||||||
|
- Keep metrics deterministic and cheap by default.
|
||||||
|
- Avoid extra LLM calls inside the metric unless the user explicitly wants judge-based evaluation.
|
||||||
|
- If the user needs LLM-as-judge scoring for a non-agent GEPA run, prefer a plain typed `AxGen` evaluator instead of writing a custom judge abstraction.
|
||||||
|
- `maxMetricCalls` must be large enough to cover the initial validation pass over `validationExamples`.
|
||||||
|
- GEPA optimizes instructions. If a tree has no instruction-bearing nodes, optimization will fail.
|
||||||
|
- Use held-out validation examples for selection. Do not reuse the training set as `validationExamples`.
|
||||||
|
- `result.optimizedProgram` is the easy-to-apply best candidate. `result.paretoFront` is the full trade-off set for multi-objective runs.
|
||||||
|
|
||||||
|
## Metric Selection
|
||||||
|
|
||||||
|
Choose the evaluation path deliberately:
|
||||||
|
|
||||||
|
- Prefer a deterministic metric when correctness can be read directly from `prediction` and `example`.
|
||||||
|
- Prefer a deterministic metric when cost, latency, recursion depth, or tool count matters.
|
||||||
|
- Use a plain typed `AxGen` evaluator only when the task is genuinely qualitative and hard to score exactly.
|
||||||
|
- For `agent.optimize(...)`, prefer the built-in judge path instead of manually wrapping a judge metric.
|
||||||
|
|
||||||
|
Rule of thumb:
|
||||||
|
|
||||||
|
- `AxGEPA` on `AxGen` or flow: use a metric first, optionally a plain typed `AxGen` evaluator if needed.
|
||||||
|
- `agent.optimize(...)`: use custom `metric` for crisp scoring, otherwise `judgeAI` plus `judgeOptions`.
|
||||||
|
|
||||||
|
## Canonical Scalar Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ai, ax, AxAIOpenAIModel, AxGEPA } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
const student = ai({
|
||||||
|
name: 'openai',
|
||||||
|
apiKey: process.env.OPENAI_APIKEY!,
|
||||||
|
config: { model: AxAIOpenAIModel.GPT4OMini },
|
||||||
|
});
|
||||||
|
|
||||||
|
const teacher = ai({
|
||||||
|
name: 'openai',
|
||||||
|
apiKey: process.env.OPENAI_APIKEY!,
|
||||||
|
config: { model: AxAIOpenAIModel.GPT4O },
|
||||||
|
});
|
||||||
|
|
||||||
|
const classifier = ax(
|
||||||
|
'emailText:string -> priority:class "high, normal, low", rationale:string'
|
||||||
|
);
|
||||||
|
|
||||||
|
const train = [
|
||||||
|
{ emailText: 'URGENT: Server down!', priority: 'high' },
|
||||||
|
{ emailText: 'Weekly newsletter', priority: 'low' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const validation = [
|
||||||
|
{ emailText: 'Invoice overdue', priority: 'high' },
|
||||||
|
{ emailText: 'Lunch plans?', priority: 'low' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const metric = ({ prediction, example }: { prediction: any; example: any }) =>
|
||||||
|
prediction?.priority === example?.priority ? 1 : 0;
|
||||||
|
|
||||||
|
const optimizer = new AxGEPA({
|
||||||
|
studentAI: student,
|
||||||
|
teacherAI: teacher,
|
||||||
|
numTrials: 12,
|
||||||
|
minibatch: true,
|
||||||
|
minibatchSize: 4,
|
||||||
|
earlyStoppingTrials: 4,
|
||||||
|
sampleCount: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await optimizer.compile(classifier, train, metric, {
|
||||||
|
validationExamples: validation,
|
||||||
|
maxMetricCalls: 120,
|
||||||
|
});
|
||||||
|
|
||||||
|
classifier.applyOptimization(result.optimizedProgram!);
|
||||||
|
console.log(result.bestScore);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Canonical Pareto Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ai, flow, AxAIOpenAIModel, AxGEPA } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
const student = ai({
|
||||||
|
name: 'openai',
|
||||||
|
apiKey: process.env.OPENAI_APIKEY!,
|
||||||
|
config: { model: AxAIOpenAIModel.GPT4OMini },
|
||||||
|
});
|
||||||
|
|
||||||
|
const teacher = ai({
|
||||||
|
name: 'openai',
|
||||||
|
apiKey: process.env.OPENAI_APIKEY!,
|
||||||
|
config: { model: AxAIOpenAIModel.GPT4O },
|
||||||
|
});
|
||||||
|
|
||||||
|
const wf = flow<{ emailText: string }>()
|
||||||
|
.n('classifier', 'emailText:string -> priority:class "high, normal, low"')
|
||||||
|
.n(
|
||||||
|
'rationale',
|
||||||
|
'emailText:string, priority:string -> rationale:string "One concise sentence"'
|
||||||
|
)
|
||||||
|
.e('classifier', (state) => ({ emailText: state.emailText }))
|
||||||
|
.e('rationale', (state) => ({
|
||||||
|
emailText: state.emailText,
|
||||||
|
priority: state.classifierResult.priority,
|
||||||
|
}))
|
||||||
|
.r((state) => ({
|
||||||
|
priority: state.classifierResult.priority,
|
||||||
|
rationale: state.rationaleResult.rationale,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const train = [
|
||||||
|
{ emailText: 'URGENT: Server down!', priority: 'high' },
|
||||||
|
{ emailText: 'Weekly newsletter', priority: 'low' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const validation = [
|
||||||
|
{ emailText: 'Invoice overdue', priority: 'high' },
|
||||||
|
{ emailText: 'Lunch plans?', priority: 'low' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const metric = ({ prediction, example }: { prediction: any; example: any }) => {
|
||||||
|
const accuracy = prediction?.priority === example?.priority ? 1 : 0;
|
||||||
|
const rationale = typeof prediction?.rationale === 'string'
|
||||||
|
? prediction.rationale
|
||||||
|
: '';
|
||||||
|
const brevity = rationale.length <= 40 ? 1 : rationale.length <= 80 ? 0.5 : 0.1;
|
||||||
|
return { accuracy, brevity };
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await new AxGEPA({
|
||||||
|
studentAI: student,
|
||||||
|
teacherAI: teacher,
|
||||||
|
numTrials: 16,
|
||||||
|
minibatch: true,
|
||||||
|
minibatchSize: 6,
|
||||||
|
earlyStoppingTrials: 5,
|
||||||
|
sampleCount: 1,
|
||||||
|
}).compile(wf, train, metric, {
|
||||||
|
validationExamples: validation,
|
||||||
|
maxMetricCalls: 240,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const point of result.paretoFront) {
|
||||||
|
console.log(point.scores, point.configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
wf.applyOptimization(result.optimizedProgram!);
|
||||||
|
console.log(result.optimizedProgram?.instructionMap);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Metric Patterns
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Scalar objective
|
||||||
|
const scalarMetric = ({ prediction, example }) =>
|
||||||
|
prediction.answer === example.answer ? 1 : 0;
|
||||||
|
|
||||||
|
// Multi-objective
|
||||||
|
const multiMetric = ({ prediction, example }) => ({
|
||||||
|
accuracy: prediction.answer === example.answer ? 1 : 0,
|
||||||
|
brevity:
|
||||||
|
typeof prediction?.reasoning === 'string' &&
|
||||||
|
prediction.reasoning.length < 120
|
||||||
|
? 1
|
||||||
|
: 0.2,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- Return plain numbers or plain object literals.
|
||||||
|
- Keep objective names stable across calls.
|
||||||
|
- Prefer normalized scores such as `0..1` so trade-offs are easy to reason about.
|
||||||
|
|
||||||
|
## Result Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { optimizedProgram, paretoFront } = result;
|
||||||
|
|
||||||
|
program.applyOptimization(optimizedProgram!);
|
||||||
|
|
||||||
|
// Save for later
|
||||||
|
const saved = JSON.stringify(optimizedProgram);
|
||||||
|
|
||||||
|
// Load later and re-apply
|
||||||
|
const loaded = JSON.parse(saved);
|
||||||
|
program.applyOptimization(loaded);
|
||||||
|
```
|
||||||
|
|
||||||
|
- Single-target runs usually populate both `optimizedProgram.instruction` and `optimizedProgram.instructionMap`.
|
||||||
|
- Tree-wide runs rely on `instructionMap`, keyed by full program ID.
|
||||||
|
- Pareto points expose candidate configs under `point.configuration.instructionMap`.
|
||||||
|
|
||||||
|
## Useful Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const optimizer = new AxGEPA({
|
||||||
|
studentAI,
|
||||||
|
teacherAI,
|
||||||
|
numTrials: 20,
|
||||||
|
minibatch: true,
|
||||||
|
minibatchSize: 5,
|
||||||
|
minibatchFullEvalSteps: 5,
|
||||||
|
earlyStoppingTrials: 5,
|
||||||
|
minImprovementThreshold: 0,
|
||||||
|
sampleCount: 1,
|
||||||
|
seed: 42,
|
||||||
|
verbose: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- `numTrials`: number of reflection/evolution rounds.
|
||||||
|
- `minibatch`: reduce per-round evaluation cost.
|
||||||
|
- `minibatchSize`: examples per minibatch.
|
||||||
|
- `earlyStoppingTrials`: stop after repeated non-improvement.
|
||||||
|
- `minImprovementThreshold`: reject tiny gains below this threshold.
|
||||||
|
- `seed`: stabilize sampling during demos and tests.
|
||||||
|
|
||||||
|
## Budgeting and Validation
|
||||||
|
|
||||||
|
- Always create distinct `train` and `validationExamples` arrays.
|
||||||
|
- Size `maxMetricCalls` for at least one full validation pass plus several rounds.
|
||||||
|
- If the user wants a strict budget, say so explicitly and set `maxMetricCalls`.
|
||||||
|
- For expensive trees, start with `auto: 'light'` or fewer `numTrials`, then scale up.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- Error about `maxMetricCalls` being too small: increase it until the initial validation pass fits.
|
||||||
|
- Empty or poor Pareto front: verify the metric returns numbers for every example.
|
||||||
|
- No tree optimization effect: ensure child programs are registered under the root and have instructions to mutate.
|
||||||
|
- Saved optimization applies only partly: use `program.applyOptimization(...)`, not just `setInstruction(...)`, so `instructionMap` reaches the full tree.
|
||||||
|
|
||||||
|
## Good Example Targets
|
||||||
|
|
||||||
|
- `/Users/vr/src/ax/src/examples/gepa.ts`
|
||||||
|
- `/Users/vr/src/ax/src/examples/gepa-flow.ts`
|
||||||
|
- `/Users/vr/src/ax/src/examples/gepa-train-inference.ts`
|
||||||
|
- `/Users/vr/src/ax/src/examples/gepa-quality-vs-speed-optimization.ts`
|
||||||
268
.agents/skills/ax-learn/SKILL.md
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
---
|
||||||
|
name: ax-learn
|
||||||
|
description: This skill helps an LLM generate correct AxLearn code using @ax-llm/ax. Use when the user asks about self-improving agents, trace-backed learning, feedback-aware updates, or AxLearn modes.
|
||||||
|
version: "19.0.33"
|
||||||
|
---
|
||||||
|
|
||||||
|
# AxLearn Codegen Rules (@ax-llm/ax)
|
||||||
|
|
||||||
|
Use this skill to generate `AxLearn` code that matches the current API.
|
||||||
|
|
||||||
|
## Core Model
|
||||||
|
|
||||||
|
- `AxLearn` wraps an `AxGen`.
|
||||||
|
- `teacher` is for judging, synthesis, and reflection.
|
||||||
|
- `runtimeAI` is the model being improved.
|
||||||
|
- `forward()` and `streamingForward()` are inference-time APIs and auto-log traces when tracing is enabled.
|
||||||
|
- `optimize()` is offline learning.
|
||||||
|
- `applyUpdate()` is a bounded update API for `continuous` and `playbook` modes.
|
||||||
|
- `ready()` should be awaited before assuming checkpoints have been restored.
|
||||||
|
- `improvement` is the score delta from the previous/restored state.
|
||||||
|
|
||||||
|
## Required Inputs
|
||||||
|
|
||||||
|
- Always provide `name`.
|
||||||
|
- Always provide `storage`.
|
||||||
|
- Always provide `teacher`.
|
||||||
|
- Always provide `runtimeAI` if you call `optimize()` or `applyUpdate()`.
|
||||||
|
|
||||||
|
## Modes
|
||||||
|
|
||||||
|
- `batch`: offline prompt learning only.
|
||||||
|
- `continuous`: offline optimization plus bounded feedback-aware `applyUpdate(...)`.
|
||||||
|
- `playbook`: structured context/playbook learning plus `applyUpdate(...)`.
|
||||||
|
|
||||||
|
## Preferred Construction
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
AxLearn,
|
||||||
|
ax,
|
||||||
|
ai,
|
||||||
|
type AxCheckpoint,
|
||||||
|
type AxStorage,
|
||||||
|
type AxTrace,
|
||||||
|
} from '@ax-llm/ax';
|
||||||
|
|
||||||
|
const storage: AxStorage = {
|
||||||
|
save: async (_name, _item) => {
|
||||||
|
// persist trace/checkpoint
|
||||||
|
},
|
||||||
|
load: async (_name, _query) => {
|
||||||
|
// return traces/checkpoints
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const teacher = ai({
|
||||||
|
name: 'openai',
|
||||||
|
apiKey: process.env.OPENAI_APIKEY!,
|
||||||
|
});
|
||||||
|
|
||||||
|
const runtimeAI = ai({
|
||||||
|
name: 'openai',
|
||||||
|
apiKey: process.env.OPENAI_APIKEY!,
|
||||||
|
});
|
||||||
|
|
||||||
|
const gen = ax(`
|
||||||
|
customerQuery:string "User message" ->
|
||||||
|
supportReply:string "Agent reply"
|
||||||
|
`);
|
||||||
|
|
||||||
|
const agent = new AxLearn(gen, {
|
||||||
|
name: 'support-bot-v1',
|
||||||
|
storage,
|
||||||
|
teacher,
|
||||||
|
runtimeAI,
|
||||||
|
mode: 'continuous',
|
||||||
|
budget: 12,
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
customerQuery: 'Where is my order?',
|
||||||
|
supportReply: 'Your order is in transit and should arrive in 2 days.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerQuery: 'I need a refund.',
|
||||||
|
supportReply: 'I can help with that. Please share your order number.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
generateExamples: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await agent.ready();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const prediction = await agent.forward(runtimeAI, {
|
||||||
|
customerQuery: 'My package is late.',
|
||||||
|
});
|
||||||
|
|
||||||
|
const traces = await agent.getTraces({ limit: 1 });
|
||||||
|
if (traces[0]) {
|
||||||
|
await agent.addFeedback(traces[0].id, {
|
||||||
|
score: 0,
|
||||||
|
label: 'needs-empathy',
|
||||||
|
comment: 'Acknowledge the frustration more directly.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Offline Optimization
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await agent.optimize({
|
||||||
|
// Optional overrides
|
||||||
|
budget: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(result.mode);
|
||||||
|
console.log(result.score);
|
||||||
|
console.log(result.improvement);
|
||||||
|
console.log(result.checkpointVersion);
|
||||||
|
```
|
||||||
|
|
||||||
|
`result.improvement` is the gain relative to the prior/restored score.
|
||||||
|
|
||||||
|
## Continuous Update
|
||||||
|
|
||||||
|
Use `applyUpdate(...)` only in `continuous` or `playbook` mode.
|
||||||
|
|
||||||
|
- In `continuous` mode, `example` may be input-only.
|
||||||
|
- `prediction` is the observed runtime output being critiqued.
|
||||||
|
- If `example` includes expected output fields, that expected-output row stays eligible for scored optimization.
|
||||||
|
- The observed `prediction` row is feedback/reflection context, not a scored train/validation row by itself.
|
||||||
|
- Feedback-bearing scored examples should stay in the training pool when non-feedback rows can fill validation.
|
||||||
|
- In `playbook` mode, `getInstruction()` returns the active composed prompt.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const update = await agent.applyUpdate({
|
||||||
|
example: {
|
||||||
|
customerQuery: 'My package is late.',
|
||||||
|
},
|
||||||
|
prediction,
|
||||||
|
feedback: {
|
||||||
|
score: 0,
|
||||||
|
label: 'needs-empathy',
|
||||||
|
comment: 'Acknowledge the frustration more directly.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Playbook Mode
|
||||||
|
|
||||||
|
- Use `mode: 'playbook'` when the learned artifact should be structured guidance, not just an instruction tweak.
|
||||||
|
- Playbook checkpoints restore through `ready()`.
|
||||||
|
- `applyUpdate(...)` in playbook mode performs an online structured update.
|
||||||
|
- `getInstruction()` should be treated as the active composed runtime prompt, even before optimization if the base prompt lives in the signature description.
|
||||||
|
- `artifact.playbookSummary` should match the persisted checkpoint `state.artifactSummary`.
|
||||||
|
|
||||||
|
## How Learning Data Is Used
|
||||||
|
|
||||||
|
- `examples` and usable traces become scored optimization rows.
|
||||||
|
- Feedback stored with `addFeedback(...)` becomes reflection feedback for later optimization.
|
||||||
|
- In continuous updates, `example + prediction + feedback` is used as an observed feedback event.
|
||||||
|
- Input-only update examples are useful for reflection, but they are not promoted into scored examples unless expected outputs are present.
|
||||||
|
|
||||||
|
## Important Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const agent = new AxLearn(gen, {
|
||||||
|
name: 'agent-id',
|
||||||
|
storage,
|
||||||
|
teacher,
|
||||||
|
runtimeAI,
|
||||||
|
mode: 'batch', // 'batch' | 'continuous' | 'playbook'
|
||||||
|
budget: 20,
|
||||||
|
metric: async ({ prediction, example }) => {
|
||||||
|
return prediction.supportReply === example.supportReply ? 1 : 0;
|
||||||
|
},
|
||||||
|
criteria: 'accuracy and tone',
|
||||||
|
judgeOptions: {},
|
||||||
|
examples: [],
|
||||||
|
useTraces: true,
|
||||||
|
generateExamples: false,
|
||||||
|
synthCount: 20,
|
||||||
|
validationSplit: 0.2,
|
||||||
|
continuousOptions: {
|
||||||
|
feedbackWindowSize: 25,
|
||||||
|
maxRecentTraces: 100,
|
||||||
|
updateBudget: 4,
|
||||||
|
},
|
||||||
|
playbookOptions: {
|
||||||
|
maxEpochs: 2,
|
||||||
|
},
|
||||||
|
onTrace: (trace) => {
|
||||||
|
console.log(trace.id);
|
||||||
|
},
|
||||||
|
onProgress: (progress) => {
|
||||||
|
console.log(progress.round, progress.score);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Result Shape
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type AxLearnResult = {
|
||||||
|
mode: 'batch' | 'continuous' | 'playbook';
|
||||||
|
score: number;
|
||||||
|
improvement: number;
|
||||||
|
checkpointVersion: number;
|
||||||
|
stats: {
|
||||||
|
trainingExamples: number;
|
||||||
|
validationExamples: number;
|
||||||
|
feedbackExamples: number;
|
||||||
|
durationMs: number;
|
||||||
|
mode: 'batch' | 'continuous' | 'playbook';
|
||||||
|
};
|
||||||
|
state?: {
|
||||||
|
mode: 'batch' | 'continuous' | 'playbook';
|
||||||
|
instruction?: string;
|
||||||
|
baseInstruction?: string;
|
||||||
|
score?: number;
|
||||||
|
continuous?: {
|
||||||
|
feedbackTraceCount?: number;
|
||||||
|
lastUpdateAt?: string;
|
||||||
|
};
|
||||||
|
playbook?: Record<string, unknown>;
|
||||||
|
artifactSummary?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
artifact?: {
|
||||||
|
playbook?: Record<string, unknown>;
|
||||||
|
playbookSummary?: {
|
||||||
|
feedbackEvents: number;
|
||||||
|
historyBatches: number;
|
||||||
|
bulletCount: number;
|
||||||
|
updatedAt?: string;
|
||||||
|
};
|
||||||
|
lastUpdateAt?: string;
|
||||||
|
feedbackExamples?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storage Notes
|
||||||
|
|
||||||
|
- `AxStorage.save(name, item)` receives either a trace or checkpoint.
|
||||||
|
- `AxStorage.load(name, query)` should return arrays of traces or checkpoints.
|
||||||
|
- Checkpoints may be returned unsorted. `AxLearn` restores the newest one client-side.
|
||||||
|
|
||||||
|
## Do This
|
||||||
|
|
||||||
|
- Use `runtimeAI` explicitly.
|
||||||
|
- Await `ready()` before relying on restored state.
|
||||||
|
- Run `optimize()` off the hot path.
|
||||||
|
- Use `continuous` mode when you want bounded feedback-aware updates.
|
||||||
|
- Use `playbook` mode when you want persistent structured guidance.
|
||||||
|
- Pass the real observed model output as `prediction` in `applyUpdate(...)`.
|
||||||
|
- Treat `getInstruction()` in playbook mode as the live composed prompt, not just the raw base instruction.
|
||||||
|
|
||||||
|
## Avoid This
|
||||||
|
|
||||||
|
- Do not assume `teacher` is the optimized runtime model.
|
||||||
|
- Do not call `applyUpdate()` in `batch` mode.
|
||||||
|
- Do not claim feedback affects learning unless you are storing it with `addFeedback(...)` or passing it to `applyUpdate(...)`.
|
||||||
|
- Do not assume checkpoints load synchronously in the constructor.
|
||||||
|
- Do not treat `prediction` as the gold answer in continuous updates.
|
||||||
192
.agents/skills/ax-signature/SKILL.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
---
|
||||||
|
name: ax-signature
|
||||||
|
description: This skill helps an LLM generate correct DSPy signature code using @ax-llm/ax. Use when the user asks about signatures, s(), f(), field types, string syntax, fluent builder API, validation constraints, or type-safe inputs/outputs.
|
||||||
|
version: "19.0.33"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ax Signature Reference
|
||||||
|
|
||||||
|
## Signature Syntax
|
||||||
|
|
||||||
|
```
|
||||||
|
[description] input1:type, input2:type -> output1:type, output2:type
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field Types
|
||||||
|
|
||||||
|
| Type | Syntax | TypeScript | Example |
|
||||||
|
|------|--------|-----------|---------|
|
||||||
|
| String | `:string` | `string` | `userName:string` |
|
||||||
|
| Number | `:number` | `number` | `score:number` |
|
||||||
|
| Boolean | `:boolean` | `boolean` | `isValid:boolean` |
|
||||||
|
| JSON | `:json` | `any` | `metadata:json` |
|
||||||
|
| Date | `:date` | `Date` | `birthDate:date` |
|
||||||
|
| DateTime | `:datetime` | `Date` | `timestamp:datetime` |
|
||||||
|
| Image | `:image` | `{mimeType, data}` | `photo:image` (input only) |
|
||||||
|
| Audio | `:audio` | `{format?, data}` | `recording:audio` (input only) |
|
||||||
|
| File | `:file` | `{mimeType, data}` | `document:file` (input only) |
|
||||||
|
| URL | `:url` | `string` | `website:url` |
|
||||||
|
| Code | `:code` | `string` | `pythonScript:code` |
|
||||||
|
| Class | `:class "a, b, c"` | `"a" \| "b" \| "c"` | `mood:class "happy, sad"` |
|
||||||
|
|
||||||
|
## Arrays, Optional, and Internal Fields
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'tags:string[] -> processedTags:string[]' // arrays
|
||||||
|
'query:string, context?:string -> response:string' // optional with ?
|
||||||
|
'problem:string -> reasoning!:string, solution:string' // internal with !
|
||||||
|
```
|
||||||
|
|
||||||
|
## Three Ways to Create Signatures
|
||||||
|
|
||||||
|
### 1. String-Based (Recommended for simple cases)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ax, s } from '@ax-llm/ax';
|
||||||
|
const gen = ax('input:string -> output:string');
|
||||||
|
const sig = s('query:string -> response:string');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Pure Fluent Builder API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { f } from '@ax-llm/ax';
|
||||||
|
const sig = f()
|
||||||
|
.input('userMessage', f.string('User input'))
|
||||||
|
.input('contextData', f.string('Additional context').optional())
|
||||||
|
.input('tags', f.string('Keywords').array())
|
||||||
|
.output('responseText', f.string('AI response'))
|
||||||
|
.output('confidenceScore', f.number('Confidence 0-1'))
|
||||||
|
.output('debugInfo', f.string('Debug info').internal())
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Hybrid
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { s, f } from '@ax-llm/ax';
|
||||||
|
const sig = s('base:string -> result:string')
|
||||||
|
.appendInputField('extra', f.json('Metadata').optional())
|
||||||
|
.appendOutputField('score', f.number('Quality score'));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fluent API Reference
|
||||||
|
|
||||||
|
Type creators:
|
||||||
|
- `f.string(desc)`, `f.number(desc)`, `f.boolean(desc)`, `f.json(desc)`
|
||||||
|
- `f.image(desc)`, `f.audio(desc)`, `f.file(desc)`, `f.url(desc)`
|
||||||
|
- `f.email(desc)`, `f.date(desc)`, `f.datetime(desc)`
|
||||||
|
- `f.class(['a','b','c'], desc)`, `f.code(desc)`
|
||||||
|
- `f.object({ field: f.string() }, desc)`
|
||||||
|
|
||||||
|
Chainable modifiers (method chaining only, no nesting):
|
||||||
|
- `.optional()` - make field optional
|
||||||
|
- `.array()` / `.array('list description')` - make field an array
|
||||||
|
- `.internal()` - output only, hidden from final output
|
||||||
|
- `.cache()` - input only, mark for prompt caching
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Correct: pure fluent chaining
|
||||||
|
f.string('description').optional().array()
|
||||||
|
f.string('context').cache().optional()
|
||||||
|
f.object({ field: f.string() }, 'item desc').array('list desc')
|
||||||
|
|
||||||
|
// Wrong: nested function calls (removed)
|
||||||
|
f.array(f.string('description')) // REMOVED
|
||||||
|
f.optional(f.string('description')) // REMOVED
|
||||||
|
f.internal(f.string('description')) // REMOVED
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Constraints
|
||||||
|
|
||||||
|
### String Constraints
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
f.string('username').min(3).max(20)
|
||||||
|
f.string('email').email()
|
||||||
|
f.string('website').url()
|
||||||
|
f.string('birthDate').date()
|
||||||
|
f.string('timestamp').datetime()
|
||||||
|
f.string('pattern').regex('^[A-Z0-9]')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Number Constraints
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
f.number('age').min(18).max(120)
|
||||||
|
f.number('score').min(0).max(100)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Validation Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const sig = f()
|
||||||
|
.input('formData', f.string('Raw form data'))
|
||||||
|
.output('user', f.object({
|
||||||
|
username: f.string('Username').min(3).max(20),
|
||||||
|
email: f.string('Email').email(),
|
||||||
|
age: f.number('Age').min(18).max(120),
|
||||||
|
bio: f.string('Bio').max(500).optional(),
|
||||||
|
website: f.string('Website').url().optional(),
|
||||||
|
tags: f.string('Tag').min(2).max(30).array()
|
||||||
|
}, 'User profile'))
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cached Input Fields
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const sig = f()
|
||||||
|
.input('staticContext', f.string('Context').cache())
|
||||||
|
.input('userQuery', f.string('Dynamic query'))
|
||||||
|
.output('answer', f.string('Response'))
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field Naming Rules
|
||||||
|
|
||||||
|
Good: `userQuestion`, `customerEmail`, `analysisResult`, `confidenceScore`
|
||||||
|
Bad: `text`, `data`, `input`, `output`, `a`, `x`, `val` (too generic), `1field` (starts with number)
|
||||||
|
|
||||||
|
## Media Type Restrictions
|
||||||
|
|
||||||
|
- Media types (image, audio, file) are **top-level input fields only**
|
||||||
|
- Cannot be nested in objects
|
||||||
|
- Cannot be output fields
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Chain of Thought
|
||||||
|
'problem:string -> reasoning!:string, solution:string'
|
||||||
|
|
||||||
|
// Classification
|
||||||
|
'email:string -> priority:class "urgent, normal, low"'
|
||||||
|
|
||||||
|
// Multi-modal
|
||||||
|
'imageData:image, question?:string -> description:string, objects:string[]'
|
||||||
|
|
||||||
|
// Data Extraction
|
||||||
|
'invoiceText:string -> invoiceNumber:string, totalAmount:number, lineItems:json[]'
|
||||||
|
|
||||||
|
// With description
|
||||||
|
'"Answer TypeScript questions" question:string -> answer:string, confidence:number'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
- Use `f()` fluent builder, NOT nested `f.array(f.string())` -- those are removed.
|
||||||
|
- Field names must be descriptive (not generic like `text`, `data`, `input`).
|
||||||
|
- Media types are input-only, top-level only.
|
||||||
|
- `.internal()` is output-only (for chain-of-thought reasoning).
|
||||||
|
- `.cache()` is input-only (for prompt caching).
|
||||||
|
- Validation errors trigger auto-retry with correction feedback.
|
||||||
|
- `f.email()`, `f.url()`, `f.date()`, `f.datetime()` are shorthand for `f.string().email()` etc.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Fetch these for full working code:
|
||||||
|
|
||||||
|
- [Fluent Signature](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/fluent-signature-example.ts) — fluent f() API
|
||||||
|
- [Structured Output](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/structured_output.ts) — structured output with validation
|
||||||
|
- [Debug Schema](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/debug_schema.ts) — JSON schema validation
|
||||||
292
.agents/skills/ax/SKILL.md
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
---
|
||||||
|
name: ax
|
||||||
|
description: This skill helps with using the @ax-llm/ax TypeScript library for building LLM applications. Use when the user asks about ax(), ai(), f(), s(), agent(), flow(), AxGen, AxAgent, AxFlow, signatures, streaming, or mentions @ax-llm/ax.
|
||||||
|
version: "19.0.33"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ax Library (@ax-llm/ax) Quick Reference
|
||||||
|
|
||||||
|
Ax is a TypeScript library for building LLM-powered applications with type-safe signatures, streaming support, and multi-provider compatibility.
|
||||||
|
|
||||||
|
> **Detailed skills available:** ax-ai (providers), ax-signature (signatures/types), ax-gen (generators), ax-agent (agents/runtime), ax-agent-optimize (agent tuning/eval), ax-flow (workflows), ax-gepa (Pareto optimization), ax-learn (self-improving agents).
|
||||||
|
|
||||||
|
## Imports & Factories
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Prefer factory functions: ax(), ai(), agent(), flow() — not new AxGen(), new AxAI(), etc.
|
||||||
|
import { ax, ai, f, s, fn, agent, flow, AxMemory, AxMCPClient, AxLearn } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
// AI provider
|
||||||
|
const llm = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY });
|
||||||
|
|
||||||
|
// Generator (from string signature)
|
||||||
|
const gen = ax('question:string -> answer:string');
|
||||||
|
|
||||||
|
// Generator (from fluent signature)
|
||||||
|
const gen = ax(
|
||||||
|
f()
|
||||||
|
.input('question', f.string('User question'))
|
||||||
|
.output('answer', f.string('AI response'))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reusable signature
|
||||||
|
const sig = s('question:string, context:string[] -> answer:string');
|
||||||
|
|
||||||
|
// Agent
|
||||||
|
const myAgent = agent('userInput:string -> response:string', {
|
||||||
|
name: 'helper',
|
||||||
|
description: 'A helpful assistant',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flow
|
||||||
|
const wf = flow<{ input: string }, { output: string }>()
|
||||||
|
.node('step1', 'input:string -> output:string')
|
||||||
|
.execute('step1', (state) => ({ input: state.input }))
|
||||||
|
.returns((state) => ({ output: state.step1Result.output }));
|
||||||
|
|
||||||
|
// Function tool
|
||||||
|
const tool = fn('search')
|
||||||
|
.description('Search the web')
|
||||||
|
.arg('query', f.string('Search query'))
|
||||||
|
.returns(f.string('Search results'))
|
||||||
|
.handler(({ query }) => searchWeb(query))
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Forward (blocking)
|
||||||
|
const result = await gen.forward(llm, { question: 'What is 2+2?' });
|
||||||
|
|
||||||
|
// Streaming
|
||||||
|
for await (const chunk of gen.streamingForward(llm, { question: 'Tell a story' })) {
|
||||||
|
if (chunk.delta.answer) process.stdout.write(chunk.delta.answer);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Forward Options Quick Reference
|
||||||
|
|
||||||
|
| Goal | Option | Example |
|
||||||
|
|------|--------|---------|
|
||||||
|
| Model override | `model` | `{ model: 'gpt-4o-mini' }` |
|
||||||
|
| Temperature | `modelConfig.temperature` | `{ modelConfig: { temperature: 0.8 } }` |
|
||||||
|
| Max tokens | `modelConfig.maxTokens` | `{ modelConfig: { maxTokens: 500 } }` |
|
||||||
|
| Retry on failure | `maxRetries` | `{ maxRetries: 3 }` |
|
||||||
|
| Max agent steps | `maxSteps` | `{ maxSteps: 10 }` |
|
||||||
|
| Fail fast | `fastFail` | `{ fastFail: true }` |
|
||||||
|
| Thinking budget | `thinkingTokenBudget` | `{ thinkingTokenBudget: 'medium' }` |
|
||||||
|
| Show thoughts | `showThoughts` | `{ showThoughts: true }` |
|
||||||
|
| Context caching | `contextCache` | `{ contextCache: { cacheBreakpoint: 'after-examples' } }` |
|
||||||
|
| Multi-sampling | `sampleCount` | `{ sampleCount: 5 }` |
|
||||||
|
| Debug logging | `debug` | `{ debug: true }` |
|
||||||
|
| Abort signal | `abortSignal` | `{ abortSignal: controller.signal }` |
|
||||||
|
| Memory | `mem` | `{ mem: new AxMemory() }` |
|
||||||
|
| Stop function | `stopFunction` | `{ stopFunction: 'finalAnswer' }` |
|
||||||
|
| Function mode | `functionCallMode` | `{ functionCallMode: 'auto' }` |
|
||||||
|
|
||||||
|
## Memory and Context
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AxMemory } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
const memory = new AxMemory();
|
||||||
|
|
||||||
|
// Multi-turn conversation
|
||||||
|
await gen.forward(llm, { userMessage: 'My name is Alice' }, { mem: memory });
|
||||||
|
const r = await gen.forward(llm, { userMessage: 'What is my name?' }, { mem: memory });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Few-Shot Examples
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const classifier = ax('reviewText:string -> sentiment:class "positive, negative, neutral"');
|
||||||
|
|
||||||
|
classifier.setExamples([
|
||||||
|
{ reviewText: 'I love this!', sentiment: 'positive' },
|
||||||
|
{ reviewText: 'Terrible.', sentiment: 'negative' },
|
||||||
|
{ reviewText: 'It works.', sentiment: 'neutral' },
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Classification
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const classifier = ax(
|
||||||
|
f()
|
||||||
|
.input('text', f.string())
|
||||||
|
.output('category', f.class(['spam', 'ham', 'uncertain']))
|
||||||
|
.output('confidence', f.number().min(0).max(1))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extraction
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const extractor = ax(
|
||||||
|
f()
|
||||||
|
.input('text', f.string())
|
||||||
|
.output('entities', f.object({
|
||||||
|
people: f.string().array(),
|
||||||
|
organizations: f.string().array(),
|
||||||
|
locations: f.string().array()
|
||||||
|
}))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-modal (Images)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const analyzer = ax(
|
||||||
|
f()
|
||||||
|
.input('image', f.image('Image to analyze'))
|
||||||
|
.input('question', f.string('Question').optional())
|
||||||
|
.output('description', f.string())
|
||||||
|
.output('objects', f.string().array())
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await analyzer.forward(llm, {
|
||||||
|
image: { mimeType: 'image/jpeg', data: base64Data },
|
||||||
|
question: 'What objects are in this image?'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Chaining Generators
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const researcher = ax('topic:string -> research:string, keyFacts:string[]');
|
||||||
|
const writer = ax('research:string, keyFacts:string[] -> article:string');
|
||||||
|
|
||||||
|
const research = await researcher.forward(llm, { topic: 'AGI' });
|
||||||
|
const draft = await writer.forward(llm, { research: research.research, keyFacts: research.keyFacts });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AxGenerateError, AxAIServiceError, AxAIServiceAbortedError } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await gen.forward(llm, { input: 'test' });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxGenerateError) {
|
||||||
|
console.error('Generation failed:', error.details.model, error.details.signature);
|
||||||
|
} else if (error instanceof AxAIServiceAbortedError) {
|
||||||
|
console.log('Request was aborted');
|
||||||
|
} else if (error instanceof AxAIServiceError) {
|
||||||
|
console.error('AI service error:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { axCreateDefaultColorLogger } from '@ax-llm/ax';
|
||||||
|
|
||||||
|
const result = await gen.forward(llm, { input: 'test' }, {
|
||||||
|
debug: true,
|
||||||
|
logger: axCreateDefaultColorLogger(),
|
||||||
|
// OpenTelemetry
|
||||||
|
tracer: openTelemetryTracer,
|
||||||
|
meter: openTelemetryMeter,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Integration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AxMCPClient } from '@ax-llm/ax';
|
||||||
|
import { AxMCPStdioTransport } from '@ax-llm/ax-tools';
|
||||||
|
|
||||||
|
// Stdio transport (local MCP server)
|
||||||
|
const transport = new AxMCPStdioTransport({
|
||||||
|
command: 'npx',
|
||||||
|
args: ['-y', '@modelcontextprotocol/server-memory'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const mcpClient = new AxMCPClient(transport, { debug: false });
|
||||||
|
await mcpClient.init();
|
||||||
|
|
||||||
|
// Use with agent
|
||||||
|
const myAgent = agent('userMessage:string -> response:string', {
|
||||||
|
name: 'assistant',
|
||||||
|
description: 'An assistant with MCP tools',
|
||||||
|
functions: [mcpClient],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP Transport (Remote MCP)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AxMCPStreambleHTTPTransport } from '@ax-llm/ax/mcp/transports/httpStreamTransport.js';
|
||||||
|
|
||||||
|
const transport = new AxMCPStreambleHTTPTransport('https://remote.mcp.pipedream.net', {
|
||||||
|
headers: { 'x-pd-project-id': projectId },
|
||||||
|
authorization: `Bearer ${accessToken}`,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### MCP Capabilities
|
||||||
|
|
||||||
|
| Capability | Prefix | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| Tools | *(none)* | Function calls |
|
||||||
|
| Prompts | `prompt_` | Prompt templates |
|
||||||
|
| Resources | `resource_` | File/data access |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const caps = mcpClient.getCapabilities();
|
||||||
|
const functions = mcpClient.toFunction();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Function Overrides
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const mcpClient = new AxMCPClient(transport, {
|
||||||
|
functionOverrides: [
|
||||||
|
{ name: 'search_documents', updates: { name: 'findDocs', description: 'Search docs' } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Reference
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class AxGen<IN, OUT> {
|
||||||
|
forward(ai: AxAIService, values: IN, options?: AxProgramForwardOptions): Promise<OUT>;
|
||||||
|
streamingForward(ai: AxAIService, values: IN, options?: AxProgramStreamingForwardOptions): AsyncGenerator<{ delta: Partial<OUT> }>;
|
||||||
|
setExamples(examples: Array<Partial<IN & OUT>>): void;
|
||||||
|
addAssert(fn: (output: OUT) => boolean, message?: string): void;
|
||||||
|
addFieldProcessor(field: keyof OUT, fn: (value: any) => any): void;
|
||||||
|
addStreamingFieldProcessor(field: keyof OUT, fn: (chunk: string, ctx: any) => void): void;
|
||||||
|
stop(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AxAgent<IN, OUT> {
|
||||||
|
forward(ai: AxAIService, values: IN, options?: AxAgentOptions): Promise<OUT>;
|
||||||
|
streamingForward(ai: AxAIService, values: IN, options?: AxAgentOptions): AsyncGenerator<{ delta: Partial<OUT> }>;
|
||||||
|
getFunction(): AxFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AxFlow<IN, OUT> {
|
||||||
|
node(name: string, signature: string | AxSignature): AxFlow;
|
||||||
|
execute(name: string, mapper: (state) => any): AxFlow;
|
||||||
|
returns(mapper: (state) => OUT): AxFlow;
|
||||||
|
forward(ai: AxAIService, values: IN): Promise<OUT>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Fetch these for full working code:
|
||||||
|
|
||||||
|
- [Chat](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/chat.ts) — multi-turn conversation
|
||||||
|
- [Marketing](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/marketing.ts) — product use case
|
||||||
|
- [MCP Integration](https://raw.githubusercontent.com/ax-llm/ax/refs/heads/main/src/examples/mcp-client-memory.ts) — MCP integration
|
||||||
18
.dockerignore
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
.claude
|
||||||
|
.mind
|
||||||
|
.superpowers
|
||||||
|
*.mind
|
||||||
|
app/dist
|
||||||
|
app/src-tauri
|
||||||
|
sidecar
|
||||||
|
docs
|
||||||
|
Brainstorming
|
||||||
|
UI-UX
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
docker-compose*.yml
|
||||||
|
render.yaml
|
||||||
|
.env*
|
||||||
|
*.log
|
||||||
76
.env.example
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# ── LLM provider API keys ───────────────────────────────────────────
|
||||||
|
# CANONICAL STORE = the encrypted vault (~/.waggle/vault.json), set via the UI
|
||||||
|
# (Settings → API Keys). The server hydrates process.env FROM the vault at boot,
|
||||||
|
# so you normally do NOT set provider keys here.
|
||||||
|
#
|
||||||
|
# Full provider set the app can use (all vault-managed, keyed by provider id):
|
||||||
|
# anthropic · openai · google (gemini) · xai · deepseek · openrouter · mistral
|
||||||
|
# · moonshot (kimi) · alibaba (dashscope/qwen) · minimax · zhipu (glm)
|
||||||
|
# · perplexity · genspark + tavily (web search)
|
||||||
|
#
|
||||||
|
# Only the following are ALSO read directly from env (for CI / headless runs
|
||||||
|
# where the vault UI isn't available):
|
||||||
|
# ANTHROPIC_API_KEY=
|
||||||
|
# OPENAI_API_KEY=
|
||||||
|
# VOYAGE_API_KEY= # embeddings only (EMBEDDING_PROVIDER=voyage)
|
||||||
|
|
||||||
|
# LiteLLM proxy master key
|
||||||
|
LITELLM_MASTER_KEY=sk-waggle-dev
|
||||||
|
|
||||||
|
# LiteLLM proxy URL (for multi-model routing)
|
||||||
|
LITELLM_BASE_URL=http://localhost:4000
|
||||||
|
|
||||||
|
# ── Embedding Provider (M2-1) ───────────────────────────────────────
|
||||||
|
# Default: auto (tries InProcess → Ollama → API → mock)
|
||||||
|
# Options: auto, inprocess, ollama, voyage, openai, mock
|
||||||
|
# EMBEDDING_PROVIDER=auto
|
||||||
|
|
||||||
|
# In-process (default — zero config, works offline)
|
||||||
|
# Model downloads ~23MB on first launch, cached in ~/.waggle/models/
|
||||||
|
# EMBEDDING_MODEL=Xenova/all-MiniLM-L6-v2
|
||||||
|
|
||||||
|
# Ollama (power user — needs Ollama installed: https://ollama.com)
|
||||||
|
# OLLAMA_HOST=http://localhost:11434
|
||||||
|
# OLLAMA_EMBED_MODEL=nomic-embed-text
|
||||||
|
|
||||||
|
# API Providers (set keys in the app: Vault → Secrets → Embedding Providers)
|
||||||
|
# Or set here for CI/headless only:
|
||||||
|
# VOYAGE_API_KEY=
|
||||||
|
|
||||||
|
# M3 Server Configuration
|
||||||
|
DATABASE_URL=postgres://waggle:waggle_dev@localhost:5434/waggle
|
||||||
|
REDIS_URL=redis://localhost:6381
|
||||||
|
CLERK_SECRET_KEY=sk_test_...
|
||||||
|
CLERK_PUBLISHABLE_KEY=pk_test_...
|
||||||
|
PORT=3100
|
||||||
|
CORS_ORIGIN=http://localhost:8080
|
||||||
|
|
||||||
|
# ── Stripe (M2-2 Billing) ──────────────────────────────────────────
|
||||||
|
# Required for paid tier checkout and subscription management.
|
||||||
|
# Get keys from https://dashboard.stripe.com/apikeys
|
||||||
|
# STRIPE_SECRET_KEY=
|
||||||
|
# STRIPE_WEBHOOK_SECRET=
|
||||||
|
# Price IDs. Since the Solo-vs-Team collapse (2026-07-05) TEAMS is the ONLY active
|
||||||
|
# checkout price. The PRO vars are legacy — still recognized on inbound webhooks
|
||||||
|
# (a legacy PRO subscriber resolves to Solo/FREE, never a removed tier), but no new
|
||||||
|
# PRO checkout is offered.
|
||||||
|
# STRIPE_PRICE_TEAMS_MONTHLY= # active — the only new-checkout price
|
||||||
|
# STRIPE_PRICE_TEAMS_ANNUAL= # active
|
||||||
|
# STRIPE_PRICE_PRO_MONTHLY= # legacy — webhook-recognized, resolves to Solo/FREE
|
||||||
|
# STRIPE_PRICE_PRO_ANNUAL= # legacy — webhook-recognized, resolves to Solo/FREE
|
||||||
|
# Legacy single-price vars (still resolved as a fallback):
|
||||||
|
# STRIPE_PRICE_BASIC= # legacy — webhook-recognized, resolves to Solo/FREE
|
||||||
|
# STRIPE_PRICE_PRO= # legacy — webhook-recognized, resolves to Solo/FREE
|
||||||
|
# STRIPE_PRICE_TEAMS= # legacy single-var fallback for TEAMS
|
||||||
|
|
||||||
|
# ── MinIO (Team File Storage) ──────────────────────────────────────
|
||||||
|
# S3-compatible object storage for shared team workspace files.
|
||||||
|
# docker compose up starts MinIO + auto-creates waggle-files bucket.
|
||||||
|
MINIO_ENDPOINT=localhost:9000
|
||||||
|
MINIO_BUCKET=waggle-files
|
||||||
|
MINIO_ACCESS_KEY=waggle
|
||||||
|
MINIO_SECRET_KEY=waggle_s3_dev
|
||||||
|
|
||||||
|
# ── Teams Server (M3 PostgreSQL) ──────────────────────────────────
|
||||||
|
# When DATABASE_URL is set, the Teams server starts alongside the local server.
|
||||||
|
# TEAMS_SERVER_PORT=3101
|
||||||
61
.gitattributes
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Sprint 12 Task 1 Blocker #1 (2026-04-22):
|
||||||
|
# Pin benchmark canonical archive + meta to byte-exact treatment. The
|
||||||
|
# dataset_version hash is computed over the raw bytes at runtime (see
|
||||||
|
# benchmarks/harness/src/datasets.ts getDatasetVersion). If Git converts
|
||||||
|
# line endings on checkout the hash drifts per-platform, breaking
|
||||||
|
# H-AUDIT-2 replication across Windows/macOS/Linux clones. `-text` marks
|
||||||
|
# these files as binary — no auto-conversion regardless of autocrlf.
|
||||||
|
benchmarks/data/locomo/locomo-1540.jsonl -text
|
||||||
|
benchmarks/data/locomo/locomo-1540.meta.json -text
|
||||||
|
|
||||||
|
# Sprint 12 Task 2.5 Stage 3 (2026-04-24):
|
||||||
|
# Same byte-exact treatment for pre-registration manifests. The YAML +
|
||||||
|
# md SHA-256 recorded in the anchor commit message is computed against
|
||||||
|
# these files' on-disk bytes; we need checkout bytes to match across
|
||||||
|
# platforms so any auditor can re-verify the hash after `git clone`.
|
||||||
|
benchmarks/results/manifest-*.md -text
|
||||||
|
benchmarks/results/manifest-*.yaml -text
|
||||||
|
benchmarks/results/stage*-gate-*.md -text
|
||||||
|
benchmarks/results/stage*-gate-*.jsonl -text
|
||||||
|
|
||||||
|
# Sprint 12 Task 2.5 Stage 3 (v5 emission 2026-04-24):
|
||||||
|
# Pre-registration manifests relocated to benchmarks/preregistration/ per
|
||||||
|
# PM brief §Step 1. Same byte-exact treatment as v4.
|
||||||
|
benchmarks/preregistration/manifest-*.md -text
|
||||||
|
benchmarks/preregistration/manifest-*.yaml -text
|
||||||
|
|
||||||
|
# Sprint 12 Task 2.5 Stage 3 §1.3f (2026-04-24):
|
||||||
|
# Vertex Batch eligibility probe artefacts — byte-exact audit.
|
||||||
|
benchmarks/probes/**/*.log -text
|
||||||
|
benchmarks/probes/**/*.jsonl -text
|
||||||
|
benchmarks/probes/**/*.py -text
|
||||||
|
benchmarks/probes/**/*.md -text
|
||||||
|
|
||||||
|
# Sprint 12 Task 2.5 Stage 3 v6 N=400 final deliverables (2026-04-25):
|
||||||
|
# Same byte-exact treatment for the agentic-cell evidence JSONL + Phase C
|
||||||
|
# 5-cell summary / Fisher analysis / memo. The agentic JSONL feeds the
|
||||||
|
# H1 secondary-endpoint S4 reproducibility chain (see commit message body
|
||||||
|
# for the file-level SHA-256 capture).
|
||||||
|
benchmarks/results/stage3-n400-v6-*.md -text
|
||||||
|
benchmarks/results/agentic-locomo-2026-04-25T*.jsonl -text
|
||||||
|
benchmarks/results/agentic-locomo-2026-04-25T*.summary.json -text
|
||||||
|
|
||||||
|
# v6 self-judge re-evaluation (2026-04-25): apples-to-apples vs Mem0.
|
||||||
|
# Same byte-exact treatment for the 2000-record results JSONL + comparison
|
||||||
|
# md + memo. SHA-256 of these files captured in commit message body.
|
||||||
|
benchmarks/results/v6-self-judge-rebench/*.jsonl -text
|
||||||
|
benchmarks/results/v6-self-judge-rebench/*.md -text
|
||||||
|
|
||||||
|
# Agentic knowledge work pilot 2026-04-26 (FAIL verdict). All artefacts
|
||||||
|
# byte-exact pinned for audit. SHA-256s captured in commit body.
|
||||||
|
benchmarks/results/pilot-2026-04-26/*.jsonl -text
|
||||||
|
benchmarks/results/pilot-2026-04-26/*.json -text
|
||||||
|
benchmarks/results/pilot-2026-04-26/*.log -text
|
||||||
|
benchmarks/results/pilot-2026-04-26/prompts-archive/*.md -text
|
||||||
|
benchmarks/results/pilot-2026-04-26/invalidated/*.jsonl -text
|
||||||
|
decisions/*.md -text
|
||||||
|
|
||||||
|
# Installer arc (steal #5, 2026-07-10): the shell installer + process manager
|
||||||
|
# are consumed by `curl | bash` on Linux/macOS. A CRLF checkout would break the
|
||||||
|
# shebang and `read`/`printf` parsing, so pin them to LF regardless of core.autocrlf.
|
||||||
|
*.sh text eol=lf
|
||||||
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Report something that isn't working as expected
|
||||||
|
title: "[bug] "
|
||||||
|
labels: bug
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
## Describe the bug
|
||||||
|
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
## To reproduce
|
||||||
|
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. See error
|
||||||
|
|
||||||
|
## Expected behavior
|
||||||
|
|
||||||
|
What you expected to happen instead.
|
||||||
|
|
||||||
|
## Screenshots / logs
|
||||||
|
|
||||||
|
If applicable, add screenshots or paste relevant log output. **Redact any API
|
||||||
|
keys, tokens, or personal data before pasting.**
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- Waggle build: [desktop (Windows/macOS) or web / `npm run dev`]
|
||||||
|
- App version or commit SHA:
|
||||||
|
- OS and version:
|
||||||
|
- Node version (`node -v`):
|
||||||
|
|
||||||
|
## Additional context
|
||||||
|
|
||||||
|
Anything else that might help — e.g. which workspace/persona, whether it is
|
||||||
|
reproducible, when it started.
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Security vulnerability
|
||||||
|
url: https://github.com/marolinik/waggle-os/security/advisories/new
|
||||||
|
about: Please report security issues privately, not as a public issue. See SECURITY.md.
|
||||||
|
- name: Question or discussion
|
||||||
|
url: https://github.com/marolinik/waggle-os/discussions
|
||||||
|
about: Ask questions, propose ideas, or discuss architecture here.
|
||||||
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea or improvement for Waggle OS
|
||||||
|
title: "[feature] "
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
What problem are you trying to solve? What's the use case? A feature request
|
||||||
|
grounded in a real workflow is much easier to evaluate than a solution in search
|
||||||
|
of a problem.
|
||||||
|
|
||||||
|
## Proposed solution
|
||||||
|
|
||||||
|
Describe what you'd like to happen.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
Any alternative approaches or workarounds you've thought about or tried.
|
||||||
|
|
||||||
|
## Additional context
|
||||||
|
|
||||||
|
Mockups, links, or references to prior art. Note if this touches an existing
|
||||||
|
area (memory, connectors, personas, marketplace, etc.).
|
||||||
37
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<!--
|
||||||
|
Thanks for contributing to Waggle OS! Please read docs/CONTRIBUTING.md and the
|
||||||
|
root CLAUDE.md before opening a PR. Keep each PR to one focused change.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## What & why
|
||||||
|
|
||||||
|
<!-- What does this change do, and why is it needed? Link any related issue. -->
|
||||||
|
|
||||||
|
Closes #
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
- [ ] `feat` — new feature
|
||||||
|
- [ ] `fix` — bug fix
|
||||||
|
- [ ] `refactor` — code restructuring (no behavior change)
|
||||||
|
- [ ] `test` — tests only
|
||||||
|
- [ ] `docs` — documentation only
|
||||||
|
- [ ] `chore` / `perf` / `ci`
|
||||||
|
|
||||||
|
## How it was tested
|
||||||
|
|
||||||
|
<!-- Commands you ran and what you observed. Bug fixes should add a regression test. -->
|
||||||
|
|
||||||
|
- [ ] `npm run test` (Vitest) passes
|
||||||
|
- [ ] `npx tsc --noEmit` passes for the package(s) I touched
|
||||||
|
- [ ] `npm run lint` passes
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] The change is scoped to one concern (no unrelated edits or reformatting).
|
||||||
|
- [ ] New behavior has accompanying tests; bug fixes have a regression test.
|
||||||
|
- [ ] No secrets, API keys, or credentials are committed.
|
||||||
|
- [ ] Docs updated if behavior, commands, or configuration changed.
|
||||||
|
- [ ] If this touches the memory substrate (`packages/hive-mind-core`), I read
|
||||||
|
CLAUDE.md §7.5 (the monorepo is the source of truth; do not author
|
||||||
|
substrate features directly on the OSS mirror).
|
||||||
39
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Dependabot — dependency & supply-chain update gate for Waggle OS.
|
||||||
|
# Docs: https://docs.github.com/code-security/dependabot/dependabot-version-updates
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# npm — one entry at the repo root. This is an npm workspaces monorepo with a
|
||||||
|
# SINGLE root package-lock.json shared by every workspace (apps/*, packages/*),
|
||||||
|
# so Dependabot's workspace-aware npm updater covers the root manifest AND all
|
||||||
|
# workspace package.json files from `directory: "/"`. (The plural `directories`
|
||||||
|
# form is for repos with independent per-directory lockfiles — not this layout.)
|
||||||
|
# Minor+patch bumps are grouped into one PR per run to keep review volume low;
|
||||||
|
# majors stay ungrouped so breaking changes land as isolated, reviewable PRs.
|
||||||
|
#
|
||||||
|
# Intentionally NOT covered: `app/` (the Tauri desktop shell) is a standalone
|
||||||
|
# npm project outside the workspace with its own app/package-lock.json, and its
|
||||||
|
# Tauri toolchain is owned by the release/tauri workflows — add a separate npm
|
||||||
|
# entry with `directory: "/app"` only if that wave asks for it. `external/**`
|
||||||
|
# is vendored third-party code and is left alone.
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
groups:
|
||||||
|
npm-minor-and-patch:
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
|
||||||
|
# GitHub Actions pinned across .github/workflows/*.
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
groups:
|
||||||
|
actions-minor-and-patch:
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
242
.github/sync.md
vendored
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
> **⚠️ DEPRECATED (2026-04-30 monorepo migration) — HISTORICAL/AUDIT REFERENCE ONLY.**
|
||||||
|
> This manual describes the dual-repo bidirectional-sync mechanism that ran while the
|
||||||
|
> substrate lived in BOTH waggle-os (`packages/core/src/{mind,harvest}/`) and an external
|
||||||
|
> `marolinik/hive-mind`. After the migration the substrate lives ONLY at
|
||||||
|
> **`packages/hive-mind-core/src/{mind,harvest}/`**, and the OSS mirror is **generated** via
|
||||||
|
> `git subtree split` — see [`packages/hive-mind-core/CONTRIBUTING.md`](../packages/hive-mind-core/CONTRIBUTING.md)
|
||||||
|
> and [`scripts/oss-subtree-split.sh`](../scripts/oss-subtree-split.sh). The
|
||||||
|
> `mind-parity-check.yml` / `sync-mind.yml` workflows referenced below are **inert deprecation
|
||||||
|
> anchors** (their `packages/core/src/...` trigger paths no longer exist, so they never fire).
|
||||||
|
> Everything below is retained for historical context — do NOT treat it as the active process.
|
||||||
|
> See CLAUDE.md §7.5 for the current mechanism.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Memory Substrate Sync — `waggle-os` ↔ `hive-mind`
|
||||||
|
|
||||||
|
This document is the operating manual for the two GitHub Actions workflows
|
||||||
|
that keep `packages/core/src/mind/` and `packages/core/src/harvest/` in
|
||||||
|
sync with the OSS release artifact at
|
||||||
|
[`marolinik/hive-mind`](https://github.com/marolinik/hive-mind).
|
||||||
|
|
||||||
|
> **Audience:** anyone modifying files under `packages/core/src/mind/` or
|
||||||
|
> `packages/core/src/harvest/`. If you only touch `packages/agent/`,
|
||||||
|
> `packages/server/`, `apps/web/`, etc., none of this applies — those
|
||||||
|
> paths are explicitly Waggle-only per
|
||||||
|
> [`hive-mind/EXTRACTION.md`](https://github.com/marolinik/hive-mind/blob/master/EXTRACTION.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
| What you did | What happens |
|
||||||
|
|--------------|--------------|
|
||||||
|
| Modify `packages/core/src/mind/foo.ts` and open a PR | `mind-parity-check` runs hive-mind's tests against your change. Failure blocks merge unless allowlisted. |
|
||||||
|
| Merge the PR to `main` | `sync-mind-to-hive-mind` opens a PR on `marolinik/hive-mind` with the filtered diff. Manual review there before merge. |
|
||||||
|
| Want to skip a parity test that's intentionally divergent | Add the basename to `.parity-allowlist` with a comment explaining why. |
|
||||||
|
| Modify `packages/core/src/mind/vault.ts` (NOT-extracted) | Sync workflow filters this out automatically — nothing leaks to hive-mind. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why this exists
|
||||||
|
|
||||||
|
Both repos carry their own copy of `packages/core/src/mind/` and
|
||||||
|
`packages/core/src/harvest/`. The audit at
|
||||||
|
`PM-Waggle-OS/decisions/2026-04-26-memory-sync-audit.md` documented the
|
||||||
|
status quo before this workflow shipped:
|
||||||
|
|
||||||
|
- No automated sync existed; bug fixes flowed in both directions ad-hoc.
|
||||||
|
- Two production bug fixes that landed in hive-mind never made it back
|
||||||
|
to waggle-os until the manual Step 1 backport (commits `89c1004` +
|
||||||
|
`fed4a20`).
|
||||||
|
- The OSS release artifact and the production substrate had silently
|
||||||
|
drifted in 2/19 mind/ files.
|
||||||
|
|
||||||
|
The two workflows below close that gap. Their goal is **detection +
|
||||||
|
human-reviewed propagation**, never automatic merge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow 1 — `mind-parity-check.yml`
|
||||||
|
|
||||||
|
**File:** `.github/workflows/mind-parity-check.yml`
|
||||||
|
**Trigger:** PR or push to `main` that touches `packages/core/src/mind/`,
|
||||||
|
`packages/core/src/harvest/`, or `packages/core/tests/mind/`.
|
||||||
|
**Outcome:** test-pass = ✅ block-clear; test-fail = ❌ merge blocked.
|
||||||
|
|
||||||
|
### What it does, step by step
|
||||||
|
|
||||||
|
1. Checks out **both** repos — waggle-os in `./waggle-os/`, hive-mind
|
||||||
|
master in `./hive-mind/`.
|
||||||
|
2. Runs the **waggle-os baseline** mind/ test suite. This is the
|
||||||
|
committed Step 2 ports plus all pre-existing waggle-os mind/ tests.
|
||||||
|
If this fails, the PR is rejected on a regular regression — same as
|
||||||
|
any other failing test.
|
||||||
|
3. **Injects** the latest hive-mind tests into the waggle-os checkout
|
||||||
|
under `<basename>-hive-mind.test.ts` filenames, with import paths
|
||||||
|
adapted via `sed` from `./*.js` to `../../src/mind/*.js`. Three rules
|
||||||
|
govern what gets injected:
|
||||||
|
- **`.parity-allowlist`**: filenames listed here are skipped entirely.
|
||||||
|
- **Already-committed `-hive-mind` file**: kept as-is. The committed
|
||||||
|
version (typically a Step 2 port with bespoke header comments
|
||||||
|
documenting provenance and adaptation rationale) is what the parity
|
||||||
|
check exercises — overwriting it with the latest hive-mind verbatim
|
||||||
|
content would silently drop those headers and any waggle-os-side
|
||||||
|
adaptations.
|
||||||
|
- **No collision**: copy hive-mind file as `<basename>-hive-mind.test.ts`
|
||||||
|
into `tests/mind/`, sed the imports.
|
||||||
|
4. Runs the **combined suite** (waggle-os baseline + injected
|
||||||
|
hive-mind tests). If hive-mind has added new test cases since the
|
||||||
|
last Step 2 port, they'll surface here. Failure here means waggle-os
|
||||||
|
has accidentally diverged from hive-mind's surface contract.
|
||||||
|
5. Emits an **informational diff** of shared substrate file sizes —
|
||||||
|
not a gate, just visibility.
|
||||||
|
|
||||||
|
### When it fails
|
||||||
|
|
||||||
|
| Failure mode | What it means | What to do |
|
||||||
|
|--------------|---------------|------------|
|
||||||
|
| Baseline waggle-os mind/ tests fail | Regular regression | Fix your change |
|
||||||
|
| `<x>-hive-mind.test.ts` (suffixed) fails | hive-mind tests a behavior waggle-os doesn't honor any more | Decide: (a) accept and fix waggle-os to match hive-mind, OR (b) document intentional divergence and add to `.parity-allowlist` |
|
||||||
|
| Test fails because of import-path adaptation drift | hive-mind reorganized imports | Update the `sed` rules in `mind-parity-check.yml` |
|
||||||
|
| `db-hive-mind.test.ts` fails (always — see allowlist) | The proprietary-tables-must-be-absent assertion mismatches Waggle's schema | This is in `.parity-allowlist` already; if you removed it, restore it |
|
||||||
|
|
||||||
|
### `.parity-allowlist` policy
|
||||||
|
|
||||||
|
The file at the repo root lists test basenames whose verbatim hive-mind
|
||||||
|
copy is intentionally skipped during the parity check.
|
||||||
|
|
||||||
|
```
|
||||||
|
# Comment describing why this entry exists
|
||||||
|
file-name.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Adding an entry** requires a comment line directly above with the
|
||||||
|
divergence rationale and at least one cross-reference (EXTRACTION.md
|
||||||
|
section, PM-Waggle-OS memo, or related PR).
|
||||||
|
|
||||||
|
**Removing an entry** is allowed only when the divergence is resolved
|
||||||
|
— either hive-mind upstream changed or waggle-os adopted the upstream
|
||||||
|
behavior. Re-running parity check should pass without the entry first.
|
||||||
|
|
||||||
|
The current single entry is `db-hive-mind.test.ts` because hive-mind's
|
||||||
|
`db.test.ts` asserts proprietary tables (ai_interactions,
|
||||||
|
execution_traces, evolution_runs, improvement_signals, install_audit)
|
||||||
|
MUST BE ABSENT — its OSS-scrub guarantee. Waggle-os carries those
|
||||||
|
tables legitimately. The waggle-os adaptation lives in committed
|
||||||
|
`db.test.ts` (no suffix) which splits the original assertion into "OSS
|
||||||
|
shared must exist" (verbatim from hive-mind) + "Waggle-specific must
|
||||||
|
exist" (inverted). See
|
||||||
|
`PM-Waggle-OS/decisions/2026-04-26-memory-sync-step2-test-port-results.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow 2 — `sync-mind.yml`
|
||||||
|
|
||||||
|
**File:** `.github/workflows/sync-mind.yml`
|
||||||
|
**Trigger:** push to `main` that touches `packages/core/src/mind/` or
|
||||||
|
`packages/core/src/harvest/`.
|
||||||
|
**Outcome:** opens a PR on `marolinik/hive-mind` with the filtered
|
||||||
|
diff. **Never auto-merges.** The PR sits for manual review on the
|
||||||
|
hive-mind side.
|
||||||
|
|
||||||
|
### Direction note (binding)
|
||||||
|
|
||||||
|
This workflow implements ONLY the **waggle-os → hive-mind** direction.
|
||||||
|
The empirically primary direction (**hive-mind → waggle-os**) requires
|
||||||
|
a workflow living **inside the hive-mind repo**, which is out of scope
|
||||||
|
for the waggle-os Step 3 PR. It will be added via a sibling PR to
|
||||||
|
hive-mind once Step 3 here is ratified by PM. The current Memory Sync
|
||||||
|
Repair audit shows hive-mind is the more active substrate repo (ahead
|
||||||
|
in 2/5 audit dimensions, +14 organic test files), so the
|
||||||
|
hive-mind-side workflow carries the heavier production burden.
|
||||||
|
|
||||||
|
### Filter list — NOT-extracted paths
|
||||||
|
|
||||||
|
The workflow excludes these paths from the patch — leaking them into
|
||||||
|
hive-mind would put Waggle-specific code into the Apache-2.0 release
|
||||||
|
artifact:
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/core/src/mind/vault.ts
|
||||||
|
packages/core/src/mind/evolution-runs.ts
|
||||||
|
packages/core/src/mind/execution-traces.ts
|
||||||
|
packages/core/src/mind/improvement-signals.ts
|
||||||
|
packages/core/src/compliance/**
|
||||||
|
```
|
||||||
|
|
||||||
|
This list mirrors the "NOT Extracted" section of
|
||||||
|
[`hive-mind/EXTRACTION.md`](https://github.com/marolinik/hive-mind/blob/master/EXTRACTION.md).
|
||||||
|
**If you add a new NOT-extracted file, update the workflow's
|
||||||
|
`excluded_paths` array AND EXTRACTION.md in the same PR** — otherwise
|
||||||
|
the file will leak on the next mind/ push.
|
||||||
|
|
||||||
|
### Setup — `HIVE_MIND_SYNC_TOKEN`
|
||||||
|
|
||||||
|
This workflow needs a fine-grained PAT scoped to `marolinik/hive-mind`
|
||||||
|
with `pull_request: write` + `contents: write` permissions. Marko
|
||||||
|
configures it via:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh secret set HIVE_MIND_SYNC_TOKEN --repo marolinik/waggle-os
|
||||||
|
gh variable set MIND_SYNC_ENABLED --body 'true' --repo marolinik/waggle-os
|
||||||
|
```
|
||||||
|
|
||||||
|
The `MIND_SYNC_ENABLED` repository variable is the kill switch — set
|
||||||
|
it to `false` to disable the workflow entirely without removing the
|
||||||
|
secret. The workflow's `if:` condition checks this before running.
|
||||||
|
|
||||||
|
If `HIVE_MIND_SYNC_TOKEN` is unset, the workflow fails fast with a
|
||||||
|
clear error rather than silently skipping.
|
||||||
|
|
||||||
|
### When the patch doesn't apply
|
||||||
|
|
||||||
|
`git apply --3way` falls back to a 3-way merge when the index
|
||||||
|
mismatch is small. If even that fails, the workflow exits with an
|
||||||
|
error pointing to the source SHA. Manual reconciliation:
|
||||||
|
|
||||||
|
1. Check out hive-mind master locally.
|
||||||
|
2. `git apply` the patch from the workflow run's
|
||||||
|
`sync-patch-<sha>` artifact.
|
||||||
|
3. Resolve conflicts; commit on a branch named
|
||||||
|
`auto-sync/waggle-os-<short-sha>`.
|
||||||
|
4. Open the PR by hand following the same body template.
|
||||||
|
|
||||||
|
This is rare in practice because hive-mind's mind/ files are mostly
|
||||||
|
verbatim extractions of waggle-os's — any genuine conflict means
|
||||||
|
hive-mind has its own change at the same lines, which is exactly the
|
||||||
|
case the human review is supposed to catch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bidirectional bug fix protocol
|
||||||
|
|
||||||
|
When you find a bug whose fix should go into BOTH repos:
|
||||||
|
|
||||||
|
1. Fix it in waggle-os first (production-impacted).
|
||||||
|
2. Merge to waggle-os main → `sync-mind.yml` auto-opens a hive-mind PR.
|
||||||
|
3. Review and merge the hive-mind PR.
|
||||||
|
4. Confirm the next `mind-parity-check` on waggle-os main is green —
|
||||||
|
that closes the loop.
|
||||||
|
|
||||||
|
When the bug originates in hive-mind (e.g. an upstream contributor
|
||||||
|
reports it):
|
||||||
|
|
||||||
|
1. Wait for the hive-mind PR (or open it yourself).
|
||||||
|
2. After it merges, the eventual hive-mind→waggle-os auto-PR (when
|
||||||
|
that workflow ships) will pick it up.
|
||||||
|
3. Until then, manual cherry-pick to waggle-os, identical to Step 1
|
||||||
|
of the original sync repair (see Step 1 results memo at
|
||||||
|
`PM-Waggle-OS/decisions/2026-04-26-memory-sync-step1-results.md`
|
||||||
|
for the canonical pattern: commit message references hive-mind SHA
|
||||||
|
verbatim).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-references
|
||||||
|
|
||||||
|
- Audit + 3-step plan: `PM-Waggle-OS/decisions/2026-04-26-memory-sync-audit.md`
|
||||||
|
- Step 1 results (forward-port + bidirectional audit): `PM-Waggle-OS/decisions/2026-04-26-memory-sync-step1-results.md`
|
||||||
|
- Step 2 results (test port): `PM-Waggle-OS/decisions/2026-04-26-memory-sync-step2-test-port-results.md`
|
||||||
|
- Step 3 results (this workflow): `PM-Waggle-OS/decisions/2026-04-26-memory-sync-step3-cicd-results.md`
|
||||||
|
- EXTRACTION.md: `D:\Projects\hive-mind\EXTRACTION.md` (or [GitHub link](https://github.com/marolinik/hive-mind/blob/master/EXTRACTION.md))
|
||||||
179
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Cache npm dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-npm-
|
||||||
|
|
||||||
|
# NOTE: the switch to `npm ci` (deterministic, lockfile-faithful) is deferred
|
||||||
|
# to the deps/lockfile hardening pass, after the root package-lock.json is
|
||||||
|
# regenerated in sync with every workspace manifest. `npm install` is the
|
||||||
|
# safe, lockfile-tolerant install until then.
|
||||||
|
- run: npm install
|
||||||
|
|
||||||
|
# Real typecheck gate. The old step ran `npx tsc --noEmit` against the root
|
||||||
|
# tsconfig.json, whose `"files": ["apps/web/src/vite-env.d.ts"]` (and no
|
||||||
|
# `include`) typechecks essentially nothing. build:packages is the actual
|
||||||
|
# `tsc --build` chain for every @waggle/* package (shared → hive-mind-core →
|
||||||
|
# core → agent → server) and also emits the dist/ that apps/web unit tests
|
||||||
|
# import; typecheck:web is the real apps/web check (tsc -p tsconfig.app.json).
|
||||||
|
- name: Type check — workspace packages (tsc --build chain)
|
||||||
|
run: npm run build:packages
|
||||||
|
|
||||||
|
- name: Type check — web (apps/web, tsc -p tsconfig.app.json)
|
||||||
|
run: npm run typecheck:web
|
||||||
|
|
||||||
|
- name: Lint (root flat config)
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Tauri TS typecheck (app/scripts)
|
||||||
|
run: npx tsc -p app/tsconfig.json
|
||||||
|
|
||||||
|
# Runtime tests pack and install workspace packages. Build every ignored
|
||||||
|
# dist/ payload explicitly so CI proves a fresh checkout.
|
||||||
|
- name: Build package-install test runtimes
|
||||||
|
run: |
|
||||||
|
npm run build:hook-runtime
|
||||||
|
npm run build --workspace @waggle/cli
|
||||||
|
npm run build --workspace @waggle-ai/waggle
|
||||||
|
npm run build --workspace waggle-memory-mcp
|
||||||
|
|
||||||
|
- name: Unit tests — packages + cross-cutting (root vitest)
|
||||||
|
run: |
|
||||||
|
npm test -- \
|
||||||
|
--exclude=packages/cli/tests/cli-runtime.test.ts \
|
||||||
|
--exclude=packages/hive-mind-cli/tests/cli-help.test.ts \
|
||||||
|
--exclude=packages/hive-mind-mcp-server/tests/runtime.test.ts \
|
||||||
|
--exclude=packages/launcher/tests/cli.test.ts \
|
||||||
|
--exclude=packages/memory-mcp/tests/runtime.test.ts
|
||||||
|
|
||||||
|
# These tests each create a temporary project and run npm install. Running
|
||||||
|
# several cold installs in parallel makes individual test timeouts measure
|
||||||
|
# runner contention rather than package correctness.
|
||||||
|
- name: Package-install runtime tests (serial)
|
||||||
|
run: |
|
||||||
|
npx vitest run \
|
||||||
|
packages/cli/tests/cli-runtime.test.ts \
|
||||||
|
packages/hive-mind-cli/tests/cli-help.test.ts \
|
||||||
|
packages/hive-mind-mcp-server/tests/runtime.test.ts \
|
||||||
|
packages/launcher/tests/cli.test.ts \
|
||||||
|
packages/memory-mcp/tests/runtime.test.ts \
|
||||||
|
--maxWorkers=1 \
|
||||||
|
--no-file-parallelism
|
||||||
|
|
||||||
|
# The root vitest.config.ts excludes `apps/**`, so apps/web's own 131-file
|
||||||
|
# suite never ran in CI. Run it via its own vitest config (jsdom). Blocking.
|
||||||
|
- name: Unit tests — apps/web
|
||||||
|
run: npm run test -w apps/web
|
||||||
|
|
||||||
|
- name: Security audit (informational)
|
||||||
|
run: npm audit --audit-level=high
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
# ADVISORY / NON-BLOCKING. `continue-on-error: true` means a red e2e run does
|
||||||
|
# NOT block merges — by design. These specs are broad product/audit journeys
|
||||||
|
# (full-product-audit, power-user-stress, competitive-benchmarks, …) that are
|
||||||
|
# historically flake-prone, so gating merges on them would produce false reds.
|
||||||
|
# The blocking `e2e-smoke` job above covers the stable launch, settings,
|
||||||
|
# memory, workspace, and mobile regression slice. This broad job stays
|
||||||
|
# advisory so exploratory audit coverage can report flakes without blocking
|
||||||
|
# merges.
|
||||||
|
e2e-smoke:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: test
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Cache npm dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-npm-
|
||||||
|
|
||||||
|
- run: npm install
|
||||||
|
- name: Build packages
|
||||||
|
run: npm run build:packages
|
||||||
|
- name: Build frontend
|
||||||
|
run: npm run build
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
- name: Run blocking Playwright smoke journeys
|
||||||
|
run: npm run test:e2e:smoke
|
||||||
|
env:
|
||||||
|
WAGGLE_ECHO_MODE: "1"
|
||||||
|
NODE_ENV: test
|
||||||
|
WAGGLE_TRUST_LOCALHOST: "1"
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: test
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Cache npm dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-npm-
|
||||||
|
|
||||||
|
- run: npm install
|
||||||
|
|
||||||
|
# apps/web's tsc build imports @waggle/shared etc. which export dist/;
|
||||||
|
# build the workspace packages first (the deploy does this via build:all).
|
||||||
|
- name: Build packages
|
||||||
|
run: npm run build:packages
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Run Playwright E2E tests
|
||||||
|
run: npx playwright test tests/e2e/
|
||||||
|
env:
|
||||||
|
WAGGLE_ECHO_MODE: "1"
|
||||||
|
NODE_ENV: test
|
||||||
|
# D1: the e2e suite hits /api/* directly without a bearer token; trust
|
||||||
|
# loopback in CI's test server (prod default stays secure). Mirrors
|
||||||
|
# vitest.setup.ts and playwright.config.ts webServer.env.
|
||||||
|
WAGGLE_TRUST_LOCALHOST: "1"
|
||||||
|
|
||||||
|
- name: Upload Playwright report
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 14
|
||||||
99
.github/workflows/deploy-www.yml
vendored
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
name: Deploy Landing Page
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'apps/www/**'
|
||||||
|
- 'docs/methodology.md'
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
- '.github/workflows/deploy-www.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: www-production
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deployment_config:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: production
|
||||||
|
url: https://waggle-os.ai
|
||||||
|
outputs:
|
||||||
|
configured: ${{ steps.vercel.outputs.configured }}
|
||||||
|
env:
|
||||||
|
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||||
|
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||||
|
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- name: Check Vercel configuration
|
||||||
|
id: vercel
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ -n "$VERCEL_TOKEN" && -n "$VERCEL_ORG_ID" && -n "$VERCEL_PROJECT_ID" ]]; then
|
||||||
|
echo "configured=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "configured=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::warning::Landing page verified but not deployed: Vercel secrets are not configured."
|
||||||
|
fi
|
||||||
|
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Test public site
|
||||||
|
run: npm run test -w apps/www -- --reporter=dot
|
||||||
|
|
||||||
|
- name: Typecheck public site
|
||||||
|
run: npx tsc --noEmit --project apps/www/tsconfig.json
|
||||||
|
|
||||||
|
- name: Build public site
|
||||||
|
run: npm run build:www
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [deployment_config, verify]
|
||||||
|
if: needs.deployment_config.outputs.configured == 'true'
|
||||||
|
environment:
|
||||||
|
name: production
|
||||||
|
url: https://waggle-os.ai
|
||||||
|
env:
|
||||||
|
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||||
|
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||||
|
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Pull Vercel production environment
|
||||||
|
working-directory: apps/www
|
||||||
|
run: npx --yes vercel pull --yes --environment=production --token="$VERCEL_TOKEN"
|
||||||
|
|
||||||
|
- name: Build Vercel prebuilt output
|
||||||
|
working-directory: apps/www
|
||||||
|
run: npx --yes vercel build --prod --token="$VERCEL_TOKEN"
|
||||||
|
|
||||||
|
- name: Deploy Vercel prebuilt output
|
||||||
|
working-directory: apps/www
|
||||||
|
run: npx --yes vercel deploy --prebuilt --prod --token="$VERCEL_TOKEN"
|
||||||
101
.github/workflows/hive-mind-cli-cross-platform.yml
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
name: hive-mind-cli cross-platform install + smoke
|
||||||
|
|
||||||
|
# Wave 1 cleanup brief 2026-04-29 §3.5 — windows-latest CI regression test.
|
||||||
|
# Verifies that `npm install -g @waggle/hive-mind-cli` followed by `hive-mind-cli doctor`
|
||||||
|
# works without ENOENT, quarantine, or manual debug on Windows + macOS + Linux.
|
||||||
|
#
|
||||||
|
# Acceptance per feedback_memory_install_dead_simple binding rule:
|
||||||
|
# - All three OS matrix cells PASS green
|
||||||
|
# - Zero manual intervention required
|
||||||
|
# - First MCP-style tool call (the doctor smoke test) succeeds in <60s
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, 'feature/**']
|
||||||
|
paths:
|
||||||
|
- 'packages/hive-mind-cli/**'
|
||||||
|
- 'packages/hive-mind-shim-core/**'
|
||||||
|
- 'packages/hive-mind-core/**'
|
||||||
|
- 'packages/hive-mind-mcp-server/**'
|
||||||
|
- 'packages/hive-mind-wiki-compiler/**'
|
||||||
|
- 'packages/hive-mind-hooks-claude-code/**'
|
||||||
|
- '.github/workflows/hive-mind-cli-cross-platform.yml'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'packages/hive-mind-cli/**'
|
||||||
|
- 'packages/hive-mind-shim-core/**'
|
||||||
|
- 'packages/hive-mind-core/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
install-and-smoke:
|
||||||
|
name: ${{ matrix.os }} install + smoke
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
node-version: ['20.x']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Install workspace deps
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
# hive-mind-core imports @waggle/shared, whose dist/ is gitignored and so
|
||||||
|
# absent on a clean checkout. hive-mind-core/tsconfig declares no project
|
||||||
|
# reference to it (the OSS subtree-split mirrors that tsconfig, so a
|
||||||
|
# ../shared reference would dangle in the export root), so `tsc --build`
|
||||||
|
# won't bootstrap it. Build shared first — same as ci.yml/tauri-build-pr.yml.
|
||||||
|
- name: Build @waggle/shared (hive-mind-core dep)
|
||||||
|
run: cd packages/shared && npx tsc --build
|
||||||
|
|
||||||
|
- name: Build hive-mind-core (substrate)
|
||||||
|
run: cd packages/hive-mind-core && npx tsc --build
|
||||||
|
|
||||||
|
- name: Build hive-mind-wiki-compiler
|
||||||
|
run: cd packages/hive-mind-wiki-compiler && npx tsc --build
|
||||||
|
|
||||||
|
- name: Build hive-mind-mcp-server
|
||||||
|
run: cd packages/hive-mind-mcp-server && npx tsc --build
|
||||||
|
|
||||||
|
- name: Build hive-mind-cli
|
||||||
|
run: cd packages/hive-mind-cli && npx tsc --build
|
||||||
|
|
||||||
|
- name: Run postinstall (bundles fix on win32, no-op POSIX)
|
||||||
|
run: node packages/hive-mind-cli/postinstall.cjs
|
||||||
|
|
||||||
|
- name: Init hive-mind data dir (scaffold personal.mind for the smoke)
|
||||||
|
run: node packages/hive-mind-cli/dist/index.js init
|
||||||
|
env:
|
||||||
|
HIVE_MIND_DATA_DIR: ${{ runner.temp }}/waggle-ci-home
|
||||||
|
|
||||||
|
- name: Smoke test — hive-mind-cli doctor (independent of upstream hook)
|
||||||
|
run: node packages/hive-mind-cli/dist/index.js doctor
|
||||||
|
env:
|
||||||
|
# doctor + init resolve the mind via HIVE_MIND_DATA_DIR (the CLI never
|
||||||
|
# reads WAGGLE_HOME — that prior env was a no-op). Isolate to a CI temp
|
||||||
|
# dir; init above scaffolds personal.mind there so doctor finds it.
|
||||||
|
HIVE_MIND_DATA_DIR: ${{ runner.temp }}/waggle-ci-home
|
||||||
|
|
||||||
|
- name: Doctor smoke result must be green (no quarantine, no ENOENT)
|
||||||
|
run: |
|
||||||
|
# If we got here, doctor exited 0 — green. Re-emit a confirmation marker
|
||||||
|
# so log scrapers see the success line clearly.
|
||||||
|
echo "::notice title=hive-mind-cli doctor passed::Cross-platform install + smoke verified on ${{ matrix.os }}"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
acceptance:
|
||||||
|
name: Wave 1 acceptance gate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: install-and-smoke
|
||||||
|
steps:
|
||||||
|
- name: All matrix cells passed
|
||||||
|
run: echo "Wave 1 dead-simple acceptance criteria met across windows-latest + macos-latest + ubuntu-latest."
|
||||||
115
.github/workflows/installer-smoke.yml
vendored
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
name: Installer Smoke
|
||||||
|
|
||||||
|
# Authoritative Linux proof for the one-line self-host installer (steal #5).
|
||||||
|
# The installer is developed on Windows, where the esbuild-hoist trap and CRLF
|
||||||
|
# quirks are Windows-only — so a green `bash -n` locally is NOT proof it works
|
||||||
|
# for the VPS/homelab audience. This job runs install.sh end-to-end from a
|
||||||
|
# clean ubuntu runner: it actually installs, builds packages, boots the sidecar,
|
||||||
|
# and asserts /health returns 200, then tears it down and asserts the port frees.
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- install.sh
|
||||||
|
- scripts/waggle-server.sh
|
||||||
|
- .github/workflows/installer-smoke.yml
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- install.sh
|
||||||
|
- scripts/waggle-server.sh
|
||||||
|
- .github/workflows/installer-smoke.yml
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
installer-smoke:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
env:
|
||||||
|
WAGGLE_PORT: "3947"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# The `runner` context is not available in job-level env, so the scratch
|
||||||
|
# paths (isolated from the checkout to exercise the real "copy + fresh
|
||||||
|
# install" path) are computed here from $RUNNER_TEMP instead.
|
||||||
|
- name: Compute scratch paths
|
||||||
|
run: |
|
||||||
|
echo "WAGGLE_INSTALL_DIR=$RUNNER_TEMP/waggle" >> "$GITHUB_ENV"
|
||||||
|
echo "WAGGLE_DATA_DIR=$RUNNER_TEMP/wdata" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
# shellcheck ships pre-installed on ubuntu-latest runners. Findings are
|
||||||
|
# failures — the installer is user-facing shell that runs unsupervised via
|
||||||
|
# `curl | bash`, so lint cleanliness is a correctness gate, not a nicety.
|
||||||
|
- name: shellcheck installer scripts
|
||||||
|
run: shellcheck install.sh scripts/waggle-server.sh
|
||||||
|
|
||||||
|
# The real E2E. --local-source copies THIS checkout (tracked + untracked,
|
||||||
|
# excluding node_modules/dist) into a scratch dir, then does a from-scratch
|
||||||
|
# npm install + build:packages there. --no-web keeps it to API/echo mode
|
||||||
|
# (no vite build); --no-start hands the boot to waggle-server.sh below so we
|
||||||
|
# test the process manager too.
|
||||||
|
- name: Install (install.sh --yes, from-scratch)
|
||||||
|
run: |
|
||||||
|
./install.sh \
|
||||||
|
--yes \
|
||||||
|
--no-web \
|
||||||
|
--no-start \
|
||||||
|
--local-source "$GITHUB_WORKSPACE" \
|
||||||
|
--dir "$WAGGLE_INSTALL_DIR" \
|
||||||
|
--data-dir "$WAGGLE_DATA_DIR" \
|
||||||
|
--port "$WAGGLE_PORT"
|
||||||
|
|
||||||
|
- name: Start the sidecar (waggle-server.sh start)
|
||||||
|
run: |
|
||||||
|
bash "$WAGGLE_INSTALL_DIR/scripts/waggle-server.sh" start \
|
||||||
|
--port "$WAGGLE_PORT" \
|
||||||
|
--data-dir "$WAGGLE_DATA_DIR"
|
||||||
|
|
||||||
|
# Defensive re-poll. waggle-server.sh already blocks until /health is 200
|
||||||
|
# (up to 60s; the sidecar spends ~9s on marketplace sync before listening),
|
||||||
|
# so this normally passes on the first attempt. The wider 90s budget guards
|
||||||
|
# against a slow cold runner without making the assertion flaky.
|
||||||
|
- name: Wait for /health (HTTP 200)
|
||||||
|
run: |
|
||||||
|
url="http://127.0.0.1:${WAGGLE_PORT}/health"
|
||||||
|
for i in $(seq 1 90); do
|
||||||
|
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "$url" || echo 000)"
|
||||||
|
if [ "$code" = "200" ]; then
|
||||||
|
echo "healthy after ${i}s (HTTP 200)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "::error::/health did not return 200 within 90s"
|
||||||
|
echo "--- last sidecar log lines ---"
|
||||||
|
tail -n 40 "$WAGGLE_DATA_DIR/server.log" 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Status
|
||||||
|
run: |
|
||||||
|
bash "$WAGGLE_INSTALL_DIR/scripts/waggle-server.sh" status \
|
||||||
|
--port "$WAGGLE_PORT" \
|
||||||
|
--data-dir "$WAGGLE_DATA_DIR"
|
||||||
|
|
||||||
|
- name: Stop
|
||||||
|
run: |
|
||||||
|
bash "$WAGGLE_INSTALL_DIR/scripts/waggle-server.sh" stop \
|
||||||
|
--port "$WAGGLE_PORT" \
|
||||||
|
--data-dir "$WAGGLE_DATA_DIR"
|
||||||
|
|
||||||
|
- name: Assert port is free after stop
|
||||||
|
run: |
|
||||||
|
url="http://127.0.0.1:${WAGGLE_PORT}/health"
|
||||||
|
if curl -fsS -o /dev/null --max-time 3 "$url"; then
|
||||||
|
echo "::error::/health still responding on port ${WAGGLE_PORT} after stop"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "port ${WAGGLE_PORT} is free"
|
||||||
|
|
||||||
|
- name: Dump sidecar log on failure
|
||||||
|
if: failure()
|
||||||
|
run: tail -n 100 "$WAGGLE_DATA_DIR/server.log" 2>/dev/null || echo "(no log file)"
|
||||||
178
.github/workflows/mind-parity-check.yml
vendored
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
name: mind-parity-check
|
||||||
|
|
||||||
|
# DEPRECATED 2026-04-30 — CC Sesija B monorepo migration §2.6 Task B22.
|
||||||
|
#
|
||||||
|
# This workflow ran the (then-external) hive-mind repo's mind/+harvest/ tests
|
||||||
|
# against waggle-os's substrate to verify behavioral parity while the same code
|
||||||
|
# lived in both repos. After CC Sesija B migration, the substrate lives ONLY in
|
||||||
|
# waggle-os/packages/hive-mind-core/, and the OSS mirror at
|
||||||
|
# github.com/marolinik/hive-mind is generated FROM waggle-os via subtree-split
|
||||||
|
# (not maintained as a parallel codebase). Parity checking is therefore
|
||||||
|
# definitionally trivial — the OSS export is byte-identical to its source.
|
||||||
|
#
|
||||||
|
# This workflow is preserved as the deprecation anchor. It will NOT fire on
|
||||||
|
# push because trigger paths (packages/core/src/mind/**) no longer exist as
|
||||||
|
# tracked content. See sync-mind.yml's deprecation note for the full migration
|
||||||
|
# context.
|
||||||
|
|
||||||
|
# Memory Sync Repair Step 3.1. Verifies that hive-mind's mind/ + harvest/
|
||||||
|
# tests pass against waggle-os's substrate. The check runs the waggle-os
|
||||||
|
# committed Step 2 ports (which include adapted versions like db.test.ts)
|
||||||
|
# AS BASELINE, then ALSO injects the latest hive-mind tests into the
|
||||||
|
# waggle-os checkout under `<basename>-hive-mind.test.ts` filenames so
|
||||||
|
# any NEW hive-mind cases since the Step 2 port get exercised.
|
||||||
|
#
|
||||||
|
# Triggers ONLY when shared substrate paths change. Failure blocks merge
|
||||||
|
# unless the failing test is allowlisted in `.parity-allowlist` at the
|
||||||
|
# repo root with a documented reason.
|
||||||
|
#
|
||||||
|
# See `.github/sync.md` for full design rationale and allowlist policy.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'packages/core/src/mind/**'
|
||||||
|
- 'packages/core/src/harvest/**'
|
||||||
|
- 'packages/core/tests/mind/**'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'packages/core/src/mind/**'
|
||||||
|
- 'packages/core/src/harvest/**'
|
||||||
|
- 'packages/core/tests/mind/**'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mind-parity-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
parity-check:
|
||||||
|
name: hive-mind ↔ waggle-os mind substrate parity
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout waggle-os
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
path: waggle-os
|
||||||
|
|
||||||
|
- name: Checkout hive-mind master
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: marolinik/hive-mind
|
||||||
|
ref: master
|
||||||
|
path: hive-mind
|
||||||
|
|
||||||
|
- name: Setup Node 20
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Cache npm dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-npm-${{ hashFiles('waggle-os/**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-npm-
|
||||||
|
|
||||||
|
- name: Install waggle-os dependencies
|
||||||
|
working-directory: waggle-os
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Run waggle-os baseline mind/ tests (committed Step 2 ports)
|
||||||
|
working-directory: waggle-os
|
||||||
|
run: npx vitest run --reporter=default packages/core/tests/mind/
|
||||||
|
|
||||||
|
- name: Inject latest hive-mind tests as -hive-mind suffix files
|
||||||
|
working-directory: waggle-os
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
allowlist_file=".parity-allowlist"
|
||||||
|
allowlist_basenames=()
|
||||||
|
if [ -f "$allowlist_file" ]; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Strip comments + leading/trailing whitespace; skip blanks
|
||||||
|
clean="${line%%#*}"
|
||||||
|
clean="$(echo "$clean" | tr -d '[:space:]')"
|
||||||
|
[ -z "$clean" ] && continue
|
||||||
|
allowlist_basenames+=("$clean")
|
||||||
|
done < "$allowlist_file"
|
||||||
|
echo "Allowlist entries: ${allowlist_basenames[@]:-<none>}"
|
||||||
|
else
|
||||||
|
echo "No .parity-allowlist file present — no skips."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Helper: is a basename in the allowlist?
|
||||||
|
is_allowlisted() {
|
||||||
|
local name="$1"
|
||||||
|
for a in "${allowlist_basenames[@]:-}"; do
|
||||||
|
[ "$a" = "$name" ] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
target_dir="packages/core/tests/mind"
|
||||||
|
source_dir="../hive-mind/packages/core/src/mind"
|
||||||
|
injected=0
|
||||||
|
skipped=0
|
||||||
|
|
||||||
|
already_committed=0
|
||||||
|
for src in "$source_dir"/*.test.ts; do
|
||||||
|
[ -e "$src" ] || continue
|
||||||
|
base="$(basename "$src" .test.ts)"
|
||||||
|
target_name="${base}-hive-mind.test.ts"
|
||||||
|
target_path="$target_dir/$target_name"
|
||||||
|
|
||||||
|
if is_allowlisted "$target_name"; then
|
||||||
|
echo " SKIP (allowlist): $target_name"
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$target_path" ]; then
|
||||||
|
# File is committed (Step 2 port). Preserve its bespoke
|
||||||
|
# header comments + any waggle-os adaptations. The
|
||||||
|
# committed version IS what the parity check should
|
||||||
|
# exercise — it's exactly what landed in main.
|
||||||
|
already_committed=$((already_committed + 1))
|
||||||
|
echo " KEEP (committed): $target_name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$src" "$target_path"
|
||||||
|
# Adapt import paths from hive-mind's adjacent style (`./x.js`) to
|
||||||
|
# waggle-os's separate-tests-folder style (`../../src/mind/x.js`).
|
||||||
|
sed -i "s|from \"\\./|from \"../../src/mind/|g" "$target_path"
|
||||||
|
sed -i "s|from '\\./|from '../../src/mind/|g" "$target_path"
|
||||||
|
injected=$((injected + 1))
|
||||||
|
echo " INJECT: $target_name"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Injected: $injected new hive-mind test file(s) under -hive-mind suffix"
|
||||||
|
echo "Kept: $already_committed already-committed Step 2 port file(s)"
|
||||||
|
echo "Skipped: $skipped allowlisted file(s)"
|
||||||
|
|
||||||
|
- name: Run combined waggle-os + injected hive-mind suite
|
||||||
|
working-directory: waggle-os
|
||||||
|
run: npx vitest run --reporter=default packages/core/tests/mind/
|
||||||
|
|
||||||
|
- name: Informational diff — shared substrate file sizes
|
||||||
|
working-directory: waggle-os
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## Substrate file size comparison (informational)"
|
||||||
|
for f in db.ts schema.ts frames.ts search.ts knowledge.ts identity.ts awareness.ts sessions.ts scoring.ts reconcile.ts ontology.ts concept-tracker.ts entity-normalizer.ts embedding-provider.ts inprocess-embedder.ts; do
|
||||||
|
wf="packages/core/src/mind/$f"
|
||||||
|
hf="../hive-mind/packages/core/src/mind/$f"
|
||||||
|
if [ -f "$wf" ] && [ -f "$hf" ]; then
|
||||||
|
wsize=$(wc -c < "$wf")
|
||||||
|
hsize=$(wc -c < "$hf")
|
||||||
|
diff_pct=$(awk -v w="$wsize" -v h="$hsize" 'BEGIN { if (h == 0) print "n/a"; else printf "%.1f%%", ((w - h) / h) * 100 }')
|
||||||
|
echo " $f: waggle-os=$wsize hive-mind=$hsize delta=$diff_pct"
|
||||||
|
fi
|
||||||
|
done
|
||||||
145
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# Waggle — Release Build Workflow
|
||||||
|
#
|
||||||
|
# Builds desktop apps for Windows (NSIS) and macOS (DMG) on tag push.
|
||||||
|
# Publishes artifacts as GitHub Release assets.
|
||||||
|
#
|
||||||
|
# Trigger: push tag v* (e.g., v1.0.0)
|
||||||
|
# Also supports manual dispatch for testing.
|
||||||
|
|
||||||
|
name: Release Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: app/src-tauri
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Build packages (shared -> core -> agent -> server)
|
||||||
|
run: npm run build:packages
|
||||||
|
|
||||||
|
- name: Build sidecar
|
||||||
|
run: node scripts/build-sidecar.mjs
|
||||||
|
|
||||||
|
- name: Bundle native dependencies
|
||||||
|
run: node scripts/bundle-native-deps.mjs
|
||||||
|
|
||||||
|
- name: Bundle Node.js runtime
|
||||||
|
run: node scripts/bundle-node.mjs
|
||||||
|
|
||||||
|
- name: Stage sidecar dependencies
|
||||||
|
run: node scripts/stage-sidecar-deps.mjs
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: cd apps/web && npx vite build
|
||||||
|
|
||||||
|
- name: Build Tauri (Windows)
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
projectPath: app
|
||||||
|
tagName: ${{ github.ref_name }}
|
||||||
|
releaseName: 'Waggle ${{ github.ref_name }}'
|
||||||
|
releaseBody: 'See the release notes for details.'
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
build-macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
target: [aarch64-apple-darwin, x86_64-apple-darwin]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: app/src-tauri
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Build packages (shared -> core -> agent -> server)
|
||||||
|
run: npm run build:packages
|
||||||
|
|
||||||
|
- name: Build sidecar
|
||||||
|
run: node scripts/build-sidecar.mjs
|
||||||
|
|
||||||
|
- name: Bundle native dependencies
|
||||||
|
run: node scripts/bundle-native-deps.mjs
|
||||||
|
env:
|
||||||
|
TARGET_ARCH: ${{ matrix.target == 'aarch64-apple-darwin' && 'arm64' || 'x64' }}
|
||||||
|
|
||||||
|
- name: Bundle Node.js runtime
|
||||||
|
run: node scripts/bundle-node.mjs
|
||||||
|
env:
|
||||||
|
TARGET_ARCH: ${{ matrix.target == 'aarch64-apple-darwin' && 'arm64' || 'x64' }}
|
||||||
|
|
||||||
|
- name: Stage sidecar dependencies
|
||||||
|
run: node scripts/stage-sidecar-deps.mjs
|
||||||
|
env:
|
||||||
|
TARGET_ARCH: ${{ matrix.target == 'aarch64-apple-darwin' && 'arm64' || 'x64' }}
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: cd apps/web && npx vite build
|
||||||
|
|
||||||
|
- name: Build Tauri (macOS)
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
projectPath: app
|
||||||
|
tagName: ${{ github.ref_name }}
|
||||||
|
releaseName: 'Waggle ${{ github.ref_name }}'
|
||||||
|
releaseBody: 'See the release notes for details.'
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: false
|
||||||
|
args: --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
# NOTE: the Tauri auto-updater is disabled for v1 (plugins.updater removed from
|
||||||
|
# tauri.conf.json — see BUILD P0-2 / P1-8). The former `update-manifest` job
|
||||||
|
# published a latest.json with EMPTY signatures, which every client rejected at
|
||||||
|
# signature verification. Re-enabling the updater requires:
|
||||||
|
# 1. Provision a TAURI_SIGNING_PRIVATE_KEY (+ password) repo secret.
|
||||||
|
# 2. Restore `plugins.updater` (endpoints + pubkey) in tauri.conf.json and
|
||||||
|
# set bundle.createUpdaterArtifacts so tauri-action emits signed .sig files.
|
||||||
|
# 3. Restore a latest.json generator that reads the real signatures from the
|
||||||
|
# build artifacts (tauri-action can publish the manifest directly).
|
||||||
230
.github/workflows/sync-mind.yml
vendored
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
name: sync-mind-to-hive-mind
|
||||||
|
|
||||||
|
# DEPRECATED 2026-04-30 — CC Sesija B monorepo migration §2.6 Task B22.
|
||||||
|
#
|
||||||
|
# This workflow was the bidirectional-sync mechanism between waggle-os and the
|
||||||
|
# now-archived `marolinik/hive-mind` repo while substrate code lived in BOTH
|
||||||
|
# places (packages/core/src/mind/ + packages/core/src/harvest/ in waggle-os,
|
||||||
|
# duplicated in hive-mind/packages/core/src/{mind,harvest}/).
|
||||||
|
#
|
||||||
|
# After CC Sesija B migration (commits ff5b4aa..b59d188 on
|
||||||
|
# feature/hive-mind-monorepo-migration), the substrate lives ONLY in
|
||||||
|
# waggle-os/packages/hive-mind-core/. The OSS distribution mechanism is now
|
||||||
|
# `git subtree split` from waggle-os monorepo to public mirror — see
|
||||||
|
# `scripts/oss-subtree-split.sh` and `packages/hive-mind-core/CONTRIBUTING.md`.
|
||||||
|
#
|
||||||
|
# This workflow is preserved for AUDIT TRAIL purposes (the historical
|
||||||
|
# trigger paths and concurrency settings are referenced in EXTRACTION.md and
|
||||||
|
# the .github/sync.md operating manual). It will NOT fire on push because the
|
||||||
|
# trigger paths (packages/core/src/mind/** + packages/core/src/harvest/**) no
|
||||||
|
# longer exist as tracked content — they were git-mv'd to packages/hive-mind-core/
|
||||||
|
# on commit 3b556c0.
|
||||||
|
#
|
||||||
|
# DO NOT delete this file as part of cleanup — leave it as the deprecation
|
||||||
|
# anchor. If the workflow ever needs reactivation, trigger paths must be
|
||||||
|
# updated to the new packages/hive-mind-core/ location AND the
|
||||||
|
# @hive-mind ↔ @waggle name remapping must be added.
|
||||||
|
|
||||||
|
# Memory Sync Repair Step 3.2 — waggle-os → hive-mind direction.
|
||||||
|
#
|
||||||
|
# Triggered when waggle-os main receives a push that touches shared
|
||||||
|
# substrate paths (mind/ or harvest/), this workflow extracts the
|
||||||
|
# filtered diff (excluding NOT-extracted files per EXTRACTION.md), opens
|
||||||
|
# a PR against marolinik/hive-mind master with the patch applied, and
|
||||||
|
# tags the PR with the source SHA.
|
||||||
|
#
|
||||||
|
# This is the SECONDARY direction empirically — Steps 1+2 found
|
||||||
|
# hive-mind is the more active substrate repo (ahead in 2/5 audit
|
||||||
|
# dimensions, +14 organic test files). The PRIMARY direction
|
||||||
|
# (hive-mind push → auto-PR ka waggle-os) is intentionally NOT
|
||||||
|
# implemented in this file because it requires a workflow living in
|
||||||
|
# the hive-mind repo, which is out of CC-2 scope. It will be added
|
||||||
|
# via a sibling PR to hive-mind once Step 3 here is ratified.
|
||||||
|
#
|
||||||
|
# See `.github/sync.md` for full design rationale, EXTRACTION.md
|
||||||
|
# filter list, and how to extend bidirectional sync.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'packages/core/src/mind/**'
|
||||||
|
- 'packages/core/src/harvest/**'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: sync-mind-${{ github.ref }}
|
||||||
|
# Don't cancel — every main push to mind/ or harvest/ deserves its own
|
||||||
|
# sync attempt (the resulting hive-mind PR is per-commit traceable).
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
open-hive-mind-pr:
|
||||||
|
name: Open auto-sync PR to marolinik/hive-mind
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
# `HIVE_MIND_SYNC_TOKEN` is a fine-grained PAT scoped to
|
||||||
|
# marolinik/hive-mind with `pull_request: write` + `contents: write`.
|
||||||
|
# Configured by Marko via `gh secret set HIVE_MIND_SYNC_TOKEN`.
|
||||||
|
# Without the secret, the job fails fast with a documented error
|
||||||
|
# rather than silently skipping.
|
||||||
|
if: ${{ vars.MIND_SYNC_ENABLED == 'true' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout waggle-os (full history for the diff)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Verify HIVE_MIND_SYNC_TOKEN is configured
|
||||||
|
run: |
|
||||||
|
if [ -z "${{ secrets.HIVE_MIND_SYNC_TOKEN }}" ]; then
|
||||||
|
echo "::error::HIVE_MIND_SYNC_TOKEN secret is not configured."
|
||||||
|
echo "::error::Run: gh secret set HIVE_MIND_SYNC_TOKEN --repo marolinik/waggle-os"
|
||||||
|
echo "::error::See .github/sync.md for full setup instructions."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Compute filtered diff (exclude NOT-extracted paths)
|
||||||
|
id: diff
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Files that MUST be excluded from sync per EXTRACTION.md
|
||||||
|
# "NOT Extracted" section. Sync attempts that include these paths
|
||||||
|
# would leak Waggle-specific code (vault, compliance, evolution,
|
||||||
|
# tier system) into the hive-mind Apache-2.0 release artifact.
|
||||||
|
excluded_paths=(
|
||||||
|
'packages/core/src/mind/vault.ts'
|
||||||
|
'packages/core/src/mind/evolution-runs.ts'
|
||||||
|
'packages/core/src/mind/execution-traces.ts'
|
||||||
|
'packages/core/src/mind/improvement-signals.ts'
|
||||||
|
'packages/core/src/compliance'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build the rev range. `${{ github.event.before }}` is the parent
|
||||||
|
# commit of this push; `${{ github.sha }}` is the new HEAD. For a
|
||||||
|
# branch's first push or a force-push, `before` may be all-zeros;
|
||||||
|
# in that case fall back to the previous merge-base via reflog.
|
||||||
|
before_sha='${{ github.event.before }}'
|
||||||
|
after_sha='${{ github.sha }}'
|
||||||
|
if [ "$before_sha" = "0000000000000000000000000000000000000000" ]; then
|
||||||
|
echo "::warning::push has no `before` SHA — falling back to HEAD~1"
|
||||||
|
before_sha="$(git rev-parse HEAD~1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# All shared-substrate files in this push, before exclusion.
|
||||||
|
changed=$(git diff --name-only "$before_sha" "$after_sha" -- \
|
||||||
|
'packages/core/src/mind/**' 'packages/core/src/harvest/**')
|
||||||
|
|
||||||
|
# Apply EXTRACTION.md "NOT extracted" exclusions.
|
||||||
|
filtered_files=()
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -z "$f" ] && continue
|
||||||
|
skip=false
|
||||||
|
for excl in "${excluded_paths[@]}"; do
|
||||||
|
case "$f" in
|
||||||
|
"$excl"|"$excl"/*)
|
||||||
|
skip=true
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
$skip || filtered_files+=("$f")
|
||||||
|
done <<< "$changed"
|
||||||
|
|
||||||
|
if [ ${#filtered_files[@]} -eq 0 ]; then
|
||||||
|
echo "No syncable changes after EXTRACTION.md filtering."
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Files to sync:"
|
||||||
|
printf ' %s\n' "${filtered_files[@]}"
|
||||||
|
|
||||||
|
# Generate a patch limited to the filtered files. This patch will
|
||||||
|
# apply to hive-mind because the directory layout matches:
|
||||||
|
# waggle-os `packages/core/src/{mind,harvest}/...` ↔
|
||||||
|
# hive-mind `packages/core/src/{mind,harvest}/...`.
|
||||||
|
mkdir -p .sync-output
|
||||||
|
git diff "$before_sha" "$after_sha" -- "${filtered_files[@]}" > .sync-output/patch.diff
|
||||||
|
|
||||||
|
# Capture the original commit subjects for the PR description.
|
||||||
|
git log --format='- %h %s' "$before_sha".."$after_sha" -- "${filtered_files[@]}" > .sync-output/commits.txt
|
||||||
|
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "after_sha=$after_sha" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "after_short=$(git rev-parse --short "$after_sha")" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "first_subject=$(git log -1 --format=%s "$after_sha")" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Checkout hive-mind for patch application
|
||||||
|
if: steps.diff.outputs.skip != 'true'
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: marolinik/hive-mind
|
||||||
|
ref: master
|
||||||
|
token: ${{ secrets.HIVE_MIND_SYNC_TOKEN }}
|
||||||
|
path: hive-mind
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Apply patch to hive-mind branch + push
|
||||||
|
if: steps.diff.outputs.skip != 'true'
|
||||||
|
working-directory: hive-mind
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.HIVE_MIND_SYNC_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
branch_name="auto-sync/waggle-os-${{ steps.diff.outputs.after_short }}"
|
||||||
|
git config user.name 'waggle-os-sync-bot'
|
||||||
|
git config user.email 'sync-bot@waggle-os.ai'
|
||||||
|
git checkout -b "$branch_name"
|
||||||
|
|
||||||
|
# Apply the filtered patch. The waggle-os layout is identical
|
||||||
|
# under packages/core/src/{mind,harvest}/ so the patch applies
|
||||||
|
# directly without `--directory` rewriting.
|
||||||
|
if ! git apply --3way ../.sync-output/patch.diff; then
|
||||||
|
echo "::error::Patch did not apply cleanly. Manual reconciliation required."
|
||||||
|
echo "::error::Source SHA: ${{ steps.diff.outputs.after_sha }}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore(sync): auto-sync from waggle-os@${{ steps.diff.outputs.after_short }}
|
||||||
|
|
||||||
|
Source: marolinik/waggle-os main @ ${{ steps.diff.outputs.after_sha }}
|
||||||
|
Subject: ${{ steps.diff.outputs.first_subject }}
|
||||||
|
|
||||||
|
See PR description for the full list of waggle-os commits in this batch."
|
||||||
|
|
||||||
|
git push origin "$branch_name"
|
||||||
|
|
||||||
|
- name: Open PR on hive-mind
|
||||||
|
if: steps.diff.outputs.skip != 'true'
|
||||||
|
working-directory: hive-mind
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.HIVE_MIND_SYNC_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
branch_name="auto-sync/waggle-os-${{ steps.diff.outputs.after_short }}"
|
||||||
|
commit_list="$(cat ../.sync-output/commits.txt)"
|
||||||
|
|
||||||
|
gh pr create \
|
||||||
|
--repo marolinik/hive-mind \
|
||||||
|
--base master \
|
||||||
|
--head "$branch_name" \
|
||||||
|
--title "auto-sync from waggle-os@${{ steps.diff.outputs.after_short }}: ${{ steps.diff.outputs.first_subject }}" \
|
||||||
|
--body "$(printf 'Automated cross-repo sync — Memory Sync Repair Step 3.2 (waggle-os → hive-mind direction).\n\n## Source\n- Repo: marolinik/waggle-os\n- Branch: main\n- HEAD: %s\n- Workflow run: %s/%s/actions/runs/%s\n\n## Filtering\nThis patch was filtered to exclude paths listed under "NOT Extracted" in EXTRACTION.md:\n- packages/core/src/mind/vault.ts\n- packages/core/src/mind/evolution-runs.ts\n- packages/core/src/mind/execution-traces.ts\n- packages/core/src/mind/improvement-signals.ts\n- packages/core/src/compliance\n\n## Originating commits in this batch\n\n%s\n\n## Review checklist\n- [ ] Patch applied cleanly to hive-mind master without 3-way conflicts\n- [ ] No NOT-extracted paths sneaked through (sanity-check the diff against EXTRACTION.md)\n- [ ] Test deltas (if any) make sense for the OSS surface; no Waggle-specific test fixtures\n- [ ] mind-parity-check on waggle-os side is GREEN before merging this PR (otherwise the sync would re-introduce a regression)' \
|
||||||
|
"${{ steps.diff.outputs.after_sha }}" \
|
||||||
|
"${{ github.server_url }}" \
|
||||||
|
"${{ github.repository }}" \
|
||||||
|
"${{ github.run_id }}" \
|
||||||
|
"$commit_list")"
|
||||||
|
|
||||||
|
- name: Upload patch as artifact (debug aid)
|
||||||
|
if: always() && steps.diff.outputs.skip != 'true'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: sync-patch-${{ steps.diff.outputs.after_short }}
|
||||||
|
path: .sync-output/
|
||||||
|
retention-days: 30
|
||||||
178
.github/workflows/tauri-build-pr.yml
vendored
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Waggle — Tauri Build Verification (per-PR + main pushes)
|
||||||
|
#
|
||||||
|
# CC Sesija A §2.4 Task A13 (PM-reframed scope). Verifies the desktop app
|
||||||
|
# builds cleanly on Win + macOS for every PR + main push, so a regression
|
||||||
|
# can't sneak in unnoticed between releases. Distinct from release.yml
|
||||||
|
# which only triggers on `v*` tags + uploads to GitHub Releases (this
|
||||||
|
# workflow only uploads to the workflow run as artifacts for download
|
||||||
|
# verification, no release publishing).
|
||||||
|
#
|
||||||
|
# Exit signal: green CI here means tag-push to release.yml is safe to
|
||||||
|
# pull the trigger on. Red CI here = same investigation flow as release.yml
|
||||||
|
# (Rust compile / Vite build / sidecar bundle / native dep failure).
|
||||||
|
|
||||||
|
name: Tauri Build Verification
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'app/**'
|
||||||
|
- 'apps/web/**'
|
||||||
|
- 'packages/**'
|
||||||
|
- 'scripts/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
- '.github/workflows/tauri-build-pr.yml'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'app/**'
|
||||||
|
- 'apps/web/**'
|
||||||
|
- 'packages/**'
|
||||||
|
- 'scripts/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 45
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: app/src-tauri
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Build packages (shared → core → agent → server)
|
||||||
|
run: npm run build:packages
|
||||||
|
|
||||||
|
- name: Build sidecar
|
||||||
|
run: node scripts/build-sidecar.mjs
|
||||||
|
|
||||||
|
- name: Bundle native dependencies
|
||||||
|
run: node scripts/bundle-native-deps.mjs
|
||||||
|
|
||||||
|
- name: Bundle Node.js runtime
|
||||||
|
run: node scripts/bundle-node.mjs
|
||||||
|
|
||||||
|
- name: Stage sidecar dependencies
|
||||||
|
run: node scripts/stage-sidecar-deps.mjs
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: cd apps/web && npx vite build
|
||||||
|
|
||||||
|
- name: Build Tauri (Windows)
|
||||||
|
# @tauri-apps/cli is declared in app/package.json devDeps but is absent
|
||||||
|
# from package-lock.json, so `npm install` never installs it and a bare
|
||||||
|
# `npx tauri` errors "could not determine executable to run". Fetch the
|
||||||
|
# CLI explicitly by package name (npx resolves the win32 binary).
|
||||||
|
run: cd app && npx --yes @tauri-apps/cli@2 build
|
||||||
|
env:
|
||||||
|
# Skip code signing for PR verification — release.yml handles signing
|
||||||
|
# only on tag push.
|
||||||
|
TAURI_PRIVATE_KEY: ''
|
||||||
|
TAURI_KEY_PASSWORD: ''
|
||||||
|
|
||||||
|
- name: Upload Windows artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: waggle-windows-${{ github.sha }}
|
||||||
|
path: |
|
||||||
|
app/src-tauri/target/release/bundle/nsis/*.exe
|
||||||
|
app/src-tauri/target/release/bundle/msi/*.msi
|
||||||
|
if-no-files-found: warn
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
verify-macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
strategy:
|
||||||
|
# Per-arch, matching release.yml. Universal builds are rejected by the
|
||||||
|
# bundle scripts (sqlite-vec / onnxruntime / node ship per-arch binaries),
|
||||||
|
# so each arch is staged and built separately.
|
||||||
|
matrix:
|
||||||
|
target: [aarch64-apple-darwin, x86_64-apple-darwin]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: app/src-tauri
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Build packages (shared → core → agent → server)
|
||||||
|
run: npm run build:packages
|
||||||
|
|
||||||
|
- name: Build sidecar
|
||||||
|
run: node scripts/build-sidecar.mjs
|
||||||
|
|
||||||
|
- name: Bundle native dependencies
|
||||||
|
run: node scripts/bundle-native-deps.mjs
|
||||||
|
env:
|
||||||
|
TARGET_ARCH: ${{ matrix.target == 'aarch64-apple-darwin' && 'arm64' || 'x64' }}
|
||||||
|
|
||||||
|
- name: Bundle Node.js runtime
|
||||||
|
run: node scripts/bundle-node.mjs
|
||||||
|
env:
|
||||||
|
TARGET_ARCH: ${{ matrix.target == 'aarch64-apple-darwin' && 'arm64' || 'x64' }}
|
||||||
|
|
||||||
|
- name: Stage sidecar dependencies
|
||||||
|
run: node scripts/stage-sidecar-deps.mjs
|
||||||
|
env:
|
||||||
|
TARGET_ARCH: ${{ matrix.target == 'aarch64-apple-darwin' && 'arm64' || 'x64' }}
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: cd apps/web && npx vite build
|
||||||
|
|
||||||
|
- name: Build Tauri (macOS ${{ matrix.target }})
|
||||||
|
# See verify-windows note: fetch @tauri-apps/cli by package name (absent
|
||||||
|
# from the lockfile). Built per-arch — universal is rejected by the
|
||||||
|
# bundle scripts (per-arch native modules), matching release.yml.
|
||||||
|
run: cd app && npx --yes @tauri-apps/cli@2 build --target ${{ matrix.target }}
|
||||||
|
env:
|
||||||
|
TAURI_PRIVATE_KEY: ''
|
||||||
|
TAURI_KEY_PASSWORD: ''
|
||||||
|
|
||||||
|
- name: Upload macOS artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: waggle-macos-${{ matrix.target }}-${{ github.sha }}
|
||||||
|
path: |
|
||||||
|
app/src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
|
||||||
|
app/src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app
|
||||||
|
if-no-files-found: warn
|
||||||
|
retention-days: 7
|
||||||
282
.gitignore
vendored
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
# Local tooling config (machine-specific MCP server wiring)
|
||||||
|
.mcp.json
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# Waggle data
|
||||||
|
.waggle/
|
||||||
|
*.mind
|
||||||
|
marketplace.db
|
||||||
|
!packages/marketplace/marketplace.db
|
||||||
|
|
||||||
|
# Code signing (LAUNCH-06 pilot self-sign artifacts — never commit private
|
||||||
|
# cert material or operator-local thumbprints into the repo)
|
||||||
|
*.pfx
|
||||||
|
app/src-tauri/.thumbprint.txt
|
||||||
|
|
||||||
|
# Preflight gate per-query raw JSONs carry retrieved frame previews that
|
||||||
|
# originate from Marko's personal AI exports. The report .md stays commit-
|
||||||
|
# eligible (adapter counts, timing, verbatim questions + model answers
|
||||||
|
# only); per-query JSONs and scratch dirs do not.
|
||||||
|
preflight-results/stage-0-query-*.json
|
||||||
|
preflight-results/stage-0-tmp/
|
||||||
|
preflight-results/stage-*-attempt-*.md
|
||||||
|
preflight-results/stage-*-exclusions.md
|
||||||
|
|
||||||
|
# Benchmark harness — data + run outputs stay local
|
||||||
|
benchmarks/data/*
|
||||||
|
!benchmarks/data/.gitkeep
|
||||||
|
# Committed sample-lock artifacts (stratified picks from public LoCoMo). Small,
|
||||||
|
# diff-reviewable, required for reproducibility. Raw LoCoMo dumps stay local.
|
||||||
|
!benchmarks/data/preflight-locomo-50.json
|
||||||
|
!benchmarks/data/failure-mode-calibration-10.jsonl
|
||||||
|
# Sprint 12 Task 1 Blocker #1 — canonical LoCoMo eval archive (1531 instances,
|
||||||
|
# derived from snap-research/locomo10.json via scripts/build-locomo-canonical.ts).
|
||||||
|
# Committed for hash determinism across clones + CI + H-AUDIT-2 replication.
|
||||||
|
!benchmarks/data/locomo/
|
||||||
|
!benchmarks/data/locomo/locomo-1540.jsonl
|
||||||
|
!benchmarks/data/locomo/locomo-1540.meta.json
|
||||||
|
# LME V1 canonical outputs — meta.json is tracked; large JSONL + raw JSON are not.
|
||||||
|
# Re-ignore the dir's contents (the `!dir/` negation re-includes everything; the
|
||||||
|
# single-level `data/*` above does not cover nested files) then whitelist meta.
|
||||||
|
!benchmarks/data/longmemeval/
|
||||||
|
benchmarks/data/longmemeval/*
|
||||||
|
!benchmarks/data/longmemeval/longmemeval.meta.json
|
||||||
|
# BEAM canonical meta — same pattern
|
||||||
|
!benchmarks/data/beam/
|
||||||
|
benchmarks/data/beam/*
|
||||||
|
!benchmarks/data/beam/beam-128K.meta.json
|
||||||
|
!benchmarks/data/beam/beam-1M.meta.json
|
||||||
|
# Memori replication corpus + outputs stay local (large; not source).
|
||||||
|
benchmarks/memori-replication/
|
||||||
|
benchmarks/results/*
|
||||||
|
!benchmarks/results/.gitkeep
|
||||||
|
# Sprint 12 Task 2.5 Stage 3 — per-stage manifest-adjacent audit artefacts:
|
||||||
|
# pre-registration anchors, clarification memos, RCA memos, probe logs.
|
||||||
|
# These are tamper-evident, NOT run output, so they MUST be tracked.
|
||||||
|
# Broadened 2026-04-24 for Stage 3 re-kick (Option A): clarification +
|
||||||
|
# RCA memos + Gate P+ probe log all need the same treatment.
|
||||||
|
!benchmarks/results/manifest-*.md
|
||||||
|
!benchmarks/results/manifest-*.yaml
|
||||||
|
!benchmarks/results/stage*-gate-*.md
|
||||||
|
!benchmarks/results/stage*-gate-*.jsonl
|
||||||
|
# Stage 3 v6 N=400 final deliverables (2026-04-25): cell-by-cell summary,
|
||||||
|
# Fisher H1 analysis, ≤300-word memo, and the agentic-cell evidence JSONL.
|
||||||
|
# Tracked under PM-RATIFY-V6-N400-COMPLETE; tamper-evident audit chain.
|
||||||
|
!benchmarks/results/stage3-n400-v6-*.md
|
||||||
|
!benchmarks/results/agentic-locomo-2026-04-25T*.jsonl
|
||||||
|
!benchmarks/results/agentic-locomo-2026-04-25T*.summary.json
|
||||||
|
# v6 self-judge re-evaluation (2026-04-25): apples-to-apples vs Mem0
|
||||||
|
# methodology. 2000 records, side-by-side comparison, ≤300-word memo.
|
||||||
|
!benchmarks/results/v6-self-judge-rebench/
|
||||||
|
!benchmarks/results/v6-self-judge-rebench/*.jsonl
|
||||||
|
!benchmarks/results/v6-self-judge-rebench/*.md
|
||||||
|
# Agentic knowledge work pilot 2026-04-26 (FAIL verdict, see
|
||||||
|
# decisions/2026-04-26-pilot-verdict-FAIL.md). 12 cell JSONLs + summary +
|
||||||
|
# run log + prompts archive + invalidated originals (audit-preserved).
|
||||||
|
!benchmarks/results/pilot-2026-04-26/
|
||||||
|
!benchmarks/results/pilot-2026-04-26/*.jsonl
|
||||||
|
!benchmarks/results/pilot-2026-04-26/*.json
|
||||||
|
!benchmarks/results/pilot-2026-04-26/*.log
|
||||||
|
!benchmarks/results/pilot-2026-04-26/prompts-archive/
|
||||||
|
!benchmarks/results/pilot-2026-04-26/prompts-archive/*.md
|
||||||
|
!benchmarks/results/pilot-2026-04-26/invalidated/
|
||||||
|
!benchmarks/results/pilot-2026-04-26/invalidated/*.jsonl
|
||||||
|
# GEPA Faza 1 corpus + run logs + spot-audit reports + NULL/Gen1/held-out
|
||||||
|
# artefacts (per launch decision §G + manifest v7 §gepa). All instances +
|
||||||
|
# generation logs + checkpoint reports tracked for audit chain.
|
||||||
|
# Exception applies to BOTH the line-47 base pattern AND the line-98
|
||||||
|
# `**/benchmarks/results/*` nested-mis-CWD catch (mirrors pilot-2026-04-26).
|
||||||
|
!benchmarks/results/gepa-faza1/
|
||||||
|
!benchmarks/results/gepa-faza1/**/*.jsonl
|
||||||
|
!benchmarks/results/gepa-faza1/**/*.json
|
||||||
|
!benchmarks/results/gepa-faza1/**/*.md
|
||||||
|
!benchmarks/results/gepa-faza1/**/*.log
|
||||||
|
!benchmarks/results/gepa-faza1/**/*.yaml
|
||||||
|
!**/benchmarks/results/gepa-faza1
|
||||||
|
!**/benchmarks/results/gepa-faza1/
|
||||||
|
!**/benchmarks/results/gepa-faza1/**
|
||||||
|
# §1.3f Vertex Batch eligibility probe (2026-04-24): standalone probe
|
||||||
|
# artefacts outside §11 frozen paths. Logs + JSONL + script + memo all
|
||||||
|
# tracked; override the *.log line-3 wildcard for this specific folder.
|
||||||
|
!benchmarks/probes/**/*.log
|
||||||
|
!benchmarks/probes/**/*.jsonl
|
||||||
|
!benchmarks/probes/**/*.py
|
||||||
|
!benchmarks/probes/**/*.md
|
||||||
|
# Also catch nested mis-CWD writes (e.g. ran from benchmarks/harness/).
|
||||||
|
# Use a trailing `*` (not `/`) so the `!` exceptions above still apply.
|
||||||
|
**/benchmarks/results/*
|
||||||
|
# Canonical LoCoMo SOTA evidence — intentionally committed (see in-dir README). Do NOT remove:
|
||||||
|
# this exact swallow hid the 87.66 result from the repo (docs/analysis/locomo-sota-evidence-drift-2026-06-30.md).
|
||||||
|
!**/benchmarks/results/locomo-sota-2026-06/
|
||||||
|
!**/benchmarks/results/locomo-sota-2026-06/**
|
||||||
|
**/benchmarks/data/*
|
||||||
|
!**/benchmarks/data/preflight-locomo-50.json
|
||||||
|
!**/benchmarks/data/failure-mode-calibration-10.jsonl
|
||||||
|
!**/benchmarks/data/locomo/
|
||||||
|
!**/benchmarks/data/locomo/locomo-1540.jsonl
|
||||||
|
!**/benchmarks/data/locomo/locomo-1540.meta.json
|
||||||
|
!**/benchmarks/data/longmemeval/
|
||||||
|
**/benchmarks/data/longmemeval/*
|
||||||
|
!**/benchmarks/data/longmemeval/longmemeval.meta.json
|
||||||
|
!**/benchmarks/data/beam/
|
||||||
|
**/benchmarks/data/beam/*
|
||||||
|
!**/benchmarks/data/beam/beam-128K.meta.json
|
||||||
|
!**/benchmarks/data/beam/beam-1M.meta.json
|
||||||
|
!**/benchmarks/data/.gitkeep
|
||||||
|
!**/benchmarks/results/.gitkeep
|
||||||
|
!**/benchmarks/results/manifest-*.md
|
||||||
|
!**/benchmarks/results/manifest-*.yaml
|
||||||
|
!**/benchmarks/results/stage*-gate-*.md
|
||||||
|
!**/benchmarks/results/stage*-gate-*.jsonl
|
||||||
|
!**/benchmarks/results/stage3-n400-v6-*.md
|
||||||
|
!**/benchmarks/results/agentic-locomo-2026-04-25T*.jsonl
|
||||||
|
!**/benchmarks/results/agentic-locomo-2026-04-25T*.summary.json
|
||||||
|
!**/benchmarks/results/v6-self-judge-rebench/
|
||||||
|
!**/benchmarks/results/v6-self-judge-rebench/*.jsonl
|
||||||
|
!**/benchmarks/results/v6-self-judge-rebench/*.md
|
||||||
|
!**/benchmarks/results/pilot-2026-04-26/
|
||||||
|
!**/benchmarks/results/pilot-2026-04-26/*.jsonl
|
||||||
|
!**/benchmarks/results/pilot-2026-04-26/*.json
|
||||||
|
!**/benchmarks/results/pilot-2026-04-26/*.log
|
||||||
|
!**/benchmarks/results/pilot-2026-04-26/prompts-archive/
|
||||||
|
!**/benchmarks/results/pilot-2026-04-26/prompts-archive/*.md
|
||||||
|
!**/benchmarks/results/pilot-2026-04-26/invalidated/
|
||||||
|
!**/benchmarks/results/pilot-2026-04-26/invalidated/*.jsonl
|
||||||
|
# Sprint 11 A2 (ratification §Q5 Tier 2): `benchmarks/archive/` holds
|
||||||
|
# gzipped JSONL from launch-claim-supporting runs (H-42a/b full-run).
|
||||||
|
# Retention: 12 months minimum from commit per ratification §Q5. Files here
|
||||||
|
# ARE committed — do NOT gitignore the folder. Spring 11 only adds the
|
||||||
|
# folder + README; actual archival runs land in a later sprint.
|
||||||
|
!benchmarks/archive/
|
||||||
|
!benchmarks/archive/**
|
||||||
|
test-regression-*.log
|
||||||
|
# SQLite WAL/SHM companions — always match the *.db they accompany
|
||||||
|
marketplace.db-shm
|
||||||
|
marketplace.db-wal
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
|
||||||
|
# Non-production workspace (planning, reviews, session docs, archives)
|
||||||
|
.workspace/
|
||||||
|
.scratch/
|
||||||
|
.mind/
|
||||||
|
.planning/
|
||||||
|
|
||||||
|
# External research checkouts (large; not part of waggle-os build)
|
||||||
|
external/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Build cache (Node.js runtime downloads)
|
||||||
|
scripts/.cache/
|
||||||
|
|
||||||
|
# Bundled resources (generated at build time)
|
||||||
|
app/src-tauri/resources/node
|
||||||
|
app/src-tauri/resources/node.exe
|
||||||
|
app/src-tauri/resources/native/
|
||||||
|
# D12: the bundled sidecar is generated by scripts/build-sidecar.mjs on every
|
||||||
|
# build path (npm tauri:build*, CI, and the tauri.conf.json beforeBuildCommand
|
||||||
|
# hook). A committed copy goes stale silently — a binary shipping an old server
|
||||||
|
# is a release-stopping defect class (UX-Refactor P4 ruling).
|
||||||
|
app/src-tauri/resources/service.js
|
||||||
|
app/src-tauri/resources/service.js.map
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.claude/
|
||||||
|
.playwright-mcp/
|
||||||
|
.superpowers/
|
||||||
|
~$*
|
||||||
|
|
||||||
|
# Test artifacts
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
tests/screenshots/
|
||||||
|
s*.png
|
||||||
|
screen*.png
|
||||||
|
# …but never the committed persona avatars (sales-rep/support-agent match s*.png)
|
||||||
|
!apps/web/src/assets/personas/*.png
|
||||||
|
tmp_bench_results.json
|
||||||
|
tmp_bench_results-v5.json
|
||||||
|
# R6/LPV eval run dumps — transient; verdicts live in docs/plans/*-RESULTS-*.md
|
||||||
|
tmp_hermes-skill-reuse.json
|
||||||
|
tmp_hermes-pilot*.json
|
||||||
|
tmp_hermes-pilot*.log
|
||||||
|
tmp_lpv*.log
|
||||||
|
tmp_lpv*.json
|
||||||
|
|
||||||
|
# Bee regen backup folders — rollback artifacts, not committed
|
||||||
|
apps/www/public/brand/_backup-*/
|
||||||
|
|
||||||
|
# Next.js (apps/www)
|
||||||
|
.next/
|
||||||
|
.vercel/
|
||||||
|
|
||||||
|
# Lighthouse audit artifacts (regenerable via `npx lighthouse`)
|
||||||
|
apps/www/lighthouse-report.json
|
||||||
|
|
||||||
|
|
||||||
|
# Vision-harness capture output (regenerated per run)
|
||||||
|
tests/vision/artifacts/
|
||||||
|
|
||||||
|
# UX Refactor v2.1 handoff package — D10 doc-authority ruling (ratified
|
||||||
|
# 2026-06-10; register: docs/ux-refactor/deltas/open-questions.md, section
|
||||||
|
# "UX Refactor v2.1 — Ratification of Decision Register D1–D15").
|
||||||
|
# Text sources stay TRACKED: PRD .md, Implementation Handoff .md,
|
||||||
|
# _blueprint_extracted.txt, NAMING-ERRATUM.md, and
|
||||||
|
# Waggle_OS_Handoff_Assets/{README.md,ASSET_MANIFEST.json}.
|
||||||
|
# Binary exports stay LOCAL-ONLY (~85 MB: pdf/docx/pptx + generated mockup
|
||||||
|
# png/jpg — mockups are visual direction, not authority; see the package's
|
||||||
|
# NAMING-ERRATUM.md for the authority chain).
|
||||||
|
docs/Waggle_OS_UX_Refactor_Master_Handoff_Package/*.pdf
|
||||||
|
docs/Waggle_OS_UX_Refactor_Master_Handoff_Package/*.docx
|
||||||
|
docs/Waggle_OS_UX_Refactor_Master_Handoff_Package/*.pptx
|
||||||
|
docs/Waggle_OS_UX_Refactor_Master_Handoff_Package/Waggle_OS_Handoff_Assets/*.png
|
||||||
|
docs/Waggle_OS_UX_Refactor_Master_Handoff_Package/Waggle_OS_Handoff_Assets/*.jpg
|
||||||
|
|
||||||
|
# Internal / pre-OSS artifacts — kept on disk, excluded from the public repo.
|
||||||
|
# These are currently tracked; run `git rm --cached <file>` once to stop tracking
|
||||||
|
# them (the entries below keep them from being re-added). Root-anchored so nested
|
||||||
|
# files of the same name elsewhere are unaffected.
|
||||||
|
/Waggle_Competitive_Intelligence_Full_Landscape_March_2026.docx
|
||||||
|
/EVAL-RESULTS.md
|
||||||
|
/EVAL-RESULTS-V5.md
|
||||||
|
/PLAN.md
|
||||||
|
|
||||||
|
# npm is the canonical package manager (CLAUDE.md); bun.lock removed to avoid a
|
||||||
|
# divergent dependency graph from a stray `bun install`.
|
||||||
|
bun.lock
|
||||||
|
|
||||||
|
# /understand tool trash — regenerable deleted-graph JSON, not for the public repo
|
||||||
|
.understand-anything/.trash*
|
||||||
|
.gstack/
|
||||||
|
|
||||||
|
# Local plaintext API keys — never commit
|
||||||
|
AI API KEYS.txt
|
||||||
23
.lovable/plan.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
|
||||||
|
## Make App Windows Draggable
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Windows currently use fixed `defaultPosition` via framer-motion `animate`, so they can't be repositioned by the user.
|
||||||
|
|
||||||
|
### Approach
|
||||||
|
Use framer-motion's built-in `drag` prop on the window container, constrained by a `dragConstraints` ref (the desktop). The title bar acts as the drag handle via `dragListener={false}` on the main div + `dragControls` triggered from the title bar.
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
**`src/components/os/AppWindow.tsx`**
|
||||||
|
- Add `useState` for `position` (initialized from `defaultPosition`)
|
||||||
|
- Use `useDragControls` from framer-motion
|
||||||
|
- Add `drag` prop to `motion.div` with `dragControls`, `dragMomentum={false}`, `dragListener={false}`
|
||||||
|
- Add `dragConstraints` prop (parent bounds or screen-based object)
|
||||||
|
- Title bar gets `onPointerDown` to start drag via `dragControls.start()`
|
||||||
|
- When maximized, disable drag
|
||||||
|
- Track position via `onDragEnd` to persist position across re-renders
|
||||||
|
- Remove the `x`/`y`/`top`/`left` from the `animate` prop (use `style` for initial positioning instead, so drag offset works correctly)
|
||||||
|
- Set `cursor-grab` / `cursor-grabbing` on title bar
|
||||||
|
|
||||||
29
.parity-allowlist
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Memory Sync Repair — `mind-parity-check` allowlist
|
||||||
|
#
|
||||||
|
# Lines list test filenames (basename, e.g. `db-hive-mind.test.ts`) whose
|
||||||
|
# verbatim hive-mind copy is INTENTIONALLY skipped during the parity check
|
||||||
|
# because waggle-os has an adapted version under the natural name (e.g.
|
||||||
|
# `db.test.ts`) that handles a legitimate API divergence per
|
||||||
|
# `D:\Projects\hive-mind\EXTRACTION.md`.
|
||||||
|
#
|
||||||
|
# Format: one filename per line. `#` starts a comment. Blank lines ignored.
|
||||||
|
#
|
||||||
|
# Adding an entry: include a comment line directly above the entry that
|
||||||
|
# explains *why* the divergence exists (link to EXTRACTION.md section, PR,
|
||||||
|
# or PM-Waggle-OS decisions/ memo).
|
||||||
|
#
|
||||||
|
# Removing an entry: only when the divergence has been resolved (either
|
||||||
|
# hive-mind upstream changed or waggle-os adopted the upstream behavior).
|
||||||
|
# Re-running parity check should pass without the allowlist entry before
|
||||||
|
# removal lands.
|
||||||
|
|
||||||
|
# `db-hive-mind.test.ts` — hive-mind asserts proprietary tables
|
||||||
|
# (ai_interactions, execution_traces, evolution_runs, improvement_signals,
|
||||||
|
# install_audit) MUST BE ABSENT, which is its OSS-scrub guarantee. Waggle-os
|
||||||
|
# legitimately carries those tables per EXTRACTION.md "NOT extracted"
|
||||||
|
# section. The waggle-os adaptation lives in committed `db.test.ts` (no
|
||||||
|
# suffix) which splits the original into "OSS shared must exist" (verbatim)
|
||||||
|
# + "Waggle-specific must exist" (inverted). See:
|
||||||
|
# - PM-Waggle-OS/decisions/2026-04-26-memory-sync-step2-test-port-results.md §2
|
||||||
|
# - hive-mind EXTRACTION.md "NOT Extracted" section
|
||||||
|
db-hive-mind.test.ts
|
||||||
191
.understand-anything/.understandignore
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# .understandignore — patterns for files/dirs to exclude from analysis
|
||||||
|
# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs)
|
||||||
|
# Lines below are suggestions — uncomment to activate.
|
||||||
|
# Use ! prefix to force-include something excluded by defaults.
|
||||||
|
#
|
||||||
|
# Built-in defaults (always excluded unless negated):
|
||||||
|
# node_modules/, .git/, dist/, build/, obj/, *.lock, *.min.js, etc.
|
||||||
|
#
|
||||||
|
|
||||||
|
# --- From .gitignore (uncomment to exclude) ---
|
||||||
|
|
||||||
|
# .mcp.json
|
||||||
|
# logs
|
||||||
|
# npm-debug.log*
|
||||||
|
# yarn-debug.log*
|
||||||
|
# yarn-error.log*
|
||||||
|
# pnpm-debug.log*
|
||||||
|
# lerna-debug.log*
|
||||||
|
# dist-ssr
|
||||||
|
# *.local
|
||||||
|
# .env
|
||||||
|
# .env.local
|
||||||
|
# .env*.local
|
||||||
|
# .waggle/
|
||||||
|
# *.mind
|
||||||
|
# marketplace.db
|
||||||
|
# *.pfx
|
||||||
|
# app/src-tauri/.thumbprint.txt
|
||||||
|
# preflight-results/stage-0-query-*.json
|
||||||
|
# preflight-results/stage-0-tmp/
|
||||||
|
# preflight-results/stage-*-attempt-*.md
|
||||||
|
# preflight-results/stage-*-exclusions.md
|
||||||
|
# benchmarks/data/*
|
||||||
|
# !benchmarks/data/.gitkeep
|
||||||
|
# !benchmarks/data/preflight-locomo-50.json
|
||||||
|
# !benchmarks/data/failure-mode-calibration-10.jsonl
|
||||||
|
# !benchmarks/data/locomo/
|
||||||
|
# !benchmarks/data/locomo/locomo-1540.jsonl
|
||||||
|
# !benchmarks/data/locomo/locomo-1540.meta.json
|
||||||
|
# !benchmarks/data/longmemeval/
|
||||||
|
# benchmarks/data/longmemeval/*
|
||||||
|
# !benchmarks/data/longmemeval/longmemeval.meta.json
|
||||||
|
# !benchmarks/data/beam/
|
||||||
|
# benchmarks/data/beam/*
|
||||||
|
# !benchmarks/data/beam/beam-128K.meta.json
|
||||||
|
# !benchmarks/data/beam/beam-1M.meta.json
|
||||||
|
# benchmarks/memori-replication/
|
||||||
|
# benchmarks/results/*
|
||||||
|
# !benchmarks/results/.gitkeep
|
||||||
|
# !benchmarks/results/manifest-*.md
|
||||||
|
# !benchmarks/results/manifest-*.yaml
|
||||||
|
# !benchmarks/results/stage*-gate-*.md
|
||||||
|
# !benchmarks/results/stage*-gate-*.jsonl
|
||||||
|
# !benchmarks/results/stage3-n400-v6-*.md
|
||||||
|
# !benchmarks/results/agentic-locomo-2026-04-25T*.jsonl
|
||||||
|
# !benchmarks/results/agentic-locomo-2026-04-25T*.summary.json
|
||||||
|
# !benchmarks/results/v6-self-judge-rebench/
|
||||||
|
# !benchmarks/results/v6-self-judge-rebench/*.jsonl
|
||||||
|
# !benchmarks/results/v6-self-judge-rebench/*.md
|
||||||
|
# !benchmarks/results/pilot-2026-04-26/
|
||||||
|
# !benchmarks/results/pilot-2026-04-26/*.jsonl
|
||||||
|
# !benchmarks/results/pilot-2026-04-26/*.json
|
||||||
|
# !benchmarks/results/pilot-2026-04-26/*.log
|
||||||
|
# !benchmarks/results/pilot-2026-04-26/prompts-archive/
|
||||||
|
# !benchmarks/results/pilot-2026-04-26/prompts-archive/*.md
|
||||||
|
# !benchmarks/results/pilot-2026-04-26/invalidated/
|
||||||
|
# !benchmarks/results/pilot-2026-04-26/invalidated/*.jsonl
|
||||||
|
# !benchmarks/results/gepa-faza1/
|
||||||
|
# !benchmarks/results/gepa-faza1/**/*.jsonl
|
||||||
|
# !benchmarks/results/gepa-faza1/**/*.json
|
||||||
|
# !benchmarks/results/gepa-faza1/**/*.md
|
||||||
|
# !benchmarks/results/gepa-faza1/**/*.log
|
||||||
|
# !benchmarks/results/gepa-faza1/**/*.yaml
|
||||||
|
# !**/benchmarks/results/gepa-faza1
|
||||||
|
# !**/benchmarks/results/gepa-faza1/
|
||||||
|
# !**/benchmarks/results/gepa-faza1/**
|
||||||
|
# !benchmarks/probes/**/*.log
|
||||||
|
# !benchmarks/probes/**/*.jsonl
|
||||||
|
# !benchmarks/probes/**/*.py
|
||||||
|
# !benchmarks/probes/**/*.md
|
||||||
|
# **/benchmarks/results/*
|
||||||
|
# **/benchmarks/data/*
|
||||||
|
# !**/benchmarks/data/preflight-locomo-50.json
|
||||||
|
# !**/benchmarks/data/failure-mode-calibration-10.jsonl
|
||||||
|
# !**/benchmarks/data/locomo/
|
||||||
|
# !**/benchmarks/data/locomo/locomo-1540.jsonl
|
||||||
|
# !**/benchmarks/data/locomo/locomo-1540.meta.json
|
||||||
|
# !**/benchmarks/data/longmemeval/
|
||||||
|
# **/benchmarks/data/longmemeval/*
|
||||||
|
# !**/benchmarks/data/longmemeval/longmemeval.meta.json
|
||||||
|
# !**/benchmarks/data/beam/
|
||||||
|
# **/benchmarks/data/beam/*
|
||||||
|
# !**/benchmarks/data/beam/beam-128K.meta.json
|
||||||
|
# !**/benchmarks/data/beam/beam-1M.meta.json
|
||||||
|
# !**/benchmarks/data/.gitkeep
|
||||||
|
# !**/benchmarks/results/.gitkeep
|
||||||
|
# !**/benchmarks/results/manifest-*.md
|
||||||
|
# !**/benchmarks/results/manifest-*.yaml
|
||||||
|
# !**/benchmarks/results/stage*-gate-*.md
|
||||||
|
# !**/benchmarks/results/stage*-gate-*.jsonl
|
||||||
|
# !**/benchmarks/results/stage3-n400-v6-*.md
|
||||||
|
# !**/benchmarks/results/agentic-locomo-2026-04-25T*.jsonl
|
||||||
|
# !**/benchmarks/results/agentic-locomo-2026-04-25T*.summary.json
|
||||||
|
# !**/benchmarks/results/v6-self-judge-rebench/
|
||||||
|
# !**/benchmarks/results/v6-self-judge-rebench/*.jsonl
|
||||||
|
# !**/benchmarks/results/v6-self-judge-rebench/*.md
|
||||||
|
# !**/benchmarks/results/pilot-2026-04-26/
|
||||||
|
# !**/benchmarks/results/pilot-2026-04-26/*.jsonl
|
||||||
|
# !**/benchmarks/results/pilot-2026-04-26/*.json
|
||||||
|
# !**/benchmarks/results/pilot-2026-04-26/*.log
|
||||||
|
# !**/benchmarks/results/pilot-2026-04-26/prompts-archive/
|
||||||
|
# !**/benchmarks/results/pilot-2026-04-26/prompts-archive/*.md
|
||||||
|
# !**/benchmarks/results/pilot-2026-04-26/invalidated/
|
||||||
|
# !**/benchmarks/results/pilot-2026-04-26/invalidated/*.jsonl
|
||||||
|
# !benchmarks/archive/
|
||||||
|
# !benchmarks/archive/**
|
||||||
|
# test-regression-*.log
|
||||||
|
# marketplace.db-shm
|
||||||
|
# marketplace.db-wal
|
||||||
|
# *.db-shm
|
||||||
|
# *.db-wal
|
||||||
|
# .workspace/
|
||||||
|
# .scratch/
|
||||||
|
# .mind/
|
||||||
|
# .planning/
|
||||||
|
# external/
|
||||||
|
# *.tsbuildinfo
|
||||||
|
# scripts/.cache/
|
||||||
|
# app/src-tauri/resources/node
|
||||||
|
# app/src-tauri/resources/node.exe
|
||||||
|
# app/src-tauri/resources/native/
|
||||||
|
# app/src-tauri/resources/service.js
|
||||||
|
# app/src-tauri/resources/service.js.map
|
||||||
|
# .vscode/*
|
||||||
|
# !.vscode/extensions.json
|
||||||
|
# .DS_Store
|
||||||
|
# *.suo
|
||||||
|
# *.ntvs*
|
||||||
|
# *.njsproj
|
||||||
|
# *.sln
|
||||||
|
# *.sw?
|
||||||
|
# .claude/
|
||||||
|
# .playwright-mcp/
|
||||||
|
# .superpowers/
|
||||||
|
# ~$*
|
||||||
|
# playwright-report/
|
||||||
|
# test-results/
|
||||||
|
# tests/screenshots/
|
||||||
|
# s*.png
|
||||||
|
# screen*.png
|
||||||
|
# tmp_bench_results.json
|
||||||
|
# tmp_bench_results-v5.json
|
||||||
|
# tmp_hermes-skill-reuse.json
|
||||||
|
# tmp_hermes-pilot*.json
|
||||||
|
# tmp_hermes-pilot*.log
|
||||||
|
# tmp_lpv*.log
|
||||||
|
# tmp_lpv*.json
|
||||||
|
# apps/www/public/brand/_backup-*/
|
||||||
|
# .vercel/
|
||||||
|
# apps/www/lighthouse-report.json
|
||||||
|
# tests/vision/artifacts/
|
||||||
|
# docs/Waggle_OS_UX_Refactor_Master_Handoff_Package/*.pdf
|
||||||
|
# docs/Waggle_OS_UX_Refactor_Master_Handoff_Package/*.docx
|
||||||
|
# docs/Waggle_OS_UX_Refactor_Master_Handoff_Package/*.pptx
|
||||||
|
# docs/Waggle_OS_UX_Refactor_Master_Handoff_Package/Waggle_OS_Handoff_Assets/*.png
|
||||||
|
# docs/Waggle_OS_UX_Refactor_Master_Handoff_Package/Waggle_OS_Handoff_Assets/*.jpg
|
||||||
|
|
||||||
|
# --- Detected directories (uncomment to exclude) ---
|
||||||
|
|
||||||
|
# docs/
|
||||||
|
# scripts/
|
||||||
|
# tests/
|
||||||
|
|
||||||
|
# --- Test file patterns (uncomment to exclude) ---
|
||||||
|
|
||||||
|
# JS / TS
|
||||||
|
# *.test.*
|
||||||
|
# *.spec.*
|
||||||
|
# *.snap
|
||||||
|
# C# / .NET
|
||||||
|
# **/*Tests.cs
|
||||||
|
# **/*Test.cs
|
||||||
|
# **/*Fixture.cs
|
||||||
|
# **/*.Tests.csproj
|
||||||
|
# Java / Kotlin
|
||||||
|
# **/src/test/**
|
||||||
|
# **/*Test.java
|
||||||
|
# **/*IT.java
|
||||||
|
# **/*Spec.kt
|
||||||
|
# Go
|
||||||
|
# **/*_test.go
|
||||||
124685
.understand-anything/fingerprints.json
Normal file
24793
.understand-anything/intermediate/scan-result.json
Normal file
107900
.understand-anything/knowledge-graph.json
Normal file
6
.understand-anything/meta.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"lastAnalyzedAt": "2026-06-26T09:19:56.431Z",
|
||||||
|
"gitCommitHash": "18aebe1f4bd035cfe2646173db7d2fa5326d2ec9",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"analyzedFiles": 2896
|
||||||
|
}
|
||||||
639
AGENTS.md
Normal file
@@ -0,0 +1,639 @@
|
|||||||
|
# AGENTS.md — Waggle OS
|
||||||
|
### Authoritative Operating Contract · All Agents · All Contributors · All Sessions
|
||||||
|
|
||||||
|
> Read this file in full before touching a single line of code.
|
||||||
|
> It is the single source of truth for architecture, strategic intent, and mechanical operating rules.
|
||||||
|
> If this file conflicts with any other document, **this file wins.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. How to Use This File
|
||||||
|
|
||||||
|
This file has two parts: **what the project is** (Sections 1-2) and **how to work on it** (Sections 3-9).
|
||||||
|
If you're about to write code, **Section 3** is the most important thing you'll read.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What Waggle OS Actually Is
|
||||||
|
|
||||||
|
**Waggle OS** is a workspace-native AI agent platform with persistent memory. It ships as a
|
||||||
|
Tauri 2.0 desktop binary for Windows and macOS, with a Vite-bundled web app and a Node.js sidecar.
|
||||||
|
|
||||||
|
**Strategic function:** Waggle is the demand-creation and qualification engine for KVARK —
|
||||||
|
Egzakta Group's sovereign enterprise AI platform.
|
||||||
|
|
||||||
|
### Tiers (verified from `packages/shared/src/tiers.ts` — 4-tier: TRIAL/FREE(Solo)/TEAMS/ENTERPRISE, Solo-vs-Team collapse 2026-07-05)
|
||||||
|
|
||||||
|
| Tier | Price | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| TRIAL | $0 / 15 days | TEAM preview — 15 days of Team, then Solo |
|
||||||
|
| FREE (Solo) | $0 forever | Everything personal: unlimited workspaces+connectors, marketplace/custom skills, cloud embeddings, PDF/JSON export, basic audit — free forever |
|
||||||
|
| TEAMS | $49/mo per seat | Shared workspaces, WaggleDance, governance |
|
||||||
|
| ENTERPRISE | Consultative | KVARK sovereign on-prem (www.kvark.ai) |
|
||||||
|
|
||||||
|
> PRO ($19/mo) was removed in the Solo-vs-Team collapse (2026-07-05); its
|
||||||
|
> capabilities folded into FREE (Solo). `TIER_LABELS` displays FREE as "Solo".
|
||||||
|
|
||||||
|
**Moat strategy:** Memory + Harvest is free forever (lock-in moat). Agents, skills,
|
||||||
|
and connectors are all free (they generate memory). Team collaboration (shared memory,
|
||||||
|
WaggleDance, governance) is the upgrade trigger.
|
||||||
|
|
||||||
|
### Key Technology Facts (Verified April 2026)
|
||||||
|
|
||||||
|
| Layer | Stack |
|
||||||
|
|---|---|
|
||||||
|
| Frontend | React **19** + TypeScript + Vite + Tailwind 4 + base-ui/react |
|
||||||
|
| Desktop | Tauri 2.0 (Rust shell) |
|
||||||
|
| Backend | Fastify sidecar (Node.js, bundled into Tauri) |
|
||||||
|
| LLM routing | LiteLLM (see `litellm-config.yaml`) |
|
||||||
|
| Database | SQLite via @waggle/core (better-sqlite3 + sqlite-vec-windows-x64) |
|
||||||
|
| Memory | FrameStore + HybridSearch + KnowledgeGraph + IdentityLayer + AwarenessLayer |
|
||||||
|
| Agent runtime | `packages/agent/src/agent-loop.ts` |
|
||||||
|
| Billing | Stripe (installed; `stripe@^21.0.1`) |
|
||||||
|
| Design | Hive DS — honey #e5a000 / hive-950 #08090c / accent #a78bfa |
|
||||||
|
| Tests | Vitest (unit) + Playwright (E2E) |
|
||||||
|
| Deploy | Dockerfile + docker-compose.production.yml + render.yaml |
|
||||||
|
|
||||||
|
Package manager: npm (root) with `bun.lock` also present. Node >= 20.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Repository Structure (Verified)
|
||||||
|
|
||||||
|
### Top level
|
||||||
|
```
|
||||||
|
waggle-os/
|
||||||
|
├── app/ # Tauri desktop shell (minimal React surface)
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # <-- MAIN web app UI (this is where most components live)
|
||||||
|
│ └── www/ # Landing page (waggle-os.ai)
|
||||||
|
├── packages/ # 16 workspace packages (see below)
|
||||||
|
├── sidecar/ # Node.js sidecar bundled into Tauri
|
||||||
|
├── scripts/ # build-sidecar, bundle-native-deps, bundle-node
|
||||||
|
├── tests/ # Cross-cutting integration tests
|
||||||
|
├── docs/ # ARCHITECTURE.md and others
|
||||||
|
├── cowork/ # Scratchpad / planning / handoff docs (historical; AGENTS.md promoted to root)
|
||||||
|
├── .planning/ .scratch/ .mind/ # Working notes
|
||||||
|
├── docker-compose.yml + .production.yml + Dockerfile + render.yaml
|
||||||
|
├── litellm-config.yaml # LLM router config
|
||||||
|
├── playwright.config.ts + playwright-e2e.config.ts
|
||||||
|
├── vitest.config.ts + vitest.setup.ts
|
||||||
|
└── package.json (workspaces: apps/*, packages/*)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Packages (`packages/`, 27 workspaces — verified 2026-05-28)
|
||||||
|
```
|
||||||
|
Core (15):
|
||||||
|
admin-web cli launcher marketplace
|
||||||
|
agent core memory-mcp optimizer
|
||||||
|
sdk server shared waggle-dance
|
||||||
|
weaver wiki-compiler worker
|
||||||
|
|
||||||
|
hive-mind OSS split (12 — synced to marolinik/hive-mind, see §7.5):
|
||||||
|
hive-mind-core hive-mind-cli hive-mind-shim-core hive-mind-mcp-server
|
||||||
|
hive-mind-wiki-compiler
|
||||||
|
hive-mind-hooks-{Codex, Codex-desktop, codex, codex-desktop,
|
||||||
|
cursor, hermes, openclaw}
|
||||||
|
```
|
||||||
|
> Note: the prior list said "16" and included `ui`, which has no `package.json`
|
||||||
|
> (not a workspace). Real count is 27. The 12 `hive-mind-*` packages were added
|
||||||
|
> since the April verification.
|
||||||
|
|
||||||
|
### `packages/agent/src/` — MOST ACTIVE (94 .ts files + 4 subdirs)
|
||||||
|
|
||||||
|
Key files (not exhaustive — grep before creating anything new):
|
||||||
|
```
|
||||||
|
agent-loop.ts Core execution loop
|
||||||
|
orchestrator.ts buildSystemPrompt(), recallMemory()
|
||||||
|
personas.ts AgentPersona interface + logic (data split out)
|
||||||
|
persona-data.ts Pure PERSONAS declarative data array
|
||||||
|
custom-personas.ts loadCustomPersonas() from disk
|
||||||
|
behavioral-spec.ts BEHAVIORAL_SPEC rules
|
||||||
|
tool-filter.ts filterToolsForContext()
|
||||||
|
injection-scanner.ts scanForInjection() — 3 pattern sets
|
||||||
|
cost-tracker.ts CostTracker + model pricing
|
||||||
|
skill-frontmatter.ts parseSkillFrontmatter()
|
||||||
|
kvark-tools.ts kvark_search, kvark_ask_document (tier-gated)
|
||||||
|
feature-flags.ts EXISTS — don't recreate
|
||||||
|
subagent-orchestrator.ts Subagent spawn/coord
|
||||||
|
workflow-composer.ts
|
||||||
|
workflow-harness.ts
|
||||||
|
workflow-templates.ts
|
||||||
|
|
||||||
|
Evolution subsystem:
|
||||||
|
evolution-orchestrator.ts evolution-deploy.ts evolution-gates.ts
|
||||||
|
evolution-llm-wiring.ts evolve-schema.ts iterative-optimizer.ts
|
||||||
|
judge.ts eval-dataset.ts compose-evolution.ts
|
||||||
|
|
||||||
|
Capability & trust:
|
||||||
|
capability-acquisition.ts capability-router.ts trust-model.ts
|
||||||
|
permissions.ts credential-pool.ts confirmation.ts
|
||||||
|
|
||||||
|
Quality & correction:
|
||||||
|
quality-controller.ts contradiction-detector.ts
|
||||||
|
correction-detector.ts improvement-detector.ts improvement-wiring.ts
|
||||||
|
loop-guard.ts iteration-budget.ts
|
||||||
|
|
||||||
|
Subdirs:
|
||||||
|
commands/ connectors/ mcp/ providers/
|
||||||
|
```
|
||||||
|
|
||||||
|
### `packages/core/src/`
|
||||||
|
```
|
||||||
|
Top-level: config.ts, cron-store.ts, file-store.ts, install-audit.ts,
|
||||||
|
logger.ts (createCoreLogger), memory-import.ts, migration.ts,
|
||||||
|
multi-mind.ts, multi-mind-cache.ts, optimization-log.ts,
|
||||||
|
skill-hashes.ts, team-sync.ts, telemetry.ts, vault.ts,
|
||||||
|
workspace-config.ts, index.ts
|
||||||
|
|
||||||
|
Subdirs:
|
||||||
|
compliance/ — compliance reporting, interaction-store, status-checker
|
||||||
|
|
||||||
|
MOVED (2026-04-30 monorepo migration): the memory substrate `mind/` (db/schema/
|
||||||
|
identity/awareness/frames/sessions/search/knowledge/scoring/reconcile/ontology/
|
||||||
|
concept-tracker/entity-normalizer/evolution-runs/execution-traces/
|
||||||
|
improvement-signals/embedding-provider/*-embedder) and `harvest/` (chatgpt/Codex/
|
||||||
|
Codex/gemini/perplexity/pdf/plaintext/markdown/url/universal adapters +
|
||||||
|
pipeline.ts + dedup.ts) now live at **packages/hive-mind-core/src/{mind,harvest}/**,
|
||||||
|
NOT under packages/core/. The OSS mirror is generated from there via subtree-split (§7.5).
|
||||||
|
```
|
||||||
|
|
||||||
|
For the deep-dive on what the mind/ substrate does, see [`docs/memory-architecture.md`](docs/memory-architecture.md).
|
||||||
|
|
||||||
|
### `packages/shared/src/`
|
||||||
|
```
|
||||||
|
types.ts User, Team, AgentDef, Task, WaggleMessage
|
||||||
|
constants.ts Team roles, job statuses
|
||||||
|
schemas.ts Zod schemas
|
||||||
|
tiers.ts TIERS + TierCapabilities (canonical 4-tier: TRIAL/FREE(Solo)/TEAMS/ENTERPRISE) + TIER_LABELS/tierLabel
|
||||||
|
mcp-catalog.ts MCP server catalog
|
||||||
|
index.ts Barrel
|
||||||
|
```
|
||||||
|
|
||||||
|
### `app/` (Tauri desktop shell)
|
||||||
|
```
|
||||||
|
app/src-tauri/ # Rust shell + capabilities/ + tauri.conf.json
|
||||||
|
app/scripts/ # build/installer/signing TS tooling (tauri-tsc gate target)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** `app/` is now the Tauri Rust shell only — there is no `app/src/`. The
|
||||||
|
React cockpit UI moved to `apps/web` long ago; the desktop binary loads the
|
||||||
|
`apps/web` dist. All React UI lives in `apps/web/src/`.
|
||||||
|
|
||||||
|
### `apps/web/src/` (MAIN UI)
|
||||||
|
```
|
||||||
|
apps/web/src/
|
||||||
|
├── assets/ components/ hooks/ lib/
|
||||||
|
├── pages/ providers/ test/
|
||||||
|
|
||||||
|
components/
|
||||||
|
├── os/
|
||||||
|
│ ├── apps/ # Per-app UI shells
|
||||||
|
│ └── overlays/ # OnboardingWizard.tsx, PersonaSwitcher.tsx live HERE
|
||||||
|
└── ui/ # Shared UI primitives
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Commands (verified from `package.json`)
|
||||||
|
```bash
|
||||||
|
npm run dev # Vite dev server (apps/web)
|
||||||
|
npm run build # Vite build to /dist (apps/web)
|
||||||
|
npm run build:packages # tsc --build: shared -> core -> agent -> server (order matters)
|
||||||
|
npm run build:all # Packages then web
|
||||||
|
npm run lint # ESLint repo-wide
|
||||||
|
npm run test # Vitest unit tests
|
||||||
|
npm run test:e2e # Playwright API tests
|
||||||
|
npm run test:visual # Playwright visual regression
|
||||||
|
npm run test:all # Full Playwright
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification Commands (run these, don't claim "it compiles")
|
||||||
|
```bash
|
||||||
|
npx tsc --noEmit --project packages/agent/tsconfig.json
|
||||||
|
npx tsc --noEmit --project packages/server/tsconfig.json # sidecar — runs via tsx (transpile-only), so NOT typechecked by `npm run build`
|
||||||
|
npx tsc --noEmit --project app/tsconfig.json
|
||||||
|
npm run test -- --run
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
> `npm run build` typechecks **only `apps/web`**. The Fastify sidecar runs via
|
||||||
|
> `tsx` (transpile-only) — server-route type errors ship undetected unless you
|
||||||
|
> run the `packages/server` tsc above. (A real type error slipped through this
|
||||||
|
> way on 2026-05-28; see `docs/addictiveness-audit-2026-05-28/REDUNDANCY-AUDIT.md`.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Behavioral Rules — How You Must Work
|
||||||
|
|
||||||
|
These rules apply to every code change. They exist because violations have cost real debugging time.
|
||||||
|
|
||||||
|
### 3.1 Think Before Coding
|
||||||
|
|
||||||
|
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||||
|
|
||||||
|
Before implementing anything:
|
||||||
|
- State your assumptions explicitly. If uncertain, ask.
|
||||||
|
- If multiple interpretations exist, present them — don't pick silently.
|
||||||
|
- If a simpler approach exists, say so. Push back when warranted.
|
||||||
|
- If something is unclear, stop. Name what's confusing. Ask.
|
||||||
|
|
||||||
|
=== CRITICAL ===
|
||||||
|
The single most expensive LLM failure mode is making wrong assumptions and building
|
||||||
|
100+ lines on top of them. The fix costs 10x what the question would have cost.
|
||||||
|
Stop. Ask. Then build.
|
||||||
|
=== END CRITICAL ===
|
||||||
|
|
||||||
|
### 3.2 Simplicity First
|
||||||
|
|
||||||
|
**Minimum code that solves the problem. Nothing speculative.**
|
||||||
|
|
||||||
|
- No features beyond what was asked.
|
||||||
|
- No abstractions for single-use code.
|
||||||
|
- No "flexibility" or "configurability" that wasn't requested.
|
||||||
|
- No error handling for impossible scenarios.
|
||||||
|
- If you write 200 lines and it could be 50, rewrite it.
|
||||||
|
|
||||||
|
Test: **"Would a senior engineer say this is overcomplicated?"** If yes, simplify.
|
||||||
|
|
||||||
|
### 3.3 Surgical Changes
|
||||||
|
|
||||||
|
**Touch only what you must. Clean up only your own mess.**
|
||||||
|
|
||||||
|
When editing existing code:
|
||||||
|
- Don't "improve" adjacent code, comments, or formatting.
|
||||||
|
- Don't refactor things that aren't broken.
|
||||||
|
- Match existing style, even if you'd do it differently.
|
||||||
|
- If you notice unrelated dead code, **mention it** — don't delete it.
|
||||||
|
|
||||||
|
When your changes create orphans:
|
||||||
|
- Remove imports/variables/functions YOUR changes made unused.
|
||||||
|
- Don't remove pre-existing dead code unless asked.
|
||||||
|
|
||||||
|
Test: **Every changed line should trace directly to the request.**
|
||||||
|
|
||||||
|
### 3.4 Goal-Driven Execution
|
||||||
|
|
||||||
|
**Define success criteria. Loop until verified.**
|
||||||
|
|
||||||
|
Transform vague tasks into verifiable goals:
|
||||||
|
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||||
|
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||||
|
- "Refactor X" → "Ensure tests pass before and after"
|
||||||
|
|
||||||
|
For multi-step tasks, state a brief plan:
|
||||||
|
```
|
||||||
|
1. [Step] -> verify: [check]
|
||||||
|
2. [Step] -> verify: [check]
|
||||||
|
3. [Step] -> verify: [check]
|
||||||
|
```
|
||||||
|
|
||||||
|
A task is not done until verification passes. "I think this works" is not verification.
|
||||||
|
|
||||||
|
### 3.5 Context Discipline
|
||||||
|
|
||||||
|
- **Context decay:** After 10+ messages, re-read any file before editing. Do not trust memory.
|
||||||
|
- **File read budget:** Files >500 LOC require chunked reads. Never assume complete view.
|
||||||
|
- **Truncation:** Tool results >50k chars are silently truncated. If sparse, re-run narrower.
|
||||||
|
- **Re-read before edit. Re-read after edit.** Max 3 edits per file before verification read.
|
||||||
|
- **Exhaustive grep on rename:** Direct refs, type-level, string literals, dynamic imports,
|
||||||
|
re-exports/barrel entries, test files. **One grep is never enough.**
|
||||||
|
|
||||||
|
### 3.6 Check Before Create
|
||||||
|
|
||||||
|
Before adding a new file, **grep first.** The repo has ~94 files in `packages/agent/src`
|
||||||
|
alone. If you're about to write something that might already exist, it probably does.
|
||||||
|
See Section 8 for known utilities.
|
||||||
|
|
||||||
|
### 3.7 Output Discipline
|
||||||
|
|
||||||
|
- **Chat reply budget.** Long specs, handoffs, audit reports, and multi-phase plans MUST
|
||||||
|
be written to files (memory/, docs/, or via `/handoff`), not rendered inline. The chat
|
||||||
|
is a pointer; the file is the deliverable.
|
||||||
|
- **Chunk long work.** Multi-phase roadmaps and >1k-line specs: implement in phases,
|
||||||
|
commit per phase, give a 3-line status, then stop and await the next instruction. Do
|
||||||
|
not stream an exhaustive summary that blows the output budget.
|
||||||
|
- **Rationale:** 13+ prior sessions were lost mid-response to the 500-output-token cap.
|
||||||
|
Surface shortly, persist richly.
|
||||||
|
|
||||||
|
### 3.8 Handoff Discipline
|
||||||
|
|
||||||
|
- **Use the skill.** End-of-session handoffs invoke `~/.Codex/skills/handoff/`, which
|
||||||
|
enforces verification (`git status`, tests N/M, `npx tsc --noEmit` on touched packages)
|
||||||
|
BEFORE writing the doc. Do not hand-write handoffs that skip the gate.
|
||||||
|
- **Canonical location.** Handoffs live at
|
||||||
|
`C:/Users/MarkoMarkovic/.Codex/projects/D--Projects-waggle-os/memory/project_session_handoff_<MMDD>_s<N>.md`,
|
||||||
|
with the memory-dir MEMORY.md "START HERE" pointer updated. That is the single source
|
||||||
|
of truth for what shipped / what's left / how to roll back.
|
||||||
|
- **Never hide failures.** Failing tests, unverified MCP reconnects, wrong build dir —
|
||||||
|
surface under "What's still open" in the handoff. Clean-looking handoffs that hide rot
|
||||||
|
cost the next session hours.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Pre-Work Protocol
|
||||||
|
|
||||||
|
Before any structural refactor on a file >300 LOC:
|
||||||
|
1. Remove dead props, unused exports, unused imports, `console.log`.
|
||||||
|
2. Commit separately: `chore(scope): dead code removal — [filename]`
|
||||||
|
|
||||||
|
**Phased execution:** Max 5 files per phase. Complete → verify → await approval → next phase.
|
||||||
|
|
||||||
|
**Senior dev override:** If architecture is flawed, state is duplicated, or patterns
|
||||||
|
are inconsistent — state it and propose a fix. Standard: *"What would a senior engineer
|
||||||
|
reject in review?"*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Persona Architecture
|
||||||
|
|
||||||
|
### Shipped (22 personas — data in `persona-data.ts`, logic in `personas.ts`)
|
||||||
|
The original 13 + 4 universal/orchestration + 5 domain personas all shipped.
|
||||||
|
The PersonaSwitcher groups them into **two tiers** (`apps/web/src/lib/persona-tier.ts`):
|
||||||
|
|
||||||
|
**Universal Modes (8 — always available in every workspace):**
|
||||||
|
general-purpose, planner, verifier, coordinator, researcher, writer, analyst, coder
|
||||||
|
- **general-purpose** — versatile default, full tool access
|
||||||
|
- **planner** — read-only strategic planning, no file writes (`isReadOnly`)
|
||||||
|
- **verifier** — adversarial QA, read-only, VERDICT output format (`isReadOnly`)
|
||||||
|
- **coordinator** — pure orchestrator, spawn/list/get_agent_result only (gated by `FEATURE_FLAGS.COORDINATOR_MODE`)
|
||||||
|
|
||||||
|
**Specialists (14 — template-scoped via `TEMPLATE_SPECIALISTS`):**
|
||||||
|
project-manager, executive-assistant, sales-rep, marketer, product-manager-senior,
|
||||||
|
hr-manager, legal-professional, finance-owner, consultant, support-agent,
|
||||||
|
ops-manager, data-engineer, recruiter, creative-director
|
||||||
|
|
||||||
|
> Note: the onboarding picker (`onboarding/constants.ts` → `ALL_ONBOARDING_PERSONAS`)
|
||||||
|
> intentionally surfaces only **19** of the 22 — it omits planner/verifier/coordinator
|
||||||
|
> (read-only/orchestration modes don't make sense as a workspace's *starting* brain) and
|
||||||
|
> uses its own 3-way grouping (universal/knowledge/domain). Same canonical personas, a
|
||||||
|
> different view for a different UI moment. `persona-data.ts` is the single source of truth.
|
||||||
|
|
||||||
|
**Split is done:** `persona-data.ts` holds the pure `PERSONAS` array;
|
||||||
|
`personas.ts` exports the `AgentPersona` interface and logic only.
|
||||||
|
|
||||||
|
### AgentPersona Interface — Shipped Fields (verified `personas.ts`)
|
||||||
|
```typescript
|
||||||
|
interface AgentPersona {
|
||||||
|
// core
|
||||||
|
id, name, description, icon, systemPrompt, modelPreference,
|
||||||
|
tools: string[], workspaceAffinity: string[],
|
||||||
|
suggestedCommands: string[], defaultWorkflow: string | null,
|
||||||
|
// guardrails + picker metadata (all optional, all shipped)
|
||||||
|
disallowedTools?: string[] // denylist — overrides tools[] on conflict
|
||||||
|
failurePatterns?: string[] // documented failure modes — shown in hover tooltip
|
||||||
|
isReadOnly?: boolean // true = no write tools ever (enforced in assembleToolPool)
|
||||||
|
tagline?: string // one sentence for picker hover
|
||||||
|
bestFor?: string[] // 3 example tasks in user-facing language
|
||||||
|
wontDo?: string // hard boundary statement
|
||||||
|
suggestedSkills?: string[] // installable from marketplace
|
||||||
|
suggestedConnectors?: string[] // connector IDs
|
||||||
|
suggestedMcpServers?: string[] // MCP server names from mcp-registry
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Onboarding & PersonaSwitcher (correct paths)
|
||||||
|
|
||||||
|
### OnboardingWizard
|
||||||
|
**Path:** `apps/web/src/components/os/overlays/OnboardingWizard.tsx`
|
||||||
|
(NOT `app/src/components/onboarding/` — that path doesn't exist.)
|
||||||
|
|
||||||
|
Shipped: 6-step flow (`first-launch → who-are-you → model-gate → memory-import →
|
||||||
|
template → first-task`). The 15 TEMPLATES + the `TEMPLATE_PERSONA` mapping (template →
|
||||||
|
one default persona id) live in `overlays/onboarding/constants.ts`, wired to the
|
||||||
|
canonical persona ids from `persona-data.ts`. The wizard surfaces a **curated 6** of the
|
||||||
|
15 (`CURATED_ONBOARDING_TEMPLATES`) for the ≤2-min flow; the full 15 are reachable from
|
||||||
|
the workspace gallery later. (Picker persona roster = `ALL_ONBOARDING_PERSONAS`, the
|
||||||
|
19-of-22 view noted in §5.)
|
||||||
|
|
||||||
|
### PersonaSwitcher
|
||||||
|
**Path:** `apps/web/src/components/os/overlays/PersonaSwitcher.tsx`
|
||||||
|
|
||||||
|
Shipped (M-01): Two-tier layout — "UNIVERSAL MODES" (8, from `UNIVERSAL_MODE_IDS`) +
|
||||||
|
"YOUR WORKSPACE SPECIALISTS" (template-scoped via `getSpecialistsForTemplate` in
|
||||||
|
`lib/persona-tier.ts`). Hover tooltip (`buildPersonaTooltip`, `lib/persona-tooltip.ts`)
|
||||||
|
shows tagline + bestFor + wontDo. "Create Custom Persona" inline form POSTs to
|
||||||
|
`/api/personas`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Security Constraints (Non-Negotiable)
|
||||||
|
|
||||||
|
1. **Vault-only secrets.** API keys in Vault or `.env` (never committed). `.env.example` has key names only.
|
||||||
|
2. **Injection defense.** `scanForInjection()` from `injection-scanner.ts` MUST be called on all connector/external input.
|
||||||
|
3. **No eval, no dynamic require.** Tauri WebView is restricted.
|
||||||
|
4. **Tauri IPC allowlist.** Explicit in `app/src-tauri/capabilities/`. Never `allowlist: all: true`.
|
||||||
|
5. **Parameterized queries.** No string interpolation in SQL. Ever. better-sqlite3 supports parameters.
|
||||||
|
6. **KVARK contact data.** Submits to your API only — no third-party form services.
|
||||||
|
7. **Secrets in `packages/core/src/vault.ts`** — use it; don't build parallel secret stores.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7.5. Memory Substrate Sync (waggle-os → hive-mind, subtree-split)
|
||||||
|
|
||||||
|
The memory substrate lives at **`packages/hive-mind-core/src/{mind,harvest}/`** (moved from
|
||||||
|
`packages/core/src/` in the 2026-04-30 monorepo migration). The public OSS mirror at
|
||||||
|
[`marolinik/hive-mind`](https://github.com/marolinik/hive-mind) is **generated FROM** this monorepo
|
||||||
|
via a **maintainer-curated forward-port** (NOT a mechanical `git subtree split` — see the
|
||||||
|
correction below). The mirror uses its own curated layout (`packages/core`, co-located tests,
|
||||||
|
rewritten imports) and **excludes** Waggle-proprietary content (see the exclusion list below).
|
||||||
|
|
||||||
|
=== CRITICAL — sync policy (founder-ratified 2026-06-11) ===
|
||||||
|
**The monorepo is the SOLE source of truth for the substrate. Never author substrate features
|
||||||
|
directly on the OSS mirror.** Parity is NOT automatic — it broke once: the cross-encoder reranker
|
||||||
|
(`inprocess-reranker.ts` + HybridSearch options) was written directly on `marolinik/hive-mind`
|
||||||
|
during the LoCoMo benchmark arc and existed ONLY there, discovered by the W4 recon and
|
||||||
|
reverse-ported in W4.2 (`f47ee8f`). Rules:
|
||||||
|
1. Substrate changes land in `packages/hive-mind-core/` here FIRST; the mirror is regenerated
|
||||||
|
via subtree-split afterward.
|
||||||
|
2. Benchmark/experiment work in a `D:/Projects/hive-mind` checkout is throwaway unless
|
||||||
|
reverse-ported here — port it the same arc, don't let it sit.
|
||||||
|
3. Run **`scripts/oss-drift-check.sh`** (file-level diff of the mapped src trees) before every
|
||||||
|
OSS release push and after any arc that touched a hive-mind checkout.
|
||||||
|
4. External PRs on the OSS repo are fine — the maintainer merges them back here via
|
||||||
|
subtree-pull, then re-splits.
|
||||||
|
=== END CRITICAL ===
|
||||||
|
|
||||||
|
=== CORRECTION — how the sync ACTUALLY works (2026-06-12 drift analysis) ===
|
||||||
|
The prior text here claimed the mirror is produced by `scripts/oss-subtree-split.sh` and that a
|
||||||
|
"subtree-split filter" handles the must-not-export files. **Both were false** (verified
|
||||||
|
2026-06-12, `docs/ux-refactor/oss-sync-finding-2026-06-12.md`):
|
||||||
|
- `scripts/oss-subtree-split.sh` produces RAW per-package branches with the WRONG layout
|
||||||
|
(`packages/hive-mind-core`, not the mirror's `packages/core`) and **no file filter ever
|
||||||
|
existed**. A raw split + push would have **leaked proprietary IP**. The script now carries a
|
||||||
|
hard ABORT guard (refuses to emit a branch containing the proprietary files) + a deprecation
|
||||||
|
header; it is for inspection / as a curation starting point ONLY, never a direct push source.
|
||||||
|
- **The real sync is a hand-curated forward-port** onto a maintainer feature branch in the OSS
|
||||||
|
clone (e.g. `feature/mono-parity-YYYY-MM-DD`): adapt the layout, rewrite imports, and STRIP the
|
||||||
|
excluded content. That curation — not a filter — is what keeps proprietary content out.
|
||||||
|
|
||||||
|
**OSS-EXCLUDED (must NOT reach the public mirror):**
|
||||||
|
- Files: `vault.ts`, `evolution-runs.ts`, `execution-traces.ts`, `improvement-signals.ts`,
|
||||||
|
`compliance/**` (vault/compliance live in `@waggle/core`; the other three are barrel-exported
|
||||||
|
from `hive-mind-core` but stripped on export). Enforced by the script's abort guard.
|
||||||
|
- **Interleaved:** the `install_audit` table DDL + its rebuild migration inside
|
||||||
|
`mind/{schema.ts,db.ts}` are ALSO excluded (capability-install trust trail / EU-AI-Act
|
||||||
|
compliance — Waggle governance, not generic substrate). A file filter cannot catch this; only
|
||||||
|
the curated edit strips it. **Consequence:** substrate changes confined to `install_audit`
|
||||||
|
(e.g. P5/D4 `'uninstalled'`, #15 `trust_source` CHECK) have **nowhere to land on the mirror —
|
||||||
|
do NOT treat them as a pending OSS port.**
|
||||||
|
=== END CORRECTION ===
|
||||||
|
|
||||||
|
**To work on the substrate or publish the OSS mirror:** see
|
||||||
|
[`packages/hive-mind-core/CONTRIBUTING.md`](./packages/hive-mind-core/CONTRIBUTING.md),
|
||||||
|
[`scripts/oss-subtree-split.sh`](./scripts/oss-subtree-split.sh) (inspection/guard only), and
|
||||||
|
[`scripts/oss-drift-check.sh`](./scripts/oss-drift-check.sh) (run before every release; note its
|
||||||
|
~50 "DIFFERS" are mostly OSS-adaptation noise — layout + import rewrites — not true drift).
|
||||||
|
|
||||||
|
**Deprecated (do not rely on; do not delete):** the old dual-repo bidirectional-sync workflows
|
||||||
|
`.github/workflows/{mind-parity-check,sync-mind}.yml` and the `.github/sync.md` manual are **preserved
|
||||||
|
as deprecation anchors** from when the substrate was duplicated across two repos. Their trigger paths
|
||||||
|
(`packages/core/src/{mind,harvest}/**`) no longer exist, so they never fire; each carries a DEPRECATED
|
||||||
|
header explaining the migration. Leave them in place for audit trail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Already Built — Do Not Recreate
|
||||||
|
|
||||||
|
Grep before creating. These exist and are functional:
|
||||||
|
|
||||||
|
| File | What it does |
|
||||||
|
|---|---|
|
||||||
|
| `packages/agent/src/injection-scanner.ts` | `scanForInjection()` — 3 pattern sets |
|
||||||
|
| `packages/agent/src/cost-tracker.ts` | `CostTracker` + model pricing table |
|
||||||
|
| `packages/agent/src/tool-filter.ts` | `filterToolsForContext()` — allowlist/denylist |
|
||||||
|
| `packages/agent/src/skill-frontmatter.ts` | `parseSkillFrontmatter()`, `ParsedSkill` |
|
||||||
|
| `packages/agent/src/kvark-tools.ts` | `kvark_search`, `kvark_ask_document` (tier-gated) |
|
||||||
|
| `packages/agent/src/feature-flags.ts` | **EXISTS** — don't create |
|
||||||
|
| `packages/agent/src/persona-data.ts` | Canonical `PERSONAS` array (pure data) |
|
||||||
|
| `packages/agent/src/custom-personas.ts` | `loadCustomPersonas()` from disk |
|
||||||
|
| `packages/agent/src/judge.ts` | Evolution judging |
|
||||||
|
| `packages/agent/src/iterative-optimizer.ts` | Self-improvement loop |
|
||||||
|
| `packages/agent/src/capability-router.ts` | Per-capability routing |
|
||||||
|
| `packages/agent/src/loop-guard.ts` | Infinite loop prevention |
|
||||||
|
| `packages/agent/src/contradiction-detector.ts` | Memory conflict detection |
|
||||||
|
| `packages/shared/src/tiers.ts` | `TIERS`, `TierCapabilities` — canonical tier system |
|
||||||
|
| `packages/shared/src/mcp-catalog.ts` | MCP server catalog |
|
||||||
|
| `packages/core/src/vault.ts` | Secret storage |
|
||||||
|
| `packages/core/src/telemetry.ts` | Telemetry pipeline |
|
||||||
|
| `packages/hive-mind-core/src/harvest/pipeline.ts` | Harvest adapters + dedup |
|
||||||
|
| `packages/core/src/compliance/` | Compliance + audit |
|
||||||
|
| `app/src/components/cockpit/` | Tauri cockpit UI |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. KVARK Integration
|
||||||
|
|
||||||
|
**Canonical copy:**
|
||||||
|
> "Everything Waggle does — on your infrastructure, connected to all your internal systems.
|
||||||
|
> Full data pipeline injection, your permissions, complete audit trail, governance.
|
||||||
|
> Your data never leaves your perimeter."
|
||||||
|
|
||||||
|
**URLs (hardcoded only in `kvark-tools.ts` and `KvarkNudge` component):**
|
||||||
|
- Product site: https://www.kvark.ai
|
||||||
|
- License server: https://license.waggle-os.ai/validate
|
||||||
|
- SaaS cloud: https://cloud.waggle-os.ai
|
||||||
|
|
||||||
|
`kvark-tools.ts` gates `kvark_search` and `kvark_ask_document` to TEAMS/ENTERPRISE tiers.
|
||||||
|
Do not recreate or expose outside gating.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Sprint Status (May 2026)
|
||||||
|
|
||||||
|
### What Landed
|
||||||
|
**April 2026 baseline:**
|
||||||
|
- `tiers.ts` shipped with a 5-tier system (TRIAL/FREE/PRO/TEAMS/ENTERPRISE). _Superseded 2026-07-05: PRO removed, now 4-tier TRIAL/FREE(Solo)/TEAMS/ENTERPRISE — see §1._
|
||||||
|
- `feature-flags.ts` shipped.
|
||||||
|
- Persona data/logic split (`persona-data.ts` ↔ `personas.ts`).
|
||||||
|
- **All 4 new personas shipped** (general-purpose, planner, verifier, coordinator) — `persona-data.ts` verified.
|
||||||
|
- **AgentPersona interface extended** with disallowedTools / failurePatterns / isReadOnly / tagline / bestFor / wontDo — verified in `personas.ts`.
|
||||||
|
- **`behavioral-spec.ts` split** into named sections with `=== CRITICAL ===` markers; `COMPACTION_PROMPT` exported.
|
||||||
|
- **Orchestrator section caching** shipped in `buildSystemPrompt()`.
|
||||||
|
- **OnboardingWizard TEMPLATES expanded to 15**, all wired to `PERSONAS`.
|
||||||
|
- Stripe installed (`stripe@^21.0.1`) in root deps.
|
||||||
|
- Evolution subsystem fully present (10+ files, closed loop end-to-end).
|
||||||
|
- **PromptAssembler v5 PoC complete** — see `docs/plans/POLISH-SPRINT-2026-04-18.md`.
|
||||||
|
- **Premium harness reached HONEST 21/21** (May 2026 S1) — every pillar regression-locked + composing. Full agent suite 2657/2657. See `memory/project_session_handoff_0519_s1.md`.
|
||||||
|
|
||||||
|
**AI-OS arc (May 2026 S1/S2, 14 commits on origin):**
|
||||||
|
- Phase 0 — Tool detection PoC (`packages/agent/src/tool-detection.ts`) for all 7 supported AI tools, hermetic + cross-platform.
|
||||||
|
- Phase 1A — WaggleDance v2 dispatcher branches wired (discovery/routed_share/model_recipe/knowledge_match/task_claim/model_recommendation).
|
||||||
|
- Phase 1B — Local sidecar surface (`/api/waggle-dance/signal` + `/signals`), SignalBus ring buffer, personal-tier-eligible.
|
||||||
|
- Phase 1C — Bridge: v2 bus → existing `/api/waggle/signals` UI stream (zero frontend changes).
|
||||||
|
- Phase 1D — Shim-core signal emitter library (`@waggle/hive-mind-shim-core` `maybeEmitDiscovery`).
|
||||||
|
- Phase 1E — Codex Stop hook wired to `maybeEmitDiscovery` (opt-in via `WAGGLE_SIGNAL_EMIT`).
|
||||||
|
- Phase 2A — Launcher backend (`/api/tools/launch`, `/api/tools/hooks`).
|
||||||
|
- Phase 2B — LauncherApp dock surface (`apps/web/src/components/os/apps/LauncherApp.tsx`).
|
||||||
|
- Phase 3 — Skill diffusion (D1 fire → `skill_share` broadcast via `onSkillDistillationFire` callback).
|
||||||
|
- Phase 4 — Full 7-tool launch cohort + Mission Control inventory tile + Memory provenance badge + launch-with-prompt textarea + process tracker / 'Running' badge.
|
||||||
|
|
||||||
|
End-to-end: detect → install hooks (reversible) → launch with `WAGGLE_WORKSPACE_ID` env → hook captures → shim emitter → bus → bridge → UI. Rollback tag: `checkpoint/pre-ai-os-2026-05-20`. AI-OS exploration doc: `docs/plans/AI-OS-EXPLORATION-2026-05-19.md`.
|
||||||
|
|
||||||
|
### Open Work
|
||||||
|
| # | File | What |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Spawn Agent + Dock wiring | P36 already wired in `Dock.tsx`+`Desktop.tsx`; P35 third-tier fallback (LiteLLM → runtime model → provider catalogs) landed `14942be`. Residual: runtime verification on a clean install. |
|
||||||
|
| 2 | Light mode finish | P40/P41 + CR-2 — semantic-token migration is done (no hive-950 references except a comment); remaining issues are render-time fine-tuning (BootScreen visual polish + a few header-styling judgments) that need a binary build to validate. |
|
||||||
|
| 3 | Wave 2/3 hook implementations | **Mostly DONE (corrected 2026-06-29).** 6 of 7 hook packages ship real bins: Codex + the 2026-06-01 Wave 2/3 port (codex, codex-desktop, cursor, hermes, openclaw). Only `hive-mind-hooks-Codex-desktop` remains a binless `export {}` stub (deferred MCP-bridge category). The dock (`LauncherApp.tsx`) now exposes hook install/verify/uninstall for all 6 via the corrected `HOOKS_COHORT` (was hardcoded `['Codex']`). Residual: Codex-desktop MCP-bridge hook only. |
|
||||||
|
|
||||||
|
**Closed during May 2026 backlog sweep:**
|
||||||
|
- ✅ OW-6 PersonaSwitcher two-tier — shipped via M-01 (`PersonaSwitcher.tsx` + `lib/persona-tier.ts` + `lib/persona-tooltip.ts`); 26/26 tests passing
|
||||||
|
- ✅ CR-7 AGENTS.md §10 update (this entry)
|
||||||
|
- ✅ P35 Spawn Agent "no models available" (`14942be`)
|
||||||
|
- ✅ QW-1..QW-5 quick wins (all already shipped per `grep` verification)
|
||||||
|
- ✅ CR-2 hive-950 → semantic tokens (only comment-level refs remain)
|
||||||
|
- ✅ M7 Stripe products — both test (`acct_1SzHlbC0mmjh4oEM`) and live (`CNCrMQy1f7`) accounts hold the full 2 products × 2 prices (monthly + annual) with `pro_monthly` / `pro_annual` / `teams_monthly` / `teams_annual` lookup keys. Verified via `stripe products list` + `stripe prices list`. Live price IDs documented in `docs/launch/drafts/2026-05-12-apps-www-deployment-readiness.md`. _Note (Solo-vs-Team collapse 2026-07-05): the PRO products/prices are **retained in Stripe for legacy-sub servicing only** — no new PRO checkout is offered. Only TEAMS is an active checkout price._
|
||||||
|
- ✅ E-10 Stripe tier-enforcement wiring — webhook handler was already complete (signature + idempotency + 3 event handlers in `packages/server/src/stripe/webhook.ts`); session closed the residual gap by extending `tierFromPriceId()` in `packages/server/src/stripe/index.ts` to resolve the full 4-var contract (`STRIPE_PRICE_PRO_MONTHLY` / `_ANNUAL` / `STRIPE_PRICE_TEAMS_MONTHLY` / `_ANNUAL`) alongside legacy single-vars + `STRIPE_PRICE_BASIC`. 17/17 webhook tests green; annual subscriptions now resolve through the webhook. _Note (Solo-vs-Team collapse 2026-07-05): `tierFromPriceId()` still reads the legacy PRO/BASIC price envs, but now maps them → `'FREE'` (Solo) so a legacy PRO subscriber lands on Solo rather than a removed tier. Only TEAMS resolves to a paid tier._
|
||||||
|
- ✅ M2 Codex export — `data-ffbb9f0b-…batch-0000.zip` (30 MB) on Desktop\MEMORIES\Codex\, dated 2026-04-17. Ready for E-11 ingestion.
|
||||||
|
- ✅ M3 Gemini export — `takeout-20260416T224803Z-3-001.zip` (437 MB) on Desktop\MEMORIES\Google\, dated 2026-04-17. Ready for E-11 ingestion.
|
||||||
|
- ⏭️ M1 ChatGPT export — skipped by Marko 2026-05-21 (export emails never arrived after multiple requests).
|
||||||
|
- ⏭️ M4 Perplexity export — skipped by Marko 2026-05-21 (research-burst usage; marginal corpus contribution).
|
||||||
|
- ✅ M6 judge roster — Opus 4.7 / GPT-5.4 / Gemini 2.5 Pro / Haiku 4.5 locked 2026-05-21.
|
||||||
|
- ✅ C-1 LoCoMo Memory SOTA — **CURRENT CANONICAL: 86.49% overall** (7-lane W4, Memori *same-judge* protocol — GPT-4.1-mini answerer+judge, N=1540), **+4.54pp over Memori 81.95** (z=4.64, p<10⁻⁵), leading/tying every category. Mem0 re-run on our ruler = **73.96** overall (**temporal +30.8pp** landslide; write-time dating vs Mem0 ingestion-time). **CORRECTED 2026-07-01: the prior 87.66% did NOT reproduce on a fresh judge pass (stale-verdict-replay inflation; archived-substrate 85.19% / current 86.49%). 86.49% is the fresh reproducible number** — pinned + offline-verifiable at [`benchmarks/results/locomo-sota-2026-06/`](benchmarks/results/locomo-sota-2026-06/) (`node recount.mjs` → 1332/1540); record in `docs/analysis/locomo-87.66-vs-85.26-integrity-2026-06-30.md`. Paper/arXiv in [`docs/paper/`](docs/paper/); OSS `marolinik/hive-mind` @ `bc4eba1`, PR #14.
|
||||||
|
- ~~_SUPERSEDED (v5 self-judge, N=320, 2026-05-11): 73.1% Opus 4.7 / 73.4% Qwen3.6, +4.6pp over Mem0 paper; trio-strict 67.8% AND-of-3. The 73.1/73.4 self-judge convergence + 67.8 trio-strict remain valid as the conservative v5-arc framing but are no longer the headline. See `D:/Projects/hive-mind-test/scripts/locomo/data/reports/RESULT-v5-2026-05-11.md`._~~
|
||||||
|
- ✅ C-2 Substrate Claim — done 2026-04-25: Stage 3 v6 N=400, Fisher one-sided p = 8.07 × 10⁻¹⁸, +19.25pp retrieval-vs-no-context lift. "GEPA Full-System canary" expansion explicitly DROPPED 2026-04-30 per PM strategic reset. See `D:/Projects/waggle-os-gaia2-wt/benchmarks/results/stage3-n400-v6-final-analysis.md`.
|
||||||
|
|
||||||
|
### Closed 2026-05-21 (E-14 + audit-trail surfacing)
|
||||||
|
|
||||||
|
- ✅ **E-14 hive-mind v0.3.0 promotion** — shipped on `marolinik/hive-mind` in 3 commits + 1 annotated tag (`b5c1e8f` wiki-web port, `842f390` benchmarks/locomo, `507e0cf` release commit, tag `v0.3.0`). README + CHANGELOG refreshed, 9 versions at 0.3.0, npm install + tsc build clean, vitest 308/312 (4 pre-existing dispatch.test.ts failures documented as v0.3.x followup — same on baseline `20bce16` so not session-induced). Substrate-claim evidence (LoCoMo 73.1% + Fisher p=8.07e-18) now publicly visible. Unblocks S-1 (OSS launch timing) — "before Waggle" is the default since the substrate claim is now public.
|
||||||
|
|
||||||
|
### New open work surfaced 2026-05-21
|
||||||
|
|
||||||
|
| # | Item | What |
|
||||||
|
|---|---|---|
|
||||||
|
| C-3 (reframed) | Full GAIA 2 Phase 4 benchmark | Phase 3 closed in HALT 2026-04-30 (commit `104aa5a` in `waggle-os-gaia2-wt`). Probe showed $4.09/invocation (9-31× over original estimate), narrow-proxy adapter approach economically non-viable. Phase 4 needs Docker + ARE proper agentic execution environment + new adapter strategy. Real engineering arc — scope + budget recalibrate pending Phase 4 design. |
|
||||||
|
|
||||||
|
For the full polish+launch backlog see `docs/plans/BACKLOG-CONSOLIDATED-2026-04-17.md` (~145 items; ~50% are stale-but-done per the May 2026 verification sweep) and the AI-OS arc in `docs/plans/AI-OS-EXPLORATION-2026-05-19.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Glossary
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
|---|---|
|
||||||
|
| Hive DS | Waggle design system — honey/hive-950/accent tokens in `waggle-theme.css` |
|
||||||
|
| FrameStore | SQLite-backed memory frame storage (`packages/hive-mind-core/src/mind/frames.ts`) |
|
||||||
|
| HybridSearch | Vector + keyword search (`packages/hive-mind-core/src/mind/search.ts`) |
|
||||||
|
| KnowledgeGraph | Entity-relation graph (`packages/hive-mind-core/src/mind/knowledge.ts`) |
|
||||||
|
| IdentityLayer | Personal identity persistence (`packages/hive-mind-core/src/mind/identity.ts`) |
|
||||||
|
| AwarenessLayer | Active task/state tracking (`packages/hive-mind-core/src/mind/awareness.ts`) |
|
||||||
|
| Cognify | Memory extraction pipeline (`packages/agent/src/cognify.ts`) |
|
||||||
|
| Harvest | Conversation/file ingestion (`packages/hive-mind-core/src/harvest/`) |
|
||||||
|
| Mind | Per-workspace persistence layer (`packages/hive-mind-core/src/mind/`) |
|
||||||
|
| BEHAVIORAL_SPEC | Core agent rules (`packages/agent/src/behavioral-spec.ts`) |
|
||||||
|
| Sidecar | Node.js Fastify server bundled into Tauri (`/sidecar`) |
|
||||||
|
| KVARK | Egzakta sovereign enterprise AI — top of the Waggle funnel |
|
||||||
|
| LiteLLM | LLM routing layer (`litellm-config.yaml`) |
|
||||||
|
| WaggleDance | Multi-agent coordination package (`packages/waggle-dance`) |
|
||||||
|
| Weaver | `packages/weaver` — (check source for current role) |
|
||||||
|
| Evolution | Self-improvement subsystem (`evolution-*.ts`, `judge.ts`, `iterative-optimizer.ts`) |
|
||||||
|
| assembleToolPool | Per-persona tool filtering from allowlist + denylist (to implement) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Maintained by Marko Markovic · Egzakta Group · April 2026
|
||||||
|
waggle-os.ai · www.kvark.ai
|
||||||
|
|
||||||
|
## Imported Claude Cowork project instructions
|
||||||
|
|
||||||
|
This is my app repo... use it for exploring and working. What ever you produce, you will put in a new folder cowork and store all there dont change the reo itself.
|
||||||
635
CLAUDE.md
Normal file
@@ -0,0 +1,635 @@
|
|||||||
|
# CLAUDE.md — Waggle OS
|
||||||
|
### Authoritative Operating Contract · All Agents · All Contributors · All Sessions
|
||||||
|
|
||||||
|
> Read this file in full before touching a single line of code.
|
||||||
|
> It is the single source of truth for architecture, strategic intent, and mechanical operating rules.
|
||||||
|
> If this file conflicts with any other document, **this file wins.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. How to Use This File
|
||||||
|
|
||||||
|
This file has two parts: **what the project is** (Sections 1-2) and **how to work on it** (Sections 3-9).
|
||||||
|
If you're about to write code, **Section 3** is the most important thing you'll read.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What Waggle OS Actually Is
|
||||||
|
|
||||||
|
**Waggle OS** is a workspace-native AI agent platform with persistent memory. It ships as a
|
||||||
|
Tauri 2.0 desktop binary for Windows and macOS, with a Vite-bundled web app and a Node.js sidecar.
|
||||||
|
|
||||||
|
**Strategic function:** Waggle is the demand-creation and qualification engine for KVARK —
|
||||||
|
Egzakta Group's sovereign enterprise AI platform.
|
||||||
|
|
||||||
|
### Tiers (verified from `packages/shared/src/tiers.ts` — 4-tier: TRIAL/FREE(Solo)/TEAMS/ENTERPRISE, Solo-vs-Team collapse 2026-07-05)
|
||||||
|
|
||||||
|
| Tier | Price | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| TRIAL | $0 / 15 days | TEAM preview — 15 days of Team, then Solo |
|
||||||
|
| FREE (Solo) | $0 forever | Everything personal: unlimited workspaces+connectors, marketplace/custom skills, cloud embeddings, PDF/JSON export, basic audit — free forever |
|
||||||
|
| TEAMS | $49/mo per seat | Shared workspaces, WaggleDance, governance |
|
||||||
|
| ENTERPRISE | Consultative | KVARK sovereign on-prem (www.kvark.ai) |
|
||||||
|
|
||||||
|
> PRO ($19/mo) was removed in the Solo-vs-Team collapse (2026-07-05); its
|
||||||
|
> capabilities folded into FREE (Solo). `TIER_LABELS` displays FREE as "Solo".
|
||||||
|
|
||||||
|
**Moat strategy:** Memory + Harvest is free forever (lock-in moat). Agents, skills,
|
||||||
|
and connectors are all free (they generate memory). Team collaboration (shared memory,
|
||||||
|
WaggleDance, governance) is the upgrade trigger.
|
||||||
|
|
||||||
|
### Key Technology Facts (Verified April 2026)
|
||||||
|
|
||||||
|
| Layer | Stack |
|
||||||
|
|---|---|
|
||||||
|
| Frontend | React **19** + TypeScript + Vite + Tailwind 4 + base-ui/react |
|
||||||
|
| Desktop | Tauri 2.0 (Rust shell) |
|
||||||
|
| Backend | Fastify sidecar (Node.js, bundled into Tauri) |
|
||||||
|
| LLM routing | LiteLLM (see `litellm-config.yaml`) |
|
||||||
|
| Database | SQLite via @waggle/core (better-sqlite3 + sqlite-vec-windows-x64) |
|
||||||
|
| Memory | FrameStore + HybridSearch + KnowledgeGraph + IdentityLayer + AwarenessLayer |
|
||||||
|
| Agent runtime | `packages/agent/src/agent-loop.ts` |
|
||||||
|
| Billing | Stripe (installed; `stripe@^21.0.1`) |
|
||||||
|
| Design | Hive DS — honey #e5a000 / hive-950 #08090c / accent #a78bfa |
|
||||||
|
| Tests | Vitest (unit) + Playwright (E2E) |
|
||||||
|
| Deploy | Dockerfile + docker-compose.production.yml + render.yaml |
|
||||||
|
|
||||||
|
Package manager: npm (root) with `bun.lock` also present. Node >= 20.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Repository Structure (Verified)
|
||||||
|
|
||||||
|
### Top level
|
||||||
|
```
|
||||||
|
waggle-os/
|
||||||
|
├── app/ # Tauri desktop shell (minimal React surface)
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # <-- MAIN web app UI (this is where most components live)
|
||||||
|
│ └── www/ # Landing page (waggle-os.ai)
|
||||||
|
├── packages/ # 16 workspace packages (see below)
|
||||||
|
├── sidecar/ # Node.js sidecar bundled into Tauri
|
||||||
|
├── scripts/ # build-sidecar, bundle-native-deps, bundle-node
|
||||||
|
├── tests/ # Cross-cutting integration tests
|
||||||
|
├── docs/ # ARCHITECTURE.md and others
|
||||||
|
├── cowork/ # Scratchpad / planning / handoff docs (historical; CLAUDE.md promoted to root)
|
||||||
|
├── .planning/ .scratch/ .mind/ # Working notes
|
||||||
|
├── docker-compose.yml + .production.yml + Dockerfile + render.yaml
|
||||||
|
├── litellm-config.yaml # LLM router config
|
||||||
|
├── playwright.config.ts + playwright-e2e.config.ts
|
||||||
|
├── vitest.config.ts + vitest.setup.ts
|
||||||
|
└── package.json (workspaces: apps/*, packages/*)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Packages (`packages/`, 27 workspaces — verified 2026-05-28)
|
||||||
|
```
|
||||||
|
Core (15):
|
||||||
|
admin-web cli launcher marketplace
|
||||||
|
agent core memory-mcp optimizer
|
||||||
|
sdk server shared waggle-dance
|
||||||
|
weaver wiki-compiler worker
|
||||||
|
|
||||||
|
hive-mind OSS split (12 — synced to marolinik/hive-mind, see §7.5):
|
||||||
|
hive-mind-core hive-mind-cli hive-mind-shim-core hive-mind-mcp-server
|
||||||
|
hive-mind-wiki-compiler
|
||||||
|
hive-mind-hooks-{claude-code, claude-desktop, codex, codex-desktop,
|
||||||
|
cursor, hermes, openclaw}
|
||||||
|
```
|
||||||
|
> Note: the prior list said "16" and included `ui`, which has no `package.json`
|
||||||
|
> (not a workspace). Real count is 27. The 12 `hive-mind-*` packages were added
|
||||||
|
> since the April verification.
|
||||||
|
|
||||||
|
### `packages/agent/src/` — MOST ACTIVE (94 .ts files + 4 subdirs)
|
||||||
|
|
||||||
|
Key files (not exhaustive — grep before creating anything new):
|
||||||
|
```
|
||||||
|
agent-loop.ts Core execution loop
|
||||||
|
orchestrator.ts buildSystemPrompt(), recallMemory()
|
||||||
|
personas.ts AgentPersona interface + logic (data split out)
|
||||||
|
persona-data.ts Pure PERSONAS declarative data array
|
||||||
|
custom-personas.ts loadCustomPersonas() from disk
|
||||||
|
behavioral-spec.ts BEHAVIORAL_SPEC rules
|
||||||
|
tool-filter.ts filterToolsForContext()
|
||||||
|
injection-scanner.ts scanForInjection() — 3 pattern sets
|
||||||
|
cost-tracker.ts CostTracker + model pricing
|
||||||
|
skill-frontmatter.ts parseSkillFrontmatter()
|
||||||
|
kvark-tools.ts kvark_search, kvark_ask_document (tier-gated)
|
||||||
|
feature-flags.ts EXISTS — don't recreate
|
||||||
|
subagent-orchestrator.ts Subagent spawn/coord
|
||||||
|
workflow-composer.ts
|
||||||
|
workflow-harness.ts
|
||||||
|
workflow-templates.ts
|
||||||
|
|
||||||
|
Evolution subsystem:
|
||||||
|
evolution-orchestrator.ts evolution-deploy.ts evolution-gates.ts
|
||||||
|
evolution-llm-wiring.ts evolve-schema.ts iterative-optimizer.ts
|
||||||
|
judge.ts eval-dataset.ts compose-evolution.ts
|
||||||
|
|
||||||
|
Capability & trust:
|
||||||
|
capability-acquisition.ts capability-router.ts trust-model.ts
|
||||||
|
permissions.ts credential-pool.ts confirmation.ts
|
||||||
|
|
||||||
|
Quality & correction:
|
||||||
|
quality-controller.ts contradiction-detector.ts
|
||||||
|
correction-detector.ts improvement-detector.ts improvement-wiring.ts
|
||||||
|
loop-guard.ts iteration-budget.ts
|
||||||
|
|
||||||
|
Subdirs:
|
||||||
|
commands/ connectors/ mcp/ providers/
|
||||||
|
```
|
||||||
|
|
||||||
|
### `packages/core/src/`
|
||||||
|
```
|
||||||
|
Top-level: config.ts, cron-store.ts, file-store.ts, install-audit.ts,
|
||||||
|
logger.ts (createCoreLogger), memory-import.ts, migration.ts,
|
||||||
|
multi-mind.ts, multi-mind-cache.ts, optimization-log.ts,
|
||||||
|
skill-hashes.ts, team-sync.ts, telemetry.ts, vault.ts,
|
||||||
|
workspace-config.ts, index.ts
|
||||||
|
|
||||||
|
Subdirs:
|
||||||
|
compliance/ — compliance reporting, interaction-store, status-checker
|
||||||
|
|
||||||
|
MOVED (2026-04-30 monorepo migration): the memory substrate `mind/` (db/schema/
|
||||||
|
identity/awareness/frames/sessions/search/knowledge/scoring/reconcile/ontology/
|
||||||
|
concept-tracker/entity-normalizer/evolution-runs/execution-traces/
|
||||||
|
improvement-signals/embedding-provider/*-embedder) and `harvest/` (chatgpt/claude/
|
||||||
|
claude-code/gemini/perplexity/pdf/plaintext/markdown/url/universal adapters +
|
||||||
|
pipeline.ts + dedup.ts) now live at **packages/hive-mind-core/src/{mind,harvest}/**,
|
||||||
|
NOT under packages/core/. The OSS mirror is generated from there via subtree-split (§7.5).
|
||||||
|
```
|
||||||
|
|
||||||
|
For the deep-dive on what the mind/ substrate does, see [`docs/memory-architecture.md`](docs/memory-architecture.md).
|
||||||
|
|
||||||
|
### `packages/shared/src/`
|
||||||
|
```
|
||||||
|
types.ts User, Team, AgentDef, Task, WaggleMessage
|
||||||
|
constants.ts Team roles, job statuses
|
||||||
|
schemas.ts Zod schemas
|
||||||
|
tiers.ts TIERS + TierCapabilities (canonical 4-tier: TRIAL/FREE(Solo)/TEAMS/ENTERPRISE) + TIER_LABELS/tierLabel
|
||||||
|
mcp-catalog.ts MCP server catalog
|
||||||
|
index.ts Barrel
|
||||||
|
```
|
||||||
|
|
||||||
|
### `app/` (Tauri desktop shell)
|
||||||
|
```
|
||||||
|
app/src-tauri/ # Rust shell + capabilities/ + tauri.conf.json
|
||||||
|
app/scripts/ # build/installer/signing TS tooling (tauri-tsc gate target)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** `app/` is now the Tauri Rust shell only — there is no `app/src/`. The
|
||||||
|
React cockpit UI moved to `apps/web` long ago; the desktop binary loads the
|
||||||
|
`apps/web` dist. All React UI lives in `apps/web/src/`.
|
||||||
|
|
||||||
|
### `apps/web/src/` (MAIN UI)
|
||||||
|
```
|
||||||
|
apps/web/src/
|
||||||
|
├── assets/ components/ hooks/ lib/
|
||||||
|
├── pages/ providers/ test/
|
||||||
|
|
||||||
|
components/
|
||||||
|
├── os/
|
||||||
|
│ ├── apps/ # Per-app UI shells
|
||||||
|
│ └── overlays/ # OnboardingWizard.tsx, PersonaSwitcher.tsx live HERE
|
||||||
|
└── ui/ # Shared UI primitives
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Commands (verified from `package.json`)
|
||||||
|
```bash
|
||||||
|
npm run dev # Vite dev server (apps/web)
|
||||||
|
npm run build # Vite build to /dist (apps/web)
|
||||||
|
npm run build:packages # tsc --build: shared -> core -> agent -> server (order matters)
|
||||||
|
npm run build:all # Packages then web
|
||||||
|
npm run lint # ESLint repo-wide
|
||||||
|
npm run test # Vitest unit tests
|
||||||
|
npm run test:e2e # Playwright API tests
|
||||||
|
npm run test:visual # Playwright visual regression
|
||||||
|
npm run test:all # Full Playwright
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification Commands (run these, don't claim "it compiles")
|
||||||
|
```bash
|
||||||
|
npx tsc --noEmit --project packages/agent/tsconfig.json
|
||||||
|
npx tsc --noEmit --project packages/server/tsconfig.json # sidecar — runs via tsx (transpile-only), so NOT typechecked by `npm run build`
|
||||||
|
npx tsc --noEmit --project app/tsconfig.json
|
||||||
|
npm run test -- --run
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
> `npm run build` typechecks **only `apps/web`**. The Fastify sidecar runs via
|
||||||
|
> `tsx` (transpile-only) — server-route type errors ship undetected unless you
|
||||||
|
> run the `packages/server` tsc above. (A real type error slipped through this
|
||||||
|
> way on 2026-05-28; see `docs/addictiveness-audit-2026-05-28/REDUNDANCY-AUDIT.md`.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Behavioral Rules — How You Must Work
|
||||||
|
|
||||||
|
These rules apply to every code change. They exist because violations have cost real debugging time.
|
||||||
|
|
||||||
|
### 3.1 Think Before Coding
|
||||||
|
|
||||||
|
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||||
|
|
||||||
|
Before implementing anything:
|
||||||
|
- State your assumptions explicitly. If uncertain, ask.
|
||||||
|
- If multiple interpretations exist, present them — don't pick silently.
|
||||||
|
- If a simpler approach exists, say so. Push back when warranted.
|
||||||
|
- If something is unclear, stop. Name what's confusing. Ask.
|
||||||
|
|
||||||
|
=== CRITICAL ===
|
||||||
|
The single most expensive LLM failure mode is making wrong assumptions and building
|
||||||
|
100+ lines on top of them. The fix costs 10x what the question would have cost.
|
||||||
|
Stop. Ask. Then build.
|
||||||
|
=== END CRITICAL ===
|
||||||
|
|
||||||
|
### 3.2 Simplicity First
|
||||||
|
|
||||||
|
**Minimum code that solves the problem. Nothing speculative.**
|
||||||
|
|
||||||
|
- No features beyond what was asked.
|
||||||
|
- No abstractions for single-use code.
|
||||||
|
- No "flexibility" or "configurability" that wasn't requested.
|
||||||
|
- No error handling for impossible scenarios.
|
||||||
|
- If you write 200 lines and it could be 50, rewrite it.
|
||||||
|
|
||||||
|
Test: **"Would a senior engineer say this is overcomplicated?"** If yes, simplify.
|
||||||
|
|
||||||
|
### 3.3 Surgical Changes
|
||||||
|
|
||||||
|
**Touch only what you must. Clean up only your own mess.**
|
||||||
|
|
||||||
|
When editing existing code:
|
||||||
|
- Don't "improve" adjacent code, comments, or formatting.
|
||||||
|
- Don't refactor things that aren't broken.
|
||||||
|
- Match existing style, even if you'd do it differently.
|
||||||
|
- If you notice unrelated dead code, **mention it** — don't delete it.
|
||||||
|
|
||||||
|
When your changes create orphans:
|
||||||
|
- Remove imports/variables/functions YOUR changes made unused.
|
||||||
|
- Don't remove pre-existing dead code unless asked.
|
||||||
|
|
||||||
|
Test: **Every changed line should trace directly to the request.**
|
||||||
|
|
||||||
|
### 3.4 Goal-Driven Execution
|
||||||
|
|
||||||
|
**Define success criteria. Loop until verified.**
|
||||||
|
|
||||||
|
Transform vague tasks into verifiable goals:
|
||||||
|
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||||
|
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||||
|
- "Refactor X" → "Ensure tests pass before and after"
|
||||||
|
|
||||||
|
For multi-step tasks, state a brief plan:
|
||||||
|
```
|
||||||
|
1. [Step] -> verify: [check]
|
||||||
|
2. [Step] -> verify: [check]
|
||||||
|
3. [Step] -> verify: [check]
|
||||||
|
```
|
||||||
|
|
||||||
|
A task is not done until verification passes. "I think this works" is not verification.
|
||||||
|
|
||||||
|
### 3.5 Context Discipline
|
||||||
|
|
||||||
|
- **Context decay:** After 10+ messages, re-read any file before editing. Do not trust memory.
|
||||||
|
- **File read budget:** Files >500 LOC require chunked reads. Never assume complete view.
|
||||||
|
- **Truncation:** Tool results >50k chars are silently truncated. If sparse, re-run narrower.
|
||||||
|
- **Re-read before edit. Re-read after edit.** Max 3 edits per file before verification read.
|
||||||
|
- **Exhaustive grep on rename:** Direct refs, type-level, string literals, dynamic imports,
|
||||||
|
re-exports/barrel entries, test files. **One grep is never enough.**
|
||||||
|
|
||||||
|
### 3.6 Check Before Create
|
||||||
|
|
||||||
|
Before adding a new file, **grep first.** The repo has ~94 files in `packages/agent/src`
|
||||||
|
alone. If you're about to write something that might already exist, it probably does.
|
||||||
|
See Section 8 for known utilities.
|
||||||
|
|
||||||
|
### 3.7 Output Discipline
|
||||||
|
|
||||||
|
- **Chat reply budget.** Long specs, handoffs, audit reports, and multi-phase plans MUST
|
||||||
|
be written to files (memory/, docs/, or via `/handoff`), not rendered inline. The chat
|
||||||
|
is a pointer; the file is the deliverable.
|
||||||
|
- **Chunk long work.** Multi-phase roadmaps and >1k-line specs: implement in phases,
|
||||||
|
commit per phase, give a 3-line status, then stop and await the next instruction. Do
|
||||||
|
not stream an exhaustive summary that blows the output budget.
|
||||||
|
- **Rationale:** 13+ prior sessions were lost mid-response to the 500-output-token cap.
|
||||||
|
Surface shortly, persist richly.
|
||||||
|
|
||||||
|
### 3.8 Handoff Discipline
|
||||||
|
|
||||||
|
- **Use the skill.** End-of-session handoffs invoke `~/.claude/skills/handoff/`, which
|
||||||
|
enforces verification (`git status`, tests N/M, `npx tsc --noEmit` on touched packages)
|
||||||
|
BEFORE writing the doc. Do not hand-write handoffs that skip the gate.
|
||||||
|
- **Canonical location.** Handoffs live at
|
||||||
|
`C:/Users/MarkoMarkovic/.claude/projects/D--Projects-waggle-os/memory/project_session_handoff_<MMDD>_s<N>.md`,
|
||||||
|
with the memory-dir MEMORY.md "START HERE" pointer updated. That is the single source
|
||||||
|
of truth for what shipped / what's left / how to roll back.
|
||||||
|
- **Never hide failures.** Failing tests, unverified MCP reconnects, wrong build dir —
|
||||||
|
surface under "What's still open" in the handoff. Clean-looking handoffs that hide rot
|
||||||
|
cost the next session hours.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Pre-Work Protocol
|
||||||
|
|
||||||
|
Before any structural refactor on a file >300 LOC:
|
||||||
|
1. Remove dead props, unused exports, unused imports, `console.log`.
|
||||||
|
2. Commit separately: `chore(scope): dead code removal — [filename]`
|
||||||
|
|
||||||
|
**Phased execution:** Max 5 files per phase. Complete → verify → await approval → next phase.
|
||||||
|
|
||||||
|
**Senior dev override:** If architecture is flawed, state is duplicated, or patterns
|
||||||
|
are inconsistent — state it and propose a fix. Standard: *"What would a senior engineer
|
||||||
|
reject in review?"*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Persona Architecture
|
||||||
|
|
||||||
|
### Shipped (22 personas — data in `persona-data.ts`, logic in `personas.ts`)
|
||||||
|
The original 13 + 4 universal/orchestration + 5 domain personas all shipped.
|
||||||
|
The PersonaSwitcher groups them into **two tiers** (`apps/web/src/lib/persona-tier.ts`):
|
||||||
|
|
||||||
|
**Universal Modes (8 — always available in every workspace):**
|
||||||
|
general-purpose, planner, verifier, coordinator, researcher, writer, analyst, coder
|
||||||
|
- **general-purpose** — versatile default, full tool access
|
||||||
|
- **planner** — read-only strategic planning, no file writes (`isReadOnly`)
|
||||||
|
- **verifier** — adversarial QA, read-only, VERDICT output format (`isReadOnly`)
|
||||||
|
- **coordinator** — pure orchestrator, spawn/list/get_agent_result only (gated by `FEATURE_FLAGS.COORDINATOR_MODE`)
|
||||||
|
|
||||||
|
**Specialists (14 — template-scoped via `TEMPLATE_SPECIALISTS`):**
|
||||||
|
project-manager, executive-assistant, sales-rep, marketer, product-manager-senior,
|
||||||
|
hr-manager, legal-professional, finance-owner, consultant, support-agent,
|
||||||
|
ops-manager, data-engineer, recruiter, creative-director
|
||||||
|
|
||||||
|
> Note: the onboarding picker (`onboarding/constants.ts` → `ALL_ONBOARDING_PERSONAS`)
|
||||||
|
> intentionally surfaces only **19** of the 22 — it omits planner/verifier/coordinator
|
||||||
|
> (read-only/orchestration modes don't make sense as a workspace's *starting* brain) and
|
||||||
|
> uses its own 3-way grouping (universal/knowledge/domain). Same canonical personas, a
|
||||||
|
> different view for a different UI moment. `persona-data.ts` is the single source of truth.
|
||||||
|
|
||||||
|
**Split is done:** `persona-data.ts` holds the pure `PERSONAS` array;
|
||||||
|
`personas.ts` exports the `AgentPersona` interface and logic only.
|
||||||
|
|
||||||
|
### AgentPersona Interface — Shipped Fields (verified `personas.ts`)
|
||||||
|
```typescript
|
||||||
|
interface AgentPersona {
|
||||||
|
// core
|
||||||
|
id, name, description, icon, systemPrompt, modelPreference,
|
||||||
|
tools: string[], workspaceAffinity: string[],
|
||||||
|
suggestedCommands: string[], defaultWorkflow: string | null,
|
||||||
|
// guardrails + picker metadata (all optional, all shipped)
|
||||||
|
disallowedTools?: string[] // denylist — overrides tools[] on conflict
|
||||||
|
failurePatterns?: string[] // documented failure modes — shown in hover tooltip
|
||||||
|
isReadOnly?: boolean // true = no write tools ever (enforced in assembleToolPool)
|
||||||
|
tagline?: string // one sentence for picker hover
|
||||||
|
bestFor?: string[] // 3 example tasks in user-facing language
|
||||||
|
wontDo?: string // hard boundary statement
|
||||||
|
suggestedSkills?: string[] // installable from marketplace
|
||||||
|
suggestedConnectors?: string[] // connector IDs
|
||||||
|
suggestedMcpServers?: string[] // MCP server names from mcp-registry
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Onboarding & PersonaSwitcher (correct paths)
|
||||||
|
|
||||||
|
### OnboardingWizard
|
||||||
|
**Path:** `apps/web/src/components/os/overlays/OnboardingWizard.tsx`
|
||||||
|
(NOT `app/src/components/onboarding/` — that path doesn't exist.)
|
||||||
|
|
||||||
|
Shipped: 6-step flow (`first-launch → who-are-you → model-gate → memory-import →
|
||||||
|
template → first-task`). The 15 TEMPLATES + the `TEMPLATE_PERSONA` mapping (template →
|
||||||
|
one default persona id) live in `overlays/onboarding/constants.ts`, wired to the
|
||||||
|
canonical persona ids from `persona-data.ts`. The wizard surfaces a **curated 6** of the
|
||||||
|
15 (`CURATED_ONBOARDING_TEMPLATES`) for the ≤2-min flow; the full 15 are reachable from
|
||||||
|
the workspace gallery later. (Picker persona roster = `ALL_ONBOARDING_PERSONAS`, the
|
||||||
|
19-of-22 view noted in §5.)
|
||||||
|
|
||||||
|
### PersonaSwitcher
|
||||||
|
**Path:** `apps/web/src/components/os/overlays/PersonaSwitcher.tsx`
|
||||||
|
|
||||||
|
Shipped (M-01): Two-tier layout — "UNIVERSAL MODES" (8, from `UNIVERSAL_MODE_IDS`) +
|
||||||
|
"YOUR WORKSPACE SPECIALISTS" (template-scoped via `getSpecialistsForTemplate` in
|
||||||
|
`lib/persona-tier.ts`). Hover tooltip (`buildPersonaTooltip`, `lib/persona-tooltip.ts`)
|
||||||
|
shows tagline + bestFor + wontDo. "Create Custom Persona" inline form POSTs to
|
||||||
|
`/api/personas`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Security Constraints (Non-Negotiable)
|
||||||
|
|
||||||
|
1. **Vault-only secrets.** API keys in Vault or `.env` (never committed). `.env.example` has key names only.
|
||||||
|
2. **Injection defense.** `scanForInjection()` from `injection-scanner.ts` MUST be called on all connector/external input.
|
||||||
|
3. **No eval, no dynamic require.** Tauri WebView is restricted.
|
||||||
|
4. **Tauri IPC allowlist.** Explicit in `app/src-tauri/capabilities/`. Never `allowlist: all: true`.
|
||||||
|
5. **Parameterized queries.** No string interpolation in SQL. Ever. better-sqlite3 supports parameters.
|
||||||
|
6. **KVARK contact data.** Submits to your API only — no third-party form services.
|
||||||
|
7. **Secrets in `packages/core/src/vault.ts`** — use it; don't build parallel secret stores.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7.5. Memory Substrate Sync (waggle-os → hive-mind, subtree-split)
|
||||||
|
|
||||||
|
The memory substrate lives at **`packages/hive-mind-core/src/{mind,harvest}/`** (moved from
|
||||||
|
`packages/core/src/` in the 2026-04-30 monorepo migration). The public OSS mirror at
|
||||||
|
[`marolinik/hive-mind`](https://github.com/marolinik/hive-mind) is **generated FROM** this monorepo
|
||||||
|
via a **maintainer-curated forward-port** (NOT a mechanical `git subtree split` — see the
|
||||||
|
correction below). The mirror uses its own curated layout (`packages/core`, co-located tests,
|
||||||
|
rewritten imports) and **excludes** Waggle-proprietary content (see the exclusion list below).
|
||||||
|
|
||||||
|
=== CRITICAL — sync policy (founder-ratified 2026-06-11) ===
|
||||||
|
**The monorepo is the SOLE source of truth for the substrate. Never author substrate features
|
||||||
|
directly on the OSS mirror.** Parity is NOT automatic — it broke once: the cross-encoder reranker
|
||||||
|
(`inprocess-reranker.ts` + HybridSearch options) was written directly on `marolinik/hive-mind`
|
||||||
|
during the LoCoMo benchmark arc and existed ONLY there, discovered by the W4 recon and
|
||||||
|
reverse-ported in W4.2 (`f47ee8f`). Rules:
|
||||||
|
1. Substrate changes land in `packages/hive-mind-core/` here FIRST; the mirror is regenerated
|
||||||
|
via subtree-split afterward.
|
||||||
|
2. Benchmark/experiment work in a `D:/Projects/hive-mind` checkout is throwaway unless
|
||||||
|
reverse-ported here — port it the same arc, don't let it sit.
|
||||||
|
3. Run **`scripts/oss-drift-check.sh`** (file-level diff of the mapped src trees) before every
|
||||||
|
OSS release push and after any arc that touched a hive-mind checkout.
|
||||||
|
4. External PRs on the OSS repo are fine — the maintainer merges them back here via
|
||||||
|
subtree-pull, then re-splits.
|
||||||
|
=== END CRITICAL ===
|
||||||
|
|
||||||
|
=== CORRECTION — how the sync ACTUALLY works (2026-06-12 drift analysis) ===
|
||||||
|
The prior text here claimed the mirror is produced by `scripts/oss-subtree-split.sh` and that a
|
||||||
|
"subtree-split filter" handles the must-not-export files. **Both were false** (verified
|
||||||
|
2026-06-12, `docs/ux-refactor/oss-sync-finding-2026-06-12.md`):
|
||||||
|
- `scripts/oss-subtree-split.sh` produces RAW per-package branches with the WRONG layout
|
||||||
|
(`packages/hive-mind-core`, not the mirror's `packages/core`) and **no file filter ever
|
||||||
|
existed**. A raw split + push would have **leaked proprietary IP**. The script now carries a
|
||||||
|
hard ABORT guard (refuses to emit a branch containing the proprietary files) + a deprecation
|
||||||
|
header; it is for inspection / as a curation starting point ONLY, never a direct push source.
|
||||||
|
- **The real sync is a hand-curated forward-port** onto a maintainer feature branch in the OSS
|
||||||
|
clone (e.g. `feature/mono-parity-YYYY-MM-DD`): adapt the layout, rewrite imports, and STRIP the
|
||||||
|
excluded content. That curation — not a filter — is what keeps proprietary content out.
|
||||||
|
|
||||||
|
**OSS-EXCLUDED (must NOT reach the public mirror):**
|
||||||
|
- Files: `vault.ts`, `evolution-runs.ts`, `execution-traces.ts`, `improvement-signals.ts`,
|
||||||
|
`compliance/**` (vault/compliance live in `@waggle/core`; the other three are barrel-exported
|
||||||
|
from `hive-mind-core` but stripped on export). Enforced by the script's abort guard.
|
||||||
|
- **Interleaved:** the `install_audit` table DDL + its rebuild migration inside
|
||||||
|
`mind/{schema.ts,db.ts}` are ALSO excluded (capability-install trust trail / EU-AI-Act
|
||||||
|
compliance — Waggle governance, not generic substrate). A file filter cannot catch this; only
|
||||||
|
the curated edit strips it. **Consequence:** substrate changes confined to `install_audit`
|
||||||
|
(e.g. P5/D4 `'uninstalled'`, #15 `trust_source` CHECK) have **nowhere to land on the mirror —
|
||||||
|
do NOT treat them as a pending OSS port.**
|
||||||
|
=== END CORRECTION ===
|
||||||
|
|
||||||
|
**To work on the substrate or publish the OSS mirror:** see
|
||||||
|
[`packages/hive-mind-core/CONTRIBUTING.md`](./packages/hive-mind-core/CONTRIBUTING.md),
|
||||||
|
[`scripts/oss-subtree-split.sh`](./scripts/oss-subtree-split.sh) (inspection/guard only), and
|
||||||
|
[`scripts/oss-drift-check.sh`](./scripts/oss-drift-check.sh) (run before every release; note its
|
||||||
|
~50 "DIFFERS" are mostly OSS-adaptation noise — layout + import rewrites — not true drift).
|
||||||
|
|
||||||
|
**Deprecated (do not rely on; do not delete):** the old dual-repo bidirectional-sync workflows
|
||||||
|
`.github/workflows/{mind-parity-check,sync-mind}.yml` and the `.github/sync.md` manual are **preserved
|
||||||
|
as deprecation anchors** from when the substrate was duplicated across two repos. Their trigger paths
|
||||||
|
(`packages/core/src/{mind,harvest}/**`) no longer exist, so they never fire; each carries a DEPRECATED
|
||||||
|
header explaining the migration. Leave them in place for audit trail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Already Built — Do Not Recreate
|
||||||
|
|
||||||
|
Grep before creating. These exist and are functional:
|
||||||
|
|
||||||
|
| File | What it does |
|
||||||
|
|---|---|
|
||||||
|
| `packages/agent/src/injection-scanner.ts` | `scanForInjection()` — 3 pattern sets |
|
||||||
|
| `packages/agent/src/cost-tracker.ts` | `CostTracker` + model pricing table |
|
||||||
|
| `packages/agent/src/tool-filter.ts` | `filterToolsForContext()` — allowlist/denylist |
|
||||||
|
| `packages/agent/src/skill-frontmatter.ts` | `parseSkillFrontmatter()`, `ParsedSkill` |
|
||||||
|
| `packages/agent/src/kvark-tools.ts` | `kvark_search`, `kvark_ask_document` (tier-gated) |
|
||||||
|
| `packages/agent/src/feature-flags.ts` | **EXISTS** — don't create |
|
||||||
|
| `packages/agent/src/persona-data.ts` | Canonical `PERSONAS` array (pure data) |
|
||||||
|
| `packages/agent/src/custom-personas.ts` | `loadCustomPersonas()` from disk |
|
||||||
|
| `packages/agent/src/judge.ts` | Evolution judging |
|
||||||
|
| `packages/agent/src/iterative-optimizer.ts` | Self-improvement loop |
|
||||||
|
| `packages/agent/src/capability-router.ts` | Per-capability routing |
|
||||||
|
| `packages/agent/src/loop-guard.ts` | Infinite loop prevention |
|
||||||
|
| `packages/agent/src/contradiction-detector.ts` | Memory conflict detection |
|
||||||
|
| `packages/shared/src/tiers.ts` | `TIERS`, `TierCapabilities` — canonical tier system |
|
||||||
|
| `packages/shared/src/mcp-catalog.ts` | MCP server catalog |
|
||||||
|
| `packages/core/src/vault.ts` | Secret storage |
|
||||||
|
| `packages/core/src/telemetry.ts` | Telemetry pipeline |
|
||||||
|
| `packages/hive-mind-core/src/harvest/pipeline.ts` | Harvest adapters + dedup |
|
||||||
|
| `packages/core/src/compliance/` | Compliance + audit |
|
||||||
|
| `app/src/components/cockpit/` | Tauri cockpit UI |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. KVARK Integration
|
||||||
|
|
||||||
|
**Canonical copy:**
|
||||||
|
> "Everything Waggle does — on your infrastructure, connected to all your internal systems.
|
||||||
|
> Full data pipeline injection, your permissions, complete audit trail, governance.
|
||||||
|
> Your data never leaves your perimeter."
|
||||||
|
|
||||||
|
**URLs (hardcoded only in `kvark-tools.ts` and `KvarkNudge` component):**
|
||||||
|
- Product site: https://www.kvark.ai
|
||||||
|
- License server: https://license.waggle-os.ai/validate
|
||||||
|
- SaaS cloud: https://cloud.waggle-os.ai
|
||||||
|
|
||||||
|
`kvark-tools.ts` gates `kvark_search` and `kvark_ask_document` to TEAMS/ENTERPRISE tiers.
|
||||||
|
Do not recreate or expose outside gating.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Sprint Status (May 2026)
|
||||||
|
|
||||||
|
### What Landed
|
||||||
|
**April 2026 baseline:**
|
||||||
|
- `tiers.ts` shipped with a 5-tier system (TRIAL/FREE/PRO/TEAMS/ENTERPRISE). _Superseded 2026-07-05: PRO removed, now 4-tier TRIAL/FREE(Solo)/TEAMS/ENTERPRISE — see §1._
|
||||||
|
- `feature-flags.ts` shipped.
|
||||||
|
- Persona data/logic split (`persona-data.ts` ↔ `personas.ts`).
|
||||||
|
- **All 4 new personas shipped** (general-purpose, planner, verifier, coordinator) — `persona-data.ts` verified.
|
||||||
|
- **AgentPersona interface extended** with disallowedTools / failurePatterns / isReadOnly / tagline / bestFor / wontDo — verified in `personas.ts`.
|
||||||
|
- **`behavioral-spec.ts` split** into named sections with `=== CRITICAL ===` markers; `COMPACTION_PROMPT` exported.
|
||||||
|
- **Orchestrator section caching** shipped in `buildSystemPrompt()`.
|
||||||
|
- **OnboardingWizard TEMPLATES expanded to 15**, all wired to `PERSONAS`.
|
||||||
|
- Stripe installed (`stripe@^21.0.1`) in root deps.
|
||||||
|
- Evolution subsystem fully present (10+ files, closed loop end-to-end).
|
||||||
|
- **PromptAssembler v5 PoC complete** — see `docs/plans/POLISH-SPRINT-2026-04-18.md`.
|
||||||
|
- **Premium harness reached HONEST 21/21** (May 2026 S1) — every pillar regression-locked + composing. Full agent suite 2657/2657. See `memory/project_session_handoff_0519_s1.md`.
|
||||||
|
|
||||||
|
**AI-OS arc (May 2026 S1/S2, 14 commits on origin):**
|
||||||
|
- Phase 0 — Tool detection PoC (`packages/agent/src/tool-detection.ts`) for all 7 supported AI tools, hermetic + cross-platform.
|
||||||
|
- Phase 1A — WaggleDance v2 dispatcher branches wired (discovery/routed_share/model_recipe/knowledge_match/task_claim/model_recommendation).
|
||||||
|
- Phase 1B — Local sidecar surface (`/api/waggle-dance/signal` + `/signals`), SignalBus ring buffer, personal-tier-eligible.
|
||||||
|
- Phase 1C — Bridge: v2 bus → existing `/api/waggle/signals` UI stream (zero frontend changes).
|
||||||
|
- Phase 1D — Shim-core signal emitter library (`@waggle/hive-mind-shim-core` `maybeEmitDiscovery`).
|
||||||
|
- Phase 1E — claude-code Stop hook wired to `maybeEmitDiscovery` (opt-in via `WAGGLE_SIGNAL_EMIT`).
|
||||||
|
- Phase 2A — Launcher backend (`/api/tools/launch`, `/api/tools/hooks`).
|
||||||
|
- Phase 2B — LauncherApp dock surface (`apps/web/src/components/os/apps/LauncherApp.tsx`).
|
||||||
|
- Phase 3 — Skill diffusion (D1 fire → `skill_share` broadcast via `onSkillDistillationFire` callback).
|
||||||
|
- Phase 4 — Full 7-tool launch cohort + Mission Control inventory tile + Memory provenance badge + launch-with-prompt textarea + process tracker / 'Running' badge.
|
||||||
|
|
||||||
|
End-to-end: detect → install hooks (reversible) → launch with `WAGGLE_WORKSPACE_ID` env → hook captures → shim emitter → bus → bridge → UI. Rollback tag: `checkpoint/pre-ai-os-2026-05-20`. AI-OS exploration doc: `docs/plans/AI-OS-EXPLORATION-2026-05-19.md`.
|
||||||
|
|
||||||
|
### Open Work
|
||||||
|
| # | File | What |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Spawn Agent + Dock wiring | P36 already wired in `Dock.tsx`+`Desktop.tsx`; P35 third-tier fallback (LiteLLM → runtime model → provider catalogs) landed `14942be`. Residual: runtime verification on a clean install. |
|
||||||
|
| 2 | Light mode finish | P40/P41 + CR-2 — semantic-token migration is done (no hive-950 references except a comment); remaining issues are render-time fine-tuning (BootScreen visual polish + a few header-styling judgments) that need a binary build to validate. |
|
||||||
|
| 3 | Wave 2/3 hook implementations | **Mostly DONE (corrected 2026-06-29).** 6 of 7 hook packages ship real bins: claude-code + the 2026-06-01 Wave 2/3 port (codex, codex-desktop, cursor, hermes, openclaw). Only `hive-mind-hooks-claude-desktop` remains a binless `export {}` stub (deferred MCP-bridge category). The dock (`LauncherApp.tsx`) now exposes hook install/verify/uninstall for all 6 via the corrected `HOOKS_COHORT` (was hardcoded `['claude-code']`). Residual: claude-desktop MCP-bridge hook only. |
|
||||||
|
|
||||||
|
**Closed during May 2026 backlog sweep:**
|
||||||
|
- ✅ OW-6 PersonaSwitcher two-tier — shipped via M-01 (`PersonaSwitcher.tsx` + `lib/persona-tier.ts` + `lib/persona-tooltip.ts`); 26/26 tests passing
|
||||||
|
- ✅ CR-7 CLAUDE.md §10 update (this entry)
|
||||||
|
- ✅ P35 Spawn Agent "no models available" (`14942be`)
|
||||||
|
- ✅ QW-1..QW-5 quick wins (all already shipped per `grep` verification)
|
||||||
|
- ✅ CR-2 hive-950 → semantic tokens (only comment-level refs remain)
|
||||||
|
- ✅ M7 Stripe products — both test (`acct_1SzHlbC0mmjh4oEM`) and live (`CNCrMQy1f7`) accounts hold the full 2 products × 2 prices (monthly + annual) with `pro_monthly` / `pro_annual` / `teams_monthly` / `teams_annual` lookup keys. Verified via `stripe products list` + `stripe prices list`. Live price IDs documented in `docs/launch/drafts/2026-05-12-apps-www-deployment-readiness.md`. _Note (Solo-vs-Team collapse 2026-07-05): the PRO products/prices are **retained in Stripe for legacy-sub servicing only** — no new PRO checkout is offered. Only TEAMS is an active checkout price._
|
||||||
|
- ✅ E-10 Stripe tier-enforcement wiring — webhook handler was already complete (signature + idempotency + 3 event handlers in `packages/server/src/stripe/webhook.ts`); session closed the residual gap by extending `tierFromPriceId()` in `packages/server/src/stripe/index.ts` to resolve the full 4-var contract (`STRIPE_PRICE_PRO_MONTHLY` / `_ANNUAL` / `STRIPE_PRICE_TEAMS_MONTHLY` / `_ANNUAL`) alongside legacy single-vars + `STRIPE_PRICE_BASIC`. 17/17 webhook tests green; annual subscriptions now resolve through the webhook. _Note (Solo-vs-Team collapse 2026-07-05): `tierFromPriceId()` still reads the legacy PRO/BASIC price envs, but now maps them → `'FREE'` (Solo) so a legacy PRO subscriber lands on Solo rather than a removed tier. Only TEAMS resolves to a paid tier._
|
||||||
|
- ✅ M2 Claude export — `data-ffbb9f0b-…batch-0000.zip` (30 MB) on Desktop\MEMORIES\Claude\, dated 2026-04-17. Ready for E-11 ingestion.
|
||||||
|
- ✅ M3 Gemini export — `takeout-20260416T224803Z-3-001.zip` (437 MB) on Desktop\MEMORIES\Google\, dated 2026-04-17. Ready for E-11 ingestion.
|
||||||
|
- ⏭️ M1 ChatGPT export — skipped by Marko 2026-05-21 (export emails never arrived after multiple requests).
|
||||||
|
- ⏭️ M4 Perplexity export — skipped by Marko 2026-05-21 (research-burst usage; marginal corpus contribution).
|
||||||
|
- ✅ M6 judge roster — Opus 4.7 / GPT-5.4 / Gemini 2.5 Pro / Haiku 4.5 locked 2026-05-21.
|
||||||
|
- ✅ C-1 LoCoMo Memory SOTA — **CURRENT CANONICAL: 86.49% overall** (7-lane W4, Memori *same-judge* protocol — GPT-4.1-mini answerer+judge, N=1540), **+4.54pp over Memori 81.95** (z=4.64, p<10⁻⁵), leading/tying every category. Mem0 re-run on our ruler = **73.96** overall (**temporal +30.8pp** landslide; write-time dating vs Mem0 ingestion-time). **CORRECTED 2026-07-01: the prior 87.66% did NOT reproduce on a fresh judge pass (stale-verdict-replay inflation; archived-substrate 85.19% / current 86.49%). 86.49% is the fresh reproducible number** — pinned + offline-verifiable at [`benchmarks/results/locomo-sota-2026-06/`](benchmarks/results/locomo-sota-2026-06/) (`node recount.mjs` → 1332/1540); record in `docs/analysis/locomo-87.66-vs-85.26-integrity-2026-06-30.md`. Paper/arXiv in [`docs/paper/`](docs/paper/); OSS `marolinik/hive-mind` @ `bc4eba1`, PR #14.
|
||||||
|
- ~~_SUPERSEDED (v5 self-judge, N=320, 2026-05-11): 73.1% Opus 4.7 / 73.4% Qwen3.6, +4.6pp over Mem0 paper; trio-strict 67.8% AND-of-3. The 73.1/73.4 self-judge convergence + 67.8 trio-strict remain valid as the conservative v5-arc framing but are no longer the headline. See `D:/Projects/hive-mind-test/scripts/locomo/data/reports/RESULT-v5-2026-05-11.md`._~~
|
||||||
|
- ✅ C-2 Substrate Claim — done 2026-04-25: Stage 3 v6 N=400, Fisher one-sided p = 8.07 × 10⁻¹⁸, +19.25pp retrieval-vs-no-context lift. "GEPA Full-System canary" expansion explicitly DROPPED 2026-04-30 per PM strategic reset. See `D:/Projects/waggle-os-gaia2-wt/benchmarks/results/stage3-n400-v6-final-analysis.md`.
|
||||||
|
|
||||||
|
### Closed 2026-05-21 (E-14 + audit-trail surfacing)
|
||||||
|
|
||||||
|
- ✅ **E-14 hive-mind v0.3.0 promotion** — shipped on `marolinik/hive-mind` in 3 commits + 1 annotated tag (`b5c1e8f` wiki-web port, `842f390` benchmarks/locomo, `507e0cf` release commit, tag `v0.3.0`). README + CHANGELOG refreshed, 9 versions at 0.3.0, npm install + tsc build clean, vitest 308/312 (4 pre-existing dispatch.test.ts failures documented as v0.3.x followup — same on baseline `20bce16` so not session-induced). Substrate-claim evidence (LoCoMo 73.1% + Fisher p=8.07e-18) now publicly visible. Unblocks S-1 (OSS launch timing) — "before Waggle" is the default since the substrate claim is now public.
|
||||||
|
|
||||||
|
### New open work surfaced 2026-05-21
|
||||||
|
|
||||||
|
| # | Item | What |
|
||||||
|
|---|---|---|
|
||||||
|
| C-3 (reframed) | Full GAIA 2 Phase 4 benchmark | Phase 3 closed in HALT 2026-04-30 (commit `104aa5a` in `waggle-os-gaia2-wt`). Probe showed $4.09/invocation (9-31× over original estimate), narrow-proxy adapter approach economically non-viable. Phase 4 needs Docker + ARE proper agentic execution environment + new adapter strategy. Real engineering arc — scope + budget recalibrate pending Phase 4 design. |
|
||||||
|
|
||||||
|
For the full polish+launch backlog see `docs/plans/BACKLOG-CONSOLIDATED-2026-04-17.md` (~145 items; ~50% are stale-but-done per the May 2026 verification sweep) and the AI-OS arc in `docs/plans/AI-OS-EXPLORATION-2026-05-19.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Glossary
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
|---|---|
|
||||||
|
| Hive DS | Waggle design system — honey/hive-950/accent tokens in `waggle-theme.css` |
|
||||||
|
| FrameStore | SQLite-backed memory frame storage (`packages/hive-mind-core/src/mind/frames.ts`) |
|
||||||
|
| HybridSearch | Vector + keyword search (`packages/hive-mind-core/src/mind/search.ts`) |
|
||||||
|
| KnowledgeGraph | Entity-relation graph (`packages/hive-mind-core/src/mind/knowledge.ts`) |
|
||||||
|
| IdentityLayer | Personal identity persistence (`packages/hive-mind-core/src/mind/identity.ts`) |
|
||||||
|
| AwarenessLayer | Active task/state tracking (`packages/hive-mind-core/src/mind/awareness.ts`) |
|
||||||
|
| Cognify | Memory extraction pipeline (`packages/agent/src/cognify.ts`) |
|
||||||
|
| Harvest | Conversation/file ingestion (`packages/hive-mind-core/src/harvest/`) |
|
||||||
|
| Mind | Per-workspace persistence layer (`packages/hive-mind-core/src/mind/`) |
|
||||||
|
| BEHAVIORAL_SPEC | Core agent rules (`packages/agent/src/behavioral-spec.ts`) |
|
||||||
|
| Sidecar | Node.js Fastify server bundled into Tauri (`/sidecar`) |
|
||||||
|
| KVARK | Egzakta sovereign enterprise AI — top of the Waggle funnel |
|
||||||
|
| LiteLLM | LLM routing layer (`litellm-config.yaml`) |
|
||||||
|
| WaggleDance | Multi-agent coordination package (`packages/waggle-dance`) |
|
||||||
|
| Weaver | `packages/weaver` — (check source for current role) |
|
||||||
|
| Evolution | Self-improvement subsystem (`evolution-*.ts`, `judge.ts`, `iterative-optimizer.ts`) |
|
||||||
|
| assembleToolPool | Per-persona tool filtering from allowlist + denylist (to implement) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Maintained by Marko Markovic · Egzakta Group · April 2026
|
||||||
|
waggle-os.ai · www.kvark.ai
|
||||||
132
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
|
identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the overall
|
||||||
|
community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
|
any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email address,
|
||||||
|
without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official email address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
**hello@egzakta.com**. All complaints will be reviewed and investigated promptly
|
||||||
|
and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of
|
||||||
|
actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
|
ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
|
community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.1, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||||
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
98
Dockerfile
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# Waggle Teams Server — Production Docker Image
|
||||||
|
# Multi-stage build: install deps + build packages & frontend, then run the
|
||||||
|
# team server (packages/server/src/index.ts → Postgres/Redis/MinIO/Clerk).
|
||||||
|
#
|
||||||
|
# Consumed by docker-compose.production.yml. NOTE: render.yaml deploys the
|
||||||
|
# SQLite *sidecar* (local/start.ts) instead — a different hosting mode.
|
||||||
|
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the full workspace before install. The monorepo has 27 workspaces
|
||||||
|
# (packages/* + apps/*); a hand-maintained COPY list drifts (it had referenced
|
||||||
|
# the nonexistent packages/ui and omitted every hive-mind-* package), so copy
|
||||||
|
# wholesale for correctness. Trades some layer-cache granularity for not
|
||||||
|
# silently breaking when a workspace is added.
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
COPY packages packages
|
||||||
|
COPY apps apps
|
||||||
|
COPY tsconfig*.json ./
|
||||||
|
COPY vitest*.ts ./
|
||||||
|
|
||||||
|
# Install all dependencies (incl. dev — needed to build). --ignore-scripts
|
||||||
|
# skips native rebuilds here (the build only typechecks + bundles); native
|
||||||
|
# modules are rebuilt in the native-builder stage below.
|
||||||
|
RUN npm install --ignore-scripts 2>/dev/null || npm install
|
||||||
|
|
||||||
|
# Build workspace packages THEN the web UI.
|
||||||
|
# build:all → build:packages (hive-mind-core → shared → core → agent → server)
|
||||||
|
# then the apps/web Vite build to root /dist. @waggle/hive-mind-core emits its
|
||||||
|
# dist/ here; @waggle/core imports it as dist/ at runtime, so this is required.
|
||||||
|
RUN npm run build:all
|
||||||
|
|
||||||
|
# ── Native module build stage ───────────────────────────────────
|
||||||
|
FROM node:20-alpine AS native-builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install native build dependencies (only needed here)
|
||||||
|
RUN apk add --no-cache python3 make g++
|
||||||
|
|
||||||
|
# Workspace manifests for production install + native rebuild
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
COPY packages packages
|
||||||
|
COPY apps apps
|
||||||
|
|
||||||
|
# Production-only dependencies, then rebuild native modules (better-sqlite3)
|
||||||
|
RUN npm install --omit=dev --ignore-scripts 2>/dev/null || npm install --omit=dev
|
||||||
|
RUN npm rebuild better-sqlite3 2>/dev/null || true
|
||||||
|
|
||||||
|
# ── Production image (no build tools) ──────────────────────────
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Production node_modules (native rebuilt) + root manifest. The @waggle/*
|
||||||
|
# entries here are workspace symlinks into ./packages, satisfied below.
|
||||||
|
COPY --from=native-builder /app/node_modules node_modules
|
||||||
|
COPY --from=native-builder /app/package.json package.json
|
||||||
|
|
||||||
|
# Built workspace packages from the builder — carries emitted dist/ alongside
|
||||||
|
# src, so @waggle/hive-mind-core/dist (a runtime dep of @waggle/core) is present.
|
||||||
|
# (The old Dockerfile copied source packages from the build context here, which
|
||||||
|
# dropped every freshly-built dist/ and broke runtime module resolution.)
|
||||||
|
COPY --from=builder /app/packages packages
|
||||||
|
|
||||||
|
# Built web UI → /app/dist (root dist is canonical since the Apr-12 migration)
|
||||||
|
COPY --from=builder /app/dist dist
|
||||||
|
|
||||||
|
# Create data directory and non-root user
|
||||||
|
RUN mkdir -p /data \
|
||||||
|
&& addgroup -S waggle && adduser -S waggle -G waggle \
|
||||||
|
&& chown -R waggle:waggle /app /data
|
||||||
|
|
||||||
|
# Set environment
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV WAGGLE_FRONTEND_DIR=/app/dist
|
||||||
|
ENV WAGGLE_DATA_DIR=/data
|
||||||
|
# The sidecar defaults to loopback (desktop-safe). A container must accept
|
||||||
|
# traffic from outside, so opt into binding all interfaces here.
|
||||||
|
ENV WAGGLE_HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Expose server port
|
||||||
|
EXPOSE 3333
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||||
|
CMD wget -q --spider http://localhost:3333/health || exit 1
|
||||||
|
|
||||||
|
# Data volume for persistence
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
# Run as non-root user
|
||||||
|
USER waggle
|
||||||
|
|
||||||
|
# Run drizzle migrations (cwd packages/server so migrate.ts's './drizzle'
|
||||||
|
# resolves), then start the team server. docker-compose can override this.
|
||||||
|
CMD ["sh", "-c", "(cd packages/server && npx tsx src/db/migrate.ts) && npx tsx packages/server/src/index.ts"]
|
||||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Marko Markovic / Egzakta Group
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
136
README.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# Waggle OS
|
||||||
|
|
||||||
|
Workspace-native AI agent platform with persistent memory, model-agnostic orchestration, and skill-extensible capabilities. It ships as a Tauri 2.0 desktop binary (Windows/macOS) with a Vite-bundled web app and a Node.js sidecar.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
waggle-os/
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # Main web app UI (React 19 + Vite + Tailwind 4 + base-ui/react)
|
||||||
|
│ ├── www/ # Marketing site (Next.js)
|
||||||
|
│ └── browser-ext/ # Browser extension (unpacked; not an npm workspace)
|
||||||
|
├── packages/ # 28 workspace packages (see "Packages" below)
|
||||||
|
├── app/ # Tauri 2.0 desktop shell (Rust) — loads the apps/web build
|
||||||
|
├── sidecar/ # Node.js sidecar bundled into the Tauri desktop binary
|
||||||
|
└── docs/ # Architecture, contributing, threat model, and guides
|
||||||
|
```
|
||||||
|
|
||||||
|
### Packages
|
||||||
|
|
||||||
|
The monorepo has **28 packages** under `packages/`. They split into two groups.
|
||||||
|
|
||||||
|
**Product packages (15, MIT):**
|
||||||
|
|
||||||
|
| Package | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `agent` | Agent loop, orchestrator, tools, personas, workflows, and the evolution subsystem |
|
||||||
|
| `core` | Config, the encrypted vault, cron store, file store, telemetry, and compliance/audit |
|
||||||
|
| `server` | Fastify sidecar — local (solo) and team routes, SSE streaming, Stripe, KVARK client |
|
||||||
|
| `shared` | Shared types, Zod schemas, the tier system, and the MCP catalog |
|
||||||
|
| `marketplace` | Package catalog with the `SecurityGate` installer |
|
||||||
|
| `optimizer` | GEPA prompt optimization |
|
||||||
|
| `weaver` | Memory consolidation daemon |
|
||||||
|
| `waggle-dance` | Multi-agent coordination protocol |
|
||||||
|
| `worker` | Background job processor (BullMQ, team mode) |
|
||||||
|
| `sdk` | Plugin / skill SDK |
|
||||||
|
| `cli` | Command-line REPL |
|
||||||
|
| `launcher` | AI-tool launcher / dock backend |
|
||||||
|
| `admin-web` | Admin dashboard for team deployments |
|
||||||
|
| `wiki-compiler` | Knowledge / wiki compiler |
|
||||||
|
| `memory-mcp` | MCP server exposing the memory substrate to external agents |
|
||||||
|
|
||||||
|
**Memory substrate — `hive-mind-*` (13, Apache-2.0):** the persistent-memory core, mirrored to the public OSS repo [`marolinik/hive-mind`](https://github.com/marolinik/hive-mind).
|
||||||
|
|
||||||
|
| Package | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `hive-mind-core` | The memory substrate: `FrameStore`, `HybridSearch`, `KnowledgeGraph`, `IdentityLayer`, `AwarenessLayer`, plus Harvest ingestion (`src/mind` + `src/harvest`) |
|
||||||
|
| `hive-mind-cli` | CLI for the substrate |
|
||||||
|
| `hive-mind-mcp-server` | MCP server for the substrate |
|
||||||
|
| `hive-mind-shim-core` | Signal-emitter shim library |
|
||||||
|
| `hive-mind-wiki-compiler` | Wiki compiler (OSS) |
|
||||||
|
| `hive-mind-hooks-core` | Shared hook library |
|
||||||
|
| `hive-mind-hooks-*` | Per-tool capture hooks: `claude-code`, `claude-desktop`, `codex`, `codex-desktop`, `cursor`, `hermes`, `openclaw` |
|
||||||
|
|
||||||
|
> The memory substrate is developed **here** and mirrored out — never the reverse.
|
||||||
|
> See the "Memory Substrate Sync" section of [`CLAUDE.md`](./CLAUDE.md) before touching `packages/hive-mind-core`.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Self-host in one line (Linux / macOS)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/marolinik/waggle-os/main/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Best for a VPS or homelab — this runs a headless Waggle server (no desktop shell):
|
||||||
|
|
||||||
|
- **Checks prerequisites, clones, builds, and starts** the Node.js sidecar, then prints the URL (`http://127.0.0.1:3333`). A 5-question wizard — install dir, port, data dir, build web UI, start now — is all Enter-defaulted; pass `--yes` to accept every default non-interactively.
|
||||||
|
- **Boots with zero API keys** in echo mode so the UI works immediately. Add a provider key later under **Settings → API Keys**, where it is stored in the encrypted vault — keys are never passed on the command line.
|
||||||
|
- **No sudo, ever.** A missing prerequisite prints the exact per-OS install command and exits; the installer never installs system packages for you.
|
||||||
|
|
||||||
|
Manage the running server with the installed wrapper: `scripts/waggle-server.sh status | logs | stop | start`. Re-running the one-liner against an existing install prints an upgrade hint instead of reinstalling.
|
||||||
|
|
||||||
|
### Run from source (development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prerequisites: Node.js >= 20, npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# (Optional) copy the env template. Provider API keys are normally set in-app
|
||||||
|
# (Settings → API Keys), which stores them in the encrypted vault — so you do
|
||||||
|
# NOT need to put keys in .env for a basic local run.
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Terminal 1 — backend sidecar (http://localhost:3333)
|
||||||
|
npm run dev:server
|
||||||
|
|
||||||
|
# Terminal 2 — web app (http://localhost:8080)
|
||||||
|
npm run dev:web
|
||||||
|
|
||||||
|
# Open http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm run dev:server` runs the Fastify sidecar via `tsx` (equivalent to
|
||||||
|
`cd packages/server && npx tsx src/local/start.ts`). `npm run dev:web` runs the
|
||||||
|
Vite dev server for `apps/web`.
|
||||||
|
|
||||||
|
**Embeddings.** By default `EMBEDDING_PROVIDER=auto` downloads a small in-process
|
||||||
|
model (~23 MB, cached under `~/.waggle/models/`) and works fully offline. For
|
||||||
|
better recall — and to reproduce any hive-mind benchmark — install
|
||||||
|
[Ollama](https://ollama.com), run `ollama pull nomic-embed-text`, and set
|
||||||
|
`EMBEDDING_PROVIDER=ollama`.
|
||||||
|
|
||||||
|
> **Windows:** if the sidecar fails to start with an esbuild platform error, see
|
||||||
|
> [Troubleshooting](docs/CONTRIBUTING.md#troubleshooting) in the contributing guide.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Provider keys are stored in the encrypted vault (set via **Settings → API Keys**)
|
||||||
|
and hydrated into the process at boot, so most of these are optional for a local
|
||||||
|
run. See [`.env.example`](./.env.example) for the full contract.
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| `ANTHROPIC_API_KEY` | Recommended | Claude API key. Optional in `.env` — can be set in-app instead (vault). |
|
||||||
|
| `OPENAI_API_KEY` | No | Enables OpenAI models and optional OpenAI embeddings. |
|
||||||
|
| `EMBEDDING_PROVIDER` | No | `auto` (default) · `inprocess` · `ollama` · `voyage` · `openai` · `mock`. `auto` tries in-process → Ollama → API → mock. |
|
||||||
|
| `LITELLM_BASE_URL` | No | LiteLLM proxy URL for multi-model routing (default `http://localhost:4000`). |
|
||||||
|
| `DATABASE_URL` | Team only | PostgreSQL connection string. |
|
||||||
|
| `REDIS_URL` | Team only | Redis for the background job queue. |
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Getting Started](docs/GETTING-STARTED.md) — first-run walkthrough
|
||||||
|
- [Architecture](docs/ARCHITECTURE.md) — package structure, data flow, extension points
|
||||||
|
- [Contributing](docs/CONTRIBUTING.md) — setup, tests, PR process, troubleshooting
|
||||||
|
- [Threat Model](THREAT_MODEL.md) — trust boundary and security controls
|
||||||
|
- [Security Policy](SECURITY.md) — how to report a vulnerability
|
||||||
|
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This repository is licensed under the [MIT License](./LICENSE), **except** the
|
||||||
|
`hive-mind-*` packages under `packages/`, which are licensed under **Apache-2.0**.
|
||||||
|
Each `hive-mind-*` package carries its own `LICENSE` file, which governs that
|
||||||
|
package. See [Packages](#packages) for the split.
|
||||||
63
SECURITY.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
Waggle OS runs on a user's own machine and touches sensitive material — API
|
||||||
|
keys, connector credentials, the local filesystem, and imported conversation
|
||||||
|
history. We take reports seriously and appreciate responsible disclosure.
|
||||||
|
|
||||||
|
For the trust boundary, the controls that enforce it, and the currently known
|
||||||
|
gaps, read [`THREAT_MODEL.md`](./THREAT_MODEL.md). It is the authoritative
|
||||||
|
description of what Waggle defends against and what it does not.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
**Please do not open a public GitHub issue for security vulnerabilities.**
|
||||||
|
|
||||||
|
Report privately through either channel:
|
||||||
|
|
||||||
|
1. **GitHub Security Advisories (preferred).** On the repository, go to the
|
||||||
|
**Security** tab → **Report a vulnerability**. This opens a private advisory
|
||||||
|
visible only to the maintainers.
|
||||||
|
2. **Email.** Send details to **marko@egzakta.com** with `SECURITY` in the
|
||||||
|
subject line.
|
||||||
|
|
||||||
|
Please include:
|
||||||
|
|
||||||
|
- A description of the issue and the impact you believe it has.
|
||||||
|
- Step-by-step reproduction instructions (or a proof of concept).
|
||||||
|
- The affected component/package and version or commit SHA.
|
||||||
|
- Your environment (OS, Node version, desktop build vs. web).
|
||||||
|
|
||||||
|
## What to Expect
|
||||||
|
|
||||||
|
- **Acknowledgement** within 5 business days.
|
||||||
|
- An initial assessment and severity classification shortly after.
|
||||||
|
- Coordinated disclosure: we will agree on a timeline with you and credit you in
|
||||||
|
the release notes unless you prefer to remain anonymous.
|
||||||
|
|
||||||
|
Please give us a reasonable window to release a fix before any public
|
||||||
|
disclosure.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
In scope:
|
||||||
|
|
||||||
|
- The desktop app (Tauri shell + bundled sidecar), the web app, and the
|
||||||
|
workspace packages under `packages/` and `apps/`.
|
||||||
|
- The memory substrate (`packages/hive-mind-core`) and its MCP servers.
|
||||||
|
- Prompt-injection paths, secret handling (the vault), the filesystem boundary,
|
||||||
|
and the capability/approval gates — see `THREAT_MODEL.md` for the details.
|
||||||
|
|
||||||
|
Out of scope (documented, not vulnerabilities):
|
||||||
|
|
||||||
|
- The known gaps enumerated in `THREAT_MODEL.md` (e.g. the pattern-based
|
||||||
|
injection scanner and the absence of an OS-level shell sandbox). If you can
|
||||||
|
demonstrate impact meaningfully beyond what is already documented there, we
|
||||||
|
still want to hear about it.
|
||||||
|
- Findings that require a compromised operator account or physical access to the
|
||||||
|
user's machine — the operator is trusted by design.
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
If a report involves an exposed secret (an API key, token, or credential), note
|
||||||
|
it explicitly so we can rotate it immediately. Never include live secrets in a
|
||||||
|
public issue or PR.
|
||||||
173
THREAT_MODEL.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Waggle OS — Threat Model
|
||||||
|
|
||||||
|
Waggle OS is a **workspace-native AI agent platform with persistent memory**, shipped as
|
||||||
|
a Tauri desktop binary (Windows/macOS) with a bundled Node.js sidecar. This document
|
||||||
|
states the trust boundary and the controls that enforce it, so contributors can reason
|
||||||
|
about security without reading the full agent + connector stack.
|
||||||
|
|
||||||
|
> Status: living document. The controls below are implemented and cited to source.
|
||||||
|
> Known gaps are open and honestly listed.
|
||||||
|
|
||||||
|
## Trust Boundary
|
||||||
|
|
||||||
|
Waggle runs **on the user's own machine, for that single user**. The human operator is
|
||||||
|
trusted: they own the workspace, the vault, the filesystem, and the connector
|
||||||
|
credentials. The threat model does **not** try to stop the operator from doing what they
|
||||||
|
are entitled to do on their own device.
|
||||||
|
|
||||||
|
The boundary Waggle defends is between the **trusted operator + Waggle's own code** and
|
||||||
|
**untrusted external content** that flows into the agent's context:
|
||||||
|
|
||||||
|
- conversation/file imports (Harvest: ChatGPT/Claude/Gemini/PDF/markdown/URL adapters)
|
||||||
|
- connector auto-fetch + tool reads (calendar, email, GitHub, web, files)
|
||||||
|
- tool output sourced from outside the process
|
||||||
|
- MCP server output
|
||||||
|
- saved memory frames derived from any of the above
|
||||||
|
|
||||||
|
The core risk is **prompt injection**: untrusted content carrying instructions that try
|
||||||
|
to hijack the agent ("ignore previous instructions", fake `SYSTEM:` authority, role
|
||||||
|
override, prompt-extraction) and make it act against the operator's intent.
|
||||||
|
|
||||||
|
## Controls (implemented)
|
||||||
|
|
||||||
|
### 1. Injection scanning at every ingress — `scanForInjection`
|
||||||
|
`packages/hive-mind-core/src/injection-scanner.ts` (re-exported via
|
||||||
|
`packages/agent/src/injection-scanner.ts`). Three pattern sets — role-override,
|
||||||
|
prompt-extraction, instruction-injection — produce a score; `safe = score < 0.3`
|
||||||
|
(instruction-injection is weighted higher, 0.6, for `tool_output` context). It gates the
|
||||||
|
three untrusted chokepoints:
|
||||||
|
- **Harvest** ingestion (external conversation exports).
|
||||||
|
- **Recall**: `orchestrator.ts` scans the joined recalled block and **drops the entire
|
||||||
|
recall** on a flag (`orchestrator.ts:777-786`) — a poisoned memory frame never silently
|
||||||
|
re-enters context on a later turn.
|
||||||
|
- **Tool output**: `tool-executor.ts` scans every executed-tool result and replaces
|
||||||
|
flagged content with a `[SECURITY] … sanitized` placeholder **before** any observer or
|
||||||
|
the model sees it.
|
||||||
|
|
||||||
|
### 2. Structural untrusted-content fence — `untrustedContextWrapper`
|
||||||
|
`packages/agent/src/untrusted-context.ts`. Tool output that *passes* the scan is still
|
||||||
|
external data. Before it enters the model's next-turn context it is wrapped in a
|
||||||
|
delimiter-guarded block with a "this is DATA, not instructions" header, applied in
|
||||||
|
`tool-executor.ts` (after scan + compression). Embedded guard markers in the body are
|
||||||
|
escaped so untrusted content **cannot break out of the fence** to forge a close-marker.
|
||||||
|
This is defense-in-depth on top of the scanner: the scanner blocks *known* attack
|
||||||
|
phrasings; the fence makes *all* external tool output structurally non-authoritative.
|
||||||
|
|
||||||
|
**Maintained invariant (taint preservation):** tool output is delivered as a **discrete
|
||||||
|
`role:'tool'` message** (`agent-loop.ts`) / Anthropic **`tool_result` content block**
|
||||||
|
(`anthropic-proxy.ts`) and is **never string-concatenated into the user turn**. Waggle's
|
||||||
|
provider adapters do no lossy role-alternation merge (`openai-compat.ts`), so the
|
||||||
|
data/instruction boundary is preserved natively. **Follow-up guard:** if a provider
|
||||||
|
adapter that merges consecutive same-role turns is ever added, it MUST insert a boundary
|
||||||
|
rather than concatenate an untrusted block into the operator's real request — re-audit at
|
||||||
|
that time.
|
||||||
|
|
||||||
|
### 3. Human-in-the-loop confirmation — `confirmation.ts`
|
||||||
|
`packages/agent/src/confirmation.ts`. State-changing tools require explicit approval:
|
||||||
|
an `ALWAYS_CONFIRM` set (`write_file`, `edit_file`, `git_push`, `install_capability`,
|
||||||
|
`create_skill`/`delete_skill`, cross-workspace reads, …) plus a `CONNECTOR_WRITE_PATTERNS`
|
||||||
|
regex that gates connector write actions (`_create_/_update_/_delete_/_send_/…`).
|
||||||
|
Read-only/informational calls flow freely; destructive ops do not inherit autonomy.
|
||||||
|
|
||||||
|
### 4. Capability install audit trail — `install-audit.ts`
|
||||||
|
`packages/core/src/install-audit.ts`. Every install-relevant action (proposed, approved,
|
||||||
|
installed, rejected, failed, uninstalled) is persisted to the `.mind` DB with risk,
|
||||||
|
approval class, initiator, and trust source — a verifiable history of what was installed,
|
||||||
|
when, why, and by whom. Backs the EU-AI-Act capability-provenance story.
|
||||||
|
|
||||||
|
### 5. Local secret storage — `vault.ts`
|
||||||
|
`packages/core/src/vault.ts`. Secrets are encrypted with AES-256-GCM under a machine-local
|
||||||
|
key file; each entry is independently encrypted. API keys live in the vault or `.env`
|
||||||
|
(never committed; `.env.example` carries key names only). No secret is ever written to a
|
||||||
|
prompt, a log, or a memory frame.
|
||||||
|
|
||||||
|
### 6. MCP tool scope gate — `scope.ts`
|
||||||
|
`packages/memory-mcp/src/scope.ts` + `packages/hive-mind-mcp-server/src/scope.ts`. An
|
||||||
|
external agent (Claude Code/Codex) granted stdio access to the memory substrate can be
|
||||||
|
scoped **read-only** via `WAGGLE_MCP_SCOPES` / `HIVE_MIND_SCOPES`: a `memory:read` scope
|
||||||
|
registers only the read tools, so a read-only client literally cannot call
|
||||||
|
save/cleanup/ingest. Default (unset) stays full read+write for backward-compat;
|
||||||
|
`memory:write` implies `memory:read`. This keeps a poisoned or buggy external agent from
|
||||||
|
writing junk into the substrate.
|
||||||
|
|
||||||
|
### 7. Workspace filesystem boundary + secret deny — `file-store.ts`
|
||||||
|
`packages/core/src/file-store.ts`. Every FileStore op resolves the caller path and asserts
|
||||||
|
it cannot escape the workspace root: a **segment-boundary** containment check (not a string
|
||||||
|
prefix — `${root}-evil` is rejected) plus **symlink-aware** containment (the realpath'd
|
||||||
|
target must stay under the realpath'd root, so a benign-named symlink/junction pointing at
|
||||||
|
`~/.ssh` or `/etc` is denied, while in-root monorepo links still work). For LINKED external
|
||||||
|
folders, `isSensitiveFilePath` additionally denies reads/writes/listing of well-known secret
|
||||||
|
material (SSH/GPG keys, cloud + terraform credentials, `.env`, `id_rsa`, `authorized_keys`,
|
||||||
|
backup copies, `*.pem`), normalized against Windows ADS (`::$DATA`) and trailing-dot/space
|
||||||
|
tricks. `searchFiles`/`listFiles` filter the same set so search never even discloses a
|
||||||
|
secret's existence. **This is real containment + a defense-in-depth BLOCKLIST — not a
|
||||||
|
sandbox:** the deny is a curated list (it cannot enumerate every secret a home dir holds) and
|
||||||
|
is deny-by-default with no per-workspace override yet.
|
||||||
|
|
||||||
|
### 8. SSRF egress guard — `url-egress-guard.ts`
|
||||||
|
`packages/agent/src/url-egress-guard.ts` (guarding `web_fetch` in `system-tools.ts`) and
|
||||||
|
the structurally-identical `packages/hive-mind-core/src/harvest/url-egress-guard.ts`
|
||||||
|
(guarding `UrlAdapter.fetchAndParse`, reached by the MCP `ingest_source` url path in
|
||||||
|
`packages/{memory-mcp,hive-mind-mcp-server}/src/tools/ingest.ts`). A URL named by untrusted
|
||||||
|
content or a user is resolved to concrete IP(s); the fetch is **refused if any resolved
|
||||||
|
address is loopback / private (RFC1918 + CGNAT) / link-local / unique-local / multicast /
|
||||||
|
reserved / unspecified**. This closes the **cloud instance-metadata** exfiltration path —
|
||||||
|
`169.254.169.254` (link-local) reaching IAM credentials — which matters because the
|
||||||
|
cloud/TEAMS sidecar binds `0.0.0.0` (`docker-compose.production.yml`, `render.yaml`).
|
||||||
|
Coverage:
|
||||||
|
- **Scheme allowlist:** only `http:`/`https:` (blocks `file:`/`gopher:`/`ftp:` redirect tricks).
|
||||||
|
- **Obfuscated literals** (octal `0177.0.0.1`, decimal `2130706433`, hex `0x7f000001`) are
|
||||||
|
normalized by the OS resolver — `net.isIP` rejects them as literals, so they route through
|
||||||
|
`dns.lookup` (getaddrinfo) which returns the canonical dotted form the classifier blocks.
|
||||||
|
- **IPv6** including `::1`, `fe80::/10`, `fc00::/7`, `ff00::/8`, and **IPv4-mapped**
|
||||||
|
(`::ffff:169.254.169.254`) which is unwrapped and classified as its embedded v4.
|
||||||
|
- **Redirects** are followed manually (`redirect: 'manual'`) and the target is **re-validated
|
||||||
|
at every hop**, so a public URL cannot 30x-bounce into a private address; a hop cap bounds it.
|
||||||
|
- **Desktop localhost:** loopback is blocked by default; a legitimate local-dev fetch is
|
||||||
|
permitted only when `WAGGLE_ALLOW_LOCAL_FETCH=1` (loopback only — private/link-local stay
|
||||||
|
blocked even then). Fail-closed: an unclassifiable/malformed address is treated as blocked.
|
||||||
|
|
||||||
|
## Known Gaps (open, honest)
|
||||||
|
|
||||||
|
1. **Pattern-based scanner.** `scanForInjection` is regex/heuristic — novel phrasings,
|
||||||
|
heavy obfuscation, or non-English attacks outside the small multilingual set can evade
|
||||||
|
it. The structural fence (control 2) is the backstop, but the fence is *advisory*: a
|
||||||
|
sufficiently capable model can still be jailbroken from inside a correctly-fenced block.
|
||||||
|
Both controls reduce, not eliminate, injection risk.
|
||||||
|
2. **No filesystem/shell sandbox.** File and command tools run as the app-process user.
|
||||||
|
Control 7 now confines FileStore ops to the workspace boundary and blocks well-known
|
||||||
|
secrets in linked dirs, but it is a path-level guard + blocklist, not OS-level confinement;
|
||||||
|
shell/command tools remain bounded only by the confirmation gate (control 3). The linked-dir
|
||||||
|
secret deny is also a curated blocklist (whole secret classes — e.g. browser profiles,
|
||||||
|
shell history, `.config/gh|gcloud` tokens — are out of scope) and has no per-workspace
|
||||||
|
override, so legitimate `.env`/`.npmrc` edits in a linked project are denied by default.
|
||||||
|
(`S3FileStore` — the TEAMS/cloud backend — now rejects `..` traversal in keys and uses a
|
||||||
|
ReDoS-safe glob matcher for search, but it has no secret-deny blocklist; its bucket prefix
|
||||||
|
is the isolation boundary.)
|
||||||
|
3. **Fence scope is tool output only.** Recalled memory carries an equivalent prose
|
||||||
|
preamble (`orchestrator.ts`) but is not yet wrapped in the same structural fence;
|
||||||
|
harvest content is scanned at ingest but not re-fenced per frame. Extending the fence to
|
||||||
|
recall is a low-marginal-value follow-up.
|
||||||
|
4. **`isReadOnly` persona gating is fail-open.** Read-only personas filter write tools by
|
||||||
|
denylist rather than an inverse allowlist; a tool missing from the denylist is not
|
||||||
|
blocked. Flip to allowlist + static mutator backstop when persona governance is next
|
||||||
|
touched.
|
||||||
|
5. **Connector endpoint URLs are not redacted before logging.** userinfo/query/fragment
|
||||||
|
on LiteLLM/connector URLs can leak credentials into logs — fold a `redactUrl` pass into
|
||||||
|
the next compliance/logging pass.
|
||||||
|
6. **Connector auto-harvest persists external content durably.** Opt-in PRO connector
|
||||||
|
harvest writes external data (e.g. inbox metadata + message previews) into the personal
|
||||||
|
mind, where it is recalled into model context on later turns. Content is injection-scanned
|
||||||
|
per frame but NOT scanned for secrets/PII; the email harvest pins `$select` to
|
||||||
|
subject/from/preview (not full bodies) to bound exposure. A secret-pattern redaction pass
|
||||||
|
before `writeFrame` is a follow-up.
|
||||||
|
7. **SSRF guard has a residual DNS-rebind TOCTOU window.** The egress guard (control 8)
|
||||||
|
resolves + validates the hostname, then hands the URL to `fetch`, which resolves it a
|
||||||
|
second time — a hostname whose DNS flips to a private IP between the two lookups could slip
|
||||||
|
through on the fetch's own resolution. The window is re-validated on every redirect hop, but
|
||||||
|
full closure needs IP-pinning (connect to the validated address) which `fetch`+HTTPS can't
|
||||||
|
do portably without breaking TLS SNI/cert validation. Also out of scope: the guard bounds
|
||||||
|
the *target* address, not response size/content, and does not defend a genuinely
|
||||||
|
public-but-malicious endpoint. The duplicated agent/hive-mind-core guard copies share one
|
||||||
|
spec and must be kept in sync (they cannot share a module — hive-mind-core is OSS-mirrored
|
||||||
|
and must not import `@waggle/agent`).
|
||||||
BIN
UAT 3/mega-test-v2/screenshots/40-chat-dark.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
UAT 3/mega-test-v2/screenshots/41-cockpit.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
UAT 3/mega-test-v2/screenshots/42-memory.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
UAT 3/mega-test-v2/screenshots/43-settings.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
UAT 3/mega-test-v2/screenshots/44-capabilities.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
UAT 3/mega-test-v2/screenshots/45-onboarding.png
Normal file
|
After Width: | Height: | Size: 288 KiB |
BIN
UAT 3/mega-test-v2/screenshots/46-events.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
UAT 3/mega-test-v2/screenshots/47-light-chat.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
UAT 3/mega-test-v2/screenshots/47-light-cockpit.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
UAT 3/mega-test-v2/screenshots/47-light-memory.png
Normal file
|
After Width: | Height: | Size: 842 KiB |
BIN
UAT 3/mega-test-v2/screenshots/54-1024-settings.png
Normal file
|
After Width: | Height: | Size: 468 KiB |
BIN
UAT 3/mega-test-v2/screenshots/54-1024x768.png
Normal file
|
After Width: | Height: | Size: 466 KiB |
BIN
UAT 3/mega-test-v2/screenshots/55-768x1024.png
Normal file
|
After Width: | Height: | Size: 478 KiB |
6
app/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
||||||
|
# Rust build artifacts
|
||||||
|
src-tauri/target/
|
||||||
25
app/components.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "base-nova",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/styles/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rtl": false,
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"menuColor": "default",
|
||||||
|
"menuAccent": "subtle",
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
25
app/icons/ICONS-README.txt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
Waggle Installer Icons
|
||||||
|
======================
|
||||||
|
|
||||||
|
This directory holds icon assets for the NSIS installer and app branding.
|
||||||
|
The Tauri build references icons from src-tauri/icons/ for the main app icon.
|
||||||
|
|
||||||
|
Required assets (replace placeholders with real designs):
|
||||||
|
|
||||||
|
icon.ico — Main application icon (256x256, multi-resolution .ico)
|
||||||
|
Used for: app window, taskbar, installer, desktop shortcut
|
||||||
|
Location: src-tauri/icons/icon.ico (already exists as placeholder)
|
||||||
|
|
||||||
|
icon.png — PNG version (512x512 recommended)
|
||||||
|
Used for: web display, documentation, store listings
|
||||||
|
|
||||||
|
header.bmp — NSIS installer header image (150x57 pixels, 24-bit BMP)
|
||||||
|
Shown at top-right of installer wizard pages
|
||||||
|
|
||||||
|
sidebar.bmp — NSIS installer sidebar image (164x314 pixels, 24-bit BMP)
|
||||||
|
Shown on welcome and finish pages
|
||||||
|
|
||||||
|
Design guidelines:
|
||||||
|
- Waggle brand: honeycomb/bee/swarm motif
|
||||||
|
- Primary colors: amber/gold (#F59E0B) on dark (#1E1B4B)
|
||||||
|
- Clean, modern, recognizable at small sizes
|
||||||
13
app/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/waggle-logo.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Waggle</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3449
app/package-lock.json
generated
Normal file
47
app/package.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "waggle-app",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"typecheck": "tsc -b",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri",
|
||||||
|
"tauri:build": "node ../scripts/build-sidecar.mjs && node ../scripts/bundle-native-deps.mjs && node ../scripts/bundle-node.mjs && node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build",
|
||||||
|
"tauri:build:local": "node ../scripts/build-sidecar.mjs && node ../scripts/bundle-native-deps.mjs && node ../scripts/bundle-node.mjs && node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build --debug",
|
||||||
|
"tauri:build:win": "node ../scripts/build-sidecar.mjs && node ../scripts/bundle-native-deps.mjs && node ../scripts/bundle-node.mjs && node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build --target x86_64-pc-windows-msvc",
|
||||||
|
"tauri:build:mac": "npm run tauri:build:mac:arm64 && npm run tauri:build:mac:x64",
|
||||||
|
"tauri:build:mac:arm64": "node ../scripts/build-sidecar.mjs && TARGET_ARCH=arm64 node ../scripts/bundle-native-deps.mjs && TARGET_ARCH=arm64 node ../scripts/bundle-node.mjs && TARGET_ARCH=arm64 node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build --target aarch64-apple-darwin",
|
||||||
|
"tauri:build:mac:x64": "node ../scripts/build-sidecar.mjs && TARGET_ARCH=x64 node ../scripts/bundle-native-deps.mjs && TARGET_ARCH=x64 node ../scripts/bundle-node.mjs && TARGET_ARCH=x64 node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build --target x86_64-apple-darwin",
|
||||||
|
"tauri:dev": "npx tauri dev",
|
||||||
|
"tauri:sign:pilot:win:setup": "powershell -ExecutionPolicy Bypass -File scripts/sign-windows-pilot.ps1 -Mode Setup",
|
||||||
|
"tauri:sign:pilot:win:apply": "node scripts/apply-signing-config.mjs",
|
||||||
|
"tauri:sign:pilot:win:sign": "powershell -ExecutionPolicy Bypass -File scripts/sign-windows-pilot.ps1 -Mode Sign -ArtifactPath",
|
||||||
|
"tauri:sign:pilot:mac:adhoc": "bash scripts/sign-macos-adhoc.sh"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@base-ui/react": "^1.3.0",
|
||||||
|
"@tauri-apps/api": "^2.5.0",
|
||||||
|
"@tauri-apps/plugin-shell": "^2.2.1",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.5.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"lucide-react": "^0.577.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"tailwind-merge": "^3.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@tauri-apps/cli": "^2.5.0",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^6.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/public/waggle-logo.jpeg
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
23
app/public/waggle-logo.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 120" fill="none">
|
||||||
|
<!-- Waggle bee logo — two hexagons (body), wings, antennae -->
|
||||||
|
|
||||||
|
<!-- Antennae -->
|
||||||
|
<path d="M42 28 L38 16" stroke="#E8920F" stroke-width="3.5" stroke-linecap="round"/>
|
||||||
|
<path d="M58 28 L62 16" stroke="#E8920F" stroke-width="3.5" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Upper hexagon (head) -->
|
||||||
|
<path d="M50 26 L65 35 L65 51 L50 60 L35 51 L35 35 Z"
|
||||||
|
stroke="#E8920F" stroke-width="3" stroke-linejoin="round" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Lower hexagon (abdomen) -->
|
||||||
|
<path d="M50 60 L65 69 L65 85 L50 94 L35 85 L35 69 Z"
|
||||||
|
stroke="#E8920F" stroke-width="3" stroke-linejoin="round" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Left wing -->
|
||||||
|
<path d="M35 51 L18 46 Q10 50 18 58 L35 60"
|
||||||
|
stroke="#E8920F" stroke-width="3" stroke-linejoin="round" stroke-linecap="round" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Right wing -->
|
||||||
|
<path d="M65 51 L82 46 Q90 50 82 58 L65 60"
|
||||||
|
stroke="#E8920F" stroke-width="3" stroke-linejoin="round" stroke-linecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
130
app/scripts/apply-signing-config.mjs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* apply-signing-config.mjs — write the captured Windows code-signing thumbprint
|
||||||
|
* into app/src-tauri/tauri.build-override.conf.json.
|
||||||
|
*
|
||||||
|
* Read by `npm run tauri:sign:pilot:win:apply`. Idempotent: re-running with the
|
||||||
|
* same thumbprint produces an identical file. Updating the cert (rotation) is
|
||||||
|
* handled by re-running the upstream cert-gen script + this CLI.
|
||||||
|
*
|
||||||
|
* Pure logic lives in `signing-config.ts`; this is the thin file-I/O wrapper.
|
||||||
|
*
|
||||||
|
* Reference: docs/code-signing-pilot-and-launch.md §1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||||
|
import { resolve, dirname } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
// The pure helpers below mirror app/scripts/signing-config.ts so this CLI has
|
||||||
|
// zero TS-loader dependency at runtime. The .ts version is the canonical
|
||||||
|
// implementation tested by signing-config.test.ts (19 cases covering parse,
|
||||||
|
// merge, idempotency, immutability). Keep the two implementations in lockstep:
|
||||||
|
// any change to parseThumbprintString or addWindowsSigningToOverride below
|
||||||
|
// MUST be mirrored in signing-config.ts and vice versa.
|
||||||
|
|
||||||
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const APP_DIR = resolve(SCRIPT_DIR, '..');
|
||||||
|
const REPO_ROOT = resolve(APP_DIR, '..');
|
||||||
|
|
||||||
|
const OVERRIDE_PATH = resolve(
|
||||||
|
APP_DIR,
|
||||||
|
'src-tauri',
|
||||||
|
'tauri.build-override.conf.json',
|
||||||
|
);
|
||||||
|
const THUMBPRINT_PATH = resolve(APP_DIR, 'src-tauri', '.thumbprint.txt');
|
||||||
|
|
||||||
|
const DEFAULT_DIGEST_ALGORITHM = 'sha256';
|
||||||
|
const DEFAULT_TIMESTAMP_URL = 'http://timestamp.digicert.com';
|
||||||
|
const THUMBPRINT_LENGTH = 40;
|
||||||
|
const HEX_PATTERN = /^[0-9A-F]+$/;
|
||||||
|
|
||||||
|
function parseThumbprintString(raw) {
|
||||||
|
if (!raw || raw.trim().length === 0) {
|
||||||
|
throw new Error('Thumbprint is empty — cert generation may have failed.');
|
||||||
|
}
|
||||||
|
const compact = raw.replace(/\s+/g, '').toUpperCase();
|
||||||
|
if (compact.length !== THUMBPRINT_LENGTH || !HEX_PATTERN.test(compact)) {
|
||||||
|
throw new Error(
|
||||||
|
`Thumbprint must be 40 hex characters; got ${compact.length} chars (sample: "${compact.slice(0, 16)}...").`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return compact;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addWindowsSigningToOverride(config, thumbprint, options = {}) {
|
||||||
|
const normalisedThumbprint = parseThumbprintString(thumbprint);
|
||||||
|
const digestAlgorithm = options.digestAlgorithm ?? DEFAULT_DIGEST_ALGORITHM;
|
||||||
|
const timestampUrl = options.timestampUrl ?? DEFAULT_TIMESTAMP_URL;
|
||||||
|
|
||||||
|
const existingBundle = config.bundle ?? {};
|
||||||
|
const existingWindows = existingBundle.windows ?? {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
bundle: {
|
||||||
|
...existingBundle,
|
||||||
|
windows: {
|
||||||
|
...existingWindows,
|
||||||
|
certificateThumbprint: normalisedThumbprint,
|
||||||
|
digestAlgorithm,
|
||||||
|
timestampUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
if (!existsSync(THUMBPRINT_PATH)) {
|
||||||
|
console.error(
|
||||||
|
`[apply-signing-config] thumbprint file missing: ${THUMBPRINT_PATH}`,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'[apply-signing-config] Run `npm run tauri:sign:pilot:win:setup` (or app/scripts/sign-windows-pilot.ps1 -Mode Setup) first.',
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!existsSync(OVERRIDE_PATH)) {
|
||||||
|
console.error(
|
||||||
|
`[apply-signing-config] override config missing: ${OVERRIDE_PATH}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawThumbprint = readFileSync(THUMBPRINT_PATH, 'utf8');
|
||||||
|
const overrideRaw = readFileSync(OVERRIDE_PATH, 'utf8');
|
||||||
|
|
||||||
|
let override;
|
||||||
|
try {
|
||||||
|
override = JSON.parse(overrideRaw);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`[apply-signing-config] failed to parse ${OVERRIDE_PATH}: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated;
|
||||||
|
try {
|
||||||
|
updated = addWindowsSigningToOverride(override, rawThumbprint);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`[apply-signing-config] failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format with 2-space indent + trailing newline (matches existing JSON files
|
||||||
|
// in the repo). Idempotent: same input → same output bytes.
|
||||||
|
const serialised = JSON.stringify(updated, null, 2) + '\n';
|
||||||
|
writeFileSync(OVERRIDE_PATH, serialised, 'utf8');
|
||||||
|
|
||||||
|
const relativePath = OVERRIDE_PATH.replace(REPO_ROOT, '').replace(/^\\/, '');
|
||||||
|
console.log(
|
||||||
|
`[apply-signing-config] wrote thumbprint ${updated.bundle.windows.certificateThumbprint.slice(0, 8)}... to ${relativePath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
180
app/scripts/bundle-runtimes.test.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import path from 'node:path';
|
||||||
|
import {
|
||||||
|
getNodeDownloadUrl,
|
||||||
|
getPythonDownloadUrl,
|
||||||
|
getResourcePaths,
|
||||||
|
getBundleStatus,
|
||||||
|
parseVersion,
|
||||||
|
isValidVersion,
|
||||||
|
} from './bundle-utils.js';
|
||||||
|
|
||||||
|
// ─── getNodeDownloadUrl ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getNodeDownloadUrl', () => {
|
||||||
|
it('returns Windows x64 URL by default', () => {
|
||||||
|
const url = getNodeDownloadUrl('20.11.1');
|
||||||
|
expect(url).toBe('https://nodejs.org/dist/v20.11.1/win-x64/node.exe');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns Windows arm64 URL', () => {
|
||||||
|
const url = getNodeDownloadUrl('20.11.1', 'win32', 'arm64');
|
||||||
|
expect(url).toBe('https://nodejs.org/dist/v20.11.1/win-arm64/node.exe');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns macOS tar.gz URL', () => {
|
||||||
|
const url = getNodeDownloadUrl('20.11.1', 'darwin', 'x64');
|
||||||
|
expect(url).toContain('darwin-x64.tar.gz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns Linux tar.xz URL', () => {
|
||||||
|
const url = getNodeDownloadUrl('20.11.1', 'linux', 'x64');
|
||||||
|
expect(url).toContain('linux-x64.tar.xz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes the version in the URL', () => {
|
||||||
|
const url = getNodeDownloadUrl('18.19.0');
|
||||||
|
expect(url).toContain('v18.19.0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getPythonDownloadUrl ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getPythonDownloadUrl', () => {
|
||||||
|
it('returns Windows amd64 embed URL by default', () => {
|
||||||
|
const url = getPythonDownloadUrl('3.11.8');
|
||||||
|
expect(url).toBe(
|
||||||
|
'https://www.python.org/ftp/python/3.11.8/python-3.11.8-embed-amd64.zip',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns Windows arm64 URL', () => {
|
||||||
|
const url = getPythonDownloadUrl('3.11.8', 'win32', 'arm64');
|
||||||
|
expect(url).toContain('embed-arm64.zip');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns macOS pkg URL', () => {
|
||||||
|
const url = getPythonDownloadUrl('3.11.8', 'darwin', 'x64');
|
||||||
|
expect(url).toContain('macos11.pkg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns Linux source URL', () => {
|
||||||
|
const url = getPythonDownloadUrl('3.11.8', 'linux', 'x64');
|
||||||
|
expect(url).toContain('Python-3.11.8.tar.xz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes the version in the URL', () => {
|
||||||
|
const url = getPythonDownloadUrl('3.12.1');
|
||||||
|
expect(url).toContain('3.12.1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getResourcePaths ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getResourcePaths', () => {
|
||||||
|
it('returns correct Windows paths when platform is win32', () => {
|
||||||
|
const dir = '/app/src-tauri/resources';
|
||||||
|
const paths = getResourcePaths(dir, 'win32');
|
||||||
|
|
||||||
|
expect(paths.node).toBe(path.join(dir, 'node', 'node.exe'));
|
||||||
|
expect(paths.python).toBe(path.join(dir, 'python', 'python.exe'));
|
||||||
|
expect(paths.litellm).toBe(
|
||||||
|
path.join(dir, 'python', 'Lib', 'site-packages', 'litellm'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Windows-style paths', () => {
|
||||||
|
const dir = 'C:\\Users\\user\\app\\resources';
|
||||||
|
const paths = getResourcePaths(dir, 'win32');
|
||||||
|
|
||||||
|
expect(paths.node).toContain('node.exe');
|
||||||
|
expect(paths.python).toContain('python.exe');
|
||||||
|
expect(paths.litellm).toContain('litellm');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct Unix paths when platform is darwin', () => {
|
||||||
|
const dir = '/app/src-tauri/resources';
|
||||||
|
const paths = getResourcePaths(dir, 'darwin');
|
||||||
|
|
||||||
|
expect(paths.node).toBe(path.join(dir, 'node', 'bin', 'node'));
|
||||||
|
expect(paths.python).toBe(path.join(dir, 'python', 'bin', 'python3'));
|
||||||
|
expect(paths.litellm).toBe(
|
||||||
|
path.join(dir, 'python', 'lib', 'python3.11', 'site-packages', 'litellm'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct Unix paths when platform is linux', () => {
|
||||||
|
const dir = '/app/src-tauri/resources';
|
||||||
|
const paths = getResourcePaths(dir, 'linux');
|
||||||
|
|
||||||
|
expect(paths.node).toBe(path.join(dir, 'node', 'bin', 'node'));
|
||||||
|
expect(paths.python).toBe(path.join(dir, 'python', 'bin', 'python3'));
|
||||||
|
expect(paths.litellm).toBe(
|
||||||
|
path.join(dir, 'python', 'lib', 'python3.11', 'site-packages', 'litellm'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getBundleStatus ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getBundleStatus', () => {
|
||||||
|
it('reports all missing when nothing exists', () => {
|
||||||
|
const mockExists = () => false;
|
||||||
|
const status = getBundleStatus('/fake/dir', mockExists);
|
||||||
|
|
||||||
|
expect(status.nodeReady).toBe(false);
|
||||||
|
expect(status.pythonReady).toBe(false);
|
||||||
|
expect(status.litellmReady).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports all ready when all exist', () => {
|
||||||
|
const mockExists = () => true;
|
||||||
|
const status = getBundleStatus('/fake/dir', mockExists);
|
||||||
|
|
||||||
|
expect(status.nodeReady).toBe(true);
|
||||||
|
expect(status.pythonReady).toBe(true);
|
||||||
|
expect(status.litellmReady).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports partial status correctly', () => {
|
||||||
|
const paths = getResourcePaths('/fake/dir', process.platform);
|
||||||
|
const existingPaths = new Set([paths.node, paths.python]);
|
||||||
|
const mockExists = (p: string) => existingPaths.has(p);
|
||||||
|
|
||||||
|
const status = getBundleStatus('/fake/dir', mockExists);
|
||||||
|
|
||||||
|
expect(status.nodeReady).toBe(true);
|
||||||
|
expect(status.pythonReady).toBe(true);
|
||||||
|
expect(status.litellmReady).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── parseVersion ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('parseVersion', () => {
|
||||||
|
it('parses a standard semver string', () => {
|
||||||
|
const v = parseVersion('20.11.1');
|
||||||
|
expect(v).toEqual({ major: 20, minor: 11, patch: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses a version with zeros', () => {
|
||||||
|
const v = parseVersion('3.0.0');
|
||||||
|
expect(v).toEqual({ major: 3, minor: 0, patch: 0 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── isValidVersion ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('isValidVersion', () => {
|
||||||
|
it('accepts a valid version', () => {
|
||||||
|
expect(isValidVersion('20.11.1')).toBe(true);
|
||||||
|
expect(isValidVersion('3.11.8')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects invalid versions', () => {
|
||||||
|
expect(isValidVersion('20.11')).toBe(false);
|
||||||
|
expect(isValidVersion('abc')).toBe(false);
|
||||||
|
expect(isValidVersion('20.11.1.2')).toBe(false);
|
||||||
|
expect(isValidVersion('')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
166
app/scripts/bundle-runtimes.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
#!/usr/bin/env npx tsx
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bundle-runtimes — Download and prepare Node.js + Python runtimes
|
||||||
|
* for embedding in the Tauri installer.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npx tsx app/scripts/bundle-runtimes.ts # Download all
|
||||||
|
* npx tsx app/scripts/bundle-runtimes.ts --status # Check what's ready
|
||||||
|
*
|
||||||
|
* Output directory: app/src-tauri/resources/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { existsSync, readdirSync, readFileSync, writeFileSync, createWriteStream } from 'node:fs';
|
||||||
|
import { mkdir, rm } from 'node:fs/promises';
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { pipeline } from 'node:stream/promises';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { Readable } from 'node:stream';
|
||||||
|
import { getNodeDownloadUrl, getPythonDownloadUrl, getResourcePaths, getBundleStatus } from './bundle-utils.js';
|
||||||
|
|
||||||
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
|
const _dirname = path.dirname(_filename);
|
||||||
|
const RESOURCES_DIR = path.resolve(_dirname, '..', 'src-tauri', 'resources');
|
||||||
|
|
||||||
|
const NODE_VERSION = '20.11.1';
|
||||||
|
const PYTHON_VERSION = '3.11.8';
|
||||||
|
|
||||||
|
// ─── Side-effect functions (download / install) ─────────────────────────────
|
||||||
|
|
||||||
|
async function downloadFile(url: string, destPath: string): Promise<void> {
|
||||||
|
console.log(` Downloading: ${url}`);
|
||||||
|
console.log(` Destination: ${destPath}`);
|
||||||
|
|
||||||
|
await mkdir(path.dirname(destPath), { recursive: true });
|
||||||
|
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status} downloading ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileStream = createWriteStream(destPath);
|
||||||
|
// Convert web ReadableStream to Node.js Readable
|
||||||
|
const body = res.body;
|
||||||
|
if (!body) throw new Error('Empty response body');
|
||||||
|
const nodeStream = Readable.fromWeb(body as import('node:stream/web').ReadableStream);
|
||||||
|
await pipeline(nodeStream, fileStream);
|
||||||
|
console.log(` Done.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadNodeBinary(version: string, resourcesDir: string): Promise<void> {
|
||||||
|
const url = getNodeDownloadUrl(version);
|
||||||
|
const dest = getResourcePaths(resourcesDir).node;
|
||||||
|
await downloadFile(url, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadPythonEmbed(version: string, resourcesDir: string): Promise<void> {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
console.warn('Warning: Python embed bundling is currently only supported on Windows.');
|
||||||
|
console.warn('On macOS/Linux, use system Python or a different bundling strategy.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = getPythonDownloadUrl(version);
|
||||||
|
const zipDest = path.join(resourcesDir, 'python', `python-${version}-embed.zip`);
|
||||||
|
await downloadFile(url, zipDest);
|
||||||
|
|
||||||
|
const pythonDir = path.join(resourcesDir, 'python');
|
||||||
|
console.log(` Extracting to ${pythonDir}...`);
|
||||||
|
execFileSync('powershell', [
|
||||||
|
'-NoProfile',
|
||||||
|
'-Command',
|
||||||
|
`Expand-Archive -Force -Path "${zipDest}" -DestinationPath "${pythonDir}"`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await rm(zipDest, { force: true });
|
||||||
|
console.log(` Extracted.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installLiteLLM(resourcesDir: string): Promise<void> {
|
||||||
|
const pythonExe = getResourcePaths(resourcesDir).python;
|
||||||
|
if (!existsSync(pythonExe)) {
|
||||||
|
throw new Error('Python must be downloaded before installing LiteLLM');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pythonDir = path.join(resourcesDir, 'python');
|
||||||
|
const pthFiles = readdirSync(pythonDir).filter((f) => f.endsWith('._pth'));
|
||||||
|
for (const pth of pthFiles) {
|
||||||
|
const pthPath = path.join(pythonDir, pth);
|
||||||
|
const content = readFileSync(pthPath, 'utf8');
|
||||||
|
const patched = content.replace(/^#\s*import site/m, 'import site');
|
||||||
|
writeFileSync(pthPath, patched);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPipUrl = 'https://bootstrap.pypa.io/get-pip.py';
|
||||||
|
const getPipDest = path.join(pythonDir, 'get-pip.py');
|
||||||
|
await downloadFile(getPipUrl, getPipDest);
|
||||||
|
|
||||||
|
console.log(' Installing pip...');
|
||||||
|
const targetDir = path.join(pythonDir, 'Lib', 'site-packages');
|
||||||
|
await mkdir(targetDir, { recursive: true });
|
||||||
|
execFileSync(pythonExe, [getPipDest, '--target', targetDir, '--no-warn-script-location'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(' Installing litellm...');
|
||||||
|
execFileSync(pythonExe, ['-m', 'pip', 'install', '--target', targetDir, 'litellm', '--no-warn-script-location'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: { ...process.env, PYTHONPATH: targetDir },
|
||||||
|
});
|
||||||
|
|
||||||
|
await rm(getPipDest, { force: true });
|
||||||
|
console.log(' LiteLLM installed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.includes('--status')) {
|
||||||
|
const status = getBundleStatus(RESOURCES_DIR);
|
||||||
|
console.log('Bundle status:');
|
||||||
|
console.log(` Node.js : ${status.nodeReady ? 'READY' : 'NOT FOUND'}`);
|
||||||
|
console.log(` Python : ${status.pythonReady ? 'READY' : 'NOT FOUND'}`);
|
||||||
|
console.log(` LiteLLM : ${status.litellmReady ? 'READY' : 'NOT FOUND'}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('=== Waggle Runtime Bundler ===\n');
|
||||||
|
console.log(`Resources dir: ${RESOURCES_DIR}`);
|
||||||
|
console.log(`Node.js ${NODE_VERSION} | Python ${PYTHON_VERSION}\n`);
|
||||||
|
|
||||||
|
await mkdir(RESOURCES_DIR, { recursive: true });
|
||||||
|
|
||||||
|
const status = getBundleStatus(RESOURCES_DIR);
|
||||||
|
|
||||||
|
if (!status.nodeReady) {
|
||||||
|
console.log('[1/3] Downloading Node.js...');
|
||||||
|
await downloadNodeBinary(NODE_VERSION, RESOURCES_DIR);
|
||||||
|
} else {
|
||||||
|
console.log('[1/3] Node.js already present, skipping.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status.pythonReady) {
|
||||||
|
console.log('[2/3] Downloading embedded Python...');
|
||||||
|
await downloadPythonEmbed(PYTHON_VERSION, RESOURCES_DIR);
|
||||||
|
} else {
|
||||||
|
console.log('[2/3] Python already present, skipping.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status.litellmReady) {
|
||||||
|
console.log('[3/3] Installing LiteLLM...');
|
||||||
|
await installLiteLLM(RESOURCES_DIR);
|
||||||
|
} else {
|
||||||
|
console.log('[3/3] LiteLLM already present, skipping.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== All runtimes ready! ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err: unknown) => {
|
||||||
|
console.error('Fatal error:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
123
app/scripts/bundle-utils.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* bundle-utils.ts — Pure utility functions for runtime bundling.
|
||||||
|
* No side effects — safe to import in tests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
// ─── URL builders ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the download URL for a Node.js binary.
|
||||||
|
*/
|
||||||
|
export function getNodeDownloadUrl(
|
||||||
|
version: string,
|
||||||
|
platform: string = 'win32',
|
||||||
|
arch: string = 'x64',
|
||||||
|
): string {
|
||||||
|
if (platform === 'win32') {
|
||||||
|
return `https://nodejs.org/dist/v${version}/win-${arch}/node.exe`;
|
||||||
|
}
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
return `https://nodejs.org/dist/v${version}/node-v${version}-darwin-${arch}.tar.gz`;
|
||||||
|
}
|
||||||
|
// linux
|
||||||
|
return `https://nodejs.org/dist/v${version}/node-v${version}-linux-${arch}.tar.xz`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the download URL for an embeddable Python zip.
|
||||||
|
*/
|
||||||
|
export function getPythonDownloadUrl(
|
||||||
|
version: string,
|
||||||
|
platform: string = 'win32',
|
||||||
|
arch: string = 'x64',
|
||||||
|
): string {
|
||||||
|
if (platform === 'win32') {
|
||||||
|
const archSuffix = arch === 'x64' ? 'amd64' : arch;
|
||||||
|
return `https://www.python.org/ftp/python/${version}/python-${version}-embed-${archSuffix}.zip`;
|
||||||
|
}
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
return `https://www.python.org/ftp/python/${version}/python-${version}-macos11.pkg`;
|
||||||
|
}
|
||||||
|
return `https://www.python.org/ftp/python/${version}/Python-${version}.tar.xz`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Path helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface ResourcePaths {
|
||||||
|
node: string;
|
||||||
|
python: string;
|
||||||
|
litellm: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the expected file paths for each bundled component.
|
||||||
|
*/
|
||||||
|
export function getResourcePaths(resourcesDir: string, platform: string = process.platform): ResourcePaths {
|
||||||
|
if (platform === 'win32') {
|
||||||
|
return {
|
||||||
|
node: path.join(resourcesDir, 'node', 'node.exe'),
|
||||||
|
python: path.join(resourcesDir, 'python', 'python.exe'),
|
||||||
|
litellm: path.join(resourcesDir, 'python', 'Lib', 'site-packages', 'litellm'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// darwin / linux
|
||||||
|
return {
|
||||||
|
node: path.join(resourcesDir, 'node', 'bin', 'node'),
|
||||||
|
python: path.join(resourcesDir, 'python', 'bin', 'python3'),
|
||||||
|
litellm: path.join(resourcesDir, 'python', 'lib', 'python3.11', 'site-packages', 'litellm'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Status ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface BundleStatus {
|
||||||
|
nodeReady: boolean;
|
||||||
|
pythonReady: boolean;
|
||||||
|
litellmReady: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check which runtimes are already present and ready.
|
||||||
|
* Accepts an optional existsSync override for testing.
|
||||||
|
*/
|
||||||
|
export function getBundleStatus(
|
||||||
|
resourcesDir: string,
|
||||||
|
_existsSync: (p: string) => boolean = existsSync,
|
||||||
|
): BundleStatus {
|
||||||
|
const paths = getResourcePaths(resourcesDir);
|
||||||
|
return {
|
||||||
|
nodeReady: _existsSync(paths.node),
|
||||||
|
pythonReady: _existsSync(paths.python),
|
||||||
|
litellmReady: _existsSync(paths.litellm),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Version helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface ParsedVersion {
|
||||||
|
major: number;
|
||||||
|
minor: number;
|
||||||
|
patch: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a version string like '20.11.1' into { major, minor, patch }.
|
||||||
|
*/
|
||||||
|
export function parseVersion(version: string): ParsedVersion {
|
||||||
|
const parts = version.split('.').map(Number);
|
||||||
|
return {
|
||||||
|
major: parts[0] || 0,
|
||||||
|
minor: parts[1] || 0,
|
||||||
|
patch: parts[2] || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a version string (must be X.Y.Z with numeric parts).
|
||||||
|
*/
|
||||||
|
export function isValidVersion(version: string): boolean {
|
||||||
|
return /^\d+\.\d+\.\d+$/.test(version);
|
||||||
|
}
|
||||||
263
app/scripts/installer-config.test.ts
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import {
|
||||||
|
getDefaultInstallerConfig,
|
||||||
|
generateNsisDefines,
|
||||||
|
validateInstallPath,
|
||||||
|
isSystemPath,
|
||||||
|
getUninstallPrompt,
|
||||||
|
getVersionFromPackage,
|
||||||
|
} from './installer-config.js';
|
||||||
|
|
||||||
|
// ─── getDefaultInstallerConfig ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getDefaultInstallerConfig', () => {
|
||||||
|
it('returns a valid config with expected defaults', () => {
|
||||||
|
const config = getDefaultInstallerConfig();
|
||||||
|
expect(config.productName).toBe('Waggle');
|
||||||
|
expect(config.version).toBe('0.1.0');
|
||||||
|
expect(config.publisher).toBe('Waggle');
|
||||||
|
expect(config.defaultInstallDir).toBe('C:\\Program Files\\Waggle');
|
||||||
|
expect(config.dataDir).toBe('~/.waggle');
|
||||||
|
expect(config.autostart).toBe(true);
|
||||||
|
expect(config.desktopShortcut).toBe(true);
|
||||||
|
expect(config.startMenuEntry).toBe(true);
|
||||||
|
expect(config.launchAfterInstall).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a new object each time', () => {
|
||||||
|
const a = getDefaultInstallerConfig();
|
||||||
|
const b = getDefaultInstallerConfig();
|
||||||
|
expect(a).toEqual(b);
|
||||||
|
expect(a).not.toBe(b);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── generateNsisDefines ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('generateNsisDefines', () => {
|
||||||
|
it('maps all config fields to NSIS defines', () => {
|
||||||
|
const config = getDefaultInstallerConfig();
|
||||||
|
const defines = generateNsisDefines(config);
|
||||||
|
|
||||||
|
expect(defines.PRODUCT_NAME).toBe('Waggle');
|
||||||
|
expect(defines.PRODUCT_VERSION).toBe('0.1.0');
|
||||||
|
expect(defines.PRODUCT_PUBLISHER).toBe('Waggle');
|
||||||
|
expect(defines.DEFAULT_INSTALL_DIR).toBe('C:\\Program Files\\Waggle');
|
||||||
|
expect(defines.DATA_DIR).toBe('~/.waggle');
|
||||||
|
expect(defines.AUTOSTART).toBe('1');
|
||||||
|
expect(defines.DESKTOP_SHORTCUT).toBe('1');
|
||||||
|
expect(defines.START_MENU_ENTRY).toBe('1');
|
||||||
|
expect(defines.LAUNCH_AFTER_INSTALL).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets boolean defines to "0" when disabled', () => {
|
||||||
|
const config = getDefaultInstallerConfig();
|
||||||
|
config.autostart = false;
|
||||||
|
config.desktopShortcut = false;
|
||||||
|
config.startMenuEntry = false;
|
||||||
|
config.launchAfterInstall = false;
|
||||||
|
|
||||||
|
const defines = generateNsisDefines(config);
|
||||||
|
|
||||||
|
expect(defines.AUTOSTART).toBe('0');
|
||||||
|
expect(defines.DESKTOP_SHORTCUT).toBe('0');
|
||||||
|
expect(defines.START_MENU_ENTRY).toBe('0');
|
||||||
|
expect(defines.LAUNCH_AFTER_INSTALL).toBe('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles custom product names and versions', () => {
|
||||||
|
const config = getDefaultInstallerConfig();
|
||||||
|
config.productName = 'Waggle Pro';
|
||||||
|
config.version = '2.5.0';
|
||||||
|
|
||||||
|
const defines = generateNsisDefines(config);
|
||||||
|
expect(defines.PRODUCT_NAME).toBe('Waggle Pro');
|
||||||
|
expect(defines.PRODUCT_VERSION).toBe('2.5.0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── validateInstallPath ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('validateInstallPath', () => {
|
||||||
|
it('accepts a standard Program Files path', () => {
|
||||||
|
const result = validateInstallPath('C:\\Program Files\\Waggle');
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
expect(result.error).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts a custom install path', () => {
|
||||||
|
const result = validateInstallPath('D:\\Apps\\Waggle');
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts forward slashes', () => {
|
||||||
|
const result = validateInstallPath('C:/Users/test/Waggle');
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts UNC paths', () => {
|
||||||
|
const result = validateInstallPath('\\\\server\\share\\Waggle');
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects empty path', () => {
|
||||||
|
const result = validateInstallPath('');
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.error).toContain('empty');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects whitespace-only path', () => {
|
||||||
|
const result = validateInstallPath(' ');
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.error).toContain('empty');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects relative paths', () => {
|
||||||
|
const result = validateInstallPath('Waggle\\bin');
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.error).toContain('absolute');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects paths with invalid characters', () => {
|
||||||
|
const result = validateInstallPath('C:\\Program Files\\Waggle<test>');
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.error).toContain('invalid characters');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects paths that are too long', () => {
|
||||||
|
const longPath = 'C:\\' + 'a'.repeat(250);
|
||||||
|
const result = validateInstallPath(longPath);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.error).toContain('too long');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects bare drive root', () => {
|
||||||
|
const result = validateInstallPath('C:\\');
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.error).toContain('drive root');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects drive letter without backslash', () => {
|
||||||
|
const result = validateInstallPath('C:');
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.error).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── isSystemPath ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('isSystemPath', () => {
|
||||||
|
it('detects Program Files', () => {
|
||||||
|
expect(isSystemPath('C:\\Program Files\\Waggle')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects Program Files (x86)', () => {
|
||||||
|
expect(isSystemPath('C:\\Program Files (x86)\\Waggle')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects Windows directory', () => {
|
||||||
|
expect(isSystemPath('C:\\Windows\\System32')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects ProgramData', () => {
|
||||||
|
expect(isSystemPath('C:\\ProgramData\\Waggle')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is case-insensitive', () => {
|
||||||
|
expect(isSystemPath('c:\\program files\\waggle')).toBe(true);
|
||||||
|
expect(isSystemPath('C:\\PROGRAM FILES\\Waggle')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles forward slashes', () => {
|
||||||
|
expect(isSystemPath('C:/Program Files/Waggle')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for user directories', () => {
|
||||||
|
expect(isSystemPath('C:\\Users\\test\\Waggle')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for custom paths', () => {
|
||||||
|
expect(isSystemPath('D:\\Apps\\Waggle')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getUninstallPrompt ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getUninstallPrompt', () => {
|
||||||
|
it('includes the data directory in the prompt', () => {
|
||||||
|
const prompt = getUninstallPrompt('~/.waggle');
|
||||||
|
expect(prompt).toContain('~/.waggle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mentions keeping data for future use', () => {
|
||||||
|
const prompt = getUninstallPrompt('C:\\Users\\test\\.waggle');
|
||||||
|
expect(prompt).toContain('keep it for future use');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mentions deleting all data option', () => {
|
||||||
|
const prompt = getUninstallPrompt('~/.waggle');
|
||||||
|
expect(prompt).toContain('delete all data');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mentions agents and memories', () => {
|
||||||
|
const prompt = getUninstallPrompt('~/.waggle');
|
||||||
|
expect(prompt).toContain('agents');
|
||||||
|
expect(prompt).toContain('memories');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getVersionFromPackage ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getVersionFromPackage', () => {
|
||||||
|
const tmpDir = path.join(tmpdir(), 'waggle-installer-test-' + Date.now());
|
||||||
|
|
||||||
|
// Setup / teardown
|
||||||
|
const setup = () => mkdirSync(tmpDir, { recursive: true });
|
||||||
|
const cleanup = () => rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
it('reads the version from a valid package.json', () => {
|
||||||
|
setup();
|
||||||
|
try {
|
||||||
|
const pkgPath = path.join(tmpDir, 'package.json');
|
||||||
|
writeFileSync(pkgPath, JSON.stringify({ name: 'test', version: '1.2.3' }));
|
||||||
|
expect(getVersionFromPackage(pkgPath)).toBe('1.2.3');
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for package.json without version', () => {
|
||||||
|
setup();
|
||||||
|
try {
|
||||||
|
const pkgPath = path.join(tmpDir, 'package.json');
|
||||||
|
writeFileSync(pkgPath, JSON.stringify({ name: 'test' }));
|
||||||
|
expect(() => getVersionFromPackage(pkgPath)).toThrow('No valid "version"');
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for non-existent file', () => {
|
||||||
|
expect(() => getVersionFromPackage('/nonexistent/package.json')).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads the actual app package.json version', () => {
|
||||||
|
const appPkgPath = path.resolve(__dirname, '..', 'package.json');
|
||||||
|
const version = getVersionFromPackage(appPkgPath);
|
||||||
|
expect(version).toMatch(/^\d+\.\d+\.\d+/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws with a descriptive error for malformed JSON', () => {
|
||||||
|
setup();
|
||||||
|
try {
|
||||||
|
const pkgPath = path.join(tmpDir, 'bad.json');
|
||||||
|
writeFileSync(pkgPath, '{ not valid json!!!');
|
||||||
|
expect(() => getVersionFromPackage(pkgPath)).toThrow('Failed to parse');
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
164
app/scripts/installer-config.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* installer-config.ts — Testable utility functions for NSIS installer configuration.
|
||||||
|
* No side effects — safe to import in tests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface InstallerConfig {
|
||||||
|
productName: string;
|
||||||
|
version: string;
|
||||||
|
publisher: string;
|
||||||
|
defaultInstallDir: string;
|
||||||
|
dataDir: string; // ~/.waggle
|
||||||
|
autostart: boolean;
|
||||||
|
desktopShortcut: boolean;
|
||||||
|
startMenuEntry: boolean;
|
||||||
|
launchAfterInstall: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Default config ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function getDefaultInstallerConfig(): InstallerConfig {
|
||||||
|
return {
|
||||||
|
productName: 'Waggle',
|
||||||
|
version: '0.1.0',
|
||||||
|
publisher: 'Waggle',
|
||||||
|
defaultInstallDir: 'C:\\Program Files\\Waggle',
|
||||||
|
dataDir: '~/.waggle',
|
||||||
|
autostart: true,
|
||||||
|
desktopShortcut: true,
|
||||||
|
startMenuEntry: true,
|
||||||
|
launchAfterInstall: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── NSIS defines ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate NSIS !define statements from an InstallerConfig.
|
||||||
|
* These become compile-time constants in the NSIS script.
|
||||||
|
*/
|
||||||
|
export function generateNsisDefines(
|
||||||
|
config: InstallerConfig,
|
||||||
|
): Record<string, string> {
|
||||||
|
return {
|
||||||
|
PRODUCT_NAME: config.productName,
|
||||||
|
PRODUCT_VERSION: config.version,
|
||||||
|
PRODUCT_PUBLISHER: config.publisher,
|
||||||
|
DEFAULT_INSTALL_DIR: config.defaultInstallDir,
|
||||||
|
DATA_DIR: config.dataDir,
|
||||||
|
AUTOSTART: config.autostart ? '1' : '0',
|
||||||
|
DESKTOP_SHORTCUT: config.desktopShortcut ? '1' : '0',
|
||||||
|
START_MENU_ENTRY: config.startMenuEntry ? '1' : '0',
|
||||||
|
LAUNCH_AFTER_INSTALL: config.launchAfterInstall ? '1' : '0',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Path validation ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a Windows install directory path.
|
||||||
|
* Returns { valid, error? }.
|
||||||
|
*/
|
||||||
|
export function validateInstallPath(
|
||||||
|
installPath: string,
|
||||||
|
): { valid: boolean; error?: string } {
|
||||||
|
if (!installPath || installPath.trim().length === 0) {
|
||||||
|
return { valid: false, error: 'Install path cannot be empty' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = installPath.trim();
|
||||||
|
|
||||||
|
// Must be an absolute path (drive letter or UNC)
|
||||||
|
const isAbsolute =
|
||||||
|
/^[A-Za-z]:[/\\]/.test(trimmed) || trimmed.startsWith('\\\\');
|
||||||
|
if (!isAbsolute) {
|
||||||
|
return { valid: false, error: 'Install path must be an absolute path' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for invalid characters (Windows filename restrictions)
|
||||||
|
// Drive prefix and backslashes/forward slashes are allowed
|
||||||
|
// For UNC paths (\\server\share\...), skip the leading \\; for drive paths, skip "C:\"
|
||||||
|
const pathBody = trimmed.startsWith('\\\\')
|
||||||
|
? trimmed.slice(2) // skip leading "\\" for UNC
|
||||||
|
: trimmed.slice(3); // skip "C:\" for drive paths
|
||||||
|
if (/[<>"|?*:]/.test(pathBody)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Install path contains invalid characters: < > " | ? * :',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path should not be too long (Windows MAX_PATH = 260, but allow some room)
|
||||||
|
if (trimmed.length > 240) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Install path is too long (max 240 characters)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not be a root drive path alone
|
||||||
|
if (/^[A-Za-z]:[/\\]?$/.test(trimmed)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Cannot install directly to a drive root',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── System path detection ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a path is under a system-protected directory (requires admin/elevation).
|
||||||
|
*/
|
||||||
|
export function isSystemPath(installPath: string): boolean {
|
||||||
|
const normalized = installPath.replace(/\//g, '\\').toLowerCase();
|
||||||
|
const systemPrefixes = [
|
||||||
|
'c:\\program files\\',
|
||||||
|
'c:\\program files (x86)\\',
|
||||||
|
'c:\\windows\\',
|
||||||
|
'c:\\programdata\\',
|
||||||
|
];
|
||||||
|
return systemPrefixes.some((prefix) => normalized.startsWith(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Uninstall prompt ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the uninstaller prompt text for data directory removal.
|
||||||
|
*/
|
||||||
|
export function getUninstallPrompt(dataDir: string): string {
|
||||||
|
return (
|
||||||
|
`Waggle stores your personal data (agents, memories, configuration) in:\n\n` +
|
||||||
|
` ${dataDir}\n\n` +
|
||||||
|
`Do you want to remove this data as well?\n\n` +
|
||||||
|
`Choose "Yes" to delete all data, or "No" to keep it for future use.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Version from package.json ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the version string from a package.json file.
|
||||||
|
* Throws if the file cannot be read or has no version field.
|
||||||
|
*/
|
||||||
|
export function getVersionFromPackage(packageJsonPath: string): string {
|
||||||
|
const raw = readFileSync(packageJsonPath, 'utf-8');
|
||||||
|
let pkg: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
pkg = JSON.parse(raw);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse ${packageJsonPath}: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!pkg.version || typeof pkg.version !== 'string') {
|
||||||
|
throw new Error(`No valid "version" field in ${packageJsonPath}`);
|
||||||
|
}
|
||||||
|
return pkg.version;
|
||||||
|
}
|
||||||
58
app/scripts/sign-macos-adhoc.sh
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# sign-macos-adhoc.sh — re-sign + verify a Tauri-built .app bundle for the
|
||||||
|
# Wave-1 Egzakta-internal pilot using ad-hoc signing.
|
||||||
|
#
|
||||||
|
# Implements docs/code-signing-pilot-and-launch.md §1.2 (macOS ad-hoc).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./sign-macos-adhoc.sh <path-to-Waggle.app>
|
||||||
|
#
|
||||||
|
# Tauri's bundle config (tauri.build-override.conf.json) already passes
|
||||||
|
# `signingIdentity: "-"` to codesign at build time, so the produced .app is
|
||||||
|
# already ad-hoc-signed. This script:
|
||||||
|
#
|
||||||
|
# 1. Re-signs the bundle with --force --deep to catch any nested helpers
|
||||||
|
# (sidecar binary, native deps) that Tauri's pass missed.
|
||||||
|
# 2. Verifies the signature with --verify --deep --strict.
|
||||||
|
#
|
||||||
|
# Distribute the result wrapped in .zip (NOT .dmg — Gatekeeper enforces
|
||||||
|
# notarization more aggressively on disk images since macOS 10.15).
|
||||||
|
#
|
||||||
|
# NOT for public Day-0 — that requires Apple Developer ID + notarization
|
||||||
|
# per docs/code-signing-pilot-and-launch.md §2.2.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_PATH="${1:-}"
|
||||||
|
|
||||||
|
if [[ -z "$APP_PATH" ]]; then
|
||||||
|
echo "usage: $0 <path-to-Waggle.app>" >&2
|
||||||
|
exit 64 # EX_USAGE
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$APP_PATH" ]]; then
|
||||||
|
echo "[sign-macos-adhoc] not a directory: $APP_PATH" >&2
|
||||||
|
exit 66 # EX_NOINPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! "$APP_PATH" =~ \.app$ ]]; then
|
||||||
|
echo "[sign-macos-adhoc] path must end in .app: $APP_PATH" >&2
|
||||||
|
exit 64
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v codesign >/dev/null 2>&1; then
|
||||||
|
echo "[sign-macos-adhoc] codesign not found — Xcode command-line tools required." >&2
|
||||||
|
exit 69 # EX_UNAVAILABLE
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[sign-macos-adhoc] re-signing: $APP_PATH"
|
||||||
|
codesign --force --deep --sign - "$APP_PATH"
|
||||||
|
|
||||||
|
echo "[sign-macos-adhoc] verifying signature"
|
||||||
|
codesign --verify --deep --strict "$APP_PATH"
|
||||||
|
|
||||||
|
echo "[sign-macos-adhoc] OK"
|
||||||
|
echo ""
|
||||||
|
echo "Next:"
|
||||||
|
echo " ditto -c -k --keepParent \"$APP_PATH\" \"${APP_PATH%.app}.zip\""
|
||||||
|
echo " # Distribute the .zip, NOT a .dmg, for the pilot."
|
||||||
203
app/scripts/sign-windows-pilot.ps1
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Self-sign cert generation + signtool wrapper for the Wave-1 Egzakta-internal pilot build.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Implements docs/code-signing-pilot-and-launch.md §1.1 (Windows self-sign).
|
||||||
|
Two modes:
|
||||||
|
|
||||||
|
-Mode Setup Generate self-signed cert (idempotent — reuses existing cert
|
||||||
|
by subject if it exists), export to .pfx, write thumbprint
|
||||||
|
to app/src-tauri/.thumbprint.txt.
|
||||||
|
-Mode Sign Sign the artefact at -ArtifactPath using the cert produced
|
||||||
|
by Setup. Wraps signtool.exe.
|
||||||
|
|
||||||
|
Password resolution order (Setup): env WAGGLE_PILOT_PFX_PASSWORD; otherwise
|
||||||
|
Read-Host -AsSecureString prompt. Setup writes the .pfx to %USERPROFILE%
|
||||||
|
so it never lands inside the repo working tree.
|
||||||
|
|
||||||
|
NOT for public Day-0 signing — that uses a real EV Authenticode cert per §2.
|
||||||
|
|
||||||
|
.PARAMETER Mode
|
||||||
|
Setup or Sign. Setup is idempotent — safe to re-run.
|
||||||
|
|
||||||
|
.PARAMETER ArtifactPath
|
||||||
|
Required when -Mode Sign. Path to the .msi or .exe to sign.
|
||||||
|
|
||||||
|
.PARAMETER Subject
|
||||||
|
Cert subject. Default: "CN=Egzakta Internal Pilot, O=Egzakta Group, C=RS".
|
||||||
|
Override only if rotating cert identity.
|
||||||
|
|
||||||
|
.PARAMETER PfxPath
|
||||||
|
Where to write the exported .pfx. Default:
|
||||||
|
$env:USERPROFILE\waggle-pilot-codesign.pfx
|
||||||
|
|
||||||
|
.PARAMETER ThumbprintFile
|
||||||
|
Where to write the captured thumbprint for downstream consumption by
|
||||||
|
apply-signing-config.mjs. Default: app/src-tauri/.thumbprint.txt
|
||||||
|
(relative to repo root, resolved via this script's location).
|
||||||
|
|
||||||
|
.PARAMETER TimestampUrl
|
||||||
|
RFC3161 timestamp server. Default: http://timestamp.digicert.com.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
PS> $env:WAGGLE_PILOT_PFX_PASSWORD = "your-strong-pw"
|
||||||
|
PS> .\sign-windows-pilot.ps1 -Mode Setup
|
||||||
|
Generates cert (or reuses existing), writes thumbprint to .thumbprint.txt.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
PS> .\sign-windows-pilot.ps1 -Mode Sign -ArtifactPath .\target\release\bundle\msi\Waggle_0.2.0_x64_en-US.msi
|
||||||
|
Signs the MSI using the cert from Setup.
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
Last updated: LAUNCH-06 (Phase 2 Step 4).
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateSet('Setup', 'Sign')]
|
||||||
|
[string]$Mode,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string]$ArtifactPath,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string]$Subject = 'CN=Egzakta Internal Pilot, O=Egzakta Group, C=RS',
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string]$PfxPath = (Join-Path $env:USERPROFILE 'waggle-pilot-codesign.pfx'),
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string]$ThumbprintFile,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string]$TimestampUrl = 'http://timestamp.digicert.com'
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# ─── Resolve repo-root paths ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$AppDir = Split-Path -Parent $ScriptDir # ...\waggle-os\app
|
||||||
|
$RepoRoot = Split-Path -Parent $AppDir # ...\waggle-os
|
||||||
|
|
||||||
|
if (-not $ThumbprintFile) {
|
||||||
|
$ThumbprintFile = Join-Path $AppDir 'src-tauri\.thumbprint.txt'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function Resolve-Password {
|
||||||
|
if ($env:WAGGLE_PILOT_PFX_PASSWORD) {
|
||||||
|
return ConvertTo-SecureString -String $env:WAGGLE_PILOT_PFX_PASSWORD -Force -AsPlainText
|
||||||
|
}
|
||||||
|
Write-Host 'WAGGLE_PILOT_PFX_PASSWORD not set in env — prompting.' -ForegroundColor Yellow
|
||||||
|
return Read-Host -Prompt 'Enter password to protect the .pfx export' -AsSecureString
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-Signtool {
|
||||||
|
# Prefer signtool from latest installed Windows SDK; fall back to PATH.
|
||||||
|
$candidates = @(
|
||||||
|
'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe',
|
||||||
|
'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64\signtool.exe',
|
||||||
|
'C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe'
|
||||||
|
)
|
||||||
|
foreach ($candidate in $candidates) {
|
||||||
|
if (Test-Path $candidate) { return $candidate }
|
||||||
|
}
|
||||||
|
$fromPath = Get-Command signtool.exe -ErrorAction SilentlyContinue
|
||||||
|
if ($fromPath) { return $fromPath.Source }
|
||||||
|
throw 'signtool.exe not found. Install Windows 10 SDK or add signtool to PATH.'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Mode: Setup ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ($Mode -eq 'Setup') {
|
||||||
|
Write-Host "[setup] subject: $Subject"
|
||||||
|
Write-Host "[setup] pfx path: $PfxPath"
|
||||||
|
Write-Host "[setup] thumbprint out: $ThumbprintFile"
|
||||||
|
|
||||||
|
# Reuse existing cert by subject if present (idempotency).
|
||||||
|
$existing = Get-ChildItem 'Cert:\CurrentUser\My' |
|
||||||
|
Where-Object { $_.Subject -eq $Subject -and $_.HasPrivateKey } |
|
||||||
|
Sort-Object NotAfter -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if ($existing -and $existing.NotAfter -gt (Get-Date)) {
|
||||||
|
Write-Host "[setup] reusing existing cert (NotAfter $($existing.NotAfter))" -ForegroundColor Green
|
||||||
|
$cert = $existing
|
||||||
|
} else {
|
||||||
|
Write-Host '[setup] generating new self-signed code-signing cert' -ForegroundColor Cyan
|
||||||
|
$cert = New-SelfSignedCertificate `
|
||||||
|
-Type CodeSigningCert `
|
||||||
|
-Subject $Subject `
|
||||||
|
-KeyUsage DigitalSignature `
|
||||||
|
-KeySpec Signature `
|
||||||
|
-KeyAlgorithm RSA -KeyLength 2048 `
|
||||||
|
-NotAfter (Get-Date).AddYears(2) `
|
||||||
|
-CertStoreLocation 'Cert:\CurrentUser\My' `
|
||||||
|
-TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3', '2.5.29.19={text}')
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export .pfx (always — re-export is harmless and refreshes the file).
|
||||||
|
$pwd = Resolve-Password
|
||||||
|
Export-PfxCertificate -Cert $cert -FilePath $PfxPath -Password $pwd | Out-Null
|
||||||
|
Write-Host "[setup] exported .pfx -> $PfxPath" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Write thumbprint where apply-signing-config.mjs expects it.
|
||||||
|
$thumbprintDir = Split-Path -Parent $ThumbprintFile
|
||||||
|
if (-not (Test-Path $thumbprintDir)) {
|
||||||
|
New-Item -ItemType Directory -Force -Path $thumbprintDir | Out-Null
|
||||||
|
}
|
||||||
|
Set-Content -Path $ThumbprintFile -Value $cert.Thumbprint -Encoding ascii -NoNewline
|
||||||
|
Write-Host "[setup] thumbprint -> $ThumbprintFile" -ForegroundColor Green
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'Next:' -ForegroundColor Cyan
|
||||||
|
Write-Host ' 1. cd app && npm run tauri:sign:pilot:win:apply'
|
||||||
|
Write-Host ' 2. npm run tauri:build:win'
|
||||||
|
Write-Host ' 3. .\scripts\sign-windows-pilot.ps1 -Mode Sign -ArtifactPath <path-to-msi>'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Mode: Sign ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ($Mode -eq 'Sign') {
|
||||||
|
if (-not $ArtifactPath) {
|
||||||
|
throw '-ArtifactPath required when -Mode Sign'
|
||||||
|
}
|
||||||
|
if (-not (Test-Path $ArtifactPath)) {
|
||||||
|
throw "Artifact not found: $ArtifactPath"
|
||||||
|
}
|
||||||
|
if (-not (Test-Path $PfxPath)) {
|
||||||
|
throw "PFX not found at $PfxPath. Run -Mode Setup first."
|
||||||
|
}
|
||||||
|
|
||||||
|
$signtool = Find-Signtool
|
||||||
|
$pwd = Resolve-Password
|
||||||
|
$plainPwd = [System.Net.NetworkCredential]::new('', $pwd).Password
|
||||||
|
|
||||||
|
Write-Host "[sign] signtool: $signtool"
|
||||||
|
Write-Host "[sign] artifact: $ArtifactPath"
|
||||||
|
|
||||||
|
& $signtool sign `
|
||||||
|
/f $PfxPath `
|
||||||
|
/p $plainPwd `
|
||||||
|
/tr $TimestampUrl `
|
||||||
|
/td sha256 /fd sha256 `
|
||||||
|
$ArtifactPath
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "signtool failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host '[sign] verifying signature' -ForegroundColor Cyan
|
||||||
|
& $signtool verify /pa /v $ArtifactPath
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "signtool verify failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host '[sign] OK' -ForegroundColor Green
|
||||||
|
return
|
||||||
|
}
|
||||||
177
app/scripts/signing-config.test.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
parseThumbprintString,
|
||||||
|
addWindowsSigningToOverride,
|
||||||
|
addMacosAdhocToOverride,
|
||||||
|
type TauriOverrideConfig,
|
||||||
|
} from './signing-config.js';
|
||||||
|
|
||||||
|
// ─── parseThumbprintString ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('parseThumbprintString', () => {
|
||||||
|
it('returns the uppercased thumbprint when given valid 40-hex input', () => {
|
||||||
|
const raw = 'abcdef0123456789abcdef0123456789abcdef01';
|
||||||
|
expect(parseThumbprintString(raw)).toBe(
|
||||||
|
'ABCDEF0123456789ABCDEF0123456789ABCDEF01',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips surrounding whitespace and trailing newline (PowerShell output shape)', () => {
|
||||||
|
const raw = ' ABCDEF0123456789ABCDEF0123456789ABCDEF01\r\n';
|
||||||
|
expect(parseThumbprintString(raw)).toBe(
|
||||||
|
'ABCDEF0123456789ABCDEF0123456789ABCDEF01',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes embedded whitespace inside the thumbprint (some clipboards introduce spaces)', () => {
|
||||||
|
const raw = 'AB CD EF 01 23 45 67 89 AB CD EF 01 23 45 67 89 AB CD EF 01';
|
||||||
|
expect(parseThumbprintString(raw)).toBe(
|
||||||
|
'ABCDEF0123456789ABCDEF0123456789ABCDEF01',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when the thumbprint is shorter than 40 chars', () => {
|
||||||
|
expect(() => parseThumbprintString('ABCDEF')).toThrow(
|
||||||
|
/must be 40 hex characters/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when the thumbprint contains non-hex characters', () => {
|
||||||
|
const raw = 'ZZCDEF0123456789ABCDEF0123456789ABCDEF01';
|
||||||
|
expect(() => parseThumbprintString(raw)).toThrow(/must be 40 hex characters/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on empty input', () => {
|
||||||
|
expect(() => parseThumbprintString('')).toThrow(/empty/i);
|
||||||
|
expect(() => parseThumbprintString(' ')).toThrow(/empty/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── addWindowsSigningToOverride ────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('addWindowsSigningToOverride', () => {
|
||||||
|
const VALID_THUMBPRINT = 'ABCDEF0123456789ABCDEF0123456789ABCDEF01';
|
||||||
|
|
||||||
|
it('writes certificateThumbprint + sensible defaults when no options given', () => {
|
||||||
|
const input: TauriOverrideConfig = { build: { beforeBuildCommand: '' } };
|
||||||
|
const out = addWindowsSigningToOverride(input, VALID_THUMBPRINT);
|
||||||
|
|
||||||
|
expect(out.bundle?.windows?.certificateThumbprint).toBe(VALID_THUMBPRINT);
|
||||||
|
expect(out.bundle?.windows?.digestAlgorithm).toBe('sha256');
|
||||||
|
expect(out.bundle?.windows?.timestampUrl).toBe(
|
||||||
|
'http://timestamp.digicert.com',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves existing top-level fields (build, app, etc.)', () => {
|
||||||
|
const input: TauriOverrideConfig = {
|
||||||
|
build: { beforeBuildCommand: 'echo hello' },
|
||||||
|
app: { security: { csp: 'default-src self' } },
|
||||||
|
};
|
||||||
|
const out = addWindowsSigningToOverride(input, VALID_THUMBPRINT);
|
||||||
|
|
||||||
|
expect(out.build).toEqual({ beforeBuildCommand: 'echo hello' });
|
||||||
|
expect(out.app).toEqual({ security: { csp: 'default-src self' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves existing bundle.windows fields not related to signing', () => {
|
||||||
|
const input: TauriOverrideConfig = {
|
||||||
|
bundle: {
|
||||||
|
windows: { nsis: { installMode: 'currentUser' } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const out = addWindowsSigningToOverride(input, VALID_THUMBPRINT);
|
||||||
|
|
||||||
|
expect(out.bundle?.windows?.nsis).toEqual({ installMode: 'currentUser' });
|
||||||
|
expect(out.bundle?.windows?.certificateThumbprint).toBe(VALID_THUMBPRINT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('overrides custom digestAlgorithm and timestampUrl when options provided', () => {
|
||||||
|
const out = addWindowsSigningToOverride({}, VALID_THUMBPRINT, {
|
||||||
|
digestAlgorithm: 'sha384',
|
||||||
|
timestampUrl: 'http://timestamp.sectigo.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(out.bundle?.windows?.digestAlgorithm).toBe('sha384');
|
||||||
|
expect(out.bundle?.windows?.timestampUrl).toBe(
|
||||||
|
'http://timestamp.sectigo.com',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not mutate the input config (immutability invariant)', () => {
|
||||||
|
const input: TauriOverrideConfig = { build: { beforeBuildCommand: '' } };
|
||||||
|
const inputSnapshot = JSON.parse(JSON.stringify(input));
|
||||||
|
addWindowsSigningToOverride(input, VALID_THUMBPRINT);
|
||||||
|
expect(input).toEqual(inputSnapshot);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is idempotent — applying twice with the same thumbprint yields equal output', () => {
|
||||||
|
const input: TauriOverrideConfig = {};
|
||||||
|
const once = addWindowsSigningToOverride(input, VALID_THUMBPRINT);
|
||||||
|
const twice = addWindowsSigningToOverride(once, VALID_THUMBPRINT);
|
||||||
|
expect(twice).toEqual(once);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces an old thumbprint when called with a new one (cert rotation)', () => {
|
||||||
|
const input: TauriOverrideConfig = {};
|
||||||
|
const v1 = addWindowsSigningToOverride(input, VALID_THUMBPRINT);
|
||||||
|
const newThumb = '1234567890ABCDEF1234567890ABCDEF12345678';
|
||||||
|
const v2 = addWindowsSigningToOverride(v1, newThumb);
|
||||||
|
expect(v2.bundle?.windows?.certificateThumbprint).toBe(newThumb);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an invalid thumbprint up front', () => {
|
||||||
|
expect(() =>
|
||||||
|
addWindowsSigningToOverride({}, 'too-short'),
|
||||||
|
).toThrow(/must be 40 hex characters/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── addMacosAdhocToOverride ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('addMacosAdhocToOverride', () => {
|
||||||
|
it('sets bundle.macOS.signingIdentity to "-" (ad-hoc sign sentinel)', () => {
|
||||||
|
const out = addMacosAdhocToOverride({});
|
||||||
|
expect(out.bundle?.macOS?.signingIdentity).toBe('-');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves existing top-level and bundle fields', () => {
|
||||||
|
const input: TauriOverrideConfig = {
|
||||||
|
build: { beforeBuildCommand: 'echo' },
|
||||||
|
bundle: {
|
||||||
|
windows: { certificateThumbprint: 'AB'.repeat(20) },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const out = addMacosAdhocToOverride(input);
|
||||||
|
|
||||||
|
expect(out.build).toEqual({ beforeBuildCommand: 'echo' });
|
||||||
|
expect(out.bundle?.windows?.certificateThumbprint).toBe('AB'.repeat(20));
|
||||||
|
expect(out.bundle?.macOS?.signingIdentity).toBe('-');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not mutate the input config', () => {
|
||||||
|
const input: TauriOverrideConfig = { bundle: { macOS: {} } };
|
||||||
|
const snapshot = JSON.parse(JSON.stringify(input));
|
||||||
|
addMacosAdhocToOverride(input);
|
||||||
|
expect(input).toEqual(snapshot);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is idempotent', () => {
|
||||||
|
const once = addMacosAdhocToOverride({});
|
||||||
|
const twice = addMacosAdhocToOverride(once);
|
||||||
|
expect(twice).toEqual(once);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves additional macOS fields (entitlements, providerShortName)', () => {
|
||||||
|
const input: TauriOverrideConfig = {
|
||||||
|
bundle: {
|
||||||
|
macOS: { entitlements: './ent.plist', providerShortName: 'TEAM' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const out = addMacosAdhocToOverride(input);
|
||||||
|
|
||||||
|
expect(out.bundle?.macOS?.entitlements).toBe('./ent.plist');
|
||||||
|
expect(out.bundle?.macOS?.providerShortName).toBe('TEAM');
|
||||||
|
expect(out.bundle?.macOS?.signingIdentity).toBe('-');
|
||||||
|
});
|
||||||
|
});
|
||||||
147
app/scripts/signing-config.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* signing-config.ts — Pure utilities for managing code-signing fields in
|
||||||
|
* Tauri's `tauri.build-override.conf.json`.
|
||||||
|
*
|
||||||
|
* Used by:
|
||||||
|
* - `apply-signing-config.mjs` (LAUNCH-06 pilot wiring)
|
||||||
|
* - `tauri:sign:pilot:win:apply` npm script
|
||||||
|
*
|
||||||
|
* No filesystem side effects — safe to import in tests. The thin CLI wrapper
|
||||||
|
* does the file I/O.
|
||||||
|
*
|
||||||
|
* Reference: docs/code-signing-pilot-and-launch.md §1.1 (Windows self-sign)
|
||||||
|
* docs/code-signing-pilot-and-launch.md §1.2 (macOS ad-hoc)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface TauriBundleWindows {
|
||||||
|
certificateThumbprint?: string;
|
||||||
|
digestAlgorithm?: string;
|
||||||
|
timestampUrl?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TauriBundleMacOS {
|
||||||
|
signingIdentity?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TauriBundle {
|
||||||
|
windows?: TauriBundleWindows;
|
||||||
|
macOS?: TauriBundleMacOS;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TauriOverrideConfig {
|
||||||
|
bundle?: TauriBundle;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WindowsSigningOptions {
|
||||||
|
digestAlgorithm?: string;
|
||||||
|
timestampUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Defaults ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const DEFAULT_DIGEST_ALGORITHM = 'sha256';
|
||||||
|
const DEFAULT_TIMESTAMP_URL = 'http://timestamp.digicert.com';
|
||||||
|
const MACOS_ADHOC_IDENTITY = '-';
|
||||||
|
const THUMBPRINT_LENGTH = 40;
|
||||||
|
const HEX_PATTERN = /^[0-9A-F]+$/;
|
||||||
|
|
||||||
|
// ─── parseThumbprintString ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalise a raw thumbprint string into the canonical 40-char uppercase form.
|
||||||
|
*
|
||||||
|
* Accepts whitespace anywhere (tab, space, newline) since PowerShell's
|
||||||
|
* `$cert.Thumbprint` plus clipboard round-tripping can introduce arbitrary
|
||||||
|
* spacing. Throws when the result is not exactly 40 hex characters.
|
||||||
|
*/
|
||||||
|
export function parseThumbprintString(raw: string): string {
|
||||||
|
if (!raw || raw.trim().length === 0) {
|
||||||
|
throw new Error('Thumbprint is empty — cert generation may have failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const compact = raw.replace(/\s+/g, '').toUpperCase();
|
||||||
|
|
||||||
|
if (compact.length !== THUMBPRINT_LENGTH || !HEX_PATTERN.test(compact)) {
|
||||||
|
throw new Error(
|
||||||
|
`Thumbprint must be 40 hex characters; got ${compact.length} chars (sample: "${compact.slice(0, 16)}...").`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return compact;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── addWindowsSigningToOverride ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new override config with Windows code-signing fields applied.
|
||||||
|
*
|
||||||
|
* Preserves all existing top-level and bundle fields; replaces only the
|
||||||
|
* three signing-specific keys under `bundle.windows`. Idempotent — calling
|
||||||
|
* twice with the same thumbprint yields an equal result.
|
||||||
|
*/
|
||||||
|
export function addWindowsSigningToOverride<T extends TauriOverrideConfig>(
|
||||||
|
config: Readonly<T>,
|
||||||
|
thumbprint: string,
|
||||||
|
options?: WindowsSigningOptions,
|
||||||
|
): T {
|
||||||
|
const normalisedThumbprint = parseThumbprintString(thumbprint);
|
||||||
|
const digestAlgorithm = options?.digestAlgorithm ?? DEFAULT_DIGEST_ALGORITHM;
|
||||||
|
const timestampUrl = options?.timestampUrl ?? DEFAULT_TIMESTAMP_URL;
|
||||||
|
|
||||||
|
const existingBundle: TauriBundle = config.bundle ?? {};
|
||||||
|
const existingWindows: TauriBundleWindows = existingBundle.windows ?? {};
|
||||||
|
|
||||||
|
const nextWindows: TauriBundleWindows = {
|
||||||
|
...existingWindows,
|
||||||
|
certificateThumbprint: normalisedThumbprint,
|
||||||
|
digestAlgorithm,
|
||||||
|
timestampUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextBundle: TauriBundle = {
|
||||||
|
...existingBundle,
|
||||||
|
windows: nextWindows,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
bundle: nextBundle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── addMacosAdhocToOverride ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new override config with macOS ad-hoc signing applied.
|
||||||
|
*
|
||||||
|
* Sets `bundle.macOS.signingIdentity` to "-" (Tauri / codesign sentinel for
|
||||||
|
* ad-hoc sign). Preserves all other fields. Used during pilot before a real
|
||||||
|
* Apple Developer ID cert is procured.
|
||||||
|
*/
|
||||||
|
export function addMacosAdhocToOverride<T extends TauriOverrideConfig>(
|
||||||
|
config: Readonly<T>,
|
||||||
|
): T {
|
||||||
|
const existingBundle: TauriBundle = config.bundle ?? {};
|
||||||
|
const existingMacOS: TauriBundleMacOS = existingBundle.macOS ?? {};
|
||||||
|
|
||||||
|
const nextMacOS: TauriBundleMacOS = {
|
||||||
|
...existingMacOS,
|
||||||
|
signingIdentity: MACOS_ADHOC_IDENTITY,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextBundle: TauriBundle = {
|
||||||
|
...existingBundle,
|
||||||
|
macOS: nextMacOS,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
bundle: nextBundle,
|
||||||
|
};
|
||||||
|
}
|
||||||
17
app/src-tauri/.cargo/config.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Intentionally NO `linker` override here.
|
||||||
|
#
|
||||||
|
# A previous version pinned the MSVC linker to an absolute, version-stamped path
|
||||||
|
# (…/MSVC/14.44.35207/…/link.exe) so Git Bash's GNU coreutils `link` couldn't
|
||||||
|
# shadow MSVC's linker on one local machine. That pin rotted: windows-latest CI
|
||||||
|
# ships a different MSVC toolset, and so does any dev box without that exact
|
||||||
|
# version, so cargo failed with:
|
||||||
|
# error: linker `…14.44.35207…\link.exe` not found
|
||||||
|
# note: The system cannot find the path specified. (os error 3)
|
||||||
|
#
|
||||||
|
# rustc/cc auto-detect the MSVC linker via vswhere on a properly installed
|
||||||
|
# toolchain — windows-latest works out of the box, no override needed.
|
||||||
|
#
|
||||||
|
# If Git's `link` shadows MSVC's linker locally, fix it in the ENVIRONMENT, not
|
||||||
|
# here: build from a "Developer Command Prompt for VS 2022" (or run
|
||||||
|
# vcvarsall.bat) so MSVC's bin precedes Git on PATH. Do NOT re-pin an absolute,
|
||||||
|
# version-stamped path — it breaks CI and every other machine.
|
||||||
6185
app/src-tauri/Cargo.lock
generated
Normal file
29
app/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[package]
|
||||||
|
name = "waggle"
|
||||||
|
version = "0.2.0"
|
||||||
|
description = "Waggle - Your personal AI agent swarm"
|
||||||
|
authors = ["Marko Markovic"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "waggle_lib"
|
||||||
|
crate-type = ["lib", "cdylib", "staticlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = ["tray-icon"] }
|
||||||
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-shell = "2"
|
||||||
|
tauri-plugin-autostart = "2"
|
||||||
|
tauri-plugin-global-shortcut = "2"
|
||||||
|
tauri-plugin-notification = "2"
|
||||||
|
tauri-plugin-single-instance = "2"
|
||||||
|
tauri-plugin-updater = "2"
|
||||||
|
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
urlencoding = "2"
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
3
app/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
12
app/src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default capabilities",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"shell:default",
|
||||||
|
"notification:default",
|
||||||
|
"global-shortcut:allow-register",
|
||||||
|
"global-shortcut:allow-unregister"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
app/src-tauri/gen/schemas/acl-manifests.json
Normal file
1
app/src-tauri/gen/schemas/capabilities.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"default":{"identifier":"default","description":"Default capabilities","local":true,"windows":["main"],"permissions":["core:default","shell:default","notification:default","global-shortcut:allow-register","global-shortcut:allow-unregister"]}}
|
||||||
2990
app/src-tauri/gen/schemas/desktop-schema.json
Normal file
2990
app/src-tauri/gen/schemas/windows-schema.json
Normal file
BIN
app/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
app/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
app/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
app/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
app/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
app/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
BIN
app/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 16 KiB |
BIN
app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 28 KiB |
BIN
app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 12 KiB |