diff --git a/commands/loop-status.md b/commands/loop-status.md
index 81dc9427..1149fc4b 100644
--- a/commands/loop-status.md
+++ b/commands/loop-status.md
@@ -40,10 +40,15 @@ tool calls that have no matching `tool_result`.
directly.
- `ecc loop-status --bash-timeout-seconds 1800` adjusts the stale Bash
threshold.
+- `ecc loop-status --watch` refreshes status until interrupted.
+- `ecc loop-status --watch --watch-count 3` emits a bounded watch stream for
+ scripts and handoffs.
## Watch Mode
-When `--watch` is present, refresh status periodically and surface state changes.
+When `--watch` is present, refresh status periodically. With `--json`, each
+refresh is emitted as one JSON object per line so another terminal or script can
+consume the stream.
## Arguments
diff --git a/scripts/loop-status.js b/scripts/loop-status.js
index 98844590..b0bee3f2 100644
--- a/scripts/loop-status.js
+++ b/scripts/loop-status.js
@@ -8,12 +8,13 @@ const path = require('path');
const DEFAULT_BASH_TIMEOUT_SECONDS = 30 * 60;
const DEFAULT_LIMIT = 10;
const DEFAULT_WAKE_GRACE_MULTIPLIER = 2;
+const DEFAULT_WATCH_INTERVAL_SECONDS = 5;
function usage() {
console.log([
'Usage:',
- ' node scripts/loop-status.js [--json] [--home
] [--limit ]',
- ' node scripts/loop-status.js --transcript [--json]',
+ ' node scripts/loop-status.js [--json] [--home ] [--limit ] [--watch]',
+ ' node scripts/loop-status.js --transcript [--json] [--watch]',
'',
'Options:',
' --json Emit machine-readable status JSON',
@@ -23,6 +24,9 @@ function usage() {
' --bash-timeout-seconds Age before a pending Bash call is stale (default: 1800)',
' --wake-grace-multiplier ScheduleWakeup grace multiplier (default: 2)',
' --now Override current time (ISO, epoch ms, or "now")',
+ ' --watch Refresh status until interrupted',
+ ' --watch-count Stop after n watch refreshes',
+ ' --watch-interval-seconds Seconds between watch refreshes (default: 5)',
'',
'Examples:',
' node scripts/loop-status.js --json',
@@ -64,7 +68,10 @@ function parseArgs(argv) {
now: null,
showHelp: false,
transcriptPaths: [],
+ watch: false,
+ watchCount: null,
wakeGraceMultiplier: DEFAULT_WAKE_GRACE_MULTIPLIER,
+ watchIntervalSeconds: DEFAULT_WATCH_INTERVAL_SECONDS,
};
for (let index = 0; index < args.length; index += 1) {
@@ -92,6 +99,14 @@ function parseArgs(argv) {
} else if (arg === '--now') {
options.now = readValue(args, index, arg);
index += 1;
+ } else if (arg === '--watch') {
+ options.watch = true;
+ } else if (arg === '--watch-count') {
+ options.watchCount = readPositiveInteger(readValue(args, index, arg), arg);
+ index += 1;
+ } else if (arg === '--watch-interval-seconds') {
+ options.watchIntervalSeconds = readPositiveNumber(readValue(args, index, arg), arg);
+ index += 1;
} else {
throw new Error(`Unknown option: ${arg}`);
}
@@ -106,7 +121,10 @@ function normalizeOptions(options = {}) {
bashTimeoutSeconds: options.bashTimeoutSeconds ?? DEFAULT_BASH_TIMEOUT_SECONDS,
limit: options.limit ?? DEFAULT_LIMIT,
transcriptPaths: options.transcriptPaths || [],
+ watch: Boolean(options.watch),
+ watchCount: options.watchCount ?? null,
wakeGraceMultiplier: options.wakeGraceMultiplier ?? DEFAULT_WAKE_GRACE_MULTIPLIER,
+ watchIntervalSeconds: options.watchIntervalSeconds ?? DEFAULT_WATCH_INTERVAL_SECONDS,
};
}
@@ -576,28 +594,57 @@ function formatText(payload) {
return lines.join('\n');
}
-function main() {
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function writeStatus(payload, options) {
+ if (options.json) {
+ console.log(options.watch ? JSON.stringify(payload) : JSON.stringify(payload, null, 2));
+ } else {
+ console.log(formatText(payload));
+ }
+}
+
+async function runWatch(options) {
+ const normalizedOptions = normalizeOptions(options);
+ let iteration = 0;
+
+ while (normalizedOptions.watchCount === null || iteration < normalizedOptions.watchCount) {
+ if (iteration > 0 && !normalizedOptions.json) {
+ console.log('');
+ }
+ writeStatus(buildStatus(normalizedOptions), normalizedOptions);
+ iteration += 1;
+
+ if (normalizedOptions.watchCount !== null && iteration >= normalizedOptions.watchCount) {
+ break;
+ }
+
+ await sleep(normalizedOptions.watchIntervalSeconds * 1000);
+ }
+}
+
+async function main() {
const options = parseArgs(process.argv);
if (options.showHelp) {
usage();
return;
}
- const payload = buildStatus(options);
- if (options.json) {
- console.log(JSON.stringify(payload, null, 2));
- } else {
- console.log(formatText(payload));
+ if (options.watch) {
+ await runWatch(options);
+ return;
}
+
+ writeStatus(buildStatus(options), options);
}
if (require.main === module) {
- try {
- main();
- } catch (error) {
+ main().catch(error => {
console.error(`[loop-status] ${error.message}`);
process.exit(1);
- }
+ });
}
module.exports = {
@@ -606,4 +653,5 @@ module.exports = {
extractToolResultIds,
extractToolUses,
parseArgs,
+ runWatch,
};
diff --git a/tests/scripts/loop-status.test.js b/tests/scripts/loop-status.test.js
index 82449d32..b7f906dd 100644
--- a/tests/scripts/loop-status.test.js
+++ b/tests/scripts/loop-status.test.js
@@ -9,7 +9,7 @@ const path = require('path');
const { execFileSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'loop-status.js');
-const { analyzeTranscript, buildStatus } = require('../../scripts/loop-status');
+const { analyzeTranscript, buildStatus, parseArgs } = require('../../scripts/loop-status');
const NOW = '2026-04-30T10:00:00.000Z';
function run(args = [], options = {}) {
@@ -396,6 +396,58 @@ function runTests() {
assert.match(result.stderr, /--limit must be a positive integer/);
})) passed++; else failed++;
+ if (test('parses watch mode controls', () => {
+ const options = parseArgs([
+ 'node',
+ 'scripts/loop-status.js',
+ '--watch',
+ '--watch-count',
+ '2',
+ '--watch-interval-seconds',
+ '0.01',
+ ]);
+
+ assert.strictEqual(options.watch, true);
+ assert.strictEqual(options.watchCount, 2);
+ assert.strictEqual(options.watchIntervalSeconds, 0.01);
+ })) passed++; else failed++;
+
+ if (test('watch mode emits repeated JSON status frames', () => {
+ const homeDir = createTempHome();
+
+ try {
+ writeTranscript(homeDir, '-Users-affoon-project-watch', 'session-watch.jsonl', [
+ toolUse('2026-04-30T09:00:00.000Z', 'session-watch', 'toolu_watch', 'ScheduleWakeup', {
+ delaySeconds: 300,
+ reason: 'Loop checkpoint',
+ }),
+ ]);
+
+ const result = run([
+ '--home',
+ homeDir,
+ '--now',
+ NOW,
+ '--json',
+ '--watch',
+ '--watch-count',
+ '2',
+ '--watch-interval-seconds',
+ '0.01',
+ ]);
+
+ assert.strictEqual(result.code, 0, result.stderr);
+ const frames = result.stdout.trim().split(/\r?\n/).map(line => JSON.parse(line));
+ assert.strictEqual(frames.length, 2);
+ assert.strictEqual(frames[0].schemaVersion, 'ecc.loop-status.v1');
+ assert.strictEqual(frames[1].schemaVersion, 'ecc.loop-status.v1');
+ assert.strictEqual(frames[0].sessions[0].sessionId, 'session-watch');
+ assert.strictEqual(frames[1].sessions[0].sessionId, 'session-watch');
+ } finally {
+ fs.rmSync(homeDir, { recursive: true, force: true });
+ }
+ })) passed++; else failed++;
+
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}