agentxchain 2.2.0 → 2.4.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/README.md CHANGED
@@ -26,6 +26,22 @@ Or run without installing:
26
26
  npx agentxchain init --governed -y
27
27
  ```
28
28
 
29
+ ## Testing
30
+
31
+ The CLI currently uses two runners on purpose: a 36-file Vitest coexistence slice for fast feedback and `node --test` for the full suite.
32
+
33
+ ```bash
34
+ npm run test:vitest
35
+ npm run test:node
36
+ npm test
37
+ ```
38
+
39
+ - `npm run test:vitest`: the current 36-file Vitest slice
40
+ - `npm run test:node`: full integration, subprocess, and E2E suite
41
+ - `npm test`: both runners in sequence; this is the CI requirement today
42
+
43
+ Duplicate execution remains intentional for the current 36-file slice until a later slice explicitly changes the redundancy model. For watch mode, run `npx vitest`.
44
+
29
45
  ## Quick Start
30
46
 
31
47
  ### Governed workflow
@@ -51,6 +67,7 @@ Built-in governed templates:
51
67
  - `generic`: baseline governed scaffold
52
68
  - `api-service`: API contract, operational readiness, error budget
53
69
  - `cli-tool`: command surface, platform support, distribution checklist
70
+ - `library`: public API, compatibility policy, release and adoption checklist
54
71
  - `web-app`: user flows, UI acceptance, browser support
55
72
 
56
73
  `step` writes a turn-scoped bundle under `.agentxchain/dispatch/turns/<turn_id>/` and expects a staged result at `.agentxchain/staging/<turn_id>/turn-result.json`. Typical continuation:
@@ -79,6 +79,7 @@ import {
79
79
  } from '../src/commands/plugin.js';
80
80
  import { templateSetCommand } from '../src/commands/template-set.js';
81
81
  import { templateListCommand } from '../src/commands/template-list.js';
82
+ import { templateValidateCommand } from '../src/commands/template-validate.js';
82
83
  import {
83
84
  multiInitCommand,
84
85
  multiStatusCommand,
@@ -86,6 +87,14 @@ import {
86
87
  multiApproveGateCommand,
87
88
  multiResyncCommand,
88
89
  } from '../src/commands/multi.js';
90
+ import { intakeRecordCommand } from '../src/commands/intake-record.js';
91
+ import { intakeTriageCommand } from '../src/commands/intake-triage.js';
92
+ import { intakeApproveCommand } from '../src/commands/intake-approve.js';
93
+ import { intakePlanCommand } from '../src/commands/intake-plan.js';
94
+ import { intakeStartCommand } from '../src/commands/intake-start.js';
95
+ import { intakeScanCommand } from '../src/commands/intake-scan.js';
96
+ import { intakeResolveCommand } from '../src/commands/intake-resolve.js';
97
+ import { intakeStatusCommand } from '../src/commands/intake-status.js';
89
98
 
90
99
  const __dirname = dirname(fileURLToPath(import.meta.url));
91
100
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -102,7 +111,7 @@ program
102
111
  .description('Create a new AgentXchain project folder')
103
112
  .option('-y, --yes', 'Skip prompts, use defaults')
104
113
  .option('--governed', 'Create a governed project (orchestrator-owned state)')
105
- .option('--template <id>', 'Governed scaffold template: generic, api-service, cli-tool, web-app')
114
+ .option('--template <id>', 'Governed scaffold template: generic, api-service, cli-tool, library, web-app')
106
115
  .option('--schema-version <version>', 'Schema version (3 for legacy, or use --governed for current)')
107
116
  .action(initCommand);
108
117
 
@@ -328,6 +337,12 @@ templateCmd
328
337
  .option('-j, --json', 'Output as JSON')
329
338
  .action(templateListCommand);
330
339
 
340
+ templateCmd
341
+ .command('validate')
342
+ .description('Validate the built-in governed template registry and current project template binding')
343
+ .option('-j, --json', 'Output as JSON')
344
+ .action(templateValidateCommand);
345
+
331
346
  const multiCmd = program
332
347
  .command('multi')
333
348
  .description('Multi-repo coordinator orchestration');
@@ -363,4 +378,85 @@ multiCmd
363
378
  .option('--dry-run', 'Detect divergence without resyncing')
364
379
  .action(multiResyncCommand);
365
380
 
381
+ // --- Intake (v3) -----------------------------------------------------------
382
+
383
+ const intakeCmd = program
384
+ .command('intake')
385
+ .description('Continuous governed delivery intake — record signals, triage intents, view status');
386
+
387
+ intakeCmd
388
+ .command('record')
389
+ .description('Record a delivery trigger event and create a detected intent')
390
+ .option('--file <path>', 'Read event payload from a JSON file')
391
+ .option('--stdin', 'Read event payload from stdin')
392
+ .option('--source <source>', 'Inline event source (manual, ci_failure, git_ref_change, schedule)')
393
+ .option('--signal <json>', 'Inline signal object (JSON string, requires --source)')
394
+ .option('--evidence <json>', 'Inline evidence entry (JSON string, requires --source)')
395
+ .option('--category <category>', 'Optional event category override')
396
+ .option('-j, --json', 'Output as JSON')
397
+ .action(intakeRecordCommand);
398
+
399
+ intakeCmd
400
+ .command('triage')
401
+ .description('Triage a detected intent — set priority, template, charter, and acceptance')
402
+ .requiredOption('--intent <id>', 'Intent ID to triage')
403
+ .option('--priority <level>', 'Priority level (p0, p1, p2, p3)')
404
+ .option('--template <id>', 'Governed template (generic, api-service, cli-tool, library, web-app)')
405
+ .option('--charter <text>', 'Delivery charter text')
406
+ .option('--acceptance <text>', 'Comma-separated acceptance criteria')
407
+ .option('--suppress', 'Suppress the intent instead of triaging')
408
+ .option('--reject', 'Reject a triaged intent')
409
+ .option('--reason <text>', 'Reason for suppress or reject')
410
+ .option('-j, --json', 'Output as JSON')
411
+ .action(intakeTriageCommand);
412
+
413
+ intakeCmd
414
+ .command('approve')
415
+ .description('Approve a triaged intent for planning')
416
+ .option('--intent <id>', 'Intent ID to approve')
417
+ .option('--approver <name>', 'Name of the approving authority', 'operator')
418
+ .option('--reason <text>', 'Reason for approval')
419
+ .option('-j, --json', 'Output as JSON')
420
+ .action(intakeApproveCommand);
421
+
422
+ intakeCmd
423
+ .command('plan')
424
+ .description('Generate planning artifacts and transition an approved intent to planned')
425
+ .option('--intent <id>', 'Intent ID to plan')
426
+ .option('--project-name <name>', 'Project name for template substitution')
427
+ .option('--force', 'Overwrite existing planning artifacts')
428
+ .option('-j, --json', 'Output as JSON')
429
+ .action(intakePlanCommand);
430
+
431
+ intakeCmd
432
+ .command('start')
433
+ .description('Start governed execution for a planned intent')
434
+ .option('--intent <id>', 'Intent ID to start')
435
+ .option('--role <role>', 'Override the default entry role for the governed phase')
436
+ .option('-j, --json', 'Output as JSON')
437
+ .action(intakeStartCommand);
438
+
439
+ intakeCmd
440
+ .command('scan')
441
+ .description('Scan a structured source snapshot into intake events')
442
+ .requiredOption('--source <id>', 'Source type: ci_failure, git_ref_change, schedule')
443
+ .option('--file <path>', 'Path to snapshot JSON file')
444
+ .option('--stdin', 'Read snapshot from stdin')
445
+ .option('-j, --json', 'Output as JSON')
446
+ .action(intakeScanCommand);
447
+
448
+ intakeCmd
449
+ .command('resolve')
450
+ .description('Resolve an executing intent by reading the governed run outcome')
451
+ .option('--intent <id>', 'Intent ID to resolve')
452
+ .option('-j, --json', 'Output as JSON')
453
+ .action(intakeResolveCommand);
454
+
455
+ intakeCmd
456
+ .command('status')
457
+ .description('Show current intake state — events, intents, and aggregate counts')
458
+ .option('--intent <id>', 'Show detail for a specific intent')
459
+ .option('-j, --json', 'Output as JSON')
460
+ .action(intakeStatusCommand);
461
+
366
462
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,9 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "dev": "node bin/agentxchain.js",
18
- "test": "node --test test/*.test.js",
18
+ "test": "npm run test:vitest && npm run test:node",
19
+ "test:vitest": "vitest run --reporter=verbose",
20
+ "test:node": "node --test test/*.test.js",
19
21
  "preflight:release": "bash scripts/release-preflight.sh",
20
22
  "preflight:release:strict": "bash scripts/release-preflight.sh --strict",
21
23
  "build:macos": "bun build bin/agentxchain.js --compile --target=bun-darwin-arm64 --outfile=dist/agentxchain-macos-arm64",
@@ -46,12 +48,17 @@
46
48
  "homepage": "https://agentxchain.dev",
47
49
  "dependencies": {
48
50
  "@anthropic-ai/tokenizer": "0.0.4",
51
+ "@modelcontextprotocol/sdk": "^1.29.0",
49
52
  "chalk": "^5.4.0",
50
53
  "commander": "^13.0.0",
51
54
  "inquirer": "^12.0.0",
52
- "ora": "^8.0.0"
55
+ "ora": "^8.0.0",
56
+ "zod": "^4.3.6"
53
57
  },
54
58
  "engines": {
55
59
  "node": ">=18.17.0 || >=20.5.0"
60
+ },
61
+ "devDependencies": {
62
+ "vitest": "^4.1.2"
56
63
  }
57
64
  }
@@ -58,16 +58,21 @@ echo "Publishing ${PACKAGE_NAME}@${RELEASE_VERSION} from ${TAG}"
58
58
  echo "Running strict release preflight..."
59
59
  bash scripts/release-preflight.sh --strict --target-version "${RELEASE_VERSION}"
60
60
 
61
- echo "Running npm publish..."
62
- if [[ -n "${NPM_TOKEN:-}" ]]; then
63
- echo "Publish auth mode: token"
64
- TMP_NPMRC="$(mktemp "${TMPDIR:-/tmp}/agentxchain-npmrc.XXXXXX")"
65
- chmod 600 "$TMP_NPMRC"
66
- printf '%s\n' "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > "$TMP_NPMRC"
67
- NPM_CONFIG_USERCONFIG="$TMP_NPMRC" npm publish --access public
61
+ EXISTING_VERSION="$(npm view "${PACKAGE_NAME}@${RELEASE_VERSION}" version 2>/dev/null || true)"
62
+ if [[ "$EXISTING_VERSION" == "$RELEASE_VERSION" ]]; then
63
+ echo "Registry already serves ${PACKAGE_NAME}@${RELEASE_VERSION}; skipping npm publish and proceeding to verification."
68
64
  else
69
- echo "Publish auth mode: trusted publishing (OIDC)"
70
- npm publish --access public
65
+ echo "Running npm publish..."
66
+ if [[ -n "${NPM_TOKEN:-}" ]]; then
67
+ echo "Publish auth mode: token"
68
+ TMP_NPMRC="$(mktemp "${TMPDIR:-/tmp}/agentxchain-npmrc.XXXXXX")"
69
+ chmod 600 "$TMP_NPMRC"
70
+ printf '%s\n' "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > "$TMP_NPMRC"
71
+ NPM_CONFIG_USERCONFIG="$TMP_NPMRC" npm publish --access public
72
+ else
73
+ echo "Publish auth mode: trusted publishing (OIDC)"
74
+ npm publish --access public
75
+ fi
71
76
  fi
72
77
 
73
78
  echo "Verifying registry visibility..."
@@ -11,7 +11,7 @@ cd "$CLI_DIR"
11
11
 
12
12
  TARGET_VERSION=""
13
13
  TAG=""
14
- RETRY_ATTEMPTS="${RELEASE_POSTFLIGHT_RETRY_ATTEMPTS:-6}"
14
+ RETRY_ATTEMPTS="${RELEASE_POSTFLIGHT_RETRY_ATTEMPTS:-12}"
15
15
  RETRY_DELAY_SECONDS="${RELEASE_POSTFLIGHT_RETRY_DELAY_SECONDS:-10}"
16
16
 
17
17
  usage() {
@@ -75,6 +75,7 @@ FAIL=0
75
75
  TARBALL_URL=""
76
76
  REGISTRY_CHECKSUM=""
77
77
  PACKAGE_NAME="$(node -e "console.log(JSON.parse(require('fs').readFileSync('package.json', 'utf8')).name)")"
78
+ PACKAGE_BIN_NAME="$(node -e "const pkg = JSON.parse(require('fs').readFileSync('package.json', 'utf8')); if (typeof pkg.bin === 'string') { console.log(pkg.name); process.exit(0); } const names = Object.keys(pkg.bin || {}); if (names.length !== 1) { console.error('package.json bin must declare exactly one entry'); process.exit(1); } console.log(names[0]);")"
78
79
 
79
80
  pass() { PASS=$((PASS + 1)); echo " PASS: $1"; }
80
81
  fail() { FAIL=$((FAIL + 1)); echo " FAIL: $1"; }
@@ -96,6 +97,45 @@ trim_last_line() {
96
97
  printf '%s\n' "$1" | awk 'NF { line=$0 } END { gsub(/^[[:space:]]+|[[:space:]]+$/, "", line); print line }'
97
98
  }
98
99
 
100
+ run_install_smoke() {
101
+ if [[ -z "$TARBALL_URL" ]]; then
102
+ echo "registry tarball metadata unavailable for install smoke" >&2
103
+ return 1
104
+ fi
105
+
106
+ local smoke_root
107
+ local bin_path
108
+ local install_status
109
+ local version_status
110
+
111
+ smoke_root="$(mktemp -d "${TMPDIR:-/tmp}/agentxchain-postflight.XXXXXX")"
112
+ bin_path="${smoke_root}/bin/${PACKAGE_BIN_NAME}"
113
+
114
+ # Isolate the install from CI auth environment (OIDC tokens from actions/setup-node
115
+ # are scoped for publish, not read, and can cause npm install to fail on public packages).
116
+ local smoke_npmrc="${smoke_root}/.npmrc"
117
+ echo "registry=https://registry.npmjs.org/" > "$smoke_npmrc"
118
+
119
+ env -u NODE_AUTH_TOKEN NPM_CONFIG_USERCONFIG="$smoke_npmrc" \
120
+ npm install --global --prefix "$smoke_root" "$TARBALL_URL" >/dev/null 2>&1
121
+ install_status=$?
122
+ if [[ "$install_status" -ne 0 ]]; then
123
+ rm -rf "$smoke_root"
124
+ return "$install_status"
125
+ fi
126
+
127
+ if [[ ! -x "$bin_path" ]]; then
128
+ echo "installed binary missing at ${bin_path}" >&2
129
+ rm -rf "$smoke_root"
130
+ return 1
131
+ fi
132
+
133
+ "$bin_path" --version
134
+ version_status=$?
135
+ rm -rf "$smoke_root"
136
+ return "$version_status"
137
+ }
138
+
99
139
  run_with_retry() {
100
140
  local __output_var="$1"
101
141
  local description="$2"
@@ -201,7 +241,7 @@ else
201
241
  fi
202
242
 
203
243
  echo "[5/5] Install smoke"
204
- if run_with_retry EXEC_OUTPUT "install smoke" nonempty "" npm exec --yes --package "${PACKAGE_NAME}@${TARGET_VERSION}" -- agentxchain --version; then
244
+ if run_with_retry EXEC_OUTPUT "install smoke" nonempty "" run_install_smoke; then
205
245
  EXEC_VERSION="$(trim_last_line "$EXEC_OUTPUT")"
206
246
  if [[ "$EXEC_VERSION" == "$TARGET_VERSION" ]]; then
207
247
  pass "published CLI executes and reports ${TARGET_VERSION}"
@@ -483,6 +483,7 @@ async function initGoverned(opts) {
483
483
  console.error(' generic Default governed scaffold');
484
484
  console.error(' api-service Governed scaffold for a backend service');
485
485
  console.error(' cli-tool Governed scaffold for a CLI tool');
486
+ console.error(' library Governed scaffold for a reusable package');
486
487
  console.error(' web-app Governed scaffold for a web application');
487
488
  process.exit(1);
488
489
  }
@@ -0,0 +1,44 @@
1
+ import chalk from 'chalk';
2
+ import { findProjectRoot } from '../lib/config.js';
3
+ import { approveIntent } from '../lib/intake.js';
4
+
5
+ export async function intakeApproveCommand(opts) {
6
+ const root = findProjectRoot(process.cwd());
7
+ if (!root) {
8
+ if (opts.json) {
9
+ console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
10
+ } else {
11
+ console.log(chalk.red('agentxchain.json not found'));
12
+ }
13
+ process.exit(2);
14
+ }
15
+
16
+ if (!opts.intent) {
17
+ const msg = '--intent <id> is required';
18
+ if (opts.json) {
19
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
20
+ } else {
21
+ console.log(chalk.red(msg));
22
+ }
23
+ process.exit(1);
24
+ }
25
+
26
+ const result = approveIntent(root, opts.intent, {
27
+ approver: opts.approver || undefined,
28
+ reason: opts.reason || undefined,
29
+ });
30
+
31
+ if (opts.json) {
32
+ console.log(JSON.stringify(result, null, 2));
33
+ } else if (result.ok) {
34
+ console.log('');
35
+ console.log(chalk.green(` Approved intent ${result.intent.intent_id}`));
36
+ console.log(chalk.dim(` Approver: ${result.intent.approved_by}`));
37
+ console.log(chalk.dim(` Status: triaged → approved`));
38
+ console.log('');
39
+ } else {
40
+ console.log(chalk.red(` ${result.error}`));
41
+ }
42
+
43
+ process.exit(result.exitCode);
44
+ }
@@ -0,0 +1,62 @@
1
+ import chalk from 'chalk';
2
+ import { findProjectRoot } from '../lib/config.js';
3
+ import { planIntent } from '../lib/intake.js';
4
+ import { basename } from 'node:path';
5
+
6
+ export async function intakePlanCommand(opts) {
7
+ const root = findProjectRoot(process.cwd());
8
+ if (!root) {
9
+ if (opts.json) {
10
+ console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
11
+ } else {
12
+ console.log(chalk.red('agentxchain.json not found'));
13
+ }
14
+ process.exit(2);
15
+ }
16
+
17
+ if (!opts.intent) {
18
+ const msg = '--intent <id> is required';
19
+ if (opts.json) {
20
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
21
+ } else {
22
+ console.log(chalk.red(msg));
23
+ }
24
+ process.exit(1);
25
+ }
26
+
27
+ const result = planIntent(root, opts.intent, {
28
+ projectName: opts.projectName || undefined,
29
+ force: opts.force || false,
30
+ });
31
+
32
+ if (opts.json) {
33
+ console.log(JSON.stringify(result, null, 2));
34
+ } else if (result.ok) {
35
+ console.log('');
36
+ console.log(chalk.green(` Planned intent ${result.intent.intent_id}`));
37
+ console.log(chalk.dim(` Template: ${result.intent.template}`));
38
+ if (result.artifacts_generated.length > 0) {
39
+ console.log(chalk.dim(' Generated planning artifacts:'));
40
+ for (const a of result.artifacts_generated) {
41
+ console.log(chalk.dim(` ${a}`));
42
+ }
43
+ } else {
44
+ console.log(chalk.dim(' No template-specific planning artifacts to generate.'));
45
+ }
46
+ console.log(chalk.dim(` Status: approved → planned`));
47
+ console.log('');
48
+ } else if (result.conflicts) {
49
+ console.log('');
50
+ console.log(chalk.red(` Cannot plan intent ${opts.intent}`));
51
+ console.log(chalk.red(' Existing planning artifacts would be overwritten:'));
52
+ for (const c of result.conflicts) {
53
+ console.log(chalk.red(` ${c}`));
54
+ }
55
+ console.log(chalk.yellow(' Use --force to overwrite, or remove the existing files first.'));
56
+ console.log('');
57
+ } else {
58
+ console.log(chalk.red(` ${result.error}`));
59
+ }
60
+
61
+ process.exit(result.exitCode);
62
+ }
@@ -0,0 +1,86 @@
1
+ import chalk from 'chalk';
2
+ import { readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { findProjectRoot } from '../lib/config.js';
5
+ import { recordEvent } from '../lib/intake.js';
6
+
7
+ export async function intakeRecordCommand(opts) {
8
+ const root = findProjectRoot(process.cwd());
9
+ if (!root) {
10
+ if (opts.json) {
11
+ console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
12
+ } else {
13
+ console.log(chalk.red('agentxchain.json not found'));
14
+ }
15
+ process.exit(2);
16
+ }
17
+
18
+ let payload;
19
+ try {
20
+ payload = parsePayload(opts);
21
+ } catch (err) {
22
+ if (opts.json) {
23
+ console.log(JSON.stringify({ ok: false, error: err.message }, null, 2));
24
+ } else {
25
+ console.log(chalk.red(err.message));
26
+ }
27
+ process.exit(1);
28
+ }
29
+
30
+ const result = recordEvent(root, payload);
31
+
32
+ if (opts.json) {
33
+ console.log(JSON.stringify(result, null, 2));
34
+ } else if (result.ok) {
35
+ if (result.deduplicated) {
36
+ console.log('');
37
+ console.log(chalk.yellow(` Event already recorded: ${result.event.event_id} (deduplicated)`));
38
+ if (result.intent) {
39
+ console.log(chalk.dim(` Linked intent: ${result.intent.intent_id} (${result.intent.status})`));
40
+ }
41
+ } else {
42
+ console.log('');
43
+ console.log(chalk.green(` Recorded event ${result.event.event_id}`));
44
+ console.log(chalk.green(` Created intent ${result.intent.intent_id} (detected)`));
45
+ }
46
+ console.log('');
47
+ } else {
48
+ console.log(chalk.red(` ${result.error}`));
49
+ }
50
+
51
+ process.exit(result.exitCode);
52
+ }
53
+
54
+ function parsePayload(opts) {
55
+ if (opts.file) {
56
+ const raw = readFileSync(resolve(opts.file), 'utf8');
57
+ return JSON.parse(raw);
58
+ }
59
+
60
+ if (opts.stdin) {
61
+ const raw = readFileSync(0, 'utf8');
62
+ return JSON.parse(raw);
63
+ }
64
+
65
+ if (opts.source) {
66
+ if (!opts.signal) throw new Error('--source requires --signal');
67
+ if (!opts.evidence) throw new Error('--source requires --evidence');
68
+
69
+ const signal = JSON.parse(opts.signal);
70
+ let evidence;
71
+ if (Array.isArray(opts.evidence)) {
72
+ evidence = opts.evidence.map(e => JSON.parse(e));
73
+ } else {
74
+ evidence = [JSON.parse(opts.evidence)];
75
+ }
76
+
77
+ return {
78
+ source: opts.source,
79
+ signal,
80
+ evidence,
81
+ category: opts.category || undefined,
82
+ };
83
+ }
84
+
85
+ throw new Error('one of --file, --stdin, or --source is required');
86
+ }
@@ -0,0 +1,45 @@
1
+ import chalk from 'chalk';
2
+ import { findProjectRoot } from '../lib/config.js';
3
+ import { resolveIntent } from '../lib/intake.js';
4
+
5
+ export async function intakeResolveCommand(opts) {
6
+ const root = findProjectRoot(process.cwd());
7
+ if (!root) {
8
+ if (opts.json) {
9
+ console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
10
+ } else {
11
+ console.log(chalk.red('agentxchain.json not found'));
12
+ }
13
+ process.exit(2);
14
+ }
15
+
16
+ if (!opts.intent) {
17
+ const msg = '--intent <id> is required';
18
+ if (opts.json) {
19
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
20
+ } else {
21
+ console.log(chalk.red(msg));
22
+ }
23
+ process.exit(1);
24
+ }
25
+
26
+ const result = resolveIntent(root, opts.intent);
27
+
28
+ if (opts.json) {
29
+ console.log(JSON.stringify(result, null, 2));
30
+ } else if (result.ok) {
31
+ console.log('');
32
+ if (result.no_change) {
33
+ console.log(chalk.yellow(` Intent ${opts.intent} — run still ${result.run_outcome}, no transition`));
34
+ } else {
35
+ console.log(chalk.green(` Resolved intent ${opts.intent}`));
36
+ console.log(chalk.dim(` ${result.previous_status} → ${result.new_status}`));
37
+ console.log(chalk.dim(` Run outcome: ${result.run_outcome}`));
38
+ }
39
+ console.log('');
40
+ } else {
41
+ console.log(chalk.red(` ${result.error}`));
42
+ }
43
+
44
+ process.exit(result.exitCode);
45
+ }
@@ -0,0 +1,87 @@
1
+ import chalk from 'chalk';
2
+ import { readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { findProjectRoot } from '../lib/config.js';
5
+ import { scanSource, SCAN_SOURCES } from '../lib/intake.js';
6
+
7
+ export async function intakeScanCommand(opts) {
8
+ const root = findProjectRoot(process.cwd());
9
+ if (!root) {
10
+ if (opts.json) {
11
+ console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
12
+ } else {
13
+ console.log(chalk.red('agentxchain.json not found'));
14
+ }
15
+ process.exit(2);
16
+ }
17
+
18
+ if (!opts.source) {
19
+ const msg = `--source is required. Supported scan sources: ${SCAN_SOURCES.join(', ')}`;
20
+ if (opts.json) {
21
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
22
+ } else {
23
+ console.log(chalk.red(msg));
24
+ }
25
+ process.exit(1);
26
+ }
27
+
28
+ if (!opts.file && !opts.stdin) {
29
+ const msg = 'one of --file or --stdin is required';
30
+ if (opts.json) {
31
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
32
+ } else {
33
+ console.log(chalk.red(msg));
34
+ }
35
+ process.exit(1);
36
+ }
37
+
38
+ if (opts.file && opts.stdin) {
39
+ const msg = '--file and --stdin are mutually exclusive';
40
+ if (opts.json) {
41
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
42
+ } else {
43
+ console.log(chalk.red(msg));
44
+ }
45
+ process.exit(1);
46
+ }
47
+
48
+ let snapshot;
49
+ try {
50
+ let raw;
51
+ if (opts.file) {
52
+ raw = readFileSync(resolve(opts.file), 'utf8');
53
+ } else {
54
+ raw = readFileSync(0, 'utf8');
55
+ }
56
+ snapshot = JSON.parse(raw);
57
+ } catch (err) {
58
+ const msg = opts.file
59
+ ? `failed to read snapshot from ${opts.file}: ${err.message}`
60
+ : `failed to read snapshot from stdin: ${err.message}`;
61
+ if (opts.json) {
62
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
63
+ } else {
64
+ console.log(chalk.red(msg));
65
+ }
66
+ process.exit(opts.file && err.code === 'ENOENT' ? 2 : 1);
67
+ }
68
+
69
+ const result = scanSource(root, opts.source, snapshot);
70
+
71
+ if (opts.json) {
72
+ console.log(JSON.stringify(result, null, 2));
73
+ } else if (result.ok) {
74
+ console.log('');
75
+ console.log(chalk.green(` Scan complete: ${result.scanned} item(s) processed`));
76
+ console.log(` Created: ${result.created}`);
77
+ console.log(` Deduplicated: ${result.deduplicated}`);
78
+ if (result.rejected > 0) {
79
+ console.log(chalk.yellow(` Rejected: ${result.rejected}`));
80
+ }
81
+ console.log('');
82
+ } else {
83
+ console.log(chalk.red(` ${result.error}`));
84
+ }
85
+
86
+ process.exit(result.exitCode);
87
+ }