osborn 0.9.233 → 0.9.235

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 (2) hide show
  1. package/dist/claude-llm.js +49 -3
  2. package/package.json +1 -1
@@ -95,6 +95,15 @@ async function buildRecallInjection(sessionId, workingDir, prompt) {
95
95
  // agent's direct tool calls count against the budget.
96
96
  let turnToolCallCount = 0;
97
97
  const TOOL_CALL_BUDGET = 5;
98
+ // Writer PROVENANCE REMINDER (Chesterton's Fence): before the writer mutates a
99
+ // version-controlled file it should review that file's git history so the edit is
100
+ // backward-informed (root cause / prior + reverted attempts / cross-component coupling),
101
+ // not forward-only. Delivered as a ONE-SHOT PreToolUse deny-with-reminder on the writer's
102
+ // first edit of an existing file each turn. This single flag is the irreducible minimum:
103
+ // a PreToolUse hook can only allow/deny/ask (no "inject note + proceed"), so the reminder
104
+ // must be a deny — and without a "already nudged this turn" flag that deny would loop
105
+ // forever. Reset on every UserPromptSubmit (new turn).
106
+ let writerHistoryReminded = false;
98
107
  /**
99
108
  * Strip markdown formatting for TTS (text-to-speech)
100
109
  * Removes **bold**, ##headers, ```code```, etc. so TTS doesn't read them literally
@@ -493,6 +502,13 @@ export const NAMED_AGENTS = {
493
502
  '4. If ANYTHING has drifted from the plan (file moved, code refactored, dependency changed):',
494
503
  ' STOP and report back to the main agent. Do NOT improvise.',
495
504
  '',
505
+ '### Step 1b: Review history before touching a versioned file',
506
+ 'For any file that ALREADY EXISTS (i.e. has git history), review its lineage BEFORE you edit it (on your first edit of an existing file each turn you\'ll get a one-time reminder to). This is Chesterton\'s Fence — understand WHY the code is there before you change it. Do:',
507
+ ' 1. `git log --follow -p -- "<file>"` — recent commits and the rationale behind them.',
508
+ ' 2. `git blame` on the exact lines you will change — who/when/which commit introduced them.',
509
+ ' 3. `git log --oneline --grep=<feature>` and `git log --oneline -S"<symbol>"` — look for related OR REVERTED work so you do not redo something already tried, and note which components were co-changed (coupling).',
510
+ 'Fix problems backward-informed — root cause, systemic implications, prior attempts — not forward-only. New files are exempt (no history to review).',
511
+ '',
496
512
  '### Step 2: Clarify unknowns',
497
513
  '1. If the plan is vague or ambiguous — ask the main agent a specific clarifying question.',
498
514
  ' Examples: "Which config format — YAML or JSON?", "New file or extend existing auth.ts?"',
@@ -1583,8 +1599,15 @@ export class ClaudeLLM extends llm.LLM {
1583
1599
  if (msg.requestId) {
1584
1600
  callbacks.eventEmitter.emit('query_request_id', { requestId: msg.requestId });
1585
1601
  }
1586
- // Stream assistant text → tts_say events
1587
- if (msg.type === 'assistant' && msg.message?.content) {
1602
+ // Stream assistant text → tts_say events.
1603
+ // SUBAGENT GUARD (regression fix): the SDK surfaces a Task sub-agent's assistant
1604
+ // messages inline in THIS same stream, tagged with a non-null parent_tool_use_id
1605
+ // ("non-null when the message was produced inside a subagent" — SDK docs). Only the
1606
+ // MAIN orchestrator (parent_tool_use_id === null) may speak or post a chat bubble;
1607
+ // a sub-agent's raw narration must NOT reach TTS/UI (its work already shows via the
1608
+ // tool_use activity events). Without this, once verifiers/researcher became real Task
1609
+ // sub-agents their text "took over the mic" and leaked raw code/markdown into audio.
1610
+ if (msg.type === 'assistant' && msg.message?.content && !msg.parent_tool_use_id) {
1588
1611
  // Assign a stable messageId for this turn (first block sets it, rest reuse)
1589
1612
  if (!this.#currentTurnMessageId) {
1590
1613
  this.#currentTurnMessageId = crypto.randomUUID();
@@ -1866,6 +1889,25 @@ class ClaudeLLMStream extends llm.LLMStream {
1866
1889
  // hardcoded per-role branches had, now in one visible place.
1867
1890
  if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
1868
1891
  const filePath = String(toolInput.file_path || '');
1892
+ // Writer PROVENANCE REMINDER (Chesterton's Fence): on the writer's FIRST edit of
1893
+ // an EXISTING (version-controlled) file this turn, deny ONCE with a reminder to
1894
+ // review the file's git history so the change is backward-informed (root cause,
1895
+ // prior/reverted attempts, cross-component coupling), not forward-only — then it
1896
+ // proceeds. New files are exempt (no history). One-shot per turn so it can't loop.
1897
+ if (agentType === 'writer' && filePath && existsSync(filePath) && !writerHistoryReminded) {
1898
+ writerHistoryReminded = true;
1899
+ const base = filePath.split('/').pop() || filePath;
1900
+ const gateReason = [
1901
+ `Before editing ${base}, first review its history so your change is backward-informed, not forward-only (Chesterton's Fence — understand WHY this code exists before changing it):`,
1902
+ ` 1. git log --follow -p -- "${filePath}" (recent commits + rationale)`,
1903
+ ` 2. git blame the specific lines you're about to change (who/when/which commit)`,
1904
+ ` 3. git log --oneline --grep=<feature> and git log --oneline -S"<symbol>" (related OR reverted work so you don't redo something already tried; note components co-changed in those commits)`,
1905
+ `Then re-attempt this edit. (One-time reminder this turn; new files are exempt.)`,
1906
+ ].join('\n');
1907
+ console.log(`🏛️ Writer provenance reminder: nudging before ${base} (first edit of an existing file this turn)`);
1908
+ this.#eventEmitter.emit('tool_blocked', { name: toolName, reason: `Review ${base}'s git history first (one-time provenance reminder).` });
1909
+ return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' }, reason: gateReason };
1910
+ }
1869
1911
  const { decision, reason } = decideWrite(agentType, filePath);
1870
1912
  console.log(`🔎 Write gate: agent=${agentType ?? 'main'} path="${filePath || '(none)'}" → ${decision}`);
1871
1913
  if (decision === 'deny') {
@@ -1937,6 +1979,8 @@ class ClaudeLLMStream extends llm.LLMStream {
1937
1979
  try {
1938
1980
  // Reset the per-turn tool call counter so the new turn starts fresh.
1939
1981
  turnToolCallCount = 0;
1982
+ // Reset the writer provenance-reminder flag for the new turn.
1983
+ writerHistoryReminded = false;
1940
1984
  const reminder = readFileSync(TURN_SHAPE_REMINDER_PATH, 'utf-8');
1941
1985
  const promptPreview = String(input?.prompt || '').substring(0, 60).replace(/\n/g, ' ');
1942
1986
  // Reliable recall: retrieve prior messages relevant to THIS prompt and inject
@@ -2391,7 +2435,9 @@ class ClaudeLLMStream extends llm.LLMStream {
2391
2435
  this.#onCheckpoint(checkpointId);
2392
2436
  }
2393
2437
  // Stream text chunks — send each assistant text block to TTS
2394
- if (message.type === 'assistant' && message.message?.content) {
2438
+ // SUBAGENT GUARD (see the persistent-consumer loop above): skip sub-agent messages
2439
+ // (non-null parent_tool_use_id) so only the main orchestrator's text speaks/streams.
2440
+ if (message.type === 'assistant' && message.message?.content && !message.parent_tool_use_id) {
2395
2441
  // Emit SDK requestId on first assistant message — identifies this query()
2396
2442
  // in the JSONL for tracking which research task produced which output
2397
2443
  const sdkRequestId = message.requestId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.233",
3
+ "version": "0.9.235",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {