agentxchain 0.8.8 → 2.2.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.
Files changed (74) hide show
  1. package/README.md +136 -136
  2. package/bin/agentxchain.js +186 -5
  3. package/dashboard/app.js +305 -0
  4. package/dashboard/components/blocked.js +145 -0
  5. package/dashboard/components/cross-repo.js +126 -0
  6. package/dashboard/components/gate.js +311 -0
  7. package/dashboard/components/hooks.js +177 -0
  8. package/dashboard/components/initiative.js +147 -0
  9. package/dashboard/components/ledger.js +165 -0
  10. package/dashboard/components/timeline.js +222 -0
  11. package/dashboard/index.html +352 -0
  12. package/package.json +14 -6
  13. package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
  14. package/scripts/publish-from-tag.sh +88 -0
  15. package/scripts/release-postflight.sh +231 -0
  16. package/scripts/release-preflight.sh +167 -0
  17. package/src/commands/accept-turn.js +160 -0
  18. package/src/commands/approve-completion.js +80 -0
  19. package/src/commands/approve-transition.js +85 -0
  20. package/src/commands/dashboard.js +70 -0
  21. package/src/commands/init.js +516 -0
  22. package/src/commands/migrate.js +348 -0
  23. package/src/commands/multi.js +549 -0
  24. package/src/commands/plugin.js +157 -0
  25. package/src/commands/reject-turn.js +204 -0
  26. package/src/commands/resume.js +389 -0
  27. package/src/commands/status.js +196 -3
  28. package/src/commands/step.js +947 -0
  29. package/src/commands/template-list.js +33 -0
  30. package/src/commands/template-set.js +279 -0
  31. package/src/commands/validate.js +20 -11
  32. package/src/commands/verify.js +71 -0
  33. package/src/lib/adapters/api-proxy-adapter.js +1076 -0
  34. package/src/lib/adapters/local-cli-adapter.js +337 -0
  35. package/src/lib/adapters/manual-adapter.js +169 -0
  36. package/src/lib/blocked-state.js +94 -0
  37. package/src/lib/config.js +97 -1
  38. package/src/lib/context-compressor.js +121 -0
  39. package/src/lib/context-section-parser.js +220 -0
  40. package/src/lib/coordinator-acceptance.js +428 -0
  41. package/src/lib/coordinator-config.js +461 -0
  42. package/src/lib/coordinator-dispatch.js +276 -0
  43. package/src/lib/coordinator-gates.js +487 -0
  44. package/src/lib/coordinator-hooks.js +239 -0
  45. package/src/lib/coordinator-recovery.js +523 -0
  46. package/src/lib/coordinator-state.js +365 -0
  47. package/src/lib/cross-repo-context.js +247 -0
  48. package/src/lib/dashboard/bridge-server.js +284 -0
  49. package/src/lib/dashboard/file-watcher.js +93 -0
  50. package/src/lib/dashboard/state-reader.js +96 -0
  51. package/src/lib/dispatch-bundle.js +568 -0
  52. package/src/lib/dispatch-manifest.js +252 -0
  53. package/src/lib/gate-evaluator.js +285 -0
  54. package/src/lib/governed-state.js +2139 -0
  55. package/src/lib/governed-templates.js +145 -0
  56. package/src/lib/hook-runner.js +788 -0
  57. package/src/lib/normalized-config.js +539 -0
  58. package/src/lib/plugin-config-schema.js +192 -0
  59. package/src/lib/plugins.js +692 -0
  60. package/src/lib/protocol-conformance.js +291 -0
  61. package/src/lib/reference-conformance-adapter.js +858 -0
  62. package/src/lib/repo-observer.js +597 -0
  63. package/src/lib/repo.js +0 -31
  64. package/src/lib/schema.js +121 -0
  65. package/src/lib/schemas/turn-result.schema.json +205 -0
  66. package/src/lib/token-budget.js +206 -0
  67. package/src/lib/token-counter.js +27 -0
  68. package/src/lib/turn-paths.js +67 -0
  69. package/src/lib/turn-result-validator.js +496 -0
  70. package/src/lib/validation.js +137 -0
  71. package/src/templates/governed/api-service.json +31 -0
  72. package/src/templates/governed/cli-tool.json +30 -0
  73. package/src/templates/governed/generic.json +10 -0
  74. package/src/templates/governed/web-app.json +30 -0
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env bash
2
+ # Release postflight — run this after publish succeeds.
3
+ # Verifies: release tag exists, npm registry serves the version, metadata is present,
4
+ # and the published package can execute its CLI entrypoint.
5
+ # Usage: bash scripts/release-postflight.sh --target-version <semver> [--tag vX.Y.Z]
6
+ set -uo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ CLI_DIR="${SCRIPT_DIR}/.."
10
+ cd "$CLI_DIR"
11
+
12
+ TARGET_VERSION=""
13
+ TAG=""
14
+ RETRY_ATTEMPTS="${RELEASE_POSTFLIGHT_RETRY_ATTEMPTS:-6}"
15
+ RETRY_DELAY_SECONDS="${RELEASE_POSTFLIGHT_RETRY_DELAY_SECONDS:-10}"
16
+
17
+ usage() {
18
+ echo "Usage: bash scripts/release-postflight.sh --target-version <semver> [--tag vX.Y.Z]" >&2
19
+ }
20
+
21
+ while [[ $# -gt 0 ]]; do
22
+ case "$1" in
23
+ --target-version)
24
+ if [[ -z "${2:-}" ]]; then
25
+ echo "Error: --target-version requires a semver argument" >&2
26
+ usage
27
+ exit 1
28
+ fi
29
+ if ! [[ "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
30
+ echo "Invalid semver: $2" >&2
31
+ usage
32
+ exit 1
33
+ fi
34
+ TARGET_VERSION="$2"
35
+ shift 2
36
+ ;;
37
+ --tag)
38
+ if [[ -z "${2:-}" ]]; then
39
+ echo "Error: --tag requires a git tag argument" >&2
40
+ usage
41
+ exit 1
42
+ fi
43
+ TAG="$2"
44
+ shift 2
45
+ ;;
46
+ *)
47
+ usage
48
+ exit 1
49
+ ;;
50
+ esac
51
+ done
52
+
53
+ if [[ -z "$TARGET_VERSION" ]]; then
54
+ echo "Error: --target-version is required" >&2
55
+ usage
56
+ exit 1
57
+ fi
58
+
59
+ if [[ -z "$TAG" ]]; then
60
+ TAG="v${TARGET_VERSION}"
61
+ fi
62
+
63
+ if ! [[ "$RETRY_ATTEMPTS" =~ ^[0-9]+$ ]] || [[ "$RETRY_ATTEMPTS" -lt 1 ]]; then
64
+ echo "Error: RELEASE_POSTFLIGHT_RETRY_ATTEMPTS must be a positive integer" >&2
65
+ exit 1
66
+ fi
67
+
68
+ if ! [[ "$RETRY_DELAY_SECONDS" =~ ^[0-9]+$ ]]; then
69
+ echo "Error: RELEASE_POSTFLIGHT_RETRY_DELAY_SECONDS must be a non-negative integer" >&2
70
+ exit 1
71
+ fi
72
+
73
+ PASS=0
74
+ FAIL=0
75
+ TARBALL_URL=""
76
+ REGISTRY_CHECKSUM=""
77
+ PACKAGE_NAME="$(node -e "console.log(JSON.parse(require('fs').readFileSync('package.json', 'utf8')).name)")"
78
+
79
+ pass() { PASS=$((PASS + 1)); echo " PASS: $1"; }
80
+ fail() { FAIL=$((FAIL + 1)); echo " FAIL: $1"; }
81
+
82
+ run_and_capture() {
83
+ local __var_name="$1"
84
+ shift
85
+
86
+ local captured_output
87
+ local status
88
+ captured_output="$("$@" 2>&1)"
89
+ status=$?
90
+
91
+ printf -v "$__var_name" '%s' "$captured_output"
92
+ return "$status"
93
+ }
94
+
95
+ trim_last_line() {
96
+ printf '%s\n' "$1" | awk 'NF { line=$0 } END { gsub(/^[[:space:]]+|[[:space:]]+$/, "", line); print line }'
97
+ }
98
+
99
+ run_with_retry() {
100
+ local __output_var="$1"
101
+ local description="$2"
102
+ local success_mode="$3"
103
+ local expected_value="$4"
104
+ shift 4
105
+
106
+ local output=""
107
+ local status=0
108
+ local value=""
109
+ local attempt=1
110
+
111
+ while [[ "$attempt" -le "$RETRY_ATTEMPTS" ]]; do
112
+ if run_and_capture output "$@"; then
113
+ status=0
114
+ else
115
+ status=$?
116
+ fi
117
+
118
+ value="$(trim_last_line "$output")"
119
+
120
+ case "$success_mode" in
121
+ equals)
122
+ if [[ "$status" -eq 0 && "$value" == "$expected_value" ]]; then
123
+ printf -v "$__output_var" '%s' "$output"
124
+ return 0
125
+ fi
126
+ ;;
127
+ nonempty)
128
+ if [[ "$status" -eq 0 && -n "$value" ]]; then
129
+ printf -v "$__output_var" '%s' "$output"
130
+ return 0
131
+ fi
132
+ ;;
133
+ *)
134
+ echo "Error: unsupported retry success mode '${success_mode}'" >&2
135
+ exit 1
136
+ ;;
137
+ esac
138
+
139
+ if [[ "$attempt" -lt "$RETRY_ATTEMPTS" ]]; then
140
+ echo " INFO: ${description} not ready (attempt ${attempt}/${RETRY_ATTEMPTS}); retrying in ${RETRY_DELAY_SECONDS}s..."
141
+ sleep "$RETRY_DELAY_SECONDS"
142
+ fi
143
+ attempt=$((attempt + 1))
144
+ done
145
+
146
+ printf -v "$__output_var" '%s' "$output"
147
+ return 1
148
+ }
149
+
150
+ echo "AgentXchain v${TARGET_VERSION} Release Postflight"
151
+ echo "====================================="
152
+ echo "Checks release truth after publish: tag, registry visibility, metadata, and install smoke."
153
+ echo ""
154
+
155
+ echo "[1/5] Git tag"
156
+ if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null 2>&1; then
157
+ pass "Git tag ${TAG} exists locally"
158
+ else
159
+ fail "Git tag ${TAG} is missing locally"
160
+ fi
161
+
162
+ echo "[2/5] Registry version"
163
+ if run_with_retry VERSION_OUTPUT "registry version" equals "${TARGET_VERSION}" npm view "${PACKAGE_NAME}@${TARGET_VERSION}" version; then
164
+ PUBLISHED_VERSION="$(trim_last_line "$VERSION_OUTPUT")"
165
+ if [[ "$PUBLISHED_VERSION" == "$TARGET_VERSION" ]]; then
166
+ pass "npm registry serves ${PACKAGE_NAME}@${TARGET_VERSION}"
167
+ else
168
+ fail "npm registry returned '${PUBLISHED_VERSION}', expected '${TARGET_VERSION}'"
169
+ fi
170
+ else
171
+ fail "npm registry does not serve ${PACKAGE_NAME}@${TARGET_VERSION}"
172
+ printf '%s\n' "$VERSION_OUTPUT" | tail -20
173
+ fi
174
+
175
+ echo "[3/5] Registry tarball metadata"
176
+ if run_with_retry TARBALL_OUTPUT "registry tarball metadata" nonempty "" npm view "${PACKAGE_NAME}@${TARGET_VERSION}" dist.tarball; then
177
+ TARBALL_URL="$(trim_last_line "$TARBALL_OUTPUT")"
178
+ if [[ -n "$TARBALL_URL" ]]; then
179
+ pass "registry exposes dist.tarball metadata"
180
+ else
181
+ fail "registry returned empty dist.tarball metadata"
182
+ fi
183
+ else
184
+ fail "registry did not return dist.tarball metadata"
185
+ printf '%s\n' "$TARBALL_OUTPUT" | tail -20
186
+ fi
187
+
188
+ echo "[4/5] Registry checksum metadata"
189
+ if run_with_retry INTEGRITY_OUTPUT "registry checksum metadata" nonempty "" npm view "${PACKAGE_NAME}@${TARGET_VERSION}" dist.integrity; then
190
+ REGISTRY_CHECKSUM="$(trim_last_line "$INTEGRITY_OUTPUT")"
191
+ fi
192
+ if [[ -z "$REGISTRY_CHECKSUM" ]]; then
193
+ if run_with_retry SHASUM_OUTPUT "registry shasum metadata" nonempty "" npm view "${PACKAGE_NAME}@${TARGET_VERSION}" dist.shasum; then
194
+ REGISTRY_CHECKSUM="$(trim_last_line "$SHASUM_OUTPUT")"
195
+ fi
196
+ fi
197
+ if [[ -n "$REGISTRY_CHECKSUM" ]]; then
198
+ pass "registry exposes checksum metadata"
199
+ else
200
+ fail "registry did not return checksum metadata"
201
+ fi
202
+
203
+ 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
205
+ EXEC_VERSION="$(trim_last_line "$EXEC_OUTPUT")"
206
+ if [[ "$EXEC_VERSION" == "$TARGET_VERSION" ]]; then
207
+ pass "published CLI executes and reports ${TARGET_VERSION}"
208
+ else
209
+ fail "published CLI reported '${EXEC_VERSION}', expected '${TARGET_VERSION}'"
210
+ fi
211
+ else
212
+ fail "published CLI install smoke failed"
213
+ printf '%s\n' "$EXEC_OUTPUT" | tail -20
214
+ fi
215
+
216
+ echo ""
217
+ echo "====================================="
218
+ echo "Results: ${PASS} passed, ${FAIL} failed"
219
+ if [[ -n "$TARBALL_URL" ]]; then
220
+ echo "Tarball: ${TARBALL_URL}"
221
+ fi
222
+ if [[ -n "$REGISTRY_CHECKSUM" ]]; then
223
+ echo "Checksum: ${REGISTRY_CHECKSUM}"
224
+ fi
225
+ if [ "$FAIL" -gt 0 ]; then
226
+ echo "POSTFLIGHT FAILED — do not mark the release complete."
227
+ exit 1
228
+ fi
229
+
230
+ echo "POSTFLIGHT PASSED — registry truth matches the release tag."
231
+ exit 0
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env bash
2
+ # Release preflight — run this before cutting a release.
3
+ # Verifies: clean tree, deps, tests, CHANGELOG entry, pack dry-run.
4
+ # Usage: bash scripts/release-preflight.sh [--strict] [--target-version <semver>]
5
+ set -uo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ CLI_DIR="${SCRIPT_DIR}/.."
9
+ cd "$CLI_DIR"
10
+
11
+ STRICT_MODE=0
12
+ TARGET_VERSION="2.0.0"
13
+
14
+ usage() {
15
+ echo "Usage: bash scripts/release-preflight.sh [--strict] [--target-version <semver>]" >&2
16
+ }
17
+
18
+ while [[ $# -gt 0 ]]; do
19
+ case "$1" in
20
+ --strict)
21
+ STRICT_MODE=1
22
+ shift
23
+ ;;
24
+ --target-version)
25
+ if [[ -z "${2:-}" ]]; then
26
+ echo "Error: --target-version requires a semver argument" >&2
27
+ usage
28
+ exit 1
29
+ fi
30
+ if ! [[ "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
31
+ echo "Invalid semver: $2" >&2
32
+ usage
33
+ exit 1
34
+ fi
35
+ TARGET_VERSION="$2"
36
+ shift 2
37
+ ;;
38
+ *)
39
+ usage
40
+ exit 1
41
+ ;;
42
+ esac
43
+ done
44
+
45
+ PASS=0
46
+ FAIL=0
47
+ WARN=0
48
+
49
+ pass() { PASS=$((PASS + 1)); echo " PASS: $1"; }
50
+ fail() { FAIL=$((FAIL + 1)); echo " FAIL: $1"; }
51
+ warn() { WARN=$((WARN + 1)); echo " WARN: $1"; }
52
+
53
+ run_and_capture() {
54
+ local __var_name="$1"
55
+ shift
56
+
57
+ local output
58
+ local status
59
+ output="$("$@" 2>&1)"
60
+ status=$?
61
+
62
+ printf -v "$__var_name" '%s' "$output"
63
+ return "$status"
64
+ }
65
+
66
+ echo "AgentXchain v${TARGET_VERSION} Release Preflight"
67
+ echo "====================================="
68
+ if [[ "$TARGET_VERSION" == "1.0.0" ]]; then
69
+ echo "Local checks only. Human-gated release items remain in .planning/V1_RELEASE_CHECKLIST.md."
70
+ else
71
+ echo "Local checks only. Human-gated release items remain in .planning/V1_RELEASE_CHECKLIST.md (v1.0) or .planning/V1_1_RELEASE_CHECKLIST.md (v1.1+)."
72
+ fi
73
+ if [[ "$STRICT_MODE" -eq 1 ]]; then
74
+ echo "Mode: STRICT (dirty tree and non-${TARGET_VERSION} package version are hard failures)"
75
+ else
76
+ echo "Mode: DEFAULT (dirty tree and pre-bump package version are warnings)"
77
+ fi
78
+ echo ""
79
+
80
+ # 1. Clean working tree
81
+ echo "[1/6] Git status"
82
+ if git diff --quiet HEAD 2>/dev/null && [ -z "$(git ls-files --others --exclude-standard 2>/dev/null)" ]; then
83
+ pass "Working tree is clean"
84
+ else
85
+ if [[ "$STRICT_MODE" -eq 1 ]]; then
86
+ fail "Working tree is not clean"
87
+ else
88
+ warn "Uncommitted or untracked files present"
89
+ fi
90
+ fi
91
+
92
+ # 2. Dependencies
93
+ echo "[2/6] Dependencies"
94
+ if run_and_capture NPM_CI_OUTPUT npm ci --ignore-scripts; then
95
+ pass "npm ci succeeded"
96
+ else
97
+ fail "npm ci failed"
98
+ printf '%s\n' "$NPM_CI_OUTPUT" | tail -20
99
+ fi
100
+
101
+ # 3. Tests
102
+ echo "[3/6] Test suite"
103
+ if run_and_capture TEST_OUTPUT npm test; then
104
+ TEST_STATUS=0
105
+ else
106
+ TEST_STATUS=$?
107
+ fi
108
+ TEST_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^# pass / { print $3 }')"
109
+ TEST_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^# fail / { print $3 }')"
110
+ if [ "$TEST_STATUS" -eq 0 ] && [ "${TEST_FAIL:-0}" = "0" ]; then
111
+ pass "${TEST_PASS} tests passed, 0 failures"
112
+ else
113
+ fail "npm test failed"
114
+ printf '%s\n' "$TEST_OUTPUT" | tail -20
115
+ fi
116
+
117
+ # 4. CHANGELOG has target version
118
+ echo "[4/6] CHANGELOG"
119
+ if grep -Fxq "## ${TARGET_VERSION}" CHANGELOG.md 2>/dev/null; then
120
+ pass "CHANGELOG.md contains ${TARGET_VERSION} entry"
121
+ else
122
+ fail "CHANGELOG.md missing ${TARGET_VERSION} entry"
123
+ fi
124
+
125
+ # 5. Package version
126
+ echo "[5/6] Package version"
127
+ PKG_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('package.json','utf8')).version)")
128
+ echo " Current version: ${PKG_VERSION}"
129
+ if [ "$PKG_VERSION" = "${TARGET_VERSION}" ]; then
130
+ pass "package.json is at ${TARGET_VERSION}"
131
+ else
132
+ if [[ "$STRICT_MODE" -eq 1 ]]; then
133
+ fail "package.json is at ${PKG_VERSION}, expected ${TARGET_VERSION}"
134
+ else
135
+ warn "package.json is at ${PKG_VERSION}, not yet bumped to ${TARGET_VERSION}"
136
+ fi
137
+ fi
138
+
139
+ # 6. Pack dry-run
140
+ echo "[6/6] npm pack --dry-run"
141
+ if run_and_capture PACK_OUTPUT npm pack --dry-run; then
142
+ pass "npm pack --dry-run succeeded"
143
+ PACK_SIZE_LINE="$(printf '%s\n' "$PACK_OUTPUT" | awk '/total files:/ { print; found=1 } END { if (!found) exit 1 }')"
144
+ if [ -n "${PACK_SIZE_LINE:-}" ]; then
145
+ echo " ${PACK_SIZE_LINE}"
146
+ else
147
+ printf '%s\n' "$PACK_OUTPUT" | tail -5
148
+ fi
149
+ else
150
+ fail "npm pack --dry-run failed"
151
+ printf '%s\n' "$PACK_OUTPUT" | tail -20
152
+ fi
153
+
154
+ # Summary
155
+ echo ""
156
+ echo "====================================="
157
+ echo "Results: ${PASS} passed, ${FAIL} failed, ${WARN} warnings"
158
+ if [ "$FAIL" -gt 0 ]; then
159
+ echo "PREFLIGHT FAILED — fix failures before release."
160
+ exit 1
161
+ elif [ "$WARN" -gt 0 ]; then
162
+ echo "PREFLIGHT PASSED WITH WARNINGS — resolve warnings before release day."
163
+ exit 0
164
+ else
165
+ echo "PREFLIGHT PASSED — ready for release."
166
+ exit 0
167
+ fi
@@ -0,0 +1,160 @@
1
+ import chalk from 'chalk';
2
+ import { loadProjectContext } from '../lib/config.js';
3
+ import { acceptGovernedTurn } from '../lib/governed-state.js';
4
+ import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
5
+
6
+ export async function acceptTurnCommand(opts = {}) {
7
+ const context = loadProjectContext();
8
+ if (!context) {
9
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
10
+ process.exit(1);
11
+ }
12
+
13
+ const { root, config } = context;
14
+
15
+ if (config.protocol_mode !== 'governed') {
16
+ console.log(chalk.red('The accept-turn command is only available for governed projects.'));
17
+ console.log(chalk.dim('Legacy projects use: agentxchain release'));
18
+ process.exit(1);
19
+ }
20
+
21
+ const result = acceptGovernedTurn(root, config, {
22
+ turnId: opts.turn,
23
+ resolutionMode: opts.resolution || 'standard',
24
+ });
25
+ if (!result.ok) {
26
+ if (result.error_code?.startsWith('hook_') || result.error_code === 'hook_blocked') {
27
+ const recovery = deriveRecoveryDescriptor(result.state);
28
+ const activeTurn = result.state?.current_turn;
29
+ const hookName = result.hookResults?.blocker?.hook_name
30
+ || result.hookResults?.results?.find((entry) => entry.hook_name)?.hook_name
31
+ || '(unknown)';
32
+
33
+ console.log('');
34
+ console.log(chalk.yellow(` ${result.accepted ? 'Turn Accepted, Hook Failure Detected' : 'Turn Acceptance Blocked By Hook'}`));
35
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
36
+ console.log('');
37
+ console.log(` ${chalk.dim('Turn:')} ${result.accepted?.turn_id || activeTurn?.turn_id || opts.turn || '(unknown)'}`);
38
+ console.log(` ${chalk.dim('Role:')} ${result.accepted?.role || activeTurn?.assigned_role || '(unknown)'}`);
39
+ if (result.accepted?.status) {
40
+ console.log(` ${chalk.dim('Status:')} ${result.accepted.status}`);
41
+ }
42
+ console.log(` ${chalk.dim('Hook:')} ${hookName}`);
43
+ console.log(` ${chalk.dim('Error:')} ${result.error}`);
44
+ if (recovery) {
45
+ console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
46
+ console.log(` ${chalk.dim('Owner:')} ${recovery.owner}`);
47
+ console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
48
+ if (recovery.detail) {
49
+ console.log(` ${chalk.dim('Detail:')} ${recovery.detail}`);
50
+ }
51
+ }
52
+ console.log('');
53
+ process.exit(1);
54
+ }
55
+
56
+ if (result.error_code === 'conflict' && result.conflict) {
57
+ console.log('');
58
+ console.log(chalk.yellow(' Acceptance Conflict Detected'));
59
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
60
+ console.log('');
61
+ console.log(` ${chalk.dim('Turn:')} ${result.conflict.conflicting_turn.turn_id}`);
62
+ console.log(` ${chalk.dim('Role:')} ${result.conflict.conflicting_turn.role}`);
63
+ console.log(` ${chalk.dim('Files:')} ${result.conflict.conflicting_files.join(', ') || '(none)'}`);
64
+ console.log(` ${chalk.dim('Overlap:')} ${(result.conflict.overlap_ratio * 100).toFixed(0)}%`);
65
+ console.log(` ${chalk.dim('Suggest:')} ${result.conflict.suggested_resolution}`);
66
+ if (result.state?.status === 'blocked') {
67
+ const recovery = deriveRecoveryDescriptor(result.state);
68
+ if (recovery) {
69
+ console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
70
+ }
71
+ }
72
+ console.log('');
73
+ process.exit(1);
74
+ }
75
+
76
+ if (result.error_code === 'lock_timeout') {
77
+ console.log('');
78
+ console.log(chalk.yellow(' Acceptance Lock Held'));
79
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
80
+ console.log('');
81
+ console.log(` ${chalk.dim('Reason:')} ${result.error}`);
82
+ console.log(` ${chalk.dim('Action:')} Wait for the other acceptance to complete, then retry.`);
83
+ console.log(` ${chalk.dim('Note:')} Stale locks are auto-reclaimed after 30 seconds.`);
84
+ console.log('');
85
+ process.exit(1);
86
+ }
87
+
88
+ if (result.error_code === 'protocol_error') {
89
+ console.log(chalk.red(result.error || 'Protocol error.'));
90
+ process.exit(1);
91
+ }
92
+
93
+ if (!result.validation) {
94
+ console.log(chalk.red(result.error || 'Failed to accept turn.'));
95
+ process.exit(1);
96
+ }
97
+
98
+ const errorClass = result.validation?.error_class || 'unknown';
99
+ const stage = result.validation?.stage || 'unknown';
100
+
101
+ console.log('');
102
+ console.log(chalk.red(` Validation failed at stage ${stage}`));
103
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
104
+ console.log('');
105
+ console.log(` ${chalk.dim('Reason:')} ${errorClass}`);
106
+ console.log(` ${chalk.dim('Owner:')} human`);
107
+ console.log(` ${chalk.dim('Action:')} Fix staged result and rerun agentxchain accept-turn, or reject with agentxchain reject-turn --reason "..."`);
108
+ console.log(` ${chalk.dim('Turn:')} retained`);
109
+ if (result.validation?.errors?.length) {
110
+ for (const err of result.validation.errors) {
111
+ console.log(` ${chalk.dim('Detail:')} ${err}`);
112
+ }
113
+ }
114
+ console.log('');
115
+ console.log(chalk.dim('Inspect the staged result with: agentxchain validate --mode turn'));
116
+ process.exit(1);
117
+ }
118
+
119
+ const accepted = result.accepted;
120
+ const turnId = accepted?.turn_id || result.state?.last_completed_turn_id || '(unknown)';
121
+
122
+ console.log('');
123
+ console.log(chalk.green(' Turn Accepted'));
124
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
125
+ console.log('');
126
+ console.log(` ${chalk.dim('Turn:')} ${turnId}`);
127
+ console.log(` ${chalk.dim('Role:')} ${accepted?.role || '(unknown)'}`);
128
+ console.log(` ${chalk.dim('Status:')} ${accepted?.status || 'completed'}`);
129
+ console.log(` ${chalk.dim('Summary:')} ${accepted?.summary || '(none)'}`);
130
+ if (accepted?.proposed_next_role) {
131
+ console.log(` ${chalk.dim('Proposed:')} ${accepted.proposed_next_role}`);
132
+ }
133
+ if (result.state?.accepted_integration_ref) {
134
+ console.log(` ${chalk.dim('Accepted:')} ${result.state.accepted_integration_ref}`);
135
+ }
136
+ if (accepted?.cost?.usd != null) {
137
+ console.log(` ${chalk.dim('Cost:')} $${formatUsd(accepted.cost.usd)}`);
138
+ }
139
+ console.log('');
140
+
141
+ const recovery = deriveRecoveryDescriptor(result.state);
142
+ if (recovery) {
143
+ console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
144
+ console.log(` ${chalk.dim('Owner:')} ${recovery.owner}`);
145
+ console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
146
+ console.log(` ${chalk.dim('Turn:')} ${recovery.turn_retained ? 'retained' : 'cleared'}`);
147
+ if (recovery.detail) {
148
+ console.log(` ${chalk.dim('Detail:')} ${recovery.detail}`);
149
+ }
150
+ } else if (accepted?.proposed_next_role && accepted.proposed_next_role !== 'human') {
151
+ console.log(chalk.dim(` Next: agentxchain resume --role ${accepted.proposed_next_role}`));
152
+ } else {
153
+ console.log(chalk.dim(' Next: review state, then run agentxchain resume when ready.'));
154
+ }
155
+ console.log('');
156
+ }
157
+
158
+ function formatUsd(value) {
159
+ return typeof value === 'number' && !Number.isNaN(value) ? value.toFixed(2) : '0.00';
160
+ }
@@ -0,0 +1,80 @@
1
+ import chalk from 'chalk';
2
+ import { loadProjectContext, loadProjectState } from '../lib/config.js';
3
+ import { approveRunCompletion } from '../lib/governed-state.js';
4
+ import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
5
+
6
+ export async function approveCompletionCommand(opts) {
7
+ const context = loadProjectContext();
8
+ if (!context) {
9
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
10
+ process.exit(1);
11
+ }
12
+
13
+ if (context.config.protocol_mode !== 'governed') {
14
+ console.log(chalk.red('approve-completion is only available in governed mode.'));
15
+ process.exit(1);
16
+ }
17
+
18
+ const { root, config } = context;
19
+ const state = loadProjectState(root, config);
20
+
21
+ if (!state?.pending_run_completion) {
22
+ console.log(chalk.yellow('No pending run completion to approve.'));
23
+ if (state?.status === 'completed') {
24
+ console.log(chalk.dim(' This run is already completed.'));
25
+ } else if (state?.phase) {
26
+ console.log(chalk.dim(` Current phase: ${state.phase}, status: ${state.status}`));
27
+ }
28
+ process.exit(1);
29
+ }
30
+
31
+ const pc = state.pending_run_completion;
32
+ console.log('');
33
+ console.log(chalk.bold(' Approving Run Completion'));
34
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
35
+ console.log(` ${chalk.dim('Phase:')} ${state.phase}`);
36
+ console.log(` ${chalk.dim('Gate:')} ${pc.gate}`);
37
+ console.log(` ${chalk.dim('Turn:')} ${pc.requested_by_turn}`);
38
+ console.log('');
39
+
40
+ const result = approveRunCompletion(root, config);
41
+
42
+ if (!result.ok) {
43
+ if (result.error_code?.startsWith('hook_') || result.error_code === 'hook_blocked') {
44
+ printGateHookFailure(result, 'run_completion', pc);
45
+ } else {
46
+ console.log(chalk.red(` Failed: ${result.error}`));
47
+ }
48
+ process.exit(1);
49
+ }
50
+
51
+ console.log(chalk.green(' \u2713 Run completed'));
52
+ console.log(chalk.dim(` Completed at: ${result.state.completed_at}`));
53
+ console.log('');
54
+ }
55
+
56
+ function printGateHookFailure(result, gateType, gateInfo) {
57
+ const recovery = deriveRecoveryDescriptor(result.state);
58
+ const hookName = result.hookResults?.blocker?.hook_name
59
+ || result.hookResults?.results?.find((entry) => entry.hook_name)?.hook_name
60
+ || '(unknown)';
61
+
62
+ console.log('');
63
+ console.log(chalk.yellow(` ${gateType === 'phase_transition' ? 'Phase Transition' : 'Run Completion'} Blocked By Hook`));
64
+ console.log(chalk.dim(' ' + '-'.repeat(44)));
65
+ console.log('');
66
+ console.log(` ${chalk.dim('Gate:')} ${gateInfo.gate}`);
67
+ console.log(` ${chalk.dim('Hook:')} ${hookName}`);
68
+ console.log(` ${chalk.dim('Error:')} ${result.error}`);
69
+ if (recovery) {
70
+ console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
71
+ console.log(` ${chalk.dim('Owner:')} ${recovery.owner}`);
72
+ console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
73
+ if (recovery.detail) {
74
+ console.log(` ${chalk.dim('Detail:')} ${recovery.detail}`);
75
+ }
76
+ } else {
77
+ console.log(` ${chalk.dim('Action:')} Fix or reconfigure hook "${hookName}", then rerun agentxchain approve-completion`);
78
+ }
79
+ console.log('');
80
+ }