u-foo 2.2.4 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/SKILLS/ufoo/SKILL.md +56 -12
  2. package/SKILLS/uinit/SKILL.md +3 -2
  3. package/modules/AGENTS.template.md +2 -1
  4. package/modules/bus/README.md +1 -1
  5. package/modules/context/SKILLS/uctx/SKILL.md +6 -4
  6. package/package.json +1 -1
  7. package/src/agent/activityStatePublisher.js +6 -2
  8. package/src/agent/codexThreadProvider.js +2 -2
  9. package/src/agent/controllerToolExecutor.js +24 -1
  10. package/src/agent/credentials/claude.js +85 -16
  11. package/src/agent/credentials/codex.js +251 -23
  12. package/src/agent/defaultBootstrap.js +3 -1
  13. package/src/agent/directAuthStatus.js +264 -0
  14. package/src/agent/internalRunner.js +18 -12
  15. package/src/agent/loopObservability.js +10 -0
  16. package/src/agent/loopRuntime.js +19 -0
  17. package/src/agent/notifier.js +12 -3
  18. package/src/agent/ufooAgent.js +43 -13
  19. package/src/agent/upstreamTransport.js +23 -8
  20. package/src/bus/index.js +6 -1
  21. package/src/bus/message.js +156 -8
  22. package/src/chat/commandExecutor.js +187 -7
  23. package/src/chat/commands.js +23 -4
  24. package/src/chat/completionController.js +30 -7
  25. package/src/chat/index.js +3 -5
  26. package/src/cli/groupCoreCommands.js +5 -0
  27. package/src/cli.js +309 -0
  28. package/src/code/UCODE_PROMPT.md +3 -2
  29. package/src/code/prompts/ufoo.js +3 -2
  30. package/src/config.js +16 -3
  31. package/src/context/doctor.js +1 -1
  32. package/src/daemon/groupOrchestrator.js +13 -9
  33. package/src/daemon/promptRequest.js +11 -2
  34. package/src/daemon/soloBootstrap.js +2 -0
  35. package/src/group/bootstrap.js +1 -1
  36. package/src/group/promptProfiles.js +106 -22
  37. package/src/group/templates.js +1 -0
  38. package/src/init/index.js +4 -0
  39. package/src/memory/historySearch.js +308 -0
  40. package/src/memory/index.js +653 -8
  41. package/src/providerapi/redactor.js +4 -1
  42. package/src/status/index.js +24 -1
  43. package/src/tools/handlers/memory.js +168 -0
  44. package/src/tools/index.js +12 -0
  45. package/src/tools/registry.js +12 -0
  46. package/src/tools/schemaFixtures.js +213 -0
  47. package/src/tools/tier1/editMemory.js +14 -0
  48. package/src/tools/tier1/forget.js +14 -0
  49. package/src/tools/tier1/recall.js +14 -0
  50. package/src/tools/tier1/remember.js +14 -0
  51. package/src/tools/tier1/searchHistory.js +14 -0
  52. package/src/tools/tier1/searchMemory.js +14 -0
  53. package/templates/groups/build-lane.json +44 -6
  54. package/templates/groups/build-ultra.json +6 -5
  55. package/templates/groups/design-system.json +84 -0
  56. package/templates/groups/product-discovery.json +9 -4
  57. package/templates/groups/ui-plan-review.json +84 -0
  58. package/templates/groups/ui-polish.json +6 -2
  59. package/templates/groups/verify-ship.json +9 -4
@@ -8,11 +8,12 @@ description: |
8
8
 
9
9
  # ufoo — Unified Agent Protocol
10
10
 
11
- ufoo is the multi-agent coordination layer. It provides three capabilities:
11
+ ufoo is the multi-agent coordination layer. It provides four capabilities:
12
12
 
13
- 1. **Context Decisions** — Persistent knowledge log shared across agents
14
- 2. **Event Bus** — Inter-agent messaging
15
- 3. **Initialization** — Project setup for ufoo modules
13
+ 1. **Context Decisions** — Sparse log of major plan-level choices shared across agents
14
+ 2. **Shared Memory** — Durable, low-noise project facts shared across agents
15
+ 3. **Event Bus** — Inter-agent messaging
16
+ 4. **Initialization** — Project setup for ufoo modules
16
17
 
17
18
  ## Session Marker
18
19
 
@@ -26,13 +27,14 @@ When you see a probe marker command like `/ufoo <marker>` (Claude) or `$ufoo <ma
26
27
 
27
28
  **"Only record decisions that matter beyond this session."**
28
29
 
29
- Record a decision for important, plan-level knowledge that other agents or your future self need. The threshold is HIGH — most tasks do NOT need a decision.
30
+ The default is **no new decision**. Record one only for important, plan-level knowledge that other agents or your future self will need.
30
31
 
31
32
  - **Always record**: architectural choices, plan-level decisions with multiple options, cross-agent coordination decisions, trade-off analysis where alternatives were considered and rejected
32
33
  - **Also record**: design patterns that set precedent, integration contracts between systems, decisions that constrain future work
33
- - **Do NOT record**: routine bug fixes, simple implementation details, trivial observations, findings that only matter within the current task
34
- - **Write the decision BEFORE acting on it** if your session dies, the knowledge survives
35
- - **Rule of thumb**: if another agent wouldn't need to know about it, don't write a decision
34
+ - **Do NOT record**: routine bug fixes, simple implementation details, trivial observations, generic planning/evaluation/recommendation requests, or findings that only matter within the current task
35
+ - **Prefer shared memory**: durable project facts and long-lived factual constraints belong in shared memory, not decisions
36
+ - **Write the decision BEFORE acting on it** but only after the high bar above is clearly met
37
+ - **Rule of thumb**: if another agent would not need this as a future constraint, do not write a decision
36
38
 
37
39
  ### Commands
38
40
 
@@ -75,9 +77,51 @@ What must follow from this?
75
77
 
76
78
  **NEVER resolve blindly.** Reading the title is not enough.
77
79
 
80
+ ### Read-First Rule
81
+
82
+ Shared context is **read-first**, not write-only:
83
+
84
+ 1. At session start or before related work, read open decisions first.
85
+ 2. Do not create a new decision just to persist ordinary work state.
86
+ 3. Consume shared memory via prompt prefix / `recall` / `search_memory` before writing new memory.
87
+
88
+ ---
89
+
90
+ ## 2. Shared Memory
91
+
92
+ Shared memory records durable project facts only. It is not a scratchpad, progress log, user-preference store, or replacement for decisions.
93
+
94
+ ### When to Record
95
+
96
+ - **Record**: permanent project invariants, external ownership facts, integration contracts, long-lived process constraints
97
+ - **Do NOT record**: current task status, transient observations, "today/current/recent" facts, agent feedback, routine findings, or anything likely to expire
98
+ - **Read first**: if memory may already exist, use `recall` / `search_memory` before `remember` or `edit_memory`
99
+ - **Prefer edit over duplicate**: update an existing memory when the fact already exists but wording changed
100
+
101
+ ### Commands
102
+
103
+ ```bash
104
+ ufoo memory add "Title" --body "Durable fact body" --tags infra,billing
105
+ ufoo memory list [--tag infra] [--all]
106
+ ufoo memory show mem-0001
107
+ ufoo memory edit mem-0001
108
+ ufoo memory forget mem-0001
109
+ ufoo memory rebuild-index
110
+ ufoo memory audit mem-0001
111
+ ```
112
+
113
+ ### Agent Tools
114
+
115
+ - `remember` — write a new durable memory fact
116
+ - `recall` — read memory by id or tags
117
+ - `search_memory` — search memory before writing or when more context is needed
118
+ - `search_history` — search local Claude/Codex session history as redacted evidence
119
+ - `edit_memory` — directly update any existing memory, with optional `expected_updated_at`
120
+ - `forget` — archive an obsolete or polluted memory entry
121
+
78
122
  ---
79
123
 
80
- ## 2. Event Bus (ubus)
124
+ ## 3. Event Bus (ubus)
81
125
 
82
126
  ### Commands
83
127
 
@@ -122,7 +166,7 @@ Notes:
122
166
 
123
167
  ---
124
168
 
125
- ## 3. Message Format
169
+ ## 4. Message Format
126
170
 
127
171
  Bus messages use a unified prefix format to distinguish sources:
128
172
 
@@ -134,7 +178,7 @@ When you see `[manual]<to:xxx>`, it's a direct user instruction to an agent —
134
178
 
135
179
  ---
136
180
 
137
- ## 4. Team Activity (Input History)
181
+ ## 5. Team Activity (Input History)
138
182
 
139
183
  Your bootstrap prompt may include a `## Team Activity` section showing recent prompts sent to all agents. Use this to understand:
140
184
  - What each agent is currently working on
@@ -150,7 +194,7 @@ ufoo history prompt [limit] # Render as injectable prompt block
150
194
 
151
195
  ---
152
196
 
153
- ## 5. Initialization (uinit)
197
+ ## 6. Initialization (uinit)
154
198
 
155
199
  Trigger: `/uinit` or `/ufoo init`
156
200
 
@@ -29,7 +29,7 @@ Please select modules to enable:
29
29
  ```
30
30
 
31
31
  Options:
32
- - `context` (recommended) - Shared context, decision recording, knowledge persistence
32
+ - `context` (recommended) - Shared context, sparse decision log for major plan-level choices
33
33
  - `bus` (recommended) - Multi-agent communication, task delegation, message passing
34
34
  - `resources` (optional) - UI tone guide, icon library
35
35
 
@@ -61,6 +61,7 @@ fi
61
61
  === ufoo initialization complete ===
62
62
 
63
63
  Enabled modules:
64
+ ✓ core memory → .ufoo/memory/
64
65
  ✓ context → .ufoo/context/
65
66
  ✓ bus → .ufoo/bus/ + .ufoo/agent/
66
67
 
@@ -73,6 +74,6 @@ Next steps:
73
74
 
74
75
  ## Notes
75
76
 
76
- - If .ufoo/context, .ufoo/bus, or .ufoo/agent already exists, skip creation
77
+ - If .ufoo/memory, .ufoo/context, .ufoo/bus, or .ufoo/agent already exists, skip creation
77
78
  - After initialization, reuse existing subscriber ID first, join only as fallback (if bus enabled)
78
79
  - AGENTS.md will have protocol description block injected
@@ -1,7 +1,8 @@
1
1
  <!-- ufoo -->
2
2
  ## ufoo Agent Protocol
3
3
 
4
- > **Record decisions before acting.** Any knowledge with information value → `ufoo ctx decisions new "Title"` BEFORE you act on it.
4
+ > **Default: do not write a decision.** Record one only for important, plan-level knowledge that should constrain future work: architectural choices, trade-off analysis, cross-agent coordination, or precedent-setting integration contracts. NOT for routine findings, simple fixes, or because the user asked for a plan/evaluation/recommendation. Durable project facts belong in shared memory, not decisions. → `ufoo ctx decisions new "Title"` BEFORE acting only when that high bar is met.
5
+ > **Read shared memory before writing it.** Durable facts live in `.ufoo/memory/`; use `ufoo memory list/show`, `recall`, `search_memory`, or redacted `search_history` evidence before `remember` / `edit_memory`.
5
6
  > **Auto-execute bus messages.** On `ubus`: execute tasks immediately, reply to sender, then `ufoo bus ack`. Never ask the user.
6
7
  > **Full protocol**: `/ufoo` skill (auto-loaded on session start). Docs: `.ufoo/docs/`
7
8
  <!-- /ufoo -->
@@ -134,7 +134,7 @@ Examples:
134
134
 
135
135
  | Module | Problem Solved |
136
136
  |--------|----------------|
137
- | context | Shared context, decision recording, knowledge persistence |
137
+ | context | Shared context, sparse decision log for major plan-level choices |
138
138
  | bus | Real-time communication, task delegation, message passing |
139
139
 
140
140
  Both are independent peer modules that can be used separately or together.
@@ -13,13 +13,15 @@ description: |
13
13
  Fast context check for daily use. Run at session start or anytime.
14
14
 
15
15
  Pre-flight reminder:
16
- - If the user is asking for an important architectural decision, a plan with multiple options, or a cross-agent coordination choice, write a decision before replying.
17
- Do NOT write decisions for routine tasks, simple bug fixes, or trivial findings.
16
+ - Default is no new decision.
17
+ - Write a decision only for important architectural choices, trade-off outcomes, cross-agent coordination, or precedent-setting integration contracts.
18
+ - Do NOT write decisions for routine tasks, simple bug fixes, trivial findings, or generic plan/evaluation/recommendation requests.
19
+ - Durable project facts belong in shared memory, not decisions.
18
20
  Use: `ufoo ctx decisions new "<Title>"`
19
21
 
20
22
  ## Decision format (canonical)
21
23
 
22
- Project context is decision-only. Decisions live at:
24
+ The context module tracks decisions only. Durable shared facts belong in shared memory. Decisions live at:
23
25
  `<project>/.ufoo/context/decisions/`
24
26
 
25
27
  Decision index (JSONL):
@@ -36,7 +38,7 @@ Each JSONL row includes:
36
38
  - `file` (decision filename)
37
39
  - `author` (decision author or resolver)
38
40
 
39
- Create a new decision (recommended before replying when required):
41
+ Create a new decision (only when the high-threshold rule above requires it):
40
42
  ```bash
41
43
  ufoo ctx decisions new "Short Title"
42
44
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.2.4",
3
+ "version": "2.3.1",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -12,6 +12,7 @@ const { writeActivityState } = require("./activityStateWriter");
12
12
  * @param {string} options.subscriber - Subscriber ID (e.g. "claude-code:abc123")
13
13
  * @param {string} options.projectRoot - Project root (unused, kept for API compat)
14
14
  * @param {boolean} [options.force=true] - Force overwrite priority-protected states
15
+ * publish(state, extra, { force }) can override this default for one transition.
15
16
  */
16
17
  function createActivityStatePublisher(options = {}) {
17
18
  const {
@@ -22,10 +23,13 @@ function createActivityStatePublisher(options = {}) {
22
23
 
23
24
  let lastState = "";
24
25
 
25
- function publish(state, extra = {}) {
26
+ function publish(state, extra = {}, publishOptions = {}) {
26
27
  if (state === lastState) return false;
27
28
  const since = extra.since || undefined;
28
- const changed = writeActivityState(agentsFile, subscriber, state, { since, force });
29
+ const effectiveForce = typeof publishOptions.force === "boolean"
30
+ ? publishOptions.force
31
+ : force;
32
+ const changed = writeActivityState(agentsFile, subscriber, state, { since, force: effectiveForce });
29
33
  if (!changed) return false;
30
34
  lastState = state;
31
35
  // Write to bus events directory for daemon bridge to pick up.
@@ -61,7 +61,7 @@ async function* defaultCodexTransportStreamFactory({
61
61
  });
62
62
  if (!result.ok) {
63
63
  const err = new Error(result.error || "Codex upstream request failed");
64
- err.code = "CODEX_UPSTREAM_FAILED";
64
+ err.code = result.errorCode || "CODEX_UPSTREAM_FAILED";
65
65
  throw err;
66
66
  }
67
67
 
@@ -145,7 +145,7 @@ class CodexThreadProvider {
145
145
  this.cwd = cwd;
146
146
  this.extraArgs = Array.isArray(extraArgs) ? extraArgs.slice() : [];
147
147
  this.streamFactory = streamFactory;
148
- this.sdk = sdk || resolveCodexSdk();
148
+ this.sdk = sdk || (streamFactory === defaultCodexStreamFactory ? resolveCodexSdk() : null);
149
149
  this.tools = Array.isArray(tools) ? tools.slice() : [];
150
150
  }
151
151
 
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const EventBus = require("../bus");
4
+ const { getToolDefinition, CALLER_TIERS } = require("../tools");
4
5
  const { dispatchMessageHandler } = require("../tools/handlers/dispatchMessage");
5
6
  const { ackBusHandler } = require("../tools/handlers/ackBus");
6
7
 
@@ -145,6 +146,25 @@ async function handleLaunchAgent(ctx, args) {
145
146
  };
146
147
  }
147
148
 
149
+ async function handleSharedRegistryTool(ctx, name, args, audit = {}) {
150
+ const definition = getToolDefinition(name);
151
+ if (!definition || typeof definition.handler !== "function") {
152
+ throw buildStructuredError("unsupported_tool", `unsupported controller tool: ${name}`);
153
+ }
154
+ if (!definition.allowed_tiers.includes(CALLER_TIERS.CONTROLLER)) {
155
+ throw buildStructuredError("forbidden_caller_tier", `controller is not allowed to invoke tool: ${name}`);
156
+ }
157
+ const eventBus = ctx.eventBus || new EventBus(ctx.projectRoot);
158
+ return definition.handler({
159
+ projectRoot: ctx.projectRoot,
160
+ subscriber: ctx.subscriber || "ufoo-agent",
161
+ caller_tier: CALLER_TIERS.CONTROLLER,
162
+ eventBus,
163
+ turn_id: audit.turn_id || "",
164
+ tool_call_id: audit.tool_call_id || "",
165
+ }, args);
166
+ }
167
+
148
168
  async function executeControllerTool(ctx, toolCall = {}) {
149
169
  const observer = ctx.observer || { emit: () => {}, audit: () => {} };
150
170
  const name = String(toolCall.name || "").trim();
@@ -169,7 +189,10 @@ async function executeControllerTool(ctx, toolCall = {}) {
169
189
  } else if (name === "launch_agent") {
170
190
  result = await handleLaunchAgent(ctx, args);
171
191
  } else {
172
- throw buildStructuredError("unsupported_tool", `unsupported controller tool: ${name}`);
192
+ result = await handleSharedRegistryTool(ctx, name, args, {
193
+ turn_id: turnId,
194
+ tool_call_id: toolCallId,
195
+ });
173
196
  }
174
197
 
175
198
  observer.emit("tool_call_finished", {
@@ -25,6 +25,7 @@ function resolveClaudeOauthPaths(options = {}) {
25
25
  const configDir = String(options.configDir || defaultClaudeConfigDir()).trim() || defaultClaudeConfigDir();
26
26
  const profile = String(options.profile || "").trim();
27
27
  const explicitTokenPath = String(options.tokenPath || "").trim();
28
+ const explicitSettingsPath = String(options.settingsPath || "").trim();
28
29
  const profileDir = profile ? path.join(configDir, "profiles", profile) : configDir;
29
30
  const tokenPath = explicitTokenPath || path.join(profileDir, "oauth.json");
30
31
  return {
@@ -33,9 +34,17 @@ function resolveClaudeOauthPaths(options = {}) {
33
34
  profileDir,
34
35
  tokenPath,
35
36
  lockPath: `${tokenPath}.lock`,
37
+ settingsPath: explicitSettingsPath || path.join(configDir, "settings.json"),
36
38
  };
37
39
  }
38
40
 
41
+ function firstString(...values) {
42
+ for (const value of values) {
43
+ if (typeof value === "string" && value.trim()) return value.trim();
44
+ }
45
+ return "";
46
+ }
47
+
39
48
  function classifyTokenState(expiresAtMs, nowMs, refreshWindowMs) {
40
49
  if (!Number.isFinite(expiresAtMs)) return "invalid";
41
50
  if (expiresAtMs <= nowMs) return "expired";
@@ -208,6 +217,61 @@ class ClaudeUpstreamCredentialResolver {
208
217
  };
209
218
  }
210
219
 
220
+ readSettingsEnv() {
221
+ let raw;
222
+ try {
223
+ raw = JSON.parse(this.fs.readFileSync(this.paths.settingsPath, "utf8"));
224
+ } catch {
225
+ return {};
226
+ }
227
+ const env = raw && raw.env && typeof raw.env === "object" && !Array.isArray(raw.env)
228
+ ? raw.env
229
+ : {};
230
+ const apiKeySource = firstString(env.ANTHROPIC_API_KEY)
231
+ ? "settings:ANTHROPIC_API_KEY"
232
+ : (firstString(env.CLAUDE_API_KEY) ? "settings:CLAUDE_API_KEY" : "");
233
+ return {
234
+ apiKey: firstString(env.ANTHROPIC_API_KEY, env.CLAUDE_API_KEY),
235
+ apiKeySource,
236
+ authToken: firstString(env.ANTHROPIC_AUTH_TOKEN),
237
+ };
238
+ }
239
+
240
+ buildApiKeyCredential(apiKey, source, credentialPath = "") {
241
+ return buildCredentialDescriptor({
242
+ provider: "claude",
243
+ credentialKind: "api-key",
244
+ source,
245
+ apiKey,
246
+ tokenType: "Bearer",
247
+ state: "fresh",
248
+ refreshable: false,
249
+ profile: this.paths.profile,
250
+ credentialPath,
251
+ nowMs: this.now(),
252
+ refreshWindowMs: this.refreshWindowMs,
253
+ });
254
+ }
255
+
256
+ buildAuthTokenCredential(accessToken, source, credentialPath = "") {
257
+ return buildCredentialDescriptor({
258
+ provider: "claude",
259
+ credentialKind: "oauth",
260
+ source,
261
+ accessToken,
262
+ tokenType: "Bearer",
263
+ state: "fresh",
264
+ refreshable: false,
265
+ profile: this.paths.profile,
266
+ credentialPath,
267
+ nowMs: this.now(),
268
+ refreshWindowMs: this.refreshWindowMs,
269
+ metadata: {
270
+ tokenPath: credentialPath,
271
+ },
272
+ });
273
+ }
274
+
211
275
  buildResolvedCredential(tokenRecord) {
212
276
  return buildCredentialDescriptor({
213
277
  provider: "claude",
@@ -269,21 +333,26 @@ class ClaudeUpstreamCredentialResolver {
269
333
  }
270
334
 
271
335
  async resolveCredentials() {
272
- const apiKey = typeof this.env.ANTHROPIC_API_KEY === "string" ? this.env.ANTHROPIC_API_KEY.trim() : "";
273
- if (apiKey) {
274
- return buildCredentialDescriptor({
275
- provider: "claude",
276
- credentialKind: "api-key",
277
- source: "api-key",
278
- apiKey,
279
- tokenType: "Bearer",
280
- state: "fresh",
281
- refreshable: false,
282
- profile: this.paths.profile,
283
- credentialPath: "",
284
- nowMs: this.now(),
285
- refreshWindowMs: this.refreshWindowMs,
286
- });
336
+ const anthropicApiKey = firstString(this.env.ANTHROPIC_API_KEY);
337
+ const claudeApiKey = firstString(this.env.CLAUDE_API_KEY);
338
+ if (anthropicApiKey || claudeApiKey) {
339
+ return this.buildApiKeyCredential(
340
+ anthropicApiKey || claudeApiKey,
341
+ anthropicApiKey ? "env:ANTHROPIC_API_KEY" : "env:CLAUDE_API_KEY",
342
+ );
343
+ }
344
+
345
+ const authToken = firstString(this.env.ANTHROPIC_AUTH_TOKEN);
346
+ if (authToken) {
347
+ return this.buildAuthTokenCredential(authToken, "env:ANTHROPIC_AUTH_TOKEN");
348
+ }
349
+
350
+ const settingsEnv = this.readSettingsEnv();
351
+ if (settingsEnv.apiKey) {
352
+ return this.buildApiKeyCredential(settingsEnv.apiKey, settingsEnv.apiKeySource || "settings:ANTHROPIC_API_KEY", this.paths.settingsPath);
353
+ }
354
+ if (settingsEnv.authToken) {
355
+ return this.buildAuthTokenCredential(settingsEnv.authToken, "settings:ANTHROPIC_AUTH_TOKEN", this.paths.settingsPath);
287
356
  }
288
357
 
289
358
  let tokenRecord;
@@ -291,7 +360,7 @@ class ClaudeUpstreamCredentialResolver {
291
360
  tokenRecord = this.readTokenFile();
292
361
  } catch (err) {
293
362
  if (err && err.code === "ENOENT") {
294
- const missing = new Error("Claude OAuth token not found and ANTHROPIC_API_KEY is unset");
363
+ const missing = new Error("Claude credentials not found in environment, Claude settings, or OAuth token file");
295
364
  missing.code = "CLAUDE_AUTH_UNAVAILABLE";
296
365
  throw missing;
297
366
  }