mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-01 06:23:28 +08:00
feat: add loop-status exit-code mode
This commit is contained in:
@@ -40,6 +40,8 @@ tool calls that have no matching `tool_result`.
|
|||||||
directly.
|
directly.
|
||||||
- `ecc loop-status --bash-timeout-seconds 1800` adjusts the stale Bash
|
- `ecc loop-status --bash-timeout-seconds 1800` adjusts the stale Bash
|
||||||
threshold.
|
threshold.
|
||||||
|
- `ecc loop-status --exit-code` exits `2` when stale loop or tool signals are
|
||||||
|
found, or `1` when transcripts cannot be scanned.
|
||||||
- `ecc loop-status --watch` refreshes status until interrupted.
|
- `ecc loop-status --watch` refreshes status until interrupted.
|
||||||
- `ecc loop-status --watch --watch-count 3` emits a bounded watch stream for
|
- `ecc loop-status --watch --watch-count 3` emits a bounded watch stream for
|
||||||
scripts and handoffs.
|
scripts and handoffs.
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function usage() {
|
|||||||
' --bash-timeout-seconds <n> Age before a pending Bash call is stale (default: 1800)',
|
' --bash-timeout-seconds <n> Age before a pending Bash call is stale (default: 1800)',
|
||||||
' --wake-grace-multiplier <n> ScheduleWakeup grace multiplier (default: 2)',
|
' --wake-grace-multiplier <n> ScheduleWakeup grace multiplier (default: 2)',
|
||||||
' --now <time> Override current time (ISO, epoch ms, or "now")',
|
' --now <time> Override current time (ISO, epoch ms, or "now")',
|
||||||
|
' --exit-code Exit 2 on attention signals, 1 on scan errors',
|
||||||
' --watch Refresh status until interrupted',
|
' --watch Refresh status until interrupted',
|
||||||
' --watch-count <n> Stop after n watch refreshes',
|
' --watch-count <n> Stop after n watch refreshes',
|
||||||
' --watch-interval-seconds <n> Seconds between watch refreshes (default: 5)',
|
' --watch-interval-seconds <n> Seconds between watch refreshes (default: 5)',
|
||||||
@@ -62,6 +63,7 @@ function parseArgs(argv) {
|
|||||||
const args = argv.slice(2);
|
const args = argv.slice(2);
|
||||||
const options = {
|
const options = {
|
||||||
bashTimeoutSeconds: DEFAULT_BASH_TIMEOUT_SECONDS,
|
bashTimeoutSeconds: DEFAULT_BASH_TIMEOUT_SECONDS,
|
||||||
|
exitCode: false,
|
||||||
home: null,
|
home: null,
|
||||||
json: false,
|
json: false,
|
||||||
limit: DEFAULT_LIMIT,
|
limit: DEFAULT_LIMIT,
|
||||||
@@ -99,6 +101,8 @@ function parseArgs(argv) {
|
|||||||
} else if (arg === '--now') {
|
} else if (arg === '--now') {
|
||||||
options.now = readValue(args, index, arg);
|
options.now = readValue(args, index, arg);
|
||||||
index += 1;
|
index += 1;
|
||||||
|
} else if (arg === '--exit-code') {
|
||||||
|
options.exitCode = true;
|
||||||
} else if (arg === '--watch') {
|
} else if (arg === '--watch') {
|
||||||
options.watch = true;
|
options.watch = true;
|
||||||
} else if (arg === '--watch-count') {
|
} else if (arg === '--watch-count') {
|
||||||
@@ -119,6 +123,7 @@ function normalizeOptions(options = {}) {
|
|||||||
return {
|
return {
|
||||||
...options,
|
...options,
|
||||||
bashTimeoutSeconds: options.bashTimeoutSeconds ?? DEFAULT_BASH_TIMEOUT_SECONDS,
|
bashTimeoutSeconds: options.bashTimeoutSeconds ?? DEFAULT_BASH_TIMEOUT_SECONDS,
|
||||||
|
exitCode: Boolean(options.exitCode),
|
||||||
limit: options.limit ?? DEFAULT_LIMIT,
|
limit: options.limit ?? DEFAULT_LIMIT,
|
||||||
transcriptPaths: options.transcriptPaths || [],
|
transcriptPaths: options.transcriptPaths || [],
|
||||||
watch: Boolean(options.watch),
|
watch: Boolean(options.watch),
|
||||||
@@ -606,15 +611,28 @@ function writeStatus(payload, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStatusExitCode(payload) {
|
||||||
|
if (payload.sessions.some(session => session.state === 'attention')) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (payload.errors.length > 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
async function runWatch(options) {
|
async function runWatch(options) {
|
||||||
const normalizedOptions = normalizeOptions(options);
|
const normalizedOptions = normalizeOptions(options);
|
||||||
let iteration = 0;
|
let iteration = 0;
|
||||||
|
let exitCode = 0;
|
||||||
|
|
||||||
while (normalizedOptions.watchCount === null || iteration < normalizedOptions.watchCount) {
|
while (normalizedOptions.watchCount === null || iteration < normalizedOptions.watchCount) {
|
||||||
if (iteration > 0 && !normalizedOptions.json) {
|
if (iteration > 0 && !normalizedOptions.json) {
|
||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
writeStatus(buildStatus(normalizedOptions), normalizedOptions);
|
const payload = buildStatus(normalizedOptions);
|
||||||
|
writeStatus(payload, normalizedOptions);
|
||||||
|
exitCode = Math.max(exitCode, getStatusExitCode(payload));
|
||||||
iteration += 1;
|
iteration += 1;
|
||||||
|
|
||||||
if (normalizedOptions.watchCount !== null && iteration >= normalizedOptions.watchCount) {
|
if (normalizedOptions.watchCount !== null && iteration >= normalizedOptions.watchCount) {
|
||||||
@@ -623,6 +641,8 @@ async function runWatch(options) {
|
|||||||
|
|
||||||
await sleep(normalizedOptions.watchIntervalSeconds * 1000);
|
await sleep(normalizedOptions.watchIntervalSeconds * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -633,11 +653,18 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.watch) {
|
if (options.watch) {
|
||||||
await runWatch(options);
|
const exitCode = await runWatch(options);
|
||||||
|
if (options.exitCode) {
|
||||||
|
process.exitCode = exitCode;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeStatus(buildStatus(options), options);
|
const payload = buildStatus(options);
|
||||||
|
writeStatus(payload, options);
|
||||||
|
if (options.exitCode) {
|
||||||
|
process.exitCode = getStatusExitCode(payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
@@ -652,6 +679,7 @@ module.exports = {
|
|||||||
buildStatus,
|
buildStatus,
|
||||||
extractToolResultIds,
|
extractToolResultIds,
|
||||||
extractToolUses,
|
extractToolUses,
|
||||||
|
getStatusExitCode,
|
||||||
parseArgs,
|
parseArgs,
|
||||||
runWatch,
|
runWatch,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const path = require('path');
|
|||||||
const { execFileSync } = require('child_process');
|
const { execFileSync } = require('child_process');
|
||||||
|
|
||||||
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'loop-status.js');
|
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'loop-status.js');
|
||||||
const { analyzeTranscript, buildStatus, parseArgs } = require('../../scripts/loop-status');
|
const { analyzeTranscript, buildStatus, getStatusExitCode, parseArgs } = require('../../scripts/loop-status');
|
||||||
const NOW = '2026-04-30T10:00:00.000Z';
|
const NOW = '2026-04-30T10:00:00.000Z';
|
||||||
|
|
||||||
function run(args = [], options = {}) {
|
function run(args = [], options = {}) {
|
||||||
@@ -400,6 +400,7 @@ function runTests() {
|
|||||||
const options = parseArgs([
|
const options = parseArgs([
|
||||||
'node',
|
'node',
|
||||||
'scripts/loop-status.js',
|
'scripts/loop-status.js',
|
||||||
|
'--exit-code',
|
||||||
'--watch',
|
'--watch',
|
||||||
'--watch-count',
|
'--watch-count',
|
||||||
'2',
|
'2',
|
||||||
@@ -407,11 +408,51 @@ function runTests() {
|
|||||||
'0.01',
|
'0.01',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
assert.strictEqual(options.exitCode, true);
|
||||||
assert.strictEqual(options.watch, true);
|
assert.strictEqual(options.watch, true);
|
||||||
assert.strictEqual(options.watchCount, 2);
|
assert.strictEqual(options.watchCount, 2);
|
||||||
assert.strictEqual(options.watchIntervalSeconds, 0.01);
|
assert.strictEqual(options.watchIntervalSeconds, 0.01);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('exit-code mode returns 2 when attention signals are present', () => {
|
||||||
|
const homeDir = createTempHome();
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeTranscript(homeDir, '-Users-affoon-project-exit-code', 'session-exit-code.jsonl', [
|
||||||
|
toolUse('2026-04-30T09:10:00.000Z', 'session-exit-code', 'toolu_exit_bash', 'Bash', {
|
||||||
|
command: 'pytest tests/integration/test_pipeline.py',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = run(['--home', homeDir, '--now', NOW, '--json', '--exit-code']);
|
||||||
|
|
||||||
|
assert.strictEqual(result.code, 2, result.stderr);
|
||||||
|
const payload = parsePayload(result.stdout);
|
||||||
|
assert.strictEqual(payload.sessions[0].state, 'attention');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('exit-code mode returns 1 for scan errors without attention signals', () => {
|
||||||
|
const missingTranscript = path.join(os.tmpdir(), 'ecc-loop-status-missing.jsonl');
|
||||||
|
const result = run(['--transcript', missingTranscript, '--now', NOW, '--json', '--exit-code']);
|
||||||
|
|
||||||
|
assert.strictEqual(result.code, 1, result.stderr);
|
||||||
|
const payload = parsePayload(result.stdout);
|
||||||
|
assert.strictEqual(payload.sessions.length, 0);
|
||||||
|
assert.strictEqual(payload.errors.length, 1);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('getStatusExitCode prioritizes attention signals over scan errors', () => {
|
||||||
|
const payload = {
|
||||||
|
errors: [{ message: 'unreadable' }],
|
||||||
|
sessions: [{ state: 'attention' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.strictEqual(getStatusExitCode(payload), 2);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('watch mode emits repeated JSON status frames', () => {
|
if (test('watch mode emits repeated JSON status frames', () => {
|
||||||
const homeDir = createTempHome();
|
const homeDir = createTempHome();
|
||||||
|
|
||||||
@@ -448,6 +489,39 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('watch mode honors exit-code after bounded refreshes', () => {
|
||||||
|
const homeDir = createTempHome();
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeTranscript(homeDir, '-Users-affoon-project-watch-exit', 'session-watch-exit.jsonl', [
|
||||||
|
toolUse('2026-04-30T09:00:00.000Z', 'session-watch-exit', 'toolu_watch_exit', 'ScheduleWakeup', {
|
||||||
|
delaySeconds: 300,
|
||||||
|
reason: 'Loop checkpoint',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = run([
|
||||||
|
'--home',
|
||||||
|
homeDir,
|
||||||
|
'--now',
|
||||||
|
NOW,
|
||||||
|
'--json',
|
||||||
|
'--watch',
|
||||||
|
'--watch-count',
|
||||||
|
'1',
|
||||||
|
'--watch-interval-seconds',
|
||||||
|
'0.01',
|
||||||
|
'--exit-code',
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.strictEqual(result.code, 2, result.stderr);
|
||||||
|
const frame = JSON.parse(result.stdout.trim());
|
||||||
|
assert.strictEqual(frame.sessions[0].state, 'attention');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
process.exit(failed > 0 ? 1 : 0);
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user