agentxchain 2.49.0 → 2.50.0
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.
- package/bin/agentxchain.js +7 -1
- package/package.json +1 -1
- package/scripts/sync-homebrew.sh +26 -3
- package/src/commands/init.js +20 -0
- package/src/commands/schedule.js +93 -0
- package/src/lib/export.js +2 -0
- package/src/lib/repo-observer.js +1 -0
- package/src/lib/run-schedule.js +67 -0
package/bin/agentxchain.js
CHANGED
|
@@ -107,7 +107,7 @@ import { intakeStatusCommand } from '../src/commands/intake-status.js';
|
|
|
107
107
|
import { demoCommand } from '../src/commands/demo.js';
|
|
108
108
|
import { historyCommand } from '../src/commands/history.js';
|
|
109
109
|
import { eventsCommand } from '../src/commands/events.js';
|
|
110
|
-
import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand } from '../src/commands/schedule.js';
|
|
110
|
+
import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand, scheduleStatusCommand } from '../src/commands/schedule.js';
|
|
111
111
|
|
|
112
112
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
113
113
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -288,6 +288,12 @@ scheduleCmd
|
|
|
288
288
|
.option('-j, --json', 'Output as JSON')
|
|
289
289
|
.action(scheduleDaemonCommand);
|
|
290
290
|
|
|
291
|
+
scheduleCmd
|
|
292
|
+
.command('status')
|
|
293
|
+
.description('Show daemon health: running, stale, not_running, or never_started')
|
|
294
|
+
.option('-j, --json', 'Output as JSON')
|
|
295
|
+
.action(scheduleStatusCommand);
|
|
296
|
+
|
|
291
297
|
program
|
|
292
298
|
.command('history')
|
|
293
299
|
.description('Show cross-run history of governed runs in this project')
|
package/package.json
CHANGED
package/scripts/sync-homebrew.sh
CHANGED
|
@@ -27,6 +27,18 @@ formula_sha() {
|
|
|
27
27
|
grep -E '^\s*sha256\s+"' "$formula_path" | sed 's/.*sha256 *"\([a-f0-9]*\)".*/\1/' || true
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
canonical_tap_matches_target() {
|
|
31
|
+
local formula_path="$1"
|
|
32
|
+
local expected_url="$2"
|
|
33
|
+
local expected_sha="$3"
|
|
34
|
+
[[ -f "$formula_path" ]] || return 1
|
|
35
|
+
local remote_url
|
|
36
|
+
local remote_sha
|
|
37
|
+
remote_url="$(formula_url "$formula_path")"
|
|
38
|
+
remote_sha="$(formula_sha "$formula_path")"
|
|
39
|
+
[[ "$remote_url" == "$expected_url" && "$remote_sha" == "$expected_sha" ]]
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
usage() {
|
|
31
43
|
echo "Usage: bash scripts/sync-homebrew.sh --target-version <semver> [--push-tap] [--dry-run]" >&2
|
|
32
44
|
}
|
|
@@ -207,10 +219,21 @@ if $PUSH_TAP; then
|
|
|
207
219
|
git add Formula/agentxchain.rb
|
|
208
220
|
git commit -m "agentxchain ${TARGET_VERSION}"
|
|
209
221
|
if ! git push origin HEAD:main; then
|
|
210
|
-
echo "
|
|
211
|
-
|
|
222
|
+
echo " Push rejected by ${CANONICAL_TAP_REPO}; verifying remote state..."
|
|
223
|
+
git fetch origin main >/dev/null 2>&1 || true
|
|
224
|
+
REMOTE_FORMULA="$(mktemp "${TMPDIR:-/tmp}/homebrew-tap-remote-formula.XXXXXX")"
|
|
225
|
+
if git show origin/main:Formula/agentxchain.rb >"$REMOTE_FORMULA" 2>/dev/null \
|
|
226
|
+
&& canonical_tap_matches_target "$REMOTE_FORMULA" "$TARBALL_URL" "$TARBALL_SHA"; then
|
|
227
|
+
rm -f "$REMOTE_FORMULA"
|
|
228
|
+
echo " Canonical tap already matches target after push rejection — treating sync as complete."
|
|
229
|
+
else
|
|
230
|
+
rm -f "$REMOTE_FORMULA"
|
|
231
|
+
echo "FAIL: could not push to ${CANONICAL_TAP_REPO} and remote tap does not match target artifact" >&2
|
|
232
|
+
exit 1
|
|
233
|
+
fi
|
|
234
|
+
else
|
|
235
|
+
echo " Pushed to ${CANONICAL_TAP_REPO}"
|
|
212
236
|
fi
|
|
213
|
-
echo " Pushed to ${CANONICAL_TAP_REPO}"
|
|
214
237
|
fi
|
|
215
238
|
)
|
|
216
239
|
|
package/src/commands/init.js
CHANGED
|
@@ -65,6 +65,20 @@ function appendAcceptanceHints(baseMatrix, acceptanceHints) {
|
|
|
65
65
|
return `${baseMatrix}\n\n## Template Guidance\n${hintLines}\n`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function findGitRoot(startDir) {
|
|
69
|
+
let current = resolve(startDir);
|
|
70
|
+
while (true) {
|
|
71
|
+
if (existsSync(join(current, '.git'))) {
|
|
72
|
+
return current;
|
|
73
|
+
}
|
|
74
|
+
const parent = dirname(current);
|
|
75
|
+
if (parent === current) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
current = parent;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
// ── Governed init ───────────────────────────────────────────────────────────
|
|
69
83
|
|
|
70
84
|
const GOVERNED_ROLES = {
|
|
@@ -950,6 +964,12 @@ async function initGoverned(opts) {
|
|
|
950
964
|
if (dir !== process.cwd()) {
|
|
951
965
|
console.log(` ${chalk.bold(`cd ${targetLabel}`)}`);
|
|
952
966
|
}
|
|
967
|
+
if (!findGitRoot(dir)) {
|
|
968
|
+
console.log(` ${chalk.bold('git init')} ${chalk.dim('# initialize the governed repo')}`);
|
|
969
|
+
}
|
|
970
|
+
console.log(` ${chalk.bold('agentxchain template validate')} ${chalk.dim('# prove the scaffold contract before the first turn')}`);
|
|
971
|
+
console.log(` ${chalk.bold('git add -A')} ${chalk.dim('# stage the governed scaffold')}`);
|
|
972
|
+
console.log(` ${chalk.bold('git commit -m "initial governed scaffold"')} ${chalk.dim('# checkpoint the starting state')}`);
|
|
953
973
|
console.log(` ${chalk.bold('agentxchain step')} ${chalk.dim('# run the first governed turn')}`);
|
|
954
974
|
console.log(` ${chalk.bold('agentxchain status')} ${chalk.dim('# inspect phase, gate, and turn state')}`);
|
|
955
975
|
console.log('');
|
package/src/commands/schedule.js
CHANGED
|
@@ -2,9 +2,15 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { loadProjectContext } from '../lib/config.js';
|
|
3
3
|
import {
|
|
4
4
|
SCHEDULE_STATE_PATH,
|
|
5
|
+
DAEMON_STATE_PATH,
|
|
5
6
|
listSchedules,
|
|
6
7
|
updateScheduleState,
|
|
7
8
|
evaluateScheduleLaunchEligibility,
|
|
9
|
+
readDaemonState,
|
|
10
|
+
writeDaemonState,
|
|
11
|
+
updateDaemonHeartbeat,
|
|
12
|
+
createDaemonState,
|
|
13
|
+
evaluateDaemonStatus,
|
|
8
14
|
} from '../lib/run-schedule.js';
|
|
9
15
|
import { executeGovernedRun } from './run.js';
|
|
10
16
|
|
|
@@ -221,6 +227,78 @@ export async function scheduleRunDueCommand(opts) {
|
|
|
221
227
|
process.exitCode = result.exitCode;
|
|
222
228
|
}
|
|
223
229
|
|
|
230
|
+
export async function scheduleStatusCommand(opts) {
|
|
231
|
+
const context = loadScheduleContext();
|
|
232
|
+
if (!context) return;
|
|
233
|
+
|
|
234
|
+
const raw = readDaemonState(context.root);
|
|
235
|
+
const evaluation = evaluateDaemonStatus(raw);
|
|
236
|
+
|
|
237
|
+
if (opts.json) {
|
|
238
|
+
const output = {
|
|
239
|
+
ok: evaluation.status === 'running' || evaluation.status === 'never_started',
|
|
240
|
+
state_file: DAEMON_STATE_PATH,
|
|
241
|
+
daemon: {
|
|
242
|
+
status: evaluation.status,
|
|
243
|
+
pid: raw?.pid ?? null,
|
|
244
|
+
started_at: raw?.started_at ?? null,
|
|
245
|
+
last_heartbeat_at: raw?.last_heartbeat_at ?? null,
|
|
246
|
+
last_cycle_result: raw?.last_cycle_result ?? null,
|
|
247
|
+
poll_seconds: raw?.poll_seconds ?? null,
|
|
248
|
+
stale_after_seconds: evaluation.stale_after_seconds ?? null,
|
|
249
|
+
last_error: raw?.last_error ?? null,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
if (evaluation.warning) output.daemon.warning = evaluation.warning;
|
|
253
|
+
console.log(JSON.stringify(output, null, 2));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Human-readable output
|
|
258
|
+
const statusColors = {
|
|
259
|
+
running: chalk.green,
|
|
260
|
+
stale: chalk.yellow,
|
|
261
|
+
not_running: chalk.red,
|
|
262
|
+
never_started: chalk.dim,
|
|
263
|
+
};
|
|
264
|
+
const colorFn = statusColors[evaluation.status] || chalk.white;
|
|
265
|
+
|
|
266
|
+
console.log(chalk.bold('Schedule Daemon Status'));
|
|
267
|
+
console.log(` State: ${colorFn(evaluation.status)}`);
|
|
268
|
+
|
|
269
|
+
if (evaluation.status === 'never_started') {
|
|
270
|
+
console.log(chalk.dim(' No daemon state file found. Run `agentxchain schedule daemon` to start.'));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (evaluation.warning) {
|
|
275
|
+
console.log(chalk.yellow(` Warning: ${evaluation.warning}`));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (raw?.pid != null) {
|
|
279
|
+
console.log(` PID: ${raw.pid}`);
|
|
280
|
+
}
|
|
281
|
+
if (raw?.started_at) {
|
|
282
|
+
console.log(` Started: ${raw.started_at}`);
|
|
283
|
+
}
|
|
284
|
+
if (raw?.last_heartbeat_at) {
|
|
285
|
+
console.log(` Heartbeat: ${raw.last_heartbeat_at}`);
|
|
286
|
+
}
|
|
287
|
+
if (raw?.last_cycle_result) {
|
|
288
|
+
const resultColor = raw.last_cycle_result === 'ok' ? chalk.green : chalk.red;
|
|
289
|
+
console.log(` Last cycle: ${resultColor(raw.last_cycle_result)}`);
|
|
290
|
+
}
|
|
291
|
+
if (raw?.poll_seconds != null) {
|
|
292
|
+
console.log(` Poll: ${raw.poll_seconds}s`);
|
|
293
|
+
}
|
|
294
|
+
if (evaluation.status === 'stale') {
|
|
295
|
+
console.log(chalk.yellow(` ⚠ Heartbeat is ${evaluation.heartbeat_age_seconds}s old (stale after ${evaluation.stale_after_seconds}s)`));
|
|
296
|
+
}
|
|
297
|
+
if (raw?.last_error) {
|
|
298
|
+
console.log(chalk.red(` Last error: ${raw.last_error}`));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
224
302
|
export async function scheduleDaemonCommand(opts) {
|
|
225
303
|
const context = loadScheduleContext();
|
|
226
304
|
if (!context) return;
|
|
@@ -239,16 +317,31 @@ export async function scheduleDaemonCommand(opts) {
|
|
|
239
317
|
}
|
|
240
318
|
|
|
241
319
|
let cycle = 0;
|
|
320
|
+
const daemonState = createDaemonState(process.pid, pollSeconds, opts.schedule || null, maxCycles);
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
writeDaemonState(context.root, daemonState);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
console.error(chalk.red(`Cannot write daemon state: ${err.message}`));
|
|
326
|
+
process.exitCode = 1;
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
242
330
|
if (!opts.json) {
|
|
243
331
|
console.log(chalk.bold('AgentXchain Schedule Daemon'));
|
|
244
332
|
console.log(chalk.dim(` Poll: ${pollSeconds}s`));
|
|
245
333
|
console.log(chalk.dim(` State: ${SCHEDULE_STATE_PATH}`));
|
|
334
|
+
console.log(chalk.dim(` Health: ${DAEMON_STATE_PATH}`));
|
|
246
335
|
console.log('');
|
|
247
336
|
}
|
|
248
337
|
|
|
249
338
|
while (true) {
|
|
250
339
|
cycle += 1;
|
|
340
|
+
daemonState.last_cycle_started_at = new Date().toISOString();
|
|
251
341
|
const result = await runDueSchedules(context, opts);
|
|
342
|
+
|
|
343
|
+
updateDaemonHeartbeat(context.root, daemonState, result);
|
|
344
|
+
|
|
252
345
|
if (opts.json) {
|
|
253
346
|
console.log(JSON.stringify({ cycle, ...result }));
|
|
254
347
|
}
|
package/src/lib/export.js
CHANGED
|
@@ -33,6 +33,7 @@ export const RUN_EXPORT_INCLUDED_ROOTS = [
|
|
|
33
33
|
'.agentxchain/run-history.jsonl',
|
|
34
34
|
'.agentxchain/events.jsonl',
|
|
35
35
|
'.agentxchain/schedule-state.json',
|
|
36
|
+
'.agentxchain/schedule-daemon.json',
|
|
36
37
|
'.agentxchain/dispatch',
|
|
37
38
|
'.agentxchain/staging',
|
|
38
39
|
'.agentxchain/transactions/accept',
|
|
@@ -57,6 +58,7 @@ export const RUN_RESTORE_ROOTS = [
|
|
|
57
58
|
'.agentxchain/run-history.jsonl',
|
|
58
59
|
'.agentxchain/events.jsonl',
|
|
59
60
|
'.agentxchain/schedule-state.json',
|
|
61
|
+
'.agentxchain/schedule-daemon.json',
|
|
60
62
|
'.agentxchain/dispatch',
|
|
61
63
|
'.agentxchain/staging',
|
|
62
64
|
'.agentxchain/transactions/accept',
|
package/src/lib/repo-observer.js
CHANGED
package/src/lib/run-schedule.js
CHANGED
|
@@ -4,7 +4,9 @@ import { safeWriteJson } from './safe-write.js';
|
|
|
4
4
|
import { loadProjectState } from './config.js';
|
|
5
5
|
|
|
6
6
|
export const SCHEDULE_STATE_PATH = '.agentxchain/schedule-state.json';
|
|
7
|
+
export const DAEMON_STATE_PATH = '.agentxchain/schedule-daemon.json';
|
|
7
8
|
const SCHEDULE_STATE_SCHEMA_VERSION = '0.1';
|
|
9
|
+
const DAEMON_STATE_SCHEMA_VERSION = '0.1';
|
|
8
10
|
|
|
9
11
|
function parseIsoTime(value) {
|
|
10
12
|
if (typeof value !== 'string' || !value.trim()) return null;
|
|
@@ -158,3 +160,68 @@ export function evaluateScheduleLaunchEligibility(root, config) {
|
|
|
158
160
|
|
|
159
161
|
return { ok: false, status, reason: `run_${status}` };
|
|
160
162
|
}
|
|
163
|
+
|
|
164
|
+
// ── Daemon Health State ─────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
export function readDaemonState(root) {
|
|
167
|
+
const absPath = join(root, DAEMON_STATE_PATH);
|
|
168
|
+
if (!existsSync(absPath)) return null;
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(readFileSync(absPath, 'utf8'));
|
|
171
|
+
} catch {
|
|
172
|
+
return { _parse_error: true };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function writeDaemonState(root, state) {
|
|
177
|
+
const absPath = join(root, DAEMON_STATE_PATH);
|
|
178
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
179
|
+
safeWriteJson(absPath, { schema_version: DAEMON_STATE_SCHEMA_VERSION, ...state });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function updateDaemonHeartbeat(root, daemonState, cycleResult) {
|
|
183
|
+
const now = new Date().toISOString();
|
|
184
|
+
const updated = {
|
|
185
|
+
...daemonState,
|
|
186
|
+
last_heartbeat_at: now,
|
|
187
|
+
last_cycle_finished_at: now,
|
|
188
|
+
last_cycle_result: cycleResult.ok ? 'ok' : 'error',
|
|
189
|
+
last_error: cycleResult.ok ? null : (cycleResult.error || 'cycle failed'),
|
|
190
|
+
};
|
|
191
|
+
writeDaemonState(root, updated);
|
|
192
|
+
return updated;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function createDaemonState(pid, pollSeconds, scheduleId, maxCycles) {
|
|
196
|
+
const now = new Date().toISOString();
|
|
197
|
+
return {
|
|
198
|
+
pid,
|
|
199
|
+
started_at: now,
|
|
200
|
+
last_heartbeat_at: now,
|
|
201
|
+
last_cycle_started_at: null,
|
|
202
|
+
last_cycle_finished_at: null,
|
|
203
|
+
last_cycle_result: null,
|
|
204
|
+
poll_seconds: pollSeconds,
|
|
205
|
+
schedule_id: scheduleId || null,
|
|
206
|
+
max_cycles: maxCycles,
|
|
207
|
+
last_error: null,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function evaluateDaemonStatus(daemonState, now = Date.now()) {
|
|
212
|
+
if (!daemonState) return { status: 'never_started' };
|
|
213
|
+
if (daemonState._parse_error) return { status: 'not_running', warning: 'state file is malformed' };
|
|
214
|
+
|
|
215
|
+
const heartbeat = parseIsoTime(daemonState.last_heartbeat_at);
|
|
216
|
+
if (heartbeat === null) return { status: 'not_running', warning: 'no heartbeat recorded' };
|
|
217
|
+
|
|
218
|
+
const pollSeconds = typeof daemonState.poll_seconds === 'number' ? daemonState.poll_seconds : 60;
|
|
219
|
+
const staleAfterSeconds = Math.max(pollSeconds * 3, 30);
|
|
220
|
+
const ageSeconds = (now - heartbeat) / 1000;
|
|
221
|
+
|
|
222
|
+
if (ageSeconds > staleAfterSeconds) {
|
|
223
|
+
return { status: 'stale', stale_after_seconds: staleAfterSeconds, heartbeat_age_seconds: Math.round(ageSeconds) };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { status: 'running', stale_after_seconds: staleAfterSeconds, heartbeat_age_seconds: Math.round(ageSeconds) };
|
|
227
|
+
}
|