agentxchain 2.22.0 → 2.24.1
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 +1 -1
- package/bin/agentxchain.js +32 -0
- package/package.json +1 -1
- package/scripts/release-bump.sh +55 -8
- package/scripts/release-postflight.sh +50 -9
- package/src/commands/proposal.js +144 -0
- package/src/commands/resume.js +9 -3
- package/src/commands/step.js +10 -5
- package/src/lib/adapters/api-proxy-adapter.js +39 -17
- package/src/lib/blocked-state.js +56 -14
- package/src/lib/config.js +2 -2
- package/src/lib/dispatch-bundle.js +176 -1
- package/src/lib/governed-state.js +215 -20
- package/src/lib/normalized-config.js +3 -3
- package/src/lib/proposal-ops.js +451 -0
- package/src/lib/repo-observer.js +1 -0
- package/src/lib/schemas/turn-result.schema.json +28 -0
- package/src/lib/turn-result-validator.js +35 -0
package/README.md
CHANGED
|
@@ -214,7 +214,7 @@ The first-party governed workflow kit includes `.planning/SYSTEM_SPEC.md` alongs
|
|
|
214
214
|
- `manual`: implemented
|
|
215
215
|
- `local_cli`: implemented
|
|
216
216
|
- `mcp`: implemented for stdio and streamable HTTP tool-contract dispatch
|
|
217
|
-
- `api_proxy`: implemented for synchronous
|
|
217
|
+
- `api_proxy`: implemented for synchronous `review_only` and `proposed` write-authority turns; stages a provider-backed result during `step`
|
|
218
218
|
|
|
219
219
|
## Legacy IDE Mode
|
|
220
220
|
|
package/bin/agentxchain.js
CHANGED
|
@@ -68,6 +68,7 @@ import { resumeCommand } from '../src/commands/resume.js';
|
|
|
68
68
|
import { escalateCommand } from '../src/commands/escalate.js';
|
|
69
69
|
import { acceptTurnCommand } from '../src/commands/accept-turn.js';
|
|
70
70
|
import { rejectTurnCommand } from '../src/commands/reject-turn.js';
|
|
71
|
+
import { proposalListCommand, proposalDiffCommand, proposalApplyCommand, proposalRejectCommand } from '../src/commands/proposal.js';
|
|
71
72
|
import { stepCommand } from '../src/commands/step.js';
|
|
72
73
|
import { runCommand } from '../src/commands/run.js';
|
|
73
74
|
import { approveTransitionCommand } from '../src/commands/approve-transition.js';
|
|
@@ -535,4 +536,35 @@ intakeCmd
|
|
|
535
536
|
.option('-j, --json', 'Output as JSON')
|
|
536
537
|
.action(intakeStatusCommand);
|
|
537
538
|
|
|
539
|
+
// --- Proposal operations ----------------------------------------------------
|
|
540
|
+
|
|
541
|
+
const proposalCmd = program
|
|
542
|
+
.command('proposal')
|
|
543
|
+
.description('Manage proposed changes from api_proxy agents');
|
|
544
|
+
|
|
545
|
+
proposalCmd
|
|
546
|
+
.command('list')
|
|
547
|
+
.description('List all proposals and their status')
|
|
548
|
+
.action(proposalListCommand);
|
|
549
|
+
|
|
550
|
+
proposalCmd
|
|
551
|
+
.command('diff <turn_id>')
|
|
552
|
+
.description('Show diff between proposed files and current workspace')
|
|
553
|
+
.option('--file <path>', 'Show diff for a single file only')
|
|
554
|
+
.action(proposalDiffCommand);
|
|
555
|
+
|
|
556
|
+
proposalCmd
|
|
557
|
+
.command('apply <turn_id>')
|
|
558
|
+
.description('Apply proposed changes to the workspace')
|
|
559
|
+
.option('--file <path>', 'Apply only a specific file')
|
|
560
|
+
.option('--dry-run', 'Show what would change without writing')
|
|
561
|
+
.option('--force', 'Override proposal conflicts or unverifiable legacy proposals')
|
|
562
|
+
.action(proposalApplyCommand);
|
|
563
|
+
|
|
564
|
+
proposalCmd
|
|
565
|
+
.command('reject <turn_id>')
|
|
566
|
+
.description('Reject a proposal without applying changes')
|
|
567
|
+
.option('--reason <reason>', 'Reason for rejection (required)')
|
|
568
|
+
.action(proposalRejectCommand);
|
|
569
|
+
|
|
538
570
|
program.parse();
|
package/package.json
CHANGED
package/scripts/release-bump.sh
CHANGED
|
@@ -6,6 +6,7 @@ set -euo pipefail
|
|
|
6
6
|
|
|
7
7
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
8
|
CLI_DIR="${SCRIPT_DIR}/.."
|
|
9
|
+
REPO_ROOT="$(cd "${CLI_DIR}/.." && pwd)"
|
|
9
10
|
cd "$CLI_DIR"
|
|
10
11
|
|
|
11
12
|
TARGET_VERSION=""
|
|
@@ -46,13 +47,56 @@ fi
|
|
|
46
47
|
echo "AgentXchain Release Identity: ${TARGET_VERSION}"
|
|
47
48
|
echo "============================================="
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
TARGET_RELEASE_DOC="website-v2/docs/releases/v${TARGET_VERSION//./-}.mdx"
|
|
51
|
+
ALLOWED_RELEASE_PATHS=(
|
|
52
|
+
"cli/CHANGELOG.md"
|
|
53
|
+
"${TARGET_RELEASE_DOC}"
|
|
54
|
+
"website-v2/sidebars.ts"
|
|
55
|
+
"website-v2/src/pages/index.tsx"
|
|
56
|
+
".agentxchain-conformance/capabilities.json"
|
|
57
|
+
"website-v2/docs/protocol-implementor-guide.mdx"
|
|
58
|
+
".planning/LAUNCH_EVIDENCE_REPORT.md"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
is_allowed_release_path() {
|
|
62
|
+
local candidate="$1"
|
|
63
|
+
local allowed
|
|
64
|
+
for allowed in "${ALLOWED_RELEASE_PATHS[@]}"; do
|
|
65
|
+
if [[ "$candidate" == "$allowed" ]]; then
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
68
|
+
done
|
|
69
|
+
return 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
stage_if_present() {
|
|
73
|
+
local rel_path="$1"
|
|
74
|
+
if [[ -e "${REPO_ROOT}/${rel_path}" ]]; then
|
|
75
|
+
git -C "$REPO_ROOT" add -- "$rel_path"
|
|
76
|
+
return 0
|
|
77
|
+
fi
|
|
78
|
+
if git -C "$REPO_ROOT" ls-files --error-unmatch "$rel_path" >/dev/null 2>&1; then
|
|
79
|
+
git -C "$REPO_ROOT" add -- "$rel_path"
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# 1. Assert only allowed release-surface dirt is present
|
|
84
|
+
echo "[1/7] Checking release-prep tree state..."
|
|
85
|
+
DISALLOWED_DIRTY=()
|
|
86
|
+
while IFS= read -r status_line; do
|
|
87
|
+
[[ -z "$status_line" ]] && continue
|
|
88
|
+
path="${status_line#?? }"
|
|
89
|
+
if ! is_allowed_release_path "$path"; then
|
|
90
|
+
DISALLOWED_DIRTY+=("$path")
|
|
91
|
+
fi
|
|
92
|
+
done < <(git -C "$REPO_ROOT" status --porcelain)
|
|
93
|
+
|
|
94
|
+
if [[ "${#DISALLOWED_DIRTY[@]}" -gt 0 ]]; then
|
|
95
|
+
echo "FAIL: Working tree contains changes outside the allowed release surfaces:" >&2
|
|
96
|
+
printf ' - %s\n' "${DISALLOWED_DIRTY[@]}" >&2
|
|
53
97
|
exit 1
|
|
54
98
|
fi
|
|
55
|
-
echo " OK: tree
|
|
99
|
+
echo " OK: tree contains only allowed release-prep changes"
|
|
56
100
|
|
|
57
101
|
# 2. Assert not already at target version
|
|
58
102
|
echo "[2/7] Checking current version..."
|
|
@@ -78,11 +122,14 @@ echo " OK: package.json updated to ${TARGET_VERSION}"
|
|
|
78
122
|
|
|
79
123
|
# 5. Stage version files
|
|
80
124
|
echo "[5/7] Staging version files..."
|
|
81
|
-
git add package.json
|
|
125
|
+
git add -- package.json
|
|
82
126
|
if [[ -f package-lock.json ]]; then
|
|
83
|
-
git add package-lock.json
|
|
127
|
+
git add -- package-lock.json
|
|
84
128
|
fi
|
|
85
|
-
|
|
129
|
+
for rel_path in "${ALLOWED_RELEASE_PATHS[@]}"; do
|
|
130
|
+
stage_if_present "$rel_path"
|
|
131
|
+
done
|
|
132
|
+
echo " OK: version files and allowed release surfaces staged"
|
|
86
133
|
|
|
87
134
|
# 6. Create release commit
|
|
88
135
|
echo "[6/7] Creating release commit..."
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Release postflight — run this after publish succeeds.
|
|
3
3
|
# Verifies: release tag exists, npm registry serves the version, metadata is present,
|
|
4
|
-
# the published package
|
|
5
|
-
# are importable in a
|
|
4
|
+
# the published package resolves through npx, the published package can execute its
|
|
5
|
+
# CLI entrypoint from the tarball, and runner package exports are importable in a
|
|
6
|
+
# clean consumer project.
|
|
6
7
|
# Usage: bash scripts/release-postflight.sh --target-version <semver> [--tag vX.Y.Z]
|
|
7
8
|
set -uo pipefail
|
|
8
9
|
|
|
@@ -139,6 +140,33 @@ run_install_smoke() {
|
|
|
139
140
|
return "$version_status"
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
run_npx_smoke() {
|
|
144
|
+
local smoke_root
|
|
145
|
+
local smoke_npmrc
|
|
146
|
+
local npx_output
|
|
147
|
+
local npx_status
|
|
148
|
+
|
|
149
|
+
smoke_root="$(mktemp -d "${TMPDIR:-/tmp}/agentxchain-npx-postflight.XXXXXX")"
|
|
150
|
+
mkdir -p "${smoke_root}/home" "${smoke_root}/cache" "${smoke_root}/npm-cache"
|
|
151
|
+
|
|
152
|
+
smoke_npmrc="${smoke_root}/.npmrc"
|
|
153
|
+
echo "registry=https://registry.npmjs.org/" > "$smoke_npmrc"
|
|
154
|
+
|
|
155
|
+
npx_output="$(
|
|
156
|
+
env -u NODE_AUTH_TOKEN \
|
|
157
|
+
HOME="${smoke_root}/home" \
|
|
158
|
+
XDG_CACHE_HOME="${smoke_root}/cache" \
|
|
159
|
+
NPM_CONFIG_CACHE="${smoke_root}/npm-cache" \
|
|
160
|
+
NPM_CONFIG_USERCONFIG="$smoke_npmrc" \
|
|
161
|
+
npx --yes "${PACKAGE_NAME}@${TARGET_VERSION}" --version 2>&1
|
|
162
|
+
)"
|
|
163
|
+
npx_status=$?
|
|
164
|
+
|
|
165
|
+
printf '%s\n' "$npx_output"
|
|
166
|
+
rm -rf "$smoke_root"
|
|
167
|
+
return "$npx_status"
|
|
168
|
+
}
|
|
169
|
+
|
|
142
170
|
run_runner_export_smoke() {
|
|
143
171
|
if [[ -z "$TARBALL_URL" ]]; then
|
|
144
172
|
echo "registry tarball metadata unavailable for runner export smoke" >&2
|
|
@@ -261,17 +289,17 @@ run_with_retry() {
|
|
|
261
289
|
|
|
262
290
|
echo "AgentXchain v${TARGET_VERSION} Release Postflight"
|
|
263
291
|
echo "====================================="
|
|
264
|
-
echo "Checks release truth after publish: tag, registry visibility, metadata, CLI install smoke, and package export smoke."
|
|
292
|
+
echo "Checks release truth after publish: tag, registry visibility, metadata, npx smoke, CLI install smoke, and package export smoke."
|
|
265
293
|
echo ""
|
|
266
294
|
|
|
267
|
-
echo "[1/
|
|
295
|
+
echo "[1/7] Git tag"
|
|
268
296
|
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null 2>&1; then
|
|
269
297
|
pass "Git tag ${TAG} exists locally"
|
|
270
298
|
else
|
|
271
299
|
fail "Git tag ${TAG} is missing locally"
|
|
272
300
|
fi
|
|
273
301
|
|
|
274
|
-
echo "[2/
|
|
302
|
+
echo "[2/7] Registry version"
|
|
275
303
|
if run_with_retry VERSION_OUTPUT "registry version" equals "${TARGET_VERSION}" npm view "${PACKAGE_NAME}@${TARGET_VERSION}" version; then
|
|
276
304
|
PUBLISHED_VERSION="$(trim_last_line "$VERSION_OUTPUT")"
|
|
277
305
|
if [[ "$PUBLISHED_VERSION" == "$TARGET_VERSION" ]]; then
|
|
@@ -284,7 +312,7 @@ else
|
|
|
284
312
|
printf '%s\n' "$VERSION_OUTPUT" | tail -20
|
|
285
313
|
fi
|
|
286
314
|
|
|
287
|
-
echo "[3/
|
|
315
|
+
echo "[3/7] Registry tarball metadata"
|
|
288
316
|
if run_with_retry TARBALL_OUTPUT "registry tarball metadata" nonempty "" npm view "${PACKAGE_NAME}@${TARGET_VERSION}" dist.tarball; then
|
|
289
317
|
TARBALL_URL="$(trim_last_line "$TARBALL_OUTPUT")"
|
|
290
318
|
if [[ -n "$TARBALL_URL" ]]; then
|
|
@@ -297,7 +325,7 @@ else
|
|
|
297
325
|
printf '%s\n' "$TARBALL_OUTPUT" | tail -20
|
|
298
326
|
fi
|
|
299
327
|
|
|
300
|
-
echo "[4/
|
|
328
|
+
echo "[4/7] Registry checksum metadata"
|
|
301
329
|
if run_with_retry INTEGRITY_OUTPUT "registry checksum metadata" nonempty "" npm view "${PACKAGE_NAME}@${TARGET_VERSION}" dist.integrity; then
|
|
302
330
|
REGISTRY_CHECKSUM="$(trim_last_line "$INTEGRITY_OUTPUT")"
|
|
303
331
|
fi
|
|
@@ -312,7 +340,20 @@ else
|
|
|
312
340
|
fail "registry did not return checksum metadata"
|
|
313
341
|
fi
|
|
314
342
|
|
|
315
|
-
echo "[5/
|
|
343
|
+
echo "[5/7] npx smoke"
|
|
344
|
+
if run_with_retry NPX_OUTPUT "npx smoke" nonempty "" run_npx_smoke; then
|
|
345
|
+
NPX_VERSION="$(trim_last_line "$NPX_OUTPUT")"
|
|
346
|
+
if [[ "$NPX_VERSION" == "$TARGET_VERSION" ]]; then
|
|
347
|
+
pass "published npx command resolves and reports ${TARGET_VERSION}"
|
|
348
|
+
else
|
|
349
|
+
fail "published npx command reported '${NPX_VERSION}', expected '${TARGET_VERSION}'"
|
|
350
|
+
fi
|
|
351
|
+
else
|
|
352
|
+
fail "published npx smoke failed"
|
|
353
|
+
printf '%s\n' "$NPX_OUTPUT" | tail -20
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
echo "[6/7] Install smoke"
|
|
316
357
|
if run_with_retry EXEC_OUTPUT "install smoke" nonempty "" run_install_smoke; then
|
|
317
358
|
EXEC_VERSION="$(trim_last_line "$EXEC_OUTPUT")"
|
|
318
359
|
if [[ "$EXEC_VERSION" == "$TARGET_VERSION" ]]; then
|
|
@@ -325,7 +366,7 @@ else
|
|
|
325
366
|
printf '%s\n' "$EXEC_OUTPUT" | tail -20
|
|
326
367
|
fi
|
|
327
368
|
|
|
328
|
-
echo "[
|
|
369
|
+
echo "[7/7] Package export smoke"
|
|
329
370
|
if run_with_retry RUNNER_EXPORT_OUTPUT "runner export smoke" nonempty "" run_runner_export_smoke; then
|
|
330
371
|
RUNNER_EXPORT_JSON="$(trim_last_line "$RUNNER_EXPORT_OUTPUT")"
|
|
331
372
|
RUNNER_EXPORT_VERSION="$(printf '%s' "$RUNNER_EXPORT_JSON" | node --input-type=module -e "process.stdin.setEncoding('utf8'); let raw=''; process.stdin.on('data', (chunk) => raw += chunk); process.stdin.on('end', () => { const parsed = JSON.parse(raw); console.log(parsed.runner_interface_version || ''); });")"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadProjectContext } from '../lib/config.js';
|
|
3
|
+
import { listProposals, diffProposal, applyProposal, rejectProposal } from '../lib/proposal-ops.js';
|
|
4
|
+
|
|
5
|
+
export async function proposalListCommand() {
|
|
6
|
+
const context = requireGovernedContext();
|
|
7
|
+
const result = listProposals(context.root);
|
|
8
|
+
if (!result.ok) {
|
|
9
|
+
console.log(chalk.red(result.error));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (result.proposals.length === 0) {
|
|
14
|
+
console.log(chalk.dim(' No proposals found.'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log('');
|
|
19
|
+
console.log(chalk.bold(' Proposals'));
|
|
20
|
+
console.log(chalk.dim(' ' + '─'.repeat(60)));
|
|
21
|
+
for (const p of result.proposals) {
|
|
22
|
+
const statusColor = p.status === 'applied' ? chalk.green : p.status === 'rejected' ? chalk.red : chalk.yellow;
|
|
23
|
+
console.log(` ${chalk.dim(p.turn_id)} ${p.role} ${p.file_count} files ${statusColor(p.status)}`);
|
|
24
|
+
}
|
|
25
|
+
console.log('');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function proposalDiffCommand(turnId, opts) {
|
|
29
|
+
const context = requireGovernedContext();
|
|
30
|
+
if (!turnId) {
|
|
31
|
+
console.log(chalk.red('Usage: agentxchain proposal diff <turn_id>'));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = diffProposal(context.root, turnId, opts.file);
|
|
36
|
+
if (!result.ok) {
|
|
37
|
+
console.log(chalk.red(result.error));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const d of result.diffs) {
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(chalk.bold(` ${d.path}`) + chalk.dim(` (${d.action})`));
|
|
44
|
+
console.log(chalk.dim(' ' + '─'.repeat(50)));
|
|
45
|
+
for (const line of d.preview.split('\n')) {
|
|
46
|
+
if (line.startsWith('+')) console.log(chalk.green(` ${line}`));
|
|
47
|
+
else if (line.startsWith('-')) console.log(chalk.red(` ${line}`));
|
|
48
|
+
else console.log(chalk.dim(` ${line}`));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
console.log('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function proposalApplyCommand(turnId, opts) {
|
|
55
|
+
const context = requireGovernedContext();
|
|
56
|
+
if (!turnId) {
|
|
57
|
+
console.log(chalk.red('Usage: agentxchain proposal apply <turn_id>'));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = applyProposal(context.root, turnId, {
|
|
62
|
+
file: opts.file,
|
|
63
|
+
dryRun: opts.dryRun,
|
|
64
|
+
force: opts.force,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!result.ok) {
|
|
68
|
+
if (Array.isArray(result.conflicts) && result.conflicts.length > 0) {
|
|
69
|
+
console.log(chalk.red(` ${result.error}`));
|
|
70
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
71
|
+
for (const conflict of result.conflicts) {
|
|
72
|
+
console.log(` ${chalk.red('•')} ${conflict.path} ${chalk.dim(`(${conflict.reason})`)}`);
|
|
73
|
+
}
|
|
74
|
+
console.log('');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
console.log(chalk.red(result.error));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log('');
|
|
82
|
+
if (result.dry_run) {
|
|
83
|
+
console.log(chalk.yellow(' Dry Run — No Changes Written'));
|
|
84
|
+
} else if (result.forced) {
|
|
85
|
+
console.log(chalk.yellow(' Proposal Applied With Force'));
|
|
86
|
+
} else {
|
|
87
|
+
console.log(chalk.green(' Proposal Applied'));
|
|
88
|
+
}
|
|
89
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
90
|
+
console.log(` ${chalk.dim('Turn:')} ${turnId}`);
|
|
91
|
+
console.log(` ${chalk.dim('Applied:')} ${result.applied_files.length} files`);
|
|
92
|
+
if (result.applied_files.length > 0) {
|
|
93
|
+
for (const f of result.applied_files) {
|
|
94
|
+
console.log(` ${chalk.dim('•')} ${f}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (result.skipped_files.length > 0) {
|
|
98
|
+
console.log(` ${chalk.dim('Skipped:')} ${result.skipped_files.length} files`);
|
|
99
|
+
for (const f of result.skipped_files) {
|
|
100
|
+
console.log(` ${chalk.dim('•')} ${f}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (result.forced && result.overridden_conflicts?.length > 0) {
|
|
104
|
+
console.log(` ${chalk.dim('Forced:')} ${result.overridden_conflicts.length} conflicts overridden`);
|
|
105
|
+
for (const conflict of result.overridden_conflicts) {
|
|
106
|
+
console.log(` ${chalk.dim('•')} ${conflict.path}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
console.log('');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function proposalRejectCommand(turnId, opts) {
|
|
113
|
+
const context = requireGovernedContext();
|
|
114
|
+
if (!turnId) {
|
|
115
|
+
console.log(chalk.red('Usage: agentxchain proposal reject <turn_id> --reason "..."'));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = rejectProposal(context.root, turnId, opts.reason);
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
console.log(chalk.red(result.error));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log(chalk.yellow(' Proposal Rejected'));
|
|
127
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
128
|
+
console.log(` ${chalk.dim('Turn:')} ${turnId}`);
|
|
129
|
+
console.log(` ${chalk.dim('Reason:')} ${opts.reason}`);
|
|
130
|
+
console.log('');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function requireGovernedContext() {
|
|
134
|
+
const context = loadProjectContext();
|
|
135
|
+
if (!context) {
|
|
136
|
+
console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
if (context.config.protocol_mode !== 'governed') {
|
|
140
|
+
console.log(chalk.red('The proposal command is only available for governed projects.'));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
return context;
|
|
144
|
+
}
|
package/src/commands/resume.js
CHANGED
|
@@ -21,6 +21,7 @@ import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
|
21
21
|
import {
|
|
22
22
|
initializeGovernedRun,
|
|
23
23
|
assignGovernedTurn,
|
|
24
|
+
deriveAfterDispatchHookRecoveryAction,
|
|
24
25
|
markRunBlocked,
|
|
25
26
|
getActiveTurns,
|
|
26
27
|
getActiveTurnCount,
|
|
@@ -358,13 +359,17 @@ function runAfterDispatchHooks(root, hooksConfig, state, turn, config) {
|
|
|
358
359
|
|| `after_dispatch hook blocked dispatch for turn ${turnId}`;
|
|
359
360
|
const errorCode = afterDispatchHooks.tamper?.error_code || 'hook_blocked';
|
|
360
361
|
|
|
362
|
+
const recoveryAction = deriveAfterDispatchHookRecoveryAction(state, config, {
|
|
363
|
+
turnRetained: true,
|
|
364
|
+
turnId,
|
|
365
|
+
});
|
|
361
366
|
markRunBlocked(root, {
|
|
362
367
|
blockedOn: `hook:after_dispatch:${hookName}`,
|
|
363
368
|
category: 'dispatch_error',
|
|
364
369
|
recovery: {
|
|
365
370
|
typed_reason: afterDispatchHooks.tamper ? 'hook_tamper' : 'hook_block',
|
|
366
371
|
owner: 'human',
|
|
367
|
-
recovery_action:
|
|
372
|
+
recovery_action: recoveryAction,
|
|
368
373
|
turn_retained: true,
|
|
369
374
|
detail,
|
|
370
375
|
},
|
|
@@ -378,6 +383,7 @@ function runAfterDispatchHooks(root, hooksConfig, state, turn, config) {
|
|
|
378
383
|
hookName,
|
|
379
384
|
error: detail,
|
|
380
385
|
errorCode,
|
|
386
|
+
recoveryAction,
|
|
381
387
|
hookResults: afterDispatchHooks,
|
|
382
388
|
});
|
|
383
389
|
|
|
@@ -387,7 +393,7 @@ function runAfterDispatchHooks(root, hooksConfig, state, turn, config) {
|
|
|
387
393
|
return { ok: true };
|
|
388
394
|
}
|
|
389
395
|
|
|
390
|
-
function printDispatchHookFailure({ turnId, roleId, hookName, error, hookResults }) {
|
|
396
|
+
function printDispatchHookFailure({ turnId, roleId, hookName, error, hookResults, recoveryAction }) {
|
|
391
397
|
const isTamper = hookResults?.tamper;
|
|
392
398
|
console.log('');
|
|
393
399
|
console.log(chalk.yellow(' Dispatch Blocked By Hook'));
|
|
@@ -399,7 +405,7 @@ function printDispatchHookFailure({ turnId, roleId, hookName, error, hookResults
|
|
|
399
405
|
console.log(` ${chalk.dim('Error:')} ${error}`);
|
|
400
406
|
console.log(` ${chalk.dim('Reason:')} ${isTamper ? 'hook_tamper' : 'hook_block'}`);
|
|
401
407
|
console.log(` ${chalk.dim('Owner:')} human`);
|
|
402
|
-
console.log(` ${chalk.dim('Action:')}
|
|
408
|
+
console.log(` ${chalk.dim('Action:')} ${recoveryAction}`);
|
|
403
409
|
console.log('');
|
|
404
410
|
}
|
|
405
411
|
|
package/src/commands/step.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
initializeGovernedRun,
|
|
29
29
|
assignGovernedTurn,
|
|
30
30
|
acceptGovernedTurn,
|
|
31
|
+
deriveAfterDispatchHookRecoveryAction,
|
|
31
32
|
rejectGovernedTurn,
|
|
32
33
|
markRunBlocked,
|
|
33
34
|
getActiveTurnCount,
|
|
@@ -338,7 +339,7 @@ export async function stepCommand(opts) {
|
|
|
338
339
|
});
|
|
339
340
|
|
|
340
341
|
if (!afterDispatchHooks.ok) {
|
|
341
|
-
const blocked = blockStepForHookIssue(root, turn, {
|
|
342
|
+
const blocked = blockStepForHookIssue(root, state, turn, {
|
|
342
343
|
hookResults: afterDispatchHooks,
|
|
343
344
|
phase: 'after_dispatch',
|
|
344
345
|
defaultDetail: `after_dispatch hook blocked dispatch for turn ${turn.turn_id}`,
|
|
@@ -654,7 +655,7 @@ export async function stepCommand(opts) {
|
|
|
654
655
|
});
|
|
655
656
|
|
|
656
657
|
if (!beforeValidationHooks.ok) {
|
|
657
|
-
const blocked = blockStepForHookIssue(root, turn, {
|
|
658
|
+
const blocked = blockStepForHookIssue(root, state, turn, {
|
|
658
659
|
hookResults: beforeValidationHooks,
|
|
659
660
|
phase: 'before_validation',
|
|
660
661
|
defaultDetail: `before_validation hook blocked validation for turn ${turn.turn_id}`,
|
|
@@ -686,7 +687,7 @@ export async function stepCommand(opts) {
|
|
|
686
687
|
});
|
|
687
688
|
|
|
688
689
|
if (!afterValidationHooks.ok) {
|
|
689
|
-
const blocked = blockStepForHookIssue(root, turn, {
|
|
690
|
+
const blocked = blockStepForHookIssue(root, state, turn, {
|
|
690
691
|
hookResults: afterValidationHooks,
|
|
691
692
|
phase: 'after_validation',
|
|
692
693
|
defaultDetail: `after_validation hook blocked acceptance for turn ${turn.turn_id}`,
|
|
@@ -775,7 +776,7 @@ function loadHookStagedTurn(root, stagingRel) {
|
|
|
775
776
|
}
|
|
776
777
|
}
|
|
777
778
|
|
|
778
|
-
function blockStepForHookIssue(root, turn, { hookResults, phase, defaultDetail, config }) {
|
|
779
|
+
function blockStepForHookIssue(root, state, turn, { hookResults, phase, defaultDetail, config }) {
|
|
779
780
|
const hookName = hookResults.blocker?.hook_name
|
|
780
781
|
|| hookResults.results?.find((entry) => entry.hook_name)?.hook_name
|
|
781
782
|
|| 'unknown';
|
|
@@ -783,13 +784,17 @@ function blockStepForHookIssue(root, turn, { hookResults, phase, defaultDetail,
|
|
|
783
784
|
|| hookResults.tamper?.message
|
|
784
785
|
|| defaultDetail;
|
|
785
786
|
const errorCode = hookResults.tamper?.error_code || 'hook_blocked';
|
|
787
|
+
const recoveryAction = deriveAfterDispatchHookRecoveryAction(state, config, {
|
|
788
|
+
turnRetained: true,
|
|
789
|
+
turnId: turn.turn_id,
|
|
790
|
+
});
|
|
786
791
|
const blocked = markRunBlocked(root, {
|
|
787
792
|
blockedOn: `hook:${phase}:${hookName}`,
|
|
788
793
|
category: phase === 'after_dispatch' ? 'dispatch_error' : 'validation_error',
|
|
789
794
|
recovery: {
|
|
790
795
|
typed_reason: hookResults.tamper ? 'hook_tamper' : 'hook_block',
|
|
791
796
|
owner: 'human',
|
|
792
|
-
recovery_action:
|
|
797
|
+
recovery_action: recoveryAction,
|
|
793
798
|
turn_retained: true,
|
|
794
799
|
detail,
|
|
795
800
|
},
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* API proxy adapter —
|
|
2
|
+
* API proxy adapter — synchronous provider calls for review and proposed authoring.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - review_only
|
|
4
|
+
* Supported write authorities:
|
|
5
|
+
* - review_only: single request/response, structured review JSON
|
|
6
|
+
* - proposed: single request/response, structured JSON with proposed_changes[]
|
|
7
|
+
* (orchestrator materializes proposals to .agentxchain/proposed/<turn_id>/)
|
|
8
|
+
*
|
|
9
|
+
* Constraints:
|
|
6
10
|
* - single request / single response (synchronous within `step`)
|
|
7
|
-
* - no tool use, no
|
|
11
|
+
* - no tool use, no direct repo writes
|
|
8
12
|
* - turn result must arrive as structured JSON
|
|
9
13
|
* - provider telemetry is authoritative for cost
|
|
10
14
|
*
|
|
@@ -46,13 +50,15 @@ const PROVIDER_ENDPOINTS = {
|
|
|
46
50
|
openai: 'https://api.openai.com/v1/chat/completions',
|
|
47
51
|
};
|
|
48
52
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
// Bundled cost rates per million tokens (USD).
|
|
54
|
+
// These are convenience defaults — operators can override via budget.cost_rates in agentxchain.json.
|
|
55
|
+
// Verified: 2026-04-07 (Anthropic via docs.anthropic.com; OpenAI from training knowledge)
|
|
56
|
+
const BUNDLED_COST_RATES = {
|
|
57
|
+
// Anthropic — verified 2026-04-07
|
|
52
58
|
'claude-sonnet-4-6': { input_per_1m: 3.00, output_per_1m: 15.00 },
|
|
53
|
-
'claude-opus-4-6': { input_per_1m:
|
|
54
|
-
'claude-haiku-4-5-20251001': { input_per_1m:
|
|
55
|
-
// OpenAI
|
|
59
|
+
'claude-opus-4-6': { input_per_1m: 5.00, output_per_1m: 25.00 },
|
|
60
|
+
'claude-haiku-4-5-20251001': { input_per_1m: 1.00, output_per_1m: 5.00 },
|
|
61
|
+
// OpenAI — verified 2026-04-07 (training knowledge, could not live-verify openai.com)
|
|
56
62
|
'gpt-4o': { input_per_1m: 2.50, output_per_1m: 10.00 },
|
|
57
63
|
'gpt-4o-mini': { input_per_1m: 0.15, output_per_1m: 0.60 },
|
|
58
64
|
'gpt-4.1': { input_per_1m: 2.00, output_per_1m: 8.00 },
|
|
@@ -63,6 +69,18 @@ const COST_RATES = {
|
|
|
63
69
|
'o4-mini': { input_per_1m: 1.10, output_per_1m: 4.40 },
|
|
64
70
|
};
|
|
65
71
|
|
|
72
|
+
// Resolve cost rates: operator-supplied cost_rates override bundled defaults
|
|
73
|
+
function getCostRates(model, config) {
|
|
74
|
+
const operatorRates = config?.budget?.cost_rates;
|
|
75
|
+
if (operatorRates && typeof operatorRates === 'object' && operatorRates[model]) {
|
|
76
|
+
const r = operatorRates[model];
|
|
77
|
+
if (Number.isFinite(r.input_per_1m) && Number.isFinite(r.output_per_1m)) {
|
|
78
|
+
return r;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return BUNDLED_COST_RATES[model] || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
const RETRYABLE_ERROR_CLASSES = [
|
|
67
85
|
'rate_limited',
|
|
68
86
|
'network_failure',
|
|
@@ -415,7 +433,7 @@ function emptyUsageTotals() {
|
|
|
415
433
|
};
|
|
416
434
|
}
|
|
417
435
|
|
|
418
|
-
function usageFromTelemetry(provider, model, usage) {
|
|
436
|
+
function usageFromTelemetry(provider, model, usage, config) {
|
|
419
437
|
if (!usage || typeof usage !== 'object') return null;
|
|
420
438
|
|
|
421
439
|
let inputTokens = 0;
|
|
@@ -429,7 +447,7 @@ function usageFromTelemetry(provider, model, usage) {
|
|
|
429
447
|
outputTokens = Number.isFinite(usage.output_tokens) ? usage.output_tokens : 0;
|
|
430
448
|
}
|
|
431
449
|
|
|
432
|
-
const rates =
|
|
450
|
+
const rates = getCostRates(model, config);
|
|
433
451
|
const usd = rates
|
|
434
452
|
? (inputTokens / 1_000_000) * rates.input_per_1m + (outputTokens / 1_000_000) * rates.output_per_1m
|
|
435
453
|
: 0;
|
|
@@ -576,6 +594,7 @@ async function executeApiCall({
|
|
|
576
594
|
requestBody,
|
|
577
595
|
timeoutSeconds,
|
|
578
596
|
signal,
|
|
597
|
+
config,
|
|
579
598
|
}) {
|
|
580
599
|
const timeoutMs = timeoutSeconds * 1000;
|
|
581
600
|
const controller = new AbortController();
|
|
@@ -672,7 +691,7 @@ async function executeApiCall({
|
|
|
672
691
|
};
|
|
673
692
|
}
|
|
674
693
|
|
|
675
|
-
const usage = usageFromTelemetry(provider, model, responseData.usage);
|
|
694
|
+
const usage = usageFromTelemetry(provider, model, responseData.usage, config);
|
|
676
695
|
const extraction = extractTurnResult(responseData, provider);
|
|
677
696
|
|
|
678
697
|
if (!extraction.ok) {
|
|
@@ -740,9 +759,9 @@ export async function dispatchApiProxy(root, state, config, options = {}) {
|
|
|
740
759
|
return { ok: false, error: `Runtime "${runtimeId}" is not an api_proxy runtime` };
|
|
741
760
|
}
|
|
742
761
|
|
|
743
|
-
// Enforce
|
|
744
|
-
if (role?.write_authority !== 'review_only') {
|
|
745
|
-
return { ok: false, error: `
|
|
762
|
+
// Enforce api_proxy restriction: review_only or proposed only
|
|
763
|
+
if (role?.write_authority !== 'review_only' && role?.write_authority !== 'proposed') {
|
|
764
|
+
return { ok: false, error: `api_proxy only supports review_only and proposed roles (got "${role?.write_authority}")` };
|
|
746
765
|
}
|
|
747
766
|
|
|
748
767
|
// Read dispatch bundle
|
|
@@ -879,6 +898,7 @@ export async function dispatchApiProxy(root, state, config, options = {}) {
|
|
|
879
898
|
requestBody,
|
|
880
899
|
timeoutSeconds,
|
|
881
900
|
signal,
|
|
901
|
+
config,
|
|
882
902
|
});
|
|
883
903
|
|
|
884
904
|
const attemptCompletedAt = new Date().toISOString();
|
|
@@ -1180,5 +1200,7 @@ export {
|
|
|
1180
1200
|
buildOpenAiRequest,
|
|
1181
1201
|
classifyError,
|
|
1182
1202
|
classifyHttpError,
|
|
1183
|
-
|
|
1203
|
+
BUNDLED_COST_RATES,
|
|
1204
|
+
BUNDLED_COST_RATES as COST_RATES, // backward compat alias
|
|
1205
|
+
getCostRates,
|
|
1184
1206
|
};
|