agentxchain 2.111.0 → 2.112.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 +39 -0
- package/dashboard/app.js +3 -0
- package/dashboard/components/mission.js +177 -0
- package/dashboard/index.html +1 -0
- package/package.json +2 -1
- package/scripts/check-release-alignment.mjs +66 -0
- package/scripts/release-bump.sh +8 -59
- package/scripts/release-preflight.sh +23 -8
- package/src/commands/mission.js +252 -0
- package/src/commands/run.js +3 -1
- package/src/lib/dashboard/bridge-server.js +8 -0
- package/src/lib/dashboard/file-watcher.js +13 -11
- package/src/lib/dashboard/mission-reader.js +14 -0
- package/src/lib/dashboard/state-reader.js +12 -3
- package/src/lib/missions.js +195 -0
- package/src/lib/release-alignment.js +336 -0
- package/src/lib/run-chain.js +36 -2
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const RELEASE_ALIGNMENT_SCOPES = {
|
|
5
|
+
PREBUMP: 'prebump',
|
|
6
|
+
CURRENT: 'current',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function read(repoRoot, relativePath) {
|
|
10
|
+
return readFileSync(join(repoRoot, relativePath), 'utf8');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readJson(repoRoot, relativePath) {
|
|
14
|
+
return JSON.parse(read(repoRoot, relativePath));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function escapeRegExp(value) {
|
|
18
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatCount(value) {
|
|
22
|
+
return new Intl.NumberFormat('en-US').format(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeEvidenceText(value) {
|
|
26
|
+
return value
|
|
27
|
+
.replace(/^\s*-\s*/, '')
|
|
28
|
+
.replace(/\.$/, '')
|
|
29
|
+
.replace(/,/g, '')
|
|
30
|
+
.trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function extractTopReleaseSection(changelog, version) {
|
|
34
|
+
const heading = `## ${version}`;
|
|
35
|
+
const start = changelog.indexOf(heading);
|
|
36
|
+
if (start === -1) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const afterStart = changelog.slice(start + heading.length);
|
|
40
|
+
const nextHeadingOffset = afterStart.search(/\n##\s+\d+\.\d+\.\d+/);
|
|
41
|
+
return nextHeadingOffset === -1 ? afterStart : afterStart.slice(0, nextHeadingOffset);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function extractAggregateEvidenceLine(text) {
|
|
45
|
+
const matches = [...text.matchAll(/(^-\s+.*\b\d[\d,]*\s+tests\b.*\b0 failures\b.*$)/gm)];
|
|
46
|
+
if (matches.length === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const aggregate = matches.reduce((best, match) => {
|
|
51
|
+
const line = match[1].replace(/\*\*/g, '').replace(/`/g, '').trim();
|
|
52
|
+
const countMatch = line.match(/\b(\d[\d,]*)\s+tests\b/);
|
|
53
|
+
const count = countMatch ? Number(countMatch[1].replace(/,/g, '')) : 0;
|
|
54
|
+
if (!best || count > best.count) {
|
|
55
|
+
return { count, line };
|
|
56
|
+
}
|
|
57
|
+
return best;
|
|
58
|
+
}, null);
|
|
59
|
+
|
|
60
|
+
return aggregate?.line ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractAggregateEvidenceCount(line) {
|
|
64
|
+
const match = line?.match(/\b(\d[\d,]*)\s+tests\b/);
|
|
65
|
+
if (!match) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return Number(match[1].replace(/,/g, ''));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getReleaseAlignmentContext(repoRoot, { targetVersion } = {}) {
|
|
72
|
+
const pkg = readJson(repoRoot, 'cli/package.json');
|
|
73
|
+
const version = targetVersion || pkg.version;
|
|
74
|
+
const releaseDocId = `v${version.replace(/\./g, '-')}`;
|
|
75
|
+
const releaseDocPath = `website-v2/docs/releases/${releaseDocId}.mdx`;
|
|
76
|
+
const releaseRoute = `/docs/releases/${releaseDocId}`;
|
|
77
|
+
const tarballUrl = `https://registry.npmjs.org/agentxchain/-/agentxchain-${version}.tgz`;
|
|
78
|
+
const changelog = read(repoRoot, 'cli/CHANGELOG.md');
|
|
79
|
+
const changelogSection = extractTopReleaseSection(changelog, version);
|
|
80
|
+
const aggregateEvidenceLine = changelogSection ? extractAggregateEvidenceLine(changelogSection) : null;
|
|
81
|
+
const aggregateEvidenceCount = extractAggregateEvidenceCount(aggregateEvidenceLine);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
packageVersion: pkg.version,
|
|
85
|
+
targetVersion: version,
|
|
86
|
+
releaseDocId,
|
|
87
|
+
releaseDocPath,
|
|
88
|
+
releaseRoute,
|
|
89
|
+
tarballUrl,
|
|
90
|
+
changelog,
|
|
91
|
+
changelogSection,
|
|
92
|
+
aggregateEvidenceLine,
|
|
93
|
+
aggregateEvidenceText: aggregateEvidenceLine ? normalizeEvidenceText(aggregateEvidenceLine) : null,
|
|
94
|
+
aggregateEvidenceCount,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function validateCurrentReleaseDoc(ctx, repoRoot) {
|
|
99
|
+
const errors = [];
|
|
100
|
+
const fullPath = join(repoRoot, ctx.releaseDocPath);
|
|
101
|
+
if (!existsSync(fullPath)) {
|
|
102
|
+
errors.push(`release notes page missing: ${ctx.releaseDocPath}`);
|
|
103
|
+
return errors;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const releaseDoc = read(repoRoot, ctx.releaseDocPath);
|
|
107
|
+
const heading = `# AgentXchain v${ctx.targetVersion}`;
|
|
108
|
+
if (!releaseDoc.includes(heading)) {
|
|
109
|
+
errors.push(`${ctx.releaseDocPath} must contain ${heading}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!releaseDoc.match(/## Evidence/i)) {
|
|
113
|
+
errors.push(`${ctx.releaseDocPath} must contain an Evidence section`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (ctx.aggregateEvidenceText && !normalizeEvidenceText(releaseDoc).includes(ctx.aggregateEvidenceText)) {
|
|
117
|
+
errors.push(`${ctx.releaseDocPath} must carry aggregate evidence line "${ctx.aggregateEvidenceLine}"`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return errors;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function validateTextIncludesVersionAndEvidence(relativePath, label) {
|
|
124
|
+
return {
|
|
125
|
+
id: label,
|
|
126
|
+
check(ctx, repoRoot) {
|
|
127
|
+
const content = read(repoRoot, relativePath);
|
|
128
|
+
const errors = [];
|
|
129
|
+
if (!content.includes(`v${ctx.targetVersion}`)) {
|
|
130
|
+
errors.push(`${relativePath} must mention v${ctx.targetVersion}`);
|
|
131
|
+
}
|
|
132
|
+
if (ctx.aggregateEvidenceText && !normalizeEvidenceText(content).includes(ctx.aggregateEvidenceText)) {
|
|
133
|
+
errors.push(`${relativePath} must carry aggregate evidence line "${ctx.aggregateEvidenceLine}"`);
|
|
134
|
+
}
|
|
135
|
+
return errors;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const RELEASE_ALIGNMENT_SURFACES = [
|
|
141
|
+
{
|
|
142
|
+
id: 'changelog',
|
|
143
|
+
label: 'cli/CHANGELOG.md top target section',
|
|
144
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
145
|
+
check(ctx) {
|
|
146
|
+
const errors = [];
|
|
147
|
+
if (!ctx.changelogSection) {
|
|
148
|
+
errors.push(`cli/CHANGELOG.md is missing top heading ## ${ctx.targetVersion}`);
|
|
149
|
+
}
|
|
150
|
+
if (!ctx.aggregateEvidenceLine) {
|
|
151
|
+
errors.push(`cli/CHANGELOG.md section ## ${ctx.targetVersion} must contain an aggregate evidence line with 0 failures`);
|
|
152
|
+
}
|
|
153
|
+
return errors;
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'release_notes',
|
|
158
|
+
label: 'current release notes page',
|
|
159
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
160
|
+
check: validateCurrentReleaseDoc,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'release_sidebar',
|
|
164
|
+
label: 'website-v2/sidebars.ts release autogen',
|
|
165
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
166
|
+
check(_ctx, repoRoot) {
|
|
167
|
+
const sidebars = read(repoRoot, 'website-v2/sidebars.ts');
|
|
168
|
+
return /label:\s*'Release Notes'[\s\S]*dirName:\s*'releases'/.test(sidebars)
|
|
169
|
+
? []
|
|
170
|
+
: [`website-v2/sidebars.ts must keep Release Notes auto-generated from dirName: 'releases'`];
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'homepage_badge',
|
|
175
|
+
label: 'homepage hero badge version',
|
|
176
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
177
|
+
check(ctx, repoRoot) {
|
|
178
|
+
const home = read(repoRoot, 'website-v2/src/pages/index.tsx');
|
|
179
|
+
return home.includes(`v${ctx.targetVersion}`)
|
|
180
|
+
? []
|
|
181
|
+
: [`website-v2/src/pages/index.tsx must mention v${ctx.targetVersion}`];
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'homepage_proof_stat',
|
|
186
|
+
label: 'homepage proof stat',
|
|
187
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
188
|
+
check(ctx, repoRoot) {
|
|
189
|
+
if (ctx.aggregateEvidenceCount == null) {
|
|
190
|
+
return ['homepage proof stat cannot be validated because CHANGELOG aggregate evidence is missing'];
|
|
191
|
+
}
|
|
192
|
+
const home = read(repoRoot, 'website-v2/src/pages/index.tsx');
|
|
193
|
+
const formatted = formatCount(ctx.aggregateEvidenceCount);
|
|
194
|
+
const errors = [];
|
|
195
|
+
if (!home.includes(`stat-number">${formatted}<`)) {
|
|
196
|
+
errors.push(`website-v2/src/pages/index.tsx must show homepage proof stat ${formatted}`);
|
|
197
|
+
}
|
|
198
|
+
if (!home.includes('Tests / 0 failures')) {
|
|
199
|
+
errors.push('website-v2/src/pages/index.tsx must keep the "Tests / 0 failures" proof label');
|
|
200
|
+
}
|
|
201
|
+
return errors;
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'capabilities_version',
|
|
206
|
+
label: '.agentxchain-conformance/capabilities.json version',
|
|
207
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
208
|
+
check(ctx, repoRoot) {
|
|
209
|
+
const capabilities = readJson(repoRoot, '.agentxchain-conformance/capabilities.json');
|
|
210
|
+
return capabilities.version === ctx.targetVersion
|
|
211
|
+
? []
|
|
212
|
+
: [`.agentxchain-conformance/capabilities.json version is "${capabilities.version}", expected "${ctx.targetVersion}"`];
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'implementor_guide_version',
|
|
217
|
+
label: 'protocol implementor guide example version',
|
|
218
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
219
|
+
check(ctx, repoRoot) {
|
|
220
|
+
const guide = read(repoRoot, 'website-v2/docs/protocol-implementor-guide.mdx');
|
|
221
|
+
return guide.includes(`"version": "${ctx.targetVersion}"`)
|
|
222
|
+
? []
|
|
223
|
+
: [`website-v2/docs/protocol-implementor-guide.mdx must contain "version": "${ctx.targetVersion}"`];
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 'launch_evidence_report',
|
|
228
|
+
label: 'launch evidence report',
|
|
229
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
230
|
+
check(ctx, repoRoot) {
|
|
231
|
+
const report = read(repoRoot, '.planning/LAUNCH_EVIDENCE_REPORT.md');
|
|
232
|
+
const errors = [];
|
|
233
|
+
if (!report.match(new RegExp(`^# Launch Evidence Report — AgentXchain v${escapeRegExp(ctx.targetVersion)}`, 'm'))) {
|
|
234
|
+
errors.push(`.planning/LAUNCH_EVIDENCE_REPORT.md title must carry v${ctx.targetVersion}`);
|
|
235
|
+
}
|
|
236
|
+
if (ctx.aggregateEvidenceText && !normalizeEvidenceText(report).includes(ctx.aggregateEvidenceText)) {
|
|
237
|
+
errors.push(`.planning/LAUNCH_EVIDENCE_REPORT.md must carry aggregate evidence line "${ctx.aggregateEvidenceLine}"`);
|
|
238
|
+
}
|
|
239
|
+
return errors;
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: 'show_hn_draft',
|
|
244
|
+
label: 'show hn draft',
|
|
245
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
246
|
+
check: validateTextIncludesVersionAndEvidence('.planning/SHOW_HN_DRAFT.md', 'show hn draft').check,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'twitter_thread',
|
|
250
|
+
label: 'twitter thread draft',
|
|
251
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
252
|
+
check: validateTextIncludesVersionAndEvidence('.planning/MARKETING/TWITTER_THREAD.md', 'twitter thread draft').check,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: 'reddit_posts',
|
|
256
|
+
label: 'reddit posts draft',
|
|
257
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
258
|
+
check: validateTextIncludesVersionAndEvidence('.planning/MARKETING/REDDIT_POSTS.md', 'reddit posts draft').check,
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 'hn_submission',
|
|
262
|
+
label: 'hn submission draft',
|
|
263
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
264
|
+
check: validateTextIncludesVersionAndEvidence('.planning/MARKETING/HN_SUBMISSION.md', 'hn submission draft').check,
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: 'llms_release_route',
|
|
268
|
+
label: 'llms current release route',
|
|
269
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
270
|
+
check(ctx, repoRoot) {
|
|
271
|
+
const llms = read(repoRoot, 'website-v2/static/llms.txt');
|
|
272
|
+
return llms.includes(ctx.releaseRoute)
|
|
273
|
+
? []
|
|
274
|
+
: [`website-v2/static/llms.txt must list ${ctx.releaseRoute}`];
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: 'homebrew_formula_url',
|
|
279
|
+
label: 'homebrew mirror formula url',
|
|
280
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
281
|
+
check(ctx, repoRoot) {
|
|
282
|
+
const formula = read(repoRoot, 'cli/homebrew/agentxchain.rb');
|
|
283
|
+
return formula.includes(`url "${ctx.tarballUrl}"`)
|
|
284
|
+
? []
|
|
285
|
+
: [`cli/homebrew/agentxchain.rb must point at ${ctx.tarballUrl}`];
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: 'homebrew_readme',
|
|
290
|
+
label: 'homebrew mirror readme',
|
|
291
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
292
|
+
check(ctx, repoRoot) {
|
|
293
|
+
const readme = read(repoRoot, 'cli/homebrew/README.md');
|
|
294
|
+
const errors = [];
|
|
295
|
+
if (!readme.includes(`- version: \`${ctx.targetVersion}\``)) {
|
|
296
|
+
errors.push(`cli/homebrew/README.md must carry version ${ctx.targetVersion}`);
|
|
297
|
+
}
|
|
298
|
+
if (!readme.includes(`- source tarball: \`${ctx.tarballUrl}\``)) {
|
|
299
|
+
errors.push(`cli/homebrew/README.md must carry tarball ${ctx.tarballUrl}`);
|
|
300
|
+
}
|
|
301
|
+
return errors;
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
export function validateReleaseAlignment(repoRoot, { targetVersion, scope = RELEASE_ALIGNMENT_SCOPES.CURRENT } = {}) {
|
|
307
|
+
if (!Object.values(RELEASE_ALIGNMENT_SCOPES).includes(scope)) {
|
|
308
|
+
throw new Error(`Unsupported release alignment scope "${scope}"`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const context = getReleaseAlignmentContext(repoRoot, { targetVersion });
|
|
312
|
+
const surfaces = RELEASE_ALIGNMENT_SURFACES.filter((surface) => surface.scopes.includes(scope));
|
|
313
|
+
const errors = [];
|
|
314
|
+
|
|
315
|
+
for (const surface of surfaces) {
|
|
316
|
+
const surfaceErrors = surface.check(context, repoRoot) || [];
|
|
317
|
+
for (const error of surfaceErrors) {
|
|
318
|
+
errors.push({
|
|
319
|
+
surface_id: surface.id,
|
|
320
|
+
label: surface.label,
|
|
321
|
+
message: error,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
ok: errors.length === 0,
|
|
328
|
+
scope,
|
|
329
|
+
targetVersion: context.targetVersion,
|
|
330
|
+
packageVersion: context.packageVersion,
|
|
331
|
+
aggregateEvidenceLine: context.aggregateEvidenceLine,
|
|
332
|
+
checkedSurfaceCount: surfaces.length,
|
|
333
|
+
checkedSurfaceIds: surfaces.map((surface) => surface.id),
|
|
334
|
+
errors,
|
|
335
|
+
};
|
|
336
|
+
}
|
package/src/lib/run-chain.js
CHANGED
|
@@ -13,6 +13,7 @@ import { randomUUID } from 'crypto';
|
|
|
13
13
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
14
14
|
import { join } from 'path';
|
|
15
15
|
import { recordRunHistory, validateParentRun } from './run-history.js';
|
|
16
|
+
import { loadMissionArtifact, loadLatestMissionArtifact, attachChainToMission } from './missions.js';
|
|
16
17
|
|
|
17
18
|
const DEFAULT_MAX_CHAINS = 5;
|
|
18
19
|
const DEFAULT_CHAIN_ON = ['completed'];
|
|
@@ -23,7 +24,7 @@ const DEFAULT_COOLDOWN_SECONDS = 5;
|
|
|
23
24
|
*
|
|
24
25
|
* @param {object} opts - CLI options
|
|
25
26
|
* @param {object} config - agentxchain.json config
|
|
26
|
-
* @returns {{ enabled: boolean, maxChains: number, chainOn: string[], cooldownSeconds: number }}
|
|
27
|
+
* @returns {{ enabled: boolean, maxChains: number, chainOn: string[], cooldownSeconds: number, mission: string|null }}
|
|
27
28
|
*/
|
|
28
29
|
export function resolveChainOptions(opts, config) {
|
|
29
30
|
const configChain = config?.run_loop?.chain || {};
|
|
@@ -43,7 +44,9 @@ export function resolveChainOptions(opts, config) {
|
|
|
43
44
|
chainOn = DEFAULT_CHAIN_ON;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
const mission = opts.mission ?? configChain.mission ?? null;
|
|
48
|
+
|
|
49
|
+
return { enabled, maxChains, chainOn, cooldownSeconds, mission };
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
/**
|
|
@@ -62,6 +65,27 @@ export async function executeChainedRun(context, opts, chainOpts, executeGoverne
|
|
|
62
65
|
const maxRuns = chainOpts.maxChains + 1; // initial + continuations
|
|
63
66
|
const startedAt = new Date().toISOString();
|
|
64
67
|
|
|
68
|
+
// ── Mission binding validation ─────────────────────────────────────────
|
|
69
|
+
let missionTarget = null;
|
|
70
|
+
if (chainOpts.mission) {
|
|
71
|
+
const missionId = chainOpts.mission;
|
|
72
|
+
if (missionId === 'latest') {
|
|
73
|
+
missionTarget = loadLatestMissionArtifact(context.root);
|
|
74
|
+
if (!missionTarget) {
|
|
75
|
+
log(` ⚠ --mission latest: no missions found. Chain will not be attached.`);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
missionTarget = loadMissionArtifact(context.root, missionId);
|
|
79
|
+
if (!missionTarget) {
|
|
80
|
+
log(` Mission not found: ${missionId}. Aborting chain.`);
|
|
81
|
+
return { exitCode: 1, chainReport: null };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (missionTarget) {
|
|
85
|
+
log(` Mission: ${missionTarget.mission_id} — "${missionTarget.title}"`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
65
89
|
const chainReport = {
|
|
66
90
|
chain_id: chainId,
|
|
67
91
|
started_at: startedAt,
|
|
@@ -173,6 +197,16 @@ export async function executeChainedRun(context, opts, chainOpts, executeGoverne
|
|
|
173
197
|
// Write chain report
|
|
174
198
|
writeChainReport(context.root, chainReport);
|
|
175
199
|
|
|
200
|
+
// Auto-attach to mission if binding is active
|
|
201
|
+
if (missionTarget) {
|
|
202
|
+
const attachResult = attachChainToMission(context.root, missionTarget.mission_id, chainReport.chain_id);
|
|
203
|
+
if (attachResult.ok) {
|
|
204
|
+
log(` Chain attached to mission: ${missionTarget.mission_id}`);
|
|
205
|
+
} else {
|
|
206
|
+
log(` ⚠ Mission attachment failed: ${attachResult.error}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
176
210
|
// Print chain summary
|
|
177
211
|
printChainSummary(chainReport, log);
|
|
178
212
|
|