agentxchain 2.135.1 → 2.136.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 +8 -1
- package/package.json +4 -2
- package/scripts/release-preflight.sh +2 -0
- package/src/commands/connector.js +136 -0
- package/src/commands/restart.js +3 -0
- package/src/commands/resume.js +15 -0
- package/src/commands/step.js +12 -0
- package/src/lib/connector-validate.js +6 -0
- package/src/lib/continuous-run.js +60 -5
- package/src/lib/dispatch-bundle.js +14 -0
- package/src/lib/governed-state.js +42 -46
- package/src/lib/intake.js +17 -57
- package/src/lib/intent-startup-migration.js +151 -0
- package/src/lib/runtime-capabilities.js +101 -81
- package/src/lib/schemas/agentxchain-config.schema.json +391 -0
- package/src/lib/schemas/connector-capabilities-output.schema.json +104 -0
package/src/lib/intake.js
CHANGED
|
@@ -17,6 +17,11 @@ import { getDispatchTurnDir } from './turn-paths.js';
|
|
|
17
17
|
import { loadCoordinatorConfig } from './coordinator-config.js';
|
|
18
18
|
import { loadCoordinatorState, readBarriers } from './coordinator-state.js';
|
|
19
19
|
import { writeCoordinatorHandoff } from './intake-handoff.js';
|
|
20
|
+
import {
|
|
21
|
+
archiveStaleIntentsForRun,
|
|
22
|
+
migratePreBug34Intents,
|
|
23
|
+
formatLegacyIntentMigrationNotice,
|
|
24
|
+
} from './intent-startup-migration.js';
|
|
20
25
|
|
|
21
26
|
const VALID_SOURCES = ['manual', 'ci_failure', 'git_ref_change', 'schedule', 'vision_scan'];
|
|
22
27
|
const VALID_PRIORITIES = ['p0', 'p1', 'p2', 'p3'];
|
|
@@ -516,16 +521,14 @@ export function findNextDispatchableIntent(root, options = {}) {
|
|
|
516
521
|
.filter((intent) => intent && DISPATCHABLE_STATUSES.has(intent.status));
|
|
517
522
|
|
|
518
523
|
// BUG-34: when run_id scoping is active, filter out intents that belong to
|
|
519
|
-
// a different run.
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
// Legacy intents (no approved_run_id, no cross_run_durable) are excluded
|
|
524
|
-
// because they are stale leftovers from prior runs.
|
|
524
|
+
// a different run. `cross_run_durable` is only a pre-run holding state for
|
|
525
|
+
// freshly approved idle intents. Once a run starts, those intents must be
|
|
526
|
+
// rebound onto that run and the flag cleared; it must never override an
|
|
527
|
+
// existing run binding.
|
|
525
528
|
if (scopeRunId) {
|
|
526
529
|
intents = intents.filter((intent) => {
|
|
527
530
|
if (intent.approved_run_id === scopeRunId) return true;
|
|
528
|
-
if (intent.cross_run_durable === true) return true;
|
|
531
|
+
if (!intent.approved_run_id && intent.cross_run_durable === true) return true;
|
|
529
532
|
// Legacy intent with no run binding — stale, skip it
|
|
530
533
|
if (!intent.approved_run_id) return false;
|
|
531
534
|
// Intent bound to a different run — stale, skip it
|
|
@@ -579,10 +582,11 @@ export function findPendingApprovedIntents(root, options = {}) {
|
|
|
579
582
|
return readJsonDir(dirs.intents)
|
|
580
583
|
.filter((intent) => {
|
|
581
584
|
if (!intent || intent.status !== 'approved') return false;
|
|
582
|
-
// BUG-34: run_id scoping — same logic as findNextDispatchableIntent
|
|
585
|
+
// BUG-34: run_id scoping — same logic as findNextDispatchableIntent.
|
|
586
|
+
// `cross_run_durable` only applies before the first run binding exists.
|
|
583
587
|
if (scopeRunId) {
|
|
584
588
|
if (intent.approved_run_id === scopeRunId) return true;
|
|
585
|
-
if (intent.cross_run_durable === true) return true;
|
|
589
|
+
if (!intent.approved_run_id && intent.cross_run_durable === true) return true;
|
|
586
590
|
return false;
|
|
587
591
|
}
|
|
588
592
|
return true;
|
|
@@ -616,51 +620,7 @@ export function findPendingApprovedIntents(root, options = {}) {
|
|
|
616
620
|
* @returns {{ archived: number }}
|
|
617
621
|
*/
|
|
618
622
|
export function archiveStaleIntents(root, newRunId) {
|
|
619
|
-
|
|
620
|
-
if (!existsSync(dirs.intents)) return { archived: 0, adopted: 0 };
|
|
621
|
-
|
|
622
|
-
const now = nowISO();
|
|
623
|
-
let archived = 0;
|
|
624
|
-
let adopted = 0;
|
|
625
|
-
|
|
626
|
-
const files = readdirSync(dirs.intents).filter(f => f.endsWith('.json') && !f.startsWith('.tmp-'));
|
|
627
|
-
for (const file of files) {
|
|
628
|
-
const intentPath = join(dirs.intents, file);
|
|
629
|
-
let intent;
|
|
630
|
-
try {
|
|
631
|
-
intent = JSON.parse(readFileSync(intentPath, 'utf8'));
|
|
632
|
-
} catch {
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (!intent || !DISPATCHABLE_STATUSES.has(intent.status)) continue;
|
|
637
|
-
if (intent.cross_run_durable === true) continue;
|
|
638
|
-
if (intent.approved_run_id === newRunId) continue;
|
|
639
|
-
|
|
640
|
-
if (intent.approved_run_id && intent.approved_run_id !== newRunId) {
|
|
641
|
-
// Intent from a different run — archive it
|
|
642
|
-
intent.status = 'suppressed';
|
|
643
|
-
intent.updated_at = now;
|
|
644
|
-
intent.archived_reason = `stale: approved under run ${intent.approved_run_id}, archived on run ${newRunId} initialization`;
|
|
645
|
-
if (!intent.history) intent.history = [];
|
|
646
|
-
intent.history.push({ from: 'approved', to: 'suppressed', at: now, reason: intent.archived_reason });
|
|
647
|
-
safeWriteJson(intentPath, intent);
|
|
648
|
-
archived++;
|
|
649
|
-
} else if (!intent.approved_run_id) {
|
|
650
|
-
// BUG-39: pre-BUG-34 legacy intent with no run binding — archive it
|
|
651
|
-
// with explicit migration reason. Do NOT adopt into current run.
|
|
652
|
-
const prevStatus = intent.status;
|
|
653
|
-
intent.status = 'archived_migration';
|
|
654
|
-
intent.updated_at = now;
|
|
655
|
-
intent.archived_reason = `pre-BUG-34 intent with no run scope; archived during migration on run ${newRunId}`;
|
|
656
|
-
if (!intent.history) intent.history = [];
|
|
657
|
-
intent.history.push({ from: prevStatus, to: 'archived_migration', at: now, reason: intent.archived_reason });
|
|
658
|
-
safeWriteJson(intentPath, intent);
|
|
659
|
-
archived++;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
return { archived, adopted };
|
|
623
|
+
return archiveStaleIntentsForRun(root, newRunId);
|
|
664
624
|
}
|
|
665
625
|
|
|
666
626
|
/**
|
|
@@ -823,9 +783,9 @@ export function approveIntent(root, intentId, options = {}) {
|
|
|
823
783
|
|
|
824
784
|
// BUG-34/39: stamp the current run_id on approval so the intent is scoped
|
|
825
785
|
// to the run that approved it. When approval happens before any governed
|
|
826
|
-
// run exists,
|
|
827
|
-
//
|
|
828
|
-
//
|
|
786
|
+
// run exists, hold the intent in a pre-run durable state so the *next* run
|
|
787
|
+
// initialization can bind it to that run. The flag is not an evergreen
|
|
788
|
+
// cross-run bypass; it exists only until first run binding happens.
|
|
829
789
|
if (!intent.approved_run_id) {
|
|
830
790
|
const statePath = join(root, '.agentxchain', 'state.json');
|
|
831
791
|
if (existsSync(statePath)) {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { safeWriteJson } from './safe-write.js';
|
|
5
|
+
|
|
6
|
+
const DISPATCHABLE_STATUSES = new Set(['planned', 'approved']);
|
|
7
|
+
|
|
8
|
+
function nowISO() {
|
|
9
|
+
return new Date().toISOString();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getIntentsDir(root) {
|
|
13
|
+
return join(root, '.agentxchain', 'intake', 'intents');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function listIntentFiles(intentsDir) {
|
|
17
|
+
if (!existsSync(intentsDir)) return [];
|
|
18
|
+
return readdirSync(intentsDir).filter((file) => file.endsWith('.json') && !file.startsWith('.tmp-'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatLegacyIntentMigrationNotice(intentIds) {
|
|
22
|
+
if (!Array.isArray(intentIds) || intentIds.length === 0) return null;
|
|
23
|
+
return `Archived ${intentIds.length} pre-BUG-34 intent(s): ${intentIds.join(', ')}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function migratePreBug34Intents(root, runId, options = {}) {
|
|
27
|
+
const intentsDir = getIntentsDir(root);
|
|
28
|
+
if (!existsSync(intentsDir)) {
|
|
29
|
+
return {
|
|
30
|
+
archived_migration_count: 0,
|
|
31
|
+
archived_migration_intent_ids: [],
|
|
32
|
+
migration_notice: null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const now = nowISO();
|
|
37
|
+
const archivedMigrationIntentIds = [];
|
|
38
|
+
const protocolVersion = options.protocolVersion || '2.x';
|
|
39
|
+
|
|
40
|
+
for (const file of listIntentFiles(intentsDir)) {
|
|
41
|
+
const intentPath = join(intentsDir, file);
|
|
42
|
+
let intent;
|
|
43
|
+
try {
|
|
44
|
+
intent = JSON.parse(readFileSync(intentPath, 'utf8'));
|
|
45
|
+
} catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!intent || !DISPATCHABLE_STATUSES.has(intent.status)) continue;
|
|
50
|
+
if (intent.cross_run_durable === true) continue;
|
|
51
|
+
if (intent.approved_run_id) continue;
|
|
52
|
+
|
|
53
|
+
const prevStatus = intent.status;
|
|
54
|
+
intent.status = 'archived_migration';
|
|
55
|
+
intent.updated_at = now;
|
|
56
|
+
intent.archived_reason = `pre-BUG-34 intent with no run scope; archived during v${protocolVersion} migration on run ${runId}`;
|
|
57
|
+
if (!intent.history) intent.history = [];
|
|
58
|
+
intent.history.push({
|
|
59
|
+
from: prevStatus,
|
|
60
|
+
to: 'archived_migration',
|
|
61
|
+
at: now,
|
|
62
|
+
reason: intent.archived_reason,
|
|
63
|
+
});
|
|
64
|
+
safeWriteJson(intentPath, intent);
|
|
65
|
+
if (intent.intent_id) archivedMigrationIntentIds.push(intent.intent_id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
archived_migration_count: archivedMigrationIntentIds.length,
|
|
70
|
+
archived_migration_intent_ids: archivedMigrationIntentIds,
|
|
71
|
+
migration_notice: formatLegacyIntentMigrationNotice(archivedMigrationIntentIds),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function archiveStaleIntentsForRun(root, runId, options = {}) {
|
|
76
|
+
const intentsDir = getIntentsDir(root);
|
|
77
|
+
if (!existsSync(intentsDir)) {
|
|
78
|
+
return {
|
|
79
|
+
archived: 0,
|
|
80
|
+
adopted: 0,
|
|
81
|
+
archived_migration_count: 0,
|
|
82
|
+
archived_migration_intent_ids: [],
|
|
83
|
+
migration_notice: null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const now = nowISO();
|
|
88
|
+
let archived = 0;
|
|
89
|
+
let adopted = 0;
|
|
90
|
+
|
|
91
|
+
for (const file of listIntentFiles(intentsDir)) {
|
|
92
|
+
const intentPath = join(intentsDir, file);
|
|
93
|
+
let intent;
|
|
94
|
+
try {
|
|
95
|
+
intent = JSON.parse(readFileSync(intentPath, 'utf8'));
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!intent || !DISPATCHABLE_STATUSES.has(intent.status)) continue;
|
|
101
|
+
|
|
102
|
+
if (intent.cross_run_durable === true && !intent.approved_run_id) {
|
|
103
|
+
intent.approved_run_id = runId;
|
|
104
|
+
delete intent.cross_run_durable;
|
|
105
|
+
intent.updated_at = now;
|
|
106
|
+
if (!intent.history) intent.history = [];
|
|
107
|
+
intent.history.push({
|
|
108
|
+
from: intent.status,
|
|
109
|
+
to: intent.status,
|
|
110
|
+
at: now,
|
|
111
|
+
reason: `pre-run durable approval bound to run ${runId}`,
|
|
112
|
+
});
|
|
113
|
+
safeWriteJson(intentPath, intent);
|
|
114
|
+
adopted += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (intent.cross_run_durable === true && intent.approved_run_id === runId) {
|
|
119
|
+
delete intent.cross_run_durable;
|
|
120
|
+
intent.updated_at = now;
|
|
121
|
+
safeWriteJson(intentPath, intent);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (intent.approved_run_id && intent.approved_run_id !== runId) {
|
|
126
|
+
const prevStatus = intent.status;
|
|
127
|
+
intent.status = 'suppressed';
|
|
128
|
+
intent.updated_at = now;
|
|
129
|
+
intent.archived_reason = `stale: approved under run ${intent.approved_run_id}, archived on run ${runId} initialization`;
|
|
130
|
+
if (!intent.history) intent.history = [];
|
|
131
|
+
intent.history.push({
|
|
132
|
+
from: prevStatus,
|
|
133
|
+
to: 'suppressed',
|
|
134
|
+
at: now,
|
|
135
|
+
reason: intent.archived_reason,
|
|
136
|
+
});
|
|
137
|
+
safeWriteJson(intentPath, intent);
|
|
138
|
+
archived += 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const migration = migratePreBug34Intents(root, runId, options);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
archived,
|
|
146
|
+
adopted,
|
|
147
|
+
archived_migration_count: migration.archived_migration_count,
|
|
148
|
+
archived_migration_intent_ids: migration.archived_migration_intent_ids,
|
|
149
|
+
migration_notice: migration.migration_notice,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -4,10 +4,28 @@ function normalizeMcpTransport(runtime) {
|
|
|
4
4
|
: 'stdio';
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
const DECLARABLE_CAPABILITY_FIELDS = new Set([
|
|
8
|
+
'can_write_files',
|
|
9
|
+
'proposal_support',
|
|
10
|
+
'workflow_artifact_ownership',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function mergeExplicitCapabilities(base, runtime) {
|
|
14
|
+
const declared = runtime?.capabilities;
|
|
15
|
+
if (!declared || typeof declared !== 'object' || Array.isArray(declared)) return base;
|
|
16
|
+
const merged = { ...base };
|
|
17
|
+
for (const field of DECLARABLE_CAPABILITY_FIELDS) {
|
|
18
|
+
if (typeof declared[field] === 'string' && declared[field].length > 0) {
|
|
19
|
+
merged[field] = declared[field];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return merged;
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
export function getRuntimeCapabilityContract(runtime = {}) {
|
|
8
26
|
switch (runtime?.type) {
|
|
9
27
|
case 'manual':
|
|
10
|
-
return {
|
|
28
|
+
return mergeExplicitCapabilities({
|
|
11
29
|
runtime_type: 'manual',
|
|
12
30
|
transport: 'manual',
|
|
13
31
|
can_write_files: 'direct',
|
|
@@ -15,10 +33,10 @@ export function getRuntimeCapabilityContract(runtime = {}) {
|
|
|
15
33
|
proposal_support: 'none',
|
|
16
34
|
requires_local_binary: false,
|
|
17
35
|
workflow_artifact_ownership: 'yes',
|
|
18
|
-
};
|
|
36
|
+
}, runtime);
|
|
19
37
|
|
|
20
38
|
case 'local_cli':
|
|
21
|
-
return {
|
|
39
|
+
return mergeExplicitCapabilities({
|
|
22
40
|
runtime_type: 'local_cli',
|
|
23
41
|
transport: 'local_cli',
|
|
24
42
|
can_write_files: 'direct',
|
|
@@ -26,10 +44,10 @@ export function getRuntimeCapabilityContract(runtime = {}) {
|
|
|
26
44
|
proposal_support: 'optional',
|
|
27
45
|
requires_local_binary: true,
|
|
28
46
|
workflow_artifact_ownership: 'yes',
|
|
29
|
-
};
|
|
47
|
+
}, runtime);
|
|
30
48
|
|
|
31
49
|
case 'api_proxy':
|
|
32
|
-
return {
|
|
50
|
+
return mergeExplicitCapabilities({
|
|
33
51
|
runtime_type: 'api_proxy',
|
|
34
52
|
transport: 'provider_api',
|
|
35
53
|
can_write_files: 'proposal_only',
|
|
@@ -37,10 +55,10 @@ export function getRuntimeCapabilityContract(runtime = {}) {
|
|
|
37
55
|
proposal_support: 'native',
|
|
38
56
|
requires_local_binary: false,
|
|
39
57
|
workflow_artifact_ownership: 'proposal_apply_required',
|
|
40
|
-
};
|
|
58
|
+
}, runtime);
|
|
41
59
|
|
|
42
60
|
case 'remote_agent':
|
|
43
|
-
return {
|
|
61
|
+
return mergeExplicitCapabilities({
|
|
44
62
|
runtime_type: 'remote_agent',
|
|
45
63
|
transport: 'remote_http',
|
|
46
64
|
can_write_files: 'proposal_only',
|
|
@@ -48,13 +66,13 @@ export function getRuntimeCapabilityContract(runtime = {}) {
|
|
|
48
66
|
proposal_support: 'native',
|
|
49
67
|
requires_local_binary: false,
|
|
50
68
|
workflow_artifact_ownership: 'proposal_apply_required',
|
|
51
|
-
};
|
|
69
|
+
}, runtime);
|
|
52
70
|
|
|
53
71
|
case 'mcp': {
|
|
54
72
|
const transport = normalizeMcpTransport(runtime) === 'streamable_http'
|
|
55
73
|
? 'mcp_streamable_http'
|
|
56
74
|
: 'mcp_stdio';
|
|
57
|
-
return {
|
|
75
|
+
return mergeExplicitCapabilities({
|
|
58
76
|
runtime_type: 'mcp',
|
|
59
77
|
transport,
|
|
60
78
|
can_write_files: 'tool_defined',
|
|
@@ -62,11 +80,11 @@ export function getRuntimeCapabilityContract(runtime = {}) {
|
|
|
62
80
|
proposal_support: 'tool_defined',
|
|
63
81
|
requires_local_binary: transport === 'mcp_stdio',
|
|
64
82
|
workflow_artifact_ownership: 'tool_defined',
|
|
65
|
-
};
|
|
83
|
+
}, runtime);
|
|
66
84
|
}
|
|
67
85
|
|
|
68
86
|
default:
|
|
69
|
-
return {
|
|
87
|
+
return mergeExplicitCapabilities({
|
|
70
88
|
runtime_type: runtime?.type || 'unknown',
|
|
71
89
|
transport: 'unknown',
|
|
72
90
|
can_write_files: 'unknown',
|
|
@@ -74,7 +92,7 @@ export function getRuntimeCapabilityContract(runtime = {}) {
|
|
|
74
92
|
proposal_support: 'unknown',
|
|
75
93
|
requires_local_binary: false,
|
|
76
94
|
workflow_artifact_ownership: 'unknown',
|
|
77
|
-
};
|
|
95
|
+
}, runtime);
|
|
78
96
|
}
|
|
79
97
|
}
|
|
80
98
|
|
|
@@ -91,79 +109,69 @@ export function getRoleRuntimeCapabilityContract(roleId, role = {}, runtime = {}
|
|
|
91
109
|
let workflowArtifactOwnership = 'unknown';
|
|
92
110
|
|
|
93
111
|
if (authority === 'review_only') {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
effectiveWritePath = 'unknown';
|
|
118
|
-
workflowArtifactOwnership = 'unknown';
|
|
119
|
-
break;
|
|
112
|
+
if (runtime?.type === 'manual') {
|
|
113
|
+
effectiveWritePath = 'planning_only';
|
|
114
|
+
workflowArtifactOwnership = 'yes';
|
|
115
|
+
appendNote(notes, 'Manual review roles can satisfy workflow-kit ownership for planning artifacts.');
|
|
116
|
+
} else if (base.can_write_files === 'direct' && runtime?.type === 'local_cli') {
|
|
117
|
+
effectiveWritePath = 'invalid_review_only_binding';
|
|
118
|
+
workflowArtifactOwnership = 'invalid';
|
|
119
|
+
appendNote(notes, 'review_only + local_cli is invalid because local_cli exposes direct repo writes.');
|
|
120
|
+
} else if (base.can_write_files === 'tool_defined' || base.workflow_artifact_ownership === 'tool_defined') {
|
|
121
|
+
effectiveWritePath = 'tool_defined';
|
|
122
|
+
workflowArtifactOwnership = 'tool_defined';
|
|
123
|
+
appendNote(notes, 'Review-only file production depends on the configured tool contract, not runtime type alone.');
|
|
124
|
+
} else if (base.can_write_files === 'proposal_only') {
|
|
125
|
+
effectiveWritePath = 'review_artifact_only';
|
|
126
|
+
workflowArtifactOwnership = 'no';
|
|
127
|
+
appendNote(notes, 'Review-only remote turns can attest and produce review artifacts, not workflow-kit files.');
|
|
128
|
+
} else if (base.can_write_files === 'direct') {
|
|
129
|
+
effectiveWritePath = base.workflow_artifact_ownership === 'yes' ? 'planning_only' : 'review_artifact_only';
|
|
130
|
+
workflowArtifactOwnership = base.workflow_artifact_ownership === 'yes' ? 'yes' : 'no';
|
|
131
|
+
appendNote(notes, 'Review-only roles constrain direct-write runtimes to planning or review artifact production only.');
|
|
132
|
+
} else {
|
|
133
|
+
effectiveWritePath = 'unknown';
|
|
134
|
+
workflowArtifactOwnership = 'unknown';
|
|
120
135
|
}
|
|
121
136
|
} else if (authority === 'proposed') {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
effectiveWritePath = 'unknown';
|
|
142
|
-
workflowArtifactOwnership = 'unknown';
|
|
143
|
-
break;
|
|
137
|
+
if (runtime?.type === 'manual') {
|
|
138
|
+
effectiveWritePath = 'patch_authoring';
|
|
139
|
+
workflowArtifactOwnership = 'yes';
|
|
140
|
+
appendNote(notes, 'This role can prepare patch-shaped work while still satisfying workflow-kit artifact ownership.');
|
|
141
|
+
} else if (base.can_write_files === 'direct') {
|
|
142
|
+
effectiveWritePath = 'patch_authoring';
|
|
143
|
+
workflowArtifactOwnership = base.workflow_artifact_ownership;
|
|
144
|
+
appendNote(notes, 'This role can prepare patch-shaped work while still satisfying workflow-kit artifact ownership.');
|
|
145
|
+
} else if (base.can_write_files === 'proposal_only') {
|
|
146
|
+
effectiveWritePath = 'proposal_apply_required';
|
|
147
|
+
workflowArtifactOwnership = 'proposal_apply_required';
|
|
148
|
+
appendNote(notes, 'Accepted proposals are staged under .agentxchain/proposed and require proposal apply before gate files exist in the repo.');
|
|
149
|
+
} else if (base.can_write_files === 'tool_defined' || base.workflow_artifact_ownership === 'tool_defined') {
|
|
150
|
+
effectiveWritePath = 'tool_defined';
|
|
151
|
+
workflowArtifactOwnership = 'tool_defined';
|
|
152
|
+
appendNote(notes, 'Proposed-authoring behavior depends on the governed tool implementation.');
|
|
153
|
+
} else {
|
|
154
|
+
effectiveWritePath = 'unknown';
|
|
155
|
+
workflowArtifactOwnership = 'unknown';
|
|
144
156
|
}
|
|
145
157
|
} else if (authority === 'authoritative') {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
default:
|
|
164
|
-
effectiveWritePath = 'unknown';
|
|
165
|
-
workflowArtifactOwnership = 'unknown';
|
|
166
|
-
break;
|
|
158
|
+
if (base.can_write_files === 'direct') {
|
|
159
|
+
effectiveWritePath = 'direct';
|
|
160
|
+
workflowArtifactOwnership = base.workflow_artifact_ownership;
|
|
161
|
+
if (runtime?.type === 'mcp') {
|
|
162
|
+
appendNote(notes, 'Authoritative MCP repo writes are accepted because the connector declared a direct write path.');
|
|
163
|
+
}
|
|
164
|
+
} else if (base.can_write_files === 'proposal_only') {
|
|
165
|
+
effectiveWritePath = 'invalid_authoritative_binding';
|
|
166
|
+
workflowArtifactOwnership = 'invalid';
|
|
167
|
+
appendNote(notes, `${runtime.type} does not support authoritative roles in v1.`);
|
|
168
|
+
} else if (base.can_write_files === 'tool_defined' || base.workflow_artifact_ownership === 'tool_defined') {
|
|
169
|
+
effectiveWritePath = 'tool_defined';
|
|
170
|
+
workflowArtifactOwnership = 'tool_defined';
|
|
171
|
+
appendNote(notes, 'Authoritative repo writes are tool-defined, not guaranteed by runtime type alone.');
|
|
172
|
+
} else {
|
|
173
|
+
effectiveWritePath = 'unknown';
|
|
174
|
+
workflowArtifactOwnership = 'unknown';
|
|
167
175
|
}
|
|
168
176
|
}
|
|
169
177
|
|
|
@@ -213,6 +221,18 @@ export function summarizeRuntimeCapabilityContract(contract) {
|
|
|
213
221
|
].join('; ');
|
|
214
222
|
}
|
|
215
223
|
|
|
224
|
+
export { DECLARABLE_CAPABILITY_FIELDS };
|
|
225
|
+
|
|
226
|
+
export function getCapabilityDeclarationWarnings(runtime = {}) {
|
|
227
|
+
const warnings = [];
|
|
228
|
+
const declared = runtime?.capabilities;
|
|
229
|
+
if (!declared || typeof declared !== 'object' || Array.isArray(declared)) return warnings;
|
|
230
|
+
if (declared.can_write_files === 'direct' && (runtime.type === 'api_proxy' || runtime.type === 'remote_agent')) {
|
|
231
|
+
warnings.push(`Runtime type "${runtime.type}" declares can_write_files=direct, which the reference runner does not support in v1. A third-party runner may.`);
|
|
232
|
+
}
|
|
233
|
+
return warnings;
|
|
234
|
+
}
|
|
235
|
+
|
|
216
236
|
export function summarizeRoleRuntimeCapability(contract) {
|
|
217
237
|
return [
|
|
218
238
|
`${contract.role_id}(${contract.role_write_authority})`,
|