agentxchain 2.155.72 → 2.156.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 +4 -8
- package/bin/agentxchain.js +22 -0
- package/dashboard/app.js +54 -0
- package/dashboard/components/org-audit-trail.js +161 -0
- package/dashboard/components/org-history.js +140 -0
- package/dashboard/components/org-overview.js +145 -0
- package/dashboard/components/org-runs.js +168 -0
- package/dashboard/index.html +4 -0
- package/package.json +4 -5
- package/scripts/migrate-node-test-to-vitest.mjs +98 -0
- package/scripts/release-postflight.sh +1 -1
- package/scripts/release-preflight.sh +5 -5
- package/scripts/verify-post-publish.sh +1 -1
- package/src/commands/ci-report.js +80 -0
- package/src/commands/doctor.js +22 -1
- package/src/commands/intake-approve.js +1 -0
- package/src/commands/replay.js +1 -0
- package/src/commands/run.js +50 -0
- package/src/commands/serve.js +64 -0
- package/src/commands/step.js +63 -1
- package/src/commands/verify.js +1 -0
- package/src/lib/adapters/local-cli-adapter.js +326 -2
- package/src/lib/api/execution-worker.js +192 -0
- package/src/lib/api/hosted-runner.js +494 -0
- package/src/lib/api/job-queue.js +152 -0
- package/src/lib/api/org-state-aggregator.js +428 -0
- package/src/lib/api/project-registry.js +148 -0
- package/src/lib/api/protocol-bridge.js +476 -0
- package/src/lib/approval-policy.js +12 -0
- package/src/lib/ci-reporter.js +188 -0
- package/src/lib/claude-local-auth.js +89 -1
- package/src/lib/connector-probe.js +21 -0
- package/src/lib/continuous-run.js +51 -3
- package/src/lib/dashboard/bridge-server.js +10 -5
- package/src/lib/dispatch-bundle.js +7 -3
- package/src/lib/dispatch-progress.js +9 -0
- package/src/lib/governed-state.js +5 -4
- package/src/lib/intake.js +32 -6
- package/src/lib/normalized-config.js +4 -0
- package/src/lib/recovery-classification.js +158 -0
- package/src/lib/report.js +91 -0
- package/src/lib/run-events.js +7 -1
- package/src/lib/schemas/agentxchain-config.schema.json +10 -0
- package/src/lib/scope-overlap.js +214 -0
- package/src/lib/turn-checkpoint.js +33 -3
- package/src/lib/turn-result-validator.js +47 -6
- package/src/lib/validation.js +11 -3
- package/src/lib/verification-replay.js +125 -4
- package/src/lib/vision-reader.js +16 -1
|
@@ -138,7 +138,7 @@ export function validateStagedTurnResult(root, state, config, opts = {}) {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
// ── Stage C: Artifact Validation ───────────────────────────────────────
|
|
141
|
-
const artifactResult = validateArtifact(turnResult, config);
|
|
141
|
+
const artifactResult = validateArtifact(turnResult, config, state);
|
|
142
142
|
if (artifactResult.errors.length > 0) {
|
|
143
143
|
return result('artifact', 'artifact_error', artifactResult.errors, artifactResult.warnings);
|
|
144
144
|
}
|
|
@@ -670,7 +670,7 @@ function validateAssignment(tr, state) {
|
|
|
670
670
|
|
|
671
671
|
// ── Stage C: Artifact Validation ─────────────────────────────────────────────
|
|
672
672
|
|
|
673
|
-
function validateArtifact(tr, config) {
|
|
673
|
+
function validateArtifact(tr, config, state = null) {
|
|
674
674
|
const errors = [];
|
|
675
675
|
const warnings = [];
|
|
676
676
|
|
|
@@ -699,6 +699,11 @@ function validateArtifact(tr, config) {
|
|
|
699
699
|
`Artifact type "workspace" requires authoritative write authority, but role "${tr.role}" has "${writeAuthority}".`
|
|
700
700
|
);
|
|
701
701
|
}
|
|
702
|
+
if ((tr.files_changed || []).length === 0 && !hasCheckpointableProducedFiles(tr)) {
|
|
703
|
+
errors.push(
|
|
704
|
+
'artifact.type: "workspace" but files_changed is empty. Use artifact type "review" for no-edit turns, or declare checkpointable verification.produced_files entries with disposition "artifact".'
|
|
705
|
+
);
|
|
706
|
+
}
|
|
702
707
|
}
|
|
703
708
|
|
|
704
709
|
// Check for reserved path modifications
|
|
@@ -722,9 +727,17 @@ function validateArtifact(tr, config) {
|
|
|
722
727
|
}
|
|
723
728
|
}
|
|
724
729
|
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
730
|
+
// Implementation-phase completion must be backed by actual product code
|
|
731
|
+
// changes. Planning/review artifacts are supplementary and should not satisfy
|
|
732
|
+
// the implementation_complete gate by themselves.
|
|
733
|
+
if (writeAuthority === 'authoritative' && state?.phase === 'implementation' && tr.status === 'completed') {
|
|
734
|
+
const productFiles = (tr.files_changed || []).filter(f => isProductChangePath(f));
|
|
735
|
+
if (productFiles.length === 0) {
|
|
736
|
+
errors.push(
|
|
737
|
+
`Role "${tr.role}" completed an implementation turn without product code changes in files_changed. ` +
|
|
738
|
+
'Implementation-phase completion requires at least one non-planning, non-review repo path; planning artifacts alone are not sufficient.'
|
|
739
|
+
);
|
|
740
|
+
}
|
|
728
741
|
}
|
|
729
742
|
|
|
730
743
|
// Validate proposed_changes for proposed runtimes that cannot write repo files directly.
|
|
@@ -772,6 +785,24 @@ function isAllowedReviewPath(filePath) {
|
|
|
772
785
|
return filePath.startsWith('.planning/') || filePath.startsWith('.agentxchain/reviews/');
|
|
773
786
|
}
|
|
774
787
|
|
|
788
|
+
function isProductChangePath(filePath) {
|
|
789
|
+
return typeof filePath === 'string'
|
|
790
|
+
&& filePath.trim().length > 0
|
|
791
|
+
&& !isAllowedReviewPath(filePath)
|
|
792
|
+
&& !filePath.startsWith('.agentxchain/staging/');
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function hasCheckpointableProducedFiles(tr) {
|
|
796
|
+
return Array.isArray(tr.verification?.produced_files)
|
|
797
|
+
&& tr.verification.produced_files.some((entry) => (
|
|
798
|
+
entry
|
|
799
|
+
&& typeof entry === 'object'
|
|
800
|
+
&& typeof entry.path === 'string'
|
|
801
|
+
&& entry.path.trim()
|
|
802
|
+
&& (entry.disposition == null || entry.disposition === 'artifact')
|
|
803
|
+
));
|
|
804
|
+
}
|
|
805
|
+
|
|
775
806
|
// ── Stage D: Verification Validation ─────────────────────────────────────────
|
|
776
807
|
|
|
777
808
|
function validateVerification(tr) {
|
|
@@ -1488,7 +1519,17 @@ export function normalizeTurnResult(tr, config, context = {}) {
|
|
|
1488
1519
|
&& !Array.isArray(normalized.artifact)
|
|
1489
1520
|
&& normalized.artifact.type === 'workspace'
|
|
1490
1521
|
&& filesChangedIsEmpty
|
|
1491
|
-
&& (
|
|
1522
|
+
&& !hasCheckpointableProducedFiles(normalized)
|
|
1523
|
+
&& (
|
|
1524
|
+
context.forceReviewArtifact
|
|
1525
|
+
|| hasExplicitNoEditLifecycleSignal
|
|
1526
|
+
// review_only roles (PM/QA) legitimately produce no-edit reviews — an empty
|
|
1527
|
+
// workspace from a COMPLETED review_only turn normalizes to a review. Authoritative
|
|
1528
|
+
// /code-writing roles claiming a completed workspace artifact with no files stay
|
|
1529
|
+
// fail-closed, and non-completed (blocked/failed) turns never normalize (BUG-78).
|
|
1530
|
+
|| (isReviewOnly && normalized.status === 'completed')
|
|
1531
|
+
|| (normalized.status === 'needs_human' && normalized.proposed_next_role === 'human')
|
|
1532
|
+
)
|
|
1492
1533
|
) {
|
|
1493
1534
|
normalized.artifact = {
|
|
1494
1535
|
...normalized.artifact,
|
package/src/lib/validation.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { validateStagedTurnResult, STAGING_PATH } from './turn-result-validator.js';
|
|
4
4
|
import { getActiveTurn } from './governed-state.js';
|
|
5
|
+
import { getTurnStagingResultPath } from './turn-paths.js';
|
|
5
6
|
import {
|
|
6
7
|
validateGovernedProjectTemplate,
|
|
7
8
|
validateGovernedTemplateRegistry,
|
|
@@ -191,11 +192,18 @@ export function validateGovernedProject(root, rawConfig, config, opts = {}) {
|
|
|
191
192
|
|
|
192
193
|
// ── Staged turn-result validation (the acceptance boundary) ─────────────
|
|
193
194
|
if (mode === 'turn' && state) {
|
|
194
|
-
|
|
195
|
+
// RB-12: prefer the turn-scoped staging path for the active turn; the shared
|
|
196
|
+
// legacy path is only a fallback. Reading the legacy path unconditionally
|
|
197
|
+
// made `validate --mode turn` look at the wrong file and falsely report a
|
|
198
|
+
// missing turn result for turns staged at the turn-scoped path.
|
|
199
|
+
const activeTurn = getActiveTurn(state) || state.current_turn || null;
|
|
200
|
+
const turnScoped = activeTurn?.turn_id ? getTurnStagingResultPath(activeTurn.turn_id) : null;
|
|
201
|
+
const stagingRel = (turnScoped && existsSync(join(root, turnScoped))) ? turnScoped : STAGING_PATH;
|
|
202
|
+
const stagingAbs = join(root, stagingRel);
|
|
195
203
|
if (!existsSync(stagingAbs)) {
|
|
196
|
-
warnings.push(`No staged turn result found at ${
|
|
204
|
+
warnings.push(`No staged turn result found at ${stagingRel}. Agent has not yet emitted a turn result.`);
|
|
197
205
|
} else {
|
|
198
|
-
const turnValidation = validateStagedTurnResult(root, state, config);
|
|
206
|
+
const turnValidation = validateStagedTurnResult(root, state, config, { stagingPath: stagingRel });
|
|
199
207
|
if (!turnValidation.ok) {
|
|
200
208
|
errors.push(
|
|
201
209
|
`Staged turn result failed at stage "${turnValidation.stage}" (${turnValidation.error_class}):`,
|
|
@@ -8,7 +8,7 @@ import { isOperationalPath } from './repo-observer.js';
|
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS = 30_000;
|
|
10
10
|
|
|
11
|
-
export function replayVerificationMachineEvidence({ root, verification, timeoutMs = DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS }) {
|
|
11
|
+
export function replayVerificationMachineEvidence({ root, verification, timeoutMs = DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS, allowCommandExecution = false }) {
|
|
12
12
|
const verifiedAt = new Date().toISOString();
|
|
13
13
|
const machineEvidence = Array.isArray(verification?.machine_evidence)
|
|
14
14
|
? verification.machine_evidence
|
|
@@ -28,6 +28,27 @@ export function replayVerificationMachineEvidence({ root, verification, timeoutM
|
|
|
28
28
|
return payload;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// SECURITY (verification-replay hardening): agent-declared machine_evidence
|
|
32
|
+
// commands are UNTRUSTED. Fail-safe default — they are recorded but NOT executed
|
|
33
|
+
// unless the operator has explicitly opted in (allowCommandExecution). When
|
|
34
|
+
// executed, they never run through a shell (see replayEvidenceCommand).
|
|
35
|
+
if (!allowCommandExecution) {
|
|
36
|
+
payload.overall = 'not_executed';
|
|
37
|
+
payload.reason = 'Command replay disabled: agent-declared verification commands are not executed unless explicitly enabled by the operator.';
|
|
38
|
+
payload.commands = machineEvidence.map((entry, index) => ({
|
|
39
|
+
index,
|
|
40
|
+
command: entry?.command ?? null,
|
|
41
|
+
declared_exit_code: entry?.exit_code,
|
|
42
|
+
actual_exit_code: null,
|
|
43
|
+
matched: false,
|
|
44
|
+
executed: false,
|
|
45
|
+
timed_out: false,
|
|
46
|
+
signal: null,
|
|
47
|
+
error: null,
|
|
48
|
+
}));
|
|
49
|
+
return payload;
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
const workspaceGuard = createReplayWorkspaceGuard(root);
|
|
32
53
|
try {
|
|
33
54
|
payload.commands = machineEvidence.map((entry, index) => replayEvidenceCommand(root, entry, index, timeoutMs));
|
|
@@ -41,11 +62,33 @@ export function replayVerificationMachineEvidence({ root, verification, timeoutM
|
|
|
41
62
|
return payload;
|
|
42
63
|
}
|
|
43
64
|
|
|
65
|
+
// SECURITY: agent-declared commands are NEVER run through a shell. The command is
|
|
66
|
+
// resolved to an argv vector and executed with shell:false, so shell metacharacters
|
|
67
|
+
// (; | && $() backticks globs) cannot chain, pipe, expand, or substitute. (A single
|
|
68
|
+
// declared binary still runs on the host — that is gated by the explicit
|
|
69
|
+
// allowCommandExecution opt-in upstream; a future sandbox/allowlist can constrain
|
|
70
|
+
// which binaries are permitted.)
|
|
44
71
|
export function replayEvidenceCommand(root, entry, index, timeoutMs = DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS) {
|
|
45
|
-
const
|
|
72
|
+
const argv = resolveEvidenceArgv(entry);
|
|
73
|
+
if (argv.length === 0) {
|
|
74
|
+
return {
|
|
75
|
+
index,
|
|
76
|
+
command: entry?.command ?? null,
|
|
77
|
+
declared_exit_code: entry?.exit_code,
|
|
78
|
+
actual_exit_code: null,
|
|
79
|
+
matched: false,
|
|
80
|
+
executed: false,
|
|
81
|
+
timed_out: false,
|
|
82
|
+
signal: null,
|
|
83
|
+
error: 'No executable command or argv vector could be resolved for this evidence entry.',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const [file, ...args] = argv;
|
|
88
|
+
const result = spawnSync(file, args, {
|
|
46
89
|
cwd: root,
|
|
47
90
|
encoding: 'utf8',
|
|
48
|
-
shell:
|
|
91
|
+
shell: false,
|
|
49
92
|
timeout: timeoutMs,
|
|
50
93
|
maxBuffer: 1024 * 1024,
|
|
51
94
|
});
|
|
@@ -56,16 +99,94 @@ export function replayEvidenceCommand(root, entry, index, timeoutMs = DEFAULT_VE
|
|
|
56
99
|
|
|
57
100
|
return {
|
|
58
101
|
index,
|
|
59
|
-
command: entry.command,
|
|
102
|
+
command: entry.command ?? argv.join(' '),
|
|
60
103
|
declared_exit_code: entry.exit_code,
|
|
61
104
|
actual_exit_code: actualExitCode,
|
|
62
105
|
matched: actualExitCode === entry.exit_code,
|
|
106
|
+
executed: true,
|
|
63
107
|
timed_out: timedOut,
|
|
64
108
|
signal: result.signal || null,
|
|
65
109
|
error: errorMessage,
|
|
66
110
|
};
|
|
67
111
|
}
|
|
68
112
|
|
|
113
|
+
// Resolve an evidence entry to an argv vector for shell-free execution.
|
|
114
|
+
// Prefers a structured `argv` string array; falls back to safe-tokenizing `command`.
|
|
115
|
+
export function resolveEvidenceArgv(entry) {
|
|
116
|
+
if (Array.isArray(entry?.argv) && entry.argv.length > 0 && entry.argv.every((a) => typeof a === 'string')) {
|
|
117
|
+
return entry.argv.slice();
|
|
118
|
+
}
|
|
119
|
+
if (typeof entry?.command === 'string' && entry.command.trim()) {
|
|
120
|
+
return tokenizeCommand(entry.command.trim());
|
|
121
|
+
}
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Minimal SAFE tokenizer: splits on whitespace, honoring single and double quotes
|
|
126
|
+
// and backslash escapes (so e.g. `node -e "{\"ok\":true}"` keeps its escaped quotes).
|
|
127
|
+
// Performs NO shell expansion — variables, globs, pipes, chaining, and command
|
|
128
|
+
// substitution are preserved as literal characters, never interpreted.
|
|
129
|
+
export function tokenizeCommand(command) {
|
|
130
|
+
if (typeof command !== 'string') {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const tokens = [];
|
|
134
|
+
let current = '';
|
|
135
|
+
let hasToken = false;
|
|
136
|
+
let i = 0;
|
|
137
|
+
const n = command.length;
|
|
138
|
+
while (i < n) {
|
|
139
|
+
const ch = command[i];
|
|
140
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
141
|
+
if (hasToken) {
|
|
142
|
+
tokens.push(current);
|
|
143
|
+
current = '';
|
|
144
|
+
hasToken = false;
|
|
145
|
+
}
|
|
146
|
+
i += 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
hasToken = true;
|
|
150
|
+
if (ch === "'") {
|
|
151
|
+
// Single-quoted: literal run until the next single quote (no escapes, POSIX-style).
|
|
152
|
+
i += 1;
|
|
153
|
+
while (i < n && command[i] !== "'") {
|
|
154
|
+
current += command[i];
|
|
155
|
+
i += 1;
|
|
156
|
+
}
|
|
157
|
+
i += 1; // consume closing quote (or run off the end)
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (ch === '"') {
|
|
161
|
+
// Double-quoted: backslash escapes only the structural `"` and `\` characters.
|
|
162
|
+
i += 1;
|
|
163
|
+
while (i < n && command[i] !== '"') {
|
|
164
|
+
if (command[i] === '\\' && i + 1 < n && (command[i + 1] === '"' || command[i + 1] === '\\')) {
|
|
165
|
+
current += command[i + 1];
|
|
166
|
+
i += 2;
|
|
167
|
+
} else {
|
|
168
|
+
current += command[i];
|
|
169
|
+
i += 1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
i += 1; // consume closing quote (or run off the end)
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (ch === '\\' && i + 1 < n) {
|
|
176
|
+
// Bare backslash escape: take the next character literally.
|
|
177
|
+
current += command[i + 1];
|
|
178
|
+
i += 2;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
current += ch;
|
|
182
|
+
i += 1;
|
|
183
|
+
}
|
|
184
|
+
if (hasToken) {
|
|
185
|
+
tokens.push(current);
|
|
186
|
+
}
|
|
187
|
+
return tokens;
|
|
188
|
+
}
|
|
189
|
+
|
|
69
190
|
export function summarizeVerificationReplay(payload) {
|
|
70
191
|
if (!payload) {
|
|
71
192
|
return null;
|
package/src/lib/vision-reader.js
CHANGED
|
@@ -15,6 +15,18 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
15
15
|
import { join, resolve as pathResolve, isAbsolute } from 'node:path';
|
|
16
16
|
import { createHash } from 'node:crypto';
|
|
17
17
|
|
|
18
|
+
const ROADMAP_TRACKING_ANNOTATION_PATTERN = /<!--\s*tracking\s*:[\s\S]*?-->/i;
|
|
19
|
+
// RB-3: a milestone left unchecked but annotated as delivered/done/shipped is
|
|
20
|
+
// already-built work — skip it so the loop derives genuinely undelivered work
|
|
21
|
+
// instead of re-picking finished items. Distinct from generic comments like
|
|
22
|
+
// `<!-- owner: dev -->`, which stay actionable.
|
|
23
|
+
const ROADMAP_DELIVERED_ANNOTATION_PATTERN = /<!--[^>]*\b(delivered|shipped|implemented|completed|done)\b[^>]*-->/i;
|
|
24
|
+
|
|
25
|
+
export function stripRoadmapTrackingAnnotations(text) {
|
|
26
|
+
if (typeof text !== 'string') return '';
|
|
27
|
+
return text.replace(ROADMAP_TRACKING_ANNOTATION_PATTERN, '').replace(/\s+/g, ' ').trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
// ---------------------------------------------------------------------------
|
|
19
31
|
// Parsing
|
|
20
32
|
// ---------------------------------------------------------------------------
|
|
@@ -259,8 +271,10 @@ export function deriveRoadmapCandidates(root, roadmapPath = '.planning/ROADMAP.m
|
|
|
259
271
|
|
|
260
272
|
const uncheckedMatch = line.match(/^\s*[-*]\s+\[\s\]\s+(.+?)\s*$/);
|
|
261
273
|
if (!uncheckedMatch || !currentMilestone) continue;
|
|
274
|
+
if (ROADMAP_TRACKING_ANNOTATION_PATTERN.test(line) || ROADMAP_DELIVERED_ANNOTATION_PATTERN.test(line)) continue;
|
|
262
275
|
|
|
263
|
-
const goal = uncheckedMatch[1]
|
|
276
|
+
const goal = stripRoadmapTrackingAnnotations(uncheckedMatch[1]);
|
|
277
|
+
if (!goal) continue;
|
|
264
278
|
const combinedGoal = `${currentMilestone}: ${goal}`;
|
|
265
279
|
if (isGoalAddressed(combinedGoal, allSignals) || isGoalAddressed(goal, allSignals)) {
|
|
266
280
|
continue;
|
|
@@ -484,6 +498,7 @@ export function detectRoadmapExhaustedVisionOpen(root, visionPath, roadmapPath =
|
|
|
484
498
|
continue;
|
|
485
499
|
}
|
|
486
500
|
if (currentMilestone && /^\s*[-*]\s+\[\s\]/.test(line)) {
|
|
501
|
+
if (ROADMAP_TRACKING_ANNOTATION_PATTERN.test(line) || ROADMAP_DELIVERED_ANNOTATION_PATTERN.test(line)) continue;
|
|
487
502
|
hasUnchecked = true;
|
|
488
503
|
}
|
|
489
504
|
}
|