From a335fad9a6e41090ef5994795373d0129ac63326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 18 Aug 2026 10:34:03 +0000 Subject: [PATCH] background jobs: tell the agent not to discard command output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Traced "(no output yet)" on a live commander to the actual running process on this machine rather than guessing: the tray's log-tail was working correctly (it's a general /chat feature, nothing to fix there) — the task's .output file was genuinely empty because the agent had written `timeout 8 ping -c 1 -W 5 google.com >/dev/null 2>&1`, throwing away ping's own output and only echoing on a failed ping. Correct for "let me know if it goes down," but it means a healthy watcher shows nothing at all, indistin- guishable at a glance from a stuck one. buildBackgroundJobPrompt gains an unconditional rule — every job shape, not only watchers — telling the agent not to redirect a command's own output to /dev/null or similar: whatever it would print in a terminal is what the owner wants to see live in the job's log. Landed at rule 8 for watchers (after the existing fire rule), 5 for a bare command, 4 for a bare prompt — whichever number falls after the rules already in that branch, so rule 7's existing identity (referenced by number in tests, docs and COMMS) is untouched. Also lightly reworded rule 7's "stay silent" line: it was already only about the agent's own narration, not the underlying command's output, but the distinction wasn't explicit and reads exactly like the instinct that produced the >/dev/null in the first place. Co-Authored-By: Claude Opus 5 --- docs/command-hq.md | 13 ++++++++++++ .../src/apps/Chat/background-job-prompt.ts | 21 +++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/command-hq.md b/docs/command-hq.md index 99ac2f37..5a482d22 100644 --- a/docs/command-hq.md +++ b/docs/command-hq.md @@ -494,6 +494,19 @@ so a commander open in the panel at the moment this fires keeps showing the old navigation or reload. The grid's cards do not have this problem — `GET /commanders` is already polled every 15s. Flagged rather than fixed; worth a `refetch()` hook if it turns out to matter in practice. +**"(no output yet)" on that same google.com watcher, traced to the actual running process on this +machine rather than guessed at.** The tray's live-log-tail (`BackgroundTaskTray` → `useTaskDetail` → +`GET /chat/tasks/:id` → `loadBackgroundTask`/`tailFile`) is a general `/chat` feature Command HQ gets for +free — nothing to build there, and it was already working correctly. The task's `.output` file was +genuinely empty: the agent had written `timeout 8 ping -c 1 -W 5 google.com >/dev/null 2>&1`, silencing +`ping`'s own output entirely and only ever `echo`ing on a FAILED ping — correct per "let me know if it goes +down," but it means a healthy watcher shows nothing at all, indistinguishable at a glance from a stuck one. +Fixed at the source rather than in the tray: `background-job-prompt.ts` gained an unconditional rule (every +job, not only watchers — number 4, 5 or 8 depending on which rules already preceded it in that particular +prompt) telling the agent not to discard a command's own output. The tray already shows whatever reaches +the log file live; the gap was that jobs were routinely told, by habit rather than by this prompt, to send +that output to `/dev/null`. + **A real bug, caught by tracing rather than by the compiler, worth stating plainly.** When the cost-tracking schema changed `recordCommandHqWatcherFired`'s signature from `(userId, sessionId)` to `(sessionRowId, taskId)`, the ONE existing call site was not updated in the same pass — and `bunx tsgo` stayed clean, because diff --git a/src/workspaces/officerdev/src/apps/Chat/background-job-prompt.ts b/src/workspaces/officerdev/src/apps/Chat/background-job-prompt.ts index 19072d9e..681d3b0c 100644 --- a/src/workspaces/officerdev/src/apps/Chat/background-job-prompt.ts +++ b/src/workspaces/officerdev/src/apps/Chat/background-job-prompt.ts @@ -83,8 +83,10 @@ function fireRule(onFire: FireBehavior): string { const exitLines = [ `7. EXIT when it fires — the exit IS the notification. A loop that prints and continues has`, - ` told no one. Stay silent until then: everything printed lands in context at once on exit.`, - ` Report what fired.`, + ` told no one. Add no narration of your own until then — no "still checking", no progress`, + ` commentary: everything YOU print lands in my context at once on exit, so keep that part`, + ` quiet. That is not the same as the command's own output, which stays visible the whole`, + ` time regardless — see the output rule below. Report what fired.`, ]; return onFire === 'handoff' @@ -139,6 +141,21 @@ export function buildBackgroundJobPrompt(spec: BackgroundJobSpec): string { ); } + // Unconditional — every shape of job gets this, not only watchers. `watching`/`spec.mode` only decide + // which number it lands on, matching whichever rules already came before it in this particular prompt. + const outputRuleNumber = watching ? 8 : spec.mode === 'command' ? 5 : 4; + rules.push( + ``, + `${outputRuleNumber}. Do not throw away the command's own output — no \`>/dev/null\`, no \`2>&1\` into a`, + ` sink, no piping into something that swallows it, even for a check you only care about the`, + ` exit code of. Whatever it would print in a terminal is what I want to see, live, in this`, + ` job's log: a ping's own reply lines, a build's own progress, whatever the command already`, + ` says on its way. I can watch that file the same way I could watch a terminal — redirect`, + ` only what is genuinely noise (a spinner repainting one line), never the signal. This is`, + ` separate from anything you narrate yourself about the job's status: that is your voice;`, + ` this is the command's, and I want to hear both.`, + ); + return [ `Start a background job. The mechanics below are load-bearing; follow them exactly.`, ``,