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/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. An intent belongs to the current run if:
520
- // (a) it has approved_run_id matching the current run, OR
521
- // (b) it has no approved_run_id AND is marked cross_run_durable, OR
522
- // (c) it was injected in the current run (approved_run_id matches)
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
- const dirs = intakeDirs(root);
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, mark the intent as cross_run_durable so the next run init
827
- // preserves this freshly approved work instead of archiving it as legacy
828
- // migration debt.
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
- switch (runtime?.type) {
95
- case 'manual':
96
- effectiveWritePath = 'planning_only';
97
- workflowArtifactOwnership = 'yes';
98
- appendNote(notes, 'Manual review roles can satisfy workflow-kit ownership for planning artifacts.');
99
- break;
100
- case 'local_cli':
101
- effectiveWritePath = 'invalid_review_only_binding';
102
- workflowArtifactOwnership = 'invalid';
103
- appendNote(notes, 'review_only + local_cli is invalid because local_cli exposes direct repo writes.');
104
- break;
105
- case 'api_proxy':
106
- case 'remote_agent':
107
- effectiveWritePath = 'review_artifact_only';
108
- workflowArtifactOwnership = 'no';
109
- appendNote(notes, 'Review-only remote turns can attest and produce review artifacts, not workflow-kit files.');
110
- break;
111
- case 'mcp':
112
- effectiveWritePath = 'tool_defined';
113
- workflowArtifactOwnership = 'tool_defined';
114
- appendNote(notes, 'MCP review-only file production depends on the configured tool, not runtime type alone.');
115
- break;
116
- default:
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
- switch (runtime?.type) {
123
- case 'manual':
124
- case 'local_cli':
125
- effectiveWritePath = 'patch_authoring';
126
- workflowArtifactOwnership = 'yes';
127
- appendNote(notes, 'This role can prepare patch-shaped work while still satisfying workflow-kit artifact ownership.');
128
- break;
129
- case 'api_proxy':
130
- case 'remote_agent':
131
- effectiveWritePath = 'proposal_apply_required';
132
- workflowArtifactOwnership = 'proposal_apply_required';
133
- appendNote(notes, 'Accepted proposals are staged under .agentxchain/proposed and require proposal apply before gate files exist in the repo.');
134
- break;
135
- case 'mcp':
136
- effectiveWritePath = 'tool_defined';
137
- workflowArtifactOwnership = 'tool_defined';
138
- appendNote(notes, 'MCP proposed-authoring behavior depends on the governed tool implementation.');
139
- break;
140
- default:
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
- switch (runtime?.type) {
147
- case 'manual':
148
- case 'local_cli':
149
- effectiveWritePath = 'direct';
150
- workflowArtifactOwnership = 'yes';
151
- break;
152
- case 'api_proxy':
153
- case 'remote_agent':
154
- effectiveWritePath = 'invalid_authoritative_binding';
155
- workflowArtifactOwnership = 'invalid';
156
- appendNote(notes, `${runtime.type} does not support authoritative roles in v1.`);
157
- break;
158
- case 'mcp':
159
- effectiveWritePath = 'tool_defined';
160
- workflowArtifactOwnership = 'tool_defined';
161
- appendNote(notes, 'MCP authoritative repo writes are tool-defined, not guaranteed by runtime type.');
162
- break;
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})`,