@cmetech/otto 1.2.4 → 1.2.5

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 (33) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/coworker-memory/index.js +1 -1
  3. package/dist/resources/extensions/coworker-scratchpad/index.js +1 -1
  4. package/dist/resources/extensions/coworker-vault/index.js +1 -1
  5. package/dist/resources/extensions/otto/commands/release-notes/_data.js +14 -2
  6. package/dist/resources/extensions/otto/index.js +31 -6
  7. package/dist/resources/extensions/slash-commands/{audit.js → audit-codebase.js} +4 -4
  8. package/dist/resources/extensions/slash-commands/extension-manifest.json +1 -1
  9. package/dist/resources/extensions/slash-commands/index.js +2 -2
  10. package/dist/resources/extensions/workflow/health-widget-core.js +1 -1
  11. package/package.json +7 -6
  12. package/packages/contracts/package.json +1 -1
  13. package/packages/daemon/package.json +3 -3
  14. package/packages/mcp-server/package.json +3 -3
  15. package/packages/native/package.json +1 -1
  16. package/packages/pi-agent-core/package.json +1 -1
  17. package/packages/pi-ai/package.json +1 -1
  18. package/packages/pi-coding-agent/package.json +2 -2
  19. package/packages/pi-tui/package.json +1 -1
  20. package/packages/rpc-client/package.json +2 -2
  21. package/pkg/package.json +1 -1
  22. package/src/resources/extensions/coworker-memory/index.ts +1 -1
  23. package/src/resources/extensions/coworker-scratchpad/index.ts +1 -1
  24. package/src/resources/extensions/coworker-vault/index.ts +1 -1
  25. package/src/resources/extensions/otto/commands/release-notes/_data.ts +14 -2
  26. package/src/resources/extensions/otto/index.ts +29 -6
  27. package/src/resources/extensions/{_coworker-paths.test.ts → shared/coworker-paths.test.ts} +2 -2
  28. package/src/resources/extensions/slash-commands/{audit.ts → audit-codebase.ts} +4 -4
  29. package/src/resources/extensions/slash-commands/extension-manifest.json +1 -1
  30. package/src/resources/extensions/slash-commands/index.ts +2 -2
  31. package/src/resources/extensions/workflow/health-widget-core.ts +1 -1
  32. /package/dist/resources/extensions/{_coworker-paths.js → shared/coworker-paths.js} +0 -0
  33. /package/src/resources/extensions/{_coworker-paths.ts → shared/coworker-paths.ts} +0 -0
@@ -1 +1 @@
1
- fe239844559e7ca1
1
+ 509dc2f42084b696
@@ -6,7 +6,7 @@ import { runRecall } from './recall-tool.js';
6
6
  import { runMemoryCommand } from './memory-command.js';
7
7
  import { onSessionShutdown } from './session-hooks.js';
8
8
  import { createCurrentScratchpadProvider } from '../coworker-scratchpad/sp-command.js';
9
- import { getCoworkerGlobalDir, getScratchpadsRoot } from '../_coworker-paths.js';
9
+ import { getCoworkerGlobalDir, getScratchpadsRoot } from '../shared/coworker-paths.js';
10
10
  export { createMemoryBundle };
11
11
  // Cross-pillar export. Scratchpad's onDataLoad closure imports this and calls
12
12
  // it lazily — returns null before session_start or after session_shutdown,
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join, basename } from 'node:path';
3
3
  import { ScratchpadManager } from '@otto/coworker-scratchpad';
4
- import { getScratchpadsRoot } from '../_coworker-paths.js';
4
+ import { getScratchpadsRoot } from '../shared/coworker-paths.js';
5
5
  import { getMemoryRecorder } from '../coworker-memory/index.js';
6
6
  import { getArtifactStore } from '../coworker-artifacts/index.js';
7
7
  import { registerSpCommand } from './sp-command.js';
@@ -2,7 +2,7 @@ import { createVaultBundle } from './vault-singleton.js';
2
2
  import { runConnect } from './connect-command.js';
3
3
  import { runDatasourceList, runDatasourceRemove, runDatasourceTest, } from './datasource-command.js';
4
4
  import { runAudit } from './audit-command.js';
5
- import { getCoworkerGlobalDir } from '../_coworker-paths.js';
5
+ import { getCoworkerGlobalDir } from '../shared/coworker-paths.js';
6
6
  export { createVaultBundle };
7
7
  export default function coworkerVaultExtension(api) {
8
8
  let bundle = null;
@@ -9,12 +9,24 @@
9
9
  // runtime command surfaces a link when a requested version isn't bundled.
10
10
  export const RELEASE_NOTES_MANIFEST = {
11
11
  truncated: false,
12
- total: 13,
12
+ total: 14,
13
13
  oldestBundled: '1.0.0',
14
- newestBundled: '1.2.4',
14
+ newestBundled: '1.2.5',
15
15
  historyUrl: 'https://github.com/cmetech/otto-cli/blob/main/CHANGELOG.md',
16
16
  };
17
17
  export const RELEASE_NOTES = [
18
+ {
19
+ version: '1.2.5',
20
+ date: '2026-06-03',
21
+ headline: 'Windows-install hotfix: silences three startup errors that fired the first time `otto` launched after a global npm install, and stops the otto extension from racing the TUI welcome banner with raw stderr writes.',
22
+ fixed: [
23
+ '**`_coworker-paths.js` "Extension does not export a valid factory function".** The shared helper module sat at the top of `src/resources/extensions/` so the pi-coding-agent extension discovery (`discoverExtensionsInDir`) walked it as an extension candidate. Moved to `src/resources/extensions/shared/coworker-paths.{ts,test.ts}` (subdir is skipped by `resolveExtensionEntries` because it has no `index.ts`/`package.json`). The three coworker-{memory,scratchpad,vault}/index.ts importers were updated in lockstep.',
24
+ '**`Cannot find module \'better-sqlite3\'` on global install.** `better-sqlite3` was declared only in `packages/coworker-memory/package.json`. Because npm only resolves the root manifest for end users of a published tarball, the native module was never installed and `@otto/coworker-memory`\'s eager re-export of `local-sqlite-backend.js` blew up at first import. Hoisted `better-sqlite3@^11.7.0` into root `dependencies`.',
25
+ '**`/audit` slash-command conflict between coworker-vault and slash-commands.** Both extensions registered the same id; the vault command (the real Phase 2 reader) supersedes, but the conflict warning fired every launch. Renamed the slash-commands command to `/audit-codebase` (file + manifest + dispatch updated).',
26
+ '**Otto session_start bled raw stderr into the TUI welcome banner.** The gateway + langflow probes wrote ANSI-colored status lines directly to `process.stderr` while the pi TUI was still painting; on Windows the rendered output collided with the yellow `─` rule and overwrote the prompt area. Now routes status through `ctx.ui.setStatus("otto-gateway"|"otto-langflow", ...)` when `ctx.hasUI` is true; falls back to stderr only in headless/RPC modes.',
27
+ '**Duplicate "OTTO" brand on the no-project landing.** The `gsd-health` widget\'s no-project line began with `" OTTO No project loaded — …"`, which appeared alongside the ASCII OTTO logo in the welcome header — looking like the brand was loading twice. Dropped the `OTTO ` prefix; the header already brands the session.',
28
+ ],
29
+ },
18
30
  {
19
31
  version: '1.2.4',
20
32
  date: '2026-06-02',
@@ -108,6 +108,10 @@ export default function Otto(pi) {
108
108
  const green = ANSI_BRAND_GREEN;
109
109
  const dim = ANSI_DIM;
110
110
  const reset = ANSI_RESET;
111
+ // When a TUI is rendering, raw stderr writes race with the welcome banner
112
+ // and overwrite the prompt area. Route status through `ctx.ui.setStatus`
113
+ // instead, and only fall back to stderr for headless/RPC modes.
114
+ const hasUI = ctx?.hasUI === true;
111
115
  // ── Gateway connection probe (preserved from Phase 1 Task 6) ──
112
116
  const gwUrl = process.env.OTTO_GATEWAY_URL?.trim();
113
117
  if (gwUrl) {
@@ -118,15 +122,30 @@ export default function Otto(pi) {
118
122
  clearTimeout(timer);
119
123
  const ok = r.ok;
120
124
  const host = new URL(gwUrl).host;
121
- process.stderr.write(` ${yellow}gateway:${reset} ${ok ? green : dim}routed → ${host}${reset}\n`);
125
+ if (hasUI) {
126
+ ctx?.ui.setStatus("otto-gateway", ok ? `Gateway → ${host}` : `Gateway → ${host} (down)`);
127
+ }
128
+ else {
129
+ process.stderr.write(` ${yellow}gateway:${reset} ${ok ? green : dim}routed → ${host}${reset}\n`);
130
+ }
122
131
  }
123
132
  catch {
124
133
  const host = new URL(gwUrl).host;
125
- process.stderr.write(` ${yellow}gateway:${reset} ${dim}routed → ${host} (unreachable)${reset}\n`);
134
+ if (hasUI) {
135
+ ctx?.ui.setStatus("otto-gateway", `Gateway → ${host} (unreachable)`);
136
+ }
137
+ else {
138
+ process.stderr.write(` ${yellow}gateway:${reset} ${dim}routed → ${host} (unreachable)${reset}\n`);
139
+ }
126
140
  }
127
141
  }
128
142
  else {
129
- process.stderr.write(` ${yellow}gateway:${reset} ${dim}direct (no OTTO_GATEWAY_URL set)${reset}\n`);
143
+ if (hasUI) {
144
+ ctx?.ui.setStatus("otto-gateway", undefined);
145
+ }
146
+ else {
147
+ process.stderr.write(` ${yellow}gateway:${reset} ${dim}direct (no OTTO_GATEWAY_URL set)${reset}\n`);
148
+ }
130
149
  }
131
150
  // ── LangFlow connection probe ──
132
151
  const cfg = effectiveLangFlowConfig();
@@ -135,18 +154,24 @@ export default function Otto(pi) {
135
154
  const lfHost = new URL(lfUrl).host;
136
155
  if (langflowDisabled) {
137
156
  ctx?.ui.setStatus("otto-langflow", undefined);
138
- process.stderr.write(` ${yellow}langflow:${reset} ${dim}disabled (${lfHost})${reset}\n`);
157
+ if (!hasUI) {
158
+ process.stderr.write(` ${yellow}langflow:${reset} ${dim}disabled (${lfHost})${reset}\n`);
159
+ }
139
160
  }
140
161
  else {
141
162
  const lfClient = getLangFlowClient();
142
163
  const lfVersion = await lfClient.getVersion();
143
164
  if (lfVersion) {
144
165
  ctx?.ui.setStatus("otto-langflow", `LangFlow ok v${lfVersion.version}`);
145
- process.stderr.write(` ${yellow}langflow:${reset} ${green}connected${reset} ${dim}(v${lfVersion.version} @ ${lfHost})${reset}\n`);
166
+ if (!hasUI) {
167
+ process.stderr.write(` ${yellow}langflow:${reset} ${green}connected${reset} ${dim}(v${lfVersion.version} @ ${lfHost})${reset}\n`);
168
+ }
146
169
  }
147
170
  else {
148
171
  ctx?.ui.setStatus("otto-langflow", "LangFlow offline");
149
- process.stderr.write(` ${yellow}langflow:${reset} ${dim}offline (${lfHost})${reset}\n`);
172
+ if (!hasUI) {
173
+ process.stderr.write(` ${yellow}langflow:${reset} ${dim}offline (${lfHost})${reset}\n`);
174
+ }
150
175
  }
151
176
  }
152
177
  });
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync } from "node:fs";
2
- export default function auditCommand(pi) {
3
- pi.registerCommand("audit", {
2
+ export default function auditCodebaseCommand(pi) {
3
+ pi.registerCommand("audit-codebase", {
4
4
  description: "Audit the current codebase against a specific goal and write a structured report to .gsd/audits/",
5
5
  async handler(args, ctx) {
6
6
  // ── Step 1: Get the audit goal ────────────────────────────────────────
@@ -8,7 +8,7 @@ export default function auditCommand(pi) {
8
8
  if (!goal) {
9
9
  const input = await ctx.ui.input("What is the audit goal?", "e.g. understand performance bottlenecks before planning a roadmap");
10
10
  if (!input?.trim()) {
11
- ctx.ui.notify("audit: No goal provided — cancelled.", "error");
11
+ ctx.ui.notify("audit-codebase: No goal provided — cancelled.", "error");
12
12
  return;
13
13
  }
14
14
  goal = input.trim();
@@ -62,7 +62,7 @@ ${goal}
62
62
 
63
63
  ---
64
64
 
65
- *Generated by /audit — read-only recce, no code was modified.*
65
+ *Generated by /audit-codebase — read-only recce, no code was modified.*
66
66
  \`\`\`
67
67
 
68
68
  After writing the file, confirm with: "✅ Audit complete — report saved to \`${outputPath}\`"`;
@@ -6,6 +6,6 @@
6
6
  "tier": "bundled",
7
7
  "requires": { "platform": ">=2.29.0" },
8
8
  "provides": {
9
- "commands": ["create-slash-command", "create-extension", "audit", "clear"]
9
+ "commands": ["create-slash-command", "create-extension", "audit-codebase", "clear"]
10
10
  }
11
11
  }
@@ -1,10 +1,10 @@
1
1
  import createSlashCommand from "./create-slash-command.js";
2
2
  import createExtension from "./create-extension.js";
3
- import auditCommand from "./audit.js";
3
+ import auditCodebaseCommand from "./audit-codebase.js";
4
4
  import clearCommand from "./clear.js";
5
5
  export default function slashCommands(pi) {
6
6
  createSlashCommand(pi);
7
7
  createExtension(pi);
8
- auditCommand(pi);
8
+ auditCodebaseCommand(pi);
9
9
  clearCommand(pi);
10
10
  }
@@ -50,7 +50,7 @@ function truncateMessage(msg, maxLen) {
50
50
  */
51
51
  export function buildHealthLines(data, width) {
52
52
  if (data.projectState === "none") {
53
- return [" OTTO No project loaded — cd into a project, then run /otto init"];
53
+ return [" No project loaded — cd into a project, then run /otto init"];
54
54
  }
55
55
  if (data.projectState === "initialized") {
56
56
  return [` ${BRAND} Project Initialized`];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmetech/otto",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Terminal-based developer chat assistant. Permanent hard fork of gsd-pi with LangFlow flow triggers, a flow builder, and optional gateway routing for compliance environments.",
5
5
  "keywords": [
6
6
  "ai",
@@ -156,6 +156,7 @@
156
156
  "ajv": "^8.17.1",
157
157
  "ajv-formats": "^3.0.1",
158
158
  "axios": "^1.16.1",
159
+ "better-sqlite3": "^11.7.0",
159
160
  "chalk": "^5.6.2",
160
161
  "chokidar": "^5.0.0",
161
162
  "date-fns": "^4.4.0",
@@ -196,11 +197,11 @@
196
197
  },
197
198
  "optionalDependencies": {
198
199
  "@anthropic-ai/claude-agent-sdk": "0.2.83",
199
- "@cmetech/otto-engine-darwin-arm64": "1.2.4",
200
- "@cmetech/otto-engine-darwin-x64": "1.2.4",
201
- "@cmetech/otto-engine-linux-arm64-gnu": "1.2.4",
202
- "@cmetech/otto-engine-linux-x64-gnu": "1.2.4",
203
- "@cmetech/otto-engine-win32-x64-msvc": "1.2.4",
200
+ "@cmetech/otto-engine-darwin-arm64": "1.2.5",
201
+ "@cmetech/otto-engine-darwin-x64": "1.2.5",
202
+ "@cmetech/otto-engine-linux-arm64-gnu": "1.2.5",
203
+ "@cmetech/otto-engine-linux-x64-gnu": "1.2.5",
204
+ "@cmetech/otto-engine-win32-x64-msvc": "1.2.5",
204
205
  "fsevents": "~2.3.3",
205
206
  "koffi": "^2.9.0"
206
207
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-build/contracts",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Shared public contracts for OTTO workspace boundaries",
5
5
  "license": "MIT",
6
6
  "otto": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-build/daemon",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "OTTO daemon — background process for project monitoring and Discord integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,8 +29,8 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@anthropic-ai/sdk": "^0.52.0",
32
- "@otto-build/contracts": "^1.2.0",
33
- "@otto-build/rpc-client": "^1.2.0",
32
+ "@otto-build/contracts": "^1.2.5",
33
+ "@otto-build/rpc-client": "^1.2.5",
34
34
  "discord.js": "^14.25.1",
35
35
  "yaml": "^2.8.0",
36
36
  "zod": "^3.24.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-build/mcp-server",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "MCP server exposing OTTO orchestration tools for compatible clients",
5
5
  "license": "MIT",
6
6
  "otto": {
@@ -34,8 +34,8 @@
34
34
  "test": "npm run build:test && node --test dist/mcp-server.test.js dist/remote-questions.test.js"
35
35
  },
36
36
  "dependencies": {
37
- "@otto-build/contracts": "^1.2.0",
38
- "@otto-build/rpc-client": "^1.2.0",
37
+ "@otto-build/contracts": "^1.2.5",
38
+ "@otto-build/rpc-client": "^1.2.5",
39
39
  "@modelcontextprotocol/sdk": "^1.27.1",
40
40
  "zod": "^4.0.0"
41
41
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto/native",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Native Rust bindings for OTTO — high-performance native modules via N-API",
5
5
  "type": "commonjs",
6
6
  "otto": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto/pi-agent-core",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "General-purpose agent core (vendored from pi-mono)",
5
5
  "type": "module",
6
6
  "otto": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto/pi-ai",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Unified LLM API (vendored from pi-mono)",
5
5
  "type": "module",
6
6
  "otto": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto/pi-coding-agent",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Coding agent CLI (vendored from pi-mono)",
5
5
  "type": "module",
6
6
  "otto": {
@@ -28,7 +28,7 @@
28
28
  "copy-assets": "node scripts/copy-assets.cjs"
29
29
  },
30
30
  "dependencies": {
31
- "@otto-build/contracts": "^1.2.0",
31
+ "@otto-build/contracts": "^1.2.5",
32
32
  "@mariozechner/jiti": "^2.6.2",
33
33
  "@silvia-odwyer/photon-node": "^0.3.4",
34
34
  "chalk": "^5.5.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto/pi-tui",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Terminal User Interface library (vendored from pi-mono)",
5
5
  "type": "module",
6
6
  "otto": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-build/rpc-client",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Standalone RPC client SDK for OTTO — zero internal dependencies",
5
5
  "license": "MIT",
6
6
  "otto": {
@@ -34,7 +34,7 @@
34
34
  "test": "node --test dist/rpc-client.test.js"
35
35
  },
36
36
  "dependencies": {
37
- "@otto-build/contracts": "^1.2.0"
37
+ "@otto-build/contracts": "^1.2.5"
38
38
  },
39
39
  "engines": {
40
40
  "node": ">=22.0.0"
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loop24/client",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "piConfig": {
5
5
  "_comment": "AUTO-SYNCED from root package.json by scripts/sync-piconfig.mjs (runs on prebuild). Do not edit this block directly — edit root package.json and re-run `npm run build` or `npm run sync-piconfig`.",
6
6
  "name": "otto",
@@ -22,7 +22,7 @@ import { runRecall, type RecallToolArgs } from './recall-tool.js';
22
22
  import { runMemoryCommand } from './memory-command.js';
23
23
  import { onSessionShutdown } from './session-hooks.js';
24
24
  import { createCurrentScratchpadProvider } from '../coworker-scratchpad/sp-command.js';
25
- import { getCoworkerGlobalDir, getScratchpadsRoot } from '../_coworker-paths.js';
25
+ import { getCoworkerGlobalDir, getScratchpadsRoot } from '../shared/coworker-paths.js';
26
26
 
27
27
  export { createMemoryBundle };
28
28
  export type { MemoryBundle, MemoryBundleOptions };
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join, basename } from 'node:path';
3
3
  import type { ExtensionAPI, ExtensionContext } from '@otto/pi-coding-agent';
4
4
  import { ScratchpadManager, type DataLoadDrawer, type ArtifactCreateDrawer } from '@otto/coworker-scratchpad';
5
- import { getScratchpadsRoot } from '../_coworker-paths.js';
5
+ import { getScratchpadsRoot } from '../shared/coworker-paths.js';
6
6
  import { getMemoryRecorder } from '../coworker-memory/index.js';
7
7
  import { getArtifactStore } from '../coworker-artifacts/index.js';
8
8
  import { registerSpCommand } from './sp-command.js';
@@ -14,7 +14,7 @@ import {
14
14
  runDatasourceTest,
15
15
  } from './datasource-command.js';
16
16
  import { runAudit, type AuditQuery } from './audit-command.js';
17
- import { getCoworkerGlobalDir } from '../_coworker-paths.js';
17
+ import { getCoworkerGlobalDir } from '../shared/coworker-paths.js';
18
18
 
19
19
  export { createVaultBundle };
20
20
  export type { VaultBundle, VaultBundleOptions };
@@ -33,13 +33,25 @@ export interface ReleaseNotesManifest {
33
33
 
34
34
  export const RELEASE_NOTES_MANIFEST: ReleaseNotesManifest = {
35
35
  truncated: false,
36
- total: 13,
36
+ total: 14,
37
37
  oldestBundled: '1.0.0',
38
- newestBundled: '1.2.4',
38
+ newestBundled: '1.2.5',
39
39
  historyUrl: 'https://github.com/cmetech/otto-cli/blob/main/CHANGELOG.md',
40
40
  };
41
41
 
42
42
  export const RELEASE_NOTES: ReleaseNote[] = [
43
+ {
44
+ version: '1.2.5',
45
+ date: '2026-06-03',
46
+ headline: 'Windows-install hotfix: silences three startup errors that fired the first time `otto` launched after a global npm install, and stops the otto extension from racing the TUI welcome banner with raw stderr writes.',
47
+ fixed: [
48
+ '**`_coworker-paths.js` "Extension does not export a valid factory function".** The shared helper module sat at the top of `src/resources/extensions/` so the pi-coding-agent extension discovery (`discoverExtensionsInDir`) walked it as an extension candidate. Moved to `src/resources/extensions/shared/coworker-paths.{ts,test.ts}` (subdir is skipped by `resolveExtensionEntries` because it has no `index.ts`/`package.json`). The three coworker-{memory,scratchpad,vault}/index.ts importers were updated in lockstep.',
49
+ '**`Cannot find module \'better-sqlite3\'` on global install.** `better-sqlite3` was declared only in `packages/coworker-memory/package.json`. Because npm only resolves the root manifest for end users of a published tarball, the native module was never installed and `@otto/coworker-memory`\'s eager re-export of `local-sqlite-backend.js` blew up at first import. Hoisted `better-sqlite3@^11.7.0` into root `dependencies`.',
50
+ '**`/audit` slash-command conflict between coworker-vault and slash-commands.** Both extensions registered the same id; the vault command (the real Phase 2 reader) supersedes, but the conflict warning fired every launch. Renamed the slash-commands command to `/audit-codebase` (file + manifest + dispatch updated).',
51
+ '**Otto session_start bled raw stderr into the TUI welcome banner.** The gateway + langflow probes wrote ANSI-colored status lines directly to `process.stderr` while the pi TUI was still painting; on Windows the rendered output collided with the yellow `─` rule and overwrote the prompt area. Now routes status through `ctx.ui.setStatus("otto-gateway"|"otto-langflow", ...)` when `ctx.hasUI` is true; falls back to stderr only in headless/RPC modes.',
52
+ '**Duplicate "OTTO" brand on the no-project landing.** The `gsd-health` widget\'s no-project line began with `" OTTO No project loaded — …"`, which appeared alongside the ASCII OTTO logo in the welcome header — looking like the brand was loading twice. Dropped the `OTTO ` prefix; the header already brands the session.',
53
+ ],
54
+ },
43
55
  {
44
56
  version: '1.2.4',
45
57
  date: '2026-06-02',
@@ -112,6 +112,11 @@ export default function Otto(pi: ExtensionAPI): void {
112
112
  const dim = ANSI_DIM;
113
113
  const reset = ANSI_RESET;
114
114
 
115
+ // When a TUI is rendering, raw stderr writes race with the welcome banner
116
+ // and overwrite the prompt area. Route status through `ctx.ui.setStatus`
117
+ // instead, and only fall back to stderr for headless/RPC modes.
118
+ const hasUI = ctx?.hasUI === true;
119
+
115
120
  // ── Gateway connection probe (preserved from Phase 1 Task 6) ──
116
121
  const gwUrl = process.env.OTTO_GATEWAY_URL?.trim();
117
122
  if (gwUrl) {
@@ -122,13 +127,25 @@ export default function Otto(pi: ExtensionAPI): void {
122
127
  clearTimeout(timer);
123
128
  const ok = r.ok;
124
129
  const host = new URL(gwUrl).host;
125
- process.stderr.write(` ${yellow}gateway:${reset} ${ok ? green : dim}routed → ${host}${reset}\n`);
130
+ if (hasUI) {
131
+ ctx?.ui.setStatus("otto-gateway", ok ? `Gateway → ${host}` : `Gateway → ${host} (down)`);
132
+ } else {
133
+ process.stderr.write(` ${yellow}gateway:${reset} ${ok ? green : dim}routed → ${host}${reset}\n`);
134
+ }
126
135
  } catch {
127
136
  const host = new URL(gwUrl).host;
128
- process.stderr.write(` ${yellow}gateway:${reset} ${dim}routed → ${host} (unreachable)${reset}\n`);
137
+ if (hasUI) {
138
+ ctx?.ui.setStatus("otto-gateway", `Gateway → ${host} (unreachable)`);
139
+ } else {
140
+ process.stderr.write(` ${yellow}gateway:${reset} ${dim}routed → ${host} (unreachable)${reset}\n`);
141
+ }
129
142
  }
130
143
  } else {
131
- process.stderr.write(` ${yellow}gateway:${reset} ${dim}direct (no OTTO_GATEWAY_URL set)${reset}\n`);
144
+ if (hasUI) {
145
+ ctx?.ui.setStatus("otto-gateway", undefined);
146
+ } else {
147
+ process.stderr.write(` ${yellow}gateway:${reset} ${dim}direct (no OTTO_GATEWAY_URL set)${reset}\n`);
148
+ }
132
149
  }
133
150
 
134
151
  // ── LangFlow connection probe ──
@@ -138,16 +155,22 @@ export default function Otto(pi: ExtensionAPI): void {
138
155
  const lfHost = new URL(lfUrl).host;
139
156
  if (langflowDisabled) {
140
157
  ctx?.ui.setStatus("otto-langflow", undefined);
141
- process.stderr.write(` ${yellow}langflow:${reset} ${dim}disabled (${lfHost})${reset}\n`);
158
+ if (!hasUI) {
159
+ process.stderr.write(` ${yellow}langflow:${reset} ${dim}disabled (${lfHost})${reset}\n`);
160
+ }
142
161
  } else {
143
162
  const lfClient = getLangFlowClient();
144
163
  const lfVersion = await lfClient.getVersion();
145
164
  if (lfVersion) {
146
165
  ctx?.ui.setStatus("otto-langflow", `LangFlow ok v${lfVersion.version}`);
147
- process.stderr.write(` ${yellow}langflow:${reset} ${green}connected${reset} ${dim}(v${lfVersion.version} @ ${lfHost})${reset}\n`);
166
+ if (!hasUI) {
167
+ process.stderr.write(` ${yellow}langflow:${reset} ${green}connected${reset} ${dim}(v${lfVersion.version} @ ${lfHost})${reset}\n`);
168
+ }
148
169
  } else {
149
170
  ctx?.ui.setStatus("otto-langflow", "LangFlow offline");
150
- process.stderr.write(` ${yellow}langflow:${reset} ${dim}offline (${lfHost})${reset}\n`);
171
+ if (!hasUI) {
172
+ process.stderr.write(` ${yellow}langflow:${reset} ${dim}offline (${lfHost})${reset}\n`);
173
+ }
151
174
  }
152
175
  }
153
176
  });
@@ -2,12 +2,12 @@ import { describe, it, before, after } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
- import { getCoworkerGlobalDir, getScratchpadsRoot } from './_coworker-paths.js';
5
+ import { getCoworkerGlobalDir, getScratchpadsRoot } from './coworker-paths.js';
6
6
 
7
7
  const ORIGINAL_GLOBAL = process.env.OTTO_COWORKER_GLOBAL_DIR;
8
8
  const ORIGINAL_SCRATCH = process.env.OTTO_SCRATCHPAD_ROOT;
9
9
 
10
- describe('_coworker-paths', () => {
10
+ describe('coworker-paths', () => {
11
11
  before(() => {
12
12
  delete process.env.OTTO_COWORKER_GLOBAL_DIR;
13
13
  delete process.env.OTTO_SCRATCHPAD_ROOT;
@@ -1,8 +1,8 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@otto/pi-coding-agent";
2
2
  import { mkdirSync } from "node:fs";
3
3
 
4
- export default function auditCommand(pi: ExtensionAPI) {
5
- pi.registerCommand("audit", {
4
+ export default function auditCodebaseCommand(pi: ExtensionAPI) {
5
+ pi.registerCommand("audit-codebase", {
6
6
  description: "Audit the current codebase against a specific goal and write a structured report to .gsd/audits/",
7
7
  async handler(args: string, ctx: ExtensionCommandContext) {
8
8
  // ── Step 1: Get the audit goal ────────────────────────────────────────
@@ -15,7 +15,7 @@ export default function auditCommand(pi: ExtensionAPI) {
15
15
  "e.g. understand performance bottlenecks before planning a roadmap",
16
16
  );
17
17
  if (!input?.trim()) {
18
- ctx.ui.notify("audit: No goal provided — cancelled.", "error");
18
+ ctx.ui.notify("audit-codebase: No goal provided — cancelled.", "error");
19
19
  return;
20
20
  }
21
21
  goal = input.trim();
@@ -77,7 +77,7 @@ ${goal}
77
77
 
78
78
  ---
79
79
 
80
- *Generated by /audit — read-only recce, no code was modified.*
80
+ *Generated by /audit-codebase — read-only recce, no code was modified.*
81
81
  \`\`\`
82
82
 
83
83
  After writing the file, confirm with: "✅ Audit complete — report saved to \`${outputPath}\`"`;
@@ -6,6 +6,6 @@
6
6
  "tier": "bundled",
7
7
  "requires": { "platform": ">=2.29.0" },
8
8
  "provides": {
9
- "commands": ["create-slash-command", "create-extension", "audit", "clear"]
9
+ "commands": ["create-slash-command", "create-extension", "audit-codebase", "clear"]
10
10
  }
11
11
  }
@@ -1,12 +1,12 @@
1
1
  import type { ExtensionAPI } from "@otto/pi-coding-agent";
2
2
  import createSlashCommand from "./create-slash-command.js";
3
3
  import createExtension from "./create-extension.js";
4
- import auditCommand from "./audit.js";
4
+ import auditCodebaseCommand from "./audit-codebase.js";
5
5
  import clearCommand from "./clear.js";
6
6
 
7
7
  export default function slashCommands(pi: ExtensionAPI) {
8
8
  createSlashCommand(pi);
9
9
  createExtension(pi);
10
- auditCommand(pi);
10
+ auditCodebaseCommand(pi);
11
11
  clearCommand(pi);
12
12
  }
@@ -68,7 +68,7 @@ function truncateMessage(msg: string, maxLen: number): string {
68
68
  */
69
69
  export function buildHealthLines(data: HealthWidgetData, width?: number): string[] {
70
70
  if (data.projectState === "none") {
71
- return [" OTTO No project loaded — cd into a project, then run /otto init"];
71
+ return [" No project loaded — cd into a project, then run /otto init"];
72
72
  }
73
73
 
74
74
  if (data.projectState === "initialized") {