agentxchain 2.62.0 → 2.64.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/bin/agentxchain.js +73 -2
- package/package.json +1 -1
- package/scripts/release-bump.sh +38 -3
- package/src/commands/gate.js +315 -0
- package/src/commands/phase.js +159 -0
- package/src/commands/role.js +106 -0
- package/src/commands/turn.js +210 -0
- package/src/commands/verify.js +225 -0
- package/src/lib/gate-evaluator.js +3 -3
package/bin/agentxchain.js
CHANGED
|
@@ -59,7 +59,7 @@ import { generateCommand } from '../src/commands/generate.js';
|
|
|
59
59
|
import { doctorCommand } from '../src/commands/doctor.js';
|
|
60
60
|
import { superviseCommand } from '../src/commands/supervise.js';
|
|
61
61
|
import { validateCommand } from '../src/commands/validate.js';
|
|
62
|
-
import { verifyExportCommand, verifyProtocolCommand } from '../src/commands/verify.js';
|
|
62
|
+
import { verifyExportCommand, verifyProtocolCommand, verifyTurnCommand } from '../src/commands/verify.js';
|
|
63
63
|
import { kickoffCommand } from '../src/commands/kickoff.js';
|
|
64
64
|
import { rebindCommand } from '../src/commands/rebind.js';
|
|
65
65
|
import { branchCommand } from '../src/commands/branch.js';
|
|
@@ -86,6 +86,10 @@ import {
|
|
|
86
86
|
} from '../src/commands/plugin.js';
|
|
87
87
|
import { templateSetCommand } from '../src/commands/template-set.js';
|
|
88
88
|
import { templateListCommand } from '../src/commands/template-list.js';
|
|
89
|
+
import { phaseCommand } from '../src/commands/phase.js';
|
|
90
|
+
import { gateCommand } from '../src/commands/gate.js';
|
|
91
|
+
import { roleCommand } from '../src/commands/role.js';
|
|
92
|
+
import { turnShowCommand } from '../src/commands/turn.js';
|
|
89
93
|
import { templateValidateCommand } from '../src/commands/template-validate.js';
|
|
90
94
|
import {
|
|
91
95
|
multiInitCommand,
|
|
@@ -328,7 +332,14 @@ program
|
|
|
328
332
|
|
|
329
333
|
const verifyCmd = program
|
|
330
334
|
.command('verify')
|
|
331
|
-
.description('Verify protocol conformance targets');
|
|
335
|
+
.description('Verify governed turns, export artifacts, and protocol conformance targets');
|
|
336
|
+
|
|
337
|
+
verifyCmd
|
|
338
|
+
.command('turn [turn_id]')
|
|
339
|
+
.description('Replay a staged turn\'s declared machine-evidence commands and compare exit codes')
|
|
340
|
+
.option('-j, --json', 'Output as JSON')
|
|
341
|
+
.option('--timeout <ms>', 'Per-command replay timeout in milliseconds', '30000')
|
|
342
|
+
.action(verifyTurnCommand);
|
|
332
343
|
|
|
333
344
|
verifyCmd
|
|
334
345
|
.command('protocol')
|
|
@@ -485,6 +496,66 @@ templateCmd
|
|
|
485
496
|
.option('-j, --json', 'Output as JSON')
|
|
486
497
|
.action(templateValidateCommand);
|
|
487
498
|
|
|
499
|
+
const phaseCmd = program
|
|
500
|
+
.command('phase')
|
|
501
|
+
.description('Inspect governed workflow phases');
|
|
502
|
+
|
|
503
|
+
phaseCmd
|
|
504
|
+
.command('list')
|
|
505
|
+
.description('List governed phases in routing order')
|
|
506
|
+
.option('-j, --json', 'Output as JSON')
|
|
507
|
+
.action((opts) => phaseCommand('list', null, opts));
|
|
508
|
+
|
|
509
|
+
phaseCmd
|
|
510
|
+
.command('show [phase]')
|
|
511
|
+
.description('Show one governed phase in detail')
|
|
512
|
+
.option('-j, --json', 'Output as JSON')
|
|
513
|
+
.action((phaseId, opts) => phaseCommand('show', phaseId, opts));
|
|
514
|
+
|
|
515
|
+
const gateCmd = program
|
|
516
|
+
.command('gate')
|
|
517
|
+
.description('Inspect governed gate definitions');
|
|
518
|
+
|
|
519
|
+
gateCmd
|
|
520
|
+
.command('list')
|
|
521
|
+
.description('List all defined gates with phase linkage and predicate summary')
|
|
522
|
+
.option('-j, --json', 'Output as JSON')
|
|
523
|
+
.action((opts) => gateCommand('list', null, opts));
|
|
524
|
+
|
|
525
|
+
gateCmd
|
|
526
|
+
.command('show <gate_id>')
|
|
527
|
+
.description('Show a single gate contract, predicates, and status')
|
|
528
|
+
.option('-j, --json', 'Output as JSON')
|
|
529
|
+
.option('--evaluate', 'Live-evaluate gate predicates against current filesystem')
|
|
530
|
+
.action((gateId, opts) => gateCommand('show', gateId, opts));
|
|
531
|
+
|
|
532
|
+
const roleCmd = program
|
|
533
|
+
.command('role')
|
|
534
|
+
.description('Inspect governed role definitions');
|
|
535
|
+
|
|
536
|
+
roleCmd
|
|
537
|
+
.command('list')
|
|
538
|
+
.description('List all defined roles with title, authority, and runtime')
|
|
539
|
+
.option('-j, --json', 'Output as JSON')
|
|
540
|
+
.action((opts) => roleCommand('list', null, opts));
|
|
541
|
+
|
|
542
|
+
roleCmd
|
|
543
|
+
.command('show <role_id>')
|
|
544
|
+
.description('Show detailed information for a single role')
|
|
545
|
+
.option('-j, --json', 'Output as JSON')
|
|
546
|
+
.action((roleId, opts) => roleCommand('show', roleId, opts));
|
|
547
|
+
|
|
548
|
+
const turnCmd = program
|
|
549
|
+
.command('turn')
|
|
550
|
+
.description('Inspect active governed turn dispatch bundles');
|
|
551
|
+
|
|
552
|
+
turnCmd
|
|
553
|
+
.command('show [turn_id]')
|
|
554
|
+
.description('Show a selected active turn and its dispatch artifacts')
|
|
555
|
+
.option('--artifact <name>', 'Print one artifact: assignment, prompt, context, or manifest')
|
|
556
|
+
.option('-j, --json', 'Output as JSON')
|
|
557
|
+
.action(turnShowCommand);
|
|
558
|
+
|
|
488
559
|
const multiCmd = program
|
|
489
560
|
.command('multi')
|
|
490
561
|
.description('Multi-repo coordinator orchestration');
|
package/package.json
CHANGED
package/scripts/release-bump.sh
CHANGED
|
@@ -198,21 +198,53 @@ echo " OK: all 9 governed version surfaces reference ${TARGET_VERSION}"
|
|
|
198
198
|
|
|
199
199
|
# 5. Auto-align Homebrew mirror to target version
|
|
200
200
|
# The formula URL and README version/tarball are updated automatically.
|
|
201
|
-
# The SHA256 is carried from the previous
|
|
201
|
+
# The SHA256 is carried from the previous committed formula — it is inherently a
|
|
202
202
|
# post-publish artifact (npm registry tarballs are not byte-identical to
|
|
203
|
-
# local npm-pack output).
|
|
203
|
+
# local npm-pack output). Any working-tree SHA edit is overwritten here.
|
|
204
|
+
# sync-homebrew.sh corrects the SHA after publish.
|
|
204
205
|
echo "[5/9] Auto-aligning Homebrew mirror to ${TARGET_VERSION}..."
|
|
205
206
|
HOMEBREW_MIRROR="${REPO_ROOT}/cli/homebrew/agentxchain.rb"
|
|
206
207
|
HOMEBREW_MIRROR_README="${REPO_ROOT}/cli/homebrew/README.md"
|
|
207
208
|
TARBALL_URL="https://registry.npmjs.org/agentxchain/-/agentxchain-${TARGET_VERSION}.tgz"
|
|
208
209
|
HOMEBREW_ALIGNED=false
|
|
210
|
+
COMMITTED_HOMEBREW_SHA=""
|
|
211
|
+
|
|
212
|
+
extract_formula_sha() {
|
|
213
|
+
sed -nE 's|^[[:space:]]*sha256 "([a-f0-9]{64})".*|\1|p' "$1" | head -n 1
|
|
214
|
+
}
|
|
209
215
|
|
|
210
216
|
if [[ -f "$HOMEBREW_MIRROR" ]]; then
|
|
217
|
+
COMMITTED_FORMULA_TMP="$(mktemp "${TMPDIR:-/tmp}/agentxchain-homebrew-head.XXXXXX")"
|
|
218
|
+
if ! git -C "$REPO_ROOT" show "HEAD:cli/homebrew/agentxchain.rb" >"$COMMITTED_FORMULA_TMP" 2>/dev/null; then
|
|
219
|
+
rm -f "$COMMITTED_FORMULA_TMP"
|
|
220
|
+
echo "FAIL: could not load HEAD:cli/homebrew/agentxchain.rb to carry the pre-publish SHA" >&2
|
|
221
|
+
exit 1
|
|
222
|
+
fi
|
|
223
|
+
COMMITTED_HOMEBREW_SHA="$(extract_formula_sha "$COMMITTED_FORMULA_TMP")"
|
|
224
|
+
rm -f "$COMMITTED_FORMULA_TMP"
|
|
225
|
+
if [[ -z "$COMMITTED_HOMEBREW_SHA" ]]; then
|
|
226
|
+
echo "FAIL: HEAD:cli/homebrew/agentxchain.rb does not contain a parseable sha256" >&2
|
|
227
|
+
exit 1
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
WORKTREE_HOMEBREW_SHA="$(extract_formula_sha "$HOMEBREW_MIRROR")"
|
|
231
|
+
if [[ -z "$WORKTREE_HOMEBREW_SHA" ]]; then
|
|
232
|
+
echo "FAIL: cli/homebrew/agentxchain.rb does not contain a parseable sha256" >&2
|
|
233
|
+
exit 1
|
|
234
|
+
fi
|
|
235
|
+
|
|
211
236
|
ESCAPED_URL="$(printf '%s' "$TARBALL_URL" | sed 's/[&/\]/\\&/g')"
|
|
237
|
+
ESCAPED_SHA="$(printf '%s' "$COMMITTED_HOMEBREW_SHA" | sed 's/[&/\]/\\&/g')"
|
|
212
238
|
sed -i.bak -E "s|^([[:space:]]*url \").*(\")|\1${ESCAPED_URL}\2|" "$HOMEBREW_MIRROR"
|
|
239
|
+
sed -i.bak -E "s|^([[:space:]]*sha256 \").*(\")|\1${ESCAPED_SHA}\2|" "$HOMEBREW_MIRROR"
|
|
213
240
|
rm -f "${HOMEBREW_MIRROR}.bak"
|
|
214
241
|
HOMEBREW_ALIGNED=true
|
|
215
242
|
echo " OK: formula URL -> ${TARBALL_URL}"
|
|
243
|
+
if [[ "$WORKTREE_HOMEBREW_SHA" != "$COMMITTED_HOMEBREW_SHA" ]]; then
|
|
244
|
+
echo " OK: formula SHA normalized back to committed pre-publish SHA ${COMMITTED_HOMEBREW_SHA}"
|
|
245
|
+
else
|
|
246
|
+
echo " OK: formula SHA carried from committed pre-publish SHA ${COMMITTED_HOMEBREW_SHA}"
|
|
247
|
+
fi
|
|
216
248
|
fi
|
|
217
249
|
|
|
218
250
|
if [[ -f "$HOMEBREW_MIRROR_README" ]]; then
|
|
@@ -223,7 +255,7 @@ if [[ -f "$HOMEBREW_MIRROR_README" ]]; then
|
|
|
223
255
|
fi
|
|
224
256
|
|
|
225
257
|
if $HOMEBREW_ALIGNED; then
|
|
226
|
-
echo " Note:
|
|
258
|
+
echo " Note: local npm pack output is not canonical release truth; sync-homebrew.sh will set the real registry SHA post-publish"
|
|
227
259
|
else
|
|
228
260
|
echo " Skipped: no Homebrew mirror files found"
|
|
229
261
|
fi
|
|
@@ -346,4 +378,7 @@ if [[ "$SKIP_PREFLIGHT" -eq 1 ]]; then
|
|
|
346
378
|
echo " npm run preflight:release:strict -- --target-version ${TARGET_VERSION}"
|
|
347
379
|
fi
|
|
348
380
|
echo ""
|
|
381
|
+
echo "Homebrew mirror is in Phase 1 (stale SHA from previous version)."
|
|
382
|
+
echo "After npm publish completes, run sync-homebrew.sh to reach Phase 3."
|
|
383
|
+
echo ""
|
|
349
384
|
echo "Next: git push origin main --follow-tags"
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
5
|
+
import {
|
|
6
|
+
evaluateArtifactSemantics,
|
|
7
|
+
evaluateWorkflowGateSemantics,
|
|
8
|
+
getSemanticIdForPath,
|
|
9
|
+
} from '../lib/workflow-gate-semantics.js';
|
|
10
|
+
import { getEffectiveGateArtifacts, hasRoleParticipationInPhase } from '../lib/gate-evaluator.js';
|
|
11
|
+
|
|
12
|
+
export function gateCommand(subcommand, gateId, opts) {
|
|
13
|
+
const context = loadProjectContext();
|
|
14
|
+
if (!context) {
|
|
15
|
+
console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { root, config, version } = context;
|
|
20
|
+
if (version !== 4 || config.protocol_mode !== 'governed') {
|
|
21
|
+
console.log(chalk.red(' Not a governed AgentXchain project (requires v4 config).'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const gateIds = Object.keys(config.gates || {});
|
|
26
|
+
if (gateIds.length === 0) {
|
|
27
|
+
console.log(chalk.red(' No gates defined in config.'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const state = loadProjectState(root, config);
|
|
32
|
+
|
|
33
|
+
if (subcommand === 'show') {
|
|
34
|
+
return showGate(gateId, { root, config, state, gateIds, opts });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return listGates({ root, config, state, gateIds, opts });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findLinkedPhase(gateId, routing) {
|
|
41
|
+
for (const [phaseId, route] of Object.entries(routing || {})) {
|
|
42
|
+
if (route.exit_gate === gateId) return phaseId;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildGateRecord(root, config, state, gateId, evaluate) {
|
|
48
|
+
const gateDef = config.gates[gateId] || {};
|
|
49
|
+
const linkedPhase = findLinkedPhase(gateId, config.routing);
|
|
50
|
+
const effectiveArtifacts = getEffectiveGateArtifacts(config, gateDef, linkedPhase);
|
|
51
|
+
const requiresFiles = Array.isArray(gateDef.requires_files) ? gateDef.requires_files : [];
|
|
52
|
+
const requiresVerification = gateDef.requires_verification_pass === true;
|
|
53
|
+
const requiresHumanApproval = gateDef.requires_human_approval === true;
|
|
54
|
+
const status = state?.phase_gate_status?.[gateId] || null;
|
|
55
|
+
|
|
56
|
+
const lastFailure = state?.last_gate_failure?.gate_id === gateId
|
|
57
|
+
? state.last_gate_failure
|
|
58
|
+
: null;
|
|
59
|
+
|
|
60
|
+
const record = {
|
|
61
|
+
id: gateId,
|
|
62
|
+
linked_phase: linkedPhase,
|
|
63
|
+
requires_files: requiresFiles,
|
|
64
|
+
effective_artifacts: effectiveArtifacts.map(normalizeEffectiveArtifact),
|
|
65
|
+
requires_verification_pass: requiresVerification,
|
|
66
|
+
requires_human_approval: requiresHumanApproval,
|
|
67
|
+
status,
|
|
68
|
+
last_failure: lastFailure
|
|
69
|
+
? {
|
|
70
|
+
gate_type: lastFailure.gate_type,
|
|
71
|
+
phase: lastFailure.phase,
|
|
72
|
+
failed_at: lastFailure.failed_at,
|
|
73
|
+
reasons: lastFailure.reasons || [],
|
|
74
|
+
missing_files: lastFailure.missing_files || [],
|
|
75
|
+
}
|
|
76
|
+
: null,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (evaluate) {
|
|
80
|
+
record.evaluation = evaluateGateSnapshot({
|
|
81
|
+
root,
|
|
82
|
+
state,
|
|
83
|
+
linkedPhase,
|
|
84
|
+
requiresVerification,
|
|
85
|
+
effectiveArtifacts,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return record;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeEffectiveArtifact(artifact) {
|
|
93
|
+
return {
|
|
94
|
+
path: artifact.path,
|
|
95
|
+
required: artifact.required !== false,
|
|
96
|
+
owned_by: artifact.owned_by || null,
|
|
97
|
+
legacy_semantics: artifact.useLegacySemantics ? getSemanticIdForPath(artifact.path) : null,
|
|
98
|
+
semantic_checks: Array.isArray(artifact.semanticChecks) ? artifact.semanticChecks : [],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getLatestAcceptedTurnForPhase(state, phase) {
|
|
103
|
+
if (!phase || !Array.isArray(state?.history)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (let index = state.history.length - 1; index >= 0; index--) {
|
|
108
|
+
const entry = state.history[index];
|
|
109
|
+
if (entry?.phase === phase) {
|
|
110
|
+
return entry;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildOwnershipFailure(artifact, linkedPhase) {
|
|
118
|
+
if (!artifact.owned_by) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!linkedPhase) {
|
|
123
|
+
return `Gate is not linked to a routing phase, so ownership for "${artifact.path}" cannot be evaluated.`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return `"${artifact.path}" requires participation from role "${artifact.owned_by}" in phase "${linkedPhase}", but no accepted turn from that role was found`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function evaluateGateSnapshot({ root, state, linkedPhase, requiresVerification, effectiveArtifacts }) {
|
|
130
|
+
const missingFiles = [];
|
|
131
|
+
const semanticFailures = [];
|
|
132
|
+
const ownershipFailures = [];
|
|
133
|
+
|
|
134
|
+
const artifacts = effectiveArtifacts.map((artifact) => {
|
|
135
|
+
const exists = existsSync(join(root, artifact.path));
|
|
136
|
+
const failures = [];
|
|
137
|
+
|
|
138
|
+
if (!exists && artifact.required) {
|
|
139
|
+
const reason = `Required file missing: ${artifact.path}`;
|
|
140
|
+
missingFiles.push(artifact.path);
|
|
141
|
+
failures.push(reason);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (exists && artifact.useLegacySemantics) {
|
|
145
|
+
const semanticCheck = evaluateWorkflowGateSemantics(root, artifact.path);
|
|
146
|
+
if (semanticCheck && !semanticCheck.ok) {
|
|
147
|
+
semanticFailures.push(semanticCheck.reason);
|
|
148
|
+
failures.push(semanticCheck.reason);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (exists) {
|
|
153
|
+
for (const semantic of artifact.semanticChecks || []) {
|
|
154
|
+
const semanticCheck = evaluateArtifactSemantics(root, {
|
|
155
|
+
path: artifact.path,
|
|
156
|
+
semantics: semantic.semantics,
|
|
157
|
+
semantics_config: semantic.semantics_config,
|
|
158
|
+
});
|
|
159
|
+
if (semanticCheck && !semanticCheck.ok) {
|
|
160
|
+
semanticFailures.push(semanticCheck.reason);
|
|
161
|
+
failures.push(semanticCheck.reason);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const ownershipSatisfied = artifact.owned_by && linkedPhase
|
|
167
|
+
? hasRoleParticipationInPhase(state, linkedPhase, artifact.owned_by)
|
|
168
|
+
: null;
|
|
169
|
+
if (artifact.owned_by && linkedPhase && ownershipSatisfied === false) {
|
|
170
|
+
const reason = buildOwnershipFailure(artifact, linkedPhase);
|
|
171
|
+
ownershipFailures.push(reason);
|
|
172
|
+
failures.push(reason);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
...normalizeEffectiveArtifact(artifact),
|
|
177
|
+
exists,
|
|
178
|
+
ownership_satisfied: ownershipSatisfied,
|
|
179
|
+
failures,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const latestAcceptedTurn = getLatestAcceptedTurnForPhase(state, linkedPhase);
|
|
184
|
+
const verificationStatus = latestAcceptedTurn?.verification?.status || null;
|
|
185
|
+
const verificationPassed = verificationStatus === 'pass' || verificationStatus === 'attested_pass';
|
|
186
|
+
const reasons = [
|
|
187
|
+
...artifacts.flatMap((artifact) => artifact.failures),
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
if (requiresVerification && !verificationPassed) {
|
|
191
|
+
reasons.push(`Verification status is "${verificationStatus || 'missing'}", requires "pass" or "attested_pass"`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
phase: linkedPhase,
|
|
196
|
+
passed: reasons.length === 0,
|
|
197
|
+
reasons,
|
|
198
|
+
missing_files: missingFiles,
|
|
199
|
+
semantic_failures: semanticFailures,
|
|
200
|
+
ownership_failures: ownershipFailures,
|
|
201
|
+
artifacts,
|
|
202
|
+
verification: {
|
|
203
|
+
required: requiresVerification,
|
|
204
|
+
source_turn_id: latestAcceptedTurn?.turn_id || null,
|
|
205
|
+
status: verificationStatus,
|
|
206
|
+
passed: requiresVerification ? verificationPassed : null,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function listGates({ root, config, state, gateIds, opts }) {
|
|
212
|
+
const gates = gateIds.map((id) => buildGateRecord(root, config, state, id, false));
|
|
213
|
+
|
|
214
|
+
if (opts.json) {
|
|
215
|
+
console.log(JSON.stringify({ gates }, null, 2));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log(chalk.bold(`\n Gates (${gates.length}):\n`));
|
|
220
|
+
for (const gate of gates) {
|
|
221
|
+
const phase = gate.linked_phase || chalk.dim('orphaned');
|
|
222
|
+
const approval = gate.requires_human_approval ? chalk.yellow('human-approval') : 'auto';
|
|
223
|
+
const predicates = [];
|
|
224
|
+
if (gate.effective_artifacts.length > 0) predicates.push(`${gate.effective_artifacts.length} artifact${gate.effective_artifacts.length > 1 ? 's' : ''}`);
|
|
225
|
+
if (gate.requires_verification_pass) predicates.push('verification');
|
|
226
|
+
const predicateStr = predicates.length > 0 ? predicates.join(' + ') : 'none';
|
|
227
|
+
const statusStr = gate.status ? ` [${gate.status}]` : '';
|
|
228
|
+
console.log(` ${chalk.cyan(gate.id)}${statusStr} — phase ${chalk.bold(phase)}, ${approval}, requires ${predicateStr}`);
|
|
229
|
+
}
|
|
230
|
+
console.log('');
|
|
231
|
+
console.log(chalk.dim(' Usage: agentxchain gate show <gate_id>\n'));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function showGate(requestedGateId, { root, config, state, gateIds, opts }) {
|
|
235
|
+
if (!requestedGateId) {
|
|
236
|
+
console.log(chalk.red(' Gate ID is required.'));
|
|
237
|
+
console.log(chalk.dim(` Available: ${gateIds.join(', ')}`));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!config.gates[requestedGateId]) {
|
|
242
|
+
console.log(chalk.red(` Unknown gate: ${requestedGateId}`));
|
|
243
|
+
console.log(chalk.dim(` Available: ${gateIds.join(', ')}`));
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const gate = buildGateRecord(root, config, state, requestedGateId, opts.evaluate);
|
|
248
|
+
|
|
249
|
+
if (opts.json) {
|
|
250
|
+
console.log(JSON.stringify(gate, null, 2));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(chalk.bold(`\n Gate: ${chalk.cyan(gate.id)}\n`));
|
|
255
|
+
console.log(` Linked phase: ${gate.linked_phase || chalk.dim('none (orphaned)')}`);
|
|
256
|
+
console.log(` Human approval: ${gate.requires_human_approval ? chalk.yellow('yes') : 'no'}`);
|
|
257
|
+
console.log(` Verification: ${gate.requires_verification_pass ? 'required' : 'not required'}`);
|
|
258
|
+
if (gate.status) {
|
|
259
|
+
const statusColor = gate.status === 'passed' ? chalk.green : gate.status === 'failed' ? chalk.red : chalk.yellow;
|
|
260
|
+
console.log(` Status: ${statusColor(gate.status)}`);
|
|
261
|
+
}
|
|
262
|
+
if (gate.evaluation) {
|
|
263
|
+
console.log(` Evaluation: ${gate.evaluation.passed ? chalk.green('pass') : chalk.red('fail')}`);
|
|
264
|
+
}
|
|
265
|
+
console.log('');
|
|
266
|
+
|
|
267
|
+
if (gate.effective_artifacts.length === 0) {
|
|
268
|
+
console.log(` ${chalk.dim('Effective artifacts:')} none\n`);
|
|
269
|
+
} else {
|
|
270
|
+
console.log(` ${chalk.dim('Effective artifacts:')}`);
|
|
271
|
+
const evaluatedArtifacts = gate.evaluation?.artifacts || [];
|
|
272
|
+
for (const artifact of gate.effective_artifacts) {
|
|
273
|
+
const artifactEval = evaluatedArtifacts.find((entry) => entry.path === artifact.path);
|
|
274
|
+
const icon = artifactEval
|
|
275
|
+
? artifactEval.failures.length === 0
|
|
276
|
+
? chalk.green('\u2713')
|
|
277
|
+
: chalk.red('\u2717')
|
|
278
|
+
: chalk.dim('-');
|
|
279
|
+
const owner = artifact.owned_by || 'none';
|
|
280
|
+
const semantics = [
|
|
281
|
+
artifact.legacy_semantics,
|
|
282
|
+
...artifact.semantic_checks.map((entry) => entry.semantics),
|
|
283
|
+
].filter(Boolean);
|
|
284
|
+
console.log(` ${icon} ${artifact.path} [${artifact.required ? 'required' : 'optional'}] [owner: ${owner}] [semantics: ${semantics.length > 0 ? semantics.join(', ') : 'none'}]`);
|
|
285
|
+
if (artifactEval?.failures.length) {
|
|
286
|
+
for (const failure of artifactEval.failures) {
|
|
287
|
+
console.log(` ${chalk.dim(`- ${failure}`)}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
console.log('');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (gate.evaluation && gate.requires_verification_pass) {
|
|
295
|
+
const vpIcon = gate.evaluation.verification.passed ? chalk.green('\u2713') : chalk.red('\u2717');
|
|
296
|
+
const source = gate.evaluation.verification.source_turn_id
|
|
297
|
+
? ` (${gate.evaluation.verification.source_turn_id})`
|
|
298
|
+
: '';
|
|
299
|
+
console.log(` Verification pass: ${vpIcon} ${gate.evaluation.verification.passed ? 'yes' : 'no'}${source}\n`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (gate.last_failure) {
|
|
303
|
+
console.log(chalk.dim(' Last failure:'));
|
|
304
|
+
console.log(` Type: ${gate.last_failure.gate_type}`);
|
|
305
|
+
console.log(` Phase: ${gate.last_failure.phase}`);
|
|
306
|
+
console.log(` At: ${gate.last_failure.failed_at}`);
|
|
307
|
+
if (gate.last_failure.reasons.length > 0) {
|
|
308
|
+
console.log(` Reasons: ${gate.last_failure.reasons.join('; ')}`);
|
|
309
|
+
}
|
|
310
|
+
if (gate.last_failure.missing_files.length > 0) {
|
|
311
|
+
console.log(` Missing: ${gate.last_failure.missing_files.join(', ')}`);
|
|
312
|
+
}
|
|
313
|
+
console.log('');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
5
|
+
import { getNextPhase } from '../lib/gate-evaluator.js';
|
|
6
|
+
import { getMaxConcurrentTurns } from '../lib/normalized-config.js';
|
|
7
|
+
|
|
8
|
+
export function phaseCommand(subcommand, phaseId, opts) {
|
|
9
|
+
const context = loadProjectContext();
|
|
10
|
+
if (!context) {
|
|
11
|
+
console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { root, config, rawConfig, version } = context;
|
|
16
|
+
if (version !== 4 || config.protocol_mode !== 'governed') {
|
|
17
|
+
console.log(chalk.red(' Not a governed AgentXchain project (requires v4 config).'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const phaseIds = Object.keys(config.routing || {});
|
|
22
|
+
if (phaseIds.length === 0) {
|
|
23
|
+
console.log(chalk.red(' No governed phases are defined in routing.'));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const state = loadProjectState(root, config);
|
|
28
|
+
|
|
29
|
+
if (subcommand === 'show') {
|
|
30
|
+
return showPhase(phaseId, { root, config, rawConfig, state, phaseIds, opts });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return listPhases({ root, config, rawConfig, state, phaseIds, opts });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function listPhases({ root, config, rawConfig, state, phaseIds, opts }) {
|
|
37
|
+
const phases = phaseIds.map((phaseId) => buildPhaseRecord(root, config, rawConfig, state, phaseId));
|
|
38
|
+
|
|
39
|
+
if (opts.json) {
|
|
40
|
+
console.log(JSON.stringify({
|
|
41
|
+
current_phase: state?.phase || null,
|
|
42
|
+
phases,
|
|
43
|
+
}, null, 2));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(chalk.bold(`\n Phases (${phases.length}):\n`));
|
|
48
|
+
for (const phase of phases) {
|
|
49
|
+
const current = phase.is_current ? chalk.green(' [current]') : '';
|
|
50
|
+
const entry = phase.entry_role || 'none';
|
|
51
|
+
const gate = phase.exit_gate || 'open';
|
|
52
|
+
const next = phase.next_phase || 'final';
|
|
53
|
+
console.log(` ${chalk.cyan(phase.id)}${current} — entry ${chalk.bold(entry)}, gate ${gate}, next ${next}, artifacts ${phase.workflow_kit.artifacts.length}`);
|
|
54
|
+
}
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log(chalk.dim(' Usage: agentxchain phase show <phase>\n'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function showPhase(requestedPhaseId, { root, config, rawConfig, state, phaseIds, opts }) {
|
|
60
|
+
const phaseId = requestedPhaseId || state?.phase || phaseIds[0];
|
|
61
|
+
if (!config.routing?.[phaseId]) {
|
|
62
|
+
console.log(chalk.red(` Unknown phase: ${phaseId}`));
|
|
63
|
+
console.log(chalk.dim(` Available: ${phaseIds.join(', ')}`));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const phase = buildPhaseRecord(root, config, rawConfig, state, phaseId);
|
|
68
|
+
|
|
69
|
+
if (opts.json) {
|
|
70
|
+
console.log(JSON.stringify(phase, null, 2));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(chalk.bold(`\n Phase: ${chalk.cyan(phase.id)}${phase.is_current ? chalk.green(' [current]') : ''}\n`));
|
|
75
|
+
console.log(` Entry role: ${phase.entry_role || chalk.dim('none')}`);
|
|
76
|
+
console.log(` Exit gate: ${phase.exit_gate || chalk.dim('open')}`);
|
|
77
|
+
if (phase.exit_gate_status) {
|
|
78
|
+
console.log(` Gate status: ${phase.exit_gate_status}`);
|
|
79
|
+
}
|
|
80
|
+
console.log(` Next phase: ${phase.next_phase || chalk.dim('final')}`);
|
|
81
|
+
console.log(` Next roles: ${phase.allowed_next_roles.length > 0 ? phase.allowed_next_roles.join(', ') : chalk.dim('none')}`);
|
|
82
|
+
console.log(` Max turns: ${phase.max_concurrent_turns}`);
|
|
83
|
+
if (phase.timeout_minutes != null || phase.timeout_action) {
|
|
84
|
+
console.log(` Timeout override: ${phase.timeout_minutes != null ? `${phase.timeout_minutes}m` : chalk.dim('default')} / ${phase.timeout_action || chalk.dim('default')}`);
|
|
85
|
+
}
|
|
86
|
+
console.log(` Workflow source: ${phase.workflow_kit.source}`);
|
|
87
|
+
if (phase.workflow_kit.template) {
|
|
88
|
+
console.log(` Workflow template: ${phase.workflow_kit.template}`);
|
|
89
|
+
}
|
|
90
|
+
console.log('');
|
|
91
|
+
|
|
92
|
+
if (phase.workflow_kit.artifacts.length === 0) {
|
|
93
|
+
console.log(` ${chalk.dim('Artifacts:')} none declared\n`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(` ${chalk.dim('Artifacts:')}`);
|
|
98
|
+
for (const artifact of phase.workflow_kit.artifacts) {
|
|
99
|
+
const icon = artifact.exists ? chalk.green('✓') : (artifact.required ? chalk.red('✗') : chalk.yellow('○'));
|
|
100
|
+
const ownerLabel = artifact.owner_resolution === 'explicit'
|
|
101
|
+
? artifact.owned_by
|
|
102
|
+
: artifact.owner_resolution === 'entry_role'
|
|
103
|
+
? `${chalk.dim(artifact.owned_by + ' (hint, not enforced)')}`
|
|
104
|
+
: 'none';
|
|
105
|
+
const semantics = artifact.semantics || 'none';
|
|
106
|
+
const required = artifact.required ? 'required' : 'optional';
|
|
107
|
+
console.log(` ${icon} ${artifact.path} [${required}] [owner: ${ownerLabel}] [semantics: ${semantics}]`);
|
|
108
|
+
}
|
|
109
|
+
if (phase.workflow_kit.artifacts.some((artifact) => artifact.owner_resolution === 'entry_role')) {
|
|
110
|
+
console.log(` ${chalk.dim('Hint: inferred ownership from entry_role is display-only. Only explicit owned_by is enforced at gate evaluation.')}`);
|
|
111
|
+
}
|
|
112
|
+
console.log('');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildPhaseRecord(root, config, rawConfig, state, phaseId) {
|
|
116
|
+
const route = config.routing?.[phaseId] || {};
|
|
117
|
+
const normalizedPhaseKit = config.workflow_kit?.phases?.[phaseId] || null;
|
|
118
|
+
const rawWorkflowKit = rawConfig.workflow_kit;
|
|
119
|
+
const rawPhaseKit = rawWorkflowKit?.phases?.[phaseId] || null;
|
|
120
|
+
const hasExplicitWorkflowKit = rawWorkflowKit !== undefined && rawWorkflowKit !== null;
|
|
121
|
+
|
|
122
|
+
const workflowSource = !hasExplicitWorkflowKit
|
|
123
|
+
? 'default'
|
|
124
|
+
: rawPhaseKit
|
|
125
|
+
? 'explicit'
|
|
126
|
+
: 'not_declared';
|
|
127
|
+
|
|
128
|
+
const artifacts = (normalizedPhaseKit?.artifacts || []).map((artifact) => {
|
|
129
|
+
const hasExplicitOwner = typeof artifact.owned_by === 'string' && artifact.owned_by.length > 0;
|
|
130
|
+
const ownedBy = hasExplicitOwner ? artifact.owned_by : (route.entry_role || null);
|
|
131
|
+
return {
|
|
132
|
+
path: artifact.path,
|
|
133
|
+
required: artifact.required !== false,
|
|
134
|
+
semantics: artifact.semantics || null,
|
|
135
|
+
owned_by: ownedBy,
|
|
136
|
+
owner_resolution: hasExplicitOwner ? 'explicit' : (ownedBy ? 'entry_role' : 'unowned'),
|
|
137
|
+
ownership_enforced: hasExplicitOwner,
|
|
138
|
+
exists: existsSync(join(root, artifact.path)),
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
id: phaseId,
|
|
144
|
+
is_current: state?.phase === phaseId,
|
|
145
|
+
entry_role: route.entry_role || null,
|
|
146
|
+
exit_gate: route.exit_gate || null,
|
|
147
|
+
exit_gate_status: route.exit_gate ? (state?.phase_gate_status?.[route.exit_gate] || null) : null,
|
|
148
|
+
next_phase: getNextPhase(phaseId, config.routing || {}),
|
|
149
|
+
allowed_next_roles: Array.isArray(route.allowed_next_roles) ? route.allowed_next_roles : [],
|
|
150
|
+
timeout_minutes: typeof route.timeout_minutes === 'number' ? route.timeout_minutes : null,
|
|
151
|
+
timeout_action: typeof route.timeout_action === 'string' ? route.timeout_action : null,
|
|
152
|
+
max_concurrent_turns: getMaxConcurrentTurns(config, phaseId),
|
|
153
|
+
workflow_kit: {
|
|
154
|
+
source: workflowSource,
|
|
155
|
+
template: typeof rawPhaseKit?.template === 'string' ? rawPhaseKit.template : null,
|
|
156
|
+
artifacts,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadProjectContext } from '../lib/config.js';
|
|
3
|
+
|
|
4
|
+
export function roleCommand(subcommand, roleId, opts) {
|
|
5
|
+
const context = loadProjectContext();
|
|
6
|
+
if (!context) {
|
|
7
|
+
console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { rawConfig, version } = context;
|
|
12
|
+
|
|
13
|
+
if (version !== 4) {
|
|
14
|
+
console.log(chalk.red(' Not a governed AgentXchain project (requires v4 config).'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const roles = rawConfig.roles || {};
|
|
19
|
+
const roleIds = Object.keys(roles);
|
|
20
|
+
|
|
21
|
+
if (subcommand === 'show') {
|
|
22
|
+
return showRole(roleId, roles, roleIds, opts);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Default: list
|
|
26
|
+
return listRoles(roles, roleIds, opts);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function listRoles(roles, roleIds, opts) {
|
|
30
|
+
if (roleIds.length === 0) {
|
|
31
|
+
if (opts.json) {
|
|
32
|
+
console.log('[]');
|
|
33
|
+
} else {
|
|
34
|
+
console.log(' No roles defined.');
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (opts.json) {
|
|
40
|
+
const output = roleIds.map((id) => ({
|
|
41
|
+
id,
|
|
42
|
+
title: roles[id].title,
|
|
43
|
+
mandate: roles[id].mandate,
|
|
44
|
+
write_authority: roles[id].write_authority,
|
|
45
|
+
runtime: roles[id].runtime,
|
|
46
|
+
}));
|
|
47
|
+
console.log(JSON.stringify(output, null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(chalk.bold(`\n Roles (${roleIds.length}):\n`));
|
|
52
|
+
for (const id of roleIds) {
|
|
53
|
+
const r = roles[id];
|
|
54
|
+
const authority = r.write_authority === 'authoritative'
|
|
55
|
+
? chalk.green(r.write_authority)
|
|
56
|
+
: r.write_authority === 'proposed'
|
|
57
|
+
? chalk.yellow(r.write_authority)
|
|
58
|
+
: chalk.dim(r.write_authority);
|
|
59
|
+
console.log(` ${chalk.cyan(id)} — ${r.title} [${authority}] → ${chalk.dim(r.runtime)}`);
|
|
60
|
+
}
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(chalk.dim(' Usage: agentxchain role show <role_id>\n'));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function showRole(roleId, roles, roleIds, opts) {
|
|
66
|
+
if (!roleId) {
|
|
67
|
+
console.log(chalk.red(' Missing role ID.'));
|
|
68
|
+
console.log(chalk.dim(` Usage: agentxchain role show <role_id>`));
|
|
69
|
+
if (roleIds.length > 0) {
|
|
70
|
+
console.log(chalk.dim(` Available: ${roleIds.join(', ')}`));
|
|
71
|
+
}
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!roles[roleId]) {
|
|
76
|
+
console.log(chalk.red(` Unknown role: ${roleId}`));
|
|
77
|
+
console.log(chalk.dim(` Available: ${roleIds.join(', ')}`));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const r = roles[roleId];
|
|
82
|
+
|
|
83
|
+
if (opts.json) {
|
|
84
|
+
console.log(JSON.stringify({
|
|
85
|
+
id: roleId,
|
|
86
|
+
title: r.title,
|
|
87
|
+
mandate: r.mandate,
|
|
88
|
+
write_authority: r.write_authority,
|
|
89
|
+
runtime: r.runtime,
|
|
90
|
+
}, null, 2));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const authority = r.write_authority === 'authoritative'
|
|
95
|
+
? chalk.green(r.write_authority)
|
|
96
|
+
: r.write_authority === 'proposed'
|
|
97
|
+
? chalk.yellow(r.write_authority)
|
|
98
|
+
: chalk.dim(r.write_authority);
|
|
99
|
+
|
|
100
|
+
console.log(chalk.bold(`\n Role: ${chalk.cyan(roleId)}\n`));
|
|
101
|
+
console.log(` Title: ${r.title}`);
|
|
102
|
+
console.log(` Mandate: ${r.mandate}`);
|
|
103
|
+
console.log(` Authority: ${authority}`);
|
|
104
|
+
console.log(` Runtime: ${chalk.dim(r.runtime)}`);
|
|
105
|
+
console.log('');
|
|
106
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
5
|
+
import { getActiveTurnCount, getActiveTurns } from '../lib/governed-state.js';
|
|
6
|
+
import {
|
|
7
|
+
getDispatchAssignmentPath,
|
|
8
|
+
getDispatchContextPath,
|
|
9
|
+
getDispatchManifestPath,
|
|
10
|
+
getDispatchPromptPath,
|
|
11
|
+
getDispatchTurnDir,
|
|
12
|
+
} from '../lib/turn-paths.js';
|
|
13
|
+
|
|
14
|
+
const ARTIFACTS = {
|
|
15
|
+
assignment: {
|
|
16
|
+
label: 'Assignment',
|
|
17
|
+
pathFor: getDispatchAssignmentPath,
|
|
18
|
+
parse: 'json',
|
|
19
|
+
},
|
|
20
|
+
prompt: {
|
|
21
|
+
label: 'Prompt',
|
|
22
|
+
pathFor: getDispatchPromptPath,
|
|
23
|
+
parse: 'text',
|
|
24
|
+
},
|
|
25
|
+
context: {
|
|
26
|
+
label: 'Context',
|
|
27
|
+
pathFor: getDispatchContextPath,
|
|
28
|
+
parse: 'text',
|
|
29
|
+
},
|
|
30
|
+
manifest: {
|
|
31
|
+
label: 'Manifest',
|
|
32
|
+
pathFor: getDispatchManifestPath,
|
|
33
|
+
parse: 'json',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function turnShowCommand(turnId, opts) {
|
|
38
|
+
const context = requireGovernedContext();
|
|
39
|
+
const state = loadProjectState(context.root, context.config);
|
|
40
|
+
|
|
41
|
+
if (!state) {
|
|
42
|
+
console.log(chalk.red(' Governed state is missing or invalid.'));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const activeTurns = getActiveTurns(state);
|
|
47
|
+
const selectedTurnId = resolveTurnId(turnId, activeTurns);
|
|
48
|
+
const turn = activeTurns[selectedTurnId];
|
|
49
|
+
const artifacts = buildArtifactIndex(context.root, selectedTurnId);
|
|
50
|
+
const assignment = readJsonArtifact(artifacts.assignment.absPath);
|
|
51
|
+
|
|
52
|
+
if (opts.artifact) {
|
|
53
|
+
return printArtifact(selectedTurnId, turn, artifacts, opts.artifact, opts.json);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (opts.json) {
|
|
57
|
+
console.log(JSON.stringify(buildTurnPayload(selectedTurnId, turn, state, artifacts, assignment), null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
printTurnSummary(selectedTurnId, turn, state, artifacts, assignment);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function requireGovernedContext() {
|
|
65
|
+
const context = loadProjectContext();
|
|
66
|
+
if (!context) {
|
|
67
|
+
console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
if (context.config.protocol_mode !== 'governed') {
|
|
71
|
+
console.log(chalk.red(' The turn command is only available for governed projects.'));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
return context;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveTurnId(requestedTurnId, activeTurns) {
|
|
78
|
+
const turnIds = Object.keys(activeTurns);
|
|
79
|
+
if (requestedTurnId) {
|
|
80
|
+
if (!activeTurns[requestedTurnId]) {
|
|
81
|
+
console.log(chalk.red(` Unknown active turn: ${requestedTurnId}`));
|
|
82
|
+
if (turnIds.length > 0) {
|
|
83
|
+
console.log(chalk.dim(` Available: ${turnIds.join(', ')}`));
|
|
84
|
+
}
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
return requestedTurnId;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (turnIds.length === 0) {
|
|
91
|
+
console.log(chalk.red(' No active turn found.'));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (turnIds.length > 1) {
|
|
96
|
+
console.log(chalk.red(' Multiple active turns are present. Re-run with `agentxchain turn show <turn_id>`.'));
|
|
97
|
+
console.log(chalk.dim(` Available: ${turnIds.join(', ')}`));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return turnIds[0];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildArtifactIndex(root, turnId) {
|
|
105
|
+
return Object.fromEntries(
|
|
106
|
+
Object.entries(ARTIFACTS).map(([id, artifact]) => {
|
|
107
|
+
const relPath = artifact.pathFor(turnId);
|
|
108
|
+
const absPath = join(root, relPath);
|
|
109
|
+
return [id, {
|
|
110
|
+
id,
|
|
111
|
+
label: artifact.label,
|
|
112
|
+
relPath,
|
|
113
|
+
absPath,
|
|
114
|
+
exists: existsSync(absPath),
|
|
115
|
+
parse: artifact.parse,
|
|
116
|
+
}];
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildTurnPayload(turnId, turn, state, artifacts, assignment) {
|
|
122
|
+
return {
|
|
123
|
+
turn_id: turnId,
|
|
124
|
+
run_id: state.run_id || assignment?.run_id || null,
|
|
125
|
+
phase: state.phase || assignment?.phase || null,
|
|
126
|
+
role: turn.assigned_role,
|
|
127
|
+
runtime: turn.runtime_id,
|
|
128
|
+
status: turn.status,
|
|
129
|
+
attempt: turn.attempt,
|
|
130
|
+
dispatch_dir: getDispatchTurnDir(turnId),
|
|
131
|
+
staging_result_path: assignment?.staging_result_path || null,
|
|
132
|
+
active_turn_count: getActiveTurnCount(state),
|
|
133
|
+
artifacts: Object.fromEntries(
|
|
134
|
+
Object.entries(artifacts).map(([id, artifact]) => [id, {
|
|
135
|
+
path: artifact.relPath,
|
|
136
|
+
exists: artifact.exists,
|
|
137
|
+
}]),
|
|
138
|
+
),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function printTurnSummary(turnId, turn, state, artifacts, assignment) {
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(chalk.bold(` Turn: ${chalk.cyan(turnId)}`));
|
|
145
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
146
|
+
console.log(` ${chalk.dim('Run:')} ${state.run_id || assignment?.run_id || chalk.dim('unknown')}`);
|
|
147
|
+
console.log(` ${chalk.dim('Phase:')} ${state.phase || assignment?.phase || chalk.dim('unknown')}`);
|
|
148
|
+
console.log(` ${chalk.dim('Role:')} ${chalk.bold(turn.assigned_role)}`);
|
|
149
|
+
console.log(` ${chalk.dim('Runtime:')} ${turn.runtime_id}`);
|
|
150
|
+
console.log(` ${chalk.dim('Status:')} ${turn.status}`);
|
|
151
|
+
console.log(` ${chalk.dim('Attempt:')} ${turn.attempt}`);
|
|
152
|
+
console.log(` ${chalk.dim('Dispatch:')} ${getDispatchTurnDir(turnId)}`);
|
|
153
|
+
if (assignment?.staging_result_path) {
|
|
154
|
+
console.log(` ${chalk.dim('Staging:')} ${assignment.staging_result_path}`);
|
|
155
|
+
}
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(` ${chalk.dim('Artifacts:')}`);
|
|
158
|
+
for (const artifact of Object.values(artifacts)) {
|
|
159
|
+
const marker = artifact.exists ? chalk.green('exists') : chalk.red('missing');
|
|
160
|
+
console.log(` ${artifact.label.padEnd(10)} ${marker} ${artifact.relPath}`);
|
|
161
|
+
}
|
|
162
|
+
console.log('');
|
|
163
|
+
console.log(chalk.dim(` View one: agentxchain turn show ${turnId} --artifact prompt`));
|
|
164
|
+
console.log('');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function printArtifact(turnId, turn, artifacts, artifactId, jsonMode) {
|
|
168
|
+
const artifact = artifacts[artifactId];
|
|
169
|
+
if (!artifact) {
|
|
170
|
+
console.log(chalk.red(` Unknown artifact: ${artifactId}`));
|
|
171
|
+
console.log(chalk.dim(` Allowed: ${Object.keys(ARTIFACTS).join(', ')}`));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
if (!artifact.exists) {
|
|
175
|
+
console.log(chalk.red(` Missing artifact: ${artifact.relPath}`));
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const raw = readFileSync(artifact.absPath, 'utf8');
|
|
180
|
+
if (!jsonMode) {
|
|
181
|
+
process.stdout.write(raw.endsWith('\n') ? raw : `${raw}\n`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const content = artifact.parse === 'json'
|
|
186
|
+
? JSON.parse(raw)
|
|
187
|
+
: raw;
|
|
188
|
+
|
|
189
|
+
console.log(JSON.stringify({
|
|
190
|
+
turn_id: turnId,
|
|
191
|
+
role: turn.assigned_role,
|
|
192
|
+
runtime: turn.runtime_id,
|
|
193
|
+
artifact: {
|
|
194
|
+
id: artifact.id,
|
|
195
|
+
path: artifact.relPath,
|
|
196
|
+
content,
|
|
197
|
+
},
|
|
198
|
+
}, null, 2));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function readJsonArtifact(absPath) {
|
|
202
|
+
if (!existsSync(absPath)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(readFileSync(absPath, 'utf8'));
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
package/src/commands/verify.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
4
|
+
import { getActiveTurns } from '../lib/governed-state.js';
|
|
5
|
+
import { normalizeVerification } from '../lib/repo-observer.js';
|
|
6
|
+
import { validateStagedTurnResult } from '../lib/turn-result-validator.js';
|
|
7
|
+
import { getTurnStagingResultPath } from '../lib/turn-paths.js';
|
|
2
8
|
import { resolve } from 'node:path';
|
|
3
9
|
import { loadExportArtifact, verifyExportArtifact } from '../lib/export-verifier.js';
|
|
4
10
|
import { verifyProtocolConformance } from '../lib/protocol-conformance.js';
|
|
@@ -95,6 +101,85 @@ export async function verifyExportCommand(opts) {
|
|
|
95
101
|
process.exit(result.ok ? 0 : 1);
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
export async function verifyTurnCommand(turnId, opts = {}) {
|
|
105
|
+
const context = loadProjectContext();
|
|
106
|
+
if (!context) {
|
|
107
|
+
console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (context.config.protocol_mode !== 'governed' || context.version !== 4) {
|
|
112
|
+
console.log(chalk.red('verify turn is only available in governed v4 projects.'));
|
|
113
|
+
process.exit(2);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const timeoutMs = Number.parseInt(String(opts.timeout || '30000'), 10);
|
|
117
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
118
|
+
console.log(chalk.red('verify turn requires a positive integer --timeout in milliseconds.'));
|
|
119
|
+
process.exit(2);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const { root, config } = context;
|
|
123
|
+
const state = loadProjectState(root, config);
|
|
124
|
+
const activeTurns = getActiveTurns(state);
|
|
125
|
+
const selectedTurnId = resolveTargetTurnId(turnId, activeTurns);
|
|
126
|
+
const selectedTurn = activeTurns[selectedTurnId];
|
|
127
|
+
const stagingPath = getTurnStagingResultPath(selectedTurnId);
|
|
128
|
+
const validationState = {
|
|
129
|
+
...state,
|
|
130
|
+
active_turns: {
|
|
131
|
+
[selectedTurnId]: selectedTurn,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
const validation = validateStagedTurnResult(root, validationState, config, { stagingPath });
|
|
135
|
+
|
|
136
|
+
if (!validation.ok) {
|
|
137
|
+
emitTurnValidationFailure(validation, opts.json);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const turnResult = validation.turnResult;
|
|
142
|
+
const runtimeType = config.runtimes?.[selectedTurn.runtime_id]?.type || 'manual';
|
|
143
|
+
const declaredStatus = turnResult.verification?.status || 'skipped';
|
|
144
|
+
const normalizedStatus = normalizeVerification(turnResult.verification, runtimeType).status;
|
|
145
|
+
const machineEvidence = Array.isArray(turnResult.verification?.machine_evidence)
|
|
146
|
+
? turnResult.verification.machine_evidence
|
|
147
|
+
: [];
|
|
148
|
+
|
|
149
|
+
const payload = {
|
|
150
|
+
turn_id: selectedTurnId,
|
|
151
|
+
role: selectedTurn.assigned_role,
|
|
152
|
+
runtime_id: selectedTurn.runtime_id,
|
|
153
|
+
runtime_type: runtimeType,
|
|
154
|
+
staging_path: stagingPath,
|
|
155
|
+
declared_status: declaredStatus,
|
|
156
|
+
normalized_status: normalizedStatus,
|
|
157
|
+
validation: {
|
|
158
|
+
ok: true,
|
|
159
|
+
warnings: validation.warnings || [],
|
|
160
|
+
},
|
|
161
|
+
timeout_ms: timeoutMs,
|
|
162
|
+
overall: 'not_reproducible',
|
|
163
|
+
replayed_commands: 0,
|
|
164
|
+
matched_commands: 0,
|
|
165
|
+
commands: [],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (machineEvidence.length === 0) {
|
|
169
|
+
payload.reason = 'No verification.machine_evidence commands were declared. commands/evidence_summary are not executable proof.';
|
|
170
|
+
emitTurnVerification(payload, opts.json);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
payload.commands = machineEvidence.map((entry, index) => replayEvidenceCommand(root, entry, index, timeoutMs));
|
|
175
|
+
payload.replayed_commands = payload.commands.length;
|
|
176
|
+
payload.matched_commands = payload.commands.filter((entry) => entry.matched).length;
|
|
177
|
+
payload.overall = payload.commands.every((entry) => entry.matched) ? 'match' : 'mismatch';
|
|
178
|
+
|
|
179
|
+
emitTurnVerification(payload, opts.json);
|
|
180
|
+
process.exit(payload.overall === 'match' ? 0 : 1);
|
|
181
|
+
}
|
|
182
|
+
|
|
98
183
|
function printProtocolReport(report) {
|
|
99
184
|
console.log('');
|
|
100
185
|
console.log(chalk.bold(' AgentXchain Protocol Conformance'));
|
|
@@ -162,3 +247,143 @@ function printExportReport(report) {
|
|
|
162
247
|
|
|
163
248
|
console.log('');
|
|
164
249
|
}
|
|
250
|
+
|
|
251
|
+
function resolveTargetTurnId(requestedTurnId, activeTurns) {
|
|
252
|
+
const turnIds = Object.keys(activeTurns);
|
|
253
|
+
|
|
254
|
+
if (requestedTurnId) {
|
|
255
|
+
if (!activeTurns[requestedTurnId]) {
|
|
256
|
+
console.log(chalk.red(`Unknown active turn: ${requestedTurnId}`));
|
|
257
|
+
if (turnIds.length > 0) {
|
|
258
|
+
console.log(chalk.dim(` Available: ${turnIds.join(', ')}`));
|
|
259
|
+
}
|
|
260
|
+
process.exit(2);
|
|
261
|
+
}
|
|
262
|
+
return requestedTurnId;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (turnIds.length === 0) {
|
|
266
|
+
console.log(chalk.red('No active turn found.'));
|
|
267
|
+
process.exit(2);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (turnIds.length > 1) {
|
|
271
|
+
console.log(chalk.red('Multiple active turns are present. Re-run with `agentxchain verify turn <turn_id>`.'));
|
|
272
|
+
console.log(chalk.dim(` Available: ${turnIds.join(', ')}`));
|
|
273
|
+
process.exit(2);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return turnIds[0];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function emitTurnValidationFailure(validation, jsonMode) {
|
|
280
|
+
const payload = {
|
|
281
|
+
overall: 'validation_failed',
|
|
282
|
+
validation: {
|
|
283
|
+
ok: false,
|
|
284
|
+
stage: validation.stage,
|
|
285
|
+
error_class: validation.error_class,
|
|
286
|
+
errors: validation.errors || [],
|
|
287
|
+
warnings: validation.warnings || [],
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (jsonMode) {
|
|
292
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log('');
|
|
297
|
+
console.log(chalk.red(' Turn Verification Blocked By Validation'));
|
|
298
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
299
|
+
console.log(` ${chalk.dim('Stage:')} ${validation.stage || 'unknown'}`);
|
|
300
|
+
console.log(` ${chalk.dim('Reason:')} ${validation.error_class || 'validation_error'}`);
|
|
301
|
+
for (const error of validation.errors || []) {
|
|
302
|
+
console.log(` ${chalk.dim('Error:')} ${error}`);
|
|
303
|
+
}
|
|
304
|
+
if ((validation.warnings || []).length > 0) {
|
|
305
|
+
console.log('');
|
|
306
|
+
for (const warning of validation.warnings || []) {
|
|
307
|
+
console.log(` ${chalk.dim('Warning:')} ${warning}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
console.log('');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function replayEvidenceCommand(root, entry, index, timeoutMs) {
|
|
314
|
+
const result = spawnSync(entry.command, {
|
|
315
|
+
cwd: root,
|
|
316
|
+
encoding: 'utf8',
|
|
317
|
+
shell: true,
|
|
318
|
+
timeout: timeoutMs,
|
|
319
|
+
maxBuffer: 1024 * 1024,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const timedOut = result.error?.code === 'ETIMEDOUT';
|
|
323
|
+
const actualExitCode = Number.isInteger(result.status) ? result.status : null;
|
|
324
|
+
const errorMessage = result.error?.message || null;
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
index,
|
|
328
|
+
command: entry.command,
|
|
329
|
+
declared_exit_code: entry.exit_code,
|
|
330
|
+
actual_exit_code: actualExitCode,
|
|
331
|
+
matched: actualExitCode === entry.exit_code,
|
|
332
|
+
timed_out: timedOut,
|
|
333
|
+
signal: result.signal || null,
|
|
334
|
+
error: errorMessage,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function emitTurnVerification(payload, jsonMode) {
|
|
339
|
+
if (jsonMode) {
|
|
340
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.log('');
|
|
345
|
+
console.log(chalk.bold(` Verify Turn: ${chalk.cyan(payload.turn_id)}`));
|
|
346
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
347
|
+
console.log(` ${chalk.dim('Role:')} ${payload.role}`);
|
|
348
|
+
console.log(` ${chalk.dim('Runtime:')} ${payload.runtime_id} (${payload.runtime_type})`);
|
|
349
|
+
console.log(` ${chalk.dim('Staging:')} ${payload.staging_path}`);
|
|
350
|
+
console.log(` ${chalk.dim('Declared:')} ${payload.declared_status}`);
|
|
351
|
+
console.log(` ${chalk.dim('Normalized:')} ${payload.normalized_status}`);
|
|
352
|
+
console.log(` ${chalk.dim('Outcome:')} ${formatOutcome(payload.overall)}`);
|
|
353
|
+
|
|
354
|
+
for (const warning of payload.validation?.warnings || []) {
|
|
355
|
+
console.log(` ${chalk.dim('Warning:')} ${warning}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (payload.reason) {
|
|
359
|
+
console.log(` ${chalk.dim('Reason:')} ${payload.reason}`);
|
|
360
|
+
console.log('');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
console.log('');
|
|
365
|
+
for (const command of payload.commands || []) {
|
|
366
|
+
const marker = command.matched ? chalk.green('match') : chalk.red('mismatch');
|
|
367
|
+
console.log(` [${marker}] ${command.command}`);
|
|
368
|
+
console.log(` declared=${command.declared_exit_code} actual=${command.actual_exit_code == null ? 'null' : command.actual_exit_code}`);
|
|
369
|
+
if (command.signal) {
|
|
370
|
+
console.log(` signal=${command.signal}`);
|
|
371
|
+
}
|
|
372
|
+
if (command.timed_out) {
|
|
373
|
+
console.log(' timed_out=true');
|
|
374
|
+
}
|
|
375
|
+
if (command.error) {
|
|
376
|
+
console.log(` error=${command.error}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
console.log('');
|
|
381
|
+
console.log(chalk.dim(' Replay uses the current workspace and shell environment. It verifies declared exit-code reproducibility, not historical stdout/stderr identity.'));
|
|
382
|
+
console.log('');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function formatOutcome(outcome) {
|
|
386
|
+
if (outcome === 'match') return chalk.green('match');
|
|
387
|
+
if (outcome === 'mismatch') return chalk.red('mismatch');
|
|
388
|
+
return chalk.yellow('not_reproducible');
|
|
389
|
+
}
|
|
@@ -30,7 +30,7 @@ function getWorkflowArtifactsForPhase(config, phase) {
|
|
|
30
30
|
return Array.isArray(artifacts) ? artifacts : [];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function
|
|
33
|
+
export function getEffectiveGateArtifacts(config, gateDef, phase) {
|
|
34
34
|
const byPath = new Map();
|
|
35
35
|
|
|
36
36
|
if (Array.isArray(gateDef?.requires_files)) {
|
|
@@ -92,7 +92,7 @@ function prefixSemanticReason(filePath, reason) {
|
|
|
92
92
|
return `${filePath}: ${reason}`;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
function hasRoleParticipationInPhase(state, phase, roleId) {
|
|
95
|
+
export function hasRoleParticipationInPhase(state, phase, roleId) {
|
|
96
96
|
const history = state?.history;
|
|
97
97
|
if (!Array.isArray(history)) {
|
|
98
98
|
return false;
|
|
@@ -104,7 +104,7 @@ function hasRoleParticipationInPhase(state, phase, roleId) {
|
|
|
104
104
|
|
|
105
105
|
function evaluateGateArtifacts({ root, config, gateDef, phase, result, state }) {
|
|
106
106
|
const failures = [];
|
|
107
|
-
const artifacts =
|
|
107
|
+
const artifacts = getEffectiveGateArtifacts(config, gateDef, phase);
|
|
108
108
|
|
|
109
109
|
for (const artifact of artifacts) {
|
|
110
110
|
const absPath = join(root, artifact.path);
|