agentxchain 2.49.0 → 2.51.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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.49.0",
3
+ "version": "2.51.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # Release identity creation — replaces raw `npm version <semver>`.
3
3
  # Creates version bump commit + annotated tag with fail-closed verification.
4
- # Usage: bash scripts/release-bump.sh --target-version <semver>
4
+ # Usage: bash scripts/release-bump.sh --target-version <semver> [--skip-preflight]
5
5
  set -euo pipefail
6
6
 
7
7
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -10,9 +10,10 @@ REPO_ROOT="$(cd "${CLI_DIR}/.." && pwd)"
10
10
  cd "$CLI_DIR"
11
11
 
12
12
  TARGET_VERSION=""
13
+ SKIP_PREFLIGHT=0
13
14
 
14
15
  usage() {
15
- echo "Usage: bash scripts/release-bump.sh --target-version <semver>" >&2
16
+ echo "Usage: bash scripts/release-bump.sh --target-version <semver> [--skip-preflight]" >&2
16
17
  }
17
18
 
18
19
  while [[ $# -gt 0 ]]; do
@@ -31,6 +32,10 @@ while [[ $# -gt 0 ]]; do
31
32
  TARGET_VERSION="$2"
32
33
  shift 2
33
34
  ;;
35
+ --skip-preflight)
36
+ SKIP_PREFLIGHT=1
37
+ shift
38
+ ;;
34
39
  *)
35
40
  usage
36
41
  exit 1
@@ -250,8 +255,67 @@ if [[ "$COMMIT_MSG" != "$TARGET_VERSION" ]]; then
250
255
  fi
251
256
  echo " OK: commit ${RELEASE_SHA:0:7} with message '${TARGET_VERSION}'"
252
257
 
258
+ # 8.5. Inline preflight gate — tests, pack, and docs build must pass before tag
259
+ if [[ "$SKIP_PREFLIGHT" -eq 1 ]]; then
260
+ echo ""
261
+ echo "[8.5/10] Inline preflight gate SKIPPED (--skip-preflight)"
262
+ else
263
+ echo ""
264
+ echo "[8.5/10] Running inline preflight gate..."
265
+ echo " Running test suite..."
266
+
267
+ # Install MCP example deps if needed (same as release-preflight.sh)
268
+ for example_dir in "${CLI_DIR}/../examples/mcp-echo-agent" "${CLI_DIR}/../examples/mcp-http-echo-agent"; do
269
+ if [[ -f "${example_dir}/package.json" && ! -d "${example_dir}/node_modules" ]]; then
270
+ echo " Installing deps for $(basename "$example_dir")..."
271
+ (cd "$example_dir" && env -u NODE_AUTH_TOKEN -u NPM_CONFIG_USERCONFIG npm install --ignore-scripts --userconfig /dev/null 2>&1) || true
272
+ fi
273
+ done
274
+
275
+ PREFLIGHT_FAILED=0
276
+
277
+ # 8.5a. Full test suite with release env vars
278
+ if env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test >/dev/null 2>&1; then
279
+ echo " OK: test suite passed"
280
+ else
281
+ echo " FAIL: test suite failed" >&2
282
+ echo " Re-running with output for diagnostics..." >&2
283
+ env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test 2>&1 | tail -30 >&2
284
+ PREFLIGHT_FAILED=1
285
+ fi
286
+
287
+ # 8.5b. npm pack dry-run
288
+ if npm pack --dry-run >/dev/null 2>&1; then
289
+ echo " OK: npm pack --dry-run passed"
290
+ else
291
+ echo " FAIL: npm pack --dry-run failed" >&2
292
+ PREFLIGHT_FAILED=1
293
+ fi
294
+
295
+ # 8.5c. Docs build
296
+ if (cd "${REPO_ROOT}/website-v2" && npm run build >/dev/null 2>&1); then
297
+ echo " OK: docs build passed"
298
+ else
299
+ echo " FAIL: docs build failed" >&2
300
+ PREFLIGHT_FAILED=1
301
+ fi
302
+
303
+ if [[ "$PREFLIGHT_FAILED" -eq 1 ]]; then
304
+ echo "" >&2
305
+ echo "PREFLIGHT FAILED — release commit created but NOT tagged." >&2
306
+ echo " Commit: ${RELEASE_SHA:0:7}" >&2
307
+ echo " Fix the failures, amend the commit, and re-run:" >&2
308
+ echo " bash scripts/release-bump.sh --target-version ${TARGET_VERSION}" >&2
309
+ echo " Or skip preflight if already verified:" >&2
310
+ echo " bash scripts/release-bump.sh --target-version ${TARGET_VERSION} --skip-preflight" >&2
311
+ exit 1
312
+ fi
313
+
314
+ echo " Inline preflight gate passed — proceeding to tag"
315
+ fi
316
+
253
317
  # 9. Create annotated tag
254
- echo "[9/9] Creating annotated tag..."
318
+ echo "[9/10] Creating annotated tag..."
255
319
  git tag -a "v${TARGET_VERSION}" -m "v${TARGET_VERSION}"
256
320
  TAG_SHA=$(git rev-parse "v${TARGET_VERSION}")
257
321
  if [[ -z "$TAG_SHA" ]]; then
@@ -276,6 +340,10 @@ echo "Release identity created successfully."
276
340
  echo " Version: ${TARGET_VERSION}"
277
341
  echo " Commit: ${RELEASE_SHA:0:7}"
278
342
  echo " Tag: v${TARGET_VERSION}"
343
+ if [[ "$SKIP_PREFLIGHT" -eq 1 ]]; then
344
+ echo ""
345
+ echo "WARNING: Inline preflight was skipped. Verify before pushing:"
346
+ echo " npm run preflight:release:strict -- --target-version ${TARGET_VERSION}"
347
+ fi
279
348
  echo ""
280
- echo "Next: npm run preflight:release:strict -- --target-version ${TARGET_VERSION}"
281
- echo "Then: git push origin main --follow-tags"
349
+ echo "Next: git push origin main --follow-tags"
@@ -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 "FAIL: could not push to ${CANONICAL_TAP_REPO}" >&2
211
- exit 1
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
 
@@ -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('');
@@ -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',
@@ -44,6 +44,7 @@ const ORCHESTRATOR_STATE_FILES = [
44
44
  '.agentxchain/events.jsonl',
45
45
  '.agentxchain/notification-audit.jsonl',
46
46
  '.agentxchain/schedule-state.json',
47
+ '.agentxchain/schedule-daemon.json',
47
48
  'TALK.md',
48
49
  ];
49
50
 
@@ -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
+ }